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.
Files changed (72) hide show
  1. package/dist/esp_loader.d.ts +13 -0
  2. package/dist/esp_loader.js +342 -176
  3. package/dist/stubs/esp32.json +3 -3
  4. package/dist/stubs/esp32c2.json +3 -3
  5. package/dist/stubs/esp32c3.json +4 -4
  6. package/dist/stubs/esp32c5.json +4 -4
  7. package/dist/stubs/esp32c6.json +4 -4
  8. package/dist/stubs/esp32c61.json +4 -4
  9. package/dist/stubs/esp32h2.json +4 -4
  10. package/dist/stubs/esp32p4.json +4 -4
  11. package/dist/stubs/esp32p4r3.json +4 -4
  12. package/dist/stubs/esp32s2.json +3 -3
  13. package/dist/stubs/esp32s3.json +4 -4
  14. package/dist/web/esp32-BRKoi17y.js +1 -0
  15. package/dist/web/esp32c2-Btgr_lwh.js +1 -0
  16. package/dist/web/esp32c3-BGQu6Tl5.js +1 -0
  17. package/dist/web/esp32c5-0b050IXn.js +1 -0
  18. package/dist/web/esp32c6-D9SxtU9b.js +1 -0
  19. package/dist/web/esp32c61-B2dSOrao.js +1 -0
  20. package/dist/web/esp32h2-BBdaXb2C.js +1 -0
  21. package/dist/web/esp32p4-BLGlFHot.js +1 -0
  22. package/dist/web/esp32p4r3-CEI3EOJv.js +1 -0
  23. package/dist/web/esp32s2-iX3WoDbg.js +1 -0
  24. package/dist/web/esp32s3-BUw3lf0r.js +1 -0
  25. package/dist/web/index.js +1 -1
  26. package/js/modules/esp32-BRKoi17y.js +1 -0
  27. package/js/modules/esp32c2-Btgr_lwh.js +1 -0
  28. package/js/modules/esp32c3-BGQu6Tl5.js +1 -0
  29. package/js/modules/esp32c5-0b050IXn.js +1 -0
  30. package/js/modules/esp32c6-D9SxtU9b.js +1 -0
  31. package/js/modules/esp32c61-B2dSOrao.js +1 -0
  32. package/js/modules/esp32h2-BBdaXb2C.js +1 -0
  33. package/js/modules/esp32p4-BLGlFHot.js +1 -0
  34. package/js/modules/esp32p4r3-CEI3EOJv.js +1 -0
  35. package/js/modules/esp32s2-iX3WoDbg.js +1 -0
  36. package/js/modules/esp32s3-BUw3lf0r.js +1 -0
  37. package/js/modules/esptool.js +1 -1
  38. package/package.json +1 -1
  39. package/src/esp_loader.ts +370 -187
  40. package/src/stubs/esp32.json +3 -3
  41. package/src/stubs/esp32c2.json +3 -3
  42. package/src/stubs/esp32c3.json +4 -4
  43. package/src/stubs/esp32c5.json +4 -4
  44. package/src/stubs/esp32c6.json +4 -4
  45. package/src/stubs/esp32c61.json +4 -4
  46. package/src/stubs/esp32h2.json +4 -4
  47. package/src/stubs/esp32p4.json +4 -4
  48. package/src/stubs/esp32p4r3.json +4 -4
  49. package/src/stubs/esp32s2.json +3 -3
  50. package/src/stubs/esp32s3.json +4 -4
  51. package/dist/web/esp32-CijhsJH1.js +0 -1
  52. package/dist/web/esp32c2-C17SM4gO.js +0 -1
  53. package/dist/web/esp32c3-DxRGijbg.js +0 -1
  54. package/dist/web/esp32c5-3mDOIGa4.js +0 -1
  55. package/dist/web/esp32c6-h6U0SQTm.js +0 -1
  56. package/dist/web/esp32c61-BKtexhPZ.js +0 -1
  57. package/dist/web/esp32h2-RtuWSEmP.js +0 -1
  58. package/dist/web/esp32p4-5nkIjxqJ.js +0 -1
  59. package/dist/web/esp32p4r3-CpHBYEwI.js +0 -1
  60. package/dist/web/esp32s2-IiDBtXxo.js +0 -1
  61. package/dist/web/esp32s3-6yv5yxum.js +0 -1
  62. package/js/modules/esp32-CijhsJH1.js +0 -1
  63. package/js/modules/esp32c2-C17SM4gO.js +0 -1
  64. package/js/modules/esp32c3-DxRGijbg.js +0 -1
  65. package/js/modules/esp32c5-3mDOIGa4.js +0 -1
  66. package/js/modules/esp32c6-h6U0SQTm.js +0 -1
  67. package/js/modules/esp32c61-BKtexhPZ.js +0 -1
  68. package/js/modules/esp32h2-RtuWSEmP.js +0 -1
  69. package/js/modules/esp32p4-5nkIjxqJ.js +0 -1
  70. package/js/modules/esp32p4r3-CpHBYEwI.js +0 -1
  71. package/js/modules/esp32s2-IiDBtXxo.js +0 -1
  72. package/js/modules/esp32s3-6yv5yxum.js +0 -1
@@ -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
- timeout = Math.min(timeout, MAX_TIMEOUT);
399
- await this.sendCommand(opcode, buffer, checksum);
400
- const [value, responseData] = await this.getResponse(opcode, timeout);
401
- if (responseData === null) {
402
- throw new Error("Didn't get enough status bytes");
403
- }
404
- let data = responseData;
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
- else if ([2, 4].includes(data.length)) {
433
- statusLen = data.length;
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
- if (data.length < statusLen) {
437
- throw new Error("Didn't get enough status bytes");
438
- }
439
- const status = data.slice(-statusLen, data.length);
440
- data = data.slice(0, -statusLen);
441
- if (this.debug) {
442
- this.logger.debug("status", status);
443
- this.logger.debug("value", value);
444
- this.logger.debug("data", data);
445
- }
446
- if (status[0] == 1) {
447
- if (status[1] == ROM_INVALID_RECV_MSG) {
448
- // Unsupported command can result in more than one error response
449
- // Use drainInputBuffer for CP210x compatibility on Windows
450
- await this.drainInputBuffer(200);
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
- throw new Error("Command failure error code " + toHex(status[1]));
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
- return [value, data];
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
- let readBytes = [];
506
+ const startTime = Date.now();
483
507
  while (true) {
484
- const stamp = Date.now();
485
- readBytes = [];
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
- if (this.debug)
501
- this.logger.debug("Read " + readBytes.length + " bytes: " + hexFormatter(readBytes));
502
- for (const b of readBytes) {
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: " + hexFormatter(readBytes));
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: " + hexFormatter(readBytes));
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
- const writer = this.port.writable.getWriter();
1215
- await writer.write(new Uint8Array(data));
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
- await this.port.writable.getWriter().close();
1233
- await new Promise((resolve) => {
1234
- if (!this._reader) {
1235
- resolve(undefined);
1335
+ try {
1336
+ this._isReconfiguring = true;
1337
+ // Wait for pending writes to complete
1338
+ try {
1339
+ await this._writeChain;
1236
1340
  }
1237
- this.addEventListener("disconnect", resolve, { once: true });
1238
- this._reader.cancel();
1239
- });
1240
- this.connected = false;
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
- this.logger.log("Reconnecting serial port...");
1252
- this.connected = false;
1253
- this.__inputBuffer = [];
1254
- // Cancel reader
1255
- if (this._reader) {
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._reader.cancel();
1396
+ await this._writeChain;
1258
1397
  }
1259
1398
  catch (err) {
1260
- this.logger.debug(`Reader cancel error: ${err}`);
1399
+ this.logger.debug(`Pending write error during reconnect: ${err}`);
1261
1400
  }
1262
- this._reader = undefined;
1263
- }
1264
- // Close port
1265
- try {
1266
- await this.port.close();
1267
- this.logger.log("Port closed");
1268
- }
1269
- catch (err) {
1270
- this.logger.debug(`Port close error: ${err}`);
1271
- }
1272
- // Open the port
1273
- this.logger.debug("Opening port...");
1274
- try {
1275
- await this.port.open({ baudRate: ESP_ROM_BAUD });
1276
- this.connected = true;
1277
- }
1278
- catch (err) {
1279
- throw new Error(`Failed to open port: ${err}`);
1280
- }
1281
- // Verify port streams are available
1282
- if (!this.port.readable || !this.port.writable) {
1283
- throw new Error(`Port streams not available after open (readable: ${!!this.port.readable}, writable: ${!!this.port.writable})`);
1284
- }
1285
- // Save chip info and flash size (no need to detect again)
1286
- const savedChipFamily = this.chipFamily;
1287
- const savedChipName = this.chipName;
1288
- const savedChipRevision = this.chipRevision;
1289
- const savedChipVariant = this.chipVariant;
1290
- const savedFlashSize = this.flashSize;
1291
- // Reinitialize
1292
- await this.hardReset(true);
1293
- if (!this._parent) {
1294
- this.__inputBuffer = [];
1295
- this.__totalBytesRead = 0;
1296
- this.readLoop();
1297
- }
1298
- await this.flushSerialBuffers();
1299
- await this.sync();
1300
- // Restore chip info
1301
- this.chipFamily = savedChipFamily;
1302
- this.chipName = savedChipName;
1303
- this.chipRevision = savedChipRevision;
1304
- this.chipVariant = savedChipVariant;
1305
- this.flashSize = savedFlashSize;
1306
- this.logger.debug(`Reconnect complete (chip: ${this.chipName})`);
1307
- // Verify port is ready
1308
- if (!this.port.writable || !this.port.readable) {
1309
- throw new Error("Port not ready after reconnect");
1310
- }
1311
- // Load stub
1312
- const stubLoader = await this.runStub(true);
1313
- this.logger.debug("Stub loaded");
1314
- // Restore baudrate if it was changed
1315
- if (this._currentBaudRate !== ESP_ROM_BAUD) {
1316
- await stubLoader.setBaudrate(this._currentBaudRate);
1317
- // Verify port is still ready after baudrate change
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(`Port not ready after baudrate change (readable: ${!!this.port.readable}, writable: ${!!this.port.writable})`);
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
- // Copy stub state to this instance if we're a stub loader
1323
- if (this.IS_STUB) {
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 = 5;
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
- this.logger.debug(`Reading chunk at 0x${currentAddr.toString(16)}, size: 0x${chunkSize.toString(16)}`);
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 final ACK for any data we did receive before the error
1433
- if (resp.length > 0) {
1434
- try {
1435
- const ackData = pack("<I", resp.length);
1436
- const slipEncodedAck = slipEncode(ackData);
1437
- await this.writeToStream(slipEncodedAck);
1438
- }
1439
- catch (ackErr) {
1440
- this.logger.debug(`ACK send error: ${ackErr}`);
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 for CP210x compatibility on Windows
1444
- // This clears any stale data that may be causing the error
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(`⚠️ ${err.message} at 0x${currentAddr.toString(16)}. Draining buffer and retrying (attempt ${retryCount}/${MAX_RETRIES})...`);
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(300);
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 new read command)
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}`);