Firmware for iterate's voice boards, and the browser installer that flashes it. Kit moved here from iterate's monorepo, iterate/iterate, in October 2026, with its history.
Kit Flasher is the browser installer at https://k.iterate.com for the
supported ESP32-S3 voice boards: HA Voice PE, FutureProofHomes Satellite1, M5StickS3,
StackChan, Waveshare AMOLED, Waveshare RLCD 4.2 and ZECTRIX NOTE4. The RLCD has an
experimental KEY-button voice release; see its
board notes. It prepares the selected project,
then flashes a firmware release that CI built from this repository and its private configuration
directly over USB.
Choose your board at k.iterate.com, then click Log in with iterate. Consent
shows that board's name and vendor icon; choose its project and authorize access.
Each setup starts a unique OAuth client before consent, including two boards of
the same model. After sign-in, pick the project, enter Wi-Fi (2.4 GHz), pick how the board says its
connection status (Status voice: Greensleeves by default, another tune, spoken, or off) and click
Flash device. A dialog prepares the project, says which serial port to pick, flashes with
esp-web-tools' flash (src/firmware/flash-device.ts), then says how to start a call on that
board (startCall in src/firmware/catalog.ts). The browser's password manager can keep the
Wi-Fi. Set up another device returns to the public selector and starts fresh
consent; it never silently changes the authorized model.
A board can belong to another iterate platform, a self-hosted one included: open
k.iterate.com/.auth/connect?issuer=<its origin>. Kit checks the origin (https, not one of
iterate's own zones, and its discovery document names it), then the selector's button says
Log in with <its host> and consent happens there. The board is flashed with that
platform's address. Set up another device keeps the platform.
Preparing checks the project's voice. The form asks for an OpenAI API key as soon as the
picked project turns out to have none. It verifies voice health before minting a token with no
expiry, scoped to the chosen project, under the same OAuth client that was authorized. Existing
voice services, secrets and project websites are preserved. Voice is the npm package
@iterate-com/voice, on the agents app iterate/agents, and the project's config repo
installs both itself (configs/voice does): a project whose config installs no voice is
refused. The check is ensureVoiceAgent (@iterate-com/voice/install), shared with
voice.iterate.com, which sets voice up without a device.
Client metadata lives at k.iterate.com/devices/<model>/clients/<uuid>.json. The
flashed token appears as Kit <board> <date> with kind Device in your sessions
list and can be revoked individually. Log out ends only the browser's setup
session and returns to device selection; already flashed tokens keep working.
Local HTTP development uses the issuer's dynamic client registration with the same
branding; hosted previews exercise the actual metadata client and consent path.
Wi-Fi and the token stay in browser memory until they are written to the
connected board's iterate_kit partition. Neither goes to the Kit worker or a
URL. The board validates the versioned CRC-protected image at boot, joins Wi-Fi,
presents the token as a bearer on OS's /api, and is ready for its activation
button or optional wake word.
Every firmware change merged to main becomes a GitHub release of each board it
affects, tagged kit-firmware/<device id>/<version>, for example
kit-firmware/home-assistant-voice-preview-edition/003104-2026-10-02-b2a4558. The
version is main's first-parent commit count (six digits), the UTC commit date and
the short sha, so versions sort as strings; the board reports it in X-Iterate-Fw.
The count includes iterate/iterate's commits before the move (COMMITS_BEFORE_THE_MOVE
in scripts/firmware-release.ts): each board's last release there,
003103-2026-10-02-bbd8934, was copied here, and versions count on from it.
A release holds the build's flash files and manifest.json, a standard
esp-web-tools manifest with one extra
field, configurationPartition, where Kit writes the install's configuration
image. src/firmware/catalog.ts (firmwareReleaseTag, FIRMWARE_VERSION_PATTERN)
and scripts/firmware-release.ts (firmwareManifest) own this contract.
The Kit Firmware workflow (.depot/workflows/kit-firmware.yml) builds a board
only when its inputs changed since its newest release: the firmware tree minus the
other boards' devices/<board> and targets/<board>, the host and Mac code, the
tests and the docs (firmwareInputs). A build fails when the board read a tracked
file outside its inputs, changed a tracked file, or produced a flash layout that
disagrees with its own partition table. Each board builds in its own leg, so main
releases in about 5 minutes. Releases are never marked Latest: each board has its
own. A pull request that touches firmware runs the same builds and lists what it
would publish.
- Recovery. Every run compares each board with its newest release, so the
daily 05:17 UTC run (or the next firmware push) releases what a failed run left
behind. A failure on main posts to #error-pulse (
scripts/ci/page.ts). - Builder changes.
scripts/firmware-release.tsis not a release input. After changing it, dispatch Kit Firmware on main withdevices=allto rebuild every board. - Yanking. Merge the fix first, which releases a newer version, then
gh release delete <tag> --cleanup-tag --yes(an admin: the "Kit firmware tags" ruleset protectskit-firmware/**, which only admins and Depot's app may create or delete). Deleting first makes the next run rebuild the bad commit, since the planner then compares with the older release. - Stray drafts. A publish killed mid-upload can leave a draft release. Drafts have no tag, so nothing lists them; delete it in the releases page.
- Bench builds.
node scripts/firmware-release.ts build --device <id> --out /tmp/kit-<id>(pnpm firmware:build) with ESP-IDF active; see the firmware guide.
Kit flashes only each board's newest kit-firmware/ release of this repository,
which the Kit Firmware workflow creates; no older release can be chosen. The page
finds it from GitHub's public API in the browser (src/firmware/releases.ts), so
the Worker holds no GitHub token. The Worker streams each release file from
/firmware/<device id>/<version>/<file> (src/firmware/firmware-proxy.ts),
because GitHub's download URLs send no CORS headers; the page checks the manifest
(src/firmware/prepare-manifest.ts) and adds the install's configuration image
at flash time. Production, the preview and pnpm dev all flash the same releases
with no setup, and deploying Kit builds no firmware. The repository must stay
public: boards and browsers download its releases without signing in.
For board structure, hardware requirements, host tests (pnpm firmware:test:host)
and air-path proof, see the firmware guide. The installer
does not replace that hardware validation. Host tests need cmake and are not part
of Kit's pnpm test, which runs the installer's Vitest suite; CI runs both on
every PR.
pnpm install
pnpm dev # the installer, signing in against os.iterate.com
pnpm typecheck && pnpm lint && pnpm format:check && pnpm test
pnpm firmware:test:host # needs cmakepnpm dev signs in against production; a gitignored .dev.vars points it at
another platform (APP_CONFIG_URLS__OS=http://localhost:8788, src/app/config.ts).
- Deploys.
envs.tsholds the two deployments:preview(kit.iterate-dev-preview.workers.dev, signed in against iterate's main on dev) andprd(k.iterate.com). Every main push that changes what the Worker ships deployspreview, thenprd(.depot/workflows/deploy.yml);pnpm run deploy --env <name>does the same by hand, with the Cloudflare token from Doppler projectkit. Nothing is created or deleted: the Workers and k.iterate.com's DNS record already exist. - CI is Depot CI (
.depot/workflows/), with one secret,DOPPLER_TOKEN. - The iterate platform. Kit is an OAuth client of an iterate platform
(iterate/core). It installs the
iterateSDK and@iterate-com/voicefrom pkg.pr.new, pinned to a commit of iterate's monorepo, which publishes from iterate/private (pkg.pr.new/iterate/private/<package>@<full sha>), and its components from shadcn and the registry in iterate/packages (pnpm exec shadcn add iterate/packages/<name>). The files undersrc/app/are trimmed copies from iterate's monorepo; each says where it came from.