Rahix
4f74e92c45
Make sure the grid cannot grow too large on big screens. Also slightly adjust the breakpoint to avoid some weird artifacts.
418 lines
12 KiB
HTML
418 lines
12 KiB
HTML
<!DOCTYPE html>
|
|
<meta charset="utf-8">
|
|
<title>succd</title>
|
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
<link rel="shortcut icon" type="image/png" href="/favicon.png">
|
|
<style>
|
|
body {
|
|
font-size: 12px;
|
|
padding: 2em;
|
|
}
|
|
table {
|
|
font-size: 40px;
|
|
}
|
|
table.status td {
|
|
width: 2em;
|
|
}
|
|
th, td {
|
|
background-color: #e8e8e8;
|
|
padding: 0.4em;
|
|
}
|
|
th {
|
|
font-weight: 100;
|
|
text-align: right;
|
|
font-size: 30px;
|
|
}
|
|
td {
|
|
text-align: left;
|
|
}
|
|
td {
|
|
font-weight: 800;
|
|
}
|
|
h2 {
|
|
font-style: italic;
|
|
font-weight: 100;
|
|
}
|
|
button {
|
|
height: 4.5em;
|
|
padding-left: 1.5em;
|
|
padding-right: 1.5em;
|
|
}
|
|
|
|
td > span {
|
|
padding: 0.2em;
|
|
}
|
|
|
|
.logo {
|
|
float: left;
|
|
margin-right: 2em;
|
|
}
|
|
|
|
.logo > img {
|
|
height: 10em;
|
|
}
|
|
|
|
.graph-container {
|
|
background-color: #e8e8e8;
|
|
text-align: center;
|
|
}
|
|
|
|
.main-grid {
|
|
margin-top: 2em;
|
|
margin-left: auto;
|
|
margin-right: auto;
|
|
max-width: 160em;
|
|
clear: both;
|
|
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(54em, 1fr));
|
|
column-gap: 2em;
|
|
row-gap: 2em;
|
|
}
|
|
</style>
|
|
|
|
<div class="logo"><img src="/favicon.png" /></div>
|
|
|
|
<h1>succd</h1>
|
|
<h2>nothing more permanent than a temporary solution</h2>
|
|
|
|
<div class="main-grid">
|
|
<table class="status">
|
|
<tr>
|
|
<th>Thresholds</th>
|
|
<th>Rough</th>
|
|
<td id="trough">{{ if .Feedback.RoughReached }}OK{{ else }}NOK{{ end }}</td>
|
|
<th>High</th>
|
|
<td id="thigh">{{ if .Feedback.HighReached }}OK{{ else }}NOK{{ end }}</td>
|
|
</tr>
|
|
<tr>
|
|
<th>Pumps</th>
|
|
<th>RP</th>
|
|
<td id="rp">{{ if .Pumps.RPOn }}ON{{ else }}OFF{{ end }}</td>
|
|
<th>DP</th>
|
|
<td id="dp">{{ if .Pumps.DPOn }}ON{{ else }}OFF{{ end }}</td>
|
|
</tr>
|
|
<tr>
|
|
<th>Safety</th>
|
|
<th style="font-size: 0.7em;">Pirani<br />Failsafe</th>
|
|
<td id="failsafe">{{ if .Safety.Failsafe }}ON{{ else }}OFF{{ end }}</td>
|
|
<th style="font-size: 0.7em;">DP<br />Lockout</th>
|
|
<td id="highpressure">{{ if .Safety.HighPressure }}ON{{ else }}OFF{{ end }}</td>
|
|
</tr>
|
|
</table>
|
|
|
|
<table>
|
|
<tr>
|
|
<th rowspan="2">Pirani Gauge</th>
|
|
<th>Voltage</th>
|
|
<td id="volts">{{ .Pirani.Volts }}</td>
|
|
</tr>
|
|
<tr>
|
|
<th>Pressure</th>
|
|
<td id="mbar">{{ .Pirani.Mbar }}</td>
|
|
</tr>
|
|
</table>
|
|
|
|
|
|
<table>
|
|
<tr>
|
|
<th rowspan="3">Control</th>
|
|
<th>RP</th>
|
|
<td colspan="3">
|
|
<button id="rpon">On</button>
|
|
<button id="rpoff">Off</button>
|
|
</td>
|
|
</tr>
|
|
<tr>
|
|
<th>DP</th>
|
|
<td colspan="3">
|
|
<button id="dpon">On</button>
|
|
<button id="dpoff">Off</button>
|
|
</td>
|
|
</tr>
|
|
<tr>
|
|
<td colspan="3">
|
|
<button id="pd">Pump Down</button>
|
|
<button id="vent">Vent</button>
|
|
</td>
|
|
</tr>
|
|
<tr>
|
|
<th>Status</th>
|
|
<td id="status" colspan="1">OK</td>
|
|
<th>Load</th>
|
|
<td id="load" colspan="1" style="width: 4em;">...</td>
|
|
</tr>
|
|
</table>
|
|
|
|
<div class="graph-container">
|
|
<canvas id="graph" width="1024" height="512" style="max-width: 100%;"></canvas>
|
|
</div>
|
|
</div>
|
|
|
|
<p style="font-style: italic; font-size: 12px; margin-top: 5em;">
|
|
{{ .System.Hostname }} | load: {{ .System.Load }} | <a href="/debug/pprof">pprof</a> | <a href="/metrics">metrics</a> | ws ping: <span id="ping">…</span>
|
|
</p>
|
|
|
|
<script>
|
|
|
|
let historical = [];
|
|
let canvas = null;
|
|
|
|
// Push a datapoint (in mbar) to the historical buffer, maintaining enough data
|
|
// for ~10 minutes.
|
|
let historicalPush = (v) => {
|
|
historical.push({v: v, time: Date.now() / 1000});
|
|
let len = historical.length;
|
|
// TODO(q3k): trim based on recorded timestamp, not constant buffer size.
|
|
let trim = len - 8192;
|
|
if (trim > 0) {
|
|
historical = historical.slice(trim);
|
|
}
|
|
};
|
|
|
|
// Draw the historical graph and schedule next draw in 100msec.
|
|
let historicalDraw = (w, h) => {
|
|
const now = Date.now() / 1000;
|
|
|
|
// TODO(q3k): better use canvas API to not have so much silly math around
|
|
// coordinate calculation.
|
|
|
|
canvas.clearRect(0, 0, w, h);
|
|
canvas.fillStyle = "#e8e8e8";
|
|
canvas.fillRect(0, 0, w, h);
|
|
|
|
// Margins of the main graph window.
|
|
const marginLeft = 64;
|
|
const marginRight = 32;
|
|
const marginTop = 32;
|
|
const marginBottom = 32;
|
|
|
|
// Draw main graph window.
|
|
canvas.fillStyle = "#f8f8f8";
|
|
canvas.strokeStyle = "#444";
|
|
canvas.lineWidth = 1;
|
|
canvas.fillRect(marginLeft, marginTop, w-(marginLeft+marginRight), h-(marginTop+marginBottom));
|
|
canvas.strokeRect(marginLeft, marginTop, w-(marginLeft+marginRight), h-(marginTop+marginBottom));
|
|
|
|
// Range of decades for Y value.
|
|
const ymin = -4;
|
|
const ymax = 4;
|
|
// Pixels per decade.
|
|
const yscale = (h - (marginTop+marginBottom)) / (ymax - ymin);
|
|
|
|
// For every decade...
|
|
for (let i = ymin; i < ymax; i++) {
|
|
const yoff = (i - ymin) * yscale + yscale / 2;
|
|
const y = Math.floor(h - marginBottom - yoff) + 0.5;
|
|
// Draw Y scale ticks.
|
|
canvas.beginPath();
|
|
canvas.moveTo(marginLeft-5, y);
|
|
canvas.lineTo(marginLeft, y);
|
|
canvas.strokeStyle = "#000";
|
|
canvas.stroke();
|
|
|
|
// Draw Y grid.
|
|
canvas.beginPath();
|
|
canvas.moveTo(marginLeft, y);
|
|
canvas.lineTo(w-marginRight-1, y);
|
|
canvas.strokeStyle = "#ccc";
|
|
canvas.stroke();
|
|
|
|
// Draw Y fine grid.
|
|
if (i > ymin) {
|
|
for (let j = 2; j < 10; j++) {
|
|
let yy = y - Math.log10(j/10) * yscale;
|
|
canvas.beginPath();
|
|
canvas.moveTo(marginLeft, yy);
|
|
canvas.lineTo(w-marginRight-1, yy);
|
|
canvas.strokeStyle = "#eee";
|
|
canvas.stroke();
|
|
}
|
|
}
|
|
|
|
// Draw Y labels.
|
|
canvas.font = "10px sans-serif";
|
|
canvas.fillStyle = "#000";
|
|
canvas.textAlign = "right";
|
|
const text = `10^${i}`;
|
|
canvas.fillText(text, marginLeft-10, y+5);
|
|
}
|
|
|
|
// How much space to leave in front of the graph.
|
|
const xhead = 10;
|
|
|
|
// Draw X labels..
|
|
canvas.textAlign = "center";
|
|
canvas.fillText("Now", w - marginRight - xhead, h - marginBottom + 15)
|
|
canvas.fillText("-10min", marginLeft, h - marginBottom + 15)
|
|
|
|
const xmax = 60 * 10;
|
|
const xscale = (w - (marginLeft+marginRight+xhead)) / (xmax);
|
|
|
|
// Clip to main window.
|
|
canvas.save();
|
|
canvas.beginPath();
|
|
canvas.rect(marginLeft, marginTop, w-(marginLeft+marginRight), h-(marginTop+marginBottom+1));
|
|
canvas.clip();
|
|
|
|
// Draw actual data line.
|
|
let first = true;
|
|
canvas.beginPath();
|
|
historical.forEach((v) => {
|
|
const time = v.time;
|
|
const mbar = v.v;
|
|
const elapsed = now-time;
|
|
if (elapsed > xmax) {
|
|
return;
|
|
}
|
|
|
|
const x = (w - marginRight - xhead) - (elapsed * xscale);
|
|
const yoff = (Math.log10(mbar) - ymin) * yscale + yscale / 2;
|
|
const y = h - marginBottom - yoff;
|
|
|
|
if (first) {
|
|
first = false;
|
|
canvas.moveTo(x, y);
|
|
} else {
|
|
canvas.lineTo(x, y);
|
|
}
|
|
});
|
|
canvas.strokeStyle = "#de1010";
|
|
canvas.lineWidth = 1;
|
|
canvas.stroke();
|
|
canvas.restore();
|
|
|
|
setTimeout(() => { historicalDraw(w, h); }, 100);
|
|
};
|
|
|
|
window.addEventListener("load", (_) => {
|
|
console.log("s u c c");
|
|
|
|
let status = document.querySelector("#status");
|
|
let failsafe = document.querySelector("#failsafe");
|
|
let highpressure = document.querySelector("#highpressure");
|
|
let volts = document.querySelector("#volts");
|
|
let mbar = document.querySelector("#mbar");
|
|
let ping = document.querySelector("#ping");
|
|
let trough = document.querySelector("#trough");
|
|
let thigh = document.querySelector("#thigh");
|
|
let load = document.querySelector("#load");
|
|
|
|
// Buttons
|
|
let pd = document.querySelector("#pd");
|
|
let vent = document.querySelector("#vent");
|
|
let rpon = document.querySelector("#rpon");
|
|
let rpoff = document.querySelector("#rpoff");
|
|
canvas = document.querySelector("#graph").getContext("2d");
|
|
|
|
// TODO(q3k): unhardcode this.
|
|
historicalDraw(1024, 512);
|
|
|
|
// Basic retry loop for connecting to WS.
|
|
let loc = window.location;
|
|
let wsloc = "";
|
|
if (loc.protocol == "https:") {
|
|
wsloc = "wss:";
|
|
} else {
|
|
wsloc = "ws:";
|
|
}
|
|
wsloc += "//" + loc.host + "/stream";
|
|
console.log("Connecting to " + wsloc + "...");
|
|
|
|
let connected = false;
|
|
let connect = () => {
|
|
const socket = new WebSocket(wsloc);
|
|
socket.addEventListener("open", (event) => {
|
|
connected = true;
|
|
console.log("Socket connected!");
|
|
status.innerHTML = "Online";
|
|
status.style = "background-color: #60f060;";
|
|
});
|
|
socket.addEventListener("message", (event) => {
|
|
const data = JSON.parse(event.data);
|
|
volts.innerHTML = data.Pirani.Volts;
|
|
mbar.innerHTML = data.Pirani.Mbar;
|
|
if (data.Safety.Failsafe) {
|
|
failsafe.innerHTML = "ON";
|
|
failsafe.style = "background-color: #f06060";
|
|
} else {
|
|
failsafe.innerHTML = "OFF";
|
|
failsafe.style = "background-color: #60f060";
|
|
}
|
|
if (data.Safety.HighPressure) {
|
|
highpressure.innerHTML = "ON";
|
|
highpressure.style = "background-color: #f06060";
|
|
} else {
|
|
highpressure.innerHTML = "OFF";
|
|
highpressure.style = "background-color: #60f060";
|
|
}
|
|
if (data.Pumps.RPOn) {
|
|
rp.innerHTML = "ON";
|
|
rp.style = "background-color: #60f060";
|
|
} else {
|
|
rp.innerHTML = "OFF";
|
|
rp.style = "background-color: #f06060";
|
|
}
|
|
if (data.Pumps.DPOn) {
|
|
dp.innerHTML = "ON";
|
|
dp.style = "background-color: #60f060";
|
|
} else {
|
|
dp.innerHTML = "OFF";
|
|
dp.style = "background-color: #f06060";
|
|
}
|
|
|
|
let t = [];
|
|
if (data.Feedback.RoughReached) {
|
|
trough.innerHTML = "OK";
|
|
trough.style = "background-color: #60f060";
|
|
} else {
|
|
trough.innerHTML = "NOK";
|
|
trough.style = "background-color: #f06060";
|
|
}
|
|
if (data.Feedback.HighReached) {
|
|
thigh.innerHTML = "OK";
|
|
thigh.style = "background-color: #60f060";
|
|
} else {
|
|
thigh.innerHTML = "NOK";
|
|
thigh.style = "background-color: #f06060";
|
|
}
|
|
load.innerHTML = data.LoopLoad.toString() + "%";
|
|
historicalPush(data.Pirani.MbarFloat);
|
|
ping.innerHTML = Date.now();
|
|
});
|
|
socket.addEventListener("close", (event) => {
|
|
status.innerHTML = "Offline";
|
|
status.style = "background-color: #f06060;";
|
|
if (connected) {
|
|
console.log("Socket dead, reconnecting...");
|
|
}
|
|
connected = false;
|
|
setTimeout(connect, 1000);
|
|
});
|
|
socket.addEventListener("error", (event) => {
|
|
socket.close();
|
|
});
|
|
};
|
|
connect();
|
|
|
|
pd.addEventListener("click", async (event) => {
|
|
await fetch("/button/pumpdown");
|
|
});
|
|
vent.addEventListener("click", async (event) => {
|
|
await fetch("/button/vent");
|
|
});
|
|
rpon.addEventListener("click", async (event) => {
|
|
await fetch("/rp/on");
|
|
});
|
|
rpoff.addEventListener("click", async (event) => {
|
|
await fetch("/rp/off");
|
|
});
|
|
dpon.addEventListener("click", async (event) => {
|
|
await fetch("/dp/on");
|
|
});
|
|
dpoff.addEventListener("click", async (event) => {
|
|
await fetch("/dp/off");
|
|
});
|
|
});
|
|
</script>
|