.Net 8与硬件设备能碰撞出怎么样的火花(使用ImageSharp和Protobuf协议通过HidApi与设备通讯)
前言
本人最近在社區(qū)里說(shuō)想做稚暉君的那個(gè)瀚文鍵盤來(lái)著,結(jié)果遇到兩個(gè)老哥一個(gè)老哥送了我電路板,一個(gè)送了我焊接好元件的電路板,既然大家這么舍得,那我也就真的投入制作了這把客制化鍵盤,當(dāng)然我為了省錢也是特意把外殼模型重新切割,用3D打印機(jī)打印了整個(gè)外殼,不得不說(shuō)省了八九百的CNC費(fèi)用。鍵盤介紹我就不說(shuō)了,鍵盤主要特色是左邊的拓展模塊,有墨水屏和手感超好的旋鈕,當(dāng)然也支持自定義開(kāi)發(fā),能開(kāi)發(fā)也是我寫這篇文章的原因,畢竟是為了開(kāi)發(fā)功能,效果圖如下,大家可以關(guān)注我的b站賬號(hào)綠蔭阿廣,來(lái)學(xué)習(xí)交流一些有趣的東西。
技術(shù)選型
在我查閱了一些社區(qū)鍵盤資料發(fā)現(xiàn)社區(qū)固件有幾個(gè)版本,稚暉君原版的固件太老了不好用,送我鍵盤的老哥的版本我覺(jué)得挺方便而且用戶量應(yīng)該也很多,于是我就基于這個(gè)版本的固件進(jìn)行dotnet版本的sdk開(kāi)發(fā)了,目前有其他版本的sdk,有python版本的,vue版本的,我是可以拿來(lái)直接參考的。
1. 框架選擇
作為一名.Net開(kāi)發(fā),我肯定是想用.net進(jìn)行開(kāi)發(fā)的,理由是這個(gè)鍵盤用在PC上,用.Net實(shí)現(xiàn)SDK對(duì)接WPF,MAUI和WinUI可以做很多的任務(wù)型的功能。選擇采用最新版本的.Net8,然后在SDK測(cè)試編寫完成之后,對(duì)接到我之前的WinUI桌面程序里,大家肯定會(huì)問(wèn),為什么不選擇MAUI,我想說(shuō)當(dāng)然因?yàn)槲視簳r(shí)不想花時(shí)間重新寫,不過(guò)SDK是支持跨平臺(tái)的,這點(diǎn)問(wèn)題不大。
2. 設(shè)備通訊協(xié)議
鍵盤采用的固件是開(kāi)源的ZMK這個(gè)代碼編寫的,設(shè)備在電腦識(shí)別為hid設(shè)備,通訊格式使用的Protobuf協(xié)議,所以針對(duì).Net也需要使用這個(gè)Protobuf進(jìn)行數(shù)據(jù)的打包,這個(gè)地方花了我一些時(shí)間,主要是有些地方不太懂,坑主要是數(shù)據(jù)轉(zhuǎn)成字節(jié)數(shù)組的時(shí)候的一些問(wèn)題,這個(gè)在后面的代碼講解里有用到。
- 設(shè)備固件地址:https://github.com/xingrz/zmk-config_helloword_hw-75
- python SDK: https://github.com/xingrz/zmkx-sdk
3. 庫(kù)選擇
本來(lái)以為.Net可以用的hid庫(kù)有很多,在本人測(cè)試了一圈以后發(fā)現(xiàn)不錯(cuò)的也就這個(gè)HidApi.Net還可以,其他的什么Device.Net,HidLibrary都不是很滿意,在我測(cè)試以后選擇了HidApi.Net和設(shè)備通訊,Google.Protobuf和Grpc.Tools加工通訊數(shù)據(jù),SixLabors.ImageSharp進(jìn)行圖片數(shù)據(jù)的轉(zhuǎn)換。
- HidApi.Net
- Google.Protobuf
- Grpc.Tools
- SixLabors.ImageSharp
最終效果如下圖:
代碼講解
項(xiàng)目代碼我這次提交到了電子腦殼的倉(cāng)庫(kù)里,因?yàn)槲乙獙⒐δ芗傻诫娮幽X殼里,所以放在了這個(gè)倉(cāng)庫(kù),目前所在分支為helloworld-keyboard,后期應(yīng)該會(huì)合并到主分支。
倉(cāng)庫(kù)地址:https://github.com/maker-community/ElectronBot.DotNet
通訊協(xié)議實(shí)現(xiàn)
通訊的核心部分是Hw75DynamicDevice的Call方法,包含了將protobuf生成的c#對(duì)象轉(zhuǎn)成byte[]并拆分成數(shù)據(jù)包發(fā)送到設(shè)備。
private MessageD2H Call(MessageH2D h2d)
{
if (_device == null)
{
throw new Exception("設(shè)備為空");
}
var bytes = h2d.EnCodeProtoMessage();
for (int i = 0; i < bytes.Length; i += PayloadSize)
{
var buf = new byte[PayloadSize];
if (i + PayloadSize > bytes.Length)
{
buf = bytes[i..];
}
else
{
buf = bytes[i..(i + PayloadSize)];
}
var list = new byte[2] { 1, (byte)buf.Length };
var result = list.Concat(buf).ToArray();
_device.Write(result);
}
Task.Delay(20);
var byteList = new List<byte>();
while (true)
{
var read = _device.Read(RePortCount + 1);
int cnt = read[1];
byteList.AddRange(read[3..(cnt + 2)]);
if (cnt < PayloadSize)
{
break;
}
}
return MessageD2H.Parser.ParseFrom(byteList.ToArray());
}
- 數(shù)據(jù)打包有個(gè)重點(diǎn)問(wèn)題,就是在圖片數(shù)據(jù)進(jìn)行拼接的時(shí)候有個(gè)byte[]長(zhǎng)度需要采用protobuf編碼之后再組裝到數(shù)據(jù)byte[]的前面這個(gè)轉(zhuǎn)成byte[]需要注意,代碼如下:
public static byte[] EnCodeProtoMessage(this MessageH2D messageH2D)
{
var msgBytes = messageH2D.ToByteArray();
using (MemoryStream ms = new MemoryStream())
{
CodedOutputStream output = new CodedOutputStream(ms);
output.WriteInt32(msgBytes.Length);
output.Flush();
byte[] byteList = ms.ToArray();
var result = byteList.Concat(msgBytes).ToArray();
return result;
}
}
- 重點(diǎn)部分是hid設(shè)備要每次發(fā)送64字節(jié),第一字節(jié)是數(shù)字1,這個(gè)是固定的,第二字節(jié)是數(shù)據(jù)長(zhǎng)度,后面的是數(shù)據(jù)內(nèi)容。
數(shù)據(jù)傳輸測(cè)試
在sdk編寫測(cè)試完成之后,就可以進(jìn)行sdk的使用了,我使用控制臺(tái)項(xiàng)目進(jìn)行測(cè)試,包含圖片的合成和文字的繪制,以及將繪制好的圖片轉(zhuǎn)成設(shè)備能夠使用的byte數(shù)據(jù)。
-
我先使用ImageSharp加載圖片,再加載字體文件將文字和圖片繪制到圖片上,這個(gè)為后面制作動(dòng)態(tài)數(shù)據(jù)做鋪墊,代碼如下:
using SixLabors.Fonts; using SixLabors.ImageSharp; using SixLabors.ImageSharp.Drawing.Processing; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing; using System.Diagnostics; using System.Numerics; byte[] byteArray = new byte[128 * 296 / 8]; var list = new List<byte>(); var collection = new FontCollection(); var family = collection.Add("./SmileySans-Oblique.ttf"); var font = family.CreateFont(18, FontStyle.Bold); using (var image = Image.Load<Rgba32>("face.jpg")) { using var overlay = Image.Load<Rgba32>("bzhan.png"); overlay.Mutate(x => { x.Resize(new Size(50,50)); }); // Convert the image to grayscale image.Mutate(x => { x.DrawImage(overlay, new Point(0, 64), opacity: 1); x.DrawText("粉絲數(shù):", font, Color.Black, new Vector2(20, 220)); x.DrawText("999999", font, Color.Black, new Vector2(20, 260)); x.Grayscale(); }); image.Save("test.jpg"); byteArray = image.EnCodeImageToBytes(); } -
然后將ImageSharp合成的圖片轉(zhuǎn)成01矩陣再組裝成byte[]這個(gè)不知道大家有沒(méi)有什么好的辦法,有的話可以推薦給我,我的邏輯寫在了EnCodeImageToBytes這個(gè)拓展方法里。
public static byte[] EnCodeImageToBytes(this Image<Rgba32> image) { // Create a 01 matrix int[,] matrix = new int[image.Height, image.Width]; for (int y = 0; y < image.Height; y++) { for (int x = 0; x < image.Width; x++) { matrix[y, x] = image[x, y].R > 128 ? 1 : 0; } } // Convert the matrix to a byte array byte[] byteArray = new byte[image.Height * image.Width / 8]; for (int y = 0; y < image.Height; y++) { for (int x = 0; x < image.Width; x += 8) { for (int k = 0; k < 8; k++) { byteArray[y * image.Width / 8 + x / 8] |= (byte)(matrix[y, x + k] << (7 - k)); } } } return byteArray; }
全部代碼如下:
using Google.Protobuf;
using Google.Protobuf.WellKnownTypes;
using HelloWordKeyboard.DotNet;
using HidApi;
using SixLabors.Fonts;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Drawing.Processing;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
using System.Diagnostics;
using System.Numerics;
byte[] byteArray = new byte[128 * 296 / 8];
var list = new List<byte>();
var collection = new FontCollection();
var family = collection.Add("./SmileySans-Oblique.ttf");
var font = family.CreateFont(18, FontStyle.Bold);
using (var image = Image.Load<Rgba32>("face.jpg"))
{
using var overlay = Image.Load<Rgba32>("bzhan.png");
overlay.Mutate(x =>
{
x.Resize(new Size(50,50));
});
// Convert the image to grayscale
image.Mutate(x =>
{
x.DrawImage(overlay, new Point(0, 64), opacity: 1);
x.DrawText("粉絲數(shù):", font, Color.Black, new Vector2(20, 220));
x.DrawText("999999", font, Color.Black, new Vector2(20, 260));
x.Grayscale();
});
image.Save("test.jpg");
byteArray = image.EnCodeImageToBytes();
}
var hidDevice = new Hw75DynamicDevice();
hidDevice.Open();
Stopwatch sw = Stopwatch.StartNew();
sw.Start();
var data111 = hidDevice.SetEInkImage(byteArray, 0, 0, 128, 296, false);
sw.Stop();
Console.WriteLine($"send data ms:{sw.ElapsedMilliseconds}");
Console.ReadKey();
Hid.Exit();
個(gè)人心得體會(huì)
這次功能的編寫讓我最有感悟的地方就是自己對(duì)Github Copilot的依賴更多了,我基本上很多的知識(shí)都是詢問(wèn)它,因?yàn)閺木W(wǎng)上搜索還要自己過(guò)濾那些數(shù)據(jù),比較耽誤時(shí)間。
還有個(gè)點(diǎn)就是這個(gè)HidApi.Net的庫(kù)是最近剛有人寫的,社區(qū)還是有新鮮的血液的,支持.net6,7,8很新,也算是個(gè)驚喜呢,希望社區(qū)的*越來(lái)越多呢!!!!
其他角度的照片展示:
參考推薦文檔項(xiàng)目如下:
-
電子腦殼
-
設(shè)備固件地址
-
python SDK
-
hidapi
-
HidApi.Net
-
LibUsbDotNet
-
電子腦殼有在使用的得意黑字體
-
項(xiàng)目模板——TemplateStudio
-
社區(qū)工具集——CommunityToolkit
-
控件庫(kù)展示demo——WinUI-Gallery
-
WASDK文檔地址
-
WinUI-Tutorial-Code WinUI測(cè)試學(xué)習(xí)代碼
總結(jié)
以上是生活随笔為你收集整理的.Net 8与硬件设备能碰撞出怎么样的火花(使用ImageSharp和Protobuf协议通过HidApi与设备通讯)的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 中班教案《自制喷水壶》反思
- 下一篇: .NET周刊【12月第1期 2023-1