tasmota-webserial-esptool 9.2.9 → 9.2.10

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
@@ -141,9 +141,13 @@ export class ESPLoader extends EventTarget {
141
141
  __inputBuffer?: number[];
142
142
  __inputBufferReadIndex?: number;
143
143
  __totalBytesRead?: number;
144
- private __currentBaudRate: number = ESP_ROM_BAUD;
144
+ public currentBaudRate: number = ESP_ROM_BAUD;
145
145
  private _maxUSBSerialBaudrate?: number;
146
146
  public __reader?: ReadableStreamDefaultReader<Uint8Array>;
147
+ private SLIP_END = 0xc0;
148
+ private SLIP_ESC = 0xdb;
149
+ private SLIP_ESC_END = 0xdc;
150
+ private SLIP_ESC_ESC = 0xdd;
147
151
  private _isESP32S2NativeUSB: boolean = false;
148
152
  private _initializationSucceeded: boolean = false;
149
153
  private __commandLock: Promise<[number, number[]]> = Promise.resolve([0, []]);
@@ -757,17 +761,14 @@ export class ESPLoader extends EventTarget {
757
761
 
758
762
  // Always read from browser's serial buffer immediately
759
763
  // to prevent browser buffer overflow. Don't apply back-pressure here.
760
- const chunk = Array.from(value);
764
+ const chunk = Array.from(value as Uint8Array);
761
765
  Array.prototype.push.apply(this._inputBuffer, chunk);
762
766
 
763
767
  // Track total bytes read from serial port
764
768
  this._totalBytesRead += value.length;
765
769
  }
766
770
  } catch {
767
- // Don't log error if this is an expected disconnect during console mode transition
768
- if (!this._consoleMode) {
769
- this.logger.error("Read loop got disconnected");
770
- }
771
+ // this.logger.error("Read loop got disconnected");
771
772
  } finally {
772
773
  // Always reset reconfiguring flag when read loop ends
773
774
  // This prevents "Cannot write during port reconfiguration" errors
@@ -1462,9 +1463,9 @@ export class ESPLoader extends EventTarget {
1462
1463
  }
1463
1464
  } catch (error) {
1464
1465
  lastError = error as Error;
1465
- this.logger.debug(
1466
- `${strategy.name} reset failed: ${(error as Error).message}`,
1467
- );
1466
+ // this.logger.debug(
1467
+ // `${strategy.name} reset failed: ${(error as Error).message}`,
1468
+ // );
1468
1469
 
1469
1470
  // Set abandon flag to stop any in-flight operations
1470
1471
  this._abandonCurrentOperation = true;
@@ -2129,47 +2130,47 @@ export class ESPLoader extends EventTarget {
2129
2130
  "Timed out waiting for packet " + waitingFor,
2130
2131
  );
2131
2132
  }
2132
- const b = this._readByte()!;
2133
+ const byte = this._readByte()!;
2133
2134
 
2134
2135
  if (partialPacket === null) {
2135
2136
  // waiting for packet header
2136
- if (b == 0xc0) {
2137
+ if (byte == this.SLIP_END) {
2137
2138
  partialPacket = [];
2138
2139
  } else {
2139
2140
  if (this.debug) {
2140
- this.logger.debug("Read invalid data: " + toHex(b));
2141
+ this.logger.debug("Read invalid data: " + toHex(byte));
2141
2142
  this.logger.debug(
2142
2143
  "Remaining data in serial buffer: " +
2143
2144
  hexFormatter(this._inputBuffer),
2144
2145
  );
2145
2146
  }
2146
2147
  throw new SlipReadError(
2147
- "Invalid head of packet (" + toHex(b) + ")",
2148
+ "Invalid head of packet (" + toHex(byte) + ")",
2148
2149
  );
2149
2150
  }
2150
2151
  } else if (inEscape) {
2151
2152
  // part-way through escape sequence
2152
2153
  inEscape = false;
2153
- if (b == 0xdc) {
2154
- partialPacket.push(0xc0);
2155
- } else if (b == 0xdd) {
2156
- partialPacket.push(0xdb);
2154
+ if (byte == this.SLIP_ESC_END) {
2155
+ partialPacket.push(this.SLIP_END);
2156
+ } else if (byte == this.SLIP_ESC_ESC) {
2157
+ partialPacket.push(this.SLIP_ESC);
2157
2158
  } else {
2158
2159
  if (this.debug) {
2159
- this.logger.debug("Read invalid data: " + toHex(b));
2160
+ this.logger.debug("Read invalid data: " + toHex(byte));
2160
2161
  this.logger.debug(
2161
2162
  "Remaining data in serial buffer: " +
2162
2163
  hexFormatter(this._inputBuffer),
2163
2164
  );
2164
2165
  }
2165
2166
  throw new SlipReadError(
2166
- "Invalid SLIP escape (0xdb, " + toHex(b) + ")",
2167
+ "Invalid SLIP escape (0xdb, " + toHex(byte) + ")",
2167
2168
  );
2168
2169
  }
2169
- } else if (b == 0xdb) {
2170
+ } else if (byte == this.SLIP_ESC) {
2170
2171
  // start of escape sequence
2171
2172
  inEscape = true;
2172
- } else if (b == 0xc0) {
2173
+ } else if (byte == this.SLIP_END) {
2173
2174
  // end of packet
2174
2175
  if (this.debug)
2175
2176
  this.logger.debug(
@@ -2180,7 +2181,7 @@ export class ESPLoader extends EventTarget {
2180
2181
  return partialPacket;
2181
2182
  } else {
2182
2183
  // normal byte in packet
2183
- partialPacket.push(b);
2184
+ partialPacket.push(byte);
2184
2185
  }
2185
2186
  }
2186
2187
  }
@@ -2214,46 +2215,46 @@ export class ESPLoader extends EventTarget {
2214
2215
  this.logger.debug(
2215
2216
  "Read " + readBytes.length + " bytes: " + hexFormatter(readBytes),
2216
2217
  );
2217
- for (const b of readBytes) {
2218
+ for (const byte of readBytes) {
2218
2219
  if (partialPacket === null) {
2219
2220
  // waiting for packet header
2220
- if (b == 0xc0) {
2221
+ if (byte == this.SLIP_END) {
2221
2222
  partialPacket = [];
2222
2223
  } else {
2223
2224
  if (this.debug) {
2224
- this.logger.debug("Read invalid data: " + toHex(b));
2225
+ this.logger.debug("Read invalid data: " + toHex(byte));
2225
2226
  this.logger.debug(
2226
2227
  "Remaining data in serial buffer: " +
2227
2228
  hexFormatter(this._inputBuffer),
2228
2229
  );
2229
2230
  }
2230
2231
  throw new SlipReadError(
2231
- "Invalid head of packet (" + toHex(b) + ")",
2232
+ "Invalid head of packet (" + toHex(byte) + ")",
2232
2233
  );
2233
2234
  }
2234
2235
  } else if (inEscape) {
2235
2236
  // part-way through escape sequence
2236
2237
  inEscape = false;
2237
- if (b == 0xdc) {
2238
- partialPacket.push(0xc0);
2239
- } else if (b == 0xdd) {
2240
- partialPacket.push(0xdb);
2238
+ if (byte == this.SLIP_ESC_END) {
2239
+ partialPacket.push(this.SLIP_END);
2240
+ } else if (byte == this.SLIP_ESC_ESC) {
2241
+ partialPacket.push(this.SLIP_ESC);
2241
2242
  } else {
2242
2243
  if (this.debug) {
2243
- this.logger.debug("Read invalid data: " + toHex(b));
2244
+ this.logger.debug("Read invalid data: " + toHex(byte));
2244
2245
  this.logger.debug(
2245
2246
  "Remaining data in serial buffer: " +
2246
2247
  hexFormatter(this._inputBuffer),
2247
2248
  );
2248
2249
  }
2249
2250
  throw new SlipReadError(
2250
- "Invalid SLIP escape (0xdb, " + toHex(b) + ")",
2251
+ "Invalid SLIP escape (0xdb, " + toHex(byte) + ")",
2251
2252
  );
2252
2253
  }
2253
- } else if (b == 0xdb) {
2254
+ } else if (byte == this.SLIP_ESC) {
2254
2255
  // start of escape sequence
2255
2256
  inEscape = true;
2256
- } else if (b == 0xc0) {
2257
+ } else if (byte == this.SLIP_END) {
2257
2258
  // end of packet
2258
2259
  if (this.debug)
2259
2260
  this.logger.debug(
@@ -2264,7 +2265,7 @@ export class ESPLoader extends EventTarget {
2264
2265
  return partialPacket;
2265
2266
  } else {
2266
2267
  // normal byte in packet
2267
- partialPacket.push(b);
2268
+ partialPacket.push(byte);
2268
2269
  }
2269
2270
  }
2270
2271
  }
@@ -2341,9 +2342,9 @@ export class ESPLoader extends EventTarget {
2341
2342
 
2342
2343
  // Track current baudrate for reconnect
2343
2344
  if (this._parent) {
2344
- this._parent._currentBaudRate = baud;
2345
+ this._parent.currentBaudRate = baud;
2345
2346
  } else {
2346
- this._currentBaudRate = baud;
2347
+ this.currentBaudRate = baud;
2347
2348
  }
2348
2349
 
2349
2350
  // Warn if baudrate exceeds USB-Serial chip capability
@@ -2429,8 +2430,8 @@ export class ESPLoader extends EventTarget {
2429
2430
  // Restart Readloop
2430
2431
  this.readLoop();
2431
2432
  } catch (e) {
2432
- this.logger.error(`Reconfigure port error: ${e}`);
2433
- throw new Error(`Unable to change the baud rate to ${baud}: ${e}`);
2433
+ // this.logger.error(`Reconfigure port error: ${e}`);
2434
+ // throw new Error(`Unable to change the baud rate to ${baud}: ${e}`);
2434
2435
  } finally {
2435
2436
  // Always reset flag, even on error or early return
2436
2437
  this._isReconfiguring = false;
@@ -3138,20 +3139,6 @@ export class ESPLoader extends EventTarget {
3138
3139
  }
3139
3140
  }
3140
3141
 
3141
- private get _currentBaudRate(): number {
3142
- return this._parent
3143
- ? this._parent._currentBaudRate
3144
- : this.__currentBaudRate;
3145
- }
3146
-
3147
- private set _currentBaudRate(value: number) {
3148
- if (this._parent) {
3149
- this._parent._currentBaudRate = value;
3150
- } else {
3151
- this.__currentBaudRate = value;
3152
- }
3153
- }
3154
-
3155
3142
  async writeToStream(data: number[]) {
3156
3143
  if (!this.port.writable) {
3157
3144
  this.logger.debug("Port writable stream not available, skipping write");
@@ -3231,7 +3218,7 @@ export class ESPLoader extends EventTarget {
3231
3218
  return;
3232
3219
  }
3233
3220
  if (!this.port.writable) {
3234
- this.logger.debug("Port already closed, skipping disconnect");
3221
+ // this.logger.debug("Port already closed, skipping disconnect");
3235
3222
  return;
3236
3223
  }
3237
3224
 
@@ -3239,7 +3226,7 @@ export class ESPLoader extends EventTarget {
3239
3226
  try {
3240
3227
  await this._writeChain;
3241
3228
  } catch (err) {
3242
- this.logger.debug(`Pending write error during disconnect: ${err}`);
3229
+ // this.logger.debug(`Pending write error during disconnect: ${err}`);
3243
3230
  }
3244
3231
 
3245
3232
  // Release persistent writer before closing
@@ -3248,7 +3235,7 @@ export class ESPLoader extends EventTarget {
3248
3235
  await this._writer.close();
3249
3236
  this._writer.releaseLock();
3250
3237
  } catch (err) {
3251
- this.logger.debug(`Writer close/release error: ${err}`);
3238
+ // this.logger.debug(`Writer close/release error: ${err}`);
3252
3239
  }
3253
3240
  this._writer = undefined;
3254
3241
  } else {
@@ -3259,7 +3246,7 @@ export class ESPLoader extends EventTarget {
3259
3246
  await writer.close();
3260
3247
  writer.releaseLock();
3261
3248
  } catch (err) {
3262
- this.logger.debug(`Direct writer close error: ${err}`);
3249
+ // this.logger.debug(`Direct writer close error: ${err}`);
3263
3250
  }
3264
3251
  }
3265
3252
 
@@ -3288,7 +3275,7 @@ export class ESPLoader extends EventTarget {
3288
3275
  try {
3289
3276
  this._reader.cancel();
3290
3277
  } catch (err) {
3291
- this.logger.debug(`Reader cancel error: ${err}`);
3278
+ // this.logger.debug(`Reader cancel error: ${err}`);
3292
3279
  // Reader already released, resolve immediately
3293
3280
  clearTimeout(timeout);
3294
3281
  resolve(undefined);
@@ -3328,7 +3315,7 @@ export class ESPLoader extends EventTarget {
3328
3315
  try {
3329
3316
  await this._writeChain;
3330
3317
  } catch (err) {
3331
- this.logger.debug(`Pending write error during release: ${err}`);
3318
+ // this.logger.debug(`Pending write error during release: ${err}`);
3332
3319
  }
3333
3320
 
3334
3321
  // Release writer
@@ -3442,6 +3429,8 @@ export class ESPLoader extends EventTarget {
3442
3429
  isUsbJtag = this.isUsbJtagOrOtg;
3443
3430
  }
3444
3431
 
3432
+ // Release reader/writer so console can create new ones
3433
+ // This is needed for Desktop (Web Serial) to unlock streams
3445
3434
  if (isUsbJtag) {
3446
3435
  // USB-JTAG/OTG devices: Use watchdog reset which closes port
3447
3436
  const wasReset = await this._resetToFirmwareIfNeeded();
@@ -3463,6 +3452,18 @@ export class ESPLoader extends EventTarget {
3463
3452
  this.logger.debug(`Could not reset device: ${err}`);
3464
3453
  }
3465
3454
 
3455
+ // For WebUSB (Android), recreate streams after hardware reset
3456
+ if (this.isWebUSB()) {
3457
+ try {
3458
+ // Use the public recreateStreams() method to safely recreate streams
3459
+ // without closing the port (important after hardware reset)
3460
+ await (this.port as any).recreateStreams();
3461
+ this.logger.debug("WebUSB streams recreated for console mode");
3462
+ } catch (err) {
3463
+ this.logger.debug(`Failed to recreate WebUSB streams: ${err}`);
3464
+ }
3465
+ }
3466
+
3466
3467
  return false; // Port stays open
3467
3468
  }
3468
3469
  }
@@ -3488,7 +3489,7 @@ export class ESPLoader extends EventTarget {
3488
3489
  : "USB-JTAG/Serial";
3489
3490
 
3490
3491
  this.logger.log(
3491
- `Resetting ${this.chipFamily} (${resetMethod}) to boot into firmware...`,
3492
+ `Resetting ${this.chipName || "device"} (${resetMethod}) to boot into firmware...`,
3492
3493
  );
3493
3494
 
3494
3495
  // Set console mode flag before reset to prevent subsequent hardReset calls
@@ -3567,7 +3568,7 @@ export class ESPLoader extends EventTarget {
3567
3568
 
3568
3569
  try {
3569
3570
  this.logger.log("Reconnecting serial port...");
3570
- const savedBaudRate = this._currentBaudRate;
3571
+ const savedBaudRate = this.currentBaudRate;
3571
3572
 
3572
3573
  this.connected = false;
3573
3574
  this.__inputBuffer = [];
@@ -3616,7 +3617,7 @@ export class ESPLoader extends EventTarget {
3616
3617
  try {
3617
3618
  await this.port.open({ baudRate: ESP_ROM_BAUD });
3618
3619
  this.connected = true;
3619
- this._currentBaudRate = ESP_ROM_BAUD;
3620
+ this.currentBaudRate = ESP_ROM_BAUD;
3620
3621
  } catch (err) {
3621
3622
  throw new Error(`Failed to open port: ${err}`);
3622
3623
  }
@@ -3758,7 +3759,7 @@ export class ESPLoader extends EventTarget {
3758
3759
  try {
3759
3760
  await this.port.open({ baudRate: ESP_ROM_BAUD });
3760
3761
  this.connected = true;
3761
- this._currentBaudRate = ESP_ROM_BAUD;
3762
+ this.currentBaudRate = ESP_ROM_BAUD;
3762
3763
  } catch (err) {
3763
3764
  throw new Error(`Failed to open port: ${err}`);
3764
3765
  }
@@ -3805,6 +3806,115 @@ export class ESPLoader extends EventTarget {
3805
3806
  }
3806
3807
  }
3807
3808
 
3809
+ /**
3810
+ * @name exitConsoleMode
3811
+ * Exit console mode and return to bootloader
3812
+ * For ESP32-S2, uses reconnectToBootloader which will trigger port change
3813
+ * @returns true if manual reconnection is needed (ESP32-S2), false otherwise
3814
+ */
3815
+ async exitConsoleMode(): Promise<boolean> {
3816
+ if (this._parent) {
3817
+ return await this._parent.exitConsoleMode();
3818
+ }
3819
+
3820
+ // Clear console mode flag
3821
+ this._consoleMode = false;
3822
+
3823
+ // Check if this is ESP32-S2 with USB-JTAG/OTG
3824
+ const isESP32S2 = this.chipFamily === CHIP_FAMILY_ESP32S2;
3825
+
3826
+ // For ESP32-S2: if _isUsbJtagOrOtg is undefined, try to detect it
3827
+ // If detection fails or is undefined, assume USB-JTAG/OTG (conservative/safe path)
3828
+ let isUsbJtagOrOtg = this._isUsbJtagOrOtg;
3829
+ if (isESP32S2 && isUsbJtagOrOtg === undefined) {
3830
+ try {
3831
+ isUsbJtagOrOtg = await this.detectUsbConnectionType();
3832
+ } catch (err) {
3833
+ this.logger.debug(
3834
+ `USB detection failed, assuming USB-JTAG/OTG for ESP32-S2: ${err}`,
3835
+ );
3836
+ isUsbJtagOrOtg = true; // Conservative fallback for ESP32-S2
3837
+ }
3838
+ }
3839
+
3840
+ if (isESP32S2 && isUsbJtagOrOtg) {
3841
+ // ESP32-S2 USB: Use reconnectToBootloader which handles the mode switch
3842
+ // This will close the port and the device will reboot to bootloader
3843
+ this.logger.log("ESP32-S2 USB detected - reconnecting to bootloader");
3844
+
3845
+ try {
3846
+ await this.reconnectToBootloader();
3847
+ } catch (err) {
3848
+ this.logger.debug(`Reconnect error (expected for ESP32-S2): ${err}`);
3849
+ }
3850
+
3851
+ // For ESP32-S2, port will change, so return true to indicate manual reconnection needed
3852
+ return true;
3853
+ }
3854
+
3855
+ // For other devices, use standard reconnectToBootloader
3856
+ await this.reconnectToBootloader();
3857
+ return false; // No manual reconnection needed
3858
+ }
3859
+
3860
+ /**
3861
+ * @name isConsoleResetSupported
3862
+ * Check if console reset is supported for this device
3863
+ * ESP32-S2 USB-JTAG/CDC does not support reset in console mode
3864
+ * because any reset causes USB port to be lost (hardware limitation)
3865
+ */
3866
+ isConsoleResetSupported(): boolean {
3867
+ if (this._parent) {
3868
+ return this._parent.isConsoleResetSupported();
3869
+ }
3870
+
3871
+ // For ESP32-S2: if _isUsbJtagOrOtg is undefined, assume USB-JTAG/OTG (conservative)
3872
+ // This means console reset is NOT supported (safer default)
3873
+ const isS2UsbJtag =
3874
+ this.chipFamily === CHIP_FAMILY_ESP32S2 &&
3875
+ (this._isUsbJtagOrOtg === true || this._isUsbJtagOrOtg === undefined);
3876
+ return !isS2UsbJtag; // Not supported for ESP32-S2 USB-JTAG/CDC
3877
+ }
3878
+
3879
+ /**
3880
+ * @name resetInConsoleMode
3881
+ * Reset device while in console mode (firmware mode)
3882
+ *
3883
+ * NOTE: For ESP32-S2 USB-JTAG/CDC, ANY reset (hardware or software) causes
3884
+ * the USB port to be lost because the device switches USB modes during reset.
3885
+ * This is a hardware limitation - use isConsoleResetSupported() to check first.
3886
+ */
3887
+ async resetInConsoleMode(): Promise<void> {
3888
+ if (this._parent) {
3889
+ return await this._parent.resetInConsoleMode();
3890
+ }
3891
+
3892
+ if (!this.isConsoleResetSupported()) {
3893
+ this.logger.debug(
3894
+ "Console reset not supported for ESP32-S2 USB-JTAG/CDC",
3895
+ );
3896
+ return; // Do nothing
3897
+ }
3898
+
3899
+ // For other devices: Use standard firmware reset
3900
+ const isWebUSB = (this.port as any).isWebUSB === true;
3901
+
3902
+ try {
3903
+ this.logger.debug("Resetting device in console mode");
3904
+
3905
+ if (isWebUSB) {
3906
+ await this.hardResetToFirmwareWebUSB();
3907
+ } else {
3908
+ await this.hardResetToFirmware();
3909
+ }
3910
+
3911
+ this.logger.debug("Device reset complete");
3912
+ } catch (err) {
3913
+ this.logger.error(`Reset failed: ${err}`);
3914
+ throw err;
3915
+ }
3916
+ }
3917
+
3808
3918
  /**
3809
3919
  * @name drainInputBuffer
3810
3920
  * Actively drain the input buffer by reading data for a specified time.
@@ -3820,7 +3930,7 @@ export class ESPLoader extends EventTarget {
3820
3930
  await sleep(bufferingTime);
3821
3931
 
3822
3932
  // Unsupported command response is sent 8 times and has
3823
- // 14 bytes length including delimiter 0xC0 bytes.
3933
+ // 14 bytes length including delimiter SLIP_END (0xC0) bytes.
3824
3934
  // At least part of it is read as a command response,
3825
3935
  // but to be safe, read it all.
3826
3936
  const bytesToDrain = 14 * 8;
@@ -4040,7 +4150,7 @@ export class ESPLoader extends EventTarget {
4040
4150
  // The stub expects 4 bytes (ACK), if we send less it will break out
4041
4151
  try {
4042
4152
  // Send SLIP frame with no data (just delimiters)
4043
- const abortFrame = [0xc0, 0xc0]; // Empty SLIP frame
4153
+ const abortFrame = [this.SLIP_END, this.SLIP_END]; // Empty SLIP frame
4044
4154
  await this.writeToStream(abortFrame);
4045
4155
  this.logger.debug(`Sent abort frame to stub`);
4046
4156
 
package/src/index.ts CHANGED
@@ -99,3 +99,6 @@ export const connectWithPort = async (port: SerialPort, logger: Logger) => {
99
99
 
100
100
  return new ESPLoader(port, logger);
101
101
  };
102
+
103
+ // Export utility functions for use in UI code
104
+ export { toHex, sleep, hexFormatter, formatMacAddr } from "./util";
package/src/util.ts CHANGED
@@ -45,5 +45,14 @@ export const toHex = (value: number, size = 2) => {
45
45
  }
46
46
  };
47
47
 
48
+ /**
49
+ * Format MAC address array to string (e.g., [0xAA, 0xBB, 0xCC] -> "AA:BB:CC:DD:EE:FF")
50
+ */
51
+ export const formatMacAddr = (macAddr: number[]): string => {
52
+ return macAddr
53
+ .map((value) => value.toString(16).toUpperCase().padStart(2, "0"))
54
+ .join(":");
55
+ };
56
+
48
57
  export const sleep = (ms: number) =>
49
58
  new Promise((resolve) => setTimeout(resolve, ms));