Files
kuaishai2/lib/features/device/services/serial_runner.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

205 lines
5.9 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: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';
/// 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 DeviceMessageService _msg;
// ignore: unused_field 持有当前运行的程序引用,便于调试与未来扩展
Program? _program;
List<Step> _steps = const [];
RunnerCallbacks? _callbacks;
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) {
if (status == RunnerStatus.running) {
callbacks.onError?.call('已有程序在运行中');
return;
}
if (steps.isEmpty) {
callbacks.onError?.call('程序步骤为空');
status = RunnerStatus.error;
return;
}
_program = program;
_steps = List.unmodifiable(steps);
_callbacks = callbacks;
_currentStepIndex = 0;
_remainingSeconds = _stepTotalSeconds(steps[0]);
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) return;
_sendControl('pause');
_stopLocalTicker();
status = RunnerStatus.paused;
}
@override
void resume() {
if (status != RunnerStatus.paused) return;
_sendControl('continue');
status = RunnerStatus.running;
_startLocalTicker();
}
@override
void stop() {
if (status == RunnerStatus.idle) return;
_sendControl('stop');
_teardown();
status = RunnerStatus.idle;
}
@override
RunnerStatus getStatus() => status;
@override
void dispose() {
_teardown();
_cancelAckSub?.call();
_cancelAckSub = null;
}
// -- 私有方法 ---------------------------------------------------------
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 _onCreateTaskAck(DeviceMessage ack) {
if (ack.ack != _pendingCreateTaskId) return;
_pendingCreateTaskId = null;
// ack 即视为下位机已接受任务,进入 running 状态
if (status == RunnerStatus.idle || status == RunnerStatus.error) {
status = RunnerStatus.running;
_startLocalTicker();
}
}
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);
}
}