react-native-web-serial-api 0.0.3 → 0.1.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.
- package/README.md +23 -0
- package/TESTING.md +301 -0
- package/android/build.gradle +2 -2
- package/lib/commonjs/UsbSerial.js +58 -26
- package/lib/commonjs/UsbSerial.js.map +1 -1
- package/lib/commonjs/WebSerial.js +169 -57
- package/lib/commonjs/WebSerial.js.map +1 -1
- package/lib/commonjs/index.js +13 -1
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/lib/dom-exception.js +176 -0
- package/lib/commonjs/lib/dom-exception.js.map +1 -0
- package/lib/commonjs/lib/event-target.js +138 -0
- package/lib/commonjs/lib/event-target.js.map +1 -0
- package/lib/commonjs/lib/promise.js +23 -0
- package/lib/commonjs/lib/promise.js.map +1 -0
- package/lib/commonjs/testing/index.js +70 -0
- package/lib/commonjs/testing/index.js.map +1 -0
- package/lib/commonjs/testing/install.js +54 -0
- package/lib/commonjs/testing/install.js.map +1 -0
- package/lib/commonjs/testing/serial-device.js +164 -0
- package/lib/commonjs/testing/serial-device.js.map +1 -0
- package/lib/commonjs/testing/virtual-serial.js +615 -0
- package/lib/commonjs/testing/virtual-serial.js.map +1 -0
- package/lib/commonjs/transport.js +61 -0
- package/lib/commonjs/transport.js.map +1 -0
- package/lib/typescript/src/UsbSerial.d.ts +24 -67
- package/lib/typescript/src/UsbSerial.d.ts.map +1 -1
- package/lib/typescript/src/WebSerial.d.ts +11 -2
- package/lib/typescript/src/WebSerial.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +2 -0
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/lib/dom-exception.d.ts +100 -0
- package/lib/typescript/src/lib/dom-exception.d.ts.map +1 -0
- package/lib/typescript/src/lib/event-target.d.ts +53 -0
- package/lib/typescript/src/lib/event-target.d.ts.map +1 -0
- package/lib/typescript/src/lib/promise.d.ts +11 -0
- package/lib/typescript/src/lib/promise.d.ts.map +1 -0
- package/lib/typescript/src/testing/index.d.ts +23 -0
- package/lib/typescript/src/testing/index.d.ts.map +1 -0
- package/lib/typescript/src/testing/install.d.ts +25 -0
- package/lib/typescript/src/testing/install.d.ts.map +1 -0
- package/lib/typescript/src/testing/serial-device.d.ts +127 -0
- package/lib/typescript/src/testing/serial-device.d.ts.map +1 -0
- package/lib/typescript/src/testing/virtual-serial.d.ts +205 -0
- package/lib/typescript/src/testing/virtual-serial.d.ts.map +1 -0
- package/lib/typescript/src/transport.d.ts +131 -0
- package/lib/typescript/src/transport.d.ts.map +1 -0
- package/package.json +38 -2
- package/src/UsbSerial.ts +65 -90
- package/src/WebSerial.ts +227 -88
- package/src/index.ts +2 -7
- package/src/lib/dom-exception.ts +129 -60
- package/src/lib/event-target.ts +46 -21
- package/src/lib/promise.ts +7 -7
- package/src/testing/index.ts +42 -0
- package/src/testing/install.ts +65 -0
- package/src/testing/serial-device.ts +193 -0
- package/src/testing/virtual-serial.ts +801 -0
- package/src/transport.ts +200 -0
- package/babel.config.js +0 -3
- package/biome.json +0 -35
- package/example/.watchmanconfig +0 -1
- package/example/App.tsx +0 -71
- package/example/__tests__/App.test.tsx +0 -16
- package/example/__tests__/connectEvents.test.tsx +0 -81
- package/example/__tests__/getPorts.test.tsx +0 -140
- package/example/android/app/build.gradle +0 -120
- package/example/android/app/debug.keystore +0 -0
- package/example/android/app/proguard-rules.pro +0 -10
- package/example/android/app/src/debug/AndroidManifest.xml +0 -9
- package/example/android/app/src/main/AndroidManifest.xml +0 -38
- package/example/android/app/src/main/java/dev/uzlopak/MainActivity.kt +0 -22
- package/example/android/app/src/main/java/dev/uzlopak/MainApplication.kt +0 -41
- package/example/android/app/src/main/res/drawable/rn_edit_text_material.xml +0 -37
- package/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png +0 -0
- package/example/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png +0 -0
- package/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png +0 -0
- package/example/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png +0 -0
- package/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png +0 -0
- package/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png +0 -0
- package/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png +0 -0
- package/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png +0 -0
- package/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png +0 -0
- package/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png +0 -0
- package/example/android/app/src/main/res/values/strings.xml +0 -3
- package/example/android/app/src/main/res/values/styles.xml +0 -9
- package/example/android/build.gradle +0 -22
- package/example/android/gradle/wrapper/gradle-wrapper.jar +0 -0
- package/example/android/gradle/wrapper/gradle-wrapper.properties +0 -7
- package/example/android/gradle.properties +0 -47
- package/example/android/gradlew +0 -252
- package/example/android/gradlew.bat +0 -94
- package/example/android/settings.gradle +0 -6
- package/example/app.json +0 -4
- package/example/babel.config.js +0 -21
- package/example/biome.json +0 -47
- package/example/deploy.sh +0 -11
- package/example/index.html +0 -26
- package/example/index.js +0 -9
- package/example/index.web.js +0 -8
- package/example/jest.config.js +0 -12
- package/example/metro.config.js +0 -58
- package/example/package-lock.json +0 -14510
- package/example/package.json +0 -48
- package/example/react-native.config.js +0 -17
- package/example/src/components/AppBar.tsx +0 -73
- package/example/src/components/Menu.tsx +0 -90
- package/example/src/components/SingleChoiceDialog.tsx +0 -120
- package/example/src/screens/ConnectScreen.tsx +0 -195
- package/example/src/screens/DevicesScreen.tsx +0 -252
- package/example/src/screens/TerminalScreen.tsx +0 -572
- package/example/src/settings.ts +0 -43
- package/example/src/theme.ts +0 -19
- package/example/src/util/TextUtil.ts +0 -129
- package/example/tsconfig.json +0 -10
- package/example/vite.config.mjs +0 -55
- package/scripts/deploy-release.sh +0 -127
- package/tsconfig.build.json +0 -7
- package/tsconfig.json +0 -20
|
@@ -1,572 +0,0 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import {
|
|
3
|
-
ScrollView,
|
|
4
|
-
StyleSheet,
|
|
5
|
-
Text,
|
|
6
|
-
TextInput,
|
|
7
|
-
TouchableOpacity,
|
|
8
|
-
View,
|
|
9
|
-
} from 'react-native';
|
|
10
|
-
import type {SerialPort} from 'react-native-web-serial-api';
|
|
11
|
-
import {AppBar} from '../components/AppBar';
|
|
12
|
-
import {SingleChoiceDialog} from '../components/SingleChoiceDialog';
|
|
13
|
-
import {type ConnectionSettings, toSerialOptions} from '../settings';
|
|
14
|
-
import {colors} from '../theme';
|
|
15
|
-
import * as TextUtil from '../util/TextUtil';
|
|
16
|
-
|
|
17
|
-
type Span = {text: string; color: string; caret: boolean};
|
|
18
|
-
type Connected = 'False' | 'Pending' | 'True';
|
|
19
|
-
type SendBtnState = 'Idle' | 'Disabled';
|
|
20
|
-
|
|
21
|
-
const NEWLINES = [
|
|
22
|
-
{label: 'CR+LF', value: TextUtil.NEWLINE_CRLF},
|
|
23
|
-
{label: 'LF', value: TextUtil.NEWLINE_LF},
|
|
24
|
-
{label: '<none>', value: ''},
|
|
25
|
-
];
|
|
26
|
-
const POLL_MS = 200;
|
|
27
|
-
const MAX_SPANS = 2000;
|
|
28
|
-
|
|
29
|
-
type Props = {
|
|
30
|
-
port: SerialPort;
|
|
31
|
-
settings: ConnectionSettings;
|
|
32
|
-
onBack: () => void;
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
export function TerminalScreen({port, settings, onBack}: Props) {
|
|
36
|
-
const [log, setLog] = React.useState<Span[]>([]);
|
|
37
|
-
const [input, setInput] = React.useState('');
|
|
38
|
-
const [connected, setConnected] = React.useState<Connected>('False');
|
|
39
|
-
// HEX mode is on by default (per product requirement).
|
|
40
|
-
const [hexEnabled, setHexEnabled] = React.useState(true);
|
|
41
|
-
const [newline, setNewline] = React.useState<string>(TextUtil.NEWLINE_CRLF);
|
|
42
|
-
const [showControlLines, setShowControlLines] = React.useState(false);
|
|
43
|
-
const [sendBtn, setSendBtn] = React.useState<SendBtnState>('Idle');
|
|
44
|
-
const [lines, setLines] = React.useState({
|
|
45
|
-
rts: false,
|
|
46
|
-
cts: false,
|
|
47
|
-
dtr: false,
|
|
48
|
-
dsr: false,
|
|
49
|
-
cd: false,
|
|
50
|
-
ri: false,
|
|
51
|
-
});
|
|
52
|
-
const [newlineDialog, setNewlineDialog] = React.useState(false);
|
|
53
|
-
|
|
54
|
-
// refs read by long-lived async loops / callbacks
|
|
55
|
-
const connectedRef = React.useRef<Connected>('False');
|
|
56
|
-
const hexRef = React.useRef(hexEnabled);
|
|
57
|
-
const newlineRef = React.useRef(newline);
|
|
58
|
-
const showCLRef = React.useRef(showControlLines);
|
|
59
|
-
const settingsRef = React.useRef(settings);
|
|
60
|
-
const pendingNewlineRef = React.useRef(false);
|
|
61
|
-
const rtsRef = React.useRef(false);
|
|
62
|
-
const dtrRef = React.useRef(false);
|
|
63
|
-
const writerRef =
|
|
64
|
-
React.useRef<WritableStreamDefaultWriter<Uint8Array> | null>(null);
|
|
65
|
-
const readerRef =
|
|
66
|
-
React.useRef<ReadableStreamDefaultReader<Uint8Array> | null>(null);
|
|
67
|
-
const pollRef = React.useRef<ReturnType<typeof setInterval> | null>(null);
|
|
68
|
-
const scrollRef = React.useRef<ScrollView>(null);
|
|
69
|
-
// Stable refs to the latest connect/teardown so the USB attach/detach
|
|
70
|
-
// listeners (registered once) never call stale closures.
|
|
71
|
-
const connectRef = React.useRef<() => Promise<void>>(async () => {});
|
|
72
|
-
const teardownRef = React.useRef<(closePort: boolean) => Promise<void>>(
|
|
73
|
-
async () => {},
|
|
74
|
-
);
|
|
75
|
-
|
|
76
|
-
React.useEffect(() => {
|
|
77
|
-
hexRef.current = hexEnabled;
|
|
78
|
-
}, [hexEnabled]);
|
|
79
|
-
React.useEffect(() => {
|
|
80
|
-
newlineRef.current = newline;
|
|
81
|
-
}, [newline]);
|
|
82
|
-
React.useEffect(() => {
|
|
83
|
-
showCLRef.current = showControlLines;
|
|
84
|
-
}, [showControlLines]);
|
|
85
|
-
React.useEffect(() => {
|
|
86
|
-
settingsRef.current = settings;
|
|
87
|
-
}, [settings]);
|
|
88
|
-
|
|
89
|
-
const append = React.useCallback((spans: Span[]) => {
|
|
90
|
-
setLog(prev => {
|
|
91
|
-
const next = prev.concat(spans);
|
|
92
|
-
return next.length > MAX_SPANS
|
|
93
|
-
? next.slice(next.length - MAX_SPANS)
|
|
94
|
-
: next;
|
|
95
|
-
});
|
|
96
|
-
}, []);
|
|
97
|
-
|
|
98
|
-
const status = React.useCallback(
|
|
99
|
-
(s: string) =>
|
|
100
|
-
append([{text: `${s}\n`, color: colors.statusText, caret: false}]),
|
|
101
|
-
[append],
|
|
102
|
-
);
|
|
103
|
-
|
|
104
|
-
// ported from TerminalFragment.receive()
|
|
105
|
-
const receive = React.useCallback((data: Uint8Array) => {
|
|
106
|
-
if (hexRef.current) {
|
|
107
|
-
setLog(prev => {
|
|
108
|
-
const next = prev.concat([
|
|
109
|
-
{
|
|
110
|
-
text: `${TextUtil.toHexString(data)}\n`,
|
|
111
|
-
color: colors.receiveText,
|
|
112
|
-
caret: false,
|
|
113
|
-
},
|
|
114
|
-
]);
|
|
115
|
-
return next.length > MAX_SPANS
|
|
116
|
-
? next.slice(next.length - MAX_SPANS)
|
|
117
|
-
: next;
|
|
118
|
-
});
|
|
119
|
-
return;
|
|
120
|
-
}
|
|
121
|
-
let msg = TextUtil.bytesToString(data);
|
|
122
|
-
const nl = newlineRef.current;
|
|
123
|
-
let dropCaret = false;
|
|
124
|
-
if (nl === TextUtil.NEWLINE_CRLF && msg.length > 0) {
|
|
125
|
-
// don't show CR as ^M directly before LF
|
|
126
|
-
msg = msg.split('\r\n').join('\n');
|
|
127
|
-
if (pendingNewlineRef.current && msg[0] === '\n') {
|
|
128
|
-
dropCaret = true; // CR/LF arrived in separate chunks -> drop the "^M"
|
|
129
|
-
}
|
|
130
|
-
pendingNewlineRef.current = msg[msg.length - 1] === '\r';
|
|
131
|
-
}
|
|
132
|
-
const runs = TextUtil.toCaretRuns(msg, nl.length !== 0).map(r => ({
|
|
133
|
-
text: r.text,
|
|
134
|
-
color: colors.receiveText,
|
|
135
|
-
caret: r.caret,
|
|
136
|
-
}));
|
|
137
|
-
setLog(prev => {
|
|
138
|
-
let next = prev;
|
|
139
|
-
if (dropCaret) {
|
|
140
|
-
const last = prev[prev.length - 1];
|
|
141
|
-
if (last?.caret && last.text === '^M') {
|
|
142
|
-
next = prev.slice(0, prev.length - 1);
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
next = next.concat(runs);
|
|
146
|
-
return next.length > MAX_SPANS
|
|
147
|
-
? next.slice(next.length - MAX_SPANS)
|
|
148
|
-
: next;
|
|
149
|
-
});
|
|
150
|
-
}, []);
|
|
151
|
-
|
|
152
|
-
const stopPoll = React.useCallback(() => {
|
|
153
|
-
if (pollRef.current) {
|
|
154
|
-
clearInterval(pollRef.current);
|
|
155
|
-
pollRef.current = null;
|
|
156
|
-
}
|
|
157
|
-
}, []);
|
|
158
|
-
|
|
159
|
-
const startPoll = React.useCallback(() => {
|
|
160
|
-
stopPoll();
|
|
161
|
-
if (!(showCLRef.current || settingsRef.current.flowControl !== 'none')) {
|
|
162
|
-
return;
|
|
163
|
-
}
|
|
164
|
-
pollRef.current = setInterval(async () => {
|
|
165
|
-
if (connectedRef.current !== 'True') {
|
|
166
|
-
return;
|
|
167
|
-
}
|
|
168
|
-
try {
|
|
169
|
-
const sig = await port.getSignals();
|
|
170
|
-
if (showCLRef.current) {
|
|
171
|
-
setLines({
|
|
172
|
-
rts: rtsRef.current,
|
|
173
|
-
dtr: dtrRef.current,
|
|
174
|
-
cts: sig.clearToSend,
|
|
175
|
-
dsr: sig.dataSetReady,
|
|
176
|
-
cd: sig.dataCarrierDetect,
|
|
177
|
-
ri: sig.ringIndicator,
|
|
178
|
-
});
|
|
179
|
-
}
|
|
180
|
-
if (settingsRef.current.flowControl === 'hardware') {
|
|
181
|
-
setSendBtn(sig.clearToSend ? 'Idle' : 'Disabled');
|
|
182
|
-
}
|
|
183
|
-
} catch {
|
|
184
|
-
// ignore transient signal read errors (e.g. mid-detach)
|
|
185
|
-
}
|
|
186
|
-
}, POLL_MS);
|
|
187
|
-
}, [port, stopPoll]);
|
|
188
|
-
|
|
189
|
-
const readLoop = React.useCallback(async () => {
|
|
190
|
-
try {
|
|
191
|
-
// Non-null: readLoop only runs after open() resolves, so readable is set.
|
|
192
|
-
const reader = port.readable!.getReader();
|
|
193
|
-
readerRef.current = reader;
|
|
194
|
-
while (true) {
|
|
195
|
-
const {value, done} = await reader.read();
|
|
196
|
-
if (done) {
|
|
197
|
-
break;
|
|
198
|
-
}
|
|
199
|
-
if (value?.length) {
|
|
200
|
-
receive(value);
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
} catch {
|
|
204
|
-
// Read errors are handled by the disconnect listener / teardown.
|
|
205
|
-
} finally {
|
|
206
|
-
try {
|
|
207
|
-
readerRef.current?.releaseLock();
|
|
208
|
-
} catch {}
|
|
209
|
-
readerRef.current = null;
|
|
210
|
-
}
|
|
211
|
-
}, [port, receive]);
|
|
212
|
-
|
|
213
|
-
/**
|
|
214
|
-
* Release this screen's reader/writer + poll loop. Idempotent. When
|
|
215
|
-
* `closePort` is true (intentional leave) we also close the port; on a
|
|
216
|
-
* physical detach the library has already reset the port, so we must NOT
|
|
217
|
-
* touch the device (closePort=false) — that's what previously produced the
|
|
218
|
-
* repeating "USB get_status request failed".
|
|
219
|
-
*/
|
|
220
|
-
const teardown = React.useCallback(
|
|
221
|
-
async (closePort: boolean) => {
|
|
222
|
-
connectedRef.current = 'False';
|
|
223
|
-
setConnected('False');
|
|
224
|
-
stopPoll(); // stop control-line polling FIRST (no more control transfers)
|
|
225
|
-
setSendBtn('Idle');
|
|
226
|
-
try {
|
|
227
|
-
if (readerRef.current) {
|
|
228
|
-
await readerRef.current.cancel().catch(() => {});
|
|
229
|
-
}
|
|
230
|
-
} catch {}
|
|
231
|
-
try {
|
|
232
|
-
if (writerRef.current) {
|
|
233
|
-
await writerRef.current.close().catch(() => {});
|
|
234
|
-
writerRef.current.releaseLock();
|
|
235
|
-
}
|
|
236
|
-
} catch {}
|
|
237
|
-
writerRef.current = null;
|
|
238
|
-
if (closePort) {
|
|
239
|
-
try {
|
|
240
|
-
await port.close();
|
|
241
|
-
} catch {}
|
|
242
|
-
}
|
|
243
|
-
},
|
|
244
|
-
[port, stopPoll],
|
|
245
|
-
);
|
|
246
|
-
|
|
247
|
-
React.useEffect(() => {
|
|
248
|
-
teardownRef.current = teardown;
|
|
249
|
-
}, [teardown]);
|
|
250
|
-
|
|
251
|
-
const connect = React.useCallback(async () => {
|
|
252
|
-
// Idempotent: ignore overlapping triggers. On re-attach the device may emit
|
|
253
|
-
// several "connect" events (USB attach + permission-grant), and port.open()
|
|
254
|
-
// itself awaits the permission dialog — so guard against re-entrancy that
|
|
255
|
-
// would otherwise cause a double-open ("port already open") and leave us
|
|
256
|
-
// stuck in the failed/closed state.
|
|
257
|
-
if (connectedRef.current !== 'False') {
|
|
258
|
-
return;
|
|
259
|
-
}
|
|
260
|
-
connectedRef.current = 'Pending';
|
|
261
|
-
setConnected('Pending');
|
|
262
|
-
try {
|
|
263
|
-
await port.open(toSerialOptions(settingsRef.current));
|
|
264
|
-
connectedRef.current = 'True';
|
|
265
|
-
setConnected('True');
|
|
266
|
-
status('connected');
|
|
267
|
-
// Non-null: writable is guaranteed set once open() has resolved.
|
|
268
|
-
writerRef.current = port.writable!.getWriter();
|
|
269
|
-
readLoop();
|
|
270
|
-
} catch (e: any) {
|
|
271
|
-
status(`connection failed: ${e?.message ?? e}`);
|
|
272
|
-
connectedRef.current = 'False';
|
|
273
|
-
setConnected('False');
|
|
274
|
-
}
|
|
275
|
-
}, [port, status, readLoop]);
|
|
276
|
-
|
|
277
|
-
React.useEffect(() => {
|
|
278
|
-
connectRef.current = connect;
|
|
279
|
-
}, [connect]);
|
|
280
|
-
|
|
281
|
-
// Connect on mount; close the port on unmount (intentional leave).
|
|
282
|
-
React.useEffect(() => {
|
|
283
|
-
connectRef.current();
|
|
284
|
-
return () => {
|
|
285
|
-
teardownRef.current(true);
|
|
286
|
-
};
|
|
287
|
-
}, []);
|
|
288
|
-
|
|
289
|
-
// Physical detach / re-attach. The library fires these on the SAME SerialPort
|
|
290
|
-
// instance (it re-associates the new Android deviceId on re-attach). We keep
|
|
291
|
-
// the log across reconnects.
|
|
292
|
-
React.useEffect(() => {
|
|
293
|
-
const onDisconnect = () => {
|
|
294
|
-
if (connectedRef.current === 'False') {
|
|
295
|
-
return;
|
|
296
|
-
}
|
|
297
|
-
status('device disconnected');
|
|
298
|
-
teardownRef.current(false);
|
|
299
|
-
};
|
|
300
|
-
const onReconnect = () => {
|
|
301
|
-
status('reconnected');
|
|
302
|
-
connectRef.current();
|
|
303
|
-
};
|
|
304
|
-
port.addEventListener('disconnect', onDisconnect);
|
|
305
|
-
port.addEventListener('connect', onReconnect);
|
|
306
|
-
return () => {
|
|
307
|
-
port.removeEventListener('disconnect', onDisconnect);
|
|
308
|
-
port.removeEventListener('connect', onReconnect);
|
|
309
|
-
};
|
|
310
|
-
}, [port, status]);
|
|
311
|
-
|
|
312
|
-
// start/stop the control-line poll loop
|
|
313
|
-
React.useEffect(() => {
|
|
314
|
-
if (
|
|
315
|
-
connected === 'True' &&
|
|
316
|
-
(showControlLines || settings.flowControl !== 'none')
|
|
317
|
-
) {
|
|
318
|
-
startPoll();
|
|
319
|
-
} else {
|
|
320
|
-
stopPoll();
|
|
321
|
-
}
|
|
322
|
-
if (settings.flowControl === 'none') {
|
|
323
|
-
setSendBtn('Idle');
|
|
324
|
-
}
|
|
325
|
-
}, [connected, showControlLines, settings.flowControl, startPoll, stopPoll]);
|
|
326
|
-
|
|
327
|
-
const onChangeInput = (t: string) =>
|
|
328
|
-
setInput(hexRef.current ? TextUtil.formatHexInput(t) : t);
|
|
329
|
-
|
|
330
|
-
const doSend = async () => {
|
|
331
|
-
if (connectedRef.current !== 'True') {
|
|
332
|
-
status('not connected');
|
|
333
|
-
return;
|
|
334
|
-
}
|
|
335
|
-
let data: Uint8Array;
|
|
336
|
-
let echo: string;
|
|
337
|
-
if (hexRef.current) {
|
|
338
|
-
const body = TextUtil.fromHexString(input);
|
|
339
|
-
const nl = TextUtil.stringToBytes(newlineRef.current);
|
|
340
|
-
data = new Uint8Array(body.length + nl.length);
|
|
341
|
-
data.set(body, 0);
|
|
342
|
-
data.set(nl, body.length);
|
|
343
|
-
echo = TextUtil.toHexString(data);
|
|
344
|
-
} else {
|
|
345
|
-
echo = input;
|
|
346
|
-
data = TextUtil.stringToBytes(input + newlineRef.current);
|
|
347
|
-
}
|
|
348
|
-
append([{text: `${echo}\n`, color: colors.sendText, caret: false}]);
|
|
349
|
-
try {
|
|
350
|
-
await writerRef.current?.write(data);
|
|
351
|
-
} catch (e: any) {
|
|
352
|
-
status(`write failed: ${e?.message ?? e}`);
|
|
353
|
-
}
|
|
354
|
-
};
|
|
355
|
-
|
|
356
|
-
const toggleRts = async () => {
|
|
357
|
-
if (connectedRef.current !== 'True') {
|
|
358
|
-
status('not connected');
|
|
359
|
-
return;
|
|
360
|
-
}
|
|
361
|
-
const v = !rtsRef.current;
|
|
362
|
-
rtsRef.current = v;
|
|
363
|
-
setLines(l => ({...l, rts: v}));
|
|
364
|
-
try {
|
|
365
|
-
await port.setSignals({requestToSend: v});
|
|
366
|
-
} catch (e: any) {
|
|
367
|
-
status(`setRTS failed: ${e?.message ?? e}`);
|
|
368
|
-
}
|
|
369
|
-
};
|
|
370
|
-
|
|
371
|
-
const toggleDtr = async () => {
|
|
372
|
-
if (connectedRef.current !== 'True') {
|
|
373
|
-
status('not connected');
|
|
374
|
-
return;
|
|
375
|
-
}
|
|
376
|
-
const v = !dtrRef.current;
|
|
377
|
-
dtrRef.current = v;
|
|
378
|
-
setLines(l => ({...l, dtr: v}));
|
|
379
|
-
try {
|
|
380
|
-
await port.setSignals({dataTerminalReady: v});
|
|
381
|
-
} catch (e: any) {
|
|
382
|
-
status(`setDTR failed: ${e?.message ?? e}`);
|
|
383
|
-
}
|
|
384
|
-
};
|
|
385
|
-
|
|
386
|
-
const sendBreak = async () => {
|
|
387
|
-
if (connectedRef.current !== 'True') {
|
|
388
|
-
status('not connected');
|
|
389
|
-
return;
|
|
390
|
-
}
|
|
391
|
-
try {
|
|
392
|
-
await port.setSignals({break: true});
|
|
393
|
-
await new Promise<void>(r => setTimeout(r, 100));
|
|
394
|
-
status('send BREAK');
|
|
395
|
-
await port.setSignals({break: false});
|
|
396
|
-
} catch (e: any) {
|
|
397
|
-
status(`send BREAK failed: ${e?.message ?? e}`);
|
|
398
|
-
}
|
|
399
|
-
};
|
|
400
|
-
|
|
401
|
-
const toggleHex = () => {
|
|
402
|
-
setHexEnabled(h => !h);
|
|
403
|
-
setInput('');
|
|
404
|
-
};
|
|
405
|
-
|
|
406
|
-
const menu = [
|
|
407
|
-
{key: 'clear', title: 'Clear', onPress: () => setLog([])},
|
|
408
|
-
{key: 'newline', title: 'Newline', onPress: () => setNewlineDialog(true)},
|
|
409
|
-
{
|
|
410
|
-
key: 'hex',
|
|
411
|
-
title: 'HEX Mode',
|
|
412
|
-
checkable: true,
|
|
413
|
-
checked: hexEnabled,
|
|
414
|
-
onPress: toggleHex,
|
|
415
|
-
},
|
|
416
|
-
{
|
|
417
|
-
key: 'controlLines',
|
|
418
|
-
title: 'Control Lines',
|
|
419
|
-
checkable: true,
|
|
420
|
-
checked: showControlLines,
|
|
421
|
-
onPress: () => setShowControlLines(s => !s),
|
|
422
|
-
},
|
|
423
|
-
{key: 'sendBreak', title: 'Send BREAK', onPress: sendBreak},
|
|
424
|
-
];
|
|
425
|
-
|
|
426
|
-
const sendDisabled = connected !== 'True' || sendBtn === 'Disabled';
|
|
427
|
-
|
|
428
|
-
return (
|
|
429
|
-
<View style={styles.container}>
|
|
430
|
-
<AppBar title="Simple USB Terminal" onBack={onBack} menu={menu} />
|
|
431
|
-
|
|
432
|
-
{showControlLines ? (
|
|
433
|
-
<View style={styles.controlLines}>
|
|
434
|
-
<CtrlButton label="RTS" on={lines.rts} onPress={toggleRts} />
|
|
435
|
-
<CtrlButton label="CTS" on={lines.cts} readOnly />
|
|
436
|
-
<View style={styles.ctrlGap} />
|
|
437
|
-
<CtrlButton label="DTR" on={lines.dtr} onPress={toggleDtr} />
|
|
438
|
-
<CtrlButton label="DSR" on={lines.dsr} readOnly />
|
|
439
|
-
<View style={styles.ctrlGap} />
|
|
440
|
-
<CtrlButton label="CD" on={lines.cd} readOnly />
|
|
441
|
-
<CtrlButton label="RI" on={lines.ri} readOnly />
|
|
442
|
-
</View>
|
|
443
|
-
) : null}
|
|
444
|
-
|
|
445
|
-
<View style={styles.divider} />
|
|
446
|
-
|
|
447
|
-
<ScrollView
|
|
448
|
-
ref={scrollRef}
|
|
449
|
-
style={styles.receive}
|
|
450
|
-
contentContainerStyle={styles.receiveContent}
|
|
451
|
-
onContentSizeChange={() =>
|
|
452
|
-
scrollRef.current?.scrollToEnd({animated: false})
|
|
453
|
-
}>
|
|
454
|
-
<Text style={styles.mono}>
|
|
455
|
-
{log.map((s, i) => (
|
|
456
|
-
<Text
|
|
457
|
-
key={i}
|
|
458
|
-
style={[{color: s.color}, s.caret ? styles.caret : null]}>
|
|
459
|
-
{s.text}
|
|
460
|
-
</Text>
|
|
461
|
-
))}
|
|
462
|
-
</Text>
|
|
463
|
-
</ScrollView>
|
|
464
|
-
|
|
465
|
-
<View style={styles.divider} />
|
|
466
|
-
|
|
467
|
-
<View style={styles.sendRow}>
|
|
468
|
-
<TextInput
|
|
469
|
-
style={styles.input}
|
|
470
|
-
value={input}
|
|
471
|
-
onChangeText={onChangeInput}
|
|
472
|
-
placeholder={hexEnabled ? 'HEX mode' : ''}
|
|
473
|
-
placeholderTextColor={colors.textSecondary}
|
|
474
|
-
autoCapitalize="none"
|
|
475
|
-
autoCorrect={false}
|
|
476
|
-
blurOnSubmit={false}
|
|
477
|
-
onSubmitEditing={doSend}
|
|
478
|
-
/>
|
|
479
|
-
<TouchableOpacity
|
|
480
|
-
style={[styles.sendBtn, sendDisabled && styles.sendBtnDisabled]}
|
|
481
|
-
onPress={doSend}
|
|
482
|
-
disabled={sendDisabled}>
|
|
483
|
-
<Text style={styles.sendIcon}>
|
|
484
|
-
{sendBtn === 'Disabled' ? '⊘' : '➤'}
|
|
485
|
-
</Text>
|
|
486
|
-
</TouchableOpacity>
|
|
487
|
-
</View>
|
|
488
|
-
|
|
489
|
-
<SingleChoiceDialog
|
|
490
|
-
visible={newlineDialog}
|
|
491
|
-
title="Newline"
|
|
492
|
-
options={NEWLINES}
|
|
493
|
-
selected={newline}
|
|
494
|
-
onSelect={setNewline}
|
|
495
|
-
onClose={() => setNewlineDialog(false)}
|
|
496
|
-
/>
|
|
497
|
-
</View>
|
|
498
|
-
);
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
function CtrlButton({
|
|
502
|
-
label,
|
|
503
|
-
on,
|
|
504
|
-
onPress,
|
|
505
|
-
readOnly,
|
|
506
|
-
}: {
|
|
507
|
-
label: string;
|
|
508
|
-
on: boolean;
|
|
509
|
-
onPress?: () => void;
|
|
510
|
-
readOnly?: boolean;
|
|
511
|
-
}) {
|
|
512
|
-
return (
|
|
513
|
-
<TouchableOpacity
|
|
514
|
-
style={[styles.ctrlBtn, on && styles.ctrlBtnOn]}
|
|
515
|
-
onPress={onPress}
|
|
516
|
-
disabled={readOnly}
|
|
517
|
-
activeOpacity={readOnly ? 1 : 0.6}>
|
|
518
|
-
<Text style={[styles.ctrlBtnText, readOnly && styles.ctrlBtnTextRo]}>
|
|
519
|
-
{label}
|
|
520
|
-
</Text>
|
|
521
|
-
</TouchableOpacity>
|
|
522
|
-
);
|
|
523
|
-
}
|
|
524
|
-
|
|
525
|
-
const styles = StyleSheet.create({
|
|
526
|
-
container: {flex: 1, backgroundColor: colors.background},
|
|
527
|
-
controlLines: {
|
|
528
|
-
flexDirection: 'row',
|
|
529
|
-
alignItems: 'center',
|
|
530
|
-
padding: 6,
|
|
531
|
-
flexWrap: 'wrap',
|
|
532
|
-
},
|
|
533
|
-
ctrlGap: {width: 8},
|
|
534
|
-
ctrlBtn: {
|
|
535
|
-
minWidth: 48,
|
|
536
|
-
paddingVertical: 8,
|
|
537
|
-
paddingHorizontal: 6,
|
|
538
|
-
marginHorizontal: 2,
|
|
539
|
-
borderWidth: 1,
|
|
540
|
-
borderColor: colors.divider,
|
|
541
|
-
borderRadius: 4,
|
|
542
|
-
alignItems: 'center',
|
|
543
|
-
},
|
|
544
|
-
ctrlBtnOn: {backgroundColor: colors.accent, borderColor: colors.accent},
|
|
545
|
-
ctrlBtnText: {fontSize: 13, fontWeight: '600', color: colors.text},
|
|
546
|
-
ctrlBtnTextRo: {color: colors.textSecondary},
|
|
547
|
-
divider: {height: 2, backgroundColor: colors.divider},
|
|
548
|
-
receive: {flex: 1, backgroundColor: colors.terminalBackground},
|
|
549
|
-
receiveContent: {padding: 8, flexGrow: 1, justifyContent: 'flex-end'},
|
|
550
|
-
mono: {
|
|
551
|
-
fontFamily: 'monospace',
|
|
552
|
-
fontSize: 13,
|
|
553
|
-
color: colors.receiveText,
|
|
554
|
-
},
|
|
555
|
-
caret: {backgroundColor: colors.caretBackground},
|
|
556
|
-
sendRow: {flexDirection: 'row', alignItems: 'center', padding: 4},
|
|
557
|
-
input: {
|
|
558
|
-
flex: 1,
|
|
559
|
-
fontSize: 15,
|
|
560
|
-
color: colors.text,
|
|
561
|
-
paddingHorizontal: 8,
|
|
562
|
-
paddingVertical: 8,
|
|
563
|
-
},
|
|
564
|
-
sendBtn: {
|
|
565
|
-
width: 44,
|
|
566
|
-
height: 44,
|
|
567
|
-
alignItems: 'center',
|
|
568
|
-
justifyContent: 'center',
|
|
569
|
-
},
|
|
570
|
-
sendBtnDisabled: {opacity: 0.35},
|
|
571
|
-
sendIcon: {fontSize: 22, color: colors.primary},
|
|
572
|
-
});
|
package/example/src/settings.ts
DELETED
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
import type {SerialOptions} from 'react-native-web-serial-api';
|
|
2
|
-
|
|
3
|
-
// Connection settings chosen on the form, passed straight into port.open().
|
|
4
|
-
export type ConnectionSettings = {
|
|
5
|
-
baudRate: number;
|
|
6
|
-
dataBits: 7 | 8;
|
|
7
|
-
stopBits: 1 | 2;
|
|
8
|
-
parity: 'none' | 'even' | 'odd';
|
|
9
|
-
flowControl: 'none' | 'hardware';
|
|
10
|
-
};
|
|
11
|
-
|
|
12
|
-
export const DEFAULT_SETTINGS: ConnectionSettings = {
|
|
13
|
-
baudRate: 115200,
|
|
14
|
-
dataBits: 8,
|
|
15
|
-
stopBits: 1,
|
|
16
|
-
parity: 'none',
|
|
17
|
-
flowControl: 'none',
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
export const BAUD_RATES = [2400, 9600, 19200, 57600, 115200] as const;
|
|
21
|
-
export const DATA_BITS = [7, 8] as const;
|
|
22
|
-
export const STOP_BITS = [1, 2] as const;
|
|
23
|
-
export const PARITIES = ['none', 'even', 'odd'] as const;
|
|
24
|
-
export const FLOW_CONTROLS = ['none', 'hardware'] as const;
|
|
25
|
-
|
|
26
|
-
export const FLOW_CONTROL_LABELS: Record<
|
|
27
|
-
ConnectionSettings['flowControl'],
|
|
28
|
-
string
|
|
29
|
-
> = {
|
|
30
|
-
none: '<none>',
|
|
31
|
-
hardware: 'RTS/CTS',
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
/** Map the form settings onto the Web Serial open() options. */
|
|
35
|
-
export function toSerialOptions(s: ConnectionSettings): SerialOptions {
|
|
36
|
-
return {
|
|
37
|
-
baudRate: s.baudRate,
|
|
38
|
-
dataBits: s.dataBits,
|
|
39
|
-
stopBits: s.stopBits,
|
|
40
|
-
parity: s.parity,
|
|
41
|
-
flowControl: s.flowControl,
|
|
42
|
-
};
|
|
43
|
-
}
|
package/example/src/theme.ts
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
// Colors ported from SimpleUsbTerminal res/values/colors.xml
|
|
2
|
-
export const colors = {
|
|
3
|
-
primary: '#d84315',
|
|
4
|
-
primaryDark: '#bf360c',
|
|
5
|
-
accent: '#ff6e40',
|
|
6
|
-
|
|
7
|
-
receiveText: '#00FF00',
|
|
8
|
-
sendText: '#82CAFF',
|
|
9
|
-
statusText: '#FFDB58',
|
|
10
|
-
caretBackground: '#666666',
|
|
11
|
-
|
|
12
|
-
// surrounding chrome
|
|
13
|
-
background: '#ffffff',
|
|
14
|
-
text: '#202020',
|
|
15
|
-
textSecondary: '#707070',
|
|
16
|
-
divider: '#d0d0d0',
|
|
17
|
-
terminalBackground: '#000000',
|
|
18
|
-
onPrimary: '#ffffff',
|
|
19
|
-
};
|