From 360b57e3e26363a30160792876f015074dd91e05 Mon Sep 17 00:00:00 2001 From: lenn Date: Mon, 11 May 2026 22:30:45 +0800 Subject: [PATCH] Add Android USB serial bridge docs --- README.md | 18 + src-tauri/capabilities/default.json | 9 +- src-tauri/gen/android/app/proguard-rules.pro | 15 +- .../com/lenn/tauri_serial/MainActivity.kt | 4 +- .../com/lenn/tauri_serial/UsbSerialPlugin.kt | 323 +++++++++++++----- src-tauri/gen/android/build.gradle.kts | 2 +- src-tauri/permissions/usb-serial/default.toml | 66 ++++ src-tauri/src/commands/serial.rs | 36 +- src-tauri/src/lib.rs | 15 +- src-tauri/src/serial_core/raw_fd_stream.rs | 7 +- 10 files changed, 395 insertions(+), 100 deletions(-) create mode 100644 src-tauri/permissions/usb-serial/default.toml diff --git a/README.md b/README.md index b1cd17a..e2662b5 100644 --- a/README.md +++ b/README.md @@ -61,6 +61,24 @@ Release APK 默认使用 debug keystore 签名(`src-tauri/gen/android/app/je-s npm run check ``` +## v0.5.0 修改记录 + +### Android USB 串口接入 + +- **Tauri 插件注册**:Android 端通过 Rust builder 注册 `usb-serial` 插件,移除 `MainActivity` 中的手动加载逻辑 +- **USB 设备枚举**:使用 `usb-serial-for-android` 的 `UsbSerialProber` 识别串口设备,并返回设备名、厂商 ID、产品 ID、权限状态等信息 +- **USB 权限申请**:完善 Android USB 授权回调,支持按设备名、vendorId/productId 解析设备并处理授权后的打开流程 +- **串口数据桥接**:Kotlin 端打开 USB serial port 后通过 Unix socketpair 将 fd 交给 Rust,Rust 端继续复用 `serial_connect_fd` 数据采集链路 +- **资源释放**:关闭连接时同步释放桥接 fd、USB serial port 和 `UsbDeviceConnection`,避免重复打开后的资源残留 + +### Tauri 权限与构建 + +- 新增 `src-tauri/permissions/usb-serial/default.toml`,声明 Android USB serial 插件命令和前端所需本地命令权限 +- `default.json` 增加 USB serial 与本地命令权限,兼容 snake_case / camelCase 插件命令名 +- Android Gradle 仓库加入 JitPack,用于解析 USB serial 驱动依赖 +- ProGuard 增加 Tauri 插件注解、`UsbSerialPlugin` 和 `com.hoho.android.usbserial` 保留规则,避免 release 包混淆后插件命令失效 +- Android 构建下 `serial_enum` 返回空列表,并仅保留 fd 连接入口,避免桌面串口枚举依赖进入 Android 编译路径 + ## v0.4.0 修改记录 ### 移动端性能优化 diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json index 41b8b91..34c4709 100644 --- a/src-tauri/capabilities/default.json +++ b/src-tauri/capabilities/default.json @@ -11,6 +11,13 @@ "core:window:allow-set-size", "core:window:allow-start-dragging", "opener:default", - "process:default" + "process:default", + "allow-usb-serial-list", + "allow-usb-serial-open", + "allow-usb-serial-close", + "allow-usb-serial-list-camel", + "allow-usb-serial-open-camel", + "allow-usb-serial-close-camel", + "allow-local-commands" ] } diff --git a/src-tauri/gen/android/app/proguard-rules.pro b/src-tauri/gen/android/app/proguard-rules.pro index 481bb43..0650506 100644 --- a/src-tauri/gen/android/app/proguard-rules.pro +++ b/src-tauri/gen/android/app/proguard-rules.pro @@ -18,4 +18,17 @@ # If you keep the line number information, uncomment this to # hide the original source file name. -#-renamesourcefileattribute SourceFile \ No newline at end of file +#-renamesourcefileattribute SourceFile + +-keepattributes RuntimeVisibleAnnotations,RuntimeInvisibleAnnotations,*Annotation*,Signature,InnerClasses,EnclosingMethod + +-keep class app.tauri.annotation.** { *; } +-keep class app.tauri.plugin.** { *; } + +-keep class com.lenn.tauri_serial.MainActivity { *; } +-keep class com.lenn.tauri_serial.UsbSerialPlugin { *; } +-keepclassmembers class com.lenn.tauri_serial.UsbSerialPlugin { + public *; +} + +-keep class com.hoho.android.usbserial.** { *; } diff --git a/src-tauri/gen/android/app/src/main/java/com/lenn/tauri_serial/MainActivity.kt b/src-tauri/gen/android/app/src/main/java/com/lenn/tauri_serial/MainActivity.kt index 13be157..383eaaf 100644 --- a/src-tauri/gen/android/app/src/main/java/com/lenn/tauri_serial/MainActivity.kt +++ b/src-tauri/gen/android/app/src/main/java/com/lenn/tauri_serial/MainActivity.kt @@ -7,7 +7,5 @@ class MainActivity : TauriActivity() { override fun onCreate(savedInstanceState: Bundle?) { enableEdgeToEdge() super.onCreate(savedInstanceState) - val plugin = UsbSerialPlugin(this) - pluginManager.load(null, "usb-serial", plugin, "") } -} \ No newline at end of file +} diff --git a/src-tauri/gen/android/app/src/main/java/com/lenn/tauri_serial/UsbSerialPlugin.kt b/src-tauri/gen/android/app/src/main/java/com/lenn/tauri_serial/UsbSerialPlugin.kt index 07f8e14..53d343c 100644 --- a/src-tauri/gen/android/app/src/main/java/com/lenn/tauri_serial/UsbSerialPlugin.kt +++ b/src-tauri/gen/android/app/src/main/java/com/lenn/tauri_serial/UsbSerialPlugin.kt @@ -10,20 +10,36 @@ import android.hardware.usb.UsbDevice import android.hardware.usb.UsbDeviceConnection import android.hardware.usb.UsbManager import android.os.Build +import android.os.ParcelFileDescriptor +import android.system.Os +import android.system.OsConstants import app.tauri.annotation.Command import app.tauri.annotation.TauriPlugin +import app.tauri.plugin.Invoke import app.tauri.plugin.JSObject import app.tauri.plugin.Plugin -import app.tauri.plugin.Invoke +import com.hoho.android.usbserial.driver.UsbSerialDriver +import com.hoho.android.usbserial.driver.UsbSerialPort +import com.hoho.android.usbserial.driver.UsbSerialProber +import java.io.FileDescriptor +import java.io.FileInputStream +import java.io.FileOutputStream +import java.io.IOException +import java.util.concurrent.atomic.AtomicBoolean +import org.json.JSONArray @TauriPlugin class UsbSerialPlugin(private val activity: Activity) : Plugin(activity) { companion object { private const val ACTION_USB_PERMISSION = "com.lenn.tauri_serial.USB_PERMISSION" + private const val BAUD_RATE = 921600 + private const val READ_TIMEOUT_MS = 100 + private const val WRITE_TIMEOUT_MS = 100 } private var pendingConnectInvoke: Invoke? = null - private var pendingConnectDevice: UsbDevice? = null + private var pendingConnectDeviceName: String? = null + private var activeBridge: SerialBridge? = null private val usbPermissionReceiver = object : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { @@ -39,10 +55,10 @@ class UsbSerialPlugin(private val activity: Activity) : Plugin(activity) { val granted = intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false) val invoke = pendingConnectInvoke - val targetDevice = pendingConnectDevice + val targetDeviceName = pendingConnectDeviceName pendingConnectInvoke = null - pendingConnectDevice = null + pendingConnectDeviceName = null if (invoke == null || device == null) return @@ -51,8 +67,8 @@ class UsbSerialPlugin(private val activity: Activity) : Plugin(activity) { return } - if (targetDevice != null && device.deviceName == targetDevice.deviceName) { - openAndReturn(invoke, device) + if (targetDeviceName != null && device.deviceName == targetDeviceName) { + openAndReturn(invoke, device.deviceName) } else { invoke.reject("USB device mismatch") } @@ -65,7 +81,9 @@ class UsbSerialPlugin(private val activity: Activity) : Plugin(activity) { val filter = IntentFilter(ACTION_USB_PERMISSION) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { activity.applicationContext.registerReceiver( - usbPermissionReceiver, filter, Context.RECEIVER_NOT_EXPORTED + usbPermissionReceiver, + filter, + Context.RECEIVER_NOT_EXPORTED ) } else { activity.applicationContext.registerReceiver(usbPermissionReceiver, filter) @@ -74,45 +92,66 @@ class UsbSerialPlugin(private val activity: Activity) : Plugin(activity) { override fun onDestroy() { super.onDestroy() + activeBridge?.close() + activeBridge = null try { activity.applicationContext.unregisterReceiver(usbPermissionReceiver) - } catch (_: Exception) {} + } catch (_: Exception) { + } } @Command fun usb_serial_list(invoke: Invoke) { + listDevices(invoke) + } + + @Command + fun usbSerialList(invoke: Invoke) { + listDevices(invoke) + } + + private fun listDevices(invoke: Invoke) { val usbManager = activity.getSystemService(Context.USB_SERVICE) as? UsbManager if (usbManager == null) { invoke.reject("USB service not available") return } - val devices = usbManager.deviceList val result = JSObject() + val serialDevices = JSONArray() - val serialDevices = mutableListOf() - for ((_, device) in devices) { - if (isUsbSerialDevice(device)) { - val obj = JSObject() - obj.put("name", device.deviceName) - obj.put("vendorId", device.vendorId) - obj.put("productId", device.productId) - obj.put("manufacturer", device.manufacturerName ?: "") - obj.put("product", device.productName ?: "") - obj.put("serial", device.serialNumber ?: "") - obj.put("hasPermission", usbManager.hasPermission(device)) - serialDevices.add(obj) - } + for (driver in UsbSerialProber.getDefaultProber().findAllDrivers(usbManager)) { + val device = driver.device + val obj = JSObject() + obj.put("name", device.deviceName) + obj.put("vendorId", device.vendorId) + obj.put("productId", device.productId) + obj.put("manufacturer", safeDeviceString { device.manufacturerName }) + obj.put("product", safeDeviceString { device.productName }) + obj.put("serial", safeDeviceString { device.serialNumber }) + obj.put("hasPermission", usbManager.hasPermission(device)) + serialDevices.put(obj) } - result.put("devices", serialDevices.toTypedArray()) + result.put("devices", serialDevices) invoke.resolve(result) } @Command fun usb_serial_open(invoke: Invoke) { + openDevice(invoke) + } + + @Command + fun usbSerialOpen(invoke: Invoke) { + openDevice(invoke) + } + + private fun openDevice(invoke: Invoke) { val args = invoke.parseArgs(JSObject::class.java) val deviceName = args.optString("name", "") + val vendorId = if (args.has("vendorId")) args.optInt("vendorId") else null + val productId = if (args.has("productId")) args.optInt("productId") else null val usbManager = activity.getSystemService(Context.USB_SERVICE) as? UsbManager if (usbManager == null) { @@ -120,98 +159,230 @@ class UsbSerialPlugin(private val activity: Activity) : Plugin(activity) { return } - val device = usbManager.deviceList[deviceName] + val device = resolveDevice(usbManager, deviceName, vendorId, productId) if (device == null) { - invoke.reject("USB device not found: $deviceName") + val available = usbManager.deviceList.values.joinToString(", ") { it.deviceName } + invoke.reject("USB device not found: $deviceName; available: $available") return } if (!usbManager.hasPermission(device)) { synchronized(this) { pendingConnectInvoke = invoke - pendingConnectDevice = device + pendingConnectDeviceName = device.deviceName } val flags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - PendingIntent.FLAG_MUTABLE + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE } else { - 0 + PendingIntent.FLAG_UPDATE_CURRENT } + val permissionRequest = Intent(ACTION_USB_PERMISSION).setPackage(activity.packageName) val permissionIntent = PendingIntent.getBroadcast( - activity, 0, Intent(ACTION_USB_PERMISSION), flags + activity, + 0, + permissionRequest, + flags ) usbManager.requestPermission(device, permissionIntent) return } - openAndReturn(invoke, device) + openAndReturn(invoke, device.deviceName) } @Command fun usb_serial_close(invoke: Invoke) { + closeBridge() invoke.resolve(JSObject()) } - private fun openAndReturn(invoke: Invoke, device: UsbDevice) { + @Command + fun usbSerialClose(invoke: Invoke) { + closeBridge() + invoke.resolve(JSObject()) + } + + private fun closeBridge() { + activeBridge?.close() + activeBridge = null + } + + private fun openAndReturn(invoke: Invoke, deviceName: String) { val usbManager = activity.getSystemService(Context.USB_SERVICE) as? UsbManager if (usbManager == null) { invoke.reject("USB service not available") return } - val connection: UsbDeviceConnection = usbManager.openDevice(device) - ?: run { - invoke.reject("Failed to open USB device") - return - } - - var claimedInterface = false - for (i in 0 until device.interfaceCount) { - val iface = device.getInterface(i) - if (iface.endpointCount >= 2) { - connection.claimInterface(iface, true) - claimedInterface = true - break - } - } - - if (!claimedInterface) { - invoke.reject("No usable USB interface found") + val driver = findDriver(usbManager, deviceName) + if (driver == null) { + invoke.reject("USB serial driver not found: $deviceName") return } - val fd = connection.fileDescriptor - val result = JSObject() - result.put("fd", fd) - result.put("name", device.deviceName) - result.put("vendorId", device.vendorId) - result.put("productId", device.productId) - invoke.resolve(result) + val connection = usbManager.openDevice(driver.device) + if (connection == null) { + invoke.reject("Failed to open USB device") + return + } + + val port = driver.ports.firstOrNull() + if (port == null) { + connection.close() + invoke.reject("No serial port found on USB device") + return + } + + try { + port.open(connection) + port.setParameters( + BAUD_RATE, + UsbSerialPort.DATABITS_8, + UsbSerialPort.STOPBITS_1, + UsbSerialPort.PARITY_NONE + ) + + val rustSide = FileDescriptor() + val bridgeSide = FileDescriptor() + Os.socketpair(OsConstants.AF_UNIX, OsConstants.SOCK_STREAM, 0, rustSide, bridgeSide) + val rustFd = ParcelFileDescriptor.dup(rustSide).detachFd() + Os.close(rustSide) + + activeBridge?.close() + activeBridge = SerialBridge(bridgeSide, port, connection).also { it.start() } + + val result = JSObject() + result.put("fd", rustFd) + result.put("name", driver.device.deviceName) + result.put("vendorId", driver.device.vendorId) + result.put("productId", driver.device.productId) + invoke.resolve(result) + } catch (error: Exception) { + try { + port.close() + } catch (_: Exception) { + } + connection.close() + invoke.reject(error.message ?: "Failed to open USB serial port") + } } - private fun isUsbSerialDevice(device: UsbDevice): Boolean { - for (i in 0 until device.interfaceCount) { - val iface = device.getInterface(i) - val classId = iface.interfaceClass - if (classId == 0x02 || classId == 0xFF) { - if (iface.endpointCount >= 2) { - return true + private fun findDriver(usbManager: UsbManager, deviceName: String): UsbSerialDriver? { + return UsbSerialProber.getDefaultProber() + .findAllDrivers(usbManager) + .firstOrNull { it.device.deviceName == deviceName || it.device.deviceName.equals(deviceName, ignoreCase = true) } + } + + private fun resolveDevice( + usbManager: UsbManager, + deviceName: String, + vendorId: Int?, + productId: Int? + ): UsbDevice? { + usbManager.deviceList[deviceName]?.let { return it } + + val devices = usbManager.deviceList.values.toList() + devices.firstOrNull { it.deviceName.equals(deviceName, ignoreCase = true) }?.let { return it } + + if (vendorId != null && productId != null) { + devices.firstOrNull { it.vendorId == vendorId && it.productId == productId }?.let { return it } + } + + val drivers = UsbSerialProber.getDefaultProber().findAllDrivers(usbManager) + drivers.firstOrNull { + it.device.deviceName == deviceName || it.device.deviceName.equals(deviceName, ignoreCase = true) + }?.device?.let { return it } + + if (drivers.size == 1) { + return drivers.first().device + } + + if (devices.size == 1) { + return devices.first() + } + + return null + } + + private fun safeDeviceString(read: () -> String?): String { + return try { + read() ?: "" + } catch (_: SecurityException) { + "" + } + } + + private class SerialBridge( + private val bridgeFd: FileDescriptor, + private val port: UsbSerialPort, + private val connection: UsbDeviceConnection + ) { + private val running = AtomicBoolean(false) + private lateinit var serialToRustThread: Thread + private lateinit var rustToSerialThread: Thread + + fun start() { + running.set(true) + serialToRustThread = Thread(::copySerialToRust, "JE-Skin-usb-serial-rx") + rustToSerialThread = Thread(::copyRustToSerial, "JE-Skin-usb-serial-tx") + serialToRustThread.start() + rustToSerialThread.start() + } + + fun close() { + if (!running.getAndSet(false)) return + + try { + Os.close(bridgeFd) + } catch (_: Exception) { + } + try { + port.close() + } catch (_: Exception) { + } + connection.close() + } + + private fun copySerialToRust() { + val output = FileOutputStream(bridgeFd) + val buffer = ByteArray(4096) + + while (running.get()) { + try { + val count = port.read(buffer, READ_TIMEOUT_MS) + if (count > 0) { + output.write(buffer, 0, count) + output.flush() + } + } catch (_: IOException) { + close() + } catch (_: Exception) { + close() } } } - val knownVendors = setOf( - 0x1A86, // CH340/CH341 - 0x10C4, // CP210x - 0x0403, // FTDI - 0x067B, // PL2303 - 0x2341, // Arduino - 0x239A, // Adafruit - ) - if (device.vendorId in knownVendors) { - return true - } + private fun copyRustToSerial() { + val input = FileInputStream(bridgeFd) + val buffer = ByteArray(4096) - return false + while (running.get()) { + try { + val count = input.read(buffer) + if (count < 0) { + close() + return + } + if (count > 0) { + port.write(buffer.copyOf(count), WRITE_TIMEOUT_MS) + } + } catch (_: IOException) { + close() + } catch (_: Exception) { + close() + } + } + } } -} \ No newline at end of file +} diff --git a/src-tauri/gen/android/build.gradle.kts b/src-tauri/gen/android/build.gradle.kts index 607240b..c2af408 100644 --- a/src-tauri/gen/android/build.gradle.kts +++ b/src-tauri/gen/android/build.gradle.kts @@ -13,10 +13,10 @@ allprojects { repositories { google() mavenCentral() + maven(url = "https://jitpack.io") } } tasks.register("clean").configure { delete("build") } - diff --git a/src-tauri/permissions/usb-serial/default.toml b/src-tauri/permissions/usb-serial/default.toml new file mode 100644 index 0000000..ae22a8c --- /dev/null +++ b/src-tauri/permissions/usb-serial/default.toml @@ -0,0 +1,66 @@ +[default] +description = "Allows Android USB serial plugin commands." +permissions = [ + "allow-usb-serial-list", + "allow-usb-serial-open", + "allow-usb-serial-close", + "allow-usb-serial-list-camel", + "allow-usb-serial-open-camel", + "allow-usb-serial-close-camel", + "allow-local-commands" +] + +[[permission]] +identifier = "allow-usb-serial-list" +description = "Allows listing Android USB serial devices." +commands.allow = ["plugin:usb-serial|usb_serial_list"] + +[[permission]] +identifier = "allow-usb-serial-open" +description = "Allows opening Android USB serial devices." +commands.allow = ["plugin:usb-serial|usb_serial_open"] + +[[permission]] +identifier = "allow-usb-serial-close" +description = "Allows closing Android USB serial devices." +commands.allow = ["plugin:usb-serial|usb_serial_close"] + +[[permission]] +identifier = "allow-usb-serial-list-camel" +description = "Allows listing Android USB serial devices via camelCase command." +commands.allow = ["plugin:usb-serial|usbSerialList"] + +[[permission]] +identifier = "allow-usb-serial-open-camel" +description = "Allows opening Android USB serial devices via camelCase command." +commands.allow = ["plugin:usb-serial|usbSerialOpen"] + +[[permission]] +identifier = "allow-usb-serial-close-camel" +description = "Allows closing Android USB serial devices via camelCase command." +commands.allow = ["plugin:usb-serial|usbSerialClose"] + +[[permission]] +identifier = "allow-local-commands" +description = "Allows application commands used by the Android frontend." +commands.allow = [ + "file_explorer_list", + "serial_enum", + "serial_connect", + "serial_connect_fd", + "serial_disconnect", + "serial_export_csv", + "serial_has_record_data", + "serial_export_csv_to_path", + "serial_import_csv", + "serial_import_csv_from_path", + "win_minimize", + "win_toggle_maximize", + "win_close", + "devkit_status", + "devkit_start", + "devkit_stop", + "devkit_get_config", + "devkit_set_config", + "devkit_process_export" +] diff --git a/src-tauri/src/commands/serial.rs b/src-tauri/src/commands/serial.rs index 736e0cb..b01c72b 100644 --- a/src-tauri/src/commands/serial.rs +++ b/src-tauri/src/commands/serial.rs @@ -13,6 +13,7 @@ use std::path::{Path, PathBuf}; use std::sync::{Arc, Mutex}; use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; use tauri::{async_runtime::JoinHandle, AppHandle, Manager, State}; +#[cfg(not(target_os = "android"))] use tokio_serial::{available_ports, SerialPortBuilderExt}; use tokio_util::sync::CancellationToken; @@ -113,22 +114,31 @@ pub async fn shutdown_active_session( #[tauri::command] pub fn serial_enum() -> Result, SerialError> { - let ports = available_ports() - .map_err(|_| SerialError::ScanError)? - .into_iter() - .filter_map(|p| { - let name = p.port_name; - #[cfg(unix)] - if !name.contains("USB") { - return None; - } - Some(name) - }) - .collect(); + #[cfg(target_os = "android")] + { + Ok(Vec::new()) + } - Ok(ports) + #[cfg(not(target_os = "android"))] + { + let ports = available_ports() + .map_err(|_| SerialError::ScanError)? + .into_iter() + .filter_map(|p| { + let name = p.port_name; + #[cfg(unix)] + if !name.contains("USB") { + return None; + } + Some(name) + }) + .collect(); + + Ok(ports) + } } +#[cfg(not(target_os = "android"))] #[tauri::command] pub async fn serial_connect( app: AppHandle, diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 386d766..5323123 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -10,6 +10,16 @@ use commands::serial::SerialConnectionState; #[cfg(feature = "devkit")] use tauri::Manager; +#[cfg(target_os = "android")] +fn usb_serial_plugin() -> tauri::plugin::TauriPlugin { + tauri::plugin::Builder::new("usb-serial") + .setup(|_app, api| { + api.register_android_plugin("com.lenn.tauri_serial", "UsbSerialPlugin")?; + Ok(()) + }) + .build() +} + #[cfg(feature = "devkit")] fn start_server_exe(exe_path: &std::path::Path) { let mut command = std::process::Command::new(exe_path); @@ -66,6 +76,9 @@ pub fn run() { .manage(SerialConnectionState::default()) .plugin(tauri_plugin_opener::init()); + #[cfg(target_os = "android")] + let builder = builder.plugin(usb_serial_plugin()); + #[cfg(not(any(target_os = "android", target_os = "ios")))] let builder = builder.plugin(tauri_plugin_updater::Builder::new().build()); @@ -177,7 +190,6 @@ pub fn run() { let builder = builder.invoke_handler(tauri::generate_handler![ commands::file_explorer::file_explorer_list, commands::serial::serial_enum, - commands::serial::serial_connect, commands::serial::serial_connect_fd, commands::serial::serial_disconnect, commands::serial::serial_export_csv, @@ -200,7 +212,6 @@ pub fn run() { let builder = builder.invoke_handler(tauri::generate_handler![ commands::file_explorer::file_explorer_list, commands::serial::serial_enum, - commands::serial::serial_connect, commands::serial::serial_connect_fd, commands::serial::serial_disconnect, commands::serial::serial_export_csv, diff --git a/src-tauri/src/serial_core/raw_fd_stream.rs b/src-tauri/src/serial_core/raw_fd_stream.rs index ff902d9..fa1b2d9 100644 --- a/src-tauri/src/serial_core/raw_fd_stream.rs +++ b/src-tauri/src/serial_core/raw_fd_stream.rs @@ -34,8 +34,9 @@ impl RawFdStream { impl Drop for RawFdStream { fn drop(&mut self) { - // We don't close the fd here - it's managed by the UsbDeviceConnection in Kotlin. - // The Kotlin side is responsible for closing. + unsafe { + libc::close(*self.inner.get_ref()); + } } } @@ -123,4 +124,4 @@ impl AsyncWrite for RawFdStream { ) -> std::task::Poll> { std::task::Poll::Ready(Ok(())) } -} \ No newline at end of file +}