esp32tool 1.1.9 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/.nojekyll +0 -0
  2. package/README.md +100 -6
  3. package/apple-touch-icon.png +0 -0
  4. package/build-electron-cli.cjs +177 -0
  5. package/build-single-binary.cjs +295 -0
  6. package/css/light.css +11 -0
  7. package/css/style.css +225 -35
  8. package/dist/cli.d.ts +17 -0
  9. package/dist/cli.js +458 -0
  10. package/dist/esp_loader.d.ts +126 -20
  11. package/dist/esp_loader.js +1190 -230
  12. package/dist/index.d.ts +2 -1
  13. package/dist/index.js +37 -4
  14. package/dist/node-usb-adapter.d.ts +47 -0
  15. package/dist/node-usb-adapter.js +725 -0
  16. package/dist/stubs/index.d.ts +1 -2
  17. package/dist/stubs/index.js +4 -0
  18. package/dist/web/index.js +1 -1
  19. package/electron/cli-main.cjs +74 -0
  20. package/electron/main.cjs +338 -0
  21. package/electron/main.js +7 -2
  22. package/favicon.ico +0 -0
  23. package/fix-cli-imports.cjs +127 -0
  24. package/generate-icons.sh +89 -0
  25. package/icons/icon-128.png +0 -0
  26. package/icons/icon-144.png +0 -0
  27. package/icons/icon-152.png +0 -0
  28. package/icons/icon-192.png +0 -0
  29. package/icons/icon-384.png +0 -0
  30. package/icons/icon-512.png +0 -0
  31. package/icons/icon-72.png +0 -0
  32. package/icons/icon-96.png +0 -0
  33. package/index.html +94 -64
  34. package/install-android.html +411 -0
  35. package/js/modules/esptool.js +1 -1
  36. package/js/script.js +165 -160
  37. package/js/webusb-serial.js +1017 -0
  38. package/license.md +1 -1
  39. package/manifest.json +89 -0
  40. package/package.cli.json +29 -0
  41. package/package.json +31 -21
  42. package/screenshots/desktop.png +0 -0
  43. package/screenshots/mobile.png +0 -0
  44. package/src/cli.ts +618 -0
  45. package/src/esp_loader.ts +1392 -261
  46. package/src/index.ts +69 -3
  47. package/src/node-usb-adapter.ts +924 -0
  48. package/src/stubs/index.ts +4 -1
  49. package/sw.js +155 -0
@@ -0,0 +1,725 @@
1
+ /**
2
+ * Node.js USB Adapter
3
+ *
4
+ * Uses node-usb to directly control USB-Serial chips (CP2102, CH340, FTDI, etc.)
5
+ * This provides the same level of control as WebUSB and avoids node-serialport issues
6
+ */
7
+ /**
8
+ * Create a Web Serial API compatible port from node-usb Device
9
+ */
10
+ export function createNodeUSBAdapter(device, logger) {
11
+ let readableStream = null;
12
+ let writableStream = null;
13
+ let interfaceNumber = null;
14
+ let controlInterface = null;
15
+ let endpointIn = null;
16
+ let endpointOut = null;
17
+ let endpointInNumber = null;
18
+ let endpointOutNumber = null;
19
+ // readLoopRunning tracked internally by stream
20
+ // Track current signal states
21
+ let currentDTR = false;
22
+ let currentRTS = false;
23
+ const vendorId = device.deviceDescriptor.idVendor;
24
+ const productId = device.deviceDescriptor.idProduct;
25
+ const adapter = {
26
+ isWebUSB: true, // Mark this as WebUSB-like behavior for reset strategy selection
27
+ get readable() {
28
+ return readableStream;
29
+ },
30
+ get writable() {
31
+ return writableStream;
32
+ },
33
+ async open(options) {
34
+ var _a;
35
+ const baudRate = options.baudRate;
36
+ logger.log(`Opening USB device at ${baudRate} baud...`);
37
+ // Open device
38
+ try {
39
+ device.open();
40
+ }
41
+ catch (err) {
42
+ throw new Error(`Failed to open USB device: ${err.message}`);
43
+ }
44
+ // Select configuration
45
+ try {
46
+ if (((_a = device.configDescriptor) === null || _a === void 0 ? void 0 : _a.bConfigurationValue) !== 1) {
47
+ device.setConfiguration(1);
48
+ }
49
+ }
50
+ catch (err) {
51
+ // Already configured
52
+ }
53
+ // Find bulk IN/OUT interface
54
+ const config = device.configDescriptor;
55
+ if (!config) {
56
+ throw new Error("No configuration descriptor");
57
+ }
58
+ // Find suitable interface with bulk endpoints
59
+ for (const iface of config.interfaces) {
60
+ for (const alt of iface) {
61
+ let hasIn = false;
62
+ let hasOut = false;
63
+ let inEpNum = null;
64
+ let outEpNum = null;
65
+ for (const ep of alt.endpoints) {
66
+ const epDesc = ep;
67
+ if (epDesc.bmAttributes === 2 || epDesc.transferType === 2) {
68
+ // Bulk transfer
69
+ const dir = epDesc.bEndpointAddress & 0x80 ? "in" : "out";
70
+ if (dir === "in" && !hasIn) {
71
+ hasIn = true;
72
+ inEpNum = epDesc.bEndpointAddress;
73
+ }
74
+ else if (dir === "out" && !hasOut) {
75
+ hasOut = true;
76
+ outEpNum = epDesc.bEndpointAddress;
77
+ }
78
+ }
79
+ }
80
+ if (hasIn && hasOut) {
81
+ interfaceNumber = iface[0].bInterfaceNumber;
82
+ endpointInNumber = inEpNum;
83
+ endpointOutNumber = outEpNum;
84
+ logger.debug(`Found interface ${interfaceNumber} with IN=0x${inEpNum === null || inEpNum === void 0 ? void 0 : inEpNum.toString(16)}, OUT=0x${outEpNum === null || outEpNum === void 0 ? void 0 : outEpNum.toString(16)}`);
85
+ break;
86
+ }
87
+ }
88
+ if (interfaceNumber !== null)
89
+ break;
90
+ }
91
+ if (interfaceNumber === null || !endpointInNumber || !endpointOutNumber) {
92
+ throw new Error("No suitable USB interface found");
93
+ }
94
+ // Claim interface
95
+ const usbInterface = device.interface(interfaceNumber);
96
+ // Detach kernel driver if active (Linux/macOS)
97
+ try {
98
+ if (usbInterface.isKernelDriverActive()) {
99
+ usbInterface.detachKernelDriver();
100
+ }
101
+ }
102
+ catch (err) {
103
+ // Ignore - may not be supported on all platforms
104
+ }
105
+ usbInterface.claim();
106
+ controlInterface = interfaceNumber;
107
+ // Get the actual endpoints from the claimed interface
108
+ const endpoints = usbInterface.endpoints;
109
+ logger.debug(`Found ${endpoints.length} endpoints on interface ${interfaceNumber}`);
110
+ // Find endpoints by address
111
+ endpointIn = endpoints.find((ep) => ep.address === endpointInNumber);
112
+ endpointOut = endpoints.find((ep) => ep.address === endpointOutNumber);
113
+ if (!endpointIn || !endpointOut) {
114
+ throw new Error(`Could not find endpoints: IN=0x${endpointInNumber === null || endpointInNumber === void 0 ? void 0 : endpointInNumber.toString(16)}, OUT=0x${endpointOutNumber === null || endpointOutNumber === void 0 ? void 0 : endpointOutNumber.toString(16)}`);
115
+ }
116
+ logger.debug(`Endpoints ready: IN=0x${endpointIn.address.toString(16)}, OUT=0x${endpointOut.address.toString(16)}`);
117
+ // Initialize chip-specific settings
118
+ try {
119
+ await initializeChip(device, vendorId, productId, baudRate, logger);
120
+ }
121
+ catch (err) {
122
+ logger.error(`Failed to initialize chip: ${err.message}`);
123
+ throw err;
124
+ }
125
+ // For CP2102: Clear any pending data
126
+ if (vendorId === 0x10c4) {
127
+ try {
128
+ // Clear halt on endpoints
129
+ await new Promise((resolve, reject) => {
130
+ device.controlTransfer(0x02, // Clear Feature, Endpoint
131
+ 0x01, // ENDPOINT_HALT
132
+ 0, endpointIn.address, Buffer.alloc(0), (err) => {
133
+ if (err)
134
+ logger.debug(`Clear halt IN failed: ${err.message}`);
135
+ resolve();
136
+ });
137
+ });
138
+ await new Promise((resolve, reject) => {
139
+ device.controlTransfer(0x02, // Clear Feature, Endpoint
140
+ 0x01, // ENDPOINT_HALT
141
+ 0, endpointOut.address, Buffer.alloc(0), (err) => {
142
+ if (err)
143
+ logger.debug(`Clear halt OUT failed: ${err.message}`);
144
+ resolve();
145
+ });
146
+ });
147
+ }
148
+ catch (err) {
149
+ // Ignore
150
+ }
151
+ }
152
+ // Wait for chip to be ready
153
+ await new Promise((resolve) => setTimeout(resolve, 100));
154
+ // Create streams
155
+ createStreams();
156
+ },
157
+ async close() {
158
+ // Stop polling and remove event listeners BEFORE cancelling streams
159
+ if (endpointIn) {
160
+ try {
161
+ endpointIn.stopPoll();
162
+ endpointIn.removeAllListeners();
163
+ }
164
+ catch (err) {
165
+ // Ignore
166
+ }
167
+ }
168
+ if (readableStream) {
169
+ try {
170
+ await readableStream.cancel();
171
+ }
172
+ catch (err) {
173
+ // Ignore
174
+ }
175
+ readableStream = null;
176
+ }
177
+ if (writableStream) {
178
+ try {
179
+ await writableStream.close();
180
+ }
181
+ catch (err) {
182
+ // Ignore
183
+ }
184
+ writableStream = null;
185
+ }
186
+ // Small delay to let any pending callbacks complete
187
+ await new Promise((resolve) => setTimeout(resolve, 50));
188
+ if (interfaceNumber !== null) {
189
+ try {
190
+ const usbInterface = device.interface(interfaceNumber);
191
+ usbInterface.release(true, () => { });
192
+ }
193
+ catch (err) {
194
+ // Ignore
195
+ }
196
+ }
197
+ try {
198
+ device.close();
199
+ }
200
+ catch (err) {
201
+ // Ignore
202
+ }
203
+ },
204
+ async setSignals(signals) {
205
+ // Preserve current state for unspecified signals
206
+ const dtr = signals.dataTerminalReady !== undefined
207
+ ? signals.dataTerminalReady
208
+ : currentDTR;
209
+ const rts = signals.requestToSend !== undefined
210
+ ? signals.requestToSend
211
+ : currentRTS;
212
+ currentDTR = dtr;
213
+ currentRTS = rts;
214
+ // logger.log(`Setting signals: DTR=${dtr}, RTS=${rts} (CP2102: GPIO0=${dtr ? 'LOW' : 'HIGH'}, EN=${rts ? 'LOW' : 'HIGH'})`);
215
+ // CP2102 (Silicon Labs VID: 0x10c4)
216
+ if (vendorId === 0x10c4) {
217
+ await setSignalsCP2102(device, dtr, rts);
218
+ }
219
+ // CH340 (WCH VID: 0x1a86, but not CH343 PID: 0x55d3)
220
+ else if (vendorId === 0x1a86 && productId !== 0x55d3) {
221
+ await setSignalsCH340(device, dtr, rts);
222
+ }
223
+ // FTDI (VID: 0x0403)
224
+ else if (vendorId === 0x0403) {
225
+ await setSignalsFTDI(device, dtr, rts);
226
+ }
227
+ // CDC/ACM (CH343, Native USB, etc.)
228
+ else {
229
+ await setSignalsCDC(device, dtr, rts, controlInterface || 0);
230
+ }
231
+ // Match WebUSB timing - 50ms delay is critical for bootloader entry
232
+ // This ensures signals are stable before next operation
233
+ await new Promise((resolve) => setTimeout(resolve, 50));
234
+ },
235
+ async setBaudRate(baudRate) {
236
+ // CP2102 (Silicon Labs VID: 0x10c4)
237
+ if (vendorId === 0x10c4) {
238
+ // logger.debug(`[USB] CP2102: Setting baudrate to ${baudRate}...`);
239
+ const baudrateBuffer = Buffer.alloc(4);
240
+ baudrateBuffer.writeUInt32LE(baudRate, 0);
241
+ await controlTransferOut(device, {
242
+ requestType: "vendor",
243
+ recipient: "interface",
244
+ request: 0x1e, // IFC_SET_BAUDRATE
245
+ value: 0,
246
+ index: 0,
247
+ }, baudrateBuffer);
248
+ }
249
+ // CH340 (WCH VID: 0x1a86, but not CH343 PID: 0x55d3)
250
+ else if (vendorId === 0x1a86 && productId !== 0x55d3) {
251
+ const CH341_BAUDBASE_FACTOR = 1532620800;
252
+ const CH341_BAUDBASE_DIVMAX = 3;
253
+ let factor = Math.floor(CH341_BAUDBASE_FACTOR / baudRate);
254
+ let divisor = CH341_BAUDBASE_DIVMAX;
255
+ while (factor > 0xfff0 && divisor > 0) {
256
+ factor >>= 3;
257
+ divisor--;
258
+ }
259
+ factor = 0x10000 - factor;
260
+ const a = (factor & 0xff00) | divisor;
261
+ const b = factor & 0xff;
262
+ await controlTransferOut(device, {
263
+ requestType: "vendor",
264
+ recipient: "device",
265
+ request: 0x9a,
266
+ value: 0x1312,
267
+ index: a,
268
+ });
269
+ await controlTransferOut(device, {
270
+ requestType: "vendor",
271
+ recipient: "device",
272
+ request: 0x9a,
273
+ value: 0x0f2c,
274
+ index: b,
275
+ });
276
+ }
277
+ // FTDI (VID: 0x0403)
278
+ else if (vendorId === 0x0403) {
279
+ const baseClock = 3000000;
280
+ const divisor = baseClock / baudRate;
281
+ const integerPart = Math.floor(divisor);
282
+ const fractionalPart = divisor - integerPart;
283
+ let subInteger;
284
+ if (fractionalPart < 0.0625)
285
+ subInteger = 0;
286
+ else if (fractionalPart < 0.1875)
287
+ subInteger = 1;
288
+ else if (fractionalPart < 0.3125)
289
+ subInteger = 2;
290
+ else if (fractionalPart < 0.4375)
291
+ subInteger = 3;
292
+ else if (fractionalPart < 0.5625)
293
+ subInteger = 4;
294
+ else if (fractionalPart < 0.6875)
295
+ subInteger = 5;
296
+ else if (fractionalPart < 0.8125)
297
+ subInteger = 6;
298
+ else
299
+ subInteger = 7;
300
+ const value = (integerPart & 0xff) |
301
+ ((subInteger & 0x07) << 14) |
302
+ (((integerPart >> 8) & 0x3f) << 8);
303
+ const index = (integerPart >> 14) & 0x03;
304
+ await controlTransferOut(device, {
305
+ requestType: "vendor",
306
+ recipient: "device",
307
+ request: 0x03,
308
+ value: value,
309
+ index: index,
310
+ });
311
+ }
312
+ // CDC/ACM (CH343, Native USB, etc.)
313
+ else {
314
+ const lineCoding = Buffer.alloc(7);
315
+ lineCoding.writeUInt32LE(baudRate, 0);
316
+ lineCoding[4] = 0x00; // 1 stop bit
317
+ lineCoding[5] = 0x00; // No parity
318
+ lineCoding[6] = 0x08; // 8 data bits
319
+ await controlTransferOut(device, {
320
+ requestType: "class",
321
+ recipient: "interface",
322
+ request: 0x20,
323
+ value: 0,
324
+ index: controlInterface || 0,
325
+ }, lineCoding);
326
+ }
327
+ },
328
+ async getSignals() {
329
+ // Not implemented for USB - return dummy values
330
+ return {
331
+ dataCarrierDetect: false,
332
+ clearToSend: false,
333
+ ringIndicator: false,
334
+ dataSetReady: false,
335
+ };
336
+ },
337
+ getInfo() {
338
+ return {
339
+ usbVendorId: vendorId,
340
+ usbProductId: productId,
341
+ };
342
+ },
343
+ };
344
+ function createStreams() {
345
+ if (!endpointIn || !endpointOut) {
346
+ throw new Error("Endpoints not configured");
347
+ }
348
+ // Start polling immediately (not in ReadableStream.start)
349
+ try {
350
+ endpointIn.startPoll(2, 64);
351
+ }
352
+ catch (err) {
353
+ logger.error(`Failed to start poll: ${err.message}`);
354
+ }
355
+ // ReadableStream for incoming data
356
+ readableStream = new ReadableStream({
357
+ start(controller) {
358
+ endpointIn.on("data", (data) => {
359
+ try {
360
+ if (data.length > 0) {
361
+ controller.enqueue(new Uint8Array(data));
362
+ }
363
+ }
364
+ catch (err) {
365
+ logger.error(`USB RX handler error: ${err.message}`);
366
+ }
367
+ });
368
+ endpointIn.on("error", (err) => {
369
+ try {
370
+ logger.error(`USB read error: ${err.message}`);
371
+ // Don't close on error, just log it
372
+ }
373
+ catch (e) {
374
+ // Ignore errors in error handler
375
+ }
376
+ });
377
+ endpointIn.on("end", () => {
378
+ try {
379
+ controller.close();
380
+ }
381
+ catch (err) {
382
+ // Ignore errors when closing controller
383
+ }
384
+ });
385
+ },
386
+ cancel() {
387
+ if (endpointIn) {
388
+ try {
389
+ endpointIn.stopPoll();
390
+ endpointIn.removeAllListeners();
391
+ }
392
+ catch (err) {
393
+ // Ignore
394
+ }
395
+ }
396
+ },
397
+ });
398
+ // WritableStream for outgoing data
399
+ writableStream = new WritableStream({
400
+ async write(chunk) {
401
+ return new Promise((resolve, reject) => {
402
+ if (!endpointOut) {
403
+ reject(new Error("Endpoint not configured"));
404
+ return;
405
+ }
406
+ endpointOut.transfer(Buffer.from(chunk), (err) => {
407
+ if (err) {
408
+ logger.error(`USB TX error: ${err.message}`);
409
+ reject(err);
410
+ }
411
+ else {
412
+ resolve();
413
+ }
414
+ });
415
+ });
416
+ },
417
+ });
418
+ }
419
+ return adapter;
420
+ }
421
+ // Chip-specific initialization functions
422
+ async function initializeChip(device, vendorId, productId, baudRate, logger) {
423
+ // CP2102 (Silicon Labs)
424
+ if (vendorId === 0x10c4) {
425
+ logger.debug("Initializing CP2102...");
426
+ // Step 1: Enable UART
427
+ logger.debug("CP2102: Enabling UART interface...");
428
+ await controlTransferOut(device, {
429
+ requestType: "vendor",
430
+ recipient: "device",
431
+ request: 0x00, // IFC_ENABLE
432
+ value: 0x01, // UART_ENABLE
433
+ index: 0x00,
434
+ });
435
+ // Step 2: Set line control (8N1)
436
+ logger.debug("CP2102: Setting line control (8N1)...");
437
+ await controlTransferOut(device, {
438
+ requestType: "vendor",
439
+ recipient: "device",
440
+ request: 0x03, // SET_LINE_CTL
441
+ value: 0x0800, // 8 data bits, no parity, 1 stop bit
442
+ index: 0x00,
443
+ });
444
+ // Step 3: Set DTR/RTS
445
+ logger.debug("CP2102: Setting DTR/RTS...");
446
+ await controlTransferOut(device, {
447
+ requestType: "vendor",
448
+ recipient: "device",
449
+ request: 0x07, // SET_MHS
450
+ value: 0x03 | 0x0100 | 0x0200, // DTR=1, RTS=1 with masks
451
+ index: 0x00,
452
+ });
453
+ // Step 4: Set baudrate
454
+ logger.debug(`CP2102: Setting baudrate to ${baudRate}...`);
455
+ const baudrateBuffer = Buffer.alloc(4);
456
+ baudrateBuffer.writeUInt32LE(baudRate, 0);
457
+ await controlTransferOut(device, {
458
+ requestType: "vendor",
459
+ recipient: "interface",
460
+ request: 0x1e, // IFC_SET_BAUDRATE
461
+ value: 0,
462
+ index: 0,
463
+ }, baudrateBuffer);
464
+ logger.debug("CP2102: Initialization complete");
465
+ }
466
+ // CH340 (WCH)
467
+ else if (vendorId === 0x1a86 && productId !== 0x55d3) {
468
+ logger.debug("Initializing CH340...");
469
+ // Initialize
470
+ await controlTransferOut(device, {
471
+ requestType: "vendor",
472
+ recipient: "device",
473
+ request: 0xa1,
474
+ value: 0x0000,
475
+ index: 0x0000,
476
+ });
477
+ // Set baudrate
478
+ const CH341_BAUDBASE_FACTOR = 1532620800;
479
+ const CH341_BAUDBASE_DIVMAX = 3;
480
+ let factor = Math.floor(CH341_BAUDBASE_FACTOR / baudRate);
481
+ let divisor = CH341_BAUDBASE_DIVMAX;
482
+ while (factor > 0xfff0 && divisor > 0) {
483
+ factor >>= 3;
484
+ divisor--;
485
+ }
486
+ factor = 0x10000 - factor;
487
+ const a = (factor & 0xff00) | divisor;
488
+ const b = factor & 0xff;
489
+ await controlTransferOut(device, {
490
+ requestType: "vendor",
491
+ recipient: "device",
492
+ request: 0x9a,
493
+ value: 0x1312,
494
+ index: a,
495
+ });
496
+ await controlTransferOut(device, {
497
+ requestType: "vendor",
498
+ recipient: "device",
499
+ request: 0x9a,
500
+ value: 0x0f2c,
501
+ index: b,
502
+ });
503
+ // Set handshake
504
+ await controlTransferOut(device, {
505
+ requestType: "vendor",
506
+ recipient: "device",
507
+ request: 0xa4,
508
+ value: ~((1 << 5) | (1 << 6)) & 0xffff,
509
+ index: 0x0000,
510
+ });
511
+ }
512
+ // FTDI
513
+ else if (vendorId === 0x0403) {
514
+ logger.debug("Initializing FTDI...");
515
+ // Reset
516
+ await controlTransferOut(device, {
517
+ requestType: "vendor",
518
+ recipient: "device",
519
+ request: 0x00,
520
+ value: 0x00,
521
+ index: 0x00,
522
+ });
523
+ // Set flow control
524
+ await controlTransferOut(device, {
525
+ requestType: "vendor",
526
+ recipient: "device",
527
+ request: 0x02,
528
+ value: 0x00,
529
+ index: 0x00,
530
+ });
531
+ // Set data characteristics (8N1)
532
+ await controlTransferOut(device, {
533
+ requestType: "vendor",
534
+ recipient: "device",
535
+ request: 0x04,
536
+ value: 0x0008,
537
+ index: 0x00,
538
+ });
539
+ // Set baudrate
540
+ const baseClock = 3000000;
541
+ const divisor = baseClock / baudRate;
542
+ const integerPart = Math.floor(divisor);
543
+ const fractionalPart = divisor - integerPart;
544
+ let subInteger;
545
+ if (fractionalPart < 0.0625)
546
+ subInteger = 0;
547
+ else if (fractionalPart < 0.1875)
548
+ subInteger = 1;
549
+ else if (fractionalPart < 0.3125)
550
+ subInteger = 2;
551
+ else if (fractionalPart < 0.4375)
552
+ subInteger = 3;
553
+ else if (fractionalPart < 0.5625)
554
+ subInteger = 4;
555
+ else if (fractionalPart < 0.6875)
556
+ subInteger = 5;
557
+ else if (fractionalPart < 0.8125)
558
+ subInteger = 6;
559
+ else
560
+ subInteger = 7;
561
+ const value = (integerPart & 0xff) |
562
+ ((subInteger & 0x07) << 14) |
563
+ (((integerPart >> 8) & 0x3f) << 8);
564
+ const index = (integerPart >> 14) & 0x03;
565
+ await controlTransferOut(device, {
566
+ requestType: "vendor",
567
+ recipient: "device",
568
+ request: 0x03,
569
+ value: value,
570
+ index: index,
571
+ });
572
+ // Set DTR/RTS
573
+ await controlTransferOut(device, {
574
+ requestType: "vendor",
575
+ recipient: "device",
576
+ request: 0x01,
577
+ value: 0x0303,
578
+ index: 0x00,
579
+ });
580
+ }
581
+ // CDC/ACM
582
+ else {
583
+ logger.debug("Initializing CDC/ACM...");
584
+ // Set line coding
585
+ const lineCoding = Buffer.alloc(7);
586
+ lineCoding.writeUInt32LE(baudRate, 0);
587
+ lineCoding[4] = 0x00; // 1 stop bit
588
+ lineCoding[5] = 0x00; // No parity
589
+ lineCoding[6] = 0x08; // 8 data bits
590
+ await controlTransferOut(device, {
591
+ requestType: "class",
592
+ recipient: "interface",
593
+ request: 0x20,
594
+ value: 0,
595
+ index: 0,
596
+ }, lineCoding);
597
+ // Set control line state
598
+ await controlTransferOut(device, {
599
+ requestType: "class",
600
+ recipient: "interface",
601
+ request: 0x22,
602
+ value: 0x03,
603
+ index: 0,
604
+ });
605
+ }
606
+ }
607
+ // Signal setting functions
608
+ async function setSignalsCP2102(device, dtr, rts) {
609
+ // CP2102 uses vendor-specific request 0x07 (SET_MHS)
610
+ // Bit 0: DTR value, Bit 1: RTS value
611
+ // Bit 8: DTR mask (MUST be set to change DTR)
612
+ // Bit 9: RTS mask (MUST be set to change RTS)
613
+ let value = 0;
614
+ value |= dtr ? 1 : 0; // DTR value
615
+ value |= rts ? 2 : 0; // RTS value
616
+ value |= 0x100; // DTR mask (ALWAYS set)
617
+ value |= 0x200; // RTS mask (ALWAYS set)
618
+ await controlTransferOut(device, {
619
+ requestType: "vendor",
620
+ recipient: "device",
621
+ request: 0x07,
622
+ value: value,
623
+ index: 0x00,
624
+ });
625
+ }
626
+ async function setSignalsCH340(device, dtr, rts) {
627
+ const value = ~((dtr ? 1 << 5 : 0) | (rts ? 1 << 6 : 0)) & 0xffff;
628
+ await controlTransferOut(device, {
629
+ requestType: "vendor",
630
+ recipient: "device",
631
+ request: 0xa4,
632
+ value: value,
633
+ index: 0,
634
+ });
635
+ }
636
+ async function setSignalsFTDI(device, dtr, rts) {
637
+ let value = 0;
638
+ value |= dtr ? 1 : 0;
639
+ value |= rts ? 2 : 0;
640
+ value |= 0x0100 | 0x0200; // Masks
641
+ await controlTransferOut(device, {
642
+ requestType: "vendor",
643
+ recipient: "device",
644
+ request: 0x01,
645
+ value: value,
646
+ index: 0x00,
647
+ });
648
+ }
649
+ async function setSignalsCDC(device, dtr, rts, interfaceNumber) {
650
+ let value = 0;
651
+ value |= dtr ? 1 : 0;
652
+ value |= rts ? 2 : 0;
653
+ await controlTransferOut(device, {
654
+ requestType: "class",
655
+ recipient: "interface",
656
+ request: 0x22,
657
+ value: value,
658
+ index: interfaceNumber,
659
+ });
660
+ }
661
+ async function controlTransferOut(device, params, data) {
662
+ return new Promise((resolve, reject) => {
663
+ // bmRequestType = Direction | Type | Recipient
664
+ // Direction: 0x00 = Host-to-Device (OUT), 0x80 = Device-to-Host (IN)
665
+ // Type: 0x00 = Standard, 0x20 = Class, 0x40 = Vendor
666
+ // Recipient: 0x00 = Device, 0x01 = Interface, 0x02 = Endpoint, 0x03 = Other
667
+ const bmRequestType = 0x00 | // Direction: Host-to-Device (OUT)
668
+ (params.requestType === "standard"
669
+ ? 0x00
670
+ : params.requestType === "class"
671
+ ? 0x20
672
+ : 0x40) |
673
+ (params.recipient === "device"
674
+ ? 0x00
675
+ : params.recipient === "interface"
676
+ ? 0x01
677
+ : params.recipient === "endpoint"
678
+ ? 0x02
679
+ : 0x03);
680
+ device.controlTransfer(bmRequestType, params.request, params.value, params.index, data || Buffer.alloc(0), (err) => {
681
+ if (err) {
682
+ reject(err);
683
+ }
684
+ else {
685
+ resolve();
686
+ }
687
+ });
688
+ });
689
+ }
690
+ /**
691
+ * List available USB serial devices
692
+ */
693
+ export async function listUSBPorts() {
694
+ try {
695
+ const usb = await import("usb");
696
+ const devices = usb.getDeviceList();
697
+ const serialDevices = devices.filter((device) => {
698
+ const vid = device.deviceDescriptor.idVendor;
699
+ // Filter for known USB-Serial chips
700
+ return (vid === 0x303a || // Espressif
701
+ vid === 0x0403 || // FTDI
702
+ vid === 0x1a86 || // CH340/CH343
703
+ vid === 0x10c4 || // CP210x
704
+ vid === 0x067b); // PL2303
705
+ });
706
+ return serialDevices.map((device) => {
707
+ const vid = device.deviceDescriptor.idVendor;
708
+ const pid = device.deviceDescriptor.idProduct;
709
+ return {
710
+ path: `USB:${vid.toString(16)}:${pid.toString(16)}`,
711
+ manufacturer: undefined,
712
+ serialNumber: undefined,
713
+ vendorId: vid.toString(16).padStart(4, "0"),
714
+ productId: pid.toString(16).padStart(4, "0"),
715
+ };
716
+ });
717
+ }
718
+ catch (err) {
719
+ if (err.code === "ERR_MODULE_NOT_FOUND" ||
720
+ err.code === "MODULE_NOT_FOUND") {
721
+ throw new Error("usb package not installed. Run: npm install usb");
722
+ }
723
+ throw err;
724
+ }
725
+ }