Architecture

One source tree builds the firmware for nine boards across two chip families. What makes that possible is not cleverness; it is a shape that is checked on every commit: a module may use the modules below it and nothing above, and the build fails when that stops being true.

Like the security page, this one describes a repository you cannot open yet. The check runs on every commit; letting you run it yourself is a step on the roadmap.

The order nothing may climb

Twenty-two modules in one order, the consumer first. A module includes its own headers and those of the modules below it; an include pointing upward fails the check, in the commit hook and again in continuous integration.

Read a row across: its dots are the modules the row's module includes, by number. All 117 includes between modules are right of the diagonal, each one below its includer in the order. The empty triangle to its left is where an include that climbs would go, and the build refuses to draw this figure if there is one.

The remedy for an upward include is never an exception. It is to move the header down, or to invert the call through an interface the lower module declares, which is how the admin panel reaches the running application without the application knowing that a panel exists. There is a mechanism for pinning an exception, site by site. It is empty, and the point of it is that it is empty: no two modules in the firmware depend on each other.

Includes are spelled from the root of the source tree, so a relative path that climbs out of its own directory fails the same check. Inside a module the check goes further and fences groups of files off from each other, and it refuses a cycle between headers. The picture in the next section is generated from those same include lines, and the check fails while the copy of it kept in the repository is out of date.

The graph

The module graph is generated from the includes themselves rather than drawn by hand, which is what stops a diagram and the code it describes from disagreeing: the tool that draws it is the tool that enforces the order.

Assembly and control

The files that name every part of one board and drive it. They sit at the top because they reach down to everything; nothing reaches them.

  1. The board's wiring boards/ One file per device: which panel, which power controller, which buttons this board actually has. uses ui/ diag/ hal/ config/
  2. The bench partner tools/ A second board that plays the computer, so the radio and the admin page can be tested against something real. uses platform/ Not in every build
  3. The state machine main.cpp Boot, then one loop: asleep, locked, open, typing, admin, and every move between them. uses debug/ states/ web/ ui/ reset/ net/ ble/ diag/ vault/ totp/ i18n/ crypto/ hal/ drivers/ time/ platform/ ota/ config/
  4. The developer's console debug/ A serial command line that can drive every screen. It is not compiled into a build you would run, and each build is checked for it. uses states/ web/ ui/ reset/ net/ ble/ diag/ vault/ totp/ i18n/ crypto/ hal/ drivers/ time/ platform/ config/ Not in every build Tested on a PC

Screens and surfaces

Everything a person looks at or types into, on the device or in a browser.

  1. The screens states/ What each state of the device shows and what a press does there: the PIN dial, the vault, the settings tree, the first run. uses web/ ui/ reset/ net/ ble/ diag/ vault/ totp/ i18n/ crypto/ hal/ drivers/ time/ platform/ config/ Tested on a PC
  2. The admin page web/ A page the device serves from its own Wi-Fi, for bulk edits and firmware updates. Off until you turn it on, and never on while the radio types. uses ui/ reset/ net/ ble/ diag/ vault/ totp/ i18n/ crypto/ hal/ drivers/ time/ platform/ config/ Tested on a PC
  3. The drawing library ui/ Every pixel: lists, dialogs, icons, brand marks. One set of parts that fits nine screens, from a 1-bit e-paper square to a color touchscreen. uses ble/ vault/ brands/ totp/ i18n/ crypto/ hal/ time/ platform/ config/ Tested on a PC

Device services

What the device does once it is open: type, connect, erase, measure itself.

  1. The erase reset/ One place that erases everything (vault, pairings, settings), so every button and every page that wipes does exactly the same thing. uses net/ ble/ vault/ platform/ Tested on a PC
  2. The network errands net/ Joining a Wi-Fi long enough to set the clock, and remembering the name the admin page answers to. uses ble/ vault/ crypto/ time/ platform/ Tested on a PC
  3. The keyboard it becomes ble/ The Bluetooth keyboard your computer sees. It only sends keystrokes: there is no path by which it could read what you type. uses time/ platform/ Tested on a PC
  4. The instruments diag/ What the device measures about itself: battery, free memory, how long a frame took to draw. uses hal/ drivers/ time/ platform/ Tested on a PC

Data and secrets

What the device holds and what locks it. Below every screen and every radio: the vault does not depend on the parts that draw it or send it.

  1. The vault vault/ Your credentials in flash, each one a sealed record of its own, and the only code that opens them. uses brands/ totp/ crypto/ hal/ time/ platform/ config/ Tested on a PC
  2. The logo lookup brands/ Turning a web address into the logo you recognize on the row, from a table built into the firmware. Nothing is fetched. uses platform/ config/ Tested on a PC
  3. The six digits totp/ The two-factor code for an account, and the clock rule that refuses to show one when the time cannot be trusted. uses time/ platform/ config/ Tested on a PC
  4. The words i18n/ Every sentence the device can show, in every language it speaks, in one table. uses platform/ Tested on a PC
  5. The lock itself crypto/ Turning your PIN into a key, and the key into sealed bytes. The arithmetic, and nothing else: it knows nothing about screens or records. uses platform/ Tested on a PC

Hardware and foundation

The chips, and the single door to the chip's own services. Nine different boards meet the rest of the firmware here.

  1. The translator hal/ One vocabulary for nine boards (draw, read a button, measure the battery), whatever the chip underneath turns out to be. uses drivers/ time/ platform/ config/ Tested on a PC
  2. The chip drivers drivers/ One driver per real chip: screen, power controller, clock, motion sensor, keyboard. Written here rather than borrowed. uses time/ platform/ config/ Tested on a PC
  3. The clock and its trust time/ What time the device thinks it is, where that came from, and whether that is good enough to show a two-factor code. uses platform/ config/ Tested on a PC
  4. The foundation platform/ The single door to the chip's own services (tasks, timing, logging, storage, sleep), so nothing above ever calls them directly. uses ota/ Tested on a PC
  5. The update rule ota/ Whether an offered firmware may be installed. Arithmetic on version numbers; it depends on nothing at all. uses no other module Tested on a PC
  6. What this board is config/ The facts about the board, fixed when the firmware is built: screen size, flash size, which parts exist at all. uses no other module
One block per module of the firmware, in dependency order: everything a block needs is below it. No block depends on a block that depends on it: not directly, and not through a chain of any length. The picture is generated from the project's real #include lines, and the build fails while it disagrees with them. One level finer, among the folders inside the blocks, it holds everywhere but the 6 folders inside ble/, which depend on each other by design and are pinned by name.

Two cores, and what is not allowed on each

The chip has two cores, and the division between them is a rule rather than a tendency.

Core 1

Takes the PIN's last digit

Draws the progress, and never waits

Draws the vault

Core 0

Idle

Derives the key: PBKDF2-SHA256, 175,000 iterations, about 1.5 s

Opens the vault's index

Clock

80 MHz

240 MHz

80 MHz

One unlock on an ESP32-S3, as a schematic: the middle column is the key derivation, about a second and a half; the columns either side of it are not drawn to scale.

A queue or a lock that either of two tasks could reach first is built before the task that consumes it exists, or inside a call that happens once. Checking whether it is null and creating it if so is the bug that looks correct: both cores pass that check, both build one, and one of them is waited on by nobody.

One tree, nine boards

A board is a configuration, not a copy of the firmware.

Each board has a directory holding its pin map, its panel, its power controller, its partition layout and its own figures, the sleep estimate the measurements page prints among them. The firmware builds on ESP-IDF 6.0.3 with no Arduino core in it, as C++26, with every compiler warning treated as an error. A guard reads all nine boards and fails on an inconsistency between them: thirty-odd invariants, including that every build carries exactly one key-derivation work factor, and that the serial console cannot be compiled into a production image.

Every board has up to four builds: an open development one, a debug one that never ships, an open production one, and a hardened one that irreversibly burns one-time fuses on its first boot. Three boards have no hardened build, because 4 MB of flash does not hold two update slots and a signed bootloader.