前言
本人最近在社群裡說想做稚暉君的那個瀚文鍵盤來著,結果遇到兩個老哥一個老哥送了我電路板,一個送了我焊接好元件的電路板,既然大家這麼捨得,那我也就真的投入製作了這把客製化鍵盤,當然我為了省錢也是特意把外殼模型重新切割,用3D印表機列印了整個外殼,不得不說省了八九百的CNC費用。鍵盤介紹我就不說了,鍵盤主要特色是左邊的擴充模組,有墨水屏和手感超好的旋鈕,當然也支援自定義開發,能開發也是我寫這篇文章的原因,畢竟是為了開發功能,效果圖如下,大家可以關注我的b站賬號綠蔭阿廣,來學習交流一些有趣的東西。
技術選型
在我查閱了一些社群鍵盤資料發現社群韌體有幾個版本,稚暉君原版的韌體太老了不好用,送我鍵盤的老哥的版本我覺得挺方便而且使用者量應該也很多,於是我就基於這個版本的韌體進行dotnet版本的sdk開發了,目前有其他版本的sdk,有python版本的,vue版本的,我是可以拿來直接參考的。
1. 框架選擇
作為一名.Net開發,我肯定是想用.net進行開發的,理由是這個鍵盤用在PC上,用.Net實現SDK對接WPF,MAUI和WinUI可以做很多的任務型的功能。選擇採用最新版本的.Net8,然後在SDK測試編寫完成之後,對接到我之前的WinUI桌面程式裡,大家肯定會問,為什麼不選擇MAUI,我想說當然因為我暫時不想花時間重新寫,不過SDK是支援跨平臺的,這點問題不大。
2. 裝置通訊協議
鍵盤採用的韌體是開源的ZMK這個程式碼編寫的,裝置在電腦識別為hid裝置,通訊格式使用的Protobuf協議,所以針對.Net也需要使用這個Protobuf進行資料的打包,這個地方花了我一些時間,主要是有些地方不太懂,坑主要是資料轉成位元組陣列的時候的一些問題,這個在後面的程式碼講解裡有用到。
- 裝置韌體地址:https://github.com/xingrz/zmk-config_helloword_hw-75
- python SDK: https://github.com/xingrz/zmkx-sdk
3. 庫選擇
本來以為.Net可以用的hid庫有很多,在本人測試了一圈以後發現不錯的也就這個HidApi.Net還可以,其他的什麼Device.Net,HidLibrary都不是很滿意,在我測試以後選擇了HidApi.Net和裝置通訊,Google.Protobuf和Grpc.Tools加工通訊資料,SixLabors.ImageSharp進行圖片資料的轉換。
- HidApi.Net
- Google.Protobuf
- Grpc.Tools
- SixLabors.ImageSharp
最終效果如下圖:
程式碼講解
專案程式碼我這次提交到了電子腦殼的倉庫裡,因為我要將功能整合到電子腦殼裡,所以放在了這個倉庫,目前所在分支為helloworld-keyboard,後期應該會合併到主分支。
倉庫地址:https://github.com/maker-community/ElectronBot.DotNet
通訊協議實現
通訊的核心部分是Hw75DynamicDevice的Call方法,包含了將protobuf生成的c#物件轉成byte[]並拆分成資料包傳送到裝置。
private MessageD2H Call(MessageH2D h2d)
{
if (_device == null)
{
throw new Exception("裝置為空");
}
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());
}
- 資料打包有個重點問題,就是在圖片資料進行拼接的時候有個byte[]長度需要採用protobuf編碼之後再組裝到資料byte[]的前面這個轉成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;
}
}
- 重點部分是hid裝置要每次傳送64位元組,第一位元組是數字1,這個是固定的,第二位元組是資料長度,後面的是資料內容。
資料傳輸測試
在sdk編寫測試完成之後,就可以進行sdk的使用了,我使用控制檯專案進行測試,包含圖片的合成和文字的繪製,以及將繪製好的圖片轉成裝置能夠使用的byte資料。
-
我先使用ImageSharp載入圖片,再載入字型檔案將文字和圖片繪製到圖片上,這個為後面製作動態資料做鋪墊,程式碼如下:
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("粉絲數:", 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合成的圖片轉成01矩陣再組裝成byte[]這個不知道大家有沒有什麼好的辦法,有的話可以推薦給我,我的邏輯寫在了EnCodeImageToBytes這個擴充方法裡。
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("粉絲數:", 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();
個人心得體會
這次功能的編寫讓我最有感悟的地方就是自己對Github Copilot的依賴更多了,我基本上很多的知識都是詢問它,因為從網上搜尋還要自己過濾那些資料,比較耽誤時間。
還有個點就是這個HidApi.Net的庫是最近剛有人寫的,社群還是有新鮮的血液的,支援.net6,7,8很新,也算是個驚喜呢,希望社群的輪子越來越多呢!!!!