商城首页欢迎来到中国正版软件门户

您的位置:首页 > 硬件相关 >.NET 8.0 与硬件设备的火花碰撞

.NET 8.0 与硬件设备的火花碰撞

  发布于2025-05-08 阅读(0)

扫一扫,手机访问

前言

近期我在社区中表达了想要制作稚晖君的瀚文键盘的意愿,幸运的是,有两位朋友慷慨相助,一位赠送了我电路板,另一位则送来了已经焊接好元件的电路板。既然大家如此大方,我也决定全力投入到这把客制化键盘的制作中。为了节省成本,我特意重新设计了外壳模型,并使用3D打印机打印了整个外壳,这样就省下了八九百元的CNC加工费。

关于键盘的基本介绍这里就不赘述了,它的主要特色在于左侧的扩展模块,配备了墨水屏和手感极佳的旋钮,当然也支持自定义开发,这也是我撰写这篇文章的动机之一,因为我想开发一些功能。以下是效果图:

.NET 8.0 与硬件设备能碰撞出怎么样的火花

技术选型

我查阅了一些社区的键盘资料,发现社区的固件有多个版本。稚晖君原版的固件年代久远,不太好用,而赠送我键盘的那位朋友的版本我觉得很方便,且用户量也很多。因此,我基于这个版本的固件进行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

最终效果如下图所示:

.NET 8.0 与硬件设备能碰撞出怎么样的火花

代码讲解

我这次将项目代码提交到了电子脑壳的仓库,因为我要将功能集成到电子脑壳中,所以放在了这个仓库的helloworld-keyboard分支,未来可能会合并到主分支。

仓库地址:https://github.com/maker-community/ElectronBot.DotNet

.NET 8.0 与硬件设备能碰撞出怎么样的火花

通讯协议实现

通讯的核心部分是Hw75DynamicDevice的Call方法,它包含了将protobuf生成的C#对象转换成字节数组并拆分成数据包发送到设备。

代码语言:javascript

代码运行次数:0

private MessageD2H Call(MessageH2D h2d) {
    if (_device == null) {
        throw new Exception("设备为空");
    }
    var bytes = h2d.EnCodeProtoMessage();
    for (int i = 0; i < bytes.Length; i += PayloadSize) {
        byte[] buf;
        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;
        }
    }
    var resultMessage = new MessageD2H();
    resultMessage.MergeFrom(byteList.ToArray());
    return resultMessage;
}

数据打包时有一个关键问题,就是在拼接图片数据时,需要将byte[]长度使用protobuf编码后再组装到数据的byte[]前面。这个转换成byte[]时需要特别注意,代码如下:

代码语言:javascript

代码运行次数:0

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;
    }
}

.NET 8.0 与硬件设备能碰撞出怎么样的火花

重点部分是HID设备每次发送64字节,第一字节是固定数字1,第二字节是数据长度,后面是数据内容。

.NET 8.0 与硬件设备能碰撞出怎么样的火花

数据传输测试

在完成SDK编写和测试后,我使用控制台项目进行测试,包括图片的合成和文字的绘制,以及将绘制好的图片转换成设备可用的字节数据。

首先使用ImageSharp加载图片,再加载字体文件,将文字和图片绘制到图片上,为后续制作动态数据做准备,代码如下:

代码语言:javascript

代码运行次数:0

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这个扩展方法中。

代码语言:javascript

代码运行次数:0

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++) {
var pixel = image[x, y];
matrix[y, x] = pixel.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) {
byte b = 0;
for (int i = 0; i < 8; i++) {
if (x + i < image.Width) {
b |= (byte)(matrix[y, x + i] << (7 - i));
}
}
byteArray[y  (image.Width / 8) + x / 8] = b;
}
}
return byteArray;
}

全部代码如下:

代码语言:javascript

代码运行次数:0

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,非常新,也算是个惊喜。希望社区的轮子越来越多!

其他角度的照片展示

.NET 8.0 与硬件设备能碰撞出怎么样的火花

.NET 8.0 与硬件设备能碰撞出怎么样的火花

热门关注