pre-agent-version

This commit is contained in:
2026-07-06 07:22:56 +02:00
parent 0ff803b4c3
commit 1b225dde1d
13 changed files with 931 additions and 0 deletions
+111
View File
@@ -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.
+37
View File
@@ -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.
+57
View File
@@ -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}
+35
View File
@@ -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.
+202
View File
@@ -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.
+53
View File
@@ -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.
+41
View File
@@ -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}.
+40
View File
@@ -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}
+169
View File
@@ -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.
+52
View File
@@ -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}
+93
View File
@@ -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())
+41
View File
@@ -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.