Enhance USB serial plugin for Android and iPadOS with DriverKit support
- Updated README.md to reflect platform support for iPadOS. - Implemented isDriverAvailable method in FlutterUsbSerialPlugin for DriverKit readiness check. - Added DriverKitSerialClient C source and header files for iPadOS integration. - Created Swift Package for iPadOS support. - Updated pubspec.yaml and flutter_usb_serial.dart to clarify platform compatibility. - Enhanced tests to verify isDriverAvailable functionality. Signed-off-by: Tony <tonylu@tony-cloud.com>
This commit is contained in:
@@ -1,18 +1,31 @@
|
||||
# flutter_usb_serial
|
||||
|
||||
A new Flutter plugin project.
|
||||
Flutter USB serial transport for Android and DriverKit-enabled iPadOS.
|
||||
|
||||
## Getting Started
|
||||
## Platforms
|
||||
|
||||
This project is a starting point for a Flutter
|
||||
[plug-in package](https://flutter.dev/to/develop-plugins),
|
||||
a specialized package that includes platform-specific implementation code for
|
||||
Android and/or iOS.
|
||||
- Android uses `usb-serial-for-android` and the normal Android USB permission
|
||||
flow.
|
||||
- iPadOS 16+ uses Swift Package Manager and a user client exposed by a
|
||||
USBDriverKit extension embedded in the host app. DriverKit USB support is
|
||||
available only on iPads with an M-series chip.
|
||||
|
||||
For help getting started with Flutter development, view the
|
||||
[online documentation](https://docs.flutter.dev), which offers tutorials,
|
||||
samples, guidance on mobile development, and a full API reference.
|
||||
The package contains the SwiftPM client plugin, not a reusable pre-signed
|
||||
DriverKit extension. Apple provisions USB transport entitlements to the host
|
||||
app and driver for a specific USB vendor. The host app must:
|
||||
|
||||
The plugin project was generated without specifying the `--platforms` flag, no platforms are currently supported.
|
||||
To add platforms, run `flutter create -t plugin --platforms <platforms> .` in this directory.
|
||||
You can also find a detailed instruction on how to add platforms in the `pubspec.yaml` at https://flutter.dev/to/pubspec-plugin-platforms.
|
||||
1. Embed a DriverKit extension whose `IOUserClass` is
|
||||
`DaliMasterEspressifUsbDriver`, or adapt the client service name.
|
||||
2. Add `com.apple.developer.driverkit.communicates-with-drivers` to the app.
|
||||
3. Sign the extension with `com.apple.developer.driverkit` and a
|
||||
`com.apple.developer.driverkit.transport.usb` entitlement approved for its
|
||||
USB vendor ID.
|
||||
4. Enable Swift Package Manager integration for Flutter iOS plugins.
|
||||
|
||||
Call `FlutterUsbSerial.isDriverAvailable()` during startup before showing a USB
|
||||
transport. It returns `true` only when a compatible driver service is loaded.
|
||||
|
||||
The DaliMaster host implementation is deliberately restricted to Espressif
|
||||
vendor ID `0x303A` and the USB Serial/JTAG interface. Serial parameter and modem
|
||||
line methods are accepted for API compatibility; the ESP USB Serial/JTAG bulk
|
||||
transport does not use UART baud or line coding.
|
||||
|
||||
@@ -127,6 +127,7 @@ class FlutterUsbSerialPlugin :
|
||||
|
||||
override fun onMethodCall(call: MethodCall, result: Result) {
|
||||
when (call.method) {
|
||||
"isDriverAvailable" -> result.success(true)
|
||||
"listDevices" -> listDevices(result)
|
||||
"requestPermission" -> requestPermission(call, result)
|
||||
"openPort" -> openPort(call, result)
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
// swift-tools-version: 5.9
|
||||
|
||||
import PackageDescription
|
||||
|
||||
let package = Package(
|
||||
name: "flutter_usb_serial",
|
||||
platforms: [
|
||||
.iOS("16.0"),
|
||||
],
|
||||
products: [
|
||||
.library(name: "flutter-usb-serial", targets: ["flutter_usb_serial"]),
|
||||
],
|
||||
dependencies: [
|
||||
.package(name: "FlutterFramework", path: "../FlutterFramework"),
|
||||
],
|
||||
targets: [
|
||||
.target(
|
||||
name: "DriverKitSerialClient",
|
||||
path: "Sources/DriverKitSerialClient",
|
||||
publicHeadersPath: "include",
|
||||
linkerSettings: [
|
||||
.linkedFramework("IOKit"),
|
||||
]
|
||||
),
|
||||
.target(
|
||||
name: "flutter_usb_serial",
|
||||
dependencies: [
|
||||
"DriverKitSerialClient",
|
||||
.product(name: "FlutterFramework", package: "FlutterFramework"),
|
||||
],
|
||||
path: "Sources/flutter_usb_serial",
|
||||
resources: [
|
||||
.process("Resources"),
|
||||
]
|
||||
),
|
||||
]
|
||||
)
|
||||
@@ -0,0 +1,195 @@
|
||||
#include "DriverKitSerialClient.h"
|
||||
|
||||
#include <IOKit/IOKitLib.h>
|
||||
#include <mach/mach.h>
|
||||
#include <string.h>
|
||||
|
||||
static const char *const kFlutterUsbDriverService =
|
||||
"DaliMasterEspressifUsbDriver";
|
||||
|
||||
enum {
|
||||
kFlutterUsbDriverGetInfo = 0,
|
||||
kFlutterUsbDriverWrite = 1,
|
||||
kFlutterUsbDriverRead = 2,
|
||||
kFlutterUsbDriverControl = 3,
|
||||
kFlutterUsbDriverConfigure = 4,
|
||||
kFlutterUsbDriverSetLineState = 5,
|
||||
kFlutterUsbDriverPurge = 6,
|
||||
};
|
||||
|
||||
static bool FlutterUsbSerialDriverGetInfo(
|
||||
io_connect_t connection,
|
||||
FlutterUsbDriverDeviceInfo *info) {
|
||||
size_t outputSize = sizeof(*info);
|
||||
memset(info, 0, sizeof(*info));
|
||||
kern_return_t result = IOConnectCallStructMethod(
|
||||
connection, kFlutterUsbDriverGetInfo, NULL, 0, info, &outputSize);
|
||||
return result == KERN_SUCCESS && outputSize == sizeof(*info) &&
|
||||
info->protocolVersion == 1 &&
|
||||
info->kind == FlutterUsbDriverKindSerial;
|
||||
}
|
||||
|
||||
size_t FlutterUsbSerialDriverCopyDevices(
|
||||
FlutterUsbDriverDeviceInfo *devices,
|
||||
size_t capacity) {
|
||||
CFMutableDictionaryRef matching =
|
||||
IOServiceNameMatching(kFlutterUsbDriverService);
|
||||
if (matching == NULL) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
io_iterator_t iterator = IO_OBJECT_NULL;
|
||||
kern_return_t result = IOServiceGetMatchingServices(
|
||||
kIOMainPortDefault, matching, &iterator);
|
||||
if (result != KERN_SUCCESS) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
size_t count = 0;
|
||||
io_service_t service = IO_OBJECT_NULL;
|
||||
while ((service = IOIteratorNext(iterator)) != IO_OBJECT_NULL) {
|
||||
io_connect_t connection = IO_OBJECT_NULL;
|
||||
FlutterUsbDriverDeviceInfo info;
|
||||
if (IOServiceOpen(service, mach_task_self_, 0, &connection) ==
|
||||
KERN_SUCCESS &&
|
||||
FlutterUsbSerialDriverGetInfo(connection, &info)) {
|
||||
uint64_t registryEntryId = 0;
|
||||
if (IORegistryEntryGetRegistryEntryID(service, ®istryEntryId) ==
|
||||
KERN_SUCCESS) {
|
||||
info.registryEntryId = registryEntryId;
|
||||
if (devices != NULL && count < capacity) {
|
||||
devices[count] = info;
|
||||
}
|
||||
count++;
|
||||
}
|
||||
}
|
||||
if (connection != IO_OBJECT_NULL) {
|
||||
IOServiceClose(connection);
|
||||
}
|
||||
IOObjectRelease(service);
|
||||
}
|
||||
IOObjectRelease(iterator);
|
||||
return count;
|
||||
}
|
||||
|
||||
bool FlutterUsbSerialDriverIsAvailable(void) {
|
||||
return FlutterUsbSerialDriverCopyDevices(NULL, 0) > 0;
|
||||
}
|
||||
|
||||
int32_t FlutterUsbSerialDriverOpen(uint64_t registryEntryId,
|
||||
uint32_t *connection) {
|
||||
if (connection == NULL) {
|
||||
return kIOReturnBadArgument;
|
||||
}
|
||||
*connection = IO_OBJECT_NULL;
|
||||
|
||||
CFMutableDictionaryRef matching =
|
||||
IOServiceNameMatching(kFlutterUsbDriverService);
|
||||
if (matching == NULL) {
|
||||
return kIOReturnNoMemory;
|
||||
}
|
||||
io_iterator_t iterator = IO_OBJECT_NULL;
|
||||
kern_return_t result = IOServiceGetMatchingServices(
|
||||
kIOMainPortDefault, matching, &iterator);
|
||||
if (result != KERN_SUCCESS) {
|
||||
return result;
|
||||
}
|
||||
|
||||
io_service_t service = IO_OBJECT_NULL;
|
||||
result = kIOReturnNotFound;
|
||||
while ((service = IOIteratorNext(iterator)) != IO_OBJECT_NULL) {
|
||||
uint64_t candidateId = 0;
|
||||
if (IORegistryEntryGetRegistryEntryID(service, &candidateId) ==
|
||||
KERN_SUCCESS &&
|
||||
candidateId == registryEntryId) {
|
||||
io_connect_t opened = IO_OBJECT_NULL;
|
||||
result = IOServiceOpen(service, mach_task_self_, 0, &opened);
|
||||
if (result == KERN_SUCCESS) {
|
||||
FlutterUsbDriverDeviceInfo info;
|
||||
if (FlutterUsbSerialDriverGetInfo(opened, &info)) {
|
||||
*connection = opened;
|
||||
} else {
|
||||
IOServiceClose(opened);
|
||||
result = kIOReturnUnsupported;
|
||||
}
|
||||
}
|
||||
IOObjectRelease(service);
|
||||
break;
|
||||
}
|
||||
IOObjectRelease(service);
|
||||
}
|
||||
IOObjectRelease(iterator);
|
||||
return result;
|
||||
}
|
||||
|
||||
void FlutterUsbSerialDriverClose(uint32_t connection) {
|
||||
if (connection != IO_OBJECT_NULL) {
|
||||
IOServiceClose((io_connect_t)connection);
|
||||
}
|
||||
}
|
||||
|
||||
int32_t FlutterUsbSerialDriverWrite(uint32_t connection,
|
||||
const uint8_t *bytes,
|
||||
uint32_t length,
|
||||
uint32_t *bytesWritten) {
|
||||
if (connection == IO_OBJECT_NULL || bytes == NULL || bytesWritten == NULL) {
|
||||
return kIOReturnBadArgument;
|
||||
}
|
||||
uint64_t output = 0;
|
||||
uint32_t outputCount = 1;
|
||||
kern_return_t result = IOConnectCallMethod(
|
||||
(io_connect_t)connection, kFlutterUsbDriverWrite, NULL, 0, bytes, length,
|
||||
&output, &outputCount, NULL, NULL);
|
||||
*bytesWritten = result == KERN_SUCCESS ? (uint32_t)output : 0;
|
||||
return result;
|
||||
}
|
||||
|
||||
int32_t FlutterUsbSerialDriverRead(uint32_t connection,
|
||||
uint8_t *bytes,
|
||||
uint32_t capacity,
|
||||
uint32_t timeoutMilliseconds,
|
||||
uint32_t *bytesRead) {
|
||||
if (connection == IO_OBJECT_NULL || bytes == NULL || bytesRead == NULL) {
|
||||
return kIOReturnBadArgument;
|
||||
}
|
||||
const uint64_t inputs[] = {capacity, timeoutMilliseconds};
|
||||
size_t outputSize = capacity;
|
||||
kern_return_t result = IOConnectCallMethod(
|
||||
(io_connect_t)connection, kFlutterUsbDriverRead, inputs, 2, NULL, 0,
|
||||
NULL, NULL, bytes, &outputSize);
|
||||
if (result == kIOReturnTimeout) {
|
||||
*bytesRead = 0;
|
||||
return KERN_SUCCESS;
|
||||
}
|
||||
*bytesRead = result == KERN_SUCCESS ? (uint32_t)outputSize : 0;
|
||||
return result;
|
||||
}
|
||||
|
||||
int32_t FlutterUsbSerialDriverConfigure(uint32_t connection,
|
||||
uint32_t baudRate,
|
||||
uint8_t dataBits,
|
||||
uint8_t parity,
|
||||
uint8_t stopBits) {
|
||||
const uint64_t inputs[] = {baudRate, dataBits, parity, stopBits};
|
||||
return IOConnectCallScalarMethod((io_connect_t)connection,
|
||||
kFlutterUsbDriverConfigure, inputs, 4,
|
||||
NULL, NULL);
|
||||
}
|
||||
|
||||
int32_t FlutterUsbSerialDriverSetLineState(uint32_t connection,
|
||||
bool dtr,
|
||||
bool rts) {
|
||||
const uint64_t inputs[] = {dtr ? 1U : 0U, rts ? 1U : 0U};
|
||||
return IOConnectCallScalarMethod((io_connect_t)connection,
|
||||
kFlutterUsbDriverSetLineState, inputs, 2,
|
||||
NULL, NULL);
|
||||
}
|
||||
|
||||
int32_t FlutterUsbSerialDriverPurge(uint32_t connection,
|
||||
bool purgeWrite,
|
||||
bool purgeRead) {
|
||||
const uint64_t inputs[] = {purgeWrite ? 1U : 0U, purgeRead ? 1U : 0U};
|
||||
return IOConnectCallScalarMethod((io_connect_t)connection,
|
||||
kFlutterUsbDriverPurge, inputs, 2,
|
||||
NULL, NULL);
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
#ifndef DRIVER_KIT_SERIAL_CLIENT_H
|
||||
#define DRIVER_KIT_SERIAL_CLIENT_H
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
enum {
|
||||
FlutterUsbDriverKindUnknown = 0,
|
||||
FlutterUsbDriverKindSerial = 1,
|
||||
FlutterUsbDriverKindHid = 2,
|
||||
};
|
||||
|
||||
typedef struct {
|
||||
uint64_t registryEntryId;
|
||||
uint32_t protocolVersion;
|
||||
uint32_t kind;
|
||||
uint16_t vendorId;
|
||||
uint16_t productId;
|
||||
uint16_t maxInputPacketSize;
|
||||
uint16_t maxOutputPacketSize;
|
||||
uint8_t interfaceNumber;
|
||||
uint8_t inputEndpoint;
|
||||
uint8_t outputEndpoint;
|
||||
uint8_t reserved;
|
||||
} FlutterUsbDriverDeviceInfo;
|
||||
|
||||
bool FlutterUsbSerialDriverIsAvailable(void);
|
||||
size_t FlutterUsbSerialDriverCopyDevices(
|
||||
FlutterUsbDriverDeviceInfo *devices,
|
||||
size_t capacity);
|
||||
int32_t FlutterUsbSerialDriverOpen(uint64_t registryEntryId,
|
||||
uint32_t *connection);
|
||||
void FlutterUsbSerialDriverClose(uint32_t connection);
|
||||
int32_t FlutterUsbSerialDriverWrite(uint32_t connection,
|
||||
const uint8_t *bytes,
|
||||
uint32_t length,
|
||||
uint32_t *bytesWritten);
|
||||
int32_t FlutterUsbSerialDriverRead(uint32_t connection,
|
||||
uint8_t *bytes,
|
||||
uint32_t capacity,
|
||||
uint32_t timeoutMilliseconds,
|
||||
uint32_t *bytesRead);
|
||||
int32_t FlutterUsbSerialDriverConfigure(uint32_t connection,
|
||||
uint32_t baudRate,
|
||||
uint8_t dataBits,
|
||||
uint8_t parity,
|
||||
uint8_t stopBits);
|
||||
int32_t FlutterUsbSerialDriverSetLineState(uint32_t connection,
|
||||
bool dtr,
|
||||
bool rts);
|
||||
int32_t FlutterUsbSerialDriverPurge(uint32_t connection,
|
||||
bool purgeWrite,
|
||||
bool purgeRead);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,331 @@
|
||||
import DriverKitSerialClient
|
||||
import Flutter
|
||||
import Foundation
|
||||
|
||||
private final class SerialPortHandle {
|
||||
let portId: Int
|
||||
let connection: UInt32
|
||||
let info: FlutterUsbDriverDeviceInfo
|
||||
let readQueue: DispatchQueue
|
||||
var running = true
|
||||
var dtr = false
|
||||
var rts = false
|
||||
|
||||
init(portId: Int, connection: UInt32, info: FlutterUsbDriverDeviceInfo) {
|
||||
self.portId = portId
|
||||
self.connection = connection
|
||||
self.info = info
|
||||
readQueue = DispatchQueue(
|
||||
label: "flutter_usb_serial.driverkit.\(portId)",
|
||||
qos: .userInitiated
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
public final class FlutterUsbSerialPlugin: NSObject, FlutterPlugin, FlutterStreamHandler {
|
||||
private static let methodChannelName = "flutter_usb_serial/methods"
|
||||
private static let eventChannelName = "flutter_usb_serial/data"
|
||||
|
||||
private var eventSink: FlutterEventSink?
|
||||
private var nextPortId = 1
|
||||
private var ports: [Int: SerialPortHandle] = [:]
|
||||
private let stateQueue = DispatchQueue(label: "flutter_usb_serial.driverkit.state")
|
||||
|
||||
public static func register(with registrar: FlutterPluginRegistrar) {
|
||||
let instance = FlutterUsbSerialPlugin()
|
||||
let methods = FlutterMethodChannel(
|
||||
name: methodChannelName,
|
||||
binaryMessenger: registrar.messenger()
|
||||
)
|
||||
registrar.addMethodCallDelegate(instance, channel: methods)
|
||||
let events = FlutterEventChannel(
|
||||
name: eventChannelName,
|
||||
binaryMessenger: registrar.messenger()
|
||||
)
|
||||
events.setStreamHandler(instance)
|
||||
}
|
||||
|
||||
public func onListen(
|
||||
withArguments arguments: Any?,
|
||||
eventSink events: @escaping FlutterEventSink
|
||||
) -> FlutterError? {
|
||||
eventSink = events
|
||||
return nil
|
||||
}
|
||||
|
||||
public func onCancel(withArguments arguments: Any?) -> FlutterError? {
|
||||
eventSink = nil
|
||||
return nil
|
||||
}
|
||||
|
||||
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
|
||||
switch call.method {
|
||||
case "isDriverAvailable":
|
||||
result(FlutterUsbSerialDriverIsAvailable())
|
||||
case "listDevices":
|
||||
result(copyDevices().map(deviceMap))
|
||||
case "requestPermission":
|
||||
result(deviceInfo(from: call.arguments) != nil)
|
||||
case "openPort":
|
||||
openPort(arguments: call.arguments, result: result)
|
||||
case "write":
|
||||
write(arguments: call.arguments, result: result)
|
||||
case "setParameters":
|
||||
setParameters(arguments: call.arguments, result: result)
|
||||
case "setDTR":
|
||||
setLineState(arguments: call.arguments, dtr: true, result: result)
|
||||
case "setRTS":
|
||||
setLineState(arguments: call.arguments, dtr: false, result: result)
|
||||
case "purgeHwBuffers":
|
||||
purge(arguments: call.arguments, result: result)
|
||||
case "close":
|
||||
close(arguments: call.arguments, result: result)
|
||||
default:
|
||||
result(FlutterMethodNotImplemented)
|
||||
}
|
||||
}
|
||||
|
||||
private func copyDevices() -> [FlutterUsbDriverDeviceInfo] {
|
||||
let count = FlutterUsbSerialDriverCopyDevices(nil, 0)
|
||||
guard count > 0 else { return [] }
|
||||
var devices = Array(repeating: FlutterUsbDriverDeviceInfo(), count: count)
|
||||
let copied = devices.withUnsafeMutableBufferPointer { buffer in
|
||||
FlutterUsbSerialDriverCopyDevices(buffer.baseAddress, buffer.count)
|
||||
}
|
||||
return Array(devices.prefix(min(copied, devices.count)))
|
||||
}
|
||||
|
||||
private func deviceMap(_ info: FlutterUsbDriverDeviceInfo) -> [String: Any] {
|
||||
let signedId = Int64(bitPattern: info.registryEntryId)
|
||||
return [
|
||||
"deviceId": NSNumber(value: signedId),
|
||||
"vendorId": Int(info.vendorId),
|
||||
"productId": Int(info.productId),
|
||||
"deviceName": "Espressif USB Serial/JTAG",
|
||||
"manufacturerName": "Espressif",
|
||||
"serialNumber": "",
|
||||
]
|
||||
}
|
||||
|
||||
private func deviceInfo(from arguments: Any?) -> FlutterUsbDriverDeviceInfo? {
|
||||
guard
|
||||
let map = arguments as? [String: Any],
|
||||
let number = map["deviceId"] as? NSNumber
|
||||
else { return nil }
|
||||
let registryId = UInt64(bitPattern: number.int64Value)
|
||||
return copyDevices().first { $0.registryEntryId == registryId }
|
||||
}
|
||||
|
||||
private func openPort(arguments: Any?, result: @escaping FlutterResult) {
|
||||
guard
|
||||
let map = arguments as? [String: Any],
|
||||
let deviceNumber = map["deviceId"] as? NSNumber
|
||||
else {
|
||||
result(argumentError("deviceId is required"))
|
||||
return
|
||||
}
|
||||
let registryId = UInt64(bitPattern: deviceNumber.int64Value)
|
||||
guard let info = copyDevices().first(where: { $0.registryEntryId == registryId }) else {
|
||||
result(pluginError("DEVICE_NOT_FOUND", "Espressif USB device is not available"))
|
||||
return
|
||||
}
|
||||
|
||||
var connection: UInt32 = 0
|
||||
let openResult = FlutterUsbSerialDriverOpen(registryId, &connection)
|
||||
guard openResult == 0 else {
|
||||
result(driverError("OPEN_FAILED", "Could not open DriverKit user client", openResult))
|
||||
return
|
||||
}
|
||||
|
||||
let baudRate = (map["baudRate"] as? NSNumber)?.uint32Value ?? 9_600
|
||||
let dataBits = (map["dataBits"] as? NSNumber)?.uint8Value ?? 8
|
||||
let parity = (map["parity"] as? NSNumber)?.uint8Value ?? 0
|
||||
let stopBits = (map["stopBits"] as? NSNumber)?.uint8Value ?? 1
|
||||
let configureResult = FlutterUsbSerialDriverConfigure(
|
||||
connection, baudRate, dataBits, parity, stopBits
|
||||
)
|
||||
guard configureResult == 0 else {
|
||||
FlutterUsbSerialDriverClose(connection)
|
||||
result(driverError("OPEN_FAILED", "Could not configure USB serial port", configureResult))
|
||||
return
|
||||
}
|
||||
|
||||
let handle: SerialPortHandle = stateQueue.sync {
|
||||
let id = nextPortId
|
||||
nextPortId += 1
|
||||
let created = SerialPortHandle(portId: id, connection: connection, info: info)
|
||||
ports[id] = created
|
||||
return created
|
||||
}
|
||||
startReading(handle)
|
||||
result(handle.portId)
|
||||
}
|
||||
|
||||
private func startReading(_ handle: SerialPortHandle) {
|
||||
handle.readQueue.async { [weak self, weak handle] in
|
||||
guard let self, let handle else { return }
|
||||
let capacity = max(Int(handle.info.maxInputPacketSize), 64)
|
||||
var buffer = [UInt8](repeating: 0, count: capacity)
|
||||
while self.stateQueue.sync(execute: { handle.running }) {
|
||||
var bytesRead: UInt32 = 0
|
||||
let readResult = buffer.withUnsafeMutableBytes { rawBuffer in
|
||||
FlutterUsbSerialDriverRead(
|
||||
handle.connection,
|
||||
rawBuffer.bindMemory(to: UInt8.self).baseAddress,
|
||||
UInt32(rawBuffer.count),
|
||||
250,
|
||||
&bytesRead
|
||||
)
|
||||
}
|
||||
if readResult != 0 {
|
||||
if self.stateQueue.sync(execute: { handle.running }) {
|
||||
self.emit([
|
||||
"portId": handle.portId,
|
||||
"type": "error",
|
||||
"message": self.driverMessage("USB read failed", readResult),
|
||||
])
|
||||
}
|
||||
break
|
||||
}
|
||||
if bytesRead > 0 {
|
||||
self.emit([
|
||||
"portId": handle.portId,
|
||||
"type": "data",
|
||||
"data": FlutterStandardTypedData(
|
||||
bytes: Data(buffer.prefix(Int(bytesRead)))
|
||||
),
|
||||
])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func write(arguments: Any?, result: @escaping FlutterResult) {
|
||||
guard
|
||||
let map = arguments as? [String: Any],
|
||||
let handle = port(from: map),
|
||||
let typedData = map["data"] as? FlutterStandardTypedData
|
||||
else {
|
||||
result(argumentError("portId and data are required"))
|
||||
return
|
||||
}
|
||||
var bytesWritten: UInt32 = 0
|
||||
let writeResult = typedData.data.withUnsafeBytes { rawBuffer in
|
||||
FlutterUsbSerialDriverWrite(
|
||||
handle.connection,
|
||||
rawBuffer.bindMemory(to: UInt8.self).baseAddress,
|
||||
UInt32(rawBuffer.count),
|
||||
&bytesWritten
|
||||
)
|
||||
}
|
||||
if writeResult == 0 {
|
||||
result(Int(bytesWritten))
|
||||
} else {
|
||||
result(driverError("WRITE_FAILED", "USB serial write failed", writeResult))
|
||||
}
|
||||
}
|
||||
|
||||
private func setParameters(arguments: Any?, result: @escaping FlutterResult) {
|
||||
guard let map = arguments as? [String: Any], let handle = port(from: map) else {
|
||||
result(argumentError("portId is required"))
|
||||
return
|
||||
}
|
||||
let callResult = FlutterUsbSerialDriverConfigure(
|
||||
handle.connection,
|
||||
(map["baudRate"] as? NSNumber)?.uint32Value ?? 9_600,
|
||||
(map["dataBits"] as? NSNumber)?.uint8Value ?? 8,
|
||||
(map["parity"] as? NSNumber)?.uint8Value ?? 0,
|
||||
(map["stopBits"] as? NSNumber)?.uint8Value ?? 1
|
||||
)
|
||||
completeVoid(callResult, code: "CONFIGURE_FAILED", result: result)
|
||||
}
|
||||
|
||||
private func setLineState(
|
||||
arguments: Any?,
|
||||
dtr: Bool,
|
||||
result: @escaping FlutterResult
|
||||
) {
|
||||
guard
|
||||
let map = arguments as? [String: Any],
|
||||
let handle = port(from: map),
|
||||
let value = map["value"] as? Bool
|
||||
else {
|
||||
result(argumentError("portId and value are required"))
|
||||
return
|
||||
}
|
||||
if dtr { handle.dtr = value } else { handle.rts = value }
|
||||
let callResult = FlutterUsbSerialDriverSetLineState(
|
||||
handle.connection, handle.dtr, handle.rts
|
||||
)
|
||||
completeVoid(callResult, code: "LINE_STATE_FAILED", result: result)
|
||||
}
|
||||
|
||||
private func purge(arguments: Any?, result: @escaping FlutterResult) {
|
||||
guard let map = arguments as? [String: Any], let handle = port(from: map) else {
|
||||
result(argumentError("portId is required"))
|
||||
return
|
||||
}
|
||||
let callResult = FlutterUsbSerialDriverPurge(
|
||||
handle.connection,
|
||||
map["purgeWriteBuffers"] as? Bool ?? true,
|
||||
map["purgeReadBuffers"] as? Bool ?? true
|
||||
)
|
||||
completeVoid(callResult, code: "PURGE_FAILED", result: result)
|
||||
}
|
||||
|
||||
private func close(arguments: Any?, result: @escaping FlutterResult) {
|
||||
guard
|
||||
let map = arguments as? [String: Any],
|
||||
let portId = (map["portId"] as? NSNumber)?.intValue
|
||||
else {
|
||||
result(argumentError("portId is required"))
|
||||
return
|
||||
}
|
||||
let handle = stateQueue.sync { () -> SerialPortHandle? in
|
||||
guard let removed = ports.removeValue(forKey: portId) else { return nil }
|
||||
removed.running = false
|
||||
return removed
|
||||
}
|
||||
if let handle {
|
||||
FlutterUsbSerialDriverClose(handle.connection)
|
||||
}
|
||||
result(nil)
|
||||
}
|
||||
|
||||
private func port(from map: [String: Any]) -> SerialPortHandle? {
|
||||
guard let portId = (map["portId"] as? NSNumber)?.intValue else { return nil }
|
||||
return stateQueue.sync { ports[portId] }
|
||||
}
|
||||
|
||||
private func completeVoid(
|
||||
_ callResult: Int32,
|
||||
code: String,
|
||||
result: @escaping FlutterResult
|
||||
) {
|
||||
if callResult == 0 {
|
||||
result(nil)
|
||||
} else {
|
||||
result(driverError(code, "DriverKit operation failed", callResult))
|
||||
}
|
||||
}
|
||||
|
||||
private func emit(_ event: [String: Any]) {
|
||||
DispatchQueue.main.async { [weak self] in self?.eventSink?(event) }
|
||||
}
|
||||
|
||||
private func argumentError(_ message: String) -> FlutterError {
|
||||
pluginError("INVALID_ARGUMENT", message)
|
||||
}
|
||||
|
||||
private func pluginError(_ code: String, _ message: String) -> FlutterError {
|
||||
FlutterError(code: code, message: message, details: nil)
|
||||
}
|
||||
|
||||
private func driverError(_ code: String, _ message: String, _ value: Int32) -> FlutterError {
|
||||
FlutterError(code: code, message: driverMessage(message, value), details: value)
|
||||
}
|
||||
|
||||
private func driverMessage(_ message: String, _ value: Int32) -> String {
|
||||
String(format: "%@ (IOReturn 0x%08X)", message, UInt32(bitPattern: value))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>NSPrivacyAccessedAPITypes</key>
|
||||
<array/>
|
||||
<key>NSPrivacyCollectedDataTypes</key>
|
||||
<array/>
|
||||
<key>NSPrivacyTracking</key>
|
||||
<false/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -1,7 +1,8 @@
|
||||
/// Flutter plugin for USB serial communication on Android.
|
||||
/// Flutter plugin for USB serial communication on Android and iPadOS.
|
||||
///
|
||||
/// Uses `usb-serial-for-android` (https://github.com/mik3y/usb-serial-for-android)
|
||||
/// as the underlying Android library.
|
||||
/// as the underlying Android library. iPadOS uses an app-embedded DriverKit
|
||||
/// extension supplied by the host application.
|
||||
library;
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
@@ -2,7 +2,8 @@ part of '../flutter_usb_serial.dart';
|
||||
|
||||
/// Main entry point for the flutter_usb_serial plugin.
|
||||
///
|
||||
/// Only supported on Android.
|
||||
/// Supported on Android and on iPadOS through an app-embedded DriverKit
|
||||
/// extension.
|
||||
class FlutterUsbSerial {
|
||||
FlutterUsbSerial._();
|
||||
|
||||
@@ -18,6 +19,15 @@ class FlutterUsbSerial {
|
||||
return _rawEventStream!;
|
||||
}
|
||||
|
||||
/// Whether the native USB serial backend is ready.
|
||||
///
|
||||
/// Android always provides its USB host backend. On iPadOS this probes for
|
||||
/// the DriverKit user-client service, so it is `false` until the matching
|
||||
/// app-embedded driver has loaded successfully.
|
||||
static Future<bool> isDriverAvailable() async {
|
||||
return await _methods.invokeMethod<bool>('isDriverAvailable') ?? false;
|
||||
}
|
||||
|
||||
/// Returns the list of currently attached USB serial devices.
|
||||
static Future<List<UsbSerialDevice>> listDevices() async {
|
||||
final result = await _methods.invokeMethod<List<Object?>>('listDevices');
|
||||
@@ -70,7 +80,8 @@ class FlutterUsbSerial {
|
||||
|
||||
final dataStream = _eventStream
|
||||
.where((e) => e['portId'] == portId)
|
||||
.transform(StreamTransformer<Map<Object?, Object?>, Uint8List>.fromHandlers(
|
||||
.transform(
|
||||
StreamTransformer<Map<Object?, Object?>, Uint8List>.fromHandlers(
|
||||
handleData: (e, sink) {
|
||||
if (e['type'] == 'data') {
|
||||
final raw = e['data'];
|
||||
@@ -80,11 +91,13 @@ class FlutterUsbSerial {
|
||||
sink.add(Uint8List.fromList(raw.cast<int>()));
|
||||
}
|
||||
} else if (e['type'] == 'error') {
|
||||
sink.addError(Exception(
|
||||
e['message'] as String? ?? 'USB IO error'));
|
||||
sink.addError(
|
||||
Exception(e['message'] as String? ?? 'USB IO error'),
|
||||
);
|
||||
}
|
||||
},
|
||||
));
|
||||
),
|
||||
);
|
||||
|
||||
return UsbSerialPort._(portId, dataStream);
|
||||
}
|
||||
|
||||
+3
-1
@@ -1,5 +1,5 @@
|
||||
name: flutter_usb_serial
|
||||
description: "Flutter plugin for USB serial communication on Android using usb-serial-for-android."
|
||||
description: "Flutter USB serial plugin for Android and iPadOS DriverKit."
|
||||
version: 0.1.0
|
||||
|
||||
environment:
|
||||
@@ -22,3 +22,5 @@ flutter:
|
||||
android:
|
||||
package: com.dalimaster.flutter_usb_serial
|
||||
pluginClass: FlutterUsbSerialPlugin
|
||||
ios:
|
||||
pluginClass: FlutterUsbSerialPlugin
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:flutter_usb_serial/flutter_usb_serial.dart';
|
||||
import 'package:flutter_usb_serial/flutter_usb_serial_method_channel.dart';
|
||||
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
test('$MethodChannelFlutterUsbSerial can be constructed', () {
|
||||
expect(MethodChannelFlutterUsbSerial(), isA<MethodChannelFlutterUsbSerial>());
|
||||
expect(
|
||||
MethodChannelFlutterUsbSerial(),
|
||||
isA<MethodChannelFlutterUsbSerial>(),
|
||||
);
|
||||
});
|
||||
|
||||
test('UsbSerialDevice maps fields from method channel payloads', () {
|
||||
@@ -25,4 +31,22 @@ void main() {
|
||||
expect(device.serialNumber, 'SN-42');
|
||||
expect(device.toMap()['deviceId'], 7);
|
||||
});
|
||||
|
||||
test(
|
||||
'isDriverAvailable forwards the native DriverKit readiness result',
|
||||
() async {
|
||||
const channel = MethodChannel('flutter_usb_serial/methods');
|
||||
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
||||
.setMockMethodCallHandler(channel, (call) async {
|
||||
expect(call.method, 'isDriverAvailable');
|
||||
return true;
|
||||
});
|
||||
addTearDown(() {
|
||||
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
||||
.setMockMethodCallHandler(channel, null);
|
||||
});
|
||||
|
||||
expect(await FlutterUsbSerial.isDriverAvailable(), isTrue);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user