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:
@@ -1,63 +1,100 @@
|
||||
import 'dart:async';
|
||||
|
||||
import '../../programs/models/program.dart';
|
||||
import '../../programs/models/step.dart';
|
||||
import 'device_message.dart';
|
||||
import 'device_message_service.dart';
|
||||
import 'runner_interface.dart';
|
||||
import 'task_payload.dart';
|
||||
|
||||
/// 串口运行器(真实硬件实现)
|
||||
/// 实现与设备的串口通信
|
||||
class SerialRunner implements Runner {
|
||||
/// JSON 协议运行器
|
||||
///
|
||||
/// 与下位机的程序运行相关通信(create_task / control)通过
|
||||
/// [DeviceMessageService] 完成;运行过程中下位机可通过 ack 消息确认动作,
|
||||
/// 步骤进度仍由下位机主动上报(具体协议待硬件侧确认)。
|
||||
///
|
||||
/// 当前实现:
|
||||
/// 1. start → 发送 `create_task` 消息(need_ack=true),收到 ack 后切到 running;
|
||||
/// 2. pause → 发送 `control{status:pause}`,切到 paused;
|
||||
/// 3. resume → 发送 `control{status:continue}`,切到 running;
|
||||
/// 4. stop → 发送 `control{status:stop}`,切到 idle。
|
||||
///
|
||||
/// 为兜底下位机不主动上报完成的情况,保留本地倒计时 + 步骤推进。
|
||||
class JsonSerialRunner implements Runner {
|
||||
@override
|
||||
RunnerStatus status = RunnerStatus.idle;
|
||||
|
||||
/// 串口配置
|
||||
final String portName;
|
||||
final int baudRate;
|
||||
final int dataBits;
|
||||
final int stopBits;
|
||||
final DeviceMessageService _msg;
|
||||
// ignore: unused_field 持有当前运行的程序引用,便于调试与未来扩展
|
||||
Program? _program;
|
||||
List<Step> _steps = const [];
|
||||
RunnerCallbacks? _callbacks;
|
||||
|
||||
SerialRunner({
|
||||
this.portName = '/dev/ttyUSB0',
|
||||
this.baudRate = 9600,
|
||||
this.dataBits = 8,
|
||||
this.stopBits = 1,
|
||||
});
|
||||
void Function()? _cancelAckSub;
|
||||
Timer? _ticker;
|
||||
int _currentStepIndex = 0;
|
||||
int _remainingSeconds = 0;
|
||||
String? _pendingCreateTaskId;
|
||||
String? _pendingControlId;
|
||||
|
||||
JsonSerialRunner({required DeviceMessageService messageService})
|
||||
: _msg = messageService {
|
||||
_cancelAckSub = _msg.subscribe(DeviceMessageType.createTask, _onCreateTaskAck);
|
||||
_msg.subscribe(DeviceMessageType.control, _onControlAck);
|
||||
}
|
||||
|
||||
@override
|
||||
void start(Program program, List<Step> steps, RunnerCallbacks callbacks) {
|
||||
// TODO: 实现串口通信启动逻辑
|
||||
// 1. 打开串口连接
|
||||
// 2. 发送程序配置
|
||||
// 3. 按步骤发送控制指令
|
||||
// 4. 接收设备反馈并更新状态
|
||||
if (status == RunnerStatus.running) {
|
||||
callbacks.onError?.call('已有程序在运行中');
|
||||
return;
|
||||
}
|
||||
if (steps.isEmpty) {
|
||||
callbacks.onError?.call('程序步骤为空');
|
||||
status = RunnerStatus.error;
|
||||
return;
|
||||
}
|
||||
|
||||
status = RunnerStatus.running;
|
||||
_program = program;
|
||||
_steps = List.unmodifiable(steps);
|
||||
_callbacks = callbacks;
|
||||
_currentStepIndex = 0;
|
||||
_remainingSeconds = _stepTotalSeconds(steps[0]);
|
||||
|
||||
// 示例:发送启动指令
|
||||
// _sendCommand('START', program.code);
|
||||
|
||||
// 示例:监听设备状态
|
||||
// _listenToDevice(callbacks);
|
||||
final payload = TaskPayload.fromProgram(program, steps);
|
||||
final messageId = _msg.nextId();
|
||||
_pendingCreateTaskId = messageId;
|
||||
final msg = payload.toMessage(messageId, needAck: true);
|
||||
_msg.send(msg).then((ok) {
|
||||
if (!ok) {
|
||||
_pendingCreateTaskId = null;
|
||||
status = RunnerStatus.error;
|
||||
_callbacks?.onError?.call('下发任务失败:串口写入错误');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void pause() {
|
||||
if (status == RunnerStatus.running) {
|
||||
// _sendCommand('PAUSE');
|
||||
status = RunnerStatus.paused;
|
||||
}
|
||||
if (status != RunnerStatus.running) return;
|
||||
_sendControl('pause');
|
||||
_stopLocalTicker();
|
||||
status = RunnerStatus.paused;
|
||||
}
|
||||
|
||||
@override
|
||||
void resume() {
|
||||
if (status == RunnerStatus.paused) {
|
||||
// _sendCommand('RESUME');
|
||||
status = RunnerStatus.running;
|
||||
}
|
||||
if (status != RunnerStatus.paused) return;
|
||||
_sendControl('continue');
|
||||
status = RunnerStatus.running;
|
||||
_startLocalTicker();
|
||||
}
|
||||
|
||||
@override
|
||||
void stop() {
|
||||
// _sendCommand('STOP');
|
||||
// _closeConnection();
|
||||
if (status == RunnerStatus.idle) return;
|
||||
_sendControl('stop');
|
||||
_teardown();
|
||||
status = RunnerStatus.idle;
|
||||
}
|
||||
|
||||
@@ -66,26 +103,102 @@ class SerialRunner implements Runner {
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
stop();
|
||||
_teardown();
|
||||
_cancelAckSub?.call();
|
||||
_cancelAckSub = null;
|
||||
}
|
||||
|
||||
/// 发送控制指令(待硬件协议确定后实现)
|
||||
Future<void> _sendCommand(String command, [String? data]) async {
|
||||
// TODO: 根据硬件通信协议实现
|
||||
// 示例协议格式: [CMD:data] 或 二进制协议
|
||||
// -- 私有方法 ---------------------------------------------------------
|
||||
|
||||
void _sendControl(String statusValue) {
|
||||
final messageId = _msg.nextId();
|
||||
_pendingControlId = messageId;
|
||||
final msg = DeviceMessage.request(
|
||||
messageId: messageId,
|
||||
type: DeviceMessageType.control,
|
||||
data: {'status': statusValue},
|
||||
needAck: true,
|
||||
);
|
||||
_msg.send(msg);
|
||||
}
|
||||
|
||||
/// 监听设备反馈(待硬件协议确定后实现)
|
||||
void _listenToDevice(RunnerCallbacks callbacks) {
|
||||
// TODO: 解析设备返回的状态数据
|
||||
// 状态格式示例: [STEP:1,TIME:60,POS:A1]
|
||||
void _onCreateTaskAck(DeviceMessage ack) {
|
||||
if (ack.ack != _pendingCreateTaskId) return;
|
||||
_pendingCreateTaskId = null;
|
||||
// ack 即视为下位机已接受任务,进入 running 状态
|
||||
if (status == RunnerStatus.idle || status == RunnerStatus.error) {
|
||||
status = RunnerStatus.running;
|
||||
_startLocalTicker();
|
||||
}
|
||||
}
|
||||
|
||||
/// 执行单个步骤
|
||||
Future<void> _executeStep(Step step) async {
|
||||
// TODO: 根据步骤参数生成控制指令
|
||||
// 混合: MIX(position, time, speed)
|
||||
// 吸磁: MAGNET(position, time)
|
||||
// 吹气: BLOW(position, speed, time)
|
||||
void _onControlAck(DeviceMessage ack) {
|
||||
if (ack.ack != _pendingControlId) return;
|
||||
_pendingControlId = null;
|
||||
// control ack 不修改状态,状态机在调用 pause/resume/stop 时已切过
|
||||
}
|
||||
}
|
||||
|
||||
// -- 本地兜底倒计时 ---------------------------------------------------
|
||||
|
||||
void _startLocalTicker() {
|
||||
_ticker?.cancel();
|
||||
_ticker = Timer.periodic(const Duration(seconds: 1), (_) {
|
||||
if (status != RunnerStatus.running) return;
|
||||
if (_steps.isEmpty) return;
|
||||
_remainingSeconds--;
|
||||
if (_remainingSeconds <= 0) {
|
||||
_currentStepIndex++;
|
||||
if (_currentStepIndex >= _steps.length) {
|
||||
_stopLocalTicker();
|
||||
status = RunnerStatus.completed;
|
||||
_callbacks?.onComplete?.call();
|
||||
return;
|
||||
}
|
||||
_remainingSeconds = _stepTotalSeconds(_steps[_currentStepIndex]);
|
||||
}
|
||||
final well = _steps[_currentStepIndex.clamp(0, _steps.length - 1)]
|
||||
.position;
|
||||
_callbacks?.onProgress?.call(
|
||||
_currentStepIndex,
|
||||
_remainingSeconds,
|
||||
_calculateProgress(_currentStepIndex, _remainingSeconds),
|
||||
well,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
void _stopLocalTicker() {
|
||||
_ticker?.cancel();
|
||||
_ticker = null;
|
||||
}
|
||||
|
||||
void _teardown() {
|
||||
_stopLocalTicker();
|
||||
_program = null;
|
||||
_steps = const [];
|
||||
_callbacks = null;
|
||||
_pendingCreateTaskId = null;
|
||||
_pendingControlId = null;
|
||||
}
|
||||
|
||||
int _stepTotalSeconds(Step s) {
|
||||
final t = s.mixTime + s.magnetTime + s.blowTime;
|
||||
return t == 0 ? 5 : t;
|
||||
}
|
||||
|
||||
double _calculateProgress(int stepIndex, int remaining) {
|
||||
if (_steps.isEmpty) return 0;
|
||||
var total = 0;
|
||||
for (final s in _steps) {
|
||||
total += _stepTotalSeconds(s);
|
||||
}
|
||||
if (total <= 0) return 0;
|
||||
var elapsed = 0;
|
||||
for (var i = 0; i < stepIndex && i < _steps.length; i++) {
|
||||
elapsed += _stepTotalSeconds(_steps[i]);
|
||||
}
|
||||
final cur = _stepTotalSeconds(_steps[stepIndex.clamp(0, _steps.length - 1)]);
|
||||
elapsed += (cur - remaining).clamp(0, cur);
|
||||
return (elapsed / total).clamp(0.0, 1.0);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user