esp32tool 1.6.5 → 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.
@@ -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
- 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));
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
- * 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
- };
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
- let device;
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
- // 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('[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
- // If no device found or forceNew is true, request a new device
92
- if (!device) {
93
- if (!navigator.usb) {
94
- throw new Error('WebUSB not available');
95
- }
96
- device = await navigator.usb.requestDevice({ filters });
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
- const port = new WebUSBSerial(logger);
100
- port.device = device;
101
- return port;
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
- * Open the USB device (mimics port.open())
106
- */
107
- async open(options = {}) {
108
- if (!this.device) {
109
- throw new Error('No device selected');
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
- const baudRate = options.baudRate || 115200;
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
- // If device is already opened, we need to close and reopen it
115
- // This is critical for ESP32-S2 which changes interfaces when switching modes
116
- if (this.device.opened) {
117
-
118
- try {
119
- // Release all interfaces
120
- if (this.interfaceNumber !== null) {
121
- try { await this.device.releaseInterface(this.interfaceNumber); } catch (e) {}
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
- if (this.device.opened) {
144
- try { await this.device.close(); } catch (e) {
145
- this._log('[WebUSB] Error closing device:', e.message);
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
- try {
150
- if (this.device.reset) {
151
- await this.device.reset();
152
- }
153
- } catch (e) {
154
- // this._log('[WebUSB] Device reset failed:', e.message);
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
- const attemptOpenAndClaim = async () => {
158
- await this.device.open();
159
- try {
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
- let config;
232
+ candidates.sort((a, b) => a.score - b.score);
233
+ let lastErr = null;
234
+ for (const cand of candidates) {
255
235
  try {
256
- config = await attemptOpenAndClaim();
257
- } catch (err) {
258
- this._log('[WebUSB] open/claim failed, retrying after reset:', err.message);
259
- try { if (this.device.reset) { await this.device.reset(); } } catch (e) { }
260
- try { await this.device.close(); } catch (e) { }
261
- try {
262
- config = await attemptOpenAndClaim();
263
- } catch (err2) {
264
- throw new Error(`Unable to claim USB interface: ${err2.message}`);
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
- // Claim control interface if not already claimed
269
- if (this.controlInterface == null) {
270
- const controlIface = config.interfaces.find(i =>
271
- i.alternates[0].interfaceClass === 0x02 &&
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
- if (controlIface) {
276
- try {
277
- await this.device.claimInterface(controlIface.interfaceNumber);
278
- try { await this.device.selectAlternateInterface(controlIface.interfaceNumber, 0); } catch (e) { }
279
- this.controlInterface = controlIface.interfaceNumber;
280
- } catch (e) {
281
- this.controlInterface = this.interfaceNumber;
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
- this.controlInterface = this.interfaceNumber;
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
- // CP2102-specific initialization sequence (must be in this exact order!)
289
- if (this.device.vendorId === 0x10c4) {
290
- try {
291
- // Step 1: Enable UART interface
292
- await this.device.controlTransferOut({
293
- requestType: 'vendor',
294
- recipient: 'device',
295
- request: 0x00, // IFC_ENABLE
296
- value: 0x01, // UART_ENABLE
297
- index: 0x00
298
- });
299
-
300
- // Step 2: Set line control (8N1: 8 data bits, no parity, 1 stop bit)
301
- await this.device.controlTransferOut({
302
- requestType: 'vendor',
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
- // FTDI-specific initialization sequence
336
- else if (this.device.vendorId === 0x0403) {
337
- try {
338
- // Step 1: Reset device
339
- await this.device.controlTransferOut({
340
- requestType: 'vendor',
341
- recipient: 'device',
342
- request: 0x00, // SIO_RESET
343
- value: 0x00, // Reset
344
- index: 0x00
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
- // Initialize DTR/RTS to idle state (both HIGH/asserted)
488
- try {
489
- await this.device.controlTransferOut({
490
- requestType: 'class',
491
- recipient: 'interface',
492
- request: 0x22, // SET_CONTROL_LINE_STATE
493
- value: 0x03, // DTR=1, RTS=1 (both asserted)
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
- // Setup disconnect handler only once
514
- if (!this._usbDisconnectHandler) {
515
- this._usbDisconnectHandler = (event) => {
516
- if (event.device === this.device) {
517
- this._fireEvent('disconnect');
518
- this._cleanup();
519
- }
520
- };
521
- navigator.usb.addEventListener('disconnect', this._usbDisconnectHandler);
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
- * Close the device (mimics port.close())
527
- */
528
- async close() {
529
- this._cleanup();
530
- if (this.device) {
531
- try {
532
- if (this.interfaceNumber !== null) {
533
- await this.device.releaseInterface(this.interfaceNumber);
534
- }
535
- if (this.controlInterface !== null && this.controlInterface !== this.interfaceNumber) {
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
- * Disconnect and clear device reference (for final cleanup)
550
- */
551
- async disconnect() {
552
- await this.close();
553
- this.device = null;
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
- * Get optimal block size for flash read operations
558
- * (maxTransferSize - 2) / 2
559
- * This accounts for SLIP overhead and escape sequences
560
- * @returns {number} Optimal block size in bytes
561
- */
562
- getOptimalReadBlockSize() {
563
- // Formula for WebUSB:
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
- * Get device info (mimics port.getInfo())
572
- */
573
- getInfo() {
574
- if (!this.device) {
575
- return {};
576
- }
577
- return {
578
- usbVendorId: this.device.vendorId,
579
- usbProductId: this.device.productId
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
- * Set DTR/RTS signals (mimics port.setSignals())
585
- * CRITICAL: Commands are serialized via queue for CP2102 compatibility
586
- * Supports both CDC/ACM (CH343) and Vendor-Specific (CP2102, CH340)
587
- */
588
- async setSignals(signals) {
589
- // Serialize all control transfers through a queue
590
- // This is CRITICAL for CP2102 - parallel commands cause hangs
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
- const vid = this.device.vendorId;
597
- const pid = this.device.productId;
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
- // Detect chip type and use appropriate control request
600
- // CP2102 (Silicon Labs VID: 0x10c4)
601
- if (vid === 0x10c4) {
602
- return await this._setSignalsCP2102(signals);
603
- }
604
- // CH340 (WCH VID: 0x1a86, but not CH343 PID: 0x55d3)
605
- else if (vid === 0x1a86 && pid !== 0x55d3) {
606
- return await this._setSignalsCH340(signals);
607
- }
608
- // CDC/ACM (CH343, Native USB, etc.)
609
- else {
610
- return await this._setSignalsCDC(signals);
611
- }
612
- }).catch(err => {
613
- this._log('[WebUSB] setSignals error:', err);
614
- throw err;
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
- return this._commandQueue;
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
- * Set signals using CDC/ACM standard (for CH343, Native USB)
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
- try {
637
- const result = await this.device.controlTransferOut({
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
- * Set signals for CP2102 (Silicon Labs vendor-specific)
655
- */
656
- async _setSignalsCP2102(signals) {
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
- try {
674
- const result = await this.device.controlTransferOut({
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
- * Set signals for CH340 (WCH vendor-specific)
692
- */
693
- async _setSignalsCH340(signals) {
694
- // Preserve current state for unspecified signals (Web Serial semantics)
695
- const dtr = signals.dataTerminalReady !== undefined ? signals.dataTerminalReady : this._currentDTR;
696
- const rts = signals.requestToSend !== undefined ? signals.requestToSend : this._currentRTS;
697
-
698
- // Update tracked state
699
- this._currentDTR = dtr;
700
- this._currentRTS = rts;
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
- try {
708
- const result = await this.device.controlTransferOut({
709
- requestType: 'vendor',
710
- recipient: 'device',
711
- request: 0xA4, // CH340 control request
712
- value: value,
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
- * Change baudrate after port is already open
726
- * This is needed for ESP stub loader which changes baudrate after uploading stub
727
- * NOTE: Only needed for vendor-specific chips (CP2102, CH340, FTDI)
728
- * CDC devices (CH343, ESP32-S2/S3/C3 Native USB) handle baudrate automatically
729
- */
730
- async setBaudRate(baudRate) {
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
- throw new Error('Device not open');
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
- // this._log(`[WebUSB] Changing baudrate to ${baudRate}...`);
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
- else if (vid === 0x10c4) {
786
- // CP210x baudrate encoding (from Silicon Labs AN571)
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
- // CH340 baudrate calculation (from Linux kernel driver)
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 devices (CH343, ESP32 Native USB) - no action needed in setBaudRate()
846
- // They are handled by close/reopen in esp_loader.ts
847
-
848
- // Wait for baudrate change to take effect
849
- await new Promise(resolve => setTimeout(resolve, 50));
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
- get readable() {
853
- return this.readableStream;
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
- get writable() {
857
- return this.writableStream;
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
- _createStreams() {
861
- // ReadableStream for incoming data
862
- this.readableStream = new ReadableStream({
863
- start: async (controller) => {
864
- this._readLoopRunning = true;
865
- let streamErrored = false;
866
-
867
- // Validate endpoints before starting read loop
868
- if (this.endpointIn == null) {
869
- controller.error(new Error('Bulk IN endpoint not configured'));
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
- * Recreate streams without closing the port
947
- * Useful after hardware reset or when switching to console mode
948
- * This stops the current read loop and creates fresh streams
949
- */
950
- recreateStreams() {
951
- // Stop the current read loop
952
- this._readLoopRunning = false;
953
-
954
- // Wait a bit for the read loop to finish
955
- // The ReadableStream will close itself when _readLoopRunning becomes false
956
- return new Promise((resolve) => {
957
- setTimeout(() => {
958
- // Create new streams
959
- this._createStreams();
960
- resolve();
961
- }, 100);
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
- _cleanup() {
966
- this._readLoopRunning = false;
967
- if (this._usbDisconnectHandler) {
968
- navigator.usb.removeEventListener('disconnect', this._usbDisconnectHandler);
969
- this._usbDisconnectHandler = null;
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
- _fireEvent(type) {
974
- const listeners = this._eventListeners[type] || [];
975
- listeners.forEach(listener => {
971
+ try {
972
+ while (this._readLoopRunning && this.device) {
976
973
  try {
977
- listener();
978
- } catch (e) {
979
- this._log(`Error in ${type} event listener:`, e);
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
- addEventListener(type, listener) {
985
- if (this._eventListeners[type]) {
986
- this._eventListeners[type].push(listener);
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
- removeEventListener(type, listener) {
991
- if (this._eventListeners[type]) {
992
- const index = this._eventListeners[type].indexOf(listener);
993
- if (index !== -1) {
994
- this._eventListeners[type].splice(index, 1);
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
- // Detect if we're on Android
1007
- const isAndroid = /Android/i.test(navigator.userAgent);
1008
- const hasSerial = 'serial' in navigator;
1009
- const hasUSB = 'usb' in navigator;
1010
-
1011
- console.log(`[requestSerialPort] Platform: ${isAndroid ? 'Android' : 'Desktop'}, Web Serial: ${hasSerial}, WebUSB: ${hasUSB}`);
1012
-
1013
- // On Android, prefer WebUSB (Web Serial doesn't work properly)
1014
- if (isAndroid && hasUSB) {
1015
- try {
1016
- return await WebUSBSerial.requestPort(null, forceNew);
1017
- } catch (err) {
1018
- console.log('WebUSB failed, trying Web Serial...', err.message);
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
- // Try Web Serial API (preferred on desktop)
1023
- if (hasSerial) {
1024
- try {
1025
- // Web Serial API doesn't support device reuse in the same way
1026
- // It always shows the picker, but the browser remembers permissions
1027
- return await navigator.serial.requestPort();
1028
- } catch (err) {
1029
- console.log('Web Serial not available or cancelled, trying WebUSB...');
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
- // Fall back to WebUSB
1034
- if (hasUSB) {
1035
- try {
1036
- return await WebUSBSerial.requestPort(null, forceNew);
1037
- } catch (err) {
1038
- throw new Error('Neither Web Serial nor WebUSB available or user cancelled');
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
- throw new Error('Neither Web Serial API nor WebUSB is supported in this browser');
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 !== 'undefined') {
1047
- globalThis.WebUSBSerial = WebUSBSerial;
1048
- globalThis.requestSerialPort = requestSerialPort;
1171
+ if (typeof globalThis !== "undefined") {
1172
+ globalThis.WebUSBSerial = WebUSBSerial;
1173
+ globalThis.requestSerialPort = requestSerialPort;
1049
1174
  }
1050
1175
 
1051
1176
  // Export as ES modules