react-native-web-serial-api 0.0.1

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 (114) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +147 -0
  3. package/android/build.gradle +62 -0
  4. package/android/gradle.properties +6 -0
  5. package/android/src/main/AndroidManifest.xml +21 -0
  6. package/android/src/main/java/dev/webserialapi/NativeUsbSerialModule.java +704 -0
  7. package/android/src/main/java/dev/webserialapi/NativeUsbSerialPackage.java +46 -0
  8. package/android/src/main/java/dev/webserialapi/PortPickerActivity.java +235 -0
  9. package/android/src/main/java/dev/webserialapi/UsbDetachReceiver.java +37 -0
  10. package/android/src/main/res/xml/device_filter.xml +13 -0
  11. package/babel.config.js +3 -0
  12. package/biome.json +35 -0
  13. package/example/.watchmanconfig +1 -0
  14. package/example/App.tsx +71 -0
  15. package/example/__tests__/App.test.tsx +16 -0
  16. package/example/android/app/build.gradle +120 -0
  17. package/example/android/app/debug.keystore +0 -0
  18. package/example/android/app/proguard-rules.pro +10 -0
  19. package/example/android/app/src/debug/AndroidManifest.xml +9 -0
  20. package/example/android/app/src/main/AndroidManifest.xml +38 -0
  21. package/example/android/app/src/main/java/dev/uzlopak/MainActivity.kt +22 -0
  22. package/example/android/app/src/main/java/dev/uzlopak/MainApplication.kt +41 -0
  23. package/example/android/app/src/main/res/drawable/rn_edit_text_material.xml +37 -0
  24. package/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png +0 -0
  25. package/example/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png +0 -0
  26. package/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png +0 -0
  27. package/example/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png +0 -0
  28. package/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png +0 -0
  29. package/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png +0 -0
  30. package/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png +0 -0
  31. package/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png +0 -0
  32. package/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png +0 -0
  33. package/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png +0 -0
  34. package/example/android/app/src/main/res/values/strings.xml +3 -0
  35. package/example/android/app/src/main/res/values/styles.xml +9 -0
  36. package/example/android/build.gradle +22 -0
  37. package/example/android/gradle/wrapper/gradle-wrapper.jar +0 -0
  38. package/example/android/gradle/wrapper/gradle-wrapper.properties +7 -0
  39. package/example/android/gradle.properties +47 -0
  40. package/example/android/gradlew +252 -0
  41. package/example/android/gradlew.bat +94 -0
  42. package/example/android/settings.gradle +6 -0
  43. package/example/app.json +4 -0
  44. package/example/babel.config.js +21 -0
  45. package/example/biome.json +47 -0
  46. package/example/deploy.sh +11 -0
  47. package/example/index.html +26 -0
  48. package/example/index.js +9 -0
  49. package/example/index.web.js +8 -0
  50. package/example/jest.config.js +12 -0
  51. package/example/metro.config.js +58 -0
  52. package/example/package-lock.json +14510 -0
  53. package/example/package.json +48 -0
  54. package/example/react-native.config.js +17 -0
  55. package/example/src/components/AppBar.tsx +73 -0
  56. package/example/src/components/Menu.tsx +90 -0
  57. package/example/src/components/SingleChoiceDialog.tsx +120 -0
  58. package/example/src/screens/ConnectScreen.tsx +195 -0
  59. package/example/src/screens/DevicesScreen.tsx +141 -0
  60. package/example/src/screens/TerminalScreen.tsx +564 -0
  61. package/example/src/settings.ts +43 -0
  62. package/example/src/theme.ts +19 -0
  63. package/example/src/util/TextUtil.ts +129 -0
  64. package/example/tsconfig.json +10 -0
  65. package/example/vite.config.mjs +55 -0
  66. package/lib/commonjs/NativeUsbSerial.js +11 -0
  67. package/lib/commonjs/NativeUsbSerial.js.map +1 -0
  68. package/lib/commonjs/NativeUsbSerial.web.js +12 -0
  69. package/lib/commonjs/NativeUsbSerial.web.js.map +1 -0
  70. package/lib/commonjs/UsbSerial.js +149 -0
  71. package/lib/commonjs/UsbSerial.js.map +1 -0
  72. package/lib/commonjs/WebSerial.js +852 -0
  73. package/lib/commonjs/WebSerial.js.map +1 -0
  74. package/lib/commonjs/index.js +44 -0
  75. package/lib/commonjs/index.js.map +1 -0
  76. package/lib/commonjs/package.json +1 -0
  77. package/lib/commonjs/serial.android.js +13 -0
  78. package/lib/commonjs/serial.android.js.map +1 -0
  79. package/lib/commonjs/serial.js +13 -0
  80. package/lib/commonjs/serial.js.map +1 -0
  81. package/lib/commonjs/serial.web.js +11 -0
  82. package/lib/commonjs/serial.web.js.map +1 -0
  83. package/lib/typescript/src/NativeUsbSerial.d.ts +51 -0
  84. package/lib/typescript/src/NativeUsbSerial.d.ts.map +1 -0
  85. package/lib/typescript/src/NativeUsbSerial.web.d.ts +3 -0
  86. package/lib/typescript/src/NativeUsbSerial.web.d.ts.map +1 -0
  87. package/lib/typescript/src/UsbSerial.d.ts +97 -0
  88. package/lib/typescript/src/UsbSerial.d.ts.map +1 -0
  89. package/lib/typescript/src/WebSerial.d.ts +236 -0
  90. package/lib/typescript/src/WebSerial.d.ts.map +1 -0
  91. package/lib/typescript/src/index.d.ts +7 -0
  92. package/lib/typescript/src/index.d.ts.map +1 -0
  93. package/lib/typescript/src/serial.android.d.ts +2 -0
  94. package/lib/typescript/src/serial.android.d.ts.map +1 -0
  95. package/lib/typescript/src/serial.d.ts +2 -0
  96. package/lib/typescript/src/serial.d.ts.map +1 -0
  97. package/lib/typescript/src/serial.web.d.ts +4 -0
  98. package/lib/typescript/src/serial.web.d.ts.map +1 -0
  99. package/package.json +78 -0
  100. package/react-native.config.js +9 -0
  101. package/scripts/deploy-release.sh +127 -0
  102. package/src/NativeUsbSerial.ts +124 -0
  103. package/src/NativeUsbSerial.web.ts +5 -0
  104. package/src/UsbSerial.ts +305 -0
  105. package/src/WebSerial.ts +1084 -0
  106. package/src/index.ts +23 -0
  107. package/src/lib/dom-exception.ts +161 -0
  108. package/src/lib/event-target.ts +170 -0
  109. package/src/lib/promise.ts +19 -0
  110. package/src/serial.android.ts +1 -0
  111. package/src/serial.ts +1 -0
  112. package/src/serial.web.ts +6 -0
  113. package/tsconfig.build.json +7 -0
  114. package/tsconfig.json +20 -0
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "react-native-web-serial-api-example",
3
+ "version": "0.0.1",
4
+ "private": true,
5
+ "scripts": {
6
+ "android": "react-native run-android",
7
+ "start": "react-native start",
8
+ "build:android": "cd android && ./gradlew assembleDebug",
9
+ "web": "vite",
10
+ "web:build": "vite build",
11
+ "web:preview": "vite preview",
12
+ "lint": "biome check",
13
+ "format": "biome check --write",
14
+ "test": "jest"
15
+ },
16
+ "dependencies": {
17
+ "react": "19.2.3",
18
+ "react-dom": "19.2.3",
19
+ "react-native": "0.85.3",
20
+ "react-native-web": "^0.21.2",
21
+ "react-native-web-serial-api": "file:.."
22
+ },
23
+ "devDependencies": {
24
+ "@babel/core": "^7.25.2",
25
+ "@babel/preset-env": "^7.25.3",
26
+ "@babel/runtime": "^7.25.0",
27
+ "@biomejs/biome": "2.4.16",
28
+ "@react-native-community/cli": "20.1.3",
29
+ "@react-native-community/cli-platform-android": "20.1.3",
30
+ "@react-native/babel-preset": "0.85.3",
31
+ "@react-native/jest-preset": "0.85.3",
32
+ "@react-native/metro-config": "0.85.3",
33
+ "@react-native/typescript-config": "0.85.3",
34
+ "@testing-library/react-native": "^13.3.3",
35
+ "@types/react": "^19.2.6",
36
+ "@types/react-dom": "^19.2.0",
37
+ "@vitejs/plugin-react": "^6.0.2",
38
+ "babel-jest": "^29.6.3",
39
+ "babel-plugin-module-resolver": "^5.0.3",
40
+ "jest": "^29.6.3",
41
+ "react-test-renderer": "19.2.3",
42
+ "typescript": "6.0.3",
43
+ "vite": "^8.0.0"
44
+ },
45
+ "engines": {
46
+ "node": ">=22"
47
+ }
48
+ }
@@ -0,0 +1,17 @@
1
+ const path = require('node:path');
2
+ const pkg = require('../package.json');
3
+
4
+ /**
5
+ * The library lives one level up (repo root) and is installed here as a symlink
6
+ * pointing to an ancestor of this example dir. React Native autolinking skips
7
+ * dependencies whose resolved path is an ancestor of the project, so we map the
8
+ * dependency explicitly to the library's android module. This mirrors what
9
+ * create-react-native-library scaffolds for its example app.
10
+ */
11
+ module.exports = {
12
+ dependencies: {
13
+ [pkg.name]: {
14
+ root: path.join(__dirname, '..'),
15
+ },
16
+ },
17
+ };
@@ -0,0 +1,73 @@
1
+ import React from 'react';
2
+ import {
3
+ Platform,
4
+ StatusBar,
5
+ StyleSheet,
6
+ Text,
7
+ TouchableOpacity,
8
+ View,
9
+ } from 'react-native';
10
+ import {colors} from '../theme';
11
+ import {Menu, type MenuItem} from './Menu';
12
+
13
+ type Props = {
14
+ title: string;
15
+ onBack?: () => void;
16
+ menu?: MenuItem[];
17
+ };
18
+
19
+ const statusBarHeight =
20
+ Platform.OS === 'android' ? (StatusBar.currentHeight ?? 0) : 0;
21
+
22
+ export function AppBar({title, onBack, menu}: Props) {
23
+ const [open, setOpen] = React.useState(false);
24
+ return (
25
+ <View style={styles.wrap}>
26
+ <View style={styles.bar}>
27
+ {onBack ? (
28
+ <TouchableOpacity onPress={onBack} style={styles.iconBtn}>
29
+ <Text style={styles.icon}>←</Text>
30
+ </TouchableOpacity>
31
+ ) : null}
32
+ <Text style={styles.title} numberOfLines={1}>
33
+ {title}
34
+ </Text>
35
+ <View style={styles.spacer} />
36
+ {menu?.length ? (
37
+ <TouchableOpacity
38
+ onPress={() => setOpen(true)}
39
+ style={styles.iconBtn}>
40
+ <Text style={styles.icon}>⋮</Text>
41
+ </TouchableOpacity>
42
+ ) : null}
43
+ </View>
44
+ {menu ? (
45
+ <Menu visible={open} items={menu} onClose={() => setOpen(false)} />
46
+ ) : null}
47
+ </View>
48
+ );
49
+ }
50
+
51
+ const styles = StyleSheet.create({
52
+ wrap: {backgroundColor: colors.primary, paddingTop: statusBarHeight},
53
+ bar: {
54
+ height: 56,
55
+ flexDirection: 'row',
56
+ alignItems: 'center',
57
+ paddingHorizontal: 4,
58
+ },
59
+ iconBtn: {
60
+ width: 44,
61
+ height: 44,
62
+ alignItems: 'center',
63
+ justifyContent: 'center',
64
+ },
65
+ icon: {color: colors.onPrimary, fontSize: 22},
66
+ title: {
67
+ color: colors.onPrimary,
68
+ fontSize: 20,
69
+ fontWeight: '600',
70
+ marginLeft: 8,
71
+ },
72
+ spacer: {flex: 1},
73
+ });
@@ -0,0 +1,90 @@
1
+ import {
2
+ Modal,
3
+ Pressable,
4
+ ScrollView,
5
+ StyleSheet,
6
+ Text,
7
+ View,
8
+ } from 'react-native';
9
+ import {colors} from '../theme';
10
+
11
+ export type MenuItem = {
12
+ key: string;
13
+ title: string;
14
+ checkable?: boolean;
15
+ checked?: boolean;
16
+ disabled?: boolean;
17
+ onPress: () => void;
18
+ };
19
+
20
+ type Props = {
21
+ visible: boolean;
22
+ items: MenuItem[];
23
+ onClose: () => void;
24
+ };
25
+
26
+ /** Android-style overflow (⋮) dropdown anchored to the top-right. */
27
+ export function Menu({visible, items, onClose}: Props) {
28
+ return (
29
+ <Modal
30
+ visible={visible}
31
+ transparent
32
+ animationType="fade"
33
+ onRequestClose={onClose}>
34
+ <Pressable style={styles.backdrop} onPress={onClose}>
35
+ <View style={styles.menu}>
36
+ <ScrollView>
37
+ {items.map(item => (
38
+ <Pressable
39
+ key={item.key}
40
+ style={styles.item}
41
+ disabled={item.disabled}
42
+ onPress={() => {
43
+ onClose();
44
+ item.onPress();
45
+ }}>
46
+ <Text
47
+ style={[styles.itemText, item.disabled && styles.disabled]}>
48
+ {item.title}
49
+ </Text>
50
+ {item.checkable ? (
51
+ <Text style={styles.check}>{item.checked ? '☑' : '☐'}</Text>
52
+ ) : null}
53
+ </Pressable>
54
+ ))}
55
+ </ScrollView>
56
+ </View>
57
+ </Pressable>
58
+ </Modal>
59
+ );
60
+ }
61
+
62
+ const styles = StyleSheet.create({
63
+ backdrop: {
64
+ flex: 1,
65
+ alignItems: 'flex-end',
66
+ paddingTop: 4,
67
+ paddingRight: 4,
68
+ },
69
+ menu: {
70
+ minWidth: 220,
71
+ maxHeight: '80%',
72
+ backgroundColor: colors.background,
73
+ borderRadius: 4,
74
+ paddingVertical: 4,
75
+ elevation: 8,
76
+ shadowColor: '#000',
77
+ shadowOpacity: 0.3,
78
+ shadowRadius: 6,
79
+ shadowOffset: {width: 0, height: 2},
80
+ },
81
+ item: {
82
+ flexDirection: 'row',
83
+ alignItems: 'center',
84
+ paddingVertical: 12,
85
+ paddingHorizontal: 16,
86
+ },
87
+ itemText: {flex: 1, fontSize: 16, color: colors.text},
88
+ disabled: {color: colors.textSecondary, opacity: 0.5},
89
+ check: {fontSize: 16, marginLeft: 16, color: colors.text},
90
+ });
@@ -0,0 +1,120 @@
1
+ import {
2
+ Alert,
3
+ Modal,
4
+ Pressable,
5
+ ScrollView,
6
+ StyleSheet,
7
+ Text,
8
+ View,
9
+ } from 'react-native';
10
+ import {colors} from '../theme';
11
+
12
+ export type Choice<T> = {label: string; value: T};
13
+
14
+ type Props<T> = {
15
+ visible: boolean;
16
+ title: string;
17
+ options: Choice<T>[];
18
+ selected: T;
19
+ onSelect: (value: T) => void;
20
+ onClose: () => void;
21
+ /** When set, shows an "Info" button that pops this message. */
22
+ infoMessage?: string;
23
+ };
24
+
25
+ /** Single-choice (radio) dialog, mirroring AlertDialog.setSingleChoiceItems. */
26
+ export function SingleChoiceDialog<T extends string | number>({
27
+ visible,
28
+ title,
29
+ options,
30
+ selected,
31
+ onSelect,
32
+ onClose,
33
+ infoMessage,
34
+ }: Props<T>) {
35
+ return (
36
+ <Modal
37
+ visible={visible}
38
+ transparent
39
+ animationType="fade"
40
+ onRequestClose={onClose}>
41
+ <Pressable style={styles.backdrop} onPress={onClose}>
42
+ <Pressable style={styles.card} onPress={() => {}}>
43
+ <Text style={styles.title}>{title}</Text>
44
+ <ScrollView style={styles.list}>
45
+ {options.map(opt => {
46
+ const isSelected = opt.value === selected;
47
+ return (
48
+ <Pressable
49
+ key={String(opt.value)}
50
+ style={styles.row}
51
+ onPress={() => {
52
+ onSelect(opt.value);
53
+ onClose();
54
+ }}>
55
+ <Text style={styles.radio}>{isSelected ? '◉' : '◯'}</Text>
56
+ <Text style={styles.label}>{opt.label}</Text>
57
+ </Pressable>
58
+ );
59
+ })}
60
+ </ScrollView>
61
+ <View style={styles.footer}>
62
+ {infoMessage ? (
63
+ <Pressable
64
+ style={styles.footerBtn}
65
+ onPress={() => Alert.alert(title, infoMessage)}>
66
+ <Text style={styles.footerText}>INFO</Text>
67
+ </Pressable>
68
+ ) : null}
69
+ <Pressable style={styles.footerBtn} onPress={onClose}>
70
+ <Text style={styles.footerText}>CANCEL</Text>
71
+ </Pressable>
72
+ </View>
73
+ </Pressable>
74
+ </Pressable>
75
+ </Modal>
76
+ );
77
+ }
78
+
79
+ const styles = StyleSheet.create({
80
+ backdrop: {
81
+ flex: 1,
82
+ backgroundColor: 'rgba(0,0,0,0.4)',
83
+ justifyContent: 'center',
84
+ alignItems: 'center',
85
+ padding: 24,
86
+ },
87
+ card: {
88
+ width: '100%',
89
+ maxWidth: 360,
90
+ maxHeight: '80%',
91
+ backgroundColor: colors.background,
92
+ borderRadius: 6,
93
+ paddingVertical: 16,
94
+ elevation: 12,
95
+ },
96
+ title: {
97
+ fontSize: 18,
98
+ fontWeight: '600',
99
+ color: colors.text,
100
+ paddingHorizontal: 20,
101
+ paddingBottom: 12,
102
+ },
103
+ list: {flexGrow: 0},
104
+ row: {
105
+ flexDirection: 'row',
106
+ alignItems: 'center',
107
+ paddingVertical: 12,
108
+ paddingHorizontal: 20,
109
+ },
110
+ radio: {fontSize: 18, color: colors.primary, marginRight: 16},
111
+ label: {fontSize: 16, color: colors.text},
112
+ footer: {
113
+ flexDirection: 'row',
114
+ justifyContent: 'flex-end',
115
+ paddingHorizontal: 12,
116
+ paddingTop: 8,
117
+ },
118
+ footerBtn: {paddingVertical: 8, paddingHorizontal: 12},
119
+ footerText: {color: colors.primary, fontWeight: '600', fontSize: 14},
120
+ });
@@ -0,0 +1,195 @@
1
+ import React from 'react';
2
+ import {
3
+ ScrollView,
4
+ StyleSheet,
5
+ Text,
6
+ TouchableOpacity,
7
+ View,
8
+ } from 'react-native';
9
+ import type {SerialPort} from 'react-native-web-serial-api';
10
+ import {AppBar} from '../components/AppBar';
11
+ import {
12
+ type Choice,
13
+ SingleChoiceDialog,
14
+ } from '../components/SingleChoiceDialog';
15
+ import {
16
+ BAUD_RATES,
17
+ type ConnectionSettings,
18
+ DATA_BITS,
19
+ DEFAULT_SETTINGS,
20
+ FLOW_CONTROL_LABELS,
21
+ FLOW_CONTROLS,
22
+ PARITIES,
23
+ STOP_BITS,
24
+ } from '../settings';
25
+ import {colors} from '../theme';
26
+
27
+ type Props = {
28
+ port: SerialPort;
29
+ initial?: ConnectionSettings;
30
+ onBack: () => void;
31
+ onConnect: (settings: ConnectionSettings) => void;
32
+ };
33
+
34
+ function hex4(n: number | undefined): string {
35
+ return (n ?? 0).toString(16).toUpperCase().padStart(4, '0');
36
+ }
37
+
38
+ export function ConnectScreen({port, initial, onBack, onConnect}: Props) {
39
+ const [settings, setSettings] = React.useState<ConnectionSettings>(
40
+ initial ?? DEFAULT_SETTINGS,
41
+ );
42
+ // Which dropdown's dialog is open (null = none).
43
+ const [open, setOpen] = React.useState<keyof ConnectionSettings | null>(null);
44
+
45
+ const info = port.getInfo();
46
+
47
+ const set = <K extends keyof ConnectionSettings>(
48
+ key: K,
49
+ value: ConnectionSettings[K],
50
+ ) => setSettings(s => ({...s, [key]: value}));
51
+
52
+ return (
53
+ <View style={styles.container}>
54
+ <AppBar title="Connection settings" onBack={onBack} />
55
+
56
+ <ScrollView contentContainerStyle={styles.form}>
57
+ <Text style={styles.device}>
58
+ {`Vendor ${hex4(info.usbVendorId)} · Product ${hex4(
59
+ info.usbProductId,
60
+ )}`}
61
+ </Text>
62
+
63
+ <Dropdown
64
+ label="Baud rate"
65
+ value={String(settings.baudRate)}
66
+ onPress={() => setOpen('baudRate')}
67
+ />
68
+ <Dropdown
69
+ label="Data bits"
70
+ value={String(settings.dataBits)}
71
+ onPress={() => setOpen('dataBits')}
72
+ />
73
+ <Dropdown
74
+ label="Stop bits"
75
+ value={String(settings.stopBits)}
76
+ onPress={() => setOpen('stopBits')}
77
+ />
78
+ <Dropdown
79
+ label="Parity"
80
+ value={settings.parity}
81
+ onPress={() => setOpen('parity')}
82
+ />
83
+ <Dropdown
84
+ label="Flow control"
85
+ value={FLOW_CONTROL_LABELS[settings.flowControl]}
86
+ onPress={() => setOpen('flowControl')}
87
+ />
88
+
89
+ <TouchableOpacity
90
+ style={styles.connectBtn}
91
+ onPress={() => onConnect(settings)}>
92
+ <Text style={styles.connectText}>Connect</Text>
93
+ </TouchableOpacity>
94
+ </ScrollView>
95
+
96
+ <SingleChoiceDialog
97
+ visible={open === 'baudRate'}
98
+ title="Baud rate"
99
+ options={BAUD_RATES.map(b => ({label: String(b), value: b}))}
100
+ selected={settings.baudRate}
101
+ onSelect={v => set('baudRate', v)}
102
+ onClose={() => setOpen(null)}
103
+ />
104
+ <SingleChoiceDialog
105
+ visible={open === 'dataBits'}
106
+ title="Data bits"
107
+ options={DATA_BITS.map(b => ({label: String(b), value: b}))}
108
+ selected={settings.dataBits}
109
+ onSelect={v => set('dataBits', v as ConnectionSettings['dataBits'])}
110
+ onClose={() => setOpen(null)}
111
+ />
112
+ <SingleChoiceDialog
113
+ visible={open === 'stopBits'}
114
+ title="Stop bits"
115
+ options={STOP_BITS.map(b => ({label: String(b), value: b}))}
116
+ selected={settings.stopBits}
117
+ onSelect={v => set('stopBits', v as ConnectionSettings['stopBits'])}
118
+ onClose={() => setOpen(null)}
119
+ />
120
+ <SingleChoiceDialog
121
+ visible={open === 'parity'}
122
+ title="Parity"
123
+ options={PARITIES.map(p => ({label: p, value: p})) as Choice<string>[]}
124
+ selected={settings.parity}
125
+ onSelect={v => set('parity', v as ConnectionSettings['parity'])}
126
+ onClose={() => setOpen(null)}
127
+ />
128
+ <SingleChoiceDialog
129
+ visible={open === 'flowControl'}
130
+ title="Flow control"
131
+ options={
132
+ FLOW_CONTROLS.map(f => ({
133
+ label: FLOW_CONTROL_LABELS[f],
134
+ value: f,
135
+ })) as Choice<string>[]
136
+ }
137
+ selected={settings.flowControl}
138
+ onSelect={v =>
139
+ set('flowControl', v as ConnectionSettings['flowControl'])
140
+ }
141
+ onClose={() => setOpen(null)}
142
+ />
143
+ </View>
144
+ );
145
+ }
146
+
147
+ function Dropdown({
148
+ label,
149
+ value,
150
+ onPress,
151
+ }: {
152
+ label: string;
153
+ value: string;
154
+ onPress: () => void;
155
+ }) {
156
+ return (
157
+ <TouchableOpacity style={styles.row} onPress={onPress}>
158
+ <Text style={styles.rowLabel}>{label}</Text>
159
+ <View style={styles.rowValueWrap}>
160
+ <Text style={styles.rowValue}>{value}</Text>
161
+ <Text style={styles.caret}>▾</Text>
162
+ </View>
163
+ </TouchableOpacity>
164
+ );
165
+ }
166
+
167
+ const styles = StyleSheet.create({
168
+ container: {flex: 1, backgroundColor: colors.background},
169
+ form: {padding: 16},
170
+ device: {
171
+ fontSize: 13,
172
+ color: colors.textSecondary,
173
+ marginBottom: 16,
174
+ },
175
+ row: {
176
+ flexDirection: 'row',
177
+ alignItems: 'center',
178
+ justifyContent: 'space-between',
179
+ paddingVertical: 14,
180
+ borderBottomWidth: 1,
181
+ borderBottomColor: colors.divider,
182
+ },
183
+ rowLabel: {fontSize: 16, color: colors.text},
184
+ rowValueWrap: {flexDirection: 'row', alignItems: 'center'},
185
+ rowValue: {fontSize: 16, color: colors.primary, fontWeight: '600'},
186
+ caret: {fontSize: 14, color: colors.primary, marginLeft: 8},
187
+ connectBtn: {
188
+ marginTop: 28,
189
+ backgroundColor: colors.primary,
190
+ borderRadius: 6,
191
+ paddingVertical: 14,
192
+ alignItems: 'center',
193
+ },
194
+ connectText: {color: colors.onPrimary, fontSize: 16, fontWeight: '600'},
195
+ });
@@ -0,0 +1,141 @@
1
+ import React from 'react';
2
+ import {FlatList, StyleSheet, Text, TouchableOpacity, View} from 'react-native';
3
+ import type {SerialPort} from 'react-native-web-serial-api';
4
+ import {serial} from 'react-native-web-serial-api';
5
+ import {AppBar} from '../components/AppBar';
6
+ import {colors} from '../theme';
7
+
8
+ type Props = {
9
+ onSelect: (port: SerialPort) => void;
10
+ };
11
+
12
+ function hex4(n: number | undefined): string {
13
+ return (n ?? 0).toString(16).toUpperCase().padStart(4, '0');
14
+ }
15
+
16
+ // The Web Serial API only exposes VID/PID (not the driver/chip class), so we
17
+ // derive a best-effort label from known USB-serial vendor IDs.
18
+ function chipLabel(vendorId: number | undefined): string {
19
+ switch (vendorId) {
20
+ case 0x0403:
21
+ return 'FTDI';
22
+ case 0x10c4:
23
+ return 'CP210x';
24
+ case 0x1a86:
25
+ return 'CH34x';
26
+ case 0x067b:
27
+ return 'Prolific';
28
+ default:
29
+ return 'USB serial';
30
+ }
31
+ }
32
+
33
+ export function DevicesScreen({onSelect}: Props) {
34
+ const [ports, setPorts] = React.useState<SerialPort[]>([]);
35
+ const [error, setError] = React.useState<string | null>(null);
36
+
37
+ const refresh = React.useCallback(async () => {
38
+ setError(null);
39
+ try {
40
+ if (!serial) {
41
+ setError('Web Serial API is not available on this platform.');
42
+ return;
43
+ }
44
+ setPorts(await serial.getPorts());
45
+ } catch (e: any) {
46
+ setError(e?.message ?? String(e));
47
+ }
48
+ }, []);
49
+
50
+ React.useEffect(() => {
51
+ refresh();
52
+ if (!serial) {
53
+ return;
54
+ }
55
+ // Auto-refresh the list when a USB device is attached or detached, so the
56
+ // user doesn't have to hit "Refresh Devices" manually.
57
+ serial.addEventListener('connect', refresh);
58
+ serial.addEventListener('disconnect', refresh);
59
+ return () => {
60
+ serial.removeEventListener('connect', refresh);
61
+ serial.removeEventListener('disconnect', refresh);
62
+ };
63
+ }, [refresh]);
64
+
65
+ const requestNew = React.useCallback(async () => {
66
+ setError(null);
67
+ try {
68
+ const port = await serial.requestPort();
69
+ onSelect(port);
70
+ } catch (e: any) {
71
+ // user cancelled the picker, or no device
72
+ setError(e?.message ?? String(e));
73
+ }
74
+ }, [onSelect]);
75
+
76
+ return (
77
+ <View style={styles.container}>
78
+ <AppBar
79
+ title="Simple USB Terminal"
80
+ menu={[
81
+ {key: 'refresh', title: 'Refresh Devices', onPress: refresh},
82
+ {key: 'request', title: 'Connect new device…', onPress: requestNew},
83
+ ]}
84
+ />
85
+
86
+ <View style={styles.header}>
87
+ <Text style={styles.headerText}>USB Devices</Text>
88
+ </View>
89
+
90
+ {error ? <Text style={styles.error}>{error}</Text> : null}
91
+
92
+ <FlatList
93
+ data={ports}
94
+ keyExtractor={(_, i) => String(i)}
95
+ ListEmptyComponent={
96
+ <Text style={styles.empty}>{'<no USB devices found>'}</Text>
97
+ }
98
+ renderItem={({item}) => {
99
+ const info = item.getInfo();
100
+ return (
101
+ <TouchableOpacity
102
+ style={styles.item}
103
+ onPress={() => onSelect(item)}>
104
+ <Text style={styles.text1}>{chipLabel(info.usbVendorId)}</Text>
105
+ <Text style={styles.text2}>
106
+ {`Vendor ${hex4(info.usbVendorId)}, Product ${hex4(
107
+ info.usbProductId,
108
+ )}`}
109
+ </Text>
110
+ </TouchableOpacity>
111
+ );
112
+ }}
113
+ />
114
+ </View>
115
+ );
116
+ }
117
+
118
+ const styles = StyleSheet.create({
119
+ container: {flex: 1, backgroundColor: colors.background},
120
+ header: {
121
+ backgroundColor: colors.divider,
122
+ paddingVertical: 12,
123
+ alignItems: 'center',
124
+ },
125
+ headerText: {fontSize: 16, color: colors.text},
126
+ error: {color: '#c62828', padding: 12},
127
+ empty: {
128
+ fontSize: 18,
129
+ textAlign: 'center',
130
+ color: colors.textSecondary,
131
+ marginTop: 24,
132
+ },
133
+ item: {paddingVertical: 8, paddingHorizontal: 12},
134
+ text1: {fontSize: 16, color: colors.text, marginTop: 4, marginHorizontal: 12},
135
+ text2: {
136
+ fontSize: 13,
137
+ color: colors.textSecondary,
138
+ marginHorizontal: 20,
139
+ marginBottom: 4,
140
+ },
141
+ });