Files
Developer 020904bd2a feat(ch934x): 添加GPIO方向控制和串口参数配置功能
- 添加Ch934xMode和GpioDirection枚举类定义
- 实现GPIO方向控制相关方法(setGpioDir/queryGpioDirFromCache)
- 添加GPIO使能控制(enableGpio)和数量/组数查询(getGpiocount/getGpiogroup)
- 实现串口参数配置(setSerialParameter)包括波特率/数据位/停止位/校验位/流控
- 添加Break信号控制(setBreak)功能
- 实现当前串口模式查询(getCurrentMode)
- 添加设备连接状态查询(isConnected)和已打开设备列表获取(getConnectedDevices)
- 更新文档说明GPIO方向控制和串口参数配置用法
- 在示例应用中添加相关功能按钮和操作逻辑
2026-07-07 16:15:11 +08:00

536 lines
19 KiB
Dart

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<Ch934xSerialExamplePage> createState() =>
_Ch934xSerialExamplePageState();
}
class _Ch934xSerialExamplePageState extends State<Ch934xSerialExamplePage> {
final Ch934xSerial _plugin = Ch934xSerial();
final TextEditingController _commandController =
TextEditingController(text: '*IDN?\n');
List<Ch934xDeviceInfo> _devices = const <Ch934xDeviceInfo>[];
Ch934xDeviceInfo? _selectedDevice;
/// 当前选中设备的可用串口索引集合(由 SDK getSerialCount 决定)。
List<int> _availablePortIndices = const <int>[];
int? _selectedPortIndex;
StreamSubscription<Ch934xException>? _exceptionSubscription;
StreamSubscription<Uint8List>? _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<void> _bindExceptionCallback() async {
_exceptionSubscription = await _plugin.setExceptionCallback((event) {
if (!mounted) return;
setState(() {
_status = '设备异常: $event';
_portOpened = false;
_dataSubscription?.cancel();
_dataSubscription = null;
});
});
}
/// 触发设备列表刷新(对应 4.1.4)。
///
/// 刷新时主动给每个设备申请一次 USB 权限,避免用户点
/// "打开"时再被弹框打断。已授权的设备 SDK 会立即返回 true。
Future<void> _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 <int>[];
_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 设备';
});
// 后台并发请求权限,失败也不阻塞 UI。
for (final device in devices) {
unawaited(_plugin.requestUsbPermission(device.deviceId));
}
} on Exception catch (e) {
if (!mounted) return;
setState(() => _status = '设备扫描失败: $e');
}
}
/// 把设备的串口描述转换为可用索引列表。
///
/// 若 SDK 还未在原生侧拿到真实串口数(需要 openPort 之后才能问出来),
/// 退回到 [Ch934xDeviceInfo.interfaceCount] 或最小占位 [0]。
List<int> _resolvePortIndices(Ch934xDeviceInfo device) {
if (device.serialPorts.isNotEmpty) {
return device.serialPorts.map((p) => p.portIndex).toList()..sort();
}
if (device.interfaceCount > 0) {
return List<int>.generate(device.interfaceCount, (i) => i);
}
return const <int>[0];
}
/// 打开设备后,主动从原生侧拉取真实串口列表。
Future<void> _refreshPortListAfterOpen(Ch934xDeviceInfo device) async {
final ports = await _plugin.getSerialPortList(
device.deviceId,
interfaceNumber: 0,
);
if (!mounted) return;
setState(() {
// 用 SDK 返回的端口索引覆盖,确保与真实设备保持一致。
_availablePortIndices =
ports.map((p) => p.portIndex).toList()..sort();
if (_availablePortIndices.isEmpty) {
_availablePortIndices = const <int>[0];
}
if (_selectedPortIndex == null ||
!_availablePortIndices.contains(_selectedPortIndex)) {
_selectedPortIndex = _availablePortIndices.first;
}
});
}
Future<void> _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();
// openDevice 之后 SDK 才能正确返回串口数,刷新下拉框。
await _refreshPortListAfterOpen(device);
if (mounted && _portOpened) {
setState(() => _status = '已打开串口 $portIndex,可用 ${_availablePortIndices.length} 路');
}
}
Future<void> _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<void> _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<void> _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<void> _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<Ch934xDeviceInfo>(
isExpanded: true,
value: _selectedDevice,
hint: const Text('选择 CH934X 设备'),
items: _devices
.map(
(d) => DropdownMenuItem<Ch934xDeviceInfo>(
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 <int>[];
_selectedPortIndex = null;
} else {
_availablePortIndices = _resolvePortIndices(device);
_selectedPortIndex = _availablePortIndices.isNotEmpty
? _availablePortIndices.first
: null;
}
});
},
),
if (_selectedDevice != null) ...[
const SizedBox(height: 8),
DropdownButton<int>(
isExpanded: true,
value: _selectedPortIndex,
hint: const Text('选择串口'),
items: _availablePortIndices
.map(
(idx) => DropdownMenuItem<int>(
value: idx,
child: Text('port #$idx'),
),
)
.toList(),
onChanged: _portOpened
? (idx) async {
if (idx == null) return;
setState(() => _selectedPortIndex = idx);
await _plugin.setActivePort(idx);
if (mounted) {
setState(() => _status = '已切换到串口 $idx');
}
}
: null,
),
],
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'),
),
OutlinedButton.icon(
onPressed: () async {
if (!_portOpened) return;
final ok = await _plugin.setBreak(true);
if (!mounted) return;
setState(() => _status = ok ? 'Break 已设置' : 'Break 失败');
},
icon: const Icon(Icons.pause),
label: const Text('SetBreak'),
),
OutlinedButton.icon(
onPressed: () async {
if (!_portOpened) return;
final mode = await _plugin.getCurrentMode();
if (!mounted) return;
final label = switch (mode) {
Ch934xMode.normal => '普通',
Ch934xMode.hardflow => '硬件流控',
Ch934xMode.gpio => 'GPIO',
_ => '未知($mode)',
};
setState(() => _status = '当前模式: $label');
},
icon: const Icon(Icons.settings),
label: const Text('当前模式'),
),
OutlinedButton.icon(
onPressed: () async {
final device = _selectedDevice;
if (device == null) {
setState(() => _status = '请先选择设备');
return;
}
final cnt = await _plugin.getGpiocount(device.deviceId);
final grp = await _plugin.getGpiogroup(device.deviceId);
if (!mounted) return;
setState(() => _status = 'GPIO 数量=$cnt, 组数=$grp');
},
icon: const Icon(Icons.developer_board),
label: const Text('GPIO 信息'),
),
OutlinedButton.icon(
onPressed: () async {
final device = _selectedDevice;
if (device == null) {
setState(() => _status = '请先选择设备');
return;
}
if (!_portOpened) {
setState(() => _status = '请先打开串口');
return;
}
final ok = await _plugin.enableGpio(
chipType: device.deviceType,
gpioGroup: 0,
enable: 1,
);
if (!mounted) return;
setState(() => _status = ok ? 'GPIO 已使能' : '使能 GPIO 失败');
},
icon: const Icon(Icons.toggle_on),
label: const Text('使能GPIO'),
),
OutlinedButton.icon(
onPressed: () async {
if (!_portOpened) return;
final ok = await _plugin.setGpioDir(
gpioGroup: 0,
gpioNumber: 0,
dir: GpioDirection.out,
);
if (!mounted) return;
setState(() => _status = ok ? 'GPIO0 方向设为输出' : '设置方向失败');
},
icon: const Icon(Icons.swap_vert),
label: const Text('GPIO方向'),
),
OutlinedButton.icon(
onPressed: () async {
if (!_portOpened) return;
final dir = await _plugin.queryGpioDirFromCache(
gpioGroup: 0,
gpioNumber: 0,
);
if (!mounted) return;
final label = switch (dir) {
GpioDirection.in_ => '输入',
GpioDirection.out => '输出',
_ => '未知($dir)',
};
setState(() => _status = 'GPIO0 方向缓存: $label');
},
icon: const Icon(Icons.visibility),
label: const Text('查方向缓存'),
),
OutlinedButton.icon(
onPressed: () async {
final device = _selectedDevice;
if (device == null) {
setState(() => _status = '请先选择设备');
return;
}
final connected = await _plugin.isConnected(device.deviceId);
if (!mounted) return;
setState(() => _status = connected ? '设备已连接' : '设备未连接');
},
icon: const Icon(Icons.link),
label: const Text('是否已连'),
),
OutlinedButton.icon(
onPressed: () async {
final ids = await _plugin.getConnectedDevices();
if (!mounted) return;
setState(() => _status = '已打开设备: ${ids.isEmpty ? "无" : ids.join(", ")}');
},
icon: const Icon(Icons.list),
label: const Text('已开设备'),
),
OutlinedButton.icon(
onPressed: () async {
if (!_portOpened) return;
final ok = await _plugin.setSerialParameter(
baud: 9600, dataBit: 8, stopBit: 1, parityBit: 0, flow: false,
);
if (!mounted) return;
setState(() => _status = ok ? '已设为 9600/8/1/N' : '设参失败');
},
icon: const Icon(Icons.tune),
label: const Text('波特率9600'),
),
OutlinedButton.icon(
onPressed: () async {
if (!_portOpened) return;
final ok = await _plugin.setSerialParameter(
baud: 115200, dataBit: 8, stopBit: 1, parityBit: 0, flow: false,
);
if (!mounted) return;
setState(() => _status = ok ? '已设为 115200/8/1/N' : '设参失败');
},
icon: const Icon(Icons.tune),
label: const Text('波特率115200'),
),
],
),
const SizedBox(height: 16),
Text(
'接收缓冲区:\n$_receivedBuffer',
style: const TextStyle(fontFamily: 'monospace'),
),
],
),
),
);
}
}