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.
- package/README.md +31 -0
- package/css/dark.css +156 -0
- package/css/light.css +156 -0
- package/css/style.css +870 -0
- package/dist/const.d.ts +277 -0
- package/dist/const.js +511 -0
- package/dist/esp_loader.d.ts +222 -0
- package/dist/esp_loader.js +1466 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +15 -0
- package/dist/lib/spiffs/index.d.ts +15 -0
- package/dist/lib/spiffs/index.js +16 -0
- package/dist/lib/spiffs/spiffs.d.ts +26 -0
- package/dist/lib/spiffs/spiffs.js +132 -0
- package/dist/lib/spiffs/spiffsBlock.d.ts +36 -0
- package/dist/lib/spiffs/spiffsBlock.js +140 -0
- package/dist/lib/spiffs/spiffsConfig.d.ts +63 -0
- package/dist/lib/spiffs/spiffsConfig.js +79 -0
- package/dist/lib/spiffs/spiffsPage.d.ts +45 -0
- package/dist/lib/spiffs/spiffsPage.js +260 -0
- package/dist/lib/spiffs/spiffsReader.d.ts +19 -0
- package/dist/lib/spiffs/spiffsReader.js +192 -0
- package/dist/partition.d.ts +26 -0
- package/dist/partition.js +129 -0
- package/dist/struct.d.ts +2 -0
- package/dist/struct.js +91 -0
- package/dist/stubs/esp32.json +8 -0
- package/dist/stubs/esp32c2.json +8 -0
- package/dist/stubs/esp32c3.json +8 -0
- package/dist/stubs/esp32c5.json +8 -0
- package/dist/stubs/esp32c6.json +8 -0
- package/dist/stubs/esp32c61.json +8 -0
- package/dist/stubs/esp32h2.json +8 -0
- package/dist/stubs/esp32p4.json +8 -0
- package/dist/stubs/esp32p4r3.json +8 -0
- package/dist/stubs/esp32s2.json +8 -0
- package/dist/stubs/esp32s3.json +8 -0
- package/dist/stubs/esp8266.json +8 -0
- package/dist/stubs/index.d.ts +10 -0
- package/dist/stubs/index.js +56 -0
- package/dist/util.d.ts +14 -0
- package/dist/util.js +46 -0
- package/dist/wasm/filesystems.d.ts +33 -0
- package/dist/wasm/filesystems.js +114 -0
- package/dist/web/esp32-D955RjN9.js +16 -0
- package/dist/web/esp32c2-CJkxHDQi.js +16 -0
- package/dist/web/esp32c3-BhUHzH0o.js +16 -0
- package/dist/web/esp32c5-Chs0HtmA.js +16 -0
- package/dist/web/esp32c6-D6mPN6ut.js +16 -0
- package/dist/web/esp32c61-CQiYCWAs.js +16 -0
- package/dist/web/esp32h2-LsKJE9AS.js +16 -0
- package/dist/web/esp32p4-7nWC-HiD.js +16 -0
- package/dist/web/esp32p4r3-CwiPecZW.js +16 -0
- package/dist/web/esp32s2-CtqVheSJ.js +16 -0
- package/dist/web/esp32s3-CRbtB0QR.js +16 -0
- package/dist/web/esp8266-nEkNAo8K.js +16 -0
- package/dist/web/index.js +7265 -0
- package/electron/main.js +333 -0
- package/electron/preload.js +37 -0
- package/eslint.config.js +22 -0
- package/index.html +408 -0
- package/js/modules/esp32-D955RjN9.js +16 -0
- package/js/modules/esp32c2-CJkxHDQi.js +16 -0
- package/js/modules/esp32c3-BhUHzH0o.js +16 -0
- package/js/modules/esp32c5-Chs0HtmA.js +16 -0
- package/js/modules/esp32c6-D6mPN6ut.js +16 -0
- package/js/modules/esp32c61-CQiYCWAs.js +16 -0
- package/js/modules/esp32h2-LsKJE9AS.js +16 -0
- package/js/modules/esp32p4-7nWC-HiD.js +16 -0
- package/js/modules/esp32p4r3-CwiPecZW.js +16 -0
- package/js/modules/esp32s2-CtqVheSJ.js +16 -0
- package/js/modules/esp32s3-CRbtB0QR.js +16 -0
- package/js/modules/esp8266-nEkNAo8K.js +16 -0
- package/js/modules/esptool.js +7265 -0
- package/js/script.js +2237 -0
- package/js/utilities.js +182 -0
- package/license.md +11 -0
- package/package.json +61 -0
- package/script/build +12 -0
- package/script/develop +17 -0
- package/src/const.ts +599 -0
- package/src/esp_loader.ts +1907 -0
- package/src/index.ts +63 -0
- package/src/lib/spiffs/index.ts +22 -0
- package/src/lib/spiffs/spiffs.ts +175 -0
- package/src/lib/spiffs/spiffsBlock.ts +204 -0
- package/src/lib/spiffs/spiffsConfig.ts +140 -0
- package/src/lib/spiffs/spiffsPage.ts +357 -0
- package/src/lib/spiffs/spiffsReader.ts +280 -0
- package/src/partition.ts +155 -0
- package/src/struct.ts +108 -0
- package/src/stubs/README.md +3 -0
- package/src/stubs/esp32.json +8 -0
- package/src/stubs/esp32c2.json +8 -0
- package/src/stubs/esp32c3.json +8 -0
- package/src/stubs/esp32c5.json +8 -0
- package/src/stubs/esp32c6.json +8 -0
- package/src/stubs/esp32c61.json +8 -0
- package/src/stubs/esp32h2.json +8 -0
- package/src/stubs/esp32p4.json +8 -0
- package/src/stubs/esp32p4r3.json +8 -0
- package/src/stubs/esp32s2.json +8 -0
- package/src/stubs/esp32s3.json +8 -0
- package/src/stubs/esp8266.json +8 -0
- package/src/stubs/index.ts +86 -0
- package/src/util.ts +49 -0
- package/src/wasm/fatfs/fatfs.wasm +0 -0
- package/src/wasm/fatfs/index.d.ts +26 -0
- package/src/wasm/fatfs/index.js +343 -0
- package/src/wasm/filesystems.ts +156 -0
- package/src/wasm/littlefs/index.d.ts +83 -0
- package/src/wasm/littlefs/index.js +529 -0
- package/src/wasm/littlefs/littlefs.js +2 -0
- package/src/wasm/littlefs/littlefs.wasm +0 -0
- 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
|
+
}
|