jeol-t330a/succbone/succd/index.html
Rahix e4fa961ef0
All checks were successful
/ test (push) Successful in 10s
/ test (pull_request) Successful in 10s
succd: Only show voltage on hover
Another HP HMI thing: "Only show information that is immediately
relevant".  Let's hide the pirani voltage as it is mostly no longer
interesting to the user.  For situations where it is, it can be revealed
by hovering over the pirani pressure value.
2024-10-05 02:53:16 +02:00

445 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;
}
.has-hidden .hidden-text {
display: none;
}
.has-hidden:hover .hidden-text {
display: block;
@media only screen and (max-width: 700px) {
body {
font-size: 6px;
}
table {
font-size: 20px;
}
th {
font-size: 15px;
}
}
</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>Pirani Pressure</th>
<td class="has-hidden">
<div id="mbar">{{ .Pirani.Mbar }}</div>
<div class="hidden-text" style="color: #606060;">
<span>Voltage: </span><span id="volts">{{ .Pirani.Volts }}</span>
</div>
</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");
let colors = {
default: "background-color: #e8e8e8",
highlightNeutral: "background-color: #8282ff",
highlightCaution: "background-color: #ff941a",
highlightFault: "background-color: #f06060",
highlightGood: "background-color: #60f060",
};
// 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 = colors.default;
});
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 = colors.highlightFault;
} else {
failsafe.innerHTML = "OFF";
failsafe.style = colors.default;
}
if (data.Safety.HighPressure) {
highpressure.innerHTML = "ON";
highpressure.style = colors.default;
} else {
highpressure.innerHTML = "OFF";
highpressure.style = colors.default;
}
if (data.Pumps.RPOn) {
rp.innerHTML = "ON";
rp.style = colors.default;
} else {
rp.innerHTML = "OFF";
rp.style = colors.highlightNeutral;
}
if (data.Pumps.DPOn) {
dp.innerHTML = "ON";
dp.style = colors.highlightCaution;
} else {
dp.innerHTML = "OFF";
dp.style = colors.default;
}
let t = [];
if (data.Feedback.RoughReached) {
trough.innerHTML = "OK";
trough.style = colors.highlightGood;
} else {
trough.innerHTML = "NOK";
trough.style = colors.default;
}
if (data.Feedback.HighReached) {
thigh.innerHTML = "OK";
thigh.style = colors.highlightGood;
} else {
thigh.innerHTML = "NOK";
thigh.style = colors.default;
}
load.innerHTML = data.LoopLoad.toString() + "%";
historicalPush(data.Pirani.MbarFloat);
ping.innerHTML = Date.now();
});
socket.addEventListener("close", (event) => {
status.innerHTML = "Offline";
status.style = colors.highlightFault;
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>