Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
29faa29
feat(android): Rust-first USB layer via JNI
s00d Jul 7, 2026
cd74b84
fix(android): serialize fail/shutdown on usb-io thread
s00d Jul 7, 2026
589efa4
chore(example): ignore src-tauri .gradle cache
s00d Jul 7, 2026
78a97a3
chore: repo hygiene
s00d Jul 7, 2026
e1eb63f
fix(queue): errors no longer halt the TX queue
s00d Jul 7, 2026
3a4bd88
fix(desktop): lock order, Opening-state open, async enable_mux
s00d Jul 7, 2026
7024fc0
refactor(rx): all reads via RX hub; restart + disconnect fail-fast
s00d Jul 7, 2026
caa5700
refactor(android): poll read via RX hub, drop JNI pollRead
s00d Jul 7, 2026
8672de9
fix: panic/poison hardening
s00d Jul 7, 2026
e9fc307
fix(android): JNI exceptions, session path re-key, PendingIntent, tea…
s00d Jul 7, 2026
9b22af7
fix(js)!: onUrc, Uint8Array raw, watch-preserving change()
s00d Jul 7, 2026
2220925
test: consolidate mocks + coverage
s00d Jul 7, 2026
c2d1e23
docs: v3 migration, Android bridge guide, remove debug.md
s00d Jul 7, 2026
1aa1682
ci: add fmt, clippy, pnpm check and build to test workflow
s00d Jul 7, 2026
c6c94dd
fix(android): cancel_exchange wakes hub waiter; remove dead listening…
s00d Jul 7, 2026
2071e15
fix: lock_or_recover in watch_registry and hub channel paths
s00d Jul 7, 2026
87805f8
test: read_request guards, stop_on_error, disconnect fail-fast, auto-…
s00d Jul 7, 2026
7ca898d
docs: CHANGELOG unreleased, fix usbserial UPDATE.md, README gaps
s00d Jul 7, 2026
4071ff6
refactor(rust): split monolith into at/exchange/hub/port/state/api/an…
s00d Jul 7, 2026
be77e2a
feat(android): JNI debug harness and sharedTest fakes
s00d Jul 7, 2026
412b4a5
test(android): Kotlin↔JNI↔Rust integration tests
s00d Jul 7, 2026
2e70ecf
ci: add android integration emulator job and windows check
s00d Jul 7, 2026
b3cc7d2
docs: integration tests guide and module layout notes
s00d Jul 7, 2026
acad6a0
chore(example): add synced androidTest sources in gen/android
s00d Jul 7, 2026
55f1cb3
fix(desktop): unblock hub drain when watch is active
s00d Jul 7, 2026
20b54c0
feat(android): pure Rust USB serial via android-usb-serial
s00d Jul 8, 2026
d309b94
Merge branch 'main' into feat/android-jni-usb-layer
s00d Jul 8, 2026
3517fe3
chore(changelog): update changelog for version 3.0.0 release
s00d Jul 8, 2026
2a69a41
chore(changelog): update changelog for version 2.24.1 release
s00d Jul 8, 2026
62e8e49
chore(deps): update slab version to 0.4.12 in Cargo.lock and add unix…
s00d Jul 8, 2026
58cb8e5
ci: fix Android targets and migrate integration to Ubuntu
s00d Jul 8, 2026
c7855a3
ci: relink file: API package after build for vue-tsc
s00d Jul 8, 2026
fa870fb
ci: install libudev before host android-usb-serial tests
s00d Jul 8, 2026
16c2b68
docs(android-usb-serial): rustdoc; fix PTY concurrency flake
s00d Jul 8, 2026
de5106d
ci: drop flaky Android emulator integration job
s00d Jul 8, 2026
474cffb
ci: restore android-integration prebuild without emulator
s00d Jul 8, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
2 changes: 1 addition & 1 deletion .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ root = true
end_of_line = lf
insert_final_newline = true

[*.{js,ts,svelte,html,json,mjs,cjs}]
[*.{js,ts,html,json,mjs,cjs,vue}]
charset = utf-8
indent_style = space
indent_size = 2
128 changes: 126 additions & 2 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -67,31 +67,59 @@ jobs:
uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt, clippy
targets: aarch64-linux-android

- name: Install cargo-nextest
run: cargo install cargo-nextest --locked --force

# Before host golden/nextest: linux serialport/nusb need -ludev (and GTK for tauri later).
- name: Install system dependencies (Ubuntu)
if: matrix.os == 'ubuntu-latest'
run: |
sudo apt-get update
sudo apt-get install -y pkg-config libglib2.0-dev libudev-dev libgtk-3-dev libgdk-pixbuf2.0-dev libwebkit2gtk-4.1-dev
sudo apt-get install -y openjdk-17-jdk pkg-config libglib2.0-dev libudev-dev libgtk-3-dev libgdk-pixbuf2.0-dev libwebkit2gtk-4.1-dev

- name: Install system dependencies (macOS)
if: matrix.os == 'macos-latest'
run: |
brew install pkg-config

- name: Check android-usb-serial (Android target)
run: cargo check -p android-usb-serial --target aarch64-linux-android

- name: Check plugin (Android target)
run: cargo check -p tauri-plugin-serialplugin --target aarch64-linux-android

- name: Golden parity (host)
run: cargo test -p android-usb-serial --features fake-transport

- name: Clippy android-usb-serial (Android target)
run: cargo clippy -p android-usb-serial --target aarch64-linux-android -- -D warnings

- name: Robolectric (Android module)
if: matrix.os == 'ubuntu-latest'
run: cd android && ./gradlew test

- name: Install dependencies
run: |
pnpm install

- name: Rustfmt
run: cargo fmt --all -- --check

- name: Clippy
run: cargo clippy --workspace --all-targets -- -D warnings

- name: TypeScript check
run: pnpm check

- name: Build JS
run: pnpm build

- name: Run Rust tests
env:
NEXTEST_EXPERIMENTAL_LIBTEST_JSON: 1
run: cargo nextest run --message-format=libtest-json-plus > test-results.jsonx
run: cargo nextest run --workspace --message-format=libtest-json-plus > test-results.jsonx

- name: Run JavaScript tests
run: pnpm test
Expand All @@ -106,3 +134,99 @@ jobs:
test-results/
test-results.jsonx
if-no-files-found: warn

android-integration:
# Build/sync JNI integration harness only — no emulator (device tests stay local).
runs-on: ubuntu-latest
timeout-minutes: 60

steps:
- uses: actions/checkout@v4

- name: Use Node.js 20.x
uses: actions/setup-node@v4
with:
node-version: 20.x

- name: Install pnpm
uses: pnpm/action-setup@v2
with:
version: 9.15.4

- name: Setup Java 17
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: 17

- name: Install Rust
uses: dtolnay/rust-toolchain@stable
with:
targets: x86_64-linux-android

- name: Setup Android SDK
uses: android-actions/setup-android@v3

- name: Install Android SDK packages
run: |
yes | sdkmanager --licenses
sdkmanager \
"platform-tools" \
"platforms;android-34" \
"build-tools;34.0.0" \
"ndk;27.2.12479018"

- name: Install cargo-ndk
run: cargo install cargo-ndk --locked

- name: Install dependencies
run: pnpm install

- name: Build JS
run: pnpm build

# file: link is snapshotted at install; refresh hardlinks after dist-js appears
- name: Relink workspace packages
run: pnpm install

- name: Prebuild Android integration artifacts
env:
ANDROID_INTEGRATION_NDK_ARCH: x86_64
ANDROID_NDK_HOME: ${{ env.ANDROID_HOME }}/ndk/27.2.12479018
NDK_HOME: ${{ env.ANDROID_HOME }}/ndk/27.2.12479018
run: chmod +x scripts/android-integration-ci.sh && ./scripts/android-integration-ci.sh prebuild

windows:
runs-on: windows-latest
timeout-minutes: 30

steps:
- uses: actions/checkout@v4

- name: Install Rust
uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt, clippy

- name: Rustfmt
run: cargo fmt --all -- --check

- name: Clippy (Windows target)
run: cargo clippy --all-targets -- -D warnings

- name: Cargo check Windows
run: cargo check --target x86_64-pc-windows-msvc

- name: Install pnpm
uses: pnpm/action-setup@v2
with:
version: 9.15.4

- name: Install dependencies
run: pnpm install

- name: TypeScript check
run: pnpm check

- name: Run JavaScript tests
run: pnpm test
8 changes: 0 additions & 8 deletions .idea/.gitignore

This file was deleted.

8 changes: 0 additions & 8 deletions .idea/modules.xml

This file was deleted.

17 changes: 0 additions & 17 deletions .idea/process.iml

This file was deleted.

6 changes: 0 additions & 6 deletions .idea/vcs.xml

This file was deleted.

81 changes: 80 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,85 @@

All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.


## [3.0.0](https://github.com/s00d/tauri-plugin-serialplugin/compare/v2.24.1...v3.0.0) (2026-07-08)

Major v3 release: Channel-based **`watch()`** API, native AT **`exchange`** / FIFO queue, unified **RX hub** on desktop and Android, and a **hard cut** from vendored Java `usb-serial-for-android` to the pure-Rust **`android-usb-serial`** crate (nusb).

### Breaking Changes

* **Streaming API:** `startListening()` / `listen()` / `disconnected()` / `cancelListen()` removed. Use **`watch({ onData, onDisconnect?, onError?, onUrc? })`** → `SerialEvent` over Tauri [`Channel`](https://v2.tauri.app/develop/calling-frontend/) (desktop + Android).
* **Capabilities:** `SerialPort.getCapabilities()` (`transport`, `platform`, `version`); commands `capabilities`, `watch`, `unwatch` replace `start_listening` / `stop_listening`.
* **Removed:** `available_ports_direct` — use `available_ports()`.
* **Removed `AtCommandQueue` / `port.at`:** Use **`sendAt()`**, **`sendAtPhases()`**, **`sendSmsPdu()`**, **`cancelAt()`**, **`configureAtSession()`**.
* **`onUrc` removed from `AtSessionOptions`:** Use **`watch({ onUrc })`** for live URC lines.
* **`exchange()`** returns **`ExchangeResponse`**, not `Uint8Array`. Use `.raw` for bytes.
* **`clearRx: true`** maps to **`purge`**; default is now **`drain`** when unset.
* **Android USB stack:** Vendored `android/usbserial` Java tree **removed**. Kotlin no longer runs serial I/O commands on the main thread — only **`UsbFdBridge`** (enumerate, permission, fd). All drivers and bulk I/O live in Rust (`android-usb-serial` + nusb).
* **Rust module layout:** Removed compat top-level modules (`desktop_api`, `mobile_api`, `port_rx_hub`, `exchange_runtime`, …). Use `api::serial`, `hub`, `exchange`, `port`, `state`, `android`.
* **guest-js:** `AtCommandResult.raw` / `ExchangeResponse.raw` are `Uint8Array`; `timedOut` is a native `boolean`.
* **`rust-version`:** **1.79** minimum.

### Migration (v2 → v3)

| v2 | v3 |
|----|-----|
| `startListening()` + `listen(fn)` | `watch({ onData: fn })` |
| `disconnected(fn)` | `watch({ onDisconnect: fn })` |
| `cancelListen()` / `stopListening()` | `handle.unwatch()` |
| `port.at.enable()` / `enqueue()` | `sendAt()` / `sendAtPhases()` |
| Kotlin `UsbBridge` + Java drivers | `UsbFdBridge` fd → Rust `driver_host` |

**Android app requirements:** `uses-feature android.hardware.usb.host`, `device_filter.xml`, runtime USB permission before `openDevice()`. Do **not** `claimInterface()` in Kotlin before handing fd to Rust. See [`crates/android-usb-serial/README.md`](crates/android-usb-serial/README.md).

### Features

* **`android-usb-serial` crate:** Pure Rust USB serial on Android via nusb; **567** golden parity fixtures; drivers for FTDI, CP21xx, CH34x, PL2303, CDC-ACM, GSM modem, Chrome CCD ([20b54c0](https://github.com/s00d/tauri-plugin-serialplugin/commit/20b54c0bbc17227465da71728333cf93d88103aa)).
* **`UsbFdBridge`:** Kotlin provides USB fd only; Rust `driver_host` owns probe, claim, drivers, RX reader, and I/O.
* **Unified `api::serial`:** Single `serialport::SerialPort` facade on desktop and Android; `PortRxHub` poll-loop on both platforms ([4071ff6](https://github.com/s00d/tauri-plugin-serialplugin/commit/4071ff6)).
* **Rust-first Android JNI** ([29faa29](https://github.com/s00d/tauri-plugin-serialplugin/commit/29faa29)): bulk-IN reader thread, chunked write, CMUX virtual paths through Rust session.
* **Unified RX hub (desktop + Android):** Single consumer per port; `watch`, `exchange`, `read`, and drain share one hub ([7024fc0](https://github.com/s00d/tauri-plugin-serialplugin/commit/7024fc0), [caa5700](https://github.com/s00d/tauri-plugin-serialplugin/commit/caa5700)).
* **`take_idle_bytes`:** Stale RX in the hub idle buffer is replayed into the next `exchange` after write.
* **Native FIFO queue (Rust + Android):** All `exchange` / AT jobs on one port serialize in FIFO order; parallel invokes **wait** instead of `"Exchange already in progress"` ([e1eb63f](https://github.com/s00d/tauri-plugin-serialplugin/commit/e1eb63f)).
* **Native `exchange` / `cancel_exchange`:** Write + read-until terminators, idle silence, wall timeout, max response size; structured `ExchangeResponse` (`status`, `lines`, `solicitedBody`, `urcLines`, `raw`).
* **Line-framed AT completion:** Final line `OK` / `ERROR` / `+CME ERROR` / `+CMS ERROR`; `completionMode: 'substring'` for legacy/binary.
* **`rxPrepare`:** Default **`drain`**; **`purge`** opt-in; **`none`** unchanged.
* **`watchAvailablePorts()` / `watch_ports`:** Hotplug via Channel — `snapshot`, then `added` / `removed` (desktop poll; Android USB attach/detach).
* **`open()` canonical path:** Returns session key (Android device path / `device#N` for multi-port).
* **CMUX virtual `exchange`:** Paths `physical#dlci=N` routed through Rust CMUX session like desktop.
* **`usb-driver-tester`:** Standalone hardware self-test app under `examples/usb-driver-tester/`.
* **guest-js:** Modular v3 SDK; auto-reconnect restores **`open()` + `watch()`** after disconnect.
* **macOS:** `available_ports({ singlePortPerDevice: true })` — one path per device (prefers `/dev/cu.*`).
* **Extended AT grammar:** Vendor prefixes, V.250 finals, `derive_solicited_prefixes(command)`; `ExchangeDemux` for live URC before echo.
* **`pauseWatch` default `false`** — watch stays on during AT; pass `pauseWatch: true` for legacy behavior.

### Bug Fixes

* **Android CH340 / weak OTG:** Bulk IN reader starts after line/DTR setup; in-flight URBs reduced to 2; clearer detach reason in logs.
* **Android enumerate:** Kotlin exports `interfaces[]`; Rust expands multi-port paths (`device#N`) via `ProbeTable` without opening fd.
* **Android write after listen:** `EndpointPair::write` no longer re-opens bulk IN owned by `SerialReader` (`endpoint already in use`).
* **Android Kotlin fd bridge:** Removed pre-`claimInterface` (fixes `io interface is busy` with nusb `detach_and_claim`).
* **Android logcat:** Rust plugin logs use tag `SerialPlugin` via `__android_log_write`.
* **Android:** USB permission `PendingIntent` uses `FLAG_MUTABLE` on API 31+; detach + IO errors → `SerialEvent::Disconnect` on Channel ([#27](https://github.com/s00d/tauri-plugin-serialplugin/issues/27)).
* **Android:** `cancel_exchange` wakes hub waiter; CMUX virtual cancel clears DLCI TX queue ([c6c94dd](https://github.com/s00d/tauri-plugin-serialplugin/commit/c6c94dd)).
* **Android:** JNI exceptions, session path re-key, teardown leaks ([e9fc307](https://github.com/s00d/tauri-plugin-serialplugin/commit/e9fc307)).
* **Android:** Serialize fail/shutdown on dedicated usb-io thread ([cd74b84](https://github.com/s00d/tauri-plugin-serialplugin/commit/cd74b84)).
* **desktop:** `write` / `write_binary` flush via `write_all` ([#29](https://github.com/s00d/tauri-plugin-serialplugin/issues/29)).
* **desktop:** Unblock hub drain when watch is active ([55f1cb3](https://github.com/s00d/tauri-plugin-serialplugin/commit/55f1cb3)).
* **desktop:** Lock order, Opening-state open, async `enable_mux` ([3a4bd88](https://github.com/s00d/tauri-plugin-serialplugin/commit/3a4bd88)).
* **desktop (Windows):** Enrich truncated USB `serial_number` from WMI ([#23](https://github.com/s00d/tauri-plugin-serialplugin/issues/23)).
* **Hub / watch:** `lock_or_recover` in watch_registry and hub channel paths ([2071e15](https://github.com/s00d/tauri-plugin-serialplugin/commit/2071e15)); panic/poison hardening ([8672de9](https://github.com/s00d/tauri-plugin-serialplugin/commit/8672de9)).
* **TX queue:** Errors no longer halt the port queue until reopen ([e1eb63f](https://github.com/s00d/tauri-plugin-serialplugin/commit/e1eb63f)).
* **guest-js:** `readBinary` requires open port; watch-preserving `change()` ([9b22af7](https://github.com/s00d/tauri-plugin-serialplugin/commit/9b22af7)).

### Build / CI / Docs

* **CI:** `cargo fmt`, `clippy`, `pnpm check` / `build`, Windows `cargo check`, Android integration emulator job ([1aa1682](https://github.com/s00d/tauri-plugin-serialplugin/commit/1aa1682), [2e70ecf](https://github.com/s00d/tauri-plugin-serialplugin/commit/2e70ecf)).
* **Tests:** Kotlin↔JNI↔Rust instrumented integration tests with `FakeTransport` ([412b4a5](https://github.com/s00d/tauri-plugin-serialplugin/commit/412b4a5), [be77e2a](https://github.com/s00d/tauri-plugin-serialplugin/commit/be77e2a)); Jest v3 lifecycle + Rust contract tests.
* **Docs:** v3 migration guide, Android bridge docs, [`examples/serialport-test/ANDROID.md`](examples/serialport-test/ANDROID.md), [`android-usb-serial` Android usage](crates/android-usb-serial/README.md#using-on-android) ([c2d1e23](https://github.com/s00d/tauri-plugin-serialplugin/commit/c2d1e23), [b3cc7d2](https://github.com/s00d/tauri-plugin-serialplugin/commit/b3cc7d2)).
* **Workspace:** `android-test-harness` feature wires `FakeTransport` for instrumented tests.


### [2.24.1](https://github.com/s00d/tauri-plugin-serialplugin/compare/v2.24.0...v2.24.1) (2026-07-08)

## [2.24.0](https://github.com/s00d/tauri-plugin-serialplugin/compare/v2.21.1...v2.24.0) (2026-07-08)
Expand Down Expand Up @@ -41,7 +120,7 @@ All notable changes to this project will be documented in this file. See [standa
* **Lifecycle:** `SerialPlugin` registers `Application.ActivityLifecycleCallbacks` and runs `SerialPortManager.cleanup()` when the host `Activity` is destroyed (close USB ports, unregister permission receiver, shut down IO executor).
* **Listening:** Incoming data is coalesced in `BufferedEmitter` / `SerialByteAccumulator` before `serialData` events; flush interval via `serialDataFlushIntervalMs` (native clamp typically 10–2000 ms).
* **USB serial (usb-serial-for-android):** Read/write use configured timeouts; `clearBuffer` maps to `purgeHwBuffers` when supported; `setFlowControl` (RTS/CTS, XON/XOFF); `SerialInputOutputManager` errors trigger `serialError` and port cleanup.
* **`bytesToRead` / `bytesToWrite` (Android):** `bytesToRead` returns bytes buffered **in the plugin** only while `startListening` is active (not the kernel queue — not exposed by `UsbSerialPort`). `bytesToWrite` is `0` (writes complete synchronously from the app’s perspective). Documented in JSDoc on the JS API.
* **`bytesToRead` / `bytesToWrite` (Android):** With active **`watch`**, `bytesToRead` is the plugin-side buffer before the next Channel flush; `bytesToWrite` is typically `0`.
* **Tooling:** Gradle wrapper and `android/` settings for local `./gradlew test`; JVM unit tests use a real `org.json` artifact (Android JSON stubs break `JSONObject.put` in tests). Kotlin tests cover models, JSON helpers, emit pipeline, `BufferedEmitter`.

### Features
Expand Down
Loading
Loading