Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
186 changes: 186 additions & 0 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -644,6 +644,192 @@ <h4>Replace the button and message with a custom one</h4>
</pre
>

<h2 id="events">Events</h2>
<p>
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
<code>&lt;esp-web-install-button&gt;</code> element with
<code>bubbles: true</code> and <code>composed: true</code>, so they can
be captured on parent elements.
</p>

<h3 id="event-types">Available Events</h3>
<p>The following events are dispatched during the flashing process:</p>

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

<h3 id="event-detail">Event Detail Structure</h3>
<p>
All events contain a <code>detail</code> object with the following
common properties:
</p>
<ul>
<li>
<code>state</code> - The current state type (matches the event name)
</li>
<li>
<code>message</code> - A human-readable message describing the current
state
</li>
<li>
<code>manifest</code> - The manifest object being used for
installation
</li>
<li><code>build</code> - The selected build for the detected chip</li>
<li>
<code>chipFamily</code> - The detected ESP chip family (e.g., "ESP32",
"ESP8266")
</li>
<li><code>port</code> - The SerialPort object being used</li>
<li>
<code>runCode(promise)</code> - Function to inject custom async code
(see examples below)
</li>
<li><code>details</code> - Event-specific additional details</li>
</ul>

<h4>Event-Specific Details</h4>
<p><b>initializing</b></p>
<ul>
<li>
<code>details.done</code> - Boolean indicating if initialization is
complete
</li>
</ul>

<p><b>preparing</b></p>
<ul>
<li>
<code>details.done</code> - Boolean indicating if preparation is
complete
</li>
</ul>

<p><b>erasing</b></p>
<ul>
<li>
<code>details.done</code> - Boolean indicating if erasing is complete
</li>
</ul>

<p><b>writing</b></p>
<ul>
<li><code>details.bytesTotal</code> - Total bytes to write</li>
<li><code>details.bytesWritten</code> - Bytes written so far</li>
<li><code>details.percentage</code> - Percentage complete (0-100)</li>
</ul>

<p><b>error</b></p>
<ul>
<li><code>details.error</code> - Error type code</li>
<li><code>details.details</code> - Detailed error information</li>
</ul>

<h3 id="event-examples">Event Handling Examples</h3>

<h4>Basic Event Monitoring</h4>
<pre>
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)`);
});</pre
>

<h4>Using the initializing Event to Inject Custom Logic</h4>
<p>
The <code>initializing</code> event is special because it allows you to
inject custom code using the <code>runCode()</code> 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.
</p>
<pre>
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();
})()
);
});
</pre
>

<h2>Why we created ESP Web Tools</h2>
<div class="videoWrapper">
<lite-youtube
Expand Down
1 change: 1 addition & 0 deletions src/connect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export const connect = async (button: InstallButton) => {
el.port = port;
el.manifestPath = button.manifest || button.getAttribute("manifest")!;
el.overrides = button.overrides;
el.button = button;
el.addEventListener(
"closed",
() => {
Expand Down
7 changes: 3 additions & 4 deletions src/const.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -39,6 +36,8 @@ export interface BaseFlashState {
manifest?: Manifest;
build?: Build;
chipFamily?: Build["chipFamily"] | "Unknown Chip";
port: SerialPort;
runCode: (promise: Promise<any>) => void;
}

export interface InitializingState extends BaseFlashState {
Expand Down
62 changes: 39 additions & 23 deletions src/flash.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,29 @@ export const flash = async (
let build: Build | undefined;
let chipFamily: Build["chipFamily"];

const fireStateEvent = (stateUpdate: FlashState) =>
onEvent({
const promises: Promise<any>[] = [];
const fireStateEvent = async (stateUpdate: {
state: FlashStateType;
message: string;
details?: any;
}) => {
const eventData: FlashState = {
...stateUpdate,
manifest,
build,
chipFamily,
});
port,
runCode: (promise: Promise<any>) => {
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({
Expand All @@ -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 },
Expand All @@ -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.",
Expand All @@ -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 },
Expand All @@ -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 },
Expand All @@ -81,7 +97,7 @@ export const flash = async (
return;
}

fireStateEvent({
await fireStateEvent({
state: FlashStateType.PREPARING,
message: "Preparing installation...",
details: { done: false },
Expand Down Expand Up @@ -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: {
Expand All @@ -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;
Expand All @@ -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;

Expand All @@ -184,7 +200,7 @@ export const flash = async (
return;
}

fireStateEvent({
await fireStateEvent({
state: FlashStateType.WRITING,
message: `Writing progress: ${newPct}%`,
details: {
Expand All @@ -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 },
Expand All @@ -206,7 +222,7 @@ export const flash = async (
return;
}

fireStateEvent({
await fireStateEvent({
state: FlashStateType.WRITING,
message: "Writing complete",
details: {
Expand All @@ -221,7 +237,7 @@ export const flash = async (
console.log("DISCONNECT");
await transport.disconnect();

fireStateEvent({
await fireStateEvent({
state: FlashStateType.FINISHED,
message: "All done!",
});
Expand Down
Loading