Add embedded ESP32 web dashboard
This commit is contained in:
@@ -12,6 +12,141 @@
|
|||||||
#include "app_config.h"
|
#include "app_config.h"
|
||||||
#include "logger.h"
|
#include "logger.h"
|
||||||
|
|
||||||
|
|
||||||
|
const char INDEX_HTML[] PROGMEM = R"rawliteral(
|
||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||||
|
<title>Overland Controller</title>
|
||||||
|
<style>
|
||||||
|
:root{--bg:#0b0f14;--card:#141b24;--muted:#7f8da3;--text:#eef4ff;--good:#48d17a;--warn:#ffd166;--bad:#ff5c7a;--line:#223044;--btn:#233249}
|
||||||
|
*{box-sizing:border-box} body{margin:0;background:var(--bg);color:var(--text);font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Arial,sans-serif}
|
||||||
|
.wrap{max-width:980px;margin:0 auto;padding:18px}
|
||||||
|
.top{display:flex;justify-content:space-between;gap:12px;align-items:center;margin-bottom:14px}
|
||||||
|
h1{font-size:22px;margin:0}.sub{color:var(--muted);font-size:13px;margin-top:4px}.pill{border:1px solid var(--line);border-radius:999px;padding:7px 10px;color:var(--muted);font-size:12px}
|
||||||
|
.grid{display:grid;grid-template-columns:repeat(12,1fr);gap:12px}.card{background:var(--card);border:1px solid var(--line);border-radius:18px;padding:16px;box-shadow:0 10px 28px rgba(0,0,0,.22)}
|
||||||
|
.span6{grid-column:span 6}.span4{grid-column:span 4}.span12{grid-column:span 12}
|
||||||
|
.label{color:var(--muted);font-size:12px;text-transform:uppercase;letter-spacing:.08em}.big{font-size:42px;font-weight:750;margin:8px 0}.row{display:flex;justify-content:space-between;gap:10px;border-top:1px solid var(--line);padding-top:10px;margin-top:10px}
|
||||||
|
.v{font-weight:650}.good{color:var(--good)}.warn{color:var(--warn)}.bad{color:var(--bad)}.muted{color:var(--muted)}
|
||||||
|
.bar{height:10px;background:#0e141c;border-radius:999px;overflow:hidden;border:1px solid var(--line)}.fill{height:100%;background:linear-gradient(90deg,var(--good),#9be15d);width:0%}
|
||||||
|
button{width:100%;border:0;border-radius:12px;padding:12px;background:var(--btn);color:var(--text);font-weight:700;font-size:15px}
|
||||||
|
button.on{background:#17462a;color:#dfffea}button.off{background:#46202a;color:#ffe2e8}.relayBtns{display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-top:12px}
|
||||||
|
.tempList,.alarmList{display:grid;gap:8px;margin-top:12px}.item{display:flex;justify-content:space-between;gap:8px;background:#0f151d;border:1px solid var(--line);border-radius:12px;padding:10px}
|
||||||
|
@media(max-width:760px){.span6,.span4{grid-column:span 12}.big{font-size:36px}.wrap{padding:12px}}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="wrap">
|
||||||
|
<div class="top">
|
||||||
|
<div>
|
||||||
|
<h1 id="device">Overland Controller</h1>
|
||||||
|
<div class="sub">Local camp dashboard</div>
|
||||||
|
</div>
|
||||||
|
<div class="pill" id="conn">Connecting...</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid">
|
||||||
|
<section class="card span6">
|
||||||
|
<div class="label">Battery</div>
|
||||||
|
<div class="big"><span id="soc">--</span>%</div>
|
||||||
|
<div class="bar"><div class="fill" id="socbar"></div></div>
|
||||||
|
<div class="row"><span class="muted">Voltage</span><span class="v" id="voltage">--</span></div>
|
||||||
|
<div class="row"><span class="muted">Current</span><span class="v" id="current">--</span></div>
|
||||||
|
<div class="row"><span class="muted">Remaining</span><span class="v" id="ah">--</span></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="card span6">
|
||||||
|
<div class="label">BMS</div>
|
||||||
|
<div class="big" id="bmsState">--</div>
|
||||||
|
<div class="row"><span class="muted">Battery temp</span><span class="v" id="battTemp">--</span></div>
|
||||||
|
<div class="row"><span class="muted">Cell delta</span><span class="v" id="delta">--</span></div>
|
||||||
|
<div class="row"><span class="muted">Cycles</span><span class="v" id="cycles">--</span></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="card span6">
|
||||||
|
<div class="label">Temperatures</div>
|
||||||
|
<div class="tempList" id="temps"></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="card span6">
|
||||||
|
<div class="label">Relays</div>
|
||||||
|
<div class="tempList" id="relays"></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="card span12">
|
||||||
|
<div class="label">Alarms / System</div>
|
||||||
|
<div class="alarmList" id="alarms"></div>
|
||||||
|
<div class="row"><span class="muted">Firmware</span><span class="v" id="fw">--</span></div>
|
||||||
|
<div class="row"><span class="muted">Uptime</span><span class="v" id="uptime">--</span></div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const $=id=>document.getElementById(id);
|
||||||
|
function fmt(n,d=1){return typeof n==="number"?n.toFixed(d):"--"}
|
||||||
|
function temp(v){return typeof v==="number"?fmt(v,1)+"°F":"Offline"}
|
||||||
|
function clsAlarm(v){return v?"bad":"good"}
|
||||||
|
async function relay(id,on){
|
||||||
|
try{await fetch(`/relay/${id}/${on?"on":"off"}`); await load()}catch(e){}
|
||||||
|
}
|
||||||
|
function render(data){
|
||||||
|
$("conn").textContent="Live";
|
||||||
|
$("conn").className="pill good";
|
||||||
|
$("device").textContent=data.config?.device_name||"Overland Controller";
|
||||||
|
const b=data.battery||{};
|
||||||
|
$("soc").textContent=fmt(b.soc,0);
|
||||||
|
$("socbar").style.width=Math.max(0,Math.min(100,b.soc||0))+"%";
|
||||||
|
$("voltage").textContent=fmt(b.voltage,2)+" V";
|
||||||
|
$("current").textContent=fmt(b.current,2)+" A";
|
||||||
|
$("ah").textContent=fmt(b.remaining_ah,1)+" Ah";
|
||||||
|
$("bmsState").textContent=b.connected?"Online":"Offline";
|
||||||
|
$("bmsState").className=b.connected?"big good":"big bad";
|
||||||
|
$("battTemp").textContent=temp(b.temperature_f);
|
||||||
|
$("delta").textContent=(typeof b.cell_delta_mv==="number"?b.cell_delta_mv:"--")+" mV";
|
||||||
|
$("cycles").textContent=b.cycle_count??"--";
|
||||||
|
|
||||||
|
$("temps").innerHTML=(data.temps||[]).map(t=>`
|
||||||
|
<div class="item">
|
||||||
|
<span>${t.name||t.id}</span>
|
||||||
|
<strong class="${t.online?"good":"muted"}">${temp(t.temperature_f)}</strong>
|
||||||
|
</div>`).join("") || `<div class="item"><span>No sensors</span><strong class="muted">--</strong></div>`;
|
||||||
|
|
||||||
|
$("relays").innerHTML=(data.relays||[]).map(r=>`
|
||||||
|
<div class="item" style="display:block">
|
||||||
|
<div style="display:flex;justify-content:space-between"><span>${r.name||r.id}</span><strong class="${r.state?"good":"muted"}">${r.state?"ON":"OFF"}</strong></div>
|
||||||
|
<div class="relayBtns">
|
||||||
|
<button class="on" onclick="relay('${r.id}',true)">ON</button>
|
||||||
|
<button class="off" onclick="relay('${r.id}',false)">OFF</button>
|
||||||
|
</div>
|
||||||
|
</div>`).join("");
|
||||||
|
|
||||||
|
const alarms=data.alarms||{};
|
||||||
|
$("alarms").innerHTML=Object.keys(alarms).map(k=>`
|
||||||
|
<div class="item"><span>${k.replaceAll("_"," ")}</span><strong class="${clsAlarm(alarms[k])}">${alarms[k]?"ALARM":"OK"}</strong></div>`).join("");
|
||||||
|
|
||||||
|
const s=data.system||{};
|
||||||
|
$("fw").textContent=(s.firmware_name||"--")+" "+(s.firmware_version||"");
|
||||||
|
$("uptime").textContent=(s.uptime_seconds||0)+" sec";
|
||||||
|
}
|
||||||
|
async function load(){
|
||||||
|
try{
|
||||||
|
const r=await fetch("/status",{cache:"no-store"});
|
||||||
|
render(await r.json());
|
||||||
|
}catch(e){
|
||||||
|
$("conn").textContent="Offline";
|
||||||
|
$("conn").className="pill bad";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
load();
|
||||||
|
setInterval(load,3000);
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
)rawliteral";
|
||||||
|
|
||||||
WebServer server(80);
|
WebServer server(80);
|
||||||
HardwareSerial DashboardSerial(2);
|
HardwareSerial DashboardSerial(2);
|
||||||
|
|
||||||
@@ -875,6 +1010,10 @@ void setup() {
|
|||||||
Serial.println(WiFi.softAPIP());
|
Serial.println(WiFi.softAPIP());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
server.on("/", HTTP_GET, []() {
|
||||||
|
server.send_P(200, "text/html", INDEX_HTML);
|
||||||
|
});
|
||||||
|
|
||||||
server.on("/status", handleStatus);
|
server.on("/status", handleStatus);
|
||||||
server.on("/relay/relay_1/on", HTTP_GET, handleGenericRelayRoute);
|
server.on("/relay/relay_1/on", HTTP_GET, handleGenericRelayRoute);
|
||||||
server.on("/relay/relay_1/off", HTTP_GET, handleGenericRelayRoute);
|
server.on("/relay/relay_1/off", HTTP_GET, handleGenericRelayRoute);
|
||||||
|
|||||||
Reference in New Issue
Block a user