Initial commit
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
*.iml
|
||||
.gradle
|
||||
/local.properties
|
||||
/.idea/workspace.xml
|
||||
/.idea/libraries
|
||||
.DS_Store
|
||||
/build
|
||||
/captures
|
||||
.cxx
|
||||
@@ -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')
|
||||
}
|
||||
@@ -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'
|
||||
)
|
||||
@@ -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 ""
|
||||
),
|
||||
)
|
||||
}
|
||||
+27
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user