esp32tool 1.6.6 → 1.6.7
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/apple-touch-icon.png +0 -0
- package/css/style.css +325 -25
- package/dist/util/console-color.js +3 -3
- package/dist/util/timestamp-transformer.js +24 -1
- package/electron/cli-main.cjs +19 -19
- package/electron/main.cjs +167 -148
- package/electron/preload.js +16 -18
- package/icons/icon-128.png +0 -0
- package/icons/icon-144.png +0 -0
- package/icons/icon-152.png +0 -0
- package/icons/icon-192.png +0 -0
- package/icons/icon-384.png +0 -0
- package/icons/icon-512.png +0 -0
- package/icons/icon-72.png +0 -0
- package/icons/icon-96.png +0 -0
- package/js/console.js +21 -12
- package/js/hex-editor.js +216 -163
- package/js/improv.js +59 -21
- package/js/nvs-editor.js +1189 -182
- package/js/script.js +1048 -845
- package/js/util/console-color.js +3 -3
- package/js/util/timestamp-transformer.js +24 -1
- package/js/webusb-serial.js +1075 -950
- package/package.cli.json +2 -2
- package/package.json +11 -12
- package/screenshots/desktop.png +0 -0
- package/screenshots/mobile.png +0 -0
- package/src/util/console-color.ts +3 -3
- package/src/util/timestamp-transformer.ts +27 -1
- package/sw.js +1 -1
package/js/webusb-serial.js
CHANGED
|
@@ -1,1000 +1,1119 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* WebUSBSerial - Web Serial API-like wrapper for WebUSB
|
|
3
3
|
* Provides a familiar interface for serial communication over USB on Android
|
|
4
|
-
*
|
|
4
|
+
*
|
|
5
5
|
* This enables to work on Android devices where Web Serial API
|
|
6
6
|
* is not available but WebUSB is supported.
|
|
7
|
-
*
|
|
7
|
+
*
|
|
8
8
|
* IMPORTANT: For Android compatibility, this class uses smaller transfer sizes
|
|
9
9
|
* to prevent SLIP synchronization errors. The maxTransferSize is set to 64 bytes
|
|
10
10
|
* (or endpoint packetSize if smaller) to ensure SLIP frames don't get split.
|
|
11
11
|
*/
|
|
12
12
|
class WebUSBSerial {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
13
|
+
constructor(logger = null) {
|
|
14
|
+
this.device = null;
|
|
15
|
+
this.interfaceNumber = null;
|
|
16
|
+
this.endpointIn = null;
|
|
17
|
+
this.endpointOut = null;
|
|
18
|
+
this.controlInterface = null;
|
|
19
|
+
this.readableStream = null;
|
|
20
|
+
this.writableStream = null;
|
|
21
|
+
this._readLoopRunning = false;
|
|
22
|
+
this._usbDisconnectHandler = null;
|
|
23
|
+
this._eventListeners = {
|
|
24
|
+
close: [],
|
|
25
|
+
disconnect: [],
|
|
26
|
+
};
|
|
27
|
+
// Transfer size optimized for WebUSB on Android
|
|
28
|
+
// CRITICAL: blockSize = (maxTransferSize - 2) / 2
|
|
29
|
+
// Set to 64 bytes for maximum compatibility with all USB-Serial adapters
|
|
30
|
+
// With 64 bytes: blockSize = (64-2)/2 = 31 bytes per SLIP packet
|
|
31
|
+
this.maxTransferSize = 64;
|
|
32
|
+
|
|
33
|
+
// Flag to indicate this is WebUSB (used by esptool to adjust block sizes)
|
|
34
|
+
this.isWebUSB = true;
|
|
35
|
+
|
|
36
|
+
// Command queue for serializing control transfers (critical for CP2102)
|
|
37
|
+
this._commandQueue = Promise.resolve();
|
|
38
|
+
|
|
39
|
+
// Track current DTR/RTS state to preserve unspecified signals
|
|
40
|
+
this._currentDTR = false;
|
|
41
|
+
this._currentRTS = false;
|
|
42
|
+
|
|
43
|
+
// Logger function (defaults to console.log if not provided)
|
|
44
|
+
this._log = logger || ((...args) => console.log(...args));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Request USB device (mimics navigator.serial.requestPort())
|
|
49
|
+
* @param {function|object} logger - Logger function or object with log() method
|
|
50
|
+
* @param {boolean} forceNew - If true, forces selection of a new device (ignores already paired devices)
|
|
51
|
+
*/
|
|
52
|
+
static async requestPort(logger = null, forceNew = false) {
|
|
53
|
+
const filters = [
|
|
54
|
+
{ vendorId: 0x303a }, // Espressif
|
|
55
|
+
{ vendorId: 0x0403 }, // FTDI
|
|
56
|
+
{ vendorId: 0x1a86 }, // CH340
|
|
57
|
+
{ vendorId: 0x10c4 }, // CP210x
|
|
58
|
+
{ vendorId: 0x067b }, // PL2303
|
|
59
|
+
];
|
|
60
|
+
|
|
61
|
+
// Helper to call logger (supports both function and object with log() method)
|
|
62
|
+
const log = (msg) => {
|
|
63
|
+
if (!logger) return;
|
|
64
|
+
if (typeof logger === "function") {
|
|
65
|
+
logger(msg);
|
|
66
|
+
} else if (typeof logger.log === "function") {
|
|
67
|
+
logger.log(msg);
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
let device;
|
|
72
|
+
|
|
73
|
+
// If forceNew is false, try to reuse a previously authorized device
|
|
74
|
+
if (!forceNew && navigator.usb && navigator.usb.getDevices) {
|
|
75
|
+
try {
|
|
76
|
+
const devices = await navigator.usb.getDevices();
|
|
77
|
+
// Find a device that matches our filters
|
|
78
|
+
device = devices.find((d) =>
|
|
79
|
+
filters.some((f) => f.vendorId === d.vendorId),
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
if (device) {
|
|
83
|
+
log("[WebUSB] Reusing previously authorized device");
|
|
84
|
+
}
|
|
85
|
+
} catch (err) {
|
|
86
|
+
// Can't use this._log in static method, use console as fallback
|
|
87
|
+
console.warn(
|
|
88
|
+
"[WebUSB] Failed to get previously authorized devices:",
|
|
89
|
+
err,
|
|
90
|
+
);
|
|
91
|
+
}
|
|
45
92
|
}
|
|
46
93
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
{ vendorId: 0x303A }, // Espressif
|
|
55
|
-
{ vendorId: 0x0403 }, // FTDI
|
|
56
|
-
{ vendorId: 0x1A86 }, // CH340
|
|
57
|
-
{ vendorId: 0x10C4 }, // CP210x
|
|
58
|
-
{ vendorId: 0x067B } // PL2303
|
|
59
|
-
];
|
|
60
|
-
|
|
61
|
-
// Helper to call logger (supports both function and object with log() method)
|
|
62
|
-
const log = (msg) => {
|
|
63
|
-
if (!logger) return;
|
|
64
|
-
if (typeof logger === 'function') {
|
|
65
|
-
logger(msg);
|
|
66
|
-
} else if (typeof logger.log === 'function') {
|
|
67
|
-
logger.log(msg);
|
|
68
|
-
}
|
|
69
|
-
};
|
|
94
|
+
// If no device found or forceNew is true, request a new device
|
|
95
|
+
if (!device) {
|
|
96
|
+
if (!navigator.usb) {
|
|
97
|
+
throw new Error("WebUSB not available");
|
|
98
|
+
}
|
|
99
|
+
device = await navigator.usb.requestDevice({ filters });
|
|
100
|
+
}
|
|
70
101
|
|
|
71
|
-
|
|
102
|
+
const port = new WebUSBSerial(logger);
|
|
103
|
+
port.device = device;
|
|
104
|
+
return port;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Open the USB device (mimics port.open())
|
|
109
|
+
*/
|
|
110
|
+
async open(options = {}) {
|
|
111
|
+
if (!this.device) {
|
|
112
|
+
throw new Error("No device selected");
|
|
113
|
+
}
|
|
72
114
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
}
|
|
85
|
-
} catch (err) {
|
|
86
|
-
// Can't use this._log in static method, use console as fallback
|
|
87
|
-
console.warn('[WebUSB] Failed to get previously authorized devices:', err);
|
|
88
|
-
}
|
|
115
|
+
const baudRate = options.baudRate || 115200;
|
|
116
|
+
|
|
117
|
+
// If device is already opened, we need to close and reopen it
|
|
118
|
+
// This is critical for ESP32-S2 which changes interfaces when switching modes
|
|
119
|
+
if (this.device.opened) {
|
|
120
|
+
try {
|
|
121
|
+
// Release all interfaces
|
|
122
|
+
if (this.interfaceNumber !== null) {
|
|
123
|
+
try {
|
|
124
|
+
await this.device.releaseInterface(this.interfaceNumber);
|
|
125
|
+
} catch (e) {}
|
|
89
126
|
}
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
127
|
+
if (
|
|
128
|
+
this.controlInterface !== null &&
|
|
129
|
+
this.controlInterface !== this.interfaceNumber
|
|
130
|
+
) {
|
|
131
|
+
try {
|
|
132
|
+
await this.device.releaseInterface(this.controlInterface);
|
|
133
|
+
} catch (e) {}
|
|
97
134
|
}
|
|
98
135
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
136
|
+
// Close the device
|
|
137
|
+
await this.device.close();
|
|
138
|
+
|
|
139
|
+
// Reset interface numbers so they get re-scanned
|
|
140
|
+
this.interfaceNumber = null;
|
|
141
|
+
this.controlInterface = null;
|
|
142
|
+
this.endpointIn = null;
|
|
143
|
+
this.endpointOut = null;
|
|
144
|
+
|
|
145
|
+
// Wait a bit for device to settle
|
|
146
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
147
|
+
} catch (e) {
|
|
148
|
+
this._log("[WebUSB] Error during close:", e.message);
|
|
149
|
+
}
|
|
102
150
|
}
|
|
103
151
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
152
|
+
if (this.device.opened) {
|
|
153
|
+
try {
|
|
154
|
+
await this.device.close();
|
|
155
|
+
} catch (e) {
|
|
156
|
+
this._log("[WebUSB] Error closing device:", e.message);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
111
159
|
|
|
112
|
-
|
|
160
|
+
try {
|
|
161
|
+
if (this.device.reset) {
|
|
162
|
+
await this.device.reset();
|
|
163
|
+
}
|
|
164
|
+
} catch (e) {
|
|
165
|
+
// this._log('[WebUSB] Device reset failed:', e.message);
|
|
166
|
+
}
|
|
113
167
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
}
|
|
123
|
-
if (this.controlInterface !== null && this.controlInterface !== this.interfaceNumber) {
|
|
124
|
-
try { await this.device.releaseInterface(this.controlInterface); } catch (e) {}
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
// Close the device
|
|
128
|
-
await this.device.close();
|
|
129
|
-
|
|
130
|
-
// Reset interface numbers so they get re-scanned
|
|
131
|
-
this.interfaceNumber = null;
|
|
132
|
-
this.controlInterface = null;
|
|
133
|
-
this.endpointIn = null;
|
|
134
|
-
this.endpointOut = null;
|
|
135
|
-
|
|
136
|
-
// Wait a bit for device to settle
|
|
137
|
-
await new Promise(resolve => setTimeout(resolve, 100));
|
|
138
|
-
} catch (e) {
|
|
139
|
-
this._log('[WebUSB] Error during close:', e.message);
|
|
140
|
-
}
|
|
168
|
+
const attemptOpenAndClaim = async () => {
|
|
169
|
+
await this.device.open();
|
|
170
|
+
try {
|
|
171
|
+
const currentCfg = this.device.configuration
|
|
172
|
+
? this.device.configuration.configurationValue
|
|
173
|
+
: null;
|
|
174
|
+
if (!currentCfg || currentCfg !== 1) {
|
|
175
|
+
await this.device.selectConfiguration(1);
|
|
141
176
|
}
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
177
|
+
} catch (e) {}
|
|
178
|
+
|
|
179
|
+
const config = this.device.configuration;
|
|
180
|
+
|
|
181
|
+
// Try to claim CDC control interface first (helps on Android/CH34x)
|
|
182
|
+
const preControlIface = config.interfaces.find(
|
|
183
|
+
(i) =>
|
|
184
|
+
i.alternates &&
|
|
185
|
+
i.alternates[0] &&
|
|
186
|
+
i.alternates[0].interfaceClass === 0x02,
|
|
187
|
+
);
|
|
188
|
+
if (preControlIface) {
|
|
189
|
+
try {
|
|
190
|
+
await this.device.claimInterface(preControlIface.interfaceNumber);
|
|
191
|
+
try {
|
|
192
|
+
await this.device.selectAlternateInterface(
|
|
193
|
+
preControlIface.interfaceNumber,
|
|
194
|
+
0,
|
|
195
|
+
);
|
|
196
|
+
} catch (e) {}
|
|
197
|
+
this.controlInterface = preControlIface.interfaceNumber;
|
|
198
|
+
} catch (e) {
|
|
199
|
+
this._log(
|
|
200
|
+
`[WebUSB] Could not pre-claim CDC control iface: ${e.message}`,
|
|
201
|
+
);
|
|
147
202
|
}
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Find bulk IN/OUT interface (prefer CDC data class)
|
|
206
|
+
const candidates = [];
|
|
207
|
+
for (const iface of config.interfaces) {
|
|
208
|
+
// Check all alternates, not just alternates[0]
|
|
209
|
+
for (let altIndex = 0; altIndex < iface.alternates.length; altIndex++) {
|
|
210
|
+
const alt = iface.alternates[altIndex];
|
|
211
|
+
let hasIn = false,
|
|
212
|
+
hasOut = false;
|
|
213
|
+
for (const ep of alt.endpoints) {
|
|
214
|
+
if (ep.type === "bulk" && ep.direction === "in") hasIn = true;
|
|
215
|
+
if (ep.type === "bulk" && ep.direction === "out") hasOut = true;
|
|
216
|
+
}
|
|
217
|
+
if (hasIn && hasOut) {
|
|
218
|
+
let score = 2;
|
|
219
|
+
if (alt.interfaceClass === 0x0a)
|
|
220
|
+
score = 0; // CDC data first
|
|
221
|
+
else if (alt.interfaceClass === 0xff) score = 1; // vendor-specific next
|
|
222
|
+
candidates.push({ iface, altIndex, alt, score });
|
|
223
|
+
break; // Found suitable alternate for this interface
|
|
224
|
+
}
|
|
155
225
|
}
|
|
226
|
+
}
|
|
156
227
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
const currentCfg = this.device.configuration ? this.device.configuration.configurationValue : null;
|
|
161
|
-
if (!currentCfg || currentCfg !== 1) {
|
|
162
|
-
await this.device.selectConfiguration(1);
|
|
163
|
-
}
|
|
164
|
-
} catch (e) { }
|
|
165
|
-
|
|
166
|
-
const config = this.device.configuration;
|
|
167
|
-
|
|
168
|
-
// Try to claim CDC control interface first (helps on Android/CH34x)
|
|
169
|
-
const preControlIface = config.interfaces.find(i => i.alternates && i.alternates[0] && i.alternates[0].interfaceClass === 0x02);
|
|
170
|
-
if (preControlIface) {
|
|
171
|
-
try {
|
|
172
|
-
await this.device.claimInterface(preControlIface.interfaceNumber);
|
|
173
|
-
try { await this.device.selectAlternateInterface(preControlIface.interfaceNumber, 0); } catch (e) { }
|
|
174
|
-
this.controlInterface = preControlIface.interfaceNumber;
|
|
175
|
-
} catch (e) {
|
|
176
|
-
this._log(`[WebUSB] Could not pre-claim CDC control iface: ${e.message}`);
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
// Find bulk IN/OUT interface (prefer CDC data class)
|
|
181
|
-
const candidates = [];
|
|
182
|
-
for (const iface of config.interfaces) {
|
|
183
|
-
// Check all alternates, not just alternates[0]
|
|
184
|
-
for (let altIndex = 0; altIndex < iface.alternates.length; altIndex++) {
|
|
185
|
-
const alt = iface.alternates[altIndex];
|
|
186
|
-
let hasIn = false, hasOut = false;
|
|
187
|
-
for (const ep of alt.endpoints) {
|
|
188
|
-
if (ep.type === 'bulk' && ep.direction === 'in') hasIn = true;
|
|
189
|
-
if (ep.type === 'bulk' && ep.direction === 'out') hasOut = true;
|
|
190
|
-
}
|
|
191
|
-
if (hasIn && hasOut) {
|
|
192
|
-
let score = 2;
|
|
193
|
-
if (alt.interfaceClass === 0x0a) score = 0; // CDC data first
|
|
194
|
-
else if (alt.interfaceClass === 0xff) score = 1; // vendor-specific next
|
|
195
|
-
candidates.push({ iface, altIndex, alt, score });
|
|
196
|
-
break; // Found suitable alternate for this interface
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
if (!candidates.length) {
|
|
202
|
-
throw new Error('No suitable USB interface found');
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
candidates.sort((a, b) => a.score - b.score);
|
|
206
|
-
let lastErr = null;
|
|
207
|
-
for (const cand of candidates) {
|
|
208
|
-
try {
|
|
209
|
-
// CORRECT ORDER per WebUSB spec: claimInterface FIRST, then selectAlternateInterface
|
|
210
|
-
await this.device.claimInterface(cand.iface.interfaceNumber);
|
|
211
|
-
try {
|
|
212
|
-
await this.device.selectAlternateInterface(cand.iface.interfaceNumber, cand.altIndex);
|
|
213
|
-
} catch (e) {
|
|
214
|
-
this._log(`[WebUSB] selectAlternateInterface failed: ${e.message}`);
|
|
215
|
-
}
|
|
216
|
-
this.interfaceNumber = cand.iface.interfaceNumber;
|
|
217
|
-
|
|
218
|
-
// Use the alternate that was found to have bulk endpoints
|
|
219
|
-
for (const ep of cand.alt.endpoints) {
|
|
220
|
-
if (ep.type === 'bulk' && ep.direction === 'in') {
|
|
221
|
-
this.endpointIn = ep.endpointNumber;
|
|
222
|
-
} else if (ep.type === 'bulk' && ep.direction === 'out') {
|
|
223
|
-
this.endpointOut = ep.endpointNumber;
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
// Validate that both endpoints were found
|
|
228
|
-
if (this.endpointIn == null || this.endpointOut == null) {
|
|
229
|
-
throw new Error(`Missing bulk endpoints (in=${this.endpointIn}, out=${this.endpointOut})`);
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
// Use endpoint packet size for transfer length (Android prefers max-packet)
|
|
233
|
-
try {
|
|
234
|
-
const inEp = cand.alt.endpoints.find(ep => ep.type === 'bulk' && ep.direction === 'in');
|
|
235
|
-
if (inEp && inEp.packetSize) {
|
|
236
|
-
// Don't limit by packetSize - use our optimized value
|
|
237
|
-
} else {
|
|
238
|
-
this._log(`[WebUSB] No packetSize found, keeping maxTransferSize=${this.maxTransferSize}`);
|
|
239
|
-
}
|
|
240
|
-
} catch (e) {
|
|
241
|
-
// Suppress packetSize check error - not critical
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
return config;
|
|
245
|
-
} catch (claimErr) {
|
|
246
|
-
lastErr = claimErr;
|
|
247
|
-
// Suppress claim failed message - this is expected when trying multiple interfaces
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
throw lastErr || new Error('Unable to claim any USB interface');
|
|
252
|
-
};
|
|
228
|
+
if (!candidates.length) {
|
|
229
|
+
throw new Error("No suitable USB interface found");
|
|
230
|
+
}
|
|
253
231
|
|
|
254
|
-
|
|
232
|
+
candidates.sort((a, b) => a.score - b.score);
|
|
233
|
+
let lastErr = null;
|
|
234
|
+
for (const cand of candidates) {
|
|
255
235
|
try {
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
236
|
+
// CORRECT ORDER per WebUSB spec: claimInterface FIRST, then selectAlternateInterface
|
|
237
|
+
await this.device.claimInterface(cand.iface.interfaceNumber);
|
|
238
|
+
try {
|
|
239
|
+
await this.device.selectAlternateInterface(
|
|
240
|
+
cand.iface.interfaceNumber,
|
|
241
|
+
cand.altIndex,
|
|
242
|
+
);
|
|
243
|
+
} catch (e) {
|
|
244
|
+
this._log(`[WebUSB] selectAlternateInterface failed: ${e.message}`);
|
|
245
|
+
}
|
|
246
|
+
this.interfaceNumber = cand.iface.interfaceNumber;
|
|
247
|
+
|
|
248
|
+
// Use the alternate that was found to have bulk endpoints
|
|
249
|
+
for (const ep of cand.alt.endpoints) {
|
|
250
|
+
if (ep.type === "bulk" && ep.direction === "in") {
|
|
251
|
+
this.endpointIn = ep.endpointNumber;
|
|
252
|
+
} else if (ep.type === "bulk" && ep.direction === "out") {
|
|
253
|
+
this.endpointOut = ep.endpointNumber;
|
|
265
254
|
}
|
|
266
|
-
|
|
255
|
+
}
|
|
267
256
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
i.interfaceNumber !== this.interfaceNumber
|
|
257
|
+
// Validate that both endpoints were found
|
|
258
|
+
if (this.endpointIn == null || this.endpointOut == null) {
|
|
259
|
+
throw new Error(
|
|
260
|
+
`Missing bulk endpoints (in=${this.endpointIn}, out=${this.endpointOut})`,
|
|
273
261
|
);
|
|
262
|
+
}
|
|
274
263
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
}
|
|
264
|
+
// Use endpoint packet size for transfer length (Android prefers max-packet)
|
|
265
|
+
try {
|
|
266
|
+
const inEp = cand.alt.endpoints.find(
|
|
267
|
+
(ep) => ep.type === "bulk" && ep.direction === "in",
|
|
268
|
+
);
|
|
269
|
+
if (inEp && inEp.packetSize) {
|
|
270
|
+
// Don't limit by packetSize - use our optimized value
|
|
283
271
|
} else {
|
|
284
|
-
|
|
272
|
+
this._log(
|
|
273
|
+
`[WebUSB] No packetSize found, keeping maxTransferSize=${this.maxTransferSize}`,
|
|
274
|
+
);
|
|
285
275
|
}
|
|
276
|
+
} catch (e) {
|
|
277
|
+
// Suppress packetSize check error - not critical
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return config;
|
|
281
|
+
} catch (claimErr) {
|
|
282
|
+
lastErr = claimErr;
|
|
283
|
+
// Suppress claim failed message - this is expected when trying multiple interfaces
|
|
286
284
|
}
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
recipient: 'device',
|
|
304
|
-
request: 0x03, // SET_LINE_CTL
|
|
305
|
-
value: 0x0800, // 8 data bits, no parity, 1 stop bit
|
|
306
|
-
index: 0x00
|
|
307
|
-
});
|
|
308
|
-
|
|
309
|
-
// Step 3: Set DTR/RTS signals (vendor-specific for CP2102)
|
|
310
|
-
await this.device.controlTransferOut({
|
|
311
|
-
requestType: 'vendor',
|
|
312
|
-
recipient: 'device',
|
|
313
|
-
request: 0x07, // SET_MHS
|
|
314
|
-
value: 0x03 | 0x0100 | 0x0200, // DTR=1, RTS=1 with masks
|
|
315
|
-
index: 0x00
|
|
316
|
-
});
|
|
317
|
-
|
|
318
|
-
// Step 4: Set baudrate (vendor-specific for CP2102)
|
|
319
|
-
// Use IFC_SET_BAUDRATE (0x1E) with direct 32-bit baudrate value
|
|
320
|
-
const baudrateBuffer = new ArrayBuffer(4);
|
|
321
|
-
const baudrateView = new DataView(baudrateBuffer);
|
|
322
|
-
baudrateView.setUint32(0, baudRate, true); // little-endian
|
|
323
|
-
|
|
324
|
-
await this.device.controlTransferOut({
|
|
325
|
-
requestType: 'vendor',
|
|
326
|
-
recipient: 'interface',
|
|
327
|
-
request: 0x1E, // IFC_SET_BAUDRATE
|
|
328
|
-
value: 0,
|
|
329
|
-
index: 0
|
|
330
|
-
}, baudrateBuffer);
|
|
331
|
-
} catch (e) {
|
|
332
|
-
this._log('[WebUSB CP2102] Initialization error:', e.message);
|
|
333
|
-
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
throw lastErr || new Error("Unable to claim any USB interface");
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
let config;
|
|
291
|
+
try {
|
|
292
|
+
config = await attemptOpenAndClaim();
|
|
293
|
+
} catch (err) {
|
|
294
|
+
this._log(
|
|
295
|
+
"[WebUSB] open/claim failed, retrying after reset:",
|
|
296
|
+
err.message,
|
|
297
|
+
);
|
|
298
|
+
try {
|
|
299
|
+
if (this.device.reset) {
|
|
300
|
+
await this.device.reset();
|
|
334
301
|
}
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
});
|
|
346
|
-
|
|
347
|
-
// Step 2: Set flow control to none
|
|
348
|
-
await this.device.controlTransferOut({
|
|
349
|
-
requestType: 'vendor',
|
|
350
|
-
recipient: 'device',
|
|
351
|
-
request: 0x02, // SIO_SET_FLOW_CTRL
|
|
352
|
-
value: 0x00, // No flow control
|
|
353
|
-
index: 0x00
|
|
354
|
-
});
|
|
355
|
-
|
|
356
|
-
// Step 3: Set data characteristics (8N1)
|
|
357
|
-
await this.device.controlTransferOut({
|
|
358
|
-
requestType: 'vendor',
|
|
359
|
-
recipient: 'device',
|
|
360
|
-
request: 0x04, // SIO_SET_DATA
|
|
361
|
-
value: 0x0008, // 8 data bits, no parity, 1 stop bit
|
|
362
|
-
index: 0x00
|
|
363
|
-
});
|
|
364
|
-
|
|
365
|
-
// Step 4: Set baudrate
|
|
366
|
-
const baseClock = 3000000; // 48MHz / 16
|
|
367
|
-
let divisor = baseClock / baudRate;
|
|
368
|
-
const integerPart = Math.floor(divisor);
|
|
369
|
-
const fractionalPart = divisor - integerPart;
|
|
370
|
-
|
|
371
|
-
let subInteger;
|
|
372
|
-
if (fractionalPart < 0.0625) subInteger = 0;
|
|
373
|
-
else if (fractionalPart < 0.1875) subInteger = 1;
|
|
374
|
-
else if (fractionalPart < 0.3125) subInteger = 2;
|
|
375
|
-
else if (fractionalPart < 0.4375) subInteger = 3;
|
|
376
|
-
else if (fractionalPart < 0.5625) subInteger = 4;
|
|
377
|
-
else if (fractionalPart < 0.6875) subInteger = 5;
|
|
378
|
-
else if (fractionalPart < 0.8125) subInteger = 6;
|
|
379
|
-
else subInteger = 7;
|
|
380
|
-
|
|
381
|
-
const value = (integerPart & 0xFF) | ((subInteger & 0x07) << 14) | (((integerPart >> 8) & 0x3F) << 8);
|
|
382
|
-
const index = (integerPart >> 14) & 0x03;
|
|
383
|
-
|
|
384
|
-
await this.device.controlTransferOut({
|
|
385
|
-
requestType: 'vendor',
|
|
386
|
-
recipient: 'device',
|
|
387
|
-
request: 0x03, // SIO_SET_BAUD_RATE
|
|
388
|
-
value: value,
|
|
389
|
-
index: index
|
|
390
|
-
});
|
|
391
|
-
|
|
392
|
-
// Step 5: Set DTR/RTS (modem control)
|
|
393
|
-
await this.device.controlTransferOut({
|
|
394
|
-
requestType: 'vendor',
|
|
395
|
-
recipient: 'device',
|
|
396
|
-
request: 0x01, // SIO_MODEM_CTRL
|
|
397
|
-
value: 0x0303, // DTR=1, RTS=1
|
|
398
|
-
index: 0x00
|
|
399
|
-
});
|
|
400
|
-
} catch (e) {
|
|
401
|
-
this._log('[WebUSB FTDI] Initialization error:', e.message);
|
|
402
|
-
}
|
|
403
|
-
}
|
|
404
|
-
// CH340-specific initialization (VID: 0x1a86, but not CH343 PID: 0x55d3)
|
|
405
|
-
else if (this.device.vendorId === 0x1a86 && this.device.productId !== 0x55d3) {
|
|
406
|
-
try {
|
|
407
|
-
// Step 1: Initialize CH340
|
|
408
|
-
await this.device.controlTransferOut({
|
|
409
|
-
requestType: 'vendor',
|
|
410
|
-
recipient: 'device',
|
|
411
|
-
request: 0xA1, // CH340 INIT
|
|
412
|
-
value: 0x0000,
|
|
413
|
-
index: 0x0000
|
|
414
|
-
});
|
|
415
|
-
|
|
416
|
-
// Step 2: Set baudrate
|
|
417
|
-
const CH341_BAUDBASE_FACTOR = 1532620800;
|
|
418
|
-
const CH341_BAUDBASE_DIVMAX = 3;
|
|
419
|
-
|
|
420
|
-
let factor = Math.floor(CH341_BAUDBASE_FACTOR / baudRate);
|
|
421
|
-
let divisor = CH341_BAUDBASE_DIVMAX;
|
|
422
|
-
|
|
423
|
-
while (factor > 0xfff0 && divisor > 0) {
|
|
424
|
-
factor >>= 3;
|
|
425
|
-
divisor--;
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
if (factor > 0xfff0) {
|
|
429
|
-
throw new Error(`Baudrate ${baudRate} not supported by CH340`);
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
factor = 0x10000 - factor;
|
|
433
|
-
const a = (factor & 0xff00) | divisor;
|
|
434
|
-
const b = factor & 0xff;
|
|
435
|
-
|
|
436
|
-
await this.device.controlTransferOut({
|
|
437
|
-
requestType: 'vendor',
|
|
438
|
-
recipient: 'device',
|
|
439
|
-
request: 0x9A,
|
|
440
|
-
value: 0x1312,
|
|
441
|
-
index: a
|
|
442
|
-
});
|
|
443
|
-
|
|
444
|
-
await this.device.controlTransferOut({
|
|
445
|
-
requestType: 'vendor',
|
|
446
|
-
recipient: 'device',
|
|
447
|
-
request: 0x9A,
|
|
448
|
-
value: 0x0f2c,
|
|
449
|
-
index: b
|
|
450
|
-
});
|
|
451
|
-
|
|
452
|
-
// Step 3: Set handshake (DTR/RTS)
|
|
453
|
-
await this.device.controlTransferOut({
|
|
454
|
-
requestType: 'vendor',
|
|
455
|
-
recipient: 'device',
|
|
456
|
-
request: 0xA4, // CH340 SET_HANDSHAKE
|
|
457
|
-
value: (~((1 << 5) | (1 << 6))) & 0xffff, // DTR=1, RTS=1 (inverted), masked to 16-bit
|
|
458
|
-
index: 0x0000
|
|
459
|
-
});
|
|
460
|
-
} catch (e) {
|
|
461
|
-
this._log('[WebUSB CH340] Initialization error:', e.message);
|
|
462
|
-
}
|
|
463
|
-
} else {
|
|
464
|
-
// Standard CDC/ACM initialization for other chips
|
|
465
|
-
try {
|
|
466
|
-
const lineCoding = new Uint8Array([
|
|
467
|
-
baudRate & 0xFF,
|
|
468
|
-
(baudRate >> 8) & 0xFF,
|
|
469
|
-
(baudRate >> 16) & 0xFF,
|
|
470
|
-
(baudRate >> 24) & 0xFF,
|
|
471
|
-
0x00, // 1 stop bit
|
|
472
|
-
0x00, // No parity
|
|
473
|
-
0x08 // 8 data bits
|
|
474
|
-
]);
|
|
475
|
-
|
|
476
|
-
await this.device.controlTransferOut({
|
|
477
|
-
requestType: 'class',
|
|
478
|
-
recipient: 'interface',
|
|
479
|
-
request: 0x20, // SET_LINE_CODING
|
|
480
|
-
value: 0,
|
|
481
|
-
index: this.controlInterface || 0
|
|
482
|
-
}, lineCoding);
|
|
483
|
-
} catch (e) {
|
|
484
|
-
this._log('Could not set line coding:', e.message);
|
|
485
|
-
}
|
|
302
|
+
} catch (e) {}
|
|
303
|
+
try {
|
|
304
|
+
await this.device.close();
|
|
305
|
+
} catch (e) {}
|
|
306
|
+
try {
|
|
307
|
+
config = await attemptOpenAndClaim();
|
|
308
|
+
} catch (err2) {
|
|
309
|
+
throw new Error(`Unable to claim USB interface: ${err2.message}`);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
486
312
|
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
index: this.controlInterface || 0
|
|
495
|
-
});
|
|
496
|
-
} catch (e) {
|
|
497
|
-
this._log('Could not set control lines:', e.message);
|
|
498
|
-
}
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
// Create streams only if they don't exist yet
|
|
502
|
-
if (!this.readableStream || !this.writableStream) {
|
|
503
|
-
this._createStreams();
|
|
504
|
-
} else {
|
|
505
|
-
// Streams exist, but make sure read loop is running
|
|
506
|
-
if (!this._readLoopRunning) {
|
|
507
|
-
this._readLoopRunning = true;
|
|
508
|
-
// Note: ReadableStream can't be restarted, we need to recreate it
|
|
509
|
-
this._createStreams();
|
|
510
|
-
}
|
|
511
|
-
}
|
|
313
|
+
// Claim control interface if not already claimed
|
|
314
|
+
if (this.controlInterface == null) {
|
|
315
|
+
const controlIface = config.interfaces.find(
|
|
316
|
+
(i) =>
|
|
317
|
+
i.alternates[0].interfaceClass === 0x02 &&
|
|
318
|
+
i.interfaceNumber !== this.interfaceNumber,
|
|
319
|
+
);
|
|
512
320
|
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
321
|
+
if (controlIface) {
|
|
322
|
+
try {
|
|
323
|
+
await this.device.claimInterface(controlIface.interfaceNumber);
|
|
324
|
+
try {
|
|
325
|
+
await this.device.selectAlternateInterface(
|
|
326
|
+
controlIface.interfaceNumber,
|
|
327
|
+
0,
|
|
328
|
+
);
|
|
329
|
+
} catch (e) {}
|
|
330
|
+
this.controlInterface = controlIface.interfaceNumber;
|
|
331
|
+
} catch (e) {
|
|
332
|
+
this.controlInterface = this.interfaceNumber;
|
|
522
333
|
}
|
|
334
|
+
} else {
|
|
335
|
+
this.controlInterface = this.interfaceNumber;
|
|
336
|
+
}
|
|
523
337
|
}
|
|
524
338
|
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
this.
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
await this.device.releaseInterface(this.controlInterface);
|
|
537
|
-
}
|
|
538
|
-
await this.device.close();
|
|
539
|
-
} catch (e) {
|
|
540
|
-
if (!e.message || !e.message.includes('disconnected')) {
|
|
541
|
-
this._log('Error closing device:', e.message || e);
|
|
542
|
-
}
|
|
543
|
-
}
|
|
544
|
-
// Keep device reference for potential reconfiguration
|
|
545
|
-
}
|
|
546
|
-
}
|
|
339
|
+
// CP2102-specific initialization sequence (must be in this exact order!)
|
|
340
|
+
if (this.device.vendorId === 0x10c4) {
|
|
341
|
+
try {
|
|
342
|
+
// Step 1: Enable UART interface
|
|
343
|
+
await this.device.controlTransferOut({
|
|
344
|
+
requestType: "vendor",
|
|
345
|
+
recipient: "device",
|
|
346
|
+
request: 0x00, // IFC_ENABLE
|
|
347
|
+
value: 0x01, // UART_ENABLE
|
|
348
|
+
index: 0x00,
|
|
349
|
+
});
|
|
547
350
|
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
351
|
+
// Step 2: Set line control (8N1: 8 data bits, no parity, 1 stop bit)
|
|
352
|
+
await this.device.controlTransferOut({
|
|
353
|
+
requestType: "vendor",
|
|
354
|
+
recipient: "device",
|
|
355
|
+
request: 0x03, // SET_LINE_CTL
|
|
356
|
+
value: 0x0800, // 8 data bits, no parity, 1 stop bit
|
|
357
|
+
index: 0x00,
|
|
358
|
+
});
|
|
555
359
|
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
// blockSize = (maxTransferSize - 2) / 2
|
|
565
|
-
// -2 for SLIP frame delimiters (0xC0 at start/end)
|
|
566
|
-
// /2 because worst case every byte could be escaped (0xDB 0xDC or 0xDB 0xDD)
|
|
567
|
-
return Math.floor((this.maxTransferSize - 2) / 2);
|
|
568
|
-
}
|
|
360
|
+
// Step 3: Set DTR/RTS signals (vendor-specific for CP2102)
|
|
361
|
+
await this.device.controlTransferOut({
|
|
362
|
+
requestType: "vendor",
|
|
363
|
+
recipient: "device",
|
|
364
|
+
request: 0x07, // SET_MHS
|
|
365
|
+
value: 0x03 | 0x0100 | 0x0200, // DTR=1, RTS=1 with masks
|
|
366
|
+
index: 0x00,
|
|
367
|
+
});
|
|
569
368
|
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
369
|
+
// Step 4: Set baudrate (vendor-specific for CP2102)
|
|
370
|
+
// Use IFC_SET_BAUDRATE (0x1E) with direct 32-bit baudrate value
|
|
371
|
+
const baudrateBuffer = new ArrayBuffer(4);
|
|
372
|
+
const baudrateView = new DataView(baudrateBuffer);
|
|
373
|
+
baudrateView.setUint32(0, baudRate, true); // little-endian
|
|
374
|
+
|
|
375
|
+
await this.device.controlTransferOut(
|
|
376
|
+
{
|
|
377
|
+
requestType: "vendor",
|
|
378
|
+
recipient: "interface",
|
|
379
|
+
request: 0x1e, // IFC_SET_BAUDRATE
|
|
380
|
+
value: 0,
|
|
381
|
+
index: 0,
|
|
382
|
+
},
|
|
383
|
+
baudrateBuffer,
|
|
384
|
+
);
|
|
385
|
+
} catch (e) {
|
|
386
|
+
this._log("[WebUSB CP2102] Initialization error:", e.message);
|
|
387
|
+
}
|
|
581
388
|
}
|
|
389
|
+
// FTDI-specific initialization sequence
|
|
390
|
+
else if (this.device.vendorId === 0x0403) {
|
|
391
|
+
try {
|
|
392
|
+
// Step 1: Reset device
|
|
393
|
+
await this.device.controlTransferOut({
|
|
394
|
+
requestType: "vendor",
|
|
395
|
+
recipient: "device",
|
|
396
|
+
request: 0x00, // SIO_RESET
|
|
397
|
+
value: 0x00, // Reset
|
|
398
|
+
index: 0x00,
|
|
399
|
+
});
|
|
582
400
|
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
this._commandQueue = this._commandQueue.then(async () => {
|
|
592
|
-
if (!this.device) {
|
|
593
|
-
throw new Error('Device not open');
|
|
594
|
-
}
|
|
401
|
+
// Step 2: Set flow control to none
|
|
402
|
+
await this.device.controlTransferOut({
|
|
403
|
+
requestType: "vendor",
|
|
404
|
+
recipient: "device",
|
|
405
|
+
request: 0x02, // SIO_SET_FLOW_CTRL
|
|
406
|
+
value: 0x00, // No flow control
|
|
407
|
+
index: 0x00,
|
|
408
|
+
});
|
|
595
409
|
|
|
596
|
-
|
|
597
|
-
|
|
410
|
+
// Step 3: Set data characteristics (8N1)
|
|
411
|
+
await this.device.controlTransferOut({
|
|
412
|
+
requestType: "vendor",
|
|
413
|
+
recipient: "device",
|
|
414
|
+
request: 0x04, // SIO_SET_DATA
|
|
415
|
+
value: 0x0008, // 8 data bits, no parity, 1 stop bit
|
|
416
|
+
index: 0x00,
|
|
417
|
+
});
|
|
598
418
|
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
419
|
+
// Step 4: Set baudrate
|
|
420
|
+
const baseClock = 3000000; // 48MHz / 16
|
|
421
|
+
let divisor = baseClock / baudRate;
|
|
422
|
+
const integerPart = Math.floor(divisor);
|
|
423
|
+
const fractionalPart = divisor - integerPart;
|
|
424
|
+
|
|
425
|
+
let subInteger;
|
|
426
|
+
if (fractionalPart < 0.0625) subInteger = 0;
|
|
427
|
+
else if (fractionalPart < 0.1875) subInteger = 1;
|
|
428
|
+
else if (fractionalPart < 0.3125) subInteger = 2;
|
|
429
|
+
else if (fractionalPart < 0.4375) subInteger = 3;
|
|
430
|
+
else if (fractionalPart < 0.5625) subInteger = 4;
|
|
431
|
+
else if (fractionalPart < 0.6875) subInteger = 5;
|
|
432
|
+
else if (fractionalPart < 0.8125) subInteger = 6;
|
|
433
|
+
else subInteger = 7;
|
|
434
|
+
|
|
435
|
+
const value =
|
|
436
|
+
(integerPart & 0xff) |
|
|
437
|
+
((subInteger & 0x07) << 14) |
|
|
438
|
+
(((integerPart >> 8) & 0x3f) << 8);
|
|
439
|
+
const index = (integerPart >> 14) & 0x03;
|
|
440
|
+
|
|
441
|
+
await this.device.controlTransferOut({
|
|
442
|
+
requestType: "vendor",
|
|
443
|
+
recipient: "device",
|
|
444
|
+
request: 0x03, // SIO_SET_BAUD_RATE
|
|
445
|
+
value: value,
|
|
446
|
+
index: index,
|
|
615
447
|
});
|
|
616
|
-
|
|
617
|
-
|
|
448
|
+
|
|
449
|
+
// Step 5: Set DTR/RTS (modem control)
|
|
450
|
+
await this.device.controlTransferOut({
|
|
451
|
+
requestType: "vendor",
|
|
452
|
+
recipient: "device",
|
|
453
|
+
request: 0x01, // SIO_MODEM_CTRL
|
|
454
|
+
value: 0x0303, // DTR=1, RTS=1
|
|
455
|
+
index: 0x00,
|
|
456
|
+
});
|
|
457
|
+
} catch (e) {
|
|
458
|
+
this._log("[WebUSB FTDI] Initialization error:", e.message);
|
|
459
|
+
}
|
|
618
460
|
}
|
|
461
|
+
// CH340-specific initialization (VID: 0x1a86, but not CH343 PID: 0x55d3)
|
|
462
|
+
else if (
|
|
463
|
+
this.device.vendorId === 0x1a86 &&
|
|
464
|
+
this.device.productId !== 0x55d3
|
|
465
|
+
) {
|
|
466
|
+
try {
|
|
467
|
+
// Step 1: Initialize CH340
|
|
468
|
+
await this.device.controlTransferOut({
|
|
469
|
+
requestType: "vendor",
|
|
470
|
+
recipient: "device",
|
|
471
|
+
request: 0xa1, // CH340 INIT
|
|
472
|
+
value: 0x0000,
|
|
473
|
+
index: 0x0000,
|
|
474
|
+
});
|
|
619
475
|
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
async _setSignalsCDC(signals) {
|
|
624
|
-
// Preserve current state for unspecified signals (Web Serial semantics)
|
|
625
|
-
const dtr = signals.dataTerminalReady !== undefined ? signals.dataTerminalReady : this._currentDTR;
|
|
626
|
-
const rts = signals.requestToSend !== undefined ? signals.requestToSend : this._currentRTS;
|
|
627
|
-
|
|
628
|
-
// Update tracked state
|
|
629
|
-
this._currentDTR = dtr;
|
|
630
|
-
this._currentRTS = rts;
|
|
631
|
-
|
|
632
|
-
let value = 0;
|
|
633
|
-
value |= dtr ? 1 : 0;
|
|
634
|
-
value |= rts ? 2 : 0;
|
|
476
|
+
// Step 2: Set baudrate
|
|
477
|
+
const CH341_BAUDBASE_FACTOR = 1532620800;
|
|
478
|
+
const CH341_BAUDBASE_DIVMAX = 3;
|
|
635
479
|
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
requestType: 'class',
|
|
639
|
-
recipient: 'interface',
|
|
640
|
-
request: 0x22, // SET_CONTROL_LINE_STATE
|
|
641
|
-
value: value,
|
|
642
|
-
index: this.controlInterface || 0
|
|
643
|
-
});
|
|
644
|
-
|
|
645
|
-
await new Promise(resolve => setTimeout(resolve, 50));
|
|
646
|
-
return result;
|
|
647
|
-
} catch (e) {
|
|
648
|
-
this._log(`[WebUSB CDC] Failed to set signals: ${e.message}`);
|
|
649
|
-
throw e;
|
|
650
|
-
}
|
|
651
|
-
}
|
|
480
|
+
let factor = Math.floor(CH341_BAUDBASE_FACTOR / baudRate);
|
|
481
|
+
let divisor = CH341_BAUDBASE_DIVMAX;
|
|
652
482
|
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
// CP2102 uses vendor-specific request 0x07 (SET_MHS)
|
|
658
|
-
// Bit 0: DTR, Bit 1: RTS, Bit 8-9: DTR/RTS mask
|
|
659
|
-
|
|
660
|
-
// Preserve current state for unspecified signals (Web Serial semantics)
|
|
661
|
-
const dtr = signals.dataTerminalReady !== undefined ? signals.dataTerminalReady : this._currentDTR;
|
|
662
|
-
const rts = signals.requestToSend !== undefined ? signals.requestToSend : this._currentRTS;
|
|
663
|
-
|
|
664
|
-
// Update tracked state
|
|
665
|
-
this._currentDTR = dtr;
|
|
666
|
-
this._currentRTS = rts;
|
|
667
|
-
|
|
668
|
-
// Build value with mask bits for both signals
|
|
669
|
-
let value = 0;
|
|
670
|
-
value |= (dtr ? 1 : 0) | 0x100; // DTR + mask
|
|
671
|
-
value |= (rts ? 2 : 0) | 0x200; // RTS + mask
|
|
483
|
+
while (factor > 0xfff0 && divisor > 0) {
|
|
484
|
+
factor >>= 3;
|
|
485
|
+
divisor--;
|
|
486
|
+
}
|
|
672
487
|
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
requestType: 'vendor',
|
|
676
|
-
recipient: 'device',
|
|
677
|
-
request: 0x07, // SET_MHS (Modem Handshaking)
|
|
678
|
-
value: value,
|
|
679
|
-
index: 0x00 // CP2102 always uses index 0
|
|
680
|
-
});
|
|
681
|
-
|
|
682
|
-
await new Promise(resolve => setTimeout(resolve, 50));
|
|
683
|
-
return result;
|
|
684
|
-
} catch (e) {
|
|
685
|
-
this._log(`[WebUSB CP2102] Failed to set signals: ${e.message}`);
|
|
686
|
-
throw e;
|
|
488
|
+
if (factor > 0xfff0) {
|
|
489
|
+
throw new Error(`Baudrate ${baudRate} not supported by CH340`);
|
|
687
490
|
}
|
|
491
|
+
|
|
492
|
+
factor = 0x10000 - factor;
|
|
493
|
+
const a = (factor & 0xff00) | divisor;
|
|
494
|
+
const b = factor & 0xff;
|
|
495
|
+
|
|
496
|
+
await this.device.controlTransferOut({
|
|
497
|
+
requestType: "vendor",
|
|
498
|
+
recipient: "device",
|
|
499
|
+
request: 0x9a,
|
|
500
|
+
value: 0x1312,
|
|
501
|
+
index: a,
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
await this.device.controlTransferOut({
|
|
505
|
+
requestType: "vendor",
|
|
506
|
+
recipient: "device",
|
|
507
|
+
request: 0x9a,
|
|
508
|
+
value: 0x0f2c,
|
|
509
|
+
index: b,
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
// Step 3: Set handshake (DTR/RTS)
|
|
513
|
+
await this.device.controlTransferOut({
|
|
514
|
+
requestType: "vendor",
|
|
515
|
+
recipient: "device",
|
|
516
|
+
request: 0xa4, // CH340 SET_HANDSHAKE
|
|
517
|
+
value: ~((1 << 5) | (1 << 6)) & 0xffff, // DTR=1, RTS=1 (inverted), masked to 16-bit
|
|
518
|
+
index: 0x0000,
|
|
519
|
+
});
|
|
520
|
+
} catch (e) {
|
|
521
|
+
this._log("[WebUSB CH340] Initialization error:", e.message);
|
|
522
|
+
}
|
|
523
|
+
} else {
|
|
524
|
+
// Standard CDC/ACM initialization for other chips
|
|
525
|
+
try {
|
|
526
|
+
const lineCoding = new Uint8Array([
|
|
527
|
+
baudRate & 0xff,
|
|
528
|
+
(baudRate >> 8) & 0xff,
|
|
529
|
+
(baudRate >> 16) & 0xff,
|
|
530
|
+
(baudRate >> 24) & 0xff,
|
|
531
|
+
0x00, // 1 stop bit
|
|
532
|
+
0x00, // No parity
|
|
533
|
+
0x08, // 8 data bits
|
|
534
|
+
]);
|
|
535
|
+
|
|
536
|
+
await this.device.controlTransferOut(
|
|
537
|
+
{
|
|
538
|
+
requestType: "class",
|
|
539
|
+
recipient: "interface",
|
|
540
|
+
request: 0x20, // SET_LINE_CODING
|
|
541
|
+
value: 0,
|
|
542
|
+
index: this.controlInterface || 0,
|
|
543
|
+
},
|
|
544
|
+
lineCoding,
|
|
545
|
+
);
|
|
546
|
+
} catch (e) {
|
|
547
|
+
this._log("Could not set line coding:", e.message);
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
// Initialize DTR/RTS to idle state (both HIGH/asserted)
|
|
551
|
+
try {
|
|
552
|
+
await this.device.controlTransferOut({
|
|
553
|
+
requestType: "class",
|
|
554
|
+
recipient: "interface",
|
|
555
|
+
request: 0x22, // SET_CONTROL_LINE_STATE
|
|
556
|
+
value: 0x03, // DTR=1, RTS=1 (both asserted)
|
|
557
|
+
index: this.controlInterface || 0,
|
|
558
|
+
});
|
|
559
|
+
} catch (e) {
|
|
560
|
+
this._log("Could not set control lines:", e.message);
|
|
561
|
+
}
|
|
688
562
|
}
|
|
689
563
|
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
// CH340 uses vendor-specific request 0xA4
|
|
703
|
-
// Bit 5: DTR, Bit 6: RTS (inverted logic!)
|
|
704
|
-
// Calculate value with bitwise NOT and mask to unsigned 16-bit
|
|
705
|
-
const value = (~((dtr ? 1 << 5 : 0) | (rts ? 1 << 6 : 0))) & 0xffff;
|
|
564
|
+
// Create streams only if they don't exist yet
|
|
565
|
+
if (!this.readableStream || !this.writableStream) {
|
|
566
|
+
this._createStreams();
|
|
567
|
+
} else {
|
|
568
|
+
// Streams exist, but make sure read loop is running
|
|
569
|
+
if (!this._readLoopRunning) {
|
|
570
|
+
this._readLoopRunning = true;
|
|
571
|
+
// Note: ReadableStream can't be restarted, we need to recreate it
|
|
572
|
+
this._createStreams();
|
|
573
|
+
}
|
|
574
|
+
}
|
|
706
575
|
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
index: 0
|
|
714
|
-
});
|
|
715
|
-
|
|
716
|
-
await new Promise(resolve => setTimeout(resolve, 50));
|
|
717
|
-
return result;
|
|
718
|
-
} catch (e) {
|
|
719
|
-
this._log(`[WebUSB CH340] Failed to set signals: ${e.message}`);
|
|
720
|
-
throw e;
|
|
576
|
+
// Setup disconnect handler only once
|
|
577
|
+
if (!this._usbDisconnectHandler) {
|
|
578
|
+
this._usbDisconnectHandler = (event) => {
|
|
579
|
+
if (event.device === this.device) {
|
|
580
|
+
this._fireEvent("disconnect");
|
|
581
|
+
this._cleanup();
|
|
721
582
|
}
|
|
583
|
+
};
|
|
584
|
+
navigator.usb.addEventListener("disconnect", this._usbDisconnectHandler);
|
|
722
585
|
}
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
/**
|
|
589
|
+
* Close the device (mimics port.close())
|
|
590
|
+
*/
|
|
591
|
+
async close() {
|
|
592
|
+
this._cleanup();
|
|
593
|
+
if (this.device) {
|
|
594
|
+
try {
|
|
595
|
+
if (this.interfaceNumber !== null) {
|
|
596
|
+
await this.device.releaseInterface(this.interfaceNumber);
|
|
597
|
+
}
|
|
598
|
+
if (
|
|
599
|
+
this.controlInterface !== null &&
|
|
600
|
+
this.controlInterface !== this.interfaceNumber
|
|
601
|
+
) {
|
|
602
|
+
await this.device.releaseInterface(this.controlInterface);
|
|
603
|
+
}
|
|
604
|
+
await this.device.close();
|
|
605
|
+
} catch (e) {
|
|
606
|
+
if (!e.message || !e.message.includes("disconnected")) {
|
|
607
|
+
this._log("Error closing device:", e.message || e);
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
// Keep device reference for potential reconfiguration
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
/**
|
|
615
|
+
* Disconnect and clear device reference (for final cleanup)
|
|
616
|
+
*/
|
|
617
|
+
async disconnect() {
|
|
618
|
+
await this.close();
|
|
619
|
+
this.device = null;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
/**
|
|
623
|
+
* Get optimal block size for flash read operations
|
|
624
|
+
* (maxTransferSize - 2) / 2
|
|
625
|
+
* This accounts for SLIP overhead and escape sequences
|
|
626
|
+
* @returns {number} Optimal block size in bytes
|
|
627
|
+
*/
|
|
628
|
+
getOptimalReadBlockSize() {
|
|
629
|
+
// Formula for WebUSB:
|
|
630
|
+
// blockSize = (maxTransferSize - 2) / 2
|
|
631
|
+
// -2 for SLIP frame delimiters (0xC0 at start/end)
|
|
632
|
+
// /2 because worst case every byte could be escaped (0xDB 0xDC or 0xDB 0xDD)
|
|
633
|
+
return Math.floor((this.maxTransferSize - 2) / 2);
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
/**
|
|
637
|
+
* Get device info (mimics port.getInfo())
|
|
638
|
+
*/
|
|
639
|
+
getInfo() {
|
|
640
|
+
if (!this.device) {
|
|
641
|
+
return {};
|
|
642
|
+
}
|
|
643
|
+
return {
|
|
644
|
+
usbVendorId: this.device.vendorId,
|
|
645
|
+
usbProductId: this.device.productId,
|
|
646
|
+
};
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
/**
|
|
650
|
+
* Set DTR/RTS signals (mimics port.setSignals())
|
|
651
|
+
* CRITICAL: Commands are serialized via queue for CP2102 compatibility
|
|
652
|
+
* Supports both CDC/ACM (CH343) and Vendor-Specific (CP2102, CH340)
|
|
653
|
+
*/
|
|
654
|
+
async setSignals(signals) {
|
|
655
|
+
// Serialize all control transfers through a queue
|
|
656
|
+
// This is CRITICAL for CP2102 - parallel commands cause hangs
|
|
657
|
+
this._commandQueue = this._commandQueue
|
|
658
|
+
.then(async () => {
|
|
731
659
|
if (!this.device) {
|
|
732
|
-
|
|
660
|
+
throw new Error("Device not open");
|
|
733
661
|
}
|
|
734
662
|
|
|
735
663
|
const vid = this.device.vendorId;
|
|
736
664
|
const pid = this.device.productId;
|
|
737
665
|
|
|
738
|
-
//
|
|
739
|
-
|
|
740
|
-
// FTDI (VID: 0x0403)
|
|
741
|
-
if (vid === 0x0403) {
|
|
742
|
-
// FTDI baudrate calculation
|
|
743
|
-
// Modern FTDI chips (FT232R, FT2232, etc.): BaseClock = 48MHz
|
|
744
|
-
// BaudDivisor = (48000000 / 16) / BaudRate = 3000000 / BaudRate
|
|
745
|
-
// Divisor encoding: 16-bit value with sub-integer divisor support
|
|
746
|
-
// Sub-integer divisor: 0, 0.125, 0.25, 0.375, 0.5, 0.625, 0.75, 0.875
|
|
747
|
-
|
|
748
|
-
const baseClock = 3000000; // 48MHz / 16
|
|
749
|
-
let divisor = baseClock / baudRate;
|
|
750
|
-
|
|
751
|
-
// Extract integer and fractional parts
|
|
752
|
-
const integerPart = Math.floor(divisor);
|
|
753
|
-
const fractionalPart = divisor - integerPart;
|
|
754
|
-
|
|
755
|
-
// Encode sub-integer divisor (0, 0.125, 0.25, 0.375, 0.5, 0.625, 0.75, 0.875)
|
|
756
|
-
let subInteger;
|
|
757
|
-
if (fractionalPart < 0.0625) subInteger = 0; // 0.0
|
|
758
|
-
else if (fractionalPart < 0.1875) subInteger = 1; // 0.125
|
|
759
|
-
else if (fractionalPart < 0.3125) subInteger = 2; // 0.25
|
|
760
|
-
else if (fractionalPart < 0.4375) subInteger = 3; // 0.375
|
|
761
|
-
else if (fractionalPart < 0.5625) subInteger = 4; // 0.5
|
|
762
|
-
else if (fractionalPart < 0.6875) subInteger = 5; // 0.625
|
|
763
|
-
else if (fractionalPart < 0.8125) subInteger = 6; // 0.75
|
|
764
|
-
else subInteger = 7; // 0.875
|
|
765
|
-
|
|
766
|
-
// Encode divisor value for FTDI
|
|
767
|
-
// Low byte: integer part (bits 0-7)
|
|
768
|
-
// High byte: (integer part >> 8) | (sub-integer << 6)
|
|
769
|
-
const value = (integerPart & 0xFF) | ((subInteger & 0x07) << 14) | (((integerPart >> 8) & 0x3F) << 8);
|
|
770
|
-
const index = (integerPart >> 14) & 0x03; // Upper 2 bits of integer part
|
|
771
|
-
|
|
772
|
-
// this._log(`[WebUSB FTDI] Setting baudrate ${baudRate} (divisor=${divisor.toFixed(3)}, value=0x${value.toString(16)}, index=0x${index.toString(16)})...`);
|
|
773
|
-
|
|
774
|
-
await this.device.controlTransferOut({
|
|
775
|
-
requestType: 'vendor',
|
|
776
|
-
recipient: 'device',
|
|
777
|
-
request: 0x03, // SIO_SET_BAUD_RATE
|
|
778
|
-
value: value,
|
|
779
|
-
index: index
|
|
780
|
-
});
|
|
781
|
-
|
|
782
|
-
// this._log('[WebUSB FTDI] Baudrate changed successfully');
|
|
783
|
-
}
|
|
666
|
+
// Detect chip type and use appropriate control request
|
|
784
667
|
// CP2102 (Silicon Labs VID: 0x10c4)
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
// For CP2102/CP2103: Use direct 32-bit baudrate value
|
|
788
|
-
// Request: IFC_SET_BAUDRATE (0x1E)
|
|
789
|
-
|
|
790
|
-
// Encode baudrate as 32-bit little-endian value
|
|
791
|
-
const baudrateBuffer = new ArrayBuffer(4);
|
|
792
|
-
const baudrateView = new DataView(baudrateBuffer);
|
|
793
|
-
baudrateView.setUint32(0, baudRate, true); // little-endian
|
|
794
|
-
|
|
795
|
-
await this.device.controlTransferOut({
|
|
796
|
-
requestType: 'vendor',
|
|
797
|
-
recipient: 'interface',
|
|
798
|
-
request: 0x1E, // IFC_SET_BAUDRATE
|
|
799
|
-
value: 0,
|
|
800
|
-
index: 0
|
|
801
|
-
}, baudrateBuffer);
|
|
668
|
+
if (vid === 0x10c4) {
|
|
669
|
+
return await this._setSignalsCP2102(signals);
|
|
802
670
|
}
|
|
803
671
|
// CH340 (WCH VID: 0x1a86, but not CH343 PID: 0x55d3)
|
|
804
672
|
else if (vid === 0x1a86 && pid !== 0x55d3) {
|
|
805
|
-
|
|
806
|
-
const CH341_BAUDBASE_FACTOR = 1532620800;
|
|
807
|
-
const CH341_BAUDBASE_DIVMAX = 3;
|
|
808
|
-
|
|
809
|
-
let factor = Math.floor(CH341_BAUDBASE_FACTOR / baudRate);
|
|
810
|
-
let divisor = CH341_BAUDBASE_DIVMAX;
|
|
811
|
-
|
|
812
|
-
// Reduce factor if too large
|
|
813
|
-
while (factor > 0xfff0 && divisor > 0) {
|
|
814
|
-
factor >>= 3;
|
|
815
|
-
divisor--;
|
|
816
|
-
}
|
|
817
|
-
|
|
818
|
-
if (factor > 0xfff0) {
|
|
819
|
-
throw new Error(`Baudrate ${baudRate} not supported by CH340`);
|
|
820
|
-
}
|
|
821
|
-
|
|
822
|
-
factor = 0x10000 - factor;
|
|
823
|
-
const a = (factor & 0xff00) | divisor;
|
|
824
|
-
const b = factor & 0xff;
|
|
825
|
-
|
|
826
|
-
// CH340 uses request 0x9A to set baudrate
|
|
827
|
-
await this.device.controlTransferOut({
|
|
828
|
-
requestType: 'vendor',
|
|
829
|
-
recipient: 'device',
|
|
830
|
-
request: 0x9A, // CH340 SET_BAUDRATE
|
|
831
|
-
value: 0x1312, // Fixed value for baudrate setting
|
|
832
|
-
index: a
|
|
833
|
-
});
|
|
834
|
-
|
|
835
|
-
// Second control transfer with b value
|
|
836
|
-
await this.device.controlTransferOut({
|
|
837
|
-
requestType: 'vendor',
|
|
838
|
-
recipient: 'device',
|
|
839
|
-
request: 0x9A,
|
|
840
|
-
value: 0x0f2c, // Fixed value
|
|
841
|
-
index: b
|
|
842
|
-
});
|
|
843
|
-
|
|
673
|
+
return await this._setSignalsCH340(signals);
|
|
844
674
|
}
|
|
845
|
-
// CDC
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
675
|
+
// CDC/ACM (CH343, Native USB, etc.)
|
|
676
|
+
else {
|
|
677
|
+
return await this._setSignalsCDC(signals);
|
|
678
|
+
}
|
|
679
|
+
})
|
|
680
|
+
.catch((err) => {
|
|
681
|
+
this._log("[WebUSB] setSignals error:", err);
|
|
682
|
+
throw err;
|
|
683
|
+
});
|
|
684
|
+
|
|
685
|
+
return this._commandQueue;
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
/**
|
|
689
|
+
* Set signals using CDC/ACM standard (for CH343, Native USB)
|
|
690
|
+
*/
|
|
691
|
+
async _setSignalsCDC(signals) {
|
|
692
|
+
// Preserve current state for unspecified signals (Web Serial semantics)
|
|
693
|
+
const dtr =
|
|
694
|
+
signals.dataTerminalReady !== undefined
|
|
695
|
+
? signals.dataTerminalReady
|
|
696
|
+
: this._currentDTR;
|
|
697
|
+
const rts =
|
|
698
|
+
signals.requestToSend !== undefined
|
|
699
|
+
? signals.requestToSend
|
|
700
|
+
: this._currentRTS;
|
|
701
|
+
|
|
702
|
+
// Update tracked state
|
|
703
|
+
this._currentDTR = dtr;
|
|
704
|
+
this._currentRTS = rts;
|
|
705
|
+
|
|
706
|
+
let value = 0;
|
|
707
|
+
value |= dtr ? 1 : 0;
|
|
708
|
+
value |= rts ? 2 : 0;
|
|
709
|
+
|
|
710
|
+
try {
|
|
711
|
+
const result = await this.device.controlTransferOut({
|
|
712
|
+
requestType: "class",
|
|
713
|
+
recipient: "interface",
|
|
714
|
+
request: 0x22, // SET_CONTROL_LINE_STATE
|
|
715
|
+
value: value,
|
|
716
|
+
index: this.controlInterface || 0,
|
|
717
|
+
});
|
|
718
|
+
|
|
719
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
720
|
+
return result;
|
|
721
|
+
} catch (e) {
|
|
722
|
+
this._log(`[WebUSB CDC] Failed to set signals: ${e.message}`);
|
|
723
|
+
throw e;
|
|
850
724
|
}
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
/**
|
|
728
|
+
* Set signals for CP2102 (Silicon Labs vendor-specific)
|
|
729
|
+
*/
|
|
730
|
+
async _setSignalsCP2102(signals) {
|
|
731
|
+
// CP2102 uses vendor-specific request 0x07 (SET_MHS)
|
|
732
|
+
// Bit 0: DTR, Bit 1: RTS, Bit 8-9: DTR/RTS mask
|
|
733
|
+
|
|
734
|
+
// Preserve current state for unspecified signals (Web Serial semantics)
|
|
735
|
+
const dtr =
|
|
736
|
+
signals.dataTerminalReady !== undefined
|
|
737
|
+
? signals.dataTerminalReady
|
|
738
|
+
: this._currentDTR;
|
|
739
|
+
const rts =
|
|
740
|
+
signals.requestToSend !== undefined
|
|
741
|
+
? signals.requestToSend
|
|
742
|
+
: this._currentRTS;
|
|
743
|
+
|
|
744
|
+
// Update tracked state
|
|
745
|
+
this._currentDTR = dtr;
|
|
746
|
+
this._currentRTS = rts;
|
|
747
|
+
|
|
748
|
+
// Build value with mask bits for both signals
|
|
749
|
+
let value = 0;
|
|
750
|
+
value |= (dtr ? 1 : 0) | 0x100; // DTR + mask
|
|
751
|
+
value |= (rts ? 2 : 0) | 0x200; // RTS + mask
|
|
752
|
+
|
|
753
|
+
try {
|
|
754
|
+
const result = await this.device.controlTransferOut({
|
|
755
|
+
requestType: "vendor",
|
|
756
|
+
recipient: "device",
|
|
757
|
+
request: 0x07, // SET_MHS (Modem Handshaking)
|
|
758
|
+
value: value,
|
|
759
|
+
index: 0x00, // CP2102 always uses index 0
|
|
760
|
+
});
|
|
761
|
+
|
|
762
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
763
|
+
return result;
|
|
764
|
+
} catch (e) {
|
|
765
|
+
this._log(`[WebUSB CP2102] Failed to set signals: ${e.message}`);
|
|
766
|
+
throw e;
|
|
854
767
|
}
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
/**
|
|
771
|
+
* Set signals for CH340 (WCH vendor-specific)
|
|
772
|
+
*/
|
|
773
|
+
async _setSignalsCH340(signals) {
|
|
774
|
+
// Preserve current state for unspecified signals (Web Serial semantics)
|
|
775
|
+
const dtr =
|
|
776
|
+
signals.dataTerminalReady !== undefined
|
|
777
|
+
? signals.dataTerminalReady
|
|
778
|
+
: this._currentDTR;
|
|
779
|
+
const rts =
|
|
780
|
+
signals.requestToSend !== undefined
|
|
781
|
+
? signals.requestToSend
|
|
782
|
+
: this._currentRTS;
|
|
783
|
+
|
|
784
|
+
// Update tracked state
|
|
785
|
+
this._currentDTR = dtr;
|
|
786
|
+
this._currentRTS = rts;
|
|
787
|
+
|
|
788
|
+
// CH340 uses vendor-specific request 0xA4
|
|
789
|
+
// Bit 5: DTR, Bit 6: RTS (inverted logic!)
|
|
790
|
+
// Calculate value with bitwise NOT and mask to unsigned 16-bit
|
|
791
|
+
const value = ~((dtr ? 1 << 5 : 0) | (rts ? 1 << 6 : 0)) & 0xffff;
|
|
792
|
+
|
|
793
|
+
try {
|
|
794
|
+
const result = await this.device.controlTransferOut({
|
|
795
|
+
requestType: "vendor",
|
|
796
|
+
recipient: "device",
|
|
797
|
+
request: 0xa4, // CH340 control request
|
|
798
|
+
value: value,
|
|
799
|
+
index: 0,
|
|
800
|
+
});
|
|
801
|
+
|
|
802
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
803
|
+
return result;
|
|
804
|
+
} catch (e) {
|
|
805
|
+
this._log(`[WebUSB CH340] Failed to set signals: ${e.message}`);
|
|
806
|
+
throw e;
|
|
858
807
|
}
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
return;
|
|
871
|
-
}
|
|
872
|
-
|
|
873
|
-
try {
|
|
874
|
-
while (this._readLoopRunning && this.device) {
|
|
875
|
-
try {
|
|
876
|
-
// CRITICAL: Check backpressure before reading more data
|
|
877
|
-
// If desiredSize is 0 or negative, the consumer can't keep up
|
|
878
|
-
// Wait for the consumer to drain the buffer before reading more
|
|
879
|
-
if (controller.desiredSize !== null && controller.desiredSize <= 0) {
|
|
880
|
-
// Consumer is backlogged - wait before reading more
|
|
881
|
-
await new Promise(r => setTimeout(r, 10));
|
|
882
|
-
continue;
|
|
883
|
-
}
|
|
884
|
-
|
|
885
|
-
const result = await this.device.transferIn(this.endpointIn, this.maxTransferSize);
|
|
886
|
-
|
|
887
|
-
if (result.status === 'ok') {
|
|
888
|
-
controller.enqueue(new Uint8Array(result.data.buffer, result.data.byteOffset, result.data.byteLength));
|
|
889
|
-
// Small delay to allow consumer to process data
|
|
890
|
-
// This prevents overwhelming the TextDecoderStream on Android
|
|
891
|
-
await new Promise(r => setTimeout(r, 1));
|
|
892
|
-
continue;
|
|
893
|
-
} else if (result.status === 'stall') {
|
|
894
|
-
await this.device.clearHalt('in', this.endpointIn);
|
|
895
|
-
await new Promise(r => setTimeout(r, 1));
|
|
896
|
-
continue;
|
|
897
|
-
}
|
|
898
|
-
// Only wait if no data was received
|
|
899
|
-
await new Promise(r => setTimeout(r, 1));
|
|
900
|
-
} catch (error) {
|
|
901
|
-
if (error.message && (error.message.includes('device unavailable') ||
|
|
902
|
-
error.message.includes('device has been lost') ||
|
|
903
|
-
error.message.includes('device was disconnected') ||
|
|
904
|
-
error.message.includes('No device selected'))) {
|
|
905
|
-
break;
|
|
906
|
-
}
|
|
907
|
-
if (error.message && (error.message.includes('transfer was cancelled') ||
|
|
908
|
-
error.message.includes('transfer error has occurred'))) {
|
|
909
|
-
continue;
|
|
910
|
-
}
|
|
911
|
-
this._log('USB read error:', error.message);
|
|
912
|
-
// Wait a bit after error before retrying
|
|
913
|
-
await new Promise(r => setTimeout(r, 10));
|
|
914
|
-
}
|
|
915
|
-
}
|
|
916
|
-
} catch (error) {
|
|
917
|
-
streamErrored = true;
|
|
918
|
-
controller.error(error);
|
|
919
|
-
} finally {
|
|
920
|
-
// Only close if stream didn't error
|
|
921
|
-
if (!streamErrored) {
|
|
922
|
-
controller.close();
|
|
923
|
-
}
|
|
924
|
-
}
|
|
925
|
-
},
|
|
926
|
-
cancel: () => {
|
|
927
|
-
this._readLoopRunning = false;
|
|
928
|
-
}
|
|
929
|
-
});
|
|
930
|
-
|
|
931
|
-
// WritableStream for outgoing data
|
|
932
|
-
this.writableStream = new WritableStream({
|
|
933
|
-
write: async (chunk) => {
|
|
934
|
-
if (!this.device) {
|
|
935
|
-
throw new Error('Device not open');
|
|
936
|
-
}
|
|
937
|
-
if (this.endpointOut == null) {
|
|
938
|
-
throw new Error('Bulk OUT endpoint not configured');
|
|
939
|
-
}
|
|
940
|
-
await this.device.transferOut(this.endpointOut, chunk);
|
|
941
|
-
}
|
|
942
|
-
});
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
/**
|
|
811
|
+
* Change baudrate after port is already open
|
|
812
|
+
* This is needed for ESP stub loader which changes baudrate after uploading stub
|
|
813
|
+
* NOTE: Only needed for vendor-specific chips (CP2102, CH340, FTDI)
|
|
814
|
+
* CDC devices (CH343, ESP32-S2/S3/C3 Native USB) handle baudrate automatically
|
|
815
|
+
*/
|
|
816
|
+
async setBaudRate(baudRate) {
|
|
817
|
+
if (!this.device) {
|
|
818
|
+
throw new Error("Device not open");
|
|
943
819
|
}
|
|
944
820
|
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
821
|
+
const vid = this.device.vendorId;
|
|
822
|
+
const pid = this.device.productId;
|
|
823
|
+
|
|
824
|
+
// this._log(`[WebUSB] Changing baudrate to ${baudRate}...`);
|
|
825
|
+
|
|
826
|
+
// FTDI (VID: 0x0403)
|
|
827
|
+
if (vid === 0x0403) {
|
|
828
|
+
// FTDI baudrate calculation
|
|
829
|
+
// Modern FTDI chips (FT232R, FT2232, etc.): BaseClock = 48MHz
|
|
830
|
+
// BaudDivisor = (48000000 / 16) / BaudRate = 3000000 / BaudRate
|
|
831
|
+
// Divisor encoding: 16-bit value with sub-integer divisor support
|
|
832
|
+
// Sub-integer divisor: 0, 0.125, 0.25, 0.375, 0.5, 0.625, 0.75, 0.875
|
|
833
|
+
|
|
834
|
+
const baseClock = 3000000; // 48MHz / 16
|
|
835
|
+
let divisor = baseClock / baudRate;
|
|
836
|
+
|
|
837
|
+
// Extract integer and fractional parts
|
|
838
|
+
const integerPart = Math.floor(divisor);
|
|
839
|
+
const fractionalPart = divisor - integerPart;
|
|
840
|
+
|
|
841
|
+
// Encode sub-integer divisor (0, 0.125, 0.25, 0.375, 0.5, 0.625, 0.75, 0.875)
|
|
842
|
+
let subInteger;
|
|
843
|
+
if (fractionalPart < 0.0625)
|
|
844
|
+
subInteger = 0; // 0.0
|
|
845
|
+
else if (fractionalPart < 0.1875)
|
|
846
|
+
subInteger = 1; // 0.125
|
|
847
|
+
else if (fractionalPart < 0.3125)
|
|
848
|
+
subInteger = 2; // 0.25
|
|
849
|
+
else if (fractionalPart < 0.4375)
|
|
850
|
+
subInteger = 3; // 0.375
|
|
851
|
+
else if (fractionalPart < 0.5625)
|
|
852
|
+
subInteger = 4; // 0.5
|
|
853
|
+
else if (fractionalPart < 0.6875)
|
|
854
|
+
subInteger = 5; // 0.625
|
|
855
|
+
else if (fractionalPart < 0.8125)
|
|
856
|
+
subInteger = 6; // 0.75
|
|
857
|
+
else subInteger = 7; // 0.875
|
|
858
|
+
|
|
859
|
+
// Encode divisor value for FTDI
|
|
860
|
+
// Low byte: integer part (bits 0-7)
|
|
861
|
+
// High byte: (integer part >> 8) | (sub-integer << 6)
|
|
862
|
+
const value =
|
|
863
|
+
(integerPart & 0xff) |
|
|
864
|
+
((subInteger & 0x07) << 14) |
|
|
865
|
+
(((integerPart >> 8) & 0x3f) << 8);
|
|
866
|
+
const index = (integerPart >> 14) & 0x03; // Upper 2 bits of integer part
|
|
867
|
+
|
|
868
|
+
// this._log(`[WebUSB FTDI] Setting baudrate ${baudRate} (divisor=${divisor.toFixed(3)}, value=0x${value.toString(16)}, index=0x${index.toString(16)})...`);
|
|
869
|
+
|
|
870
|
+
await this.device.controlTransferOut({
|
|
871
|
+
requestType: "vendor",
|
|
872
|
+
recipient: "device",
|
|
873
|
+
request: 0x03, // SIO_SET_BAUD_RATE
|
|
874
|
+
value: value,
|
|
875
|
+
index: index,
|
|
876
|
+
});
|
|
877
|
+
|
|
878
|
+
// this._log('[WebUSB FTDI] Baudrate changed successfully');
|
|
963
879
|
}
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
880
|
+
// CP2102 (Silicon Labs VID: 0x10c4)
|
|
881
|
+
else if (vid === 0x10c4) {
|
|
882
|
+
// CP210x baudrate encoding (from Silicon Labs AN571)
|
|
883
|
+
// For CP2102/CP2103: Use direct 32-bit baudrate value
|
|
884
|
+
// Request: IFC_SET_BAUDRATE (0x1E)
|
|
885
|
+
|
|
886
|
+
// Encode baudrate as 32-bit little-endian value
|
|
887
|
+
const baudrateBuffer = new ArrayBuffer(4);
|
|
888
|
+
const baudrateView = new DataView(baudrateBuffer);
|
|
889
|
+
baudrateView.setUint32(0, baudRate, true); // little-endian
|
|
890
|
+
|
|
891
|
+
await this.device.controlTransferOut(
|
|
892
|
+
{
|
|
893
|
+
requestType: "vendor",
|
|
894
|
+
recipient: "interface",
|
|
895
|
+
request: 0x1e, // IFC_SET_BAUDRATE
|
|
896
|
+
value: 0,
|
|
897
|
+
index: 0,
|
|
898
|
+
},
|
|
899
|
+
baudrateBuffer,
|
|
900
|
+
);
|
|
901
|
+
}
|
|
902
|
+
// CH340 (WCH VID: 0x1a86, but not CH343 PID: 0x55d3)
|
|
903
|
+
else if (vid === 0x1a86 && pid !== 0x55d3) {
|
|
904
|
+
// CH340 baudrate calculation (from Linux kernel driver)
|
|
905
|
+
const CH341_BAUDBASE_FACTOR = 1532620800;
|
|
906
|
+
const CH341_BAUDBASE_DIVMAX = 3;
|
|
907
|
+
|
|
908
|
+
let factor = Math.floor(CH341_BAUDBASE_FACTOR / baudRate);
|
|
909
|
+
let divisor = CH341_BAUDBASE_DIVMAX;
|
|
910
|
+
|
|
911
|
+
// Reduce factor if too large
|
|
912
|
+
while (factor > 0xfff0 && divisor > 0) {
|
|
913
|
+
factor >>= 3;
|
|
914
|
+
divisor--;
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
if (factor > 0xfff0) {
|
|
918
|
+
throw new Error(`Baudrate ${baudRate} not supported by CH340`);
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
factor = 0x10000 - factor;
|
|
922
|
+
const a = (factor & 0xff00) | divisor;
|
|
923
|
+
const b = factor & 0xff;
|
|
924
|
+
|
|
925
|
+
// CH340 uses request 0x9A to set baudrate
|
|
926
|
+
await this.device.controlTransferOut({
|
|
927
|
+
requestType: "vendor",
|
|
928
|
+
recipient: "device",
|
|
929
|
+
request: 0x9a, // CH340 SET_BAUDRATE
|
|
930
|
+
value: 0x1312, // Fixed value for baudrate setting
|
|
931
|
+
index: a,
|
|
932
|
+
});
|
|
933
|
+
|
|
934
|
+
// Second control transfer with b value
|
|
935
|
+
await this.device.controlTransferOut({
|
|
936
|
+
requestType: "vendor",
|
|
937
|
+
recipient: "device",
|
|
938
|
+
request: 0x9a,
|
|
939
|
+
value: 0x0f2c, // Fixed value
|
|
940
|
+
index: b,
|
|
941
|
+
});
|
|
971
942
|
}
|
|
943
|
+
// CDC devices (CH343, ESP32 Native USB) - no action needed in setBaudRate()
|
|
944
|
+
// They are handled by close/reopen in esp_loader.ts
|
|
945
|
+
|
|
946
|
+
// Wait for baudrate change to take effect
|
|
947
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
get readable() {
|
|
951
|
+
return this.readableStream;
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
get writable() {
|
|
955
|
+
return this.writableStream;
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
_createStreams() {
|
|
959
|
+
// ReadableStream for incoming data
|
|
960
|
+
this.readableStream = new ReadableStream({
|
|
961
|
+
start: async (controller) => {
|
|
962
|
+
this._readLoopRunning = true;
|
|
963
|
+
let streamErrored = false;
|
|
964
|
+
|
|
965
|
+
// Validate endpoints before starting read loop
|
|
966
|
+
if (this.endpointIn == null) {
|
|
967
|
+
controller.error(new Error("Bulk IN endpoint not configured"));
|
|
968
|
+
return;
|
|
969
|
+
}
|
|
972
970
|
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
listeners.forEach(listener => {
|
|
971
|
+
try {
|
|
972
|
+
while (this._readLoopRunning && this.device) {
|
|
976
973
|
try {
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
974
|
+
// CRITICAL: Check backpressure before reading more data
|
|
975
|
+
// If desiredSize is 0 or negative, the consumer can't keep up
|
|
976
|
+
// Wait for the consumer to drain the buffer before reading more
|
|
977
|
+
if (
|
|
978
|
+
controller.desiredSize !== null &&
|
|
979
|
+
controller.desiredSize <= 0
|
|
980
|
+
) {
|
|
981
|
+
// Consumer is backlogged - wait before reading more
|
|
982
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
983
|
+
continue;
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
const result = await this.device.transferIn(
|
|
987
|
+
this.endpointIn,
|
|
988
|
+
this.maxTransferSize,
|
|
989
|
+
);
|
|
990
|
+
|
|
991
|
+
if (result.status === "ok") {
|
|
992
|
+
controller.enqueue(
|
|
993
|
+
new Uint8Array(
|
|
994
|
+
result.data.buffer,
|
|
995
|
+
result.data.byteOffset,
|
|
996
|
+
result.data.byteLength,
|
|
997
|
+
),
|
|
998
|
+
);
|
|
999
|
+
// Small delay to allow consumer to process data
|
|
1000
|
+
// This prevents overwhelming the TextDecoderStream on Android
|
|
1001
|
+
await new Promise((r) => setTimeout(r, 1));
|
|
1002
|
+
continue;
|
|
1003
|
+
} else if (result.status === "stall") {
|
|
1004
|
+
await this.device.clearHalt("in", this.endpointIn);
|
|
1005
|
+
await new Promise((r) => setTimeout(r, 1));
|
|
1006
|
+
continue;
|
|
1007
|
+
}
|
|
1008
|
+
// Only wait if no data was received
|
|
1009
|
+
await new Promise((r) => setTimeout(r, 1));
|
|
1010
|
+
} catch (error) {
|
|
1011
|
+
if (
|
|
1012
|
+
error.message &&
|
|
1013
|
+
(error.message.includes("device unavailable") ||
|
|
1014
|
+
error.message.includes("device has been lost") ||
|
|
1015
|
+
error.message.includes("device was disconnected") ||
|
|
1016
|
+
error.message.includes("No device selected"))
|
|
1017
|
+
) {
|
|
1018
|
+
break;
|
|
1019
|
+
}
|
|
1020
|
+
if (
|
|
1021
|
+
error.message &&
|
|
1022
|
+
(error.message.includes("transfer was cancelled") ||
|
|
1023
|
+
error.message.includes("transfer error has occurred"))
|
|
1024
|
+
) {
|
|
1025
|
+
continue;
|
|
1026
|
+
}
|
|
1027
|
+
this._log("USB read error:", error.message);
|
|
1028
|
+
// Wait a bit after error before retrying
|
|
1029
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
980
1030
|
}
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
1031
|
+
}
|
|
1032
|
+
} catch (error) {
|
|
1033
|
+
streamErrored = true;
|
|
1034
|
+
controller.error(error);
|
|
1035
|
+
} finally {
|
|
1036
|
+
// Only close if stream didn't error
|
|
1037
|
+
if (!streamErrored) {
|
|
1038
|
+
controller.close();
|
|
1039
|
+
}
|
|
987
1040
|
}
|
|
988
|
-
|
|
1041
|
+
},
|
|
1042
|
+
cancel: () => {
|
|
1043
|
+
this._readLoopRunning = false;
|
|
1044
|
+
},
|
|
1045
|
+
});
|
|
989
1046
|
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
1047
|
+
// WritableStream for outgoing data
|
|
1048
|
+
this.writableStream = new WritableStream({
|
|
1049
|
+
write: async (chunk) => {
|
|
1050
|
+
if (!this.device) {
|
|
1051
|
+
throw new Error("Device not open");
|
|
1052
|
+
}
|
|
1053
|
+
if (this.endpointOut == null) {
|
|
1054
|
+
throw new Error("Bulk OUT endpoint not configured");
|
|
996
1055
|
}
|
|
1056
|
+
await this.device.transferOut(this.endpointOut, chunk);
|
|
1057
|
+
},
|
|
1058
|
+
});
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
/**
|
|
1062
|
+
* Recreate streams without closing the port
|
|
1063
|
+
* Useful after hardware reset or when switching to console mode
|
|
1064
|
+
* This stops the current read loop and creates fresh streams
|
|
1065
|
+
*/
|
|
1066
|
+
recreateStreams() {
|
|
1067
|
+
// Stop the current read loop
|
|
1068
|
+
this._readLoopRunning = false;
|
|
1069
|
+
|
|
1070
|
+
// Wait a bit for the read loop to finish
|
|
1071
|
+
// The ReadableStream will close itself when _readLoopRunning becomes false
|
|
1072
|
+
return new Promise((resolve) => {
|
|
1073
|
+
setTimeout(() => {
|
|
1074
|
+
// Create new streams
|
|
1075
|
+
this._createStreams();
|
|
1076
|
+
resolve();
|
|
1077
|
+
}, 100);
|
|
1078
|
+
});
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
_cleanup() {
|
|
1082
|
+
this._readLoopRunning = false;
|
|
1083
|
+
if (this._usbDisconnectHandler) {
|
|
1084
|
+
navigator.usb.removeEventListener(
|
|
1085
|
+
"disconnect",
|
|
1086
|
+
this._usbDisconnectHandler,
|
|
1087
|
+
);
|
|
1088
|
+
this._usbDisconnectHandler = null;
|
|
997
1089
|
}
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
_fireEvent(type) {
|
|
1093
|
+
const listeners = this._eventListeners[type] || [];
|
|
1094
|
+
listeners.forEach((listener) => {
|
|
1095
|
+
try {
|
|
1096
|
+
listener();
|
|
1097
|
+
} catch (e) {
|
|
1098
|
+
this._log(`Error in ${type} event listener:`, e);
|
|
1099
|
+
}
|
|
1100
|
+
});
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
addEventListener(type, listener) {
|
|
1104
|
+
if (this._eventListeners[type]) {
|
|
1105
|
+
this._eventListeners[type].push(listener);
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
removeEventListener(type, listener) {
|
|
1110
|
+
if (this._eventListeners[type]) {
|
|
1111
|
+
const index = this._eventListeners[type].indexOf(listener);
|
|
1112
|
+
if (index !== -1) {
|
|
1113
|
+
this._eventListeners[type].splice(index, 1);
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
998
1117
|
}
|
|
999
1118
|
|
|
1000
1119
|
/**
|
|
@@ -1003,49 +1122,55 @@ class WebUSBSerial {
|
|
|
1003
1122
|
* @param {boolean} forceNew - If true, forces selection of a new device (ignores already paired devices)
|
|
1004
1123
|
*/
|
|
1005
1124
|
async function requestSerialPort(forceNew = false) {
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1125
|
+
// Detect if we're on Android
|
|
1126
|
+
const isAndroid = /Android/i.test(navigator.userAgent);
|
|
1127
|
+
const hasSerial = "serial" in navigator;
|
|
1128
|
+
const hasUSB = "usb" in navigator;
|
|
1129
|
+
|
|
1130
|
+
console.log(
|
|
1131
|
+
`[requestSerialPort] Platform: ${isAndroid ? "Android" : "Desktop"}, Web Serial: ${hasSerial}, WebUSB: ${hasUSB}`,
|
|
1132
|
+
);
|
|
1133
|
+
|
|
1134
|
+
// On Android, prefer WebUSB (Web Serial doesn't work properly)
|
|
1135
|
+
if (isAndroid && hasUSB) {
|
|
1136
|
+
try {
|
|
1137
|
+
return await WebUSBSerial.requestPort(null, forceNew);
|
|
1138
|
+
} catch (err) {
|
|
1139
|
+
console.log("WebUSB failed, trying Web Serial...", err.message);
|
|
1020
1140
|
}
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
// Try Web Serial API (preferred on desktop)
|
|
1144
|
+
if (hasSerial) {
|
|
1145
|
+
try {
|
|
1146
|
+
// Web Serial API doesn't support device reuse in the same way
|
|
1147
|
+
// It always shows the picker, but the browser remembers permissions
|
|
1148
|
+
return await navigator.serial.requestPort();
|
|
1149
|
+
} catch (err) {
|
|
1150
|
+
console.log("Web Serial not available or cancelled, trying WebUSB...");
|
|
1031
1151
|
}
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
// Fall back to WebUSB
|
|
1155
|
+
if (hasUSB) {
|
|
1156
|
+
try {
|
|
1157
|
+
return await WebUSBSerial.requestPort(null, forceNew);
|
|
1158
|
+
} catch (err) {
|
|
1159
|
+
throw new Error(
|
|
1160
|
+
"Neither Web Serial nor WebUSB available or user cancelled",
|
|
1161
|
+
);
|
|
1040
1162
|
}
|
|
1041
|
-
|
|
1042
|
-
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
throw new Error(
|
|
1166
|
+
"Neither Web Serial API nor WebUSB is supported in this browser",
|
|
1167
|
+
);
|
|
1043
1168
|
}
|
|
1044
1169
|
|
|
1045
1170
|
// Also set on globalThis for non-module usage (e.g., dynamic script loading)
|
|
1046
|
-
if (typeof globalThis !==
|
|
1047
|
-
|
|
1048
|
-
|
|
1171
|
+
if (typeof globalThis !== "undefined") {
|
|
1172
|
+
globalThis.WebUSBSerial = WebUSBSerial;
|
|
1173
|
+
globalThis.requestSerialPort = requestSerialPort;
|
|
1049
1174
|
}
|
|
1050
1175
|
|
|
1051
1176
|
// Export as ES modules
|