├── 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 |
--------------------------------------------------------------------------------