feat(device): 添加USB设备通信支持和程序参数优化

- 在AndroidManifest.xml中添加USB Host权限和设备过滤器配置
- 新增设备控制国际化词条包括速度档位、吹气时间等
- 重构数据库结构将速度相关字段统一为档位数值存储
- 添加通用KV存储方法用于settings表数据读写
- 优化首页导航实现tab间跳转和状态保持功能
- 更新程序详情页面布局和参数表单界面
- 移除模拟运行器相关测试代码
- 添加USB串口通信依赖包usb_serial
This commit is contained in:
Developer
2026-06-04 15:13:36 +08:00
parent 67e2c7c76c
commit d53c41c300
24 changed files with 795 additions and 635 deletions

View File

@@ -0,0 +1,126 @@
import 'dart:convert';
/// 串口奇偶校验
enum SerialParity { none, odd, even, mark, space }
/// 串口流控
enum SerialFlowControl { none, rtsCts, xonXoff, dtrDsr }
/// 串口配置
///
/// 持久化到 settings 表的 `serial_config` key 中;
/// 打开串口时根据此配置构造底层 `UsbConfig`。
class SerialConfig {
/// 设备 Vendor ID十六进制
final int vendorId;
/// 设备 Product ID十六进制0 表示不指定
final int productId;
/// 波特率
final int baudRate;
/// 数据位 (5/6/7/8)
final int dataBits;
/// 停止位 (1/2)
final int stopBits;
/// 校验位
final SerialParity parity;
/// 流控
final SerialFlowControl flowControl;
/// 读超时(毫秒)
final int readTimeoutMs;
/// 写超时(毫秒)
final int writeTimeoutMs;
const SerialConfig({
this.vendorId = 0x1A86,
this.productId = 0x7523,
this.baudRate = 9600,
this.dataBits = 8,
this.stopBits = 1,
this.parity = SerialParity.none,
this.flowControl = SerialFlowControl.none,
this.readTimeoutMs = 2000,
this.writeTimeoutMs = 2000,
});
/// 默认配置
static const SerialConfig defaults = SerialConfig();
/// 常用波特率
static const List<int> commonBaudRates = [
1200, 2400, 4800, 9600, 19200, 38400, 57600, 115200, 230400, 460800,
];
SerialConfig copyWith({
int? vendorId,
int? productId,
int? baudRate,
int? dataBits,
int? stopBits,
SerialParity? parity,
SerialFlowControl? flowControl,
int? readTimeoutMs,
int? writeTimeoutMs,
}) {
return SerialConfig(
vendorId: vendorId ?? this.vendorId,
productId: productId ?? this.productId,
baudRate: baudRate ?? this.baudRate,
dataBits: dataBits ?? this.dataBits,
stopBits: stopBits ?? this.stopBits,
parity: parity ?? this.parity,
flowControl: flowControl ?? this.flowControl,
readTimeoutMs: readTimeoutMs ?? this.readTimeoutMs,
writeTimeoutMs: writeTimeoutMs ?? this.writeTimeoutMs,
);
}
/// 编码为 JSON 字符串(用于持久化)
String toJsonString() => jsonEncode({
'vendorId': vendorId,
'productId': productId,
'baudRate': baudRate,
'dataBits': dataBits,
'stopBits': stopBits,
'parity': parity.name,
'flowControl': flowControl.name,
'readTimeoutMs': readTimeoutMs,
'writeTimeoutMs': writeTimeoutMs,
});
/// 从 JSON 字符串解码;解析失败时返回默认值
factory SerialConfig.fromJsonString(String? raw) {
if (raw == null || raw.isEmpty) return defaults;
try {
final map = jsonDecode(raw) as Map<String, dynamic>;
return SerialConfig(
vendorId: (map['vendorId'] as num?)?.toInt() ?? defaults.vendorId,
productId: (map['productId'] as num?)?.toInt() ?? defaults.productId,
baudRate: (map['baudRate'] as num?)?.toInt() ?? defaults.baudRate,
dataBits: (map['dataBits'] as num?)?.toInt() ?? defaults.dataBits,
stopBits: (map['stopBits'] as num?)?.toInt() ?? defaults.stopBits,
parity: SerialParity.values.firstWhere(
(e) => e.name == map['parity'],
orElse: () => defaults.parity,
),
flowControl: SerialFlowControl.values.firstWhere(
(e) => e.name == map['flowControl'],
orElse: () => defaults.flowControl,
),
readTimeoutMs:
(map['readTimeoutMs'] as num?)?.toInt() ?? defaults.readTimeoutMs,
writeTimeoutMs:
(map['writeTimeoutMs'] as num?)?.toInt() ?? defaults.writeTimeoutMs,
);
} catch (_) {
return defaults;
}
}
}

View File

@@ -1,17 +1,18 @@
import 'dart:async';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../programs/models/program.dart';
import '../../programs/models/step.dart';
import '../../programs/services/program_service.dart';
import '../services/mock_runner.dart';
import '../services/runner_interface.dart';
import 'serial_provider.dart';
/// 运行状态枚举
enum RunStatus {
idle, // 待机
running, // 运行中
paused, // 已暂停
completed,// 已完成
error, // 错误
idle, // 待机
running, // 运行中
paused, // 已暂停
completed, // 已完成
error, // 错误
}
/// 运行状态
@@ -23,6 +24,7 @@ class RunState {
final int remainingSeconds;
final double progress;
final String? currentWell;
final String? errorMessage;
const RunState({
this.status = RunStatus.idle,
@@ -32,6 +34,7 @@ class RunState {
this.remainingSeconds = 0,
this.progress = 0,
this.currentWell,
this.errorMessage,
});
RunState copyWith({
@@ -42,17 +45,21 @@ class RunState {
int? remainingSeconds,
double? progress,
String? currentWell,
String? errorMessage,
bool clearProgram = false,
bool clearWell = false,
bool clearError = false,
}) {
return RunState(
status: status ?? this.status,
currentProgram: clearProgram ? null : (currentProgram ?? this.currentProgram),
currentProgram:
clearProgram ? null : (currentProgram ?? this.currentProgram),
steps: steps ?? this.steps,
currentStepIndex: currentStepIndex ?? this.currentStepIndex,
remainingSeconds: remainingSeconds ?? this.remainingSeconds,
progress: progress ?? this.progress,
currentWell: clearWell ? null : (currentWell ?? this.currentWell),
errorMessage: clearError ? null : (errorMessage ?? this.errorMessage),
);
}
@@ -68,8 +75,8 @@ class RunState {
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')}';
'${minutes.toString().padLeft(2, '0')}:'
'${seconds.toString().padLeft(2, '0')}';
}
/// 格式化进度百分比
@@ -80,18 +87,24 @@ class RunState {
/// 运行状态 Notifier
class RunStateNotifier extends StateNotifier<RunState> {
final MockRunner _runner;
final Runner _runner;
final ProgramService _programService;
RunStateNotifier(this._runner, this._programService) : super(const RunState());
/// 开始运行程序
Future<void> start(Program program) async {
// 获取程序步骤(这里使用模拟数据,实际应从数据库读取)
final steps = await _loadSteps(program.id!);
if (state.status == RunStatus.running ||
state.status == RunStatus.paused) {
return;
}
final steps = await _programService.getStepsByProgramId(program.id!);
if (steps.isEmpty) {
state = state.copyWith(status: RunStatus.error);
state = state.copyWith(
status: RunStatus.error,
errorMessage: '程序步骤为空',
);
return;
}
@@ -101,26 +114,36 @@ class RunStateNotifier extends StateNotifier<RunState> {
steps: steps,
currentStepIndex: 0,
progress: 0,
currentWell: steps.first.position,
clearError: true,
);
_runner.start(
program,
steps,
(stepIndex, remaining, progress, well) {
state = state.copyWith(
currentStepIndex: stepIndex,
remainingSeconds: remaining,
progress: progress,
currentWell: well,
);
},
() {
state = state.copyWith(
status: RunStatus.completed,
progress: 1,
clearWell: true,
);
},
RunnerCallbacks(
onProgress: (stepIndex, remaining, progress, well) {
state = state.copyWith(
currentStepIndex: stepIndex,
remainingSeconds: remaining,
progress: progress,
currentWell: well,
);
},
onComplete: () {
state = state.copyWith(
status: RunStatus.completed,
progress: 1,
currentWell: steps.last.position,
);
},
onError: (msg) {
state = state.copyWith(
status: RunStatus.error,
errorMessage: msg,
);
},
),
);
}
@@ -142,26 +165,17 @@ class RunStateNotifier extends StateNotifier<RunState> {
/// 停止运行
void stop() {
_runner.stop();
if (state.status == RunStatus.running ||
state.status == RunStatus.paused) {
_runner.stop();
}
state = const RunState(status: RunStatus.idle);
}
/// 重置状态
void reset() {
stop();
}
/// 加载程序步骤(从数据库读取)
Future<List<Step>> _loadSteps(int programId) async {
return await _programService.getStepsByProgramId(programId);
}
void reset() => stop();
}
/// MockRunner Provider
final mockRunnerProvider = Provider<MockRunner>((ref) {
return MockRunner();
});
/// ProgramService Provider
final programServiceProvider = Provider<ProgramService>((ref) {
return ProgramService.instance;
@@ -170,7 +184,7 @@ final programServiceProvider = Provider<ProgramService>((ref) {
/// 运行状态 Provider
final runStateProvider =
StateNotifierProvider<RunStateNotifier, RunState>((ref) {
final runner = ref.watch(mockRunnerProvider);
final runner = ref.watch(runnerProvider);
final programService = ref.watch(programServiceProvider);
return RunStateNotifier(runner, programService);
});
@@ -185,4 +199,4 @@ final isRunningProvider = Provider<bool>((ref) {
final isPausedProvider = Provider<bool>((ref) {
final status = ref.watch(runStateProvider).status;
return status == RunStatus.paused;
});
});

View File

@@ -1,190 +0,0 @@
import 'dart:async';
import '../../programs/models/step.dart';
import '../../programs/models/program.dart';
/// 模拟运行器回调
typedef RunProgressCallback = void Function(
int currentStepIndex,
int remainingSeconds,
double progress,
String currentWell,
);
typedef RunCompleteCallback = void Function();
/// 模拟运行器
/// 用于在没有实际硬件连接时模拟程序执行过程
class MockRunner {
Timer? _timer;
Program? _currentProgram;
List<Step> _steps = [];
int _currentStepIndex = 0;
int _remainingSeconds = 0;
bool _isPaused = false;
RunProgressCallback? _onProgress;
RunCompleteCallback? _onComplete;
/// 是否正在运行
bool get isRunning => _timer != null && !_isPaused;
/// 是否已暂停
bool get isPaused => _isPaused;
/// 当前程序
Program? get currentProgram => _currentProgram;
/// 开始运行程序
void start(
Program program,
List<Step> steps,
RunProgressCallback onProgress,
RunCompleteCallback onComplete,
) {
_currentProgram = program;
_steps = steps;
_onProgress = onProgress;
_onComplete = onComplete;
_currentStepIndex = 0;
_isPaused = false;
if (steps.isEmpty) {
onComplete();
return;
}
// 开始执行第一个步骤
_startStep(steps[0]);
}
/// 暂停运行
void pause() {
if (_timer != null && !_isPaused) {
_isPaused = true;
_timer!.cancel();
_timer = null;
}
}
/// 继续运行
void resume() {
if (_isPaused && _currentProgram != null) {
_isPaused = false;
_resumeStep();
}
}
/// 停止运行
void stop() {
_timer?.cancel();
_timer = null;
_currentProgram = null;
_steps = [];
_currentStepIndex = 0;
_remainingSeconds = 0;
_isPaused = false;
}
/// 开始执行步骤
void _startStep(Step step) {
// 计算步骤总时间(混合时间 + 吸磁时间 + 吹气时间)
_remainingSeconds = step.mixTime + step.magnetTime + step.blowTime;
// 如果步骤时间为0设置最小演示时间5秒
if (_remainingSeconds == 0) {
_remainingSeconds = 5;
}
// 启动定时器,每秒更新
_timer = Timer.periodic(const Duration(seconds: 1), (timer) {
_remainingSeconds--;
// 计算总进度
final totalSeconds = _calculateTotalSeconds();
final elapsedSeconds = _calculateElapsedSeconds();
final progress = totalSeconds > 0 ? elapsedSeconds / totalSeconds : 0.0;
// 回调进度更新
_onProgress?.call(
_currentStepIndex,
_remainingSeconds,
progress,
step.position,
);
// 步骤完成
if (_remainingSeconds <= 0) {
timer.cancel();
_timer = null;
_nextStep();
}
});
}
/// 继续执行步骤(从暂停恢复)
void _resumeStep() {
if (_currentStepIndex >= _steps.length) return;
final step = _steps[_currentStepIndex];
_timer = Timer.periodic(const Duration(seconds: 1), (timer) {
_remainingSeconds--;
final totalSeconds = _calculateTotalSeconds();
final elapsedSeconds = _calculateElapsedSeconds();
final progress = totalSeconds > 0 ? elapsedSeconds / totalSeconds : 0.0;
_onProgress?.call(
_currentStepIndex,
_remainingSeconds,
progress,
step.position,
);
if (_remainingSeconds <= 0) {
timer.cancel();
_timer = null;
_nextStep();
}
});
}
/// 执行下一个步骤
void _nextStep() {
_currentStepIndex++;
if (_currentStepIndex >= _steps.length) {
// 所有步骤完成
_onComplete?.call();
stop();
} else {
// 执行下一个步骤
_startStep(_steps[_currentStepIndex]);
}
}
/// 计算总执行时间
int _calculateTotalSeconds() {
int total = 0;
for (final step in _steps) {
int stepTime = step.mixTime + step.magnetTime + step.blowTime;
if (stepTime == 0) stepTime = 5;
total += stepTime;
}
return total;
}
/// 计算已执行时间
int _calculateElapsedSeconds() {
int elapsed = 0;
for (int i = 0; i < _currentStepIndex; i++) {
int stepTime = _steps[i].mixTime + _steps[i].magnetTime + _steps[i].blowTime;
if (stepTime == 0) stepTime = 5;
elapsed += stepTime;
}
// 加上当前步骤已执行的时间
final currentStep = _steps[_currentStepIndex];
int currentStepTime = currentStep.mixTime + currentStep.magnetTime + currentStep.blowTime;
if (currentStepTime == 0) currentStepTime = 5;
elapsed += currentStepTime - _remainingSeconds;
return elapsed;
}
}

View File

@@ -1,114 +0,0 @@
import '../../programs/models/program.dart';
import '../../programs/models/step.dart';
import 'runner_interface.dart';
/// 模拟运行器(用于开发测试)
/// 模拟硬件运行过程
class MockRunner implements Runner {
@override
RunnerStatus status = RunnerStatus.idle;
bool _isRunning = false;
int _currentStep = 0;
int _remainingSeconds = 0;
RunnerCallbacks? _callbacks;
List<Step> _steps = [];
@override
void start(Program program, List<Step> steps, RunnerCallbacks callbacks) {
if (steps.isEmpty) {
callbacks.onError?.call('No steps to run');
status = RunnerStatus.error;
return;
}
_steps = steps;
_callbacks = callbacks;
_currentStep = 0;
_isRunning = true;
status = RunnerStatus.running;
// 开始模拟运行
_runSimulation();
}
void _runSimulation() {
if (!_isRunning || _currentStep >= _steps.length) {
_completeRun();
return;
}
final step = _steps[_currentStep];
// 计算步骤时间(混合时间 + 吸磁时间 + 吹气时间 + 5秒最小
final stepTime = (step.mixTime ?? 0) + (step.magnetTime ?? 0) + (step.blowTime ?? 0) + 5;
_remainingSeconds = stepTime.clamp(5, 300);
// 模拟倒计时
_simulateStepProgress(stepTime);
}
void _simulateStepProgress(int totalSeconds) {
// 简化模拟:每秒更新进度
int elapsed = 0;
while (_isRunning && elapsed < totalSeconds) {
elapsed++;
final remaining = totalSeconds - elapsed;
final progress = elapsed / totalSeconds;
_callbacks?.onProgress?.call(
_currentStep,
remaining,
(_currentStep + progress) / _steps.length,
_steps[_currentStep].position,
);
// 实际实现需要使用 Timer
// await Future.delayed(Duration(seconds: 1));
}
if (_isRunning) {
_currentStep++;
_runSimulation();
}
}
void _completeRun() {
status = RunnerStatus.completed;
_isRunning = false;
_callbacks?.onComplete?.call();
}
@override
void pause() {
if (status == RunnerStatus.running) {
_isRunning = false;
status = RunnerStatus.paused;
}
}
@override
void resume() {
if (status == RunnerStatus.paused) {
_isRunning = true;
status = RunnerStatus.running;
// 继续运行
_runSimulation();
}
}
@override
void stop() {
_isRunning = false;
status = RunnerStatus.idle;
_currentStep = 0;
_remainingSeconds = 0;
}
@override
RunnerStatus getStatus() => status;
@override
void dispose() {
stop();
}
}