pre-agent-version
This commit is contained in:
+111
@@ -0,0 +1,111 @@
|
|||||||
|
# Contributing to DigiRadio
|
||||||
|
|
||||||
|
Thanks for your interest in DigiRadio. This document covers the firmware
|
||||||
|
development conventions. The full, authoritative rules live in
|
||||||
|
[`Software/AGENTS.md`](Software/AGENTS.md); this is the human-facing
|
||||||
|
summary.
|
||||||
|
|
||||||
|
DigiRadio is dual-licensed: hardware under **CERN-OHL-S v2**, firmware
|
||||||
|
under **Apache-2.0**. By contributing, you agree your contributions are
|
||||||
|
licensed under the same terms as the part of the project they touch.
|
||||||
|
|
||||||
|
## Toolchain
|
||||||
|
|
||||||
|
| Item | Choice |
|
||||||
|
|-----------|------------------------------------------|
|
||||||
|
| Framework | ESP-IDF v5.5.x (native, not Arduino) |
|
||||||
|
| Language | C++23, pinned `-std=gnu++23` |
|
||||||
|
| Errors | `std::expected<T, Error>`; exceptions off|
|
||||||
|
| Docs | Doxygen (build must pass, see below) |
|
||||||
|
|
||||||
|
On macOS, the host unit tests need a C++23 standard library: use
|
||||||
|
Homebrew `llvm` (>= 18) or `gcc-14`, not the system Apple Clang.
|
||||||
|
|
||||||
|
## Build, test, docs
|
||||||
|
|
||||||
|
Device build / flash / monitor:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
idf.py set-target esp32s3
|
||||||
|
idf.py build
|
||||||
|
idf.py -p <port> flash monitor
|
||||||
|
```
|
||||||
|
|
||||||
|
Host unit tests (pure core, no hardware):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cmake -S components/core/test -B build-host \
|
||||||
|
-DCMAKE_CXX_COMPILER="$(brew --prefix llvm)/bin/clang++"
|
||||||
|
cmake --build build-host
|
||||||
|
ctest --test-dir build-host --output-on-failure
|
||||||
|
```
|
||||||
|
|
||||||
|
Documentation (must exit 0 with an empty warnings log):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
doxygen Doxyfile
|
||||||
|
```
|
||||||
|
|
||||||
|
## Coding conventions
|
||||||
|
|
||||||
|
The guiding idea is *Code That Fits in Your Head*: code must fit in
|
||||||
|
human working memory at every zoom level.
|
||||||
|
|
||||||
|
- **Complexity `<= 7`** per method; **methods fit an 80x24 box** (<= 80
|
||||||
|
columns, <= 24 lines). One method does one thing.
|
||||||
|
- **Strong typing.** No primitive obsession: domain quantities are their
|
||||||
|
own types. `enum class` always; no booleans for mode selection.
|
||||||
|
Validate untrusted input once at the boundary.
|
||||||
|
- **RAII and `const` by default.** Wrap every hardware/OS handle; no raw
|
||||||
|
`new`/`delete`; borrow with `std::span`, not pointer + length.
|
||||||
|
- **Functional core, imperative shell.** Pure logic in
|
||||||
|
`components/core` (no ESP-IDF headers, host-tested); hardware access in
|
||||||
|
the shell.
|
||||||
|
- **No silent failure.** Fallible operations return `std::expected`;
|
||||||
|
every timeout is an explicit error.
|
||||||
|
- **Embedded discipline.** No dynamic allocation in audio or ISR paths;
|
||||||
|
no virtual calls in IRAM-safe ISRs; every wait has a timeout.
|
||||||
|
- **Never invent** a register address, opcode, or boot sequence — cite
|
||||||
|
the datasheet section in a comment, or stop and ask.
|
||||||
|
|
||||||
|
## File headers and documentation
|
||||||
|
|
||||||
|
Every source file starts with the Apache-2.0 header (see
|
||||||
|
[`Software/apache-header.txt`](Software/apache-header.txt)), with
|
||||||
|
`@file`, `@author`, and `@date` filled in.
|
||||||
|
|
||||||
|
Every class and method carries a Doxygen block with, in order: name
|
||||||
|
(`@dname`), parameters (`@param`), return (`@return`), public state used
|
||||||
|
(`@pubstate`), a description of intent (not a restatement of the code),
|
||||||
|
and `@author` / `@date`. The `doxygen Doxyfile` build enforces this —
|
||||||
|
an undocumented symbol fails the build.
|
||||||
|
|
||||||
|
## Commits and pull requests
|
||||||
|
|
||||||
|
- **Small, focused commits.** Each commit compiles and keeps host tests
|
||||||
|
green. One logical change per commit.
|
||||||
|
- **Commit messages: 50/72.** Summary line <= 50 characters, imperative
|
||||||
|
mood; blank line; body wrapped at 72 explaining *why*.
|
||||||
|
- Keep `main` always building. Use feature branches for work in
|
||||||
|
progress; no commented-out code in commits.
|
||||||
|
|
||||||
|
## Definition of Done
|
||||||
|
|
||||||
|
Before opening a PR, confirm:
|
||||||
|
|
||||||
|
- [ ] Compiles with warnings-as-errors; clang-tidy clean.
|
||||||
|
- [ ] 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.
|
||||||
|
- [ ] Every method fits 80x24 and complexity <= 7.
|
||||||
|
- [ ] Fallible paths return typed results; no silent failure.
|
||||||
|
- [ ] Pure-core logic has passing host unit tests.
|
||||||
|
- [ ] No secret is loggable or stored in plaintext.
|
||||||
|
- [ ] Register-level decisions cite the datasheet section.
|
||||||
|
- [ ] No dynamic allocation in audio/ISR paths.
|
||||||
|
|
||||||
|
## Editor setup (optional)
|
||||||
|
|
||||||
|
The repository ships Cursor rules under `Software/.cursor/rules/`. If you
|
||||||
|
use Cursor, open the `Software/` directory as the project so the rules
|
||||||
|
and `AGENTS.md` are picked up automatically.
|
||||||
@@ -259,6 +259,40 @@ version rather than assuming.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
### 3.4 Manual synchronisation
|
||||||
|
|
||||||
|
The LaTeX manual documents the firmware at the **design level** and must
|
||||||
|
never fall behind the code. The split of duties is strict:
|
||||||
|
|
||||||
|
- **Doxygen** documents the API — exact signatures, parameters, returns,
|
||||||
|
per §3.2. Generated from the code.
|
||||||
|
- **The manual** documents design — what a component is for, its
|
||||||
|
responsibility, its collaborators and invariants, and how it fits the
|
||||||
|
system. Written prose plus diagrams. It does **not** repeat the
|
||||||
|
per-method API.
|
||||||
|
|
||||||
|
Rule: **every public, architecturally-significant class has a matching
|
||||||
|
manual section.** These are the classes with a public interface —
|
||||||
|
drivers, application services, and public domain-core types (the ones
|
||||||
|
declared under an `include/` directory). Each gets a `\subsection` (or
|
||||||
|
`\subsubsection`) in the manual, tagged with a stable label
|
||||||
|
`\label{cls:ClassName}`, describing its responsibility, collaborators,
|
||||||
|
key invariants, and how it is used.
|
||||||
|
|
||||||
|
- When a public class is **added**, its manual section is written in the
|
||||||
|
same change.
|
||||||
|
- When its **public interface changes**, the manual section is updated in
|
||||||
|
the same change.
|
||||||
|
- When it is **removed**, its manual section is removed.
|
||||||
|
- Internal / private helper classes (declared only in `src/`, not
|
||||||
|
exposed through `include/`) do **not** require a manual section.
|
||||||
|
|
||||||
|
This is enforceable, not aspirational: `tools/check-manual-sync.py`
|
||||||
|
lists the public classes and fails if any lacks a `\label{cls:...}` in
|
||||||
|
the manual sources. Wire it into CI alongside the Doxygen check.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 4. Architecture
|
## 4. Architecture
|
||||||
|
|
||||||
Layered, dependencies point inward only:
|
Layered, dependencies point inward only:
|
||||||
@@ -444,6 +478,8 @@ Before a slice is considered complete:
|
|||||||
- [ ] Every file has the Apache-2.0 header block (§3.1).
|
- [ ] Every file has the Apache-2.0 header block (§3.1).
|
||||||
- [ ] Every class and method has its documentation block (§3.2).
|
- [ ] Every class and method has its documentation block (§3.2).
|
||||||
- [ ] `doxygen Doxyfile` exits 0 with an empty warnings log (§3.3).
|
- [ ] `doxygen Doxyfile` exits 0 with an empty warnings log (§3.3).
|
||||||
|
- [ ] Manual section exists/updated for any added or changed public
|
||||||
|
class; `tools/check-manual-sync.py` passes (§3.4).
|
||||||
- [ ] Every method <= 80x24, complexity <= 7.
|
- [ ] Every method <= 80x24, complexity <= 7.
|
||||||
- [ ] No primitive obsession in public interfaces.
|
- [ ] No primitive obsession in public interfaces.
|
||||||
- [ ] Fallible paths return typed results; no silent failure.
|
- [ ] Fallible paths return typed results; no silent failure.
|
||||||
@@ -459,6 +495,7 @@ Before a slice is considered complete:
|
|||||||
|
|
||||||
- Ship a file without the Apache-2.0 licence header.
|
- Ship a file without the Apache-2.0 licence header.
|
||||||
- Ship a class or method without its documentation block.
|
- Ship a class or method without its documentation block.
|
||||||
|
- Add or change a public class without updating its manual section.
|
||||||
- Invent register addresses, opcodes, bit fields, or boot sequences.
|
- Invent register addresses, opcodes, bit fields, or boot sequences.
|
||||||
- Put business logic in a driver or in an ISR.
|
- Put business logic in a driver or in an ISR.
|
||||||
- Return a bare error code or swallow a failure.
|
- Return a bare error code or swallow a failure.
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
\chapter{Building and Flashing}
|
||||||
|
\label{ch:build}
|
||||||
|
|
||||||
|
The firmware is built with ESP-IDF; the hardware-free domain core also
|
||||||
|
builds and is unit-tested on the host machine.
|
||||||
|
|
||||||
|
\section{Toolchain}
|
||||||
|
|
||||||
|
\begin{table}[htbp]
|
||||||
|
\centering
|
||||||
|
\begin{tabular}{@{}ll@{}}
|
||||||
|
\drhead Item & Choice \\
|
||||||
|
\midrule
|
||||||
|
Framework & ESP-IDF v5.5.x (native) \\
|
||||||
|
Language & C++23 (\texttt{-std=gnu++23}) \\
|
||||||
|
Error model & \texttt{std::expected}; exceptions off \\
|
||||||
|
Documentation & Doxygen (build must pass) \\
|
||||||
|
\bottomrule
|
||||||
|
\end{tabular}
|
||||||
|
\caption{Firmware toolchain.}
|
||||||
|
\label{tab:build-toolchain}
|
||||||
|
\end{table}
|
||||||
|
|
||||||
|
\section{Device build}
|
||||||
|
|
||||||
|
\begin{drcode}[Build, flash, monitor]
|
||||||
|
idf.py set-target esp32s3
|
||||||
|
idf.py build
|
||||||
|
idf.py -p <port> flash monitor
|
||||||
|
\end{drcode}
|
||||||
|
|
||||||
|
\section{Host unit tests}
|
||||||
|
|
||||||
|
The pure core is tested on the host, with no board attached. On macOS the
|
||||||
|
tests need a C++23 standard library --- use a Homebrew LLVM (>= 18) or
|
||||||
|
GCC~14 rather than the system Apple Clang.
|
||||||
|
|
||||||
|
\begin{drcode}[Host tests]
|
||||||
|
cmake -S components/core/test -B build-host \
|
||||||
|
-DCMAKE_CXX_COMPILER="$(brew --prefix llvm)/bin/clang++"
|
||||||
|
cmake --build build-host
|
||||||
|
ctest --test-dir build-host --output-on-failure
|
||||||
|
\end{drcode}
|
||||||
|
|
||||||
|
\section{Documentation}
|
||||||
|
|
||||||
|
The API documentation is generated with Doxygen and must build cleanly;
|
||||||
|
an undocumented class, method, or parameter fails the build.
|
||||||
|
|
||||||
|
\begin{drcode}[Docs]
|
||||||
|
doxygen Doxyfile
|
||||||
|
\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.
|
||||||
|
\end{drcaution}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
\chapter{Class Reference}
|
||||||
|
\label{ch:classes}
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
\begin{drnote}[How this chapter grows]
|
||||||
|
Per the development rules, every public, architecturally-significant class
|
||||||
|
(the drivers, the application services, and the public domain-core types)
|
||||||
|
gets a section here, tagged \texttt{\textbackslash label\{cls:ClassName\}},
|
||||||
|
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.
|
||||||
|
\end{drnote}
|
||||||
|
|
||||||
|
The firmware is under active development. As each public class lands, its
|
||||||
|
section appears below, grouped by layer: domain core, application
|
||||||
|
services, and hardware drivers.
|
||||||
|
|
||||||
|
% ------------------------------------------------------------------
|
||||||
|
% Per-class sections are added here as classes are implemented, e.g.:
|
||||||
|
%
|
||||||
|
% \section{Si4684Driver}\label{cls:Si4684Driver}
|
||||||
|
% Responsibility, collaborators, invariants, usage.
|
||||||
|
%
|
||||||
|
% \section{Adau1701Driver}\label{cls:Adau1701Driver}
|
||||||
|
% \section{Bt1035Driver}\label{cls:Bt1035Driver}
|
||||||
|
% \section{TunerService}\label{cls:TunerService}
|
||||||
|
% ...
|
||||||
|
% ------------------------------------------------------------------
|
||||||
|
|
||||||
|
\section*{(No public classes documented yet)}
|
||||||
|
Sections will appear here as the firmware is implemented.
|
||||||
@@ -0,0 +1,202 @@
|
|||||||
|
% ============================================================
|
||||||
|
% DigiRadio — Manual section: Firmware Architecture
|
||||||
|
% Drop this into the manual as a \section (or \chapter).
|
||||||
|
%
|
||||||
|
% Required packages (add to the preamble if not present):
|
||||||
|
% \usepackage{tikz}
|
||||||
|
% \usetikzlibrary{positioning, arrows.meta, fit, backgrounds}
|
||||||
|
% \usepackage{booktabs} % for the interface table
|
||||||
|
%
|
||||||
|
% The environments used here are standard. Replace itemize / framed
|
||||||
|
% blocks with your own box taxonomy where you prefer.
|
||||||
|
% ============================================================
|
||||||
|
|
||||||
|
\chapter{Firmware Architecture}
|
||||||
|
\label{sec:firmware-architecture}
|
||||||
|
|
||||||
|
The DigiRadio firmware runs on the ESP32-S3 and orchestrates three
|
||||||
|
companion chips into a single high-fidelity audio path: the Si4684
|
||||||
|
DAB+/FM tuner, the ADAU1701 SigmaDSP, and the FSC-BT1035 Bluetooth
|
||||||
|
transmitter. This section describes how the firmware is organised, how
|
||||||
|
audio flows through the system, and how each chip is controlled. The
|
||||||
|
day-to-day coding conventions (style, documentation, review rules) are
|
||||||
|
intentionally kept out of this manual and live in the repository's
|
||||||
|
\texttt{CONTRIBUTING.md} and \texttt{AGENTS.md}.
|
||||||
|
|
||||||
|
\section{Design principles}
|
||||||
|
\label{sec:fw-principles}
|
||||||
|
|
||||||
|
Two ideas shape the whole codebase:
|
||||||
|
|
||||||
|
\begin{itemize}
|
||||||
|
\item \textbf{Strong typing.} Domain quantities (frequency, gain,
|
||||||
|
station identifier, band) are dedicated types rather than raw
|
||||||
|
integers or floats. Invalid states are made unrepresentable: input
|
||||||
|
from the network, UART, or flash is validated once at the boundary
|
||||||
|
into a domain type, and trusted thereafter.
|
||||||
|
\item \textbf{Functional core, imperative shell.} All hardware-free
|
||||||
|
logic --- coefficient math, boot-image framing, configuration
|
||||||
|
parsing, station-list operations --- lives in a pure \emph{core}
|
||||||
|
that compiles and is unit-tested on the host, with no dependency on
|
||||||
|
ESP-IDF. Every access to the hardware (I\textsuperscript{2}C, SPI,
|
||||||
|
UART, flash) lives in a thin \emph{shell} around that core. This
|
||||||
|
keeps the logic testable without a board attached.
|
||||||
|
\end{itemize}
|
||||||
|
|
||||||
|
\section{Layered structure}
|
||||||
|
\label{sec:fw-layers}
|
||||||
|
|
||||||
|
The firmware is organised in three layers, with dependencies pointing
|
||||||
|
inward only (Figure~\ref{fig:fw-layers}). The outer shell may depend on
|
||||||
|
the services and the core; the pure core depends on nothing outside
|
||||||
|
itself.
|
||||||
|
|
||||||
|
\begin{figure}[htbp]
|
||||||
|
\centering
|
||||||
|
\begin{tikzpicture}[
|
||||||
|
layer/.style={draw, rounded corners, minimum width=10cm,
|
||||||
|
minimum height=1.5cm, align=center, font=\small},
|
||||||
|
node distance=4mm]
|
||||||
|
\node[layer, fill=black!5] (shell) {%
|
||||||
|
\textbf{Imperative shell}\\[2pt]
|
||||||
|
chip drivers \textbullet\ web server \textbullet\ NVS \textbullet\
|
||||||
|
FreeRTOS tasks \textbullet\ ISRs};
|
||||||
|
\node[layer, fill=black!8, below=of shell] (services) {%
|
||||||
|
\textbf{Application services}\\[2pt]
|
||||||
|
TunerService \textbullet\ AudioService \textbullet\
|
||||||
|
ConfigService \textbullet\ NetworkService};
|
||||||
|
\node[layer, fill=black!12, below=of services] (core) {%
|
||||||
|
\textbf{Domain core (pure, host-tested)}\\[2pt]
|
||||||
|
Station \textbullet\ Frequency \textbullet\ EqProfile \textbullet\
|
||||||
|
MixerState \textbullet\ Credential \textbullet\ validation};
|
||||||
|
\draw[-{Latex}] (shell) -- (services);
|
||||||
|
\draw[-{Latex}] (services) -- (core);
|
||||||
|
\end{tikzpicture}
|
||||||
|
\caption{Firmware layering. Dependencies point inward; the pure core
|
||||||
|
has no hardware dependencies.}
|
||||||
|
\label{fig:fw-layers}
|
||||||
|
\end{figure}
|
||||||
|
|
||||||
|
Application services take driver \emph{interfaces}
|
||||||
|
(\texttt{ITuner}, \texttt{IDsp}, \texttt{IBtModule},
|
||||||
|
\texttt{ISecureStore}) injected at construction. Because the services
|
||||||
|
depend on abstractions rather than concrete hardware, the same logic can
|
||||||
|
be exercised against fakes in host tests and against real silicon on the
|
||||||
|
device.
|
||||||
|
|
||||||
|
\section{Audio signal path}
|
||||||
|
\label{sec:fw-audio}
|
||||||
|
|
||||||
|
The audio chain is built around sound quality. The Si4684 produces the
|
||||||
|
DAB+/FM audio stream; the ADAU1701 applies equalisation and mixes it
|
||||||
|
with the ESP32 audio source; the FSC-BT1035 streams the result over
|
||||||
|
Bluetooth with aptX Adaptive (Figure~\ref{fig:fw-audio}).
|
||||||
|
|
||||||
|
\begin{figure}[htbp]
|
||||||
|
\centering
|
||||||
|
\begin{tikzpicture}[
|
||||||
|
block/.style={draw, rounded corners, minimum height=1cm,
|
||||||
|
minimum width=2.4cm, align=center, font=\small},
|
||||||
|
>={Latex}, node distance=8mm]
|
||||||
|
\node[block] (si) {Si4684\\\scriptsize DAB+/FM};
|
||||||
|
\node[block, right=16mm of si] (dsp) {ADAU1701\\\scriptsize EQ + mixer};
|
||||||
|
\node[block, right=16mm of dsp] (bt) {FSC-BT1035\\\scriptsize aptX Adaptive};
|
||||||
|
\node[block, below=12mm of dsp] (esp) {ESP32-S3\\\scriptsize audio src};
|
||||||
|
\draw[->] (si) -- (dsp) node[midway, above, font=\scriptsize]{audio};
|
||||||
|
\draw[->] (dsp) -- (bt) node[midway, above, font=\scriptsize]{out};
|
||||||
|
\draw[->] (esp) -- (dsp) node[midway, right, font=\scriptsize]{mix in};
|
||||||
|
\end{tikzpicture}
|
||||||
|
\caption{Audio signal path. The ADAU1701 input mixer selects/blends
|
||||||
|
the Si4684 and ESP32 sources; equalisation is applied before the
|
||||||
|
Bluetooth output stage.}
|
||||||
|
\label{fig:fw-audio}
|
||||||
|
\end{figure}
|
||||||
|
|
||||||
|
Equalisation and input mixing are exposed to the rest of the firmware as
|
||||||
|
typed operations --- setting an EQ band from a gain, centre frequency,
|
||||||
|
and Q, or setting the mix level of a given source --- never as raw DSP
|
||||||
|
cell addresses. The biquad coefficient math runs in the pure core and is
|
||||||
|
verified against reference values in host tests.
|
||||||
|
|
||||||
|
\section{Chip control and boot}
|
||||||
|
\label{sec:fw-chips}
|
||||||
|
|
||||||
|
\paragraph{Si4684 (tuner).}
|
||||||
|
At power-up the driver follows the documented boot sequence: power on,
|
||||||
|
load the patch/bootloader, load the firmware image (FM or DAB), then
|
||||||
|
boot. Firmware images are large and are streamed to the chip in bounded
|
||||||
|
chunks from flash rather than buffered whole in RAM.
|
||||||
|
|
||||||
|
\begin{drref}[Datasheet]
|
||||||
|
The exact power-up, patch, image-load, and boot command sequence follows
|
||||||
|
the Si468x programming guide (AN649). Each step in the driver cites the
|
||||||
|
relevant section; no command byte is issued without a documented source.
|
||||||
|
\end{drref} The public
|
||||||
|
interface is intent-level (\texttt{powerUp}, \texttt{loadImage},
|
||||||
|
\texttt{tuneTo}, \texttt{readRsq}); register-level detail is private to
|
||||||
|
the driver.
|
||||||
|
|
||||||
|
\paragraph{ADAU1701 (DSP).}
|
||||||
|
The board carries no self-boot EEPROM. Instead, the ESP32 writes the
|
||||||
|
SigmaStudio-exported program into the DSP's RAM at \emph{every} boot.
|
||||||
|
Runtime changes to equalisation and mixing use the ADAU1701 safeload
|
||||||
|
mechanism, so parameter updates are click-free while audio is playing.
|
||||||
|
|
||||||
|
\begin{drcaution}[Safeload]
|
||||||
|
Writing DSP parameter cells directly while audio is running produces
|
||||||
|
audible clicks. All runtime EQ and mixer changes must go through the
|
||||||
|
safeload registers.
|
||||||
|
\end{drcaution}
|
||||||
|
|
||||||
|
\paragraph{FSC-BT1035 (Bluetooth).}
|
||||||
|
The module is controlled by AT commands over UART. The initialisation
|
||||||
|
sequence includes enabling Line-In mode (\texttt{AT+AUXCFG=1}), which is
|
||||||
|
required for the wired audio path from the DSP; command responses are
|
||||||
|
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.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
\section{Error-handling model}
|
||||||
|
\label{sec:fw-errors}
|
||||||
|
|
||||||
|
Every operation that can fail returns a typed result
|
||||||
|
(\texttt{std::expected<T, Error>}); nothing fails silently and every
|
||||||
|
timeout is an explicit error value. Errors propagate to the layer that
|
||||||
|
can act on them --- a driver reports, a service decides (retry, degrade,
|
||||||
|
or surface to the UI), and the top level logs. C++ exceptions are
|
||||||
|
disabled.
|
||||||
|
|
||||||
|
\section{Toolchain}
|
||||||
|
\label{sec:fw-toolchain}
|
||||||
|
|
||||||
|
\begin{table}[htbp]
|
||||||
|
\centering
|
||||||
|
\begin{tabular}{@{}ll@{}}
|
||||||
|
\toprule
|
||||||
|
\textbf{Item} & \textbf{Choice} \\
|
||||||
|
\midrule
|
||||||
|
Framework & ESP-IDF v5.5.x (native) \\
|
||||||
|
Language & C++23 (\texttt{-std=gnu++23}) \\
|
||||||
|
Error model & \texttt{std::expected}; exceptions off \\
|
||||||
|
Hardware licence & CERN-OHL-S v2 \\
|
||||||
|
Firmware licence & Apache-2.0 \\
|
||||||
|
\bottomrule
|
||||||
|
\end{tabular}
|
||||||
|
\caption{Firmware toolchain and licensing summary.}
|
||||||
|
\label{tab:fw-toolchain}
|
||||||
|
\end{table}
|
||||||
|
|
||||||
|
The firmware source, build instructions, and development conventions are
|
||||||
|
maintained in the \texttt{Software/} directory of the project
|
||||||
|
repository.
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
\chapter{Hardware Overview}
|
||||||
|
\label{ch:hardware}
|
||||||
|
|
||||||
|
This chapter describes the board at a block level. The complete hardware
|
||||||
|
design --- schematics, PCB, Gerbers, and bill of materials --- is
|
||||||
|
published in the repository under the CERN-OHL-S v2 licence.
|
||||||
|
|
||||||
|
\section{System block diagram}
|
||||||
|
|
||||||
|
DigiRadio is built around four devices coordinated by the ESP32-S3. The
|
||||||
|
tuner produces the audio stream, the DSP conditions and mixes it, and the
|
||||||
|
Bluetooth module transmits it; the host manages all three and provides
|
||||||
|
the network interface.
|
||||||
|
|
||||||
|
\begin{table}[htbp]
|
||||||
|
\centering
|
||||||
|
\begin{tabular}{@{}lll@{}}
|
||||||
|
\drhead Device & Role & Interface \\
|
||||||
|
\midrule
|
||||||
|
Si4684 & DAB+/FM tuner & SPI / I\textsuperscript{2}C \\
|
||||||
|
ADAU1701 & Audio DSP (EQ + mixer) & I\textsuperscript{2}C \\
|
||||||
|
ESP32-S3-WROOM-1 & Host controller, Wi-Fi/BLE & --- \\
|
||||||
|
FSC-BT1035 & Bluetooth 5.2 (aptX Adaptive) & UART (AT) \\
|
||||||
|
\bottomrule
|
||||||
|
\end{tabular}
|
||||||
|
\caption{Principal integrated circuits and their roles.}
|
||||||
|
\label{tab:hw-ics}
|
||||||
|
\end{table}
|
||||||
|
|
||||||
|
\section{Power}
|
||||||
|
|
||||||
|
The board is powered from USB-C. An AP63203 buck converter generates the
|
||||||
|
main rails from the USB input, feeding the digital and analogue sections.
|
||||||
|
The tuner's sensitive supplies are derived and filtered separately to
|
||||||
|
keep RF performance clean.
|
||||||
|
|
||||||
|
\section{RF and antennas}
|
||||||
|
|
||||||
|
Two 2.4\,GHz radios are present --- the ESP32-S3 and the Bluetooth module.
|
||||||
|
Their antennas are placed on opposite diagonal corners of the board to
|
||||||
|
maximise separation and minimise mutual desense. Each module keeps its
|
||||||
|
antenna keep-out clear of copper on all layers.
|
||||||
|
|
||||||
|
\begin{drnote}[Board]
|
||||||
|
The PCB is a six-layer stack-up with controlled impedance on the USB and
|
||||||
|
RF sections. The full fabrication data is in the repository.
|
||||||
|
\end{drnote}
|
||||||
|
|
||||||
|
\section{User interface and connectors}
|
||||||
|
|
||||||
|
Configuration is done over Wi-Fi through a built-in web interface (see
|
||||||
|
Chapter~\ref{ch:firmware}); no display is required on the board itself.
|
||||||
|
Audio reaches the listener wirelessly over Bluetooth.
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
\chapter{Introduction}
|
||||||
|
\label{ch:introduction}
|
||||||
|
|
||||||
|
DigiRadio is an open-source, high-fidelity digital radio receiver. It
|
||||||
|
receives DAB+ and FM broadcasts, processes the audio through a dedicated
|
||||||
|
signal processor, and streams the result over Bluetooth using a
|
||||||
|
high-resolution codec. The whole project --- hardware and firmware --- is
|
||||||
|
released as open source for the maker and audio community to study,
|
||||||
|
build, and improve.
|
||||||
|
|
||||||
|
\section{What DigiRadio is}
|
||||||
|
|
||||||
|
Most DAB+/FM hobby projects stop at basic reception. DigiRadio adds two
|
||||||
|
things that lift it into hi-fi territory: a real audio DSP stage for
|
||||||
|
equalisation and mixing, and a premium-codec Bluetooth output. The result
|
||||||
|
is a genuine wireless audio source rather than a bench demo, and, being
|
||||||
|
fully documented, a practical reference for anyone learning multi-chip
|
||||||
|
audio design or RF-aware PCB layout.
|
||||||
|
|
||||||
|
\begin{drkey}[At a glance]
|
||||||
|
A four-chip design --- Si4684 tuner, ADAU1701 DSP, ESP32-S3 host, and an
|
||||||
|
FSC-BT1035 Bluetooth transmitter --- on a six-layer board, powered from
|
||||||
|
USB-C, configured through an elegant web interface.
|
||||||
|
\end{drkey}
|
||||||
|
|
||||||
|
\section{Who this manual is for}
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
\section{Licensing summary}
|
||||||
|
|
||||||
|
The hardware is licensed under CERN-OHL-S v2 and the firmware under
|
||||||
|
Apache-2.0; full details are in Chapter~\ref{ch:licensing}. The project
|
||||||
|
repository is at \url{https://github.com/manvalan/DigiRadio}.
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
\chapter{Licensing}
|
||||||
|
\label{ch:licensing}
|
||||||
|
|
||||||
|
DigiRadio is dual-licensed, with hardware and firmware under separate but
|
||||||
|
compatible open-source licences.
|
||||||
|
|
||||||
|
\section{Hardware}
|
||||||
|
|
||||||
|
The hardware --- schematics, PCB layout, Gerbers, and bill of materials
|
||||||
|
--- is licensed under the \textbf{CERN Open Hardware Licence Version~2,
|
||||||
|
Strongly Reciprocal (CERN-OHL-S v2)}. Anyone who modifies and distributes
|
||||||
|
the hardware must release their changes under the same terms.
|
||||||
|
|
||||||
|
\section{Firmware}
|
||||||
|
|
||||||
|
The firmware is licensed under the \textbf{Apache License 2.0}. It grants
|
||||||
|
patent rights explicitly and pairs naturally with the ESP-IDF ecosystem,
|
||||||
|
which is Apache-licensed as well. Every source file carries the Apache
|
||||||
|
header with an SPDX identifier.
|
||||||
|
|
||||||
|
\section{Documentation and project pages}
|
||||||
|
|
||||||
|
Where a hosting platform does not offer the CERN-OHL-S licence in its
|
||||||
|
selector, the closest share-alike option is used for that page, with the
|
||||||
|
authoritative licence stated as CERN-OHL-S v2. The definitive licensing
|
||||||
|
is always the one declared in the project repository.
|
||||||
|
|
||||||
|
\section{Third-party components}
|
||||||
|
|
||||||
|
The design integrates third-party silicon (Silicon Labs, Analog Devices,
|
||||||
|
Espressif, Feasycom/Qualcomm) whose datasheets, firmware images, and DSP
|
||||||
|
tools remain subject to their respective vendors' terms. Those terms are
|
||||||
|
unaffected by the DigiRadio licences and must be observed when
|
||||||
|
redistributing vendor-supplied binaries.
|
||||||
|
|
||||||
|
\begin{drnote}[Manufacturing partner]
|
||||||
|
Prototype fabrication and assembly are supported by PCBWay. Their
|
||||||
|
contribution is acknowledged in the project documentation and on the
|
||||||
|
board.
|
||||||
|
\end{drnote}
|
||||||
@@ -0,0 +1,169 @@
|
|||||||
|
% ============================================================
|
||||||
|
% digiradio-manual.sty — visual style for the DigiRadio manual
|
||||||
|
% Optima-like humanist sans, 12pt, 1.3 leading, restrained colour,
|
||||||
|
% and clear at-a-glance tcolorbox callouts.
|
||||||
|
%
|
||||||
|
% Use with a 12pt KOMA class, e.g.:
|
||||||
|
% \documentclass[12pt,a4paper]{scrartcl}
|
||||||
|
% \usepackage{digiradio-manual}
|
||||||
|
%
|
||||||
|
% FONT: compile with LuaLaTeX or XeLaTeX to get the real Optima
|
||||||
|
% (a system font on macOS). With pdfLaTeX it falls back to Linux
|
||||||
|
% Biolinum, a humanist sans in the same spirit. The package detects
|
||||||
|
% the engine automatically.
|
||||||
|
% ============================================================
|
||||||
|
\NeedsTeXFormat{LaTeX2e}
|
||||||
|
\ProvidesPackage{digiradio-manual}[2026/07/06 DigiRadio manual style]
|
||||||
|
|
||||||
|
\RequirePackage[table,dvipsnames]{xcolor}
|
||||||
|
\RequirePackage{iftex}
|
||||||
|
\RequirePackage{geometry}
|
||||||
|
\RequirePackage{microtype}
|
||||||
|
\RequirePackage{setspace}
|
||||||
|
\RequirePackage{titlesec}
|
||||||
|
\RequirePackage{booktabs}
|
||||||
|
\RequirePackage{array}
|
||||||
|
\RequirePackage{enumitem}
|
||||||
|
\RequirePackage[most]{tcolorbox}
|
||||||
|
\RequirePackage{graphicx}
|
||||||
|
|
||||||
|
% ---------- Fonts (engine-aware) ---------------------------------------
|
||||||
|
\ifPDFTeX
|
||||||
|
\RequirePackage[T1]{fontenc}
|
||||||
|
\RequirePackage[utf8]{inputenc}
|
||||||
|
\RequirePackage{libertine} % Linux Libertine + Biolinum
|
||||||
|
\renewcommand{\familydefault}{\sfdefault} % Biolinum as body (Optima-like)
|
||||||
|
\else
|
||||||
|
\RequirePackage{fontspec}
|
||||||
|
% Real Optima on macOS; change the name if yours differs.
|
||||||
|
\IfFontExistsTF{Optima}{%
|
||||||
|
\setmainfont{Optima}%
|
||||||
|
}{%
|
||||||
|
\setmainfont{Linux Biolinum O}% fallback if Optima not installed
|
||||||
|
}
|
||||||
|
\fi
|
||||||
|
|
||||||
|
% ---------- Colour palette (elegant, restrained) -----------------------
|
||||||
|
% Two signature colours (petrol + amber), a muted red reserved for
|
||||||
|
% cautions, plus ink and neutral. Backgrounds are light tints of these.
|
||||||
|
\definecolor{drPetrol}{HTML}{0F4C5C} % primary — headings, notes
|
||||||
|
\definecolor{drAmber} {HTML}{C46210} % accent — key ideas, emphasis
|
||||||
|
\definecolor{drTeal} {HTML}{1A7F72} % secondary — references
|
||||||
|
\definecolor{drRed} {HTML}{A4303F} % caution only
|
||||||
|
\definecolor{drInk} {HTML}{1B1B1D} % body text
|
||||||
|
\definecolor{drGray} {HTML}{5A5F63} % captions, rules
|
||||||
|
|
||||||
|
\color{drInk}
|
||||||
|
|
||||||
|
% ---------- Page geometry & spacing ------------------------------------
|
||||||
|
\geometry{a4paper, top=2.6cm, bottom=2.6cm, inner=2.6cm, outer=3.2cm,
|
||||||
|
marginparwidth=2.4cm}
|
||||||
|
\setstretch{1.3}
|
||||||
|
\setlength{\parskip}{0.5\baselineskip}
|
||||||
|
\setlength{\parindent}{0pt}
|
||||||
|
|
||||||
|
% ---------- Links -------------------------------------------------------
|
||||||
|
\RequirePackage{hyperref}
|
||||||
|
\hypersetup{colorlinks=true, linkcolor=drPetrol, urlcolor=drAmber,
|
||||||
|
citecolor=drTeal}
|
||||||
|
|
||||||
|
% ---------- Chapter heading (for report-class manuals) -----------------
|
||||||
|
\titleformat{\chapter}[display]
|
||||||
|
{\color{drPetrol}\bfseries}
|
||||||
|
{\filright\color{drAmber}\large\MakeUppercase{\chaptertitlename}\ \thechapter}
|
||||||
|
{6pt}{\Huge}
|
||||||
|
[\vspace{3pt}{\color{drAmber}\titlerule[1.5pt]}]
|
||||||
|
\titlespacing*{\chapter}{0pt}{0pt}{22pt}
|
||||||
|
|
||||||
|
% ---------- Section headings (coloured, sans, distinctive) -------------
|
||||||
|
\titleformat{\section}
|
||||||
|
{\color{drPetrol}\Large\bfseries}
|
||||||
|
{\color{drAmber}\thesection\quad}{0pt}{}
|
||||||
|
[{\color{drAmber}\titlerule[1.2pt]}]
|
||||||
|
\titleformat{\subsection}
|
||||||
|
{\color{drPetrol}\large\bfseries}
|
||||||
|
{\color{drAmber}\thesubsection\quad}{0pt}{}
|
||||||
|
\titleformat{\subsubsection}
|
||||||
|
{\color{drPetrol}\normalsize\bfseries}
|
||||||
|
{\thesubsubsection\quad}{0pt}{}
|
||||||
|
\titleformat{\paragraph}[runin]
|
||||||
|
{\bfseries\color{drPetrol}}{}{0pt}{}[\;]
|
||||||
|
\titlespacing*{\paragraph}{0pt}{0.6\baselineskip}{0.8em}
|
||||||
|
\titlespacing*{\section}{0pt}{1.4\baselineskip}{0.5\baselineskip}
|
||||||
|
|
||||||
|
% ---------- List markers (coloured) ------------------------------------
|
||||||
|
\setlist[itemize,1]{label=\textcolor{drAmber}{\textbullet}, leftmargin=*}
|
||||||
|
\setlist[itemize,2]{label=\textcolor{drTeal}{\textendash}}
|
||||||
|
\setlist[enumerate,1]{label=\textcolor{drAmber}{\bfseries\arabic*.},
|
||||||
|
leftmargin=*}
|
||||||
|
|
||||||
|
% ---------- Callout boxes (clear at a glance) --------------------------
|
||||||
|
\tcbuselibrary{skins,breakable}
|
||||||
|
|
||||||
|
% Shared base: rounded, light tinted body, bold coloured title bar
|
||||||
|
% clipped to the top-left, and an accent rule down the left edge.
|
||||||
|
\tcbset{dr@base/.style={
|
||||||
|
enhanced, breakable, boxrule=0pt, frame hidden, sharp corners=downhill,
|
||||||
|
arc=1.4mm, left=3.5mm, right=3.5mm, top=3mm, bottom=3mm,
|
||||||
|
fonttitle=\bfseries\small, coltitle=white,
|
||||||
|
attach boxed title to top left={xshift=3mm, yshift=-2.4mm},
|
||||||
|
boxed title style={boxrule=0pt, arc=0.8mm,
|
||||||
|
left=2.2mm, right=2.2mm, top=0.6mm, bottom=0.6mm},
|
||||||
|
before skip=8pt, after skip=8pt,
|
||||||
|
}}
|
||||||
|
|
||||||
|
\newtcolorbox{drnote}[1][Note]{dr@base,
|
||||||
|
colback=drPetrol!6, colbacktitle=drPetrol,
|
||||||
|
borderline west={2.6pt}{0pt}{drPetrol}, title={#1}}
|
||||||
|
|
||||||
|
\newtcolorbox{drkey}[1][Key idea]{dr@base,
|
||||||
|
colback=drAmber!9, colbacktitle=drAmber,
|
||||||
|
borderline west={2.6pt}{0pt}{drAmber}, title={#1}}
|
||||||
|
|
||||||
|
\newtcolorbox{drcaution}[1][Caution]{dr@base,
|
||||||
|
colback=drRed!7, colbacktitle=drRed,
|
||||||
|
borderline west={2.6pt}{0pt}{drRed}, title={#1}}
|
||||||
|
|
||||||
|
\newtcolorbox{drref}[1][Datasheet]{dr@base,
|
||||||
|
colback=drTeal!7, colbacktitle=drTeal,
|
||||||
|
borderline west={2.6pt}{0pt}{drTeal}, title={#1}}
|
||||||
|
|
||||||
|
\newtcolorbox{drdecision}[1][Design decision]{dr@base,
|
||||||
|
colback=drPetrol!5, colbacktitle=drGray,
|
||||||
|
borderline west={2.6pt}{0pt}{drGray}, title={#1}}
|
||||||
|
|
||||||
|
% ---------- Table helpers ----------------------------------------------
|
||||||
|
% A coloured header row for booktabs tables. Usage:
|
||||||
|
% \begin{tabular}{...}
|
||||||
|
% \drhead Col A & Col B \\ % coloured header row
|
||||||
|
% \midrule ...
|
||||||
|
\newcommand{\drhead}{\rowcolor{drPetrol}\color{white}\bfseries}
|
||||||
|
% Subtle zebra striping if wanted: \rowcolors{2}{drPetrol!5}{white}
|
||||||
|
% ---------- Code listings (styled to match the callouts) ---------------
|
||||||
|
\tcbuselibrary{listings}
|
||||||
|
\lstdefinestyle{drcpp}{%
|
||||||
|
language=C++,
|
||||||
|
basicstyle=\ttfamily\small,
|
||||||
|
keywordstyle=\color{drPetrol}\bfseries,
|
||||||
|
commentstyle=\color{drGray}\itshape,
|
||||||
|
stringstyle=\color{drTeal},
|
||||||
|
numberstyle=\color{drGray}\scriptsize,
|
||||||
|
numbers=left, numbersep=9pt,
|
||||||
|
showstringspaces=false,
|
||||||
|
tabsize=2, breaklines=true, keepspaces=true,
|
||||||
|
morekeywords={constexpr,noexcept,nullptr,override,final,explicit,
|
||||||
|
enum,class,namespace,using,co_await,std,expected,span,
|
||||||
|
uint32_t,uint8_t},
|
||||||
|
}
|
||||||
|
% drcode: a code block with the same title bar + left accent as the boxes
|
||||||
|
\newtcblisting{drcode}[1][Listing]{%
|
||||||
|
dr@base,
|
||||||
|
colback=drPetrol!4, colbacktitle=drGray,
|
||||||
|
borderline west={2.6pt}{0pt}{drGray},
|
||||||
|
title={#1},
|
||||||
|
listing only, listing style=drcpp,
|
||||||
|
}
|
||||||
|
% inline code: \drcode|std::expected<T,E>|
|
||||||
|
\newcommand{\code}[1]{{\ttfamily\color{drPetrol}#1}}
|
||||||
|
|
||||||
|
\endinput
|
||||||
Binary file not shown.
@@ -0,0 +1,52 @@
|
|||||||
|
% ============================================================
|
||||||
|
% DigiRadio — Technical Manual (main file)
|
||||||
|
%
|
||||||
|
% Compile with LuaLaTeX or XeLaTeX for the real Optima font
|
||||||
|
% (a system font on macOS):
|
||||||
|
% latexmk -lualatex manual.tex
|
||||||
|
% pdfLaTeX also works and falls back to Linux Biolinum.
|
||||||
|
% ============================================================
|
||||||
|
\documentclass[12pt,a4paper,oneside]{scrreprt}
|
||||||
|
|
||||||
|
\usepackage{digiradio-manual}
|
||||||
|
\usepackage{tikz}
|
||||||
|
\usetikzlibrary{positioning, arrows.meta, fit, backgrounds}
|
||||||
|
|
||||||
|
\begin{document}
|
||||||
|
|
||||||
|
% ---------------- Cover ----------------
|
||||||
|
\begin{titlepage}
|
||||||
|
\thispagestyle{empty}
|
||||||
|
\vspace*{3.5cm}
|
||||||
|
{\color{drPetrol}\fontsize{48}{52}\selectfont\bfseries DigiRadio\par}
|
||||||
|
\vspace{3mm}
|
||||||
|
{\color{drAmber}\rule{\linewidth}{2.5pt}}
|
||||||
|
\vspace{6mm}
|
||||||
|
|
||||||
|
{\color{drInk}\LARGE Open-Source Hi-Fi DAB+/FM Receiver\par}
|
||||||
|
\vspace{3mm}
|
||||||
|
{\color{drGray}\Large Technical Manual\par}
|
||||||
|
|
||||||
|
\vfill
|
||||||
|
{\color{drInk}\large Michele Bigi\par}
|
||||||
|
\vspace{1mm}
|
||||||
|
{\color{drGray} 2026 \quad\textbullet\quad Hardware: CERN-OHL-S v2
|
||||||
|
\quad\textbullet\quad Firmware: Apache-2.0\par}
|
||||||
|
\vspace{2mm}
|
||||||
|
{\color{drGray}\small \url{https://github.com/manvalan/DigiRadio}\par}
|
||||||
|
\end{titlepage}
|
||||||
|
|
||||||
|
% ---------------- Contents ----------------
|
||||||
|
\pagestyle{plain}
|
||||||
|
{\color{drPetrol}\tableofcontents}
|
||||||
|
\clearpage
|
||||||
|
|
||||||
|
% ---------------- Chapters ----------------
|
||||||
|
\include{ch-intro}
|
||||||
|
\include{ch-hardware}
|
||||||
|
\include{ch-firmware}
|
||||||
|
\include{ch-classes}
|
||||||
|
\include{ch-build}
|
||||||
|
\include{ch-licensing}
|
||||||
|
|
||||||
|
\end{document}
|
||||||
Executable
+93
@@ -0,0 +1,93 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""check-manual-sync.py — enforce AGENTS.md §3.4.
|
||||||
|
|
||||||
|
Every public, architecturally-significant class (declared in an
|
||||||
|
`include/` directory) must have a matching manual section tagged
|
||||||
|
`\\label{cls:ClassName}` in the LaTeX manual sources.
|
||||||
|
|
||||||
|
Exit code 0 if every public class is documented in the manual, 1
|
||||||
|
otherwise (printing the missing ones). Intended for CI, next to the
|
||||||
|
Doxygen check.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
tools/check-manual-sync.py [--src components] [--manual docs/manual]
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# A public class declaration: `class Name {` / `class Name :` / `class Name`
|
||||||
|
# but NOT a forward declaration `class Name;` and NOT `enum class`.
|
||||||
|
CLASS_RE = re.compile(
|
||||||
|
r"^\s*(?:template\s*<[^>]*>\s*)?class\s+([A-Z]\w*)\s*(?:final\s*)?(?:[:{]|$)"
|
||||||
|
)
|
||||||
|
# A pure forward declaration: `class Name;` (optionally templated).
|
||||||
|
FORWARD_RE = re.compile(r"^\s*(?:template\s*<[^>]*>\s*)?class\s+\w+\s*;\s*$")
|
||||||
|
LABEL_RE = re.compile(r"\\label\{cls:(\w+)\}")
|
||||||
|
|
||||||
|
|
||||||
|
def public_classes(src_root: Path) -> dict[str, Path]:
|
||||||
|
"""Collect class names declared under any include/ directory."""
|
||||||
|
found: dict[str, Path] = {}
|
||||||
|
for header in src_root.rglob("*.hpp"):
|
||||||
|
if "include" not in header.parts:
|
||||||
|
continue
|
||||||
|
for line in header.read_text(encoding="utf-8").splitlines():
|
||||||
|
if FORWARD_RE.match(line): # forward declaration
|
||||||
|
continue
|
||||||
|
if "enum class" in line:
|
||||||
|
continue
|
||||||
|
m = CLASS_RE.match(line)
|
||||||
|
if m:
|
||||||
|
found.setdefault(m.group(1), header)
|
||||||
|
return found
|
||||||
|
|
||||||
|
|
||||||
|
def documented_classes(manual_root: Path) -> set[str]:
|
||||||
|
"""Collect class names that have a \\label{cls:...} in the manual."""
|
||||||
|
labelled: set[str] = set()
|
||||||
|
for tex in manual_root.rglob("*.tex"):
|
||||||
|
labelled.update(LABEL_RE.findall(tex.read_text(encoding="utf-8")))
|
||||||
|
return labelled
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
ap = argparse.ArgumentParser()
|
||||||
|
ap.add_argument("--src", default="components",
|
||||||
|
help="root of the source tree (default: components)")
|
||||||
|
ap.add_argument("--manual", default="docs/manual",
|
||||||
|
help="root of the manual sources (default: docs/manual)")
|
||||||
|
args = ap.parse_args()
|
||||||
|
|
||||||
|
src_root = Path(args.src)
|
||||||
|
manual_root = Path(args.manual)
|
||||||
|
if not src_root.exists():
|
||||||
|
print(f"source root not found: {src_root}", file=sys.stderr)
|
||||||
|
return 2
|
||||||
|
if not manual_root.exists():
|
||||||
|
print(f"manual root not found: {manual_root}", file=sys.stderr)
|
||||||
|
return 2
|
||||||
|
|
||||||
|
classes = public_classes(src_root)
|
||||||
|
documented = documented_classes(manual_root)
|
||||||
|
|
||||||
|
missing = {name: path for name, path in classes.items()
|
||||||
|
if name not in documented}
|
||||||
|
|
||||||
|
if missing:
|
||||||
|
print("Manual out of sync — missing \\label{cls:...} sections:")
|
||||||
|
for name, path in sorted(missing.items()):
|
||||||
|
print(f" - {name} (declared in {path})")
|
||||||
|
print(f"\n{len(missing)} public class(es) undocumented in the manual.")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
print(f"Manual in sync: {len(classes)} public class(es) documented.")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
# DigiRadio — repository documentation package
|
||||||
|
|
||||||
|
Copy the contents into your repo keeping this structure.
|
||||||
|
|
||||||
|
```
|
||||||
|
DigiRadio/ <- repo root
|
||||||
|
├── LICENSE CERN-OHL-S v2 (hardware)
|
||||||
|
├── CONTRIBUTING.md dev conventions (human-facing)
|
||||||
|
├── .gitignore
|
||||||
|
└── Software/ firmware project root (open THIS in Cursor)
|
||||||
|
├── LICENSE Apache-2.0 (firmware)
|
||||||
|
├── AGENTS.md authoritative coding rules
|
||||||
|
├── instructions.md agent kickoff briefing (Slice 1)
|
||||||
|
├── Doxyfile API docs generation + enforcement
|
||||||
|
├── apache-header.txt header to paste in each source file
|
||||||
|
├── .cursor/rules/*.mdc Cursor scoped rules (6 files)
|
||||||
|
├── tools/
|
||||||
|
│ └── check-manual-sync.py enforces "a section per public class"
|
||||||
|
└── docs/
|
||||||
|
└── manual/ the technical manual (LaTeX)
|
||||||
|
├── manual.tex main file
|
||||||
|
├── digiradio-manual.sty style (Optima-like, boxes, listings)
|
||||||
|
├── ch-*.tex chapters
|
||||||
|
└── manual.pdf compiled preview
|
||||||
|
```
|
||||||
|
|
||||||
|
## Build the manual
|
||||||
|
cd Software/docs/manual
|
||||||
|
latexmk -lualatex manual.tex # real Optima on macOS
|
||||||
|
# or: pdflatex manual.tex x3 # Biolinum fallback
|
||||||
|
|
||||||
|
## Enforcement in CI (run from Software/)
|
||||||
|
doxygen Doxyfile # API docs must pass
|
||||||
|
python3 tools/check-manual-sync.py # manual must be in sync
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- Two LICENSE files: CERN-OHL-S at root (hardware), Apache-2.0 in
|
||||||
|
Software/ (firmware). GitHub auto-detects both.
|
||||||
|
- docs/api/ (Doxygen output) is git-ignored; the manual PDF is optional
|
||||||
|
to commit (source .tex is the master).
|
||||||
|
- Open Software/ as the Cursor project so rules and AGENTS.md load.
|
||||||
Reference in New Issue
Block a user