Files
kuaishai2/lib/features/device/services/device_message.dart
Developer 819889684f 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 错误
2026-06-04 13:00:21 +08:00

165 lines
4.5 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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';
}
}