- 更新设备屏幕尺寸配置从1920*1080调整为1024x600 - 添加完成页面的AppBar导航和返回功能 - 重构CompletePage布局,使用SafeArea和ConstrainedBox适配不同屏幕 - 添加国际化支持的完成按钮文本 - 优化完成页面视觉元素,包括图标大小和间距调整 - 实现串口连接状态的响应式管理,解决UI状态同步问题 - 优化串口运行器的状态更新逻辑,实现乐观更新机制 - 调整完成页面按钮布局,提供完成和重新运行选项
217 lines
6.7 KiB
Dart
217 lines
6.7 KiB
Dart
import 'dart:async';
|
||
|
||
import '../../programs/models/program.dart';
|
||
import '../../programs/models/step.dart';
|
||
import 'device_log.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);
|
||
// 乐观更新:与 RunStateNotifier 的运行态保持一致,
|
||
// 避免在 create_task 应答到达前的窗口里,pause/stop 被状态守卫静默丢弃。
|
||
status = RunnerStatus.running;
|
||
DeviceLog.info('Runner.start: program="${program.name}" steps=${steps.length} '
|
||
'temperature=${program.temperature} airflow=${program.airflowTime}');
|
||
_msg.send(msg).then((ok) {
|
||
if (!ok) {
|
||
_pendingCreateTaskId = null;
|
||
status = RunnerStatus.error;
|
||
_callbacks?.onError?.call('下发任务失败:串口写入错误');
|
||
}
|
||
});
|
||
}
|
||
|
||
@override
|
||
void pause() {
|
||
if (status != RunnerStatus.running) return;
|
||
DeviceLog.info('Runner.pause');
|
||
_sendControl('pause');
|
||
_stopLocalTicker();
|
||
status = RunnerStatus.paused;
|
||
}
|
||
|
||
@override
|
||
void resume() {
|
||
if (status != RunnerStatus.paused) return;
|
||
DeviceLog.info('Runner.resume');
|
||
_sendControl('continue');
|
||
status = RunnerStatus.running;
|
||
_startLocalTicker();
|
||
}
|
||
|
||
@override
|
||
void stop() {
|
||
if (status == RunnerStatus.idle) return;
|
||
DeviceLog.info('Runner.stop');
|
||
_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;
|
||
DeviceLog.info('Runner received create_task ack: id=${ack.messageId}');
|
||
// 状态已由 start() 乐观置为 running,此处仅启动本地兜底倒计时。
|
||
// 若发送失败,.then() 已将 status 置为 error,不应再启动倒计时。
|
||
if (status == RunnerStatus.running) {
|
||
_startLocalTicker();
|
||
}
|
||
}
|
||
|
||
void _onControlAck(DeviceMessage ack) {
|
||
if (ack.ack != _pendingControlId) return;
|
||
_pendingControlId = null;
|
||
DeviceLog.info('Runner received control ack: id=${ack.messageId}');
|
||
// 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) {
|
||
DeviceLog.info('Runner 本地倒计时完成 (timeout fallback)');
|
||
_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);
|
||
}
|
||
}
|