esp32tool 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (115) hide show
  1. package/README.md +31 -0
  2. package/css/dark.css +156 -0
  3. package/css/light.css +156 -0
  4. package/css/style.css +870 -0
  5. package/dist/const.d.ts +277 -0
  6. package/dist/const.js +511 -0
  7. package/dist/esp_loader.d.ts +222 -0
  8. package/dist/esp_loader.js +1466 -0
  9. package/dist/index.d.ts +10 -0
  10. package/dist/index.js +15 -0
  11. package/dist/lib/spiffs/index.d.ts +15 -0
  12. package/dist/lib/spiffs/index.js +16 -0
  13. package/dist/lib/spiffs/spiffs.d.ts +26 -0
  14. package/dist/lib/spiffs/spiffs.js +132 -0
  15. package/dist/lib/spiffs/spiffsBlock.d.ts +36 -0
  16. package/dist/lib/spiffs/spiffsBlock.js +140 -0
  17. package/dist/lib/spiffs/spiffsConfig.d.ts +63 -0
  18. package/dist/lib/spiffs/spiffsConfig.js +79 -0
  19. package/dist/lib/spiffs/spiffsPage.d.ts +45 -0
  20. package/dist/lib/spiffs/spiffsPage.js +260 -0
  21. package/dist/lib/spiffs/spiffsReader.d.ts +19 -0
  22. package/dist/lib/spiffs/spiffsReader.js +192 -0
  23. package/dist/partition.d.ts +26 -0
  24. package/dist/partition.js +129 -0
  25. package/dist/struct.d.ts +2 -0
  26. package/dist/struct.js +91 -0
  27. package/dist/stubs/esp32.json +8 -0
  28. package/dist/stubs/esp32c2.json +8 -0
  29. package/dist/stubs/esp32c3.json +8 -0
  30. package/dist/stubs/esp32c5.json +8 -0
  31. package/dist/stubs/esp32c6.json +8 -0
  32. package/dist/stubs/esp32c61.json +8 -0
  33. package/dist/stubs/esp32h2.json +8 -0
  34. package/dist/stubs/esp32p4.json +8 -0
  35. package/dist/stubs/esp32p4r3.json +8 -0
  36. package/dist/stubs/esp32s2.json +8 -0
  37. package/dist/stubs/esp32s3.json +8 -0
  38. package/dist/stubs/esp8266.json +8 -0
  39. package/dist/stubs/index.d.ts +10 -0
  40. package/dist/stubs/index.js +56 -0
  41. package/dist/util.d.ts +14 -0
  42. package/dist/util.js +46 -0
  43. package/dist/wasm/filesystems.d.ts +33 -0
  44. package/dist/wasm/filesystems.js +114 -0
  45. package/dist/web/esp32-D955RjN9.js +16 -0
  46. package/dist/web/esp32c2-CJkxHDQi.js +16 -0
  47. package/dist/web/esp32c3-BhUHzH0o.js +16 -0
  48. package/dist/web/esp32c5-Chs0HtmA.js +16 -0
  49. package/dist/web/esp32c6-D6mPN6ut.js +16 -0
  50. package/dist/web/esp32c61-CQiYCWAs.js +16 -0
  51. package/dist/web/esp32h2-LsKJE9AS.js +16 -0
  52. package/dist/web/esp32p4-7nWC-HiD.js +16 -0
  53. package/dist/web/esp32p4r3-CwiPecZW.js +16 -0
  54. package/dist/web/esp32s2-CtqVheSJ.js +16 -0
  55. package/dist/web/esp32s3-CRbtB0QR.js +16 -0
  56. package/dist/web/esp8266-nEkNAo8K.js +16 -0
  57. package/dist/web/index.js +7265 -0
  58. package/electron/main.js +333 -0
  59. package/electron/preload.js +37 -0
  60. package/eslint.config.js +22 -0
  61. package/index.html +408 -0
  62. package/js/modules/esp32-D955RjN9.js +16 -0
  63. package/js/modules/esp32c2-CJkxHDQi.js +16 -0
  64. package/js/modules/esp32c3-BhUHzH0o.js +16 -0
  65. package/js/modules/esp32c5-Chs0HtmA.js +16 -0
  66. package/js/modules/esp32c6-D6mPN6ut.js +16 -0
  67. package/js/modules/esp32c61-CQiYCWAs.js +16 -0
  68. package/js/modules/esp32h2-LsKJE9AS.js +16 -0
  69. package/js/modules/esp32p4-7nWC-HiD.js +16 -0
  70. package/js/modules/esp32p4r3-CwiPecZW.js +16 -0
  71. package/js/modules/esp32s2-CtqVheSJ.js +16 -0
  72. package/js/modules/esp32s3-CRbtB0QR.js +16 -0
  73. package/js/modules/esp8266-nEkNAo8K.js +16 -0
  74. package/js/modules/esptool.js +7265 -0
  75. package/js/script.js +2237 -0
  76. package/js/utilities.js +182 -0
  77. package/license.md +11 -0
  78. package/package.json +61 -0
  79. package/script/build +12 -0
  80. package/script/develop +17 -0
  81. package/src/const.ts +599 -0
  82. package/src/esp_loader.ts +1907 -0
  83. package/src/index.ts +63 -0
  84. package/src/lib/spiffs/index.ts +22 -0
  85. package/src/lib/spiffs/spiffs.ts +175 -0
  86. package/src/lib/spiffs/spiffsBlock.ts +204 -0
  87. package/src/lib/spiffs/spiffsConfig.ts +140 -0
  88. package/src/lib/spiffs/spiffsPage.ts +357 -0
  89. package/src/lib/spiffs/spiffsReader.ts +280 -0
  90. package/src/partition.ts +155 -0
  91. package/src/struct.ts +108 -0
  92. package/src/stubs/README.md +3 -0
  93. package/src/stubs/esp32.json +8 -0
  94. package/src/stubs/esp32c2.json +8 -0
  95. package/src/stubs/esp32c3.json +8 -0
  96. package/src/stubs/esp32c5.json +8 -0
  97. package/src/stubs/esp32c6.json +8 -0
  98. package/src/stubs/esp32c61.json +8 -0
  99. package/src/stubs/esp32h2.json +8 -0
  100. package/src/stubs/esp32p4.json +8 -0
  101. package/src/stubs/esp32p4r3.json +8 -0
  102. package/src/stubs/esp32s2.json +8 -0
  103. package/src/stubs/esp32s3.json +8 -0
  104. package/src/stubs/esp8266.json +8 -0
  105. package/src/stubs/index.ts +86 -0
  106. package/src/util.ts +49 -0
  107. package/src/wasm/fatfs/fatfs.wasm +0 -0
  108. package/src/wasm/fatfs/index.d.ts +26 -0
  109. package/src/wasm/fatfs/index.js +343 -0
  110. package/src/wasm/filesystems.ts +156 -0
  111. package/src/wasm/littlefs/index.d.ts +83 -0
  112. package/src/wasm/littlefs/index.js +529 -0
  113. package/src/wasm/littlefs/littlefs.js +2 -0
  114. package/src/wasm/littlefs/littlefs.wasm +0 -0
  115. package/src/wasm/shared/types.ts +13 -0
@@ -0,0 +1,1907 @@
1
+ /// <reference types="@types/w3c-web-serial" />
2
+
3
+ import {
4
+ CHIP_FAMILY_ESP32,
5
+ CHIP_FAMILY_ESP32S2,
6
+ CHIP_FAMILY_ESP32S3,
7
+ CHIP_FAMILY_ESP32C2,
8
+ CHIP_FAMILY_ESP32C3,
9
+ CHIP_FAMILY_ESP32C5,
10
+ CHIP_FAMILY_ESP32C6,
11
+ CHIP_FAMILY_ESP32C61,
12
+ CHIP_FAMILY_ESP32H2,
13
+ CHIP_FAMILY_ESP32H4,
14
+ CHIP_FAMILY_ESP32H21,
15
+ CHIP_FAMILY_ESP32P4,
16
+ CHIP_FAMILY_ESP32S31,
17
+ CHIP_FAMILY_ESP8266,
18
+ MAX_TIMEOUT,
19
+ Logger,
20
+ DEFAULT_TIMEOUT,
21
+ ERASE_REGION_TIMEOUT_PER_MB,
22
+ ESP_CHANGE_BAUDRATE,
23
+ ESP_CHECKSUM_MAGIC,
24
+ ESP_FLASH_BEGIN,
25
+ ESP_FLASH_DATA,
26
+ ESP_FLASH_END,
27
+ ESP_MEM_BEGIN,
28
+ ESP_MEM_DATA,
29
+ ESP_MEM_END,
30
+ ESP_READ_REG,
31
+ ESP_WRITE_REG,
32
+ ESP_SPI_ATTACH,
33
+ ESP_SYNC,
34
+ ESP_GET_SECURITY_INFO,
35
+ FLASH_SECTOR_SIZE,
36
+ FLASH_WRITE_SIZE,
37
+ STUB_FLASH_WRITE_SIZE,
38
+ MEM_END_ROM_TIMEOUT,
39
+ ROM_INVALID_RECV_MSG,
40
+ SYNC_PACKET,
41
+ SYNC_TIMEOUT,
42
+ USB_RAM_BLOCK,
43
+ ChipFamily,
44
+ ESP_ERASE_FLASH,
45
+ ESP_READ_FLASH,
46
+ CHIP_ERASE_TIMEOUT,
47
+ FLASH_READ_TIMEOUT,
48
+ timeoutPerMb,
49
+ ESP_ROM_BAUD,
50
+ USB_JTAG_SERIAL_PID,
51
+ ESP_FLASH_DEFL_BEGIN,
52
+ ESP_FLASH_DEFL_DATA,
53
+ ESP_FLASH_DEFL_END,
54
+ getSpiFlashAddresses,
55
+ SpiFlashAddresses,
56
+ DETECTED_FLASH_SIZES,
57
+ CHIP_DETECT_MAGIC_REG_ADDR,
58
+ CHIP_DETECT_MAGIC_VALUES,
59
+ CHIP_ID_TO_INFO,
60
+ ESP32P4_EFUSE_BLOCK1_ADDR,
61
+ SlipReadError,
62
+ } from "./const";
63
+ import { getStubCode } from "./stubs";
64
+ import { hexFormatter, sleep, slipEncode, toHex } from "./util";
65
+ // @ts-expect-error pako ESM module doesn't have proper type definitions
66
+ import { deflate } from "pako/dist/pako.esm.mjs";
67
+ import { pack, unpack } from "./struct";
68
+
69
+ export class ESPLoader extends EventTarget {
70
+ chipFamily!: ChipFamily;
71
+ chipName: string | null = null;
72
+ chipRevision: number | null = null;
73
+ chipVariant: string | null = null;
74
+ _efuses = new Array(4).fill(0);
75
+ _flashsize = 4 * 1024 * 1024;
76
+ debug = false;
77
+ IS_STUB = false;
78
+ connected = true;
79
+ flashSize: string | null = null;
80
+
81
+ __inputBuffer?: number[];
82
+ __totalBytesRead?: number;
83
+ private _currentBaudRate: number = ESP_ROM_BAUD;
84
+ private _maxUSBSerialBaudrate?: number;
85
+ private _reader?: ReadableStreamDefaultReader<Uint8Array>;
86
+ private _isESP32S2NativeUSB: boolean = false;
87
+ private _initializationSucceeded: boolean = false;
88
+
89
+ constructor(
90
+ public port: SerialPort,
91
+ public logger: Logger,
92
+ private _parent?: ESPLoader,
93
+ ) {
94
+ super();
95
+ }
96
+
97
+ private get _inputBuffer(): number[] {
98
+ return this._parent ? this._parent._inputBuffer : this.__inputBuffer!;
99
+ }
100
+
101
+ private get _totalBytesRead(): number {
102
+ return this._parent
103
+ ? this._parent._totalBytesRead
104
+ : this.__totalBytesRead || 0;
105
+ }
106
+
107
+ private set _totalBytesRead(value: number) {
108
+ if (this._parent) {
109
+ this._parent._totalBytesRead = value;
110
+ } else {
111
+ this.__totalBytesRead = value;
112
+ }
113
+ }
114
+
115
+ private detectUSBSerialChip(
116
+ vendorId: number,
117
+ productId: number,
118
+ ): { name: string; maxBaudrate?: number } {
119
+ // Common USB-Serial chip vendors and their products
120
+ const chips: Record<
121
+ number,
122
+ Record<number, { name: string; maxBaudrate?: number }>
123
+ > = {
124
+ 0x1a86: {
125
+ // QinHeng Electronics
126
+ 0x7522: { name: "CH340", maxBaudrate: 460800 },
127
+ 0x7523: { name: "CH340", maxBaudrate: 460800 },
128
+ 0x7584: { name: "CH340", maxBaudrate: 460800 },
129
+ 0x5523: { name: "CH341", maxBaudrate: 2000000 },
130
+ 0x55d3: { name: "CH343", maxBaudrate: 6000000 },
131
+ 0x55d4: { name: "CH9102", maxBaudrate: 6000000 },
132
+ 0x55d8: { name: "CH9101", maxBaudrate: 3000000 },
133
+ },
134
+ 0x10c4: {
135
+ // Silicon Labs
136
+ 0xea60: { name: "CP2102(n)", maxBaudrate: 3000000 },
137
+ 0xea70: { name: "CP2105", maxBaudrate: 2000000 },
138
+ 0xea71: { name: "CP2108", maxBaudrate: 2000000 },
139
+ },
140
+ 0x0403: {
141
+ // FTDI
142
+ 0x6001: { name: "FT232R", maxBaudrate: 3000000 },
143
+ 0x6010: { name: "FT2232", maxBaudrate: 3000000 },
144
+ 0x6011: { name: "FT4232", maxBaudrate: 3000000 },
145
+ 0x6014: { name: "FT232H", maxBaudrate: 12000000 },
146
+ 0x6015: { name: "FT230X", maxBaudrate: 3000000 },
147
+ },
148
+ 0x303a: {
149
+ // Espressif (native USB)
150
+ 0x2: { name: "ESP32-S2 Native USB", maxBaudrate: 2000000 },
151
+ 0x1001: { name: "ESP32 Native USB", maxBaudrate: 2000000 },
152
+ 0x1002: { name: "ESP32 Native USB", maxBaudrate: 2000000 },
153
+ 0x4002: { name: "ESP32 Native USB", maxBaudrate: 2000000 },
154
+ 0x1000: { name: "ESP32 Native USB", maxBaudrate: 2000000 },
155
+ },
156
+ };
157
+
158
+ const vendor = chips[vendorId];
159
+ if (vendor && vendor[productId]) {
160
+ return vendor[productId];
161
+ }
162
+
163
+ return {
164
+ name: `Unknown (VID: 0x${vendorId.toString(16)}, PID: 0x${productId.toString(16)})`,
165
+ };
166
+ }
167
+
168
+ async initialize() {
169
+ await this.hardReset(true);
170
+
171
+ if (!this._parent) {
172
+ this.__inputBuffer = [];
173
+ this.__totalBytesRead = 0;
174
+
175
+ // Detect and log USB-Serial chip info
176
+ const portInfo = this.port.getInfo();
177
+ if (portInfo.usbVendorId && portInfo.usbProductId) {
178
+ const chipInfo = this.detectUSBSerialChip(
179
+ portInfo.usbVendorId,
180
+ portInfo.usbProductId,
181
+ );
182
+ this.logger.log(
183
+ `USB-Serial: ${chipInfo.name} (VID: 0x${portInfo.usbVendorId.toString(16)}, PID: 0x${portInfo.usbProductId.toString(16)})`,
184
+ );
185
+ if (chipInfo.maxBaudrate) {
186
+ this._maxUSBSerialBaudrate = chipInfo.maxBaudrate;
187
+ this.logger.log(`Max baudrate: ${chipInfo.maxBaudrate}`);
188
+ }
189
+ // Detect ESP32-S2 Native USB
190
+ if (portInfo.usbVendorId === 0x303a && portInfo.usbProductId === 0x2) {
191
+ this._isESP32S2NativeUSB = true;
192
+ }
193
+ }
194
+
195
+ // Don't await this promise so it doesn't block rest of method.
196
+ this.readLoop();
197
+ }
198
+
199
+ // Clear buffer again after starting read loop
200
+ await this.flushSerialBuffers();
201
+ await this.sync();
202
+
203
+ // Detect chip type
204
+ await this.detectChip();
205
+
206
+ // Read the OTP data for this chip and store into this.efuses array
207
+ const FlAddr = getSpiFlashAddresses(this.getChipFamily());
208
+ const AddrMAC = FlAddr.macFuse;
209
+ for (let i = 0; i < 4; i++) {
210
+ this._efuses[i] = await this.readRegister(AddrMAC + 4 * i);
211
+ }
212
+ this.logger.log(`Chip type ${this.chipName}`);
213
+ this.logger.debug(
214
+ `Bootloader flash offset: 0x${FlAddr.flashOffs.toString(16)}`,
215
+ );
216
+
217
+ // Mark initialization as successful
218
+ this._initializationSucceeded = true;
219
+ }
220
+
221
+ /**
222
+ * Detect chip type using GET_SECURITY_INFO (for newer chips) or magic value (for older chips)
223
+ */
224
+ async detectChip() {
225
+ try {
226
+ // Try GET_SECURITY_INFO command first (ESP32-C3 and later)
227
+ const securityInfo = await this.getSecurityInfo();
228
+ const chipId = securityInfo.chipId;
229
+
230
+ const chipInfo = CHIP_ID_TO_INFO[chipId];
231
+ if (chipInfo) {
232
+ this.chipName = chipInfo.name;
233
+ this.chipFamily = chipInfo.family;
234
+
235
+ // Get chip revision for ESP32-P4
236
+ if (this.chipFamily === CHIP_FAMILY_ESP32P4) {
237
+ this.chipRevision = await this.getChipRevision();
238
+ this.logger.debug(`ESP32-P4 revision: ${this.chipRevision}`);
239
+
240
+ // Set chip variant based on revision
241
+ if (this.chipRevision >= 300) {
242
+ this.chipVariant = "rev300";
243
+ } else {
244
+ this.chipVariant = "rev0";
245
+ }
246
+ this.logger.debug(`ESP32-P4 variant: ${this.chipVariant}`);
247
+ }
248
+
249
+ this.logger.debug(
250
+ `Detected chip via IMAGE_CHIP_ID: ${chipId} (${this.chipName})`,
251
+ );
252
+ return;
253
+ }
254
+
255
+ this.logger.debug(
256
+ `Unknown IMAGE_CHIP_ID: ${chipId}, falling back to magic value detection`,
257
+ );
258
+ } catch (error) {
259
+ // GET_SECURITY_INFO not supported, fall back to magic value detection
260
+ this.logger.debug(
261
+ `GET_SECURITY_INFO failed, using magic value detection: ${error}`,
262
+ );
263
+
264
+ // Clear input buffer and re-sync to recover from failed command
265
+ this._inputBuffer.length = 0;
266
+ await sleep(SYNC_TIMEOUT);
267
+
268
+ // Re-sync with the chip to ensure clean communication
269
+ try {
270
+ await this.sync();
271
+ } catch (syncErr) {
272
+ this.logger.debug(
273
+ `Re-sync after GET_SECURITY_INFO failure: ${syncErr}`,
274
+ );
275
+ }
276
+ }
277
+
278
+ // Fallback: Use magic value detection for ESP8266, ESP32, ESP32-S2
279
+ const chipMagicValue = await this.readRegister(CHIP_DETECT_MAGIC_REG_ADDR);
280
+ const chip = CHIP_DETECT_MAGIC_VALUES[chipMagicValue >>> 0];
281
+ if (chip === undefined) {
282
+ throw new Error(
283
+ `Unknown Chip: Hex: ${toHex(
284
+ chipMagicValue >>> 0,
285
+ 8,
286
+ ).toLowerCase()} Number: ${chipMagicValue}`,
287
+ );
288
+ }
289
+ this.chipName = chip.name;
290
+ this.chipFamily = chip.family;
291
+
292
+ if (this.chipFamily === CHIP_FAMILY_ESP32P4) {
293
+ this.chipRevision = await this.getChipRevision();
294
+ this.logger.debug(`ESP32-P4 revision: ${this.chipRevision}`);
295
+
296
+ if (this.chipRevision >= 300) {
297
+ this.chipVariant = "rev300";
298
+ } else {
299
+ this.chipVariant = "rev0";
300
+ }
301
+ this.logger.debug(`ESP32-P4 variant: ${this.chipVariant}`);
302
+ }
303
+
304
+ this.logger.debug(
305
+ `Detected chip via magic value: ${toHex(chipMagicValue >>> 0, 8)} (${this.chipName})`,
306
+ );
307
+ }
308
+
309
+ /**
310
+ * Get chip revision for ESP32-P4
311
+ */
312
+ async getChipRevision(): Promise<number> {
313
+ if (this.chipFamily !== CHIP_FAMILY_ESP32P4) {
314
+ return 0;
315
+ }
316
+
317
+ // Read from EFUSE_BLOCK1 to get chip revision
318
+ // Word 2 contains revision info for ESP32-P4
319
+ const word2 = await this.readRegister(ESP32P4_EFUSE_BLOCK1_ADDR + 8);
320
+
321
+ // Minor revision: bits [3:0]
322
+ const minorRev = word2 & 0x0f;
323
+
324
+ // Major revision: bits [23] << 2 | bits [5:4]
325
+ const majorRev = (((word2 >> 23) & 1) << 2) | ((word2 >> 4) & 0x03);
326
+
327
+ // Revision is major * 100 + minor
328
+ return majorRev * 100 + minorRev;
329
+ }
330
+
331
+ /**
332
+ * Get security info including chip ID (ESP32-C3 and later)
333
+ */
334
+ async getSecurityInfo(): Promise<{
335
+ flags: number;
336
+ flashCryptCnt: number;
337
+ keyPurposes: number[];
338
+ chipId: number;
339
+ apiVersion: number;
340
+ }> {
341
+ const [, responseData] = await this.checkCommand(
342
+ ESP_GET_SECURITY_INFO,
343
+ [],
344
+ 0,
345
+ );
346
+
347
+ // Some chips/ROM versions return empty response or don't support this command
348
+ if (responseData.length === 0) {
349
+ throw new Error(
350
+ `GET_SECURITY_INFO not supported or returned empty response`,
351
+ );
352
+ }
353
+
354
+ if (responseData.length < 12) {
355
+ throw new Error(
356
+ `Invalid security info response length: ${responseData.length} (expected at least 12 bytes)`,
357
+ );
358
+ }
359
+
360
+ const flags = unpack("<I", responseData.slice(0, 4))[0];
361
+ const flashCryptCnt = responseData[4];
362
+ const keyPurposes = Array.from(responseData.slice(5, 12));
363
+ const chipId =
364
+ responseData.length >= 16
365
+ ? unpack("<I", responseData.slice(12, 16))[0]
366
+ : 0;
367
+ const apiVersion =
368
+ responseData.length >= 20
369
+ ? unpack("<I", responseData.slice(16, 20))[0]
370
+ : 0;
371
+
372
+ return {
373
+ flags,
374
+ flashCryptCnt,
375
+ keyPurposes,
376
+ chipId,
377
+ apiVersion,
378
+ };
379
+ }
380
+
381
+ /**
382
+ * @name readLoop
383
+ * Reads data from the input stream and places it in the inputBuffer
384
+ */
385
+ async readLoop() {
386
+ if (this.debug) {
387
+ this.logger.debug("Starting read loop");
388
+ }
389
+
390
+ this._reader = this.port.readable!.getReader();
391
+
392
+ try {
393
+ let keepReading = true;
394
+ while (keepReading) {
395
+ const { value, done } = await this._reader.read();
396
+ if (done) {
397
+ this._reader.releaseLock();
398
+ keepReading = false;
399
+ break;
400
+ }
401
+ if (!value || value.length === 0) {
402
+ continue;
403
+ }
404
+
405
+ // Always read from browser's serial buffer immediately
406
+ // to prevent browser buffer overflow. Don't apply back-pressure here.
407
+ const chunk = Array.from(value);
408
+ Array.prototype.push.apply(this._inputBuffer, chunk);
409
+
410
+ // Track total bytes read from serial port
411
+ this._totalBytesRead += value.length;
412
+ }
413
+ } catch {
414
+ this.logger.error("Read loop got disconnected");
415
+ }
416
+ // Disconnected!
417
+ this.connected = false;
418
+
419
+ // Check if this is ESP32-S2 Native USB that needs port reselection
420
+ // Only trigger reconnect if initialization did NOT succeed (wrong port)
421
+ if (this._isESP32S2NativeUSB && !this._initializationSucceeded) {
422
+ this.logger.log(
423
+ "ESP32-S2 Native USB detected - requesting port reselection",
424
+ );
425
+ this.dispatchEvent(
426
+ new CustomEvent("esp32s2-usb-reconnect", {
427
+ detail: { message: "ESP32-S2 Native USB requires port reselection" },
428
+ }),
429
+ );
430
+ }
431
+
432
+ this.dispatchEvent(new Event("disconnect"));
433
+ this.logger.debug("Finished read loop");
434
+ }
435
+
436
+ sleep(ms = 100) {
437
+ return new Promise((resolve) => setTimeout(resolve, ms));
438
+ }
439
+
440
+ state_DTR = false;
441
+ async setRTS(state: boolean) {
442
+ await this.port.setSignals({ requestToSend: state });
443
+ // Work-around for adapters on Windows using the usbser.sys driver:
444
+ // generate a dummy change to DTR so that the set-control-line-state
445
+ // request is sent with the updated RTS state and the same DTR state
446
+ // Referenced to esptool.py
447
+ await this.setDTR(this.state_DTR);
448
+ }
449
+
450
+ async setDTR(state: boolean) {
451
+ this.state_DTR = state;
452
+ await this.port.setSignals({ dataTerminalReady: state });
453
+ }
454
+
455
+ async hardReset(bootloader = false) {
456
+ if (bootloader) {
457
+ // enter flash mode
458
+ 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.");
478
+ } 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.");
490
+ }
491
+ } else {
492
+ // just reset
493
+ await this.setRTS(true); // EN->LOW
494
+ await this.sleep(100);
495
+ await this.setRTS(false);
496
+ this.logger.log("Hard reset.");
497
+ }
498
+ await new Promise((resolve) => setTimeout(resolve, 1000));
499
+ }
500
+
501
+ /**
502
+ * @name macAddr
503
+ * The MAC address burned into the OTP memory of the ESP chip
504
+ */
505
+ macAddr() {
506
+ const macAddr = new Array(6).fill(0);
507
+ const mac0 = this._efuses[0];
508
+ const mac1 = this._efuses[1];
509
+ const mac2 = this._efuses[2];
510
+ const mac3 = this._efuses[3];
511
+ let oui;
512
+ if (this.chipFamily == CHIP_FAMILY_ESP8266) {
513
+ if (mac3 != 0) {
514
+ oui = [(mac3 >> 16) & 0xff, (mac3 >> 8) & 0xff, mac3 & 0xff];
515
+ } else if (((mac1 >> 16) & 0xff) == 0) {
516
+ oui = [0x18, 0xfe, 0x34];
517
+ } else if (((mac1 >> 16) & 0xff) == 1) {
518
+ oui = [0xac, 0xd0, 0x74];
519
+ } else {
520
+ throw new Error("Couldnt determine OUI");
521
+ }
522
+
523
+ macAddr[0] = oui[0];
524
+ macAddr[1] = oui[1];
525
+ macAddr[2] = oui[2];
526
+ macAddr[3] = (mac1 >> 8) & 0xff;
527
+ macAddr[4] = mac1 & 0xff;
528
+ macAddr[5] = (mac0 >> 24) & 0xff;
529
+ } else if (this.chipFamily == CHIP_FAMILY_ESP32) {
530
+ macAddr[0] = (mac2 >> 8) & 0xff;
531
+ macAddr[1] = mac2 & 0xff;
532
+ macAddr[2] = (mac1 >> 24) & 0xff;
533
+ macAddr[3] = (mac1 >> 16) & 0xff;
534
+ macAddr[4] = (mac1 >> 8) & 0xff;
535
+ macAddr[5] = mac1 & 0xff;
536
+ } else if (
537
+ this.chipFamily == CHIP_FAMILY_ESP32S2 ||
538
+ this.chipFamily == CHIP_FAMILY_ESP32S3 ||
539
+ this.chipFamily == CHIP_FAMILY_ESP32C2 ||
540
+ this.chipFamily == CHIP_FAMILY_ESP32C3 ||
541
+ this.chipFamily == CHIP_FAMILY_ESP32C5 ||
542
+ this.chipFamily == CHIP_FAMILY_ESP32C6 ||
543
+ this.chipFamily == CHIP_FAMILY_ESP32C61 ||
544
+ this.chipFamily == CHIP_FAMILY_ESP32H2 ||
545
+ this.chipFamily == CHIP_FAMILY_ESP32H4 ||
546
+ this.chipFamily == CHIP_FAMILY_ESP32H21 ||
547
+ this.chipFamily == CHIP_FAMILY_ESP32P4 ||
548
+ this.chipFamily == CHIP_FAMILY_ESP32S31
549
+ ) {
550
+ macAddr[0] = (mac1 >> 8) & 0xff;
551
+ macAddr[1] = mac1 & 0xff;
552
+ macAddr[2] = (mac0 >> 24) & 0xff;
553
+ macAddr[3] = (mac0 >> 16) & 0xff;
554
+ macAddr[4] = (mac0 >> 8) & 0xff;
555
+ macAddr[5] = mac0 & 0xff;
556
+ } else {
557
+ throw new Error("Unknown chip family");
558
+ }
559
+ return macAddr;
560
+ }
561
+
562
+ async readRegister(reg: number) {
563
+ if (this.debug) {
564
+ this.logger.debug("Reading from Register " + toHex(reg, 8));
565
+ }
566
+ const packet = pack("<I", reg);
567
+ await this.sendCommand(ESP_READ_REG, packet);
568
+ const [val] = await this.getResponse(ESP_READ_REG);
569
+ return val;
570
+ }
571
+
572
+ /**
573
+ * @name checkCommand
574
+ * Send a command packet, check that the command succeeded and
575
+ * return a tuple with the value and data.
576
+ * See the ESP Serial Protocol for more details on what value/data are
577
+ */
578
+ async checkCommand(
579
+ opcode: number,
580
+ buffer: number[],
581
+ checksum = 0,
582
+ timeout = DEFAULT_TIMEOUT,
583
+ ): Promise<[number, number[]]> {
584
+ timeout = Math.min(timeout, MAX_TIMEOUT);
585
+ await this.sendCommand(opcode, buffer, checksum);
586
+ const [value, responseData] = await this.getResponse(opcode, timeout);
587
+
588
+ if (responseData === null) {
589
+ throw new Error("Didn't get enough status bytes");
590
+ }
591
+
592
+ let data = responseData;
593
+ let statusLen = 0;
594
+
595
+ if (this.IS_STUB || this.chipFamily == CHIP_FAMILY_ESP8266) {
596
+ statusLen = 2;
597
+ } else if (
598
+ [
599
+ CHIP_FAMILY_ESP32,
600
+ CHIP_FAMILY_ESP32S2,
601
+ CHIP_FAMILY_ESP32S3,
602
+ CHIP_FAMILY_ESP32C2,
603
+ CHIP_FAMILY_ESP32C3,
604
+ CHIP_FAMILY_ESP32C5,
605
+ CHIP_FAMILY_ESP32C6,
606
+ CHIP_FAMILY_ESP32C61,
607
+ CHIP_FAMILY_ESP32H2,
608
+ CHIP_FAMILY_ESP32H4,
609
+ CHIP_FAMILY_ESP32H21,
610
+ CHIP_FAMILY_ESP32P4,
611
+ CHIP_FAMILY_ESP32S31,
612
+ ].includes(this.chipFamily)
613
+ ) {
614
+ statusLen = 4;
615
+ } else {
616
+ // When chipFamily is not yet set (e.g., during GET_SECURITY_INFO in detectChip),
617
+ // assume modern chips use 4-byte status
618
+ if (opcode === ESP_GET_SECURITY_INFO) {
619
+ statusLen = 4;
620
+ } else if ([2, 4].includes(data.length)) {
621
+ statusLen = data.length;
622
+ }
623
+ }
624
+
625
+ if (data.length < statusLen) {
626
+ throw new Error("Didn't get enough status bytes");
627
+ }
628
+ const status = data.slice(-statusLen, data.length);
629
+ data = data.slice(0, -statusLen);
630
+ if (this.debug) {
631
+ this.logger.debug("status", status);
632
+ this.logger.debug("value", value);
633
+ this.logger.debug("data", data);
634
+ }
635
+ if (status[0] == 1) {
636
+ if (status[1] == ROM_INVALID_RECV_MSG) {
637
+ // Unsupported command can result in more than one error response
638
+ // Use drainInputBuffer for CP210x compatibility on Windows
639
+ await this.drainInputBuffer(200);
640
+ throw new Error("Invalid (unsupported) command " + toHex(opcode));
641
+ } else {
642
+ throw new Error("Command failure error code " + toHex(status[1]));
643
+ }
644
+ }
645
+
646
+ return [value, data];
647
+ }
648
+
649
+ /**
650
+ * @name sendCommand
651
+ * Send a slip-encoded, checksummed command over the UART,
652
+ * does not check response
653
+ */
654
+ async sendCommand(opcode: number, buffer: number[], checksum = 0) {
655
+ const packet = slipEncode([
656
+ ...pack("<BBHI", 0x00, opcode, buffer.length, checksum),
657
+ ...buffer,
658
+ ]);
659
+ if (this.debug) {
660
+ this.logger.debug(
661
+ `Writing ${packet.length} byte${packet.length == 1 ? "" : "s"}:`,
662
+ packet,
663
+ );
664
+ }
665
+ await this.writeToStream(packet);
666
+ }
667
+
668
+ /**
669
+ * @name readPacket
670
+ * Generator to read SLIP packets from a serial port.
671
+ * Yields one full SLIP packet at a time, raises exception on timeout or invalid data.
672
+ */
673
+
674
+ async readPacket(timeout: number): Promise<number[]> {
675
+ let partialPacket: number[] | null = null;
676
+ let inEscape = false;
677
+ let readBytes: number[] = [];
678
+ while (true) {
679
+ const stamp = Date.now();
680
+ readBytes = [];
681
+ while (Date.now() - stamp < timeout) {
682
+ if (this._inputBuffer.length > 0) {
683
+ readBytes.push(this._inputBuffer.shift()!);
684
+ break;
685
+ } else {
686
+ // Reduced sleep time for faster response during high-speed transfers
687
+ await sleep(1);
688
+ }
689
+ }
690
+ if (readBytes.length == 0) {
691
+ const waitingFor = partialPacket === null ? "header" : "content";
692
+ throw new SlipReadError("Timed out waiting for packet " + waitingFor);
693
+ }
694
+ if (this.debug)
695
+ this.logger.debug(
696
+ "Read " + readBytes.length + " bytes: " + hexFormatter(readBytes),
697
+ );
698
+ for (const b of readBytes) {
699
+ if (partialPacket === null) {
700
+ // waiting for packet header
701
+ if (b == 0xc0) {
702
+ partialPacket = [];
703
+ } else {
704
+ if (this.debug) {
705
+ this.logger.debug(
706
+ "Read invalid data: " + hexFormatter(readBytes),
707
+ );
708
+ this.logger.debug(
709
+ "Remaining data in serial buffer: " +
710
+ hexFormatter(this._inputBuffer),
711
+ );
712
+ }
713
+ throw new SlipReadError(
714
+ "Invalid head of packet (" + toHex(b) + ")",
715
+ );
716
+ }
717
+ } else if (inEscape) {
718
+ // part-way through escape sequence
719
+ inEscape = false;
720
+ if (b == 0xdc) {
721
+ partialPacket.push(0xc0);
722
+ } else if (b == 0xdd) {
723
+ partialPacket.push(0xdb);
724
+ } else {
725
+ if (this.debug) {
726
+ this.logger.debug(
727
+ "Read invalid data: " + hexFormatter(readBytes),
728
+ );
729
+ this.logger.debug(
730
+ "Remaining data in serial buffer: " +
731
+ hexFormatter(this._inputBuffer),
732
+ );
733
+ }
734
+ throw new SlipReadError(
735
+ "Invalid SLIP escape (0xdb, " + toHex(b) + ")",
736
+ );
737
+ }
738
+ } else if (b == 0xdb) {
739
+ // start of escape sequence
740
+ inEscape = true;
741
+ } else if (b == 0xc0) {
742
+ // end of packet
743
+ if (this.debug)
744
+ this.logger.debug(
745
+ "Received full packet: " + hexFormatter(partialPacket),
746
+ );
747
+ return partialPacket;
748
+ } else {
749
+ // normal byte in packet
750
+ partialPacket.push(b);
751
+ }
752
+ }
753
+ }
754
+ throw new SlipReadError("Invalid state");
755
+ }
756
+
757
+ /**
758
+ * @name getResponse
759
+ * Read response data and decodes the slip packet, then parses
760
+ * out the value/data and returns as a tuple of (value, data) where
761
+ * each is a list of bytes
762
+ */
763
+ async getResponse(
764
+ opcode: number,
765
+ timeout = DEFAULT_TIMEOUT,
766
+ ): Promise<[number, number[]]> {
767
+ for (let i = 0; i < 100; i++) {
768
+ const packet = await this.readPacket(timeout);
769
+
770
+ if (packet.length < 8) {
771
+ continue;
772
+ }
773
+
774
+ const [resp, opRet, , val] = unpack("<BBHI", packet.slice(0, 8));
775
+ if (resp != 1) {
776
+ continue;
777
+ }
778
+ const data = packet.slice(8);
779
+ if (opcode == null || opRet == opcode) {
780
+ return [val, data];
781
+ }
782
+ if (data[0] != 0 && data[1] == ROM_INVALID_RECV_MSG) {
783
+ // Unsupported command can result in more than one error response
784
+ // Use drainInputBuffer for CP210x compatibility on Windows
785
+ await this.drainInputBuffer(200);
786
+ throw new Error(`Invalid (unsupported) command ${toHex(opcode)}`);
787
+ }
788
+ }
789
+ throw "Response doesn't match request";
790
+ }
791
+
792
+ /**
793
+ * @name checksum
794
+ * Calculate checksum of a blob, as it is defined by the ROM
795
+ */
796
+ checksum(data: number[], state = ESP_CHECKSUM_MAGIC) {
797
+ for (const b of data) {
798
+ state ^= b;
799
+ }
800
+ return state;
801
+ }
802
+
803
+ async setBaudrate(baud: number) {
804
+ if (this.chipFamily == CHIP_FAMILY_ESP8266) {
805
+ throw new Error("Changing baud rate is not supported on the ESP8266");
806
+ }
807
+
808
+ try {
809
+ // Send ESP_ROM_BAUD(115200) as the old one if running STUB otherwise 0
810
+ const buffer = pack("<II", baud, this.IS_STUB ? ESP_ROM_BAUD : 0);
811
+ await this.checkCommand(ESP_CHANGE_BAUDRATE, buffer);
812
+ } catch (e) {
813
+ this.logger.error(`Baudrate change error: ${e}`);
814
+ throw new Error(
815
+ `Unable to change the baud rate to ${baud}: No response from set baud rate command.`,
816
+ );
817
+ }
818
+
819
+ if (this._parent) {
820
+ await this._parent.reconfigurePort(baud);
821
+ } else {
822
+ await this.reconfigurePort(baud);
823
+ }
824
+
825
+ // Wait for port to be ready after baudrate change
826
+ await sleep(SYNC_TIMEOUT);
827
+
828
+ // Track current baudrate for reconnect
829
+ if (this._parent) {
830
+ this._parent._currentBaudRate = baud;
831
+ } else {
832
+ this._currentBaudRate = baud;
833
+ }
834
+
835
+ // Warn if baudrate exceeds USB-Serial chip capability
836
+ const maxBaud = this._parent
837
+ ? this._parent._maxUSBSerialBaudrate
838
+ : this._maxUSBSerialBaudrate;
839
+ if (maxBaud && baud > maxBaud) {
840
+ this.logger.log(
841
+ `⚠️ WARNING: Baudrate ${baud} exceeds USB-Serial chip limit (${maxBaud})!`,
842
+ );
843
+ this.logger.log(
844
+ `⚠️ This may cause data corruption or connection failures!`,
845
+ );
846
+ }
847
+
848
+ this.logger.log(`Changed baud rate to ${baud}`);
849
+ }
850
+
851
+ async reconfigurePort(baud: number) {
852
+ try {
853
+ // SerialPort does not allow to be reconfigured while open so we close and re-open
854
+ // reader.cancel() causes the Promise returned by the read() operation running on
855
+ // the readLoop to return immediately with { value: undefined, done: true } and thus
856
+ // breaking the loop and exiting readLoop();
857
+ await this._reader?.cancel();
858
+ await this.port.close();
859
+
860
+ // Reopen Port
861
+ await this.port.open({ baudRate: baud });
862
+
863
+ // Clear buffer again
864
+ await this.flushSerialBuffers();
865
+
866
+ // Restart Readloop
867
+ this.readLoop();
868
+ } catch (e) {
869
+ this.logger.error(`Reconfigure port error: ${e}`);
870
+ throw new Error(`Unable to change the baud rate to ${baud}: ${e}`);
871
+ }
872
+ }
873
+
874
+ /**
875
+ * @name sync
876
+ * Put into ROM bootload mode & attempt to synchronize with the
877
+ * ESP ROM bootloader, we will retry a few times
878
+ */
879
+ async sync() {
880
+ for (let i = 0; i < 5; i++) {
881
+ this._inputBuffer.length = 0;
882
+ const response = await this._sync();
883
+ if (response) {
884
+ await sleep(SYNC_TIMEOUT);
885
+ return true;
886
+ }
887
+ await sleep(SYNC_TIMEOUT);
888
+ }
889
+
890
+ throw new Error("Couldn't sync to ESP. Try resetting.");
891
+ }
892
+
893
+ /**
894
+ * @name _sync
895
+ * Perform a soft-sync using AT sync packets, does not perform
896
+ * any hardware resetting
897
+ */
898
+ async _sync() {
899
+ await this.sendCommand(ESP_SYNC, SYNC_PACKET);
900
+ for (let i = 0; i < 8; i++) {
901
+ try {
902
+ const [, data] = await this.getResponse(ESP_SYNC, SYNC_TIMEOUT);
903
+ if (data.length > 1 && data[0] == 0 && data[1] == 0) {
904
+ return true;
905
+ }
906
+ } catch {
907
+ // If read packet fails.
908
+ }
909
+ }
910
+ return false;
911
+ }
912
+
913
+ /**
914
+ * @name getFlashWriteSize
915
+ * Get the Flash write size based on the chip
916
+ */
917
+ getFlashWriteSize() {
918
+ if (this.IS_STUB) {
919
+ return STUB_FLASH_WRITE_SIZE;
920
+ }
921
+ return FLASH_WRITE_SIZE;
922
+ }
923
+
924
+ /**
925
+ * @name flashData
926
+ * Program a full, uncompressed binary file into SPI Flash at
927
+ * a given offset. If an ESP32 and md5 string is passed in, will also
928
+ * verify memory. ESP8266 does not have checksum memory verification in
929
+ * ROM
930
+ */
931
+ async flashData(
932
+ binaryData: ArrayBuffer,
933
+ updateProgress: (bytesWritten: number, totalBytes: number) => void,
934
+ offset = 0,
935
+ compress = false,
936
+ ) {
937
+ if (binaryData.byteLength >= 8) {
938
+ // unpack the (potential) image header
939
+ const header = Array.from(new Uint8Array(binaryData, 0, 4));
940
+ const headerMagic = header[0];
941
+ const headerFlashMode = header[2];
942
+ const headerFlashSizeFreq = header[3];
943
+
944
+ this.logger.log(
945
+ `Image header, Magic=${toHex(headerMagic)}, FlashMode=${toHex(
946
+ headerFlashMode,
947
+ )}, FlashSizeFreq=${toHex(headerFlashSizeFreq)}`,
948
+ );
949
+ }
950
+
951
+ const uncompressedFilesize = binaryData.byteLength;
952
+ let compressedFilesize = 0;
953
+
954
+ let dataToFlash;
955
+ let timeout = DEFAULT_TIMEOUT;
956
+
957
+ if (compress) {
958
+ dataToFlash = deflate(new Uint8Array(binaryData), {
959
+ level: 9,
960
+ }).buffer;
961
+ compressedFilesize = dataToFlash.byteLength;
962
+ this.logger.log(
963
+ `Writing data with filesize: ${uncompressedFilesize}. Compressed Size: ${compressedFilesize}`,
964
+ );
965
+ timeout = await this.flashDeflBegin(
966
+ uncompressedFilesize,
967
+ compressedFilesize,
968
+ offset,
969
+ );
970
+ } else {
971
+ this.logger.log(`Writing data with filesize: ${uncompressedFilesize}`);
972
+ dataToFlash = binaryData;
973
+ await this.flashBegin(uncompressedFilesize, offset);
974
+ }
975
+
976
+ let block = [];
977
+ let seq = 0;
978
+ let written = 0;
979
+ let position = 0;
980
+ const stamp = Date.now();
981
+ const flashWriteSize = this.getFlashWriteSize();
982
+
983
+ const filesize = compress ? compressedFilesize : uncompressedFilesize;
984
+
985
+ while (filesize - position > 0) {
986
+ if (this.debug) {
987
+ this.logger.log(
988
+ `Writing at ${toHex(offset + seq * flashWriteSize, 8)} `,
989
+ );
990
+ }
991
+ if (filesize - position >= flashWriteSize) {
992
+ block = Array.from(
993
+ new Uint8Array(dataToFlash, position, flashWriteSize),
994
+ );
995
+ } else {
996
+ // Pad the last block only if we are sending uncompressed data.
997
+ block = Array.from(
998
+ new Uint8Array(dataToFlash, position, filesize - position),
999
+ );
1000
+ if (!compress) {
1001
+ block = block.concat(
1002
+ new Array(flashWriteSize - block.length).fill(0xff),
1003
+ );
1004
+ }
1005
+ }
1006
+ if (compress) {
1007
+ await this.flashDeflBlock(block, seq, timeout);
1008
+ } else {
1009
+ await this.flashBlock(block, seq);
1010
+ }
1011
+ seq += 1;
1012
+ // If using compression we update the progress with the proportional size of the block taking into account the compression ratio.
1013
+ // This way we report progress on the uncompressed size
1014
+ written += compress
1015
+ ? Math.round((block.length * uncompressedFilesize) / compressedFilesize)
1016
+ : block.length;
1017
+ position += flashWriteSize;
1018
+ updateProgress(
1019
+ Math.min(written, uncompressedFilesize),
1020
+ uncompressedFilesize,
1021
+ );
1022
+ }
1023
+ this.logger.log(
1024
+ "Took " + (Date.now() - stamp) + "ms to write " + filesize + " bytes",
1025
+ );
1026
+
1027
+ // Only send flashF finish if running the stub because ir causes the ROM to exit and run user code
1028
+ if (this.IS_STUB) {
1029
+ await this.flashBegin(0, 0);
1030
+ if (compress) {
1031
+ await this.flashDeflFinish();
1032
+ } else {
1033
+ await this.flashFinish();
1034
+ }
1035
+ }
1036
+ }
1037
+
1038
+ /**
1039
+ * @name flashBlock
1040
+ * Send one block of data to program into SPI Flash memory
1041
+ */
1042
+ async flashBlock(data: number[], seq: number, timeout = DEFAULT_TIMEOUT) {
1043
+ await this.checkCommand(
1044
+ ESP_FLASH_DATA,
1045
+ pack("<IIII", data.length, seq, 0, 0).concat(data),
1046
+ this.checksum(data),
1047
+ timeout,
1048
+ );
1049
+ }
1050
+ async flashDeflBlock(data: number[], seq: number, timeout = DEFAULT_TIMEOUT) {
1051
+ await this.checkCommand(
1052
+ ESP_FLASH_DEFL_DATA,
1053
+ pack("<IIII", data.length, seq, 0, 0).concat(data),
1054
+ this.checksum(data),
1055
+ timeout,
1056
+ );
1057
+ }
1058
+
1059
+ /**
1060
+ * @name flashBegin
1061
+ * Prepare for flashing by attaching SPI chip and erasing the
1062
+ * number of blocks requred.
1063
+ */
1064
+ async flashBegin(size = 0, offset = 0, encrypted = false) {
1065
+ // Flush serial buffers before flash write operation
1066
+ await this.flushSerialBuffers();
1067
+
1068
+ let eraseSize;
1069
+ const flashWriteSize = this.getFlashWriteSize();
1070
+ if (
1071
+ !this.IS_STUB &&
1072
+ [
1073
+ CHIP_FAMILY_ESP32,
1074
+ CHIP_FAMILY_ESP32S2,
1075
+ CHIP_FAMILY_ESP32S3,
1076
+ CHIP_FAMILY_ESP32C2,
1077
+ CHIP_FAMILY_ESP32C3,
1078
+ CHIP_FAMILY_ESP32C5,
1079
+ CHIP_FAMILY_ESP32C6,
1080
+ CHIP_FAMILY_ESP32C61,
1081
+ CHIP_FAMILY_ESP32H2,
1082
+ CHIP_FAMILY_ESP32H4,
1083
+ CHIP_FAMILY_ESP32H21,
1084
+ CHIP_FAMILY_ESP32P4,
1085
+ CHIP_FAMILY_ESP32S31,
1086
+ ].includes(this.chipFamily)
1087
+ ) {
1088
+ await this.checkCommand(ESP_SPI_ATTACH, new Array(8).fill(0));
1089
+ }
1090
+ const numBlocks = Math.floor((size + flashWriteSize - 1) / flashWriteSize);
1091
+ if (this.chipFamily == CHIP_FAMILY_ESP8266) {
1092
+ eraseSize = this.getEraseSize(offset, size);
1093
+ } else {
1094
+ eraseSize = size;
1095
+ }
1096
+
1097
+ const timeout = this.IS_STUB
1098
+ ? DEFAULT_TIMEOUT
1099
+ : timeoutPerMb(ERASE_REGION_TIMEOUT_PER_MB, size);
1100
+
1101
+ const stamp = Date.now();
1102
+ let buffer = pack("<IIII", eraseSize, numBlocks, flashWriteSize, offset);
1103
+ if (
1104
+ this.chipFamily == CHIP_FAMILY_ESP32 ||
1105
+ this.chipFamily == CHIP_FAMILY_ESP32S2 ||
1106
+ this.chipFamily == CHIP_FAMILY_ESP32S3 ||
1107
+ this.chipFamily == CHIP_FAMILY_ESP32C2 ||
1108
+ this.chipFamily == CHIP_FAMILY_ESP32C3 ||
1109
+ this.chipFamily == CHIP_FAMILY_ESP32C5 ||
1110
+ this.chipFamily == CHIP_FAMILY_ESP32C6 ||
1111
+ this.chipFamily == CHIP_FAMILY_ESP32C61 ||
1112
+ this.chipFamily == CHIP_FAMILY_ESP32H2 ||
1113
+ this.chipFamily == CHIP_FAMILY_ESP32H4 ||
1114
+ this.chipFamily == CHIP_FAMILY_ESP32H21 ||
1115
+ this.chipFamily == CHIP_FAMILY_ESP32P4 ||
1116
+ this.chipFamily == CHIP_FAMILY_ESP32S31
1117
+ ) {
1118
+ buffer = buffer.concat(pack("<I", encrypted ? 1 : 0));
1119
+ }
1120
+ this.logger.log(
1121
+ "Erase size " +
1122
+ eraseSize +
1123
+ ", blocks " +
1124
+ numBlocks +
1125
+ ", block size " +
1126
+ toHex(flashWriteSize, 4) +
1127
+ ", offset " +
1128
+ toHex(offset, 4) +
1129
+ ", encrypted " +
1130
+ (encrypted ? "yes" : "no"),
1131
+ );
1132
+ await this.checkCommand(ESP_FLASH_BEGIN, buffer, 0, timeout);
1133
+ if (size != 0 && !this.IS_STUB) {
1134
+ this.logger.log(
1135
+ "Took " + (Date.now() - stamp) + "ms to erase " + numBlocks + " bytes",
1136
+ );
1137
+ }
1138
+ return numBlocks;
1139
+ }
1140
+
1141
+ /**
1142
+ * @name flashDeflBegin
1143
+ *
1144
+ */
1145
+
1146
+ async flashDeflBegin(size = 0, compressedSize = 0, offset = 0) {
1147
+ // Start downloading compressed data to Flash (performs an erase)
1148
+ // Returns number of blocks to write.
1149
+ const flashWriteSize = this.getFlashWriteSize();
1150
+ const numBlocks = Math.floor(
1151
+ (compressedSize + flashWriteSize - 1) / flashWriteSize,
1152
+ );
1153
+ const eraseBlocks = Math.floor(
1154
+ (size + flashWriteSize - 1) / flashWriteSize,
1155
+ );
1156
+ let writeSize = 0;
1157
+ let timeout = 0;
1158
+
1159
+ if (this.IS_STUB) {
1160
+ writeSize = size; // stub expects number of bytes here, manages erasing internally
1161
+ timeout = timeoutPerMb(ERASE_REGION_TIMEOUT_PER_MB, writeSize); // ROM performs the erase up front
1162
+ } else {
1163
+ writeSize = eraseBlocks * flashWriteSize; // ROM expects rounded up to erase block size
1164
+ timeout = DEFAULT_TIMEOUT;
1165
+ }
1166
+ const buffer = pack("<IIII", writeSize, numBlocks, flashWriteSize, offset);
1167
+
1168
+ await this.checkCommand(ESP_FLASH_DEFL_BEGIN, buffer, 0, timeout);
1169
+
1170
+ return timeout;
1171
+ }
1172
+
1173
+ async flashFinish() {
1174
+ const buffer = pack("<I", 1);
1175
+ await this.checkCommand(ESP_FLASH_END, buffer);
1176
+ }
1177
+
1178
+ async flashDeflFinish() {
1179
+ const buffer = pack("<I", 1);
1180
+ await this.checkCommand(ESP_FLASH_DEFL_END, buffer);
1181
+ }
1182
+
1183
+ getBootloaderOffset() {
1184
+ const bootFlashOffs = getSpiFlashAddresses(this.getChipFamily());
1185
+ const BootldrFlashOffs = bootFlashOffs.flashOffs;
1186
+ return BootldrFlashOffs;
1187
+ }
1188
+
1189
+ async flashId() {
1190
+ const SPIFLASH_RDID = 0x9f;
1191
+ const result = await this.runSpiFlashCommand(SPIFLASH_RDID, [], 24);
1192
+ return result;
1193
+ }
1194
+
1195
+ getChipFamily() {
1196
+ return this._parent ? this._parent.chipFamily : this.chipFamily;
1197
+ }
1198
+
1199
+ async writeRegister(
1200
+ address: number,
1201
+ value: number,
1202
+ mask = 0xffffffff,
1203
+ delayUs = 0,
1204
+ delayAfterUs = 0,
1205
+ ) {
1206
+ let buffer = pack("<IIII", address, value, mask, delayUs);
1207
+ if (delayAfterUs > 0) {
1208
+ // add a dummy write to a date register as an excuse to have a delay
1209
+ buffer = buffer.concat(
1210
+ pack(
1211
+ "<IIII",
1212
+ getSpiFlashAddresses(this.getChipFamily()).uartDateReg,
1213
+ 0,
1214
+ 0,
1215
+ delayAfterUs,
1216
+ ),
1217
+ );
1218
+ }
1219
+ await this.checkCommand(ESP_WRITE_REG, buffer);
1220
+ }
1221
+
1222
+ async setDataLengths(
1223
+ spiAddresses: SpiFlashAddresses,
1224
+ mosiBits: number,
1225
+ misoBits: number,
1226
+ ) {
1227
+ if (spiAddresses.mosiDlenOffs != -1) {
1228
+ // Actual MCUs have a more sophisticated way to set up "user" commands
1229
+ const SPI_MOSI_DLEN_REG =
1230
+ spiAddresses.regBase + spiAddresses.mosiDlenOffs;
1231
+ const SPI_MISO_DLEN_REG =
1232
+ spiAddresses.regBase + spiAddresses.misoDlenOffs;
1233
+ if (mosiBits > 0) {
1234
+ await this.writeRegister(SPI_MOSI_DLEN_REG, mosiBits - 1);
1235
+ }
1236
+ if (misoBits > 0) {
1237
+ await this.writeRegister(SPI_MISO_DLEN_REG, misoBits - 1);
1238
+ }
1239
+ } else {
1240
+ const SPI_DATA_LEN_REG = spiAddresses.regBase + spiAddresses.usr1Offs;
1241
+ const SPI_MOSI_BITLEN_S = 17;
1242
+ const SPI_MISO_BITLEN_S = 8;
1243
+ const mosiMask = mosiBits == 0 ? 0 : mosiBits - 1;
1244
+ const misoMask = misoBits == 0 ? 0 : misoBits - 1;
1245
+ const value =
1246
+ (misoMask << SPI_MISO_BITLEN_S) | (mosiMask << SPI_MOSI_BITLEN_S);
1247
+ await this.writeRegister(SPI_DATA_LEN_REG, value);
1248
+ }
1249
+ }
1250
+ async waitDone(spiCmdReg: number, spiCmdUsr: number) {
1251
+ for (let i = 0; i < 10; i++) {
1252
+ const cmdValue = await this.readRegister(spiCmdReg);
1253
+ if ((cmdValue & spiCmdUsr) == 0) {
1254
+ return;
1255
+ }
1256
+ }
1257
+ throw Error("SPI command did not complete in time");
1258
+ }
1259
+
1260
+ async runSpiFlashCommand(
1261
+ spiflashCommand: number,
1262
+ data: number[],
1263
+ readBits = 0,
1264
+ ) {
1265
+ // Run an arbitrary SPI flash command.
1266
+
1267
+ // This function uses the "USR_COMMAND" functionality in the ESP
1268
+ // SPI hardware, rather than the precanned commands supported by
1269
+ // hardware. So the value of spiflash_command is an actual command
1270
+ // byte, sent over the wire.
1271
+
1272
+ // After writing command byte, writes 'data' to MOSI and then
1273
+ // reads back 'read_bits' of reply on MISO. Result is a number.
1274
+
1275
+ // SPI_USR register flags
1276
+ const SPI_USR_COMMAND = 1 << 31;
1277
+ const SPI_USR_MISO = 1 << 28;
1278
+ const SPI_USR_MOSI = 1 << 27;
1279
+
1280
+ // SPI registers, base address differs
1281
+ const spiAddresses = getSpiFlashAddresses(this.getChipFamily());
1282
+ const base = spiAddresses.regBase;
1283
+ const SPI_CMD_REG = base;
1284
+ const SPI_USR_REG = base + spiAddresses.usrOffs;
1285
+ const SPI_USR2_REG = base + spiAddresses.usr2Offs;
1286
+ const SPI_W0_REG = base + spiAddresses.w0Offs;
1287
+
1288
+ // SPI peripheral "command" bitmasks for SPI_CMD_REG
1289
+ const SPI_CMD_USR = 1 << 18;
1290
+
1291
+ // shift values
1292
+ const SPI_USR2_COMMAND_LEN_SHIFT = 28;
1293
+
1294
+ if (readBits > 32) {
1295
+ throw new Error(
1296
+ "Reading more than 32 bits back from a SPI flash operation is unsupported",
1297
+ );
1298
+ }
1299
+ if (data.length > 64) {
1300
+ throw new Error(
1301
+ "Writing more than 64 bytes of data with one SPI command is unsupported",
1302
+ );
1303
+ }
1304
+
1305
+ const dataBits = data.length * 8;
1306
+ const oldSpiUsr = await this.readRegister(SPI_USR_REG);
1307
+ const oldSpiUsr2 = await this.readRegister(SPI_USR2_REG);
1308
+
1309
+ let flags = SPI_USR_COMMAND;
1310
+
1311
+ if (readBits > 0) {
1312
+ flags |= SPI_USR_MISO;
1313
+ }
1314
+ if (dataBits > 0) {
1315
+ flags |= SPI_USR_MOSI;
1316
+ }
1317
+
1318
+ await this.setDataLengths(spiAddresses, dataBits, readBits);
1319
+
1320
+ await this.writeRegister(SPI_USR_REG, flags);
1321
+ await this.writeRegister(
1322
+ SPI_USR2_REG,
1323
+ (7 << SPI_USR2_COMMAND_LEN_SHIFT) | spiflashCommand,
1324
+ );
1325
+ if (dataBits == 0) {
1326
+ await this.writeRegister(SPI_W0_REG, 0); // clear data register before we read it
1327
+ } else {
1328
+ const padLen = (4 - (data.length % 4)) % 4;
1329
+ data = data.concat(new Array(padLen).fill(0x00)); // pad to 32-bit multiple
1330
+
1331
+ const words = unpack("I".repeat(Math.floor(data.length / 4)), data);
1332
+ let nextReg = SPI_W0_REG;
1333
+
1334
+ this.logger.debug(`Words Length: ${words.length}`);
1335
+
1336
+ for (const word of words) {
1337
+ this.logger.debug(
1338
+ `Writing word ${toHex(word)} to register offset ${toHex(nextReg)}`,
1339
+ );
1340
+ await this.writeRegister(nextReg, word);
1341
+ nextReg += 4;
1342
+ }
1343
+ }
1344
+ await this.writeRegister(SPI_CMD_REG, SPI_CMD_USR);
1345
+ await this.waitDone(SPI_CMD_REG, SPI_CMD_USR);
1346
+
1347
+ const status = await this.readRegister(SPI_W0_REG);
1348
+ // restore some SPI controller registers
1349
+ await this.writeRegister(SPI_USR_REG, oldSpiUsr);
1350
+ await this.writeRegister(SPI_USR2_REG, oldSpiUsr2);
1351
+ return status;
1352
+ }
1353
+ async detectFlashSize() {
1354
+ this.logger.log("Detecting Flash Size");
1355
+
1356
+ const flashId = await this.flashId();
1357
+ const manufacturer = flashId & 0xff;
1358
+ const flashIdLowbyte = (flashId >> 16) & 0xff;
1359
+
1360
+ this.logger.log(`FlashId: ${toHex(flashId)}`);
1361
+ this.logger.log(`Flash Manufacturer: ${manufacturer.toString(16)}`);
1362
+ this.logger.log(
1363
+ `Flash Device: ${((flashId >> 8) & 0xff).toString(
1364
+ 16,
1365
+ )}${flashIdLowbyte.toString(16)}`,
1366
+ );
1367
+
1368
+ this.flashSize = DETECTED_FLASH_SIZES[flashIdLowbyte];
1369
+ this.logger.log(`Auto-detected Flash size: ${this.flashSize}`);
1370
+ }
1371
+
1372
+ /**
1373
+ * @name getEraseSize
1374
+ * Calculate an erase size given a specific size in bytes.
1375
+ * Provides a workaround for the bootloader erase bug on ESP8266.
1376
+ */
1377
+ getEraseSize(offset: number, size: number) {
1378
+ const sectorsPerBlock = 16;
1379
+ const sectorSize = FLASH_SECTOR_SIZE;
1380
+ const numSectors = Math.floor((size + sectorSize - 1) / sectorSize);
1381
+ const startSector = Math.floor(offset / sectorSize);
1382
+
1383
+ let headSectors = sectorsPerBlock - (startSector % sectorsPerBlock);
1384
+ if (numSectors < headSectors) {
1385
+ headSectors = numSectors;
1386
+ }
1387
+
1388
+ if (numSectors < 2 * headSectors) {
1389
+ return Math.floor(((numSectors + 1) / 2) * sectorSize);
1390
+ }
1391
+
1392
+ return (numSectors - headSectors) * sectorSize;
1393
+ }
1394
+
1395
+ /**
1396
+ * @name memBegin (592)
1397
+ * Start downloading an application image to RAM
1398
+ */
1399
+ async memBegin(
1400
+ size: number,
1401
+ blocks: number,
1402
+ blocksize: number,
1403
+ offset: number,
1404
+ ) {
1405
+ return await this.checkCommand(
1406
+ ESP_MEM_BEGIN,
1407
+ pack("<IIII", size, blocks, blocksize, offset),
1408
+ );
1409
+ }
1410
+
1411
+ /**
1412
+ * @name memBlock (609)
1413
+ * Send a block of an image to RAM
1414
+ */
1415
+ async memBlock(data: number[], seq: number) {
1416
+ return await this.checkCommand(
1417
+ ESP_MEM_DATA,
1418
+ pack("<IIII", data.length, seq, 0, 0).concat(data),
1419
+ this.checksum(data),
1420
+ );
1421
+ }
1422
+
1423
+ /**
1424
+ * @name memFinish (615)
1425
+ * Leave download mode and run the application
1426
+ *
1427
+ * Sending ESP_MEM_END usually sends a correct response back, however sometimes
1428
+ * (with ROM loader) the executed code may reset the UART or change the baud rate
1429
+ * before the transmit FIFO is empty. So in these cases we set a short timeout and
1430
+ * ignore errors.
1431
+ */
1432
+ async memFinish(entrypoint = 0) {
1433
+ const timeout = this.IS_STUB ? DEFAULT_TIMEOUT : MEM_END_ROM_TIMEOUT;
1434
+ const data = pack("<II", entrypoint == 0 ? 1 : 0, entrypoint);
1435
+ return await this.checkCommand(ESP_MEM_END, data, 0, timeout);
1436
+ }
1437
+
1438
+ async runStub(skipFlashDetection = false): Promise<EspStubLoader> {
1439
+ const stub = await getStubCode(this.chipFamily, this.chipRevision);
1440
+
1441
+ // No stub available for this chip, return ROM loader
1442
+ if (stub === null) {
1443
+ this.logger.log(
1444
+ `Stub flasher is not yet supported on ${this.chipName}, using ROM loader`,
1445
+ );
1446
+ return this as unknown as EspStubLoader;
1447
+ }
1448
+
1449
+ // We're transferring over USB, right?
1450
+ const ramBlock = USB_RAM_BLOCK;
1451
+
1452
+ // Upload
1453
+ this.logger.log("Uploading stub...");
1454
+ for (const field of ["text", "data"] as const) {
1455
+ const fieldData = stub[field];
1456
+ const offset = stub[`${field}_start` as "text_start" | "data_start"];
1457
+ const length = fieldData.length;
1458
+ const blocks = Math.floor((length + ramBlock - 1) / ramBlock);
1459
+ await this.memBegin(length, blocks, ramBlock, offset);
1460
+ for (const seq of Array(blocks).keys()) {
1461
+ const fromOffs = seq * ramBlock;
1462
+ let toOffs = fromOffs + ramBlock;
1463
+ if (toOffs > length) {
1464
+ toOffs = length;
1465
+ }
1466
+ await this.memBlock(fieldData.slice(fromOffs, toOffs), seq);
1467
+ }
1468
+ }
1469
+ await this.memFinish(stub.entry);
1470
+
1471
+ const p = await this.readPacket(500);
1472
+ const pChar = String.fromCharCode(...p);
1473
+
1474
+ if (pChar != "OHAI") {
1475
+ throw new Error("Failed to start stub. Unexpected response: " + pChar);
1476
+ }
1477
+ this.logger.log("Stub is now running...");
1478
+ const espStubLoader = new EspStubLoader(this.port, this.logger, this);
1479
+
1480
+ // Try to autodetect the flash size.
1481
+ if (!skipFlashDetection) {
1482
+ await espStubLoader.detectFlashSize();
1483
+ }
1484
+
1485
+ return espStubLoader;
1486
+ }
1487
+
1488
+ async writeToStream(data: number[]) {
1489
+ if (!this.port.writable) {
1490
+ this.logger.debug("Port writable stream not available, skipping write");
1491
+ return;
1492
+ }
1493
+ const writer = this.port.writable.getWriter();
1494
+ await writer.write(new Uint8Array(data));
1495
+ try {
1496
+ writer.releaseLock();
1497
+ } catch (err) {
1498
+ this.logger.error(`Ignoring release lock error: ${err}`);
1499
+ }
1500
+ }
1501
+
1502
+ async disconnect() {
1503
+ if (this._parent) {
1504
+ await this._parent.disconnect();
1505
+ return;
1506
+ }
1507
+ if (!this.port.writable) {
1508
+ this.logger.debug("Port already closed, skipping disconnect");
1509
+ return;
1510
+ }
1511
+ await this.port.writable.getWriter().close();
1512
+ await new Promise((resolve) => {
1513
+ if (!this._reader) {
1514
+ resolve(undefined);
1515
+ }
1516
+ this.addEventListener("disconnect", resolve, { once: true });
1517
+ this._reader!.cancel();
1518
+ });
1519
+ this.connected = false;
1520
+ }
1521
+
1522
+ /**
1523
+ * @name reconnectAndResume
1524
+ * Reconnect the serial port to flush browser buffers and reload stub
1525
+ */
1526
+ async reconnect(): Promise<void> {
1527
+ if (this._parent) {
1528
+ await this._parent.reconnect();
1529
+ return;
1530
+ }
1531
+
1532
+ this.logger.log("Reconnecting serial port...");
1533
+
1534
+ this.connected = false;
1535
+ this.__inputBuffer = [];
1536
+
1537
+ // Cancel reader
1538
+ if (this._reader) {
1539
+ try {
1540
+ await this._reader.cancel();
1541
+ } catch (err) {
1542
+ this.logger.debug(`Reader cancel error: ${err}`);
1543
+ }
1544
+ this._reader = undefined;
1545
+ }
1546
+
1547
+ // Close port
1548
+ try {
1549
+ await this.port.close();
1550
+ this.logger.log("Port closed");
1551
+ } catch (err) {
1552
+ this.logger.debug(`Port close error: ${err}`);
1553
+ }
1554
+
1555
+ // Open the port
1556
+ this.logger.debug("Opening port...");
1557
+ try {
1558
+ await this.port.open({ baudRate: ESP_ROM_BAUD });
1559
+ this.connected = true;
1560
+ } catch (err) {
1561
+ throw new Error(`Failed to open port: ${err}`);
1562
+ }
1563
+
1564
+ // Verify port streams are available
1565
+ if (!this.port.readable || !this.port.writable) {
1566
+ throw new Error(
1567
+ `Port streams not available after open (readable: ${!!this.port.readable}, writable: ${!!this.port.writable})`,
1568
+ );
1569
+ }
1570
+
1571
+ // Save chip info and flash size (no need to detect again)
1572
+ const savedChipFamily = this.chipFamily;
1573
+ const savedChipName = this.chipName;
1574
+ const savedChipRevision = this.chipRevision;
1575
+ const savedChipVariant = this.chipVariant;
1576
+ const savedFlashSize = this.flashSize;
1577
+
1578
+ // Reinitialize
1579
+ await this.hardReset(true);
1580
+
1581
+ if (!this._parent) {
1582
+ this.__inputBuffer = [];
1583
+ this.__totalBytesRead = 0;
1584
+ this.readLoop();
1585
+ }
1586
+
1587
+ await this.flushSerialBuffers();
1588
+ await this.sync();
1589
+
1590
+ // Restore chip info
1591
+ this.chipFamily = savedChipFamily;
1592
+ this.chipName = savedChipName;
1593
+ this.chipRevision = savedChipRevision;
1594
+ this.chipVariant = savedChipVariant;
1595
+ this.flashSize = savedFlashSize;
1596
+
1597
+ this.logger.debug(`Reconnect complete (chip: ${this.chipName})`);
1598
+
1599
+ // Verify port is ready
1600
+ if (!this.port.writable || !this.port.readable) {
1601
+ throw new Error("Port not ready after reconnect");
1602
+ }
1603
+
1604
+ // Load stub
1605
+ const stubLoader = await this.runStub(true);
1606
+ this.logger.debug("Stub loaded");
1607
+
1608
+ // Restore baudrate if it was changed
1609
+ if (this._currentBaudRate !== ESP_ROM_BAUD) {
1610
+ await stubLoader.setBaudrate(this._currentBaudRate);
1611
+
1612
+ // Verify port is still ready after baudrate change
1613
+ if (!this.port.writable || !this.port.readable) {
1614
+ throw new Error(
1615
+ `Port not ready after baudrate change (readable: ${!!this.port.readable}, writable: ${!!this.port.writable})`,
1616
+ );
1617
+ }
1618
+ }
1619
+
1620
+ // Copy stub state to this instance if we're a stub loader
1621
+ if (this.IS_STUB) {
1622
+ Object.assign(this, stubLoader);
1623
+ }
1624
+ this.logger.debug("Reconnection successful");
1625
+ }
1626
+
1627
+ /**
1628
+ * @name drainInputBuffer
1629
+ * Actively drain the input buffer by reading data for a specified time.
1630
+ * Simple approach for some drivers (especially CP210x on Windows) that have
1631
+ * issues with buffer flushing.
1632
+ *
1633
+ * Based on esptool.py fix: https://github.com/espressif/esptool/commit/5338ea054e5099ac7be235c54034802ac8a43162
1634
+ *
1635
+ * @param bufferingTime - Time in milliseconds to wait for the buffer to fill
1636
+ */
1637
+ private async drainInputBuffer(bufferingTime = 200): Promise<void> {
1638
+ // Wait for the buffer to fill
1639
+ await sleep(bufferingTime);
1640
+
1641
+ // Unsupported command response is sent 8 times and has
1642
+ // 14 bytes length including delimiter 0xC0 bytes.
1643
+ // At least part of it is read as a command response,
1644
+ // but to be safe, read it all.
1645
+ const bytesToDrain = 14 * 8;
1646
+ let drained = 0;
1647
+
1648
+ // Drain the buffer by reading available data
1649
+ const drainStart = Date.now();
1650
+ const drainTimeout = 100; // Short timeout for draining
1651
+
1652
+ while (drained < bytesToDrain && Date.now() - drainStart < drainTimeout) {
1653
+ if (this._inputBuffer.length > 0) {
1654
+ const byte = this._inputBuffer.shift();
1655
+ if (byte !== undefined) {
1656
+ drained++;
1657
+ }
1658
+ } else {
1659
+ // Small sleep to avoid busy waiting
1660
+ await sleep(1);
1661
+ }
1662
+ }
1663
+
1664
+ if (drained > 0) {
1665
+ this.logger.debug(`Drained ${drained} bytes from input buffer`);
1666
+ }
1667
+
1668
+ // Final clear of application buffer
1669
+ if (!this._parent) {
1670
+ this.__inputBuffer = [];
1671
+ }
1672
+ }
1673
+
1674
+ /**
1675
+ * @name flushSerialBuffers
1676
+ * Flush any pending data in the TX and RX serial port buffers
1677
+ * This clears both the application RX buffer and waits for hardware buffers to drain
1678
+ */
1679
+ private async flushSerialBuffers(): Promise<void> {
1680
+ // Clear application buffer
1681
+ if (!this._parent) {
1682
+ this.__inputBuffer = [];
1683
+ }
1684
+
1685
+ // Wait for any pending data
1686
+ await sleep(SYNC_TIMEOUT);
1687
+
1688
+ // Final clear
1689
+ if (!this._parent) {
1690
+ this.__inputBuffer = [];
1691
+ }
1692
+
1693
+ this.logger.debug("Serial buffers flushed");
1694
+ }
1695
+
1696
+ /**
1697
+ * @name readFlash
1698
+ * Read flash memory from the chip (only works with stub loader)
1699
+ * @param addr - Address to read from
1700
+ * @param size - Number of bytes to read
1701
+ * @param onPacketReceived - Optional callback function called when packet is received
1702
+ * @returns Uint8Array containing the flash data
1703
+ */
1704
+ async readFlash(
1705
+ addr: number,
1706
+ size: number,
1707
+ onPacketReceived?: (
1708
+ packet: Uint8Array,
1709
+ progress: number,
1710
+ totalSize: number,
1711
+ ) => void,
1712
+ ): Promise<Uint8Array> {
1713
+ if (!this.IS_STUB) {
1714
+ throw new Error(
1715
+ "Reading flash is only supported in stub mode. Please run runStub() first.",
1716
+ );
1717
+ }
1718
+
1719
+ // Flush serial buffers before flash read operation
1720
+ await this.flushSerialBuffers();
1721
+
1722
+ this.logger.log(
1723
+ `Reading ${size} bytes from flash at address 0x${addr.toString(16)}...`,
1724
+ );
1725
+
1726
+ const CHUNK_SIZE = 0x10000; // 64KB chunks
1727
+
1728
+ let allData = new Uint8Array(0);
1729
+ let currentAddr = addr;
1730
+ let remainingSize = size;
1731
+
1732
+ while (remainingSize > 0) {
1733
+ const chunkSize = Math.min(CHUNK_SIZE, remainingSize);
1734
+ let chunkSuccess = false;
1735
+ let retryCount = 0;
1736
+ const MAX_RETRIES = 3;
1737
+
1738
+ // Retry loop for this chunk
1739
+ while (!chunkSuccess && retryCount <= MAX_RETRIES) {
1740
+ try {
1741
+ this.logger.debug(
1742
+ `Reading chunk at 0x${currentAddr.toString(16)}, size: 0x${chunkSize.toString(16)}`,
1743
+ );
1744
+
1745
+ // Send read flash command for this chunk
1746
+ const pkt = pack("<IIII", currentAddr, chunkSize, 0x1000, 1024);
1747
+ const [res] = await this.checkCommand(ESP_READ_FLASH, pkt);
1748
+
1749
+ if (res != 0) {
1750
+ throw new Error("Failed to read memory: " + res);
1751
+ }
1752
+
1753
+ let resp = new Uint8Array(0);
1754
+
1755
+ while (resp.length < chunkSize) {
1756
+ // Read a SLIP packet
1757
+ let packet: number[];
1758
+ try {
1759
+ packet = await this.readPacket(FLASH_READ_TIMEOUT);
1760
+ } catch (err) {
1761
+ if (err instanceof SlipReadError) {
1762
+ this.logger.debug(
1763
+ `SLIP read error at ${resp.length} bytes: ${err.message}`,
1764
+ );
1765
+ // If we've read all the data we need, break
1766
+ if (resp.length >= chunkSize) {
1767
+ break;
1768
+ }
1769
+ }
1770
+ throw err;
1771
+ }
1772
+
1773
+ if (packet && packet.length > 0) {
1774
+ const packetData = new Uint8Array(packet);
1775
+
1776
+ // Append to response
1777
+ const newResp = new Uint8Array(resp.length + packetData.length);
1778
+ newResp.set(resp);
1779
+ newResp.set(packetData, resp.length);
1780
+ resp = newResp;
1781
+
1782
+ // Send acknowledgment
1783
+ const ackData = pack("<I", resp.length);
1784
+ const slipEncodedAck = slipEncode(ackData);
1785
+ await this.writeToStream(slipEncodedAck);
1786
+ }
1787
+ }
1788
+
1789
+ // Chunk read successfully - append to all data
1790
+ const newAllData = new Uint8Array(allData.length + resp.length);
1791
+ newAllData.set(allData);
1792
+ newAllData.set(resp, allData.length);
1793
+ allData = newAllData;
1794
+
1795
+ chunkSuccess = true;
1796
+ } catch (err) {
1797
+ retryCount++;
1798
+
1799
+ // Check if it's a timeout error
1800
+ if (
1801
+ err instanceof SlipReadError &&
1802
+ err.message.includes("Timed out")
1803
+ ) {
1804
+ if (retryCount <= MAX_RETRIES) {
1805
+ this.logger.log(
1806
+ `⚠️ Timeout error at 0x${currentAddr.toString(16)}. Reconnecting and retrying (attempt ${retryCount}/${MAX_RETRIES})...`,
1807
+ );
1808
+
1809
+ try {
1810
+ await this.reconnect();
1811
+ // Continue to retry the same chunk
1812
+ } catch (reconnectErr) {
1813
+ throw new Error(`Reconnect failed: ${reconnectErr}`);
1814
+ }
1815
+ } else {
1816
+ throw new Error(
1817
+ `Failed to read chunk at 0x${currentAddr.toString(16)} after ${MAX_RETRIES} retries: ${err}`,
1818
+ );
1819
+ }
1820
+ } else {
1821
+ // Non-timeout error, don't retry
1822
+ throw err;
1823
+ }
1824
+ }
1825
+ }
1826
+
1827
+ // Update progress (use empty array since we already appended to allData)
1828
+ if (onPacketReceived) {
1829
+ onPacketReceived(new Uint8Array(chunkSize), allData.length, size);
1830
+ }
1831
+
1832
+ currentAddr += chunkSize;
1833
+ remainingSize -= chunkSize;
1834
+
1835
+ this.logger.debug(
1836
+ `Total progress: 0x${allData.length.toString(16)} from 0x${size.toString(16)} bytes`,
1837
+ );
1838
+ }
1839
+
1840
+ this.logger.debug(`Successfully read ${allData.length} bytes from flash`);
1841
+ return allData;
1842
+ }
1843
+ }
1844
+
1845
+ class EspStubLoader extends ESPLoader {
1846
+ /*
1847
+ The Stubloader has commands that run on the uploaded Stub Code in RAM
1848
+ rather than built in commands.
1849
+ */
1850
+ IS_STUB = true;
1851
+
1852
+ /**
1853
+ * @name memBegin (592)
1854
+ * Start downloading an application image to RAM
1855
+ */
1856
+ async memBegin(
1857
+ size: number,
1858
+ _blocks: number,
1859
+ _blocksize: number,
1860
+ offset: number,
1861
+ ): Promise<[number, number[]]> {
1862
+ const stub = await getStubCode(this.chipFamily, this.chipRevision);
1863
+
1864
+ // Stub may be null for chips without stub support
1865
+ if (stub === null) {
1866
+ return [0, []];
1867
+ }
1868
+
1869
+ const load_start = offset;
1870
+ const load_end = offset + size;
1871
+ this.logger.debug(
1872
+ `Load range: ${toHex(load_start, 8)}-${toHex(load_end, 8)}`,
1873
+ );
1874
+ this.logger.debug(
1875
+ `Stub data: ${toHex(stub.data_start, 8)}, len: ${stub.data.length}, text: ${toHex(stub.text_start, 8)}, len: ${stub.text.length}`,
1876
+ );
1877
+ for (const [start, end] of [
1878
+ [stub.data_start, stub.data_start + stub.data.length],
1879
+ [stub.text_start, stub.text_start + stub.text.length],
1880
+ ]) {
1881
+ if (load_start < end && load_end > start) {
1882
+ throw new Error(
1883
+ "Software loader is resident at " +
1884
+ toHex(start, 8) +
1885
+ "-" +
1886
+ toHex(end, 8) +
1887
+ ". " +
1888
+ "Can't load binary at overlapping address range " +
1889
+ toHex(load_start, 8) +
1890
+ "-" +
1891
+ toHex(load_end, 8) +
1892
+ ". " +
1893
+ "Try changing the binary loading address.",
1894
+ );
1895
+ }
1896
+ }
1897
+ return [0, []];
1898
+ }
1899
+
1900
+ /**
1901
+ * @name getEraseSize
1902
+ * depending on flash chip model the erase may take this long (maybe longer!)
1903
+ */
1904
+ async eraseFlash() {
1905
+ await this.checkCommand(ESP_ERASE_FLASH, [], 0, CHIP_ERASE_TIMEOUT);
1906
+ }
1907
+ }