Pārlūkot izejas kodu

Add Runtime parameters

Benjamin Harris 1 mēnesi atpakaļ
vecāks
revīzija
e976fb3815
4 mainītis faili ar 415 papildinājumiem un 14 dzēšanām
  1. 81 0
      ModulosDSP_101.ino
  2. 34 14
      README.md
  3. 15 0
      index_html.h
  4. 285 0
      params_html.h

+ 81 - 0
ModulosDSP_101.ino

@@ -88,6 +88,7 @@ WebServer  httpServer(80);
 #include "index_html.h"
 #include "ota_html.h"
 #include "ap_html.h"
+#include "params_html.h"
 
 hd44780_I2Cexp lcd;
 static bool s_lcdOk = false;
@@ -589,6 +590,83 @@ static void handleDspStatus() {
   httpServer.send(allOk ? 200 : 500, "application/json", buf);
 }
 
+//=============================================================
+// HTTP parameter tuner  GET /params  GET /param  POST /param
+//=============================================================
+static void handleParamsPage() {
+  String html = FPSTR(PARAMS_HTML);
+  html.replace("{{IP}}", WiFi.localIP().toString());
+  httpServer.send(200, "text/html; charset=utf-8", html);
+}
+
+static void handleParamGet() {
+  if (!httpServer.hasArg("addr")) {
+    httpServer.send(400, "text/plain", "Missing 'addr'.");
+    return;
+  }
+  long addr = strtol(httpServer.arg("addr").c_str(), nullptr, 0);
+  if (addr < 0 || addr > (long)DSP_PARAM_RAM_END) {
+    httpServer.send(400, "text/plain", "addr must be 0x0000-0x03FF.");
+    return;
+  }
+
+  uint8_t raw[4] = {0, 0, 0, 0};
+  bool ok = dspReadBlock((uint16_t)addr, raw, 4);
+
+  // 5.23 fixed-point → float
+  int32_t fixed = ((int32_t)raw[0] << 24) | ((int32_t)raw[1] << 16) |
+                  ((int32_t)raw[2] <<  8) |  (int32_t)raw[3];
+  float fval = (float)fixed / 8388608.0f;
+
+  char buf[128];
+  snprintf(buf, sizeof(buf),
+    "{\"ok\":%s,\"addr\":%ld,\"hex\":\"0x%02X%02X%02X%02X\",\"float\":%.7f}",
+    ok ? "true" : "false", addr,
+    raw[0], raw[1], raw[2], raw[3], fval);
+  httpServer.send(ok ? 200 : 500, "application/json", buf);
+}
+
+static void handleParamSet() {
+  if (!httpServer.hasArg("addr") || !httpServer.hasArg("value")) {
+    httpServer.send(400, "text/plain", "Missing 'addr' or 'value'.");
+    return;
+  }
+  long addr = strtol(httpServer.arg("addr").c_str(), nullptr, 0);
+  if (addr < 0 || addr > (long)DSP_PARAM_RAM_END) {
+    httpServer.send(400, "text/plain", "addr must be 0x0000-0x03FF.");
+    return;
+  }
+
+  String mode = httpServer.arg("mode");
+  DSPWriter dspWriter;
+
+  if (mode == "hex") {
+    String hexStr = httpServer.arg("value");
+    if (hexStr.startsWith("0x") || hexStr.startsWith("0X")) hexStr = hexStr.substring(2);
+    hexStr.replace(" ", "");
+    hexStr.replace("_", "");
+    if (hexStr.length() != 8) {
+      httpServer.send(400, "text/plain", "Hex value must be exactly 8 hex characters.");
+      return;
+    }
+    uint32_t v = strtoul(hexStr.c_str(), nullptr, 16);
+    uint8_t sl[5] = { 0x00,
+      (uint8_t)((v >> 24) & 0xFF),
+      (uint8_t)((v >> 16) & 0xFF),
+      (uint8_t)((v >>  8) & 0xFF),
+      (uint8_t)( v        & 0xFF) };
+    dspWriter.safeload_writeRegister((uint16_t)addr, sl, true);
+    Serial.printf("Param write (hex) addr=0x%04lX val=0x%08X\n", addr, v);
+  } else {
+    // Float mode — safeload_writeRegister handles 5.23 conversion internally
+    float fval = httpServer.arg("value").toFloat();
+    dspWriter.safeload_writeRegister((uint16_t)addr, fval, true);
+    Serial.printf("Param write (float) addr=0x%04lX val=%.7f\n", addr, fval);
+  }
+
+  httpServer.send(200, "text/plain", "OK");
+}
+
 //=============================================================
 // HTTP WiFi reset handler  POST /wifi_reset
 //=============================================================
@@ -765,6 +843,9 @@ void setup() {
   httpServer.on("/gpio",       HTTP_GET,  handleGpioGet);
   httpServer.on("/gpio",       HTTP_POST, handleGpioSet);
   httpServer.on("/wifi_reset", HTTP_POST, handleWifiReset);
+  httpServer.on("/params",     HTTP_GET,  handleParamsPage);
+  httpServer.on("/param",      HTTP_GET,  handleParamGet);
+  httpServer.on("/param",      HTTP_POST, handleParamSet);
   httpServer.onNotFound([]() {
     String uri = httpServer.uri();
     if (streamFromFS(uri)) return;

+ 34 - 14
README.md

@@ -1,19 +1,36 @@
 # ModulosDSP — ESP32-S3 WiFi Bridge for ADAU14xx DSP
 
-An Arduino sketch for the **Waveshare ESP32-S3 Zero** that provides a WiFi interface to Analog Devices ADAU1401/1701 DSP chips:
-
-- **SigmaTCP bridge** (port 8086) — lets SigmaStudio connect wirelessly to a physical DSP over I²C
-- **HTTP EEPROM uploader** (port 80, path `/`) — flash DSP firmware binaries to a 24C256 EEPROM from a browser
-- **HTTP OTA updater** (port 80, path `/ota`) — update the ESP32 firmware itself over WiFi
-
-This sketch runs on the companion **MSD ADAU14-1701 Adapter** PCB.
+An Arduino sketch for the **Waveshare ESP32-S3 Zero** that provides a WiFi interface to Analog Devices ADAU1401/1701 DSP chips. Runs on the companion **MSD ADAU14-1701 Adapter** PCB.
+
+## Feature Summary
+
+| Feature | Endpoint / Port | Description |
+| ------- | --------------- | ----------- |
+| **SigmaTCP bridge** | TCP 8086 | Wireless SigmaStudio ↔ DSP bridge over I²C |
+| **EEPROM upload** | `POST /upload` | Flash DSP firmware to 24C256 with optional CRC32 verify |
+| **OTA firmware update** | `POST /ota_do` | Flash new ESP32 firmware over WiFi |
+| **DSP live status** | `GET /dsp_status` | Core Register, running flag, GPIO, ADC 0–3 — polled every 2 s |
+| **DSP soft reset** | `POST /dsp_reset` | Restart DSP execution without reloading EEPROM |
+| **GPIO register control** | `GET/POST /gpio` | Read and write GPIO All Register; GP0–GP3 toggle buttons in UI |
+| **Runtime parameter tuner** | `GET/POST /param` | Read/write any param RAM address live; safeload writes for glitch-free updates |
+| **NVS credential storage** | — | WiFi credentials stored in NVS; survive reboots |
+| **AP config portal** | SoftAP `ModulosDSP-Setup` | Browser-based WiFi setup on first boot or connect failure |
+| **WiFi credential reset** | `POST /wifi_reset` | Clear NVS and reboot into AP setup mode |
+| **mDNS hostname** | — | Device reachable at `modulos-dsp.local` (Bonjour/Zeroconf) |
+| **WiFi watchdog** | — | Auto-reconnect every 5 s; restores TCP socket and mDNS on recovery |
+| **TCP idle timeout** | — | Sessions auto-close after 30 s of inactivity |
+| **Safeload flush** | — | Pending safeload slots flushed at session start and disconnect |
+| **I²C error propagation** | — | Write failures reported in SigmaTCP ACK; logged to Serial |
+| **50 KB TCP receive buffer** | — | Decoupled drain/process loop prevents TCP ZeroWindow stall |
+| **NeoPixel status LED** | — | Colour-coded activity indicator (see Status LED section) |
+| **Optional LCD** | — | 20×4 hd44780 LCD for boot status and IP; graceful fallback if absent |
 
 ---
 
 ## Hardware
 
 | Component | Part | Notes |
-|-----------|------|-------|
+| --------- | ---- | ----- |
 | Microcontroller | Waveshare ESP32-S3 Zero | Onboard WS2812 NeoPixel on GPIO 21 |
 | DSP | ADAU1401 / ADAU1701 | I²C address 0x68 (7-bit: 0x34) |
 | EEPROM | 24C256 (32 KB) | I²C address 0xA0 (7-bit: 0x50) |
@@ -22,7 +39,7 @@ This sketch runs on the companion **MSD ADAU14-1701 Adapter** PCB.
 ### Pin Assignments
 
 | Signal | GPIO |
-|--------|------|
+| ------ | ---- |
 | I²C SDA | 13 |
 | I²C SCL | 12 |
 | Status LED (NeoPixel) | 21 (built-in) |
@@ -36,7 +53,7 @@ The DSP, EEPROM, and LCD all share the same I²C bus at 400 kHz. The LCD is opti
 Install via **Sketch → Include Library → Manage Libraries**:
 
 | Library | Purpose |
-|---------|---------|
+| ------- | ------- |
 | `WiFi` | ESP32 WiFi (built-in with ESP32 core) |
 | `WebServer` | HTTP server (built-in) |
 | `Wire` | I²C master (built-in) |
@@ -65,7 +82,7 @@ Credentials are stored in NVS (non-volatile flash) using the `Preferences` libra
 
 **If the connection fails** (wrong password, network out of range) the device falls back to AP mode automatically.
 
-**Resetting credentials:** on the Device Management page (`/ota`) click **Reset WiFi Credentials**. The device clears NVS and reboots into AP mode.
+**Resetting credentials:** on the Device Management page (`/`) click **Reset WiFi Credentials**. The device clears NVS and reboots into AP mode.
 
 ### Hostname
 
@@ -82,7 +99,7 @@ The hostname is the only compile-time network constant. It is registered via mDN
 The onboard NeoPixel (GPIO 21) shows current activity at a glance:
 
 | Color | Meaning |
-|-------|---------|
+| ----- | ------- |
 | Off | Idle — no TCP client connected |
 | Green | DSP write in progress (SigmaTCP) |
 | Blue | DSP read in progress (SigmaTCP) |
@@ -164,12 +181,14 @@ Navigate to `http://modulos-dsp.local/ota` to flash new ESP32 firmware:
 SigmaTCP is the protocol used by Analog Devices' SigmaStudio to talk to hardware like the USBi programmer. This sketch implements the server side over TCP/WiFi and forwards all register traffic to the DSP or EEPROM via I²C.
 
 **WRITE (opcode 0x09):**
+
 - 10-byte header: command, safeload flag, placement, totalLen, chipAddr, dataLen, startAddress
 - `totalLen` is validated against `WRITE_HDR_LEN + dataLen` before buffering — mismatched packets drop the connection
 - Payload forwarded to DSP or EEPROM via I²C
 - I²C errors are reported in the 4-byte ACK `{0x09, 0x00, 0x04, status}` so SigmaStudio sees real write failures
 
 **READ (opcode 0x0A):**
+
 - 8-byte header: command, totalLen, chipAddr, dataLen, startAddress
 - I²C data read in 32-byte chunks
 - 6-byte response header (`0x0B, totalLen_hi, totalLen_lo, status, dataLen_hi, dataLen_lo`) + payload sent back
@@ -196,7 +215,7 @@ The ADAU1401 uses **5.23 fixed-point** for parameter RAM (4 bytes per word). Saf
 
 ## File Structure
 
-```
+```text
 ModulosDSP_101/
 ├── ModulosDSP_101.ino   Main sketch: WiFi, TCP/HTTP servers, I²C bridge
 ├── DSPWriter.h          DSP register write interface and address map
@@ -205,6 +224,7 @@ ModulosDSP_101/
 ├── DataConversion.cpp   Fixed-point conversion implementations
 ├── index_html.h         Main device management UI (DSP status, EEPROM upload, controls)
 ├── ota_html.h           OTA firmware flash form
+├── params_html.h        Runtime parameter tuner UI
 └── ap_html.h            SoftAP WiFi config portal (first-boot / failed-connect)
 ```
 
@@ -213,7 +233,7 @@ ModulosDSP_101/
 ## ADAU1401 Address Map
 
 | Region | Address Range | I²C bytes per register |
-|--------|--------------|------------------------|
+| ------ | ------------- | ---------------------- |
 | Parameter RAM | 0x0000–0x03FF | 4 |
 | Program RAM | 0x0400–0x07FF | 5 |
 | Interface Registers 0–7 | 0x0800–0x0807 | 1 |

+ 15 - 0
index_html.h

@@ -109,6 +109,21 @@ static const char INDEX_HTML[] PROGMEM = R"HTML(
           </div>
         </div>
 
+        <!-- Parameter Tuner link -->
+        <div class="card mb-3">
+          <div class="card-header py-2">
+            <span class="small fw-semibold"><i class="fa fa-sliders"></i> Parameter Tuner</span>
+          </div>
+          <div class="card-body py-2 px-3">
+            <a href="/params" class="btn btn-outline-primary w-100">
+              <i class="fa fa-edit"></i>&nbsp; Open Parameter Tuner
+            </a>
+            <div class="form-text">
+              Read and write DSP parameter RAM (0x0000&ndash;0x03FF) in real time using safeload.
+            </div>
+          </div>
+        </div>
+
         <!-- WiFi Settings -->
         <div class="card mb-3">
           <div class="card-header py-2">

+ 285 - 0
params_html.h

@@ -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&ndash;0x03FF) in real time using safeload &mdash;
+          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 &copy; <span id="year"></span> &ndash; Modulos Audio &ndash; DSP Controller &ndash; 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 + '">&mdash;</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') +
+      ' &mdash; 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";