按 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 错误
165 lines
4.5 KiB
Dart
165 lines
4.5 KiB
Dart
import 'dart:convert';
|
||
|
||
/// 下位机消息类型
|
||
///
|
||
/// 对应《下位机交互数据模型.md》中定义的四种 type 字段。
|
||
enum DeviceMessageType {
|
||
/// 设备基本信息:门状态 / 任务运行状态 / 灯状态
|
||
deviceInfo('device_info'),
|
||
|
||
/// 下发任务:步骤列表 + 温度 + 吹气时间
|
||
createTask('create_task'),
|
||
|
||
/// 灯光控制:开 / 关
|
||
lightControl('light_control'),
|
||
|
||
/// 任务控制:继续 / 停止 / 暂停
|
||
control('control');
|
||
|
||
const DeviceMessageType(this.wireName);
|
||
|
||
/// 协议层使用的字符串名称
|
||
final String wireName;
|
||
|
||
/// 从协议字符串解析消息类型;未知值返回 null
|
||
static DeviceMessageType? fromWire(String? name) {
|
||
if (name == null) return null;
|
||
for (final t in values) {
|
||
if (t.wireName == name) return t;
|
||
}
|
||
return null;
|
||
}
|
||
}
|
||
|
||
/// 下位机消息
|
||
///
|
||
/// 完整对应数据模型:
|
||
/// ```json
|
||
/// {
|
||
/// "message_id": "uuid",
|
||
/// "type": "device_info",
|
||
/// "ack": "uuid-of-original",
|
||
/// "need_ack": true,
|
||
/// "data": { ... }
|
||
/// }
|
||
/// ```
|
||
class DeviceMessage {
|
||
/// 唯一识别码。发送时由调用方生成(UUID 字符串);
|
||
/// 接收时来自协议,便于 ack 关联。
|
||
final String messageId;
|
||
|
||
/// 消息类型
|
||
final DeviceMessageType type;
|
||
|
||
/// 当本条消息是某条消息的响应时,填写被响应的 message_id;否则为 null
|
||
final String? ack;
|
||
|
||
/// 是否需要对方响应
|
||
final bool needAck;
|
||
|
||
/// 业务负载;具体结构由 [type] 决定
|
||
final Map<String, dynamic> data;
|
||
|
||
const DeviceMessage({
|
||
required this.messageId,
|
||
required this.type,
|
||
required this.data,
|
||
this.ack,
|
||
this.needAck = false,
|
||
});
|
||
|
||
/// 构造主动请求时使用的便捷工厂
|
||
factory DeviceMessage.request({
|
||
required DeviceMessageType type,
|
||
required Map<String, dynamic> data,
|
||
required String messageId,
|
||
bool needAck = false,
|
||
}) {
|
||
return DeviceMessage(
|
||
messageId: messageId,
|
||
type: type,
|
||
data: data,
|
||
ack: null,
|
||
needAck: needAck,
|
||
);
|
||
}
|
||
|
||
/// 构造应答时使用的便捷工厂
|
||
factory DeviceMessage.ackFor(DeviceMessage original, Map<String, dynamic> data) {
|
||
return DeviceMessage(
|
||
messageId: '${original.messageId}-ack',
|
||
type: original.type,
|
||
data: data,
|
||
ack: original.messageId,
|
||
needAck: false,
|
||
);
|
||
}
|
||
|
||
/// 序列化为协议层 Map
|
||
Map<String, dynamic> toJson() => {
|
||
'message_id': messageId,
|
||
'type': type.wireName,
|
||
'ack': ack,
|
||
'need_ack': needAck,
|
||
'data': data,
|
||
};
|
||
|
||
/// 序列化为 JSON 字符串
|
||
String encode() => jsonEncode(toJson());
|
||
|
||
/// 从 JSON Map 解析;不抛异常,解析失败时返回 null
|
||
static DeviceMessage? fromJson(Map<String, dynamic> json) {
|
||
final type = DeviceMessageType.fromWire(json['type'] as String?);
|
||
if (type == null) return null;
|
||
final messageId = json['message_id'] as String?;
|
||
if (messageId == null || messageId.isEmpty) return null;
|
||
return DeviceMessage(
|
||
messageId: messageId,
|
||
type: type,
|
||
ack: (json['ack'] as String?)?.isEmpty == true ? null : json['ack'] as String?,
|
||
needAck: json['need_ack'] as bool? ?? false,
|
||
data: _coerceData(json['data']),
|
||
);
|
||
}
|
||
|
||
/// 从 JSON 字符串解析;解析失败时返回 null
|
||
static DeviceMessage? decode(String raw) {
|
||
try {
|
||
final map = jsonDecode(raw);
|
||
if (map is! Map<String, dynamic>) return null;
|
||
return fromJson(map);
|
||
} catch (_) {
|
||
return null;
|
||
}
|
||
}
|
||
|
||
/// JSON 中的 data 既可能是 Map,也可能是字符串(容错处理)
|
||
static Map<String, dynamic> _coerceData(dynamic raw) {
|
||
if (raw is Map<String, dynamic>) return raw;
|
||
if (raw is Map) return Map<String, dynamic>.from(raw);
|
||
return <String, dynamic>{};
|
||
}
|
||
|
||
@override
|
||
String toString() => 'DeviceMessage(id=$messageId, type=${type.wireName}, '
|
||
'ack=$ack, needAck=$needAck, data=${data.keys.toList()})';
|
||
}
|
||
|
||
/// 消息 ID 生成器
|
||
///
|
||
/// 使用时间戳 + 随机数生成全局唯一 ID(避免引入 uuid 依赖)。
|
||
/// 格式:`<millis>-<rand>`,例如 `1717500000000-1a2b3c`
|
||
class MessageIdGenerator {
|
||
int _counter = 0;
|
||
|
||
/// 生成下一个唯一 ID
|
||
String next() {
|
||
_counter = (_counter + 1) & 0xFFFFFF;
|
||
final ts = DateTime.now().millisecondsSinceEpoch.toRadixString(36);
|
||
final rand = (_counter.toRadixString(36) +
|
||
(DateTime.now().microsecondsSinceEpoch & 0xFFFF).toRadixString(36))
|
||
.padLeft(4, '0');
|
||
return '$ts-$rand';
|
||
}
|
||
}
|