|
@@ -0,0 +1,285 @@
|
|
|
|
|
+#pragma once
|
|
|
|
|
+#include <pgmspace.h>
|
|
|
|
|
+
|
|
|
|
|
+static const char PARAMS_HTML[] PROGMEM = R"HTML(
|
|
|
|
|
+<!doctype html>
|
|
|
|
|
+<html lang="en-AU">
|
|
|
|
|
+<head>
|
|
|
|
|
+ <meta charset="utf-8">
|
|
|
|
|
+ <meta name="viewport" content="width=device-width, initial-scale=1">
|
|
|
|
|
+ <link rel="icon" type="image/x-icon" href="/favicon.ico">
|
|
|
|
|
+ <link rel="stylesheet" href="yeti-bootstrap.min.css">
|
|
|
|
|
+ <link rel="stylesheet" href="font-awesome.min.css">
|
|
|
|
|
+ <script src="jquery-3.7.1.slim.min.js" crossorigin="anonymous"></script>
|
|
|
|
|
+
|
|
|
|
|
+ <title>Modulos DSP - Parameter Tuner</title>
|
|
|
|
|
+ <style>
|
|
|
|
|
+ @font-face {
|
|
|
|
|
+ font-family: 'nasalization';
|
|
|
|
|
+ src: url('/nasalization-rg.woff2') format('woff2');
|
|
|
|
|
+ }
|
|
|
|
|
+ .nasalization { font-family: 'nasalization'; }
|
|
|
|
|
+ .font-monospace { font-family: monospace; }
|
|
|
|
|
+ .cur-sub { font-size: 0.72rem; color: #6c757d; }
|
|
|
|
|
+ td.val-cell { line-height: 1.3; font-size: 0.85rem; }
|
|
|
|
|
+ </style>
|
|
|
|
|
+</head>
|
|
|
|
|
+
|
|
|
|
|
+<body class="bg-light" style="padding-top: 5rem;">
|
|
|
|
|
+ <nav class="navbar navbar-expand-md navbar-dark bg-dark fixed-top">
|
|
|
|
|
+ <div class="container">
|
|
|
|
|
+ <a class="navbar-brand nasalization text-uppercase" href="/">
|
|
|
|
|
+ <img src="/logo-horizontal.webp" height="30" class="d-inline-block align-top" loading="lazy">
|
|
|
|
|
+ </a>
|
|
|
|
|
+ <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navNav"
|
|
|
|
|
+ aria-controls="navNav" aria-expanded="false" aria-label="Toggle navigation">
|
|
|
|
|
+ <span class="navbar-toggler-icon"></span>
|
|
|
|
|
+ </button>
|
|
|
|
|
+ <div class="collapse navbar-collapse" id="navNav">
|
|
|
|
|
+ <span class="col align-self-end text-end text-white">
|
|
|
|
|
+ <span>{{IP}}</span>
|
|
|
|
|
+ <i class="text-success fas fa-wifi"></i>
|
|
|
|
|
+ </span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </nav>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="container pb-5">
|
|
|
|
|
+ <div class="row mb-2">
|
|
|
|
|
+ <div class="col">
|
|
|
|
|
+ <h2 class="pt-2 font-weight-bold">Parameter Tuner</h2>
|
|
|
|
|
+ <p class="text-muted small mb-2">
|
|
|
|
|
+ Read and write DSP parameter RAM (0x0000–0x03FF) in real time using safeload —
|
|
|
|
|
+ no audio glitches on a live DSP. Parameter list is saved to browser storage.
|
|
|
|
|
+ </p>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="mb-3 d-flex flex-wrap gap-2 align-items-center">
|
|
|
|
|
+ <button class="btn btn-primary btn-sm" onclick="addRow()">
|
|
|
|
|
+ <i class="fa fa-plus"></i> Add Parameter
|
|
|
|
|
+ </button>
|
|
|
|
|
+ <button class="btn btn-outline-secondary btn-sm" onclick="readAll()">
|
|
|
|
|
+ <i class="fa fa-refresh"></i> Read All
|
|
|
|
|
+ </button>
|
|
|
|
|
+ <button class="btn btn-outline-danger btn-sm" onclick="clearRows()">
|
|
|
|
|
+ <i class="fa fa-trash"></i> Clear All
|
|
|
|
|
+ </button>
|
|
|
|
|
+ <a href="/" class="btn btn-outline-secondary btn-sm ms-auto">
|
|
|
|
|
+ <i class="fa fa-arrow-left"></i> Device Management
|
|
|
|
|
+ </a>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="card">
|
|
|
|
|
+ <div class="table-responsive">
|
|
|
|
|
+ <table class="table table-sm table-hover align-middle mb-0" id="paramTable">
|
|
|
|
|
+ <thead class="table-dark">
|
|
|
|
|
+ <tr>
|
|
|
|
|
+ <th style="width:88px">Address</th>
|
|
|
|
|
+ <th>Label</th>
|
|
|
|
|
+ <th style="width:148px">Current Value</th>
|
|
|
|
|
+ <th style="width:118px">New Value</th>
|
|
|
|
|
+ <th style="width:130px">Mode</th>
|
|
|
|
|
+ <th style="width:90px"></th>
|
|
|
|
|
+ </tr>
|
|
|
|
|
+ </thead>
|
|
|
|
|
+ <tbody id="paramRows"></tbody>
|
|
|
|
|
+ </table>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="card-footer text-muted small" id="statusBar">
|
|
|
|
|
+ No parameters added. Click <strong>Add Parameter</strong> to begin.
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <footer class="fixed-bottom mt-auto py-3 border-top bg-dark">
|
|
|
|
|
+ <div class="container">
|
|
|
|
|
+ <p class="text-white text-center mb-0">
|
|
|
|
|
+ Copyright © <span id="year"></span> – Modulos Audio – DSP Controller – All Rights Reserved
|
|
|
|
|
+ </p>
|
|
|
|
|
+ <script>document.getElementById('year').innerHTML = new Date().getFullYear();</script>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </footer>
|
|
|
|
|
+
|
|
|
|
|
+ <script src="/bootstrap.bundle.min.js" crossorigin="anonymous"></script>
|
|
|
|
|
+ <script>
|
|
|
|
|
+var rowCount = 0;
|
|
|
|
|
+
|
|
|
|
|
+function addRow(addr, label) {
|
|
|
|
|
+ var id = ++rowCount;
|
|
|
|
|
+ var addrVal = (addr !== undefined) ? addr : '';
|
|
|
|
|
+ var labelVal = (label !== undefined) ? label : '';
|
|
|
|
|
+
|
|
|
|
|
+ var tr = document.createElement('tr');
|
|
|
|
|
+ tr.id = 'row-' + id;
|
|
|
|
|
+ tr.innerHTML =
|
|
|
|
|
+ '<td>' +
|
|
|
|
|
+ '<input type="text" class="form-control form-control-sm font-monospace" ' +
|
|
|
|
|
+ 'id="addr-' + id + '" value="' + addrVal + '" placeholder="0x0000" ' +
|
|
|
|
|
+ 'style="width:78px" oninput="saveState()">' +
|
|
|
|
|
+ '</td>' +
|
|
|
|
|
+ '<td>' +
|
|
|
|
|
+ '<input type="text" class="form-control form-control-sm" ' +
|
|
|
|
|
+ 'id="lbl-' + id + '" value="' + labelVal + '" placeholder="e.g. Master Volume" ' +
|
|
|
|
|
+ 'oninput="saveState()">' +
|
|
|
|
|
+ '</td>' +
|
|
|
|
|
+ '<td class="val-cell" id="cur-' + id + '">—</td>' +
|
|
|
|
|
+ '<td>' +
|
|
|
|
|
+ '<input type="text" class="form-control form-control-sm font-monospace" ' +
|
|
|
|
|
+ 'id="val-' + id + '" placeholder="0.0" ' +
|
|
|
|
|
+ 'onkeydown="if(event.key===\'Enter\')writeParam(' + id + ')">' +
|
|
|
|
|
+ '</td>' +
|
|
|
|
|
+ '<td>' +
|
|
|
|
|
+ '<select class="form-select form-select-sm" id="mode-' + id + '" ' +
|
|
|
|
|
+ 'onchange="modeChanged(' + id + ')">' +
|
|
|
|
|
+ '<option value="float">Float (5.23)</option>' +
|
|
|
|
|
+ '<option value="hex">Hex (8 chars)</option>' +
|
|
|
|
|
+ '</select>' +
|
|
|
|
|
+ '</td>' +
|
|
|
|
|
+ '<td class="text-nowrap">' +
|
|
|
|
|
+ '<button class="btn btn-outline-primary btn-sm me-1" onclick="readParam(' + id + ')" title="Read">' +
|
|
|
|
|
+ '<i class="fa fa-download"></i>' +
|
|
|
|
|
+ '</button>' +
|
|
|
|
|
+ '<button class="btn btn-outline-success btn-sm me-1" onclick="writeParam(' + id + ')" title="Write">' +
|
|
|
|
|
+ '<i class="fa fa-upload"></i>' +
|
|
|
|
|
+ '</button>' +
|
|
|
|
|
+ '<button class="btn btn-outline-danger btn-sm" onclick="removeRow(' + id + ')" title="Remove">' +
|
|
|
|
|
+ '<i class="fa fa-times"></i>' +
|
|
|
|
|
+ '</button>' +
|
|
|
|
|
+ '</td>';
|
|
|
|
|
+
|
|
|
|
|
+ document.getElementById('paramRows').appendChild(tr);
|
|
|
|
|
+ updateStatusBar();
|
|
|
|
|
+ saveState();
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function removeRow(id) {
|
|
|
|
|
+ var r = document.getElementById('row-' + id);
|
|
|
|
|
+ if (r) r.remove();
|
|
|
|
|
+ updateStatusBar();
|
|
|
|
|
+ saveState();
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function clearRows() {
|
|
|
|
|
+ if (!confirm('Remove all parameters?')) return;
|
|
|
|
|
+ document.getElementById('paramRows').innerHTML = '';
|
|
|
|
|
+ rowCount = 0;
|
|
|
|
|
+ updateStatusBar();
|
|
|
|
|
+ saveState();
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function modeChanged(id) {
|
|
|
|
|
+ var ph = document.getElementById('mode-' + id).value === 'hex' ? '00000000' : '0.0';
|
|
|
|
|
+ document.getElementById('val-' + id).placeholder = ph;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function getAddr(id) {
|
|
|
|
|
+ var v = document.getElementById('addr-' + id).value.trim();
|
|
|
|
|
+ if (v.startsWith('0x') || v.startsWith('0X')) v = v.substring(2);
|
|
|
|
|
+ var n = parseInt(v, 16);
|
|
|
|
|
+ if (isNaN(n) || n < 0 || n > 0x03FF) return null;
|
|
|
|
|
+ return n;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function readParam(id) {
|
|
|
|
|
+ var addr = getAddr(id);
|
|
|
|
|
+ var el = document.getElementById('cur-' + id);
|
|
|
|
|
+ if (addr === null) {
|
|
|
|
|
+ el.innerHTML = '<span class="text-danger small">Bad address</span>';
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ el.innerHTML = '<i class="fa fa-spinner fa-spin text-muted"></i>';
|
|
|
|
|
+ var xhr = new XMLHttpRequest();
|
|
|
|
|
+ xhr.open('GET', '/param?addr=' + addr, true);
|
|
|
|
|
+ xhr.timeout = 2000;
|
|
|
|
|
+ xhr.onload = function() {
|
|
|
|
|
+ if (xhr.status === 200) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ var d = JSON.parse(xhr.responseText);
|
|
|
|
|
+ el.innerHTML =
|
|
|
|
|
+ '<span class="font-monospace">' + d.float.toFixed(7) + '</span>' +
|
|
|
|
|
+ '<br><span class="cur-sub font-monospace">' + d.hex + '</span>';
|
|
|
|
|
+ } catch(e) { el.textContent = 'Parse error'; }
|
|
|
|
|
+ } else {
|
|
|
|
|
+ el.innerHTML = '<span class="text-danger small">I²C error</span>';
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+ xhr.ontimeout = xhr.onerror = function() {
|
|
|
|
|
+ el.innerHTML = '<span class="text-danger small">Offline</span>';
|
|
|
|
|
+ };
|
|
|
|
|
+ xhr.send();
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function writeParam(id) {
|
|
|
|
|
+ var addr = getAddr(id);
|
|
|
|
|
+ if (addr === null) return;
|
|
|
|
|
+ var val = document.getElementById('val-' + id).value.trim();
|
|
|
|
|
+ var mode = document.getElementById('mode-' + id).value;
|
|
|
|
|
+ if (!val) return;
|
|
|
|
|
+
|
|
|
|
|
+ var xhr = new XMLHttpRequest();
|
|
|
|
|
+ xhr.open('POST', '/param', true);
|
|
|
|
|
+ xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
|
|
|
|
|
+ xhr.onload = function() {
|
|
|
|
|
+ if (xhr.status === 200) {
|
|
|
|
|
+ readParam(id);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ document.getElementById('cur-' + id).innerHTML =
|
|
|
|
|
+ '<span class="text-danger small">' + xhr.responseText + '</span>';
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+ xhr.onerror = function() {
|
|
|
|
|
+ document.getElementById('cur-' + id).innerHTML =
|
|
|
|
|
+ '<span class="text-danger small">Request failed</span>';
|
|
|
|
|
+ };
|
|
|
|
|
+ xhr.send('addr=' + addr + '&value=' + encodeURIComponent(val) + '&mode=' + mode);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function readAll() {
|
|
|
|
|
+ var rows = document.getElementById('paramRows').children;
|
|
|
|
|
+ for (var i = 0; i < rows.length; i++) {
|
|
|
|
|
+ readParam(parseInt(rows[i].id.replace('row-', '')));
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function updateStatusBar() {
|
|
|
|
|
+ var n = document.getElementById('paramRows').children.length;
|
|
|
|
|
+ var bar = document.getElementById('statusBar');
|
|
|
|
|
+ bar.innerHTML = n === 0
|
|
|
|
|
+ ? 'No parameters added. Click <strong>Add Parameter</strong> to begin.'
|
|
|
|
|
+ : n + ' parameter' + (n === 1 ? '' : 's') +
|
|
|
|
|
+ ' — click <strong>Read All</strong> to refresh values.';
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function saveState() {
|
|
|
|
|
+ var rows = [];
|
|
|
|
|
+ var els = document.getElementById('paramRows').children;
|
|
|
|
|
+ for (var i = 0; i < els.length; i++) {
|
|
|
|
|
+ var id = parseInt(els[i].id.replace('row-', ''));
|
|
|
|
|
+ rows.push({
|
|
|
|
|
+ addr: document.getElementById('addr-' + id).value,
|
|
|
|
|
+ label: document.getElementById('lbl-' + id).value
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ try { localStorage.setItem('modulos_dsp_params', JSON.stringify(rows)); } catch(e) {}
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function loadState() {
|
|
|
|
|
+ try {
|
|
|
|
|
+ var s = localStorage.getItem('modulos_dsp_params');
|
|
|
|
|
+ if (s) {
|
|
|
|
|
+ var rows = JSON.parse(s);
|
|
|
|
|
+ if (rows.length > 0) {
|
|
|
|
|
+ rows.forEach(function(r) { addRow(r.addr, r.label); });
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch(e) {}
|
|
|
|
|
+ addRow();
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+loadState();
|
|
|
|
|
+ </script>
|
|
|
|
|
+</body>
|
|
|
|
|
+</html>
|
|
|
|
|
+)HTML";
|