Files
kuaishai2/lib/features/device/services/serial_runner.dart
Developer 8c2e26ec87 feat(home): 更新完成页面UI并优化串口连接状态管理
- 更新设备屏幕尺寸配置从1920*1080调整为1024x600
- 添加完成页面的AppBar导航和返回功能
- 重构CompletePage布局,使用SafeArea和ConstrainedBox适配不同屏幕
- 添加国际化支持的完成按钮文本
- 优化完成页面视觉元素,包括图标大小和间距调整
- 实现串口连接状态的响应式管理,解决UI状态同步问题
- 优化串口运行器的状态更新逻辑,实现乐观更新机制
- 调整完成页面按钮布局,提供完成和重新运行选项
2026-06-05 10:03:41 +08:00

217 lines
6.7 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_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);
}
}