tasmota-webserial-esptool 9.1.5 → 9.1.7

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/src/esp_loader.ts CHANGED
@@ -42,6 +42,7 @@ import {
42
42
  USB_RAM_BLOCK,
43
43
  ChipFamily,
44
44
  ESP_ERASE_FLASH,
45
+ ESP_ERASE_REGION,
45
46
  ESP_READ_FLASH,
46
47
  CHIP_ERASE_TIMEOUT,
47
48
  FLASH_READ_TIMEOUT,
@@ -613,6 +614,21 @@ export class ESPLoader extends EventTarget {
613
614
  };
614
615
  }
615
616
 
617
+ /**
618
+ * Get MAC address from efuses
619
+ */
620
+ async getMacAddress(): Promise<string> {
621
+ if (!this._initializationSucceeded) {
622
+ throw new Error(
623
+ "getMacAddress() requires initialize() to have completed successfully",
624
+ );
625
+ }
626
+ const macBytes = this.macAddr(); // chip-family-aware
627
+ return macBytes
628
+ .map((b) => b.toString(16).padStart(2, "0").toUpperCase())
629
+ .join(":");
630
+ }
631
+
616
632
  /**
617
633
  * @name readLoop
618
634
  * Reads data from the input stream and places it in the inputBuffer
@@ -647,7 +663,13 @@ export class ESPLoader extends EventTarget {
647
663
  }
648
664
  } catch {
649
665
  this.logger.error("Read loop got disconnected");
666
+ } finally {
667
+ // Always reset reconfiguring flag when read loop ends
668
+ // This prevents "Cannot write during port reconfiguration" errors
669
+ // when the read loop dies unexpectedly
670
+ this._isReconfiguring = false;
650
671
  }
672
+
651
673
  // Disconnected!
652
674
  this.connected = false;
653
675
 
@@ -1256,7 +1278,9 @@ export class ESPLoader extends EventTarget {
1256
1278
  }
1257
1279
  }
1258
1280
 
1259
- // All strategies failed
1281
+ // All strategies failed - reset abandon flag before throwing
1282
+ this._abandonCurrentOperation = false;
1283
+
1260
1284
  throw new Error(
1261
1285
  `Couldn't sync to ESP. Try resetting manually. Last error: ${lastError?.message}`,
1262
1286
  );
@@ -2608,50 +2632,51 @@ export class ESPLoader extends EventTarget {
2608
2632
  return;
2609
2633
  }
2610
2634
 
2635
+ // Wait for pending writes to complete
2611
2636
  try {
2612
- // Wait for pending writes to complete
2637
+ await this._writeChain;
2638
+ } catch (err) {
2639
+ this.logger.debug(`Pending write error during disconnect: ${err}`);
2640
+ }
2641
+
2642
+ // Release persistent writer before closing
2643
+ if (this._writer) {
2613
2644
  try {
2614
- await this._writeChain;
2645
+ await this._writer.close();
2646
+ this._writer.releaseLock();
2615
2647
  } catch (err) {
2616
- this.logger.debug(`Pending write error during disconnect: ${err}`);
2648
+ this.logger.debug(`Writer close/release error: ${err}`);
2617
2649
  }
2650
+ this._writer = undefined;
2651
+ } else {
2652
+ // No persistent writer exists, close stream directly
2653
+ // This path is taken when no writes have been queued
2654
+ try {
2655
+ const writer = this.port.writable.getWriter();
2656
+ await writer.close();
2657
+ writer.releaseLock();
2658
+ } catch (err) {
2659
+ this.logger.debug(`Direct writer close error: ${err}`);
2660
+ }
2661
+ }
2618
2662
 
2619
- // Block new writes during disconnect
2620
- this._isReconfiguring = true;
2621
-
2622
- // Release persistent writer before closing
2623
- if (this._writer) {
2624
- try {
2625
- await this._writer.close();
2626
- this._writer.releaseLock();
2627
- } catch (err) {
2628
- this.logger.debug(`Writer close/release error: ${err}`);
2629
- }
2630
- this._writer = undefined;
2631
- } else {
2632
- // No persistent writer exists, close stream directly
2633
- // This path is taken when no writes have been queued
2634
- try {
2635
- const writer = this.port.writable.getWriter();
2636
- await writer.close();
2637
- writer.releaseLock();
2638
- } catch (err) {
2639
- this.logger.debug(`Direct writer close error: ${err}`);
2640
- }
2663
+ await new Promise((resolve) => {
2664
+ if (!this._reader) {
2665
+ resolve(undefined);
2666
+ return;
2641
2667
  }
2668
+ this.addEventListener("disconnect", resolve, { once: true });
2642
2669
 
2643
- await new Promise((resolve) => {
2644
- if (!this._reader) {
2645
- resolve(undefined);
2646
- return;
2647
- }
2648
- this.addEventListener("disconnect", resolve, { once: true });
2649
- this._reader!.cancel();
2650
- });
2651
- this.connected = false;
2652
- } finally {
2653
- this._isReconfiguring = false;
2654
- }
2670
+ // Only cancel if reader is still active
2671
+ try {
2672
+ this._reader.cancel();
2673
+ } catch (err) {
2674
+ this.logger.debug(`Reader cancel error: ${err}`);
2675
+ // Reader already released, resolve immediately
2676
+ resolve(undefined);
2677
+ }
2678
+ });
2679
+ this.connected = false;
2655
2680
  }
2656
2681
 
2657
2682
  /**
@@ -3029,8 +3054,9 @@ export class ESPLoader extends EventTarget {
3029
3054
  newResp.set(packetData, resp.length);
3030
3055
  resp = newResp;
3031
3056
 
3032
- // Send acknowledgment after receiving maxInFlight bytes
3033
- // This unblocks the stub to send the next batch of packets
3057
+ // Send acknowledgment when we've received maxInFlight bytes
3058
+ // The stub sends packets until (num_sent - num_acked) >= max_in_flight
3059
+ // We MUST wait for all packets before sending ACK
3034
3060
  const shouldAck =
3035
3061
  resp.length >= chunkSize || // End of chunk
3036
3062
  resp.length >= lastAckedLength + maxInFlight; // Received all packets
@@ -3274,10 +3300,55 @@ class EspStubLoader extends ESPLoader {
3274
3300
  }
3275
3301
 
3276
3302
  /**
3277
- * @name getEraseSize
3278
- * depending on flash chip model the erase may take this long (maybe longer!)
3303
+ * @name eraseFlash
3304
+ * Erase entire flash chip
3279
3305
  */
3280
3306
  async eraseFlash() {
3281
3307
  await this.checkCommand(ESP_ERASE_FLASH, [], 0, CHIP_ERASE_TIMEOUT);
3282
3308
  }
3309
+
3310
+ /**
3311
+ * @name eraseRegion
3312
+ * Erase a specific region of flash
3313
+ */
3314
+ async eraseRegion(offset: number, size: number) {
3315
+ // Validate inputs
3316
+ if (offset < 0) {
3317
+ throw new Error(`Invalid offset: ${offset} (must be non-negative)`);
3318
+ }
3319
+ if (size < 0) {
3320
+ throw new Error(`Invalid size: ${size} (must be non-negative)`);
3321
+ }
3322
+
3323
+ // No-op for zero size
3324
+ if (size === 0) {
3325
+ this.logger.log("eraseRegion: size is 0, skipping erase");
3326
+ return;
3327
+ }
3328
+
3329
+ // Check for sector alignment
3330
+ if (offset % FLASH_SECTOR_SIZE !== 0) {
3331
+ throw new Error(
3332
+ `Offset ${offset} (0x${offset.toString(16)}) is not aligned to flash sector size ${FLASH_SECTOR_SIZE} (0x${FLASH_SECTOR_SIZE.toString(16)})`,
3333
+ );
3334
+ }
3335
+ if (size % FLASH_SECTOR_SIZE !== 0) {
3336
+ throw new Error(
3337
+ `Size ${size} (0x${size.toString(16)}) is not aligned to flash sector size ${FLASH_SECTOR_SIZE} (0x${FLASH_SECTOR_SIZE.toString(16)})`,
3338
+ );
3339
+ }
3340
+
3341
+ // Check for reasonable bounds (prevent wrapping in pack)
3342
+ const maxValue = 0xffffffff; // 32-bit unsigned max
3343
+ if (offset > maxValue) {
3344
+ throw new Error(`Offset ${offset} exceeds maximum value ${maxValue}`);
3345
+ }
3346
+ if (size > maxValue) {
3347
+ throw new Error(`Size ${size} exceeds maximum value ${maxValue}`);
3348
+ }
3349
+
3350
+ const timeout = timeoutPerMb(ERASE_REGION_TIMEOUT_PER_MB, size);
3351
+ const buffer = pack("<II", offset, size);
3352
+ await this.checkCommand(ESP_ERASE_REGION, buffer, 0, timeout);
3353
+ }
3283
3354
  }