diff --git a/Hardware/3D/3D_PCB1_2026-07-05.png b/Hardware/3D/3D_PCB1_2026-07-05.png new file mode 100644 index 0000000..f73f6a9 Binary files /dev/null and b/Hardware/3D/3D_PCB1_2026-07-05.png differ diff --git a/Hardware/bom/3D_PCB1_2026-07-05.png b/Hardware/bom/3D_PCB1_2026-07-05.png new file mode 100644 index 0000000..f73f6a9 Binary files /dev/null and b/Hardware/bom/3D_PCB1_2026-07-05.png differ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..114486f --- /dev/null +++ b/LICENSE @@ -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. diff --git a/LICENSE/cern_ohl_s_v2.pdf b/LICENSE/cern_ohl_s_v2.pdf deleted file mode 100644 index 39051cb..0000000 Binary files a/LICENSE/cern_ohl_s_v2.pdf and /dev/null differ diff --git a/Software/.cursor/rules/00-core.mdc b/Software/.cursor/rules/00-core.mdc new file mode 100644 index 0000000..e1cad6a --- /dev/null +++ b/Software/.cursor/rules/00-core.mdc @@ -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` (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. diff --git a/Software/.cursor/rules/10-cpp-docs.mdc b/Software/.cursor/rules/10-cpp-docs.mdc new file mode 100644 index 0000000..2999464 --- /dev/null +++ b/Software/.cursor/rules/10-cpp-docs.mdc @@ -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 + * @brief + * + * DigiRadio firmware — https://github.com/manvalan/DigiRadio + * + * Copyright 2026 Michele Bigi + * SPDX-License-Identifier: Apache-2.0 + * + * @author Michele Bigi + * @date + */ +``` + +## Doc block — every class AND every method +Fields, in order, using Doxygen tags (aliases @dname/@pubstate defined +in Doxyfile): +```cpp +/** + * @brief — one-line intent. + * + * @dname + * @param

// per parameter; none -> "none" + * @return // or void / n/a + * @pubstate + * + * Description: intent and contract, NOT a restatement of the code. + * + * @author Michele Bigi + * @date + */ +``` +`doxygen Doxyfile` must exit 0 with an empty warnings log. Do not use +`EXTRACT_ALL = YES` to silence missing-doc warnings. diff --git a/Software/.cursor/rules/20-drivers.mdc b/Software/.cursor/rules/20-drivers.mdc new file mode 100644 index 0000000..7b24947 --- /dev/null +++ b/Software/.cursor/rules/20-drivers.mdc @@ -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. diff --git a/Software/.cursor/rules/30-network-ui.mdc b/Software/.cursor/rules/30-network-ui.mdc new file mode 100644 index 0000000..7499c8f --- /dev/null +++ b/Software/.cursor/rules/30-network-ui.mdc @@ -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). diff --git a/Software/.cursor/rules/40-security.mdc b/Software/.cursor/rules/40-security.mdc new file mode 100644 index 0000000..5393c1d --- /dev/null +++ b/Software/.cursor/rules/40-security.mdc @@ -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. diff --git a/Software/.cursor/rules/50-testing.mdc b/Software/.cursor/rules/50-testing.mdc new file mode 100644 index 0000000..7763d44 --- /dev/null +++ b/Software/.cursor/rules/50-testing.mdc @@ -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. diff --git a/Software/AGENTS.md b/Software/AGENTS.md new file mode 100644 index 0000000..5bfb266 --- /dev/null +++ b/Software/AGENTS.md @@ -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`. +- **`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 + */ +``` + +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 + */ +``` + +**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 + */ +``` + +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` — 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 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. diff --git a/Software/Doxyfile b/Software/Doxyfile new file mode 100644 index 0000000..30655bc --- /dev/null +++ b/Software/Doxyfile @@ -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 diff --git a/Software/LICENSE/LICENSE-Apache-2.0_1.txt b/Software/LICENSE similarity index 100% rename from Software/LICENSE/LICENSE-Apache-2.0_1.txt rename to Software/LICENSE diff --git a/Software/README.md b/Software/README.md deleted file mode 100644 index d2c3f71..0000000 --- a/Software/README.md +++ /dev/null @@ -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. diff --git a/Software/apache-header.txt b/Software/apache-header.txt new file mode 100644 index 0000000..e71f556 --- /dev/null +++ b/Software/apache-header.txt @@ -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. + */ diff --git a/Software/instructions.md b/Software/instructions.md new file mode 100644 index 0000000..69f6f52 --- /dev/null +++ b/Software/instructions.md @@ -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` (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.