From c0eee4a4ad548176fa03b16880e2a8faf07dde19 Mon Sep 17 00:00:00 2001 From: Michele Bigi Date: Wed, 19 Aug 2026 09:23:36 +0200 Subject: [PATCH] Firmware review pass: BT1035 boot retry, BluetoothJson tests, doc catch-up MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit BT1035Driver::boot() had zero retry on the reset+AT-init sequence — a single hardware RESET# pulse followed immediately by AT commands, with no second attempt if the module didn't come up in time. This is the most plausible explanation for the intermittent "no spontaneous UART bytes after hardware reset" / "AT init failed" boot failures logged in docs/si4684-rf-investigation-report.md and observed again live this morning on otherwise-identical hardware/wiring — classic power-up timing jitter, not a permanent fault. Extracted the reset+ init sequence into resetAndInitOnce() and wrapped it in a 3-attempt retry loop with a short delay between attempts; the one-time GPIO config and UART driver install stay outside the loop since they don't need repeating. Root cause of the underlying jitter is still open. BluetoothJson.hpp was the only *Json.hpp module in the core with zero host test coverage (status/scan/paired serialisation, auto-reconnect/ connect/speaker parsing) — every sibling module already has one. Added bluetooth_json_test.cpp following the existing tuner_json_test pattern; ctest now covers 20 suites instead of 19. Documentation catch-up, found doing a full firmware re-review at the user's request: - POST /api/tuner/calibrate-antenna and the antcap field on POST /api/tuner/tune (added in a previous commit, never documented) are now in ch-api.tex. - kFirmwareVersion was still hardcoded "0.8.5" despite the RF fixes, BLE provisioning, phone streaming, antenna calibration, and generic DSP param API landed since that version's actual release commit (0a1188a). Bumped to 0.9.0 everywhere it's mentioned (health JSON, the manual's title page, intro, classes, and API chapters). - instructions.md and docs/TODO.md still described the firmware as frozen at 0.8.5 awaiting hardware-in-the-loop testing that has since happened extensively; docs/TODO.md's H5 verdict specifically still said "suspect U6 RF ground (re-open PCBWay)" for a bug that turned out to be firmware, not hardware — actively misleading, corrected. Both files now summarise the post-0.8.5 HIL findings and current open items (BT1035 root cause, intermittent HTTP unresponsiveness under load, antenna-limited signal quality, possibly-undersized 24 KB nvs partition). Verified: idf.py build, doxygen (0 warnings), check-manual-sync, check_si4684_blobs, ctest (20/20), two-pass xelatex manual build all green. Flashed and confirmed live: fw reports 0.9.0, BT1035 booted on the first attempt post-flash. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_0178rASQ6ZETPMUamvpoR2KR --- Software/components/core/test/CMakeLists.txt | 4 + .../core/test/bluetooth_json_test.cpp | 220 ++++++++++++++++++ .../bt1035/include/bt1035/Bt1035Driver.hpp | 1 + .../drivers/bt1035/src/Bt1035Driver.cpp | 47 +++- .../components/net/src/SetupWebServer.cpp | 2 +- Software/docs/TODO.md | 50 +++- Software/docs/manual/ch-api.tex | 39 +++- Software/docs/manual/ch-classes.tex | 2 +- Software/docs/manual/ch-intro.tex | 2 +- Software/docs/manual/manual.tex | 2 +- Software/instructions.md | 54 ++++- 11 files changed, 393 insertions(+), 30 deletions(-) create mode 100644 Software/components/core/test/bluetooth_json_test.cpp diff --git a/Software/components/core/test/CMakeLists.txt b/Software/components/core/test/CMakeLists.txt index 32a576c..784f300 100644 --- a/Software/components/core/test/CMakeLists.txt +++ b/Software/components/core/test/CMakeLists.txt @@ -94,6 +94,10 @@ add_executable(dsp_param_json_test dsp_param_json_test.cpp) target_link_libraries(dsp_param_json_test PRIVATE digiradio_core) add_test(NAME dsp_param_json_test COMMAND dsp_param_json_test) +add_executable(bluetooth_json_test bluetooth_json_test.cpp) +target_link_libraries(bluetooth_json_test PRIVATE digiradio_core) +add_test(NAME bluetooth_json_test COMMAND bluetooth_json_test) + add_executable(frequency_khz_test frequency_khz_test.cpp) target_link_libraries(frequency_khz_test PRIVATE digiradio_core) add_test(NAME frequency_khz_test COMMAND frequency_khz_test) diff --git a/Software/components/core/test/bluetooth_json_test.cpp b/Software/components/core/test/bluetooth_json_test.cpp new file mode 100644 index 0000000..29f0a6e --- /dev/null +++ b/Software/components/core/test/bluetooth_json_test.cpp @@ -0,0 +1,220 @@ +/** + * @file bluetooth_json_test.cpp + * @brief Host tests for Bluetooth JSON parse/serialise. + * + * DigiRadio firmware — https://github.com/manvalan/DigiRadio + * + * Copyright 2026 Michele Bigi + * SPDX-License-Identifier: Apache-2.0 + * + * @author Michele Bigi + * @date 2026-08-19 + */ + +#include "core/BluetoothJson.hpp" +#include "core/ParseError.hpp" + +#include +#include +#include + +namespace { + +[[nodiscard]] bool expectEqual(const std::string& actual, + const std::string& expected) +{ + if (actual == expected) { + return true; + } + std::cerr << "expected: " << expected << "\nactual: " << actual << '\n'; + return false; +} + +[[nodiscard]] int runStatusSerialiseTest() +{ + const core::BluetoothStatus status{ + .booted = true, + .pairing = false, + .a2dpState = core::Bt1035A2dpState::Streaming, + .deviceName = "DigiRadio-CC4DB4", + .autoReconnect = 3U, + }; + const std::string json = core::serializeBluetoothStatusJson(status); + if (!expectEqual(json, + R"({"booted":true,"pairing":false,"a2dp":"streaming",)" + R"("device_name":"DigiRadio-CC4DB4","auto_reconnect":3})")) { + return EXIT_FAILURE; + } + return EXIT_SUCCESS; +} + +[[nodiscard]] int runScanSerialiseTest() +{ + const std::vector devices{ + core::Bt1035ScannedDevice{ + .index = 1U, + .addressType = 2U, + .mac = "001122334455", + .rssiDbm = -58, + .name = "Bose SoundLink", + .deviceClass = "240404", + }, + }; + const std::string json = core::serializeBluetoothScanJson(devices); + if (!expectEqual(json, + R"({"devices":[{"index":1,"mac":"001122334455",)" + R"("name":"Bose SoundLink","rssi_dbm":-58}]})")) { + return EXIT_FAILURE; + } + return EXIT_SUCCESS; +} + +[[nodiscard]] int runPairedSerialiseTest() +{ + const std::vector devices{ + core::Bt1035PairedDevice{.index = 1U, .mac = "AABBCCDDEEFF", .name = "Phone"}, + }; + const std::string json = core::serializeBluetoothPairedJson(devices); + if (!expectEqual(json, + R"({"devices":[{"index":1,"mac":"AABBCCDDEEFF","name":"Phone"}]})")) { + return EXIT_FAILURE; + } + return EXIT_SUCCESS; +} + +[[nodiscard]] int runAutoReconnectParseTest() +{ + const auto ok = core::parseBluetoothAutoReconnectJson(R"({"times":5})"); + if (!ok || *ok != 5U) { + std::cerr << "auto-reconnect valid parse failed\n"; + return EXIT_FAILURE; + } + const auto tooHigh = core::parseBluetoothAutoReconnectJson(R"({"times":16})"); + if (tooHigh) { + std::cerr << "auto-reconnect out-of-range accepted\n"; + return EXIT_FAILURE; + } + const auto missing = core::parseBluetoothAutoReconnectJson(R"({})"); + if (missing || missing.error() != core::ParseError::MissingField) { + std::cerr << "auto-reconnect missing field mis-reported\n"; + return EXIT_FAILURE; + } + return EXIT_SUCCESS; +} + +[[nodiscard]] int runConnectJsonParseTest() +{ + const auto ok = + core::parseBluetoothConnectJson(R"({"mac":"001122334455"})"); + if (!ok || *ok != "001122334455") { + std::cerr << "connect mac parse failed\n"; + return EXIT_FAILURE; + } + // Normalises to uppercase. + const auto lower = + core::parseBluetoothConnectJson(R"({"mac":"aabbccddeeff"})"); + if (!lower || *lower != "AABBCCDDEEFF") { + std::cerr << "connect mac uppercasing failed\n"; + return EXIT_FAILURE; + } + const auto invalid = core::parseBluetoothConnectJson(R"({"mac":"not-a-mac"})"); + if (invalid) { + std::cerr << "connect invalid mac accepted\n"; + return EXIT_FAILURE; + } + return EXIT_SUCCESS; +} + +[[nodiscard]] int runConnectRequestParseTest() +{ + const auto request = core::parseBluetoothConnectRequest( + R"({"mac":"001122334455","name":"Bose SoundLink","save":true})"); + if (request.mac != "001122334455" || request.name != "Bose SoundLink" + || !request.save) { + std::cerr << "connect request full parse failed\n"; + return EXIT_FAILURE; + } + const auto minimal = + core::parseBluetoothConnectRequest(R"({"mac":"001122334455"})"); + if (minimal.mac != "001122334455" || !minimal.name.empty() + || minimal.save) { + std::cerr << "connect request minimal parse failed\n"; + return EXIT_FAILURE; + } + const auto badMac = core::parseBluetoothConnectRequest(R"({})"); + if (!badMac.mac.empty()) { + std::cerr << "connect request missing mac should stay empty\n"; + return EXIT_FAILURE; + } + return EXIT_SUCCESS; +} + +[[nodiscard]] int runSpeakerRoundTripTest() +{ + const core::BtSpeakerTarget target{.mac = "001122334455", + .name = "Bose SoundLink"}; + const std::string json = core::serializeBluetoothSpeakerJson(&target); + if (!expectEqual(json, + R"({"configured":true,"mac":"001122334455",)" + R"("name":"Bose SoundLink"})")) { + return EXIT_FAILURE; + } + const std::string unset = core::serializeBluetoothSpeakerJson(nullptr); + if (!expectEqual(unset, R"({"configured":false})")) { + return EXIT_FAILURE; + } + + const auto parsed = core::parseBluetoothSpeakerJson( + R"({"mac":"001122334455","name":"Bose SoundLink"})"); + if (!parsed || parsed->mac != "001122334455" + || parsed->name != "Bose SoundLink") { + std::cerr << "speaker parse round-trip failed\n"; + return EXIT_FAILURE; + } + const auto invalid = core::parseBluetoothSpeakerJson(R"({"mac":"bad"})"); + if (invalid) { + std::cerr << "speaker parse invalid mac accepted\n"; + return EXIT_FAILURE; + } + return EXIT_SUCCESS; +} + +[[nodiscard]] int runErrorSerialiseTest() +{ + const std::string json = core::serializeBluetoothErrorJson("scan_failed"); + if (!expectEqual(json, R"({"status":"error","reason":"scan_failed"})")) { + return EXIT_FAILURE; + } + return EXIT_SUCCESS; +} + +} // namespace + +int main() +{ + if (runStatusSerialiseTest() != EXIT_SUCCESS) { + return EXIT_FAILURE; + } + if (runScanSerialiseTest() != EXIT_SUCCESS) { + return EXIT_FAILURE; + } + if (runPairedSerialiseTest() != EXIT_SUCCESS) { + return EXIT_FAILURE; + } + if (runAutoReconnectParseTest() != EXIT_SUCCESS) { + return EXIT_FAILURE; + } + if (runConnectJsonParseTest() != EXIT_SUCCESS) { + return EXIT_FAILURE; + } + if (runConnectRequestParseTest() != EXIT_SUCCESS) { + return EXIT_FAILURE; + } + if (runSpeakerRoundTripTest() != EXIT_SUCCESS) { + return EXIT_FAILURE; + } + if (runErrorSerialiseTest() != EXIT_SUCCESS) { + return EXIT_FAILURE; + } + return EXIT_SUCCESS; +} diff --git a/Software/components/drivers/bt1035/include/bt1035/Bt1035Driver.hpp b/Software/components/drivers/bt1035/include/bt1035/Bt1035Driver.hpp index 5e02254..3d65d06 100644 --- a/Software/components/drivers/bt1035/include/bt1035/Bt1035Driver.hpp +++ b/Software/components/drivers/bt1035/include/bt1035/Bt1035Driver.hpp @@ -343,6 +343,7 @@ public: private: [[nodiscard]] std::expected ensureBooted() const; [[nodiscard]] std::expected runInitSequence(); + [[nodiscard]] std::expected resetAndInitOnce(); [[nodiscard]] std::expected transmitAndCollect( std::string_view commandLine, int timeoutMs = kResponseTimeoutMs); [[nodiscard]] std::expected transmitAndCollectUntil( diff --git a/Software/components/drivers/bt1035/src/Bt1035Driver.cpp b/Software/components/drivers/bt1035/src/Bt1035Driver.cpp index 208cf90..86274a2 100644 --- a/Software/components/drivers/bt1035/src/Bt1035Driver.cpp +++ b/Software/components/drivers/bt1035/src/Bt1035Driver.cpp @@ -38,6 +38,13 @@ constexpr int kUartTxBuffer = 256; constexpr int kResponseTimeoutMs = 2000; constexpr int kPostResetMs = 500; constexpr int kPostUartMs = 100; +/** Observed intermittently: the module sometimes needs a second RESET# + * pulse to come up (power-up timing jitter between cold/warm boots) — + * a single attempt with no retry was found to explain sporadic total + * boot failures ("no spontaneous UART bytes" -> "AT init failed") on + * otherwise-identical hardware/wiring. */ +constexpr int kBootAttempts = 3; +constexpr int kBootRetryDelayMs = 300; void flushUartRx(int uartPort) noexcept { @@ -904,6 +911,21 @@ std::expected Bt1035Driver::runInitSequence() return {}; } +std::expected Bt1035Driver::resetAndInitOnce() +{ + gpio_set_level(static_cast(pins_.sysCtlGpio), 1); + gpio_set_level(static_cast(pins_.resetGpio), 0); + vTaskDelay(pdMS_TO_TICKS(100)); + gpio_set_level(static_cast(pins_.resetGpio), 1); + vTaskDelay(pdMS_TO_TICKS(kPostResetMs)); + + logRawUartBoot(uartPort_); + uart_flush_input(static_cast(uartPort_)); + vTaskDelay(pdMS_TO_TICKS(kPostUartMs)); + + return runInitSequence(); +} + std::expected Bt1035Driver::boot() { if (booted_) { @@ -924,12 +946,6 @@ std::expected Bt1035Driver::boot() return std::unexpected(Bt1035Error::ResetFailed); } - gpio_set_level(static_cast(pins_.sysCtlGpio), 1); - gpio_set_level(static_cast(pins_.resetGpio), 0); - vTaskDelay(pdMS_TO_TICKS(100)); - gpio_set_level(static_cast(pins_.resetGpio), 1); - vTaskDelay(pdMS_TO_TICKS(kPostResetMs)); - if (!uartInstalled_) { const uart_config_t uartCfg = { .baud_rate = kBaudRate, @@ -961,12 +977,19 @@ std::expected Bt1035Driver::boot() uartInstalled_ = true; } - logRawUartBoot(uartPort_); - uart_flush_input(static_cast(uartPort_)); - vTaskDelay(pdMS_TO_TICKS(kPostUartMs)); - - if (auto init = runInitSequence(); !init) { - ESP_LOGE(kTag, "AT init failed"); + std::expected init = std::unexpected(Bt1035Error::UnexpectedResponse); + for (int attempt = 1; attempt <= kBootAttempts; ++attempt) { + init = resetAndInitOnce(); + if (init) { + break; + } + ESP_LOGW(kTag, "boot attempt %d/%d failed", attempt, kBootAttempts); + if (attempt < kBootAttempts) { + vTaskDelay(pdMS_TO_TICKS(kBootRetryDelayMs)); + } + } + if (!init) { + ESP_LOGE(kTag, "AT init failed after %d attempts", kBootAttempts); return init; } diff --git a/Software/components/net/src/SetupWebServer.cpp b/Software/components/net/src/SetupWebServer.cpp index 1522730..5cdf1c7 100644 --- a/Software/components/net/src/SetupWebServer.cpp +++ b/Software/components/net/src/SetupWebServer.cpp @@ -68,7 +68,7 @@ namespace net { namespace { constexpr char kTag[] = "SetupWebServer"; -constexpr char kFirmwareVersion[] = "0.8.5"; +constexpr char kFirmwareVersion[] = "0.9.0"; constexpr unsigned kRebootDelaySec = 3; extern const uint8_t index_html_gz_start[] asm( diff --git a/Software/docs/TODO.md b/Software/docs/TODO.md index 8e61cf5..128dc06 100644 --- a/Software/docs/TODO.md +++ b/Software/docs/TODO.md @@ -3,9 +3,13 @@ Agent task list and hardware-in-the-loop backlog. Working directory for all commands is `Software/`. -**Current firmware:** `0.8.5` — BT1035 I2S slave boot init, dual OTA + DSP blob updates, EEPROM identity, -NVS + flash encryption (dev mode), tabbed Web UI with System uploads, CI gate -(4 jobs). +**Current firmware:** `0.9.0` — everything in 0.8.5, plus: Si4684 RF +blackout root-caused and fixed (real FM/DAB lock and audio on real +hardware), DAB service list fixed (two rounds), FM ANTCAP antenna +calibration persisted to EEPROM, generic ADAU1701 parameter API, phone PCM +streaming, BLE Wi-Fi provisioning, full FM band scan, BT1035 boot retry. +See "Post-0.8.5 hardware-in-the-loop findings" below and +`docs/si4684-rf-investigation-report.md` for the full story. **Before writing code, read `AGENTS.md`, `.cursor/rules/`, and `instructions.md`.** Definition of Done: Apache header, doc blocks, @@ -62,10 +66,42 @@ verify ADAU replay after reboot. After H1 passes, trial build with `sdkconfig.defaults.production` overlay on a sacrificial unit; confirm RELEASE mode policy before shipping. -### H5. Si4684 FM/DAB no-lock — blob integrity checked, verdict: hardware -**Verdict (2026-08-13): blob OK → suspect U6 RF ground (re-open PCBWay)**, not -a firmware/blob defect. Full investigation, evidence, and the two byte-offset -bugs found/fixed while verifying this: [`docs/si4684-rf-investigation-report.md`](si4684-rf-investigation-report.md). +### H5. Si4684 FM/DAB no-lock — RESOLVED, was firmware after all +**Superseded verdict (2026-08-13): blob OK → suspected U6 RF ground, PCBWay +dispute opened.** That verdict was wrong. The actual cause was +`writeCommand()`'s ARG1 byte being mis-offset across FM/DAB tune, seek, and +several status/ack commands — the chip always answered correctly, so every +signal pointed at hardware, but it never actually tuned. Fixed; real FM +lock, real DAB ensemble lock, real audio confirmed live on the same board. +No PCB rework was needed. Full investigation, the wrong initial verdict, +and the eventual root cause: [`docs/si4684-rf-investigation-report.md`](si4684-rf-investigation-report.md). + +--- + +## Post-0.8.5 hardware-in-the-loop findings + +The board arrived and testing against it (not just host tests) found real +bugs the host-testable core couldn't catch, since they live in +ESP-IDF-only drivers. Full detail and evidence in +[`docs/si4684-rf-investigation-report.md`](si4684-rf-investigation-report.md). +Short version: + +- Si4684 total RF blackout (H5 above) — firmware bug, fixed. +- Si4684→ADAU1701 digital audio silence — `PIN_CONFIG_ENABLE` mutual + exclusion + `SerialInputRegister` polarity, fixed. +- DAB service list empty/garbled — response-parsing offset bugs (two + rounds) plus `DAB_EVENT_INTERRUPT_SOURCE` (0xB300) never configured, + fixed. +- FM front-end auto-tune measurably suboptimal on this board's actual + matching network — ANTCAP calibration swept and persisted to EEPROM, + `POST /api/tuner/calibrate-antenna`. +- BT1035 intermittent boot failure — root cause still unknown, but a + reset+init retry loop (up to 3 attempts) was added since the failure + looked like power-up timing jitter, not a permanent fault. +- **Still open**: BT1035 root cause; intermittent multi-second HTTP + unresponsiveness under load; DAB signal quality still antenna-limited; + 24 KB `nvs` partition may be undersized (`saveProfile()` `store_failed` + seen intermittently, error code never captured). --- diff --git a/Software/docs/manual/ch-api.tex b/Software/docs/manual/ch-api.tex index 1231f26..0a92232 100644 --- a/Software/docs/manual/ch-api.tex +++ b/Software/docs/manual/ch-api.tex @@ -6,7 +6,7 @@ implemented in \texttt{SetupWebServer}. Request bodies are parsed into domain types in the pure core (\texttt{components/core}) before any persistence or driver call. Exact C++ signatures live in the generated Doxygen output under \texttt{docs/api/}; this chapter documents the -wire protocol and behaviour as shipped in firmware~0.8.5. +wire protocol and behaviour as shipped in firmware~0.9.0. \section{Transport and reachability} @@ -36,7 +36,7 @@ Returns a health-check DTO serialised by \begin{drnote}[Response schema] \begin{drcode}[JSON] -{"status":"ok","fw":"0.8.5","serialNumber":"0004A3123456", +{"status":"ok","fw":"0.9.0","serialNumber":"0004A3123456", "chips":{"si4684":true,"adau1701":true,"bt1035":true}} \end{drcode} \begin{itemize} @@ -172,7 +172,13 @@ or {"band":"fm","frequency_khz":101500} \end{drcode} DAB \texttt{freq\_index} is 0--37; FM \texttt{frequency\_khz} is -64\,000--108\,000. +64\,000--108\,000. FM accepts an optional \texttt{antcap} (0--128): +overrides the front-end antenna varactor for this one tune, e.g. while +running a calibration sweep. Omit it (the normal case) to use the board's +saved calibration --- see +\texttt{POST /api/tuner/calibrate-antenna} +(Section~\ref{sec:api-tuner-calibrate-antenna}) --- or hardware auto-tune +if never calibrated. Ignored for \texttt{"dab"}. \end{drnote} On success returns the updated status JSON (same shape as @@ -267,6 +273,33 @@ per-station poll budget ran out. HTTP status: \textbf{200 OK}; \textbf{409} on a hardware/driver failure mid-sweep; \textbf{503} when the tuner service is unavailable. +\apiendpoint{POST}{/api/tuner/calibrate-antenna} +\label{sec:api-tuner-calibrate-antenna} + +Commits an FM ANTCAP value (AN851 Appendix A front-end calibration) as the +board's permanent default, found by sweeping \texttt{antcap} on +\texttt{POST /api/tuner/tune} (Section~\ref{sec:api-tuner-tune}) across +0--128 and comparing \texttt{rssi\_dbuv}/\texttt{snr\_db}. Persists to the +24AA025E48 EEPROM's user-writable region (separate from the factory-locked +EUI-48) and takes effect immediately on the live tuner --- no reboot +required, though it also survives one since \texttt{HardwareBootstrap::boot()} +reloads it at every boot. Once saved, every ordinary FM tune (seek, scan, +station recall, live UI) uses this value automatically instead of the +chip's own front-end auto-tune, unless a request explicitly overrides +\texttt{antcap} for that one call. + +\begin{drnote}[Request schema] +\begin{drcode}[JSON] +{"antcap":102} +\end{drcode} +\texttt{antcap} is required, 0--128. +\end{drnote} + +Success response: \texttt{\{"status":"saved","antcap":102\}}. HTTP status: +\textbf{200 OK}; \textbf{400} for invalid/missing \texttt{antcap}; +\textbf{500} on an EEPROM write failure; \textbf{503} when the tuner +service is unavailable. + \apiendpoint{GET}{/api/audio/profile} \label{sec:api-audio-profile-get} diff --git a/Software/docs/manual/ch-classes.tex b/Software/docs/manual/ch-classes.tex index 524b1dc..afcaf20 100644 --- a/Software/docs/manual/ch-classes.tex +++ b/Software/docs/manual/ch-classes.tex @@ -16,7 +16,7 @@ added in the same change that introduces the class. A tooling check keeps this chapter in step with the code, so it is always current. \end{drnote} -The class reference tracks firmware~0.8.5 on \texttt{main}. Public classes +The class reference tracks firmware~0.9.0 on \texttt{main}. Public classes are grouped by layer: domain core, application services, and hardware drivers. % ------------------------------------------------------------------ diff --git a/Software/docs/manual/ch-intro.tex b/Software/docs/manual/ch-intro.tex index 3274d99..f5a1bf1 100644 --- a/Software/docs/manual/ch-intro.tex +++ b/Software/docs/manual/ch-intro.tex @@ -4,7 +4,7 @@ DigiRadio is an open-source, high-fidelity digital radio receiver. It receives DAB+ and FM broadcasts, processes the audio through a dedicated signal processor, and streams the result over Bluetooth using a -high-resolution codec. Firmware~0.8.5 on \texttt{main} provides encrypted +high-resolution codec. Firmware~0.9.0 on \texttt{main} provides encrypted storage, a tabbed configuration web UI, and the full REST API documented in Chapter~\ref{ch:api}. The whole project --- hardware and firmware --- is released as open source for the maker and audio community to study, diff --git a/Software/docs/manual/manual.tex b/Software/docs/manual/manual.tex index 45bd431..c7157c5 100644 --- a/Software/docs/manual/manual.tex +++ b/Software/docs/manual/manual.tex @@ -30,7 +30,7 @@ \vfill {\color{drInk}\large Michele Bigi\par} \vspace{2mm} -{\color{drGray}Firmware 0.8.5 \quad\textbullet\quad 2026\par} +{\color{drGray}Firmware 0.9.0 \quad\textbullet\quad 2026\par} \vspace{2mm} {\color{drGray}Hardware: CERN-OHL-S v2 \quad\textbullet\quad Firmware: Apache-2.0\par} \vspace{2mm} diff --git a/Software/instructions.md b/Software/instructions.md index a3f0af2..8d1a8c3 100644 --- a/Software/instructions.md +++ b/Software/instructions.md @@ -4,8 +4,8 @@ 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 the current state on `main`. -**Firmware on `main`:** **0.8.5** — agent tasks T1–T12 complete; device HIL -pending PCB arrival. +**Firmware on `main`:** **0.9.0** — agent tasks T1–T12 complete; device HIL +now largely done on the first real PCB (see below), not pending anymore. ## What DigiRadio is @@ -53,8 +53,54 @@ Repository: https://github.com/manvalan/DigiRadio | T8 NVS encryption | Done (0.8.3) | `initEncryptedStorage`; HIL when PCB ready | | T9–T12 Platform | Done (0.8.4) | Dual OTA, EEPROM identity, DSP + firmware OTA | -Next work: **hardware-in-the-loop** (`docs/TODO.md` § P4), not new features -unless the user requests them. +## Post-0.8.5 HIL work (first real PCB, not in the table above) + +The board arrived and most of Slice 3–8's HIL assumptions turned out to be +wrong in ways that needed real fixes, not just testing. Summary (full +detail in `docs/si4684-rf-investigation-report.md`): + +- **Si4684 total RF blackout, root-caused and fixed.** `writeCommand()`'s + ARG1 byte was mis-offset across FM/DAB tune, seek, DAB service commands, + and several ARG1-only status/ack commands — the chip answered every + command correctly but never actually tuned. Real FM lock, real DAB + ensemble lock (3+ ensembles), real audio confirmed live. +- **Si4684→ADAU1701 digital audio silence, fixed.** `PIN_CONFIG_ENABLE` + had both I2SOUTEN and DACOUTEN set (chip falls back to unused analog + out per AN649); `SerialInputRegister` IBP polarity was wrong (ADAU + sampling on the wrong BCLK edge). +- **DAB service list, two rounds of offset bugs fixed.** Response-parsing + offsets were wrong in a way that made `GET_DIGITAL_SERVICE_LIST` return + empty/garbled; confirmed live with 22 real, correctly-decoded station + labels. Also found `DAB_EVENT_INTERRUPT_SOURCE` (property 0xB300) was + never configured, so the service-list-ready event could never fire. +- **BT1035 boot failure is intermittent, not fixed at the root** — the + module sometimes sends zero UART bytes after a hardware reset. Made + non-fatal early on; a retry loop (up to 3 reset+init attempts) was added + once the timing-jitter theory held up, but the underlying cause is + still open. +- **FM front-end calibration.** The board's actual matching network + differs from the AN851 reference the chip's auto-tune constants assume. + Swept ANTCAP (AN851 Appendix A) and found a fixed override that beats + auto-tune by 6–11 dB RSSI/SNR across the whole band; persisted to the + 24AA025E48 EEPROM (`POST /api/tuner/calibrate-antenna`) and applied by + default to every FM tune. +- **New features, not in the original Slice plan:** full FM band scan + (`POST /api/tuner/scan/full`), generic ADAU1701 parameter access + (`GET`/`PUT /api/dsp/param`, an escape hatch onto any SigmaStudio cell + beyond the curated mixer/EQ API), phone PCM streaming + (`PUT /api/stream/phone`), BLE Wi-Fi provisioning + (`net::ble_provisioning`, ESP-IDF's own `wifi_provisioning` over the + ESP32-S3's onboard BLE, additive alongside the SoftAP), web radio + streaming stutter fix (batched I2S writes). +- **Still open**: BT1035 root cause; intermittent multi-second HTTP + unresponsiveness under load (candidate cause: a blocking Si4684 SPI wait + colliding with `max_open_sockets=3`); DAB signal quality still + antenna-limited even after calibration; NVS partition (24 KB) may be + undersized given the accumulated write traffic (`saveProfile()` + `store_failed` seen intermittently, never root-caused). + +Next work: keep chasing the open items above as the user prioritises them, +not new features unless requested. - **Blockers first** — state risks before solutions. - **One vertical slice at a time** — `main` always builds; host tests green.