自定义帧
自定义帧用于处理厂家私有协议。常见格式是:帧头、长度、业务载荷、校验。
AA 55 02 10 34
帧头 长度 载荷
上面这帧里,真正的业务载荷只有:
10 34
Zeus 的 LengthHeaderFrameCodec 负责自动补帧头、写长度、算校验、处理半包和粘包。你的业务代码只需要关心载荷。
什么时候用它
| 设备手册里的格式 | 是否适合自定义帧 |
|---|---|
AA 55 + 长度 + 数据 + 校验 | 适合 |
7E + 长度 + 命令 + 数据 + CRC | 适合,配置帧头和校验 |
| Modbus RTU / TCP | 不建议,直接用 Modbus |
| 纯文本协议,例如一行一个命令 | 不一定需要,直接按文本收发也可以 |
最小请求-响应
await using var app = ZeusHost.Create(builder => builder.AddVirtualChannel("bus"));
await using var session = app.CreateFrameSession(
"bus",
new FrameLayout([0xAA, 0x55], FrameLengthKind.UInt8, FrameChecksumKind.None));
await app.StartAsync();
var reply = await session.RequestAsync(new byte[] { 0x10, 0x34 });
这段代码的意思是:
| 代码 | 含义 |
|---|---|
AddVirtualChannel("bus") | 先用虚拟通道联调,不接真实设备 |
CreateFrameSession("bus", layout) | 在 bus 通道上创建一个帧会话 |
FrameLayout([0xAA, 0x55], UInt8, None) | 帧头是 AA 55,长度 1 字节,没有校验 |
RequestAsync(new byte[] { 0x10, 0x34 }) | 发送业务载荷 10 34,等待下一帧完整应答 |
如果使用默认布局 AA 55 + 1 字节长度 + 无校验,可以简写:
await using var session = app.CreateFrameSession("bus");
载荷和完整帧的区别
你传给 RequestAsync 的是载荷:
10 34
Zeus 写到线上的是完整帧:
AA 55 02 10 34
如果开启 CRC,线上还会多出校验字节。收到数据时反过来:Zeus 先验证完整帧,再只把载荷返回给你。
等待匹配应答
简单设备通常“发一帧,下一帧就是响应”。但真实现场可能会遇到:
- 设备主动上报状态。
- 上一次请求的响应迟到了。
- 同一总线上有其它设备帧。
- 响应里带序号、命令字或地址。
这时不要直接等下一帧,而是给 RequestAsync 一个匹配器:
var sequence = (byte)0x34;
var reply = await session.RequestAsync(
new byte[] { 0x10, sequence },
response => response.Length >= 2
&& response.Span[0] == 0x90
&& response.Span[1] == sequence);
这段代码逐行看:
| 代码 | 含义 |
|---|---|
sequence = 0x34 | 本次请求序号 |
new byte[] { 0x10, sequence } | 发送载荷:命令 10 + 序号 34 |
response.Length >= 2 | 响应至少要有“命令 + 序号”两个字节 |
response.Span[0] == 0x90 | 响应命令必须是 90,例如协议规定请求 10 对应响应 90 |
response.Span[1] == sequence | 响应序号必须等于本次请求序号 34 |
只有这种响应会被返回:
90 34 01
响应命令 序号 数据
这些完整帧会被跳过,继续等待:
90 33 01 // 命令对,但序号不对,可能是旧响应
91 34 01 // 序号对,但命令不对,可能是别的命令响应
20 01 // 主动上报帧,不是本次请求响应
被跳过的完整帧不会丢掉,会先留在会话收件箱里。
只发送不等应答
广播、心跳或不需要响应的命令使用 SendAsync:
await session.SendAsync(new byte[] { 0x20, 0x01 });
帧选项
| 类型 | 含义 |
|---|---|
FrameLengthKind.UInt8 | 长度字段 1 字节,载荷最长 255 字节 |
UInt16LittleEndian | 长度字段 2 字节,小端 |
UInt16BigEndian | 长度字段 2 字节,大端 |
FrameChecksumKind.None | 无校验 |
FrameChecksumKind.Xor8 | 1 字节异或校验 |
FrameChecksumKind.Sum8 | 1 字节累加和校验 |
FrameChecksumKind.Crc16Modbus | Modbus CRC16,小端输出 |
校验覆盖长度域和载荷,不覆盖帧头。
常见问题
| 现象 | 原因 | 处理 |
|---|---|---|
| 超时未收到完整应答 | 帧头、长度、校验或设备没有回包 | 用串口工具抓原始字节,对照设备手册 |
| 超时未收到匹配应答 | 收到了帧,但匹配器一直返回 false | 打印响应载荷,核对序号、命令字或地址 |
| 载荷超过 255 字节 | 使用了 1 字节长度字段 | 改用 UInt16LittleEndian 或 UInt16BigEndian |
| 一次收到多帧 | 正常粘包 | LengthHeaderFrameCodec 会连续拆出完整帧 |
| 一帧分多次收到 | 正常半包 | 编解码器会缓存,直到完整帧到齐 |
下一步:如果你的设备是标准 Modbus,直接看 Modbus。