From 8d826b4d73b2dd047c49b99905e283a48182e8c2 Mon Sep 17 00:00:00 2001 From: Michele Bigi Date: Tue, 7 Jul 2026 07:36:06 +0200 Subject: [PATCH] Complete configuration Web UI with tabbed SPA (fw 0.8.2). Add now-playing view with metadata polling, six-band EQ controls, full REST coverage, companion-chip badges, and gzip-www helper for the embedded setup page. Co-authored-by: Cursor --- README.md | 14 +- Software/README.md | 7 +- .../components/net/src/SetupWebServer.cpp | 2 +- Software/components/net/www/index.html | 1210 ++++++++++------- Software/components/net/www/index.html.gz | Bin 5427 -> 7192 bytes Software/docs/TODO.md | 21 +- Software/docs/manual/ch-adau1701.tex | 11 +- Software/docs/manual/ch-api.tex | 2 +- Software/tools/gzip-www.sh | 8 + 9 files changed, 765 insertions(+), 510 deletions(-) create mode 100755 Software/tools/gzip-www.sh 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 913be0928eb8046ab06f63dc6fa18e0181e46728..fc5290ae2e1907d017424a436b566237df58f416 100644 GIT binary patch literal 7192 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 ad$=e55xc{^dA^8_Zv8K%1T{HnhyVa4_8!py literal 5427 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 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"/}"