Ver Fonte

move controls to index page

Benjamin Harris há 1 mês atrás
pai
commit
5341b87e5f
4 ficheiros alterados com 298 adições e 244 exclusões
  1. 3 3
      CLAUDE.md
  2. 24 16
      README.md
  3. 267 23
      index_html.h
  4. 4 202
      ota_html.h

+ 3 - 3
CLAUDE.md

@@ -209,6 +209,6 @@ ADAU1401 parameter RAM uses **5.23 fixed-point** (4 bytes per word). `DataConver
 | `DSPWriter.h` | I²C address constants, register enums, `DSPWriter` class declaration |
 | `DSPWriter.cpp` | `writeRegister`, `writeRegisterBlock` (returns bool), safeload with flush |
 | `DataConversion.h` / `.cpp` | 5.23 fixed-point to C type conversions |
-| `index_html.h` | Embedded HTML string for the EEPROM upload UI |
-| `ota_html.h` | Embedded HTML string for the OTA / device management UI |
-| `ap_html.h` | Embedded HTML string for the SoftAP WiFi config portal |
+| `index_html.h` | Main device management UI: DSP status, EEPROM upload, DSP/GPIO/WiFi control |
+| `ota_html.h` | OTA firmware flash form only (no other controls) |
+| `ap_html.h` | SoftAP WiFi config portal (first-boot / failed-connect) |

+ 24 - 16
README.md

@@ -119,13 +119,11 @@ The 24C256 holds **32 KB**. The DSP reads this EEPROM at power-up via its SELFBO
 
 ---
 
-## Device Management Page (`/ota`)
+## Device Management (`/`)
 
-Navigate to `http://modulos-dsp.local/ota` in any browser.
+The main page at `http://modulos-dsp.local/` is the device management hub:
 
-**Live DSP Status:**
-
-The top of the page shows a live status card that polls `GET /dsp_status` every 2 seconds:
+**DSP Status** — live card polling `GET /dsp_status` every 2 seconds:
 
 | Field | Register | Description |
 | ----- | -------- | ----------- |
@@ -134,18 +132,28 @@ The top of the page shows a live status card that polls `GET /dsp_status` every
 | GPIO | 0x0808 | GPIO All Register value (hex) |
 | ADC 0–3 | 0x0809–0x080C | Raw ADC input values (hex) |
 
-**Firmware Update (OTA):**
+**EEPROM Upload** — flash DSP firmware to the 24C256:
+
+1. Click **Choose File** and select a SigmaStudio-exported binary.
+2. Optionally tick **Verify (CRC32)** to read back and confirm the write.
+3. Click **Upload and Program** — the Magenta LED pulses during the write.
+
+**DSP Control** — click **Soft Reset DSP** to restart DSP execution without reloading from EEPROM.
+
+**GPIO Control** — GP0–GP3 toggle buttons write to GPIO All Register (0x0808). Buttons go green when the pin is high. Output-only pins respond (configured via MpCfg registers).
+
+**WiFi Settings** — click **Reset WiFi Credentials** to clear NVS and reboot into AP setup mode.
+
+**Firmware Update** — link to the OTA page (`/ota`) for flashing new ESP32 firmware.
+
+## Firmware Update Page (`/ota`)
+
+Navigate to `http://modulos-dsp.local/ota` to flash new ESP32 firmware:
 
 1. In Arduino IDE, export the compiled binary: **Sketch → Export Compiled Binary** (produces a `.bin` file).
 2. Click **Choose File** and select the `.bin`.
 3. Click **Flash Firmware** — the Cyan LED indicates the write is in progress.
-4. On success the device reboots automatically. The page waits 5 seconds then redirects back to `/`.
-
-**DSP Soft Reset:**
-
-- Click **Soft Reset DSP** to stop and restart DSP program execution.
-- Parameter and program RAM are preserved — this does **not** reload from EEPROM.
-- Useful for recovering a stuck DSP without power-cycling the whole board.
+4. On success the device reboots automatically. The page redirects to `/` after 5 seconds.
 
 ---
 
@@ -195,9 +203,9 @@ ModulosDSP_101/
 ├── DSPWriter.cpp        I²C write and safeload implementations
 ├── DataConversion.h     5.23 fixed-point conversion declarations
 ├── DataConversion.cpp   Fixed-point conversion implementations
-├── index_html.h         Embedded HTML for the EEPROM upload web UI
-├── ota_html.h           Embedded HTML for the OTA / device management web UI
-└── ap_html.h            Embedded HTML for the SoftAP WiFi config portal
+├── index_html.h         Main device management UI (DSP status, EEPROM upload, controls)
+├── ota_html.h           OTA firmware flash form
+└── ap_html.h            SoftAP WiFi config portal (first-boot / failed-connect)
 ```
 
 ---

+ 267 - 23
index_html.h

@@ -12,13 +12,15 @@ static const char INDEX_HTML[] PROGMEM = R"HTML(
   <link rel="stylesheet" href="font-awesome.min.css">
   <script src="jquery-3.7.1.slim.min.js" crossorigin="anonymous"></script>
 
-  <title>Modulos EEPROM Uploader</title>
+  <title>Modulos DSP - Device Management</title>
   <style>
     @font-face {
       font-family: 'nasalization';
       src: url('/nasalization-rg.woff2') format('woff2');
     }
     .nasalization { font-family: 'nasalization'; }
+    .reg-row td   { font-family: monospace; font-size: 0.85rem; padding: 2px 8px; }
+    .reg-row td:first-child { color: #6c757d; width: 120px; }
   </style>
 </head>
 
@@ -28,60 +30,302 @@ static const char INDEX_HTML[] PROGMEM = R"HTML(
       <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="#navbarText" aria-controls="navbarText" aria-expanded="false" aria-label="Toggle navigation">
+      <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav"
+              aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
         <span class="navbar-toggler-icon"></span>
       </button>
-
-      <div class="collapse navbar-collapse" id="navbarText">
+      <div class="collapse navbar-collapse" id="navbarNav">
         <span class="col align-self-end text-end text-white">
           <span>{{IP}}</span>
           <i class="text-success fas fa-wifi"></i>
-          <!--<i class="fas fa-network-wired"></i>-->
-          <!--<i class="fas fa-diagram-project"></i>-->
         </span>
       </div>
     </div>
   </nav>
 
-  <div class="container">
+  <div class="container pb-5">
     <div class="row">
       <div class="col-sm">
-        <h2 class="text-center pt-2 font-weight-bold" id="title">Modulos EEPROM Uploader (24C256)</h2>
+        <h2 class="text-center pt-2 font-weight-bold">Device Management</h2>
       </div>
     </div>
 
-    <div class="row font-weight-bold border-top border-bottom">
-      <p>EEPROM I2C: 0x50, Size: 32768 bytes, Page: 64 bytes</p>
+    <div class="row g-3">
+
+      <!-- ── Left column ─────────────────────────────────────── -->
+      <div class="col-lg-6">
+
+        <!-- DSP live status card -->
+        <div class="card mb-3">
+          <div class="card-header d-flex justify-content-between align-items-center py-2">
+            <span class="small fw-semibold"><i class="fa fa-tachometer"></i> DSP Status</span>
+            <span id="dspBadge" class="badge bg-secondary">Polling&hellip;</span>
+          </div>
+          <div class="card-body py-2 px-3">
+            <table class="w-100">
+              <tr class="reg-row"><td>Core Register</td><td id="sCoreReg">&mdash;</td></tr>
+              <tr class="reg-row"><td>Running</td>      <td id="sRunning">&mdash;</td></tr>
+              <tr class="reg-row"><td>GPIO</td>         <td id="sGpio">&mdash;</td></tr>
+              <tr class="reg-row"><td>ADC 0&ndash;3</td><td id="sAdc">&mdash;</td></tr>
+            </table>
+            <p class="text-muted mb-0 mt-1" style="font-size:0.72rem">
+              Updated <span id="sUpdated">&mdash;</span> &bull; auto-refreshes every 2 s
+            </p>
+          </div>
+        </div>
+
+        <!-- DSP Control -->
+        <div class="card mb-3">
+          <div class="card-header py-2">
+            <span class="small fw-semibold"><i class="fa fa-refresh"></i> DSP Control</span>
+          </div>
+          <div class="card-body py-2 px-3">
+            <button id="resetBtn" class="btn btn-outline-secondary w-100" onclick="dspReset()">
+              <i class="fa fa-power-off"></i>&nbsp; Soft Reset DSP
+            </button>
+            <div class="form-text">
+              Restarts DSP program execution. RAM is preserved &mdash; does not reload from EEPROM.
+            </div>
+            <div id="resetResult" style="display:none;" class="alert alert-sm mt-2" role="alert"></div>
+          </div>
+        </div>
+
+        <!-- GPIO Control -->
+        <div class="card mb-3">
+          <div class="card-header py-2">
+            <span class="small fw-semibold"><i class="fa fa-toggle-on"></i> GPIO Control</span>
+          </div>
+          <div class="card-body py-2 px-3">
+            <div class="d-flex gap-2 mb-1">
+              <button id="gp0" class="btn btn-outline-secondary btn-sm" onclick="toggleGpioPin(0)">GP0</button>
+              <button id="gp1" class="btn btn-outline-secondary btn-sm" onclick="toggleGpioPin(1)">GP1</button>
+              <button id="gp2" class="btn btn-outline-secondary btn-sm" onclick="toggleGpioPin(2)">GP2</button>
+              <button id="gp3" class="btn btn-outline-secondary btn-sm" onclick="toggleGpioPin(3)">GP3</button>
+            </div>
+            <div class="form-text">
+              Writes to GPIO All Register (0x0808). Only output-configured pins respond (set via MpCfg).
+            </div>
+            <div id="gpioResult" style="display:none;" class="alert alert-sm mt-1" role="alert"></div>
+          </div>
+        </div>
 
-      <form method="POST" action="/upload" enctype="multipart/form-data">
-        <div class="mb-3">
-          <input class="form-control" type="file" name="bin" required>
+        <!-- WiFi Settings -->
+        <div class="card mb-3">
+          <div class="card-header py-2">
+            <span class="small fw-semibold"><i class="fa fa-wifi"></i> WiFi Settings</span>
+          </div>
+          <div class="card-body py-2 px-3">
+            <button class="btn btn-outline-danger w-100" onclick="wifiReset()">
+              <i class="fa fa-trash-o"></i>&nbsp; Reset WiFi Credentials
+            </button>
+            <div class="form-text">
+              Clears stored credentials and reboots into setup mode.
+              Connect to <strong>ModulosDSP-Setup</strong> then open <strong>192.168.4.1</strong> to reconfigure.
+            </div>
+            <div id="wifiResetResult" style="display:none;" class="alert alert-sm mt-1" role="alert"></div>
+          </div>
         </div>
 
-        <div class="mb-3 form-check">
-          <input class="form-check-input" type="checkbox" name="verify" value="1" checked>
-          <label class="form-check-label">Verify after write (CRC32)</label>
+        <!-- Firmware Update link -->
+        <div class="card mb-3">
+          <div class="card-header py-2">
+            <span class="small fw-semibold"><i class="fa fa-microchip"></i> Firmware Update</span>
+          </div>
+          <div class="card-body py-2 px-3">
+            <a href="/ota" class="btn btn-outline-warning w-100">
+              <i class="fa fa-upload"></i>&nbsp; Flash ESP32 Firmware (OTA)
+            </a>
+            <div class="form-text">Update the ESP32 firmware via over-the-air upload.</div>
+          </div>
         </div>
 
-        <button style="background-color:#114378;border-color:#114378;color:#e8b434;"
-                type="submit" class="btn">Upload and Program</button>
-      </form>
+      </div>
+
+      <!-- ── Right column ────────────────────────────────────── -->
+      <div class="col-lg-6">
 
-      <p><a href="/status">Status</a></p>
+        <!-- EEPROM Upload -->
+        <div class="card">
+          <div class="card-header py-2">
+            <span class="small fw-semibold"><i class="fa fa-database"></i> EEPROM Upload (24C256)</span>
+          </div>
+          <div class="card-body">
+            <p class="text-muted small mb-3">I&sup2;C 0x50 &bull; 32&thinsp;768 bytes &bull; 64-byte pages</p>
+            <form method="POST" action="/upload" enctype="multipart/form-data">
+              <div class="mb-3">
+                <label class="form-label fw-semibold">DSP firmware binary</label>
+                <input class="form-control" type="file" name="bin" required>
+                <div class="form-text">Export from SigmaStudio: <em>Action &rarr; Export System Files</em></div>
+              </div>
+              <div class="mb-3 form-check">
+                <input class="form-check-input" type="checkbox" name="verify" value="1" id="verifyChk" checked>
+                <label class="form-check-label" for="verifyChk">Verify after write (CRC32)</label>
+              </div>
+              <button style="background-color:#114378;border-color:#114378;color:#e8b434;"
+                      type="submit" class="btn w-100">
+                <i class="fa fa-upload"></i>&nbsp; Upload and Program
+              </button>
+            </form>
+            <p class="mt-3 mb-0">
+              <a href="/status" class="text-muted small">View upload status</a>
+            </p>
+          </div>
+        </div>
+
+      </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> - Modulos Audio - DSP Controller - All Rights Reserved
+        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>
+function pollStatus() {
+  var xhr = new XMLHttpRequest();
+  xhr.open('GET', '/dsp_status', true);
+  xhr.timeout = 1800;
+  xhr.onload = function() {
+    var badge = document.getElementById('dspBadge');
+    if (xhr.status !== 200) {
+      badge.className = 'badge bg-danger'; badge.textContent = 'I2C Error';
+      return;
+    }
+    try {
+      var d = JSON.parse(xhr.responseText);
+      if (d.running) {
+        badge.className = 'badge bg-success'; badge.textContent = 'Running';
+      } else {
+        badge.className = 'badge bg-danger';  badge.textContent = 'Stopped';
+      }
+      document.getElementById('sCoreReg').textContent = d.coreReg;
+      document.getElementById('sRunning').textContent = d.running ? 'Yes' : 'No';
+      document.getElementById('sGpio').textContent    = d.gpio;
+      document.getElementById('sAdc').textContent     = d.adc.join('  ');
+      document.getElementById('sUpdated').textContent = new Date().toLocaleTimeString();
+    } catch(e) {
+      badge.className = 'badge bg-warning'; badge.textContent = 'Parse error';
+    }
+  };
+  xhr.ontimeout = xhr.onerror = function() {
+    var badge = document.getElementById('dspBadge');
+    badge.className = 'badge bg-secondary'; badge.textContent = 'Offline';
+  };
+  xhr.send();
+}
+pollStatus();
+setInterval(pollStatus, 2000);
+
+function dspReset() {
+  var btn = document.getElementById('resetBtn');
+  var box = document.getElementById('resetResult');
+  btn.disabled = true;
+  box.style.display = 'none';
+  var xhr = new XMLHttpRequest();
+  xhr.open('POST', '/dsp_reset', true);
+  xhr.onload = function() {
+    box.style.display = 'block';
+    if (xhr.status === 200) {
+      box.className = 'alert alert-success alert-sm';
+      box.innerHTML = '<i class="fa fa-check"></i> ' + xhr.responseText;
+    } else {
+      box.className = 'alert alert-danger alert-sm';
+      box.innerHTML = '<i class="fa fa-times"></i> Reset failed.';
+    }
+    btn.disabled = false;
+  };
+  xhr.onerror = function() {
+    box.style.display = 'block';
+    box.className = 'alert alert-danger alert-sm';
+    box.innerHTML = '<i class="fa fa-times"></i> Request failed.';
+    btn.disabled = false;
+  };
+  xhr.send();
+}
+
+var gpioVal = null;
+
+function updateGpioBtns(val) {
+  for (var i = 0; i < 4; i++) {
+    var btn = document.getElementById('gp' + i);
+    if (val & (1 << i)) {
+      btn.className = 'btn btn-success btn-sm';
+    } else {
+      btn.className = 'btn btn-outline-secondary btn-sm';
+    }
+  }
+}
+
+function fetchGpio() {
+  var xhr = new XMLHttpRequest();
+  xhr.open('GET', '/gpio', true);
+  xhr.timeout = 1800;
+  xhr.onload = function() {
+    if (xhr.status === 200) {
+      try {
+        var d = JSON.parse(xhr.responseText);
+        gpioVal = d.gpio;
+        updateGpioBtns(gpioVal);
+      } catch(e) {}
+    }
+  };
+  xhr.send();
+}
+
+function toggleGpioPin(pin) {
+  if (gpioVal === null) return;
+  gpioVal ^= (1 << pin);
+  updateGpioBtns(gpioVal);
+  var box = document.getElementById('gpioResult');
+  var xhr = new XMLHttpRequest();
+  xhr.open('POST', '/gpio', true);
+  xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
+  xhr.onload = function() {
+    box.style.display = 'block';
+    if (xhr.status === 200) {
+      box.className = 'alert alert-success alert-sm';
+      box.innerHTML = '<i class="fa fa-check"></i> GP' + pin + ' toggled (0x' +
+                      (gpioVal & 0xFF).toString(16).padStart(2,'0').toUpperCase() + ')';
+    } else {
+      box.className = 'alert alert-danger alert-sm';
+      box.innerHTML = '<i class="fa fa-times"></i> GPIO write failed.';
+    }
+  };
+  xhr.onerror = function() {
+    box.style.display = 'block';
+    box.className = 'alert alert-danger alert-sm';
+    box.innerHTML = '<i class="fa fa-times"></i> Request failed.';
+  };
+  xhr.send('value=' + gpioVal);
+}
+
+fetchGpio();
+setInterval(fetchGpio, 5000);
+
+function wifiReset() {
+  if (!confirm('Clear WiFi credentials and reboot into setup mode?')) return;
+  var box = document.getElementById('wifiResetResult');
+  var xhr = new XMLHttpRequest();
+  xhr.open('POST', '/wifi_reset', true);
+  xhr.onload = function() {
+    box.style.display = 'block';
+    box.className = 'alert alert-warning alert-sm';
+    box.innerHTML = '<i class="fa fa-info-circle"></i> ' + xhr.responseText;
+  };
+  xhr.onerror = function() {
+    box.style.display = 'block';
+    box.className = 'alert alert-danger alert-sm';
+    box.innerHTML = '<i class="fa fa-times"></i> Request failed.';
+  };
+  xhr.send();
+}
+  </script>
 </body>
 </html>
-)HTML";
+)HTML";

+ 4 - 202
ota_html.h

@@ -10,43 +10,20 @@ static const char OTA_HTML[] PROGMEM = R"HTML(
   <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 - Device Management</title>
+  <title>Modulos DSP - Firmware Update</title>
   <style>
     body { padding-top: 40px; }
     .ota-card { max-width: 520px; margin: 0 auto; }
     #progressWrap { display: none; margin-top: 1.2rem; }
     #resultBox    { display: none; margin-top: 1rem; }
-    #resetResult  { display: none; margin-top: 0.6rem; }
-    .reg-row td   { font-family: monospace; font-size: 0.85rem; padding: 2px 8px; }
-    .reg-row td:first-child { color: #6c757d; width: 120px; }
   </style>
 </head>
 <body>
 <div class="container ota-card">
 
-  <h4 class="mb-1"><i class="fa fa-microchip"></i> Device Management</h4>
-  <p class="text-muted small mb-2">Device: <strong>{{IP}}</strong></p>
-
-  <!-- DSP live status card -->
-  <div class="card mb-4">
-    <div class="card-header d-flex justify-content-between align-items-center py-2">
-      <span class="small fw-semibold"><i class="fa fa-tachometer"></i> DSP Status</span>
-      <span id="dspBadge" class="badge bg-secondary">Polling&hellip;</span>
-    </div>
-    <div class="card-body py-2 px-3">
-      <table class="w-100">
-        <tr class="reg-row"><td>Core Register</td><td id="sCoreReg">&mdash;</td></tr>
-        <tr class="reg-row"><td>Running</td>      <td id="sRunning">&mdash;</td></tr>
-        <tr class="reg-row"><td>GPIO</td>         <td id="sGpio">&mdash;</td></tr>
-        <tr class="reg-row"><td>ADC 0&ndash;3</td><td id="sAdc">&mdash;</td></tr>
-      </table>
-      <p class="text-muted mb-0 mt-1" style="font-size:0.72rem">
-        Updated <span id="sUpdated">&mdash;</span> &bull; auto-refreshes every 2 s
-      </p>
-    </div>
-  </div>
+  <h4 class="mb-1"><i class="fa fa-upload"></i> Firmware Update</h4>
+  <p class="text-muted small mb-3">Device: <strong>{{IP}}</strong></p>
 
   <form id="otaForm" action="/ota_do" method="POST" enctype="multipart/form-data">
     <div class="mb-3">
@@ -75,185 +52,10 @@ static const char OTA_HTML[] PROGMEM = R"HTML(
   <div id="resultBox" class="alert" role="alert"></div>
 
   <hr class="mt-4">
-  <h6 class="text-muted mb-2"><i class="fa fa-refresh"></i> DSP Control</h6>
-  <button id="resetBtn" class="btn btn-outline-secondary w-100"
-          onclick="dspReset()">
-    <i class="fa fa-power-off"></i>&nbsp; Soft Reset DSP
-  </button>
-  <div class="form-text mb-1">
-    Restarts DSP program execution. RAM is preserved — does not reload from EEPROM.
-  </div>
-  <div id="resetResult" class="alert alert-sm" role="alert"></div>
-
-  <hr class="mt-3">
-  <h6 class="text-muted mb-2"><i class="fa fa-toggle-on"></i> GPIO Control</h6>
-  <div class="d-flex gap-2 mb-1">
-    <button id="gp0" class="btn btn-outline-secondary btn-sm" onclick="toggleGpioPin(0)">GP0</button>
-    <button id="gp1" class="btn btn-outline-secondary btn-sm" onclick="toggleGpioPin(1)">GP1</button>
-    <button id="gp2" class="btn btn-outline-secondary btn-sm" onclick="toggleGpioPin(2)">GP2</button>
-    <button id="gp3" class="btn btn-outline-secondary btn-sm" onclick="toggleGpioPin(3)">GP3</button>
-  </div>
-  <div class="form-text mb-1">
-    Writes to GPIO All Register (0x0808). Only output-configured pins respond (set via MpCfg).
-  </div>
-  <div id="gpioResult" style="display:none; margin-top:0.4rem;" class="alert alert-sm" role="alert"></div>
-
-  <hr class="mt-3">
-  <h6 class="text-muted mb-2"><i class="fa fa-wifi"></i> WiFi Settings</h6>
-  <button class="btn btn-outline-danger w-100" onclick="wifiReset()">
-    <i class="fa fa-trash-o"></i>&nbsp; Reset WiFi Credentials
-  </button>
-  <div class="form-text mb-1">
-    Clears stored credentials and reboots into setup mode.
-    Connect to <strong>ModulosDSP-Setup</strong> then open <strong>192.168.4.1</strong> to reconfigure.
-  </div>
-  <div id="wifiResetResult" style="display:none; margin-top:0.4rem;" class="alert alert-sm" role="alert"></div>
-
-  <hr class="mt-3">
-  <a href="/" class="text-muted small"><i class="fa fa-arrow-left"></i> Back to EEPROM Uploader</a>
+  <a href="/" class="text-muted small"><i class="fa fa-arrow-left"></i> Back to Device Management</a>
 </div>
 
 <script>
-function pollStatus() {
-  var xhr = new XMLHttpRequest();
-  xhr.open('GET', '/dsp_status', true);
-  xhr.timeout = 1800;
-  xhr.onload = function() {
-    var badge = document.getElementById('dspBadge');
-    if (xhr.status !== 200) {
-      badge.className = 'badge bg-danger'; badge.textContent = 'I2C Error';
-      return;
-    }
-    try {
-      var d = JSON.parse(xhr.responseText);
-      if (d.running) {
-        badge.className = 'badge bg-success'; badge.textContent = 'Running';
-      } else {
-        badge.className = 'badge bg-danger';  badge.textContent = 'Stopped';
-      }
-      document.getElementById('sCoreReg').textContent = d.coreReg;
-      document.getElementById('sRunning').textContent = d.running ? 'Yes' : 'No';
-      document.getElementById('sGpio').textContent    = d.gpio;
-      document.getElementById('sAdc').textContent     = d.adc.join('  ');
-      document.getElementById('sUpdated').textContent =
-        new Date().toLocaleTimeString();
-    } catch(e) {
-      badge.className = 'badge bg-warning'; badge.textContent = 'Parse error';
-    }
-  };
-  xhr.ontimeout = xhr.onerror = function() {
-    var badge = document.getElementById('dspBadge');
-    badge.className = 'badge bg-secondary'; badge.textContent = 'Offline';
-  };
-  xhr.send();
-}
-pollStatus();
-setInterval(pollStatus, 2000);
-
-function dspReset() {
-  var btn = document.getElementById('resetBtn');
-  var box = document.getElementById('resetResult');
-  btn.disabled = true;
-  box.style.display = 'none';
-  var xhr = new XMLHttpRequest();
-  xhr.open('POST', '/dsp_reset', true);
-  xhr.onload = function() {
-    box.style.display = 'block';
-    if (xhr.status === 200) {
-      box.className = 'alert alert-success alert-sm';
-      box.innerHTML = '<i class="fa fa-check"></i> ' + xhr.responseText;
-    } else {
-      box.className = 'alert alert-danger alert-sm';
-      box.innerHTML = '<i class="fa fa-times"></i> Reset failed.';
-    }
-    btn.disabled = false;
-  };
-  xhr.onerror = function() {
-    box.style.display = 'block';
-    box.className = 'alert alert-danger alert-sm';
-    box.innerHTML = '<i class="fa fa-times"></i> Request failed.';
-    btn.disabled = false;
-  };
-  xhr.send();
-}
-
-var gpioVal = null;
-
-function updateGpioBtns(val) {
-  for (var i = 0; i < 4; i++) {
-    var btn = document.getElementById('gp' + i);
-    if (val & (1 << i)) {
-      btn.className = 'btn btn-success btn-sm';
-    } else {
-      btn.className = 'btn btn-outline-secondary btn-sm';
-    }
-  }
-}
-
-function fetchGpio() {
-  var xhr = new XMLHttpRequest();
-  xhr.open('GET', '/gpio', true);
-  xhr.timeout = 1800;
-  xhr.onload = function() {
-    if (xhr.status === 200) {
-      try {
-        var d = JSON.parse(xhr.responseText);
-        gpioVal = d.gpio;
-        updateGpioBtns(gpioVal);
-      } catch(e) {}
-    }
-  };
-  xhr.send();
-}
-
-function toggleGpioPin(pin) {
-  if (gpioVal === null) return;
-  gpioVal ^= (1 << pin);
-  updateGpioBtns(gpioVal);
-  var box = document.getElementById('gpioResult');
-  var xhr = new XMLHttpRequest();
-  xhr.open('POST', '/gpio', true);
-  xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
-  xhr.onload = function() {
-    box.style.display = 'block';
-    if (xhr.status === 200) {
-      box.className = 'alert alert-success alert-sm';
-      box.innerHTML = '<i class="fa fa-check"></i> GP' + pin + ' toggled (0x' +
-                      (gpioVal & 0xFF).toString(16).padStart(2,'0').toUpperCase() + ')';
-    } else {
-      box.className = 'alert alert-danger alert-sm';
-      box.innerHTML = '<i class="fa fa-times"></i> GPIO write failed.';
-    }
-  };
-  xhr.onerror = function() {
-    box.style.display = 'block';
-    box.className = 'alert alert-danger alert-sm';
-    box.innerHTML = '<i class="fa fa-times"></i> Request failed.';
-  };
-  xhr.send('value=' + gpioVal);
-}
-
-fetchGpio();
-setInterval(fetchGpio, 5000);
-
-function wifiReset() {
-  if (!confirm('Clear WiFi credentials and reboot into setup mode?')) return;
-  var box = document.getElementById('wifiResetResult');
-  var xhr = new XMLHttpRequest();
-  xhr.open('POST', '/wifi_reset', true);
-  xhr.onload = function() {
-    box.style.display = 'block';
-    box.className = 'alert alert-warning alert-sm';
-    box.innerHTML = '<i class="fa fa-info-circle"></i> ' + xhr.responseText;
-  };
-  xhr.onerror = function() {
-    box.style.display = 'block';
-    box.className = 'alert alert-danger alert-sm';
-    box.innerHTML = '<i class="fa fa-times"></i> Request failed.';
-  };
-  xhr.send();
-}
-
 document.getElementById('otaForm').addEventListener('submit', function(e) {
   e.preventDefault();
   var file = document.getElementById('fwFile').files[0];