feat(device): 实现下位机 JSON 协议(data model 对齐)

按 docs/下位机交互数据模型.md 重构串口协议层:

协议层
- 新增 DeviceMessage 模型,对应 message_id/type/ack/need_ack/data
- 新增 JsonProtocolService,4 字节大端长度前缀 + UTF-8 JSON 帧
- 删除原二进制协议(serial_protocol.dart)

服务层
- 新增 DeviceMessageService,集中收发并按 type 分发
- 重写 SerialRunner 为 JsonSerialRunner,使用 create_task/control 消息

数据模型
- DeviceState 增加 doorStatus/lightStatus/taskStatus/lastInfoAt
- 新增 DeviceInfoNotifier 订阅 device_info 上行
- 灯光按钮接通 light_control 消息

测试
- 新增 device_protocol_test.dart(14 用例)
- 修复 models_test.dart 残留的 Step mixSpeed/blowSpeed 错误
This commit is contained in:
Developer
2026-06-04 13:00:21 +08:00
parent 5d28bf631b
commit 819889684f
13 changed files with 1689 additions and 122 deletions

View File

@@ -0,0 +1,95 @@
import 'dart:convert';
import 'dart:typed_data';
import 'device_message.dart';
/// JSON 协议层帧编解码器
///
/// 由于串口为字节流,需要在 JSON 字符串之外增加帧定界。
/// 采用「4 字节大端长度前缀 + UTF-8 JSON + 换行符」的简单可靠方案:
/// ```
/// [LEN(4B BE)] [JSON_UTF8...] ['\n']
/// ```
/// - LENJSON 部分的字节数(不含 LEN 自身与换行符)
/// - JSON完整 `DeviceMessage.toJson()` 序列化结果
/// - 换行符:辅助日志/调试,便于人工嗅探串口
///
/// 解析器按累积缓冲区增量解码,单次返回一条已完整解析的消息。
class JsonProtocolService {
static const int _lengthPrefixSize = 4;
static const int _maxFrameBytes = 64 * 1024;
final MessageIdGenerator _idGenerator = MessageIdGenerator();
final List<int> _buffer = [];
/// 生成下一个消息 ID
String nextId() => _idGenerator.next();
/// 将消息编码为可下发的字节流
Uint8List encode(DeviceMessage message) {
final json = utf8.encode(message.encode());
if (json.isEmpty) {
throw ArgumentError('encoded JSON is empty');
}
if (json.length > _maxFrameBytes) {
throw ArgumentError('message too large: ${json.length}B');
}
final buf = Uint8List(_lengthPrefixSize + json.length + 1);
// 大端长度
final len = json.length;
buf[0] = (len >> 24) & 0xFF;
buf[1] = (len >> 16) & 0xFF;
buf[2] = (len >> 8) & 0xFF;
buf[3] = len & 0xFF;
buf.setRange(_lengthPrefixSize, _lengthPrefixSize + json.length, json);
buf[buf.length - 1] = 0x0A; // '\n'
return buf;
}
/// 尝试从累积缓冲区解析一条完整消息
///
/// 返回 (message, consumedBytes)。consumedBytes 表示已消费字节数,
/// 调用方应从缓冲区中移除。
/// 不足一帧时返回 (null, 0)。
(DeviceMessage?, int) tryDecode(List<int> incoming) {
if (incoming.isEmpty) return (null, 0);
_buffer.addAll(incoming);
// 防止缓冲区无限增长
if (_buffer.length > _maxFrameBytes * 2) {
_buffer.removeRange(0, _buffer.length - _maxFrameBytes);
}
if (_buffer.length < _lengthPrefixSize) return (null, 0);
final len = (_buffer[0] << 24) |
(_buffer[1] << 16) |
(_buffer[2] << 8) |
_buffer[3];
if (len <= 0 || len > _maxFrameBytes) {
// 长度异常,丢弃首字节重新对齐
_buffer.removeAt(0);
return (null, 0);
}
final totalNeeded = _lengthPrefixSize + len + 1; // +1 换行符
if (_buffer.length < totalNeeded) return (null, 0);
final jsonBytes = _buffer.sublist(_lengthPrefixSize, _lengthPrefixSize + len);
final tail = _buffer[_lengthPrefixSize + len];
// 换行符不是必需的,缺失也接受;存在则跳过
final consumed = tail == 0x0A ? totalNeeded : totalNeeded - 1;
_buffer.removeRange(0, consumed);
try {
final json = utf8.decode(jsonBytes);
final msg = DeviceMessage.decode(json);
return (msg, consumed);
} catch (_) {
return (null, consumed);
}
}
/// 重置内部缓冲区(断线/异常时调用)
void reset() => _buffer.clear();
}