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
|
# 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
|
- Android uses `usb-serial-for-android` and the normal Android USB permission
|
||||||
[plug-in package](https://flutter.dev/to/develop-plugins),
|
flow.
|
||||||
a specialized package that includes platform-specific implementation code for
|
- iPadOS 16+ uses Swift Package Manager and a user client exposed by a
|
||||||
Android and/or iOS.
|
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
|
The package contains the SwiftPM client plugin, not a reusable pre-signed
|
||||||
[online documentation](https://docs.flutter.dev), which offers tutorials,
|
DriverKit extension. Apple provisions USB transport entitlements to the host
|
||||||
samples, guidance on mobile development, and a full API reference.
|
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.
|
1. Embed a DriverKit extension whose `IOUserClass` is
|
||||||
To add platforms, run `flutter create -t plugin --platforms <platforms> .` in this directory.
|
`DaliMasterEspressifUsbDriver`, or adapt the client service name.
|
||||||
You can also find a detailed instruction on how to add platforms in the `pubspec.yaml` at https://flutter.dev/to/pubspec-plugin-platforms.
|
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) {
|
override fun onMethodCall(call: MethodCall, result: Result) {
|
||||||
when (call.method) {
|
when (call.method) {
|
||||||
|
"isDriverAvailable" -> result.success(true)
|
||||||
"listDevices" -> listDevices(result)
|
"listDevices" -> listDevices(result)
|
||||||
"requestPermission" -> requestPermission(call, result)
|
"requestPermission" -> requestPermission(call, result)
|
||||||
"openPort" -> openPort(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)
|
/// 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;
|
library;
|
||||||
|
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|||||||
@@ -2,7 +2,8 @@ part of '../flutter_usb_serial.dart';
|
|||||||
|
|
||||||
/// Main entry point for the flutter_usb_serial plugin.
|
/// 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 {
|
class FlutterUsbSerial {
|
||||||
FlutterUsbSerial._();
|
FlutterUsbSerial._();
|
||||||
|
|
||||||
@@ -18,6 +19,15 @@ class FlutterUsbSerial {
|
|||||||
return _rawEventStream!;
|
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.
|
/// Returns the list of currently attached USB serial devices.
|
||||||
static Future<List<UsbSerialDevice>> listDevices() async {
|
static Future<List<UsbSerialDevice>> listDevices() async {
|
||||||
final result = await _methods.invokeMethod<List<Object?>>('listDevices');
|
final result = await _methods.invokeMethod<List<Object?>>('listDevices');
|
||||||
@@ -70,21 +80,24 @@ class FlutterUsbSerial {
|
|||||||
|
|
||||||
final dataStream = _eventStream
|
final dataStream = _eventStream
|
||||||
.where((e) => e['portId'] == portId)
|
.where((e) => e['portId'] == portId)
|
||||||
.transform(StreamTransformer<Map<Object?, Object?>, Uint8List>.fromHandlers(
|
.transform(
|
||||||
handleData: (e, sink) {
|
StreamTransformer<Map<Object?, Object?>, Uint8List>.fromHandlers(
|
||||||
if (e['type'] == 'data') {
|
handleData: (e, sink) {
|
||||||
final raw = e['data'];
|
if (e['type'] == 'data') {
|
||||||
if (raw is Uint8List) {
|
final raw = e['data'];
|
||||||
sink.add(raw);
|
if (raw is Uint8List) {
|
||||||
} else if (raw is List) {
|
sink.add(raw);
|
||||||
sink.add(Uint8List.fromList(raw.cast<int>()));
|
} 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'),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} else if (e['type'] == 'error') {
|
},
|
||||||
sink.addError(Exception(
|
),
|
||||||
e['message'] as String? ?? 'USB IO error'));
|
);
|
||||||
}
|
|
||||||
},
|
|
||||||
));
|
|
||||||
|
|
||||||
return UsbSerialPort._(portId, dataStream);
|
return UsbSerialPort._(portId, dataStream);
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-1
@@ -1,5 +1,5 @@
|
|||||||
name: flutter_usb_serial
|
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
|
version: 0.1.0
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
@@ -22,3 +22,5 @@ flutter:
|
|||||||
android:
|
android:
|
||||||
package: com.dalimaster.flutter_usb_serial
|
package: com.dalimaster.flutter_usb_serial
|
||||||
pluginClass: FlutterUsbSerialPlugin
|
pluginClass: FlutterUsbSerialPlugin
|
||||||
|
ios:
|
||||||
|
pluginClass: FlutterUsbSerialPlugin
|
||||||
|
|||||||
@@ -1,10 +1,16 @@
|
|||||||
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_test/flutter_test.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.dart';
|
||||||
import 'package:flutter_usb_serial/flutter_usb_serial_method_channel.dart';
|
import 'package:flutter_usb_serial/flutter_usb_serial_method_channel.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
|
TestWidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
||||||
test('$MethodChannelFlutterUsbSerial can be constructed', () {
|
test('$MethodChannelFlutterUsbSerial can be constructed', () {
|
||||||
expect(MethodChannelFlutterUsbSerial(), isA<MethodChannelFlutterUsbSerial>());
|
expect(
|
||||||
|
MethodChannelFlutterUsbSerial(),
|
||||||
|
isA<MethodChannelFlutterUsbSerial>(),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('UsbSerialDevice maps fields from method channel payloads', () {
|
test('UsbSerialDevice maps fields from method channel payloads', () {
|
||||||
@@ -25,4 +31,22 @@ void main() {
|
|||||||
expect(device.serialNumber, 'SN-42');
|
expect(device.serialNumber, 'SN-42');
|
||||||
expect(device.toMap()['deviceId'], 7);
|
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