Document HTTP API and complete manual for slices 1–2

Add ch-api.tex for REST endpoints and boot flow, update ch-firmware,
ch-build, and ch-intro. Extend CONTRIBUTING, instructions, and README
with manual sync checks and API reference.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-06 11:04:22 +02:00
co-authored by Cursor
parent ddbee70c23
commit 425b3f98cf
9 changed files with 238 additions and 45 deletions
+9 -1
View File
@@ -40,12 +40,17 @@ cmake --build build-host
ctest --test-dir build-host --output-on-failure ctest --test-dir build-host --output-on-failure
``` ```
Documentation (must exit 0 with an empty warnings log): Documentation (must exit 0; run from `Software/`):
```bash ```bash
doxygen Doxyfile doxygen Doxyfile
python3 tools/check-manual-sync.py
``` ```
The LaTeX manual (`docs/manual/`) documents design and the HTTP JSON API
(`ch-api.tex`); Doxygen documents C++ signatures. Rebuild the PDF with
`latexmk -lualatex manual.tex` inside `docs/manual/`.
## Coding conventions ## Coding conventions
The guiding idea is *Code That Fits in Your Head*: code must fit in The guiding idea is *Code That Fits in Your Head*: code must fit in
@@ -97,6 +102,9 @@ Before opening a PR, confirm:
- [ ] Every file has the Apache-2.0 header. - [ ] Every file has the Apache-2.0 header.
- [ ] 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.
- [ ] Manual updated: `ch-classes.tex` for new/changed public classes;
`ch-api.tex` for new/changed HTTP endpoints.
- [ ] Every method fits 80x24 and complexity <= 7. - [ ] Every method fits 80x24 and complexity <= 7.
- [ ] Fallible paths return typed results; no silent failure. - [ ] Fallible paths return typed results; no silent failure.
- [ ] Pure-core logic has passing host unit tests. - [ ] Pure-core logic has passing host unit tests.
+16
View File
@@ -31,6 +31,22 @@ doxygen Doxyfile
python3 tools/check-manual-sync.py python3 tools/check-manual-sync.py
``` ```
Manual PDF (design + HTTP API + class reference):
```bash
cd docs/manual && latexmk -lualatex manual.tex
```
## HTTP API (fw 0.2.0)
| Method | Path | Purpose |
|--------|------|---------|
| GET | `/api/health` | `{"status":"ok","fw":"0.2.0"}` |
| POST | `/api/wifi` | Provision STA credentials; reboot on success |
Full schemas, error tokens, and boot flow: [`docs/manual/ch-api.tex`](docs/manual/ch-api.tex).
C++ signatures: generate with `doxygen Doxyfile``docs/api/html/index.html`.
## Layout ## Layout
See [`AGENTS.md`](AGENTS.md) §12 and [`instructions.md`](instructions.md) for See [`AGENTS.md`](AGENTS.md) §12 and [`instructions.md`](instructions.md) for
+125
View File
@@ -0,0 +1,125 @@
\chapter{HTTP API}
\label{ch:api}
The setup web interface is a thin client over a typed JSON REST API
implemented in \texttt{SetupWebServer}. Request bodies are parsed into
domain types in the pure core (\texttt{components/core}) before any
persistence or driver call. Exact C++ signatures live in the generated
Doxygen output under \texttt{docs/api/}; this chapter documents the
wire protocol and behaviour as shipped in firmware~0.2.0 (Slices~1--2).
\section{Transport and reachability}
\begin{itemize}
\item \textbf{Port:} TCP~80 on the ESP32-S3 HTTP server.
\item \textbf{Setup mode (\texttt{NetState::SoftApSetup}):} join the
SoftAP \texttt{DigiRadio-setup} (open network), then open
\url{http://192.168.4.1/}. The root path serves a gzipped HTML
page embedded from flash.
\item \textbf{STA mode (\texttt{NetState::StaConnected}):} after
successful provisioning and reboot, the same API is available at
the device's DHCP address on the configured LAN.
\end{itemize}
All responses use \texttt{Content-Type: application/json} except
\texttt{GET /}, which returns gzipped HTML with
\texttt{Content-Encoding: gzip}.
\section{Endpoints}
\subsection{\texttt{GET /api/health}}
\label{sec:api-health}
Returns a health-check DTO serialised by
\texttt{core::serializeHealthStatusJson()} from a
\texttt{core::HealthStatus} value.
\begin{drnote}[Response schema]
\begin{drcode}[JSON]
{"status":"ok","fw":"0.2.0"}
\end{drcode}
\begin{itemize}
\item \texttt{status} --- coarse indicator; \texttt{ok} when the
firmware is running normally.
\item \texttt{fw} --- firmware release string
(\texttt{core::FirmwareVersion}).
\end{itemize}
\end{drnote}
HTTP status: \textbf{200 OK} on success.
\subsection{\texttt{POST /api/wifi}}
\label{sec:api-wifi}
Accepts Wi-Fi credentials for station (STA) join. The body is parsed by
\texttt{core::parseWifiProvisionJson()} into a \texttt{core::WifiCredentials}
value (SSID as \texttt{core::WifiSsid}, password as \texttt{core::Secret}).
On success the credentials are persisted through
\texttt{core::ISecureStore} (device implementation:
\texttt{secure\_store::NvsSecureStore}) and the device schedules a reboot
so the next boot can enter STA mode.
\begin{drnote}[Request schema]
\begin{drcode}[JSON]
{"ssid":"MyNetwork","password":"secret1234"}
\end{drcode}
\begin{itemize}
\item \texttt{ssid} --- required, 1--32 characters (802.11 SSID limits).
\item \texttt{password} --- optional for open networks; for WPA-PSK,
8--63 characters. Validated by
\texttt{core::WifiCredentials::isPasswordValid()}.
\end{itemize}
\end{drnote}
\begin{drnote}[Success response]
\begin{drcode}[JSON]
{"status":"saved","reboot_in_sec":3}
\end{drcode}
Serialised by \texttt{core::serializeWifiProvisionSavedJson()}. The device
reboots after the indicated delay.
\end{drnote}
\begin{drnote}[Error response]
\begin{drcode}[JSON]
{"status":"error","reason":"invalid_ssid"}
\end{drcode}
Serialised by \texttt{core::serializeWifiProvisionErrorJson()}. Reason
tokens (never include secrets): \texttt{invalid\_json},
\texttt{missing\_field}, \texttt{invalid\_ssid}, \texttt{invalid\_password},
\texttt{store\_failed}.
\end{drnote}
HTTP status: \textbf{200 OK} on save success; \textbf{400 Bad Request} for
parse/validation failures; \textbf{500 Internal Server Error} when NVS
persistence fails.
\section{Boot and network state machine}
\label{sec:api-boot-flow}
At boot, \texttt{net::NetBootstrap::start()} consults
\texttt{ISecureStore::hasWifiCredentials()}:
\begin{enumerate}
\item \textbf{Credentials present} --- create STA netif, connect via
\texttt{net::StaClient} (30\,s timeout). On success:
\texttt{NetState::StaConnected} and HTTP on the LAN address.
\item \textbf{No credentials, or STA join fails} --- fall back to
SoftAP setup mode (\texttt{NetState::SoftApSetup}).
\end{enumerate}
This explicit \texttt{enum class NetState} replaces ad-hoc flags; see
\texttt{net/NetState.hpp} in the source tree.
\section{Credential storage (Slice~2)}
\label{sec:api-storage}
Wi-Fi credentials are stored in NVS namespace \texttt{digiradio}, keys
\texttt{wifi\_ssid} and \texttt{wifi\_pwd}. Passwords are wrapped in
\texttt{core::Secret} in RAM and are never logged or returned by the API.
\begin{drcaution}[Encryption at rest]
Development builds use plain NVS. Production should enable NVS encryption
using the reserved \texttt{nvs\_keys} partition (see
\texttt{partitions.csv} and \texttt{sdkconfig.defaults} comments) per
current ESP-IDF security guidance.
\end{drcaution}
+25 -5
View File
@@ -44,14 +44,34 @@ ctest --test-dir build-host --output-on-failure
\section{Documentation} \section{Documentation}
The API documentation is generated with Doxygen and must build cleanly; Documentation has two enforced checks, run from the \texttt{Software/}
an undocumented class, method, or parameter fails the build. directory:
\begin{drcode}[Docs] \begin{enumerate}
\item \textbf{Doxygen} --- C++ API reference from source doc blocks.
An undocumented public class, method, or parameter fails the build.
\item \textbf{Manual sync} --- every public class under an
\texttt{include/} tree must have a matching
\texttt{\textbackslash label\{cls:ClassName\}} section in
\texttt{docs/manual/ch-classes.tex}. Design-level HTTP API
documentation lives in Chapter~\ref{ch:api}.
\end{enumerate}
\begin{drcode}[Docs (from Software/)]
doxygen Doxyfile doxygen Doxyfile
python3 tools/check-manual-sync.py
\end{drcode}
To rebuild the PDF manual (requires a LaTeX installation):
\begin{drcode}[Manual PDF]
cd docs/manual
latexmk -lualatex manual.tex
\end{drcode} \end{drcode}
\begin{drcaution}[Keep it green] \begin{drcaution}[Keep it green]
The Doxygen build and the manual-synchronisation check are part of the Both checks are part of the definition of done. A firmware change that
definition of done. A change that leaves either failing is not complete. 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
\texttt{ch-api.tex} (for HTTP) in the same change.
\end{drcaution} \end{drcaution}
+2 -1
View File
@@ -5,7 +5,8 @@ This chapter documents the firmware's public classes at the design level:
what each class is responsible for, which collaborators it depends on, its what each class is responsible for, which collaborators it depends on, its
key invariants, and how it fits the system. It complements --- and does key invariants, and how it fits the system. It complements --- and does
not duplicate --- the generated API documentation, which carries the exact not duplicate --- the generated API documentation, which carries the exact
method signatures. method signatures. The HTTP JSON endpoints are documented separately in
Chapter~\ref{ch:api}.
\begin{drnote}[How this chapter grows] \begin{drnote}[How this chapter grows]
Per the development rules, every public, architecturally-significant class Per the development rules, every public, architecturally-significant class
+25 -8
View File
@@ -157,15 +157,32 @@ parsed explicitly, with timeouts treated as errors.
\section{Configuration, storage, and user interface} \section{Configuration, storage, and user interface}
\label{sec:fw-config} \label{sec:fw-config}
Network provisioning uses a SoftAP/captive-portal for first-time setup, Network provisioning is implemented as an explicit state machine
then joins the configured network as a station. Configuration is exposed (\texttt{net::NetState}): on first boot (or when STA join fails) the
through an elegant, minimal web interface served from flash, backed by a device opens a setup SoftAP (\texttt{DigiRadio-setup}) and serves a
typed JSON API; the interface holds no business logic. minimal gzipped web UI from flash. After the user submits credentials via
\texttt{POST /api/wifi}, values are validated in the pure core, persisted
through \texttt{core::ISecureStore}, and the device reboots into STA mode
on the next boot. The HTTP endpoints and JSON schemas are documented in
Chapter~\ref{ch:api}.
Sensitive data --- Wi-Fi credentials, user credentials, and the \paragraph{Implemented (Slices~1--2).}
station/frequency list --- is held in encrypted storage at rest. Secrets \begin{itemize}
are wrapped in a dedicated type that cannot be logged or implicitly \item \texttt{GET /api/health} --- health DTO
converted to a string, and buffers are cleared after use. (\texttt{core::HealthStatus}, serialised in the pure core).
\item \texttt{POST /api/wifi} --- Wi-Fi provisioning
(\texttt{core::WifiCredentials} via \texttt{parseWifiProvisionJson}).
\item \texttt{secure\_store::NvsSecureStore} --- NVS persistence for
SSID and PSK (\texttt{core::Secret}); station list and user
credentials arrive in later slices on the same
\texttt{ISecureStore} interface.
\end{itemize}
Sensitive data --- Wi-Fi credentials today; user credentials and the
station/frequency list in later slices --- uses \texttt{core::Secret} so
values cannot be logged or implicitly converted to a string; buffers are
cleared on destruction. Production builds should enable NVS encryption
at rest (see Chapter~\ref{sec:api-storage}).
\section{Error-handling model} \section{Error-handling model}
\label{sec:fw-errors} \label{sec:fw-errors}
+6 -5
View File
@@ -28,11 +28,12 @@ USB-C, configured through an elegant web interface.
This manual documents the system for someone building, flashing, or This manual documents the system for someone building, flashing, or
extending DigiRadio: the hardware at a block level extending DigiRadio: the hardware at a block level
(Chapter~\ref{ch:hardware}), the firmware architecture (Chapter~\ref{ch:hardware}), the firmware architecture
(Chapter~\ref{ch:firmware}), the per-class reference that grows with the (Chapter~\ref{ch:firmware}), the HTTP JSON API exposed by the web UI
code (Chapter~\ref{ch:classes}), and how to build and flash (Chapter~\ref{ch:api}), the per-class design reference that grows with
(Chapter~\ref{ch:build}). The exact register-level programming of each the code (Chapter~\ref{ch:classes}), and how to build and flash
chip lives in the source code and its generated API documentation; this (Chapter~\ref{ch:build}). Exact C++ signatures are generated by Doxygen
manual explains the design and the reasoning behind it. from the source; this manual explains design, behaviour, and the
reasoning behind it.
\section{Licensing summary} \section{Licensing summary}
+1
View File
@@ -45,6 +45,7 @@
\include{ch-intro} \include{ch-intro}
\include{ch-hardware} \include{ch-hardware}
\include{ch-firmware} \include{ch-firmware}
\include{ch-api}
\include{ch-classes} \include{ch-classes}
\include{ch-build} \include{ch-build}
\include{ch-licensing} \include{ch-licensing}
+29 -25
View File
@@ -44,25 +44,23 @@ Repository: https://github.com/manvalan/DigiRadio
## Roadmap (slices, in order) ## Roadmap (slices, in order)
1. **Walking skeleton**boot, a task, SoftAP, web server, one JSON 1. **Walking skeleton**done (Slice 1).
endpoint, one host test, docs green. No chip drivers yet. (Spec below.) 2. **Secure store + Wi-Fi provisioning** — done (Slice 2).
2. Secure store (`ISecureStore`) + Wi-Fi provisioning UI (STA join).
3. Station/frequency list model + persistence + UI. 3. Station/frequency list model + persistence + UI.
4. Si4684 driver: power-up, load image, tune, read RSQ. 4. Si4684 driver: power-up, load image, tune, read RSQ.
5. ADAU1701 driver: RAM boot, then safeload EQ + input mixer. 5. ADAU1701 driver: RAM boot, then safeload EQ + input mixer.
6. FSC-BT1035 driver: AT init (incl. `AT+AUXCFG=1`), audio out. 6. FSC-BT1035 driver: AT init (incl. `AT+AUXCFG=1`), audio out.
7. Integration: TunerService + AudioService end to end. 7. Integration: TunerService + AudioService end to end.
## Slice 1 — Walking skeleton (the first task) ## Slice 1 — Walking skeleton (complete)
Goal: exercise the whole toolchain end to end with zero chip hardware, Goal: exercise the whole toolchain end to end with zero chip hardware,
so every later slice drops into a working frame. so every later slice drops into a working frame.
Build: Build:
- Top-level ESP-IDF project targeting `esp32s3`. - Top-level ESP-IDF project targeting `esp32s3`.
- `sdkconfig.defaults` sets C++23, exceptions off, and the flash/NVS - `sdkconfig.defaults` sets C++23, exceptions off, and documents flash/NVS
encryption options (leave encryption keys/enablement documented, not encryption options (not hard-enabled until production).
hard-enabled, until we decide on the secure-store slice).
- The `components/core` component compiles both under ESP-IDF and - The `components/core` component compiles both under ESP-IDF and
standalone on the host. standalone on the host.
@@ -71,24 +69,30 @@ Behaviour:
- Bring up SoftAP with a known SSID (e.g. `DigiRadio-setup`). - Bring up SoftAP with a known SSID (e.g. `DigiRadio-setup`).
- Start an HTTP server serving one minimal gzipped page from flash. - Start an HTTP server serving one minimal gzipped page from flash.
- Expose `GET /api/health` returning a typed DTO serialised by the pure - Expose `GET /api/health` returning a typed DTO serialised by the pure
core, e.g. `{"status":"ok","fw":"0.1.0"}`. core, e.g. `{"status":"ok","fw":"0.2.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)
Goal: persist Wi-Fi credentials and join the configured network after
provisioning; fall back to SoftAP when no credentials or join fails.
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: Acceptance criteria:
- [ ] `idf.py build` succeeds; `flash monitor` shows the heartbeat. - [x] Provisioning via SoftAP saves credentials and reboots; next boot joins STA.
- [ ] A phone/laptop can join the SoftAP, load the page, and get a valid - [x] Host tests for health JSON and Wi-Fi provision parse/serialise.
JSON response from `/api/health`. - [x] Doxygen green; manual sync green; `ch-api.tex` documents endpoints.
- [ ] The health DTO is defined and serialised in `components/core`, - [x] No ESP-IDF headers in `components/core`.
with a host unit test that passes under `ctest`.
- [ ] `doxygen Doxyfile` exits 0 with an empty warnings log.
- [ ] Every file has the Apache header; every class/method its doc block.
- [ ] No ESP-IDF headers included from `components/core`.
Out of scope for Slice 1: any Si4684 / ADAU1701 / BT1035 code, real Out of scope: station list, user credentials, NVS encryption enablement
credentials, encryption enablement. Those come in later slices. (production), chip drivers.
## First message to the agent
Ask it to read `AGENTS.md`, `.cursor/rules/`, and this file, then
respond with: (1) the confirmed stack, (2) the exact repo layout it will
create, (3) how it will satisfy each Slice 1 acceptance criterion, and
(4) any blockers or questions — **before** writing code.