react-native-web-serial-api 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (112) hide show
  1. package/README.md +188 -117
  2. package/TESTING.md +417 -176
  3. package/android/build.gradle +14 -0
  4. package/android/src/main/java/dev/webserialapi/NativeUsbSerialModule.java +74 -11
  5. package/android/src/main/java/dev/webserialapi/PortPickerActivity.java +61 -59
  6. package/bin/expose-serial.js +205 -0
  7. package/lib/commonjs/UsbSerial.js +1 -1
  8. package/lib/commonjs/WebSerial.js +110 -26
  9. package/lib/commonjs/WebSerial.js.map +1 -1
  10. package/lib/commonjs/index.js +2 -2
  11. package/lib/commonjs/index.js.map +1 -1
  12. package/lib/commonjs/lib/event-target.js +3 -1
  13. package/lib/commonjs/lib/event-target.js.map +1 -1
  14. package/lib/commonjs/lib/web-streams.js +42 -0
  15. package/lib/commonjs/lib/web-streams.js.map +1 -0
  16. package/lib/commonjs/testing/device-fixture.js +70 -0
  17. package/lib/commonjs/testing/device-fixture.js.map +1 -0
  18. package/lib/commonjs/testing/expose.js +91 -0
  19. package/lib/commonjs/testing/expose.js.map +1 -0
  20. package/lib/commonjs/testing/harness.js +98 -0
  21. package/lib/commonjs/testing/harness.js.map +1 -0
  22. package/lib/commonjs/testing/{virtual-serial.js → in-memory-serial-transport.js} +66 -28
  23. package/lib/commonjs/testing/in-memory-serial-transport.js.map +1 -0
  24. package/lib/commonjs/testing/index.js +100 -17
  25. package/lib/commonjs/testing/index.js.map +1 -1
  26. package/lib/commonjs/testing/install-in-memory-serial-transport.js +54 -0
  27. package/lib/commonjs/testing/install-in-memory-serial-transport.js.map +1 -0
  28. package/lib/commonjs/testing/serial-client.js +277 -0
  29. package/lib/commonjs/testing/serial-client.js.map +1 -0
  30. package/lib/commonjs/testing/{serial-device.js → simulated-device.js} +17 -17
  31. package/lib/commonjs/testing/simulated-device.js.map +1 -0
  32. package/lib/commonjs/testing/test-suite.js +142 -0
  33. package/lib/commonjs/testing/test-suite.js.map +1 -0
  34. package/lib/commonjs/transport.js +3 -3
  35. package/lib/commonjs/websocket/WebSocketSerialTransport.js +659 -0
  36. package/lib/commonjs/websocket/WebSocketSerialTransport.js.map +1 -0
  37. package/lib/commonjs/websocket/bridge.js +234 -0
  38. package/lib/commonjs/websocket/bridge.js.map +1 -0
  39. package/lib/commonjs/websocket/index.js +33 -0
  40. package/lib/commonjs/websocket/index.js.map +1 -0
  41. package/lib/commonjs/websocket/protocol.js +55 -0
  42. package/lib/commonjs/websocket/protocol.js.map +1 -0
  43. package/lib/commonjs/websocket/serial-device-bridge.js +130 -0
  44. package/lib/commonjs/websocket/serial-device-bridge.js.map +1 -0
  45. package/lib/typescript/src/UsbSerial.d.ts +1 -1
  46. package/lib/typescript/src/WebSerial.d.ts +7 -7
  47. package/lib/typescript/src/WebSerial.d.ts.map +1 -1
  48. package/lib/typescript/src/index.d.ts +1 -1
  49. package/lib/typescript/src/index.d.ts.map +1 -1
  50. package/lib/typescript/src/lib/event-target.d.ts +2 -0
  51. package/lib/typescript/src/lib/event-target.d.ts.map +1 -1
  52. package/lib/typescript/src/lib/web-streams.d.ts +9 -0
  53. package/lib/typescript/src/lib/web-streams.d.ts.map +1 -0
  54. package/lib/typescript/src/testing/device-fixture.d.ts +70 -0
  55. package/lib/typescript/src/testing/device-fixture.d.ts.map +1 -0
  56. package/lib/typescript/src/testing/expose.d.ts +71 -0
  57. package/lib/typescript/src/testing/expose.d.ts.map +1 -0
  58. package/lib/typescript/src/testing/harness.d.ts +34 -0
  59. package/lib/typescript/src/testing/harness.d.ts.map +1 -0
  60. package/lib/typescript/src/testing/{virtual-serial.d.ts → in-memory-serial-transport.d.ts} +37 -26
  61. package/lib/typescript/src/testing/in-memory-serial-transport.d.ts.map +1 -0
  62. package/lib/typescript/src/testing/index.d.ts +18 -8
  63. package/lib/typescript/src/testing/index.d.ts.map +1 -1
  64. package/lib/typescript/src/testing/install-in-memory-serial-transport.d.ts +25 -0
  65. package/lib/typescript/src/testing/install-in-memory-serial-transport.d.ts.map +1 -0
  66. package/lib/typescript/src/testing/serial-client.d.ts +62 -0
  67. package/lib/typescript/src/testing/serial-client.d.ts.map +1 -0
  68. package/lib/typescript/src/testing/{serial-device.d.ts → simulated-device.d.ts} +23 -23
  69. package/lib/typescript/src/testing/simulated-device.d.ts.map +1 -0
  70. package/lib/typescript/src/testing/test-suite.d.ts +75 -0
  71. package/lib/typescript/src/testing/test-suite.d.ts.map +1 -0
  72. package/lib/typescript/src/transport.d.ts +3 -3
  73. package/lib/typescript/src/websocket/WebSocketSerialTransport.d.ts +111 -0
  74. package/lib/typescript/src/websocket/WebSocketSerialTransport.d.ts.map +1 -0
  75. package/lib/typescript/src/websocket/bridge.d.ts +66 -0
  76. package/lib/typescript/src/websocket/bridge.d.ts.map +1 -0
  77. package/lib/typescript/src/websocket/index.d.ts +19 -0
  78. package/lib/typescript/src/websocket/index.d.ts.map +1 -0
  79. package/lib/typescript/src/websocket/protocol.d.ts +64 -0
  80. package/lib/typescript/src/websocket/protocol.d.ts.map +1 -0
  81. package/lib/typescript/src/websocket/serial-device-bridge.d.ts +32 -0
  82. package/lib/typescript/src/websocket/serial-device-bridge.d.ts.map +1 -0
  83. package/package.json +21 -3
  84. package/src/UsbSerial.ts +1 -1
  85. package/src/WebSerial.ts +134 -35
  86. package/src/index.ts +4 -1
  87. package/src/lib/event-target.ts +12 -0
  88. package/src/lib/web-streams.ts +43 -0
  89. package/src/testing/device-fixture.ts +150 -0
  90. package/src/testing/expose.ts +147 -0
  91. package/src/testing/harness.ts +124 -0
  92. package/src/testing/{virtual-serial.ts → in-memory-serial-transport.ts} +95 -56
  93. package/src/testing/index.ts +69 -21
  94. package/src/testing/install-in-memory-serial-transport.ts +65 -0
  95. package/src/testing/serial-client.ts +313 -0
  96. package/src/testing/{serial-device.ts → simulated-device.ts} +23 -23
  97. package/src/testing/test-suite.ts +186 -0
  98. package/src/transport.ts +3 -3
  99. package/src/websocket/WebSocketSerialTransport.ts +796 -0
  100. package/src/websocket/bridge.ts +299 -0
  101. package/src/websocket/index.ts +38 -0
  102. package/src/websocket/protocol.ts +101 -0
  103. package/src/websocket/serial-device-bridge.ts +160 -0
  104. package/lib/commonjs/testing/install.js +0 -54
  105. package/lib/commonjs/testing/install.js.map +0 -1
  106. package/lib/commonjs/testing/serial-device.js.map +0 -1
  107. package/lib/commonjs/testing/virtual-serial.js.map +0 -1
  108. package/lib/typescript/src/testing/install.d.ts +0 -25
  109. package/lib/typescript/src/testing/install.d.ts.map +0 -1
  110. package/lib/typescript/src/testing/serial-device.d.ts.map +0 -1
  111. package/lib/typescript/src/testing/virtual-serial.d.ts.map +0 -1
  112. package/src/testing/install.ts +0 -65
package/README.md CHANGED
@@ -1,18 +1,26 @@
1
1
  # react-native-web-serial-api
2
2
 
3
- > The [W3C Web Serial API](https://wicg.github.io/serial/) (`navigator.serial`) for **React Native on Android**, backed by a USB-serial TurboModule built on top of [`mik3y/usb-serial-for-android`](https://github.com/mik3y/usb-serial-for-android).
3
+ > The [W3C Web Serial API](https://wicg.github.io/serial/) (`navigator.serial`) for React Native on Android, backed by a USB-serial TurboModule built on top of [`mik3y/usb-serial-for-android`](https://github.com/mik3y/usb-serial-for-android).
4
4
 
5
- Talk to USB serial devices (FTDI, CP210x, CH340/CH341, PL2303, CDC-ACM …) from React Native using the **exact same API you already know from the browser** `serial.requestPort()`, `port.open()`, `port.readable`, `port.writable`, and so on.
5
+ Use the same API you already know from the browser - `serial.requestPort()`, `port.open()`, `port.readable`, `port.writable`, `getPorts()`, `setSignals()`, and `getSignals()` - in a React Native app that talks to USB serial devices.
6
6
 
7
- - Spec-compliant `Serial` / `SerialPort` implementation (`getPorts`, `requestPort`, `open`, `close`, `readable`, `writable`, `setSignals`, `getSignals`, `forget`, `connect`/`disconnect` events)
8
- - New Architecture **TurboModule**
9
- - Native port-picker dialog + USB permission handling
10
- - Backed by Web Streams (`ReadableStream` / `WritableStream`)
11
- - Drop-in for code written against the browser Web Serial API (on web it transparently uses the native `navigator.serial`)
7
+ ## At a glance
12
8
 
13
- > **Platform support:** Android only. On web (`react-native-web`) the package delegates to the browser's native Web Serial API. There is no iOS implementation (iOS does not allow generic USB-serial access), so iOS autolinking is disabled.
9
+ - Spec-style `Serial` / `SerialPort` implementation
10
+ - New Architecture TurboModule
11
+ - Native port picker and Android USB permission handling
12
+ - Web Streams under the hood (`ReadableStream` / `WritableStream`)
13
+ - Works with browser-style code on web by delegating to the native `navigator.serial`
14
14
 
15
- ## Installation
15
+ ## Platform support
16
+
17
+ | Platform | Support | Notes |
18
+ | --- | --- | --- |
19
+ | Android | Yes | Native USB-serial support through the TurboModule. |
20
+ | Web | Yes | Delegates to the browser's native `navigator.serial`. |
21
+ | iOS | No | Generic USB-serial access is not available, so autolinking is disabled. |
22
+
23
+ ## Quick start
16
24
 
17
25
  ```sh
18
26
  npm install react-native-web-serial-api
@@ -20,49 +28,27 @@ npm install react-native-web-serial-api
20
28
  yarn add react-native-web-serial-api
21
29
  ```
22
30
 
23
- This is a New Architecture library, so make sure your app has the New Architecture enabled (default in recent React Native). No manual linking is required the module is autolinked.
24
-
25
- ### Android setup
31
+ This is a New Architecture library. Make sure your app has the New Architecture enabled. No manual linking is required - the module is autolinked.
26
32
 
27
- The library ships its own `AndroidManifest.xml` that declares the port-picker activity, the detach receiver, and the `android.hardware.usb.host` feature, so usually **no extra configuration is needed**.
28
-
29
- If you want your app to be **launched automatically when a matching device is plugged in**, add an intent filter to your launcher activity in `android/app/src/main/AndroidManifest.xml`:
30
-
31
- ```xml
32
- <activity android:name=".MainActivity" ...>
33
- <intent-filter>
34
- <action android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED" />
35
- </intent-filter>
36
- <!-- The device_filter resource is provided by the library -->
37
- <meta-data
38
- android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED"
39
- android:resource="@xml/device_filter" />
40
- </activity>
41
- ```
42
-
43
- The bundled `@xml/device_filter` matches the common USB-serial chips (CDC-ACM, FTDI `0x0403`, CP210x `0x10C4`, CH34x `0x1A86`, PL2303 `0x067B`). Provide your own `res/xml/device_filter.xml` to override it.
44
-
45
- ## Usage
33
+ ### Minimal usage
46
34
 
47
35
  ```ts
48
36
  import {serial} from 'react-native-web-serial-api';
49
37
 
50
38
  async function run() {
51
- // Shows a native dialog to pick a port, then requests USB permission.
39
+ // Must be called from a user gesture on web.
52
40
  const port = await serial.requestPort({
53
- filters: [{usbVendorId: 0x0403}], // optional e.g. only FTDI devices
41
+ filters: [{usbVendorId: 0x0403}], // optional, for example FTDI only
54
42
  });
55
43
 
56
44
  await port.open({baudRate: 115200, dataBits: 8, stopBits: 1, parity: 'none'});
57
45
 
58
- // Write
59
46
  const writer = port.writable.getWriter();
60
47
  await writer.write(new TextEncoder().encode('Hello\n'));
61
48
  writer.releaseLock();
62
49
 
63
- // Read
64
50
  const reader = port.readable.getReader();
65
- const {value} = await reader.read(); // value is a Uint8Array
51
+ const {value} = await reader.read();
66
52
  console.log(value);
67
53
  reader.releaseLock();
68
54
 
@@ -70,150 +56,235 @@ async function run() {
70
56
  }
71
57
  ```
72
58
 
73
- In Android USB mode, `allowedBluetoothServiceClassIds` is not supported by
74
- `requestPort()` and will throw a `TypeError` if provided.
59
+ ## Android setup
75
60
 
76
- ### Control & status signals
61
+ The library ships its own `AndroidManifest.xml` that declares the port picker activity, the detach receiver, and the `android.hardware.usb.host` feature, so usually no extra configuration is needed.
77
62
 
78
- ```ts
79
- await port.setSignals({dataTerminalReady: true, requestToSend: false});
80
- const {clearToSend, dataCarrierDetect, ringIndicator, dataSetReady} =
81
- await port.getSignals();
63
+ If you want your app to launch automatically when a matching device is plugged in, add an intent filter to your launcher activity in `android/app/src/main/AndroidManifest.xml`:
64
+
65
+ ```xml
66
+ <activity android:name=".MainActivity" ...>
67
+ <intent-filter>
68
+ <action android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED" />
69
+ </intent-filter>
70
+ <meta-data
71
+ android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED"
72
+ android:resource="@xml/device_filter" />
73
+ </activity>
82
74
  ```
83
75
 
84
- ### Connect / disconnect events
76
+ The bundled `@xml/device_filter` matches common USB-serial chips:
77
+
78
+ - CDC-ACM
79
+ - FTDI `0x0403`
80
+ - CP210x `0x10C4`
81
+ - CH34x `0x1A86`
82
+ - PL2303 `0x067B`
83
+
84
+ Provide your own `res/xml/device_filter.xml` to override it.
85
+
86
+ ## Which API should I use?
87
+
88
+ | Use case | Start with | Why |
89
+ | --- | --- | --- |
90
+ | Real hardware in your app | `Serial` / `SerialPort` | The browser-style Web Serial API you already know. |
91
+ | Quick in-memory smoke tests | `InMemorySerialTransport` + `LoopbackDevice` | Fastest way to exercise bytes without hardware. |
92
+ | Protocol tests against a simulated peripheral | `SimulatedDevice` + `createDeviceFixture` + `SerialClient` | Gives you both sides of the conversation in one test. |
93
+ | App-to-app or emulator-to-host testing | `exposeSimulatedDevice` + `WebSocketSerialTransport` | Runs the same simulated device behind a real WebSocket bridge. |
94
+
95
+ ## Core concepts
96
+
97
+ ### `serial`
98
+
99
+ The package exports a ready-to-use `serial` singleton, which is the equivalent of `navigator.serial`.
85
100
 
86
101
  ```ts
87
- serial.addEventListener('connect', () => console.log('device attached'));
88
- serial.addEventListener('disconnect', () => console.log('device detached'));
102
+ import {serial} from 'react-native-web-serial-api';
89
103
 
90
- port.addEventListener('disconnect', () => console.log('this port went away'));
104
+ const ports = await serial.getPorts();
91
105
  ```
92
106
 
93
- On Android, `serial` fires `connect` when a USB device is **attached** _and_ when
94
- the app is **granted USB permission** for a device (the latter matters because
95
- Android revokes permission on unplug, so a re-attached device only becomes
96
- accessible — and shows up in `getPorts()` — once permission is re-granted).
97
- Simply subscribing with `serial.addEventListener('connect', …)` is enough to
98
- receive these; you don't need to call `getPorts()` first. A common pattern is to
99
- re-run `getPorts()` on every `connect`/`disconnect` to keep a device list fresh.
107
+ ### Permissions
108
+
109
+ There are two different permission concepts:
110
+
111
+ - `serial.requestPort()` is the Web Serial permission flow. In Android mode it shows the native picker and requests Android USB permission for the selected device.
112
+ - Android USB permission can also be granted outside the app, for example through the system attach dialog or an `USB_DEVICE_ATTACHED` intent filter.
100
113
 
101
- ### Listing already-permitted ports
114
+ On Android, `serial.getPorts()` returns the devices the app can currently access through Android USB permission, regardless of how that permission was obtained. On web, `getPorts()` returns ports previously granted by the site in the browser's persistent permission store.
102
115
 
103
116
  ```ts
104
117
  const ports = await serial.getPorts();
118
+ const port = await serial.requestPort();
105
119
  ```
106
120
 
107
- ## Permission model (Android vs. Web Serial)
108
-
109
- There are two distinct notions of "permission" in play, and they behave
110
- differently on Android than in the browser:
111
-
112
- - **Web Serial permission grant** — `serial.requestPort()`. In the browser this
113
- records a site-level grant for the chosen port; on Android it shows a native
114
- picker and requests the Android USB permission for the selected device. This
115
- is the mechanism for gaining access to a device you don't have access to yet,
116
- and it is **unchanged** by anything below.
117
- - **Native Android USB permission** — `UsbManager` permission for a device.
118
- This can be granted **outside** the app entirely: when you plug a device in,
119
- Android may show its own _"Open <app> to handle this USB device? / use by
120
- default for this device"_ dialog, or the app may have been launched via a
121
- `USB_DEVICE_ATTACHED` intent filter. In those cases the app already holds USB
122
- permission without ever calling `requestPort()`.
123
-
124
- **`serial.getPorts()` in Android/native mode** returns **every probed
125
- USB-serial port the app can currently access through Android USB permission** —
126
- regardless of how that permission was obtained. So a device granted via the
127
- system attach dialog appears in `getPorts()` even though `requestPort()` was
128
- never called for it. Probed devices the app does **not** yet have permission for
129
- are excluded; use `requestPort()` to gain access to those.
121
+ ### Connect and disconnect events
130
122
 
131
123
  ```ts
132
- // All devices accessible right now (natively-granted OR previously requested):
133
- const ports = await serial.getPorts();
124
+ serial.addEventListener('connect', () => console.log('device attached'));
125
+ serial.addEventListener('disconnect', () => console.log('device detached'));
134
126
 
135
- // Gain access to a device you don't have permission for yet:
136
- const port = await serial.requestPort();
127
+ port.addEventListener('disconnect', () => console.log('this port went away'));
128
+ ```
129
+
130
+ On Android, `serial` fires `connect` when a USB device is attached and when the app is granted USB permission for a device. A common pattern is to re-run `getPorts()` on every `connect` / `disconnect` so your device list stays fresh.
131
+
132
+ ### Control and status signals
133
+
134
+ ```ts
135
+ await port.setSignals({dataTerminalReady: true, requestToSend: false});
136
+ const {clearToSend, dataCarrierDetect, ringIndicator, dataSetReady} =
137
+ await port.getSignals();
137
138
  ```
138
139
 
139
- On the **web**, `getPorts()` returns the ports the user has previously granted
140
- the site via `requestPort()` (the browser's persistent permission store) — the
141
- native "attach dialog" notion does not apply.
140
+ ### Browser-only option note
142
141
 
143
- > Note: this is a deliberate, Android-appropriate reading of the Web Serial
144
- > spec's `getPorts()` ("ports the site has been granted access to"). On Android
145
- > the unit of access is the OS-level USB permission, so a device the OS has
146
- > already authorized for the app is, by definition, one the app has been
147
- > granted access to.
142
+ In Android USB mode, `allowedBluetoothServiceClassIds` is not supported by `requestPort()` and will throw a `TypeError` if provided.
148
143
 
149
- ## API
144
+ ## API reference
150
145
 
151
146
  The package exposes:
152
147
 
153
148
  | Export | Description |
154
149
  | --- | --- |
155
- | `serial` | A ready-to-use `Serial` instance (`navigator.serial` equivalent). |
150
+ | `serial` | A ready-to-use `Serial` instance. |
156
151
  | `Serial`, `SerialPort` | The Web Serial API classes. |
157
152
  | `UsbSerial` | Lower-level access to the raw USB-serial TurboModule (Android only). |
158
- | `Event`, `EventTarget` | The event primitives used by the polyfill. |
153
+ | `Event`, `EventTarget` | Polyfill implementations used only when the runtime does not already provide these globals. |
159
154
  | Types | `SerialOptions`, `SerialOutputSignals`, `SerialInputSignals`, `SerialPortInfo`, `SerialPortFilter`, `SerialPortRequestOptions`. |
160
155
 
161
156
  ## Example app
162
157
 
163
- The [`example/`](./example) app is a React Native port of [SimpleUsbTerminal](https://github.com/kai-morich/SimpleUsbTerminal) built entirely on this package's Web Serial API a **Devices** list (with baud-rate selection) and a **Terminal** (colored receive/send log, HEX mode, newline selection, clear, control-lines row with RTS/DTR toggles, flow control, and Send BREAK).
158
+ The [`example/`](./example) app is a React Native port of [SimpleUsbTerminal](https://github.com/kai-morich/SimpleUsbTerminal) built on this package's Web Serial API. It includes:
159
+
160
+ - a Devices screen with baud-rate selection
161
+ - a Terminal screen with colored send / receive logs
162
+ - HEX mode
163
+ - newline selection
164
+ - clear
165
+ - control lines with RTS / DTR toggles
166
+ - flow control
167
+ - Send BREAK
168
+
169
+ ### Run the example on Android
164
170
 
165
171
  ```sh
166
- # install the library's build tooling
167
172
  npm install
168
-
169
- # install and run the example on Android
170
173
  cd example
171
174
  npm install
172
175
  npm run android
173
176
  ```
174
177
 
175
- Because it uses the Web Serial API, a few SimpleUsbTerminal details map differently:
178
+ Because the example uses the Web Serial API, a few details differ from SimpleUsbTerminal:
176
179
 
177
- - **Driver/chip name** isn't exposed by the Web Serial API (`getInfo()` returns only VID/PID), so rows show `Vendor/Product` plus a best-effort chip label from known vendor IDs.
178
- - **Flow control** is limited to *None* / *Hardware (RTS-CTS)* XON/XOFF and DTR/DSR aren't in the Web Serial spec. Changing it reconnects the port.
179
- - The Android background **foreground-service notification** is omitted (it's service plumbing unrelated to serial I/O).
180
+ - Driver / chip name is not exposed by the Web Serial API. Rows show `Vendor/Product` plus a best-effort chip label from known vendor IDs.
181
+ - Flow control is limited to `None` and `Hardware (RTS-CTS)`. XON/XOFF and DTR/DSR are not in the Web Serial spec. Changing it reconnects the port.
182
+ - The Android foreground-service notification is omitted because it is service plumbing unrelated to serial I/O.
180
183
 
181
184
  ### Run the example in the browser
182
185
 
183
- The example also runs as a web app via [`react-native-web`](https://necolas.github.io/react-native-web/) + [Vite](https://vite.dev/). On web, the package delegates to the browser's native `navigator.serial`, so the exact same `App.tsx` talks to real serial hardware over WebUSB-style permissions.
186
+ The example also runs as a web app via [`react-native-web`](https://necolas.github.io/react-native-web/) and [Vite](https://vite.dev/). On web, the package delegates to the browser's native `navigator.serial`, so the same `App.tsx` talks to real serial hardware through browser permissions.
184
187
 
185
188
  ```sh
186
189
  cd example
187
190
  npm install
188
- npm run web # dev server at http://localhost:5173
189
- # npm run web:build # production build into example/dist
191
+ npm run web
192
+ # npm run web:build
190
193
  ```
191
194
 
192
- > Web Serial only works in **Chromium-based browsers** (Chrome / Edge / Opera) over a **secure context** (`http://localhost` counts), and `requestPort()` must be called from a user gesture the demo's "Request port" button handles that.
195
+ Web Serial works in Chromium-based browsers over a secure context. `http://localhost` counts. `requestPort()` must be called from a user gesture, and the example's Request port button handles that.
193
196
 
194
- ## How it works
195
-
196
- The JavaScript layer (`src/`) implements the Web Serial API on top of a thin TurboModule (`NativeUsbSerial`) whose native Android implementation (`android/src/main/java/dev/webserialapi/`) wraps `usb-serial-for-android`. Reads/writes are bridged to `ReadableStream`/`WritableStream` via [`web-streams-polyfill`](https://github.com/MattiasBuelens/web-streams-polyfill).
197
+ ## Testing and simulation
197
198
 
198
- ## Testing
199
+ The transport layer lets you test without USB hardware, whether you want a quick loopback check, a stateful simulated peripheral, or a full WebSocket E2E path. The example app also includes a Self Test screen and a Virtual device (demo) mode.
199
200
 
200
- The hardware layer sits behind a single injectable `SerialTransport` interface, so you can test against a pure-JS **virtual serial device** instead of real USB hardware — in Jest *and* live on a device/emulator/browser. The same conformance suite runs in both places, and the example app has a **Self Test** screen plus a **Virtual device (demo)** mode.
201
+ ### Fast in-memory test
201
202
 
202
203
  ```ts
203
204
  import {Serial} from 'react-native-web-serial-api';
204
- import {VirtualSerialTransport, EchoDevice} from 'react-native-web-serial-api/testing';
205
+ import {
206
+ InMemorySerialTransport,
207
+ LoopbackDevice,
208
+ } from 'react-native-web-serial-api/testing';
209
+
210
+ const transport = new InMemorySerialTransport();
211
+ transport.addDevice(
212
+ new LoopbackDevice({usbVendorId: 0x0403, usbProductId: 0x6001}),
213
+ {hasPermission: true},
214
+ );
215
+
216
+ const serial = new Serial(transport);
217
+ ```
218
+
219
+ ### Host-side protocol test
220
+
221
+ ```ts
222
+ import {createDeviceFixture, SimulatedDevice} from 'react-native-web-serial-api/testing';
223
+
224
+ class Thermometer extends SimulatedDevice {
225
+ readonly usbVendorId = 0x10c4;
226
+ readonly usbProductId = 0xea60;
227
+
228
+ onOpen() {
229
+ this.send('READY\r\n');
230
+ }
231
+
232
+ emitTemperature(value: number) {
233
+ this.send(`temp=${value}\r\n`);
234
+ }
235
+ }
236
+
237
+ const {client, simulatedDevice, whenOpened} =
238
+ await createDeviceFixture(new Thermometer());
205
239
 
206
- const transport = new VirtualSerialTransport();
207
- transport.addDevice(new EchoDevice(), {hasPermission: true});
208
- const serial = new Serial(transport); // no native module, no hardware
240
+ await client.open({baudRate: 115200});
241
+ await whenOpened();
242
+ simulatedDevice.emitTemperature(21.5);
243
+ expect(await client.readLine()).toBe('temp=21.5');
244
+ await client.close();
209
245
  ```
210
246
 
247
+ For the full guide to `SerialClient`, `createDeviceFixture`, fault injection, `runTestSuite`, `compareTestResults`, `exposeSimulatedDevice`, and the conformance suites, see [TESTING.md](TESTING.md).
248
+
249
+ ## Remote serial over WebSocket
250
+
251
+ You can drive a real serial port plugged into another machine. This is useful for developing in a Chromium browser or an Android emulator that cannot see the USB device directly, or for remote debugging.
252
+
253
+ On the host machine, run the bundled CLI:
254
+
211
255
  ```sh
212
- npm test # unit + WPT conformance suites
213
- npm run test:coverage # coverage report (HTML in coverage/lcov-report)
256
+ npx -p react-native-web-serial-api expose-serial-websocket \
257
+ --port /dev/ttyUSB0 --baudrate 115200
258
+ # add --allow-remote to bind 0.0.0.0
259
+ ```
260
+
261
+ In the app:
262
+
263
+ ```ts
264
+ import {Serial} from 'react-native-web-serial-api';
265
+ import {WebSocketSerialTransport} from 'react-native-web-serial-api/websocket';
266
+
267
+ const serial = new Serial(new WebSocketSerialTransport('ws://localhost:8080'));
268
+ const [port] = await serial.getPorts();
269
+ await port.open({baudRate: 115200});
270
+ const writer = port.writable!.getWriter();
271
+ await writer.write(new TextEncoder().encode('Hello serial!\n'));
214
272
  ```
215
273
 
216
- See **[TESTING.md](TESTING.md)** for the full guide (authoring a `SerialDevice`, the conformance/WPT suites, on-device E2E, and coverage).
274
+ The example app includes this under Devices -> menu -> Remote serial (WebSocket).
275
+
276
+ The WebSocket carries raw serial bytes as binary frames and a small JSON control protocol as text frames (`setLineCoding`, `setSignals` / `getSignals`, `startReading` / `stopReading`, `flush`, `break`, and so on). By default the bridge binds to `localhost`. Use `--allow-remote` only on trusted networks.
277
+
278
+ ## Troubleshooting
279
+
280
+ - If Android never shows your device, confirm USB host support and the device filter.
281
+ - If `requestPort()` does nothing on web, make sure it is called from a button tap or other user gesture.
282
+ - If the app works on web but not Android, check that New Architecture is enabled and the device has Android USB permission.
283
+ - If `getPorts()` is empty on Android, unplug and replug the device, then grant permission again if Android revoked it.
284
+
285
+ ## How it works
286
+
287
+ The JavaScript layer in `src/` implements the Web Serial API on top of a thin TurboModule (`NativeUsbSerial`) whose native Android implementation in `android/src/main/java/dev/webserialapi/` wraps `usb-serial-for-android`. Reads and writes are bridged to `ReadableStream` / `WritableStream` via [`web-streams-polyfill`](https://github.com/MattiasBuelens/web-streams-polyfill) when the runtime does not already provide Web Streams globals.
217
288
 
218
289
  ## License
219
290