diff --git a/README.md b/README.md index 177bd7b..c5be1ba 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ **Open-source digital radio — Si4684 tuner · ADAU1701 SigmaDSP · Bluetooth aptX Adaptive · ESP32-S3** ![Status](https://img.shields.io/badge/hardware-verified-brightgreen) -![Firmware](https://img.shields.io/badge/firmware-0.8.1-blue) +![Firmware](https://img.shields.io/badge/firmware-0.8.2-blue) ![CI](https://github.com/manvalan/DigiRadio/actions/workflows/ci.yml/badge.svg) ![PCB](https://img.shields.io/badge/PCB-6--layer-blue) ![Hardware License](https://img.shields.io/badge/hardware-CERN--OHL--S-lightgrey) @@ -57,7 +57,7 @@ no analogue conversions are introduced between the tuner and the wireless link. - **6-layer impedance-controlled PCB**, 50 × 90 mm, two ground planes - Three antennas (ESP32 2.4 GHz, BT1035 2.4 GHz, FM/DAB SMA) with proper keep-outs -### Firmware (ESP-IDF, C++23 — fw **0.8.1**) +### Firmware (ESP-IDF, C++23 — fw **0.8.2**) | Area | Capability | |------|------------| @@ -67,7 +67,7 @@ no analogue conversions are introduced between the tuner and the wireless link. | **Audio** | 5-band EQ, input mixer, stereo/bass enhancement overlays, profile persist in NVS | | **Presets** | Station list CRUD, reorder, recall with audio profile re-apply, last-preset restore at boot | | **Bluetooth** | Discoverable pairing, A2DP status, disconnect | -| **Network** | SoftAP setup mode, STA provisioning, gzipped SPA (`/`), typed JSON REST API | +| **Network** | SoftAP setup mode, STA provisioning, tabbed gzipped SPA (`/`), typed JSON REST API | | **Security** | NVS-backed credential and preset storage (flash encryption planned — see roadmap) | | **Quality** | **13** host unit tests, Doxygen gate, LaTeX manual sync check, GitHub Actions CI on `main` | @@ -83,6 +83,7 @@ Vertical slices landed on `main` (newest first): | Version | Highlights | |---------|------------| +| **0.8.2** | Tabbed configuration Web UI — now-playing, 6-band EQ, full API coverage | | **0.8.1** | `IntegrationService` — boot preset recall, tune orchestration (tuner + audio + NVS `last_preset`); services stub removed | | **0.8.0** | Preset reorder API/UI; broadcast metadata (RDS + DAB DLS); `readDabServiceData` driver path | | **0.7.1** | CI workflow (host tests, Doxygen, manual sync); Doxygen warnings cleared | @@ -90,8 +91,7 @@ Vertical slices landed on `main` (newest first): | **0.5–0.6** | ADAU1701 runtime EQ/mixer, Si4684 tuning & DAB service list, BT1035 driver | | **0.3–0.4** | Secure store, Wi-Fi provisioning, companion-chip boot, walking skeleton | -**Next up** ([`Software/docs/TODO.md`](Software/docs/TODO.md)): polished configuration Web UI (T6), -Si4684 blob procurement docs (T7), NVS/flash encryption (T8). +**Next up** ([`Software/docs/TODO.md`](Software/docs/TODO.md)): Si4684 blob procurement docs (T7), NVS/flash encryption (T8). --- @@ -221,8 +221,8 @@ Generated C++ API reference: run `doxygen Doxyfile` → `Software/docs/api/html/ | **PCB layout** | 6-layer, DRC clean, plane continuity verified | | **BOM** | Finalised (manufacturable / sourced) | | **Prototype** | In fabrication (PCBWay) | -| **Firmware** | **0.8.1** on `main` — CI green; tuner, DSP, BT, presets, RDS/DLS, integration service | -| **Web UI** | Functional SPA; full polish tracked as T6 | +| **Firmware** | **0.8.2** on `main` — CI green; full Web UI, integration, RDS/DLS | +| **Web UI** | Tabbed SPA covering every REST endpoint | | **Production hardening** | Si4684 blob policy (T7), NVS encryption (T8) — open | Agent task list: [`Software/docs/TODO.md`](Software/docs/TODO.md). diff --git a/Software/README.md b/Software/README.md index d3791f2..fa147ad 100644 --- a/Software/README.md +++ b/Software/README.md @@ -2,8 +2,8 @@ Open-source Hi-Fi DAB+/FM receiver firmware for the ESP32-S3. -**Status:** fw **0.8.1** — `IntegrationService` (boot preset recall, tune + -audio profile); RDS/DLS now-playing metadata; **13** host tests; CI on `main`. +**Status:** fw **0.8.2** — tabbed Web UI (now-playing, 6-band EQ, all APIs); +`IntegrationService`; **13** host tests; CI on `main`. See [`docs/TODO.md`](docs/TODO.md) for the agent task list. ## Quick start @@ -31,6 +31,7 @@ Documentation gates (must exit 0 before merging; also enforced in CI): ```bash doxygen Doxyfile python3 tools/check-manual-sync.py +python3 tools/gzip-www.sh # after editing components/net/www/index.html ``` Manual PDF (design + HTTP API + class reference): @@ -39,7 +40,7 @@ Manual PDF (design + HTTP API + class reference): cd docs/manual && latexmk -lualatex manual.tex ``` -## HTTP API (fw 0.8.1) +## HTTP API (fw 0.8.2) | Method | Path | Purpose | |--------|------|---------| diff --git a/Software/components/net/src/SetupWebServer.cpp b/Software/components/net/src/SetupWebServer.cpp index da51bb1..43f16b7 100644 --- a/Software/components/net/src/SetupWebServer.cpp +++ b/Software/components/net/src/SetupWebServer.cpp @@ -52,7 +52,7 @@ namespace net { namespace { constexpr char kTag[] = "SetupWebServer"; -constexpr char kFirmwareVersion[] = "0.8.1"; +constexpr char kFirmwareVersion[] = "0.8.2"; constexpr unsigned kRebootDelaySec = 3; extern const uint8_t www_index_html_gz_start[] asm( diff --git a/Software/components/net/www/index.html b/Software/components/net/www/index.html index 2624568..e6b900c 100644 --- a/Software/components/net/www/index.html +++ b/Software/components/net/www/index.html @@ -10,14 +10,20 @@ --space-2: 1rem; --space-3: 1.5rem; --space-4: 2.5rem; + --text-sm: 0.8125rem; --text-base: 1rem; --text-lg: 1.25rem; --text-xl: 1.75rem; + --text-2xl: 2.25rem; --accent: #c47a2c; + --accent-dim: #8f5a1f; --bg: #0f1114; --surface: #1a1d22; + --surface-2: #242830; --text: #e8eaed; --muted: #9aa0a6; + --ok: #7cb342; + --err: #e57373; --radius: 0.5rem; --font: system-ui, -apple-system, "Segoe UI", sans-serif; } @@ -29,27 +35,91 @@ background: var(--bg); color: var(--text); min-height: 100vh; - display: flex; - align-items: flex-start; - justify-content: center; - padding: var(--space-3); + padding: var(--space-2); } - main { + .shell { + max-width: 40rem; + margin: 0 auto; + } + header { background: var(--surface); border-radius: var(--radius); - padding: var(--space-4); - max-width: 36rem; - width: 100%; + padding: var(--space-3); border-top: 3px solid var(--accent); + margin-bottom: var(--space-2); + } + header h1 { font-size: var(--text-xl); font-weight: 600; } + .meta { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: var(--space-1) var(--space-2); + margin-top: var(--space-1); + font-size: var(--text-sm); + color: var(--muted); + } + .dot { + width: 0.55rem; + height: 0.55rem; + border-radius: 50%; + background: var(--accent); + flex-shrink: 0; + } + .chips { display: flex; flex-wrap: wrap; gap: var(--space-1); } + .chip { + font-size: 0.75rem; + padding: 0.15rem 0.45rem; + border-radius: 999px; + background: var(--surface-2); + color: var(--muted); + } + .chip.on { color: var(--ok); border: 1px solid var(--ok); } + .chip.off { opacity: 0.55; } + nav.tabs { + display: flex; + flex-wrap: wrap; + gap: 0.35rem; + margin-bottom: var(--space-2); + } + nav.tabs button { + flex: 1 1 auto; + min-width: 4.5rem; + padding: 0.55rem 0.65rem; + border: 1px solid #333; + border-radius: var(--radius); + background: var(--surface); + color: var(--muted); + font: inherit; + font-size: var(--text-sm); + cursor: pointer; + } + nav.tabs button.active { + color: var(--text); + border-color: var(--accent); + background: var(--surface-2); + } + .panel { + display: none; + background: var(--surface); + border-radius: var(--radius); + padding: var(--space-3); + } + .panel.active { display: block; } + h2 { + font-size: var(--text-lg); + font-weight: 600; + margin-bottom: var(--space-1); + } + .lede { + color: var(--muted); + font-size: var(--text-sm); + margin-bottom: var(--space-2); } - h1 { font-size: var(--text-xl); font-weight: 600; margin-bottom: var(--space-2); } - h2 { font-size: var(--text-lg); font-weight: 600; margin-bottom: var(--space-2); } - p { color: var(--muted); margin-bottom: var(--space-2); } label { display: block; - font-size: 0.875rem; + font-size: var(--text-sm); color: var(--muted); - margin-bottom: var(--space-1); + margin-bottom: 0.25rem; } input, select { width: 100%; @@ -61,8 +131,8 @@ color: var(--text); font: inherit; } - input[type="range"] { padding: 0; } - button { + input[type="range"] { padding: 0; accent-color: var(--accent); } + button.action { width: 100%; padding: var(--space-2); border: none; @@ -73,232 +143,631 @@ font-weight: 600; cursor: pointer; } - button.secondary { + button.action.secondary { background: transparent; color: var(--accent); border: 1px solid var(--accent); } - button:disabled { opacity: 0.6; cursor: wait; } - .status { - display: flex; - align-items: center; - gap: var(--space-1); - font-size: var(--text-lg); - margin-bottom: var(--space-3); + button.action:disabled { opacity: 0.55; cursor: wait; } + button.action.compact { + width: auto; + padding: 0.35rem 0.65rem; + font-size: var(--text-sm); + font-weight: 500; } - .dot { - width: 0.6rem; - height: 0.6rem; - border-radius: 50%; - background: var(--accent); + .row { display: flex; gap: var(--space-2); margin-bottom: var(--space-2); } + .row .action { flex: 1; } + .msg { + font-size: var(--text-sm); + margin-top: var(--space-2); + min-height: 1.25rem; } - .msg { font-size: 0.875rem; margin-top: var(--space-2); min-height: 1.25rem; } - .msg.ok { color: #7cb342; } - .msg.err { color: #e57373; } + .msg.ok { color: var(--ok); } + .msg.err { color: var(--err); } code { font-size: 0.9em; background: var(--bg); padding: 0.1em 0.35em; border-radius: 0.25rem; } - section + section { - margin-top: var(--space-4); - padding-top: var(--space-3); - border-top: 1px solid #333; - } - .row { display: flex; gap: var(--space-2); margin-bottom: var(--space-2); } - .row button { flex: 1; } - .tuner-status { - background: var(--bg); - border-radius: var(--radius); - padding: var(--space-2); - font-size: 0.875rem; - margin-bottom: var(--space-2); - white-space: pre-wrap; + .now-title { + font-size: var(--text-2xl); + font-weight: 600; + line-height: 1.2; + margin-bottom: var(--space-1); word-break: break-word; } - .panel { display: none; } - .panel.active { display: block; } - ul.services { + .now-sub { + color: var(--muted); + font-size: var(--text-base); + margin-bottom: var(--space-2); + min-height: 1.5rem; + word-break: break-word; + } + .stat-grid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: var(--space-1); + margin-bottom: var(--space-2); + } + .stat { + background: var(--bg); + border-radius: var(--radius); + padding: var(--space-1) var(--space-2); + font-size: var(--text-sm); + } + .stat span { display: block; color: var(--muted); font-size: 0.75rem; } + .band-panel { display: none; } + .band-panel.show { display: block; } + ul.list { list-style: none; margin-bottom: var(--space-2); - max-height: 12rem; + max-height: 14rem; overflow-y: auto; } - ul.services li { + ul.list li { display: flex; justify-content: space-between; align-items: center; - gap: var(--space-2); + gap: var(--space-1); padding: var(--space-1) 0; border-bottom: 1px solid #2a2d33; - font-size: 0.875rem; + font-size: var(--text-sm); } - ul.services button { - width: auto; - padding: 0.35rem 0.75rem; - font-size: 0.8rem; + ul.list li .label { + flex: 1; + min-width: 0; + word-break: break-word; + } + ul.list li .actions { + display: flex; + flex-wrap: wrap; + gap: 0.25rem; + justify-content: flex-end; + } + .eq-band { + display: grid; + grid-template-columns: 4rem 1fr 3rem; + align-items: center; + gap: var(--space-1); + margin-bottom: var(--space-1); + } + .eq-band label { margin: 0; font-size: 0.75rem; } + .eq-band output { + font-size: var(--text-sm); + text-align: right; + color: var(--muted); + } + .slider-row { + display: grid; + grid-template-columns: 1fr 3.5rem; + align-items: center; + gap: var(--space-1); + margin-bottom: var(--space-1); + } + .slider-row output { + font-size: var(--text-sm); + text-align: right; + color: var(--muted); } -
-

DigiRadio

-
- - Checking health… -
+
+
+

DigiRadio

+
+ + Checking… + +
+
-
-

Wi‑Fi

-

Connect this radio to your network (setup mode).

-
- - - - - -
-

+ + +
+

Now playing

+

Live metadata from RDS (FM) or Dynamic Label (DAB).

+
+
+
+
+ +
-
-

Radio

-

Control the DAB/FM tuner (available after companion chips boot).

-
No status yet.
-
- -
- + -
+ -
-

Bluetooth

-

FSC-BT1035 aptX transmitter — pair headphones or speakers.

-
No status yet.
+ + + -
-

Audio

-

ADAU1701 mixer and master volume (safeload at runtime).

- - - - - - - - - - -
- - -
-

+ -
+ + diff --git a/Software/components/net/www/index.html.gz b/Software/components/net/www/index.html.gz index 913be09..fc5290a 100644 Binary files a/Software/components/net/www/index.html.gz and b/Software/components/net/www/index.html.gz differ diff --git a/Software/docs/TODO.md b/Software/docs/TODO.md index ea3fb3e..1c1ef44 100644 --- a/Software/docs/TODO.md +++ b/Software/docs/TODO.md @@ -12,8 +12,8 @@ errors, no plaintext secrets. Working directory for all commands is `Software/`. -**Current firmware:** `0.8.1` — integration service (preset recall + -audio profile), RDS/DLS metadata, CI gate. +**Current firmware:** `0.8.2` — tabbed configuration Web UI (full API +coverage), integration service, RDS/DLS metadata, CI gate. --- @@ -59,18 +59,11 @@ audio profile re-apply, and last-preset NVS. Stub removed; `app_main` and ## P2 — User interface -### T6. Complete the configuration Web UI -**Why:** `components/net/www/index.html` is functional but the -requirement is an *elegant, essential* SPA. It should cover: -Wi-Fi provisioning, station list management, tuner control (FM/DAB, -seek, play), live EQ/volume/enhancement control, Bluetooth pairing, and -now-playing metadata. -**What:** a minimal single-page app served gzipped from flash, design -tokens defined once (spacing, type scale, one accent), thin client over -the existing typed JSON API. No heavy frameworks. No business logic in -the UI. -**Done when:** every API capability has a UI control; the page is served -gzipped; no debug endpoint is exposed in a shipping build. +### T6. Complete the configuration Web UI — **DONE (fw 0.8.2)** +Tabbed SPA (`Now` / `Radio` / `Presets` / `Audio` / `BT` / `Wi‑Fi`): +now-playing hero with 5 s metadata poll, six-band EQ sliders, all REST +endpoints wired, companion-chip badges, `tools/gzip-www.sh` for the +embedded gzip blob. No debug routes in `SetupWebServer`. --- diff --git a/Software/docs/manual/ch-adau1701.tex b/Software/docs/manual/ch-adau1701.tex index 61c0c72..e66f422 100644 --- a/Software/docs/manual/ch-adau1701.tex +++ b/Software/docs/manual/ch-adau1701.tex @@ -369,10 +369,13 @@ delegate to \texttt{AudioService}: \item \texttt{POST /api/audio/bass-enhance} --- same schema. \end{itemize} -The gzipped setup page (\texttt{components/net/www/index.html}) exposes master -volume, Si4684/ESP32 path levels, and enhancement sliders; saving sends -\texttt{PUT /api/audio/profile}, while slider release on enhancements sends -the dedicated POST routes for immediate safeload + persist. +The gzipped setup page (\texttt{components/net/www/index.html}) is a tabbed +SPA: a \emph{Now playing} view (RDS/DLS metadata), tuner and preset panels, +master/path levels, six PEQ band gain sliders, enhancement overlays, Bluetooth +controls, and Wi-Fi provisioning. Saving sends \texttt{PUT /api/audio/profile}; +enhancement sliders call the dedicated POST routes for immediate safeload + +persist. Regenerate \texttt{index.html.gz} with \texttt{tools/gzip-www.sh} +after editing the HTML source. \section{Typical usage patterns} \label{sec:adau1701-patterns} diff --git a/Software/docs/manual/ch-api.tex b/Software/docs/manual/ch-api.tex index 9c15b72..b0a0e23 100644 --- a/Software/docs/manual/ch-api.tex +++ b/Software/docs/manual/ch-api.tex @@ -36,7 +36,7 @@ Returns a health-check DTO serialised by \begin{drnote}[Response schema] \begin{drcode}[JSON] -{"status":"ok","fw":"0.8.1", +{"status":"ok","fw":"0.8.2", "chips":{"si4684":true,"adau1701":true,"bt1035":true}} \end{drcode} \begin{itemize} diff --git a/Software/tools/gzip-www.sh b/Software/tools/gzip-www.sh new file mode 100755 index 0000000..22fbcda --- /dev/null +++ b/Software/tools/gzip-www.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +# Regenerate the gzipped setup UI embedded by components/net/CMakeLists.txt. +set -euo pipefail +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +SRC="${ROOT}/components/net/www/index.html" +OUT="${ROOT}/components/net/www/index.html.gz" +gzip -c -9 "$SRC" > "$OUT" +echo "Wrote $(wc -c < "$OUT" | tr -d ' ') bytes -> ${OUT#"$ROOT"/}"