tasmota-webserial-esptool 9.1.8 → 9.2.0

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
@@ -101,8 +101,7 @@ export class ESPLoader extends EventTarget {
101
101
  private __isReconfiguring: boolean = false;
102
102
  private __abandonCurrentOperation: boolean = false;
103
103
 
104
- // Adaptive speed adjustment for flash read operations - DISABLED
105
- // Using fixed conservative values that work reliably
104
+ // Adaptive speed adjustment for flash read operations
106
105
  private __adaptiveBlockMultiplier: number = 1;
107
106
  private __adaptiveMaxInFlightMultiplier: number = 1;
108
107
  private __consecutiveSuccessfulChunks: number = 0;
@@ -118,6 +117,7 @@ export class ESPLoader extends EventTarget {
118
117
  }
119
118
 
120
119
  // Chip properties with parent delegation
120
+ // chipFamily accessed before initialization as designed
121
121
  get chipFamily(): ChipFamily {
122
122
  return this._parent ? this._parent.chipFamily : this.__chipFamily!;
123
123
  }
@@ -167,7 +167,13 @@ export class ESPLoader extends EventTarget {
167
167
  }
168
168
 
169
169
  private get _inputBuffer(): number[] {
170
- return this._parent ? this._parent._inputBuffer : this.__inputBuffer!;
170
+ if (this._parent) {
171
+ return this._parent._inputBuffer;
172
+ }
173
+ if (this.__inputBuffer === undefined) {
174
+ throw new Error("_inputBuffer accessed before initialization");
175
+ }
176
+ return this.__inputBuffer;
171
177
  }
172
178
 
173
179
  private get _inputBufferReadIndex(): number {
@@ -715,6 +721,15 @@ export class ESPLoader extends EventTarget {
715
721
  await this.port.setSignals({ dataTerminalReady: state });
716
722
  }
717
723
 
724
+ async setDTRandRTS(dtr: boolean, rts: boolean) {
725
+ this.state_DTR = dtr;
726
+ this.state_RTS = rts;
727
+ await this.port.setSignals({
728
+ dataTerminalReady: dtr,
729
+ requestToSend: rts,
730
+ });
731
+ }
732
+
718
733
  /**
719
734
  * @name hardResetUSBJTAGSerial
720
735
  * USB-JTAG/Serial reset for Web Serial (Desktop)
@@ -741,7 +756,7 @@ export class ESPLoader extends EventTarget {
741
756
 
742
757
  /**
743
758
  * @name hardResetClassic
744
- * Classic reset for Web Serial (Desktop)
759
+ * Classic reset for Web Serial (Desktop) DTR = IO0, RTS = EN
745
760
  */
746
761
  async hardResetClassic() {
747
762
  await this.setDTR(false); // IO0=HIGH
@@ -755,6 +770,23 @@ export class ESPLoader extends EventTarget {
755
770
  await this.sleep(200);
756
771
  }
757
772
 
773
+ /**
774
+ * @name hardResetUnixTight
775
+ * Unix Tight reset for Web Serial (Desktop) - sets DTR and RTS simultaneously
776
+ */
777
+ async hardResetUnixTight() {
778
+ await this.setDTRandRTS(true, true);
779
+ await this.setDTRandRTS(false, false);
780
+ await this.setDTRandRTS(false, true); // IO0=HIGH & EN=LOW, chip in reset
781
+ await this.sleep(100);
782
+ await this.setDTRandRTS(true, false); // IO0=LOW & EN=HIGH, chip out of reset
783
+ await this.sleep(50);
784
+ await this.setDTRandRTS(false, false); // IO0=HIGH, done
785
+ await this.setDTR(false); // Needed in some environments to ensure IO0=HIGH
786
+
787
+ await this.sleep(200);
788
+ }
789
+
758
790
  // ============================================================================
759
791
  // WebUSB (Android) - DTR/RTS Signal Handling & Reset Strategies
760
792
  // ============================================================================
@@ -971,10 +1003,12 @@ export class ESPLoader extends EventTarget {
971
1003
  // eslint-disable-next-line @typescript-eslint/no-this-alias
972
1004
  const self = this;
973
1005
 
1006
+ // Detect if this is a USB-Serial chip (needs different sync approach)
1007
+ const isUSBSerialChip = !isUSBJTAGSerial && !isEspressifUSB;
1008
+
974
1009
  // WebUSB (Android) uses different reset methods than Web Serial (Desktop)
975
1010
  if (this.isWebUSB()) {
976
1011
  // For USB-Serial chips (CP2102, CH340, etc.), try inverted strategies first
977
- const isUSBSerialChip = !isUSBJTAGSerial && !isEspressifUSB;
978
1012
 
979
1013
  // Detect specific chip types once
980
1014
  const isCP2102 = portInfo.usbVendorId === 0x10c4;
@@ -993,7 +1027,7 @@ export class ESPLoader extends EventTarget {
993
1027
  // Strategy 1: USB-JTAG/Serial (works in CDC mode on Desktop)
994
1028
  resetStrategies.push({
995
1029
  name: "USB-JTAG/Serial (WebUSB) - ESP32-S2",
996
- fn: async function () {
1030
+ fn: async () => {
997
1031
  return await self.hardResetUSBJTAGSerialWebUSB();
998
1032
  },
999
1033
  });
@@ -1001,7 +1035,7 @@ export class ESPLoader extends EventTarget {
1001
1035
  // Strategy 2: USB-JTAG/Serial Inverted DTR (works in JTAG mode)
1002
1036
  resetStrategies.push({
1003
1037
  name: "USB-JTAG/Serial Inverted DTR (WebUSB) - ESP32-S2",
1004
- fn: async function () {
1038
+ fn: async () => {
1005
1039
  return await self.hardResetUSBJTAGSerialInvertedDTRWebUSB();
1006
1040
  },
1007
1041
  });
@@ -1009,7 +1043,7 @@ export class ESPLoader extends EventTarget {
1009
1043
  // Strategy 3: UnixTight (CDC fallback)
1010
1044
  resetStrategies.push({
1011
1045
  name: "UnixTight (WebUSB) - ESP32-S2 CDC",
1012
- fn: async function () {
1046
+ fn: async () => {
1013
1047
  return await self.hardResetUnixTightWebUSB();
1014
1048
  },
1015
1049
  });
@@ -1017,7 +1051,7 @@ export class ESPLoader extends EventTarget {
1017
1051
  // Strategy 4: Classic reset (CDC fallback)
1018
1052
  resetStrategies.push({
1019
1053
  name: "Classic (WebUSB) - ESP32-S2 CDC",
1020
- fn: async function () {
1054
+ fn: async () => {
1021
1055
  return await self.hardResetClassicWebUSB();
1022
1056
  },
1023
1057
  });
@@ -1025,19 +1059,19 @@ export class ESPLoader extends EventTarget {
1025
1059
  // Other USB-JTAG chips: Try Inverted DTR first - works best for ESP32-H2 and other JTAG chips
1026
1060
  resetStrategies.push({
1027
1061
  name: "USB-JTAG/Serial Inverted DTR (WebUSB)",
1028
- fn: async function () {
1062
+ fn: async () => {
1029
1063
  return await self.hardResetUSBJTAGSerialInvertedDTRWebUSB();
1030
1064
  },
1031
1065
  });
1032
1066
  resetStrategies.push({
1033
1067
  name: "USB-JTAG/Serial (WebUSB)",
1034
- fn: async function () {
1068
+ fn: async () => {
1035
1069
  return await self.hardResetUSBJTAGSerialWebUSB();
1036
1070
  },
1037
1071
  });
1038
1072
  resetStrategies.push({
1039
1073
  name: "Inverted DTR Classic (WebUSB)",
1040
- fn: async function () {
1074
+ fn: async () => {
1041
1075
  return await self.hardResetInvertedDTRWebUSB();
1042
1076
  },
1043
1077
  });
@@ -1050,31 +1084,31 @@ export class ESPLoader extends EventTarget {
1050
1084
  // CH340/CH343: UnixTight works best (like CP2102)
1051
1085
  resetStrategies.push({
1052
1086
  name: "UnixTight (WebUSB) - CH34x",
1053
- fn: async function () {
1087
+ fn: async () => {
1054
1088
  return await self.hardResetUnixTightWebUSB();
1055
1089
  },
1056
1090
  });
1057
1091
  resetStrategies.push({
1058
1092
  name: "Classic (WebUSB) - CH34x",
1059
- fn: async function () {
1093
+ fn: async () => {
1060
1094
  return await self.hardResetClassicWebUSB();
1061
1095
  },
1062
1096
  });
1063
1097
  resetStrategies.push({
1064
1098
  name: "Inverted Both (WebUSB) - CH34x",
1065
- fn: async function () {
1099
+ fn: async () => {
1066
1100
  return await self.hardResetInvertedWebUSB();
1067
1101
  },
1068
1102
  });
1069
1103
  resetStrategies.push({
1070
1104
  name: "Inverted RTS (WebUSB) - CH34x",
1071
- fn: async function () {
1105
+ fn: async () => {
1072
1106
  return await self.hardResetInvertedRTSWebUSB();
1073
1107
  },
1074
1108
  });
1075
1109
  resetStrategies.push({
1076
1110
  name: "Inverted DTR (WebUSB) - CH34x",
1077
- fn: async function () {
1111
+ fn: async () => {
1078
1112
  return await self.hardResetInvertedDTRWebUSB();
1079
1113
  },
1080
1114
  });
@@ -1084,35 +1118,35 @@ export class ESPLoader extends EventTarget {
1084
1118
 
1085
1119
  resetStrategies.push({
1086
1120
  name: "UnixTight (WebUSB) - CP2102",
1087
- fn: async function () {
1121
+ fn: async () => {
1088
1122
  return await self.hardResetUnixTightWebUSB();
1089
1123
  },
1090
1124
  });
1091
1125
 
1092
1126
  resetStrategies.push({
1093
1127
  name: "Classic (WebUSB) - CP2102",
1094
- fn: async function () {
1128
+ fn: async () => {
1095
1129
  return await self.hardResetClassicWebUSB();
1096
1130
  },
1097
1131
  });
1098
1132
 
1099
1133
  resetStrategies.push({
1100
1134
  name: "Inverted Both (WebUSB) - CP2102",
1101
- fn: async function () {
1135
+ fn: async () => {
1102
1136
  return await self.hardResetInvertedWebUSB();
1103
1137
  },
1104
1138
  });
1105
1139
 
1106
1140
  resetStrategies.push({
1107
1141
  name: "Inverted RTS (WebUSB) - CP2102",
1108
- fn: async function () {
1142
+ fn: async () => {
1109
1143
  return await self.hardResetInvertedRTSWebUSB();
1110
1144
  },
1111
1145
  });
1112
1146
 
1113
1147
  resetStrategies.push({
1114
1148
  name: "Inverted DTR (WebUSB) - CP2102",
1115
- fn: async function () {
1149
+ fn: async () => {
1116
1150
  return await self.hardResetInvertedDTRWebUSB();
1117
1151
  },
1118
1152
  });
@@ -1120,7 +1154,7 @@ export class ESPLoader extends EventTarget {
1120
1154
  // For other USB-Serial chips, try UnixTight first, then multiple strategies
1121
1155
  resetStrategies.push({
1122
1156
  name: "UnixTight (WebUSB)",
1123
- fn: async function () {
1157
+ fn: async () => {
1124
1158
  return await self.hardResetUnixTightWebUSB();
1125
1159
  },
1126
1160
  });
@@ -1198,7 +1232,6 @@ export class ESPLoader extends EventTarget {
1198
1232
  }
1199
1233
  }
1200
1234
  } else {
1201
- // Web Serial (Desktop) strategies
1202
1235
  // Strategy: USB-JTAG/Serial reset
1203
1236
  if (isUSBJTAGSerial || isEspressifUSB) {
1204
1237
  resetStrategies.push({
@@ -1209,11 +1242,11 @@ export class ESPLoader extends EventTarget {
1209
1242
  });
1210
1243
  }
1211
1244
 
1212
- // Strategy: Classic reset
1245
+ // Strategy: UnixTight reset
1213
1246
  resetStrategies.push({
1214
- name: "Classic",
1247
+ name: "UnixTight",
1215
1248
  fn: async function () {
1216
- return await self.hardResetClassic();
1249
+ return await self.hardResetUnixTight();
1217
1250
  },
1218
1251
  });
1219
1252
 
@@ -1244,17 +1277,44 @@ export class ESPLoader extends EventTarget {
1244
1277
 
1245
1278
  await strategy.fn();
1246
1279
 
1247
- // Try to sync after reset with internally time-bounded sync (3 seconds per strategy)
1248
- const syncSuccess = await this.syncWithTimeout(3000);
1280
+ // Try to sync after reset
1281
+ // USB-Serial / native USB chips needs different sync approaches
1249
1282
 
1250
- if (syncSuccess) {
1251
- // Sync succeeded
1252
- this.logger.log(
1253
- `Connected successfully with ${strategy.name} reset.`,
1254
- );
1255
- return;
1283
+ if (isUSBSerialChip) {
1284
+ // USB-Serial chips: Use timeout strategy (2 seconds)
1285
+ // this.logger.log(`USB-Serial chip detected, using sync with timeout.`);
1286
+ const syncSuccess = await this.syncWithTimeout(2000);
1287
+
1288
+ if (syncSuccess) {
1289
+ // Sync succeeded
1290
+ this.logger.log(
1291
+ `Connected USB Serial successfully with ${strategy.name} reset.`,
1292
+ );
1293
+ return;
1294
+ } else {
1295
+ throw new Error("Sync timeout or abandoned");
1296
+ }
1256
1297
  } else {
1257
- throw new Error("Sync timeout or abandoned");
1298
+ // Native USB chips
1299
+ // Note: We use Promise.race with sync() directly instead of syncWithTimeout()
1300
+ // because syncWithTimeout causes CDC/JTAG devices to hang for unknown reasons.
1301
+ // The abandon flag in readPacket() prevents overlapping I/O.
1302
+ // this.logger.log(`Native USB chip detected, using CDC/JTAG sync.`);
1303
+ const syncPromise = this.sync();
1304
+ const timeoutPromise = new Promise<void>((_, reject) =>
1305
+ setTimeout(() => reject(new Error("Sync timeout")), 1000),
1306
+ );
1307
+
1308
+ try {
1309
+ await Promise.race([syncPromise, timeoutPromise]);
1310
+ // Sync succeeded
1311
+ this.logger.log(
1312
+ `Connected CDC/JTAG successfully with ${strategy.name} reset.`,
1313
+ );
1314
+ return;
1315
+ } catch (error) {
1316
+ throw new Error("Sync timeout or abandoned");
1317
+ }
1258
1318
  }
1259
1319
  } catch (error) {
1260
1320
  lastError = error as Error;
@@ -1309,9 +1369,9 @@ export class ESPLoader extends EventTarget {
1309
1369
  // just reset (no bootloader mode)
1310
1370
  if (this.isWebUSB()) {
1311
1371
  // WebUSB: Use longer delays for better compatibility
1312
- await this.setRTS(true); // EN->LOW
1372
+ await this.setRTSWebUSB(true); // EN->LOW
1313
1373
  await this.sleep(200);
1314
- await this.setRTS(false);
1374
+ await this.setRTSWebUSB(false);
1315
1375
  await this.sleep(200);
1316
1376
  this.logger.log("Hard reset (WebUSB).");
1317
1377
  } else {
@@ -1560,7 +1620,6 @@ export class ESPLoader extends EventTarget {
1560
1620
  "Timed out waiting for packet " + waitingFor,
1561
1621
  );
1562
1622
  }
1563
-
1564
1623
  const b = this._readByte()!;
1565
1624
 
1566
1625
  if (partialPacket === null) {
@@ -2676,7 +2735,21 @@ export class ESPLoader extends EventTarget {
2676
2735
  resolve(undefined);
2677
2736
  return;
2678
2737
  }
2679
- this.addEventListener("disconnect", resolve, { once: true });
2738
+
2739
+ // Set a timeout to prevent hanging (important for node-usb)
2740
+ const timeout = setTimeout(() => {
2741
+ this.logger.debug("Disconnect timeout - forcing resolution");
2742
+ resolve(undefined);
2743
+ }, 1000);
2744
+
2745
+ this.addEventListener(
2746
+ "disconnect",
2747
+ () => {
2748
+ clearTimeout(timeout);
2749
+ resolve(undefined);
2750
+ },
2751
+ { once: true },
2752
+ );
2680
2753
 
2681
2754
  // Only cancel if reader is still active
2682
2755
  try {
@@ -2684,10 +2757,19 @@ export class ESPLoader extends EventTarget {
2684
2757
  } catch (err) {
2685
2758
  this.logger.debug(`Reader cancel error: ${err}`);
2686
2759
  // Reader already released, resolve immediately
2760
+ clearTimeout(timeout);
2687
2761
  resolve(undefined);
2688
2762
  }
2689
2763
  });
2690
2764
  this.connected = false;
2765
+
2766
+ // Close the port (important for node-usb adapter)
2767
+ try {
2768
+ await this.port.close();
2769
+ this.logger.debug("Port closed successfully");
2770
+ } catch (err) {
2771
+ this.logger.debug(`Port close error: ${err}`);
2772
+ }
2691
2773
  }
2692
2774
 
2693
2775
  /**
@@ -3357,6 +3439,12 @@ class EspStubLoader extends ESPLoader {
3357
3439
  if (size > maxValue) {
3358
3440
  throw new Error(`Size ${size} exceeds maximum value ${maxValue}`);
3359
3441
  }
3442
+ // Check for wrap-around
3443
+ if (offset + size > maxValue) {
3444
+ throw new Error(
3445
+ `Region end (offset + size = ${offset + size}) exceeds maximum addressable range ${maxValue}`,
3446
+ );
3447
+ }
3360
3448
 
3361
3449
  const timeout = timeoutPerMb(ERASE_REGION_TIMEOUT_PER_MB, size);
3362
3450
  const buffer = pack("<II", offset, size);
package/src/index.ts CHANGED
@@ -21,39 +21,68 @@ export {
21
21
  CHIP_FAMILY_ESP32H21,
22
22
  CHIP_FAMILY_ESP32P4,
23
23
  CHIP_FAMILY_ESP32S31,
24
+ // Command constants
25
+ ESP_FLASH_BEGIN,
26
+ ESP_FLASH_DATA,
27
+ ESP_FLASH_END,
28
+ ESP_MEM_BEGIN,
29
+ ESP_MEM_END,
30
+ ESP_MEM_DATA,
31
+ ESP_SYNC,
32
+ ESP_WRITE_REG,
33
+ ESP_READ_REG,
34
+ ESP_ERASE_FLASH,
35
+ ESP_ERASE_REGION,
36
+ ESP_READ_FLASH,
37
+ ESP_SPI_SET_PARAMS,
38
+ ESP_SPI_ATTACH,
39
+ ESP_CHANGE_BAUDRATE,
40
+ ESP_SPI_FLASH_MD5,
41
+ ESP_GET_SECURITY_INFO,
42
+ ESP_CHECKSUM_MAGIC,
43
+ ESP_FLASH_DEFL_BEGIN,
44
+ ESP_FLASH_DEFL_DATA,
45
+ ESP_FLASH_DEFL_END,
46
+ ROM_INVALID_RECV_MSG,
47
+ // Block size constants
48
+ USB_RAM_BLOCK,
49
+ ESP_RAM_BLOCK,
50
+ // Timeout constants
51
+ DEFAULT_TIMEOUT,
52
+ CHIP_ERASE_TIMEOUT,
53
+ MAX_TIMEOUT,
54
+ SYNC_TIMEOUT,
55
+ ERASE_REGION_TIMEOUT_PER_MB,
56
+ MEM_END_ROM_TIMEOUT,
57
+ FLASH_READ_TIMEOUT,
24
58
  } from "./const";
25
59
 
26
60
  export const connect = async (logger: Logger) => {
61
+ // - Request a port and open a connection.
62
+ // Try to use requestSerialPort if available (supports WebUSB for Android)
27
63
  let port: SerialPort;
28
-
29
- // Check if a custom requestSerialPort function is available (e.g., from WebUSB wrapper)
30
64
  const customRequestPort = (
31
65
  globalThis as { requestSerialPort?: () => Promise<SerialPort> }
32
66
  ).requestSerialPort;
33
-
34
67
  if (typeof customRequestPort === "function") {
35
- // Use custom port request function (handles Android/WebUSB automatically)
36
- logger.log("Using custom port request function");
37
68
  port = await customRequestPort();
38
69
  } else {
39
- // Fallback to standard Web Serial API
70
+ // Check if Web Serial API is available
40
71
  if (!navigator.serial) {
41
72
  throw new Error(
42
73
  "Web Serial API is not supported in this browser. " +
43
- "Please use Chrome 89+, Edge 89+, or Opera on desktop, or Chrome 61+ on Android with USB OTG. " +
74
+ "Please use Chrome, Edge, or Opera on desktop, or Chrome on Android. " +
44
75
  "Note: The page must be served over HTTPS or localhost.",
45
76
  );
46
77
  }
47
78
  port = await navigator.serial.requestPort();
48
79
  }
49
80
 
50
- // Only open if not already open (WebUSB may return an opened port)
81
+ // Only open if not already open (requestSerialPort may return an opened port)
51
82
  if (!port.readable || !port.writable) {
52
83
  await port.open({ baudRate: ESP_ROM_BAUD });
53
84
  }
54
85
 
55
- logger.log("Connected successfully.");
56
-
57
86
  return new ESPLoader(port, logger);
58
87
  };
59
88
 
@@ -63,12 +92,10 @@ export const connectWithPort = async (port: SerialPort, logger: Logger) => {
63
92
  throw new Error("Port is required");
64
93
  }
65
94
 
66
- // Only open if not already open
95
+ // Check if port is already open, if not open it
67
96
  if (!port.readable || !port.writable) {
68
97
  await port.open({ baudRate: ESP_ROM_BAUD });
69
98
  }
70
99
 
71
- logger.log("Connected successfully.");
72
-
73
100
  return new ESPLoader(port, logger);
74
101
  };