Add onboard web UI for direct ESP controller operation

This commit is contained in:
2026-03-13 22:54:27 +00:00
parent ea996b332a
commit fc4c4a88f7
2 changed files with 152 additions and 1 deletions
+3
View File
@@ -30,6 +30,9 @@ Implemented endpoints:
- `POST /api/safety/reset` - `POST /api/safety/reset`
- `GET /api/heartbeat` - `GET /api/heartbeat`
Built-in web interface:
- `GET /` serves a lightweight control UI for power, setpoint, PID, and safety reset.
Not implemented in this firmware: Not implemented in this firmware:
- Scheduler and dual-nail routing. - Scheduler and dual-nail routing.
- Autotune endpoints. - Autotune endpoints.
+149 -1
View File
@@ -297,9 +297,157 @@ void handleSafetyReset() {
sendJson(out); sendJson(out);
} }
const char UI_HTML[] PROGMEM = R"HTML(
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>piNail ESP32-C3</title>
<style>
:root { color-scheme: dark; }
body { margin: 0; font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif; background:#111; color:#eee; }
.wrap { max-width: 760px; margin: 0 auto; padding: 16px; }
.card { background:#1a1a1a; border:1px solid #333; border-radius:12px; padding:14px; margin-bottom:12px; }
.grid { display:grid; grid-template-columns:1fr 1fr; gap:10px; }
.row { display:flex; gap:8px; align-items:center; flex-wrap:wrap; }
.k { color:#aaa; font-size:13px; }
.v { font-size:22px; font-weight:700; }
input { background:#0f0f0f; color:#eee; border:1px solid #444; border-radius:8px; padding:8px; width:110px; }
button { background:#d35400; color:#fff; border:0; border-radius:8px; padding:9px 12px; cursor:pointer; }
button.alt { background:#2d2d2d; }
button.warn { background:#8e2a2a; }
.ok { color:#39d98a; }
.bad { color:#ff5a5a; }
</style>
</head>
<body>
<div class="wrap">
<div class="card">
<h2 style="margin:0 0 8px 0">piNail ESP32-C3 (Single Nail)</h2>
<div id="net" class="k">Connecting...</div>
</div>
<div class="card grid">
<div>
<div class="k">Temperature</div>
<div class="v" id="temp">--</div>
</div>
<div>
<div class="k">Setpoint</div>
<div class="v" id="setpoint">--</div>
</div>
<div>
<div class="k">Relay</div>
<div class="v" id="relay">--</div>
</div>
<div>
<div class="k">Output (ms/window)</div>
<div class="v" id="output">--</div>
</div>
</div>
<div class="card">
<div class="row" style="margin-bottom:8px">
<button onclick="setPower(true)">Power ON</button>
<button class="alt" onclick="setPower(false)">Power OFF</button>
<button class="warn" onclick="resetSafety()">Reset Safety</button>
</div>
<div class="row">
<label>Setpoint F <input id="setpointIn" type="number" min="0" max="800" step="5" value="530"></label>
<button onclick="applySetpoint()">Apply Setpoint</button>
</div>
</div>
<div class="card">
<div class="row" style="margin-bottom:8px"><strong>PID</strong></div>
<div class="row">
<label>Kp <input id="kpIn" type="number" step="0.0001"></label>
<label>Ki <input id="kiIn" type="number" step="0.0001"></label>
<label>Kd <input id="kdIn" type="number" step="0.0001"></label>
<button onclick="applyPid()">Apply PID</button>
</div>
</div>
<div class="card">
<div class="k">Safety</div>
<div id="safety" class="v">--</div>
<div id="reason" class="k" style="margin-top:6px"></div>
</div>
</div>
<script>
async function jget(url) {
const r = await fetch(url);
if (!r.ok) throw new Error(await r.text());
return await r.json();
}
async function jpost(url, body) {
const r = await fetch(url, {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(body)});
if (!r.ok) throw new Error(await r.text());
return await r.json();
}
async function refresh() {
try {
const s = await jget('/api/status');
document.getElementById('net').textContent = 'Connected: ' + location.host;
document.getElementById('temp').textContent = (s.temp ?? 0).toFixed(1) + ' F';
document.getElementById('setpoint').textContent = (s.setpoint ?? 0).toFixed(0) + ' F';
document.getElementById('relay').textContent = s.relay_on ? 'ON' : 'OFF';
document.getElementById('output').textContent = (s.output ?? 0).toFixed(0);
document.getElementById('setpointIn').value = Math.round(s.setpoint ?? 530);
document.getElementById('kpIn').value = s.pid?.kP ?? '';
document.getElementById('kiIn').value = s.pid?.kI ?? '';
document.getElementById('kdIn').value = s.pid?.kD ?? '';
const safe = document.getElementById('safety');
const reason = document.getElementById('reason');
if (s.safety_tripped) {
safe.textContent = 'TRIPPED';
safe.className = 'v bad';
reason.textContent = s.safety_reason || 'Safety tripped';
} else {
safe.textContent = 'OK';
safe.className = 'v ok';
reason.textContent = '';
}
} catch (e) {
document.getElementById('net').textContent = 'Error: ' + e.message;
}
}
async function setPower(enabled) {
await jpost('/api/power', {enabled});
await refresh();
}
async function applySetpoint() {
const v = Number(document.getElementById('setpointIn').value || 530);
await jpost('/api/setpoint', {setpoint:v});
await refresh();
}
async function applyPid() {
const kP = Number(document.getElementById('kpIn').value);
const kI = Number(document.getElementById('kiIn').value);
const kD = Number(document.getElementById('kdIn').value);
await jpost('/api/pid', {kP, kI, kD});
await refresh();
}
async function resetSafety() {
await jpost('/api/safety/reset', {});
await refresh();
}
refresh();
setInterval(refresh, 1000);
</script>
</body>
</html>
)HTML";
void setupRoutes() { void setupRoutes() {
server.on("/", HTTP_GET, []() { server.on("/", HTTP_GET, []() {
server.send(200, "text/plain", "piNail ESP32-C3 controller online"); server.send_P(200, "text/html", UI_HTML);
}); });
server.on("/api/status", HTTP_GET, handleStatus); server.on("/api/status", HTTP_GET, handleStatus);
server.on("/api/history", HTTP_GET, handleHistory); server.on("/api/history", HTTP_GET, handleHistory);