tasmota-webserial-esptool 9.1.4 → 9.1.6

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,
@@ -77,10 +78,10 @@ interface WebUSBSerialPort extends SerialPort {
77
78
  }
78
79
 
79
80
  export class ESPLoader extends EventTarget {
80
- chipFamily!: ChipFamily;
81
- chipName: string | null = null;
82
- chipRevision: number | null = null;
83
- chipVariant: string | null = null;
81
+ __chipFamily?: ChipFamily;
82
+ __chipName: string | null = null;
83
+ __chipRevision: number | null = null;
84
+ __chipVariant: string | null = null;
84
85
  _efuses = new Array(4).fill(0);
85
86
  _flashsize = 4 * 1024 * 1024;
86
87
  debug = false;
@@ -116,6 +117,55 @@ export class ESPLoader extends EventTarget {
116
117
  super();
117
118
  }
118
119
 
120
+ // Chip properties with parent delegation
121
+ get chipFamily(): ChipFamily {
122
+ return this._parent ? this._parent.chipFamily : this.__chipFamily!;
123
+ }
124
+
125
+ set chipFamily(value: ChipFamily) {
126
+ if (this._parent) {
127
+ this._parent.chipFamily = value;
128
+ } else {
129
+ this.__chipFamily = value;
130
+ }
131
+ }
132
+
133
+ get chipName(): string | null {
134
+ return this._parent ? this._parent.chipName : this.__chipName;
135
+ }
136
+
137
+ set chipName(value: string | null) {
138
+ if (this._parent) {
139
+ this._parent.chipName = value;
140
+ } else {
141
+ this.__chipName = value;
142
+ }
143
+ }
144
+
145
+ get chipRevision(): number | null {
146
+ return this._parent ? this._parent.chipRevision : this.__chipRevision;
147
+ }
148
+
149
+ set chipRevision(value: number | null) {
150
+ if (this._parent) {
151
+ this._parent.chipRevision = value;
152
+ } else {
153
+ this.__chipRevision = value;
154
+ }
155
+ }
156
+
157
+ get chipVariant(): string | null {
158
+ return this._parent ? this._parent.chipVariant : this.__chipVariant;
159
+ }
160
+
161
+ set chipVariant(value: string | null) {
162
+ if (this._parent) {
163
+ this._parent.chipVariant = value;
164
+ } else {
165
+ this.__chipVariant = value;
166
+ }
167
+ }
168
+
119
169
  private get _inputBuffer(): number[] {
120
170
  return this._parent ? this._parent._inputBuffer : this.__inputBuffer!;
121
171
  }
@@ -564,6 +614,21 @@ export class ESPLoader extends EventTarget {
564
614
  };
565
615
  }
566
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
+
567
632
  /**
568
633
  * @name readLoop
569
634
  * Reads data from the input stream and places it in the inputBuffer
@@ -598,7 +663,13 @@ export class ESPLoader extends EventTarget {
598
663
  }
599
664
  } catch {
600
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;
601
671
  }
672
+
602
673
  // Disconnected!
603
674
  this.connected = false;
604
675
 
@@ -2559,50 +2630,43 @@ export class ESPLoader extends EventTarget {
2559
2630
  return;
2560
2631
  }
2561
2632
 
2633
+ // Wait for pending writes to complete
2562
2634
  try {
2563
- // Wait for pending writes to complete
2635
+ await this._writeChain;
2636
+ } catch (err) {
2637
+ this.logger.debug(`Pending write error during disconnect: ${err}`);
2638
+ }
2639
+
2640
+ // Release persistent writer before closing
2641
+ if (this._writer) {
2564
2642
  try {
2565
- await this._writeChain;
2643
+ await this._writer.close();
2644
+ this._writer.releaseLock();
2566
2645
  } catch (err) {
2567
- this.logger.debug(`Pending write error during disconnect: ${err}`);
2646
+ this.logger.debug(`Writer close/release error: ${err}`);
2568
2647
  }
2569
-
2570
- // Block new writes during disconnect
2571
- this._isReconfiguring = true;
2572
-
2573
- // Release persistent writer before closing
2574
- if (this._writer) {
2575
- try {
2576
- await this._writer.close();
2577
- this._writer.releaseLock();
2578
- } catch (err) {
2579
- this.logger.debug(`Writer close/release error: ${err}`);
2580
- }
2581
- this._writer = undefined;
2582
- } else {
2583
- // No persistent writer exists, close stream directly
2584
- // This path is taken when no writes have been queued
2585
- try {
2586
- const writer = this.port.writable.getWriter();
2587
- await writer.close();
2588
- writer.releaseLock();
2589
- } catch (err) {
2590
- this.logger.debug(`Direct writer close error: ${err}`);
2591
- }
2648
+ this._writer = undefined;
2649
+ } else {
2650
+ // No persistent writer exists, close stream directly
2651
+ // This path is taken when no writes have been queued
2652
+ try {
2653
+ const writer = this.port.writable.getWriter();
2654
+ await writer.close();
2655
+ writer.releaseLock();
2656
+ } catch (err) {
2657
+ this.logger.debug(`Direct writer close error: ${err}`);
2592
2658
  }
2593
-
2594
- await new Promise((resolve) => {
2595
- if (!this._reader) {
2596
- resolve(undefined);
2597
- return;
2598
- }
2599
- this.addEventListener("disconnect", resolve, { once: true });
2600
- this._reader!.cancel();
2601
- });
2602
- this.connected = false;
2603
- } finally {
2604
- this._isReconfiguring = false;
2605
2659
  }
2660
+
2661
+ await new Promise((resolve) => {
2662
+ if (!this._reader) {
2663
+ resolve(undefined);
2664
+ return;
2665
+ }
2666
+ this.addEventListener("disconnect", resolve, { once: true });
2667
+ this._reader!.cancel();
2668
+ });
2669
+ this.connected = false;
2606
2670
  }
2607
2671
 
2608
2672
  /**
@@ -2980,8 +3044,9 @@ export class ESPLoader extends EventTarget {
2980
3044
  newResp.set(packetData, resp.length);
2981
3045
  resp = newResp;
2982
3046
 
2983
- // Send acknowledgment after receiving maxInFlight bytes
2984
- // This unblocks the stub to send the next batch of packets
3047
+ // Send acknowledgment when we've received maxInFlight bytes
3048
+ // The stub sends packets until (num_sent - num_acked) >= max_in_flight
3049
+ // We MUST wait for all packets before sending ACK
2985
3050
  const shouldAck =
2986
3051
  resp.length >= chunkSize || // End of chunk
2987
3052
  resp.length >= lastAckedLength + maxInFlight; // Received all packets
@@ -3225,10 +3290,55 @@ class EspStubLoader extends ESPLoader {
3225
3290
  }
3226
3291
 
3227
3292
  /**
3228
- * @name getEraseSize
3229
- * depending on flash chip model the erase may take this long (maybe longer!)
3293
+ * @name eraseFlash
3294
+ * Erase entire flash chip
3230
3295
  */
3231
3296
  async eraseFlash() {
3232
3297
  await this.checkCommand(ESP_ERASE_FLASH, [], 0, CHIP_ERASE_TIMEOUT);
3233
3298
  }
3299
+
3300
+ /**
3301
+ * @name eraseRegion
3302
+ * Erase a specific region of flash
3303
+ */
3304
+ async eraseRegion(offset: number, size: number) {
3305
+ // Validate inputs
3306
+ if (offset < 0) {
3307
+ throw new Error(`Invalid offset: ${offset} (must be non-negative)`);
3308
+ }
3309
+ if (size < 0) {
3310
+ throw new Error(`Invalid size: ${size} (must be non-negative)`);
3311
+ }
3312
+
3313
+ // No-op for zero size
3314
+ if (size === 0) {
3315
+ this.logger.log("eraseRegion: size is 0, skipping erase");
3316
+ return;
3317
+ }
3318
+
3319
+ // Check for sector alignment
3320
+ if (offset % FLASH_SECTOR_SIZE !== 0) {
3321
+ throw new Error(
3322
+ `Offset ${offset} (0x${offset.toString(16)}) is not aligned to flash sector size ${FLASH_SECTOR_SIZE} (0x${FLASH_SECTOR_SIZE.toString(16)})`,
3323
+ );
3324
+ }
3325
+ if (size % FLASH_SECTOR_SIZE !== 0) {
3326
+ throw new Error(
3327
+ `Size ${size} (0x${size.toString(16)}) is not aligned to flash sector size ${FLASH_SECTOR_SIZE} (0x${FLASH_SECTOR_SIZE.toString(16)})`,
3328
+ );
3329
+ }
3330
+
3331
+ // Check for reasonable bounds (prevent wrapping in pack)
3332
+ const maxValue = 0xffffffff; // 32-bit unsigned max
3333
+ if (offset > maxValue) {
3334
+ throw new Error(`Offset ${offset} exceeds maximum value ${maxValue}`);
3335
+ }
3336
+ if (size > maxValue) {
3337
+ throw new Error(`Size ${size} exceeds maximum value ${maxValue}`);
3338
+ }
3339
+
3340
+ const timeout = timeoutPerMb(ERASE_REGION_TIMEOUT_PER_MB, size);
3341
+ const buffer = pack("<II", offset, size);
3342
+ await this.checkCommand(ESP_ERASE_REGION, buffer, 0, timeout);
3343
+ }
3234
3344
  }