From 425b3f98cf3f59168d44c607dcb2eca043e1f77e Mon Sep 17 00:00:00 2001 From: Michele Bigi Date: Mon, 6 Jul 2026 11:04:22 +0200 Subject: [PATCH] =?UTF-8?q?Document=20HTTP=20API=20and=20complete=20manual?= =?UTF-8?q?=20for=20slices=201=E2=80=932?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- CONTRIBUTING.md | 10 ++- Software/README.md | 16 ++++ Software/docs/manual/ch-api.tex | 125 +++++++++++++++++++++++++++ Software/docs/manual/ch-build.tex | 30 +++++-- Software/docs/manual/ch-classes.tex | 3 +- Software/docs/manual/ch-firmware.tex | 33 +++++-- Software/docs/manual/ch-intro.tex | 11 +-- Software/docs/manual/manual.tex | 1 + Software/instructions.md | 54 ++++++------ 9 files changed, 238 insertions(+), 45 deletions(-) create mode 100644 Software/docs/manual/ch-api.tex diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7b7311a..cb8b71f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -40,12 +40,17 @@ cmake --build build-host 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 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 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 class and method has its documentation block. - [ ] `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. - [ ] Fallible paths return typed results; no silent failure. - [ ] Pure-core logic has passing host unit tests. diff --git a/Software/README.md b/Software/README.md index 39458b9..3fb5bdb 100644 --- a/Software/README.md +++ b/Software/README.md @@ -31,6 +31,22 @@ doxygen Doxyfile 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 See [`AGENTS.md`](AGENTS.md) §12 and [`instructions.md`](instructions.md) for diff --git a/Software/docs/manual/ch-api.tex b/Software/docs/manual/ch-api.tex new file mode 100644 index 0000000..1f2eecd --- /dev/null +++ b/Software/docs/manual/ch-api.tex @@ -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} diff --git a/Software/docs/manual/ch-build.tex b/Software/docs/manual/ch-build.tex index 9d30a28..80b54c5 100644 --- a/Software/docs/manual/ch-build.tex +++ b/Software/docs/manual/ch-build.tex @@ -44,14 +44,34 @@ ctest --test-dir build-host --output-on-failure \section{Documentation} -The API documentation is generated with Doxygen and must build cleanly; -an undocumented class, method, or parameter fails the build. +Documentation has two enforced checks, run from the \texttt{Software/} +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 +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} \begin{drcaution}[Keep it green] -The Doxygen build and the manual-synchronisation check are part of the -definition of done. A change that leaves either failing is not complete. +Both 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 +update the Doxygen doc blocks, \texttt{ch-classes.tex} (for classes), and +\texttt{ch-api.tex} (for HTTP) in the same change. \end{drcaution} diff --git a/Software/docs/manual/ch-classes.tex b/Software/docs/manual/ch-classes.tex index 6994723..37ab567 100644 --- a/Software/docs/manual/ch-classes.tex +++ b/Software/docs/manual/ch-classes.tex @@ -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 key invariants, and how it fits the system. It complements --- and does 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] Per the development rules, every public, architecturally-significant class diff --git a/Software/docs/manual/ch-firmware.tex b/Software/docs/manual/ch-firmware.tex index 8562719..df0c4f7 100644 --- a/Software/docs/manual/ch-firmware.tex +++ b/Software/docs/manual/ch-firmware.tex @@ -157,15 +157,32 @@ parsed explicitly, with timeouts treated as errors. \section{Configuration, storage, and user interface} \label{sec:fw-config} -Network provisioning uses a SoftAP/captive-portal for first-time setup, -then joins the configured network as a station. Configuration is exposed -through an elegant, minimal web interface served from flash, backed by a -typed JSON API; the interface holds no business logic. +Network provisioning is implemented as an explicit state machine +(\texttt{net::NetState}): on first boot (or when STA join fails) the +device opens a setup SoftAP (\texttt{DigiRadio-setup}) and serves a +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 -station/frequency list --- is held in encrypted storage at rest. Secrets -are wrapped in a dedicated type that cannot be logged or implicitly -converted to a string, and buffers are cleared after use. +\paragraph{Implemented (Slices~1--2).} +\begin{itemize} + \item \texttt{GET /api/health} --- health DTO + (\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} \label{sec:fw-errors} diff --git a/Software/docs/manual/ch-intro.tex b/Software/docs/manual/ch-intro.tex index 105e283..1f5e57b 100644 --- a/Software/docs/manual/ch-intro.tex +++ b/Software/docs/manual/ch-intro.tex @@ -28,11 +28,12 @@ USB-C, configured through an elegant web interface. This manual documents the system for someone building, flashing, or extending DigiRadio: the hardware at a block level (Chapter~\ref{ch:hardware}), the firmware architecture -(Chapter~\ref{ch:firmware}), the per-class reference that grows with the -code (Chapter~\ref{ch:classes}), and how to build and flash -(Chapter~\ref{ch:build}). The exact register-level programming of each -chip lives in the source code and its generated API documentation; this -manual explains the design and the reasoning behind it. +(Chapter~\ref{ch:firmware}), the HTTP JSON API exposed by the web UI +(Chapter~\ref{ch:api}), the per-class design reference that grows with +the code (Chapter~\ref{ch:classes}), and how to build and flash +(Chapter~\ref{ch:build}). Exact C++ signatures are generated by Doxygen +from the source; this manual explains design, behaviour, and the +reasoning behind it. \section{Licensing summary} diff --git a/Software/docs/manual/manual.tex b/Software/docs/manual/manual.tex index 041c4f2..460080a 100644 --- a/Software/docs/manual/manual.tex +++ b/Software/docs/manual/manual.tex @@ -45,6 +45,7 @@ \include{ch-intro} \include{ch-hardware} \include{ch-firmware} +\include{ch-api} \include{ch-classes} \include{ch-build} \include{ch-licensing} diff --git a/Software/instructions.md b/Software/instructions.md index 69f6f52..4e88282 100644 --- a/Software/instructions.md +++ b/Software/instructions.md @@ -44,25 +44,23 @@ Repository: https://github.com/manvalan/DigiRadio ## Roadmap (slices, in order) -1. **Walking skeleton** — boot, a task, SoftAP, web server, one JSON - endpoint, one host test, docs green. No chip drivers yet. (Spec below.) -2. Secure store (`ISecureStore`) + Wi-Fi provisioning UI (STA join). +1. **Walking skeleton** — done (Slice 1). +2. **Secure store + Wi-Fi provisioning** — done (Slice 2). 3. Station/frequency list model + persistence + UI. 4. Si4684 driver: power-up, load image, tune, read RSQ. 5. ADAU1701 driver: RAM boot, then safeload EQ + input mixer. 6. FSC-BT1035 driver: AT init (incl. `AT+AUXCFG=1`), audio out. 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, so every later slice drops into a working frame. Build: - Top-level ESP-IDF project targeting `esp32s3`. -- `sdkconfig.defaults` sets C++23, exceptions off, and the flash/NVS - encryption options (leave encryption keys/enablement documented, not - hard-enabled, until we decide on the secure-store slice). +- `sdkconfig.defaults` sets C++23, exceptions off, and documents flash/NVS + encryption options (not hard-enabled until production). - The `components/core` component compiles both under ESP-IDF and standalone on the host. @@ -71,24 +69,30 @@ Behaviour: - 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.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: -- [ ] `idf.py build` succeeds; `flash monitor` shows the heartbeat. -- [ ] A phone/laptop can join the SoftAP, load the page, and get a valid - JSON response from `/api/health`. -- [ ] The health DTO is defined and serialised 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`. +- [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 1: any Si4684 / ADAU1701 / BT1035 code, real -credentials, encryption enablement. Those come in later slices. - -## 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. +Out of scope: station list, user credentials, NVS encryption enablement +(production), chip drivers.