feat(device): 启动自动连接 USB 串口 + 隐藏设置页配置项 + 标题栏连接状态
- 新增 AutoSerialConnect 服务:启动后自动连接第一个 USB 串口设备, 固定 115200/8/N/1,连接失败时每 3s 重试,断开后重新进入重试循环 - main.dart 通过 ProviderContainer 在 runApp 之前触发 autoSerialConnectProvider - 移除设置页「串口配置」菜单项及对应面板分支 - StatusBar 在「设备运行状态」前增加串口连接状态指示(已连接/连接中/未连接)
This commit is contained in:
@@ -2,6 +2,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|||||||
|
|
||||||
import '../../../core/database/database_service.dart';
|
import '../../../core/database/database_service.dart';
|
||||||
import '../models/serial_config.dart';
|
import '../models/serial_config.dart';
|
||||||
|
import '../services/auto_serial_connect.dart';
|
||||||
import '../services/device_message_service.dart';
|
import '../services/device_message_service.dart';
|
||||||
import '../services/json_protocol.dart';
|
import '../services/json_protocol.dart';
|
||||||
import '../services/runner_interface.dart';
|
import '../services/runner_interface.dart';
|
||||||
@@ -15,6 +16,18 @@ final serialPortServiceProvider = Provider<SerialPortService>((ref) {
|
|||||||
return service;
|
return service;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/// 启动自动连接服务
|
||||||
|
///
|
||||||
|
/// 通过 [main] 中的 ProviderContainer 在 runApp 之前触发一次,
|
||||||
|
/// 服务内部立即尝试连接第一个 USB 串口设备,失败时按 3s 间隔重试。
|
||||||
|
final autoSerialConnectProvider = Provider<AutoSerialConnect>((ref) {
|
||||||
|
final service = ref.watch(serialPortServiceProvider);
|
||||||
|
final auto = AutoSerialConnect(service);
|
||||||
|
auto.start();
|
||||||
|
ref.onDispose(auto.dispose);
|
||||||
|
return auto;
|
||||||
|
});
|
||||||
|
|
||||||
/// JSON 协议编解码器(可在调试/真机协议不一致时整体替换)
|
/// JSON 协议编解码器(可在调试/真机协议不一致时整体替换)
|
||||||
final jsonProtocolProvider = Provider<JsonProtocolService>((ref) {
|
final jsonProtocolProvider = Provider<JsonProtocolService>((ref) {
|
||||||
return JsonProtocolService();
|
return JsonProtocolService();
|
||||||
|
|||||||
109
lib/features/device/services/auto_serial_connect.dart
Normal file
109
lib/features/device/services/auto_serial_connect.dart
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:usb_serial/usb_serial.dart';
|
||||||
|
|
||||||
|
import '../models/serial_config.dart';
|
||||||
|
import 'device_log.dart';
|
||||||
|
import 'serial_port_service.dart';
|
||||||
|
|
||||||
|
/// 启动自动连接服务
|
||||||
|
///
|
||||||
|
/// App 启动时调用 [start],自动连接第一个可用的 USB 串口设备:
|
||||||
|
/// - 固定 115200 波特率 / 8 数据位 / 1 停止位 / 默认其它
|
||||||
|
/// - 连接失败时每 3 秒重试一次
|
||||||
|
/// - 连接成功后停止重试;连接断开后重新进入重试循环
|
||||||
|
class AutoSerialConnect {
|
||||||
|
/// 重试间隔
|
||||||
|
static const Duration retryInterval = Duration(seconds: 3);
|
||||||
|
|
||||||
|
/// 自动连接使用的固定参数
|
||||||
|
static const SerialConfig autoConfig = SerialConfig(
|
||||||
|
baudRate: 115200,
|
||||||
|
dataBits: 8,
|
||||||
|
stopBits: 1,
|
||||||
|
);
|
||||||
|
|
||||||
|
final SerialPortService _service;
|
||||||
|
Timer? _retryTimer;
|
||||||
|
StreamSubscription<SerialConnectionState>? _stateSub;
|
||||||
|
bool _disposed = false;
|
||||||
|
|
||||||
|
AutoSerialConnect(this._service);
|
||||||
|
|
||||||
|
/// 启动自动连接:立即尝试一次,失败则按 [retryInterval] 周期重试
|
||||||
|
void start() {
|
||||||
|
if (_stateSub != null) return;
|
||||||
|
_stateSub = _service.connectionStateChanges.listen(_onStateChange);
|
||||||
|
unawaited(_tryConnect());
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onStateChange(SerialConnectionState s) {
|
||||||
|
if (_disposed) return;
|
||||||
|
switch (s) {
|
||||||
|
case SerialConnectionState.connected:
|
||||||
|
// 连接成功:停止重试
|
||||||
|
_cancelRetry();
|
||||||
|
case SerialConnectionState.disconnected:
|
||||||
|
case SerialConnectionState.error:
|
||||||
|
// 设备断开或出错:进入重试
|
||||||
|
_scheduleRetry();
|
||||||
|
case SerialConnectionState.connecting:
|
||||||
|
// 正在连接中,忽略
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _tryConnect() async {
|
||||||
|
if (_disposed) return;
|
||||||
|
if (_service.isConnected ||
|
||||||
|
_service.state == SerialConnectionState.connecting) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final List<UsbDevice> devices;
|
||||||
|
try {
|
||||||
|
devices = await _service.listDevices();
|
||||||
|
} catch (e) {
|
||||||
|
DeviceLog.warn('列出 USB 设备失败: $e,${retryInterval.inSeconds}s 后重试');
|
||||||
|
_scheduleRetry();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (devices.isEmpty) {
|
||||||
|
DeviceLog.info('未检测到 USB 串口设备,${retryInterval.inSeconds}s 后重试');
|
||||||
|
_scheduleRetry();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final device = devices.first;
|
||||||
|
final ok = await _service.connect(device, autoConfig);
|
||||||
|
if (!ok) {
|
||||||
|
DeviceLog.warn('串口自动连接失败: '
|
||||||
|
'${_service.lastError ?? "未知错误"},${retryInterval.inSeconds}s 后重试');
|
||||||
|
_scheduleRetry();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _scheduleRetry() {
|
||||||
|
if (_disposed) return;
|
||||||
|
if (_retryTimer != null) return;
|
||||||
|
_retryTimer = Timer(retryInterval, () {
|
||||||
|
_retryTimer = null;
|
||||||
|
unawaited(_tryConnect());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _cancelRetry() {
|
||||||
|
_retryTimer?.cancel();
|
||||||
|
_retryTimer = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 释放资源
|
||||||
|
Future<void> dispose() async {
|
||||||
|
if (_disposed) return;
|
||||||
|
_disposed = true;
|
||||||
|
_cancelRetry();
|
||||||
|
await _stateSub?.cancel();
|
||||||
|
_stateSub = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,6 +5,8 @@ import '../../../core/localization/app_localizations.dart';
|
|||||||
import '../../../core/theme/app_theme.dart';
|
import '../../../core/theme/app_theme.dart';
|
||||||
import '../../../shared/widgets/status_indicator.dart';
|
import '../../../shared/widgets/status_indicator.dart';
|
||||||
import '../../device/providers/device_info_provider.dart';
|
import '../../device/providers/device_info_provider.dart';
|
||||||
|
import '../../device/providers/serial_provider.dart';
|
||||||
|
import '../../device/services/serial_port_service.dart';
|
||||||
|
|
||||||
/// 状态栏标签项数据
|
/// 状态栏标签项数据
|
||||||
class StatusBarTab {
|
class StatusBarTab {
|
||||||
@@ -76,6 +78,7 @@ class _StatusBarState extends ConsumerState<StatusBar> {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final l10n = AppLocalizations.of(context);
|
final l10n = AppLocalizations.of(context);
|
||||||
final deviceInfo = ref.watch(deviceInfoProvider);
|
final deviceInfo = ref.watch(deviceInfoProvider);
|
||||||
|
final serialState = ref.watch(serialPortServiceProvider).state;
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
height: 56,
|
height: 56,
|
||||||
@@ -117,6 +120,8 @@ class _StatusBarState extends ConsumerState<StatusBar> {
|
|||||||
onTap: _onLightTap,
|
onTap: _onLightTap,
|
||||||
),
|
),
|
||||||
const SizedBox(width: 20),
|
const SizedBox(width: 20),
|
||||||
|
_SerialConnectionIndicator(state: serialState),
|
||||||
|
const SizedBox(width: 20),
|
||||||
StatusIndicator(
|
StatusIndicator(
|
||||||
text: widget.isRunning
|
text: widget.isRunning
|
||||||
? (l10n?.running ?? '运行中')
|
? (l10n?.running ?? '运行中')
|
||||||
@@ -262,3 +267,49 @@ class _LightToggleButton extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 串口连接状态指示器
|
||||||
|
///
|
||||||
|
/// 位于「设备运行状态」之前,反映当前 USB 串口的连接情况。
|
||||||
|
class _SerialConnectionIndicator extends StatelessWidget {
|
||||||
|
final SerialConnectionState state;
|
||||||
|
const _SerialConnectionIndicator({required this.state});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final connected = state == SerialConnectionState.connected;
|
||||||
|
final connecting = state == SerialConnectionState.connecting;
|
||||||
|
final text = connected
|
||||||
|
? '已连接'
|
||||||
|
: connecting
|
||||||
|
? '连接中'
|
||||||
|
: '未连接';
|
||||||
|
final color = connected
|
||||||
|
? AppTheme.statusRunning
|
||||||
|
: connecting
|
||||||
|
? AppTheme.statusPaused
|
||||||
|
: AppTheme.statusStopped;
|
||||||
|
return Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
width: 10,
|
||||||
|
height: 10,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
color: color,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 6),
|
||||||
|
Text(
|
||||||
|
text,
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
fontSize: 13,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,11 +5,10 @@ import '../../../core/theme/app_theme.dart';
|
|||||||
import '../../../shared/widgets/common_button.dart';
|
import '../../../shared/widgets/common_button.dart';
|
||||||
import '../widgets/language_panel.dart';
|
import '../widgets/language_panel.dart';
|
||||||
import '../widgets/password_panel.dart';
|
import '../widgets/password_panel.dart';
|
||||||
import '../widgets/serial_config_panel.dart';
|
|
||||||
import '../widgets/usb_import_panel.dart';
|
import '../widgets/usb_import_panel.dart';
|
||||||
|
|
||||||
/// 设置页菜单
|
/// 设置页菜单
|
||||||
enum _SettingsMenu { upgrade, language, password, usbImport, serialConfig }
|
enum _SettingsMenu { upgrade, language, password, usbImport }
|
||||||
|
|
||||||
/// 系统设置页面
|
/// 系统设置页面
|
||||||
class SettingsPage extends ConsumerStatefulWidget {
|
class SettingsPage extends ConsumerStatefulWidget {
|
||||||
@@ -70,14 +69,6 @@ class _SettingsPageState extends ConsumerState<SettingsPage> {
|
|||||||
onTap: () => setState(
|
onTap: () => setState(
|
||||||
() => _currentMenu = _SettingsMenu.upgrade),
|
() => _currentMenu = _SettingsMenu.upgrade),
|
||||||
),
|
),
|
||||||
// 串口配置
|
|
||||||
_buildMenuItem(
|
|
||||||
icon: Icons.settings_input_hdmi,
|
|
||||||
title: '串口配置',
|
|
||||||
selected: _currentMenu == _SettingsMenu.serialConfig,
|
|
||||||
onTap: () => setState(
|
|
||||||
() => _currentMenu = _SettingsMenu.serialConfig),
|
|
||||||
),
|
|
||||||
// 语言设置
|
// 语言设置
|
||||||
_buildMenuItem(
|
_buildMenuItem(
|
||||||
icon: Icons.language,
|
icon: Icons.language,
|
||||||
@@ -127,7 +118,6 @@ class _SettingsPageState extends ConsumerState<SettingsPage> {
|
|||||||
|
|
||||||
Widget _buildContent() {
|
Widget _buildContent() {
|
||||||
return switch (_currentMenu) {
|
return switch (_currentMenu) {
|
||||||
_SettingsMenu.serialConfig => const SerialConfigPanel(),
|
|
||||||
_SettingsMenu.language => const LanguagePanel(),
|
_SettingsMenu.language => const LanguagePanel(),
|
||||||
_SettingsMenu.password => const PasswordPanel(),
|
_SettingsMenu.password => const PasswordPanel(),
|
||||||
_SettingsMenu.usbImport => const UsbImportPanel(),
|
_SettingsMenu.usbImport => const UsbImportPanel(),
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import 'core/theme/app_theme.dart';
|
|||||||
import 'core/localization/app_localizations.dart';
|
import 'core/localization/app_localizations.dart';
|
||||||
import 'core/localization/locale_provider.dart';
|
import 'core/localization/locale_provider.dart';
|
||||||
import 'core/database/database_service.dart';
|
import 'core/database/database_service.dart';
|
||||||
|
import 'features/device/providers/serial_provider.dart';
|
||||||
|
|
||||||
/// 应用入口
|
/// 应用入口
|
||||||
void main() async {
|
void main() async {
|
||||||
@@ -22,7 +23,17 @@ void main() async {
|
|||||||
final db = DatabaseService.instance;
|
final db = DatabaseService.instance;
|
||||||
await db.database;
|
await db.database;
|
||||||
await db.initTestData();
|
await db.initTestData();
|
||||||
runApp(const ProviderScope(child: KuaishaiApp()));
|
|
||||||
|
// 使用 ProviderContainer 在 runApp 之前触发启动自动连接
|
||||||
|
final container = ProviderContainer();
|
||||||
|
container.read(autoSerialConnectProvider);
|
||||||
|
|
||||||
|
runApp(
|
||||||
|
UncontrolledProviderScope(
|
||||||
|
container: container,
|
||||||
|
child: const KuaishaiApp(),
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 应用主体
|
/// 应用主体
|
||||||
|
|||||||
Reference in New Issue
Block a user