Sync all project docs for firmware 0.8.3 completion.

Update READMEs, manual chapters, agent guides, CONTRIBUTING, and TODO to reflect T1–T8 done, encrypted NVS, CI gates, and pending HIL checklist.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-07 07:49:07 +02:00
co-authored by Cursor
parent a8f2fd1c6c
commit 8439ec4055
15 changed files with 283 additions and 260 deletions
+25 -8
View File
@@ -5,6 +5,9 @@ development conventions. The full, authoritative rules live in
[`Software/AGENTS.md`](Software/AGENTS.md); this is the human-facing [`Software/AGENTS.md`](Software/AGENTS.md); this is the human-facing
summary. summary.
**Current firmware:** **0.8.3** on `main` — agent tasks T1T8 complete;
device HIL pending first PCB.
DigiRadio is dual-licensed: hardware under **CERN-OHL-S v2**, firmware DigiRadio is dual-licensed: hardware under **CERN-OHL-S v2**, firmware
under **Apache-2.0**. By contributing, you agree your contributions are under **Apache-2.0**. By contributing, you agree your contributions are
licensed under the same terms as the part of the project they touch. licensed under the same terms as the part of the project they touch.
@@ -16,19 +19,28 @@ licensed under the same terms as the part of the project they touch.
| Framework | ESP-IDF v5.5.x (native, not Arduino) | | Framework | ESP-IDF v5.5.x (native, not Arduino) |
| Language | C++23, pinned `-std=gnu++23` | | Language | C++23, pinned `-std=gnu++23` |
| Errors | `std::expected<T, Error>`; exceptions off| | Errors | `std::expected<T, Error>`; exceptions off|
| Docs | Doxygen (build must pass, see below) | | Security | NVS + flash encryption (dev mode) |
| Docs | Doxygen + LaTeX manual sync (CI enforced)|
On macOS, the host unit tests need a C++23 standard library: use On macOS, the host unit tests need a C++23 standard library: use
Homebrew `llvm` (>= 18) or `gcc-14`, not the system Apple Clang. Homebrew `llvm` (>= 18) or `gcc-14`, not the system Apple Clang.
## Build, test, docs ## Build, test, docs
Device build / flash / monitor: Si4684 blobs (local only, not in git):
```bash
cd Software
python3 tools/fetch_si4684_firmware.py --dab-only
python3 tools/fetch_si4684_firmware.py --si46xx-dir /path/to/si46xx_firmware
```
Device build / flash / monitor (first encrypted flash: include `erase-flash`):
```bash ```bash
idf.py set-target esp32s3 idf.py set-target esp32s3
idf.py build idf.py build
idf.py -p <port> flash monitor idf.py erase-flash flash monitor
``` ```
Host unit tests (pure core, no hardware): Host unit tests (pure core, no hardware):
@@ -40,17 +52,20 @@ cmake --build build-host
ctest --test-dir build-host --output-on-failure ctest --test-dir build-host --output-on-failure
``` ```
Documentation (must exit 0; run from `Software/`): Documentation and policy checks (must exit 0; run from `Software/`):
```bash ```bash
doxygen Doxyfile doxygen Doxyfile
python3 tools/check-manual-sync.py python3 tools/check-manual-sync.py
python3 tools/check_si4684_blobs.py
``` ```
The LaTeX manual lives in `Software/docs/manual/` (canonical). The repository After editing the web UI: `python3 tools/gzip-www.sh`.
root `docs/` is a symbolic link to that folder for convenience and Overleaf
/GitHub browsing. Design and HTTP JSON API: `ch-api.tex`; rebuild the PDF with The LaTeX manual lives in `Software/docs/manual/` (canonical). Design and
`latexmk -lualatex manual.tex` inside `docs/` or `Software/docs/manual/`. HTTP JSON API: `ch-api.tex`; security: `docs/security-flash-nvs.md`.
Rebuild the PDF with `latexmk -lualatex manual.tex` inside
`Software/docs/manual/`.
## Coding conventions ## Coding conventions
@@ -104,6 +119,7 @@ Before opening a PR, confirm:
- [ ] Every class and method has its documentation block. - [ ] Every class and method has its documentation block.
- [ ] `doxygen Doxyfile` exits 0 with an empty warnings log. - [ ] `doxygen Doxyfile` exits 0 with an empty warnings log.
- [ ] `python3 tools/check-manual-sync.py` passes. - [ ] `python3 tools/check-manual-sync.py` passes.
- [ ] `python3 tools/check_si4684_blobs.py` passes.
- [ ] Manual updated: `ch-classes.tex` for new/changed public classes; - [ ] Manual updated: `ch-classes.tex` for new/changed public classes;
`ch-api.tex` for new/changed HTTP endpoints. `ch-api.tex` for new/changed HTTP endpoints.
- [ ] Every method fits 80x24 and complexity <= 7. - [ ] Every method fits 80x24 and complexity <= 7.
@@ -112,6 +128,7 @@ Before opening a PR, confirm:
- [ ] No secret is loggable or stored in plaintext. - [ ] No secret is loggable or stored in plaintext.
- [ ] Register-level decisions cite the datasheet section. - [ ] Register-level decisions cite the datasheet section.
- [ ] No dynamic allocation in audio/ISR paths. - [ ] No dynamic allocation in audio/ISR paths.
- [ ] Web UI changes regenerate `index.html.gz` via `tools/gzip-www.sh`.
## Editor setup (optional) ## Editor setup (optional)
+9 -8
View File
@@ -84,7 +84,7 @@ Vertical slices landed on `main` (newest first):
| Version | Highlights | | Version | Highlights |
|---------|------------| |---------|------------|
| **0.8.3** | NVS + flash encryption (development mode), `initEncryptedStorage`, security docs | | **0.8.3** | NVS + flash encryption (development mode), `initEncryptedStorage`, security docs |
| **0.8.2** | Tabbed configuration Web UI — now-playing, 6-band EQ, full API coverage | | **0.8.2** | Tabbed Web UI + Si4684 blob CI policy |
| **0.8.1** | `IntegrationService` — boot preset recall, tune orchestration (tuner + audio + NVS `last_preset`); services stub removed | | **0.8.1** | `IntegrationService` — boot preset recall, tune orchestration (tuner + audio + NVS `last_preset`); services stub removed |
| **0.8.0** | Preset reorder API/UI; broadcast metadata (RDS + DAB DLS); `readDabServiceData` driver path | | **0.8.0** | Preset reorder API/UI; broadcast metadata (RDS + DAB DLS); `readDabServiceData` driver path |
| **0.7.1** | CI workflow (host tests, Doxygen, manual sync); Doxygen warnings cleared | | **0.7.1** | CI workflow (host tests, Doxygen, manual sync); Doxygen warnings cleared |
@@ -101,7 +101,7 @@ Vertical slices landed on `main` (newest first):
``` ```
DigiRadio/ DigiRadio/
├── .github/workflows/ CI — host tests, Doxygen, manual class sync ├── .github/workflows/ CI — host tests, Doxygen, manual sync, Si4684 blob policy
├── Hardware/ ├── Hardware/
│ ├── schematics/ Schematic (PDF) │ ├── schematics/ Schematic (PDF)
│ ├── gerber/ Gerber + drill (fabrication) │ ├── gerber/ Gerber + drill (fabrication)
@@ -121,7 +121,8 @@ DigiRadio/
│ │ └── net/ Wi-Fi, HTTP server, gzipped web UI │ │ └── net/ Wi-Fi, HTTP server, gzipped web UI
│ ├── docs/ │ ├── docs/
│ │ ├── manual/ LaTeX technical manual (canonical) │ │ ├── manual/ LaTeX technical manual (canonical)
│ │ ── TODO.md Prioritised firmware backlog │ │ ── security-flash-nvs.md NVS/flash encryption + HIL checklist
│ │ └── TODO.md Agent backlog and completed tasks
│ ├── Firmware/ Si4684 blobs + ADAU1701 SigmaStudio export │ ├── Firmware/ Si4684 blobs + ADAU1701 SigmaStudio export
│ ├── main/ app_main, hardware bootstrap │ ├── main/ app_main, hardware bootstrap
│ └── tools/ Manual sync checker, Si4684 blob helpers │ └── tools/ Manual sync checker, Si4684 blob helpers
@@ -148,7 +149,8 @@ Open **`Software/`** as the Cursor project so `AGENTS.md` and `.cursor/rules/` l
cd Software cd Software
idf.py set-target esp32s3 idf.py set-target esp32s3
idf.py build idf.py build
idf.py -p <port> flash monitor idf.py erase-flash flash # once when first enabling encryption (0.8.3+)
idf.py -p <port> monitor
``` ```
Host unit tests (no hardware): Host unit tests (no hardware):
@@ -167,6 +169,7 @@ Quality gates (also enforced in CI):
cd Software cd Software
doxygen Doxyfile doxygen Doxyfile
python3 tools/check-manual-sync.py python3 tools/check-manual-sync.py
python3 tools/check_si4684_blobs.py
``` ```
Full build notes, API table, and component map: [`Software/README.md`](Software/README.md). Full build notes, API table, and component map: [`Software/README.md`](Software/README.md).
@@ -225,10 +228,8 @@ Generated C++ API reference: run `doxygen Doxyfile` → `Software/docs/api/html/
| **Prototype** | In fabrication (PCBWay) | | **Prototype** | In fabrication (PCBWay) |
| **Firmware** | **0.8.3** on `main` — encrypted NVS, full Web UI, integration, RDS/DLS | | **Firmware** | **0.8.3** on `main` — encrypted NVS, full Web UI, integration, RDS/DLS |
| **Web UI** | Tabbed SPA covering every REST endpoint | | **Web UI** | Tabbed SPA covering every REST endpoint |
| **HIL** | Security checklist pending PCB (`Software/docs/security-flash-nvs.md`) | | **HIL** | Device validation pending PCB (`Software/docs/security-flash-nvs.md`) |
| **Production hardening** | Si4684 blob policy (T7), NVS encryption (T8) — open | | **Agent backlog** | Feature-complete on `main` (fw 0.8.3); see [`Software/docs/TODO.md`](Software/docs/TODO.md) |
Agent task list: [`Software/docs/TODO.md`](Software/docs/TODO.md).
--- ---
+4 -4
View File
@@ -8,10 +8,10 @@ alwaysApply: false
Full spec: @AGENTS.md §7.5. Full spec: @AGENTS.md §7.5.
- Stores Wi-Fi SSID/password, user credentials, station list. Encrypted - Stores Wi-Fi SSID/password, station list, audio profiles, last-preset index.
at rest (NVS encryption on an encrypted partition, or a device key in **Encrypted at rest** — `CONFIG_NVS_ENCRYPTION` + flash encryption in
eFuse). Confirm the mechanism against current ESP-IDF security docs `sdkconfig.defaults` (development mode); `secure_store::initEncryptedStorage()`
before implementing. before any NVS access. See `docs/security-flash-nvs.md`.
- A `Secret` wrapper: no operator<<, no implicit conversion to a loggable - A `Secret` wrapper: no operator<<, no implicit conversion to a loggable
string, buffer zeroised on destruction. string, buffer zeroised on destruction.
- Secrets are never logged, never placed in URLs, never serialised to - Secrets are never logged, never placed in URLs, never serialised to
+13 -10
View File
@@ -300,8 +300,8 @@ Layered, dependencies point inward only:
``` ```
Shell (imperative): drivers, web server, NVS, tasks, ISRs Shell (imperative): drivers, web server, NVS, tasks, ISRs
— thin, no business logic [ESP-IDF, HW] — thin, no business logic [ESP-IDF, HW]
Application services: TunerService, AudioService, Application services: TunerService, AudioService, StationService,
ConfigService, NetworkService [orchestration] BluetoothService, IntegrationService [orchestration]
Domain core (pure, host-testable): Station, Frequency, Domain core (pure, host-testable): Station, Frequency,
EqProfile, MixerState, Credential, boot-blob EqProfile, MixerState, Credential, boot-blob
framing, validation [no HW headers] framing, validation [no HW headers]
@@ -420,11 +420,12 @@ Layered, dependencies point inward only:
### 7.5 Secure storage ### 7.5 Secure storage
- Stores: Wi-Fi SSID + password, user credentials (name + password), - Stores: Wi-Fi SSID + password, station/frequency list, audio profiles,
station/frequency list. **Encrypted at rest** — use NVS encryption on last-preset index. **Encrypted at rest** — NVS encryption with flash
an encrypted partition (with flash encryption enabled), or encrypt encryption enabled in `sdkconfig.defaults` (development mode); keys in
payloads with a device key held in eFuse. Confirm the chosen mechanism `nvs_keys` partition; init via `secure_store::initEncryptedStorage()`.
against current ESP-IDF security docs before implementing. Production release mode: `sdkconfig.defaults.production`. See
`docs/security-flash-nvs.md`.
- **Secrets never leave their type.** A `Secret` wrapper: no `operator<<`, - **Secrets never leave their type.** A `Secret` wrapper: no `operator<<`,
no implicit conversion to a loggable string, buffer zeroised on no implicit conversion to a loggable string, buffer zeroised on
destruction. Secrets are never logged, never placed in URLs, never destruction. Secrets are never logged, never placed in URLs, never
@@ -537,11 +538,11 @@ ESP-IDF header, so the host build stays hardware-free.
### Commands ### Commands
Device build / flash / monitor: Device build / flash / monitor (first encrypted flash: erase once):
``` ```
idf.py set-target esp32s3 idf.py set-target esp32s3
idf.py build idf.py build
idf.py -p <port> flash monitor idf.py erase-flash flash monitor
``` ```
Host unit tests (pure core; needs a C++23 stdlib compiler): Host unit tests (pure core; needs a C++23 stdlib compiler):
@@ -552,9 +553,11 @@ cmake --build build-host
ctest --test-dir build-host --output-on-failure ctest --test-dir build-host --output-on-failure
``` ```
Docs (must exit 0, empty warnings log): Docs and policy (must exit 0, from `Software/`):
``` ```
doxygen Doxyfile doxygen Doxyfile
python3 tools/check-manual-sync.py
python3 tools/check_si4684_blobs.py
``` ```
### Host toolchain note (macOS) ### Host toolchain note (macOS)
+3 -2
View File
@@ -83,8 +83,9 @@ C++ signatures: `doxygen Doxyfile` → `docs/api/html/index.html`.
| `docs/security-flash-nvs.md` | NVS + flash encryption and HIL checklist | | `docs/security-flash-nvs.md` | NVS + flash encryption and HIL checklist |
| `docs/TODO.md` | Agent task list (prioritised backlog) | | `docs/TODO.md` | Agent task list (prioritised backlog) |
See [`AGENTS.md`](AGENTS.md) §12 and [`instructions.md`](instructions.md) for See [`AGENTS.md`](AGENTS.md), [`instructions.md`](instructions.md),
coding rules and slice roadmap. [`docs/security-flash-nvs.md`](docs/security-flash-nvs.md), and
[`docs/TODO.md`](docs/TODO.md) for coding rules, security, and backlog.
## Licence ## Licence
+54 -67
View File
@@ -1,93 +1,80 @@
# TODO — DigiRadio firmware # TODO — DigiRadio firmware
Task list for the coding agent. Work top to bottom; each task is a Agent task list and hardware-in-the-loop backlog. Working directory for all
vertical slice that keeps `main` building and the host tests green. commands is `Software/`.
**Before writing code, read `AGENTS.md`, `.cursor/rules/`, and
`instructions.md`.** Every task below must satisfy the Definition of Done
in `AGENTS.md §10`: Apache header on new files, doc block on every class
and method, `doxygen Doxyfile` exits 0, host tests pass,
`tools/check-manual-sync.py` passes, no primitive obsession, typed
errors, no plaintext secrets.
Working directory for all commands is `Software/`.
**Current firmware:** `0.8.3` — NVS + flash encryption (dev mode), tabbed Web **Current firmware:** `0.8.3` — NVS + flash encryption (dev mode), tabbed Web
UI, integration service, CI gate. UI, integration service, RDS/DLS metadata, CI gate (4 jobs).
**Before writing code, read `AGENTS.md`, `.cursor/rules/`, and
`instructions.md`.** Definition of Done: Apache header, doc blocks,
`doxygen Doxyfile` exits 0, host tests pass, `check-manual-sync.py` and
`check_si4684_blobs.py` pass, no plaintext secrets.
--- ---
## Completed (fw 0.7.00.8.3) ## Completed agent tasks (T1T8, fw 0.7.10.8.3)
- **Integration service (T5)** — preset recall with audio profile re-apply, | Task | Version | Summary |
last-preset NVS, \texttt{app\_main} orchestration. |------|---------|---------|
- **BT1035 pairing** — `AT+PAIR`, `AT+A2DPSTAT`, `AT+A2DPDISC`, | **T1** | 0.7.1 | Doxygen warnings cleared |
`BluetoothService`, REST + UI (not numbered below; landed with Slice 7). | **T2** | 0.7.1 | CI workflow (host tests, Doxygen, manual sync) |
| **T3** | 0.7.2 | Preset reorder API/UI, DAB playing ids in status |
| **T4** | 0.8.0 | RDS/DLS broadcast metadata |
| **T5** | 0.8.1 | `IntegrationService` — startup, preset recall, last-preset NVS |
| **T6** | 0.8.2 | Tabbed configuration Web UI (full REST coverage) |
| **T7** | 0.8.2 | Si4684 blob policy — gitignore, docs, `check_si4684_blobs.py` |
| **T8** | 0.8.3 | NVS + flash encryption — `initEncryptedStorage`, security docs |
Also landed (not numbered): BT1035 pairing (`BluetoothService`), station presets
(fw 0.7.0), companion-chip boot (Slice 3), ADAU1701 runtime (Slice 5).
--- ---
## P0Fix the build gate (do this first) ## P4Hardware-in-the-loop (when PCB arrives)
### T1. Clear the 16 Doxygen warnings — **DONE (fw 0.7.1)** Manual validation only — does not block host CI.
Fixed invalid `\texttt`/`\r`/`\ref` in doc blocks; documented
`Station` accessors and `NetBootstrap`/`SetupWebServer` parameters.
`doxygen Doxyfile` exits 0 with an empty warnings log.
### T2. Add the CI workflow — **DONE (fw 0.7.1)** ### H1. Encrypted NVS boot path
`.github/workflows/ci.yml`: host `ctest`, Doxygen, manual sync on every Follow [`docs/security-flash-nvs.md`](security-flash-nvs.md): first flash with
push/PR to `main`. `idf.py erase-flash flash`, verify boot logs, Wi-Fi provisioning survives
reboot, presets and `last_preset` survive power cycle.
### H2. End-to-end listening
Si4684 DAB/FM tune, ADAU1701 profile apply, BT1035 A2DP to headphones,
now-playing metadata in UI and `/api/tuner/status`.
### H3. Production flash encryption (optional)
After H1 passes, trial build with `sdkconfig.defaults.production` overlay on
a sacrificial unit; confirm RELEASE mode policy before shipping.
--- ---
## P1 — Missing domain features ## Open firmware polish (non-blocking)
### T3. Station / preset list — polish — **DONE (fw 0.7.2)** - BT1035: device name, paired-device list, auto-reconnect AT (driver stubs open).
Reorder API (`POST /api/stations/reorder`), DAB playing ids in tuner - Si4684: optional commands (STOP_DIGITAL_SERVICE, ensemble info) if product needs them.
status and preset save, UI Up/Dn, host tests. **Remaining:** device HIL - FM seek down (API today is seek-up only).
(preset survives reboot) — manual only.
### T4. Broadcast metadata (RDS / DLS) — **DONE (fw 0.8.0)**
`BroadcastLabel`, RDS accumulator, DAB DLS accumulator, driver
`readDabServiceData`, status JSON fields, UI now-playing lines, host tests.
### T5. Remove the services stub — integration service — **DONE (fw 0.8.1)**
`integration::IntegrationService` orchestrates startup, preset recall,
audio profile re-apply, and last-preset NVS. Stub removed; `app_main` and
`POST /api/stations/tune` delegate here.
--- ---
## P2 — User interface ## Quality gates (run from `Software/` before merge)
### T6. Complete the configuration Web UI — **DONE (fw 0.8.2)** ```bash
Tabbed SPA (`Now` / `Radio` / `Presets` / `Audio` / `BT` / `WiFi`): cmake -S components/core/test -B build-host && cmake --build build-host
now-playing hero with 5 s metadata poll, six-band EQ sliders, all REST ctest --test-dir build-host --output-on-failure
endpoints wired, companion-chip badges, `tools/gzip-www.sh` for the doxygen Doxyfile
embedded gzip blob. No debug routes in `SetupWebServer`. python3 tools/check-manual-sync.py
python3 tools/check_si4684_blobs.py
```
--- After editing the web UI: `tools/gzip-www.sh`.
## P3 — Procurement & hardening
### T7. Si4684 firmware blob strategy (legal) — **DONE (fw 0.8.2)**
`Firmware/Si4684-Firmware/*.bin` gitignored; no blobs in git history.
Procurement documented in `Si4684-Firmware/README.md`; CI job
`si4684-blobs` runs `tools/check_si4684_blobs.py`.
### T8. Flash/NVS encryption enablement — **DONE (fw 0.8.3)**
`CONFIG_NVS_ENCRYPTION` + flash encryption (development mode) in
`sdkconfig.defaults`; `secure_store::initEncryptedStorage()`; production
overlay `sdkconfig.defaults.production`; HIL checklist in
`docs/security-flash-nvs.md`. **Pending:** device verification when PCB
arrives.
--- ---
## Notes for the agent ## Notes for the agent
- Prefer extending existing patterns over inventing new ones: copy the
shape of `AudioProfile` / `AudioProfileJson` / `IAudioProfileStore` for - Extend existing patterns (`AudioProfile` / `IAudioProfileStore` shape).
new persisted models. - Never invent Si4684 opcodes — cite AN649.
- Never invent Si4684 register/command details — cite AN649. - One logical change per commit; 50/72 messages.
- One logical change per commit; 50/72 commit messages. - Update `ch-classes.tex` / `ch-api.tex` when public API or HTTP changes.
- After each task, run: host `ctest`, `doxygen Doxyfile`,
`tools/check-manual-sync.py` — all must pass before moving on.
+1 -1
View File
@@ -6,7 +6,7 @@ implemented in \texttt{SetupWebServer}. Request bodies are parsed into
domain types in the pure core (\texttt{components/core}) before any domain types in the pure core (\texttt{components/core}) before any
persistence or driver call. Exact C++ signatures live in the generated persistence or driver call. Exact C++ signatures live in the generated
Doxygen output under \texttt{docs/api/}; this chapter documents the Doxygen output under \texttt{docs/api/}; this chapter documents the
wire protocol and behaviour as shipped in firmware~0.5.0 (Slices~1--5). wire protocol and behaviour as shipped in firmware~0.8.3.
\section{Transport and reachability} \section{Transport and reachability}
+37 -4
View File
@@ -14,6 +14,7 @@ builds and is unit-tested on the host machine.
Framework & ESP-IDF v5.5.x (native) \\ Framework & ESP-IDF v5.5.x (native) \\
Language & C++23 (\texttt{-std=gnu++23}) \\ Language & C++23 (\texttt{-std=gnu++23}) \\
Error model & \texttt{std::expected}; exceptions off \\ Error model & \texttt{std::expected}; exceptions off \\
Security & NVS + flash encryption (dev mode); see \texttt{docs/security-flash-nvs.md} \\
Documentation & Doxygen (build must pass) \\ Documentation & Doxygen (build must pass) \\
\bottomrule \bottomrule
\end{tabular} \end{tabular}
@@ -21,14 +22,32 @@ builds and is unit-tested on the host machine.
\label{tab:build-toolchain} \label{tab:build-toolchain}
\end{table} \end{table}
\section{Si4684 firmware blobs (local only)}
Proprietary Si4684 images are \textbf{not} in git. Before the first device
build:
\begin{drcode}[Populate blobs (from Software/)]
python3 tools/fetch_si4684_firmware.py --dab-only
python3 tools/fetch_si4684_firmware.py --si46xx-dir /path/to/si46xx_firmware
python3 tools/check_si4684_blobs.py
\end{drcode}
See \texttt{Firmware/Si4684-Firmware/README.md} for procurement options.
\section{Device build} \section{Device build}
First flash after enabling encryption (fw~0.8.3+) requires a one-time erase:
\begin{drcode}[Build, flash, monitor] \begin{drcode}[Build, flash, monitor]
idf.py set-target esp32s3 idf.py set-target esp32s3
idf.py build idf.py build
idf.py -p <port> flash monitor idf.py erase-flash flash monitor
\end{drcode} \end{drcode}
Production flash-encryption release mode uses \texttt{sdkconfig.defaults.production}
as an overlay --- irreversible on the chip; see the security doc before use.
\section{Host unit tests} \section{Host unit tests}
The pure core is tested on the host, with no board attached. On macOS the The pure core is tested on the host, with no board attached. On macOS the
@@ -42,9 +61,12 @@ cmake --build build-host
ctest --test-dir build-host --output-on-failure ctest --test-dir build-host --output-on-failure
\end{drcode} \end{drcode}
Thirteen test executables cover JSON parsing, audio design, station list,
integration service, and broadcast metadata accumulators.
\section{Documentation} \section{Documentation}
Documentation has two enforced checks, run from the \texttt{Software/} Documentation has enforced checks, run from the \texttt{Software/}
directory: directory:
\begin{enumerate} \begin{enumerate}
@@ -55,17 +77,21 @@ directory:
\texttt{\textbackslash label\{cls:ClassName\}} section in \texttt{\textbackslash label\{cls:ClassName\}} section in
\texttt{docs/manual/ch-classes.tex}. Design-level HTTP API \texttt{docs/manual/ch-classes.tex}. Design-level HTTP API
documentation lives in Chapter~\ref{ch:api}. documentation lives in Chapter~\ref{ch:api}.
\item \textbf{Si4684 blob policy} --- \texttt{tools/check\_si4684\_blobs.py}
ensures no proprietary \texttt{.bin} is tracked in git.
\end{enumerate} \end{enumerate}
\begin{drcode}[Docs (from Software/)] \begin{drcode}[Docs (from Software/)]
doxygen Doxyfile doxygen Doxyfile
python3 tools/check-manual-sync.py python3 tools/check-manual-sync.py
python3 tools/check_si4684_blobs.py
python3 tools/gzip-www.sh # after editing components/net/www/index.html
\end{drcode} \end{drcode}
\section{Continuous integration} \section{Continuous integration}
Every push and pull request to \texttt{main} runs Every push and pull request to \texttt{main} runs
\texttt{.github/workflows/ci.yml} at the repository root (three parallel \texttt{.github/workflows/ci.yml} at the repository root (four parallel
jobs, all from the \texttt{Software/} directory): jobs, all from the \texttt{Software/} directory):
\begin{enumerate} \begin{enumerate}
@@ -74,6 +100,7 @@ jobs, all from the \texttt{Software/} directory):
\item \textbf{Doxygen} --- must exit 0 with an empty \item \textbf{Doxygen} --- must exit 0 with an empty
\texttt{docs/api/doxygen-warnings.log}. \texttt{docs/api/doxygen-warnings.log}.
\item \textbf{Manual sync} --- \texttt{tools/check-manual-sync.py}. \item \textbf{Manual sync} --- \texttt{tools/check-manual-sync.py}.
\item \textbf{Si4684 blobs} --- \texttt{tools/check\_si4684\_blobs.py}.
\end{enumerate} \end{enumerate}
To rebuild the PDF manual (requires a LaTeX installation): To rebuild the PDF manual (requires a LaTeX installation):
@@ -84,8 +111,14 @@ latexmk -lualatex manual.tex
\end{drcode} \end{drcode}
\begin{drcaution}[Keep it green] \begin{drcaution}[Keep it green]
Both checks are part of the definition of done. A firmware change that All checks are part of the definition of done. A firmware change that
adds or modifies a public class, a REST endpoint, or its behaviour must adds or modifies a public class, a REST endpoint, or its behaviour must
update the Doxygen doc blocks, \texttt{ch-classes.tex} (for classes), and update the Doxygen doc blocks, \texttt{ch-classes.tex} (for classes), and
\texttt{ch-api.tex} (for HTTP) in the same change. \texttt{ch-api.tex} (for HTTP) in the same change.
\end{drcaution} \end{drcaution}
\section{Hardware validation}
Automated CI does not attach to a board. When the PCB is available, run the
checklist in \texttt{docs/security-flash-nvs.md} and the HIL items in
\texttt{docs/TODO.md} (section P4).
+12 -12
View File
@@ -16,16 +16,15 @@ added in the same change that introduces the class. A tooling check keeps
this chapter in step with the code, so it is always current. this chapter in step with the code, so it is always current.
\end{drnote} \end{drnote}
The firmware is under active development. As each public class lands, its The class reference tracks firmware~0.8.3 on \texttt{main}. Public classes
section appears below, grouped by layer: domain core, application are grouped by layer: domain core, application services, and hardware drivers.
services, and hardware drivers.
% ------------------------------------------------------------------ % ------------------------------------------------------------------
% Domain core (Slice 1) % Domain core (Slice 1)
% ------------------------------------------------------------------ % ------------------------------------------------------------------
\section{FirmwareVersion}\label{cls:FirmwareVersion} \section{FirmwareVersion}\label{cls:FirmwareVersion}
Strong type wrapping the firmware release identifier (e.g.\ \texttt{0.1.0}). Strong type wrapping the firmware release identifier (e.g.\ \texttt{0.8.3}).
Used by \texttt{HealthStatus} and the \texttt{/api/health} endpoint so Used by \texttt{HealthStatus} and the \texttt{/api/health} endpoint so
version strings are never passed as bare \texttt{char*} across module version strings are never passed as bare \texttt{char*} across module
boundaries. Invariant: non-empty at construction. boundaries. Invariant: non-empty at construction.
@@ -65,11 +64,10 @@ tuner via \texttt{tuner::TunerService}; audio via
presets via \texttt{station::StationService}. presets via \texttt{station::StationService}.
\section{NetBootstrap}\label{cls:NetBootstrap} \section{NetBootstrap}\label{cls:NetBootstrap}
Owns network resources for setup or STA mode. Owns network resources for setup or STA mode. Initialises encrypted NVS via
\texttt{start(store, tuner)} initialises the platform, joins stored Wi-Fi \texttt{secure\_store::initEncryptedStorage()}, then Wi-Fi and the HTTP server.
when credentials exist, or falls back to the \texttt{DigiRadio-setup} Wires tuner, audio, Bluetooth, station, and integration services into REST
SoftAP. Must outlive \texttt{app\_main} for the process lifetime. handlers. Must outlive \texttt{app\_main} for the process lifetime.
\texttt{start(store, tuner, audio)} also wires the audio REST routes.
% ------------------------------------------------------------------ % ------------------------------------------------------------------
% Domain core + secure store (Slice 2) % Domain core + secure store (Slice 2)
@@ -89,9 +87,11 @@ Domain value pairing \texttt{WifiSsid} with a \texttt{Secret} password.
Open networks use an empty password; WPA-PSK requires 8--63 characters. Open networks use an empty password; WPA-PSK requires 8--63 characters.
\section{ISecureStore}\label{cls:ISecureStore} \section{ISecureStore}\label{cls:ISecureStore}
Abstract persistence boundary for credentials at rest. Slice~2 implements Abstract persistence boundary for credentials and preset data at rest.
Wi-Fi credential save/load/clear; station list and user credentials arrive Wi-Fi credentials, station list JSON, and last-preset index use
in later slices. Host tests use fakes; the shell uses \texttt{NvsSecureStore}. \texttt{NvsSecureStore}; audio profiles use \texttt{NvsAudioProfileStore}.
Host tests use fakes; the shell uses NVS backends after
\texttt{initEncryptedStorage()}.
\section{StaClient}\label{cls:StaClient} \section{StaClient}\label{cls:StaClient}
RAII STA join helper with an explicit connect timeout. Assumes RAII STA join helper with an explicit connect timeout. Assumes
+24 -15
View File
@@ -64,7 +64,8 @@ itself.
\node[layer, fill=black!8, below=of shell] (services) {% \node[layer, fill=black!8, below=of shell] (services) {%
\textbf{Application services}\\[2pt] \textbf{Application services}\\[2pt]
TunerService \textbullet\ AudioService \textbullet\ TunerService \textbullet\ AudioService \textbullet\
ConfigService \textbullet\ NetworkService}; StationService \textbullet\ BluetoothService \textbullet\
IntegrationService};
\node[layer, fill=black!12, below=of services] (core) {% \node[layer, fill=black!12, below=of services] (core) {%
\textbf{Domain core (pure, host-tested)}\\[2pt] \textbf{Domain core (pure, host-tested)}\\[2pt]
Station \textbullet\ Frequency \textbullet\ EqProfile \textbullet\ Station \textbullet\ Frequency \textbullet\ EqProfile \textbullet\
@@ -165,23 +166,31 @@ through \texttt{core::ISecureStore}, and the device reboots into STA mode
on the next boot. The HTTP endpoints and JSON schemas are documented in on the next boot. The HTTP endpoints and JSON schemas are documented in
Chapter~\ref{ch:api}. Chapter~\ref{ch:api}.
\paragraph{Implemented (Slices~1--2).} \paragraph{Implemented (fw 0.8.3).}
\begin{itemize} \begin{itemize}
\item \texttt{GET /api/health} --- health DTO \item \textbf{Network} --- \texttt{GET /api/health}, \texttt{POST /api/wifi},
(\texttt{core::HealthStatus}, serialised in the pure core). SoftAP/STA state machine (\texttt{NetState}), tabbed gzipped web UI.
\item \texttt{POST /api/wifi} --- Wi-Fi provisioning \item \textbf{Tuner} --- \texttt{/api/tuner/*} (FM/DAB tune, seek, services,
(\texttt{core::WifiCredentials} via \texttt{parseWifiProvisionJson}). play); RDS and DAB dynamic labels in status JSON.
\item \texttt{secure\_store::NvsSecureStore} --- NVS persistence for \item \textbf{Audio} --- \texttt{/api/audio/*} (profile, reset, stereo/bass
SSID and PSK (\texttt{core::Secret}); station list and user enhance); six-band EQ via \texttt{AudioService}.
credentials arrive in later slices on the same \item \textbf{Bluetooth} --- \texttt{/api/bluetooth/*} (pair, stop, disconnect,
\texttt{ISecureStore} interface. status).
\item \textbf{Presets} --- \texttt{/api/stations/*} including reorder and
integrated recall via \texttt{IntegrationService}.
\item \textbf{Storage} --- \texttt{NvsSecureStore}, \texttt{NvsAudioProfileStore},
encrypted NVS + flash encryption (development mode); keys in
\texttt{nvs\_keys} partition; init via \texttt{initEncryptedStorage()}.
\end{itemize} \end{itemize}
Sensitive data --- Wi-Fi credentials today; user credentials and the \paragraph{Legacy note (Slices 1--2).}
station/frequency list in later slices --- uses \texttt{core::Secret} so The first slices introduced health, Wi-Fi provisioning, and the secure-store
values cannot be logged or implicitly converted to a string; buffers are boundary; the bullets above supersede the original slice-scoped list.
cleared on destruction. Firmware~0.8.3+ enables NVS and flash encryption at
rest via \texttt{secure\_store::initEncryptedStorage()} (see Sensitive data uses \texttt{core::Secret} where applicable so values cannot
be logged or implicitly converted to a string; buffers are cleared on
destruction. Firmware~0.8.3 enables NVS and flash encryption at rest via
\texttt{secure\_store::initEncryptedStorage()} (see
\texttt{docs/security-flash-nvs.md} and Chapter~\ref{sec:api-storage}). \texttt{docs/security-flash-nvs.md} and Chapter~\ref{sec:api-storage}).
\section{Companion-chip boot at power-up} \section{Companion-chip boot at power-up}
+3 -1
View File
@@ -4,7 +4,9 @@
DigiRadio is an open-source, high-fidelity digital radio receiver. It DigiRadio is an open-source, high-fidelity digital radio receiver. It
receives DAB+ and FM broadcasts, processes the audio through a dedicated receives DAB+ and FM broadcasts, processes the audio through a dedicated
signal processor, and streams the result over Bluetooth using a signal processor, and streams the result over Bluetooth using a
high-resolution codec. The whole project --- hardware and firmware --- is high-resolution codec. Firmware~0.8.3 on \texttt{main} provides encrypted
storage, a tabbed configuration web UI, and the full REST API documented
in Chapter~\ref{ch:api}. The whole project --- hardware and firmware --- is
released as open source for the maker and audio community to study, released as open source for the maker and audio community to study,
build, and improve. build, and improve.
+1 -1
View File
@@ -218,7 +218,7 @@ SigmaStudio export (Section~\ref{sec:ss-export}).
\label{sec:ss-enhancements} \label{sec:ss-enhancements}
The SigmaStudio export does not include dedicated stereo widener or bass The SigmaStudio export does not include dedicated stereo widener or bass
boost blocks. Firmware~0.5.0 maps enhancement levels (0--100) onto the boost blocks. Firmware maps enhancement levels (0--100) onto the
existing Param EQ1 bands at runtime: existing Param EQ1 bands at runtime:
\begin{itemize} \begin{itemize}
+4 -3
View File
@@ -29,9 +29,10 @@
\vfill \vfill
{\color{drInk}\large Michele Bigi\par} {\color{drInk}\large Michele Bigi\par}
\vspace{1mm} \vspace{2mm}
{\color{drGray} 2026 \quad\textbullet\quad Hardware: CERN-OHL-S v2 {\color{drGray}Firmware 0.8.3 \quad\textbullet\quad 2026\par}
\quad\textbullet\quad Firmware: Apache-2.0\par} \vspace{2mm}
{\color{drGray}Hardware: CERN-OHL-S v2 \quad\textbullet\quad Firmware: Apache-2.0\par}
\vspace{2mm} \vspace{2mm}
{\color{drGray}\small \url{https://github.com/manvalan/DigiRadio}\par} {\color{drGray}\small \url{https://github.com/manvalan/DigiRadio}\par}
\end{titlepage} \end{titlepage}
+57 -98
View File
@@ -2,7 +2,10 @@
Read this together with `AGENTS.md` and everything under Read this together with `AGENTS.md` and everything under
`.cursor/rules/`. Those define *how* to write code; this file defines `.cursor/rules/`. Those define *how* to write code; this file defines
*what we are building* and *what to do first*. *what we are building* and the current state on `main`.
**Firmware on `main`:** **0.8.3** — all agent tasks T1T8 complete; device
HIL pending PCB arrival.
## What DigiRadio is ## What DigiRadio is
@@ -15,8 +18,8 @@ ESP32-S3 and coordinates three companion chips:
- **FSC-BT1035 (QCC3056)** — Bluetooth 5.2 out with aptX Adaptive, - **FSC-BT1035 (QCC3056)** — Bluetooth 5.2 out with aptX Adaptive,
controlled by AT commands over UART. controlled by AT commands over UART.
Plus: an elegant, essential web UI for network configuration; encrypted Plus: tabbed web UI for provisioning and control; **encrypted NVS** for
storage for Wi-Fi and user credentials and the station list. Wi-Fi credentials, presets, audio profiles, and last-preset index.
Repository: https://github.com/manvalan/DigiRadio Repository: https://github.com/manvalan/DigiRadio
@@ -29,116 +32,72 @@ Repository: https://github.com/manvalan/DigiRadio
| Errors | `std::expected<T, Error>` (native); exceptions OFF | | Errors | `std::expected<T, Error>` (native); exceptions OFF |
| DSP boot | ESP32 writes ADAU1701 RAM at every boot (no EEPROM) | | DSP boot | ESP32 writes ADAU1701 RAM at every boot (no EEPROM) |
| Architecture| Functional core (pure, host-tested) + imperative shell| | Architecture| Functional core (pure, host-tested) + imperative shell|
| Docs | Doxygen, build must exit 0 (enforced) | | Security | NVS + flash encryption (dev mode); see `docs/security-flash-nvs.md` |
| Docs | Doxygen + LaTeX manual sync (CI enforced) |
| HW licence | CERN-OHL-S v2 · FW licence: Apache-2.0 | | HW licence | CERN-OHL-S v2 · FW licence: Apache-2.0 |
## Roadmap status
| Slice / task | Status | Notes |
|--------------|--------|-------|
| 1 Walking skeleton | Done | SoftAP, gzipped UI, `/api/health` |
| 2 Secure store + Wi-Fi | Done | `ISecureStore`, STA provisioning |
| 3 Companion-chip boot | Done | Si4684 + ADAU1701 from `Firmware/` |
| 4 Station presets | Done (0.7.0) | NVS `station_list`, full `/api/stations/*` |
| 5 ADAU1701 runtime | Done | EQ, mixer, enhancements, audio API |
| 6 Si4684 tuning | Done | FM/DAB tune, seek, RSQ, RDS, DAB services/DLS |
| 7 BT1035 | Mostly done | Pairing, A2DP stat/disconnect; name/plist AT open |
| 8 Integration | Done (0.8.1) | `IntegrationService`, last-preset NVS |
| T6 Web UI | Done (0.8.2) | Tabbed SPA, all REST endpoints |
| T7 Si4684 blobs | Done (0.8.2) | Local-only `.bin`, CI policy check |
| T8 NVS encryption | Done (0.8.3) | `initEncryptedStorage`; HIL when PCB ready |
Next work: **hardware-in-the-loop** (`docs/TODO.md` § P4), not new features
unless the user requests them.
## Working agreement ## Working agreement
- **Confirm understanding before writing code.** On kickoff, summarise - **Blockers first** — state risks before solutions.
the plan and list any blockers or unclear hardware invariants first. - **One vertical slice at a time** — `main` always builds; host tests green.
- **Blockers first**, always. State risks before solutions. - Apache header + Doxygen doc blocks on every file/class/method.
- **One vertical slice at a time.** `main` always builds and runs. - Never invent register/opcode/boot steps — cite the datasheet or stop.
- Every file gets the Apache header; every class/method its doc block; - After changes: `ctest`, `doxygen`, `check-manual-sync.py`, `check_si4684_blobs.py`.
`doxygen Doxyfile` stays green. Small commits, 50/72 messages.
- Never invent a register/opcode/boot step — cite the datasheet or stop.
## Roadmap (slices, in order)
1. **Walking skeleton** — done (Slice 1).
2. **Secure store + Wi-Fi provisioning** — done (Slice 2).
3. **Companion-chip boot** — done (Slice 3): Si4684 DAB + ADAU1701 RAM load.
4. **Station/frequency list model + persistence + UI** — done (fw 0.7.0):
`Station`/`StationList`, NVS key `station_list`, `/api/stations/*`, Presets UI.
5. Si4684 tuning: RSQ, station list, DAB properties.
6. **ADAU1701 runtime** — done (Slice 5): safeload EQ + input mixer + HTTP.
7. **FSC-BT1035 driver** — init + pairing (AT+PAIR, A2DP stat/disconnect) done;
name/plist/reconnect AT still open.
8. Integration: TunerService + AudioService end to end.
## Slice 1 — Walking skeleton (complete) ## Slice 1 — Walking skeleton (complete)
Goal: exercise the whole toolchain end to end with zero chip hardware, - ESP-IDF `esp32s3`, C++23, `components/core` host-testable.
so every later slice drops into a working frame. - SoftAP `DigiRadio-setup`, gzipped page, `GET /api/health`.
- Current health JSON includes `fw` (today **0.8.3**) and companion-chip flags.
Build:
- Top-level ESP-IDF project targeting `esp32s3`.
- `sdkconfig.defaults` sets C++23, exceptions off, NVS + flash encryption
(development mode). Production overlay: `sdkconfig.defaults.production`.
- The `components/core` component compiles both under ESP-IDF and
standalone on the host.
Behaviour:
- `app_main` starts a FreeRTOS task that logs a heartbeat on a timer.
- Bring up SoftAP with a known SSID (e.g. `DigiRadio-setup`).
- Start an HTTP server serving one minimal gzipped page from flash.
- Expose `GET /api/health` returning a typed DTO serialised by the pure
core, e.g. `{"status":"ok","fw":"0.3.0"}`.
Documentation (required):
- Doxygen doc blocks on every class/method; `doxygen Doxyfile` green.
- Manual: class sections in `docs/manual/ch-classes.tex`;
HTTP API in `docs/manual/ch-api.tex`.
- `python3 tools/check-manual-sync.py` green.
## Slice 2 — Secure store + Wi-Fi STA (complete) ## Slice 2 — Secure store + Wi-Fi STA (complete)
Goal: persist Wi-Fi credentials and join the configured network after - `ISecureStore`, `NvsSecureStore`, `StaClient`, `NetBootstrap`.
provisioning; fall back to SoftAP when no credentials or join fails. - `POST /api/wifi` + Wi-Fi tab in web UI.
- NVS encryption enabled in fw 0.8.3 via `initEncryptedStorage()`.
Build on Slice 1:
- `core::ISecureStore` interface + `secure_store::NvsSecureStore` (NVS).
- `core::Secret`, `WifiSsid`, `WifiCredentials`, `parseWifiProvisionJson`.
- `net::StaClient`, `NetBootstrap::start(store)` state machine.
- `POST /api/wifi` + provisioning form in the web UI; reboot after save.
Acceptance criteria:
- [x] Provisioning via SoftAP saves credentials and reboots; next boot joins STA.
- [x] Host tests for health JSON and Wi-Fi provision parse/serialise.
- [x] Doxygen green; manual sync green; `ch-api.tex` documents endpoints.
- [x] No ESP-IDF headers in `components/core`.
Out of scope for Slice 2: station list (later slices). NVS encryption landed
in fw 0.8.3 — see `docs/security-flash-nvs.md`.
(production), chip drivers.
## Slice 3 — Companion-chip boot (complete) ## Slice 3 — Companion-chip boot (complete)
Goal: load Si4684 DAB firmware and ADAU1701 SigmaStudio program from - Si4684 blobs local-only (`tools/fetch_si4684_firmware.py`).
`Firmware/` on every boot, before network bring-up. - `Si4684Driver`, `Adau1701Driver`, `HardwareBootstrap` before network.
- Device flash: pending HIL on first PCB.
Build: ## Slices 48 — Presets, audio, tuner, BT, integration (complete)
- `Firmware/Si4684-Firmware/``rom_patch_016.bin`, `dab_firmware.bin`,
`fm_firmware.bin` (FM via `tools/fetch_si4684_firmware.py --si46xx-dir`).
- `Firmware/ADAU1701-Firmware/` — SigmaStudio export (`DigiRadio_IC_1.h`, …).
- `core::IFirmwareBlobReader` + `EmbeddedBlobReader` for chunked HOST_LOAD.
- `si4684::Si4684Driver`, `adau1701::Adau1701Driver`, `HardwareBootstrap`.
Behaviour: - Presets: `StationService`, reorder, integration recall with audio profile.
- `app_main` calls `HardwareBootstrap::boot()` first (Si4684, then ADAU1701). - Audio: six-band EQ, enhancements, `NvsAudioProfileStore`.
- On failure, firmware logs and halts before Wi-Fi. - Tuner: RDS/DLS metadata in status JSON and Now Playing UI.
- Bluetooth: `BluetoothService`, pairing REST + UI.
- Integration: boot loads last preset; `POST /api/stations/tune` orchestrates tune + audio + NVS.
Acceptance criteria: ## Quality gates (from `Software/`)
- [x] AN649 boot sequence with streaming blobs (no full image on heap).
- [x] ADAU1701 reset + I2C + `default_download_IC_1()` replay.
- [x] Host test for `EmbeddedBlobReader`; manual sync green.
- [ ] Device flash verified (requires ESP-IDF toolchain on build host).
## Slice 5 — ADAU1701 runtime + audio API (complete) ```bash
cmake -S components/core/test -B build-host && cmake --build build-host
ctest --test-dir build-host --output-on-failure
doxygen Doxyfile
python3 tools/check-manual-sync.py
python3 tools/check_si4684_blobs.py
```
Goal: safeload mixer/EQ/master on the ADAU1701 at runtime; persist user First device flash with encryption: `idf.py erase-flash flash` — see
profiles in NVS; expose REST and web UI controls. `docs/security-flash-nvs.md`.
Build on Slice 34:
- Pure core: `GainDb`, `EqProfile`, `AudioProfile`, `IDsp`, biquad design,
`parseAudioProfileJson` / `serializeAudioProfileJson`.
- Driver: `sigma_safeload_*`, extended `Adau1701Driver`, `Adau1701Dsp`.
- Service: `audio::AudioService`, `secure_store::NvsAudioProfileStore`.
- HTTP: `GET/PUT /api/audio/profile`, `POST /api/audio/reset`; Audio
section in the web UI. Firmware **0.5.0**.
Acceptance criteria:
- [x] Safeload volume/mixer/EQ without direct param RAM writes during audio.
- [x] Profile load/apply after ADAU boot; NVS round-trip via JSON.
- [x] Host tests for biquad fixpoint and audio profile JSON.
- [x] Doxygen green; manual sync green; `ch-api.tex` documents audio routes.
- [ ] Device flash verified on hardware.
+36 -26
View File
@@ -1,44 +1,54 @@
# DigiRadio — repository documentation package # DigiRadio — repository documentation package
Copy the contents into your repo keeping this structure. Canonical layout for this repository:
``` ```
DigiRadio/ <- repo root DigiRadio/ <- repo root (README, Hardware, LICENSE)
├── LICENSE CERN-OHL-S v2 (hardware)
├── CONTRIBUTING.md dev conventions (human-facing) ├── CONTRIBUTING.md dev conventions (human-facing)
├── .gitignore ├── README.md project overview (fw 0.8.3)
└── Software/ firmware project root (open THIS in Cursor) └── Software/ firmware project root (open THIS in Cursor)
├── LICENSE Apache-2.0 (firmware) ├── LICENSE Apache-2.0 (firmware)
├── AGENTS.md authoritative coding rules ├── AGENTS.md authoritative coding rules
├── instructions.md agent kickoff briefing (Slice 1) ├── instructions.md agent kickoff + roadmap status
├── sdkconfig.defaults C++23, NVS + flash encryption (dev mode)
├── sdkconfig.defaults.production release-mode overlay (irreversible)
├── partitions.csv nvs + nvs_keys partitions
├── Doxyfile API docs generation + enforcement ├── Doxyfile API docs generation + enforcement
├── apache-header.txt header to paste in each source file ├── apache-header.txt header to paste in each source file
├── .cursor/rules/*.mdc Cursor scoped rules (6 files) ├── .cursor/rules/*.mdc Cursor scoped rules (6 files)
├── tools/ ├── tools/
── check-manual-sync.py enforces "a section per public class" ── check-manual-sync.py one LaTeX section per public class
│ ├── check_si4684_blobs.py no proprietary .bin in git
│ ├── fetch_si4684_firmware.py local blob procurement
│ └── gzip-www.sh regenerate embedded web UI gzip
└── docs/ └── docs/
── manual/ the technical manual (LaTeX) — canonical ── TODO.md agent + HIL backlog
├── manual.tex main file ├── security-flash-nvs.md encryption + device checklist
├── digiradio-manual.sty style (Optima-like, boxes, listings) └── manual/ LaTeX technical manual (canonical)
├── ch-*.tex chapters ├── manual.tex
── manual.pdf compiled preview ── ch-*.tex
└── manual.pdf optional compiled preview
At the **repository root**, `docs/` is a **symbolic link** to
`Software/docs/manual/` (one source of truth; do not duplicate .tex here).
``` ```
## Build the manual LaTeX sources live in `Software/docs/manual/`. Build the PDF from there:
cd docs # symlink → Software/docs/manual
latexmk -lualatex manual.tex # real Optima on macOS
# or: cd Software/docs/manual && latexmk -lualatex manual.tex
## Enforcement in CI (run from Software/) ```bash
doxygen Doxyfile # API docs must pass cd Software/docs/manual && latexmk -lualatex manual.tex
python3 tools/check-manual-sync.py # manual must be in sync ```
## CI enforcement (from `Software/`)
```bash
doxygen Doxyfile
python3 tools/check-manual-sync.py
python3 tools/check_si4684_blobs.py
```
Host tests: `cmake -S components/core/test -B build-host && ctest --test-dir build-host`.
## Notes ## Notes
- Two LICENSE files: CERN-OHL-S at root (hardware), Apache-2.0 in
Software/ (firmware). GitHub auto-detects both. - Two LICENSE files: CERN-OHL-S at repo root (hardware), Apache-2.0 in
- docs/api/ (Doxygen output) is git-ignored; the manual PDF is optional `Software/` (firmware).
to commit (source .tex is the master). - Si4684 `.bin` blobs are gitignored — never commit them.
- Open Software/ as the Cursor project so rules and AGENTS.md load. - Open `Software/` as the Cursor project so rules and `AGENTS.md` load.