Add Android USB serial bridge docs

This commit is contained in:
lenn
2026-05-11 22:30:45 +08:00
parent c5f4f854bf
commit 360b57e3e2
10 changed files with 395 additions and 100 deletions

View File

@@ -61,6 +61,24 @@ Release APK 默认使用 debug keystore 签名(`src-tauri/gen/android/app/je-s
npm run check 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 交给 RustRust 端继续复用 `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 修改记录 ## v0.4.0 修改记录
### 移动端性能优化 ### 移动端性能优化

View File

@@ -11,6 +11,13 @@
"core:window:allow-set-size", "core:window:allow-set-size",
"core:window:allow-start-dragging", "core:window:allow-start-dragging",
"opener:default", "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"
] ]
} }

View File

@@ -19,3 +19,16 @@
# If you keep the line number information, uncomment this to # If you keep the line number information, uncomment this to
# hide the original source file name. # hide the original source file name.
#-renamesourcefileattribute SourceFile #-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.** { *; }

View File

@@ -7,7 +7,5 @@ class MainActivity : TauriActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge() enableEdgeToEdge()
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
val plugin = UsbSerialPlugin(this)
pluginManager.load(null, "usb-serial", plugin, "")
} }
} }

View File

@@ -10,20 +10,36 @@ import android.hardware.usb.UsbDevice
import android.hardware.usb.UsbDeviceConnection import android.hardware.usb.UsbDeviceConnection
import android.hardware.usb.UsbManager import android.hardware.usb.UsbManager
import android.os.Build 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.Command
import app.tauri.annotation.TauriPlugin import app.tauri.annotation.TauriPlugin
import app.tauri.plugin.Invoke
import app.tauri.plugin.JSObject import app.tauri.plugin.JSObject
import app.tauri.plugin.Plugin 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 @TauriPlugin
class UsbSerialPlugin(private val activity: Activity) : Plugin(activity) { class UsbSerialPlugin(private val activity: Activity) : Plugin(activity) {
companion object { companion object {
private const val ACTION_USB_PERMISSION = "com.lenn.tauri_serial.USB_PERMISSION" 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 pendingConnectInvoke: Invoke? = null
private var pendingConnectDevice: UsbDevice? = null private var pendingConnectDeviceName: String? = null
private var activeBridge: SerialBridge? = null
private val usbPermissionReceiver = object : BroadcastReceiver() { private val usbPermissionReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) { 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 granted = intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false)
val invoke = pendingConnectInvoke val invoke = pendingConnectInvoke
val targetDevice = pendingConnectDevice val targetDeviceName = pendingConnectDeviceName
pendingConnectInvoke = null pendingConnectInvoke = null
pendingConnectDevice = null pendingConnectDeviceName = null
if (invoke == null || device == null) return if (invoke == null || device == null) return
@@ -51,8 +67,8 @@ class UsbSerialPlugin(private val activity: Activity) : Plugin(activity) {
return return
} }
if (targetDevice != null && device.deviceName == targetDevice.deviceName) { if (targetDeviceName != null && device.deviceName == targetDeviceName) {
openAndReturn(invoke, device) openAndReturn(invoke, device.deviceName)
} else { } else {
invoke.reject("USB device mismatch") invoke.reject("USB device mismatch")
} }
@@ -65,7 +81,9 @@ class UsbSerialPlugin(private val activity: Activity) : Plugin(activity) {
val filter = IntentFilter(ACTION_USB_PERMISSION) val filter = IntentFilter(ACTION_USB_PERMISSION)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
activity.applicationContext.registerReceiver( activity.applicationContext.registerReceiver(
usbPermissionReceiver, filter, Context.RECEIVER_NOT_EXPORTED usbPermissionReceiver,
filter,
Context.RECEIVER_NOT_EXPORTED
) )
} else { } else {
activity.applicationContext.registerReceiver(usbPermissionReceiver, filter) activity.applicationContext.registerReceiver(usbPermissionReceiver, filter)
@@ -74,45 +92,66 @@ class UsbSerialPlugin(private val activity: Activity) : Plugin(activity) {
override fun onDestroy() { override fun onDestroy() {
super.onDestroy() super.onDestroy()
activeBridge?.close()
activeBridge = null
try { try {
activity.applicationContext.unregisterReceiver(usbPermissionReceiver) activity.applicationContext.unregisterReceiver(usbPermissionReceiver)
} catch (_: Exception) {} } catch (_: Exception) {
}
} }
@Command @Command
fun usb_serial_list(invoke: Invoke) { 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 val usbManager = activity.getSystemService(Context.USB_SERVICE) as? UsbManager
if (usbManager == null) { if (usbManager == null) {
invoke.reject("USB service not available") invoke.reject("USB service not available")
return return
} }
val devices = usbManager.deviceList
val result = JSObject() val result = JSObject()
val serialDevices = JSONArray()
val serialDevices = mutableListOf<JSObject>() for (driver in UsbSerialProber.getDefaultProber().findAllDrivers(usbManager)) {
for ((_, device) in devices) { val device = driver.device
if (isUsbSerialDevice(device)) {
val obj = JSObject() val obj = JSObject()
obj.put("name", device.deviceName) obj.put("name", device.deviceName)
obj.put("vendorId", device.vendorId) obj.put("vendorId", device.vendorId)
obj.put("productId", device.productId) obj.put("productId", device.productId)
obj.put("manufacturer", device.manufacturerName ?: "") obj.put("manufacturer", safeDeviceString { device.manufacturerName })
obj.put("product", device.productName ?: "") obj.put("product", safeDeviceString { device.productName })
obj.put("serial", device.serialNumber ?: "") obj.put("serial", safeDeviceString { device.serialNumber })
obj.put("hasPermission", usbManager.hasPermission(device)) obj.put("hasPermission", usbManager.hasPermission(device))
serialDevices.add(obj) serialDevices.put(obj)
}
} }
result.put("devices", serialDevices.toTypedArray()) result.put("devices", serialDevices)
invoke.resolve(result) invoke.resolve(result)
} }
@Command @Command
fun usb_serial_open(invoke: Invoke) { 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 args = invoke.parseArgs(JSObject::class.java)
val deviceName = args.optString("name", "") 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 val usbManager = activity.getSystemService(Context.USB_SERVICE) as? UsbManager
if (usbManager == null) { if (usbManager == null) {
@@ -120,98 +159,230 @@ class UsbSerialPlugin(private val activity: Activity) : Plugin(activity) {
return return
} }
val device = usbManager.deviceList[deviceName] val device = resolveDevice(usbManager, deviceName, vendorId, productId)
if (device == null) { 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 return
} }
if (!usbManager.hasPermission(device)) { if (!usbManager.hasPermission(device)) {
synchronized(this) { synchronized(this) {
pendingConnectInvoke = invoke pendingConnectInvoke = invoke
pendingConnectDevice = device pendingConnectDeviceName = device.deviceName
} }
val flags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { val flags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
PendingIntent.FLAG_MUTABLE PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE
} else { } else {
0 PendingIntent.FLAG_UPDATE_CURRENT
} }
val permissionRequest = Intent(ACTION_USB_PERMISSION).setPackage(activity.packageName)
val permissionIntent = PendingIntent.getBroadcast( val permissionIntent = PendingIntent.getBroadcast(
activity, 0, Intent(ACTION_USB_PERMISSION), flags activity,
0,
permissionRequest,
flags
) )
usbManager.requestPermission(device, permissionIntent) usbManager.requestPermission(device, permissionIntent)
return return
} }
openAndReturn(invoke, device) openAndReturn(invoke, device.deviceName)
} }
@Command @Command
fun usb_serial_close(invoke: Invoke) { fun usb_serial_close(invoke: Invoke) {
closeBridge()
invoke.resolve(JSObject()) 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 val usbManager = activity.getSystemService(Context.USB_SERVICE) as? UsbManager
if (usbManager == null) { if (usbManager == null) {
invoke.reject("USB service not available") invoke.reject("USB service not available")
return return
} }
val connection: UsbDeviceConnection = usbManager.openDevice(device) val driver = findDriver(usbManager, deviceName)
?: run { if (driver == null) {
invoke.reject("USB serial driver not found: $deviceName")
return
}
val connection = usbManager.openDevice(driver.device)
if (connection == null) {
invoke.reject("Failed to open USB device") invoke.reject("Failed to open USB device")
return return
} }
var claimedInterface = false val port = driver.ports.firstOrNull()
for (i in 0 until device.interfaceCount) { if (port == null) {
val iface = device.getInterface(i) connection.close()
if (iface.endpointCount >= 2) { invoke.reject("No serial port found on USB device")
connection.claimInterface(iface, true)
claimedInterface = true
break
}
}
if (!claimedInterface) {
invoke.reject("No usable USB interface found")
return return
} }
val fd = connection.fileDescriptor try {
val result = JSObject() port.open(connection)
result.put("fd", fd) port.setParameters(
result.put("name", device.deviceName) BAUD_RATE,
result.put("vendorId", device.vendorId) UsbSerialPort.DATABITS_8,
result.put("productId", device.productId) UsbSerialPort.STOPBITS_1,
invoke.resolve(result) UsbSerialPort.PARITY_NONE
}
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
}
}
}
val knownVendors = setOf(
0x1A86, // CH340/CH341
0x10C4, // CP210x
0x0403, // FTDI
0x067B, // PL2303
0x2341, // Arduino
0x239A, // Adafruit
) )
if (device.vendorId in knownVendors) {
return true 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")
}
} }
return false 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()
}
}
}
private fun copyRustToSerial() {
val input = FileInputStream(bridgeFd)
val buffer = ByteArray(4096)
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()
}
}
}
} }
} }

View File

@@ -13,10 +13,10 @@ allprojects {
repositories { repositories {
google() google()
mavenCentral() mavenCentral()
maven(url = "https://jitpack.io")
} }
} }
tasks.register("clean").configure { tasks.register("clean").configure {
delete("build") delete("build")
} }

View File

@@ -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"
]

View File

@@ -13,6 +13,7 @@ use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
use tauri::{async_runtime::JoinHandle, AppHandle, Manager, State}; use tauri::{async_runtime::JoinHandle, AppHandle, Manager, State};
#[cfg(not(target_os = "android"))]
use tokio_serial::{available_ports, SerialPortBuilderExt}; use tokio_serial::{available_ports, SerialPortBuilderExt};
use tokio_util::sync::CancellationToken; use tokio_util::sync::CancellationToken;
@@ -113,6 +114,13 @@ pub async fn shutdown_active_session(
#[tauri::command] #[tauri::command]
pub fn serial_enum() -> Result<Vec<String>, SerialError> { pub fn serial_enum() -> Result<Vec<String>, SerialError> {
#[cfg(target_os = "android")]
{
Ok(Vec::new())
}
#[cfg(not(target_os = "android"))]
{
let ports = available_ports() let ports = available_ports()
.map_err(|_| SerialError::ScanError)? .map_err(|_| SerialError::ScanError)?
.into_iter() .into_iter()
@@ -127,8 +135,10 @@ pub fn serial_enum() -> Result<Vec<String>, SerialError> {
.collect(); .collect();
Ok(ports) Ok(ports)
}
} }
#[cfg(not(target_os = "android"))]
#[tauri::command] #[tauri::command]
pub async fn serial_connect( pub async fn serial_connect(
app: AppHandle, app: AppHandle,

View File

@@ -10,6 +10,16 @@ use commands::serial::SerialConnectionState;
#[cfg(feature = "devkit")] #[cfg(feature = "devkit")]
use tauri::Manager; use tauri::Manager;
#[cfg(target_os = "android")]
fn usb_serial_plugin<R: tauri::Runtime>() -> tauri::plugin::TauriPlugin<R> {
tauri::plugin::Builder::new("usb-serial")
.setup(|_app, api| {
api.register_android_plugin("com.lenn.tauri_serial", "UsbSerialPlugin")?;
Ok(())
})
.build()
}
#[cfg(feature = "devkit")] #[cfg(feature = "devkit")]
fn start_server_exe(exe_path: &std::path::Path) { fn start_server_exe(exe_path: &std::path::Path) {
let mut command = std::process::Command::new(exe_path); let mut command = std::process::Command::new(exe_path);
@@ -66,6 +76,9 @@ pub fn run() {
.manage(SerialConnectionState::default()) .manage(SerialConnectionState::default())
.plugin(tauri_plugin_opener::init()); .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")))] #[cfg(not(any(target_os = "android", target_os = "ios")))]
let builder = builder.plugin(tauri_plugin_updater::Builder::new().build()); 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![ let builder = builder.invoke_handler(tauri::generate_handler![
commands::file_explorer::file_explorer_list, commands::file_explorer::file_explorer_list,
commands::serial::serial_enum, commands::serial::serial_enum,
commands::serial::serial_connect,
commands::serial::serial_connect_fd, commands::serial::serial_connect_fd,
commands::serial::serial_disconnect, commands::serial::serial_disconnect,
commands::serial::serial_export_csv, commands::serial::serial_export_csv,
@@ -200,7 +212,6 @@ pub fn run() {
let builder = builder.invoke_handler(tauri::generate_handler![ let builder = builder.invoke_handler(tauri::generate_handler![
commands::file_explorer::file_explorer_list, commands::file_explorer::file_explorer_list,
commands::serial::serial_enum, commands::serial::serial_enum,
commands::serial::serial_connect,
commands::serial::serial_connect_fd, commands::serial::serial_connect_fd,
commands::serial::serial_disconnect, commands::serial::serial_disconnect,
commands::serial::serial_export_csv, commands::serial::serial_export_csv,

View File

@@ -34,8 +34,9 @@ impl RawFdStream {
impl Drop for RawFdStream { impl Drop for RawFdStream {
fn drop(&mut self) { fn drop(&mut self) {
// We don't close the fd here - it's managed by the UsbDeviceConnection in Kotlin. unsafe {
// The Kotlin side is responsible for closing. libc::close(*self.inner.get_ref());
}
} }
} }