SerialPlugin → UsbFdBridge (Kotlin) → openDeviceFd / enumerateJson
UsbNative (JNI) ↔ fd_bridge.rs ↔ driver_host.rs ↔ android-usb-serial (nusb)
RX: Rust reader thread → PortRxHub poll-loop (same as desktop)
Kotlin keeps UsbDeviceConnection and the raw fd. Rust duplicates the fd (dup), opens nusb::Device::from_fd, claims interfaces, and runs vendor drivers from the android-usb-serial crate. There is no vendored usb-serial-for-android Java tree in this repo.
- Declare USB host in the app manifest (
uses-feature android.hardware.usb.hostoptional). - Ship a
device_filter.xmlaligned withdevice_filter.xml/ProbeTable::default_table(). - Request runtime USB permission before open (the plugin uses
PendingIntent.FLAG_MUTABLE+setPackage()).
Gradle 8.9 / AGP 8.5 only run on JDK 11–21 (JDK 22+ dies with
Unsupported class file major version). Prefer 17. The helper picks a
compatible install even if your shell JAVA_HOME is 27:
export JAVA_HOME="$(./scripts/java-home-for-gradle.sh)"
cd android && ./gradlew lintDebug testLint is the Android-side analogue of clippy (API misuse, permissions, …) and runs in CI on Ubuntu together with Robolectric.
Stable sources: examples/serialport-test/android-integration/.
Uses FakeTransport via android-test-harness (no Kotlin USB fakes). This is the supported way to exercise the JNI/fd path on an emulator: macOS cannot pass a host USB serial adapter into the AVD, so real Quectel/WCH sticks stay on desktop (SERIAL_SMOKE_PORT) or a physical phone with USB host.
# Emulator (arm64 AVD) or device — FakeTransport, no real USB:
ANDROID_INTEGRATION_NDK_ARCH=arm64-v8a ./scripts/android-integration-ci.sh all
# Desktop Mac smoke against a real AT modem (e.g. Quectel on /dev/cu.wchusbserial*):
SERIAL_SMOKE_PORT=/dev/cu.wchusbserial210 cargo test --lib smoke_real_serial -- --ignored --nocaptureDebug harness JNI (test_harness.rs): testHarnessReset, testOpenFakePort, testFakeInjectRx, testFakeTakeTx, testFakeInjectError, testFakeEnableAtModem, testHubBufferedLen, testInvokeWrite, testRegistryHasPort, testEnumerateJsonFromWorkerThread.
Optional AT modem sim on FakeTransport (enable_at_modem / testFakeEnableAtModem): scripted Quectel-like replies (AT→OK, AT+CSQ with +CREG URC, unknown AT*→ERROR).
| Approach | Why skip here |
|---|---|
AVD -usb-passthrough on macOS |
Emulator accepts the flag but attaches nothing (Command line USB devices: ()); host keeps /dev/cu.* |
Robolectric ShadowUsbManager |
Useful for Kotlin-only USB glue; drivers run in Rust via fd + FakeTransport already |
| Genymotion USB | Not on Apple Silicon |
- Linux CI + Bliss OS / QEMU + USB: when you need real
UsbManagerin automation — see Depau’s GH Actions + KVM write-up. QEMUusb-serialor-device usb-host. - Mac → Linux USB/IP: export WCH with pyusbip / usbipd-mac, attach in Waydroid/Bliss on Linux. Heavy; phone OTG is usually simpler.
| File | Role |
|---|---|
SerialPlugin |
Tauri plugin load → UsbFdBridge |
UsbFdBridge |
enumerate, permission, fd open/close, attach/detach |
UsbNative |
JNI for enumerate + fd lifecycle |
MobileBridge |
JNI callbacks (onUsbError, onDeviceDetached, port list change, …) |
Rust drivers are verified against frozen JSON fixtures (crates/android-usb-serial/tests/fixtures/).
cargo run -p android-usb-serial --features fake-transport --bin golden_record
cargo test -p android-usb-serial --features fake-transport --test golden_paritySee docs/golden-recorder-archive/README.md for the retired JVM recorder workflow.
Logcat: adb logcat -s UsbFdBridge SerialPlugin RustStdoutStderr
exchange/atonphysical#dlci=N: Routed through the Rust CMUX session (same as desktop).cancel_exchange: Sets the virtual cancel flag, fails the active DLCI waiter, and clears the virtual TX queue.rx_prepare: drain: Uses the shared RX hub drain before write (idle bytes may still be replayed viatake_idle_bytes).