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:
+9
-1
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
+29
-25
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user