Skip to content

Latest commit

 

History

History
90 lines (61 loc) · 4.66 KB

README.md

File metadata and controls

90 lines (61 loc) · 4.66 KB

Android serial plugin

Architecture (fd + Rust drivers)

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.

Consumer app requirements

  1. Declare USB host in the app manifest (uses-feature android.hardware.usb.host optional).
  2. Ship a device_filter.xml aligned with device_filter.xml / ProbeTable::default_table().
  3. Request runtime USB permission before open (the plugin uses PendingIntent.FLAG_MUTABLE + setPackage()).

JVM unit tests + Android Lint

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 test

Lint is the Android-side analogue of clippy (API misuse, permissions, …) and runs in CI on Ubuntu together with Robolectric.

Kotlin ↔ Rust JNI integration tests (instrumented)

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 --nocapture

Debug 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).

What we do not rely on

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

Heavier e2e options (not in default CI)

  • Linux CI + Bliss OS / QEMU + USB: when you need real UsbManager in automation — see Depau’s GH Actions + KVM write-up. QEMU usb-serial or -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.

Layout

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, …)

Golden fixture regen

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_parity

See docs/golden-recorder-archive/README.md for the retired JVM recorder workflow.

Logcat: adb logcat -s UsbFdBridge SerialPlugin RustStdoutStderr

CMUX virtual paths

  • exchange / at on physical#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 via take_idle_bytes).