diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..7b7311a --- /dev/null +++ b/CONTRIBUTING.md @@ -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`; 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 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. diff --git a/Software/AGENTS.md b/Software/AGENTS.md index 5bfb266..361f967 100644 --- a/Software/AGENTS.md +++ b/Software/AGENTS.md @@ -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 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 class and method has its documentation block (§3.2). - [ ] `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. - [ ] No primitive obsession in public interfaces. - [ ] 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 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. - Put business logic in a driver or in an ISR. - Return a bare error code or swallow a failure. diff --git a/Software/docs/manual/ch-build.tex b/Software/docs/manual/ch-build.tex new file mode 100644 index 0000000..9d30a28 --- /dev/null +++ b/Software/docs/manual/ch-build.tex @@ -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 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} diff --git a/Software/docs/manual/ch-classes.tex b/Software/docs/manual/ch-classes.tex new file mode 100644 index 0000000..a97204a --- /dev/null +++ b/Software/docs/manual/ch-classes.tex @@ -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. diff --git a/Software/docs/manual/ch-firmware.tex b/Software/docs/manual/ch-firmware.tex new file mode 100644 index 0000000..8562719 --- /dev/null +++ b/Software/docs/manual/ch-firmware.tex @@ -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}); 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. diff --git a/Software/docs/manual/ch-hardware.tex b/Software/docs/manual/ch-hardware.tex new file mode 100644 index 0000000..746e0ce --- /dev/null +++ b/Software/docs/manual/ch-hardware.tex @@ -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. diff --git a/Software/docs/manual/ch-intro.tex b/Software/docs/manual/ch-intro.tex new file mode 100644 index 0000000..105e283 --- /dev/null +++ b/Software/docs/manual/ch-intro.tex @@ -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}. diff --git a/Software/docs/manual/ch-licensing.tex b/Software/docs/manual/ch-licensing.tex new file mode 100644 index 0000000..031d594 --- /dev/null +++ b/Software/docs/manual/ch-licensing.tex @@ -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} diff --git a/Software/docs/manual/digiradio-manual.sty b/Software/docs/manual/digiradio-manual.sty new file mode 100644 index 0000000..ae35a01 --- /dev/null +++ b/Software/docs/manual/digiradio-manual.sty @@ -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| +\newcommand{\code}[1]{{\ttfamily\color{drPetrol}#1}} + +\endinput diff --git a/Software/docs/manual/manual.pdf b/Software/docs/manual/manual.pdf new file mode 100644 index 0000000..a70e27f Binary files /dev/null and b/Software/docs/manual/manual.pdf differ diff --git a/Software/docs/manual/manual.tex b/Software/docs/manual/manual.tex new file mode 100644 index 0000000..041c4f2 --- /dev/null +++ b/Software/docs/manual/manual.tex @@ -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} diff --git a/Software/tools/check-manual-sync.py b/Software/tools/check-manual-sync.py new file mode 100755 index 0000000..e3ecb17 --- /dev/null +++ b/Software/tools/check-manual-sync.py @@ -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()) diff --git a/WHERE-TO-PUT-THESE.md b/WHERE-TO-PUT-THESE.md new file mode 100644 index 0000000..351e179 --- /dev/null +++ b/WHERE-TO-PUT-THESE.md @@ -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.