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:
95
lib/features/device/services/json_protocol.dart
Normal file
95
lib/features/device/services/json_protocol.dart
Normal 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']
|
||||
/// ```
|
||||
/// - 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();
|
||||
}
|
||||
Reference in New Issue
Block a user