espcdcflash
v0.1.57
Published
Streamlined ESP32-S3 USB CDC WebSerial flasher
Downloads
109
Readme
espcdcflash
espcdcflash is an opiniated WebSerial flasher for ESP32-S3 firmware in the browser.
It is designed for USB CDC flashing flows with a small, stateless API: isSupported(), inspect(input?), and flash(input, options?).
Requirements
- Chromium browser with WebSerial support (Chrome/Edge).
- Secure context (
https://orhttp://localhost). - ESP32-S3 target and compatible firmware image(s).
Install
npm install espcdcflashimport { flash, inspect, isSupported } from "espcdcflash";Quick Start (minimal)
import { flash } from "espcdcflash";
// No port passed: flash() will open the web serial picker.
// Plain file input defaults to memory offset 0x10000.
const report = await flash(file);
if (!report.ok) {
console.error(report.error);
} else {
console.log("Flash successful.");
}Passing a plain firmware file (File/Uint8Array) to flash assumes the app start address 0x10000.
Use an Image object ({ file, address }) to set a different address.
Optional explicit browser/context guard:
import { isSupported } from "espcdcflash";
if (!isSupported()) {
throw new Error("WebSerial is unavailable in this browser/context.");
}Preflight Validation with inspect
Use inspect to validate one or more firmware inputs before flashing.
import { inspect } from "espcdcflash";
const report = await inspect(file);
if (!report.ok) {
console.error(report.error);
}
for (const item of report.items) {
console.log(item.index, item.ok, item.address, item.chipId, item.error);
}Notes:
inspect()with no argument returns a non-OK report.inspectis preflight sanity checking, not a guarantee for all target-specific runtime conditions.
Progressive Usage Examples
A) File input (defaults to memory offset 0x10000)
const report = await flash(file);B) Explicit single image object
const report = await flash({
file,
address: 0x10000,
});C) Multiple images
const report = await flash([
{ file: bootloaderBin, address: 0x0000 },
{ file: partitionsBin, address: 0x8000 },
{ file: appBin, address: 0x10000 },
]);For image arrays, ranges must not overlap. Range checks use 4-byte padded image size (same as flash write behavior).
// Valid adjacency (no overlap): first image length 33 -> padded to 36 bytes.
await flash([
{ file: first33Bytes, address: 0x10000 }, // [0x10000, 0x10024)
{ file: second, address: 0x10024 }, // starts exactly at previous end
]);
// Invalid overlap:
await flash([
{ file: first33Bytes, address: 0x10000 }, // [0x10000, 0x10024)
{ file: second, address: 0x10020 }, // overlaps previous range
]);D) Full options with events
const report = await flash(file, {
port, // optional
baud: 115200,
touch1200: true,
onEvent: (event) => {
if (event.type === "message") {
console.log("[message]", event.message);
return;
}
if (event.type === "progress") {
console.log("[progress]", event.progress.stage, event.progress.percent, event.progress.message);
return;
}
console.log("[report]", event.report);
},
});E) Cancellation with AbortController
const abort = new AbortController();
const run = flash(file, {
signal: abort.signal,
});
// Later:
abort.abort();
const report = await run;
console.log(report);Full API Reference
export type Stage =
| "touch"
| "rebind"
| "connect"
| "write"
| "reset"
| "done"
| "error";
export type Firmware = File | Uint8Array;
export interface Image {
file: Firmware;
address?: number;
}
export type Input = Firmware | Image | Image[];
export interface Progress {
stage: Stage;
percent: number;
message: string;
}
export interface Report {
ok: boolean;
error?: string;
}
export interface InspectItem {
index: number;
ok: boolean;
size: number;
address: number;
chipId?: number;
error?: string;
}
export interface InspectReport {
ok: boolean;
items: InspectItem[];
error?: string;
}
export type Event =
| { type: "message"; message: string }
| { type: "progress"; progress: Progress }
| { type: "report"; report: Report };
export interface Options {
port?: SerialPort;
baud?: number;
touch1200?: boolean;
signal?: AbortSignal;
onEvent?: (event: Event) => void;
}
export function isSupported(): boolean;
export function inspect(input?: Input): Promise<InspectReport>;
export function flash(input: Input, options?: Options): Promise<Report>;Options & Behavior
| Option | Type | Default | Behavior |
|---|---|---|---|
| port | SerialPort | none | If omitted, flash() opens the WebSerial chooser. |
| baud | number | 115200 | Positive integer UART baud used for flashing transport. |
| touch1200 | boolean | true | Runs 1200-baud touch reset flow before connect. |
| signal | AbortSignal | none | Cooperative cancellation for the current run. |
| onEvent | (event: Event) => void | none | Receives message/progress/report events during a run. |
Address precedence:
image.address(per item inInput)- default
0x10000(app start address assumption for plain file input)
For image arrays:
- Ranges must not overlap.
- Collision checks use padded size (
sizerounded up to 4-byte alignment).
Event Flow
Typical successful stage progression:
touch -> rebind -> connect -> write -> reset -> done
On failure, stage error is emitted.
Notes:
- Progress percentages are stage-oriented UX signals, not exact end-to-end byte percentages.
event.type === "report"is terminal and mirrors the returnedReportfromflash().
Error Handling Guide
Common failure categories:
- Unsupported browser/context (no WebSerial or not secure context).
- Permission denied / chooser canceled.
- Serial busy or restarting.
- Sync/connect timeout.
- Invalid firmware image (bad magic/header/chip ID mismatch).
Recommended handling pattern:
const report = await flash(file, { onEvent: console.log });
if (!report.ok) {
// Treat this as the source of truth for run outcome.
console.error(report.error);
}UI Integration: File Picker + Serial Port Selection
Manual port selection flow
<input id="firmware" type="file" accept=".bin,application/octet-stream" />
<button id="select-port">Select Port</button>
<button id="flash" disabled>Flash</button>
<p id="status">Idle</p>
<script type="module">
import { flash, isSupported } from "espcdcflash";
const fileInput = document.getElementById("firmware");
const portBtn = document.getElementById("select-port");
const flashBtn = document.getElementById("flash");
const status = document.getElementById("status");
let file = null;
let port = null;
let busy = false;
function updateUi() {
flashBtn.disabled = busy || !file || !port;
portBtn.disabled = busy || !isSupported();
}
fileInput.addEventListener("change", () => {
file = fileInput.files?.[0] ?? null;
updateUi();
});
portBtn.addEventListener("click", async () => {
try {
if (!isSupported()) throw new Error("WebSerial unavailable.");
port = await navigator.serial.requestPort({});
status.textContent = "Port selected.";
updateUi();
} catch (error) {
status.textContent = error instanceof Error ? error.message : String(error);
}
});
flashBtn.addEventListener("click", async () => {
if (!file || !port || busy) return;
busy = true;
updateUi();
status.textContent = "Flashing...";
try {
const report = await flash(file, {
port,
onEvent: (event) => {
if (event.type === "progress") status.textContent = event.progress.message;
},
});
status.textContent = report.ok ? "Done." : `Failed: ${report.error ?? "Unknown error"}`;
} finally {
busy = false;
updateUi();
}
});
updateUi();
</script>Auto chooser flow (no manual port selection)
const report = await flash(file, {
onEvent: (event) => {
if (event.type === "progress") {
console.log(event.progress.message);
}
},
});
// If no port was provided, flash() prompts for one.
console.log(report);Limitations / Non-goals
- ESP32-S3 only.
- WebSerial only (Chromium secure context required).
- Browser runtime only (not Node.js).
inspectvalidates firmware format/chip targeting, but is not a full target safety proof.
Attribution
espcdcflash is a focused/narrower alternative to generic flashing stacks and aims to mimic practical esptool.py flashing behavior for USB CDC usage.
Vendored stub data: src/stub/esp32s3.stub.json (from esptool-js, Apache-2.0).
See THIRD_PARTY_NOTICES.md and LICENSE.md.
