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", safeDeviceString { device.manufacturerName })
obj.put("manufacturer", device.manufacturerName ?: "") obj.put("product", safeDeviceString { device.productName })
obj.put("product", device.productName ?: "") obj.put("serial", safeDeviceString { device.serialNumber })
obj.put("serial", device.serialNumber ?: "") obj.put("hasPermission", usbManager.hasPermission(device))
obj.put("hasPermission", usbManager.hasPermission(device)) serialDevices.put(obj)
serialDevices.add(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("Failed to open USB device") invoke.reject("USB serial driver not found: $deviceName")
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")
return return
} }
val fd = connection.fileDescriptor val connection = usbManager.openDevice(driver.device)
val result = JSObject() if (connection == null) {
result.put("fd", fd) invoke.reject("Failed to open USB device")
result.put("name", device.deviceName) return
result.put("vendorId", device.vendorId) }
result.put("productId", device.productId)
invoke.resolve(result) 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 { private fun findDriver(usbManager: UsbManager, deviceName: String): UsbSerialDriver? {
for (i in 0 until device.interfaceCount) { return UsbSerialProber.getDefaultProber()
val iface = device.getInterface(i) .findAllDrivers(usbManager)
val classId = iface.interfaceClass .firstOrNull { it.device.deviceName == deviceName || it.device.deviceName.equals(deviceName, ignoreCase = true) }
if (classId == 0x02 || classId == 0xFF) { }
if (iface.endpointCount >= 2) {
return 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( private fun copyRustToSerial() {
0x1A86, // CH340/CH341 val input = FileInputStream(bridgeFd)
0x10C4, // CP210x val buffer = ByteArray(4096)
0x0403, // FTDI
0x067B, // PL2303
0x2341, // Arduino
0x239A, // Adafruit
)
if (device.vendorId in knownVendors) {
return true
}
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()
}
}
}
} }
} }

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,22 +114,31 @@ 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> {
let ports = available_ports() #[cfg(target_os = "android")]
.map_err(|_| SerialError::ScanError)? {
.into_iter() Ok(Vec::new())
.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"))]
{
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] #[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());
}
} }
} }