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