Files
DigiRadio/Software/components/net/www/index.html
T
micheleandCursor fa532e068f Add stereo and bass enhance APIs via PEQ overlay.
Expose POST /api/audio/stereo-enhance and bass-enhance with 0–100 levels,
persist enhancements in AudioProfile, and document the virtual EQ mapping in
the manual and SigmaStudio chapter.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-06 17:00:12 +02:00

528 lines
20 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>DigiRadio</title>
<style>
:root {
--space-1: 0.5rem;
--space-2: 1rem;
--space-3: 1.5rem;
--space-4: 2.5rem;
--text-base: 1rem;
--text-lg: 1.25rem;
--text-xl: 1.75rem;
--accent: #c47a2c;
--bg: #0f1114;
--surface: #1a1d22;
--text: #e8eaed;
--muted: #9aa0a6;
--radius: 0.5rem;
--font: system-ui, -apple-system, "Segoe UI", sans-serif;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: var(--font);
font-size: var(--text-base);
line-height: 1.5;
background: var(--bg);
color: var(--text);
min-height: 100vh;
display: flex;
align-items: flex-start;
justify-content: center;
padding: var(--space-3);
}
main {
background: var(--surface);
border-radius: var(--radius);
padding: var(--space-4);
max-width: 36rem;
width: 100%;
border-top: 3px solid var(--accent);
}
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;
color: var(--muted);
margin-bottom: var(--space-1);
}
input, select {
width: 100%;
padding: var(--space-1) var(--space-2);
margin-bottom: var(--space-2);
border: 1px solid #333;
border-radius: var(--radius);
background: var(--bg);
color: var(--text);
font: inherit;
}
input[type="range"] { padding: 0; }
button {
width: 100%;
padding: var(--space-2);
border: none;
border-radius: var(--radius);
background: var(--accent);
color: var(--bg);
font: inherit;
font-weight: 600;
cursor: pointer;
}
button.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);
}
.dot {
width: 0.6rem;
height: 0.6rem;
border-radius: 50%;
background: var(--accent);
}
.msg { font-size: 0.875rem; margin-top: var(--space-2); min-height: 1.25rem; }
.msg.ok { color: #7cb342; }
.msg.err { color: #e57373; }
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;
word-break: break-word;
}
.panel { display: none; }
.panel.active { display: block; }
ul.services {
list-style: none;
margin-bottom: var(--space-2);
max-height: 12rem;
overflow-y: auto;
}
ul.services li {
display: flex;
justify-content: space-between;
align-items: center;
gap: var(--space-2);
padding: var(--space-1) 0;
border-bottom: 1px solid #2a2d33;
font-size: 0.875rem;
}
ul.services button {
width: auto;
padding: 0.35rem 0.75rem;
font-size: 0.8rem;
}
</style>
</head>
<body>
<main>
<h1>DigiRadio</h1>
<div class="status" id="health">
<span class="dot" aria-hidden="true"></span>
<span>Checking health…</span>
</div>
<section id="wifi-section">
<h2>WiFi</h2>
<p>Connect this radio to your network (setup mode).</p>
<form id="wifi-form">
<label for="ssid">Network name (SSID)</label>
<input id="ssid" name="ssid" autocomplete="off" required maxlength="32">
<label for="password">Password</label>
<input id="password" name="password" type="password" autocomplete="off" maxlength="63">
<button type="submit" id="save-btn">Save &amp; connect</button>
</form>
<p class="msg" id="wifi-msg" aria-live="polite"></p>
</section>
<section id="tuner-section">
<h2>Radio</h2>
<p>Control the DAB/FM tuner (available after companion chips boot).</p>
<div class="tuner-status" id="tuner-status">No status yet.</div>
<div class="row">
<button type="button" class="secondary" id="refresh-status">Refresh status</button>
</div>
<label for="band">Band</label>
<select id="band">
<option value="dab">DAB</option>
<option value="fm">FM</option>
</select>
<div id="panel-dab" class="panel active">
<label for="dab-index">Ensemble index (037)</label>
<input id="dab-index" type="number" min="0" max="37" value="12">
<button type="button" id="tune-dab">Tune DAB</button>
<div class="row" style="margin-top: var(--space-2);">
<button type="button" class="secondary" id="load-services">Load services</button>
</div>
<ul class="services" id="service-list"></ul>
</div>
<div id="panel-fm" class="panel">
<label for="fm-khz">Frequency (kHz)</label>
<input id="fm-khz" type="number" min="64000" max="108000" step="100" value="101500">
<button type="button" id="tune-fm">Tune FM</button>
<div class="row" style="margin-top: var(--space-2);">
<button type="button" class="secondary" id="seek-fm">Seek up</button>
</div>
</div>
<p class="msg" id="tuner-msg" aria-live="polite"></p>
</section>
<section id="audio-section">
<h2>Audio</h2>
<p>ADAU1701 mixer and master volume (safeload at runtime).</p>
<label for="master-db">Master (dB)</label>
<input id="master-db" type="range" min="-60" max="12" step="0.5" value="0">
<label for="si4684-db">Radio path Si4684 (dB)</label>
<input id="si4684-db" type="range" min="-60" max="12" step="0.5" value="0">
<label for="esp32-db">ESP32 path (dB)</label>
<input id="esp32-db" type="range" min="-60" max="12" step="0.5" value="0">
<label for="stereo-level">Stereo depth (0100)</label>
<input id="stereo-level" type="range" min="0" max="100" step="1" value="0">
<label for="bass-level">Bass enhance (0100)</label>
<input id="bass-level" type="range" min="0" max="100" step="1" value="0">
<div class="row">
<button type="button" id="save-audio">Save profile</button>
<button type="button" class="secondary" id="reset-audio">Reset flat</button>
</div>
<p class="msg" id="audio-msg" aria-live="polite"></p>
</section>
</main>
<script>
function showMsg(el, text, ok) {
el.textContent = text;
el.className = "msg " + (ok ? "ok" : "err");
}
function formatTunerStatus(s) {
var lines = [
"Booted: " + s.booted,
"Band: " + s.band,
"Locked: " + s.locked,
"Volume: " + s.volume
];
if (s.dab) {
lines.push("DAB index: " + (s.dab.freq_index != null ? s.dab.freq_index : "—"));
if (s.dab.fic_quality != null) lines.push("FIC: " + s.dab.fic_quality);
}
if (s.fm) {
if (s.fm.frequency_khz != null) lines.push("FM: " + s.fm.frequency_khz + " kHz");
if (s.fm.rssi_dbuv != null) lines.push("RSSI: " + s.fm.rssi_dbuv + " dBµV");
if (s.fm.stereo != null) lines.push("Stereo: " + s.fm.stereo);
}
return lines.join("\n");
}
fetch("/api/health")
.then(function (r) { return r.json(); })
.then(function (d) {
document.getElementById("health").innerHTML =
'<span class="dot"></span><span>Status: <code>' +
d.status + "</code> · FW <code>" + d.fw + "</code></span>";
})
.catch(function () {
document.getElementById("health").textContent = "Health check failed.";
});
document.getElementById("wifi-form").addEventListener("submit", function (e) {
e.preventDefault();
var btn = document.getElementById("save-btn");
var msg = document.getElementById("wifi-msg");
btn.disabled = true;
showMsg(msg, "Saving…", true);
fetch("/api/wifi", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
ssid: document.getElementById("ssid").value,
password: document.getElementById("password").value
})
})
.then(function (r) { return r.json().then(function (d) { return { ok: r.ok, d: d }; }); })
.then(function (res) {
if (res.ok && res.d.status === "saved") {
showMsg(msg, "Saved. Rebooting in " + res.d.reboot_in_sec + "s…", true);
} else {
showMsg(msg, "Error: " + (res.d.reason || "unknown"), false);
btn.disabled = false;
}
})
.catch(function () {
showMsg(msg, "Request failed.", false);
btn.disabled = false;
});
});
document.getElementById("band").addEventListener("change", function () {
var dab = document.getElementById("panel-dab");
var fm = document.getElementById("panel-fm");
if (this.value === "dab") {
dab.classList.add("active");
fm.classList.remove("active");
} else {
fm.classList.add("active");
dab.classList.remove("active");
}
});
function refreshTunerStatus() {
var msg = document.getElementById("tuner-msg");
return fetch("/api/tuner/status")
.then(function (r) { return r.json().then(function (d) { return { ok: r.ok, d: d }; }); })
.then(function (res) {
if (res.ok && res.d.booted != null) {
document.getElementById("tuner-status").textContent = formatTunerStatus(res.d);
if (res.d.band === "fm") {
document.getElementById("band").value = "fm";
document.getElementById("band").dispatchEvent(new Event("change"));
}
showMsg(msg, "", true);
} else {
showMsg(msg, "Status error: " + (res.d.reason || "unknown"), false);
}
})
.catch(function () { showMsg(msg, "Status request failed.", false); });
}
document.getElementById("refresh-status").addEventListener("click", refreshTunerStatus);
document.getElementById("tune-dab").addEventListener("click", function () {
var msg = document.getElementById("tuner-msg");
var idx = parseInt(document.getElementById("dab-index").value, 10);
fetch("/api/tuner/tune", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ band: "dab", freq_index: idx })
})
.then(function (r) { return r.json().then(function (d) { return { ok: r.ok, d: d }; }); })
.then(function (res) {
if (res.ok && res.d.booted != null) {
document.getElementById("tuner-status").textContent = formatTunerStatus(res.d);
showMsg(msg, "DAB tuned.", true);
} else {
showMsg(msg, "Tune failed: " + (res.d.reason || "unknown"), false);
}
})
.catch(function () { showMsg(msg, "Tune request failed.", false); });
});
document.getElementById("tune-fm").addEventListener("click", function () {
var msg = document.getElementById("tuner-msg");
var khz = parseInt(document.getElementById("fm-khz").value, 10);
fetch("/api/tuner/tune", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ band: "fm", frequency_khz: khz })
})
.then(function (r) { return r.json().then(function (d) { return { ok: r.ok, d: d }; }); })
.then(function (res) {
if (res.ok && res.d.booted != null) {
document.getElementById("tuner-status").textContent = formatTunerStatus(res.d);
showMsg(msg, "FM tuned.", true);
} else {
showMsg(msg, "Tune failed: " + (res.d.reason || "unknown"), false);
}
})
.catch(function () { showMsg(msg, "Tune request failed.", false); });
});
document.getElementById("seek-fm").addEventListener("click", function () {
var msg = document.getElementById("tuner-msg");
fetch("/api/tuner/seek", { method: "POST" })
.then(function (r) { return r.json().then(function (d) { return { ok: r.ok, d: d }; }); })
.then(function (res) {
if (res.ok && res.d.frequency_khz != null) {
document.getElementById("fm-khz").value = res.d.frequency_khz;
showMsg(msg, "Seeked to " + res.d.frequency_khz + " kHz.", true);
refreshTunerStatus();
} else {
showMsg(msg, "Seek failed: " + (res.d.reason || "unknown"), false);
}
})
.catch(function () { showMsg(msg, "Seek request failed.", false); });
});
document.getElementById("load-services").addEventListener("click", function () {
var msg = document.getElementById("tuner-msg");
var list = document.getElementById("service-list");
list.innerHTML = "";
fetch("/api/tuner/services")
.then(function (r) { return r.json().then(function (d) { return { ok: r.ok, d: d }; }); })
.then(function (res) {
if (!res.ok || !res.d.services) {
showMsg(msg, "Services error: " + (res.d.reason || "unknown"), false);
return;
}
res.d.services.forEach(function (svc) {
var li = document.createElement("li");
var label = document.createElement("span");
label.textContent = svc.label || ("SID " + svc.service_id);
var btn = document.createElement("button");
btn.textContent = "Play";
btn.addEventListener("click", function () {
fetch("/api/tuner/play", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
service_id: svc.service_id,
component_id: svc.component_id
})
})
.then(function (r) { return r.json(); })
.then(function (d) {
if (d.status === "playing") {
showMsg(msg, "Playing " + (svc.label || svc.service_id), true);
} else {
showMsg(msg, "Play failed: " + (d.reason || "unknown"), false);
}
})
.catch(function () { showMsg(msg, "Play request failed.", false); });
});
li.appendChild(label);
li.appendChild(btn);
list.appendChild(li);
});
showMsg(msg, res.d.services.length + " service(s).", true);
})
.catch(function () { showMsg(msg, "Services request failed.", false); });
});
refreshTunerStatus();
var audioProfile = null;
function loadAudioProfile() {
var msg = document.getElementById("audio-msg");
return fetch("/api/audio/profile")
.then(function (r) { return r.json(); })
.then(function (d) {
audioProfile = d;
document.getElementById("master-db").value = d.master.left_db;
document.getElementById("si4684-db").value = d.mixer.si4684_left_db;
document.getElementById("esp32-db").value = d.mixer.esp32_left_db;
if (d.enhancements) {
document.getElementById("stereo-level").value = d.enhancements.stereo_level;
document.getElementById("bass-level").value = d.enhancements.bass_level;
}
showMsg(msg, "", true);
})
.catch(function () { showMsg(msg, "Audio profile load failed.", false); });
}
document.getElementById("save-audio").addEventListener("click", function () {
var msg = document.getElementById("audio-msg");
if (!audioProfile) {
showMsg(msg, "Profile not loaded.", false);
return;
}
var master = parseFloat(document.getElementById("master-db").value, 10);
var si4684 = parseFloat(document.getElementById("si4684-db").value, 10);
var esp32 = parseFloat(document.getElementById("esp32-db").value, 10);
audioProfile.master.left_db = master;
audioProfile.master.right_db = master;
audioProfile.mixer.si4684_left_db = si4684;
audioProfile.mixer.si4684_right_db = si4684;
audioProfile.mixer.esp32_left_db = esp32;
audioProfile.mixer.esp32_right_db = esp32;
if (!audioProfile.enhancements) {
audioProfile.enhancements = { stereo_level: 0, bass_level: 0 };
}
audioProfile.enhancements.stereo_level =
parseInt(document.getElementById("stereo-level").value, 10);
audioProfile.enhancements.bass_level =
parseInt(document.getElementById("bass-level").value, 10);
fetch("/api/audio/profile", {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(audioProfile)
})
.then(function (r) { return r.json().then(function (d) { return { ok: r.ok, d: d }; }); })
.then(function (res) {
if (res.ok && res.d.status === "saved") {
showMsg(msg, "Audio profile saved.", true);
} else {
showMsg(msg, "Save failed: " + (res.d.reason || "unknown"), false);
}
})
.catch(function () { showMsg(msg, "Save request failed.", false); });
});
function postEnhance(url, level, msg) {
fetch(url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ level: level })
})
.then(function (r) { return r.json().then(function (d) { return { ok: r.ok, d: d }; }); })
.then(function (res) {
if (res.ok && res.d.status === "saved") {
showMsg(msg, "Enhancement applied.", true);
} else {
showMsg(msg, "Enhance failed: " + (res.d.reason || "unknown"), false);
}
})
.catch(function () { showMsg(msg, "Enhance request failed.", false); });
}
document.getElementById("stereo-level").addEventListener("change", function () {
var msg = document.getElementById("audio-msg");
var level = parseInt(document.getElementById("stereo-level").value, 10);
postEnhance("/api/audio/stereo-enhance", level, msg);
});
document.getElementById("bass-level").addEventListener("change", function () {
var msg = document.getElementById("audio-msg");
var level = parseInt(document.getElementById("bass-level").value, 10);
postEnhance("/api/audio/bass-enhance", level, msg);
});
document.getElementById("reset-audio").addEventListener("click", function () {
var msg = document.getElementById("audio-msg");
fetch("/api/audio/reset", { method: "POST" })
.then(function (r) { return r.json().then(function (d) { return { ok: r.ok, d: d }; }); })
.then(function (res) {
if (res.ok && res.d.status === "saved") {
showMsg(msg, "Factory-flat profile applied.", true);
loadAudioProfile();
} else {
showMsg(msg, "Reset failed: " + (res.d.reason || "unknown"), false);
}
})
.catch(function () { showMsg(msg, "Reset request failed.", false); });
});
loadAudioProfile();
</script>
</body>
</html>