When the succbone connection breaks, add hints to the UI that values may no longer be correct.
		
			
				
	
	
		
			453 lines
		
	
	
	
		
			13 KiB
		
	
	
	
		
			HTML
		
	
	
	
	
	
			
		
		
	
	
			453 lines
		
	
	
	
		
			13 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;
 | 
						|
 | 
						|
            // Indicate all process values as unknown
 | 
						|
 | 
						|
            [failsafe, highpressure, rp, dp, trough, thigh, volts, mbar].forEach((el) => {
 | 
						|
                if (!el.innerHTML.includes("??")) {
 | 
						|
                    el.innerHTML += "??";
 | 
						|
                }
 | 
						|
            });
 | 
						|
 | 
						|
            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>
 |