<!-- The markdown twin of https://kleidos.dev/architecture/, converted from that page at build time. The page is the original; this is the same words with the markup taken off. The site was last changed on 2026-10-25. -->

# 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.

On this page

1. [Layers](https://kleidos.dev/#layers)

2. [The graph](https://kleidos.dev/#graph)

3. [Two cores](https://kleidos.dev/#cores)

4. [One source tree](https://kleidos.dev/#onetree)

## 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

Point at a block, or tap it, to light up the blocks it depends on. They are all below it.

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.

### Core 1: what you look at

The main loop, the state machine, the screen and the buttons. It never blocks on a radio or on storage, because every millisecond it spends waiting is a millisecond the device feels slow in your hand.

### Core 0: what you wait for

The Bluetooth host, Wi-Fi and the web server, the filesystem, and the cryptography. Deriving the key takes about a second and a half: it runs here, with a progress indicator drawn on the other core, and at the fastest clock the chip has, whereas every screen runs at the slowest.

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.
