Fix agent (Cursor) rules
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 337 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 337 KiB |
@@ -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.
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.1–7.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.
|
||||
@@ -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).
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
- **Arrange–Act–Assert**, 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.
|
||||
@@ -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
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
*/
|
||||
@@ -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.
|
||||
Reference in New Issue
Block a user