import 'dart:async'; import 'dart:typed_data'; import 'package:ch934x_serial/ch934x_serial.dart'; import 'package:flutter/material.dart'; /// CH934X 插件示例应用,演示: /// 1. 设备查找; /// 2. 串口打开/关闭; /// 3. 数据发送与接收(GPIO/Modem 控制以按钮形式呈现)。 void main() { runApp(const Ch934xSerialExampleApp()); } class Ch934xSerialExampleApp extends StatelessWidget { const Ch934xSerialExampleApp({super.key}); @override Widget build(BuildContext context) { return MaterialApp( title: 'CH934X Serial Example', theme: ThemeData( colorScheme: ColorScheme.fromSeed(seedColor: Colors.indigo), useMaterial3: true, ), home: const Ch934xSerialExamplePage(), ); } } class Ch934xSerialExamplePage extends StatefulWidget { const Ch934xSerialExamplePage({super.key}); @override State createState() => _Ch934xSerialExamplePageState(); } class _Ch934xSerialExamplePageState extends State { final Ch934xSerial _plugin = Ch934xSerial(); final TextEditingController _commandController = TextEditingController(text: '*IDN?\n'); List _devices = const []; Ch934xDeviceInfo? _selectedDevice; /// 当前选中设备的可用串口索引集合(由 SDK getSerialCount 决定)。 List _availablePortIndices = const []; int? _selectedPortIndex; StreamSubscription? _exceptionSubscription; StreamSubscription? _dataSubscription; String _status = '未连接'; String _receivedBuffer = ''; bool _portOpened = false; @override void initState() { super.initState(); _bindExceptionCallback(); _refreshDeviceList(); } @override void dispose() { _exceptionSubscription?.cancel(); _dataSubscription?.cancel(); if (_portOpened) { // 关闭串口,保证释放 USB 接口。 _plugin.closePort(); } _commandController.dispose(); super.dispose(); } /// 注册异常回调(对应 7.3.1)。 Future _bindExceptionCallback() async { _exceptionSubscription = await _plugin.setExceptionCallback((event) { if (!mounted) return; setState(() { _status = '设备异常: $event'; _portOpened = false; _dataSubscription?.cancel(); _dataSubscription = null; }); }); } /// 触发设备列表刷新(对应 4.1.4)。 Future _refreshDeviceList() async { try { final devices = await _plugin.getDeviceList(); if (!mounted) return; setState(() { _devices = devices; // 清理已不再列表中的旧选项,避免 DropdownButton value 不在 items 中报错。 final selected = _selectedDevice; if (selected != null && !devices.contains(selected)) { _selectedDevice = null; _availablePortIndices = const []; _selectedPortIndex = null; } else if (selected != null) { _availablePortIndices = _resolvePortIndices(selected); final current = _selectedPortIndex; if (current == null || !_availablePortIndices.contains(current)) { _selectedPortIndex = _availablePortIndices.isNotEmpty ? _availablePortIndices.first : null; } } _status = '已扫描到 ${devices.length} 台 CH934X 设备'; }); } on Exception catch (e) { if (!mounted) return; setState(() => _status = '设备扫描失败: $e'); } } /// 把设备的串口描述转换为可用索引列表。 /// /// 优先使用 SDK 返回的 [Ch934xSerialPortInfo.portIndex],缺 /// 失时回退到 0..N-1,确保 UI 始终有可选项。 List _resolvePortIndices(Ch934xDeviceInfo device) { if (device.serialPorts.isNotEmpty) { return device.serialPorts.map((p) => p.portIndex).toList()..sort(); } if (device.interfaceCount > 0) { return List.generate(device.interfaceCount, (i) => i); } // 最坏情况下,允许打开串口 0;用户可通过业务调用切换。 return const [0]; } Future _openPort() async { final device = _selectedDevice; final portIndex = _selectedPortIndex; if (device == null || portIndex == null) { setState(() => _status = '请先选择设备与串口'); return; } final target = Ch934xPortTarget( deviceId: device.deviceId, interfaceNumber: 0, serialPortIndex: portIndex, ); final ok = await _plugin.openPort(target); if (!ok) { setState(() => _status = '打开串口失败,请确认已授予 USB 权限'); return; } setState(() { _portOpened = true; _status = '串口 $portIndex 已打开'; }); _startReceiving(); } Future _closePort() async { await _dataSubscription?.cancel(); _dataSubscription = null; final ok = await _plugin.closePort(); setState(() { _portOpened = false; _status = ok ? '串口已关闭' : '关闭串口失败'; }); } /// 启动数据流订阅(基于 [Ch934xSerial.dataStream])。 void _startReceiving() { _dataSubscription?.cancel(); _dataSubscription = _plugin.dataStream().listen((chunk) { final text = String.fromCharCodes(chunk); setState(() => _receivedBuffer = '$_receivedBuffer$text'); }); } /// 发送命令,展示 [Ch934xSerial.write] 的用法。 Future _sendCommand() async { if (!_portOpened) { setState(() => _status = '请先打开串口'); return; } final data = Uint8List.fromList(_commandController.text.codeUnits); final written = await _plugin.write(data); setState(() => _status = '已写入 $written 字节'); } /// 查询 Modem 状态,展示 7.2.2 接口。 Future _queryModemStatus() async { if (!_portOpened) return; final status = await _plugin.getModemStatus(); setState(() { _status = 'Modem 状态位: 0x${status.toRadixString(16).padLeft(2, '0')} ' '(CTS=${ModemStatus.isSet(status, ModemStatus.cts)}, ' 'DSR=${ModemStatus.isSet(status, ModemStatus.dsr)})'; }); } /// 控制 DTR/RTS,展示 7.2.1 接口。 Future _toggleModem() async { if (!_portOpened) return; final ok = await _plugin.setModemControl(dtr: 1, rts: 1); setState(() => _status = ok ? 'DTR/RTS 置高' : 'Modem 控制失败'); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('CH934X Serial Example'), actions: [ IconButton( tooltip: '刷新设备列表', icon: const Icon(Icons.refresh), onPressed: _refreshDeviceList, ), ], ), body: Padding( padding: const EdgeInsets.all(16), child: ListView( children: [ Text('状态: $_status'), const SizedBox(height: 8), if (_devices.isEmpty) const Card( child: ListTile( leading: Icon(Icons.usb_off), title: Text('未发现 CH934X 设备'), subtitle: Text('请确认 OTG 已连接,并授予 USB 权限'), ), ) else DropdownButton( isExpanded: true, value: _selectedDevice, hint: const Text('选择 CH934X 设备'), items: _devices .map( (d) => DropdownMenuItem( value: d, child: Text( 'VID=0x${d.vendorId.toRadixString(16)} ' 'PID=0x${d.productId.toRadixString(16)} ' 'SN=${d.serialNumber ?? "-"}', ), ), ) .toList(), onChanged: (device) { setState(() { _selectedDevice = device; if (device == null) { _availablePortIndices = const []; _selectedPortIndex = null; } else { _availablePortIndices = _resolvePortIndices(device); _selectedPortIndex = _availablePortIndices.isNotEmpty ? _availablePortIndices.first : null; } }); }, ), if (_selectedDevice != null) ...[ const SizedBox(height: 8), DropdownButton( isExpanded: true, value: _selectedPortIndex, hint: const Text('选择串口'), items: _availablePortIndices .map( (idx) => DropdownMenuItem( value: idx, child: Text('port #$idx'), ), ) .toList(), onChanged: (idx) => setState(() => _selectedPortIndex = idx), ), ], const SizedBox(height: 16), Row( children: [ Expanded( child: FilledButton.icon( onPressed: _portOpened ? null : _openPort, icon: const Icon(Icons.power_settings_new), label: const Text('打开'), ), ), const SizedBox(width: 8), Expanded( child: OutlinedButton.icon( onPressed: _portOpened ? _closePort : null, icon: const Icon(Icons.close), label: const Text('关闭'), ), ), ], ), const SizedBox(height: 16), TextField( controller: _commandController, decoration: const InputDecoration( labelText: '发送数据', border: OutlineInputBorder(), ), maxLines: 3, ), const SizedBox(height: 8), Wrap( spacing: 8, runSpacing: 8, children: [ FilledButton.icon( onPressed: _sendCommand, icon: const Icon(Icons.send), label: const Text('写入'), ), OutlinedButton.icon( onPressed: _queryModemStatus, icon: const Icon(Icons.info_outline), label: const Text('查询 Modem'), ), OutlinedButton.icon( onPressed: _toggleModem, icon: const Icon(Icons.cable), label: const Text('DTR/RTS=1'), ), OutlinedButton.icon( onPressed: () async { if (!_portOpened) return; final v = await _plugin.getGpioInput(0); if (!mounted) return; setState(() => _status = 'GPIO0 = $v'); }, icon: const Icon(Icons.input), label: const Text('读取 GPIO0'), ), ], ), const SizedBox(height: 16), Text( '接收缓冲区:\n$_receivedBuffer', style: const TextStyle(fontFamily: 'monospace'), ), ], ), ), ); } }