按 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 错误
96 lines
3.1 KiB
Dart
96 lines
3.1 KiB
Dart
import 'dart:convert';
|
||
import 'dart:typed_data';
|
||
|
||
import 'device_message.dart';
|
||
|
||
/// JSON 协议层帧编解码器
|
||
///
|
||
/// 由于串口为字节流,需要在 JSON 字符串之外增加帧定界。
|
||
/// 采用「4 字节大端长度前缀 + UTF-8 JSON + 换行符」的简单可靠方案:
|
||
/// ```
|
||
/// [LEN(4B BE)] [JSON_UTF8...] ['\n']
|
||
/// ```
|
||
/// - LEN:JSON 部分的字节数(不含 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();
|
||
}
|