tasmota-webserial-esptool 7.2.6 → 7.3.1
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/dist/esp_loader.d.ts +13 -0
- package/dist/esp_loader.js +342 -176
- package/dist/stubs/esp32.json +3 -3
- package/dist/stubs/esp32c2.json +3 -3
- package/dist/stubs/esp32c3.json +4 -4
- package/dist/stubs/esp32c5.json +4 -4
- package/dist/stubs/esp32c6.json +4 -4
- package/dist/stubs/esp32c61.json +4 -4
- package/dist/stubs/esp32h2.json +4 -4
- package/dist/stubs/esp32p4.json +4 -4
- package/dist/stubs/esp32p4r3.json +4 -4
- package/dist/stubs/esp32s2.json +3 -3
- package/dist/stubs/esp32s3.json +4 -4
- package/dist/web/esp32-BRKoi17y.js +1 -0
- package/dist/web/esp32c2-Btgr_lwh.js +1 -0
- package/dist/web/esp32c3-BGQu6Tl5.js +1 -0
- package/dist/web/esp32c5-0b050IXn.js +1 -0
- package/dist/web/esp32c6-D9SxtU9b.js +1 -0
- package/dist/web/esp32c61-B2dSOrao.js +1 -0
- package/dist/web/esp32h2-BBdaXb2C.js +1 -0
- package/dist/web/esp32p4-BLGlFHot.js +1 -0
- package/dist/web/esp32p4r3-CEI3EOJv.js +1 -0
- package/dist/web/esp32s2-iX3WoDbg.js +1 -0
- package/dist/web/esp32s3-BUw3lf0r.js +1 -0
- package/dist/web/index.js +1 -1
- package/js/modules/esp32-BRKoi17y.js +1 -0
- package/js/modules/esp32c2-Btgr_lwh.js +1 -0
- package/js/modules/esp32c3-BGQu6Tl5.js +1 -0
- package/js/modules/esp32c5-0b050IXn.js +1 -0
- package/js/modules/esp32c6-D9SxtU9b.js +1 -0
- package/js/modules/esp32c61-B2dSOrao.js +1 -0
- package/js/modules/esp32h2-BBdaXb2C.js +1 -0
- package/js/modules/esp32p4-BLGlFHot.js +1 -0
- package/js/modules/esp32p4r3-CEI3EOJv.js +1 -0
- package/js/modules/esp32s2-iX3WoDbg.js +1 -0
- package/js/modules/esp32s3-BUw3lf0r.js +1 -0
- package/js/modules/esptool.js +1 -1
- package/package.json +1 -1
- package/src/esp_loader.ts +370 -187
- package/src/stubs/esp32.json +3 -3
- package/src/stubs/esp32c2.json +3 -3
- package/src/stubs/esp32c3.json +4 -4
- package/src/stubs/esp32c5.json +4 -4
- package/src/stubs/esp32c6.json +4 -4
- package/src/stubs/esp32c61.json +4 -4
- package/src/stubs/esp32h2.json +4 -4
- package/src/stubs/esp32p4.json +4 -4
- package/src/stubs/esp32p4r3.json +4 -4
- package/src/stubs/esp32s2.json +3 -3
- package/src/stubs/esp32s3.json +4 -4
- package/dist/web/esp32-CijhsJH1.js +0 -1
- package/dist/web/esp32c2-C17SM4gO.js +0 -1
- package/dist/web/esp32c3-DxRGijbg.js +0 -1
- package/dist/web/esp32c5-3mDOIGa4.js +0 -1
- package/dist/web/esp32c6-h6U0SQTm.js +0 -1
- package/dist/web/esp32c61-BKtexhPZ.js +0 -1
- package/dist/web/esp32h2-RtuWSEmP.js +0 -1
- package/dist/web/esp32p4-5nkIjxqJ.js +0 -1
- package/dist/web/esp32p4r3-CpHBYEwI.js +0 -1
- package/dist/web/esp32s2-IiDBtXxo.js +0 -1
- package/dist/web/esp32s3-6yv5yxum.js +0 -1
- package/js/modules/esp32-CijhsJH1.js +0 -1
- package/js/modules/esp32c2-C17SM4gO.js +0 -1
- package/js/modules/esp32c3-DxRGijbg.js +0 -1
- package/js/modules/esp32c5-3mDOIGa4.js +0 -1
- package/js/modules/esp32c6-h6U0SQTm.js +0 -1
- package/js/modules/esp32c61-BKtexhPZ.js +0 -1
- package/js/modules/esp32h2-RtuWSEmP.js +0 -1
- package/js/modules/esp32p4-5nkIjxqJ.js +0 -1
- package/js/modules/esp32p4r3-CpHBYEwI.js +0 -1
- package/js/modules/esp32s2-IiDBtXxo.js +0 -1
- package/js/modules/esp32s3-6yv5yxum.js +0 -1
package/dist/esp_loader.js
CHANGED
|
@@ -23,7 +23,10 @@ export class ESPLoader extends EventTarget {
|
|
|
23
23
|
this._currentBaudRate = ESP_ROM_BAUD;
|
|
24
24
|
this._isESP32S2NativeUSB = false;
|
|
25
25
|
this._initializationSucceeded = false;
|
|
26
|
+
this.__commandLock = Promise.resolve([0, []]);
|
|
27
|
+
this._isReconfiguring = false;
|
|
26
28
|
this.state_DTR = false;
|
|
29
|
+
this.__writeChain = Promise.resolve();
|
|
27
30
|
}
|
|
28
31
|
get _inputBuffer() {
|
|
29
32
|
return this._parent ? this._parent._inputBuffer : this.__inputBuffer;
|
|
@@ -41,6 +44,17 @@ export class ESPLoader extends EventTarget {
|
|
|
41
44
|
this.__totalBytesRead = value;
|
|
42
45
|
}
|
|
43
46
|
}
|
|
47
|
+
get _commandLock() {
|
|
48
|
+
return this._parent ? this._parent._commandLock : this.__commandLock;
|
|
49
|
+
}
|
|
50
|
+
set _commandLock(value) {
|
|
51
|
+
if (this._parent) {
|
|
52
|
+
this._parent._commandLock = value;
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
this.__commandLock = value;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
44
58
|
detectUSBSerialChip(vendorId, productId) {
|
|
45
59
|
// Common USB-Serial chip vendors and their products
|
|
46
60
|
const chips = {
|
|
@@ -393,68 +407,78 @@ export class ESPLoader extends EventTarget {
|
|
|
393
407
|
* Send a command packet, check that the command succeeded and
|
|
394
408
|
* return a tuple with the value and data.
|
|
395
409
|
* See the ESP Serial Protocol for more details on what value/data are
|
|
410
|
+
*
|
|
411
|
+
* Commands are serialized to prevent concurrent execution which can cause
|
|
412
|
+
* WritableStream lock contention on CP210x adapters under Windows
|
|
396
413
|
*/
|
|
397
414
|
async checkCommand(opcode, buffer, checksum = 0, timeout = DEFAULT_TIMEOUT) {
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
let statusLen = 0;
|
|
406
|
-
if (this.IS_STUB || this.chipFamily == CHIP_FAMILY_ESP8266) {
|
|
407
|
-
statusLen = 2;
|
|
408
|
-
}
|
|
409
|
-
else if ([
|
|
410
|
-
CHIP_FAMILY_ESP32,
|
|
411
|
-
CHIP_FAMILY_ESP32S2,
|
|
412
|
-
CHIP_FAMILY_ESP32S3,
|
|
413
|
-
CHIP_FAMILY_ESP32C2,
|
|
414
|
-
CHIP_FAMILY_ESP32C3,
|
|
415
|
-
CHIP_FAMILY_ESP32C5,
|
|
416
|
-
CHIP_FAMILY_ESP32C6,
|
|
417
|
-
CHIP_FAMILY_ESP32C61,
|
|
418
|
-
CHIP_FAMILY_ESP32H2,
|
|
419
|
-
CHIP_FAMILY_ESP32H4,
|
|
420
|
-
CHIP_FAMILY_ESP32H21,
|
|
421
|
-
CHIP_FAMILY_ESP32P4,
|
|
422
|
-
CHIP_FAMILY_ESP32S31,
|
|
423
|
-
].includes(this.chipFamily)) {
|
|
424
|
-
statusLen = 4;
|
|
425
|
-
}
|
|
426
|
-
else {
|
|
427
|
-
// When chipFamily is not yet set (e.g., during GET_SECURITY_INFO in detectChip),
|
|
428
|
-
// assume modern chips use 4-byte status
|
|
429
|
-
if (opcode === ESP_GET_SECURITY_INFO) {
|
|
430
|
-
statusLen = 4;
|
|
415
|
+
// Serialize command execution to prevent lock contention
|
|
416
|
+
const executeCommand = async () => {
|
|
417
|
+
timeout = Math.min(timeout, MAX_TIMEOUT);
|
|
418
|
+
await this.sendCommand(opcode, buffer, checksum);
|
|
419
|
+
const [value, responseData] = await this.getResponse(opcode, timeout);
|
|
420
|
+
if (responseData === null) {
|
|
421
|
+
throw new Error("Didn't get enough status bytes");
|
|
431
422
|
}
|
|
432
|
-
|
|
433
|
-
|
|
423
|
+
let data = responseData;
|
|
424
|
+
let statusLen = 0;
|
|
425
|
+
if (this.IS_STUB || this.chipFamily == CHIP_FAMILY_ESP8266) {
|
|
426
|
+
statusLen = 2;
|
|
434
427
|
}
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
throw new Error("Invalid (unsupported) command " + toHex(opcode));
|
|
428
|
+
else if ([
|
|
429
|
+
CHIP_FAMILY_ESP32,
|
|
430
|
+
CHIP_FAMILY_ESP32S2,
|
|
431
|
+
CHIP_FAMILY_ESP32S3,
|
|
432
|
+
CHIP_FAMILY_ESP32C2,
|
|
433
|
+
CHIP_FAMILY_ESP32C3,
|
|
434
|
+
CHIP_FAMILY_ESP32C5,
|
|
435
|
+
CHIP_FAMILY_ESP32C6,
|
|
436
|
+
CHIP_FAMILY_ESP32C61,
|
|
437
|
+
CHIP_FAMILY_ESP32H2,
|
|
438
|
+
CHIP_FAMILY_ESP32H4,
|
|
439
|
+
CHIP_FAMILY_ESP32H21,
|
|
440
|
+
CHIP_FAMILY_ESP32P4,
|
|
441
|
+
CHIP_FAMILY_ESP32S31,
|
|
442
|
+
].includes(this.chipFamily)) {
|
|
443
|
+
statusLen = 4;
|
|
452
444
|
}
|
|
453
445
|
else {
|
|
454
|
-
|
|
446
|
+
// When chipFamily is not yet set (e.g., during GET_SECURITY_INFO in detectChip),
|
|
447
|
+
// assume modern chips use 4-byte status
|
|
448
|
+
if (opcode === ESP_GET_SECURITY_INFO) {
|
|
449
|
+
statusLen = 4;
|
|
450
|
+
}
|
|
451
|
+
else if ([2, 4].includes(data.length)) {
|
|
452
|
+
statusLen = data.length;
|
|
453
|
+
}
|
|
455
454
|
}
|
|
456
|
-
|
|
457
|
-
|
|
455
|
+
if (data.length < statusLen) {
|
|
456
|
+
throw new Error("Didn't get enough status bytes");
|
|
457
|
+
}
|
|
458
|
+
const status = data.slice(-statusLen, data.length);
|
|
459
|
+
data = data.slice(0, -statusLen);
|
|
460
|
+
if (this.debug) {
|
|
461
|
+
this.logger.debug("status", status);
|
|
462
|
+
this.logger.debug("value", value);
|
|
463
|
+
this.logger.debug("data", data);
|
|
464
|
+
}
|
|
465
|
+
if (status[0] == 1) {
|
|
466
|
+
if (status[1] == ROM_INVALID_RECV_MSG) {
|
|
467
|
+
// Unsupported command can result in more than one error response
|
|
468
|
+
// Use drainInputBuffer for CP210x compatibility on Windows
|
|
469
|
+
await this.drainInputBuffer(200);
|
|
470
|
+
throw new Error("Invalid (unsupported) command " + toHex(opcode));
|
|
471
|
+
}
|
|
472
|
+
else {
|
|
473
|
+
throw new Error("Command failure error code " + toHex(status[1]));
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
return [value, data];
|
|
477
|
+
};
|
|
478
|
+
// Chain command execution through the lock
|
|
479
|
+
// Use both .then() handlers to ensure lock continues even on error
|
|
480
|
+
this._commandLock = this._commandLock.then(executeCommand, executeCommand);
|
|
481
|
+
return this._commandLock;
|
|
458
482
|
}
|
|
459
483
|
/**
|
|
460
484
|
* @name sendCommand
|
|
@@ -479,27 +503,22 @@ export class ESPLoader extends EventTarget {
|
|
|
479
503
|
async readPacket(timeout) {
|
|
480
504
|
let partialPacket = null;
|
|
481
505
|
let inEscape = false;
|
|
482
|
-
|
|
506
|
+
const startTime = Date.now();
|
|
483
507
|
while (true) {
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
while (Date.now() - stamp < timeout) {
|
|
487
|
-
if (this._inputBuffer.length > 0) {
|
|
488
|
-
readBytes.push(this._inputBuffer.shift());
|
|
489
|
-
break;
|
|
490
|
-
}
|
|
491
|
-
else {
|
|
492
|
-
// Reduced sleep time for faster response during high-speed transfers
|
|
493
|
-
await sleep(1);
|
|
494
|
-
}
|
|
495
|
-
}
|
|
496
|
-
if (readBytes.length == 0) {
|
|
508
|
+
// Check timeout
|
|
509
|
+
if (Date.now() - startTime > timeout) {
|
|
497
510
|
const waitingFor = partialPacket === null ? "header" : "content";
|
|
498
511
|
throw new SlipReadError("Timed out waiting for packet " + waitingFor);
|
|
499
512
|
}
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
513
|
+
// If no data available, wait a bit
|
|
514
|
+
if (this._inputBuffer.length === 0) {
|
|
515
|
+
await sleep(1);
|
|
516
|
+
continue;
|
|
517
|
+
}
|
|
518
|
+
// Process all available bytes without going back to outer loop
|
|
519
|
+
// This is critical for handling high-speed burst transfers
|
|
520
|
+
while (this._inputBuffer.length > 0) {
|
|
521
|
+
const b = this._inputBuffer.shift();
|
|
503
522
|
if (partialPacket === null) {
|
|
504
523
|
// waiting for packet header
|
|
505
524
|
if (b == 0xc0) {
|
|
@@ -507,7 +526,7 @@ export class ESPLoader extends EventTarget {
|
|
|
507
526
|
}
|
|
508
527
|
else {
|
|
509
528
|
if (this.debug) {
|
|
510
|
-
this.logger.debug("Read invalid data: " +
|
|
529
|
+
this.logger.debug("Read invalid data: " + toHex(b));
|
|
511
530
|
this.logger.debug("Remaining data in serial buffer: " +
|
|
512
531
|
hexFormatter(this._inputBuffer));
|
|
513
532
|
}
|
|
@@ -525,7 +544,7 @@ export class ESPLoader extends EventTarget {
|
|
|
525
544
|
}
|
|
526
545
|
else {
|
|
527
546
|
if (this.debug) {
|
|
528
|
-
this.logger.debug("Read invalid data: " +
|
|
547
|
+
this.logger.debug("Read invalid data: " + toHex(b));
|
|
529
548
|
this.logger.debug("Remaining data in serial buffer: " +
|
|
530
549
|
hexFormatter(this._inputBuffer));
|
|
531
550
|
}
|
|
@@ -548,7 +567,6 @@ export class ESPLoader extends EventTarget {
|
|
|
548
567
|
}
|
|
549
568
|
}
|
|
550
569
|
}
|
|
551
|
-
throw new SlipReadError("Invalid state");
|
|
552
570
|
}
|
|
553
571
|
/**
|
|
554
572
|
* @name getResponse
|
|
@@ -630,6 +648,24 @@ export class ESPLoader extends EventTarget {
|
|
|
630
648
|
async reconfigurePort(baud) {
|
|
631
649
|
var _a;
|
|
632
650
|
try {
|
|
651
|
+
this._isReconfiguring = true;
|
|
652
|
+
// Wait for pending writes to complete
|
|
653
|
+
try {
|
|
654
|
+
await this._writeChain;
|
|
655
|
+
}
|
|
656
|
+
catch (err) {
|
|
657
|
+
this.logger.debug(`Pending write error during reconfigure: ${err}`);
|
|
658
|
+
}
|
|
659
|
+
// Release persistent writer before closing
|
|
660
|
+
if (this._writer) {
|
|
661
|
+
try {
|
|
662
|
+
this._writer.releaseLock();
|
|
663
|
+
}
|
|
664
|
+
catch (err) {
|
|
665
|
+
this.logger.debug(`Writer release error during reconfigure: ${err}`);
|
|
666
|
+
}
|
|
667
|
+
this._writer = undefined;
|
|
668
|
+
}
|
|
633
669
|
// SerialPort does not allow to be reconfigured while open so we close and re-open
|
|
634
670
|
// reader.cancel() causes the Promise returned by the read() operation running on
|
|
635
671
|
// the readLoop to return immediately with { value: undefined, done: true } and thus
|
|
@@ -647,6 +683,9 @@ export class ESPLoader extends EventTarget {
|
|
|
647
683
|
this.logger.error(`Reconfigure port error: ${e}`);
|
|
648
684
|
throw new Error(`Unable to change the baud rate to ${baud}: ${e}`);
|
|
649
685
|
}
|
|
686
|
+
finally {
|
|
687
|
+
this._isReconfiguring = false;
|
|
688
|
+
}
|
|
650
689
|
}
|
|
651
690
|
/**
|
|
652
691
|
* @name connectWithResetStrategies
|
|
@@ -1206,19 +1245,83 @@ export class ESPLoader extends EventTarget {
|
|
|
1206
1245
|
}
|
|
1207
1246
|
return espStubLoader;
|
|
1208
1247
|
}
|
|
1248
|
+
get _writer() {
|
|
1249
|
+
return this._parent ? this._parent._writer : this.__writer;
|
|
1250
|
+
}
|
|
1251
|
+
set _writer(value) {
|
|
1252
|
+
if (this._parent) {
|
|
1253
|
+
this._parent._writer = value;
|
|
1254
|
+
}
|
|
1255
|
+
else {
|
|
1256
|
+
this.__writer = value;
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
get _writeChain() {
|
|
1260
|
+
return this._parent ? this._parent._writeChain : this.__writeChain;
|
|
1261
|
+
}
|
|
1262
|
+
set _writeChain(value) {
|
|
1263
|
+
if (this._parent) {
|
|
1264
|
+
this._parent._writeChain = value;
|
|
1265
|
+
}
|
|
1266
|
+
else {
|
|
1267
|
+
this.__writeChain = value;
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1209
1270
|
async writeToStream(data) {
|
|
1210
1271
|
if (!this.port.writable) {
|
|
1211
1272
|
this.logger.debug("Port writable stream not available, skipping write");
|
|
1212
1273
|
return;
|
|
1213
1274
|
}
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
try {
|
|
1217
|
-
writer.releaseLock();
|
|
1218
|
-
}
|
|
1219
|
-
catch (err) {
|
|
1220
|
-
this.logger.error(`Ignoring release lock error: ${err}`);
|
|
1275
|
+
if (this._isReconfiguring) {
|
|
1276
|
+
throw new Error("Cannot write during port reconfiguration");
|
|
1221
1277
|
}
|
|
1278
|
+
// Queue writes to prevent lock contention (critical for CP2102 on Windows)
|
|
1279
|
+
this._writeChain = this._writeChain
|
|
1280
|
+
.then(async () => {
|
|
1281
|
+
// Check if port is still writable before attempting write
|
|
1282
|
+
if (!this.port.writable) {
|
|
1283
|
+
throw new Error("Port became unavailable during write");
|
|
1284
|
+
}
|
|
1285
|
+
// Get or create persistent writer
|
|
1286
|
+
if (!this._writer) {
|
|
1287
|
+
try {
|
|
1288
|
+
this._writer = this.port.writable.getWriter();
|
|
1289
|
+
}
|
|
1290
|
+
catch (err) {
|
|
1291
|
+
this.logger.error(`Failed to get writer: ${err}`);
|
|
1292
|
+
throw err;
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1295
|
+
// Perform the write
|
|
1296
|
+
await this._writer.write(new Uint8Array(data));
|
|
1297
|
+
}, async () => {
|
|
1298
|
+
// Previous write failed, but still attempt this write
|
|
1299
|
+
if (!this.port.writable) {
|
|
1300
|
+
throw new Error("Port became unavailable during write");
|
|
1301
|
+
}
|
|
1302
|
+
// Writer was likely cleaned up by previous error, create new one
|
|
1303
|
+
if (!this._writer) {
|
|
1304
|
+
this._writer = this.port.writable.getWriter();
|
|
1305
|
+
}
|
|
1306
|
+
await this._writer.write(new Uint8Array(data));
|
|
1307
|
+
})
|
|
1308
|
+
.catch((err) => {
|
|
1309
|
+
this.logger.error(`Write error: ${err}`);
|
|
1310
|
+
// Ensure writer is cleaned up on any error
|
|
1311
|
+
if (this._writer) {
|
|
1312
|
+
try {
|
|
1313
|
+
this._writer.releaseLock();
|
|
1314
|
+
}
|
|
1315
|
+
catch (e) {
|
|
1316
|
+
// Ignore release errors
|
|
1317
|
+
}
|
|
1318
|
+
this._writer = undefined;
|
|
1319
|
+
}
|
|
1320
|
+
// Re-throw to propagate error
|
|
1321
|
+
throw err;
|
|
1322
|
+
});
|
|
1323
|
+
// Always await the write chain to ensure errors are caught
|
|
1324
|
+
await this._writeChain;
|
|
1222
1325
|
}
|
|
1223
1326
|
async disconnect() {
|
|
1224
1327
|
if (this._parent) {
|
|
@@ -1229,15 +1332,50 @@ export class ESPLoader extends EventTarget {
|
|
|
1229
1332
|
this.logger.debug("Port already closed, skipping disconnect");
|
|
1230
1333
|
return;
|
|
1231
1334
|
}
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1335
|
+
try {
|
|
1336
|
+
this._isReconfiguring = true;
|
|
1337
|
+
// Wait for pending writes to complete
|
|
1338
|
+
try {
|
|
1339
|
+
await this._writeChain;
|
|
1236
1340
|
}
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1341
|
+
catch (err) {
|
|
1342
|
+
this.logger.debug(`Pending write error during disconnect: ${err}`);
|
|
1343
|
+
}
|
|
1344
|
+
// Release persistent writer before closing
|
|
1345
|
+
if (this._writer) {
|
|
1346
|
+
try {
|
|
1347
|
+
await this._writer.close();
|
|
1348
|
+
this._writer.releaseLock();
|
|
1349
|
+
}
|
|
1350
|
+
catch (err) {
|
|
1351
|
+
this.logger.debug(`Writer close/release error: ${err}`);
|
|
1352
|
+
}
|
|
1353
|
+
this._writer = undefined;
|
|
1354
|
+
}
|
|
1355
|
+
else {
|
|
1356
|
+
// No persistent writer exists, close stream directly
|
|
1357
|
+
// This path is taken when no writes have been queued
|
|
1358
|
+
try {
|
|
1359
|
+
const writer = this.port.writable.getWriter();
|
|
1360
|
+
await writer.close();
|
|
1361
|
+
writer.releaseLock();
|
|
1362
|
+
}
|
|
1363
|
+
catch (err) {
|
|
1364
|
+
this.logger.debug(`Direct writer close error: ${err}`);
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1367
|
+
await new Promise((resolve) => {
|
|
1368
|
+
if (!this._reader) {
|
|
1369
|
+
resolve(undefined);
|
|
1370
|
+
}
|
|
1371
|
+
this.addEventListener("disconnect", resolve, { once: true });
|
|
1372
|
+
this._reader.cancel();
|
|
1373
|
+
});
|
|
1374
|
+
this.connected = false;
|
|
1375
|
+
}
|
|
1376
|
+
finally {
|
|
1377
|
+
this._isReconfiguring = false;
|
|
1378
|
+
}
|
|
1241
1379
|
}
|
|
1242
1380
|
/**
|
|
1243
1381
|
* @name reconnectAndResume
|
|
@@ -1248,82 +1386,105 @@ export class ESPLoader extends EventTarget {
|
|
|
1248
1386
|
await this._parent.reconnect();
|
|
1249
1387
|
return;
|
|
1250
1388
|
}
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1389
|
+
try {
|
|
1390
|
+
this._isReconfiguring = true;
|
|
1391
|
+
this.logger.log("Reconnecting serial port...");
|
|
1392
|
+
this.connected = false;
|
|
1393
|
+
this.__inputBuffer = [];
|
|
1394
|
+
// Wait for pending writes to complete
|
|
1256
1395
|
try {
|
|
1257
|
-
await this.
|
|
1396
|
+
await this._writeChain;
|
|
1258
1397
|
}
|
|
1259
1398
|
catch (err) {
|
|
1260
|
-
this.logger.debug(`
|
|
1399
|
+
this.logger.debug(`Pending write error during reconnect: ${err}`);
|
|
1261
1400
|
}
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
await
|
|
1317
|
-
|
|
1401
|
+
// Release persistent writer
|
|
1402
|
+
if (this._writer) {
|
|
1403
|
+
try {
|
|
1404
|
+
this._writer.releaseLock();
|
|
1405
|
+
}
|
|
1406
|
+
catch (err) {
|
|
1407
|
+
this.logger.debug(`Writer release error during reconnect: ${err}`);
|
|
1408
|
+
}
|
|
1409
|
+
this._writer = undefined;
|
|
1410
|
+
}
|
|
1411
|
+
// Cancel reader
|
|
1412
|
+
if (this._reader) {
|
|
1413
|
+
try {
|
|
1414
|
+
await this._reader.cancel();
|
|
1415
|
+
}
|
|
1416
|
+
catch (err) {
|
|
1417
|
+
this.logger.debug(`Reader cancel error: ${err}`);
|
|
1418
|
+
}
|
|
1419
|
+
this._reader = undefined;
|
|
1420
|
+
}
|
|
1421
|
+
// Close port
|
|
1422
|
+
try {
|
|
1423
|
+
await this.port.close();
|
|
1424
|
+
this.logger.log("Port closed");
|
|
1425
|
+
}
|
|
1426
|
+
catch (err) {
|
|
1427
|
+
this.logger.debug(`Port close error: ${err}`);
|
|
1428
|
+
}
|
|
1429
|
+
// Open the port
|
|
1430
|
+
this.logger.debug("Opening port...");
|
|
1431
|
+
try {
|
|
1432
|
+
await this.port.open({ baudRate: ESP_ROM_BAUD });
|
|
1433
|
+
this.connected = true;
|
|
1434
|
+
}
|
|
1435
|
+
catch (err) {
|
|
1436
|
+
throw new Error(`Failed to open port: ${err}`);
|
|
1437
|
+
}
|
|
1438
|
+
// Verify port streams are available
|
|
1439
|
+
if (!this.port.readable || !this.port.writable) {
|
|
1440
|
+
throw new Error(`Port streams not available after open (readable: ${!!this.port.readable}, writable: ${!!this.port.writable})`);
|
|
1441
|
+
}
|
|
1442
|
+
// Save chip info and flash size (no need to detect again)
|
|
1443
|
+
const savedChipFamily = this.chipFamily;
|
|
1444
|
+
const savedChipName = this.chipName;
|
|
1445
|
+
const savedChipRevision = this.chipRevision;
|
|
1446
|
+
const savedChipVariant = this.chipVariant;
|
|
1447
|
+
const savedFlashSize = this.flashSize;
|
|
1448
|
+
// Reinitialize
|
|
1449
|
+
await this.hardReset(true);
|
|
1450
|
+
if (!this._parent) {
|
|
1451
|
+
this.__inputBuffer = [];
|
|
1452
|
+
this.__totalBytesRead = 0;
|
|
1453
|
+
this.readLoop();
|
|
1454
|
+
}
|
|
1455
|
+
await this.flushSerialBuffers();
|
|
1456
|
+
await this.sync();
|
|
1457
|
+
// Restore chip info
|
|
1458
|
+
this.chipFamily = savedChipFamily;
|
|
1459
|
+
this.chipName = savedChipName;
|
|
1460
|
+
this.chipRevision = savedChipRevision;
|
|
1461
|
+
this.chipVariant = savedChipVariant;
|
|
1462
|
+
this.flashSize = savedFlashSize;
|
|
1463
|
+
this.logger.debug(`Reconnect complete (chip: ${this.chipName})`);
|
|
1464
|
+
// Verify port is ready
|
|
1318
1465
|
if (!this.port.writable || !this.port.readable) {
|
|
1319
|
-
throw new Error(
|
|
1466
|
+
throw new Error("Port not ready after reconnect");
|
|
1467
|
+
}
|
|
1468
|
+
// Load stub
|
|
1469
|
+
const stubLoader = await this.runStub(true);
|
|
1470
|
+
this.logger.debug("Stub loaded");
|
|
1471
|
+
// Restore baudrate if it was changed
|
|
1472
|
+
if (this._currentBaudRate !== ESP_ROM_BAUD) {
|
|
1473
|
+
await stubLoader.setBaudrate(this._currentBaudRate);
|
|
1474
|
+
// Verify port is still ready after baudrate change
|
|
1475
|
+
if (!this.port.writable || !this.port.readable) {
|
|
1476
|
+
throw new Error(`Port not ready after baudrate change (readable: ${!!this.port.readable}, writable: ${!!this.port.writable})`);
|
|
1477
|
+
}
|
|
1320
1478
|
}
|
|
1479
|
+
// Copy stub state to this instance if we're a stub loader
|
|
1480
|
+
if (this.IS_STUB) {
|
|
1481
|
+
Object.assign(this, stubLoader);
|
|
1482
|
+
}
|
|
1483
|
+
this.logger.debug("Reconnection successful");
|
|
1321
1484
|
}
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
Object.assign(this, stubLoader);
|
|
1485
|
+
finally {
|
|
1486
|
+
this._isReconfiguring = false;
|
|
1325
1487
|
}
|
|
1326
|
-
this.logger.debug("Reconnection successful");
|
|
1327
1488
|
}
|
|
1328
1489
|
/**
|
|
1329
1490
|
* @name drainInputBuffer
|
|
@@ -1408,13 +1569,17 @@ export class ESPLoader extends EventTarget {
|
|
|
1408
1569
|
const chunkSize = Math.min(CHUNK_SIZE, remainingSize);
|
|
1409
1570
|
let chunkSuccess = false;
|
|
1410
1571
|
let retryCount = 0;
|
|
1411
|
-
const MAX_RETRIES =
|
|
1572
|
+
const MAX_RETRIES = 15;
|
|
1412
1573
|
// Retry loop for this chunk
|
|
1413
1574
|
while (!chunkSuccess && retryCount <= MAX_RETRIES) {
|
|
1414
1575
|
let resp = new Uint8Array(0);
|
|
1415
1576
|
try {
|
|
1416
|
-
|
|
1577
|
+
// Only log on first attempt or retries
|
|
1578
|
+
if (retryCount === 0) {
|
|
1579
|
+
this.logger.debug(`Reading chunk at 0x${currentAddr.toString(16)}, size: 0x${chunkSize.toString(16)}`);
|
|
1580
|
+
}
|
|
1417
1581
|
// Send read flash command for this chunk
|
|
1582
|
+
// This must be inside the retry loop so we send a fresh command after errors
|
|
1418
1583
|
const pkt = pack("<IIII", currentAddr, chunkSize, 0x1000, 1024);
|
|
1419
1584
|
const [res] = await this.checkCommand(ESP_READ_FLASH, pkt);
|
|
1420
1585
|
if (res != 0) {
|
|
@@ -1429,20 +1594,21 @@ export class ESPLoader extends EventTarget {
|
|
|
1429
1594
|
catch (err) {
|
|
1430
1595
|
if (err instanceof SlipReadError) {
|
|
1431
1596
|
this.logger.debug(`SLIP read error at ${resp.length} bytes: ${err.message}`);
|
|
1432
|
-
// Send
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1597
|
+
// Send empty SLIP frame to abort the stub's read operation
|
|
1598
|
+
// The stub expects 4 bytes (ACK), if we send less it will break out
|
|
1599
|
+
try {
|
|
1600
|
+
// Send SLIP frame with no data (just delimiters)
|
|
1601
|
+
const abortFrame = [0xc0, 0xc0]; // Empty SLIP frame
|
|
1602
|
+
await this.writeToStream(abortFrame);
|
|
1603
|
+
this.logger.debug(`Sent abort frame to stub`);
|
|
1604
|
+
// Give stub time to process abort
|
|
1605
|
+
await sleep(50);
|
|
1606
|
+
}
|
|
1607
|
+
catch (abortErr) {
|
|
1608
|
+
this.logger.debug(`Abort frame error: ${abortErr}`);
|
|
1442
1609
|
}
|
|
1443
|
-
// Drain input buffer
|
|
1444
|
-
|
|
1445
|
-
await this.drainInputBuffer(300);
|
|
1610
|
+
// Drain input buffer to clear any stale data
|
|
1611
|
+
await this.drainInputBuffer(200);
|
|
1446
1612
|
// If we've read all the data we need, break
|
|
1447
1613
|
if (resp.length >= chunkSize) {
|
|
1448
1614
|
break;
|
|
@@ -1475,14 +1641,14 @@ export class ESPLoader extends EventTarget {
|
|
|
1475
1641
|
// Check if it's a timeout error or SLIP error
|
|
1476
1642
|
if (err instanceof SlipReadError) {
|
|
1477
1643
|
if (retryCount <= MAX_RETRIES) {
|
|
1478
|
-
this.logger.log(
|
|
1644
|
+
this.logger.log(`${err.message} at 0x${currentAddr.toString(16)}. Draining buffer and retrying (attempt ${retryCount}/${MAX_RETRIES})...`);
|
|
1479
1645
|
try {
|
|
1480
|
-
await this.drainInputBuffer(
|
|
1646
|
+
await this.drainInputBuffer(200);
|
|
1481
1647
|
// Clear application buffer
|
|
1482
1648
|
await this.flushSerialBuffers();
|
|
1483
1649
|
// Wait before retry to let hardware settle
|
|
1484
1650
|
await sleep(SYNC_TIMEOUT);
|
|
1485
|
-
// Continue to retry the same chunk (will send
|
|
1651
|
+
// Continue to retry the same chunk (will send NEW read command)
|
|
1486
1652
|
}
|
|
1487
1653
|
catch (drainErr) {
|
|
1488
1654
|
this.logger.debug(`Buffer drain error: ${drainErr}`);
|