Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

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:// or http://localhost).
  • ESP32-S3 target and compatible firmware image(s).

Install

npm install espcdcflash
import { 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.
  • inspect is 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:

  1. image.address (per item in Input)
  2. default 0x10000 (app start address assumption for plain file input)

For image arrays:

  • Ranges must not overlap.
  • Collision checks use padded size (size rounded 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 returned Report from flash().

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).
  • inspect validates 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.

About

espcdcflash is a focused/narrower alternative to esptool-js and aims to mimic practical esptool.py flashing behavior for USB CDC usage.

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages