tasmota-webserial-esptool 7.1.0 → 7.2.1

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,13 @@ 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
+ // PID 0x0002 = TinyUSB CDC (after flash)
104
+ // PID 0x1001 = ROM Bootloader (before flash)
105
+ if (portInfo.usbVendorId === 0x303a &&
106
+ (portInfo.usbProductId === 0x2 || portInfo.usbProductId === 0x1001)) {
107
+ this._isESP32S2NativeUSB = true;
108
+ }
95
109
  }
96
110
  // Don't await this promise so it doesn't block rest of method.
97
111
  this.readLoop();
@@ -109,6 +123,8 @@ export class ESPLoader extends EventTarget {
109
123
  }
110
124
  this.logger.log(`Chip type ${this.chipName}`);
111
125
  this.logger.debug(`Bootloader flash offset: 0x${FlAddr.flashOffs.toString(16)}`);
126
+ // Mark initialization as successful
127
+ this._initializationSucceeded = true;
112
128
  }
113
129
  /**
114
130
  * Detect chip type using GET_SECURITY_INFO (for newer chips) or magic value (for older chips)
@@ -154,7 +170,7 @@ export class ESPLoader extends EventTarget {
154
170
  this.logger.debug(`Re-sync after GET_SECURITY_INFO failure: ${syncErr}`);
155
171
  }
156
172
  }
157
- // Fallback: Use magic value detection for ESP8266, ESP32, ESP32-S2, and ESP32-P4 RC versions
173
+ // Fallback: Use magic value detection for ESP8266, ESP32, ESP32-S2
158
174
  const chipMagicValue = await this.readRegister(CHIP_DETECT_MAGIC_REG_ADDR);
159
175
  const chip = CHIP_DETECT_MAGIC_VALUES[chipMagicValue >>> 0];
160
176
  if (chip === undefined) {
@@ -162,7 +178,6 @@ export class ESPLoader extends EventTarget {
162
178
  }
163
179
  this.chipName = chip.name;
164
180
  this.chipFamily = chip.family;
165
- // For ESP32-P4 detected via magic value (old revisions), set variant
166
181
  if (this.chipFamily === CHIP_FAMILY_ESP32P4) {
167
182
  this.chipRevision = await this.getChipRevision();
168
183
  this.logger.debug(`ESP32-P4 revision: ${this.chipRevision}`);
@@ -256,6 +271,14 @@ export class ESPLoader extends EventTarget {
256
271
  }
257
272
  // Disconnected!
258
273
  this.connected = false;
274
+ // Check if this is ESP32-S2 Native USB that needs port reselection
275
+ // Only trigger reconnect if initialization did NOT succeed (wrong port)
276
+ if (this._isESP32S2NativeUSB && !this._initializationSucceeded) {
277
+ this.logger.log("ESP32-S2 Native USB detected - requesting port reselection");
278
+ this.dispatchEvent(new CustomEvent("esp32s2-usb-reconnect", {
279
+ detail: { message: "ESP32-S2 Native USB requires port reselection" },
280
+ }));
281
+ }
259
282
  this.dispatchEvent(new Event("disconnect"));
260
283
  this.logger.debug("Finished read loop");
261
284
  }
@@ -907,7 +930,7 @@ export class ESPLoader extends EventTarget {
907
930
  }
908
931
  async setDataLengths(spiAddresses, mosiBits, misoBits) {
909
932
  if (spiAddresses.mosiDlenOffs != -1) {
910
- // ESP32/32S2/32S3/32C3 has a more sophisticated way to set up "user" commands
933
+ // Actual MCUs have a more sophisticated way to set up "user" commands
911
934
  const SPI_MOSI_DLEN_REG = spiAddresses.regBase + spiAddresses.mosiDlenOffs;
912
935
  const SPI_MISO_DLEN_REG = spiAddresses.regBase + spiAddresses.misoDlenOffs;
913
936
  if (mosiBits > 0) {
@@ -948,7 +971,7 @@ export class ESPLoader extends EventTarget {
948
971
  const SPI_USR_COMMAND = 1 << 31;
949
972
  const SPI_USR_MISO = 1 << 28;
950
973
  const SPI_USR_MOSI = 1 << 27;
951
- // SPI registers, base address differs ESP32* vs 8266
974
+ // SPI registers, base address differs
952
975
  const spiAddresses = getSpiFlashAddresses(this.getChipFamily());
953
976
  const base = spiAddresses.regBase;
954
977
  const SPI_CMD_REG = base;
@@ -1100,6 +1123,10 @@ export class ESPLoader extends EventTarget {
1100
1123
  return espStubLoader;
1101
1124
  }
1102
1125
  async writeToStream(data) {
1126
+ if (!this.port.writable) {
1127
+ this.logger.debug("Port writable stream not available, skipping write");
1128
+ return;
1129
+ }
1103
1130
  const writer = this.port.writable.getWriter();
1104
1131
  await writer.write(new Uint8Array(data));
1105
1132
  try {
@@ -1114,6 +1141,10 @@ export class ESPLoader extends EventTarget {
1114
1141
  await this._parent.disconnect();
1115
1142
  return;
1116
1143
  }
1144
+ if (!this.port.writable) {
1145
+ this.logger.debug("Port already closed, skipping disconnect");
1146
+ return;
1147
+ }
1117
1148
  await this.port.writable.getWriter().close();
1118
1149
  await new Promise((resolve) => {
1119
1150
  if (!this._reader) {
@@ -1146,7 +1177,6 @@ export class ESPLoader extends EventTarget {
1146
1177
  }
1147
1178
  this._reader = undefined;
1148
1179
  }
1149
- await sleep(SYNC_TIMEOUT);
1150
1180
  // Close port
1151
1181
  try {
1152
1182
  await this.port.close();
@@ -1155,8 +1185,6 @@ export class ESPLoader extends EventTarget {
1155
1185
  catch (err) {
1156
1186
  this.logger.debug(`Port close error: ${err}`);
1157
1187
  }
1158
- // Wait for port to fully close
1159
- await sleep(SYNC_TIMEOUT);
1160
1188
  // Open the port
1161
1189
  this.logger.debug("Opening port...");
1162
1190
  try {
@@ -1166,8 +1194,6 @@ export class ESPLoader extends EventTarget {
1166
1194
  catch (err) {
1167
1195
  throw new Error(`Failed to open port: ${err}`);
1168
1196
  }
1169
- // Wait for port to be fully ready
1170
- await sleep(SYNC_TIMEOUT);
1171
1197
  // Verify port streams are available
1172
1198
  if (!this.port.readable || !this.port.writable) {
1173
1199
  throw new Error(`Port streams not available after open (readable: ${!!this.port.readable}, writable: ${!!this.port.writable})`);
@@ -1178,7 +1204,7 @@ export class ESPLoader extends EventTarget {
1178
1204
  const savedChipRevision = this.chipRevision;
1179
1205
  const savedChipVariant = this.chipVariant;
1180
1206
  const savedFlashSize = this.flashSize;
1181
- // Reinitialize without chip detection
1207
+ // Reinitialize
1182
1208
  await this.hardReset(true);
1183
1209
  if (!this._parent) {
1184
1210
  this.__inputBuffer = [];
@@ -1187,7 +1213,7 @@ export class ESPLoader extends EventTarget {
1187
1213
  }
1188
1214
  await this.flushSerialBuffers();
1189
1215
  await this.sync();
1190
- // Restore chip info (skip detection)
1216
+ // Restore chip info
1191
1217
  this.chipFamily = savedChipFamily;
1192
1218
  this.chipName = savedChipName;
1193
1219
  this.chipRevision = savedChipRevision;
@@ -1198,14 +1224,12 @@ export class ESPLoader extends EventTarget {
1198
1224
  if (!this.port.writable || !this.port.readable) {
1199
1225
  throw new Error("Port not ready after reconnect");
1200
1226
  }
1201
- // Load stub (skip flash detection)
1227
+ // Load stub
1202
1228
  const stubLoader = await this.runStub(true);
1203
1229
  this.logger.debug("Stub loaded");
1204
1230
  // Restore baudrate if it was changed
1205
1231
  if (this._currentBaudRate !== ESP_ROM_BAUD) {
1206
1232
  await stubLoader.setBaudrate(this._currentBaudRate);
1207
- // Wait for port to be ready after baudrate change
1208
- await sleep(SYNC_TIMEOUT);
1209
1233
  // Verify port is still ready after baudrate change
1210
1234
  if (!this.port.writable || !this.port.readable) {
1211
1235
  throw new Error(`Port not ready after baudrate change (readable: ${!!this.port.readable}, writable: ${!!this.port.writable})`);
@@ -1223,18 +1247,12 @@ export class ESPLoader extends EventTarget {
1223
1247
  * This clears both the application RX buffer and waits for hardware buffers to drain
1224
1248
  */
1225
1249
  async flushSerialBuffers() {
1226
- // Clear application RX buffer
1250
+ // Clear application buffer
1227
1251
  if (!this._parent) {
1228
1252
  this.__inputBuffer = [];
1229
1253
  }
1230
- // Wait for any pending TX operations and in-flight RX data
1254
+ // Wait for any pending data
1231
1255
  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
1256
  // Final clear
1239
1257
  if (!this._parent) {
1240
1258
  this.__inputBuffer = [];
@@ -1253,20 +1271,6 @@ export class ESPLoader extends EventTarget {
1253
1271
  if (!this.IS_STUB) {
1254
1272
  throw new Error("Reading flash is only supported in stub mode. Please run runStub() first.");
1255
1273
  }
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
1274
  // Flush serial buffers before flash read operation
1271
1275
  await this.flushSerialBuffers();
1272
1276
  this.logger.log(`Reading ${size} bytes from flash at address 0x${addr.toString(16)}...`);
@@ -1275,16 +1279,6 @@ export class ESPLoader extends EventTarget {
1275
1279
  let currentAddr = addr;
1276
1280
  let remainingSize = size;
1277
1281
  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
1282
  const chunkSize = Math.min(CHUNK_SIZE, remainingSize);
1289
1283
  let chunkSuccess = false;
1290
1284
  let retryCount = 0;