react-native-web-serial-api 0.0.3 → 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 (177) hide show
  1. package/README.md +198 -104
  2. package/TESTING.md +542 -0
  3. package/android/build.gradle +16 -2
  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 +58 -26
  8. package/lib/commonjs/UsbSerial.js.map +1 -1
  9. package/lib/commonjs/WebSerial.js +273 -77
  10. package/lib/commonjs/WebSerial.js.map +1 -1
  11. package/lib/commonjs/index.js +15 -3
  12. package/lib/commonjs/index.js.map +1 -1
  13. package/lib/commonjs/lib/dom-exception.js +176 -0
  14. package/lib/commonjs/lib/dom-exception.js.map +1 -0
  15. package/lib/commonjs/lib/event-target.js +140 -0
  16. package/lib/commonjs/lib/event-target.js.map +1 -0
  17. package/lib/commonjs/lib/promise.js +23 -0
  18. package/lib/commonjs/lib/promise.js.map +1 -0
  19. package/lib/commonjs/lib/web-streams.js +42 -0
  20. package/lib/commonjs/lib/web-streams.js.map +1 -0
  21. package/lib/commonjs/testing/device-fixture.js +70 -0
  22. package/lib/commonjs/testing/device-fixture.js.map +1 -0
  23. package/lib/commonjs/testing/expose.js +91 -0
  24. package/lib/commonjs/testing/expose.js.map +1 -0
  25. package/lib/commonjs/testing/harness.js +98 -0
  26. package/lib/commonjs/testing/harness.js.map +1 -0
  27. package/lib/commonjs/testing/in-memory-serial-transport.js +653 -0
  28. package/lib/commonjs/testing/in-memory-serial-transport.js.map +1 -0
  29. package/lib/commonjs/testing/index.js +153 -0
  30. package/lib/commonjs/testing/index.js.map +1 -0
  31. package/lib/commonjs/testing/install-in-memory-serial-transport.js +54 -0
  32. package/lib/commonjs/testing/install-in-memory-serial-transport.js.map +1 -0
  33. package/lib/commonjs/testing/serial-client.js +277 -0
  34. package/lib/commonjs/testing/serial-client.js.map +1 -0
  35. package/lib/commonjs/testing/simulated-device.js +164 -0
  36. package/lib/commonjs/testing/simulated-device.js.map +1 -0
  37. package/lib/commonjs/testing/test-suite.js +142 -0
  38. package/lib/commonjs/testing/test-suite.js.map +1 -0
  39. package/lib/commonjs/transport.js +61 -0
  40. package/lib/commonjs/transport.js.map +1 -0
  41. package/lib/commonjs/websocket/WebSocketSerialTransport.js +659 -0
  42. package/lib/commonjs/websocket/WebSocketSerialTransport.js.map +1 -0
  43. package/lib/commonjs/websocket/bridge.js +234 -0
  44. package/lib/commonjs/websocket/bridge.js.map +1 -0
  45. package/lib/commonjs/websocket/index.js +33 -0
  46. package/lib/commonjs/websocket/index.js.map +1 -0
  47. package/lib/commonjs/websocket/protocol.js +55 -0
  48. package/lib/commonjs/websocket/protocol.js.map +1 -0
  49. package/lib/commonjs/websocket/serial-device-bridge.js +130 -0
  50. package/lib/commonjs/websocket/serial-device-bridge.js.map +1 -0
  51. package/lib/typescript/src/UsbSerial.d.ts +24 -67
  52. package/lib/typescript/src/UsbSerial.d.ts.map +1 -1
  53. package/lib/typescript/src/WebSerial.d.ts +16 -7
  54. package/lib/typescript/src/WebSerial.d.ts.map +1 -1
  55. package/lib/typescript/src/index.d.ts +3 -1
  56. package/lib/typescript/src/index.d.ts.map +1 -1
  57. package/lib/typescript/src/lib/dom-exception.d.ts +100 -0
  58. package/lib/typescript/src/lib/dom-exception.d.ts.map +1 -0
  59. package/lib/typescript/src/lib/event-target.d.ts +55 -0
  60. package/lib/typescript/src/lib/event-target.d.ts.map +1 -0
  61. package/lib/typescript/src/lib/promise.d.ts +11 -0
  62. package/lib/typescript/src/lib/promise.d.ts.map +1 -0
  63. package/lib/typescript/src/lib/web-streams.d.ts +9 -0
  64. package/lib/typescript/src/lib/web-streams.d.ts.map +1 -0
  65. package/lib/typescript/src/testing/device-fixture.d.ts +70 -0
  66. package/lib/typescript/src/testing/device-fixture.d.ts.map +1 -0
  67. package/lib/typescript/src/testing/expose.d.ts +71 -0
  68. package/lib/typescript/src/testing/expose.d.ts.map +1 -0
  69. package/lib/typescript/src/testing/harness.d.ts +34 -0
  70. package/lib/typescript/src/testing/harness.d.ts.map +1 -0
  71. package/lib/typescript/src/testing/in-memory-serial-transport.d.ts +216 -0
  72. package/lib/typescript/src/testing/in-memory-serial-transport.d.ts.map +1 -0
  73. package/lib/typescript/src/testing/index.d.ts +33 -0
  74. package/lib/typescript/src/testing/index.d.ts.map +1 -0
  75. package/lib/typescript/src/testing/install-in-memory-serial-transport.d.ts +25 -0
  76. package/lib/typescript/src/testing/install-in-memory-serial-transport.d.ts.map +1 -0
  77. package/lib/typescript/src/testing/serial-client.d.ts +62 -0
  78. package/lib/typescript/src/testing/serial-client.d.ts.map +1 -0
  79. package/lib/typescript/src/testing/simulated-device.d.ts +127 -0
  80. package/lib/typescript/src/testing/simulated-device.d.ts.map +1 -0
  81. package/lib/typescript/src/testing/test-suite.d.ts +75 -0
  82. package/lib/typescript/src/testing/test-suite.d.ts.map +1 -0
  83. package/lib/typescript/src/transport.d.ts +131 -0
  84. package/lib/typescript/src/transport.d.ts.map +1 -0
  85. package/lib/typescript/src/websocket/WebSocketSerialTransport.d.ts +111 -0
  86. package/lib/typescript/src/websocket/WebSocketSerialTransport.d.ts.map +1 -0
  87. package/lib/typescript/src/websocket/bridge.d.ts +66 -0
  88. package/lib/typescript/src/websocket/bridge.d.ts.map +1 -0
  89. package/lib/typescript/src/websocket/index.d.ts +19 -0
  90. package/lib/typescript/src/websocket/index.d.ts.map +1 -0
  91. package/lib/typescript/src/websocket/protocol.d.ts +64 -0
  92. package/lib/typescript/src/websocket/protocol.d.ts.map +1 -0
  93. package/lib/typescript/src/websocket/serial-device-bridge.d.ts +32 -0
  94. package/lib/typescript/src/websocket/serial-device-bridge.d.ts.map +1 -0
  95. package/package.json +57 -3
  96. package/src/UsbSerial.ts +65 -90
  97. package/src/WebSerial.ts +351 -113
  98. package/src/index.ts +6 -8
  99. package/src/lib/dom-exception.ts +129 -60
  100. package/src/lib/event-target.ts +58 -21
  101. package/src/lib/promise.ts +7 -7
  102. package/src/lib/web-streams.ts +43 -0
  103. package/src/testing/device-fixture.ts +150 -0
  104. package/src/testing/expose.ts +147 -0
  105. package/src/testing/harness.ts +124 -0
  106. package/src/testing/in-memory-serial-transport.ts +840 -0
  107. package/src/testing/index.ts +90 -0
  108. package/src/testing/install-in-memory-serial-transport.ts +65 -0
  109. package/src/testing/serial-client.ts +313 -0
  110. package/src/testing/simulated-device.ts +193 -0
  111. package/src/testing/test-suite.ts +186 -0
  112. package/src/transport.ts +200 -0
  113. package/src/websocket/WebSocketSerialTransport.ts +796 -0
  114. package/src/websocket/bridge.ts +299 -0
  115. package/src/websocket/index.ts +38 -0
  116. package/src/websocket/protocol.ts +101 -0
  117. package/src/websocket/serial-device-bridge.ts +160 -0
  118. package/babel.config.js +0 -3
  119. package/biome.json +0 -35
  120. package/example/.watchmanconfig +0 -1
  121. package/example/App.tsx +0 -71
  122. package/example/__tests__/App.test.tsx +0 -16
  123. package/example/__tests__/connectEvents.test.tsx +0 -81
  124. package/example/__tests__/getPorts.test.tsx +0 -140
  125. package/example/android/app/build.gradle +0 -120
  126. package/example/android/app/debug.keystore +0 -0
  127. package/example/android/app/proguard-rules.pro +0 -10
  128. package/example/android/app/src/debug/AndroidManifest.xml +0 -9
  129. package/example/android/app/src/main/AndroidManifest.xml +0 -38
  130. package/example/android/app/src/main/java/dev/uzlopak/MainActivity.kt +0 -22
  131. package/example/android/app/src/main/java/dev/uzlopak/MainApplication.kt +0 -41
  132. package/example/android/app/src/main/res/drawable/rn_edit_text_material.xml +0 -37
  133. package/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png +0 -0
  134. package/example/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png +0 -0
  135. package/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png +0 -0
  136. package/example/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png +0 -0
  137. package/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png +0 -0
  138. package/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png +0 -0
  139. package/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png +0 -0
  140. package/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png +0 -0
  141. package/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png +0 -0
  142. package/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png +0 -0
  143. package/example/android/app/src/main/res/values/strings.xml +0 -3
  144. package/example/android/app/src/main/res/values/styles.xml +0 -9
  145. package/example/android/build.gradle +0 -22
  146. package/example/android/gradle/wrapper/gradle-wrapper.jar +0 -0
  147. package/example/android/gradle/wrapper/gradle-wrapper.properties +0 -7
  148. package/example/android/gradle.properties +0 -47
  149. package/example/android/gradlew +0 -252
  150. package/example/android/gradlew.bat +0 -94
  151. package/example/android/settings.gradle +0 -6
  152. package/example/app.json +0 -4
  153. package/example/babel.config.js +0 -21
  154. package/example/biome.json +0 -47
  155. package/example/deploy.sh +0 -11
  156. package/example/index.html +0 -26
  157. package/example/index.js +0 -9
  158. package/example/index.web.js +0 -8
  159. package/example/jest.config.js +0 -12
  160. package/example/metro.config.js +0 -58
  161. package/example/package-lock.json +0 -14510
  162. package/example/package.json +0 -48
  163. package/example/react-native.config.js +0 -17
  164. package/example/src/components/AppBar.tsx +0 -73
  165. package/example/src/components/Menu.tsx +0 -90
  166. package/example/src/components/SingleChoiceDialog.tsx +0 -120
  167. package/example/src/screens/ConnectScreen.tsx +0 -195
  168. package/example/src/screens/DevicesScreen.tsx +0 -252
  169. package/example/src/screens/TerminalScreen.tsx +0 -572
  170. package/example/src/settings.ts +0 -43
  171. package/example/src/theme.ts +0 -19
  172. package/example/src/util/TextUtil.ts +0 -129
  173. package/example/tsconfig.json +0 -10
  174. package/example/vite.config.mjs +0 -55
  175. package/scripts/deploy-release.sh +0 -127
  176. package/tsconfig.build.json +0 -7
  177. package/tsconfig.json +0 -20
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
26
-
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.
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.
44
32
 
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,127 +56,235 @@ async function run() {
70
56
  }
71
57
  ```
72
58
 
73
- ### Control & status signals
59
+ ## Android setup
74
60
 
75
- ```ts
76
- await port.setSignals({dataTerminalReady: true, requestToSend: false});
77
- const {clearToSend, dataCarrierDetect, ringIndicator, dataSetReady} =
78
- await port.getSignals();
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.
62
+
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>
79
74
  ```
80
75
 
81
- ### 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`.
82
100
 
83
101
  ```ts
84
- serial.addEventListener('connect', () => console.log('device attached'));
85
- serial.addEventListener('disconnect', () => console.log('device detached'));
102
+ import {serial} from 'react-native-web-serial-api';
86
103
 
87
- port.addEventListener('disconnect', () => console.log('this port went away'));
104
+ const ports = await serial.getPorts();
88
105
  ```
89
106
 
90
- On Android, `serial` fires `connect` when a USB device is **attached** _and_ when
91
- the app is **granted USB permission** for a device (the latter matters because
92
- Android revokes permission on unplug, so a re-attached device only becomes
93
- accessible — and shows up in `getPorts()` — once permission is re-granted).
94
- Simply subscribing with `serial.addEventListener('connect', …)` is enough to
95
- receive these; you don't need to call `getPorts()` first. A common pattern is to
96
- re-run `getPorts()` on every `connect`/`disconnect` to keep a device list fresh.
107
+ ### Permissions
108
+
109
+ There are two different permission concepts:
97
110
 
98
- ### Listing already-permitted ports
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.
113
+
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.
99
115
 
100
116
  ```ts
101
117
  const ports = await serial.getPorts();
118
+ const port = await serial.requestPort();
102
119
  ```
103
120
 
104
- ## Permission model (Android vs. Web Serial)
105
-
106
- There are two distinct notions of "permission" in play, and they behave
107
- differently on Android than in the browser:
108
-
109
- - **Web Serial permission grant** — `serial.requestPort()`. In the browser this
110
- records a site-level grant for the chosen port; on Android it shows a native
111
- picker and requests the Android USB permission for the selected device. This
112
- is the mechanism for gaining access to a device you don't have access to yet,
113
- and it is **unchanged** by anything below.
114
- - **Native Android USB permission** — `UsbManager` permission for a device.
115
- This can be granted **outside** the app entirely: when you plug a device in,
116
- Android may show its own _"Open <app> to handle this USB device? / use by
117
- default for this device"_ dialog, or the app may have been launched via a
118
- `USB_DEVICE_ATTACHED` intent filter. In those cases the app already holds USB
119
- permission without ever calling `requestPort()`.
120
-
121
- **`serial.getPorts()` in Android/native mode** returns **every probed
122
- USB-serial port the app can currently access through Android USB permission** —
123
- regardless of how that permission was obtained. So a device granted via the
124
- system attach dialog appears in `getPorts()` even though `requestPort()` was
125
- never called for it. Probed devices the app does **not** yet have permission for
126
- are excluded; use `requestPort()` to gain access to those.
121
+ ### Connect and disconnect events
127
122
 
128
123
  ```ts
129
- // All devices accessible right now (natively-granted OR previously requested):
130
- const ports = await serial.getPorts();
124
+ serial.addEventListener('connect', () => console.log('device attached'));
125
+ serial.addEventListener('disconnect', () => console.log('device detached'));
131
126
 
132
- // Gain access to a device you don't have permission for yet:
133
- 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();
134
138
  ```
135
139
 
136
- On the **web**, `getPorts()` returns the ports the user has previously granted
137
- the site via `requestPort()` (the browser's persistent permission store) — the
138
- native "attach dialog" notion does not apply.
140
+ ### Browser-only option note
139
141
 
140
- > Note: this is a deliberate, Android-appropriate reading of the Web Serial
141
- > spec's `getPorts()` ("ports the site has been granted access to"). On Android
142
- > the unit of access is the OS-level USB permission, so a device the OS has
143
- > already authorized for the app is, by definition, one the app has been
144
- > granted access to.
142
+ In Android USB mode, `allowedBluetoothServiceClassIds` is not supported by `requestPort()` and will throw a `TypeError` if provided.
145
143
 
146
- ## API
144
+ ## API reference
147
145
 
148
146
  The package exposes:
149
147
 
150
148
  | Export | Description |
151
149
  | --- | --- |
152
- | `serial` | A ready-to-use `Serial` instance (`navigator.serial` equivalent). |
150
+ | `serial` | A ready-to-use `Serial` instance. |
153
151
  | `Serial`, `SerialPort` | The Web Serial API classes. |
154
152
  | `UsbSerial` | Lower-level access to the raw USB-serial TurboModule (Android only). |
155
- | `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. |
156
154
  | Types | `SerialOptions`, `SerialOutputSignals`, `SerialInputSignals`, `SerialPortInfo`, `SerialPortFilter`, `SerialPortRequestOptions`. |
157
155
 
158
156
  ## Example app
159
157
 
160
- 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
161
170
 
162
171
  ```sh
163
- # install the library's build tooling
164
172
  npm install
165
-
166
- # install and run the example on Android
167
173
  cd example
168
174
  npm install
169
175
  npm run android
170
176
  ```
171
177
 
172
- 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:
173
179
 
174
- - **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.
175
- - **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.
176
- - 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.
177
183
 
178
184
  ### Run the example in the browser
179
185
 
180
- 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.
181
187
 
182
188
  ```sh
183
189
  cd example
184
190
  npm install
185
- npm run web # dev server at http://localhost:5173
186
- # npm run web:build # production build into example/dist
191
+ npm run web
192
+ # npm run web:build
193
+ ```
194
+
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.
196
+
197
+ ## Testing and simulation
198
+
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.
200
+
201
+ ### Fast in-memory test
202
+
203
+ ```ts
204
+ import {Serial} from 'react-native-web-serial-api';
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);
187
217
  ```
188
218
 
189
- > 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.
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());
239
+
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();
245
+ ```
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
+
255
+ ```sh
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'));
272
+ ```
273
+
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.
190
284
 
191
285
  ## How it works
192
286
 
193
- 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).
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.
194
288
 
195
289
  ## License
196
290