diff --git a/index.html b/index.html index e4966360..f7ac9262 100644 --- a/index.html +++ b/index.html @@ -644,6 +644,192 @@

Replace the button and message with a custom one

+

Events

+

+ ESP Web Tools dispatches several events during the flashing process that + allow you to monitor progress, handle errors, and inject custom logic. + All events are dispatched on the + <esp-web-install-button> element with + bubbles: true and composed: true, so they can + be captured on parent elements. +

+ +

Available Events

+

The following events are dispatched during the flashing process:

+ + + + + + + + + + + + + + + + + + + + + + + + + + +
initializing + Fired when the device initialization begins and completes. Supports + custom code injection via runCode(). +
preparing + Fired when firmware files are being downloaded and prepared for + installation. +
erasing + Fired when the device flash memory is being erased (only if erase + was requested). +
writing + Fired repeatedly during firmware writing with progress updates. +
finishedFired when the flashing process completes successfully.
error + Fired when an error occurs during any phase of the flashing process. +
+ +

Event Detail Structure

+

+ All events contain a detail object with the following + common properties: +

+ + +

Event-Specific Details

+

initializing

+ + +

preparing

+ + +

erasing

+ + +

writing

+ + +

error

+ + +

Event Handling Examples

+ +

Basic Event Monitoring

+
+const button = document.querySelector('esp-web-install-button');
+
+// Monitor all events
+['initializing', 'preparing', 'erasing', 'writing', 'finished', 'error'].forEach(eventName => {
+  button.addEventListener(eventName, (ev) => {
+    console.log(`Event: ${eventName}`, ev.detail);
+  });
+});
+
+// Track progress
+button.addEventListener('writing', (ev) => {
+  const { percentage, bytesWritten, bytesTotal } = ev.detail.details;
+  console.log(`Progress: ${percentage}% (${bytesWritten}/${bytesTotal} bytes)`);
+});
+ +

Using the initializing Event to Inject Custom Logic

+

+ The initializing event is special because it allows you to + inject custom code using the runCode() function. This is + useful for devices that require special sequences to enter bootloader + mode, like the Home Assistant Connect ZWA-2 with ESP bridge firmware. +

+
+const button = document.querySelector("esp-web-install-button");
+
+button.addEventListener("initializing", (ev) => {
+  const { state, port, details, runCode } = ev.detail;
+
+  // Only inject code at the start of initialization, not when it's done
+  if (details.done) {
+    console.log("Initialization complete:", state, details);
+    return;
+  }
+
+  // Check if this is a device that needs special handling
+  const portInfo = port.getInfo();
+  if (portInfo.usbVendorId !== 12346 || portInfo.usbProductId !== 16385) {
+    // Not our special device, skip custom logic
+    console.log("Standard device detected, using normal initialization");
+    return;
+  }
+
+  console.log("Special device detected! Injecting custom bootloader entry sequence...");
+
+  // Inject custom async code to run before initialization continues
+  runCode(
+    (async () => {
+      // Open the serial port at a specific baud rate
+      await port.open({ baudRate: 115200 });
+
+      // Send custom commands or sequences
+      await enterCustomBootloaderMode(port);
+
+      // Close the port so ESPLoader can take over
+      await port.close();
+    })()
+  );
+});
+
+

Why we created ESP Web Tools

{ el.port = port; el.manifestPath = button.manifest || button.getAttribute("manifest")!; el.overrides = button.overrides; + el.button = button; el.addEventListener( "closed", () => { diff --git a/src/const.ts b/src/const.ts index 07498025..304b7c7b 100644 --- a/src/const.ts +++ b/src/const.ts @@ -14,10 +14,7 @@ export interface Build { | "ESP32-S2" | "ESP32-S3" | "ESP8266"; - parts: { - path: string; - offset: number; - }[]; + parts: { path: string; offset: number }[]; } export interface Manifest { @@ -39,6 +36,8 @@ export interface BaseFlashState { manifest?: Manifest; build?: Build; chipFamily?: Build["chipFamily"] | "Unknown Chip"; + port: SerialPort; + runCode: (promise: Promise) => void; } export interface InitializingState extends BaseFlashState { diff --git a/src/flash.ts b/src/flash.ts index 0784d6f4..ef5728b0 100644 --- a/src/flash.ts +++ b/src/flash.ts @@ -18,13 +18,29 @@ export const flash = async ( let build: Build | undefined; let chipFamily: Build["chipFamily"]; - const fireStateEvent = (stateUpdate: FlashState) => - onEvent({ + const promises: Promise[] = []; + const fireStateEvent = async (stateUpdate: { + state: FlashStateType; + message: string; + details?: any; + }) => { + const eventData: FlashState = { ...stateUpdate, manifest, build, chipFamily, - }); + port, + runCode: (promise: Promise) => { + promises.push(promise); + }, + } as FlashState; + onEvent(eventData); + // Wait for all promises added by event listeners + if (promises.length > 0) { + await Promise.all(promises); + promises.length = 0; // Clear the array + } + }; const transport = new Transport(port); const esploader = new ESPLoader({ @@ -37,7 +53,7 @@ export const flash = async ( // For debugging (window as any).esploader = esploader; - fireStateEvent({ + await fireStateEvent({ state: FlashStateType.INITIALIZING, message: "Initializing...", details: { done: false }, @@ -48,7 +64,7 @@ export const flash = async ( await esploader.flashId(); } catch (err: any) { console.error(err); - fireStateEvent({ + await fireStateEvent({ state: FlashStateType.ERROR, message: "Failed to initialize. Try resetting your device or holding the BOOT button while clicking INSTALL.", @@ -62,7 +78,7 @@ export const flash = async ( chipFamily = esploader.chip.CHIP_NAME as any; - fireStateEvent({ + await fireStateEvent({ state: FlashStateType.INITIALIZING, message: `Initialized. Found ${chipFamily}`, details: { done: true }, @@ -71,7 +87,7 @@ export const flash = async ( build = manifest.builds.find((b) => b.chipFamily === chipFamily); if (!build) { - fireStateEvent({ + await fireStateEvent({ state: FlashStateType.ERROR, message: `Your ${chipFamily} board is not supported.`, details: { error: FlashError.NOT_SUPPORTED, details: chipFamily }, @@ -81,7 +97,7 @@ export const flash = async ( return; } - fireStateEvent({ + await fireStateEvent({ state: FlashStateType.PREPARING, message: "Preparing installation...", details: { done: false }, @@ -115,7 +131,7 @@ export const flash = async ( fileArray.push({ data, address: build.parts[part].offset }); totalSize += data.length; } catch (err: any) { - fireStateEvent({ + await fireStateEvent({ state: FlashStateType.ERROR, message: err.message, details: { @@ -129,34 +145,30 @@ export const flash = async ( } } - fireStateEvent({ + await fireStateEvent({ state: FlashStateType.PREPARING, message: "Installation prepared", details: { done: true }, }); if (eraseFirst) { - fireStateEvent({ + await fireStateEvent({ state: FlashStateType.ERASING, message: "Erasing device...", details: { done: false }, }); await esploader.eraseFlash(); - fireStateEvent({ + await fireStateEvent({ state: FlashStateType.ERASING, message: "Device erased", details: { done: true }, }); } - fireStateEvent({ + await fireStateEvent({ state: FlashStateType.WRITING, message: `Writing progress: 0%`, - details: { - bytesTotal: totalSize, - bytesWritten: 0, - percentage: 0, - }, + details: { bytesTotal: totalSize, bytesWritten: 0, percentage: 0 }, }); let totalWritten = 0; @@ -170,7 +182,11 @@ export const flash = async ( eraseAll: false, compress: true, // report progress - reportProgress: (fileIndex: number, written: number, total: number) => { + reportProgress: async ( + fileIndex: number, + written: number, + total: number, + ) => { const uncompressedWritten = (written / total) * fileArray[fileIndex].data.length; @@ -184,7 +200,7 @@ export const flash = async ( return; } - fireStateEvent({ + await fireStateEvent({ state: FlashStateType.WRITING, message: `Writing progress: ${newPct}%`, details: { @@ -196,7 +212,7 @@ export const flash = async ( }, }); } catch (err: any) { - fireStateEvent({ + await fireStateEvent({ state: FlashStateType.ERROR, message: err.message, details: { error: FlashError.WRITE_FAILED, details: err }, @@ -206,7 +222,7 @@ export const flash = async ( return; } - fireStateEvent({ + await fireStateEvent({ state: FlashStateType.WRITING, message: "Writing complete", details: { @@ -221,7 +237,7 @@ export const flash = async ( console.log("DISCONNECT"); await transport.disconnect(); - fireStateEvent({ + await fireStateEvent({ state: FlashStateType.FINISHED, message: "All done!", }); diff --git a/src/install-dialog.ts b/src/install-dialog.ts index 95bb9e4a..bc223ce4 100644 --- a/src/install-dialog.ts +++ b/src/install-dialog.ts @@ -26,6 +26,7 @@ import { refreshIcon, } from "./components/svg"; import { Logger, Manifest, FlashStateType, FlashState } from "./const.js"; +import type { InstallButton } from "./install-button"; import { ImprovSerial, Ssid } from "improv-wifi-serial-sdk/dist/serial"; import { ImprovSerialCurrentState, @@ -53,6 +54,8 @@ export class EwtInstallDialog extends LitElement { public manifestPath!: string; + public button!: InstallButton; + public logger: Logger = console; public overrides?: { @@ -916,10 +919,19 @@ export class EwtInstallDialog extends LitElement { // Close port. ESPLoader likes opening it. await this.port.close(); + flash( (state) => { this._installState = state; + this.button.dispatchEvent( + new CustomEvent(state.state, { + detail: state, + bubbles: true, + composed: true, + }), + ); + if (state.state === FlashStateType.FINISHED) { sleep(100) // Flashing closes the port