tasmota-webserial-esptool 7.1.0 → 7.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/README.md CHANGED
@@ -8,7 +8,7 @@ WebSerial ESPTool is **not** based on esptool.js
8
8
  ## Local development
9
9
 
10
10
  - Clone this repository.
11
- - Install dependencies with `yarn`
11
+ - Install dependencies with `npm install`
12
12
  - Run `script/develop`
13
13
  - Open http://localhost:5004/
14
14
 
package/css/style.css CHANGED
@@ -70,6 +70,24 @@
70
70
  color: #000;
71
71
  }
72
72
 
73
+ .header button.highlight {
74
+ background-color: #ff6b00;
75
+ border-color: #ff6b00;
76
+ color: #fff;
77
+ animation: pulse 1.5s infinite;
78
+ }
79
+
80
+ @keyframes pulse {
81
+ 0%, 100% {
82
+ transform: scale(1);
83
+ box-shadow: 0 0 0 0 rgba(255, 107, 0, 0.7);
84
+ }
85
+ 50% {
86
+ transform: scale(1.05);
87
+ box-shadow: 0 0 0 10px rgba(255, 107, 0, 0);
88
+ }
89
+ }
90
+
73
91
  .header select {
74
92
  height: 30px;
75
93
  }
@@ -550,3 +568,80 @@ div.clear {
550
568
  .center {
551
569
  text-align: center;
552
570
  }
571
+
572
+ /* ESP32-S2 Reconnect Modal */
573
+ .modal {
574
+ display: flex;
575
+ position: fixed;
576
+ z-index: 9999;
577
+ left: 0;
578
+ top: 0;
579
+ width: 100%;
580
+ height: 100%;
581
+ background-color: rgba(0, 0, 0, 0.7);
582
+ align-items: center;
583
+ justify-content: center;
584
+ }
585
+
586
+ .modal.hidden {
587
+ display: none;
588
+ }
589
+
590
+ .modal-content {
591
+ background-color: #fff;
592
+ padding: 40px;
593
+ border-radius: 15px;
594
+ box-shadow: 0 5px 30px rgba(0, 0, 0, 0.3);
595
+ text-align: center;
596
+ max-width: 500px;
597
+ animation: modalSlideIn 0.3s ease-out;
598
+ }
599
+
600
+ @keyframes modalSlideIn {
601
+ from {
602
+ transform: translateY(-50px);
603
+ opacity: 0;
604
+ }
605
+ to {
606
+ transform: translateY(0);
607
+ opacity: 1;
608
+ }
609
+ }
610
+
611
+ .modal-content h2 {
612
+ color: #4a4a4a;
613
+ margin-top: 0;
614
+ font-size: 24px;
615
+ margin-bottom: 20px;
616
+ }
617
+
618
+ .modal-content p {
619
+ font-size: 16px;
620
+ line-height: 1.6;
621
+ margin: 15px 0;
622
+ color: #333;
623
+ }
624
+
625
+ .modal-button {
626
+ background-color: #fff;
627
+ color: #6b6b6b;
628
+ border: 2px solid #6b6b6b;
629
+ padding: 12px 40px;
630
+ font-size: 18px;
631
+ font-weight: 600;
632
+ border-radius: 25px;
633
+ cursor: pointer;
634
+ margin-top: 20px;
635
+ transition: background-color 0.2s, color 0.2s, border-color 0.2s;
636
+ display: inline-flex;
637
+ align-items: center;
638
+ justify-content: center;
639
+ line-height: normal;
640
+ text-align: center;
641
+ }
642
+
643
+ .modal-button:hover {
644
+ background-color: #71ae1e;
645
+ color: #fff;
646
+ border-color: #71ae1e;
647
+ }
package/dist/const.js CHANGED
@@ -244,30 +244,6 @@ export const CHIP_DETECT_MAGIC_VALUES = {
244
244
  0xfff0c101: { name: "ESP8266", family: CHIP_FAMILY_ESP8266 },
245
245
  0x00f01d83: { name: "ESP32", family: CHIP_FAMILY_ESP32 },
246
246
  0x000007c6: { name: "ESP32-S2", family: CHIP_FAMILY_ESP32S2 },
247
- 0x9: { name: "ESP32-S3", family: CHIP_FAMILY_ESP32S3 },
248
- 0x0c21e06f: { name: "ESP32-C2", family: CHIP_FAMILY_ESP32C2 },
249
- 0x6f51306f: { name: "ESP32-C2", family: CHIP_FAMILY_ESP32C2 },
250
- 0x7c41a06f: { name: "ESP32-C2", family: CHIP_FAMILY_ESP32C2 },
251
- 0x1b31506f: { name: "ESP32-C3", family: CHIP_FAMILY_ESP32C3 },
252
- 0x4361606f: { name: "ESP32-C3", family: CHIP_FAMILY_ESP32C3 },
253
- 0x4881606f: { name: "ESP32-C3", family: CHIP_FAMILY_ESP32C3 },
254
- 0x6921506f: { name: "ESP32-C3", family: CHIP_FAMILY_ESP32C3 },
255
- 0x1101406f: { name: "ESP32-C5", family: CHIP_FAMILY_ESP32C5 },
256
- 0x5fd1406f: { name: "ESP32-C5", family: CHIP_FAMILY_ESP32C5 },
257
- 0x5c501458: { name: "ESP32-C5", family: CHIP_FAMILY_ESP32C5 },
258
- 0x63e1406f: { name: "ESP32-C5", family: CHIP_FAMILY_ESP32C5 },
259
- 0x2ce0806f: { name: "ESP32-C6", family: CHIP_FAMILY_ESP32C6 },
260
- 0x2421606f: { name: "ESP32-C61", family: CHIP_FAMILY_ESP32C61 },
261
- 0x33f0206f: { name: "ESP32-C61", family: CHIP_FAMILY_ESP32C61 },
262
- 0x4f81606f: { name: "ESP32-C61", family: CHIP_FAMILY_ESP32C61 },
263
- 0x7211606f: { name: "ESP32-C61", family: CHIP_FAMILY_ESP32C61 },
264
- 0x97e30068: { name: "ESP32-H2", family: CHIP_FAMILY_ESP32H2 },
265
- 0xd7b73e80: { name: "ESP32-H2", family: CHIP_FAMILY_ESP32H2 },
266
- // ESP32-P4 old revisions (< Rev. 300) - use magic value detection
267
- // Rev. 300+ uses IMAGE_CHIP_ID detection instead
268
- 0x0: { name: "ESP32-P4", family: CHIP_FAMILY_ESP32P4 },
269
- 0x7039ad9: { name: "ESP32-P4", family: CHIP_FAMILY_ESP32P4 },
270
- 0x0addbad0: { name: "ESP32-P4", family: CHIP_FAMILY_ESP32P4 },
271
247
  };
272
248
  // Commands supported by ESP8266 ROM bootloader
273
249
  export const ESP_FLASH_BEGIN = 0x02;
@@ -18,6 +18,8 @@ export declare class ESPLoader extends EventTarget {
18
18
  private _currentBaudRate;
19
19
  private _maxUSBSerialBaudrate?;
20
20
  private _reader?;
21
+ private _isESP32S2NativeUSB;
22
+ private _initializationSucceeded;
21
23
  constructor(port: SerialPort, logger: Logger, _parent?: ESPLoader | undefined);
22
24
  private get _inputBuffer();
23
25
  private get _totalBytesRead();
@@ -21,6 +21,8 @@ export class ESPLoader extends EventTarget {
21
21
  this.connected = true;
22
22
  this.flashSize = null;
23
23
  this._currentBaudRate = ESP_ROM_BAUD;
24
+ this._isESP32S2NativeUSB = false;
25
+ this._initializationSucceeded = false;
24
26
  this.state_DTR = false;
25
27
  }
26
28
  get _inputBuffer() {
@@ -44,8 +46,13 @@ export class ESPLoader extends EventTarget {
44
46
  const chips = {
45
47
  0x1a86: {
46
48
  // QinHeng Electronics
49
+ 0x7522: { name: "CH340", maxBaudrate: 460800 },
47
50
  0x7523: { name: "CH340", maxBaudrate: 460800 },
51
+ 0x7584: { name: "CH340", maxBaudrate: 460800 },
52
+ 0x5523: { name: "CH341", maxBaudrate: 2000000 },
53
+ 0x55d3: { name: "CH343", maxBaudrate: 6000000 },
48
54
  0x55d4: { name: "CH9102", maxBaudrate: 6000000 },
55
+ 0x55d8: { name: "CH9101", maxBaudrate: 3000000 },
49
56
  },
50
57
  0x10c4: {
51
58
  // Silicon Labs
@@ -92,6 +99,10 @@ export class ESPLoader extends EventTarget {
92
99
  this._maxUSBSerialBaudrate = chipInfo.maxBaudrate;
93
100
  this.logger.log(`Max baudrate: ${chipInfo.maxBaudrate}`);
94
101
  }
102
+ // Detect ESP32-S2 Native USB
103
+ if (portInfo.usbVendorId === 0x303a && portInfo.usbProductId === 0x2) {
104
+ this._isESP32S2NativeUSB = true;
105
+ }
95
106
  }
96
107
  // Don't await this promise so it doesn't block rest of method.
97
108
  this.readLoop();
@@ -109,6 +120,8 @@ export class ESPLoader extends EventTarget {
109
120
  }
110
121
  this.logger.log(`Chip type ${this.chipName}`);
111
122
  this.logger.debug(`Bootloader flash offset: 0x${FlAddr.flashOffs.toString(16)}`);
123
+ // Mark initialization as successful
124
+ this._initializationSucceeded = true;
112
125
  }
113
126
  /**
114
127
  * Detect chip type using GET_SECURITY_INFO (for newer chips) or magic value (for older chips)
@@ -154,7 +167,7 @@ export class ESPLoader extends EventTarget {
154
167
  this.logger.debug(`Re-sync after GET_SECURITY_INFO failure: ${syncErr}`);
155
168
  }
156
169
  }
157
- // Fallback: Use magic value detection for ESP8266, ESP32, ESP32-S2, and ESP32-P4 RC versions
170
+ // Fallback: Use magic value detection for ESP8266, ESP32, ESP32-S2
158
171
  const chipMagicValue = await this.readRegister(CHIP_DETECT_MAGIC_REG_ADDR);
159
172
  const chip = CHIP_DETECT_MAGIC_VALUES[chipMagicValue >>> 0];
160
173
  if (chip === undefined) {
@@ -162,7 +175,6 @@ export class ESPLoader extends EventTarget {
162
175
  }
163
176
  this.chipName = chip.name;
164
177
  this.chipFamily = chip.family;
165
- // For ESP32-P4 detected via magic value (old revisions), set variant
166
178
  if (this.chipFamily === CHIP_FAMILY_ESP32P4) {
167
179
  this.chipRevision = await this.getChipRevision();
168
180
  this.logger.debug(`ESP32-P4 revision: ${this.chipRevision}`);
@@ -256,6 +268,14 @@ export class ESPLoader extends EventTarget {
256
268
  }
257
269
  // Disconnected!
258
270
  this.connected = false;
271
+ // Check if this is ESP32-S2 Native USB that needs port reselection
272
+ // Only trigger reconnect if initialization did NOT succeed (wrong port)
273
+ if (this._isESP32S2NativeUSB && !this._initializationSucceeded) {
274
+ this.logger.log("ESP32-S2 Native USB detected - requesting port reselection");
275
+ this.dispatchEvent(new CustomEvent("esp32s2-usb-reconnect", {
276
+ detail: { message: "ESP32-S2 Native USB requires port reselection" },
277
+ }));
278
+ }
259
279
  this.dispatchEvent(new Event("disconnect"));
260
280
  this.logger.debug("Finished read loop");
261
281
  }
@@ -907,7 +927,7 @@ export class ESPLoader extends EventTarget {
907
927
  }
908
928
  async setDataLengths(spiAddresses, mosiBits, misoBits) {
909
929
  if (spiAddresses.mosiDlenOffs != -1) {
910
- // ESP32/32S2/32S3/32C3 has a more sophisticated way to set up "user" commands
930
+ // Actual MCUs have a more sophisticated way to set up "user" commands
911
931
  const SPI_MOSI_DLEN_REG = spiAddresses.regBase + spiAddresses.mosiDlenOffs;
912
932
  const SPI_MISO_DLEN_REG = spiAddresses.regBase + spiAddresses.misoDlenOffs;
913
933
  if (mosiBits > 0) {
@@ -948,7 +968,7 @@ export class ESPLoader extends EventTarget {
948
968
  const SPI_USR_COMMAND = 1 << 31;
949
969
  const SPI_USR_MISO = 1 << 28;
950
970
  const SPI_USR_MOSI = 1 << 27;
951
- // SPI registers, base address differs ESP32* vs 8266
971
+ // SPI registers, base address differs
952
972
  const spiAddresses = getSpiFlashAddresses(this.getChipFamily());
953
973
  const base = spiAddresses.regBase;
954
974
  const SPI_CMD_REG = base;
@@ -1100,6 +1120,10 @@ export class ESPLoader extends EventTarget {
1100
1120
  return espStubLoader;
1101
1121
  }
1102
1122
  async writeToStream(data) {
1123
+ if (!this.port.writable) {
1124
+ this.logger.debug("Port writable stream not available, skipping write");
1125
+ return;
1126
+ }
1103
1127
  const writer = this.port.writable.getWriter();
1104
1128
  await writer.write(new Uint8Array(data));
1105
1129
  try {
@@ -1114,6 +1138,10 @@ export class ESPLoader extends EventTarget {
1114
1138
  await this._parent.disconnect();
1115
1139
  return;
1116
1140
  }
1141
+ if (!this.port.writable) {
1142
+ this.logger.debug("Port already closed, skipping disconnect");
1143
+ return;
1144
+ }
1117
1145
  await this.port.writable.getWriter().close();
1118
1146
  await new Promise((resolve) => {
1119
1147
  if (!this._reader) {
@@ -1146,7 +1174,6 @@ export class ESPLoader extends EventTarget {
1146
1174
  }
1147
1175
  this._reader = undefined;
1148
1176
  }
1149
- await sleep(SYNC_TIMEOUT);
1150
1177
  // Close port
1151
1178
  try {
1152
1179
  await this.port.close();
@@ -1155,8 +1182,6 @@ export class ESPLoader extends EventTarget {
1155
1182
  catch (err) {
1156
1183
  this.logger.debug(`Port close error: ${err}`);
1157
1184
  }
1158
- // Wait for port to fully close
1159
- await sleep(SYNC_TIMEOUT);
1160
1185
  // Open the port
1161
1186
  this.logger.debug("Opening port...");
1162
1187
  try {
@@ -1166,8 +1191,6 @@ export class ESPLoader extends EventTarget {
1166
1191
  catch (err) {
1167
1192
  throw new Error(`Failed to open port: ${err}`);
1168
1193
  }
1169
- // Wait for port to be fully ready
1170
- await sleep(SYNC_TIMEOUT);
1171
1194
  // Verify port streams are available
1172
1195
  if (!this.port.readable || !this.port.writable) {
1173
1196
  throw new Error(`Port streams not available after open (readable: ${!!this.port.readable}, writable: ${!!this.port.writable})`);
@@ -1178,7 +1201,7 @@ export class ESPLoader extends EventTarget {
1178
1201
  const savedChipRevision = this.chipRevision;
1179
1202
  const savedChipVariant = this.chipVariant;
1180
1203
  const savedFlashSize = this.flashSize;
1181
- // Reinitialize without chip detection
1204
+ // Reinitialize
1182
1205
  await this.hardReset(true);
1183
1206
  if (!this._parent) {
1184
1207
  this.__inputBuffer = [];
@@ -1187,7 +1210,7 @@ export class ESPLoader extends EventTarget {
1187
1210
  }
1188
1211
  await this.flushSerialBuffers();
1189
1212
  await this.sync();
1190
- // Restore chip info (skip detection)
1213
+ // Restore chip info
1191
1214
  this.chipFamily = savedChipFamily;
1192
1215
  this.chipName = savedChipName;
1193
1216
  this.chipRevision = savedChipRevision;
@@ -1198,14 +1221,12 @@ export class ESPLoader extends EventTarget {
1198
1221
  if (!this.port.writable || !this.port.readable) {
1199
1222
  throw new Error("Port not ready after reconnect");
1200
1223
  }
1201
- // Load stub (skip flash detection)
1224
+ // Load stub
1202
1225
  const stubLoader = await this.runStub(true);
1203
1226
  this.logger.debug("Stub loaded");
1204
1227
  // Restore baudrate if it was changed
1205
1228
  if (this._currentBaudRate !== ESP_ROM_BAUD) {
1206
1229
  await stubLoader.setBaudrate(this._currentBaudRate);
1207
- // Wait for port to be ready after baudrate change
1208
- await sleep(SYNC_TIMEOUT);
1209
1230
  // Verify port is still ready after baudrate change
1210
1231
  if (!this.port.writable || !this.port.readable) {
1211
1232
  throw new Error(`Port not ready after baudrate change (readable: ${!!this.port.readable}, writable: ${!!this.port.writable})`);
@@ -1223,18 +1244,12 @@ export class ESPLoader extends EventTarget {
1223
1244
  * This clears both the application RX buffer and waits for hardware buffers to drain
1224
1245
  */
1225
1246
  async flushSerialBuffers() {
1226
- // Clear application RX buffer
1247
+ // Clear application buffer
1227
1248
  if (!this._parent) {
1228
1249
  this.__inputBuffer = [];
1229
1250
  }
1230
- // Wait for any pending TX operations and in-flight RX data
1251
+ // Wait for any pending data
1231
1252
  await sleep(SYNC_TIMEOUT);
1232
- // Clear RX buffer again
1233
- if (!this._parent) {
1234
- this.__inputBuffer = [];
1235
- }
1236
- // Wait longer to ensure all stale data has been received and discarded
1237
- await sleep(SYNC_TIMEOUT * 2);
1238
1253
  // Final clear
1239
1254
  if (!this._parent) {
1240
1255
  this.__inputBuffer = [];
@@ -1253,20 +1268,6 @@ export class ESPLoader extends EventTarget {
1253
1268
  if (!this.IS_STUB) {
1254
1269
  throw new Error("Reading flash is only supported in stub mode. Please run runStub() first.");
1255
1270
  }
1256
- // Check if we should reconnect BEFORE starting the read
1257
- // Reconnect if total bytes read >= 4MB to ensure clean state
1258
- if (this._totalBytesRead >= 4 * 1024 * 1024) {
1259
- this.logger.log(
1260
- // `Total bytes read: ${this._totalBytesRead}. Reconnecting before new read...`,
1261
- `Reconnecting before new read...`);
1262
- try {
1263
- await this.reconnect();
1264
- }
1265
- catch (err) {
1266
- // If reconnect fails, throw error - don't continue with potentially broken state
1267
- throw new Error(`Reconnect failed: ${err}`);
1268
- }
1269
- }
1270
1271
  // Flush serial buffers before flash read operation
1271
1272
  await this.flushSerialBuffers();
1272
1273
  this.logger.log(`Reading ${size} bytes from flash at address 0x${addr.toString(16)}...`);
@@ -1275,16 +1276,6 @@ export class ESPLoader extends EventTarget {
1275
1276
  let currentAddr = addr;
1276
1277
  let remainingSize = size;
1277
1278
  while (remainingSize > 0) {
1278
- // Reconnect every 4MB to prevent browser buffer issues
1279
- if (allData.length > 0 && allData.length % (4 * 1024 * 1024) === 0) {
1280
- this.logger.debug(`Read ${allData.length} bytes. Reconnecting to clear buffers...`);
1281
- try {
1282
- await this.reconnect();
1283
- }
1284
- catch (err) {
1285
- throw new Error(`Reconnect failed during read: ${err}`);
1286
- }
1287
- }
1288
1279
  const chunkSize = Math.min(CHUNK_SIZE, remainingSize);
1289
1280
  let chunkSuccess = false;
1290
1281
  let retryCount = 0;