Add onboard web UI for direct ESP controller operation
This commit is contained in:
@@ -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
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user