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:
Developer
2026-06-04 13:00:21 +08:00
parent 5d28bf631b
commit 819889684f
13 changed files with 1689 additions and 122 deletions

View File

@@ -1,18 +1,64 @@
/// 设备状态模型
enum DeviceStatus { idle, running, paused, error }
/// 设备运行状态
///
/// 与下位机 `device_info.task_status` 字段对齐:
/// - `running` 运行中
/// - `pause` 暂停
/// - `idle` 空闲
enum DeviceTaskStatus { running, pause, idle }
/// 设备状态数据
/// 设备状态
///
/// 与下位机 `device_info.door_status` 字段对齐:
/// - `open` 开
/// - `close` 关
enum DeviceDoorStatus { open, close }
/// 设备灯光状态
///
/// 与下位机 `device_info.light_status` / `light_control.status` 字段对齐:
/// - `on` 开
/// - `off` 关
enum DeviceLightStatus { on, off }
/// 上位机内部统一的设备状态模型
///
/// 既承载 [DeviceState](运行态监控),也承载下位机 `device_info` 上报
/// 的门状态 / 灯光状态 / 任务状态,供 UI 各处订阅展示与联动。
class DeviceState {
/// 本机内部识别的运行状态(用于驱动运行/暂停/完成流程)
final DeviceStatus status;
final String? currentProgram;
final String? currentPosition;
final int? currentStepNo;
final String? currentStepName;
final int? remainingSeconds;
final double? progress;
final bool lightingOn;
DeviceState({
/// 当前运行程序名(仅运行态有效)
final String? currentProgram;
/// 当前步骤的孔位(例如 A1
final String? currentPosition;
/// 当前步骤号
final int? currentStepNo;
/// 当前步骤名
final String? currentStepName;
/// 当前步骤剩余时间(秒)
final int? remainingSeconds;
/// 总进度 [0, 1]
final double? progress;
/// 灯光状态(来自 device_info主动权在下位机
final DeviceLightStatus lightStatus;
/// 门状态(来自 device_info
final DeviceDoorStatus doorStatus;
/// 任务状态(来自 device_info
final DeviceTaskStatus taskStatus;
/// 上次 device_info 上报的时间,便于 UI 显示「通讯正常 / 已断开」
final DateTime? lastInfoAt;
const DeviceState({
this.status = DeviceStatus.idle,
this.currentProgram,
this.currentPosition,
@@ -20,7 +66,10 @@ class DeviceState {
this.currentStepName,
this.remainingSeconds,
this.progress,
this.lightingOn = false,
this.lightStatus = DeviceLightStatus.off,
this.doorStatus = DeviceDoorStatus.close,
this.taskStatus = DeviceTaskStatus.idle,
this.lastInfoAt,
});
bool get isRunning => status == DeviceStatus.running;
@@ -28,6 +77,18 @@ class DeviceState {
bool get isIdle => status == DeviceStatus.idle;
bool get hasError => status == DeviceStatus.error;
bool get lightingOn => lightStatus == DeviceLightStatus.on;
bool get doorOpen => doorStatus == DeviceDoorStatus.open;
/// 下位机超过该时间未上报则视为通讯异常
static const Duration infoStaleAfter = Duration(seconds: 10);
bool get infoStale {
final ts = lastInfoAt;
if (ts == null) return true;
return DateTime.now().difference(ts) > infoStaleAfter;
}
String statusText() {
switch (status) {
case DeviceStatus.running:
@@ -46,7 +107,9 @@ class DeviceState {
final hours = remainingSeconds! ~/ 3600;
final minutes = (remainingSeconds! % 3600) ~/ 60;
final seconds = remainingSeconds! % 60;
return '${hours.toString().padLeft(2, '0')}:${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}';
return '${hours.toString().padLeft(2, '0')}:'
'${minutes.toString().padLeft(2, '0')}:'
'${seconds.toString().padLeft(2, '0')}';
}
DeviceState copyWith({
@@ -57,17 +120,28 @@ class DeviceState {
String? currentStepName,
int? remainingSeconds,
double? progress,
bool? lightingOn,
DeviceLightStatus? lightStatus,
DeviceDoorStatus? doorStatus,
DeviceTaskStatus? taskStatus,
DateTime? lastInfoAt,
bool clearProgram = false,
bool clearProgress = false,
}) {
return DeviceState(
status: status ?? this.status,
currentProgram: currentProgram ?? this.currentProgram,
currentProgram: clearProgram ? null : (currentProgram ?? this.currentProgram),
currentPosition: currentPosition ?? this.currentPosition,
currentStepNo: currentStepNo ?? this.currentStepNo,
currentStepName: currentStepName ?? this.currentStepName,
remainingSeconds: remainingSeconds ?? this.remainingSeconds,
progress: progress ?? this.progress,
lightingOn: lightingOn ?? this.lightingOn,
progress: clearProgress ? null : (progress ?? this.progress),
lightStatus: lightStatus ?? this.lightStatus,
doorStatus: doorStatus ?? this.doorStatus,
taskStatus: taskStatus ?? this.taskStatus,
lastInfoAt: lastInfoAt ?? this.lastInfoAt,
);
}
}
}
/// 简单的状态枚举(与 [DeviceState.status] 配合使用)
enum DeviceStatus { idle, running, paused, error }