tasmota-webserial-esptool 9.2.9 → 9.2.11
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/dist/const.js +1 -1
- package/dist/esp_loader.d.ts +30 -5
- package/dist/esp_loader.js +178 -82
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2 -0
- package/dist/util.d.ts +4 -0
- package/dist/util.js +8 -0
- package/dist/web/index.js +1 -1
- package/js/modules/esptool.js +1 -1
- package/js/script.js +207 -180
- package/js/webusb-serial.js +21 -1
- package/package.json +3 -3
- package/src/const.ts +1 -1
- package/src/esp_loader.ts +206 -87
- package/src/index.ts +3 -0
- package/src/util.ts +9 -0
package/js/script.js
CHANGED
|
@@ -7,10 +7,27 @@ if (!globalThis.requestSerialPort) {
|
|
|
7
7
|
globalThis.requestSerialPort = requestSerialPort;
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
+
// Utility functions imported from esptool module
|
|
11
|
+
let toHex, formatMacAddr, sleep;
|
|
12
|
+
|
|
13
|
+
// Load utilities from esptool package
|
|
14
|
+
window.esptoolPackage.then((esptoolMod) => {
|
|
15
|
+
toHex = esptoolMod.toHex;
|
|
16
|
+
formatMacAddr = esptoolMod.formatMacAddr;
|
|
17
|
+
sleep = esptoolMod.sleep;
|
|
18
|
+
});
|
|
19
|
+
|
|
10
20
|
let espStub;
|
|
11
21
|
let esp32s2ReconnectInProgress = false;
|
|
22
|
+
let currentChipName = null; // Store chip name globally
|
|
23
|
+
let currentMacAddr = null; // Store MAC address globally
|
|
12
24
|
let isConnected = false; // Track connection state
|
|
13
|
-
let
|
|
25
|
+
let consoleInstance = null; // ESP32ToolConsole instance
|
|
26
|
+
let baudRateBeforeConsole = null; // Store baudrate before opening console
|
|
27
|
+
let espLoaderBeforeConsole = null; // Store original ESPLoader before console
|
|
28
|
+
let chipFamilyBeforeConsole = null; // Store chipFamily before opening console
|
|
29
|
+
let consoleResetHandler = null;
|
|
30
|
+
let consoleCloseHandler = null;
|
|
14
31
|
|
|
15
32
|
/**
|
|
16
33
|
* Clear all cached data and state on disconnect
|
|
@@ -32,14 +49,11 @@ function clearAllCachedData() {
|
|
|
32
49
|
}
|
|
33
50
|
|
|
34
51
|
const baudRates = [2000000, 1500000, 921600, 500000, 460800, 230400, 153600, 128000, 115200];
|
|
35
|
-
const bufferSize = 512;
|
|
36
|
-
const colors = ["#00a7e9", "#f89521", "#be1e2d"];
|
|
37
|
-
const measurementPeriodId = "0001";
|
|
38
52
|
|
|
39
53
|
const maxLogLength = 100;
|
|
40
54
|
const log = document.getElementById("log");
|
|
41
55
|
const butConnect = document.getElementById("butConnect");
|
|
42
|
-
const
|
|
56
|
+
const baudRateSelect = document.getElementById("baudRate");
|
|
43
57
|
const butClear = document.getElementById("butClear");
|
|
44
58
|
const butErase = document.getElementById("butErase");
|
|
45
59
|
const butProgram = document.getElementById("butProgram");
|
|
@@ -60,10 +74,91 @@ const progress = document.querySelectorAll(".upload .progress-bar");
|
|
|
60
74
|
const offsets = document.querySelectorAll(".upload .offset");
|
|
61
75
|
const appDiv = document.getElementById("app");
|
|
62
76
|
|
|
77
|
+
// Mobile detection
|
|
78
|
+
function isMobileDevice() {
|
|
79
|
+
const userAgent = navigator.userAgent || navigator.vendor || window.opera;
|
|
80
|
+
|
|
81
|
+
// Check for mobile user agents
|
|
82
|
+
const mobileRegex = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i;
|
|
83
|
+
const isMobileUA = mobileRegex.test(userAgent);
|
|
84
|
+
|
|
85
|
+
// Check for touch support
|
|
86
|
+
const hasTouch = 'ontouchstart' in window || navigator.maxTouchPoints > 0;
|
|
87
|
+
|
|
88
|
+
// Check screen size
|
|
89
|
+
const isSmallScreen = window.innerWidth <= 768;
|
|
90
|
+
|
|
91
|
+
return isMobileUA || (hasTouch && isSmallScreen);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Detect if we're using WebUSB (mobile/Android) or Web Serial (desktop)
|
|
96
|
+
* WebUSB is typically used on Android devices
|
|
97
|
+
* Web Serial is used on desktop browsers
|
|
98
|
+
*/
|
|
99
|
+
function isUsingWebUSB() {
|
|
100
|
+
// If we have an active connection, check the port's isWebUSB property
|
|
101
|
+
if (espStub && espStub.port && typeof espStub.port.isWebUSB !== 'undefined') {
|
|
102
|
+
return espStub.port.isWebUSB === true;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Fallback: Check if we're on a mobile device (likely using WebUSB)
|
|
106
|
+
if (isMobileDevice()) {
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Check if Web Serial is NOT available but USB is (WebUSB only)
|
|
111
|
+
if (!("serial" in navigator) && "usb" in navigator) {
|
|
112
|
+
return true;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Default to Web Serial (desktop)
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Update mobile classes and padding
|
|
120
|
+
function updateMobileClasses() {
|
|
121
|
+
const isMobile = isMobileDevice();
|
|
122
|
+
|
|
123
|
+
if (isMobile) {
|
|
124
|
+
document.body.classList.add('mobile-device');
|
|
125
|
+
document.body.classList.add('no-hover');
|
|
126
|
+
} else {
|
|
127
|
+
document.body.classList.remove('mobile-device');
|
|
128
|
+
document.body.classList.remove('no-hover');
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Update main padding to match header height
|
|
132
|
+
updateMainPadding();
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Debounce helper
|
|
136
|
+
function debounce(func, wait) {
|
|
137
|
+
let timeout;
|
|
138
|
+
return function executedFunction(...args) {
|
|
139
|
+
const later = () => {
|
|
140
|
+
clearTimeout(timeout);
|
|
141
|
+
func(...args);
|
|
142
|
+
};
|
|
143
|
+
clearTimeout(timeout);
|
|
144
|
+
timeout = setTimeout(later, wait);
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Debounced resize handler
|
|
149
|
+
const debouncedUpdateMobileClasses = debounce(updateMobileClasses, 250);
|
|
150
|
+
|
|
151
|
+
// Apply mobile class on load
|
|
152
|
+
updateMobileClasses();
|
|
153
|
+
|
|
154
|
+
// Update on resize and orientation change
|
|
155
|
+
window.addEventListener('resize', debouncedUpdateMobileClasses);
|
|
156
|
+
window.addEventListener('orientationchange', debouncedUpdateMobileClasses);
|
|
157
|
+
|
|
63
158
|
document.addEventListener("DOMContentLoaded", () => {
|
|
64
159
|
butConnect.addEventListener("click", () => {
|
|
65
160
|
clickConnect().catch(async (e) => {
|
|
66
|
-
|
|
161
|
+
debugMsg('Connection error: ' + e);
|
|
67
162
|
errorMsg(e.message || e);
|
|
68
163
|
if (espStub) {
|
|
69
164
|
await espStub.disconnect();
|
|
@@ -87,54 +182,27 @@ document.addEventListener("DOMContentLoaded", () => {
|
|
|
87
182
|
updateUploadRowsVisibility();
|
|
88
183
|
|
|
89
184
|
autoscroll.addEventListener("click", clickAutoscroll);
|
|
90
|
-
|
|
185
|
+
baudRateSelect.addEventListener("change", changeBaudRate);
|
|
91
186
|
darkMode.addEventListener("click", clickDarkMode);
|
|
92
187
|
debugMode.addEventListener("click", clickDebugMode);
|
|
93
188
|
showLog.addEventListener("click", clickShowLog);
|
|
94
189
|
window.addEventListener("error", function (event) {
|
|
95
190
|
console.log("Got an uncaught error: ", event.error);
|
|
96
191
|
});
|
|
97
|
-
|
|
98
|
-
//
|
|
99
|
-
const header = document.querySelector(".header");
|
|
100
|
-
const main = document.querySelector(".main");
|
|
101
|
-
|
|
102
|
-
// Show header on mouse enter at top of page
|
|
103
|
-
main.addEventListener("mousemove", (e) => {
|
|
104
|
-
if (e.clientY < 5 && header.classList.contains("header-hidden")) {
|
|
105
|
-
header.classList.remove("header-hidden");
|
|
106
|
-
main.classList.remove("no-header-padding");
|
|
107
|
-
}
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
// Keep header visible when mouse is over it
|
|
111
|
-
header.addEventListener("mouseenter", () => {
|
|
112
|
-
header.classList.remove("header-hidden");
|
|
113
|
-
main.classList.remove("no-header-padding");
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
// Hide header when mouse leaves (only if connected)
|
|
117
|
-
header.addEventListener("mouseleave", () => {
|
|
118
|
-
if (espStub && header.classList.contains("header-hidden") === false) {
|
|
119
|
-
setTimeout(() => {
|
|
120
|
-
if (!header.matches(":hover")) {
|
|
121
|
-
header.classList.add("header-hidden");
|
|
122
|
-
main.classList.add("no-header-padding");
|
|
123
|
-
}
|
|
124
|
-
}, 1000);
|
|
125
|
-
}
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
// Check if Web Serial API or WebUSB is supported
|
|
192
|
+
|
|
193
|
+
// Check for Web Serial or WebUSB support
|
|
129
194
|
if ("serial" in navigator || "usb" in navigator) {
|
|
130
195
|
const notSupported = document.getElementById("notSupported");
|
|
131
|
-
notSupported
|
|
196
|
+
notSupported.classList.add("hidden");
|
|
132
197
|
}
|
|
133
198
|
|
|
134
199
|
initBaudRate();
|
|
135
200
|
loadAllSettings();
|
|
136
201
|
updateTheme();
|
|
137
202
|
logMsg("WebSerial ESPTool loaded.");
|
|
203
|
+
|
|
204
|
+
// Set initial main padding based on header height
|
|
205
|
+
updateMainPadding();
|
|
138
206
|
});
|
|
139
207
|
|
|
140
208
|
function initBaudRate() {
|
|
@@ -142,7 +210,7 @@ function initBaudRate() {
|
|
|
142
210
|
var option = document.createElement("option");
|
|
143
211
|
option.text = rate + " Baud";
|
|
144
212
|
option.value = rate;
|
|
145
|
-
|
|
213
|
+
baudRateSelect.add(option);
|
|
146
214
|
}
|
|
147
215
|
}
|
|
148
216
|
|
|
@@ -234,26 +302,6 @@ function enableStyleSheet(node, enabled) {
|
|
|
234
302
|
node.disabled = !enabled;
|
|
235
303
|
}
|
|
236
304
|
|
|
237
|
-
/**
|
|
238
|
-
* Format a MAC address byte array as colon-separated uppercase hexadecimal octets.
|
|
239
|
-
* @param {Array<number>|Uint8Array} macAddr - Array of bytes representing the MAC address (each 0–255).
|
|
240
|
-
* @returns {string} Colon-separated uppercase hex octets, e.g. "AA:BB:CC:DD:EE:FF".
|
|
241
|
-
*/
|
|
242
|
-
function formatMacAddr(macAddr) {
|
|
243
|
-
return macAddr
|
|
244
|
-
.map((value) => value.toString(16).toUpperCase().padStart(2, "0"))
|
|
245
|
-
.join(":");
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
/**
|
|
249
|
-
* Format a byte value as a two-digit hexadecimal string prefixed with `0x`.
|
|
250
|
-
* @param {number} value - Numeric value to format (treated as a byte).
|
|
251
|
-
* @returns {string} Hex string in the form `0xNN` with lowercase letters and at least two digits.
|
|
252
|
-
*/
|
|
253
|
-
function toHex(value) {
|
|
254
|
-
return "0x" + value.toString(16).padStart(2, "0");
|
|
255
|
-
}
|
|
256
|
-
|
|
257
305
|
/**
|
|
258
306
|
* Parse flash size string (e.g., "256KB", "4MB") to bytes
|
|
259
307
|
* @param {string} sizeStr - Flash size string with unit (KB or MB)
|
|
@@ -285,36 +333,8 @@ function parseFlashSize(sizeStr) {
|
|
|
285
333
|
}
|
|
286
334
|
|
|
287
335
|
/**
|
|
288
|
-
*
|
|
289
|
-
|
|
290
|
-
async function loadWebUSBSerial() {
|
|
291
|
-
// Check if already loaded
|
|
292
|
-
if (globalThis.requestSerialPort) {
|
|
293
|
-
return;
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
// Dynamically load the WebUSB serial script as ES6 module
|
|
297
|
-
return new Promise((resolve, reject) => {
|
|
298
|
-
const script = document.createElement("script");
|
|
299
|
-
script.type = "module"; // CRITICAL: Load as ES6 module to support export statements
|
|
300
|
-
script.src = "js/webusb-serial.js";
|
|
301
|
-
script.onload = () => {
|
|
302
|
-
// Verify it loaded correctly
|
|
303
|
-
if (globalThis.requestSerialPort) {
|
|
304
|
-
resolve();
|
|
305
|
-
} else {
|
|
306
|
-
reject(new Error("WebUSB serial script loaded but requestSerialPort not found"));
|
|
307
|
-
}
|
|
308
|
-
};
|
|
309
|
-
script.onerror = () => reject(new Error("Failed to load WebUSB serial script"));
|
|
310
|
-
document.head.appendChild(script);
|
|
311
|
-
});
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
/**
|
|
315
|
-
* Toggle the connection state: connect to an ESP device (using WebUSB on Android or Web Serial on desktop) or disconnect if already connected.
|
|
316
|
-
*
|
|
317
|
-
* On connect, detect platform and transport, initialize the esploader, handle ESP32-S2 native USB reconnection flow when required (showing a modal on desktop or guidance on Android), run the device stub, update UI state, set the detected flash size and selected baud rate, and install a disconnect handler. On disconnect, remove the handler, close the port, clear the stub, and update the UI.
|
|
336
|
+
* @name clickConnect
|
|
337
|
+
* Click handler for the connect/disconnect button.
|
|
318
338
|
*/
|
|
319
339
|
async function clickConnect() {
|
|
320
340
|
console.log('[clickConnect] Function called');
|
|
@@ -346,43 +366,41 @@ async function clickConnect() {
|
|
|
346
366
|
|
|
347
367
|
// Platform detection: Android always uses WebUSB, Desktop uses Web Serial
|
|
348
368
|
const userAgent = navigator.userAgent || '';
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
// Load WebUSB support for Android
|
|
352
|
-
if (isAndroidPlatform && "usb" in navigator) {
|
|
353
|
-
try {
|
|
354
|
-
await loadWebUSBSerial();
|
|
355
|
-
logMsg("WebUSB support loaded");
|
|
356
|
-
} catch (err) {
|
|
357
|
-
errorMsg(`Failed to load WebUSB support: ${err.message}`);
|
|
358
|
-
throw err;
|
|
359
|
-
}
|
|
360
|
-
}
|
|
369
|
+
const isAndroid = /Android/i.test(userAgent);
|
|
361
370
|
|
|
362
371
|
// Only log platform details to UI in debug mode (avoid fingerprinting surface)
|
|
363
372
|
if (debugMode.checked) {
|
|
364
|
-
const platformMsg = `Platform: ${
|
|
373
|
+
const platformMsg = `Platform: ${isAndroid ? 'Android' : 'Desktop'} (UA: ${userAgent.substring(0, 50)}...)`;
|
|
365
374
|
logMsg(platformMsg);
|
|
366
375
|
}
|
|
367
|
-
logMsg(`Using: ${
|
|
376
|
+
logMsg(`Using: ${isAndroid ? 'WebUSB' : 'Web Serial'}`);
|
|
368
377
|
|
|
369
|
-
// Use esploaderMod.connect() which will automatically use globalThis.requestSerialPort if available
|
|
370
378
|
let esploader;
|
|
371
|
-
|
|
379
|
+
|
|
380
|
+
if (isAndroid) {
|
|
381
|
+
// Android: Use WebUSB directly
|
|
382
|
+
console.log('[Connect] Using WebUSB for Android');
|
|
383
|
+
try {
|
|
384
|
+
const port = await WebUSBSerial.requestPort((...args) => logMsg(...args));
|
|
385
|
+
esploader = await esploaderMod.connectWithPort(port, {
|
|
386
|
+
log: (...args) => logMsg(...args),
|
|
387
|
+
debug: (...args) => debugMsg(...args),
|
|
388
|
+
error: (...args) => errorMsg(...args),
|
|
389
|
+
});
|
|
390
|
+
} catch (err) {
|
|
391
|
+
logMsg(`WebUSB connection failed: ${err.message || err}`);
|
|
392
|
+
throw err;
|
|
393
|
+
}
|
|
394
|
+
} else {
|
|
395
|
+
// Desktop: Use Web Serial (standard esptool connect)
|
|
396
|
+
console.log('[Connect] Using Web Serial for Desktop');
|
|
372
397
|
esploader = await esploaderMod.connect({
|
|
373
398
|
log: (...args) => logMsg(...args),
|
|
374
399
|
debug: (...args) => debugMsg(...args),
|
|
375
400
|
error: (...args) => errorMsg(...args),
|
|
376
401
|
});
|
|
377
|
-
} catch (err) {
|
|
378
|
-
logMsg(`Connection failed: ${err.message || err}`);
|
|
379
|
-
throw err;
|
|
380
402
|
}
|
|
381
403
|
|
|
382
|
-
// Store port info for ESP32-S2 detection
|
|
383
|
-
let portInfo = esploader.port?.getInfo ? esploader.port.getInfo() : {};
|
|
384
|
-
let isESP32S2 = portInfo.usbVendorId === 0x303a && portInfo.usbProductId === 0x0002;
|
|
385
|
-
|
|
386
404
|
// Handle ESP32-S2 Native USB reconnection requirement for BROWSER
|
|
387
405
|
// Only add listener if not already in reconnect mode
|
|
388
406
|
if (!esp32s2ReconnectInProgress) {
|
|
@@ -395,58 +413,47 @@ async function clickConnect() {
|
|
|
395
413
|
esp32s2ReconnectInProgress = true;
|
|
396
414
|
logMsg("ESP32-S2 Native USB detected!");
|
|
397
415
|
toggleUIConnected(false);
|
|
416
|
+
const previousStubPort = espStub?.port;
|
|
398
417
|
espStub = undefined;
|
|
399
418
|
|
|
400
419
|
try {
|
|
401
420
|
// Close the port first
|
|
402
|
-
await esploader.port
|
|
403
|
-
|
|
404
|
-
//
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
logMsg("ESP32-S2 has switched to CDC mode");
|
|
408
|
-
logMsg("Please press and HOLD the BOOT button on your ESP32-S2, then click Connect");
|
|
409
|
-
toggleUIConnected(false);
|
|
410
|
-
esp32s2ReconnectInProgress = false;
|
|
411
|
-
return;
|
|
412
|
-
}
|
|
413
|
-
// For Desktop Web Serial: Use the modal dialog approach
|
|
414
|
-
if (esploader.port?.forget) {
|
|
415
|
-
await esploader.port.forget();
|
|
421
|
+
await esploader.port.close();
|
|
422
|
+
|
|
423
|
+
// Use the modal dialog approach
|
|
424
|
+
if (previousStubPort && previousStubPort.readable) {
|
|
425
|
+
await previousStubPort.close();
|
|
416
426
|
}
|
|
417
|
-
} catch (
|
|
418
|
-
// Ignore
|
|
419
|
-
|
|
427
|
+
} catch (closeErr) {
|
|
428
|
+
// Ignore port close errors
|
|
429
|
+
debugMsg(`Port close error (ignored): ${closeErr.message}`);
|
|
420
430
|
}
|
|
421
431
|
|
|
422
|
-
// Show modal dialog
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
const reconnectBtn = document.getElementById("butReconnectS2");
|
|
432
|
+
// Show modal dialog
|
|
433
|
+
const modal = document.getElementById("esp32s2Modal");
|
|
434
|
+
const reconnectBtn = document.getElementById("butReconnectS2");
|
|
426
435
|
|
|
427
|
-
|
|
436
|
+
modal.classList.remove("hidden");
|
|
428
437
|
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
438
|
+
// Handle reconnect button click
|
|
439
|
+
const handleReconnect = async () => {
|
|
440
|
+
modal.classList.add("hidden");
|
|
441
|
+
reconnectBtn.removeEventListener("click", handleReconnect);
|
|
433
442
|
|
|
434
|
-
|
|
443
|
+
logMsg("Requesting new device selection...");
|
|
435
444
|
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
reconnectBtn.addEventListener("click", handleReconnect);
|
|
449
|
-
}
|
|
445
|
+
// Trigger port selection
|
|
446
|
+
try {
|
|
447
|
+
await clickConnect();
|
|
448
|
+
// Reset flag on successful connection
|
|
449
|
+
esp32s2ReconnectInProgress = false;
|
|
450
|
+
} catch (err) {
|
|
451
|
+
errorMsg("Failed to reconnect: " + err);
|
|
452
|
+
// Reset flag on error so user can try again
|
|
453
|
+
esp32s2ReconnectInProgress = false;
|
|
454
|
+
}
|
|
455
|
+
};
|
|
456
|
+
reconnectBtn.addEventListener("click", handleReconnect);
|
|
450
457
|
});
|
|
451
458
|
}
|
|
452
459
|
|
|
@@ -471,11 +478,15 @@ async function clickConnect() {
|
|
|
471
478
|
logMsg("Connected to " + esploader.chipName);
|
|
472
479
|
logMsg("MAC Address: " + formatMacAddr(esploader.macAddr()));
|
|
473
480
|
|
|
481
|
+
// Store chip info globally
|
|
482
|
+
currentChipName = esploader.chipName;
|
|
483
|
+
currentMacAddr = formatMacAddr(esploader.macAddr());
|
|
484
|
+
|
|
474
485
|
espStub = await esploader.runStub();
|
|
475
486
|
|
|
476
487
|
toggleUIConnected(true);
|
|
477
488
|
toggleUIToolbar(true);
|
|
478
|
-
|
|
489
|
+
|
|
479
490
|
// Set detected flash size in the read size field
|
|
480
491
|
if (espStub.flashSize) {
|
|
481
492
|
const flashSizeBytes = parseFlashSize(espStub.flashSize);
|
|
@@ -483,7 +494,7 @@ async function clickConnect() {
|
|
|
483
494
|
}
|
|
484
495
|
|
|
485
496
|
// Set the selected baud rate
|
|
486
|
-
let baud = parseInt(
|
|
497
|
+
let baud = parseInt(baudRateSelect.value);
|
|
487
498
|
if (baudRates.includes(baud)) {
|
|
488
499
|
await espStub.setBaudrate(baud);
|
|
489
500
|
}
|
|
@@ -491,7 +502,7 @@ async function clickConnect() {
|
|
|
491
502
|
// Store disconnect handler so we can remove it later
|
|
492
503
|
const handleDisconnect = () => {
|
|
493
504
|
toggleUIConnected(false);
|
|
494
|
-
espStub =
|
|
505
|
+
espStub = false;
|
|
495
506
|
};
|
|
496
507
|
espStub.handleDisconnect = handleDisconnect; // Store reference on espStub
|
|
497
508
|
espStub.addEventListener("disconnect", handleDisconnect);
|
|
@@ -502,9 +513,9 @@ async function clickConnect() {
|
|
|
502
513
|
* Change handler for the Baud Rate selector.
|
|
503
514
|
*/
|
|
504
515
|
async function changeBaudRate() {
|
|
505
|
-
saveSetting("baudrate",
|
|
516
|
+
saveSetting("baudrate", baudRateSelect.value);
|
|
506
517
|
if (espStub) {
|
|
507
|
-
let baud = parseInt(
|
|
518
|
+
let baud = parseInt(baudRateSelect.value);
|
|
508
519
|
if (baudRates.includes(baud)) {
|
|
509
520
|
await espStub.setBaudrate(baud);
|
|
510
521
|
}
|
|
@@ -566,6 +577,27 @@ function updateLogVisibility() {
|
|
|
566
577
|
}
|
|
567
578
|
}
|
|
568
579
|
|
|
580
|
+
/**
|
|
581
|
+
* @name updateMainPadding
|
|
582
|
+
* Dynamically adjust main content padding based on header height
|
|
583
|
+
*/
|
|
584
|
+
function updateMainPadding() {
|
|
585
|
+
// Use requestAnimationFrame to ensure DOM has updated
|
|
586
|
+
requestAnimationFrame(() => {
|
|
587
|
+
const header = document.querySelector('.header');
|
|
588
|
+
const main = document.querySelector('.main');
|
|
589
|
+
|
|
590
|
+
// Guard against missing elements
|
|
591
|
+
if (!header || !main) {
|
|
592
|
+
return;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
const headerHeight = header.offsetHeight;
|
|
596
|
+
// Add small buffer (10px) for better spacing
|
|
597
|
+
main.style.paddingTop = (headerHeight + 10) + 'px';
|
|
598
|
+
});
|
|
599
|
+
}
|
|
600
|
+
|
|
569
601
|
/**
|
|
570
602
|
* @name clickErase
|
|
571
603
|
* Click handler for the erase button.
|
|
@@ -574,7 +606,6 @@ async function clickErase() {
|
|
|
574
606
|
if (
|
|
575
607
|
window.confirm("This will erase the entire flash. Click OK to continue.")
|
|
576
608
|
) {
|
|
577
|
-
baudRate.disabled = true;
|
|
578
609
|
butErase.disabled = true;
|
|
579
610
|
butProgram.disabled = true;
|
|
580
611
|
try {
|
|
@@ -586,7 +617,7 @@ async function clickErase() {
|
|
|
586
617
|
errorMsg(e);
|
|
587
618
|
} finally {
|
|
588
619
|
butErase.disabled = false;
|
|
589
|
-
|
|
620
|
+
baudRateSelect.disabled = false;
|
|
590
621
|
butProgram.disabled = getValidFiles().length == 0;
|
|
591
622
|
}
|
|
592
623
|
}
|
|
@@ -613,7 +644,7 @@ async function clickProgram() {
|
|
|
613
644
|
});
|
|
614
645
|
};
|
|
615
646
|
|
|
616
|
-
|
|
647
|
+
baudRateSelect.disabled = true;
|
|
617
648
|
butErase.disabled = true;
|
|
618
649
|
butProgram.disabled = true;
|
|
619
650
|
for (let i = 0; i < firmware.length; i++) {
|
|
@@ -647,7 +678,7 @@ async function clickProgram() {
|
|
|
647
678
|
progress[i].querySelector("div").style.width = "0";
|
|
648
679
|
}
|
|
649
680
|
butErase.disabled = false;
|
|
650
|
-
|
|
681
|
+
baudRateSelect.disabled = false;
|
|
651
682
|
butProgram.disabled = getValidFiles().length == 0;
|
|
652
683
|
logMsg("To run the new firmware, please reset your device.");
|
|
653
684
|
}
|
|
@@ -749,7 +780,6 @@ async function clickReadFlash() {
|
|
|
749
780
|
return;
|
|
750
781
|
}
|
|
751
782
|
|
|
752
|
-
baudRate.disabled = true;
|
|
753
783
|
butErase.disabled = true;
|
|
754
784
|
butProgram.disabled = true;
|
|
755
785
|
butReadFlash.disabled = true;
|
|
@@ -789,7 +819,7 @@ async function clickReadFlash() {
|
|
|
789
819
|
readProgress.classList.add("hidden");
|
|
790
820
|
readProgress.querySelector("div").style.width = "0";
|
|
791
821
|
butErase.disabled = false;
|
|
792
|
-
|
|
822
|
+
baudRateSelect.disabled = false;
|
|
793
823
|
butProgram.disabled = getValidFiles().length == 0;
|
|
794
824
|
butReadFlash.disabled = false;
|
|
795
825
|
readOffset.disabled = false;
|
|
@@ -933,31 +963,37 @@ function displayPartitions(partitions) {
|
|
|
933
963
|
|
|
934
964
|
// Name
|
|
935
965
|
const nameCell = document.createElement("td");
|
|
966
|
+
nameCell.setAttribute("data-label", "Name");
|
|
936
967
|
nameCell.textContent = partition.name;
|
|
937
968
|
row.appendChild(nameCell);
|
|
938
969
|
|
|
939
970
|
// Type
|
|
940
971
|
const typeCell = document.createElement("td");
|
|
972
|
+
typeCell.setAttribute("data-label", "Type");
|
|
941
973
|
typeCell.textContent = partition.typeName;
|
|
942
974
|
row.appendChild(typeCell);
|
|
943
975
|
|
|
944
976
|
// SubType
|
|
945
977
|
const subtypeCell = document.createElement("td");
|
|
978
|
+
subtypeCell.setAttribute("data-label", "SubType");
|
|
946
979
|
subtypeCell.textContent = partition.subtypeName;
|
|
947
980
|
row.appendChild(subtypeCell);
|
|
948
981
|
|
|
949
982
|
// Offset
|
|
950
983
|
const offsetCell = document.createElement("td");
|
|
984
|
+
offsetCell.setAttribute("data-label", "Offset");
|
|
951
985
|
offsetCell.textContent = `0x${partition.offset.toString(16)}`;
|
|
952
986
|
row.appendChild(offsetCell);
|
|
953
987
|
|
|
954
988
|
// Size
|
|
955
989
|
const sizeCell = document.createElement("td");
|
|
990
|
+
sizeCell.setAttribute("data-label", "Size");
|
|
956
991
|
sizeCell.textContent = formatSize(partition.size);
|
|
957
992
|
row.appendChild(sizeCell);
|
|
958
993
|
|
|
959
994
|
// Action
|
|
960
995
|
const actionCell = document.createElement("td");
|
|
996
|
+
actionCell.setAttribute("data-label", "Action");
|
|
961
997
|
const downloadBtn = document.createElement("button");
|
|
962
998
|
downloadBtn.textContent = "Download";
|
|
963
999
|
downloadBtn.className = "partition-download-btn";
|
|
@@ -1088,16 +1124,11 @@ function toggleUIConnected(connected) {
|
|
|
1088
1124
|
|
|
1089
1125
|
if (connected) {
|
|
1090
1126
|
lbl = "Disconnect";
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
header.classList.add("header-hidden");
|
|
1094
|
-
main.classList.add("no-header-padding");
|
|
1095
|
-
}, 2000); // Hide after 2 seconds
|
|
1127
|
+
isConnected = true;
|
|
1128
|
+
|
|
1096
1129
|
} else {
|
|
1130
|
+
isConnected = false;
|
|
1097
1131
|
toggleUIToolbar(false);
|
|
1098
|
-
// Show header when disconnected
|
|
1099
|
-
header.classList.remove("header-hidden");
|
|
1100
|
-
main.classList.remove("no-header-padding");
|
|
1101
1132
|
}
|
|
1102
1133
|
butConnect.textContent = lbl;
|
|
1103
1134
|
}
|
|
@@ -1105,9 +1136,9 @@ function toggleUIConnected(connected) {
|
|
|
1105
1136
|
function loadAllSettings() {
|
|
1106
1137
|
// Load all saved settings or defaults
|
|
1107
1138
|
autoscroll.checked = loadSetting("autoscroll", true);
|
|
1108
|
-
|
|
1139
|
+
baudRateSelect.value = loadSetting("baudrate", 2000000);
|
|
1109
1140
|
darkMode.checked = loadSetting("darkmode", false);
|
|
1110
|
-
debugMode.checked = loadSetting("debugmode",
|
|
1141
|
+
debugMode.checked = loadSetting("debugmode", false);
|
|
1111
1142
|
showLog.checked = loadSetting("showlog", false);
|
|
1112
1143
|
|
|
1113
1144
|
// Apply show log setting
|
|
@@ -1133,7 +1164,3 @@ function ucWords(text) {
|
|
|
1133
1164
|
.toLowerCase()
|
|
1134
1165
|
.replace(/(?<= )[^\s]|^./g, (a) => a.toUpperCase());
|
|
1135
1166
|
}
|
|
1136
|
-
|
|
1137
|
-
function sleep(ms) {
|
|
1138
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1139
|
-
}
|
package/js/webusb-serial.js
CHANGED
|
@@ -931,6 +931,26 @@ class WebUSBSerial {
|
|
|
931
931
|
});
|
|
932
932
|
}
|
|
933
933
|
|
|
934
|
+
/**
|
|
935
|
+
* Recreate streams without closing the port
|
|
936
|
+
* Useful after hardware reset or when switching to console mode
|
|
937
|
+
* This stops the current read loop and creates fresh streams
|
|
938
|
+
*/
|
|
939
|
+
recreateStreams() {
|
|
940
|
+
// Stop the current read loop
|
|
941
|
+
this._readLoopRunning = false;
|
|
942
|
+
|
|
943
|
+
// Wait a bit for the read loop to finish
|
|
944
|
+
// The ReadableStream will close itself when _readLoopRunning becomes false
|
|
945
|
+
return new Promise((resolve) => {
|
|
946
|
+
setTimeout(() => {
|
|
947
|
+
// Create new streams
|
|
948
|
+
this._createStreams();
|
|
949
|
+
resolve();
|
|
950
|
+
}, 100);
|
|
951
|
+
});
|
|
952
|
+
}
|
|
953
|
+
|
|
934
954
|
_cleanup() {
|
|
935
955
|
this._readLoopRunning = false;
|
|
936
956
|
if (this._usbDisconnectHandler) {
|
|
@@ -1018,4 +1038,4 @@ if (typeof globalThis !== 'undefined') {
|
|
|
1018
1038
|
}
|
|
1019
1039
|
|
|
1020
1040
|
// Export as ES modules
|
|
1021
|
-
export { WebUSBSerial, requestSerialPort };
|
|
1041
|
+
export { WebUSBSerial, requestSerialPort };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tasmota-webserial-esptool",
|
|
3
|
-
"version": "9.2.
|
|
3
|
+
"version": "9.2.11",
|
|
4
4
|
"description": "World's first tool for flashing and reading ESP devices on Android mobile devices using WebUSB. Flash & Read ESP devices using WebSerial/WebUSB - up to 10x faster flash read than esptool.py",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"repository": {
|
|
@@ -33,10 +33,10 @@
|
|
|
33
33
|
"eslint-config-prettier": "^10.1.8",
|
|
34
34
|
"eslint-plugin-prettier": "^5.5.4",
|
|
35
35
|
"prettier": "^3.7.3",
|
|
36
|
-
"rollup": "^4.
|
|
36
|
+
"rollup": "^4.57.0",
|
|
37
37
|
"serve": "^14.2.4",
|
|
38
38
|
"typescript": "^5.7.3",
|
|
39
|
-
"typescript-eslint": "^8.
|
|
39
|
+
"typescript-eslint": "^8.54.0"
|
|
40
40
|
},
|
|
41
41
|
"dependencies": {
|
|
42
42
|
"pako": "^2.1.0",
|
package/src/const.ts
CHANGED
|
@@ -155,7 +155,7 @@ export const ESP32S3_UARTDEV_BUF_NO_USB_JTAG_SERIAL = 4; // The above var when U
|
|
|
155
155
|
|
|
156
156
|
export const ESP32C2_SPI_REG_BASE = 0x60002000;
|
|
157
157
|
export const ESP32C2_BASEFUSEADDR = 0x60008800;
|
|
158
|
-
export const ESP32C2_MACFUSEADDR =
|
|
158
|
+
export const ESP32C2_MACFUSEADDR = ESP32C2_BASEFUSEADDR + 0x040;
|
|
159
159
|
export const ESP32C2_SPI_USR_OFFS = 0x18;
|
|
160
160
|
export const ESP32C2_SPI_USR1_OFFS = 0x1c;
|
|
161
161
|
export const ESP32C2_SPI_USR2_OFFS = 0x20;
|