Fix agent (Cursor) rules

This commit is contained in:
2026-07-06 01:12:37 +02:00
parent 93b081d518
commit 0ff803b4c3
16 changed files with 1199 additions and 72 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 337 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 337 KiB

+289
View File
@@ -0,0 +1,289 @@
CERN Open Hardware Licence Version 2 - Strongly Reciprocal
Preamble
CERN has developed this licence to promote collaboration among
hardware designers and to provide a legal tool which supports the
freedom to use, study, modify, share and distribute hardware designs
and products based on those designs. Version 2 of the CERN Open
Hardware Licence comes in three variants: CERN-OHL-P (permissive); and
two reciprocal licences: CERN-OHL-W (weakly reciprocal) and this
licence, CERN-OHL-S (strongly reciprocal).
The CERN-OHL-S is copyright CERN 2020. Anyone is welcome to use it, in
unmodified form only.
Use of this Licence does not imply any endorsement by CERN of any
Licensor or their designs nor does it imply any involvement by CERN in
their development.
1 Definitions
1.1 'Licence' means this CERN-OHL-S.
1.2 'Compatible Licence' means
a) any earlier version of the CERN Open Hardware licence, or
b) any version of the CERN-OHL-S, or
c) any licence which permits You to treat the Source to which
it applies as licensed under CERN-OHL-S provided that on
Conveyance of any such Source, or any associated Product You
treat the Source in question as being licensed under
CERN-OHL-S.
1.3 'Source' means information such as design materials or digital
code which can be applied to Make or test a Product or to
prepare a Product for use, Conveyance or sale, regardless of its
medium or how it is expressed. It may include Notices.
1.4 'Covered Source' means Source that is explicitly made available
under this Licence.
1.5 'Product' means any device, component, work or physical object,
whether in finished or intermediate form, arising from the use,
application or processing of Covered Source.
1.6 'Make' means to create or configure something, whether by
manufacture, assembly, compiling, loading or applying Covered
Source or another Product or otherwise.
1.7 'Available Component' means any part, sub-assembly, library or
code which:
a) is licensed to You as Complete Source under a Compatible
Licence; or
b) is available, at the time a Product or the Source containing
it is first Conveyed, to You and any other prospective
licensees
i) as a physical part with sufficient rights and
information (including any configuration and
programming files and information about its
characteristics and interfaces) to enable it either to
be Made itself, or to be sourced and used to Make the
Product; or
ii) as part of the normal distribution of a tool used to
design or Make the Product.
1.8 'Complete Source' means the set of all Source necessary to Make
a Product, in the preferred form for making modifications,
including necessary installation and interfacing information
both for the Product, and for any included Available Components.
If the format is proprietary, it must also be made available in
a format (if the proprietary tool can create it) which is
viewable with a tool available to potential licensees and
licensed under a licence approved by the Free Software
Foundation or the Open Source Initiative. Complete Source need
not include the Source of any Available Component, provided that
You include in the Complete Source sufficient information to
enable a recipient to Make or source and use the Available
Component to Make the Product.
1.9 'Source Location' means a location where a Licensor has placed
Covered Source, and which that Licensor reasonably believes will
remain easily accessible for at least three years for anyone to
obtain a digital copy.
1.10 'Notice' means copyright, acknowledgement and trademark notices,
Source Location references, modification notices (subsection
3.3(b)) and all notices that refer to this Licence and to the
disclaimer of warranties that are included in the Covered
Source.
1.11 'Licensee' or 'You' means any person exercising rights under
this Licence.
1.12 'Licensor' means a natural or legal person who creates or
modifies Covered Source. A person may be a Licensee and a
Licensor at the same time.
1.13 'Convey' means to communicate to the public or distribute.
2 Applicability
2.1 This Licence governs the use, copying, modification, Conveying
of Covered Source and Products, and the Making of Products. By
exercising any right granted under this Licence, You irrevocably
accept these terms and conditions.
2.2 This Licence is granted by the Licensor directly to You, and
shall apply worldwide and without limitation in time.
2.3 You shall not attempt to restrict by contract or otherwise the
rights granted under this Licence to other Licensees.
2.4 This Licence is not intended to restrict fair use, fair dealing,
or any other similar right.
3 Copying, modifying and Conveying Covered Source
3.1 You may copy and Convey verbatim copies of Covered Source, in
any medium, provided You retain all Notices.
3.2 You may modify Covered Source, other than Notices, provided that
You irrevocably undertake to make that modified Covered Source
available from a Source Location should You Convey a Product in
circumstances where the recipient does not otherwise receive a
copy of the modified Covered Source. In each case subsection 3.3
shall apply.
You may only delete Notices if they are no longer applicable to
the corresponding Covered Source as modified by You and You may
add additional Notices applicable to Your modifications.
Including Covered Source in a larger work is modifying the
Covered Source, and the larger work becomes modified Covered
Source.
3.3 You may Convey modified Covered Source (with the effect that You
shall also become a Licensor) provided that You:
a) retain Notices as required in subsection 3.2;
b) add a Notice to the modified Covered Source stating that You
have modified it, with the date and brief description of how
You have modified it;
c) add a Source Location Notice for the modified Covered Source
if You Convey in circumstances where the recipient does not
otherwise receive a copy of the modified Covered Source; and
d) license the modified Covered Source under the terms and
conditions of this Licence (or, as set out in subsection
8.3, a later version, if permitted by the licence of the
original Covered Source). Such modified Covered Source must
be licensed as a whole, but excluding Available Components
contained in it, which remain licensed under their own
applicable licences.
4 Making and Conveying Products
You may Make Products, and/or Convey them, provided that You either
provide each recipient with a copy of the Complete Source or ensure
that each recipient is notified of the Source Location of the Complete
Source. That Complete Source is Covered Source, and You must
accordingly satisfy Your obligations set out in subsection 3.3. If
specified in a Notice, the Product must visibly and securely display
the Source Location on it or its packaging or documentation in the
manner specified in that Notice.
5 Research and Development
You may Convey Covered Source, modified Covered Source or Products to
a legal entity carrying out development, testing or quality assurance
work on Your behalf provided that the work is performed on terms which
prevent the entity from both using the Source or Products for its own
internal purposes and Conveying the Source or Products or any
modifications to them to any person other than You. Any modifications
made by the entity shall be deemed to be made by You pursuant to
subsection 3.2.
6 DISCLAIMER AND LIABILITY
6.1 DISCLAIMER OF WARRANTY -- The Covered Source and any Products
are provided 'as is' and any express or implied warranties,
including, but not limited to, implied warranties of
merchantability, of satisfactory quality, non-infringement of
third party rights, and fitness for a particular purpose or use
are disclaimed in respect of any Source or Product to the
maximum extent permitted by law. The Licensor makes no
representation that any Source or Product does not or will not
infringe any patent, copyright, trade secret or other
proprietary right. The entire risk as to the use, quality, and
performance of any Source or Product shall be with You and not
the Licensor. This disclaimer of warranty is an essential part
of this Licence and a condition for the grant of any rights
granted under this Licence.
6.2 EXCLUSION AND LIMITATION OF LIABILITY -- The Licensor shall, to
the maximum extent permitted by law, have no liability for
direct, indirect, special, incidental, consequential, exemplary,
punitive or other damages of any character including, without
limitation, procurement of substitute goods or services, loss of
use, data or profits, or business interruption, however caused
and on any theory of contract, warranty, tort (including
negligence), product liability or otherwise, arising in any way
in relation to the Covered Source, modified Covered Source
and/or the Making or Conveyance of a Product, even if advised of
the possibility of such damages, and You shall hold the
Licensor(s) free and harmless from any liability, costs,
damages, fees and expenses, including claims by third parties,
in relation to such use.
7 Patents
7.1 Subject to the terms and conditions of this Licence, each
Licensor hereby grants to You a perpetual, worldwide,
non-exclusive, no-charge, royalty-free, irrevocable (except as
stated in subsections 7.2 and 8.4) patent license to Make, have
Made, use, offer to sell, sell, import, and otherwise transfer
the Covered Source and Products, where such licence applies only
to those patent claims licensable by such Licensor that are
necessarily infringed by exercising rights under the Covered
Source as Conveyed by that Licensor.
7.2 If You institute patent litigation against any entity (including
a cross-claim or counterclaim in a lawsuit) alleging that the
Covered Source or a Product constitutes direct or contributory
patent infringement, or You seek any declaration that a patent
licensed to You under this Licence is invalid or unenforceable
then any rights granted to You under this Licence shall
terminate as of the date such process is initiated.
8 General
8.1 If any provisions of this Licence are or subsequently become
invalid or unenforceable for any reason, the remaining
provisions shall remain effective.
8.2 You shall not use any of the name (including acronyms and
abbreviations), image, or logo by which the Licensor or CERN is
known, except where needed to comply with section 3, or where
the use is otherwise allowed by law. Any such permitted use
shall be factual and shall not be made so as to suggest any kind
of endorsement or implication of involvement by the Licensor or
its personnel.
8.3 CERN may publish updated versions and variants of this Licence
which it considers to be in the spirit of this version, but may
differ in detail to address new problems or concerns. New
versions will be published with a unique version number and a
variant identifier specifying the variant. If the Licensor has
specified that a given variant applies to the Covered Source
without specifying a version, You may treat that Covered Source
as being released under any version of the CERN-OHL with that
variant. If no variant is specified, the Covered Source shall be
treated as being released under CERN-OHL-S. The Licensor may
also specify that the Covered Source is subject to a specific
version of the CERN-OHL or any later version in which case You
may apply this or any later version of CERN-OHL with the same
variant identifier published by CERN.
8.4 This Licence shall terminate with immediate effect if You fail
to comply with any of its terms and conditions.
8.5 However, if You cease all breaches of this Licence, then Your
Licence from any Licensor is reinstated unless such Licensor has
terminated this Licence by giving You, while You remain in
breach, a notice specifying the breach and requiring You to cure
it within 30 days, and You have failed to come into compliance
in all material respects by the end of the 30 day period. Should
You repeat the breach after receipt of a cure notice and
subsequent reinstatement, this Licence will terminate
immediately and permanently. Section 6 shall continue to apply
after any termination.
8.6 This Licence shall not be enforceable except by a Licensor
acting as such, and third party beneficiary rights are
specifically excluded.
Binary file not shown.
+45
View File
@@ -0,0 +1,45 @@
---
description: DigiRadio core non-negotiables (always on)
alwaysApply: true
---
# DigiRadio — core rules
Firmware for ESP32-S3 on ESP-IDF v5.5.x. C++23 (-std=gnu++23),
strongly typed, class-based. C++ exceptions disabled. Companion chips:
Si4684 (DAB+/FM), ADAU1701 (SigmaDSP), FSC-BT1035 (Bluetooth).
Full spec: @AGENTS.md
## Behaviour
- Blockers first: state what breaks the build or the hardware before the
solution.
- Never invent a register address, opcode, bit field, or boot sequence.
If it is not in the datasheet, say so and stop. Cite the doc section.
- No silent failure: every fallible op returns a typed error.
- Ask when a hardware invariant is unclear — don't assume.
## Code That Fits in Your Head (hard limits)
- Cyclomatic complexity <= 7 per method; at 8, decompose.
- Method fits an 80x24 box: <= 80 cols wide, <= 24 lines tall.
- One method does one thing at one level of abstraction.
- Name for intent (`tuneTo`), not mechanism (`writeReg0x30`).
- Delete before you add.
## Errors
- Typed result `std::expected<T, Error>` (native under C++23), never a
bare int code. Every timeout is an explicit error value. No C++
exceptions (disabled in ESP-IDF).
## Embedded
- No dynamic allocation in audio or ISR paths, ever.
- No virtual calls in IRAM-safe ISRs (vtables live in flash).
- Every wait has a timeout and a defined failure path.
## Version control
- Small commits, each compiles and keeps tests green.
- Commit messages: 50/72 (summary <= 50 chars, body wrapped at 72).
## Definition of Done (summary — full list in @AGENTS.md §10)
Compiles warnings-as-errors; clang-tidy clean; Apache header on every
file; doc block on every class/method; `doxygen Doxyfile` exits 0;
typed errors; host tests green; no plaintext secrets.
+59
View File
@@ -0,0 +1,59 @@
---
description: C++ typing, style, file headers and mandatory doc blocks
globs: **/*.hpp, **/*.h, **/*.cpp
alwaysApply: false
---
# C++ typing, headers and documentation
Full spec: @AGENTS.md §2 and §3.
## Typing
- No primitive obsession: a frequency, a gain, a station id are their own
types, not int/float/uint8_t. Validate at construction.
- `enum class` always; never a bare enum. No bool for mode selection
(`setBand(Band::Dab)`, not `setBand(true)`).
- Parse untrusted input (network, UART, flash) once at the boundary into
a domain type; trust it downstream.
- `const` by default; `[[nodiscard]]` on status/value returns.
- Rule of zero: wrap every HW/OS handle in RAII. No raw new/delete.
- Borrow with `std::span`, never pointer+length.
- Command Query Separation. Functional core (pure, host-testable) /
imperative shell (all I2C/SPI/UART/flash). Core includes no ESP-IDF.
## File header (every .hpp/.cpp) — Apache-2.0
```cpp
/**
* @file <name>
* @brief <one line>
*
* DigiRadio firmware — https://github.com/manvalan/DigiRadio
*
* Copyright 2026 Michele Bigi
* SPDX-License-Identifier: Apache-2.0
*
* @author Michele Bigi
* @date <YYYY-MM-DD of creation>
*/
```
## Doc block — every class AND every method
Fields, in order, using Doxygen tags (aliases @dname/@pubstate defined
in Doxyfile):
```cpp
/**
* @brief <name> — one-line intent.
*
* @dname <name>
* @param <p> <meaning> // per parameter; none -> "none"
* @return <success + each error cause> // or void / n/a
* @pubstate <member state read/written; injected deps used; or none>
*
* Description: intent and contract, NOT a restatement of the code.
*
* @author Michele Bigi
* @date <YYYY-MM-DD>
*/
```
`doxygen Doxyfile` must exit 0 with an empty warnings log. Do not use
`EXTRACT_ALL = YES` to silence missing-doc warnings.
+34
View File
@@ -0,0 +1,34 @@
---
description: Chip driver rules (Si4684, ADAU1701, FSC-BT1035)
globs: **/*Driver.*, **/drivers/**, **/*Adau*, **/*Si4684*, **/*Bt1035*
alwaysApply: false
---
# Chip drivers
Full spec: @AGENTS.md §7.17.3.
## Si4684 (DAB+/FM tuner)
- Boot flow POWER_UP -> load patch -> load image -> BOOT must follow the
AN649 sequence exactly; cite the section per step in a comment.
- Stream firmware images in bounded chunks from flash via an injected
`IFirmwareSource`; never load a whole image into a heap buffer.
- Opcodes/property IDs are `enum class`; validate the CTS/STATUS byte
before trusting any payload. Public API is intent-level; registers
are private. One driver owns one SPI/I2C handle via RAII.
## ADAU1701 (SigmaDSP, RAM boot, no EEPROM)
- ESP32 writes the SigmaStudio export to DSP RAM at every boot. Model the
export as an ordered list of RegisterWrite{address, bytes} parsed in
the pure core, replayed by the shell over I2C.
- EQ and mixer runtime changes use safeload (click-free). A raw param
write while audio runs is a bug.
- Typed control surfaces: setEqBand(EqBandIndex, GainDb, FrequencyHz, Q);
setInputMix(MixSource, GainDb) with enum class MixSource {Si4684,Esp32}.
- Biquad/gain math lives in the pure core with host tests vs reference.
## FSC-BT1035 (QCC3056, AT over UART)
- Typed command builder; explicit OK/ERROR/timeout parsing.
- AT+AUXCFG=1 (Line-In) is mandatory in the init sequence and covered by
a test on the command string. Unknown responses are an error, not
ignored.
+21
View File
@@ -0,0 +1,21 @@
---
description: Network provisioning and web UI rules
globs: **/net/**, **/network/**, **/web/**, **/ui/**, **/*.html, **/*.css, **/*.js
alwaysApply: false
---
# Network config + Web UI
Full spec: @AGENTS.md §7.4.
- Provisioning: SoftAP/captive portal first, then STA. State machine is
an explicit `enum class NetState` — no ad-hoc flags.
- UI: minimal single-page app served gzipped from flash. No heavy
frameworks. Design tokens (spacing, type scale, one accent) defined
once and reused — consistency over decoration. The UI is a thin client
over a typed JSON API and holds no business logic.
- API: typed DTOs. Parse every request body into a domain type at the
boundary before use; reject malformed input with a clear status; never
partially apply.
- No raw filesystem or debug endpoint in a shipping build (guard behind a
build flag).
+19
View File
@@ -0,0 +1,19 @@
---
description: Secure storage and secret handling
globs: **/*Secret*, **/*Store*, **/secure/**, **/*Credential*, **/*Config*
alwaysApply: false
---
# Secure storage
Full spec: @AGENTS.md §7.5.
- Stores Wi-Fi SSID/password, user credentials, station list. Encrypted
at rest (NVS encryption on an encrypted partition, or a device key in
eFuse). Confirm the mechanism against current ESP-IDF security docs
before implementing.
- A `Secret` wrapper: no operator<<, no implicit conversion to a loggable
string, buffer zeroised on destruction.
- Secrets are never logged, never placed in URLs, never serialised to
plaintext. Access goes through `ISecureStore` so core and tests never
touch real flash or real keys.
+19
View File
@@ -0,0 +1,19 @@
---
description: Testing conventions (host-first, TDD for the pure core)
globs: **/test/**, **/tests/**, **/*Test*, **/*_test.*
alwaysApply: false
---
# Testing
Full spec: @AGENTS.md §8.
- Pure core is developed test-first (red -> green -> refactor):
coefficient math, blob framing, config parsing, station-list logic —
all host-tested, zero hardware.
- Arrange-Act-Assert; one behaviour per test; names state the behaviour
(`tuneTo_rejectsFrequencyOutsideFmBand`).
- Fakes over mocks for driver interfaces; assert on observable behaviour,
not internal call order.
- Hardware-in-the-loop tests are separate, explicitly marked, and never
block the host suite.
+527
View File
@@ -0,0 +1,527 @@
# AGENTS.md — DigiRadio Firmware
Rules for any coding agent (Claude Code, etc.) working on the DigiRadio
firmware. This file is authoritative. If a request conflicts with these
rules, stop and surface the conflict before writing code.
Target: ESP32-S3-WROOM-1. Framework: **ESP-IDF v5.5.x** (stable).
Language: **C++23** (`-std=gnu++23`), strongly typed, class-based.
C++ exceptions: **disabled** (ESP-IDF default; keep off).
Companion chips: Si4684 (DAB+/FM tuner), ADAU1701 (SigmaDSP audio),
FSC-BT1035 / QCC3056 (Bluetooth audio, AT-controlled over UART).
---
## 0. How this agent must behave
- **Blockers first.** Open every response with what will stop the build
or the hardware from working. State the risk before the solution.
- **Zero tolerance for guessing at the hardware.** Never invent a
register address, an opcode, a bit field, or a boot sequence. If it is
not in the datasheet / programming guide, say so and stop. Cite the
document and section for every register-level decision.
- **No silent failure.** Every fallible operation returns a typed error
(see §6). Nothing is swallowed, nothing is logged-and-ignored.
- **Small steps.** One vertical slice at a time, compiling and testable
at every commit. No big-bang subsystems.
- **Ask when the invariant is unclear.** A wrong assumption baked into a
driver costs a re-flash and a debugging session. Confirm, don't assume.
---
## 1. Prime directive — Code That Fits in Your Head
Human working memory holds about seven things. Every unit of code must
fit in that budget at its own zoom level (methods, classes, modules —
fractal). Concretely:
- **Cyclomatic complexity <= 7 per method.** At 8, decompose. No
exceptions for "it's just a switch over registers" — extract a table.
- **The 80x24 box.** A method fits in an old terminal screen: <= 80
columns wide, <= 24 lines tall. If it doesn't fit, it's doing too much.
- **A method does one thing** at one level of abstraction. Mixing I2C
byte-twiddling and business logic in the same method is a smell.
- **Name for intent, not mechanism.** `tuneTo(Frequency)` not
`writeReg0x30()`. The datasheet detail lives *inside* the method.
- **Delete before you add.** The cheapest code to maintain is the code
that isn't there. Prefer removing a branch to adding one.
- **Chunk.** A reader should grasp a class from its public interface
without reading the bodies. If they can't, the interface leaks.
These are hard limits, enforced in CI where possible (clang-tidy
`readability-function-cognitive-complexity`, line-length lint).
---
## 2. Language and typing rules
C++ standard: **C++23**, pinned as `-std=gnu++23` in CMake (do not rely
on the toolchain default, which differs between ESP-IDF 5.x and 6.x).
This makes `std::expected` available natively (see §6).
### 2.1 Make illegal states unrepresentable
- **No primitive obsession.** Domain quantities get their own types.
A frequency is not an `int`; a gain is not a `float`; a station id is
not a `uint8_t`. Use a small strong-typedef template (a `NamedType`)
or dedicated value classes:
```cpp
class FrequencyKHz { // 80x24, one invariant, immutable
public:
explicit constexpr FrequencyKHz(std::uint32_t khz); // validates
constexpr std::uint32_t value() const noexcept;
private:
std::uint32_t khz_; // invariant: within band limits
};
```
- **Parse at the boundary, then trust.** Validate untrusted input
(network, UART, flash) once, at the edge, into a domain type. After
that, the type *is* the guarantee — no re-checking downstream.
- **`enum class` always.** Never a bare `enum`. Opcodes, states, bands,
and modes are enums, not magic numbers.
- **No booleans in public APIs for mode selection.** `setBand(Band::Dab)`
not `setBand(true)`.
### 2.2 Const-correctness, ownership, RAII
- **`const` by default.** Mutable is the exception you justify.
- **`[[nodiscard]]`** on every function returning a status or a value
that must not be dropped.
- **Rule of zero.** Wrap every OS/hardware handle (I2C bus, SPI device,
NVS handle, task, mutex) in a RAII type. No raw `new`/`delete`, no
manual `*_delete()` calls scattered in code — the destructor owns it.
- **Own with values and smart pointers**, borrow with references or
`std::span`. Never pass `pointer + length`; pass `std::span<std::byte>`.
- **`noexcept`** on anything that genuinely cannot throw (hot paths,
destructors, move ops).
### 2.3 Functions and purity
- **Command Query Separation.** A method either changes state (returns
void / status) or answers a question (returns a value, no side
effects). Never both.
- **Functional core, imperative shell.** Pure logic — station-list
operations, EQ coefficient math, config parsing/serialisation, boot
blob framing — lives in a hardware-free core that compiles and tests
on the host. All I2C/SPI/UART/flash lives in a thin shell that calls
the core. The core has zero `#include` of ESP-IDF headers.
---
## 3. File headers and code documentation
These are mandatory and checked in the Definition of Done. A file
without its licence header, or a class/method without its documentation
block, is not done.
### 3.1 File header (every source and header file)
Every `.hpp` / `.cpp` starts with this block, filled in for the file.
Use the SPDX identifier plus the short Apache notice — firmware is
Apache-2.0.
```cpp
/**
* @file Si4684Driver.hpp
* @brief Si4684 DAB+/FM tuner driver (intent-level interface).
*
* DigiRadio firmware — https://github.com/manvalan/DigiRadio
*
* Copyright 2026 Michele Bigi
* SPDX-License-Identifier: Apache-2.0
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
*
* @author Michele Bigi
* @date <YYYY-MM-DD of creation>
*/
```
Rules:
- The `@date` is the file's creation date and is not rewritten on later
edits (history lives in version control).
- The copyright year matches the creation year.
- Never place a secret, token, or path to a private resource in a header.
### 3.2 Documentation block — every class and every method
Every class and every method carries a Doxygen block with the fields
below, in this order. Native Doxygen tags are used for the standard
fields; two project aliases (`@dname`, `@pubstate`, defined in the
Doxyfile, §3.3) render the non-standard fields as titled sections in the
generated documentation. This means the required format *is* the tool's
format — one source of truth, no drift.
**Method block:**
```cpp
/**
* @brief tuneTo — set the tuner to a validated frequency.
*
* @dname tuneTo
* @param freq Target frequency, already validated to the active band.
* @return Ok on success, or Error::TunerTimeout / Error::NotBooted.
* @pubstate reads band_ (range-check); writes lastRsq_ (refreshed after
* a successful tune); uses spi_ (injected SPI dependency).
*
* States intent and the contract upheld — why this method exists and
* what it guarantees. Do NOT restate the code line by line.
*
* @author Michele Bigi
* @date <YYYY-MM-DD>
*/
```
**Class block:**
```cpp
/**
* @brief Si4684Driver — owns one Si4684, exposes intent-level tuning.
*
* @dname Si4684Driver
* @param spi Injected SPI device, borrowed for the driver's life.
* @param fw Injected firmware source for boot images.
* @return n/a (type)
* @pubstate Public interface: powerUp(), loadImage(Band),
* tuneTo(FrequencyKHz), readRsq(). Owns one SPI handle (RAII).
* No public data members.
*
* Single responsibility of the class in one or two sentences, plus its
* key invariants (e.g. tuneTo is valid only after a successful
* loadImage).
*
* @author Michele Bigi
* @date <YYYY-MM-DD>
*/
```
The mapping: **Name** → `@dname`, **Parameters** → `@param` (per param;
constructor/template params for a class), **Return** → `@return`,
**Public variables used** → `@pubstate`, **Description** → the free
text, **Author/Date** → `@author` / `@date`.
Field rules:
- **Name** — the class or method name, verbatim.
- **Parameters** — one `@param` per parameter; for a class, the
constructor / template parameters. Write "none" if there are none.
- **Return** — `@return` with success value and each error cause it can
return; "void" or "n/a" where applicable.
- **Public state used** — member state read/written and injected
dependencies touched. With proper encapsulation there are normally no
public data members, so this documents the shared/member state and
collaborators the method relies on. Write "none" for a pure function.
- **Description** — intent, contract, and invariants. Explains *why*,
never a restatement of the implementation.
- **Author / Date** — `@author Michele Bigi` and the `@date`.
Keep the block honest: if a method's "Public state used" list grows
long, that is a design signal to split the method (§1), not to write a
longer comment.
### 3.3 Documentation tooling — Doxygen
The documentation format above is backed by **Doxygen** (the standard
C++ documentation generator), configured by the `Doxyfile` at the repo
root. The tool does two jobs:
1. **Renders** the doc blocks into browsable HTML under `docs/api/`. The
`@dname` and `@pubstate` aliases turn the project-specific fields into
proper titled sections, so the generated docs match this spec exactly.
2. **Enforces** the rule. The Doxyfile sets `EXTRACT_ALL = NO`,
`WARN_IF_UNDOCUMENTED = YES`, `WARN_NO_PARAMDOC = YES`, and
`WARN_AS_ERROR = FAIL_ON_WARNINGS`. Any class, method, or parameter
without its documentation block makes `doxygen` exit non-zero.
Rules for the agent:
- **The docs build is part of Done.** Run `doxygen Doxyfile` and it must
exit 0 with an empty `docs/api/doxygen-warnings.log`. A non-zero exit
means something is undocumented or malformed — fix it, don't suppress
the warning.
- **Wire it into CI** as a required job, so an undocumented symbol blocks
the merge exactly like a failing test does.
- **Do not use `EXTRACT_ALL = YES` to silence warnings.** That flag hides
missing documentation instead of reporting it, defeating the purpose.
- Generated output (`docs/api/`) is a build artifact — git-ignore it,
don't commit it.
- Graphviz (`dot`) is optional but enabled: it produces class and
collaboration diagrams, which help keep the structure "in your head".
If `dot` is unavailable in an environment, set `HAVE_DOT = NO` there.
The `WARN_AS_ERROR = FAIL_ON_WARNINGS` value requires a recent Doxygen
(1.9.x+); on an older version use `WARN_AS_ERROR = YES`. Verify the
version rather than assuming.
---
## 4. Architecture
Layered, dependencies point inward only:
```
Shell (imperative): drivers, web server, NVS, tasks, ISRs
— thin, no business logic [ESP-IDF, HW]
Application services: TunerService, AudioService,
ConfigService, NetworkService [orchestration]
Domain core (pure, host-testable): Station, Frequency,
EqProfile, MixerState, Credential, boot-blob
framing, validation [no HW headers]
```
- **Depend on abstractions.** Services take driver *interfaces*
(e.g. `ITuner`, `IDsp`, `IBtModule`, `ISecureStore`), injected via the
constructor. This is what makes the core testable without hardware.
- **No god object.** No single `DigiRadio` class that knows everything.
Compose small services.
- **One class = one responsibility.** If a class name needs "and", split
it.
---
## 5. Embedded constraints (ESP32-S3)
- **Heap discipline.** Prefer static / stack / pool allocation. No
dynamic allocation in audio or ISR paths, ever. Allocate at init,
reuse buffers. Watch fragmentation — long-running device.
- **ISR rules.** ISRs do the minimum: read/clear flag, signal a task.
No logging, no allocation, no blocking, no C++ exceptions in an ISR.
No virtual function calls in IRAM-safe ISRs — vtables live in flash and
are inaccessible when the flash cache is disabled.
- **Tasks and concurrency.** Each subsystem that needs its own timeline
gets a FreeRTOS task with an explicit stack size and priority,
documented. Shared state crosses task boundaries only through queues
or mutex-guarded types — never raw shared globals.
- **Exception policy.** C++ exceptions are disabled by default in
ESP-IDF and stay disabled here. All recoverable errors use the typed
result (§6). Destructors and hot paths are `noexcept`.
- **Blocking.** No busy-wait spin loops. Use event groups / notifications
with timeouts. Every wait has a timeout and a defined failure path.
---
## 6. Error handling
- **Typed results, not error codes floating in `int`.** Use
`std::expected<T, Error>` — available natively under C++23, so no
vendored library is needed. `Error` is an `enum class` with a stable
set of causes plus optional context.
- **Errors propagate to a place that can act.** A driver reports; a
service decides (retry, degrade, surface to UI); the top level logs.
Do not decide policy deep in a driver.
- **Every timeout is an error value**, handled explicitly — never a
silent return.
- **No `assert` for runtime-reachable conditions.** `assert` is only for
programmer-invariant violations that are bugs by definition. Hardware
can fail; that's a result, not an assertion.
---
## 7. Subsystem rules
### 7.1 Si4684 tuner driver
- The boot flow (POWER_UP → load patch/bootloader → load firmware image
→ BOOT) must follow the AN649 / programming-guide sequence exactly.
**Cite the section** for each step in a comment.
- Firmware images (FM, DAB) are large blobs. The driver **streams** them
in bounded chunks from flash — never loads a whole image into a heap
buffer. Blob source is an injected interface (`IFirmwareSource`) so it
can be faked in host tests.
- Command opcodes and property IDs are `enum class`. A `Command` builder
frames bytes; a `Response` parser validates the CTS/STATUS byte before
any payload is trusted.
- The public interface is intent-level: `powerUp()`, `loadImage(Band)`,
`tuneTo(FrequencyKHz)`, `readRsq()`. Register access is private.
- One driver instance owns one SPI (or I2C) device handle via RAII.
### 7.2 ADAU1701 DSP driver (RAM boot, no EEPROM)
- The ESP32 writes the SigmaStudio-exported program **to DSP RAM at every
boot** (self-boot EEPROM removed by design). Model the export as a
domain type — an ordered list of `RegisterWrite{ address, bytes }` —
parsed in the pure core, replayed by the shell over I2C.
- **Safeload for live updates.** EQ and mixer parameter changes at
runtime use the ADAU1701 safeload mechanism (write to the safeload
registers + IST) so audio updates are click-free. A raw param write
while audio runs is a bug.
- Provide typed control surfaces, not raw cell addresses to callers:
- `setEqBand(EqBandIndex, GainDb, FrequencyHz, Q)` → computes
biquad coefficients in the pure core, then safeloads them.
- `setInputMix(MixSource, GainDb)` where
`enum class MixSource { Si4684, Esp32 }` — this is the input mixer
between the tuner and the ESP32 audio path.
- Coefficient math (biquad design, gain-to-linear) lives in the pure
core with host unit tests against known reference values. No DSP math
hidden inside an I2C method.
### 7.3 FSC-BT1035 (QCC3056) driver
- Controlled by AT commands over UART. Build commands with a typed
builder; parse responses with explicit `OK`/`ERROR`/timeout handling.
- **Line-In mode is mandatory:** the `AT+AUXCFG=1` step must be part of
the documented init sequence and covered by a test on the command
string. Losing it silently breaks the audio path.
- The AT subset in use is enumerated and documented; unknown responses
are an error value, not ignored.
### 7.4 Network configuration + Web UI
- **Provisioning:** captive portal / SoftAP for first setup, then STA.
State machine is explicit (`enum class NetState`), no ad-hoc flags.
- **UI: elegant and essential.** A minimal single-page app served
gzipped from flash. No heavy frameworks; small, fast, legible. Design
tokens (spacing, type scale, one accent colour) defined once and
reused — consistency over decoration. The UI is a thin client over a
typed JSON API; it holds no business logic.
- **API:** REST/JSON with typed DTOs on the firmware side. Every request
body is parsed into a domain type at the boundary (§2.1) before use.
Reject malformed input with a clear status; never partially apply.
- Serve UI assets read-only; never expose a raw filesystem or debug
endpoint in a shipping build (guard behind a build flag).
### 7.5 Secure storage
- Stores: Wi-Fi SSID + password, user credentials (name + password),
station/frequency list. **Encrypted at rest** — use NVS encryption on
an encrypted partition (with flash encryption enabled), or encrypt
payloads with a device key held in eFuse. Confirm the chosen mechanism
against current ESP-IDF security docs before implementing.
- **Secrets never leave their type.** A `Secret` wrapper: no `operator<<`,
no implicit conversion to a loggable string, buffer zeroised on
destruction. Secrets are never logged, never placed in URLs, never
serialised to plaintext.
- Access is behind `ISecureStore` so the core and tests never touch real
flash or real keys.
### 7.6 Station / frequency list
- A `Station` is a value type: name, band, frequency (or DAB service id),
optional preset slot. The list is a domain collection with CRUD in the
pure core; persistence goes through `ISecureStore`.
- All list operations (add, remove, reorder, find, validate duplicates)
are host-tested with zero hardware.
---
## 8. Testing
- **TDD where it pays:** the pure core is developed test-first
(red → green → refactor). Coefficient math, blob framing, config
parsing, station-list logic — all covered on the host.
- **ArrangeActAssert**, one behaviour per test, names that state the
behaviour: `tuneTo_rejectsFrequencyOutsideFmBand`.
- **Fakes over mocks** for the driver interfaces; assert on observable
behaviour, not on internal call sequences.
- **Hardware-in-the-loop** tests are separate, explicitly marked, and
never block the host test suite.
- A change without a test for its logic is not done (hardware-only glue
excepted, and that glue must be trivially thin).
---
## 9. Version control and workflow
- **Small, frequent commits.** Each commit compiles and keeps tests
green. One logical change per commit.
- **Commit messages: the 50/72 rule.** Summary line <= 50 chars,
imperative mood; blank line; body wrapped at 72 explaining *why*.
- **Feature flags / branches by abstraction** for anything half-built —
`main` always builds and runs.
- No commented-out code committed. Version control is the history.
---
## 10. Definition of Done (checklist)
Before a slice is considered complete:
- [ ] Compiles with warnings-as-errors; clang-tidy clean.
- [ ] 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).
- [ ] Every method <= 80x24, complexity <= 7.
- [ ] No primitive obsession in public interfaces.
- [ ] Fallible paths return typed results; no silent failure.
- [ ] Pure-core logic has host unit tests, all green.
- [ ] No secret is loggable or stored in plaintext.
- [ ] Register-level decisions cite datasheet section in comments.
- [ ] No dynamic allocation in audio/ISR paths.
- [ ] Public interface is understandable without reading bodies.
---
## 11. The agent must NOT
- Ship a file without the Apache-2.0 licence header.
- Ship a class or method without its documentation block.
- 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.
- Introduce a class whose name needs "and".
- Exceed the complexity / size limits "just this once".
- Store or log a credential in plaintext.
- Ship a slice that doesn't compile or breaks the host tests.
- Proceed past an unclear hardware invariant without asking.
---
## 12. Repo layout, build and test
### Layout (ESP-IDF project; core is host-testable)
```
Software/
├── AGENTS.md Doxyfile instructions.md
├── CMakeLists.txt top-level ESP-IDF project
├── sdkconfig.defaults C++23, exceptions off, flash/NVS encryption
├── partitions.csv includes an encrypted NVS partition
├── .cursor/rules/*.mdc
├── main/ imperative shell entry (app_main)
├── components/
│ ├── core/ PURE domain core — no ESP-IDF headers
│ │ ├── include/core/ public headers
│ │ ├── src/
│ │ └── test/ host unit tests (plain CMake + ctest)
│ ├── drivers/{si4684,adau1701,bt1035}/
│ ├── net/ provisioning + web server
│ ├── secure_store/
│ └── services/ TunerService, AudioService, ...
└── docs/api/ Doxygen output (git-ignored)
```
Rule: `components/core` compiles two ways — as an ESP-IDF component AND
standalone on the host for unit tests. It must never `#include` an
ESP-IDF header, so the host build stays hardware-free.
### Commands
Device build / flash / monitor:
```
idf.py set-target esp32s3
idf.py build
idf.py -p <port> flash monitor
```
Host unit tests (pure core; needs a C++23 stdlib compiler):
```
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
```
Docs (must exit 0, empty warnings log):
```
doxygen Doxyfile
```
### Host toolchain note (macOS)
On the M4 Mac, `std::expected` needs a recent C++23 stdlib. Use Homebrew
`llvm` (>= 18) or `gcc-14` for the host test build — the system Apple
Clang may be too old. This affects only host tests, not the firmware.
+77
View File
@@ -0,0 +1,77 @@
# Doxyfile — DigiRadio firmware
# Curated (non-default settings only; Doxygen fills the rest with defaults).
# Verified against Doxygen 1.9.x.
#---------------------------------------------------------------------------
# Project
#---------------------------------------------------------------------------
PROJECT_NAME = "DigiRadio Firmware"
PROJECT_BRIEF = "Open-source Hi-Fi DAB+/FM receiver firmware (ESP32-S3)"
OUTPUT_DIRECTORY = docs/api
CREATE_SUBDIRS = YES
#---------------------------------------------------------------------------
# Input
#---------------------------------------------------------------------------
INPUT = src include
FILE_PATTERNS = *.hpp *.h *.cpp
RECURSIVE = YES
# Exclude vendored / generated code from the doc requirement:
EXCLUDE_PATTERNS = */vendor/* */generated/* */build/*
#---------------------------------------------------------------------------
# Build: enforce documentation (this is what makes the rule real)
#---------------------------------------------------------------------------
# Do NOT auto-document everything: we want undocumented symbols to warn.
EXTRACT_ALL = NO
EXTRACT_PRIVATE = NO
EXTRACT_STATIC = YES
HIDE_UNDOC_MEMBERS = NO
HIDE_UNDOC_CLASSES = NO
#---------------------------------------------------------------------------
# Warnings: fail the build on any missing or malformed documentation
#---------------------------------------------------------------------------
QUIET = YES
WARNINGS = YES
WARN_IF_UNDOCUMENTED = YES
WARN_IF_DOC_ERROR = YES
WARN_IF_INCOMPLETE_DOC = YES
WARN_NO_PARAMDOC = YES
# FAIL_ON_WARNINGS turns any of the above into a non-zero exit -> CI fails.
WARN_AS_ERROR = FAIL_ON_WARNINGS
WARN_LOGFILE = docs/api/doxygen-warnings.log
#---------------------------------------------------------------------------
# Custom field aliases — map the DigiRadio doc-block fields to Doxygen
#---------------------------------------------------------------------------
# ^^ is a newline inside an alias. These render as titled sections in the
# generated docs, so "Public state used" becomes a real doc section.
ALIASES += "dname=\par Name:^^"
ALIASES += "pubstate=\par Public state used:^^"
#---------------------------------------------------------------------------
# C++ language handling
#---------------------------------------------------------------------------
OPTIMIZE_OUTPUT_FOR_C = NO
BUILTIN_STL_SUPPORT = YES
JAVADOC_AUTOBRIEF = YES
MARKDOWN_SUPPORT = YES
#---------------------------------------------------------------------------
# Output formats
#---------------------------------------------------------------------------
GENERATE_HTML = YES
GENERATE_LATEX = NO
# XML is handy if you later feed the docs to another tool (e.g. Sphinx):
GENERATE_XML = NO
#---------------------------------------------------------------------------
# Diagrams (optional, needs Graphviz 'dot'; great for "fits in your head")
#---------------------------------------------------------------------------
HAVE_DOT = YES
CLASS_GRAPH = YES
COLLABORATION_GRAPH = YES
CALL_GRAPH = NO
CALLER_GRAPH = NO
DOT_IMAGE_FORMAT = svg
-72
View File
@@ -1,72 +0,0 @@
# DigiRadio — Firmware
> ⚠️ **Status: in active development.**
> The firmware will be released here after hardware bring-up and validation on the
> first prototype. Publishing firmware before it can be tested on real hardware
> would not be meaningful, so this directory currently documents the **planned
> architecture** only.
---
## Target Platform
- **Host MCU:** Espressif **ESP32-S3** (native USB, Wi-Fi, BLE)
- **Toolchain:** ESP-IDF (planned)
- **DSP tooling:** Analog Devices **SigmaStudio** for the ADAU1701 audio flow
---
## Planned Architecture
The ESP32-S3 is the system host and orchestrates the three audio subsystems over
three separate buses.
### Boot sequence
1. ESP32-S3 releases the ADAU1701 reset line (GPIO-controlled).
2. ESP32-S3 loads the compiled SigmaStudio program into the DSP program/parameter
RAM over **I²C** (host-load model; on-board self-boot EEPROM is a DNP option).
3. ESP32-S3 starts the DSP core.
4. ESP32-S3 loads the Si4684 firmware/patch over **SPI**.
5. FSC-BT1035 is brought up over **UART** (Feasycom ASCII command set).
### Runtime control
- **Audio parameters** (volume, EQ, source mix): written to the ADAU1701
**safeload registers** over I²C to avoid audio pops.
- **Tuner:** DAB/FM band and station control via Si4684 over SPI.
- **Bluetooth:** A2DP source control (aptX Adaptive), pairing and status via
FSC-BT1035 UART commands.
- **Connectivity:** optional Wi-Fi internet-radio stream injected into the DSP via
a second I²S input.
---
## Bus Map (summary)
| Bus | Devices | Notes |
|---|---|---|
| **I²C** | ADAU1701, MAC EEPROM, (self-boot EEPROM DNP) | single system bus; distinct addresses |
| **SPI** | Si4684 | firmware/patch load + control |
| **UART** | FSC-BT1035 | ASCII command set, 4-wire with flow control |
| **I²S** | Si4684 → DSP → BT1035 (+ ESP32 in) | DSP is I²S master (48 kHz) |
See the [Technical Reference Manual](../docs/DigiRadio_Manual.pdf) for the complete
pin assignment and address map.
---
## Roadmap
- [ ] ESP32-S3 project skeleton (ESP-IDF)
- [ ] ADAU1701 program loader (SigmaStudio export → I²C write)
- [ ] Si4684 firmware loader + DAB/FM control
- [ ] FSC-BT1035 UART driver (A2DP source, aptX Adaptive)
- [ ] Runtime control (volume / EQ / source) via safeload
- [ ] Wi-Fi internet-radio source (optional)
- [ ] Bring-up notes and validated example configuration
---
## License
Firmware in this directory is licensed under the [MIT License](../LICENSE) once
released.
+15
View File
@@ -0,0 +1,15 @@
/*
* Copyright 2026 Michele Bigi
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+94
View File
@@ -0,0 +1,94 @@
# instructions.md — DigiRadio firmware, agent kickoff
Read this together with `AGENTS.md` and everything under
`.cursor/rules/`. Those define *how* to write code; this file defines
*what we are building* and *what to do first*.
## What DigiRadio is
An open-source Hi-Fi DAB+/FM digital radio board. Firmware runs on an
ESP32-S3 and coordinates three companion chips:
- **Si4684** — DAB+/FM tuner (delivers the audio stream).
- **ADAU1701** — SigmaDSP: equaliser + input mixer between the Si4684
and the ESP32 audio path. Program is written to DSP RAM at every boot
(no self-boot EEPROM).
- **FSC-BT1035 (QCC3056)** — Bluetooth 5.2 out with aptX Adaptive,
controlled by AT commands over UART.
Plus: an elegant, essential web UI for network configuration; encrypted
storage for Wi-Fi and user credentials and the station list.
Repository: https://github.com/manvalan/DigiRadio
## Confirmed technical decisions (do not re-litigate)
| Area | Decision |
|-------------|-------------------------------------------------------|
| Framework | ESP-IDF v5.5.x (native, not Arduino) |
| Language | C++23, pinned `-std=gnu++23` |
| Errors | `std::expected<T, Error>` (native); exceptions OFF |
| DSP boot | ESP32 writes ADAU1701 RAM at every boot (no EEPROM) |
| Architecture| Functional core (pure, host-tested) + imperative shell|
| Docs | Doxygen, build must exit 0 (enforced) |
| HW licence | CERN-OHL-S v2 · FW licence: Apache-2.0 |
## Working agreement
- **Confirm understanding before writing code.** On kickoff, summarise
the plan and list any blockers or unclear hardware invariants first.
- **Blockers first**, always. State risks before solutions.
- **One vertical slice at a time.** `main` always builds and runs.
- Every file gets the Apache header; every class/method its doc block;
`doxygen Doxyfile` stays green. Small commits, 50/72 messages.
- Never invent a register/opcode/boot step — cite the datasheet or stop.
## Roadmap (slices, in order)
1. **Walking skeleton** — boot, a task, SoftAP, web server, one JSON
endpoint, one host test, docs green. No chip drivers yet. (Spec below.)
2. Secure store (`ISecureStore`) + Wi-Fi provisioning UI (STA join).
3. Station/frequency list model + persistence + UI.
4. Si4684 driver: power-up, load image, tune, read RSQ.
5. ADAU1701 driver: RAM boot, then safeload EQ + input mixer.
6. FSC-BT1035 driver: AT init (incl. `AT+AUXCFG=1`), audio out.
7. Integration: TunerService + AudioService end to end.
## Slice 1 — Walking skeleton (the first task)
Goal: exercise the whole toolchain end to end with zero chip hardware,
so every later slice drops into a working frame.
Build:
- Top-level ESP-IDF project targeting `esp32s3`.
- `sdkconfig.defaults` sets C++23, exceptions off, and the flash/NVS
encryption options (leave encryption keys/enablement documented, not
hard-enabled, until we decide on the secure-store slice).
- The `components/core` component compiles both under ESP-IDF and
standalone on the host.
Behaviour:
- `app_main` starts a FreeRTOS task that logs a heartbeat on a timer.
- Bring up SoftAP with a known SSID (e.g. `DigiRadio-setup`).
- Start an HTTP server serving one minimal gzipped page from flash.
- Expose `GET /api/health` returning a typed DTO serialised by the pure
core, e.g. `{"status":"ok","fw":"0.1.0"}`.
Acceptance criteria:
- [ ] `idf.py build` succeeds; `flash monitor` shows the heartbeat.
- [ ] A phone/laptop can join the SoftAP, load the page, and get a valid
JSON response from `/api/health`.
- [ ] The health DTO is defined and serialised in `components/core`,
with a host unit test that passes under `ctest`.
- [ ] `doxygen Doxyfile` exits 0 with an empty warnings log.
- [ ] Every file has the Apache header; every class/method its doc block.
- [ ] No ESP-IDF headers included from `components/core`.
Out of scope for Slice 1: any Si4684 / ADAU1701 / BT1035 code, real
credentials, encryption enablement. Those come in later slices.
## First message to the agent
Ask it to read `AGENTS.md`, `.cursor/rules/`, and this file, then
respond with: (1) the confirmed stack, (2) the exact repo layout it will
create, (3) how it will satisfy each Slice 1 acceptance criterion, and
(4) any blockers or questions — **before** writing code.