tasmota-esp-web-tools 12.1.2 → 12.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
@@ -108,6 +108,36 @@ The `chipVariant` field is optional. If omitted, the build will match any varian
108
108
 
109
109
  See [manifest-example-p4-variants.json](manifest-example-p4-variants.json) for a complete example.
110
110
 
111
+ ## Flash Size Support
112
+
113
+ For chips with different flash sizes, you can specify `flashSizeMB` to target specific hardware configurations. This is useful for ESP32-S3 modules that come with various flash sizes.
114
+
115
+ ```json
116
+ {
117
+ "name": "My Firmware",
118
+ "builds": [
119
+ {
120
+ "chipFamily": "ESP32-S3",
121
+ "flashSizeMB": 16,
122
+ "parts": [{ "path": "s3-16mb.bin", "offset": 0 }]
123
+ },
124
+ {
125
+ "chipFamily": "ESP32-S3",
126
+ "flashSizeMB": 4,
127
+ "parts": [{ "path": "s3-4mb.bin", "offset": 0 }]
128
+ },
129
+ {
130
+ "chipFamily": "ESP32-S3",
131
+ "parts": [{ "path": "s3-generic.bin", "offset": 0 }]
132
+ }
133
+ ]
134
+ }
135
+ ```
136
+
137
+ A device with ESP32-S3 and 16MB flash gets the first build, 4MB gets the second, and any other ESP32-S3 falls back to the third.
138
+
139
+ The `flashSizeMB` field is **optional**. If omitted, the build will match any flash size. Builds with matching `flashSizeMB` are preferred over builds without it (most-specific-matching algorithm).
140
+
111
141
  ## Performance
112
142
 
113
143
  ESP Web Tools supports configurable baud rates for flashing. By default, it uses 115200 baud for maximum compatibility. You can increase the baud rate for significantly faster flashing speeds.
@@ -20,11 +20,13 @@ async function loadLittleFS() {
20
20
  try {
21
21
  // Try to import from the calculated path
22
22
  const indexUrl = _wasmBasePath + "index.js";
23
+ // eslint-disable-next-line no-console
23
24
  console.log("[LittleFS] Loading module from:", indexUrl);
24
25
  _littleFSModule = await import(/* @vite-ignore */ indexUrl);
25
26
  return _littleFSModule;
26
27
  }
27
28
  catch (err) {
29
+ // eslint-disable-next-line no-console
28
30
  console.error("[LittleFS] Failed to load from calculated path:", _wasmBasePath, err);
29
31
  // Fallback to relative import (for local development)
30
32
  try {
@@ -32,6 +34,7 @@ async function loadLittleFS() {
32
34
  return _littleFSModule;
33
35
  }
34
36
  catch (fallbackErr) {
37
+ // eslint-disable-next-line no-console
35
38
  console.error("[LittleFS] Fallback import also failed:", fallbackErr);
36
39
  throw new Error(`Failed to load LittleFS module: ${err}`);
37
40
  }
package/dist/connect.js CHANGED
@@ -50,6 +50,7 @@ export const connect = async (button) => {
50
50
  esploader = await esptoolConnect({
51
51
  log: () => { }, // Silent logger for connection
52
52
  debug: () => { },
53
+ // eslint-disable-next-line no-console
53
54
  error: (msg) => console.error(msg),
54
55
  });
55
56
  }
package/dist/const.d.ts CHANGED
@@ -6,6 +6,7 @@ export interface Logger {
6
6
  export interface Build {
7
7
  chipFamily: "ESP32" | "ESP8266" | "ESP32-S2" | "ESP32-S3" | "ESP32-C2" | "ESP32-C3" | "ESP32-C5" | "ESP32-C6" | "ESP32-C61" | "ESP32-H2" | "ESP32-P4";
8
8
  chipVariant?: string;
9
+ flashSizeMB?: number;
9
10
  parts: {
10
11
  path: string;
11
12
  offset: number;
@@ -29,6 +30,7 @@ export interface BaseFlashState {
29
30
  build?: Build;
30
31
  chipFamily?: Build["chipFamily"] | "Unknown Chip";
31
32
  chipVariant?: string | null;
33
+ flashSize?: string;
32
34
  }
33
35
  export interface InitializingState extends BaseFlashState {
34
36
  state: FlashStateType.INITIALIZING;
package/dist/flash.js CHANGED
@@ -1,18 +1,74 @@
1
1
  import { getChipFamilyName } from "./util/chip-family-name";
2
2
  import { sleep } from "./util/sleep";
3
3
  import { corsProxyFetch } from "./util/cors-proxy";
4
+ /**
5
+ * Parse flash size string (e.g., "4MB", "8MB", "16MB") to megabytes number
6
+ */
7
+ function parseFlashSizeToMB(flashSize) {
8
+ if (!flashSize)
9
+ return undefined;
10
+ const match = flashSize.match(/^(\d+)(MB|GB)$/);
11
+ if (!match)
12
+ return undefined;
13
+ const size = parseInt(match[1], 10);
14
+ const unit = match[2];
15
+ if (unit === "GB")
16
+ return size * 1024;
17
+ return size;
18
+ }
19
+ /**
20
+ * Select the best build using most-specific-matching algorithm
21
+ * - Builds with matching flashSizeMB are preferred
22
+ * - Among builds with same specificity, first one wins
23
+ * - Builds without flashSizeMB are fallback options
24
+ */
25
+ function selectBestBuild(builds, detectedFlashSizeMB) {
26
+ if (builds.length === 0)
27
+ return undefined;
28
+ // Score builds: higher score = more specific match
29
+ let bestBuild;
30
+ let bestScore = -Infinity;
31
+ for (const build of builds) {
32
+ let score = 0;
33
+ // Flash size match gives highest priority
34
+ if (build.flashSizeMB !== undefined && detectedFlashSizeMB !== undefined) {
35
+ if (build.flashSizeMB === detectedFlashSizeMB) {
36
+ score += 100; // Exact flash size match
37
+ }
38
+ else {
39
+ score -= 1; // Penalize non-matching specific builds
40
+ }
41
+ }
42
+ else if (build.flashSizeMB !== undefined) {
43
+ // flashSizeMB is defined but detectedFlashSizeMB is undefined
44
+ score -= 1; // Penalize non-matching specific builds
45
+ }
46
+ // Generic builds (flashSizeMB undefined) stay at score 0
47
+ // Prefer this build if it has higher score
48
+ // If same score, keep the first one (stable selection)
49
+ if (score > bestScore) {
50
+ bestScore = score;
51
+ bestBuild = build;
52
+ }
53
+ }
54
+ return bestScore >= 0 ? bestBuild : undefined;
55
+ }
4
56
  export const flash = async (onEvent, esploader, // ESPLoader instance from tasmota-webserial-esptool
5
57
  logger, manifestPath, eraseFirst, firmwareBuffer, _baudRate) => {
6
58
  let manifest;
7
59
  let build;
60
+ // eslint-disable-next-line prefer-const
8
61
  let chipFamily;
9
62
  let chipVariant = null;
63
+ // eslint-disable-next-line prefer-const
64
+ let flashSize;
10
65
  const fireStateEvent = (stateUpdate) => onEvent({
11
66
  ...stateUpdate,
12
67
  manifest,
13
68
  build,
14
69
  chipFamily,
15
70
  chipVariant,
71
+ flashSize,
16
72
  });
17
73
  let manifestProm = null;
18
74
  let manifestURL = "";
@@ -51,9 +107,20 @@ logger, manifestPath, eraseFirst, firmwareBuffer, _baudRate) => {
51
107
  }
52
108
  chipFamily = getChipFamilyName(esploader);
53
109
  chipVariant = esploader.chipVariant;
110
+ // Detect flash size if not already detected
111
+ if (!esploader.flashSize && esploader.detectFlashSize) {
112
+ try {
113
+ await esploader.detectFlashSize();
114
+ }
115
+ catch (err) {
116
+ logger.debug("Failed to detect flash size:", err);
117
+ }
118
+ }
119
+ flashSize = esploader.flashSize; // e.g., "4MB", "8MB"
120
+ const flashSizeMB = flashSize ? parseFlashSizeToMB(flashSize) : undefined;
54
121
  fireStateEvent({
55
122
  state: "initializing" /* FlashStateType.INITIALIZING */,
56
- message: `Initialized. Found ${chipFamily}${chipVariant ? ` (${chipVariant})` : ""}`,
123
+ message: `Initialized. Found ${chipFamily}${chipVariant ? ` (${chipVariant})` : ""}${flashSize ? `, ${flashSize}` : ""}`,
57
124
  details: { done: true },
58
125
  });
59
126
  fireStateEvent({
@@ -73,17 +140,24 @@ logger, manifestPath, eraseFirst, firmwareBuffer, _baudRate) => {
73
140
  await esploader.disconnect();
74
141
  return;
75
142
  }
76
- build = manifest.builds.find((b) => {
77
- // Match chipFamily and optionally chipVariant
143
+ // Filter builds by chipFamily and chipVariant
144
+ const compatibleBuilds = manifest.builds.filter((b) => {
78
145
  if (b.chipFamily !== chipFamily) {
79
146
  return false;
80
147
  }
81
- // If build specifies chipVariant, it must match
82
148
  if (b.chipVariant && b.chipVariant !== chipVariant) {
83
149
  return false;
84
150
  }
85
151
  return true;
86
152
  });
153
+ // Select the best build using most-specific-matching algorithm
154
+ // Prefer builds with more matching qualifiers (flashSizeMB)
155
+ const exactVariantBuilds = compatibleBuilds.filter((b) => b.chipVariant !== undefined && b.chipVariant === chipVariant);
156
+ const variantAgnosticBuilds = compatibleBuilds.filter((b) => b.chipVariant === undefined);
157
+ build = selectBestBuild(exactVariantBuilds, flashSizeMB);
158
+ if (!build) {
159
+ build = selectBestBuild(variantAgnosticBuilds, flashSizeMB);
160
+ }
87
161
  if (!build) {
88
162
  fireStateEvent({
89
163
  state: "error" /* FlashStateType.ERROR */,
@@ -45,12 +45,14 @@ export declare class EwtInstallDialog extends LitElement {
45
45
  private _consoleInitialized;
46
46
  private _improvSupported;
47
47
  private _isUsbJtagOrOtgDevice;
48
+ private _flashSize?;
48
49
  private _openConsoleAfterReconnect;
49
50
  private _visitDeviceAfterReconnect;
50
51
  private _addToHAAfterReconnect;
51
52
  private _changeWiFiAfterReconnect;
52
53
  private _ensureStub;
53
54
  private get _port();
55
+ private _probeFlashSize;
54
56
  private _isUsbJtagOrOtg;
55
57
  private _isWebUsbWithExternalSerial;
56
58
  private _releaseReaderWriter;
@@ -57,6 +57,8 @@ export class EwtInstallDialog extends LitElement {
57
57
  this._handleDisconnect = () => {
58
58
  this._state = "ERROR";
59
59
  this._error = "Disconnected";
60
+ // Reset flash size when device is actually disconnected
61
+ this._flashSize = undefined;
60
62
  };
61
63
  }
62
64
  // Ensure stub is initialized (called before any operation that needs it)
@@ -116,6 +118,8 @@ export class EwtInstallDialog extends LitElement {
116
118
  const espStub = await this.esploader.runStub();
117
119
  this.logger.log(`Stub created: IS_STUB=${espStub.IS_STUB}, chipFamily=${getChipFamilyName(espStub)}`);
118
120
  this._espStub = espStub;
121
+ // Detect flash size AFTER stub creation (flash size is only available after stub)
122
+ await this._probeFlashSize(espStub);
119
123
  // Set baudrate BEFORE any operations (use user-selected baudrate if available)
120
124
  if (this.baudRate && this.baudRate > 115200) {
121
125
  this.logger.log(`Setting baudrate to ${this.baudRate}...`);
@@ -138,6 +142,19 @@ export class EwtInstallDialog extends LitElement {
138
142
  get _port() {
139
143
  return this.esploader.port;
140
144
  }
145
+ // Helper to probe flash size from a running stub (only available after stub)
146
+ async _probeFlashSize(espStub) {
147
+ if (espStub.detectFlashSize && !this._flashSize) {
148
+ try {
149
+ await espStub.detectFlashSize();
150
+ this._flashSize = espStub.flashSize;
151
+ this.logger.log(`Flash size detected: ${this._flashSize}`);
152
+ }
153
+ catch (err) {
154
+ this.logger.debug("Failed to detect flash size:", err);
155
+ }
156
+ }
157
+ }
141
158
  // Helper to check if device is using USB-JTAG or USB-OTG (not external serial chip)
142
159
  async _isUsbJtagOrOtg() {
143
160
  // Use detectUsbConnectionType from tasmota-webserial-esptool
@@ -504,16 +521,17 @@ export class EwtInstallDialog extends LitElement {
504
521
  }
505
522
  _renderDashboard() {
506
523
  const heading = this._info.name;
507
- let content;
508
524
  const hideActions = true;
509
525
  const allowClosing = true;
510
- content = html `
526
+ const content = html `
511
527
  <ew-list>
512
528
  <ew-list-item>
513
529
  <div slot="headline">Connected to ${this._info.name}</div>
514
530
  <div slot="supporting-text">
515
531
  ${this._info.firmware}&nbsp;${this._info.version}
516
- (${this._info.chipFamily})
532
+ (${this._info.chipFamily}${this._flashSize
533
+ ? `, ${this._flashSize}`
534
+ : ""})
517
535
  </div>
518
536
  </ew-list-item>
519
537
  ${!this._isSameVersion
@@ -827,11 +845,25 @@ export class EwtInstallDialog extends LitElement {
827
845
  }
828
846
  _renderDashboardNoImprov() {
829
847
  const heading = "Device Dashboard";
830
- let content;
831
848
  const hideActions = true;
832
849
  const allowClosing = true;
833
- content = html `
850
+ // Build device info string if available
851
+ const chipFamily = this.esploader.chipFamily
852
+ ? getChipFamilyName(this.esploader)
853
+ : null;
854
+ const deviceInfo = chipFamily
855
+ ? `(${chipFamily}${this._flashSize ? `, ${this._flashSize}` : ""})`
856
+ : null;
857
+ const content = html `
834
858
  <ew-list>
859
+ ${deviceInfo
860
+ ? html `
861
+ <ew-list-item>
862
+ <div slot="headline">${chipFamily}</div>
863
+ <div slot="supporting-text">${deviceInfo}</div>
864
+ </ew-list-item>
865
+ `
866
+ : ""}
835
867
  <ew-list-item
836
868
  type="button"
837
869
  ?disabled=${this._busy}
@@ -1219,6 +1251,7 @@ export class EwtInstallDialog extends LitElement {
1219
1251
  return [heading, content];
1220
1252
  }
1221
1253
  _renderInstall() {
1254
+ var _a, _b, _c;
1222
1255
  let heading;
1223
1256
  let content;
1224
1257
  let hideActions = false;
@@ -1247,11 +1280,20 @@ export class EwtInstallDialog extends LitElement {
1247
1280
  else if (!this._installConfirmed) {
1248
1281
  heading = "Confirm Installation";
1249
1282
  const action = isUpdate ? "update to" : "install";
1283
+ // Build device info with flash size if available
1284
+ const deviceInfo = this._flashSize
1285
+ ? html ` (${((_a = this._info) === null || _a === void 0 ? void 0 : _a.chipFamily) || ""}${((_b = this._info) === null || _b === void 0 ? void 0 : _b.chipFamily)
1286
+ ? `, ${this._flashSize}`
1287
+ : this._flashSize})`
1288
+ : "";
1250
1289
  content = html `
1251
1290
  ${isUpdate
1252
1291
  ? html `Your device is running
1253
- ${this._info.firmware}&nbsp;${this._info.version}.<br /><br />`
1254
- : ""}
1292
+ ${this._info.firmware}&nbsp;${this._info
1293
+ .version}${deviceInfo}.<br /><br />`
1294
+ : deviceInfo
1295
+ ? html `Device detected: ${deviceInfo}<br /><br />`
1296
+ : ""}
1255
1297
  Do you want to ${action}
1256
1298
  ${this._manifest.name}&nbsp;${this._manifest.version}?
1257
1299
  ${this._installErase
@@ -1274,7 +1316,11 @@ export class EwtInstallDialog extends LitElement {
1274
1316
  this._installState.state === "manifest" /* FlashStateType.MANIFEST */ ||
1275
1317
  this._installState.state === "preparing" /* FlashStateType.PREPARING */) {
1276
1318
  heading = "Installing";
1277
- content = this._renderProgress("Preparing installation");
1319
+ // Show flash size in preparing message if available
1320
+ const preparingMsg = ((_c = this._installState) === null || _c === void 0 ? void 0 : _c.flashSize)
1321
+ ? `Preparing installation (${this._installState.flashSize})`
1322
+ : "Preparing installation";
1323
+ content = this._renderProgress(preparingMsg);
1278
1324
  hideActions = true;
1279
1325
  }
1280
1326
  else if (this._installState.state === "erasing" /* FlashStateType.ERASING */) {
@@ -1355,9 +1401,8 @@ export class EwtInstallDialog extends LitElement {
1355
1401
  }
1356
1402
  _renderLogs() {
1357
1403
  const heading = `Logs`;
1358
- let content;
1359
1404
  const hideActions = false;
1360
- content = html `
1405
+ const content = html `
1361
1406
  <ew-console
1362
1407
  .port=${this._port}
1363
1408
  .logger=${this.logger}
@@ -1422,7 +1467,22 @@ export class EwtInstallDialog extends LitElement {
1422
1467
  `;
1423
1468
  }
1424
1469
  else {
1470
+ // Build device info with flash size for partition view
1471
+ const chipFamily = this.esploader.chipFamily
1472
+ ? getChipFamilyName(this.esploader)
1473
+ : null;
1474
+ const deviceInfo = chipFamily
1475
+ ? `${chipFamily}${this._flashSize ? `, ${this._flashSize}` : ""}`
1476
+ : null;
1425
1477
  content = html `
1478
+ ${deviceInfo
1479
+ ? html `<div
1480
+ class="device-info"
1481
+ style="margin-bottom: 16px; font-size: 14px; color: var(--md-sys-color-on-surface-variant, #666);"
1482
+ >
1483
+ Device: ${deviceInfo}
1484
+ </div>`
1485
+ : ""}
1426
1486
  <div class="partition-list">
1427
1487
  <table class="partition-table">
1428
1488
  <thead>
@@ -1786,6 +1846,8 @@ export class EwtInstallDialog extends LitElement {
1786
1846
  this._espStub = await this.esploader.runStub();
1787
1847
  this.logger.log(`Stub created: IS_STUB=${this._espStub.IS_STUB}`);
1788
1848
  }
1849
+ // Detect flash size after stub is running
1850
+ await this._probeFlashSize(this._espStub);
1789
1851
  // CRITICAL: Save parent loader
1790
1852
  const loaderToSave = this._espStub._parent || this._espStub;
1791
1853
  this._savedLoaderBeforeConsole = loaderToSave;
@@ -2500,6 +2562,8 @@ export class EwtInstallDialog extends LitElement {
2500
2562
  await this._closeClientWithoutEvents(this._client);
2501
2563
  }
2502
2564
  document.body.style.overflow = (_a = this._bodyOverflow) !== null && _a !== void 0 ? _a : "";
2565
+ // Reset flash size when dialog is closed
2566
+ this._flashSize = undefined;
2503
2567
  fireEvent(this, "closed");
2504
2568
  this.parentNode.removeChild(this);
2505
2569
  }
@@ -2689,4 +2753,7 @@ __decorate([
2689
2753
  __decorate([
2690
2754
  state()
2691
2755
  ], EwtInstallDialog.prototype, "_isUsbJtagOrOtgDevice", void 0);
2756
+ __decorate([
2757
+ state()
2758
+ ], EwtInstallDialog.prototype, "_flashSize", void 0);
2692
2759
  customElements.define("ewt-install-dialog", EwtInstallDialog);
@@ -11,7 +11,9 @@ export class LineBreakTransformer {
11
11
  let lastIndex = 0;
12
12
  let match;
13
13
  while ((match = re.exec(this.chunks)) !== null) {
14
- if (match[0] === "\r" && re.lastIndex === this.chunks.length) {
14
+ // If this is a lone \r at the very end of the buffer, leave it so it can
15
+ // be combined with a possible following \n in the next chunk.
16
+ if (match[0] === "\r" && match.index === this.chunks.length - 1) {
15
17
  break;
16
18
  }
17
19
  const line = this.chunks.substring(lastIndex, match.index);
@@ -7,6 +7,7 @@ export const downloadManifest = async (manifestPath) => {
7
7
  }
8
8
  const manifest = await resp.json();
9
9
  if ("new_install_skip_erase" in manifest) {
10
+ // eslint-disable-next-line no-console
10
11
  console.warn('Manifest option "new_install_skip_erase" is deprecated. Use "new_install_prompt_erase" instead.');
11
12
  if (manifest.new_install_skip_erase) {
12
13
  manifest.new_install_prompt_erase = true;
@@ -7,6 +7,9 @@
7
7
  // [HH:MM:SS.mmm] wall-clock bracket with millis
8
8
  // HH:MM:SS.mmm plain wall-clock
9
9
  const DEVICE_TIMESTAMP_RE = /^\s*(?:\[\d{2}:\d{2}:\d{2}(?:\.\d+)?\]|(?:\d{2}:){2}\d{2}\.\d)/;
10
+ // Matches leading ANSI SGR (color/style) codes at the start of a string
11
+ // biome-ignore lint/suspicious/noControlCharactersInRegex: ANSI escape sequences
12
+ const LEADING_ANSI_RE = /^(\x1b\[(?:\d+;)*\d*m)+/;
10
13
  export class TimestampTransformer {
11
14
  constructor() {
12
15
  this.deviceHasTimestamps = false;
@@ -25,11 +28,30 @@ export class TimestampTransformer {
25
28
  controller.enqueue(chunk);
26
29
  return;
27
30
  }
31
+ // Extract leading ANSI codes to preserve them across line splits
32
+ const ansiMatch = chunk.match(LEADING_ANSI_RE);
33
+ const leadingAnsi = ansiMatch ? ansiMatch[0] : "";
34
+ const contentWithoutAnsi = leadingAnsi
35
+ ? chunk.slice(leadingAnsi.length)
36
+ : chunk;
28
37
  const date = new Date();
29
38
  const h = date.getHours().toString().padStart(2, "0");
30
39
  const m = date.getMinutes().toString().padStart(2, "0");
31
40
  const s = date.getSeconds().toString().padStart(2, "0");
32
- controller.enqueue(`[${h}:${m}:${s}] ${chunk}`);
41
+ const timestamp = `[${h}:${m}:${s}]`;
42
+ // For multi-line chunks, we need to preserve ANSI codes on each line
43
+ // Split on newlines, but keep the newline characters
44
+ const lines = contentWithoutAnsi.split(/(\r?\n)/);
45
+ let result = "";
46
+ for (const part of lines) {
47
+ if (part === "\n" || part === "\r\n") {
48
+ result += part;
49
+ }
50
+ else if (part !== "") {
51
+ result += leadingAnsi + timestamp + " " + part;
52
+ }
53
+ }
54
+ controller.enqueue(result);
33
55
  }
34
56
  reset() {
35
57
  this.deviceHasTimestamps = false;
@@ -1,4 +1,4 @@
1
- import{y as e,z as t,k as o,_ as l,t as r,b as i,h as a}from"./styles-D69gtq6_.js";const n=e`
1
+ import{y as e,z as t,k as o,_ as l,t as r,b as i,h as a}from"./styles-D4zXLz1s.js";const n=e`
2
2
  <svg
3
3
  version="1.1"
4
4
  id="Capa_1"