import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:usb_serial/usb_serial.dart'; import '../../../core/theme/app_theme.dart'; import '../../device/models/serial_config.dart'; import '../../device/providers/serial_provider.dart'; import '../../device/services/device_message.dart'; import '../../device/services/serial_port_service.dart'; /// 串口配置面板 /// /// 展示当前连接状态、可用设备列表、串口参数,并提供 /// 「刷新设备 / 连接 / 断开 / 测试」等操作。 class SerialConfigPanel extends ConsumerStatefulWidget { const SerialConfigPanel({super.key}); @override ConsumerState createState() => _SerialConfigPanelState(); } class _SerialConfigPanelState extends ConsumerState { List _devices = const []; UsbDevice? _selectedDevice; bool _loadingDevices = false; bool _operating = false; @override void initState() { super.initState(); WidgetsBinding.instance.addPostFrameCallback((_) => _refreshDevices()); } Future _refreshDevices() async { setState(() => _loadingDevices = true); try { final service = ref.read(serialPortServiceProvider); final list = await service.listDevices(); if (!mounted) return; setState(() { _devices = list; _loadingDevices = false; if (_selectedDevice == null && list.isNotEmpty) { _selectedDevice = list.first; } }); } catch (e) { if (!mounted) return; setState(() => _loadingDevices = false); _showSnack('扫描设备失败: $e', AppTheme.errorColor); } } Future _connect() async { final device = _selectedDevice; if (device == null) { _showSnack('请先选择串口设备', AppTheme.warningColor); return; } setState(() => _operating = true); final service = ref.read(serialPortServiceProvider); final config = ref.read(serialConfigProvider); final ok = await service.connect(device, config); if (!mounted) return; setState(() => _operating = false); _showSnack( ok ? '连接成功' : '连接失败: ${service.lastError ?? "未知错误"}', ok ? AppTheme.successColor : AppTheme.errorColor, ); } Future _disconnect() async { setState(() => _operating = true); final service = ref.read(serialPortServiceProvider); await service.disconnect(); if (!mounted) return; setState(() => _operating = false); _showSnack('已断开串口', AppTheme.infoColor); } Future _testConnection() async { final service = ref.read(serialPortServiceProvider); if (!service.isConnected) { _showSnack('请先连接串口', AppTheme.warningColor); return; } final msgService = ref.read(deviceMessageServiceProvider); final ok = await msgService.send(DeviceMessage.request( messageId: msgService.nextId(), type: DeviceMessageType.deviceInfo, data: const {}, needAck: true, )); if (!mounted) return; _showSnack( ok ? '已发送 device_info 查询' : '发送失败', ok ? AppTheme.successColor : AppTheme.errorColor, ); } void _showSnack(String msg, Color color) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(msg), backgroundColor: color), ); } @override Widget build(BuildContext context) { final config = ref.watch(serialConfigProvider); final state = ref.watch(serialPortServiceProvider).state; return ListView( padding: const EdgeInsets.all(0), children: [ _buildStatusCard(state), const SizedBox(height: 16), _buildDeviceCard(), const SizedBox(height: 16), _buildParamCard(config), const SizedBox(height: 16), _buildActionRow(), ], ); } // -- 状态卡 ---------------------------------------------------------- Widget _buildStatusCard(SerialConnectionState state) { final color = switch (state) { SerialConnectionState.connected => AppTheme.successColor, SerialConnectionState.connecting => AppTheme.warningColor, SerialConnectionState.error => AppTheme.errorColor, SerialConnectionState.disconnected => AppTheme.idleColor, }; final text = switch (state) { SerialConnectionState.connected => '已连接', SerialConnectionState.connecting => '连接中...', SerialConnectionState.error => '错误', SerialConnectionState.disconnected => '未连接', }; return Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: AppTheme.backgroundColor, borderRadius: BorderRadius.circular(8), border: Border.all(color: color.withValues(alpha: 0.4)), ), child: Row( children: [ Container( width: 12, height: 12, decoration: BoxDecoration(color: color, shape: BoxShape.circle), ), const SizedBox(width: 12), Text('串口状态: $text', style: TextStyle( color: AppTheme.textPrimary, fontSize: 16, fontWeight: FontWeight.w600, )), ], ), ); } // -- 设备列表 -------------------------------------------------------- Widget _buildDeviceCard() { return _SectionCard( title: '可用串口设备', trailing: TextButton.icon( onPressed: _loadingDevices ? null : _refreshDevices, icon: const Icon(Icons.refresh, size: 18), label: const Text('刷新'), ), child: _loadingDevices ? const Padding( padding: EdgeInsets.all(24), child: Center(child: CircularProgressIndicator()), ) : _devices.isEmpty ? _buildEmptyDevice() : Column( children: _devices .map((d) => _deviceTile(d)) .toList(growable: false), ), ); } Widget _buildEmptyDevice() { return Padding( padding: const EdgeInsets.symmetric(vertical: 24, horizontal: 8), child: Column( children: [ Icon(Icons.usb_off, size: 40, color: AppTheme.idleColor), const SizedBox(height: 8), Text('未检测到 USB 串口设备', style: TextStyle(color: AppTheme.textSecondary)), const SizedBox(height: 4), Text('请确认下位机已上电并通过 USB 接入设备', style: TextStyle(color: AppTheme.textSecondary, fontSize: 12)), ], ), ); } Widget _deviceTile(UsbDevice d) { final selected = _selectedDevice == d; return InkWell( onTap: () => setState(() => _selectedDevice = d), child: Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12), decoration: BoxDecoration( color: selected ? AppTheme.primaryColor.withValues(alpha: 0.08) : Colors.transparent, border: Border( bottom: BorderSide(color: AppTheme.borderSubtle, width: 0.5), ), ), child: Row( children: [ Icon( selected ? Icons.radio_button_checked : Icons.radio_button_off, color: selected ? AppTheme.primaryColor : AppTheme.idleColor, size: 20, ), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(SerialPortService.deviceLabel(d), style: TextStyle( color: AppTheme.textPrimary, fontWeight: FontWeight.w500, )), const SizedBox(height: 2), Text( 'VID: 0x${(d.vid ?? 0).toRadixString(16).toUpperCase()} ' 'PID: 0x${(d.pid ?? 0).toRadixString(16).toUpperCase()}', style: TextStyle( color: AppTheme.textSecondary, fontSize: 12), ), ], ), ), ], ), ), ); } // -- 参数配置 -------------------------------------------------------- Widget _buildParamCard(SerialConfig config) { return _SectionCard( title: '串口参数', child: Column( children: [ _baudRateRow(config), _dropdownRow( label: '数据位', value: config.dataBits, options: const [5, 6, 7, 8], display: (v) => '$v', onChanged: (v) => _updateConfig((c) => c.copyWith(dataBits: v)), ), _dropdownRow( label: '停止位', value: config.stopBits, options: const [1, 2], display: (v) => '$v', onChanged: (v) => _updateConfig((c) => c.copyWith(stopBits: v)), ), _dropdownRow( label: '校验位', value: config.parity, options: SerialParity.values, display: (v) => switch (v) { SerialParity.none => '无', SerialParity.odd => '奇', SerialParity.even => '偶', SerialParity.mark => '标记', SerialParity.space => '空', }, onChanged: (v) => _updateConfig((c) => c.copyWith(parity: v)), ), _dropdownRow( label: '流控', value: config.flowControl, options: SerialFlowControl.values, display: (v) => switch (v) { SerialFlowControl.none => '无', SerialFlowControl.rtsCts => 'RTS/CTS', SerialFlowControl.xonXoff => 'XON/XOFF', SerialFlowControl.dtrDsr => 'DTR/DSR', }, onChanged: (v) => _updateConfig((c) => c.copyWith(flowControl: v)), ), const SizedBox(height: 8), Align( alignment: Alignment.centerRight, child: Text( '参数修改后自动保存', style: TextStyle(color: AppTheme.textSecondary, fontSize: 12), ), ), ], ), ); } Widget _baudRateRow(SerialConfig config) { final ctrl = TextEditingController(text: config.baudRate.toString()); return Padding( padding: const EdgeInsets.symmetric(vertical: 8), child: Row( children: [ SizedBox( width: 80, child: Text('波特率', style: TextStyle(color: AppTheme.textPrimary))), const SizedBox(width: 12), SizedBox( width: 140, child: TextField( controller: ctrl, keyboardType: TextInputType.number, inputFormatters: [FilteringTextInputFormatter.digitsOnly], decoration: const InputDecoration( isDense: true, contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 10), border: OutlineInputBorder(), ), onSubmitted: (v) => _applyBaudRate(v, config), onEditingComplete: () => _applyBaudRate(ctrl.text, config), ), ), const SizedBox(width: 12), Expanded( child: Wrap( spacing: 6, runSpacing: 6, children: SerialConfig.commonBaudRates .map((b) => _baudChip(b, config.baudRate)) .toList(), ), ), ], ), ); } Widget _baudChip(int baud, int current) { final selected = baud == current; return ChoiceChip( label: Text('$baud'), selected: selected, onSelected: (_) => _updateConfig((c) => c.copyWith(baudRate: baud)), labelStyle: TextStyle( color: selected ? Colors.white : AppTheme.textPrimary, fontSize: 12, ), selectedColor: AppTheme.primaryColor, backgroundColor: AppTheme.backgroundColor, ); } Widget _dropdownRow({ required String label, required T value, required List options, required String Function(T) display, required ValueChanged onChanged, }) { return Padding( padding: const EdgeInsets.symmetric(vertical: 8), child: Row( children: [ SizedBox( width: 80, child: Text(label, style: TextStyle(color: AppTheme.textPrimary))), const SizedBox(width: 12), Expanded( child: DropdownButtonFormField( value: value, isDense: true, decoration: const InputDecoration( isDense: true, contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 10), border: OutlineInputBorder(), ), items: options .map((o) => DropdownMenuItem( value: o, child: Text(display(o)), )) .toList(), onChanged: (v) { if (v != null) onChanged(v); }, ), ), ], ), ); } void _applyBaudRate(String raw, SerialConfig config) { final v = int.tryParse(raw.trim()); if (v == null || v <= 0) return; if (v == config.baudRate) return; _updateConfig((c) => c.copyWith(baudRate: v)); } Future _updateConfig( SerialConfig Function(SerialConfig) mutator) async { await ref.read(serialConfigProvider.notifier).update(mutator); } // -- 操作按钮 -------------------------------------------------------- Widget _buildActionRow() { return Wrap( spacing: 12, runSpacing: 12, children: [ ElevatedButton.icon( onPressed: _operating ? null : _connect, icon: const Icon(Icons.link), label: const Text('连接'), ), ElevatedButton.icon( onPressed: _operating ? null : _disconnect, style: ElevatedButton.styleFrom(backgroundColor: AppTheme.errorColor), icon: const Icon(Icons.link_off), label: const Text('断开'), ), OutlinedButton.icon( onPressed: _operating ? null : _testConnection, icon: const Icon(Icons.send), label: const Text('发送测试帧'), ), ], ); } } class _SectionCard extends StatelessWidget { final String title; final Widget? trailing; final Widget child; const _SectionCard({ required this.title, required this.child, this.trailing, }); @override Widget build(BuildContext context) { return Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(8), border: Border.all(color: AppTheme.borderSubtle), ), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Row( children: [ Expanded( child: Text( title, style: TextStyle( color: AppTheme.textPrimary, fontSize: 16, fontWeight: FontWeight.w600, ), ), ), ?trailing, ], ), const Divider(), child, ], ), ); } }