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.
- package/.nojekyll +0 -0
- package/README.md +100 -6
- package/apple-touch-icon.png +0 -0
- package/build-electron-cli.cjs +177 -0
- package/build-single-binary.cjs +295 -0
- package/css/light.css +11 -0
- package/css/style.css +225 -35
- package/dist/cli.d.ts +17 -0
- package/dist/cli.js +458 -0
- package/dist/esp_loader.d.ts +129 -21
- package/dist/esp_loader.js +1227 -222
- package/dist/index.d.ts +2 -1
- package/dist/index.js +37 -4
- package/dist/node-usb-adapter.d.ts +47 -0
- package/dist/node-usb-adapter.js +725 -0
- package/dist/stubs/index.d.ts +1 -2
- package/dist/stubs/index.js +4 -0
- package/dist/web/index.js +1 -1
- package/electron/cli-main.cjs +74 -0
- package/electron/main.cjs +338 -0
- package/electron/main.js +7 -2
- package/favicon.ico +0 -0
- package/fix-cli-imports.cjs +127 -0
- package/generate-icons.sh +89 -0
- package/icons/icon-128.png +0 -0
- package/icons/icon-144.png +0 -0
- package/icons/icon-152.png +0 -0
- package/icons/icon-192.png +0 -0
- package/icons/icon-384.png +0 -0
- package/icons/icon-512.png +0 -0
- package/icons/icon-72.png +0 -0
- package/icons/icon-96.png +0 -0
- package/index.html +94 -64
- package/install-android.html +411 -0
- package/js/modules/esptool.js +1 -1
- package/js/script.js +165 -160
- package/js/webusb-serial.js +1017 -0
- package/license.md +1 -1
- package/manifest.json +89 -0
- package/package.cli.json +29 -0
- package/package.json +31 -21
- package/screenshots/desktop.png +0 -0
- package/screenshots/mobile.png +0 -0
- package/src/cli.ts +618 -0
- package/src/esp_loader.ts +1438 -254
- package/src/index.ts +69 -3
- package/src/node-usb-adapter.ts +924 -0
- package/src/stubs/index.ts +4 -1
- 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 };
|