Skip to content

Commit a64c3d2

Browse files
committed
chore: release 2.22.0
- Android: bytesToRead from BufferedEmitter while listening; KDoc; BufferedEmitterTest - guest-js: JSDoc bytesToRead/bytesToWrite; stricter SerialportOptions/Options; Record<PortInfo> - CHANGELOG 2.22.0; android/README table/tests list - Bump Cargo/npm versions and lockfiles Made-with: Cursor
1 parent 2e56cad commit a64c3d2

11 files changed

Lines changed: 77 additions & 21 deletions

File tree

CHANGELOG.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,22 @@
22

33
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.
44

5+
For **Android/USB-focused** details (behavior, limits, testing), see also [`android/README.md`](android/README.md).
6+
7+
## [2.22.0](https://github.com/s00d/tauri-plugin-serialplugin/compare/v2.21.1...v2.22.0) (2026-03-21)
8+
9+
### Android / USB
10+
11+
* **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).
12+
* **Listening:** Incoming data is coalesced in `BufferedEmitter` / `SerialByteAccumulator` before `serialData` events; flush interval via `serialDataFlushIntervalMs` (native clamp typically 10–2000 ms).
13+
* **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.
14+
* **`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.
15+
* **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`.
16+
17+
### Features
18+
19+
* **guest-js:** Stricter `SerialportOptions` / `Options` typing (removed open index signatures); `Record<string, PortInfo>` for `available_ports` maps.
20+
521
### [2.21.1](https://github.com/s00d/tauri-plugin-serialplugin/compare/v2.21.0...v2.21.1) (2025-11-05)
622

723
## [2.21.0](https://github.com/s00d/tauri-plugin-serialplugin/compare/v2.20.0...v2.21.0) (2025-10-11)

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "tauri-plugin-serialplugin"
3-
version = "2.21.1"
3+
version = "2.22.0"
44
description = "Access the current process of your Tauri application."
55
edition = "2021"
66
authors = ["Tauri Programme within The Commons Conservancy"]

android/README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ Tests live under `src/test/kotlin/`. Pure JVM tests (no Robolectric required):
1616
- `SerialByteAccumulatorTest` — thread-safe byte coalescing for `BufferedEmitter`
1717
- `SerialDataEmitFieldsTest``serialDataPayloadFromChunk` / `flushAccumulatorToEmit` / `applyToJSObject` (binary + UTF-8)
1818
- `SerialPluginConversionTest``Map.toJSObject` / `List.toJSArray` helpers from `SerialPlugin.kt`
19+
- `BufferedEmitterTest``pendingByteCount()` before flush
1920

2021
JVM unit tests use **`testImplementation("org.json:json:…")`** so `JSONObject.put` is not the Android stub that throws (“Method … not mocked”).
2122

@@ -38,4 +39,5 @@ This repo includes a Gradle wrapper (`gradlew`, `gradle/wrapper/`). The module d
3839
| `setTimeout` | Stored in `SerialPortConfig.timeout` and used for `write()` and as a fallback for `read()` when the call passes `timeout == 0`. |
3940
| `clearBuffer` | `UsbSerialPort.purgeHwBuffers()` (when supported by the driver). |
4041
| `setFlowControl` | `UsbSerialPort.setFlowControl(RTS_CTS / XON_XOFF / NONE)`. |
41-
| `bytesToRead` / `bytesToWrite` | Not exposed by usb-serial — returns `0`. |
42+
| `bytesToRead` | With **listening** (`startListening`): bytes in the plugin’s [BufferedEmitter] before the next `serialData` flush (not the kernel queue). Without listening: `0`. |
43+
| `bytesToWrite` | Always `0`: writes are synchronous; usb-serial exposes no TX backlog. |

android/src/main/kotlin/app/tauri/serialplugin/manager/BufferedEmitter.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,12 @@ internal class BufferedEmitter(
4949
accumulator.append(data)
5050
}
5151

52+
/**
53+
* Bytes received via [addData] but not yet emitted to JS (waiting for the next flush).
54+
* Does not include data still inside the USB/driver stack — only this plugin buffer.
55+
*/
56+
fun pendingByteCount(): Int = accumulator.pendingByteCount()
57+
5258
fun stop() {
5359
scheduled.cancel(false)
5460
scheduler.shutdown()

android/src/main/kotlin/app/tauri/serialplugin/manager/SerialPortManager.kt

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -626,17 +626,25 @@ class SerialPortManager(
626626
}
627627
}
628628

629+
/**
630+
* When [startListening] is active, returns bytes accumulated in [BufferedEmitter] before the next
631+
* `serialData` flush — **not** the OS/driver queue (UsbSerialPort exposes no such API).
632+
* Without an active listener, returns `0`.
633+
*/
629634
fun bytesToRead(path: String): Int {
630-
// UsbSerialPort (mik3y) has no incoming-queue length API — kernel buffer is opaque.
631635
return try {
632636
if (!portMap.containsKey(path)) throw IOException("Port not found")
633-
0
637+
emitters[path]?.pendingByteCount() ?: 0
634638
} catch (e: Exception) {
635639
Log.e("SerialPortManager", "Failed to get bytes to read: ${e.message}", e)
636640
0
637641
}
638642
}
639643

644+
/**
645+
* [UsbSerialPort] does not expose a pending TX queue; [writeToPort] is synchronous, so there is
646+
* nothing application-level queued after a successful write — always `0` here.
647+
*/
640648
fun bytesToWrite(path: String): Int {
641649
return try {
642650
if (!portMap.containsKey(path)) throw IOException("Port not found")
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package app.tauri.serialplugin.manager
2+
3+
import org.junit.Assert.assertEquals
4+
import org.junit.Test
5+
6+
class BufferedEmitterTest {
7+
@Test
8+
fun pendingByteCount_tracks_data_before_flush() {
9+
// Max flush interval (2000 ms) so the first scheduled flush is late; we only assert immediately.
10+
val emitter = BufferedEmitter("/dev/usbX", 2000L) { _ -> }
11+
try {
12+
assertEquals(0, emitter.pendingByteCount())
13+
emitter.addData(byteArrayOf(10, 20, 30))
14+
assertEquals(3, emitter.pendingByteCount())
15+
} finally {
16+
emitter.stop()
17+
}
18+
}
19+
}

examples/serialport-test/src-tauri/Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

guest-js/index.ts

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,9 @@ export interface SerialportOptions {
3333
size?: number;
3434
/** Android: batch interval (ms) for `serialData` events when listening (native default 100). */
3535
serialDataFlushIntervalMs?: number;
36-
[key: string]: any;
3736
}
3837

38+
/** Normalized port settings used after construction (see {@link SerialportOptions}). */
3939
export interface Options {
4040
path?: string;
4141
baudRate?: number;
@@ -46,7 +46,6 @@ export interface Options {
4646
size?: number;
4747
timeout: number;
4848
serialDataFlushIntervalMs?: number;
49-
[key: string]: any;
5049
}
5150

5251
export interface ReadOptions {
@@ -141,11 +140,11 @@ class SerialPort {
141140

142141
/**
143142
* @description Lists all available serial ports
144-
* @returns {Promise<{ [key: string]: PortInfo }>} A promise that resolves to a map of port names to port information
143+
* @returns {Promise<Record<string, PortInfo>>} A promise that resolves to a map of port names to port information
145144
*/
146-
static async available_ports(): Promise<{ [key: string]: PortInfo }> {
145+
static async available_ports(): Promise<Record<string, PortInfo>> {
147146
try {
148-
const result = await invoke<{ [key: string]: PortInfo }>('plugin:serialplugin|available_ports');
147+
const result = await invoke<Record<string, PortInfo>>('plugin:serialplugin|available_ports');
149148
return Promise.resolve(result)
150149
} catch (error) {
151150
return Promise.reject(error);
@@ -154,11 +153,11 @@ class SerialPort {
154153

155154
/**
156155
* @description Lists all available serial ports using platform-specific commands
157-
* @returns {Promise<{ [key: string]: PortInfo }>} A promise that resolves to a map of port names to port information
156+
* @returns {Promise<Record<string, PortInfo>>} A promise that resolves to a map of port names to port information
158157
*/
159-
static async available_ports_direct(): Promise<{ [key: string]: PortInfo }> {
158+
static async available_ports_direct(): Promise<Record<string, PortInfo>> {
160159
try {
161-
const result = await invoke<{ [key: string]: PortInfo }>('plugin:serialplugin|available_ports_direct');
160+
const result = await invoke<Record<string, PortInfo>>('plugin:serialplugin|available_ports_direct');
162161
return Promise.resolve(result)
163162
} catch (error) {
164163
return Promise.reject(error);
@@ -1004,8 +1003,12 @@ class SerialPort {
10041003
}
10051004

10061005
/**
1007-
* @description Gets the number of bytes available to read
1008-
* @returns {Promise<number>} A promise that resolves to the number of bytes
1006+
* @description Gets an estimate of bytes ready to read.
1007+
* **Desktop:** driver/OS input queue (same idea as POSIX `FIONREAD` / `ioctl`).
1008+
* **Android:** only while `startListening` is active — bytes already received by the native
1009+
* listener but not yet flushed to JS as `serialData` (plugin-side buffer). Otherwise `0`.
1010+
* This is **not** the same as desktop when listening is off or on other platforms.
1011+
* @returns {Promise<number>} Byte count (platform-specific semantics above).
10091012
*/
10101013
async bytesToRead(): Promise<number> {
10111014
try {
@@ -1018,8 +1021,10 @@ class SerialPort {
10181021
}
10191022

10201023
/**
1021-
* @description Gets the number of bytes waiting to be written
1022-
* @returns {Promise<number>} A promise that resolves to the number of bytes
1024+
* @description Gets the number of bytes waiting to be written (output queue).
1025+
* **Android:** typically always `0` after a successful `write` — the USB stack does not expose
1026+
* a pending-TX count and writes complete synchronously from the app’s perspective.
1027+
* @returns {Promise<number>} Pending output bytes (often `0` on Android).
10231028
*/
10241029
async bytesToWrite(): Promise<number> {
10251030
try {

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)