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.
+ |
+
+
+ finished |
+ Fired 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:
+
+
+ -
+
state - The current state type (matches the event name)
+
+ -
+
message - A human-readable message describing the current
+ state
+
+ -
+
manifest - The manifest object being used for
+ installation
+
+ build - The selected build for the detected chip
+ -
+
chipFamily - The detected ESP chip family (e.g., "ESP32",
+ "ESP8266")
+
+ port - The SerialPort object being used
+ -
+
runCode(promise) - Function to inject custom async code
+ (see examples below)
+
+ details - Event-specific additional details
+
+
+ Event-Specific Details
+ initializing
+
+ -
+
details.done - Boolean indicating if initialization is
+ complete
+
+
+
+ preparing
+
+ -
+
details.done - Boolean indicating if preparation is
+ complete
+
+
+
+ erasing
+
+ -
+
details.done - Boolean indicating if erasing is complete
+
+
+
+ writing
+
+ details.bytesTotal - Total bytes to write
+ details.bytesWritten - Bytes written so far
+ details.percentage - Percentage complete (0-100)
+
+
+ error
+
+ details.error - Error type code
+ details.details - Detailed error information
+
+
+ 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