tasmota-webserial-esptool 7.2.4 → 7.2.6

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tasmota-webserial-esptool",
3
- "version": "7.2.4",
3
+ "version": "7.2.6",
4
4
  "description": "Flash & Read ESP devices using WebSerial",
5
5
  "main": "dist/index.js",
6
6
  "repository": {
@@ -14,7 +14,7 @@
14
14
  "license": "MIT",
15
15
  "type": "module",
16
16
  "scripts": {
17
- "prepublishOnly": "script/build",
17
+ "build": "script/build",
18
18
  "format": "npm exec -- prettier --write src",
19
19
  "develop": "script/develop",
20
20
  "lint": "eslint src/",
package/src/esp_loader.ts CHANGED
@@ -166,8 +166,6 @@ export class ESPLoader extends EventTarget {
166
166
  }
167
167
 
168
168
  async initialize() {
169
- await this.hardReset(true);
170
-
171
169
  if (!this._parent) {
172
170
  this.__inputBuffer = [];
173
171
  this.__totalBytesRead = 0;
@@ -196,9 +194,8 @@ export class ESPLoader extends EventTarget {
196
194
  this.readLoop();
197
195
  }
198
196
 
199
- // Clear buffer again after starting read loop
200
- await this.flushSerialBuffers();
201
- await this.sync();
197
+ // Try to connect with different reset strategies
198
+ await this.connectWithResetStrategies();
202
199
 
203
200
  // Detect chip type
204
201
  await this.detectChip();
@@ -261,6 +258,10 @@ export class ESPLoader extends EventTarget {
261
258
  `GET_SECURITY_INFO failed, using magic value detection: ${error}`,
262
259
  );
263
260
 
261
+ // Drain input buffer for CP210x compatibility on Windows
262
+ // This ensures all error responses are cleared before continuing
263
+ await this.drainInputBuffer(200);
264
+
264
265
  // Clear input buffer and re-sync to recover from failed command
265
266
  this._inputBuffer.length = 0;
266
267
  await sleep(SYNC_TIMEOUT);
@@ -456,37 +457,11 @@ export class ESPLoader extends EventTarget {
456
457
  if (bootloader) {
457
458
  // enter flash mode
458
459
  if (this.port.getInfo().usbProductId === USB_JTAG_SERIAL_PID) {
459
- // esp32c3 esp32s3 etc. build-in USB serial.
460
- // when connect to computer direct via usb, using following signals
461
- // to enter flash mode automatically.
462
- await this.setDTR(false);
463
- await this.setRTS(false);
464
- await this.sleep(100);
465
-
466
- await this.setDTR(true);
467
- await this.setRTS(false);
468
- await this.sleep(100);
469
-
470
- await this.setRTS(true);
471
- await this.setDTR(false);
472
- await this.setRTS(true);
473
-
474
- await this.sleep(100);
475
- await this.setDTR(false);
476
- await this.setRTS(false);
477
- this.logger.log("USB MCU reset.");
460
+ await this.hardResetUSBJTAGSerial();
461
+ this.logger.log("USB-JTAG/Serial reset.");
478
462
  } else {
479
- // otherwise, esp chip should be connected to computer via usb-serial
480
- // bridge chip like ch340,CP2102 etc.
481
- // use normal way to enter flash mode.
482
- await this.setDTR(false);
483
- await this.setRTS(true);
484
- await this.sleep(100);
485
- await this.setDTR(true);
486
- await this.setRTS(false);
487
- await this.sleep(50);
488
- await this.setDTR(false);
489
- this.logger.log("DTR/RTS USB serial chip reset.");
463
+ await this.hardResetClassic();
464
+ this.logger.log("Classic reset.");
490
465
  }
491
466
  } else {
492
467
  // just reset
@@ -871,6 +846,135 @@ export class ESPLoader extends EventTarget {
871
846
  }
872
847
  }
873
848
 
849
+ /**
850
+ * @name connectWithResetStrategies
851
+ * Try different reset strategies to enter bootloader mode
852
+ * Similar to esptool.py's connect() method with multiple reset strategies
853
+ */
854
+ async connectWithResetStrategies() {
855
+ const portInfo = this.port.getInfo();
856
+ const isUSBJTAGSerial = portInfo.usbProductId === USB_JTAG_SERIAL_PID;
857
+ const isEspressifUSB = portInfo.usbVendorId === 0x303a;
858
+
859
+ this.logger.log(
860
+ `Detected USB: VID=0x${portInfo.usbVendorId?.toString(16) || "unknown"}, PID=0x${portInfo.usbProductId?.toString(16) || "unknown"}`,
861
+ );
862
+
863
+ // Define reset strategies to try in order
864
+ const resetStrategies: Array<{ name: string; fn: () => Promise<void> }> =
865
+ [];
866
+
867
+ // Strategy 1: USB-JTAG/Serial reset (for ESP32-C3, C6, S3, etc.)
868
+ // Try this first if we detect Espressif USB VID or the specific PID
869
+ if (isUSBJTAGSerial || isEspressifUSB) {
870
+ resetStrategies.push({
871
+ name: "USB-JTAG/Serial",
872
+ fn: async () => await this.hardResetUSBJTAGSerial(),
873
+ });
874
+ }
875
+
876
+ // Strategy 2: Classic reset (for USB-to-Serial bridges)
877
+ resetStrategies.push({
878
+ name: "Classic",
879
+ fn: async () => await this.hardResetClassic(),
880
+ });
881
+
882
+ // Strategy 3: If USB-JTAG/Serial was not tried yet, try it as fallback
883
+ if (!isUSBJTAGSerial && !isEspressifUSB) {
884
+ resetStrategies.push({
885
+ name: "USB-JTAG/Serial (fallback)",
886
+ fn: async () => await this.hardResetUSBJTAGSerial(),
887
+ });
888
+ }
889
+
890
+ let lastError: Error | null = null;
891
+
892
+ // Try each reset strategy
893
+ for (const strategy of resetStrategies) {
894
+ try {
895
+ this.logger.log(`Trying ${strategy.name} reset...`);
896
+
897
+ // Check if port is still open, if not, skip this strategy
898
+ if (!this.connected || !this.port.writable) {
899
+ this.logger.log(`Port disconnected, skipping ${strategy.name} reset`);
900
+ continue;
901
+ }
902
+
903
+ await strategy.fn();
904
+
905
+ // Try to sync after reset
906
+ await this.sync();
907
+
908
+ // If we get here, sync succeeded
909
+ this.logger.log(`Connected successfully with ${strategy.name} reset.`);
910
+ return;
911
+ } catch (error) {
912
+ lastError = error as Error;
913
+ this.logger.log(
914
+ `${strategy.name} reset failed: ${(error as Error).message}`,
915
+ );
916
+
917
+ // If port got disconnected, we can't try more strategies
918
+ if (!this.connected || !this.port.writable) {
919
+ this.logger.log(`Port disconnected during reset attempt`);
920
+ break;
921
+ }
922
+
923
+ // Clear buffers before trying next strategy
924
+ this._inputBuffer.length = 0;
925
+ await this.drainInputBuffer(200);
926
+ await this.flushSerialBuffers();
927
+ }
928
+ }
929
+
930
+ // All strategies failed
931
+ throw new Error(
932
+ `Couldn't sync to ESP. Try resetting manually. Last error: ${lastError?.message}`,
933
+ );
934
+ }
935
+
936
+ /**
937
+ * @name hardResetUSBJTAGSerial
938
+ * USB-JTAG/Serial reset sequence for ESP32-C3, ESP32-S3, ESP32-C6, etc.
939
+ */
940
+ async hardResetUSBJTAGSerial() {
941
+ await this.setRTS(false);
942
+ await this.setDTR(false); // Idle
943
+ await this.sleep(100);
944
+
945
+ await this.setDTR(true); // Set IO0
946
+ await this.setRTS(false);
947
+ await this.sleep(100);
948
+
949
+ await this.setRTS(true); // Reset. Calls inverted to go through (1,1) instead of (0,0)
950
+ await this.setDTR(false);
951
+ await this.setRTS(true); // RTS set as Windows only propagates DTR on RTS setting
952
+ await this.sleep(100);
953
+
954
+ await this.setDTR(false);
955
+ await this.setRTS(false); // Chip out of reset
956
+
957
+ // Wait for chip to boot into bootloader
958
+ await this.sleep(200);
959
+ }
960
+
961
+ /**
962
+ * @name hardResetClassic
963
+ * Classic reset sequence for USB-to-Serial bridge chips (CH340, CP2102, etc.)
964
+ */
965
+ async hardResetClassic() {
966
+ await this.setDTR(false); // IO0=HIGH
967
+ await this.setRTS(true); // EN=LOW, chip in reset
968
+ await this.sleep(100);
969
+ await this.setDTR(true); // IO0=LOW
970
+ await this.setRTS(false); // EN=HIGH, chip out of reset
971
+ await this.sleep(50);
972
+ await this.setDTR(false); // IO0=HIGH, done
973
+
974
+ // Wait for chip to boot into bootloader
975
+ await this.sleep(200);
976
+ }
977
+
874
978
  /**
875
979
  * @name sync
876
980
  * Put into ROM bootload mode & attempt to synchronize with the
@@ -1634,7 +1738,7 @@ export class ESPLoader extends EventTarget {
1634
1738
  *
1635
1739
  * @param bufferingTime - Time in milliseconds to wait for the buffer to fill
1636
1740
  */
1637
- private async drainInputBuffer(bufferingTime = 200): Promise<void> {
1741
+ async drainInputBuffer(bufferingTime = 200): Promise<void> {
1638
1742
  // Wait for the buffer to fill
1639
1743
  await sleep(bufferingTime);
1640
1744
 
@@ -1676,7 +1780,7 @@ export class ESPLoader extends EventTarget {
1676
1780
  * Flush any pending data in the TX and RX serial port buffers
1677
1781
  * This clears both the application RX buffer and waits for hardware buffers to drain
1678
1782
  */
1679
- private async flushSerialBuffers(): Promise<void> {
1783
+ async flushSerialBuffers(): Promise<void> {
1680
1784
  // Clear application buffer
1681
1785
  if (!this._parent) {
1682
1786
  this.__inputBuffer = [];
@@ -1733,10 +1837,12 @@ export class ESPLoader extends EventTarget {
1733
1837
  const chunkSize = Math.min(CHUNK_SIZE, remainingSize);
1734
1838
  let chunkSuccess = false;
1735
1839
  let retryCount = 0;
1736
- const MAX_RETRIES = 3;
1840
+ const MAX_RETRIES = 5;
1737
1841
 
1738
1842
  // Retry loop for this chunk
1739
1843
  while (!chunkSuccess && retryCount <= MAX_RETRIES) {
1844
+ let resp = new Uint8Array(0);
1845
+
1740
1846
  try {
1741
1847
  this.logger.debug(
1742
1848
  `Reading chunk at 0x${currentAddr.toString(16)}, size: 0x${chunkSize.toString(16)}`,
@@ -1750,8 +1856,6 @@ export class ESPLoader extends EventTarget {
1750
1856
  throw new Error("Failed to read memory: " + res);
1751
1857
  }
1752
1858
 
1753
- let resp = new Uint8Array(0);
1754
-
1755
1859
  while (resp.length < chunkSize) {
1756
1860
  // Read a SLIP packet
1757
1861
  let packet: number[];
@@ -1762,6 +1866,22 @@ export class ESPLoader extends EventTarget {
1762
1866
  this.logger.debug(
1763
1867
  `SLIP read error at ${resp.length} bytes: ${err.message}`,
1764
1868
  );
1869
+
1870
+ // Send final ACK for any data we did receive before the error
1871
+ if (resp.length > 0) {
1872
+ try {
1873
+ const ackData = pack("<I", resp.length);
1874
+ const slipEncodedAck = slipEncode(ackData);
1875
+ await this.writeToStream(slipEncodedAck);
1876
+ } catch (ackErr) {
1877
+ this.logger.debug(`ACK send error: ${ackErr}`);
1878
+ }
1879
+ }
1880
+
1881
+ // Drain input buffer for CP210x compatibility on Windows
1882
+ // This clears any stale data that may be causing the error
1883
+ await this.drainInputBuffer(300);
1884
+
1765
1885
  // If we've read all the data we need, break
1766
1886
  if (resp.length >= chunkSize) {
1767
1887
  break;
@@ -1796,21 +1916,25 @@ export class ESPLoader extends EventTarget {
1796
1916
  } catch (err) {
1797
1917
  retryCount++;
1798
1918
 
1799
- // Check if it's a timeout error
1800
- if (
1801
- err instanceof SlipReadError &&
1802
- err.message.includes("Timed out")
1803
- ) {
1919
+ // Check if it's a timeout error or SLIP error
1920
+ if (err instanceof SlipReadError) {
1804
1921
  if (retryCount <= MAX_RETRIES) {
1805
1922
  this.logger.log(
1806
- `⚠️ Timeout error at 0x${currentAddr.toString(16)}. Reconnecting and retrying (attempt ${retryCount}/${MAX_RETRIES})...`,
1923
+ `⚠️ ${err.message} at 0x${currentAddr.toString(16)}. Draining buffer and retrying (attempt ${retryCount}/${MAX_RETRIES})...`,
1807
1924
  );
1808
1925
 
1809
1926
  try {
1810
- await this.reconnect();
1811
- // Continue to retry the same chunk
1812
- } catch (reconnectErr) {
1813
- throw new Error(`Reconnect failed: ${reconnectErr}`);
1927
+ await this.drainInputBuffer(300);
1928
+
1929
+ // Clear application buffer
1930
+ await this.flushSerialBuffers();
1931
+
1932
+ // Wait before retry to let hardware settle
1933
+ await sleep(SYNC_TIMEOUT);
1934
+
1935
+ // Continue to retry the same chunk (will send new read command)
1936
+ } catch (drainErr) {
1937
+ this.logger.debug(`Buffer drain error: ${drainErr}`);
1814
1938
  }
1815
1939
  } else {
1816
1940
  throw new Error(
@@ -1818,7 +1942,7 @@ export class ESPLoader extends EventTarget {
1818
1942
  );
1819
1943
  }
1820
1944
  } else {
1821
- // Non-timeout error, don't retry
1945
+ // Non-SLIP error, don't retry
1822
1946
  throw err;
1823
1947
  }
1824
1948
  }
package/index.html DELETED
@@ -1,344 +0,0 @@
1
- <!doctype html>
2
- <html lang="en">
3
- <head>
4
- <title>WebSerial ESPTool</title>
5
- <meta charset="utf-8" />
6
- <meta http-equiv="X-UA-Compatible" content="IE=edge" />
7
- <meta name="viewport" content="width=device-width, initial-scale=1" />
8
- <script>
9
- // Redirect to HTTPS if HTTP is requested.
10
- if (
11
- window.location.hostname !== "localhost" &&
12
- window.location.protocol === "http:"
13
- ) {
14
- window.location.href = "https:" + window.location.href.substring(5);
15
- }
16
- </script>
17
-
18
- <!-- import the web page's stylesheets along with the themes -->
19
- <link rel="stylesheet" href="css/style.css" />
20
- <link
21
- rel="stylesheet"
22
- href="css/light.css"
23
- id="light"
24
- class="alternate"
25
- disabled
26
- />
27
- <link
28
- rel="stylesheet"
29
- href="css/dark.css"
30
- id="dark"
31
- class="alternate"
32
- disabled
33
- />
34
-
35
- <!-- import the webpage's javascript file -->
36
- <script module>
37
- window.esptoolPackage = import(
38
- // In development we import locally.
39
- window.location.hostname === "localhost"
40
- ? "./js/modules/esptool.js"
41
- : "./js/modules/esptool.js"
42
- );
43
- </script>
44
- <script src="js/script.js" module defer></script>
45
- </head>
46
- <body>
47
- <header class="header">
48
- <div class="left">
49
- <div class="title">WebSerial ESPTool</div>
50
- </div>
51
- <div class="right">
52
- <div class="controls-row">
53
- <label for="showlog">Show Log</label>
54
- <div class="onoffswitch">
55
- <input
56
- type="checkbox"
57
- name="showlog"
58
- class="onoffswitch-checkbox"
59
- id="showlog"
60
- />
61
- <label class="onoffswitch-label" for="showlog">
62
- <span class="onoffswitch-inner"></span>
63
- <span class="onoffswitch-switch"></span>
64
- </label>
65
- </div>
66
-
67
- <span class="log-controls">
68
- <label for="autoscroll">Autoscroll</label>
69
- <div class="onoffswitch">
70
- <input
71
- type="checkbox"
72
- name="autoscroll"
73
- class="onoffswitch-checkbox"
74
- id="autoscroll"
75
- />
76
- <label class="onoffswitch-label" for="autoscroll">
77
- <span class="onoffswitch-inner"></span>
78
- <span class="onoffswitch-switch"></span>
79
- </label>
80
- </div>
81
- <button id="butClear" type="button" class="small-btn">
82
- Clear
83
- </button>
84
- </span>
85
-
86
- <label for="debugmode">Debug</label>
87
- <div class="onoffswitch">
88
- <input
89
- type="checkbox"
90
- name="debugmode"
91
- class="onoffswitch-checkbox"
92
- id="debugmode"
93
- />
94
- <label class="onoffswitch-label" for="debugmode">
95
- <span class="onoffswitch-inner"></span>
96
- <span class="onoffswitch-switch"></span>
97
- </label>
98
- </div>
99
-
100
- <label for="darkmode">Dark</label>
101
- <div class="onoffswitch">
102
- <input
103
- type="checkbox"
104
- name="darkmode"
105
- class="onoffswitch-checkbox"
106
- id="darkmode"
107
- />
108
- <label class="onoffswitch-label" for="darkmode">
109
- <span class="onoffswitch-inner"></span>
110
- <span class="onoffswitch-switch"></span>
111
- </label>
112
- </div>
113
- </div>
114
- <select id="baudRate"></select>
115
- <button id="butConnect" type="button">Connect</button>
116
- </div>
117
- </header>
118
- <main class="main">
119
- <div id="notSupported" class="notSupported">
120
- Sorry, <b>Web Serial</b> is not supported on this device, make sure
121
- you're running Chrome 89 or later.
122
- </div>
123
- <div id="app">
124
- <div id="commands">
125
- <div class="upload">
126
- <label
127
- >Offset: 0x
128
- <input class="offset" type="text" value="0" />
129
- </label>
130
- <label class="firmware">
131
- <input type="file" accept=".bin" />
132
- <svg
133
- xmlns="http://www.w3.org/2000/svg"
134
- width="20"
135
- height="17"
136
- viewbox="0 0 20 17"
137
- >
138
- <path
139
- d="M10 0l-5.2 4.9h3.3v5.1h3.8v-5.1h3.3l-5.2-4.9zm9.3 11.5l-3.2-2.1h-2l3.4 2.6h-3.5c-.1 0-.2.1-.2.1l-.8 2.3h-6l-.8-2.2c-.1-.1-.1-.2-.2-.2h-3.6l3.4-2.6h-2l-3.2 2.1c-.4.3-.7 1-.6 1.5l.6 3.1c.1.5.7.9 1.2.9h16.3c.6 0 1.1-.4 1.3-.9l.6-3.1c.1-.5-.2-1.2-.7-1.5z"
140
- />
141
- </svg>
142
- <span>Choose a file&hellip;</span>
143
- </label>
144
- <div class="progress-bar hidden"><div></div></div>
145
- </div>
146
- <div class="upload">
147
- <label
148
- >Offset: 0x
149
- <input class="offset" type="text" value="0" />
150
- </label>
151
- <label class="firmware">
152
- <input type="file" accept=".bin" />
153
- <svg
154
- xmlns="http://www.w3.org/2000/svg"
155
- width="20"
156
- height="17"
157
- viewbox="0 0 20 17"
158
- >
159
- <path
160
- d="M10 0l-5.2 4.9h3.3v5.1h3.8v-5.1h3.3l-5.2-4.9zm9.3 11.5l-3.2-2.1h-2l3.4 2.6h-3.5c-.1 0-.2.1-.2.1l-.8 2.3h-6l-.8-2.2c-.1-.1-.1-.2-.2-.2h-3.6l3.4-2.6h-2l-3.2 2.1c-.4.3-.7 1-.6 1.5l.6 3.1c.1.5.7.9 1.2.9h16.3c.6 0 1.1-.4 1.3-.9l.6-3.1c.1-.5-.2-1.2-.7-1.5z"
161
- />
162
- </svg>
163
- <span>Choose a file&hellip;</span>
164
- </label>
165
- <div class="progress-bar hidden"><div></div></div>
166
- </div>
167
- <div class="upload">
168
- <label
169
- >Offset: 0x
170
- <input class="offset" type="text" value="0" />
171
- </label>
172
- <label class="firmware">
173
- <input type="file" accept=".bin" />
174
- <svg
175
- xmlns="http://www.w3.org/2000/svg"
176
- width="20"
177
- height="17"
178
- viewbox="0 0 20 17"
179
- >
180
- <path
181
- d="M10 0l-5.2 4.9h3.3v5.1h3.8v-5.1h3.3l-5.2-4.9zm9.3 11.5l-3.2-2.1h-2l3.4 2.6h-3.5c-.1 0-.2.1-.2.1l-.8 2.3h-6l-.8-2.2c-.1-.1-.1-.2-.2-.2h-3.6l3.4-2.6h-2l-3.2 2.1c-.4.3-.7 1-.6 1.5l.6 3.1c.1.5.7.9 1.2.9h16.3c.6 0 1.1-.4 1.3-.9l.6-3.1c.1-.5-.2-1.2-.7-1.5z"
182
- />
183
- </svg>
184
- <span>Choose a file&hellip;</span>
185
- </label>
186
- <div class="progress-bar hidden"><div></div></div>
187
- </div>
188
- <div class="upload">
189
- <label
190
- >Offset: 0x
191
- <input class="offset" type="text" value="0" />
192
- </label>
193
- <label class="firmware">
194
- <input type="file" accept=".bin" />
195
- <svg
196
- xmlns="http://www.w3.org/2000/svg"
197
- width="20"
198
- height="17"
199
- viewbox="0 0 20 17"
200
- >
201
- <path
202
- d="M10 0l-5.2 4.9h3.3v5.1h3.8v-5.1h3.3l-5.2-4.9zm9.3 11.5l-3.2-2.1h-2l3.4 2.6h-3.5c-.1 0-.2.1-.2.1l-.8 2.3h-6l-.8-2.2c-.1-.1-.1-.2-.2-.2h-3.6l3.4-2.6h-2l-3.2 2.1c-.4.3-.7 1-.6 1.5l.6 3.1c.1.5.7.9 1.2.9h16.3c.6 0 1.1-.4 1.3-.9l.6-3.1c.1-.5-.2-1.2-.7-1.5z"
203
- />
204
- </svg>
205
- <span>Choose a file&hellip;</span>
206
- </label>
207
- <div class="progress-bar hidden"><div></div></div>
208
- </div>
209
- <div class="upload">
210
- <label
211
- >Offset: 0x
212
- <input class="offset" type="text" value="0" />
213
- </label>
214
- <label class="firmware">
215
- <input type="file" accept=".bin" />
216
- <svg
217
- xmlns="http://www.w3.org/2000/svg"
218
- width="20"
219
- height="17"
220
- viewbox="0 0 20 17"
221
- >
222
- <path
223
- d="M10 0l-5.2 4.9h3.3v5.1h3.8v-5.1h3.3l-5.2-4.9zm9.3 11.5l-3.2-2.1h-2l3.4 2.6h-3.5c-.1 0-.2.1-.2.1l-.8 2.3h-6l-.8-2.2c-.1-.1-.1-.2-.2-.2h-3.6l3.4-2.6h-2l-3.2 2.1c-.4.3-.7 1-.6 1.5l.6 3.1c.1.5.7.9 1.2.9h16.3c.6 0 1.1-.4 1.3-.9l.6-3.1c.1-.5-.2-1.2-.7-1.5z"
224
- />
225
- </svg>
226
- <span>Choose a file&hellip;</span>
227
- </label>
228
- <div class="progress-bar hidden"><div></div></div>
229
- </div>
230
- <div class="upload">
231
- <label
232
- >Offset: 0x
233
- <input class="offset" type="text" value="0" />
234
- </label>
235
- <label class="firmware">
236
- <input type="file" accept=".bin" />
237
- <svg
238
- xmlns="http://www.w3.org/2000/svg"
239
- width="20"
240
- height="17"
241
- viewbox="0 0 20 17"
242
- >
243
- <path
244
- d="M10 0l-5.2 4.9h3.3v5.1h3.8v-5.1h3.3l-5.2-4.9zm9.3 11.5l-3.2-2.1h-2l3.4 2.6h-3.5c-.1 0-.2.1-.2.1l-.8 2.3h-6l-.8-2.2c-.1-.1-.1-.2-.2-.2h-3.6l3.4-2.6h-2l-3.2 2.1c-.4.3-.7 1-.6 1.5l.6 3.1c.1.5.7.9 1.2.9h16.3c.6 0 1.1-.4 1.3-.9l.6-3.1c.1-.5-.2-1.2-.7-1.5z"
245
- />
246
- </svg>
247
- <span>Choose a file&hellip;</span>
248
- </label>
249
- <div class="progress-bar hidden"><div></div></div>
250
- </div>
251
- <div class="upload">
252
- <label
253
- >Offset: 0x
254
- <input class="offset" type="text" value="0" />
255
- </label>
256
- <label class="firmware">
257
- <input type="file" accept=".bin" />
258
- <svg
259
- xmlns="http://www.w3.org/2000/svg"
260
- width="20"
261
- height="17"
262
- viewbox="0 0 20 17"
263
- >
264
- <path
265
- d="M10 0l-5.2 4.9h3.3v5.1h3.8v-5.1h3.3l-5.2-4.9zm9.3 11.5l-3.2-2.1h-2l3.4 2.6h-3.5c-.1 0-.2.1-.2.1l-.8 2.3h-6l-.8-2.2c-.1-.1-.1-.2-.2-.2h-3.6l3.4-2.6h-2l-3.2 2.1c-.4.3-.7 1-.6 1.5l.6 3.1c.1.5.7.9 1.2.9h16.3c.6 0 1.1-.4 1.3-.9l.6-3.1c.1-.5-.2-1.2-.7-1.5z"
266
- />
267
- </svg>
268
- <span>Choose a file&hellip;</span>
269
- </label>
270
- <div class="progress-bar hidden"><div></div></div>
271
- </div>
272
- <div class="upload">
273
- <label
274
- >Offset: 0x
275
- <input class="offset" type="text" value="0" />
276
- </label>
277
- <label class="firmware">
278
- <input type="file" accept=".bin" />
279
- <svg
280
- xmlns="http://www.w3.org/2000/svg"
281
- width="20"
282
- height="17"
283
- viewbox="0 0 20 17"
284
- >
285
- <path
286
- d="M10 0l-5.2 4.9h3.3v5.1h3.8v-5.1h3.3l-5.2-4.9zm9.3 11.5l-3.2-2.1h-2l3.4 2.6h-3.5c-.1 0-.2.1-.2.1l-.8 2.3h-6l-.8-2.2c-.1-.1-.1-.2-.2-.2h-3.6l3.4-2.6h-2l-3.2 2.1c-.4.3-.7 1-.6 1.5l.6 3.1c.1.5.7.9 1.2.9h16.3c.6 0 1.1-.4 1.3-.9l.6-3.1c.1-.5-.2-1.2-.7-1.5z"
287
- />
288
- </svg>
289
- <span>Choose a file&hellip;</span>
290
- </label>
291
- <div class="progress-bar hidden"><div></div></div>
292
- </div>
293
- <div class="buttons">
294
- <button id="butErase" type="button" disabled="disabled">
295
- Erase
296
- </button>
297
- <button id="butProgram" type="button" disabled="disabled">
298
- Program
299
- </button>
300
- </div>
301
- <div class="partition-table">
302
- <button id="butReadPartitions" type="button" disabled="disabled">
303
- Read Partition Table
304
- </button>
305
- <div class="progress-bar hidden" id="partitionProgress">
306
- <div></div>
307
- </div>
308
- <div id="partitionList" class="hidden"></div>
309
- </div>
310
- <div class="read-flash">
311
- <div class="progress-bar hidden" id="readProgress"><div></div></div>
312
- <div class="read-flash-inputs">
313
- <label>
314
- Address: 0x
315
- <input id="readOffset" type="text" value="0" />
316
- </label>
317
- <label>
318
- Size: 0x
319
- <input id="readSize" type="text" value="1000" />
320
- </label>
321
- <button id="butReadFlash" type="button" disabled="disabled">
322
- Read Flash
323
- </button>
324
- </div>
325
- </div>
326
- </div>
327
- <div id="log"></div>
328
- </div>
329
- </main>
330
-
331
- <!-- ESP32-S2 Reconnect Modal -->
332
- <div id="esp32s2Modal" class="modal hidden">
333
- <div class="modal-content">
334
- <h2>ESP32-S2 has switched to USB CDC mode</h2>
335
- <p>Please click the button below to select the new serial port.</p>
336
- <button id="butReconnectS2" type="button" class="modal-button">
337
- Select New Port
338
- </button>
339
- </div>
340
- </div>
341
-
342
- <footer class="footer"></footer>
343
- </body>
344
- </html>