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
+9
View File
@@ -0,0 +1,9 @@
*.iml
.gradle
/local.properties
/.idea/workspace.xml
/.idea/libraries
.DS_Store
/build
/captures
.cxx
+51
View File
@@ -0,0 +1,51 @@
group = "com.dalimaster.flutter_usb_serial"
version = "1.0-SNAPSHOT"
buildscript {
ext.kotlin_version = "2.1.0"
repositories {
google()
mavenCentral()
}
dependencies {
classpath("com.android.tools.build:gradle:8.9.1")
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version")
}
}
allprojects {
repositories {
google()
mavenCentral()
}
}
apply plugin: "com.android.library"
apply plugin: "kotlin-android"
android {
namespace = "com.dalimaster.flutter_usb_serial"
compileSdk = 36
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_11
}
sourceSets {
main.java.srcDirs += "src/main/kotlin"
}
defaultConfig {
minSdk = 21
}
}
dependencies {
// usb-serial-for-android as a local module included via settings.gradle
implementation project(':usbSerialForAndroid')
}
+7
View File
@@ -0,0 +1,7 @@
// Include the usb-serial-for-android library as a local module.
// The path is relative to this settings.gradle file.
include ':usbSerialForAndroid'
project(':usbSerialForAndroid').projectDir = new File(
rootProject.projectDir,
'../../../temp/usb-serial-for-android/usbSerialForAndroid'
)
+4
View File
@@ -0,0 +1,4 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Required to enumerate USB devices -->
<uses-feature android:name="android.hardware.usb.host" android:required="false" />
</manifest>
@@ -0,0 +1,322 @@
package com.dalimaster.flutter_usb_serial
import android.app.PendingIntent
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.hardware.usb.UsbDevice
import android.hardware.usb.UsbManager
import android.os.Build
import com.hoho.android.usbserial.driver.UsbSerialPort
import com.hoho.android.usbserial.driver.UsbSerialProber
import com.hoho.android.usbserial.util.SerialInputOutputManager
import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.embedding.engine.plugins.activity.ActivityAware
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding
import io.flutter.plugin.common.EventChannel
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodChannel.MethodCallHandler
import io.flutter.plugin.common.MethodChannel.Result
import java.io.IOException
class FlutterUsbSerialPlugin :
FlutterPlugin,
MethodCallHandler,
EventChannel.StreamHandler,
ActivityAware {
companion object {
private const val ACTION_USB_PERMISSION =
"com.dalimaster.flutter_usb_serial.USB_PERMISSION"
private const val METHOD_CHANNEL = "flutter_usb_serial/methods"
private const val EVENT_CHANNEL = "flutter_usb_serial/data"
}
private lateinit var methodChannel: MethodChannel
private lateinit var eventChannel: EventChannel
private lateinit var context: Context
private lateinit var usbManager: UsbManager
@Volatile
private var eventSink: EventChannel.EventSink? = null
private data class PortHandle(
val id: Int,
val port: UsbSerialPort,
val ioManager: SerialInputOutputManager,
)
private var nextPortId = 1
private val openPorts = mutableMapOf<Int, PortHandle>()
private val portsLock = Any()
private data class PendingPermission(val device: UsbDevice, val result: Result)
@Volatile
private var pendingPermission: PendingPermission? = null
private val permissionLock = Any()
private val permissionReceiver = object : BroadcastReceiver() {
override fun onReceive(ctx: Context?, intent: Intent?) {
if (intent?.action != ACTION_USB_PERMISSION) return
val device: UsbDevice? = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
intent.getParcelableExtra(UsbManager.EXTRA_DEVICE, UsbDevice::class.java)
} else {
@Suppress("DEPRECATION")
intent.getParcelableExtra(UsbManager.EXTRA_DEVICE)
}
val granted = intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false)
var pending: PendingPermission? = null
synchronized(permissionLock) {
if (device != null && pendingPermission?.device?.deviceId == device.deviceId) {
pending = pendingPermission
pendingPermission = null
}
}
pending?.result?.success(granted)
}
}
// ── FlutterPlugin ────────────────────────────────────────────────────────
override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) {
context = binding.applicationContext
usbManager = context.getSystemService(Context.USB_SERVICE) as UsbManager
methodChannel = MethodChannel(binding.binaryMessenger, METHOD_CHANNEL)
methodChannel.setMethodCallHandler(this)
eventChannel = EventChannel(binding.binaryMessenger, EVENT_CHANNEL)
eventChannel.setStreamHandler(this)
val filter = IntentFilter(ACTION_USB_PERMISSION)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
context.registerReceiver(permissionReceiver, filter, Context.RECEIVER_NOT_EXPORTED)
} else {
context.registerReceiver(permissionReceiver, filter)
}
}
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
methodChannel.setMethodCallHandler(null)
eventChannel.setStreamHandler(null)
context.unregisterReceiver(permissionReceiver)
closeAllPorts()
}
// ── ActivityAware ────────────────────────────────────────────────────────
override fun onAttachedToActivity(binding: ActivityPluginBinding) {}
override fun onDetachedFromActivityForConfigChanges() {}
override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) {}
override fun onDetachedFromActivity() {}
// ── EventChannel.StreamHandler ───────────────────────────────────────────
override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
eventSink = events
}
override fun onCancel(arguments: Any?) {
eventSink = null
}
// ── MethodCallHandler ────────────────────────────────────────────────────
override fun onMethodCall(call: MethodCall, result: Result) {
when (call.method) {
"listDevices" -> listDevices(result)
"requestPermission" -> requestPermission(call, result)
"openPort" -> openPort(call, result)
"write" -> write(call, result)
"close" -> closePort(call, result)
else -> result.notImplemented()
}
}
// ── Method implementations ───────────────────────────────────────────────
private fun listDevices(result: Result) {
val devices = usbManager.deviceList.values.mapNotNull { device ->
encodeDevice(device)
}
result.success(devices)
}
private fun requestPermission(call: MethodCall, result: Result) {
val deviceId = call.argument<Int>("deviceId") ?: run {
result.error("INVALID_ARGUMENT", "deviceId required", null)
return
}
val device = usbManager.deviceList.values.firstOrNull { it.deviceId == deviceId }
if (device == null) {
result.error("DEVICE_NOT_FOUND", "USB device $deviceId not found", null)
return
}
if (usbManager.hasPermission(device)) {
result.success(true)
return
}
synchronized(permissionLock) {
pendingPermission = PendingPermission(device, result)
}
val flags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S)
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
else
PendingIntent.FLAG_UPDATE_CURRENT
val intent = Intent(ACTION_USB_PERMISSION).apply { setPackage(context.packageName) }
val pi = PendingIntent.getBroadcast(context, 0, intent, flags)
usbManager.requestPermission(device, pi)
}
private fun openPort(call: MethodCall, result: Result) {
val deviceId = call.argument<Int>("deviceId") ?: run {
result.error("INVALID_ARGUMENT", "deviceId required", null)
return
}
val baudRate = call.argument<Int>("baudRate") ?: 9600
val dataBits = call.argument<Int>("dataBits") ?: 8
val parity = call.argument<Int>("parity") ?: UsbSerialPort.PARITY_NONE
val stopBits = call.argument<Int>("stopBits") ?: UsbSerialPort.STOPBITS_1
val device = usbManager.deviceList.values.firstOrNull { it.deviceId == deviceId }
if (device == null) {
result.error("DEVICE_NOT_FOUND", "USB device $deviceId not found", null)
return
}
if (!usbManager.hasPermission(device)) {
result.error("PERMISSION_DENIED", "USB permission not granted for device $deviceId", null)
return
}
val driver = UsbSerialProber.getDefaultProber().probeDevice(device)
if (driver == null || driver.ports.isEmpty()) {
result.error("NO_DRIVER", "No serial driver found for device $deviceId", null)
return
}
val connection = usbManager.openDevice(device)
if (connection == null) {
result.error("OPEN_FAILED", "Failed to open USB connection for device $deviceId", null)
return
}
val port = driver.ports[0]
try {
port.open(connection)
port.setParameters(baudRate, dataBits, stopBits, parity)
} catch (e: IOException) {
result.error("OPEN_FAILED", "Failed to configure port: ${e.message}", null)
return
}
val portId: Int
synchronized(portsLock) {
portId = nextPortId++
}
val ioManager = SerialInputOutputManager(
port,
object : SerialInputOutputManager.Listener {
override fun onNewData(data: ByteArray) {
val sink = eventSink ?: return
val event = mapOf("portId" to portId, "type" to "data", "data" to data)
// EventSink must be called on the main thread
android.os.Handler(android.os.Looper.getMainLooper()).post {
sink.success(event)
}
}
override fun onRunError(e: Exception) {
// Clean up immediately so the port is no longer usable
synchronized(portsLock) { openPorts.remove(portId) }
try { port.close() } catch (_: IOException) {}
val sink = eventSink ?: return
val event = mapOf(
"portId" to portId,
"type" to "error",
"message" to (e.message ?: "IO error"),
)
android.os.Handler(android.os.Looper.getMainLooper()).post {
sink.success(event)
}
}
}
)
ioManager.start()
synchronized(portsLock) {
openPorts[portId] = PortHandle(portId, port, ioManager)
}
result.success(portId)
}
private fun write(call: MethodCall, result: Result) {
val portId = call.argument<Int>("portId") ?: run {
result.error("INVALID_ARGUMENT", "portId required", null)
return
}
val data = call.argument<ByteArray>("data") ?: run {
result.error("INVALID_ARGUMENT", "data required", null)
return
}
val handle = synchronized(portsLock) { openPorts[portId] }
if (handle == null) {
result.error("PORT_NOT_FOUND", "Port $portId is not open", null)
return
}
try {
handle.port.write(data, 2000)
result.success(data.size)
} catch (e: IOException) {
result.error("WRITE_FAILED", e.message, null)
}
}
private fun closePort(call: MethodCall, result: Result) {
val portId = call.argument<Int>("portId") ?: run {
result.error("INVALID_ARGUMENT", "portId required", null)
return
}
val handle = synchronized(portsLock) { openPorts.remove(portId) }
if (handle != null) {
handle.ioManager.stop()
try {
handle.port.close()
} catch (_: IOException) {}
}
result.success(null)
}
private fun closeAllPorts() {
val handles = synchronized(portsLock) {
val all = openPorts.values.toList()
openPorts.clear()
all
}
for (handle in handles) {
handle.ioManager.stop()
try {
handle.port.close()
} catch (_: IOException) {}
}
}
// ── Helpers ──────────────────────────────────────────────────────────────
private fun encodeDevice(device: UsbDevice): Map<String, Any?> = mapOf(
"deviceId" to device.deviceId,
"vendorId" to device.vendorId,
"productId" to device.productId,
"deviceName" to device.deviceName,
"manufacturerName" to (
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP)
try { device.manufacturerName ?: "" } catch (_: SecurityException) { "" }
else ""
),
"serialNumber" to (
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP)
try { device.serialNumber ?: "" } catch (_: SecurityException) { "" }
else ""
),
)
}
@@ -0,0 +1,27 @@
package com.dalimaster.flutter_usb_serial
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import org.mockito.Mockito
import kotlin.test.Test
/*
* This demonstrates a simple unit test of the Kotlin portion of this plugin's implementation.
*
* Once you have built the plugin's example app, you can run these tests from the command
* line by running `./gradlew testDebugUnitTest` in the `example/android/` directory, or
* you can run them directly from IDEs that support JUnit such as Android Studio.
*/
internal class FlutterUsbSerialPluginTest {
@Test
fun onMethodCall_getPlatformVersion_returnsExpectedValue() {
val plugin = FlutterUsbSerialPlugin()
val call = MethodCall("getPlatformVersion", null)
val mockResult: MethodChannel.Result = Mockito.mock(MethodChannel.Result::class.java)
plugin.onMethodCall(call, mockResult)
Mockito.verify(mockResult).success("Android " + android.os.Build.VERSION.RELEASE)
}
}