├── LICENSE ├── README.md └── esp-flasher.js /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 g3gg0.de 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Embeddable Web Serial ESP32 Flasher (C3/C6/S2/S3) 2 | 3 | A JavaScript library using the Web Serial API to flash ESP32-C3, ESP32-C6, ESP32-S2, and ESP32-S3 devices directly from a web browser. It features a simple interface designed for easy embedding into your HTML projects. 4 | 5 | This flasher communicates with the ESP32's ROM bootloader and can upload a flashing stub for faster and more reliable flashing operations. 6 | 7 | ## Key Features 8 | 9 | * **Browser-Based Flashing:** Flash supported ESP32 chips directly from Chrome, Edge, or Opera using the Web Serial API. 10 | * **No External Tools:** Does not require users to install Python, esptool, or drivers (uses the browser's built-in API). 11 | * **Simple Interface:** Provides a straightforward JavaScript class (`ESPFlasher`) for easy integration. 12 | * **Embeddable:** Designed to be easily included and used within web pages and applications. 13 | * **Supported Chips:** Currently supports ESP32-C3, ESP32-C6, ESP32-S2, and ESP32-S3. 14 | * **Automatic Chip Detection:** Identifies the connected chip based on its magic value read from memory. 15 | * **Stub Loader:** Includes and utilizes pre-compiled flashing stubs for supported chips to enhance speed and reliability. 16 | * **Core Bootloader Commands:** Implements essential commands like Sync, Read/Write Register, Memory Download, and Flash operations. 17 | * **SLIP Protocol:** Handles SLIP encoding and decoding for serial communication framing. 18 | * **Utility Functions:** Includes methods for reading the MAC address, basic reliability testing, and performing a blank check. 19 | 20 | ## Supported Chips 21 | 22 | * ESP32-C3 23 | * ESP32-C6 24 | * ESP32-S2 25 | * ESP32-S3 26 | 27 | *(Support for other chips like the original ESP32 or ESP32-S2 require adding their specific stubs and magic values).* 28 | 29 | ## Requirements 30 | 31 | 1. **Browser:** A modern web browser that supports the Web Serial API (e.g., Google Chrome, Microsoft Edge, Opera). 32 | 2. **Hardware:** An ESP32-C3, C6, S2, or S3 device connected via USB. 33 | 3. **Bootloader Mode:** You may have to put the ESP32 manually into **Serial Bootloader Mode**. This is typically done by: 34 | * Holding down the `BOOT` (or `IO0`) button. 35 | * Pressing and releasing the `RESET` (or `EN`) button. 36 | * Releasing the `BOOT` button. 37 | * Alternatively, hold `BOOT` while plugging in the USB cable. 38 | 39 | ## How it Works 40 | 41 | 1. The user initiates a connection via a button click on your web page. 42 | 2. The browser prompts the user to select a serial port (`navigator.serial.requestPort()`). 43 | 3. The `ESPFlasher` library opens the selected port. 44 | 4. It attempts to synchronize with the ESP32's ROM bootloader (`SYNC` command). 45 | 5. It reads a magic value from a specific memory address to identify the chip type. 46 | 6. It downloads and executes a small "stub" program to the ESP32's RAM (`MEM_BEGIN`, `MEM_DATA`, `MEM_END`). This stub handles flash operations more efficiently than the ROM bootloader alone. 47 | 7. The library then uses commands like `FLASH_BEGIN`, `FLASH_DATA`, and `FLASH_END` (communicating with the stub) to write the firmware binary to the device's flash memory. 48 | 8. All communication is framed using the SLIP protocol. 49 | 50 | ## Usage Example 51 | 52 | ```html 53 | 54 | 55 | 56 | ESP32 Web Flasher 57 | 58 | 59 |

ESP32 Web Flasher (C3/C6/S2/S3)

60 | 61 | 62 | 63 | 64 | 65 | 66 |

 67 |     
 68 | 
 69 |     
 70 |     
 71 | 
 72 |     
207 | 
208 | 
209 | ```
210 | 
211 | ## API Overview (`ESPFlasher` class)
212 | 
213 | *   `constructor()`: Creates a new flasher instance.
214 | *   `async openPort()`: Prompts the user to select a serial port and opens it.
215 | *   `async disconnect()`: Closes the serial port and cleans up.
216 | *   `async sync()`: Synchronizes with the ESP32 ROM bootloader and detects the chip type.
217 | *   `async downloadStub()`: Downloads the appropriate flashing stub to the ESP32's RAM. Returns `true` on success, `false` on failure.
218 | *   `async writeFlash(address, data, progressCallback)`: Writes the provided `Uint8Array` data to the specified flash address. `progressCallback(bytesWritten, totalBytes)` is called periodically.
219 | *   `async readMac()`: Reads and returns the MAC address of the connected device as a string (e.g., "AA:BB:CC:11:22:33").
220 | *   `async readReg(address)`: Reads a 32-bit value from the specified memory/register address. Returns the value as a number.
221 | *   `async testReliability(callback)`: Performs repeated register reads for a short duration to test connection stability. `callback(progressPercentage)` is called periodically. Returns `true` if successful, `false` on error or mismatch.
222 | *   `async blankCheck(callback)`: Reads through the flash memory to check how much of it is erased (contains `0xFF`). `callback(currentAddress, startAddress, endAddress, blockSize, erasedBytesInBlock, totalErasedBytes)` is called after each block read.
223 | *   `logDebug`: Assign a function `(message) => { ... }` to handle debug logging. Defaults to no-op.
224 | *   `logError`: Assign a function `(message) => { ... }` to handle error logging. Defaults to no-op.
225 | *   `devMode`: Set to `true` to enable verbose packet logging to the browser console.
226 | *   `disconnected`: Assign a function `() => { ... }` to be called when the port is disconnected unexpectedly or via `disconnect()`.
227 | *   `current_chip`: (Read-only property) String identifier of the detected chip ("esp32c3", "esp32s3", etc.) after a successful `sync()`.
228 | *   `stubLoaded`: (Read-only property) Boolean indicating if the flashing stub has been successfully loaded after `downloadStub()`.
229 | 
230 | *(Note: Some less common bootloader commands like `ERASE_FLASH`, `ERASE_REGION`, `SPI_ATTACH`, `CHANGE_BAUDRATE`, etc., are defined as constants but might not have dedicated high-level methods implemented in this version.)*
231 | 
232 | ## Limitations
233 | 
234 | *   **Chip Support:** Only the listed chips (C3, C6, S2, S3) are currently supported due to the included stubs and detection logic.
235 | *   **Browser Support:** Requires a browser with Web Serial API support.
236 | *   **Error Handling:** While basic error handling exists, it might be less robust than dedicated tools like `esptool.py`.
237 | *   **Advanced Features:** Does not currently support features like flash encryption, secure boot interactions, or eFuse programming.
238 | *   **Stub Size:** Embedding stub data directly in the JS increases the initial script size.
239 | 
240 | ## License
241 | 
242 | ```
243 | MIT License
244 | 
245 | Copyright (c) 2025 g3gg0
246 | 
247 | Permission is hereby granted, free of charge, to any person obtaining a copy
248 | of this software and associated documentation files (the "Software"), to deal
249 | in the Software without restriction, including without limitation the rights
250 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
251 | copies of the Software, and to permit persons to whom the Software is
252 | furnished to do so, subject to the following conditions:
253 | 
254 | The above copyright notice and this permission notice shall be included in all
255 | copies or substantial portions of the Software.
256 | 
257 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
258 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
259 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
260 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
261 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
262 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
263 | SOFTWARE.
264 | ```
265 | 
266 | 
267 | 


--------------------------------------------------------------------------------
/esp-flasher.js:
--------------------------------------------------------------------------------
  1 | 
  2 | /* Command defines */
  3 | const FLASH_BEGIN = 0x02;
  4 | const FLASH_DATA = 0x03;
  5 | const FLASH_END = 0x04;
  6 | const MEM_BEGIN = 0x05;
  7 | const MEM_END = 0x06;
  8 | const MEM_DATA = 0x07;
  9 | const SYNC = 0x08;
 10 | const WRITE_REG = 0x09;
 11 | const READ_REG = 0x0a;
 12 | const SPI_SET_PARAMS = 0x0b;
 13 | const SPI_ATTACH = 0x0d;
 14 | const CHANGE_BAUDRATE = 0x0f;
 15 | const FLASH_DEFL_BEGIN = 0x10;
 16 | const FLASH_DEFL_DATA = 0x11;
 17 | const FLASH_DEFL_END = 0x12;
 18 | const SPI_FLASH_MD5 = 0x13;
 19 | const GET_SECURITY_INFO = 0x14;
 20 | const ERASE_FLASH = 0xd0;
 21 | const ERASE_REGION = 0xd1;
 22 | const READ_FLASH = 0xd2;
 23 | const RUN_USER_CODE = 0xd3;
 24 | 
 25 | 
 26 | class SlipLayer {
 27 |     constructor() {
 28 |         this.buffer = [];
 29 |         this.escaping = false;
 30 |     }
 31 | 
 32 |     encode(packet) {
 33 |         const SLIP_END = 0xC0;
 34 |         const SLIP_ESC = 0xDB;
 35 |         const SLIP_ESC_END = 0xDC;
 36 |         const SLIP_ESC_ESC = 0xDD;
 37 | 
 38 |         let slipFrame = [SLIP_END];
 39 | 
 40 |         for (let byte of packet) {
 41 |             if (byte === SLIP_END) {
 42 |                 slipFrame.push(SLIP_ESC, SLIP_ESC_END);
 43 |             } else if (byte === SLIP_ESC) {
 44 |                 slipFrame.push(SLIP_ESC, SLIP_ESC_ESC);
 45 |             } else {
 46 |                 slipFrame.push(byte);
 47 |             }
 48 |         }
 49 | 
 50 |         slipFrame.push(SLIP_END);
 51 |         return new Uint8Array(slipFrame);
 52 |     }
 53 | 
 54 |     decode(value) {
 55 |         const SLIP_END = 0xC0;
 56 |         const SLIP_ESC = 0xDB;
 57 |         const SLIP_ESC_END = 0xDC;
 58 |         const SLIP_ESC_ESC = 0xDD;
 59 | 
 60 |         let outputPackets = [];
 61 | 
 62 |         for (let byte of value) {
 63 |             if (byte === SLIP_END) {
 64 |                 if (this.buffer.length > 0) {
 65 |                     outputPackets.push(new Uint8Array(this.buffer));
 66 |                     this.buffer = [];
 67 |                 }
 68 |             } else if (this.escaping) {
 69 |                 if (byte === SLIP_ESC_END) {
 70 |                     this.buffer.push(0xC0);
 71 |                 } else if (byte === SLIP_ESC_ESC) {
 72 |                     this.buffer.push(0xDB);
 73 |                 }
 74 |                 this.escaping = false;
 75 |             } else if (byte === SLIP_ESC) {
 76 |                 this.escaping = true;
 77 |             } else {
 78 |                 this.buffer.push(byte);
 79 |             }
 80 |         }
 81 | 
 82 |         return outputPackets;
 83 |     }
 84 | }
 85 | 
 86 | class ESPFlasher {
 87 | 
 88 |     constructor() {
 89 |         this.port = null;
 90 |         this.currentAddress = 0x0000;
 91 | 
 92 |         this.current_chip = "none";
 93 |         this.devMode = false;
 94 |         this.stubLoaded = false;
 95 |         this.responseHandlers = new Map();
 96 |         this.chip_magic_addr = 0x40001000;
 97 |         this.chip_descriptions =
 98 |         {
 99 |             "esp32s2": {
100 |                 "mac_efuse_reg": 0x3F41A044,
101 |                 "magic_value": 0x000007C6,
102 |                 "stub":
103 |                 {
104 |                     "entry": 1077381760,
105 |                     "text": "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",
106 |                     "text_start": 1077379072,
107 |                     "data": "XADKP16ON0AzjzdAR5Q3QL2PN0BTjzdAvY83QB2QN0A6kTdArJE3QFWRN0DpjTdA0JA3QCyRN0BAkDdA0JE3QGiQN0DQkTdAIY83QH6PN0C9jzdAHZA3QDmPN0AqjjdAkJI3QA2UN0AAjTdALZQ3QACNN0AAjTdAAI03QACNN0AAjTdAAI03QACNN0AAjTdAKpI3QACNN0AlkzdADZQ3QAQInwAAAAAAAAAYAQQIBQAAAAAAAAAIAQQIBgAAAAAAAAAAAQQIIQAAAAAAIAAAEQQI3AAAAAAAIAAAEQQIDAAAAAAAIAAAAQQIEgAAAAAAIAAAESAoDAAQAQAA",
108 |                     "data_start": 1070279676,
109 |                     "bss_start": 1070202880,
110 |                 }
111 |             },
112 |             "esp32s3": {
113 |                 "mac_efuse_reg": 0x60007044,
114 |                 "magic_value": 0x00000009,
115 |                 "stub":
116 |                 {
117 |                     "entry": 1077381760,
118 |                     "text": "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",
119 |                     "text_start": 1077379072,
120 |                     "data": "XADKP16ON0AzjzdAR5Q3QL2PN0BTjzdAvY83QB2QN0A6kTdArJE3QFWRN0DpjTdA0JA3QCyRN0BAkDdA0JE3QGiQN0DQkTdAIY83QH6PN0C9jzdAHZA3QDmPN0AqjjdAkJI3QA2UN0AAjTdALZQ3QACNN0AAjTdAAI03QACNN0AAjTdAAI03QACNN0AAjTdAKpI3QACNN0AlkzdADZQ3QAQInwAAAAAAAAAYAQQIBQAAAAAAAAAIAQQIBgAAAAAAAAAAAQQIIQAAAAAAIAAAEQQI3AAAAAAAIAAAEQQIDAAAAAAAIAAAAQQIEgAAAAAAIAAAESAoDAAQAQAA",
121 |                     "data_start": 1070279676,
122 |                     "bss_start": 1070202880
123 |                 }
124 |             },
125 |             "esp32c3": {
126 |                 "mac_efuse_reg": 0x60008844,
127 |                 "magic_value": [0x6921506F, 0x1B31506F, 0x4881606F, 0x4361606F],
128 |                 "stub":
129 |                 {
130 |                     "entry": 1077413584,
131 |                     "text": "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",
132 |                     "text_start": 1077411840,
133 |                     "data": "GEDIP8AKOEAQCzhAaAs4QDYMOECiDDhAUAw4QHIJOEDyCzhAMgw4QHwLOEAiCThAsAs4QCIJOECaCjhA4Ao4QBALOEBoCzhArAo4QNYJOEAgCjhAqAo4QPoOOEAQCzhAug04QLIOOEBiCDhA2g44QGIIOEBiCDhAYgg4QGIIOEBiCDhAYgg4QGIIOEBiCDhAVg04QGIIOEDYDThAsg44QA==",
134 |                     "data_start": 1070164916,
135 |                     "bss_start": 1070088192
136 |                 }
137 |             },
138 |             "esp32c6": {
139 |                 "mac_efuse_reg": 0x600B0844,
140 |                 "magic_value": 0x2CE0806F,
141 |                 "stub":
142 |                 {
143 |                     "entry": 1082132164,
144 |                     "text": "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",
145 |                     "text_start": 1082130432,
146 |                     "data": "FACEQHIKgEDCCoBAGguAQOgLgEBUDIBAAgyAQD4JgECkC4BA5AuAQC4LgEDuCIBAYguAQO4IgEBMCoBAkgqAQMIKgEAaC4BAXgqAQKIJgEDSCYBAWgqAQKwOgEDCCoBAbA2AQGQOgEAuCIBAjA6AQC4IgEAuCIBALgiAQC4IgEAuCIBALgiAQC4IgEAuCIBACA2AQC4IgECKDYBAZA6AQA==",
147 |                     "data_start": 1082469296,
148 |                     "bss_start": 1082392576
149 |                 }
150 |             }
151 |         }
152 | 
153 |         this.buffer = [];
154 |         this.escaping = false;
155 |         this.slipLayer = new SlipLayer();
156 | 
157 |         this.logDebug = () => { };
158 |         this.logError = () => { };
159 |     }
160 | 
161 |     async openPort() {
162 |         return new Promise(async (resolve, reject) => {
163 | 
164 |             /* Request a port and open a connection */
165 |             try {
166 |                 this.port = await navigator.serial.requestPort();
167 |                 await this.port.open({ baudRate: 921600 });
168 |             } catch (error) {
169 |                 reject(error);
170 |                 return;
171 |             }
172 | 
173 |             // Register for device lost
174 |             navigator.serial.addEventListener('disconnect', (event) => {
175 |                 if (event.target === this.port) {
176 |                     logError(`The device was disconnected`);
177 |                     this.disconnect();
178 |                 }
179 |             });
180 | 
181 |             // Register for port closing
182 |             this.port.addEventListener('close', () => {
183 |                 logError('Serial port closed');
184 |                 this.disconnect();
185 |             });
186 | 
187 |             resolve();
188 | 
189 |             /* Set up reading from the port */
190 |             const reader = this.port.readable.getReader();
191 | 
192 |             while (true) {
193 |                 const { value, done } = await reader.read();
194 |                 if (done) {
195 |                     reader.releaseLock();
196 |                     break;
197 |                 }
198 |                 if (value) {
199 |                     const packets = this.slipLayer.decode(value);
200 |                     for (let packet of packets) {
201 |                         await this.processPacket(packet);
202 |                     }
203 |                 }
204 |             }
205 |         });
206 |     }
207 | 
208 |     async readReg(addr) {
209 |         return this.executeCommand(this.buildCommandPacketU32(READ_REG, addr),
210 |             async (resolve, reject, responsePacket) => {
211 |                 if (responsePacket) {
212 |                     resolve(responsePacket.value);
213 |                 } else {
214 |                     reject('Failed to read register');
215 |                 }
216 |             });
217 |     }
218 | 
219 |     async executeCommand(packet, callback, default_callback, timeout = 100) {
220 |         if (!this.port || !this.port.writable) {
221 |             throw new Error("Port is not writable.");
222 |         }
223 | 
224 |         var pkt = this.parsePacket(packet.payload);
225 |         this.dumpPacket(pkt);
226 | 
227 |         const responsePromise = new Promise((resolve, reject) => {
228 | 
229 |             this.responseHandlers.clear();
230 |             this.responseHandlers.set(packet.command, async (response) => {
231 |                 if (callback) {
232 |                     return callback(resolve, reject, response);
233 |                 }
234 |             });
235 | 
236 |             if (default_callback) {
237 |                 this.responseHandlers.set(-1, async (response) => {
238 |                     return default_callback(resolve, reject, response);
239 |                 });
240 |             }
241 | 
242 |             setTimeout(() => {
243 |                 reject(new Error(`Timeout after ${timeout} ms waiting for response to command ${packet.command}`));
244 |             }, timeout);
245 |         });
246 | 
247 |         // Send the packet
248 |         const writer = this.port.writable.getWriter();
249 |         const slipFrame = this.slipLayer.encode(packet.payload);
250 |         await writer.write(slipFrame);
251 |         writer.releaseLock();
252 | 
253 |         return responsePromise;
254 |     }
255 | 
256 |     async disconnect() {
257 | 
258 |         if (this.port) {
259 |             try {
260 |                 if (this.port.readable) {
261 |                     this.port.readable.cancel();
262 |                 }
263 |                 if (this.port.writable) {
264 |                     this.port.writable.getWriter().releaseLock();
265 |                 }
266 |                 this.port.close();
267 |             } catch (error) {
268 |                 this.logError('Error during disconnect:', error);
269 |             }
270 |             this.port = null;
271 |         }
272 | 
273 |         this.disconnected && this.disconnected();
274 |     }
275 | 
276 | 
277 |     /**
278 |      * Attempts to put the ESP device into bootloader mode using RTS/DTR signals.
279 |      * Relies on the common DTR=EN, RTS=GPIO0 circuit. May not work on all boards.
280 |      * @returns {Promise} True if the sequence was sent, false if an error occurred (e.g., signals not supported).
281 |      */
282 |     async hardReset(bootloader = true) {
283 |         if (!this.port) {
284 |             this.logError("Port is not open. Cannot set signals.");
285 |             return false;
286 |         }
287 | 
288 |         this.logDebug("Automatic bootloader reset sequence...");
289 | 
290 |         try {
291 |             await this.port.setSignals({
292 |                 dataTerminalReady: false,
293 |                 requestToSend: false,
294 |             });
295 |             await this.port.setSignals({
296 |                 dataTerminalReady: bootloader,
297 |                 requestToSend: true,
298 |             });
299 |             await this.port.setSignals({
300 |                 dataTerminalReady: false,
301 |                 requestToSend: bootloader,
302 |             });
303 |             await new Promise((resolve) => setTimeout(resolve, 100));
304 | 
305 |             return true;
306 |         } catch (error) {
307 |             this.logError(`Could not set signals for automatic reset: ${error}. Please ensure device is in bootloader mode manually.`);
308 |             return false;
309 |         }
310 |     }
311 | 
312 | 
313 |     base64ToByteArray(base64) {
314 |         const binaryString = atob(base64);
315 |         const byteArray = new Uint8Array(binaryString.length);
316 |         for (let index = 0; index < binaryString.length; index++) {
317 |             byteArray[index] = binaryString.charCodeAt(index);
318 |         }
319 |         return byteArray;
320 |     }
321 | 
322 |     async downloadMem(address, payload) {
323 |         var binary = this.base64ToByteArray(payload);
324 | 
325 |         await this.executeCommand(this.buildCommandPacketU32(MEM_BEGIN, binary.length, 1, binary.length, address),
326 |             async (resolve, reject, responsePacket) => {
327 |                 resolve();
328 |             });
329 |         await this.executeCommand(this.buildCommandPacketU32(MEM_DATA, binary.length, 0, 0, 0, binary),
330 |             async (resolve, reject, responsePacket) => {
331 |                 resolve();
332 |             });
333 |     }
334 | 
335 |     async sync() {
336 |         const maxRetries = 10;
337 |         const retryDelayMs = 100; // Delay between retries
338 |         const syncTimeoutMs = 250; // Timeout for each individual sync attempt
339 |         let synchronized = false;
340 | 
341 |         this.logDebug(`Attempting to synchronize (${maxRetries} attempts)...`);
342 | 
343 |         const syncData = new Uint8Array([0x07, 0x07, 0x12, 0x20, ...Array(32).fill(0x55)]);
344 |         const syncPacket = this.buildCommandPacket(SYNC, syncData);
345 | 
346 |         for (let attempt = 1; attempt <= maxRetries; attempt++) {
347 |             this.logDebug(`Sync attempt ${attempt}...`);
348 |             try {
349 |                 await this.executeCommand(
350 |                     syncPacket,
351 |                     async (resolve, reject, responsePacket) => {
352 |                         // The ROM bootloader responds to SYNC with 0x08 0x00 status - check value maybe?
353 |                         // For now, just receiving *any* response to SYNC is considered success here.
354 |                         // If the command times out, the catch block below handles it.
355 |                         resolve(); // Signal success for this attempt
356 |                     },
357 |                     null, // No default callback needed here
358 |                     syncTimeoutMs // Use a specific timeout for sync
359 |                 );
360 | 
361 |                 // If executeCommand resolved without throwing/rejecting:
362 |                 this.logDebug(`Synchronized successfully on attempt ${attempt}.`);
363 |                 synchronized = true;
364 |                 break; // Exit the retry loop on success
365 | 
366 |             } catch (error) {
367 |                 this.logDebug(`Sync attempt ${attempt} failed: ${error.message}`);
368 |                 if (attempt === maxRetries) {
369 |                     this.logError(`Failed to synchronize after ${maxRetries} attempts.`);
370 |                     // Throw an error to indicate overall failure of the sync process
371 |                     throw new Error(`Failed to synchronize with device after ${maxRetries} attempts.`);
372 |                 }
373 |                 // Wait before the next retry
374 |                 await new Promise(resolve => setTimeout(resolve, retryDelayMs));
375 |             }
376 |         }
377 | 
378 |         // This part only runs if synchronized was set to true
379 |         if (!synchronized) {
380 |             // This should technically not be reached if the error is thrown above,
381 |             // but adding as a safeguard.
382 |             throw new Error("Synchronization failed (unexpected state).");
383 |         }
384 | 
385 |         // --- Chip Detection (Runs only after successful sync) ---
386 |         this.logDebug("Reading chip magic value...");
387 |         let currentValue;
388 |         try {
389 |             // Use a slightly longer timeout for register reads if needed
390 |             currentValue = await this.readReg(this.chip_magic_addr);
391 |         } catch (readError) {
392 |             this.logError(`Failed to read magic value after sync: ${readError}`);
393 |             throw new Error(`Successfully synced, but failed to read chip magic value: ${readError.message}`);
394 |         }
395 | 
396 | 
397 |         /* Function to check if the value matches any of the magic values */
398 |         const isMagicValue = (stub, value) => {
399 |             if (Array.isArray(stub.magic_value)) {
400 |                 return stub.magic_value.includes(value);
401 |             } else {
402 |                 return stub.magic_value === value;
403 |             }
404 |         };
405 | 
406 |         let chipDetected = false;
407 |         /* Iterate through each stub in the object */
408 |         for (const desc in this.chip_descriptions) {
409 |             if (this.chip_descriptions.hasOwnProperty(desc)) {
410 |                 const checkStub = this.chip_descriptions[desc];
411 |                 if (isMagicValue(checkStub, currentValue)) {
412 |                     this.logDebug(`Detected Chip: ${desc} (Magic: 0x${currentValue.toString(16)})`);
413 |                     this.current_chip = desc;
414 |                     chipDetected = true;
415 |                     break; // Found the chip
416 |                 }
417 |             }
418 |         }
419 | 
420 |         if (!chipDetected) {
421 |             this.logError(`Synced, but chip magic value 0x${currentValue.toString(16)} is unknown.`);
422 |             this.current_chip = "unknown"; // Mark as unknown
423 |             // Depending on requirements, you might want to throw an error here
424 |             // throw new Error(`Synced, but failed to identify chip type (Magic: 0x${currentValue.toString(16)}).`);
425 |         }
426 | 
427 |         // If we reached here without throwing, sync and detection (or lack thereof) is complete.
428 |         // The function implicitly returns a resolved promise.
429 |     }
430 | 
431 |     async readMac() {
432 | 
433 |         // Read the MAC address registers
434 |         var chip = this.chip_descriptions[this.current_chip];
435 |         const register1 = await flasher.readReg(chip.mac_efuse_reg);
436 |         const register2 = await flasher.readReg(chip.mac_efuse_reg + 4);
437 | 
438 |         if (!register1 || !register2) {
439 |             return;
440 |         }
441 | 
442 |         const lower = (register1 >>> 0);
443 |         const higher = (register2 >>> 0) & 0xFFFF;
444 | 
445 |         // Construct MAC address from register values
446 |         const macBytes = new Uint8Array(6);
447 |         macBytes[0] = (higher >> 8) & 0xFF;
448 |         macBytes[1] = higher & 0xFF;
449 |         macBytes[2] = (lower >> 24) & 0xFF;
450 |         macBytes[3] = (lower >> 16) & 0xFF;
451 |         macBytes[4] = (lower >> 8) & 0xFF;
452 |         macBytes[5] = lower & 0xFF;
453 | 
454 |         function toHex(byte) {
455 |             const hexString = byte.toString(16);
456 |             return hexString.length === 1 ? '0' + hexString : hexString;
457 |         }
458 |         const mac = Array.from(macBytes)
459 |             .map(byte => toHex(byte))
460 |             .join(':');
461 | 
462 |         return mac;
463 |     }
464 | 
465 |     async testReliability(cbr) {
466 | 
467 |         var chip = this.chip_descriptions[this.current_chip];
468 |         var reference = 0;
469 | 
470 |         try {
471 |             reference = await this.executeCommand(this.buildCommandPacketU32(READ_REG, chip.mac_efuse_reg),
472 |                 async (resolve, reject, responsePacket) => {
473 |                     if (responsePacket) {
474 |                         resolve(responsePacket.value);
475 |                     } else {
476 |                         this.logError(`Test read failed`);
477 |                         reject(`Test read failed`);
478 |                     }
479 |                 });
480 |         } catch (error) {
481 |             this.logError(`Test read failed due to an error: ${error.message}`);
482 |             return false;
483 |         }
484 | 
485 |         var duration = 1000;
486 |         const endTime = Date.now() + duration;
487 | 
488 |         let totalReads = 0;
489 |         let totalTime = 0;
490 | 
491 |         while (Date.now() < endTime) {
492 |             try {
493 |                 const startTime = Date.now();
494 | 
495 |                 var testread = await this.executeCommand(this.buildCommandPacketU32(READ_REG, chip.mac_efuse_reg),
496 |                     async (resolve, reject, responsePacket) => {
497 |                         if (responsePacket) {
498 |                             resolve(responsePacket.value);
499 |                         } else {
500 |                             reject(`Test read failed`);
501 |                         }
502 |                     });
503 | 
504 |                 const endTimeRead = Date.now();
505 |                 const readDuration = endTimeRead - startTime;
506 | 
507 |                 totalTime += readDuration;
508 |                 totalReads++;
509 | 
510 |                 /* Update the progress bar */
511 |                 const elapsed = Date.now() - (endTime - duration); // duration is the total time period (change to 30000 for 30 seconds)
512 |                 const progressPercentage = Math.min(100, (elapsed / duration) * 100); // Cap at 100%
513 | 
514 |                 cbr && cbr(progressPercentage);
515 | 
516 |                 /* Check if the read value differs from the reference */
517 |                 if (testread !== reference) {
518 |                     this.logError(`Test read failed! Expected: 0x${reference.toString(16).padStart(8, '0')}, but got: 0x${testread.toString(16).padStart(8, '0')}`);
519 |                     break;
520 |                 }
521 |             } catch (error) {
522 |                 this.logError(`Test read failed due to an error: ${error.message}`);
523 |                 return false;
524 |             }
525 |         }
526 | 
527 |         if (totalReads > 0) {
528 |             const averageTime = totalTime / totalReads;
529 |             this.logDebug(`Average read time: ${averageTime.toFixed(2)} ms over ${totalReads} reads.`);
530 |         }
531 | 
532 |         return true;
533 |     }
534 | 
535 |     async downloadStub() {
536 | 
537 |         var stub = this.chip_descriptions[this.current_chip].stub
538 | 
539 |         await this.downloadMem(stub.text_start, stub.text);
540 |         await this.downloadMem(stub.data_start, stub.data);
541 | 
542 |         try {
543 |             await this.executeCommand(this.buildCommandPacketU32(MEM_END, 0, stub.entry),
544 |                 async (resolve, reject, responsePacket) => {
545 |                     console.log("Final MEM_END ACK");
546 |                 },
547 |                 async (resolve, reject, rawData) => {
548 |                     const decoder = new TextDecoder('utf-8');
549 |                     const responseData = decoder.decode(rawData);
550 | 
551 |                     if (responseData == "OHAI") {
552 |                         this.logDebug(`Stub loader executed successfully(received ${responseData})`);
553 |                         this.stubLoaded = true;
554 |                         resolve();
555 |                     }
556 |                 });
557 |         } catch (error) {
558 |             this.logError("Failed to execute stub - is the device locked?");
559 |             return false;
560 |         }
561 | 
562 |         await this.executeCommand(this.buildCommandPacketU32(SPI_SET_PARAMS, 0, 0x800000, 64 * 1024, 4 * 1024, 256, 0xFFFF), async (resolve, reject, responsePacket) => {
563 |             console.log("SPI_SET_PARAMS", responsePacket);
564 |             resolve();
565 |         });
566 | 
567 |         return true;
568 |     }
569 | 
570 |     async writeFlash(address, data, progressCallback) {
571 | 
572 |         const MAX_PACKET_SIZE = 1024;
573 |         const packets = Math.ceil(data.length / MAX_PACKET_SIZE);
574 | 
575 |         /* Send FLASH_BEGIN command with the total data size */
576 |         await this.executeCommand(
577 |             this.buildCommandPacketU32(FLASH_BEGIN, data.length, packets,
578 |                 Math.min(MAX_PACKET_SIZE, data.length),
579 |                 address, this.stubLoaded ? undefined : 0
580 |             ),
581 |             async (resolve) => {
582 |                 resolve();
583 |             }
584 |         );
585 | 
586 |         /* Split data into chunks and send FLASH_DATA commands */
587 |         var seq = 0;
588 |         for (let offset = 0; offset < data.length; offset += MAX_PACKET_SIZE) {
589 |             const chunk = data.slice(offset, offset + MAX_PACKET_SIZE);
590 | 
591 |             /* Four 32-bit words: data size, sequence number, 0, 0, then data. Uses Checksum. */
592 |             await this.executeCommand(
593 |                 this.buildCommandPacketU32(FLASH_DATA, chunk.length, seq++, 0, 0, chunk),
594 |                 async (resolve) => {
595 |                     resolve();
596 |                 },
597 |                 null,
598 |                 1000
599 |             );
600 |             if (progressCallback) {
601 |                 progressCallback(offset, data.length);
602 |             }
603 |         }
604 |     }
605 | 
606 |     async readFlash(address, blockSize = 0x100) {
607 |         let packet = 0;
608 |         var data = {};
609 | 
610 |         if (blockSize > 0x1000) {
611 |             blockSize = 0x1000;
612 |         }
613 | 
614 |         return this.executeCommand(
615 |             this.buildCommandPacketU32(READ_FLASH, address, blockSize, 0x1000, 1),
616 |             async (resolve, reject, responsePacket) => {
617 |                 packet = 0;
618 |             },
619 |             async (resolve, reject, rawData) => {
620 |                 if (packet == 0) {
621 |                     data = rawData;
622 | 
623 |                     /* Prepare response */
624 |                     var resp = new Uint8Array(4);
625 |                     resp[0] = (blockSize >> 0) & 0xFF;
626 |                     resp[1] = (blockSize >> 8) & 0xFF;
627 |                     resp[2] = (blockSize >> 16) & 0xFF;
628 |                     resp[3] = (blockSize >> 24) & 0xFF;
629 | 
630 |                     /* Encode and write response */
631 |                     const slipFrame = this.slipLayer.encode(resp);
632 | 
633 |                     const writer = this.port.writable.getWriter();
634 |                     await writer.write(slipFrame);
635 |                     await writer.releaseLock();
636 |                 } else if (packet == 1) {
637 |                     resolve(data);
638 |                 }
639 |                 packet++;
640 |             },
641 |             1000
642 |         );
643 |     }
644 | 
645 |     async blankCheck(cbr) {
646 |         const startAddress = 0x000000;
647 |         const endAddress = 0x800000;
648 |         const blockSize = 0x200;
649 | 
650 |         let totalReads = 0;
651 |         let totalTime = 0;
652 |         let erasedBytesTotal = 0;
653 |         let currentAddress = startAddress;
654 | 
655 |         while (currentAddress < endAddress) {
656 | 
657 |             try {
658 |                 const startTime = Date.now();
659 |                 var rawData = await this.readFlash(currentAddress, blockSize);
660 |                 const endTimeRead = Date.now();
661 |                 const readDuration = endTimeRead - startTime;
662 | 
663 |                 var erasedBytes = 0;
664 |                 for (var pos = 0; pos < rawData.length; pos++) {
665 |                     if (rawData[pos] == 0xFF) {
666 |                         erasedBytes++;
667 |                     }
668 |                 }
669 | 
670 |                 currentAddress += rawData.length;
671 |                 erasedBytesTotal += erasedBytes;
672 |                 totalTime += readDuration;
673 |                 totalReads++;
674 | 
675 |                 cbr && cbr(currentAddress, startAddress, endAddress, blockSize, erasedBytes, erasedBytesTotal);
676 |             } catch (error) {
677 |                 this.logError(`Test read failed due to an error: ${error.message}`);
678 |                 this.disconnect();
679 |                 break;
680 |             }
681 |         }
682 | 
683 |         if (totalReads > 0) {
684 |             const averageTime = totalTime / totalReads;
685 |             this.logDebug(`Average read time: ${averageTime.toFixed(2)} ms over ${totalReads} reads.`);
686 |         }
687 |     }
688 | 
689 |     buildCommandPacketU32(command, ...values) {
690 |         /* Calculate total length for data */
691 |         let totalLength = 0;
692 |         values.forEach(value => {
693 |             if (typeof value === 'number') {
694 |                 totalLength += 4; // uint32 is 4 bytes
695 |             } else if (value instanceof Uint8Array) {
696 |                 totalLength += value.length;
697 |             }
698 |         });
699 | 
700 |         /* Convert each uint32_t to little-endian bytes or append byte arrays */
701 |         const data = new Uint8Array(totalLength);
702 |         let offset = 0;
703 |         values.forEach(value => {
704 |             if (typeof value === 'number') {
705 |                 data[offset] = (value >> 0) & 0xFF;
706 |                 data[offset + 1] = (value >> 8) & 0xFF;
707 |                 data[offset + 2] = (value >> 16) & 0xFF;
708 |                 data[offset + 3] = (value >> 24) & 0xFF;
709 |                 offset += 4;
710 |             } else if (value instanceof Uint8Array) {
711 |                 data.set(value, offset);
712 |                 offset += value.length;
713 |             }
714 |         });
715 | 
716 |         /* Call the original function with the constructed data */
717 |         return this.buildCommandPacket(command, data);
718 |     }
719 | 
720 |     buildCommandPacket(command, data) {
721 |         /* Construct command packet */
722 |         const direction = 0x00;
723 |         const size = data.length;
724 |         let checksum = 0;
725 | 
726 |         if (size > 32) {
727 |             checksum = 0xEF;
728 |             for (let index = 16; index < size; index++) {
729 |                 checksum ^= data[index];
730 |             }
731 |         }
732 | 
733 |         const packet = new Uint8Array(8 + size);
734 |         packet[0] = direction;
735 |         packet[1] = command;
736 |         packet[2] = size & 0xff;
737 |         packet[3] = (size >> 8) & 0xff;
738 |         packet[4] = checksum & 0xff;
739 |         packet[5] = (checksum >> 8) & 0xff;
740 |         packet[6] = (checksum >> 16) & 0xff;
741 |         packet[7] = (checksum >> 24) & 0xff;
742 |         packet.set(data, 8);
743 | 
744 |         var ret = {};
745 | 
746 |         ret.command = command;
747 |         ret.payload = packet;
748 | 
749 |         return ret;
750 |     }
751 | 
752 |     dumpPacket(pkt) {
753 | 
754 |         if (!this.devMode) {
755 |             return;
756 |         }
757 |         if (pkt.dir == 0) {
758 |             console.log(`Command: `, pkt);
759 |             console.log(`Command raw: ${Array.from(pkt.raw).map(byte => byte.toString(16).padStart(2, '0')).join(' ')}`);
760 |         }
761 |         if (pkt.dir == 1) {
762 |             console.log(`Response: `, pkt);
763 |             console.log(`Response raw: ${Array.from(pkt.raw).map(byte => byte.toString(16).padStart(2, '0')).join(' ')}`);
764 |         }
765 |     }
766 | 
767 |     parsePacket(packet) {
768 |         var pkt = {};
769 | 
770 |         pkt.dir = packet[0];
771 |         pkt.command = packet[1];
772 |         pkt.size = packet[2] | (packet[3] << 8);
773 |         pkt.value = (packet[4] | (packet[5] << 8) | (packet[6] << 16) | (packet[7] << 24)) >>> 0;
774 | 
775 |         if (pkt.dir > 2 || packet.length != 8 + pkt.size) {
776 |             return null;
777 |         }
778 |         pkt.data = packet.slice(8, 8 + pkt.size);
779 |         pkt.raw = packet;
780 | 
781 |         return pkt;
782 |     }
783 | 
784 |     async processPacket(packet) {
785 |         var pkt = this.parsePacket(packet);
786 | 
787 |         if (pkt && pkt.dir === 0x01) {
788 | 
789 |             this.dumpPacket(pkt);
790 |             /* Call response handler if registered */
791 |             if (this.responseHandlers.has(pkt.command)) {
792 |                 var handler = this.responseHandlers.get(pkt.command);
793 |                 await handler(pkt);
794 |             }
795 |         } else {
796 |             //console.log(`Received raw SLIP: ${ Array.from(packet).map(byte => byte.toString(16).padStart(2, '0')).join(' ') }`);
797 | 
798 |             if (this.responseHandlers.has(-1)) {
799 |                 var handler = this.responseHandlers.get(-1);
800 |                 await handler(packet);
801 |             }
802 |         }
803 |     }
804 | }
805 | 


--------------------------------------------------------------------------------