feat(android): 添加 CH934X USB 转串口芯片支持
- 在 AndroidManifest.xml 中添加 USB Host 权限声明 - 集成 CH934X Android SDK 并通过反射调用原生功能 - 实现设备查找、串口读写、GPIO 和 Modem 控制功能 - 添加异常回调机制处理设备拔出等情况 - 提供 Stream 数据流支持实时串口数据监听 - 完善单元测试覆盖所有核心功能模块
This commit is contained in:
@@ -1,26 +1,100 @@
|
||||
import 'package:ch934x_serial/ch934x_serial_method_channel.dart';
|
||||
import 'package:ch934x_serial/src/models/models.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:ch934x_serial/ch934x_serial_method_channel.dart';
|
||||
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
MethodChannelCh934xSerial platform = MethodChannelCh934xSerial();
|
||||
const MethodChannel channel = MethodChannel('ch934x_serial');
|
||||
const channel = MethodChannel('ch934x_serial');
|
||||
late MethodChannelCh934xSerial platform;
|
||||
|
||||
setUp(() {
|
||||
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
||||
.setMockMethodCallHandler(channel, (MethodCall methodCall) async {
|
||||
return '42';
|
||||
});
|
||||
platform = MethodChannelCh934xSerial();
|
||||
});
|
||||
|
||||
tearDown(() {
|
||||
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
||||
.setMockMethodCallHandler(channel, null);
|
||||
platform.dispose();
|
||||
});
|
||||
|
||||
test('getPlatformVersion', () async {
|
||||
expect(await platform.getPlatformVersion(), '42');
|
||||
/// 显式替换 mock handler 并返回,后续 `await platform.xxx` 会触发该 handler。
|
||||
void mockCall(Future<Object?> Function(MethodCall) handler) {
|
||||
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
||||
.setMockMethodCallHandler(channel, (MethodCall call) async {
|
||||
return handler(call);
|
||||
});
|
||||
}
|
||||
|
||||
test('getDeviceList 解析为 Ch934xDeviceInfo 列表', () async {
|
||||
mockCall((call) async {
|
||||
expect(call.method, 'getDeviceList');
|
||||
return <Map<String, Object>>[
|
||||
<String, Object>{
|
||||
'deviceId': 1,
|
||||
'vendorId': 0x1a86,
|
||||
'productId': 0xfe0c,
|
||||
'deviceType': Ch934xDeviceType.ch9344,
|
||||
'serialNumber': 'ABC',
|
||||
'interfaceCount': 1,
|
||||
'serialPorts': <Map<String, Object>>[
|
||||
<String, Object>{'portIndex': 0},
|
||||
],
|
||||
},
|
||||
];
|
||||
});
|
||||
final result = await platform.getDeviceList();
|
||||
expect(result, hasLength(1));
|
||||
expect(result.first.serialNumber, 'ABC');
|
||||
expect(result.first.serialPorts.single.portIndex, 0);
|
||||
});
|
||||
|
||||
test('read 透传 length 字段并返回字节', () async {
|
||||
mockCall((call) async {
|
||||
expect(call.method, 'read');
|
||||
expect(call.arguments, <String, Object>{'length': 4});
|
||||
return Uint8List.fromList([1, 2, 3, 4]);
|
||||
});
|
||||
final result = await platform.read(4);
|
||||
expect(result, Uint8List.fromList([1, 2, 3, 4]));
|
||||
});
|
||||
|
||||
test('read 接收非正长度直接返回空数组', () async {
|
||||
// 未注册 mock handler,验证短路逻辑不调用原生层。
|
||||
final result = await platform.read(0);
|
||||
expect(result, isEmpty);
|
||||
});
|
||||
|
||||
test('write 将数据写入原生层并回传字节数', () async {
|
||||
mockCall((call) async {
|
||||
expect(call.method, 'write');
|
||||
expect((call.arguments as Map)['data'], isA<Uint8List>());
|
||||
return 3;
|
||||
});
|
||||
final written = await platform.write(Uint8List.fromList([9, 8, 7]));
|
||||
expect(written, 3);
|
||||
});
|
||||
|
||||
test('getModemStatus 默认回退到 0', () async {
|
||||
mockCall((_) async => null);
|
||||
expect(await platform.getModemStatus(), 0);
|
||||
});
|
||||
|
||||
test('dispatchException 推送给 setExceptionCallback 订阅者', () async {
|
||||
mockCall((call) async {
|
||||
expect(call.method, 'setExceptionCallback');
|
||||
return null;
|
||||
});
|
||||
final received = <Ch934xException>[];
|
||||
await platform.setExceptionCallback(received.add);
|
||||
platform.dispatchException(
|
||||
type: Ch934xExceptionType.deviceDetached,
|
||||
message: 'detached',
|
||||
);
|
||||
// 等待 microtask 队列执行。
|
||||
await Future<void>.delayed(Duration.zero);
|
||||
expect(received, hasLength(1));
|
||||
expect(received.first.message, 'detached');
|
||||
});
|
||||
}
|
||||
|
||||
+141
-14
@@ -1,28 +1,155 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:ch934x_serial/ch934x_serial.dart';
|
||||
import 'package:ch934x_serial/ch934x_serial_platform_interface.dart';
|
||||
import 'package:ch934x_serial/ch934x_serial_method_channel.dart';
|
||||
import 'package:ch934x_serial/ch934x_serial_platform_interface.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:plugin_platform_interface/plugin_platform_interface.dart';
|
||||
|
||||
class MockCh934xSerialPlatform
|
||||
with MockPlatformInterfaceMixin
|
||||
implements Ch934xSerialPlatform {
|
||||
/// 用 mock 替代真实平台,以便在宿主测试中验证上层调用。
|
||||
class _MockCh934xSerialPlatform extends Ch934xSerialPlatform
|
||||
with MockPlatformInterfaceMixin {
|
||||
_MockCh934xSerialPlatform({
|
||||
// ignore: unused_element_parameter
|
||||
this.deviceList = const <Ch934xDeviceInfo>[],
|
||||
// ignore: unused_element_parameter
|
||||
this.serialNumber,
|
||||
// ignore: unused_element_parameter
|
||||
this.deviceType = Ch934xDeviceType.ch9344,
|
||||
// ignore: unused_element_parameter
|
||||
this.ports = const <Ch934xSerialPortInfo>[],
|
||||
// ignore: unused_element_parameter
|
||||
this.openResult = true,
|
||||
// ignore: unused_element_parameter
|
||||
this.closeResult = true,
|
||||
// ignore: unused_element_parameter
|
||||
this.readBytes,
|
||||
// ignore: unused_element_parameter
|
||||
this.writeResult = 0,
|
||||
// ignore: unused_element_parameter
|
||||
this.gpioOutputResult = true,
|
||||
// ignore: unused_element_parameter
|
||||
this.gpioInput = 1,
|
||||
// ignore: unused_element_parameter
|
||||
this.modemControlResult = true,
|
||||
// ignore: unused_element_parameter
|
||||
this.modemStatus = ModemStatus.cts,
|
||||
});
|
||||
|
||||
List<Ch934xDeviceInfo> deviceList;
|
||||
String? serialNumber;
|
||||
int deviceType;
|
||||
List<Ch934xSerialPortInfo> ports;
|
||||
bool openResult;
|
||||
bool closeResult;
|
||||
Uint8List? readBytes;
|
||||
int writeResult;
|
||||
bool gpioOutputResult;
|
||||
int gpioInput;
|
||||
bool modemControlResult;
|
||||
int modemStatus;
|
||||
|
||||
@override
|
||||
Future<String?> getPlatformVersion() => Future.value('42');
|
||||
Future<List<Ch934xDeviceInfo>> getDeviceList() async => deviceList;
|
||||
|
||||
@override
|
||||
Future<String?> getSerialNumber(int deviceId) async => serialNumber;
|
||||
|
||||
@override
|
||||
Future<int> getDeviceType(int deviceId) async => deviceType;
|
||||
|
||||
@override
|
||||
Future<List<Ch934xSerialPortInfo>> getSerialPortList(
|
||||
int deviceId, {
|
||||
required int interfaceNumber,
|
||||
}) async =>
|
||||
ports;
|
||||
|
||||
@override
|
||||
Future<bool> openPort(Ch934xPortTarget target) async => openResult;
|
||||
|
||||
@override
|
||||
Future<bool> closePort() async => closeResult;
|
||||
|
||||
@override
|
||||
Future<Uint8List> read(int length) async => readBytes ?? Uint8List(0);
|
||||
|
||||
@override
|
||||
Future<int> write(Uint8List data) async => writeResult;
|
||||
|
||||
@override
|
||||
Future<bool> setGpioOutput({
|
||||
required int gpioNumber,
|
||||
required int level,
|
||||
}) async =>
|
||||
gpioOutputResult;
|
||||
|
||||
@override
|
||||
Future<int> getGpioInput(int gpioNumber) async => gpioInput;
|
||||
|
||||
@override
|
||||
Future<bool> setModemControl({required int dtr, required int rts}) async =>
|
||||
modemControlResult;
|
||||
|
||||
@override
|
||||
Future<int> getModemStatus() async => modemStatus;
|
||||
|
||||
@override
|
||||
Future<void> setExceptionCallback(
|
||||
void Function(Ch934xException exception) onException,
|
||||
) async {}
|
||||
}
|
||||
|
||||
void main() {
|
||||
final Ch934xSerialPlatform initialPlatform = Ch934xSerialPlatform.instance;
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
test('$MethodChannelCh934xSerial is the default instance', () {
|
||||
expect(initialPlatform, isInstanceOf<MethodChannelCh934xSerial>());
|
||||
test('默认平台实现是 MethodChannelCh934xSerial', () {
|
||||
expect(Ch934xSerialPlatform.instance, isInstanceOf<MethodChannelCh934xSerial>());
|
||||
});
|
||||
|
||||
test('getPlatformVersion', () async {
|
||||
Ch934xSerial ch934xSerialPlugin = Ch934xSerial();
|
||||
MockCh934xSerialPlatform fakePlatform = MockCh934xSerialPlatform();
|
||||
Ch934xSerialPlatform.instance = fakePlatform;
|
||||
test('Ch934xSerial 委托 platform 调用 getDeviceList', () async {
|
||||
final mock = _MockCh934xSerialPlatform(
|
||||
deviceList: const [
|
||||
Ch934xDeviceInfo(
|
||||
deviceId: 1,
|
||||
vendorId: 0x1a86,
|
||||
productId: 0xfe0c,
|
||||
deviceType: Ch934xDeviceType.ch9344,
|
||||
serialNumber: 'SN-1',
|
||||
),
|
||||
],
|
||||
);
|
||||
final plugin = Ch934xSerial.withPlatform(mock);
|
||||
final list = await plugin.getDeviceList();
|
||||
expect(list.single.serialNumber, 'SN-1');
|
||||
});
|
||||
|
||||
expect(await ch934xSerialPlugin.getPlatformVersion(), '42');
|
||||
test('ModemStatus.isSet 按位与工作', () {
|
||||
const status = ModemStatus.cts | ModemStatus.dcd;
|
||||
expect(ModemStatus.isSet(status, ModemStatus.cts), isTrue);
|
||||
expect(ModemStatus.isSet(status, ModemStatus.dsr), isFalse);
|
||||
});
|
||||
|
||||
test('Ch934xPortTarget.toMap 字段可被 MethodChannel 直接消费', () {
|
||||
const target = Ch934xPortTarget(
|
||||
deviceId: 2,
|
||||
interfaceNumber: 1,
|
||||
serialPortIndex: 3,
|
||||
);
|
||||
expect(target.toMap(), <String, Object>{
|
||||
'deviceId': 2,
|
||||
'interfaceNumber': 1,
|
||||
'serialPortIndex': 3,
|
||||
});
|
||||
});
|
||||
|
||||
test('Ch934xException.toString 汇总关键字段', () {
|
||||
final ex = Ch934xException(
|
||||
type: Ch934xExceptionType.deviceDetached,
|
||||
message: '设备被拔出',
|
||||
);
|
||||
expect(ex.toString(), contains('deviceDetached'));
|
||||
expect(ex.toString(), contains('设备被拔出'));
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user