Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 42 additions & 5 deletions .github/workflows/docs.yml
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
# Checks the adopter docs (Story 10.2): every relative link and referenced repository path
# resolves, and every command block of the build guide (docs/build/*.md) either runs against the
# firmware crates (`<!-- ci:run <dir> -->`, builds only) or, when it needs a board
# (`<!-- ci:hardware -->`), names only real espflash subcommands. Nothing is ever flashed, and no
# external link is fetched. Not a required status check.
# Checks the adopter docs (Stories 10.2 and 10.4): every relative link and referenced repository
# path resolves; every command block of the build guide and the firmware reference
# (docs/build/*.md, docs/firmware/*.md) either runs against the firmware crates
# (`<!-- ci:run <dir> -->`, builds only) or, when it needs a board (`<!-- ci:hardware -->`), names
# only real espflash subcommands; and the API docs of the packages/rs crates build without a
# rustdoc warning, a public item without docs included. Nothing is ever flashed, and no external
# link is fetched. Not a required status check.
#
# Actions are pinned to a commit; the comment names the release. The firmware setup of the
# `commands` job is copied from the `firmware` job of ci.yml: keep the two in step.
Expand All @@ -16,6 +18,9 @@ on:
- "hardware/**"
- "README.md"
- "LICENSE"
- "Cargo.toml"
- "Cargo.lock"
- "rust-toolchain.toml"
- ".gitignore"
- "apps/**"
- "packages/**"
Expand All @@ -31,6 +36,9 @@ on:
- "hardware/**"
- "README.md"
- "LICENSE"
- "Cargo.toml"
- "Cargo.lock"
- "rust-toolchain.toml"
- ".gitignore"
- "apps/**"
- "packages/**"
Expand Down Expand Up @@ -135,3 +143,32 @@ jobs:
run: |
docs/check-commands.sh --list
docs/check-commands.sh

# Story 10.4: the firmware crates are a learning reference, so their API docs build without a
# single warning (a missing doc, a broken or ambiguous intra-doc link, a bare URL). It lives here
# rather than in ci.yml, whose required `rust` job already runs clippy with `missing_docs`,
# because any ci.yml edit makes every ci.yml job run.
rustdoc:
name: Rust docs
runs-on: ubuntu-24.04
timeout-minutes: 20
steps:
- name: Check out
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

# Installs the toolchain named in rust-toolchain.toml.
- name: Set up Rust
run: rustup toolchain install

- name: Cache
uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2

- name: Build the API docs
env:
RUSTDOCFLAGS: -D warnings
run: >-
cargo doc --no-deps --locked --features coldframe-hal/mock
-p coldframe-hal -p coldframe-crypto -p coldframe-protocol -p coldframe-setup
-p coldframe-uplink -p coldframe-sensing -p coldframe-transport
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ and deployment all live in this repository.
- [Build guide](docs/build/README.md): build, flash and add your own Node and Hub.
- [Deploy guide](docs/deploy/README.md): run Coldframe on your home server, then
[build your own apps](docs/build/apps.md).
- [Firmware reference](docs/firmware/README.md): learn ESP-NOW, BLE provisioning, eFuse keys and
deep sleep from the Node and Hub firmware.
- [Hardware design](hardware/README.md): schematics, BOM, wiring, enclosure and probe sealing.
- [Hub radio coexistence spike](docs/spikes/hub-radio-coexistence.md): what the Hub's radio can do at once.

Expand Down

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions apps/rs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,7 @@ cargo build --features dev-mode
./check-image.sh
```

To learn how the firmware works (ESP-NOW, BLE setup, eFuse keys, sealing, deep sleep), read the
[firmware reference](../../docs/firmware/README.md).

On-device behaviour is checked by hand against the checklists in [`docs/bench`](../../docs/bench).
19 changes: 11 additions & 8 deletions docs/check-commands.sh
Original file line number Diff line number Diff line change
@@ -1,18 +1,21 @@
#!/usr/bin/env bash
# Runs the commands the build guide documents (Story 10.2). Firmware builds only, never flashing.
# Runs the commands the build guide (Story 10.2) and the firmware reference (Story 10.4) document.
# Firmware builds only, never flashing.
#
# docs/check-commands.sh [--list] [file...]
#
# The default files are docs/build/*.md, relative to the repository root unless absolute. Every
# fenced bash/sh block must be preceded by a marker (blank lines between them are allowed):
# The default files are docs/build/*.md and docs/firmware/*.md (Story 10.4), relative to the
# repository root unless absolute. Every fenced bash/sh block must be preceded by a marker (blank
# lines between them are allowed):
#
# <!-- ci:run <repo-relative dir> --> the block runs in that directory, after
# `source ~/export-esp.sh` when it exists (the guide's
# install block creates it), with errexit and pipefail;
# <!-- ci:hardware --> the block needs a board: only `espflash <subcommand> --help`
# runs, for every espflash subcommand in it;
# <!-- ci:manual --> the block needs a phone, Xcode, the Android SDK or a cluster
# (Story 10.3): nothing runs; --list shows it as manual.
# <!-- ci:manual --> the block needs a phone, Xcode, the Android SDK, a cluster
# (Story 10.3) or a browser (Story 10.4): nothing runs;
# --list shows it as manual.
#
# An unmarked block, a marker that no bash/sh block follows, an unknown ci: marker or a ci:run
# directory that does not exist fails. --list prints every block with its marker and runs nothing.
Expand All @@ -31,7 +34,7 @@ for arg in "$@"; do
case ${arg} in
--list) list=true ;;
-h | --help)
sed -n '2,21p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'
sed -n '2,24p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'
exit 0
;;
-*)
Expand All @@ -43,7 +46,7 @@ for arg in "$@"; do
esac
done
if [[ ${#files[@]} -eq 0 ]]; then
for file in "${repo}"/docs/build/*.md; do
for file in "${repo}"/docs/build/*.md "${repo}"/docs/firmware/*.md; do
[[ -f ${file} ]] && files+=("${file}")
done
fi
Expand Down Expand Up @@ -199,7 +202,7 @@ for file in "${files[@]}"; do
echo "${where}: ci:manual"
indent "${block}"
else
echo "==> ${where}: ci:manual (not run: needs a phone, Xcode, the Android SDK or a cluster)"
echo "==> ${where}: ci:manual (not run: needs a phone, Xcode, the Android SDK, a cluster or a browser)"
fi
;;
esac
Expand Down
74 changes: 74 additions & 0 deletions docs/firmware/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# Firmware reference

The Coldframe firmware is written in Rust for the ESP32-S3, and it is meant to be read. This
reference explains how the Node and the Hub use ESP-NOW, BLE provisioning, eFuse keys, frame
sealing and deep sleep, and points to the code that does it. To build and flash the firmware, use
the [build guide](../build/README.md) instead; this reference assumes you have the repository open
next to it.

Each page explains one mechanism in prose, lists its key constants and timings, and ends with
**Where to read the code**: repository paths plus the symbol to look for. The architecture
decisions (AD-n) are in the
[architecture spine](../../_bmad-output/planning-artifacts/architecture/architecture-coldframe-2026-09-26/ARCHITECTURE-SPINE.md),
and the Node and Hub requirements (N-n, H-n) in the
[device hardware spec](../../_bmad-output/specs/spec-coldframe/device-hardware.md). Where those
planning documents and the code disagree, the pages describe the code and say so.

## Reading order

1. [Identity](identity.md): the eFuse root key, the HKDF purpose keys and the Device ID.
2. [Sealing](sealing.md): ChaCha20-Poly1305 frames, the frame counter and the replay window.
3. [ESP-NOW](espnow.md): how a Node finds its Hub, sends a burst and follows the Hub's channel.
4. [Deep sleep](deep-sleep.md): the Node's wake cycle and the power budget.
5. [Buttons](buttons.md): the setup button, short press and long press.
6. [BLE setup](ble-setup.md): the custom setup protocol and its proof-of-possession code.
7. [Radio coexistence](radio-coexistence.md): Wi-Fi, ESP-NOW and BLE on one Hub radio.

Identity and sealing come first because every later page sends or stores something sealed with
those keys.

## Crate map

The logic lives in host-testable crates under `packages/rs`, behind the hardware traits of
`coldframe-hal` (AD-24). Only the `src/board` module of a firmware crate names esp-hal types.
[`packages/rs/README.md`](../../packages/rs/README.md) and
[`apps/rs/README.md`](../../apps/rs/README.md) describe each crate in full.

| Crate | Folder | What to learn from it |
| --- | --- | --- |
| `coldframe-hal` | `packages/rs/hal` | The hardware traits (radio, eFuse, HMAC, flash, BLE, Wi-Fi) and their host mocks |
| `coldframe-crypto` | `packages/rs/crypto` | The key hierarchy, frame sealing, the replay window, HPKE enrolment, the setup session keys |
| `coldframe-protocol` | `packages/rs/protocol` | The wire messages generated from `packages/proto`, and the ESP-NOW message codec |
| `coldframe-setup` | `packages/rs/setup` | The BLE setup service: setup code, framing, session state machine, stored records |
| `coldframe-uplink` | `packages/rs/uplink` | The Hub's Wi-Fi uplink: access-point choice, heartbeats, the relay of Node frames |
| `coldframe-sensing` | `packages/rs/sensing` | The Node's wake cycle, the button, the flash-reserved counter |
| `coldframe-transport` | `packages/rs/transport` | The Node's ESP-NOW transport, report buffer and channel following |
| `coldframe-hub` | `apps/rs/hub` | The Hub firmware: boot order, board drivers. See [its README](../../apps/rs/hub/README.md) |
| `coldframe-node` | `apps/rs/node` | The Node firmware: one wake per boot, board drivers. See [its README](../../apps/rs/node/README.md) |

## Two toolchains

- **Host, Rust 1.97.** The Cargo workspace in the repository root holds the seven `packages/rs`
crates and their tests in `tests/rs`. `rust-toolchain.toml` pins the host toolchain, and the
workspace runs clippy, the tests and the API docs with it.
- **esp, Rust 1.95.** The firmware crates in `apps/rs/hub` and `apps/rs/node` build for
`xtensa-esp32s3-none-elf` with the `esp` toolchain from `espup`, so they stay outside the
workspace. Because the firmware links the `packages/rs` crates, those crates declare
`rust-version = "1.95"` and must not use anything newer. [flash.md](../build/flash.md#toolchain)
has the versions CI pins.

## Open the API docs

Every public item of the `packages/rs` crates is documented, including the wire types generated
from the `.proto` comments. CI builds the docs with warnings as errors. To browse one crate's docs
from the repository root:

<!-- ci:manual -->

```sh
cargo doc --no-deps -p coldframe-transport --open
```

Replace `coldframe-transport` with any crate of the map above that lives in `packages/rs`. The
firmware crates themselves need the esp toolchain: run `cargo doc --no-deps --open` in `apps/rs/node` or
`apps/rs/hub` after `source ~/export-esp.sh`.
Loading
Loading