esp32tool 1.4.1 → 1.5.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/js/script.js CHANGED
@@ -1,6 +1,7 @@
1
1
  // Import WebUSB serial support for Android compatibility
2
2
  import { WebUSBSerial, requestSerialPort } from './webusb-serial.js';
3
3
  import { ESP32ToolConsole } from './console.js';
4
+ import { HexEditor } from './hex-editor.js';
4
5
 
5
6
  // Make requestSerialPort available globally for esptool.js
6
7
  // Use defensive assignment to avoid accidental overwrites
@@ -32,6 +33,7 @@ let currentChipName = null; // Store chip name globally
32
33
  let currentMacAddr = null; // Store MAC address globally
33
34
  let isConnected = false; // Track connection state
34
35
  let consoleInstance = null; // ESP32ToolConsole instance
36
+ let hexEditorInstance = null; // HexEditor instance
35
37
  let baudRateBeforeConsole = null; // Store baudrate before opening console
36
38
  let espLoaderBeforeConsole = null; // Store original ESPLoader before console
37
39
  let chipFamilyBeforeConsole = null; // Store chipFamily before opening console
@@ -228,6 +230,8 @@ const butCloseFileViewer = document.getElementById("butCloseFileViewer");
228
230
  const butDownloadFromViewer = document.getElementById("butDownloadFromViewer");
229
231
  const tabText = document.getElementById("tabText");
230
232
  const tabHex = document.getElementById("tabHex");
233
+ const butHexEditor = document.getElementById("butHexEditor");
234
+ const hexeditorContainer = document.getElementById("hexeditor-container");
231
235
 
232
236
  let currentViewedFile = null;
233
237
  let currentViewedFileData = null;
@@ -343,6 +347,7 @@ document.addEventListener("DOMContentLoaded", () => {
343
347
  butErase.addEventListener("click", clickErase);
344
348
  butProgram.addEventListener("click", clickProgram);
345
349
  butReadFlash.addEventListener("click", clickReadFlash);
350
+ butHexEditor.addEventListener("click", clickHexEditor);
346
351
  butReadPartitions.addEventListener("click", clickReadPartitions);
347
352
  butDetectFS.addEventListener("click", clickDetectFS);
348
353
  butOpenFSManager.addEventListener("click", clickOpenFSManager);
@@ -565,6 +570,36 @@ function enableStyleSheet(node, enabled) {
565
570
  node.disabled = !enabled;
566
571
  }
567
572
 
573
+ /**
574
+ * Build advanced flash read/write options from the UI controls.
575
+ * Returns the options object or undefined if advanced mode is off.
576
+ * @returns {{ chunkSize?: number, blockSize?: number, maxInFlight?: number } | undefined}
577
+ */
578
+ function buildAdvancedOptions() {
579
+ if (!advancedMode.checked) return undefined;
580
+
581
+ const validate = (name, value) => {
582
+ if (value === undefined) return undefined;
583
+ if (!Number.isFinite(value) || value <= 0) {
584
+ throw new Error(`Invalid ${name}: ${value}`);
585
+ }
586
+ return value;
587
+ };
588
+
589
+ const chunkSize = validate("chunkSize", parseInt(chunkSizeSelect.value));
590
+ const blockSize = validate("blockSize", parseInt(blockSizeSelect.value));
591
+ const maxInFlight = validate("maxInFlight", parseInt(maxInFlightSelect.value));
592
+
593
+ // blockSize and maxInFlight must both be finite or both non-finite
594
+ const hasBlockSize = Number.isFinite(blockSize);
595
+ const hasMaxInFlight = Number.isFinite(maxInFlight);
596
+ if (hasBlockSize !== hasMaxInFlight) {
597
+ throw new Error("blockSize and maxInFlight must be provided together");
598
+ }
599
+
600
+ return { chunkSize, blockSize, maxInFlight };
601
+ }
602
+
568
603
  /**
569
604
  * Parse flash size string (e.g., "256KB", "4MB") to bytes
570
605
  * @param {string} sizeStr - Flash size string with unit (KB or MB)
@@ -1721,30 +1756,8 @@ async function clickReadFlash() {
1721
1756
  const progressBar = readProgress.querySelector("div");
1722
1757
 
1723
1758
  // Prepare options object if advanced mode is enabled
1724
- // Option validation helpers
1725
- const validateOption = (name, value) => {
1726
- if (value === undefined) return undefined;
1727
- if (!Number.isFinite(value) || value <= 0) {
1728
- throw new Error(`Invalid ${name}: ${value}`);
1729
- }
1730
- return value;
1731
- };
1732
-
1733
- let options = undefined;
1734
- let chunkSizeOpt, blockSizeOpt, maxInFlightOpt;
1735
- if (advancedMode.checked) {
1736
- chunkSizeOpt = validateOption("chunkSize", parseInt(chunkSizeSelect.value));
1737
- blockSizeOpt = validateOption("blockSize", parseInt(blockSizeSelect.value));
1738
- maxInFlightOpt = validateOption("maxInFlight", parseInt(maxInFlightSelect.value));
1739
- if ((blockSizeOpt ?? maxInFlightOpt) &&
1740
- (blockSizeOpt === undefined || maxInFlightOpt === undefined)) {
1741
- throw new Error("blockSize and maxInFlight must be provided together");
1742
- }
1743
- options = {
1744
- chunkSize: chunkSizeOpt,
1745
- blockSize: blockSizeOpt,
1746
- maxInFlight: maxInFlightOpt
1747
- };
1759
+ const options = buildAdvancedOptions();
1760
+ if (options) {
1748
1761
  logMsg(`Advanced mode: chunkSize=0x${options.chunkSize?.toString(16)}, blockSize=${options.blockSize}, maxInFlight=${options.maxInFlight}`);
1749
1762
  }
1750
1763
 
@@ -1802,6 +1815,126 @@ async function clickReadFlash() {
1802
1815
  }
1803
1816
  }
1804
1817
 
1818
+ /**
1819
+ * @name clickHexEditor
1820
+ * Click handler for the Hex Editor button.
1821
+ * Reads the entire flash into a hex editor window.
1822
+ */
1823
+ async function clickHexEditor() {
1824
+ const offset = parseInt(readOffset.value, 16);
1825
+ const size = parseInt(readSize.value, 16);
1826
+
1827
+ if (isNaN(offset) || isNaN(size) || size <= 0) {
1828
+ errorMsg("Invalid offset or size value");
1829
+ return;
1830
+ }
1831
+
1832
+ // Disable button to prevent concurrent reads
1833
+ butHexEditor.disabled = true;
1834
+
1835
+ try {
1836
+ // Create and show hex editor
1837
+ if (!hexEditorInstance) {
1838
+ hexEditorInstance = new HexEditor(hexeditorContainer);
1839
+ }
1840
+
1841
+ // Show the container and progress overlay immediately
1842
+ hexeditorContainer.classList.remove('hidden');
1843
+ document.body.classList.add('hexeditor-active');
1844
+
1845
+ // Build a temporary UI for progress display
1846
+ hexEditorInstance.initProgressUI();
1847
+ hexEditorInstance.showProgress('Reading flash...', 0);
1848
+
1849
+ // Prepare options
1850
+ const options = buildAdvancedOptions();
1851
+
1852
+ const data = await espStub.readFlash(
1853
+ offset,
1854
+ size,
1855
+ (packet, progress, totalSize) => {
1856
+ const pct = Math.floor((progress / totalSize) * 100);
1857
+ hexEditorInstance.showProgress(
1858
+ `Reading flash... ${pct}% (${(progress / 1024).toFixed(0)} / ${(totalSize / 1024).toFixed(0)} KB)`,
1859
+ pct
1860
+ );
1861
+ },
1862
+ options
1863
+ );
1864
+
1865
+ logMsg(`Successfully read ${data.length} bytes from flash for hex editor`);
1866
+
1867
+ // Open hex editor with the data
1868
+ hexEditorInstance.open(data, offset);
1869
+
1870
+ // Set up write handler
1871
+ hexEditorInstance.onWriteFlash = async (editedData, modifiedOffsets) => {
1872
+ // Snapshot the editor instance to avoid null dereference if disconnected mid-write
1873
+ const editor = hexEditorInstance;
1874
+ if (!editor) return;
1875
+
1876
+ // Group modified bytes into contiguous sectors (4KB aligned)
1877
+ const SECTOR_SIZE = 0x1000;
1878
+ const sectors = new Set();
1879
+ for (const off of modifiedOffsets) {
1880
+ sectors.add(Math.floor(off / SECTOR_SIZE) * SECTOR_SIZE);
1881
+ }
1882
+
1883
+ const sortedSectors = [...sectors].sort((a, b) => a - b);
1884
+ let written = 0;
1885
+ const total = sortedSectors.length;
1886
+
1887
+ for (const sectorOff of sortedSectors) {
1888
+ const sectorEnd = Math.min(sectorOff + SECTOR_SIZE, editedData.length);
1889
+ const sectorData = editedData.slice(sectorOff, sectorEnd);
1890
+ const flashAddr = offset + sectorOff;
1891
+
1892
+ editor.showProgress(
1893
+ `Writing sector at 0x${flashAddr.toString(16).toUpperCase()}... (${written + 1}/${total})`,
1894
+ Math.floor((written / total) * 100)
1895
+ );
1896
+
1897
+ await espStub.flashData(
1898
+ sectorData.buffer,
1899
+ (bytesWritten, totalBytes) => {
1900
+ const sectorPct = Math.floor((bytesWritten / totalBytes) * 100);
1901
+ editor.showProgress(
1902
+ `Writing sector at 0x${flashAddr.toString(16).toUpperCase()}... ${sectorPct}% (${written + 1}/${total})`,
1903
+ Math.floor(((written + bytesWritten / totalBytes) / total) * 100)
1904
+ );
1905
+ },
1906
+ flashAddr
1907
+ );
1908
+ written++;
1909
+ }
1910
+
1911
+ editor.showProgress('Write complete!', 100);
1912
+ logMsg(`Successfully wrote ${total} sector(s) to flash`);
1913
+ await sleep(500);
1914
+ editor.hideProgress();
1915
+ };
1916
+
1917
+ // Set up close handler
1918
+ hexEditorInstance.onClose = () => {
1919
+ hexeditorContainer.classList.add('hidden');
1920
+ document.body.classList.remove('hexeditor-active');
1921
+ hexEditorInstance = null;
1922
+ };
1923
+
1924
+ } catch (e) {
1925
+ errorMsg("Failed to read flash for hex editor: " + e);
1926
+ hexeditorContainer.classList.add('hidden');
1927
+ document.body.classList.remove('hexeditor-active');
1928
+ if (hexEditorInstance) {
1929
+ // close() handles ResizeObserver/keydown cleanup; onClose is not yet wired here so null manually
1930
+ try { hexEditorInstance.close(); } catch (_) {}
1931
+ hexEditorInstance = null;
1932
+ }
1933
+ } finally {
1934
+ butHexEditor.disabled = false;
1935
+ }
1936
+ }
1937
+
1805
1938
  /**
1806
1939
  * @name clickOpenFSManager
1807
1940
  * Click handler for the Open FS Manager button (ESP8266)
@@ -2106,6 +2239,7 @@ function toggleUIToolbar(show) {
2106
2239
  }
2107
2240
  butErase.disabled = !show;
2108
2241
  butReadFlash.disabled = !show;
2242
+ butHexEditor.disabled = !show;
2109
2243
  butReadPartitions.disabled = !show;
2110
2244
  }
2111
2245
 
@@ -2136,6 +2270,12 @@ function toggleUIConnected(connected) {
2136
2270
  if (commands) commands.classList.remove("hidden");
2137
2271
  consoleSwitch.checked = false;
2138
2272
  saveSetting("console", false);
2273
+
2274
+ // Close hex editor if open
2275
+ if (hexEditorInstance) {
2276
+ hexEditorInstance.close();
2277
+ hexEditorInstance = null;
2278
+ }
2139
2279
  }
2140
2280
  butConnect.textContent = lbl;
2141
2281
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "esp32tool",
3
- "version": "1.4.1",
3
+ "version": "1.5.0",
4
4
  "type": "module",
5
5
  "description": "Flash & Read ESP devices using WebSerial, Electron, and also Android mobile via WebUSB",
6
6
  "main": "electron/main.cjs",
@@ -41,7 +41,7 @@
41
41
  "test-pwa": "npx serve . -p 5004"
42
42
  },
43
43
  "devDependencies": {
44
- "@electron-forge/cli": "^7.11.1",
44
+ "@electron-forge/cli": "^7.6.1",
45
45
  "@electron-forge/maker-deb": "^7.11.1",
46
46
  "@electron-forge/maker-dmg": "^7.11.1",
47
47
  "@electron-forge/maker-rpm": "^7.11.1",
@@ -59,17 +59,17 @@
59
59
  "@types/serialport": "^10.2.0",
60
60
  "@types/w3c-web-serial": "^1.0.7",
61
61
  "archiver": "^7.0.1",
62
- "electron": "^40.2.1",
62
+ "electron": "^40.4.1",
63
63
  "electron-squirrel-startup": "^1.0.1",
64
64
  "eslint": "^9.39.2",
65
- "eslint-config-prettier": "^10.1.8",
66
- "eslint-plugin-prettier": "^5.5.5",
65
+ "eslint-config-prettier": "^9.1.0",
66
+ "eslint-plugin-prettier": "^5.2.1",
67
67
  "npm-run-all": "^4.1.5",
68
68
  "prettier": "^3.8.1",
69
69
  "rollup": "^4.57.0",
70
- "serve": "^14.2.4",
70
+ "serve": "^14.2.5",
71
71
  "typescript": "^5.7.3",
72
- "typescript-eslint": "^8.55.0"
72
+ "typescript-eslint": "^8.56.0"
73
73
  },
74
74
  "dependencies": {
75
75
  "pako": "^2.1.0",
Binary file
Binary file
package/src/cli.ts CHANGED
@@ -280,7 +280,7 @@ async function connectViaUSB(
280
280
  if (webPort) {
281
281
  try {
282
282
  await webPort.close();
283
- } catch (closeErr) {
283
+ } catch (_closeErr) {
284
284
  // Ignore close errors
285
285
  }
286
286
  }
@@ -582,7 +582,7 @@ async function main() {
582
582
  if (esploader) {
583
583
  try {
584
584
  await esploader.disconnect();
585
- } catch (disconnectErr) {
585
+ } catch (_disconnectErr) {
586
586
  // Ignore disconnect errors during error handling
587
587
  }
588
588
  }
package/src/const.ts CHANGED
@@ -1,11 +1,9 @@
1
1
  import { toByteArray } from "./util";
2
2
 
3
3
  export interface Logger {
4
- /* eslint-disable @typescript-eslint/no-explicit-any */
5
4
  log(msg: string, ...args: any[]): void;
6
5
  error(msg: string, ...args: any[]): void;
7
6
  debug(msg: string, ...args: any[]): void;
8
- /* eslint-enable @typescript-eslint/no-explicit-any */
9
7
  }
10
8
 
11
9
  export const baudRates = [
package/src/esp_loader.ts CHANGED
@@ -1026,14 +1026,18 @@ export class ESPLoader extends EventTarget {
1026
1026
  }
1027
1027
  } else {
1028
1028
  if (step.dtr !== undefined) {
1029
- webusb
1030
- ? await this.setDTRWebUSB(step.dtr)
1031
- : await this.setDTR(step.dtr);
1029
+ if (webusb) {
1030
+ await this.setDTRWebUSB(step.dtr);
1031
+ } else {
1032
+ await this.setDTR(step.dtr);
1033
+ }
1032
1034
  }
1033
1035
  if (step.rts !== undefined) {
1034
- webusb
1035
- ? await this.setRTSWebUSB(step.rts)
1036
- : await this.setRTS(step.rts);
1036
+ if (webusb) {
1037
+ await this.setRTSWebUSB(step.rts);
1038
+ } else {
1039
+ await this.setRTS(step.rts);
1040
+ }
1037
1041
  }
1038
1042
  }
1039
1043
  if (step.delayMs) await sleep(step.delayMs);
@@ -1558,7 +1562,7 @@ export class ESPLoader extends EventTarget {
1558
1562
  `Connected CDC/JTAG successfully with ${strategy.name} reset.`,
1559
1563
  );
1560
1564
  return;
1561
- } catch (error) {
1565
+ } catch (_error) {
1562
1566
  throw new Error("Sync timeout or abandoned");
1563
1567
  }
1564
1568
  }
@@ -2492,7 +2496,7 @@ export class ESPLoader extends EventTarget {
2492
2496
 
2493
2497
  // Restart Readloop
2494
2498
  this.readLoop();
2495
- } catch (e) {
2499
+ } catch (_e) {
2496
2500
  // this.logger.error(`Reconfigure port error: ${e}`);
2497
2501
  // throw new Error(`Unable to change the baud rate to ${baud}: ${e}`);
2498
2502
  } finally {
@@ -3285,7 +3289,7 @@ export class ESPLoader extends EventTarget {
3285
3289
  // Wait for pending writes to complete
3286
3290
  try {
3287
3291
  await this._writeChain;
3288
- } catch (err) {
3292
+ } catch (_err) {
3289
3293
  // this.logger.debug(`Pending write error during disconnect: ${err}`);
3290
3294
  }
3291
3295
 
@@ -3294,7 +3298,7 @@ export class ESPLoader extends EventTarget {
3294
3298
  try {
3295
3299
  await this._writer.close();
3296
3300
  this._writer.releaseLock();
3297
- } catch (err) {
3301
+ } catch (_err) {
3298
3302
  // this.logger.debug(`Writer close/release error: ${err}`);
3299
3303
  }
3300
3304
  this._writer = undefined;
@@ -3305,7 +3309,7 @@ export class ESPLoader extends EventTarget {
3305
3309
  const writer = this.port.writable.getWriter();
3306
3310
  await writer.close();
3307
3311
  writer.releaseLock();
3308
- } catch (err) {
3312
+ } catch (_err) {
3309
3313
  // this.logger.debug(`Direct writer close error: ${err}`);
3310
3314
  }
3311
3315
  }
@@ -3334,7 +3338,7 @@ export class ESPLoader extends EventTarget {
3334
3338
  // Only cancel if reader is still active
3335
3339
  try {
3336
3340
  this._reader.cancel();
3337
- } catch (err) {
3341
+ } catch (_err) {
3338
3342
  // Reader already released, resolve immediately
3339
3343
  clearTimeout(timeout);
3340
3344
  resolve(undefined);
@@ -3365,7 +3369,7 @@ export class ESPLoader extends EventTarget {
3365
3369
  // Wait for pending writes to complete
3366
3370
  try {
3367
3371
  await this._writeChain;
3368
- } catch (err) {
3372
+ } catch (_err) {
3369
3373
  // this.logger.debug(`Pending write error during release: ${err}`);
3370
3374
  }
3371
3375
 
@@ -87,7 +87,7 @@ export function createNodeUSBAdapter(
87
87
  if (device.configDescriptor?.bConfigurationValue !== 1) {
88
88
  device.setConfiguration(1);
89
89
  }
90
- } catch (err) {
90
+ } catch (_err) {
91
91
  // Already configured
92
92
  }
93
93
 
@@ -145,7 +145,7 @@ export function createNodeUSBAdapter(
145
145
  if (usbInterface.isKernelDriverActive()) {
146
146
  usbInterface.detachKernelDriver();
147
147
  }
148
- } catch (err) {
148
+ } catch (_err) {
149
149
  // Ignore - may not be supported on all platforms
150
150
  }
151
151
 
@@ -189,7 +189,7 @@ export function createNodeUSBAdapter(
189
189
  if (vendorId === 0x10c4) {
190
190
  try {
191
191
  // Clear halt on endpoints
192
- await new Promise<void>((resolve, reject) => {
192
+ await new Promise<void>((resolve, _reject) => {
193
193
  device.controlTransfer(
194
194
  0x02, // Clear Feature, Endpoint
195
195
  0x01, // ENDPOINT_HALT
@@ -203,7 +203,7 @@ export function createNodeUSBAdapter(
203
203
  );
204
204
  });
205
205
 
206
- await new Promise<void>((resolve, reject) => {
206
+ await new Promise<void>((resolve, _reject) => {
207
207
  device.controlTransfer(
208
208
  0x02, // Clear Feature, Endpoint
209
209
  0x01, // ENDPOINT_HALT
@@ -216,7 +216,7 @@ export function createNodeUSBAdapter(
216
216
  },
217
217
  );
218
218
  });
219
- } catch (err) {
219
+ } catch (_err) {
220
220
  // Ignore
221
221
  }
222
222
  }
@@ -234,7 +234,7 @@ export function createNodeUSBAdapter(
234
234
  try {
235
235
  endpointIn.stopPoll();
236
236
  endpointIn.removeAllListeners();
237
- } catch (err) {
237
+ } catch (_err) {
238
238
  // Ignore
239
239
  }
240
240
  }
@@ -242,7 +242,7 @@ export function createNodeUSBAdapter(
242
242
  if (readableStream) {
243
243
  try {
244
244
  await readableStream.cancel();
245
- } catch (err) {
245
+ } catch (_err) {
246
246
  // Ignore
247
247
  }
248
248
  readableStream = null;
@@ -251,7 +251,7 @@ export function createNodeUSBAdapter(
251
251
  if (writableStream) {
252
252
  try {
253
253
  await writableStream.close();
254
- } catch (err) {
254
+ } catch (_err) {
255
255
  // Ignore
256
256
  }
257
257
  writableStream = null;
@@ -264,14 +264,14 @@ export function createNodeUSBAdapter(
264
264
  try {
265
265
  const usbInterface = device.interface(interfaceNumber);
266
266
  usbInterface.release(true, () => {});
267
- } catch (err) {
267
+ } catch (_err) {
268
268
  // Ignore
269
269
  }
270
270
  }
271
271
 
272
272
  try {
273
273
  device.close();
274
- } catch (err) {
274
+ } catch (_err) {
275
275
  // Ignore
276
276
  }
277
277
  },
@@ -469,7 +469,7 @@ export function createNodeUSBAdapter(
469
469
  try {
470
470
  logger.error(`USB read error: ${err.message}`);
471
471
  // Don't close on error, just log it
472
- } catch (e) {
472
+ } catch (_e) {
473
473
  // Ignore errors in error handler
474
474
  }
475
475
  });
@@ -477,7 +477,7 @@ export function createNodeUSBAdapter(
477
477
  endpointIn!.on("end", () => {
478
478
  try {
479
479
  controller.close();
480
- } catch (err) {
480
+ } catch (_err) {
481
481
  // Ignore errors when closing controller
482
482
  }
483
483
  });
@@ -488,7 +488,7 @@ export function createNodeUSBAdapter(
488
488
  try {
489
489
  endpointIn.stopPoll();
490
490
  endpointIn.removeAllListeners();
491
- } catch (err) {
491
+ } catch (_err) {
492
492
  // Ignore
493
493
  }
494
494
  }
@@ -29,6 +29,7 @@ export class ColoredConsole {
29
29
 
30
30
  addLine(line: string) {
31
31
  // biome-ignore lint/suspicious/noControlCharactersInRegex: ANSI escape sequences
32
+ // eslint-disable-next-line no-control-regex
32
33
  const re = /(?:\x1B|\\x1B)(?:\[(.*?)[@-~]|\].*?(?:\x07|\x1B\\))/g;
33
34
  let i = 0;
34
35
 
package/sw.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // Service Worker for ESP32Tool PWA
2
- const CACHE_NAME = 'esp32tool-v1.4.1';
2
+ const CACHE_NAME = 'esp32tool-v1.5.0';
3
3
  const RUNTIME_CACHE = 'esp32tool-runtime';
4
4
 
5
5
  // Core files to cache on install (relative paths work for any deployment path)
@@ -17,6 +17,7 @@ const CORE_ASSETS = [
17
17
 
18
18
  // JavaScript
19
19
  './js/script.js',
20
+ './js/hex-editor.js',
20
21
  './js/utilities.js',
21
22
  './js/webusb-serial.js',
22
23
  './js/modules/esptool.js',