jeol-t330a/succbone/succd/index.html

506 lines
15 KiB
HTML
Raw Normal View History

2024-09-12 01:02:57 +00:00
<!DOCTYPE html>
<meta charset="utf-8">
<title>succd</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
2024-09-27 00:10:39 +00:00
<link rel="shortcut icon" type="image/png" href="/favicon.png">
2024-09-12 01:02:57 +00:00
<style>
body {
font-size: 12px;
2024-09-27 00:31:38 +00:00
padding: 2em;
2024-09-12 01:02:57 +00:00
}
table {
font-size: 30px;
2024-09-28 06:32:07 +00:00
}
table.status td {
width: 2em;
2024-09-12 01:02:57 +00:00
}
th, td {
2024-09-27 00:31:38 +00:00
background-color: #e8e8e8;
padding: 0.3em;
2024-09-12 01:02:57 +00:00
}
th {
2024-09-27 00:31:38 +00:00
font-weight: 100;
text-align: right;
font-size: 25px;
2024-09-12 01:02:57 +00:00
}
td {
2024-09-27 00:31:38 +00:00
text-align: left;
2024-09-12 01:02:57 +00:00
}
td {
2024-09-27 00:31:38 +00:00
font-weight: 800;
2024-09-12 01:02:57 +00:00
}
h2 {
2024-09-27 00:31:38 +00:00
font-style: italic;
font-weight: 100;
2024-09-12 01:02:57 +00:00
}
button {
height: 3.3em;
padding-left: 1.5em;
padding-right: 1.5em;
}
2024-09-27 00:10:39 +00:00
td > span {
padding: 0.2em;
}
2024-09-27 00:10:39 +00:00
.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;
}
}
2024-09-12 01:02:57 +00:00
</style>
2024-09-27 00:10:39 +00:00
<div class="logo"><img src="/favicon.png" /></div>
2024-09-12 01:02:57 +00:00
<h1>succd</h1>
<h2>nothing more permanent than a temporary solution</h2>
<div class="main-grid">
2024-09-28 06:32:07 +00:00
<table class="status">
<tr>
<th>Thresholds</th>
2024-09-28 06:32:07 +00:00
<th>Rough</th>
2024-09-28 05:49:03 +00:00
<td id="trough">{{ if .Feedback.RoughReached }}OK{{ else }}NOK{{ end }}</td>
2024-09-28 06:32:07 +00:00
<th>High</th>
2024-09-28 05:49:03 +00:00
<td id="thigh">{{ if .Feedback.HighReached }}OK{{ else }}NOK{{ end }}</td>
</tr>
2024-09-27 00:31:38 +00:00
<tr>
<th>Pumps</th>
2024-09-28 06:32:07 +00:00
<th>RP</th>
2024-09-28 05:49:03 +00:00
<td id="rp">{{ if .Pumps.RPOn }}ON{{ else }}OFF{{ end }}</td>
2024-09-28 06:32:07 +00:00
<th>DP</th>
2024-09-28 05:49:03 +00:00
<td id="dp">{{ if .Pumps.DPOn }}ON{{ else }}OFF{{ end }}</td>
2024-09-27 00:31:38 +00:00
</tr>
<tr>
2024-09-28 06:32:07 +00:00
<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="3">Control</th>
<th>RP</th>
<td colspan="3">
2024-09-28 06:32:07 +00:00
<button id="rpon">On</button>
<button id="rpoff">Off</button>
</td>
</tr>
2024-09-27 00:31:38 +00:00
<tr>
2024-09-28 06:32:07 +00:00
<th>DP</th>
<td colspan="3">
2024-09-28 06:32:07 +00:00
<button id="dpon">On</button>
<button id="dpoff">Off</button>
</td>
2024-09-27 00:31:38 +00:00
</tr>
<tr>
<td colspan="3">
2024-09-28 06:32:07 +00:00
<button id="pd">Pump Down</button>
<button id="vent">Vent</button>
</td>
</tr>
<tr>
2024-09-28 06:32:07 +00:00
<th>Status</th>
<td id="status" colspan="1">OK</td>
<th>Load</th>
<td id="load" colspan="1" style="width: 4em;">...</td>
</tr>
2024-09-27 00:31:38 +00:00
</table>
<table>
<tr>
<th>Pirani Pressure</th>
<td colspan="2" 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>
<tr>
<th rowspan="4">Temperatures</th>
<th>DP Bottom</th>
<td id="temp-dp-bottom">{{ .Temperatures.DPBottom }}&nbsp;°C</td>
</tr>
<tr>
<th>DP Top</th>
<td id="temp-dp-top">{{ .Temperatures.DPTop }}&nbsp;°C</td>
</tr>
<tr>
<th>DP Inlet</th>
<td id="temp-dp-inlet">{{ .Temperatures.DPInlet }}&nbsp;°C</td>
</tr>
<tr>
<th>SEM Environment</th>
<td id="temp-sem">{{ .Temperatures.SEM }}&nbsp;°C</td>
</tr>
<tr>
<th>Humidity</th>
<th>SEM Environment</th>
<td id="humidity-sem">{{ .Humidity.SEM }}%</td>
</tr>
</table>
<div class="graph-container">
<canvas id="graph" width="1024" height="512" style="max-width: 100%;"></canvas>
</div>
</div>
2024-09-12 01:02:57 +00:00
<p style="font-style: italic; font-size: 12px; margin-top: 5em;">
2024-09-28 05:49:03 +00:00
{{ .System.Hostname }} | load: {{ .System.Load }} | <a href="/debug/pprof">pprof</a> | <a href="/metrics">metrics</a> | ws ping: <span id="ping"></span>
2024-09-12 01:02:57 +00:00
</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";
2024-09-12 01:02:57 +00:00
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", (_) => {
2024-09-27 00:31:38 +00:00
console.log("s u c c");
2024-09-12 01:02:57 +00:00
2024-09-27 00:31:38 +00:00
let status = document.querySelector("#status");
let failsafe = document.querySelector("#failsafe");
let highpressure = document.querySelector("#highpressure");
2024-09-27 00:31:38 +00:00
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");
2024-11-10 00:22:18 +00:00
let tempSEM = document.querySelector("#temp-sem");
let tempDPBottom = document.querySelector("#temp-dp-bottom");
let tempDPTop = document.querySelector("#temp-dp-top");
let tempDPInlet = document.querySelector("#temp-dp-inlet");
let humiditySEM = document.querySelector('#humidity-sem');
// Buttons
2024-09-27 00:31:38 +00:00
let pd = document.querySelector("#pd");
let vent = document.querySelector("#vent");
let rpon = document.querySelector("#rpon");
let rpoff = document.querySelector("#rpoff");
2024-09-12 01:02:57 +00:00
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.
2024-09-12 01:02:57 +00:00
historicalDraw(1024, 512);
// Basic retry loop for connecting to WS.
2024-09-27 00:31:38 +00:00
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;
2024-09-27 00:31:38 +00:00
});
socket.addEventListener("message", (event) => {
const data = JSON.parse(event.data);
2024-09-28 05:49:03 +00:00
volts.innerHTML = data.Pirani.Volts;
mbar.innerHTML = data.Pirani.Mbar;
if (data.Safety.Failsafe) {
failsafe.innerHTML = "ON";
failsafe.style = colors.highlightFault;
} else {
2024-09-28 05:49:03 +00:00
failsafe.innerHTML = "OFF";
failsafe.style = colors.default;
}
2024-09-28 05:49:03 +00:00
if (data.Safety.HighPressure) {
highpressure.innerHTML = "ON";
highpressure.style = colors.default;
} else {
2024-09-28 05:49:03 +00:00
highpressure.innerHTML = "OFF";
highpressure.style = colors.default;
}
2024-09-28 05:49:03 +00:00
if (data.Pumps.RPOn) {
rp.innerHTML = "ON";
rp.style = colors.default;
} else {
2024-09-28 05:49:03 +00:00
rp.innerHTML = "OFF";
rp.style = colors.highlightNeutral;
}
2024-09-28 05:49:03 +00:00
if (data.Pumps.DPOn) {
dp.innerHTML = "ON";
dp.style = colors.highlightCaution;
} else {
2024-09-28 05:49:03 +00:00
dp.innerHTML = "OFF";
dp.style = colors.default;
}
2024-11-10 00:22:18 +00:00
tempSEM.innerHTML = data.Temperatures.SEM + "&nbsp;°C";
tempSEM.style = (data.Temperatures.SEM > 30) ?
colors.highlightCaution : colors.default;
humiditySEM.innerHTML = data.Humidity.SEM + "%";
humiditySEM.style = (data.Humidity.SEM > 59) ?
colors.highlightCaution : colors.default;
2024-11-10 00:22:18 +00:00
tempDPTop.innerHTML = data.Temperatures.DPTop + "&nbsp;°C";
tempDPTop.style = (data.Temperatures.DPTop > 30) ?
colors.highlightCaution : colors.default;
2024-11-10 00:22:18 +00:00
tempDPInlet.innerHTML = data.Temperatures.DPInlet + "&nbsp;°C";
tempDPInlet.style = (data.Temperatures.DPInlet > 30) ?
colors.highlightCaution : colors.default;
tempDPBottom.innerHTML = data.Temperatures.DPBottom + "&nbsp;°C";
if (data.Temperatures.DPBottom > 200) {
tempDPBottom.style = colors.highlightFault;
} else if (data.Temperatures.DPBottom > 50) {
tempDPBottom.style = colors.highlightNeutral;
} else {
tempDPBottom.style = colors.default;
}
2024-11-10 00:22:18 +00:00
let t = [];
2024-09-28 05:49:03 +00:00
if (data.Feedback.RoughReached) {
trough.innerHTML = "OK";
trough.style = colors.highlightGood;
} else {
trough.innerHTML = "NOK";
trough.style = colors.default;
}
2024-09-28 05:49:03 +00:00
if (data.Feedback.HighReached) {
thigh.innerHTML = "OK";
thigh.style = colors.highlightGood;
} else {
thigh.innerHTML = "NOK";
thigh.style = colors.default;
}
load.innerHTML = data.LoopLoad.toString() + "%";
2024-09-28 05:49:03 +00:00
historicalPush(data.Pirani.MbarFloat);
2024-09-27 00:31:38 +00:00
ping.innerHTML = Date.now();
});
socket.addEventListener("close", (event) => {
status.innerHTML = "Offline";
status.style = colors.highlightFault;
// Indicate all process values as unknown
2024-11-10 00:22:18 +00:00
[failsafe, highpressure, rp, dp, trough, thigh, volts, mbar, tempDPBottom, tempDPTop, tempDPInlet].forEach((el) => {
if (!el.innerHTML.includes("??")) {
el.innerHTML += "??";
}
});
2024-09-27 00:31:38 +00:00
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");
});
2024-09-12 01:02:57 +00:00
});
2024-09-27 00:31:38 +00:00
</script>