Initial commit

This commit is contained in:
Tony
2026-04-27 03:19:57 +08:00
commit 523bb073fb
48 changed files with 1659 additions and 0 deletions
+13
View File
@@ -0,0 +1,13 @@
/// Flutter plugin for USB serial communication on Android.
///
/// Uses `usb-serial-for-android` (https://github.com/mik3y/usb-serial-for-android)
/// as the underlying Android library.
library;
import 'dart:async';
import 'package:flutter/services.dart';
part 'src/usb_serial_device.dart';
part 'src/usb_serial_port.dart';
part 'src/flutter_usb_serial.dart';
@@ -0,0 +1,17 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'flutter_usb_serial_platform_interface.dart';
/// An implementation of [FlutterUsbSerialPlatform] that uses method channels.
class MethodChannelFlutterUsbSerial extends FlutterUsbSerialPlatform {
/// The method channel used to interact with the native platform.
@visibleForTesting
final methodChannel = const MethodChannel('flutter_usb_serial');
@override
Future<String?> getPlatformVersion() async {
final version = await methodChannel.invokeMethod<String>('getPlatformVersion');
return version;
}
}
@@ -0,0 +1,29 @@
import 'package:plugin_platform_interface/plugin_platform_interface.dart';
import 'flutter_usb_serial_method_channel.dart';
abstract class FlutterUsbSerialPlatform extends PlatformInterface {
/// Constructs a FlutterUsbSerialPlatform.
FlutterUsbSerialPlatform() : super(token: _token);
static final Object _token = Object();
static FlutterUsbSerialPlatform _instance = MethodChannelFlutterUsbSerial();
/// The default instance of [FlutterUsbSerialPlatform] to use.
///
/// Defaults to [MethodChannelFlutterUsbSerial].
static FlutterUsbSerialPlatform get instance => _instance;
/// Platform-specific implementations should set this with their own
/// platform-specific class that extends [FlutterUsbSerialPlatform] when
/// they register themselves.
static set instance(FlutterUsbSerialPlatform instance) {
PlatformInterface.verifyToken(instance, _token);
_instance = instance;
}
Future<String?> getPlatformVersion() {
throw UnimplementedError('platformVersion() has not been implemented.');
}
}
+102
View File
@@ -0,0 +1,102 @@
part of '../flutter_usb_serial.dart';
/// Main entry point for the flutter_usb_serial plugin.
///
/// Only supported on Android.
class FlutterUsbSerial {
FlutterUsbSerial._();
static const _methods = MethodChannel('flutter_usb_serial/methods');
static const _events = EventChannel('flutter_usb_serial/data');
static Stream<Map<Object?, Object?>>? _rawEventStream;
static Stream<Map<Object?, Object?>> get _eventStream {
_rawEventStream ??= _events
.receiveBroadcastStream()
.cast<Map<Object?, Object?>>();
return _rawEventStream!;
}
/// Returns the list of currently attached USB serial devices.
static Future<List<UsbSerialDevice>> listDevices() async {
final result = await _methods.invokeMethod<List<Object?>>('listDevices');
if (result == null) return [];
return result
.whereType<Map<Object?, Object?>>()
.map(UsbSerialDevice.fromMap)
.toList();
}
/// Requests Android USB permission for [device] if not already granted.
///
/// Returns `true` if permission was granted (or was already held).
static Future<bool> requestPermission(UsbSerialDevice device) async {
final granted = await _methods.invokeMethod<bool>('requestPermission', {
'deviceId': device.deviceId,
});
return granted ?? false;
}
/// Opens the first port of [device] with the given serial parameters.
///
/// [baudRate] defaults to 9600.
/// [dataBits] must be 5, 6, 7, or 8. Defaults to 8.
/// [parity] defaults to [UsbSerialParity.none].
/// [stopBits] defaults to [UsbSerialStopBits.one].
///
/// The returned [UsbSerialPort] streams incoming data via [UsbSerialPort.inputStream].
/// Call [UsbSerialPort.close] when done.
static Future<UsbSerialPort> openPort(
UsbSerialDevice device, {
int baudRate = 9600,
int dataBits = 8,
UsbSerialParity parity = UsbSerialParity.none,
UsbSerialStopBits stopBits = UsbSerialStopBits.one,
}) async {
final portId = await _methods.invokeMethod<int>('openPort', {
'deviceId': device.deviceId,
'baudRate': baudRate,
'dataBits': dataBits,
'parity': parity.index,
'stopBits': _stopBitsIndex(stopBits),
});
if (portId == null) {
throw PlatformException(
code: 'OPEN_FAILED',
message: 'Failed to open USB serial port for device ${device.deviceId}',
);
}
final dataStream = _eventStream
.where((e) => e['portId'] == portId)
.transform(StreamTransformer<Map<Object?, Object?>, Uint8List>.fromHandlers(
handleData: (e, sink) {
if (e['type'] == 'data') {
final raw = e['data'];
if (raw is Uint8List) {
sink.add(raw);
} else if (raw is List) {
sink.add(Uint8List.fromList(raw.cast<int>()));
}
} else if (e['type'] == 'error') {
sink.addError(Exception(
e['message'] as String? ?? 'USB IO error'));
}
},
));
return UsbSerialPort._(portId, dataStream);
}
static int _stopBitsIndex(UsbSerialStopBits sb) {
switch (sb) {
case UsbSerialStopBits.one:
return 1;
case UsbSerialStopBits.onePointFive:
return 3; // UsbSerialPort.STOPBITS_1_5
case UsbSerialStopBits.two:
return 2;
}
}
}
+56
View File
@@ -0,0 +1,56 @@
part of '../flutter_usb_serial.dart';
/// Represents a USB serial device found during enumeration.
class UsbSerialDevice {
/// Unique device ID (Android USB device ID).
final int deviceId;
/// Vendor ID (VID).
final int vendorId;
/// Product ID (PID).
final int productId;
/// Human-readable device name (product string from descriptor, may be empty).
final String deviceName;
/// Manufacturer string (may be empty).
final String manufacturerName;
/// Serial number string (may be empty).
final String serialNumber;
const UsbSerialDevice({
required this.deviceId,
required this.vendorId,
required this.productId,
required this.deviceName,
required this.manufacturerName,
required this.serialNumber,
});
factory UsbSerialDevice.fromMap(Map<Object?, Object?> map) {
return UsbSerialDevice(
deviceId: (map['deviceId'] as int?) ?? 0,
vendorId: (map['vendorId'] as int?) ?? 0,
productId: (map['productId'] as int?) ?? 0,
deviceName: (map['deviceName'] as String?) ?? '',
manufacturerName: (map['manufacturerName'] as String?) ?? '',
serialNumber: (map['serialNumber'] as String?) ?? '',
);
}
Map<String, dynamic> toMap() => {
'deviceId': deviceId,
'vendorId': vendorId,
'productId': productId,
'deviceName': deviceName,
'manufacturerName': manufacturerName,
'serialNumber': serialNumber,
};
@override
String toString() =>
'UsbSerialDevice(id=$deviceId, vid=0x${vendorId.toRadixString(16).padLeft(4, '0')}, '
'pid=0x${productId.toRadixString(16).padLeft(4, '0')}, name="$deviceName")';
}
+41
View File
@@ -0,0 +1,41 @@
part of '../flutter_usb_serial.dart';
/// Parity modes for serial communication.
enum UsbSerialParity { none, odd, even, mark, space }
/// Stop bits for serial communication.
enum UsbSerialStopBits { one, onePointFive, two }
/// An open serial port handle returned by [FlutterUsbSerial.openPort].
///
/// Call [close] when done to release native resources.
class UsbSerialPort {
final int _portId;
UsbSerialPort._(this._portId, this._dataStream);
final Stream<Uint8List> _dataStream;
/// Stream of raw bytes received on this port.
Stream<Uint8List> get inputStream => _dataStream;
static const _methods = MethodChannel('flutter_usb_serial/methods');
/// Write [data] to the port.
///
/// Returns the number of bytes actually written, or throws on error.
Future<int> write(Uint8List data) async {
final written = await _methods.invokeMethod<int>('write', {
'portId': _portId,
'data': data,
});
return written ?? 0;
}
/// Close the port and release native resources.
Future<void> close() async {
await _methods.invokeMethod<void>('close', {'portId': _portId});
}
int get portId => _portId;
}