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
+33
View File
@@ -0,0 +1,33 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.build/
.buildlog/
.history
.svn/
.swiftpm/
migrate_working_dir/
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
#.vscode/
# Flutter/Dart/Pub related
# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock.
/pubspec.lock
**/doc/api/
.dart_tool/
.flutter-plugins-dependencies
/build/
/coverage/
+30
View File
@@ -0,0 +1,30 @@
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled and should not be manually edited.
version:
revision: "d693b4b9dbac2acd4477aea4555ca6dcbea44ba2"
channel: "stable"
project_type: plugin
# Tracks metadata for the flutter migrate command
migration:
platforms:
- platform: root
create_revision: d693b4b9dbac2acd4477aea4555ca6dcbea44ba2
base_revision: d693b4b9dbac2acd4477aea4555ca6dcbea44ba2
- platform: android
create_revision: d693b4b9dbac2acd4477aea4555ca6dcbea44ba2
base_revision: d693b4b9dbac2acd4477aea4555ca6dcbea44ba2
# User provided section
# List of Local paths (relative to this file) that should be
# ignored by the migrate tool.
#
# Files that are not part of the templates will be ignored by default.
unmanaged_files:
- 'lib/main.dart'
- 'ios/Runner.xcodeproj/project.pbxproj'
+3
View File
@@ -0,0 +1,3 @@
## 0.0.1
* TODO: Describe initial release.
+1
View File
@@ -0,0 +1 @@
TODO: Add your license here.
+18
View File
@@ -0,0 +1,18 @@
# flutter_usb_serial
A new Flutter plugin project.
## Getting Started
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.
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 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.
+4
View File
@@ -0,0 +1,4 @@
include: package:flutter_lints/flutter.yaml
# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options
+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)
}
}
+45
View File
@@ -0,0 +1,45 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.build/
.buildlog/
.history
.svn/
.swiftpm/
migrate_working_dir/
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
#.vscode/
# Flutter/Dart/Pub related
**/doc/api/
**/ios/Flutter/.last_build_id
.dart_tool/
.flutter-plugins-dependencies
.pub-cache/
.pub/
/build/
/coverage/
# Symbolication related
app.*.symbols
# Obfuscation related
app.*.map.json
# Android Studio will place build artifacts here
/android/app/debug
/android/app/profile
/android/app/release
+16
View File
@@ -0,0 +1,16 @@
# flutter_usb_serial_example
Demonstrates how to use the flutter_usb_serial plugin.
## Getting Started
This project is a starting point for a Flutter application.
A few resources to get you started if this is your first Flutter project:
- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab)
- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook)
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.
+28
View File
@@ -0,0 +1,28 @@
# This file configures the analyzer, which statically analyzes Dart code to
# check for errors, warnings, and lints.
#
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
# invoked from the command line by running `flutter analyze`.
# The following line activates a set of recommended lints for Flutter apps,
# packages, and plugins designed to encourage good coding practices.
include: package:flutter_lints/flutter.yaml
linter:
# The lint rules applied to this project can be customized in the
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
# included above or to enable additional rules. A list of all available lints
# and their documentation is published at https://dart.dev/lints.
#
# Instead of disabling a lint rule for the entire project in the
# section below, it can also be suppressed for a single line of code
# or a specific dart file by using the `// ignore: name_of_lint` and
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
# producing the lint.
rules:
# avoid_print: false # Uncomment to disable the `avoid_print` rule
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options
+14
View File
@@ -0,0 +1,14 @@
gradle-wrapper.jar
/.gradle
/captures/
/gradlew
/gradlew.bat
/local.properties
GeneratedPluginRegistrant.java
.cxx/
# Remember to never publicly share your keystore.
# See https://flutter.dev/to/reference-keystore
key.properties
**/*.keystore
**/*.jks
+44
View File
@@ -0,0 +1,44 @@
plugins {
id("com.android.application")
id("kotlin-android")
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
id("dev.flutter.flutter-gradle-plugin")
}
android {
namespace = "com.dalimaster.flutter_usb_serial_example"
compileSdk = flutter.compileSdkVersion
ndkVersion = flutter.ndkVersion
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_11.toString()
}
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId = "com.dalimaster.flutter_usb_serial_example"
// You can update the following values to match your application needs.
// For more information, see: https://flutter.dev/to/review-gradle-config.
minSdk = flutter.minSdkVersion
targetSdk = flutter.targetSdkVersion
versionCode = flutter.versionCode
versionName = flutter.versionName
}
buildTypes {
release {
// TODO: Add your own signing config for the release build.
// Signing with the debug keys for now, so `flutter run --release` works.
signingConfig = signingConfigs.getByName("debug")
}
}
}
flutter {
source = "../.."
}
@@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>
@@ -0,0 +1,45 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:label="flutter_usb_serial_example"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:taskAffinity=""
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues
to determine the Window background behind the Flutter UI. -->
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data
android:name="flutterEmbedding"
android:value="2" />
</application>
<!-- Required to query activities that can process text, see:
https://developer.android.com/training/package-visibility and
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
<queries>
<intent>
<action android:name="android.intent.action.PROCESS_TEXT"/>
<data android:mimeType="text/plain"/>
</intent>
</queries>
</manifest>
@@ -0,0 +1,5 @@
package com.dalimaster.flutter_usb_serial_example
import io.flutter.embedding.android.FlutterActivity
class MainActivity : FlutterActivity()
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="?android:colorBackground" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@android:color/white" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>
Binary file not shown.

After

Width:  |  Height:  |  Size: 544 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 442 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 721 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>
@@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>
+24
View File
@@ -0,0 +1,24 @@
allprojects {
repositories {
google()
mavenCentral()
}
}
val newBuildDir: Directory =
rootProject.layout.buildDirectory
.dir("../../build")
.get()
rootProject.layout.buildDirectory.value(newBuildDir)
subprojects {
val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name)
project.layout.buildDirectory.value(newSubprojectBuildDir)
}
subprojects {
project.evaluationDependsOn(":app")
}
tasks.register<Delete>("clean") {
delete(rootProject.layout.buildDirectory)
}
+3
View File
@@ -0,0 +1,3 @@
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
android.useAndroidX=true
android.enableJetifier=true
@@ -0,0 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-all.zip
+26
View File
@@ -0,0 +1,26 @@
pluginManagement {
val flutterSdkPath =
run {
val properties = java.util.Properties()
file("local.properties").inputStream().use { properties.load(it) }
val flutterSdkPath = properties.getProperty("flutter.sdk")
require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" }
flutterSdkPath
}
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
plugins {
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
id("com.android.application") version "8.9.1" apply false
id("org.jetbrains.kotlin.android") version "2.1.0" apply false
}
include(":app")
@@ -0,0 +1,25 @@
// This is a basic Flutter integration test.
//
// Since integration tests run in a full Flutter application, they can interact
// with the host side of a plugin implementation, unlike Dart unit tests.
//
// For more information about Flutter integration tests, please see
// https://flutter.dev/to/integration-testing
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:flutter_usb_serial/flutter_usb_serial.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
testWidgets('getPlatformVersion test', (WidgetTester tester) async {
final FlutterUsbSerial plugin = FlutterUsbSerial();
final String? version = await plugin.getPlatformVersion();
// The version string depends on the host platform running the test, so
// just assert that some non-empty string is returned.
expect(version?.isNotEmpty, true);
});
}
+63
View File
@@ -0,0 +1,63 @@
import 'package:flutter/material.dart';
import 'dart:async';
import 'package:flutter/services.dart';
import 'package:flutter_usb_serial/flutter_usb_serial.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatefulWidget {
const MyApp({super.key});
@override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
String _platformVersion = 'Unknown';
final _flutterUsbSerialPlugin = FlutterUsbSerial();
@override
void initState() {
super.initState();
initPlatformState();
}
// Platform messages are asynchronous, so we initialize in an async method.
Future<void> initPlatformState() async {
String platformVersion;
// Platform messages may fail, so we use a try/catch PlatformException.
// We also handle the message potentially returning null.
try {
platformVersion =
await _flutterUsbSerialPlugin.getPlatformVersion() ?? 'Unknown platform version';
} on PlatformException {
platformVersion = 'Failed to get platform version.';
}
// If the widget was removed from the tree while the asynchronous platform
// message was in flight, we want to discard the reply rather than calling
// setState to update our non-existent appearance.
if (!mounted) return;
setState(() {
_platformVersion = platformVersion;
});
}
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: const Text('Plugin example app'),
),
body: Center(
child: Text('Running on: $_platformVersion\n'),
),
),
);
}
}
+283
View File
@@ -0,0 +1,283 @@
# Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile
packages:
async:
dependency: transitive
description:
name: async
sha256: e2eb0491ba5ddb6177742d2da23904574082139b07c1e33b8503b9f46f3e1a37
url: "https://pub.dev"
source: hosted
version: "2.13.1"
boolean_selector:
dependency: transitive
description:
name: boolean_selector
sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea"
url: "https://pub.dev"
source: hosted
version: "2.1.2"
characters:
dependency: transitive
description:
name: characters
sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803
url: "https://pub.dev"
source: hosted
version: "1.4.0"
clock:
dependency: transitive
description:
name: clock
sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b
url: "https://pub.dev"
source: hosted
version: "1.1.2"
collection:
dependency: transitive
description:
name: collection
sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76"
url: "https://pub.dev"
source: hosted
version: "1.19.1"
cupertino_icons:
dependency: "direct main"
description:
name: cupertino_icons
sha256: "41e005c33bd814be4d3096aff55b1908d419fde52ca656c8c47719ec745873cd"
url: "https://pub.dev"
source: hosted
version: "1.0.9"
fake_async:
dependency: transitive
description:
name: fake_async
sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44"
url: "https://pub.dev"
source: hosted
version: "1.3.3"
file:
dependency: transitive
description:
name: file
sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4
url: "https://pub.dev"
source: hosted
version: "7.0.1"
flutter:
dependency: "direct main"
description: flutter
source: sdk
version: "0.0.0"
flutter_driver:
dependency: transitive
description: flutter
source: sdk
version: "0.0.0"
flutter_lints:
dependency: "direct dev"
description:
name: flutter_lints
sha256: "5398f14efa795ffb7a33e9b6a08798b26a180edac4ad7db3f231e40f82ce11e1"
url: "https://pub.dev"
source: hosted
version: "5.0.0"
flutter_test:
dependency: "direct dev"
description: flutter
source: sdk
version: "0.0.0"
flutter_usb_serial:
dependency: "direct main"
description:
path: ".."
relative: true
source: path
version: "0.1.0"
fuchsia_remote_debug_protocol:
dependency: transitive
description: flutter
source: sdk
version: "0.0.0"
integration_test:
dependency: "direct dev"
description: flutter
source: sdk
version: "0.0.0"
leak_tracker:
dependency: transitive
description:
name: leak_tracker
sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de"
url: "https://pub.dev"
source: hosted
version: "11.0.2"
leak_tracker_flutter_testing:
dependency: transitive
description:
name: leak_tracker_flutter_testing
sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1"
url: "https://pub.dev"
source: hosted
version: "3.0.10"
leak_tracker_testing:
dependency: transitive
description:
name: leak_tracker_testing
sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1"
url: "https://pub.dev"
source: hosted
version: "3.0.2"
lints:
dependency: transitive
description:
name: lints
sha256: c35bb79562d980e9a453fc715854e1ed39e24e7d0297a880ef54e17f9874a9d7
url: "https://pub.dev"
source: hosted
version: "5.1.1"
matcher:
dependency: transitive
description:
name: matcher
sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2
url: "https://pub.dev"
source: hosted
version: "0.12.17"
material_color_utilities:
dependency: transitive
description:
name: material_color_utilities
sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec
url: "https://pub.dev"
source: hosted
version: "0.11.1"
meta:
dependency: transitive
description:
name: meta
sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c
url: "https://pub.dev"
source: hosted
version: "1.16.0"
path:
dependency: transitive
description:
name: path
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
url: "https://pub.dev"
source: hosted
version: "1.9.1"
platform:
dependency: transitive
description:
name: platform
sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984"
url: "https://pub.dev"
source: hosted
version: "3.1.6"
plugin_platform_interface:
dependency: transitive
description:
name: plugin_platform_interface
sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02"
url: "https://pub.dev"
source: hosted
version: "2.1.8"
process:
dependency: transitive
description:
name: process
sha256: c6248e4526673988586e8c00bb22a49210c258dc91df5227d5da9748ecf79744
url: "https://pub.dev"
source: hosted
version: "5.0.5"
sky_engine:
dependency: transitive
description: flutter
source: sdk
version: "0.0.0"
source_span:
dependency: transitive
description:
name: source_span
sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab"
url: "https://pub.dev"
source: hosted
version: "1.10.2"
stack_trace:
dependency: transitive
description:
name: stack_trace
sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1"
url: "https://pub.dev"
source: hosted
version: "1.12.1"
stream_channel:
dependency: transitive
description:
name: stream_channel
sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d"
url: "https://pub.dev"
source: hosted
version: "2.1.4"
string_scanner:
dependency: transitive
description:
name: string_scanner
sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43"
url: "https://pub.dev"
source: hosted
version: "1.4.1"
sync_http:
dependency: transitive
description:
name: sync_http
sha256: "7f0cd72eca000d2e026bcd6f990b81d0ca06022ef4e32fb257b30d3d1014a961"
url: "https://pub.dev"
source: hosted
version: "0.3.1"
term_glyph:
dependency: transitive
description:
name: term_glyph
sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e"
url: "https://pub.dev"
source: hosted
version: "1.2.2"
test_api:
dependency: transitive
description:
name: test_api
sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00"
url: "https://pub.dev"
source: hosted
version: "0.7.6"
vector_math:
dependency: transitive
description:
name: vector_math
sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b
url: "https://pub.dev"
source: hosted
version: "2.2.0"
vm_service:
dependency: transitive
description:
name: vm_service
sha256: "046d3928e16fa4dc46e8350415661755ab759d9fc97fc21b5ab295f71e4f0499"
url: "https://pub.dev"
source: hosted
version: "15.1.0"
webdriver:
dependency: transitive
description:
name: webdriver
sha256: "2f3a14ca026957870cfd9c635b83507e0e51d8091568e90129fbf805aba7cade"
url: "https://pub.dev"
source: hosted
version: "3.1.0"
sdks:
dart: ">=3.9.2 <4.0.0"
flutter: ">=3.18.0-18.0.pre.54"
+85
View File
@@ -0,0 +1,85 @@
name: flutter_usb_serial_example
description: "Demonstrates how to use the flutter_usb_serial plugin."
# The following line prevents the package from being accidentally published to
# pub.dev using `flutter pub publish`. This is preferred for private packages.
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
environment:
sdk: ^3.9.2
# Dependencies specify other packages that your package needs in order to work.
# To automatically upgrade your package dependencies to the latest versions
# consider running `flutter pub upgrade --major-versions`. Alternatively,
# dependencies can be manually updated by changing the version numbers below to
# the latest version available on pub.dev. To see which dependencies have newer
# versions available, run `flutter pub outdated`.
dependencies:
flutter:
sdk: flutter
flutter_usb_serial:
# When depending on this package from a real application you should use:
# flutter_usb_serial: ^x.y.z
# See https://dart.dev/tools/pub/dependencies#version-constraints
# The example app is bundled with the plugin so we use a path dependency on
# the parent directory to use the current plugin's version.
path: ../
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.8
dev_dependencies:
integration_test:
sdk: flutter
flutter_test:
sdk: flutter
# The "flutter_lints" package below contains a set of recommended lints to
# encourage good coding practices. The lint set provided by the package is
# activated in the `analysis_options.yaml` file located at the root of your
# package. See that file for information about deactivating specific lint
# rules and activating additional ones.
flutter_lints: ^5.0.0
# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec
# The following section is specific to Flutter packages.
flutter:
# The following line ensures that the Material Icons font is
# included with your application, so that you can use the icons in
# the material Icons class.
uses-material-design: true
# To add assets to your application, add an assets section, like this:
# assets:
# - images/a_dot_burr.jpeg
# - images/a_dot_ham.jpeg
# An image asset can refer to one or more resolution-specific "variants", see
# https://flutter.dev/to/resolution-aware-images
# For details regarding adding assets from package dependencies, see
# https://flutter.dev/to/asset-from-package
# To add custom fonts to your application, add a fonts section here,
# in this "flutter" section. Each entry in this list should have a
# "family" key with the font family name, and a "fonts" key with a
# list giving the asset and other descriptors for the font. For
# example:
# fonts:
# - family: Schyler
# fonts:
# - asset: fonts/Schyler-Regular.ttf
# - asset: fonts/Schyler-Italic.ttf
# style: italic
# - family: Trajan Pro
# fonts:
# - asset: fonts/TrajanPro.ttf
# - asset: fonts/TrajanPro_Bold.ttf
# weight: 700
#
# For details regarding fonts from package dependencies,
# see https://flutter.dev/to/font-from-package
+27
View File
@@ -0,0 +1,27 @@
// This is a basic Flutter widget test.
//
// To perform an interaction with a widget in your test, use the WidgetTester
// utility in the flutter_test package. For example, you can send tap and scroll
// gestures. You can also use WidgetTester to find child widgets in the widget
// tree, read text, and verify that the values of widget properties are correct.
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_usb_serial_example/main.dart';
void main() {
testWidgets('Verify Platform version', (WidgetTester tester) async {
// Build our app and trigger a frame.
await tester.pumpWidget(const MyApp());
// Verify that platform version is retrieved.
expect(
find.byWidgetPredicate(
(Widget widget) => widget is Text &&
widget.data!.startsWith('Running on:'),
),
findsOneWidget,
);
});
}
+13
View File
@@ -0,0 +1,13 @@
/// Flutter plugin for USB serial communication on Android.
///
/// Uses `usb-serial-for-android` (https://github.com/mik3y/usb-serial-for-android)
/// as the underlying Android library.
library;
import 'dart:async';
import 'package:flutter/services.dart';
part 'src/usb_serial_device.dart';
part 'src/usb_serial_port.dart';
part 'src/flutter_usb_serial.dart';
@@ -0,0 +1,17 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'flutter_usb_serial_platform_interface.dart';
/// An implementation of [FlutterUsbSerialPlatform] that uses method channels.
class MethodChannelFlutterUsbSerial extends FlutterUsbSerialPlatform {
/// The method channel used to interact with the native platform.
@visibleForTesting
final methodChannel = const MethodChannel('flutter_usb_serial');
@override
Future<String?> getPlatformVersion() async {
final version = await methodChannel.invokeMethod<String>('getPlatformVersion');
return version;
}
}
@@ -0,0 +1,29 @@
import 'package:plugin_platform_interface/plugin_platform_interface.dart';
import 'flutter_usb_serial_method_channel.dart';
abstract class FlutterUsbSerialPlatform extends PlatformInterface {
/// Constructs a FlutterUsbSerialPlatform.
FlutterUsbSerialPlatform() : super(token: _token);
static final Object _token = Object();
static FlutterUsbSerialPlatform _instance = MethodChannelFlutterUsbSerial();
/// The default instance of [FlutterUsbSerialPlatform] to use.
///
/// Defaults to [MethodChannelFlutterUsbSerial].
static FlutterUsbSerialPlatform get instance => _instance;
/// Platform-specific implementations should set this with their own
/// platform-specific class that extends [FlutterUsbSerialPlatform] when
/// they register themselves.
static set instance(FlutterUsbSerialPlatform instance) {
PlatformInterface.verifyToken(instance, _token);
_instance = instance;
}
Future<String?> getPlatformVersion() {
throw UnimplementedError('platformVersion() has not been implemented.');
}
}
+102
View File
@@ -0,0 +1,102 @@
part of '../flutter_usb_serial.dart';
/// Main entry point for the flutter_usb_serial plugin.
///
/// Only supported on Android.
class FlutterUsbSerial {
FlutterUsbSerial._();
static const _methods = MethodChannel('flutter_usb_serial/methods');
static const _events = EventChannel('flutter_usb_serial/data');
static Stream<Map<Object?, Object?>>? _rawEventStream;
static Stream<Map<Object?, Object?>> get _eventStream {
_rawEventStream ??= _events
.receiveBroadcastStream()
.cast<Map<Object?, Object?>>();
return _rawEventStream!;
}
/// Returns the list of currently attached USB serial devices.
static Future<List<UsbSerialDevice>> listDevices() async {
final result = await _methods.invokeMethod<List<Object?>>('listDevices');
if (result == null) return [];
return result
.whereType<Map<Object?, Object?>>()
.map(UsbSerialDevice.fromMap)
.toList();
}
/// Requests Android USB permission for [device] if not already granted.
///
/// Returns `true` if permission was granted (or was already held).
static Future<bool> requestPermission(UsbSerialDevice device) async {
final granted = await _methods.invokeMethod<bool>('requestPermission', {
'deviceId': device.deviceId,
});
return granted ?? false;
}
/// Opens the first port of [device] with the given serial parameters.
///
/// [baudRate] defaults to 9600.
/// [dataBits] must be 5, 6, 7, or 8. Defaults to 8.
/// [parity] defaults to [UsbSerialParity.none].
/// [stopBits] defaults to [UsbSerialStopBits.one].
///
/// The returned [UsbSerialPort] streams incoming data via [UsbSerialPort.inputStream].
/// Call [UsbSerialPort.close] when done.
static Future<UsbSerialPort> openPort(
UsbSerialDevice device, {
int baudRate = 9600,
int dataBits = 8,
UsbSerialParity parity = UsbSerialParity.none,
UsbSerialStopBits stopBits = UsbSerialStopBits.one,
}) async {
final portId = await _methods.invokeMethod<int>('openPort', {
'deviceId': device.deviceId,
'baudRate': baudRate,
'dataBits': dataBits,
'parity': parity.index,
'stopBits': _stopBitsIndex(stopBits),
});
if (portId == null) {
throw PlatformException(
code: 'OPEN_FAILED',
message: 'Failed to open USB serial port for device ${device.deviceId}',
);
}
final dataStream = _eventStream
.where((e) => e['portId'] == portId)
.transform(StreamTransformer<Map<Object?, Object?>, Uint8List>.fromHandlers(
handleData: (e, sink) {
if (e['type'] == 'data') {
final raw = e['data'];
if (raw is Uint8List) {
sink.add(raw);
} else if (raw is List) {
sink.add(Uint8List.fromList(raw.cast<int>()));
}
} else if (e['type'] == 'error') {
sink.addError(Exception(
e['message'] as String? ?? 'USB IO error'));
}
},
));
return UsbSerialPort._(portId, dataStream);
}
static int _stopBitsIndex(UsbSerialStopBits sb) {
switch (sb) {
case UsbSerialStopBits.one:
return 1;
case UsbSerialStopBits.onePointFive:
return 3; // UsbSerialPort.STOPBITS_1_5
case UsbSerialStopBits.two:
return 2;
}
}
}
+56
View File
@@ -0,0 +1,56 @@
part of '../flutter_usb_serial.dart';
/// Represents a USB serial device found during enumeration.
class UsbSerialDevice {
/// Unique device ID (Android USB device ID).
final int deviceId;
/// Vendor ID (VID).
final int vendorId;
/// Product ID (PID).
final int productId;
/// Human-readable device name (product string from descriptor, may be empty).
final String deviceName;
/// Manufacturer string (may be empty).
final String manufacturerName;
/// Serial number string (may be empty).
final String serialNumber;
const UsbSerialDevice({
required this.deviceId,
required this.vendorId,
required this.productId,
required this.deviceName,
required this.manufacturerName,
required this.serialNumber,
});
factory UsbSerialDevice.fromMap(Map<Object?, Object?> map) {
return UsbSerialDevice(
deviceId: (map['deviceId'] as int?) ?? 0,
vendorId: (map['vendorId'] as int?) ?? 0,
productId: (map['productId'] as int?) ?? 0,
deviceName: (map['deviceName'] as String?) ?? '',
manufacturerName: (map['manufacturerName'] as String?) ?? '',
serialNumber: (map['serialNumber'] as String?) ?? '',
);
}
Map<String, dynamic> toMap() => {
'deviceId': deviceId,
'vendorId': vendorId,
'productId': productId,
'deviceName': deviceName,
'manufacturerName': manufacturerName,
'serialNumber': serialNumber,
};
@override
String toString() =>
'UsbSerialDevice(id=$deviceId, vid=0x${vendorId.toRadixString(16).padLeft(4, '0')}, '
'pid=0x${productId.toRadixString(16).padLeft(4, '0')}, name="$deviceName")';
}
+41
View File
@@ -0,0 +1,41 @@
part of '../flutter_usb_serial.dart';
/// Parity modes for serial communication.
enum UsbSerialParity { none, odd, even, mark, space }
/// Stop bits for serial communication.
enum UsbSerialStopBits { one, onePointFive, two }
/// An open serial port handle returned by [FlutterUsbSerial.openPort].
///
/// Call [close] when done to release native resources.
class UsbSerialPort {
final int _portId;
UsbSerialPort._(this._portId, this._dataStream);
final Stream<Uint8List> _dataStream;
/// Stream of raw bytes received on this port.
Stream<Uint8List> get inputStream => _dataStream;
static const _methods = MethodChannel('flutter_usb_serial/methods');
/// Write [data] to the port.
///
/// Returns the number of bytes actually written, or throws on error.
Future<int> write(Uint8List data) async {
final written = await _methods.invokeMethod<int>('write', {
'portId': _portId,
'data': data,
});
return written ?? 0;
}
/// Close the port and release native resources.
Future<void> close() async {
await _methods.invokeMethod<void>('close', {'portId': _portId});
}
int get portId => _portId;
}
+24
View File
@@ -0,0 +1,24 @@
name: flutter_usb_serial
description: "Flutter plugin for USB serial communication on Android using usb-serial-for-android."
version: 0.1.0
environment:
sdk: ^3.9.2
flutter: '>=3.3.0'
dependencies:
flutter:
sdk: flutter
plugin_platform_interface: ^2.0.2
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^5.0.0
flutter:
plugin:
platforms:
android:
package: com.dalimaster.flutter_usb_serial
pluginClass: FlutterUsbSerialPlugin
@@ -0,0 +1,27 @@
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_usb_serial/flutter_usb_serial_method_channel.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
MethodChannelFlutterUsbSerial platform = MethodChannelFlutterUsbSerial();
const MethodChannel channel = MethodChannel('flutter_usb_serial');
setUp(() {
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(
channel,
(MethodCall methodCall) async {
return '42';
},
);
});
tearDown(() {
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(channel, null);
});
test('getPlatformVersion', () async {
expect(await platform.getPlatformVersion(), '42');
});
}
+29
View File
@@ -0,0 +1,29 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_usb_serial/flutter_usb_serial.dart';
import 'package:flutter_usb_serial/flutter_usb_serial_platform_interface.dart';
import 'package:flutter_usb_serial/flutter_usb_serial_method_channel.dart';
import 'package:plugin_platform_interface/plugin_platform_interface.dart';
class MockFlutterUsbSerialPlatform
with MockPlatformInterfaceMixin
implements FlutterUsbSerialPlatform {
@override
Future<String?> getPlatformVersion() => Future.value('42');
}
void main() {
final FlutterUsbSerialPlatform initialPlatform = FlutterUsbSerialPlatform.instance;
test('$MethodChannelFlutterUsbSerial is the default instance', () {
expect(initialPlatform, isInstanceOf<MethodChannelFlutterUsbSerial>());
});
test('getPlatformVersion', () async {
FlutterUsbSerial flutterUsbSerialPlugin = FlutterUsbSerial();
MockFlutterUsbSerialPlatform fakePlatform = MockFlutterUsbSerialPlatform();
FlutterUsbSerialPlatform.instance = fakePlatform;
expect(await flutterUsbSerialPlugin.getPlatformVersion(), '42');
});
}