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**

-
+



@@ -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…
-
+
+
-
- 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.
-
-
-
-
+
+ Tuner
+ DAB ensemble tune, service play, or FM frequency and seek.
-
-
+
-
-
-
+
+
+
-
+
-
-
+
-
-
-
+
+
+
-
-
+
-
- Bluetooth
- FSC-BT1035 aptX transmitter — pair headphones or speakers.
- No status yet.
+
+
+
+ Bluetooth
+ FSC-BT1035 aptX — pair headphones or speakers.
+
+
+
+
-
-
+
+
-
+