esp32tool 1.1.8 → 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 +129 -21
  11. package/dist/esp_loader.js +1227 -222
  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 +1438 -254
  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
package/src/cli.ts ADDED
@@ -0,0 +1,618 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * ESP32Tool CLI - Command Line Interface for ESP device flashing
5
+ *
6
+ * Provides esptool.py-like commands for flashing ESP devices via WebSerial/WebUSB
7
+ *
8
+ * Usage:
9
+ * esp32tool flash-id
10
+ * esp32tool read-flash <offset> <size> <filename>
11
+ * esp32tool write-flash <offset> <filename>
12
+ * esp32tool erase-flash
13
+ * esp32tool erase-region <offset> <size>
14
+ * esp32tool chip-id
15
+ * esp32tool read-mac
16
+ */
17
+
18
+ import { ESPLoader } from "./esp_loader";
19
+ import { Logger } from "./const";
20
+ import { createNodeUSBAdapter, listUSBPorts } from "./node-usb-adapter";
21
+ import * as fs from "fs";
22
+
23
+ // CLI Logger
24
+ const cliLogger: Logger = {
25
+ log: (msg: string) => console.log(msg),
26
+ error: (msg: string) => console.error(`ERROR: ${msg}`),
27
+ debug: (msg: string) => {
28
+ if (process.env.DEBUG) {
29
+ console.log(`DEBUG: ${msg}`);
30
+ }
31
+ },
32
+ };
33
+
34
+ // Parse command line arguments
35
+ interface CLIArgs {
36
+ command: string;
37
+ args: string[];
38
+ port?: string;
39
+ baudRate?: number;
40
+ }
41
+
42
+ function parseArgs(): CLIArgs {
43
+ const args = process.argv.slice(2);
44
+
45
+ if (args.length === 0) {
46
+ showHelp();
47
+ process.exit(1);
48
+ }
49
+
50
+ const result: CLIArgs = {
51
+ command: "",
52
+ args: [],
53
+ baudRate: 115200,
54
+ };
55
+
56
+ let i = 0;
57
+ while (i < args.length) {
58
+ const arg = args[i];
59
+
60
+ if (arg === "--port" || arg === "-p") {
61
+ if (i + 1 >= args.length) {
62
+ console.error("Error: --port requires a value");
63
+ process.exit(1);
64
+ }
65
+ result.port = args[++i];
66
+ } else if (arg === "--baud" || arg === "-b") {
67
+ if (i + 1 >= args.length) {
68
+ console.error("Error: --baud requires a value");
69
+ process.exit(1);
70
+ }
71
+ const baud = parseInt(args[++i], 10);
72
+ if (isNaN(baud) || baud <= 0) {
73
+ console.error("Error: --baud must be a positive integer");
74
+ process.exit(1);
75
+ }
76
+ result.baudRate = baud;
77
+ } else if (arg === "--help" || arg === "-h") {
78
+ showHelp();
79
+ process.exit(0);
80
+ } else if (!result.command) {
81
+ result.command = arg;
82
+ } else {
83
+ result.args.push(arg);
84
+ }
85
+ i++;
86
+ }
87
+
88
+ return result;
89
+ }
90
+
91
+ function parseHexValue(value: string, paramName: string): number {
92
+ // Remove optional 0x prefix
93
+ const cleanValue = value.toLowerCase().startsWith("0x")
94
+ ? value.slice(2)
95
+ : value;
96
+
97
+ // Validate hex format
98
+ if (!/^[0-9a-f]+$/i.test(cleanValue)) {
99
+ throw new Error(
100
+ `Invalid ${paramName}: '${value}' is not a valid hexadecimal value`,
101
+ );
102
+ }
103
+
104
+ const parsed = parseInt(cleanValue, 16);
105
+
106
+ if (isNaN(parsed)) {
107
+ throw new Error(
108
+ `Invalid ${paramName}: '${value}' could not be parsed as hexadecimal`,
109
+ );
110
+ }
111
+
112
+ if (parsed < 0) {
113
+ throw new Error(`Invalid ${paramName}: '${value}' must be non-negative`);
114
+ }
115
+
116
+ return parsed;
117
+ }
118
+
119
+ function showHelp() {
120
+ console.log(`
121
+ ESP32Tool CLI - Flash ESP devices via WebSerial/WebUSB
122
+
123
+ Usage: esp32tool [options] <command> [args...]
124
+
125
+ Options:
126
+ --port, -p <port> Serial port path (e.g., /dev/ttyUSB0)
127
+ --baud, -b <rate> Baud rate (default: 115200)
128
+ --help, -h Show this help message
129
+
130
+ Commands:
131
+ list-ports List available serial ports
132
+ chip-id Read chip ID
133
+ flash-id Read SPI flash manufacturer and device ID
134
+ read-mac Read MAC address
135
+ read-flash <offset> <size> <filename>
136
+ Read flash memory to file
137
+ write-flash <offset> <filename>
138
+ Write file to flash memory
139
+ erase-flash Erase entire flash chip
140
+ erase-region <offset> <size>
141
+ Erase a region of flash
142
+ verify-flash <offset> <filename>
143
+ Verify flash contents against file
144
+
145
+ Examples:
146
+ esp32tool list-ports
147
+ esp32tool --port /dev/ttyUSB0 chip-id
148
+ esp32tool --port /dev/ttyUSB0 flash-id
149
+ esp32tool --port /dev/ttyUSB0 read-mac
150
+ esp32tool --port /dev/ttyUSB0 read-flash 0x0 0x400000 flash_dump.bin
151
+ esp32tool --port /dev/ttyUSB0 write-flash 0x1000 bootloader.bin
152
+ esp32tool --port /dev/ttyUSB0 erase-flash
153
+ esp32tool --port /dev/ttyUSB0 erase-region 0x9000 0x6000
154
+
155
+ Note: This CLI requires Node.js with SerialPort support.
156
+ For browser-based usage, use the web interface instead.
157
+ `);
158
+ }
159
+
160
+ // Connect to ESP device
161
+ async function connectToDevice(
162
+ portPath?: string,
163
+ baudRate: number = 115200,
164
+ ): Promise<ESPLoader> {
165
+ // List available ports if none specified
166
+ if (!portPath) {
167
+ cliLogger.log("No port specified. Available USB devices:");
168
+
169
+ const usbPorts = await listUSBPorts();
170
+ if (usbPorts.length === 0) {
171
+ throw new Error("No USB devices found");
172
+ }
173
+
174
+ usbPorts.forEach((port, idx) => {
175
+ console.log(` [${idx}] ${port.path}`);
176
+ });
177
+
178
+ throw new Error("Please specify a device with --port <USB:vid:pid>");
179
+ }
180
+
181
+ cliLogger.log(`Connecting to ${portPath} at ${baudRate} baud...`);
182
+
183
+ // Parse port path - support USB:vid:pid format
184
+ let targetVid: number | undefined;
185
+ let targetPid: number | undefined;
186
+
187
+ if (portPath.toUpperCase().startsWith("USB:")) {
188
+ // Format: USB:vid:pid
189
+ const parts = portPath.split(":");
190
+ if (parts.length === 3) {
191
+ targetVid = parseInt(parts[1], 16);
192
+ targetPid = parseInt(parts[2], 16);
193
+ }
194
+ }
195
+
196
+ return await connectViaUSB(targetVid, targetPid, baudRate);
197
+ }
198
+
199
+ // Connect via USB (direct USB access)
200
+ async function connectViaUSB(
201
+ targetVid: number | undefined,
202
+ targetPid: number | undefined,
203
+ baudRate: number,
204
+ ): Promise<ESPLoader> {
205
+ let webPort: any = null;
206
+
207
+ try {
208
+ const usb = await import("usb");
209
+
210
+ // Find USB device
211
+ const devices = usb.getDeviceList();
212
+ let device;
213
+
214
+ if (targetVid !== undefined && targetPid !== undefined) {
215
+ device = devices.find(
216
+ (d) =>
217
+ d.deviceDescriptor.idVendor === targetVid &&
218
+ d.deviceDescriptor.idProduct === targetPid,
219
+ );
220
+
221
+ if (!device) {
222
+ throw new Error(
223
+ `USB device not found: VID=0x${targetVid.toString(16)}, PID=0x${targetPid.toString(16)}`,
224
+ );
225
+ }
226
+ } else {
227
+ // Find first USB-Serial device
228
+ device = devices.find((d) => {
229
+ const vid = d.deviceDescriptor.idVendor;
230
+ return (
231
+ vid === 0x303a || // Espressif
232
+ vid === 0x0403 || // FTDI
233
+ vid === 0x1a86 || // CH340/CH343
234
+ vid === 0x10c4 || // CP210x
235
+ vid === 0x067b
236
+ ); // PL2303
237
+ });
238
+
239
+ if (!device) {
240
+ throw new Error(
241
+ "No USB-Serial device found. Run 'list-ports' to see available devices.",
242
+ );
243
+ }
244
+
245
+ cliLogger.log(
246
+ `Auto-detected USB device: VID=0x${device.deviceDescriptor.idVendor.toString(16)}, PID=0x${device.deviceDescriptor.idProduct.toString(16)}`,
247
+ );
248
+ }
249
+
250
+ // Create USB adapter
251
+ webPort = createNodeUSBAdapter(device, cliLogger);
252
+
253
+ // ALWAYS open at 115200 baud (ROM bootloader speed)
254
+ await webPort.open({ baudRate: 115200 });
255
+
256
+ // Create ESPLoader instance
257
+ const esploader = new ESPLoader(webPort as any, cliLogger);
258
+
259
+ // Initialize connection at ROM speed
260
+ await esploader.initialize();
261
+
262
+ cliLogger.log(`Connected to ${esploader.chipName || esploader.chipFamily}`);
263
+
264
+ // Load stub code for better performance and features
265
+ const stub = await esploader.runStub();
266
+
267
+ // Change to requested baudrate if different from ROM speed
268
+ if (baudRate !== 115200) {
269
+ try {
270
+ await stub.setBaudrate(baudRate);
271
+ cliLogger.log(`Baudrate changed to ${baudRate}`);
272
+ } catch (err: any) {
273
+ cliLogger.log(`Warning: Could not change baudrate: ${err.message}`);
274
+ }
275
+ }
276
+
277
+ return stub;
278
+ } catch (err: any) {
279
+ // Clean up port on failure
280
+ if (webPort) {
281
+ try {
282
+ await webPort.close();
283
+ } catch (closeErr) {
284
+ // Ignore close errors
285
+ }
286
+ }
287
+
288
+ // Check for permission errors
289
+ if (err.message && err.message.includes("LIBUSB_ERROR_ACCESS")) {
290
+ throw new Error(
291
+ "USB access denied. On macOS/Linux, you may need to run with sudo:\n" +
292
+ "Or use the Electron GUI version which doesn't require special permissions.",
293
+ );
294
+ }
295
+
296
+ throw err;
297
+ }
298
+ }
299
+
300
+ // Command implementations
301
+ async function cmdChipId(esploader: ESPLoader) {
302
+ cliLogger.log(`Chip Family: ${esploader.chipFamily}`);
303
+ cliLogger.log(`Chip Name: ${esploader.chipName || "Unknown"}`);
304
+ if (esploader.chipRevision !== null) {
305
+ cliLogger.log(`Chip Revision: ${esploader.chipRevision}`);
306
+ }
307
+ if (esploader.chipVariant) {
308
+ cliLogger.log(`Chip Variant: ${esploader.chipVariant}`);
309
+ }
310
+ }
311
+
312
+ async function cmdFlashId(esploader: ESPLoader) {
313
+ // Detect flash size using the stub
314
+ const stub = await esploader.runStub();
315
+ // detectFlashSize() is already called by runStub()
316
+ cliLogger.log(`Flash Size: ${stub.flashSize || "Unknown"}`);
317
+ }
318
+
319
+ async function cmdReadMac(esploader: ESPLoader) {
320
+ const mac = await esploader.getMacAddress();
321
+ cliLogger.log(`MAC Address: ${mac}`);
322
+ }
323
+
324
+ async function cmdReadFlash(
325
+ esploader: ESPLoader,
326
+ offset: number,
327
+ size: number,
328
+ filename: string,
329
+ ) {
330
+ cliLogger.log(
331
+ `Reading ${size} bytes from offset 0x${offset.toString(16)}...`,
332
+ );
333
+
334
+ // esploader is already a stub loader from connectViaUSB with correct baudrate
335
+ let lastProgress = 0;
336
+ const data = await esploader.readFlash(
337
+ offset,
338
+ size,
339
+ (packet: Uint8Array, progress: number, totalSize: number) => {
340
+ const percent = Math.round((progress / totalSize) * 100);
341
+ // Only update display every 1% to avoid too many updates
342
+ if (percent > lastProgress) {
343
+ lastProgress = percent;
344
+ process.stdout.write(
345
+ `\rProgress: ${percent}% (${progress}/${totalSize} bytes)`,
346
+ );
347
+ }
348
+ },
349
+ );
350
+
351
+ console.log(""); // New line after progress
352
+ fs.writeFileSync(filename, Buffer.from(data));
353
+
354
+ cliLogger.log(`Saved to ${filename}`);
355
+ }
356
+
357
+ async function cmdWriteFlash(
358
+ esploader: ESPLoader,
359
+ offset: number,
360
+ filename: string,
361
+ ) {
362
+ if (!fs.existsSync(filename)) {
363
+ throw new Error(`File not found: ${filename}`);
364
+ }
365
+
366
+ const fileData = fs.readFileSync(filename);
367
+ const size = fileData.byteLength;
368
+
369
+ cliLogger.log(`Writing ${size} bytes to offset 0x${offset.toString(16)}...`);
370
+
371
+ // Use stub for writing
372
+ const stub = await esploader.runStub();
373
+
374
+ // Write flash using the stub's flash methods
375
+ // Create a proper ArrayBuffer from the Buffer to avoid byteOffset issues
376
+ // Node.js Buffer can share an underlying ArrayBuffer with non-zero byteOffset
377
+ const arrayBuffer = fileData.buffer.slice(
378
+ fileData.byteOffset,
379
+ fileData.byteOffset + fileData.byteLength,
380
+ );
381
+ await stub.flashData(
382
+ arrayBuffer,
383
+ (bytesWritten: number, totalBytes: number) => {
384
+ const percent = Math.round((bytesWritten / totalBytes) * 100);
385
+ process.stdout.write(
386
+ `\rProgress: ${percent}% (${bytesWritten}/${totalBytes} bytes)`,
387
+ );
388
+ },
389
+ offset,
390
+ );
391
+
392
+ console.log(""); // New line after progress
393
+ cliLogger.log("Write complete!");
394
+ }
395
+
396
+ async function cmdEraseFlash(esploader: ESPLoader) {
397
+ cliLogger.log("Erasing entire flash chip...");
398
+
399
+ // Use stub for erasing
400
+ const stub = await esploader.runStub();
401
+
402
+ // Erase flash
403
+ await stub.eraseFlash();
404
+
405
+ cliLogger.log("Erase complete!");
406
+ }
407
+
408
+ async function cmdEraseRegion(
409
+ esploader: ESPLoader,
410
+ offset: number,
411
+ size: number,
412
+ ) {
413
+ cliLogger.log(
414
+ `Erasing region: offset=0x${offset.toString(16)}, size=0x${size.toString(16)}...`,
415
+ );
416
+
417
+ // Use stub for erasing
418
+ const stub = await esploader.runStub();
419
+
420
+ // Erase region
421
+ await stub.eraseRegion(offset, size);
422
+
423
+ cliLogger.log("Erase complete!");
424
+ }
425
+
426
+ async function cmdVerifyFlash(
427
+ esploader: ESPLoader,
428
+ offset: number,
429
+ filename: string,
430
+ ) {
431
+ if (!fs.existsSync(filename)) {
432
+ throw new Error(`File not found: ${filename}`);
433
+ }
434
+
435
+ const fileData = fs.readFileSync(filename);
436
+ const size = fileData.length;
437
+
438
+ cliLogger.log(
439
+ `Verifying ${size} bytes at offset 0x${offset.toString(16)}...`,
440
+ );
441
+
442
+ // Use stub for reading
443
+ const stub = await esploader.runStub();
444
+
445
+ let lastProgress = 0;
446
+ const flashData = await stub.readFlash(
447
+ offset,
448
+ size,
449
+ (packet: Uint8Array, progress: number, totalSize: number) => {
450
+ const percent = Math.round((progress / totalSize) * 100);
451
+ // Only update display every 1% to avoid too many updates
452
+ if (percent > lastProgress) {
453
+ lastProgress = percent;
454
+ process.stdout.write(
455
+ `\rReading: ${percent}% (${progress}/${totalSize} bytes)`,
456
+ );
457
+ }
458
+ },
459
+ );
460
+
461
+ console.log(""); // New line after progress
462
+
463
+ cliLogger.log("Comparing data...");
464
+ if (Buffer.compare(Buffer.from(flashData), fileData) === 0) {
465
+ cliLogger.log("Verification successful!");
466
+ } else {
467
+ throw new Error("Verification failed! Flash contents do not match file.");
468
+ }
469
+ }
470
+
471
+ // Main CLI entry point
472
+ async function main() {
473
+ const cliArgs = parseArgs();
474
+
475
+ let esploader: ESPLoader | null = null;
476
+
477
+ try {
478
+ // Validate command
479
+ if (!cliArgs.command) {
480
+ showHelp();
481
+ process.exit(1);
482
+ }
483
+
484
+ // Special command: list-ports (doesn't need device connection)
485
+ if (cliArgs.command === "list-ports") {
486
+ const usbPorts = await listUSBPorts();
487
+
488
+ if (usbPorts.length === 0) {
489
+ cliLogger.log("No USB devices found");
490
+ } else {
491
+ cliLogger.log("Available USB devices:");
492
+ usbPorts.forEach((port) => {
493
+ console.log(
494
+ ` ${port.path}${port.manufacturer ? ` (${port.manufacturer})` : ""}${port.serialNumber ? ` [${port.serialNumber}]` : ""}`,
495
+ );
496
+ });
497
+ }
498
+
499
+ // Clean exit without process.exit() to avoid native module cleanup issues
500
+ return;
501
+ }
502
+
503
+ // Connect to device
504
+ esploader = await connectToDevice(cliArgs.port, cliArgs.baudRate);
505
+
506
+ // Execute command
507
+ switch (cliArgs.command) {
508
+ case "chip-id":
509
+ await cmdChipId(esploader);
510
+ break;
511
+
512
+ case "flash-id":
513
+ await cmdFlashId(esploader);
514
+ break;
515
+
516
+ case "read-mac":
517
+ await cmdReadMac(esploader);
518
+ break;
519
+
520
+ case "read-flash": {
521
+ if (cliArgs.args.length < 3) {
522
+ throw new Error("read-flash requires: <offset> <size> <filename>");
523
+ }
524
+ const offset = parseHexValue(cliArgs.args[0], "offset");
525
+ const size = parseHexValue(cliArgs.args[1], "size");
526
+ const filename = cliArgs.args[2];
527
+ await cmdReadFlash(esploader, offset, size, filename);
528
+ break;
529
+ }
530
+
531
+ case "write-flash": {
532
+ if (cliArgs.args.length < 2) {
533
+ throw new Error("write-flash requires: <offset> <filename>");
534
+ }
535
+ const offset = parseHexValue(cliArgs.args[0], "offset");
536
+ const filename = cliArgs.args[1];
537
+ await cmdWriteFlash(esploader, offset, filename);
538
+ break;
539
+ }
540
+
541
+ case "erase-flash":
542
+ await cmdEraseFlash(esploader);
543
+ break;
544
+
545
+ case "erase-region": {
546
+ if (cliArgs.args.length < 2) {
547
+ throw new Error("erase-region requires: <offset> <size>");
548
+ }
549
+ const offset = parseHexValue(cliArgs.args[0], "offset");
550
+ const size = parseHexValue(cliArgs.args[1], "size");
551
+ await cmdEraseRegion(esploader, offset, size);
552
+ break;
553
+ }
554
+
555
+ case "verify-flash": {
556
+ if (cliArgs.args.length < 2) {
557
+ throw new Error("verify-flash requires: <offset> <filename>");
558
+ }
559
+ const offset = parseHexValue(cliArgs.args[0], "offset");
560
+ const filename = cliArgs.args[1];
561
+ await cmdVerifyFlash(esploader, offset, filename);
562
+ break;
563
+ }
564
+
565
+ default:
566
+ throw new Error(`Unknown command: ${cliArgs.command}`);
567
+ }
568
+
569
+ // Disconnect
570
+ await esploader.disconnect();
571
+
572
+ // Force exit after a short delay to ensure clean shutdown
573
+ // This is necessary for node-usb which may have pending callbacks
574
+ setTimeout(() => {
575
+ process.exit(0);
576
+ }, 100);
577
+
578
+ // Clean exit - let Electron handle the exit
579
+ return;
580
+ } catch (error: any) {
581
+ // Clean up device connection on failure
582
+ if (esploader) {
583
+ try {
584
+ await esploader.disconnect();
585
+ } catch (disconnectErr) {
586
+ // Ignore disconnect errors during error handling
587
+ }
588
+ }
589
+
590
+ cliLogger.error(error.message);
591
+ if (process.env.DEBUG) {
592
+ console.error(error.stack);
593
+ }
594
+ process.exit(1);
595
+ }
596
+ }
597
+
598
+ // Run CLI - only execute when this file is run directly (not when imported)
599
+ // Check if we're being run directly by Node.js (not imported by Electron)
600
+ const isDirectRun =
601
+ process.argv[1] &&
602
+ (process.argv[1].endsWith("/cli.js") ||
603
+ process.argv[1].endsWith("\\cli.js") ||
604
+ process.argv[1].endsWith("/cli-fixed.js") ||
605
+ process.argv[1].endsWith("\\cli-fixed.js") ||
606
+ process.argv[1].endsWith("/esp32tool") ||
607
+ process.argv[1].endsWith("\\esp32tool") ||
608
+ process.argv[1].endsWith("/esp32tool.exe") ||
609
+ process.argv[1].endsWith("\\esp32tool.exe"));
610
+
611
+ if (isDirectRun) {
612
+ main().catch((err) => {
613
+ console.error(err);
614
+ process.exit(1);
615
+ });
616
+ }
617
+
618
+ export { main as runCLI };