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:
164
lib/features/device/services/device_message.dart
Normal file
164
lib/features/device/services/device_message.dart
Normal file
@@ -0,0 +1,164 @@
|
||||
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';
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user