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