Compare commits
30 commits
Author | SHA1 | Date | |
---|---|---|---|
Rahix | 4e271a01a9 | ||
3b9c1ba912 | |||
9ec580c26f | |||
Rahix | 152290f5a3 | ||
6f93b96c39 | |||
d8a467a0c4 | |||
e7fd2dd7d7 | |||
606d470577 | |||
Rahix | 0e45650972 | ||
Rahix | edb9051708 | ||
Rahix | 4ac1b5eb32 | ||
Rahix | c02d24414f | ||
2e6a3be100 | |||
4edabc5c56 | |||
Rahix | 52231a9e9c | ||
Rahix | 0aba323779 | ||
Rahix | bd40c4f8df | ||
Rahix | d5c42a4899 | ||
Rahix | 637f8748a8 | ||
Rahix | 1e21222705 | ||
Rahix | 7a56b0fe70 | ||
Rahix | f4339e54ef | ||
Rahix | f2c0d67eed | ||
Rahix | e2fc15ed9b | ||
Rahix | ef959f4be3 | ||
Rahix | 8645718748 | ||
Rahix | ed8adad611 | ||
Rahix | 313569e1dc | ||
Rahix | 102393a689 | ||
Serge Bazanski | 2899677059 |
|
@ -11,4 +11,8 @@ So far, the connectors seem to match up with the ones shown in the SEM schematic
|
||||||
|
|
||||||
There is also a Z80 based system + an EEPROM on the board.
|
There is also a Z80 based system + an EEPROM on the board.
|
||||||
|
|
||||||
|
## Front Side
|
||||||
![Image of PH00524-2](./ph00524-2.jpg)
|
![Image of PH00524-2](./ph00524-2.jpg)
|
||||||
|
|
||||||
|
## Back Side
|
||||||
|
![Image of the back side of PH00524-2](./ph00524-2-back.jpg)
|
||||||
|
|
BIN
modules/ph00524/ph00524-2-back.jpg
(Stored with Git LFS)
Normal file
BIN
modules/ph00524/ph00524-2-back.jpg
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
succbone/panel.pdf
(Stored with Git LFS)
BIN
succbone/panel.pdf
(Stored with Git LFS)
Binary file not shown.
BIN
succbone/panel.qet
(Stored with Git LFS)
BIN
succbone/panel.qet
(Stored with Git LFS)
Binary file not shown.
BIN
succbone/succbone-control-panel.jpg
(Stored with Git LFS)
BIN
succbone/succbone-control-panel.jpg
(Stored with Git LFS)
Binary file not shown.
BIN
succbone/succbone-din-mount.FCStd
(Stored with Git LFS)
BIN
succbone/succbone-din-mount.FCStd
(Stored with Git LFS)
Binary file not shown.
BIN
succbone/succbone-din-mount.stl
(Stored with Git LFS)
Normal file
BIN
succbone/succbone-din-mount.stl
(Stored with Git LFS)
Normal file
Binary file not shown.
|
@ -37,6 +37,7 @@ Running locally
|
||||||
---
|
---
|
||||||
|
|
||||||
```
|
```
|
||||||
|
$ ./ci.sh # run tests
|
||||||
$ go run . -fake
|
$ go run . -fake
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
@ -4,5 +4,8 @@ go 1.22.3
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/coder/websocket v1.8.12
|
github.com/coder/websocket v1.8.12
|
||||||
|
github.com/simonvetter/modbus v1.6.3
|
||||||
k8s.io/klog v1.0.0
|
k8s.io/klog v1.0.0
|
||||||
)
|
)
|
||||||
|
|
||||||
|
require github.com/goburrow/serial v0.1.0 // indirect
|
||||||
|
|
|
@ -1,5 +1,9 @@
|
||||||
github.com/coder/websocket v1.8.12 h1:5bUXkEPPIbewrnkU8LTCLVaxi4N4J8ahufH2vlo4NAo=
|
github.com/coder/websocket v1.8.12 h1:5bUXkEPPIbewrnkU8LTCLVaxi4N4J8ahufH2vlo4NAo=
|
||||||
github.com/coder/websocket v1.8.12/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs=
|
github.com/coder/websocket v1.8.12/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs=
|
||||||
github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas=
|
github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas=
|
||||||
|
github.com/goburrow/serial v0.1.0 h1:v2T1SQa/dlUqQiYIT8+Cu7YolfqAi3K96UmhwYyuSrA=
|
||||||
|
github.com/goburrow/serial v0.1.0/go.mod h1:sAiqG0nRVswsm1C97xsttiYCzSLBmUZ/VSlVLZJ8haA=
|
||||||
|
github.com/simonvetter/modbus v1.6.3 h1:kDzwVfIPczsM4Iz09il/Dij/bqlT4XiJVa0GYaOVA9w=
|
||||||
|
github.com/simonvetter/modbus v1.6.3/go.mod h1:hh90ZaTaPLcK2REj6/fpTbiV0J6S7GWmd8q+GVRObPw=
|
||||||
k8s.io/klog v1.0.0 h1:Pt+yjF5aB1xDSVbau4VsWe+dQNzA0qv1LlXdC2dF6Q8=
|
k8s.io/klog v1.0.0 h1:Pt+yjF5aB1xDSVbau4VsWe+dQNzA0qv1LlXdC2dF6Q8=
|
||||||
k8s.io/klog v1.0.0/go.mod h1:4Bi6QPql/J/LkTDqv7R/cd3hPo4k2DG6Ptcz060Ez5I=
|
k8s.io/klog v1.0.0/go.mod h1:4Bi6QPql/J/LkTDqv7R/cd3hPo4k2DG6Ptcz060Ez5I=
|
||||||
|
|
|
@ -62,6 +62,17 @@ type apiData struct {
|
||||||
// DPOn means the diffusion pump is turned on.
|
// DPOn means the diffusion pump is turned on.
|
||||||
DPOn bool
|
DPOn bool
|
||||||
}
|
}
|
||||||
|
// Temperature state.
|
||||||
|
Temperatures struct {
|
||||||
|
DPBottom float32
|
||||||
|
DPTop float32
|
||||||
|
DPInlet float32
|
||||||
|
SEM float32
|
||||||
|
}
|
||||||
|
// Humidity state.
|
||||||
|
Humidity struct {
|
||||||
|
SEM float32
|
||||||
|
}
|
||||||
// Pressure feedback into evacuation board.
|
// Pressure feedback into evacuation board.
|
||||||
Feedback struct {
|
Feedback struct {
|
||||||
// RoughReached is true when the system has reached a rough vacuum
|
// RoughReached is true when the system has reached a rough vacuum
|
||||||
|
@ -115,6 +126,11 @@ func (s *webServer) apiData(skipSystem bool) *apiData {
|
||||||
ad.Pirani.MbarFloat = mbar
|
ad.Pirani.MbarFloat = mbar
|
||||||
ad.Pumps.RPOn = state.rpOn
|
ad.Pumps.RPOn = state.rpOn
|
||||||
ad.Pumps.DPOn = state.dpOn
|
ad.Pumps.DPOn = state.dpOn
|
||||||
|
ad.Temperatures.DPBottom = state.tempDPBottom
|
||||||
|
ad.Temperatures.DPTop = state.tempDPTop
|
||||||
|
ad.Temperatures.DPInlet = state.tempDPInlet
|
||||||
|
ad.Temperatures.SEM = state.tempSEM
|
||||||
|
ad.Humidity.SEM = state.humiditySEM
|
||||||
ad.Feedback.RoughReached = rough
|
ad.Feedback.RoughReached = rough
|
||||||
ad.Feedback.HighReached = high
|
ad.Feedback.HighReached = high
|
||||||
ad.LoopLoad = s.d.loopLoad()
|
ad.LoopLoad = s.d.loopLoad()
|
||||||
|
@ -163,16 +179,80 @@ func (s *webServer) viewStream(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func boolToFloat(b bool) float32 {
|
||||||
|
if b {
|
||||||
|
return 1.0
|
||||||
|
} else {
|
||||||
|
return 0.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// httpMetrics serves minimalistic Prometheus-compatible metrics.
|
// httpMetrics serves minimalistic Prometheus-compatible metrics.
|
||||||
func (s *webServer) viewMetrics(w http.ResponseWriter, r *http.Request) {
|
func (s *webServer) viewMetrics(w http.ResponseWriter, r *http.Request) {
|
||||||
// TODO(q3k): also serve Go stuff using the actual Prometheus metrics client
|
// TODO(q3k): also serve Go stuff using the actual Prometheus metrics client
|
||||||
// library.
|
// library.
|
||||||
// TODO(q3k): serve the rest of the data model
|
// TODO(q3k): serve the rest of the data model
|
||||||
state := s.d.snapshot()
|
state := s.d.snapshot()
|
||||||
mbar := state.piraniMbar100.mbar
|
|
||||||
|
// sem_pressure_mbar is meant to represent the fused pressure value
|
||||||
|
// from all data sources once we have more vacuum sensors in the
|
||||||
|
// system. sem_pirani_mbar is just the reading from the pirani gauge.
|
||||||
fmt.Fprintf(w, "# HELP sem_pressure_mbar Pressure in the SEM chamber, in millibar\n")
|
fmt.Fprintf(w, "# HELP sem_pressure_mbar Pressure in the SEM chamber, in millibar\n")
|
||||||
fmt.Fprintf(w, "# TYPE sem_pressure_mbar gauge\n")
|
fmt.Fprintf(w, "# TYPE sem_pressure_mbar gauge\n")
|
||||||
fmt.Fprintf(w, "sem_pressure_mbar %f\n", mbar)
|
fmt.Fprintf(w, "sem_pressure_mbar %f\n", state.piraniMbar100.mbar)
|
||||||
|
|
||||||
|
fmt.Fprintf(w, "# HELP sem_pirani_mbar Pressure reading by the Pirani gauge, in millibar\n")
|
||||||
|
fmt.Fprintf(w, "# TYPE sem_pirani_mbar gauge\n")
|
||||||
|
fmt.Fprintf(w, "sem_pirani_mbar %f\n", state.piraniMbar100.mbar)
|
||||||
|
|
||||||
|
fmt.Fprintf(w, "# HELP sem_pirani_volts Voltage output from the Pirani gauge, in volts\n")
|
||||||
|
fmt.Fprintf(w, "# TYPE sem_pirani_volts gauge\n")
|
||||||
|
fmt.Fprintf(w, "sem_pirani_volts %f\n", state.piraniVolts100.avg)
|
||||||
|
|
||||||
|
fmt.Fprintf(w, "# HELP sem_pirani_failsafe_active Whether pirani gauge failsafe mode is active (boolean)\n")
|
||||||
|
fmt.Fprintf(w, "# TYPE sem_pirani_failsafe_active gauge\n")
|
||||||
|
fmt.Fprintf(w, "sem_pirani_failsafe_active %f\n", boolToFloat(state.safety.failsafe))
|
||||||
|
|
||||||
|
fmt.Fprintf(w, "# HELP sem_dp_lockout_active Whether diffusion pump lockout is active (boolean)\n")
|
||||||
|
fmt.Fprintf(w, "# TYPE sem_dp_lockout_active gauge\n")
|
||||||
|
fmt.Fprintf(w, "sem_dp_lockout_active %f\n", boolToFloat(state.safety.highPressure))
|
||||||
|
|
||||||
|
fmt.Fprintf(w, "# HELP sem_pump_diffusion_running Whether the diffusion pump is running (boolean)\n")
|
||||||
|
fmt.Fprintf(w, "# TYPE sem_pump_diffusion_running gauge\n")
|
||||||
|
fmt.Fprintf(w, "sem_pump_diffusion_running %f\n", boolToFloat(state.dpOn))
|
||||||
|
|
||||||
|
fmt.Fprintf(w, "# HELP sem_pump_roughing_running Whether the roughing pump is running (boolean)\n")
|
||||||
|
fmt.Fprintf(w, "# TYPE sem_pump_roughing_running gauge\n")
|
||||||
|
fmt.Fprintf(w, "sem_pump_roughing_running %f\n", boolToFloat(state.rpOn))
|
||||||
|
|
||||||
|
rough, high := state.vacuumStatus()
|
||||||
|
fmt.Fprintf(w, "# HELP sem_vacuum_rough_reached Whether a rough vacuum has been reached (boolean)\n")
|
||||||
|
fmt.Fprintf(w, "# TYPE sem_vacuum_rough_reached gauge\n")
|
||||||
|
fmt.Fprintf(w, "sem_vacuum_rough_reached %f\n", boolToFloat(rough))
|
||||||
|
|
||||||
|
fmt.Fprintf(w, "# HELP sem_vacuum_high_reached Whether a high vacuum has been reached (boolean)\n")
|
||||||
|
fmt.Fprintf(w, "# TYPE sem_vacuum_high_reached gauge\n")
|
||||||
|
fmt.Fprintf(w, "sem_vacuum_high_reached %f\n", boolToFloat(high))
|
||||||
|
|
||||||
|
fmt.Fprintf(w, "# HELP sem_environment_temperature_celsius Environmental temperature of the SEM, in degrees celsius\n")
|
||||||
|
fmt.Fprintf(w, "# TYPE sem_environment_temperature_celsius gauge\n")
|
||||||
|
fmt.Fprintf(w, "sem_environment_temperature_celsius %f\n", state.tempSEM)
|
||||||
|
|
||||||
|
fmt.Fprintf(w, "# HELP sem_environment_humidity_percent Environmental relative humidity of the SEM, in percent\n")
|
||||||
|
fmt.Fprintf(w, "# TYPE sem_environment_humidity_percent gauge\n")
|
||||||
|
fmt.Fprintf(w, "sem_environment_humidity_percent %f\n", state.humiditySEM)
|
||||||
|
|
||||||
|
fmt.Fprintf(w, "# HELP sem_dp_bottom_temperature_celsius Temperature of the DP bottom, in degrees celsius\n")
|
||||||
|
fmt.Fprintf(w, "# TYPE sem_dp_bottom_temperature_celsius gauge\n")
|
||||||
|
fmt.Fprintf(w, "sem_dp_bottom_temperature_celsius %f\n", state.tempDPBottom)
|
||||||
|
|
||||||
|
fmt.Fprintf(w, "# HELP sem_dp_top_temperature_celsius Temperature of the DP top, in degrees celsius\n")
|
||||||
|
fmt.Fprintf(w, "# TYPE sem_dp_top_temperature_celsius gauge\n")
|
||||||
|
fmt.Fprintf(w, "sem_dp_top_temperature_celsius %f\n", state.tempDPTop)
|
||||||
|
|
||||||
|
fmt.Fprintf(w, "# HELP sem_dp_inlet_temperature_celsius Temperature of the DP inlet flange, in degrees celsius\n")
|
||||||
|
fmt.Fprintf(w, "# TYPE sem_dp_inlet_temperature_celsius gauge\n")
|
||||||
|
fmt.Fprintf(w, "sem_dp_inlet_temperature_celsius %f\n", state.tempDPInlet)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *webServer) viewRoughingPumpEnable(w http.ResponseWriter, r *http.Request) {
|
func (s *webServer) viewRoughingPumpEnable(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
|
@ -5,24 +5,23 @@
|
||||||
<link rel="shortcut icon" type="image/png" href="/favicon.png">
|
<link rel="shortcut icon" type="image/png" href="/favicon.png">
|
||||||
<style>
|
<style>
|
||||||
body {
|
body {
|
||||||
font-size: 14px;
|
font-size: 12px;
|
||||||
padding: 2em;
|
padding: 2em;
|
||||||
}
|
}
|
||||||
table {
|
table {
|
||||||
font-size: 40px;
|
font-size: 30px;
|
||||||
margin-top: 1em;
|
|
||||||
}
|
}
|
||||||
table.status td {
|
table.status td {
|
||||||
width: 2em;
|
width: 2em;
|
||||||
}
|
}
|
||||||
th, td {
|
th, td {
|
||||||
background-color: #e8e8e8;
|
background-color: #e8e8e8;
|
||||||
padding: 0.4em;
|
padding: 0.3em;
|
||||||
}
|
}
|
||||||
th {
|
th {
|
||||||
font-weight: 100;
|
font-weight: 100;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
font-size: 30px;
|
font-size: 25px;
|
||||||
}
|
}
|
||||||
td {
|
td {
|
||||||
text-align: left;
|
text-align: left;
|
||||||
|
@ -35,7 +34,7 @@ h2 {
|
||||||
font-weight: 100;
|
font-weight: 100;
|
||||||
}
|
}
|
||||||
button {
|
button {
|
||||||
height: 4.5em;
|
height: 3.3em;
|
||||||
padding-left: 1.5em;
|
padding-left: 1.5em;
|
||||||
padding-right: 1.5em;
|
padding-right: 1.5em;
|
||||||
}
|
}
|
||||||
|
@ -52,6 +51,44 @@ td > span {
|
||||||
.logo > img {
|
.logo > img {
|
||||||
height: 10em;
|
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>
|
</style>
|
||||||
|
|
||||||
<div class="logo"><img src="/favicon.png" /></div>
|
<div class="logo"><img src="/favicon.png" /></div>
|
||||||
|
@ -59,19 +96,7 @@ td > span {
|
||||||
<h1>succd</h1>
|
<h1>succd</h1>
|
||||||
<h2>nothing more permanent than a temporary solution</h2>
|
<h2>nothing more permanent than a temporary solution</h2>
|
||||||
|
|
||||||
<p style="margin-top: 2em; clear: both;">
|
<div class="main-grid">
|
||||||
<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 class="status">
|
<table class="status">
|
||||||
<tr>
|
<tr>
|
||||||
<th>Thresholds</th>
|
<th>Thresholds</th>
|
||||||
|
@ -122,13 +147,49 @@ td > span {
|
||||||
<th>Status</th>
|
<th>Status</th>
|
||||||
<td id="status" colspan="1">OK</td>
|
<td id="status" colspan="1">OK</td>
|
||||||
<th>Load</th>
|
<th>Load</th>
|
||||||
<td id="load" colspan="1">...</td>
|
<td id="load" colspan="1" style="width: 4em;">...</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</p>
|
|
||||||
<p style="margin-top: 2em;">
|
<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 }} °C</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>DP Top</th>
|
||||||
|
<td id="temp-dp-top">{{ .Temperatures.DPTop }} °C</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>DP Inlet</th>
|
||||||
|
<td id="temp-dp-inlet">{{ .Temperatures.DPInlet }} °C</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>SEM Environment</th>
|
||||||
|
<td id="temp-sem">{{ .Temperatures.SEM }} °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>
|
<canvas id="graph" width="1024" height="512" style="max-width: 100%;"></canvas>
|
||||||
</p>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<p style="font-style: italic; font-size: 12px; margin-top: 5em;">
|
<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>
|
{{ .System.Hostname }} | load: {{ .System.Load }} | <a href="/debug/pprof">pprof</a> | <a href="/metrics">metrics</a> | ws ping: <span id="ping">…</span>
|
||||||
|
@ -159,7 +220,7 @@ let historicalDraw = (w, h) => {
|
||||||
// coordinate calculation.
|
// coordinate calculation.
|
||||||
|
|
||||||
canvas.clearRect(0, 0, w, h);
|
canvas.clearRect(0, 0, w, h);
|
||||||
canvas.fillStyle = "#f0f0f0";
|
canvas.fillStyle = "#e8e8e8";
|
||||||
canvas.fillRect(0, 0, w, h);
|
canvas.fillRect(0, 0, w, h);
|
||||||
|
|
||||||
// Margins of the main graph window.
|
// Margins of the main graph window.
|
||||||
|
@ -278,6 +339,11 @@ window.addEventListener("load", (_) => {
|
||||||
let trough = document.querySelector("#trough");
|
let trough = document.querySelector("#trough");
|
||||||
let thigh = document.querySelector("#thigh");
|
let thigh = document.querySelector("#thigh");
|
||||||
let load = document.querySelector("#load");
|
let load = document.querySelector("#load");
|
||||||
|
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
|
// Buttons
|
||||||
let pd = document.querySelector("#pd");
|
let pd = document.querySelector("#pd");
|
||||||
|
@ -286,6 +352,14 @@ window.addEventListener("load", (_) => {
|
||||||
let rpoff = document.querySelector("#rpoff");
|
let rpoff = document.querySelector("#rpoff");
|
||||||
canvas = document.querySelector("#graph").getContext("2d");
|
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.
|
// TODO(q3k): unhardcode this.
|
||||||
historicalDraw(1024, 512);
|
historicalDraw(1024, 512);
|
||||||
|
|
||||||
|
@ -307,7 +381,7 @@ window.addEventListener("load", (_) => {
|
||||||
connected = true;
|
connected = true;
|
||||||
console.log("Socket connected!");
|
console.log("Socket connected!");
|
||||||
status.innerHTML = "Online";
|
status.innerHTML = "Online";
|
||||||
status.style = "background-color: #60f060;";
|
status.style = colors.default;
|
||||||
});
|
});
|
||||||
socket.addEventListener("message", (event) => {
|
socket.addEventListener("message", (event) => {
|
||||||
const data = JSON.parse(event.data);
|
const data = JSON.parse(event.data);
|
||||||
|
@ -315,47 +389,71 @@ window.addEventListener("load", (_) => {
|
||||||
mbar.innerHTML = data.Pirani.Mbar;
|
mbar.innerHTML = data.Pirani.Mbar;
|
||||||
if (data.Safety.Failsafe) {
|
if (data.Safety.Failsafe) {
|
||||||
failsafe.innerHTML = "ON";
|
failsafe.innerHTML = "ON";
|
||||||
failsafe.style = "background-color: #f06060";
|
failsafe.style = colors.highlightFault;
|
||||||
} else {
|
} else {
|
||||||
failsafe.innerHTML = "OFF";
|
failsafe.innerHTML = "OFF";
|
||||||
failsafe.style = "background-color: #60f060";
|
failsafe.style = colors.default;
|
||||||
}
|
}
|
||||||
if (data.Safety.HighPressure) {
|
if (data.Safety.HighPressure) {
|
||||||
highpressure.innerHTML = "ON";
|
highpressure.innerHTML = "ON";
|
||||||
highpressure.style = "background-color: #f06060";
|
highpressure.style = colors.default;
|
||||||
} else {
|
} else {
|
||||||
highpressure.innerHTML = "OFF";
|
highpressure.innerHTML = "OFF";
|
||||||
highpressure.style = "background-color: #60f060";
|
highpressure.style = colors.default;
|
||||||
}
|
}
|
||||||
if (data.Pumps.RPOn) {
|
if (data.Pumps.RPOn) {
|
||||||
rp.innerHTML = "ON";
|
rp.innerHTML = "ON";
|
||||||
rp.style = "background-color: #60f060";
|
rp.style = colors.default;
|
||||||
} else {
|
} else {
|
||||||
rp.innerHTML = "OFF";
|
rp.innerHTML = "OFF";
|
||||||
rp.style = "background-color: #f06060";
|
rp.style = colors.highlightNeutral;
|
||||||
}
|
}
|
||||||
if (data.Pumps.DPOn) {
|
if (data.Pumps.DPOn) {
|
||||||
dp.innerHTML = "ON";
|
dp.innerHTML = "ON";
|
||||||
dp.style = "background-color: #60f060";
|
dp.style = colors.highlightCaution;
|
||||||
} else {
|
} else {
|
||||||
dp.innerHTML = "OFF";
|
dp.innerHTML = "OFF";
|
||||||
dp.style = "background-color: #f06060";
|
dp.style = colors.default;
|
||||||
|
}
|
||||||
|
|
||||||
|
tempSEM.innerHTML = data.Temperatures.SEM + " °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;
|
||||||
|
|
||||||
|
tempDPTop.innerHTML = data.Temperatures.DPTop + " °C";
|
||||||
|
tempDPTop.style = (data.Temperatures.DPTop > 30) ?
|
||||||
|
colors.highlightCaution : colors.default;
|
||||||
|
|
||||||
|
tempDPInlet.innerHTML = data.Temperatures.DPInlet + " °C";
|
||||||
|
tempDPInlet.style = (data.Temperatures.DPInlet > 30) ?
|
||||||
|
colors.highlightCaution : colors.default;
|
||||||
|
|
||||||
|
tempDPBottom.innerHTML = data.Temperatures.DPBottom + " °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;
|
||||||
}
|
}
|
||||||
|
|
||||||
let t = [];
|
let t = [];
|
||||||
if (data.Feedback.RoughReached) {
|
if (data.Feedback.RoughReached) {
|
||||||
trough.innerHTML = "OK";
|
trough.innerHTML = "OK";
|
||||||
trough.style = "background-color: #60f060";
|
trough.style = colors.highlightGood;
|
||||||
} else {
|
} else {
|
||||||
trough.innerHTML = "NOK";
|
trough.innerHTML = "NOK";
|
||||||
trough.style = "background-color: #f06060";
|
trough.style = colors.default;
|
||||||
}
|
}
|
||||||
if (data.Feedback.HighReached) {
|
if (data.Feedback.HighReached) {
|
||||||
thigh.innerHTML = "OK";
|
thigh.innerHTML = "OK";
|
||||||
thigh.style = "background-color: #60f060";
|
thigh.style = colors.highlightGood;
|
||||||
} else {
|
} else {
|
||||||
thigh.innerHTML = "NOK";
|
thigh.innerHTML = "NOK";
|
||||||
thigh.style = "background-color: #f06060";
|
thigh.style = colors.default;
|
||||||
}
|
}
|
||||||
load.innerHTML = data.LoopLoad.toString() + "%";
|
load.innerHTML = data.LoopLoad.toString() + "%";
|
||||||
historicalPush(data.Pirani.MbarFloat);
|
historicalPush(data.Pirani.MbarFloat);
|
||||||
|
@ -363,7 +461,16 @@ window.addEventListener("load", (_) => {
|
||||||
});
|
});
|
||||||
socket.addEventListener("close", (event) => {
|
socket.addEventListener("close", (event) => {
|
||||||
status.innerHTML = "Offline";
|
status.innerHTML = "Offline";
|
||||||
status.style = "background-color: #f06060;";
|
status.style = colors.highlightFault;
|
||||||
|
|
||||||
|
// Indicate all process values as unknown
|
||||||
|
|
||||||
|
[failsafe, highpressure, rp, dp, trough, thigh, volts, mbar, tempDPBottom, tempDPTop, tempDPInlet].forEach((el) => {
|
||||||
|
if (!el.innerHTML.includes("??")) {
|
||||||
|
el.innerHTML += "??";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
if (connected) {
|
if (connected) {
|
||||||
console.log("Socket dead, reconnecting...");
|
console.log("Socket dead, reconnecting...");
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,14 +14,18 @@ var (
|
||||||
flagFake bool
|
flagFake bool
|
||||||
flagListenHTTP string
|
flagListenHTTP string
|
||||||
flagPressureThresholdRough = ScientificNotationValue(1e-1)
|
flagPressureThresholdRough = ScientificNotationValue(1e-1)
|
||||||
|
flagPressureThresholdRoughHysteresis = ScientificNotationValue(3e-2)
|
||||||
flagPressureThresholdHigh = ScientificNotationValue(1e-4)
|
flagPressureThresholdHigh = ScientificNotationValue(1e-4)
|
||||||
|
flagPressureThresholdHighHysteresis = ScientificNotationValue(2e-5)
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
flag.BoolVar(&flagFake, "fake", false, "Enable fake mode which allows to run succd for tests outside the succbone")
|
flag.BoolVar(&flagFake, "fake", false, "Enable fake mode which allows to run succd for tests outside the succbone")
|
||||||
flag.StringVar(&flagListenHTTP, "listen_http", ":8080", "Address at which to listen for HTTP requests")
|
flag.StringVar(&flagListenHTTP, "listen_http", ":8080", "Address at which to listen for HTTP requests")
|
||||||
flag.TextVar(&flagPressureThresholdRough, "pressure_threshold_rough", &flagPressureThresholdRough, "Threshold for opening up diffusion pump (mbar)")
|
flag.TextVar(&flagPressureThresholdRough, "pressure_threshold_rough", &flagPressureThresholdRough, "Threshold for opening up diffusion pump (mbar)")
|
||||||
|
flag.TextVar(&flagPressureThresholdRoughHysteresis, "pressure_threshold_rough_hysteresis", &flagPressureThresholdRoughHysteresis, "+-Hysteresis for rough threshold (mbar)")
|
||||||
flag.TextVar(&flagPressureThresholdHigh, "pressure_threshold_high", &flagPressureThresholdHigh, "Threshold for enabling high voltage circuits (mbar)")
|
flag.TextVar(&flagPressureThresholdHigh, "pressure_threshold_high", &flagPressureThresholdHigh, "Threshold for enabling high voltage circuits (mbar)")
|
||||||
|
flag.TextVar(&flagPressureThresholdHighHysteresis, "pressure_threshold_high_hysteresis", &flagPressureThresholdHighHysteresis, "+-Hysteresis for high threshold (mbar)")
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
ctx, _ := signal.NotifyContext(context.Background(), os.Interrupt)
|
ctx, _ := signal.NotifyContext(context.Background(), os.Interrupt)
|
||||||
|
@ -31,17 +35,19 @@ func main() {
|
||||||
d.daemonState.piraniVolts3.limit = 3
|
d.daemonState.piraniVolts3.limit = 3
|
||||||
d.daemonState.piraniVolts100.limit = 100
|
d.daemonState.piraniVolts100.limit = 100
|
||||||
|
|
||||||
|
d.tempDPBottom = 420.6
|
||||||
|
d.tempDPTop = 69.0
|
||||||
|
d.tempDPInlet = 42.0
|
||||||
|
d.tempSEM = 42.5
|
||||||
|
d.humiditySEM = 66.6
|
||||||
|
|
||||||
d.aboveRough.threshold = float64(flagPressureThresholdRough)
|
d.aboveRough.threshold = float64(flagPressureThresholdRough)
|
||||||
|
d.aboveRough.hysteresis = float64(flagPressureThresholdRoughHysteresis)
|
||||||
d.aboveHigh.threshold = float64(flagPressureThresholdHigh)
|
d.aboveHigh.threshold = float64(flagPressureThresholdHigh)
|
||||||
|
d.aboveHigh.hysteresis = float64(flagPressureThresholdHighHysteresis)
|
||||||
if flagFake {
|
if flagFake {
|
||||||
klog.Infof("Starting with fake peripherals")
|
klog.Infof("Starting with fake peripherals")
|
||||||
d.adcPirani = &fakeADC{}
|
d.adcPirani = &fakeADC{}
|
||||||
d.gpioRoughingPump = &fakeGPIO{desc: "rp"}
|
|
||||||
d.gpioDiffusionPump = &fakeGPIO{desc: "~dp"}
|
|
||||||
d.gpioBtnPumpDown = &fakeGPIO{desc: "~pd"}
|
|
||||||
d.gpioBtnVent = &fakeGPIO{desc: "~vent"}
|
|
||||||
d.gpioBelowRough = &fakeGPIO{desc: "~rough"}
|
|
||||||
d.gpioBelowHigh = &fakeGPIO{desc: "~high"}
|
|
||||||
} else {
|
} else {
|
||||||
adc, err := newBBADC(0)
|
adc, err := newBBADC(0)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -49,21 +55,9 @@ func main() {
|
||||||
}
|
}
|
||||||
d.adcPirani = adc
|
d.adcPirani = adc
|
||||||
|
|
||||||
for _, c := range []struct {
|
err = d.modbusConnect()
|
||||||
out *gpio
|
|
||||||
num int
|
|
||||||
}{
|
|
||||||
{&d.gpioRoughingPump, 115},
|
|
||||||
{&d.gpioDiffusionPump, 49},
|
|
||||||
{&d.gpioBtnPumpDown, 48},
|
|
||||||
{&d.gpioBtnVent, 60},
|
|
||||||
{&d.gpioBelowRough, 30},
|
|
||||||
{&d.gpioBelowHigh, 7},
|
|
||||||
} {
|
|
||||||
*c.out, err = newBBGPIO(c.num, true)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
klog.Exitf("Failed to setup GPIO: %v", err)
|
klog.Exitf("Failed to connect to modbus %v", err)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -80,5 +74,9 @@ func main() {
|
||||||
}()
|
}()
|
||||||
|
|
||||||
go d.process(ctx)
|
go d.process(ctx)
|
||||||
|
if !flagFake {
|
||||||
|
go d.modbusProcess(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
<-ctx.Done()
|
<-ctx.Done()
|
||||||
}
|
}
|
||||||
|
|
186
succbone/succd/modbus.go
Normal file
186
succbone/succd/modbus.go
Normal file
|
@ -0,0 +1,186 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/simonvetter/modbus"
|
||||||
|
"k8s.io/klog"
|
||||||
|
)
|
||||||
|
|
||||||
|
func modbusValuesToFloat(v uint16) float32 {
|
||||||
|
return float32(v) / 10.0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *daemon) modbusConnect() error {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
d.mu.Lock()
|
||||||
|
defer d.mu.Unlock()
|
||||||
|
|
||||||
|
// Setup modbus client
|
||||||
|
d.modbusClient, err = modbus.NewClient(&modbus.ClientConfiguration{
|
||||||
|
URL: "tcp://10.250.241.20:8887",
|
||||||
|
Timeout: 1 * time.Second,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// Connect to modbus client
|
||||||
|
err = d.modbusClient.Open()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *daemon) modbusRestart() error {
|
||||||
|
d.modbusClient.Close()
|
||||||
|
return d.modbusClient.Open()
|
||||||
|
}
|
||||||
|
|
||||||
|
// There are currently two devices connected to the modbus.
|
||||||
|
// The first one (slave 1) is a temperature/humidity sensor.
|
||||||
|
// The second one (slave 2) is a PTA8D08 transmitter
|
||||||
|
//
|
||||||
|
// Returns whether modbus should restart (only in case of an underlying network error)
|
||||||
|
func (d *daemon) modbusUpdate() bool {
|
||||||
|
var err error
|
||||||
|
var numDevicesNotResponding int
|
||||||
|
|
||||||
|
// Switch to slave 1 (BTA1)
|
||||||
|
d.modbusClient.SetUnitId(1)
|
||||||
|
|
||||||
|
// Read temperature and humidity
|
||||||
|
var registersBTA1 []uint16 // temperature, humidity
|
||||||
|
registersBTA1, err = d.modbusClient.ReadRegisters(1, 2, modbus.INPUT_REGISTER)
|
||||||
|
if err != nil {
|
||||||
|
numDevicesNotResponding += 1
|
||||||
|
klog.Warningf("error while reading registers from BTA1 %v", err)
|
||||||
|
} else if len(registersBTA1) != 2 {
|
||||||
|
klog.Warningf("expected two registers from modbus slave 1, but got %d", len(registersBTA1))
|
||||||
|
} else {
|
||||||
|
d.mu.Lock()
|
||||||
|
d.daemonState.tempSEM = modbusValuesToFloat(registersBTA1[0])
|
||||||
|
d.daemonState.humiditySEM = modbusValuesToFloat(registersBTA1[1])
|
||||||
|
d.mu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Switch to slave 2 (KEC2)
|
||||||
|
d.modbusClient.SetUnitId(2)
|
||||||
|
|
||||||
|
// PT100 mapping
|
||||||
|
// Channel 0: Cable -WGA6, Sensor "dp bottom"
|
||||||
|
// Channel 1: Cable -WGA8, Sensor "dp inlet"
|
||||||
|
// Channel 2: Cable WGA7, Sensor "dp top"
|
||||||
|
var registersKEC2 []uint16 // temperatures dp
|
||||||
|
registersKEC2, err = d.modbusClient.ReadRegisters(0, 3, modbus.HOLDING_REGISTER)
|
||||||
|
if err != nil {
|
||||||
|
numDevicesNotResponding += 1
|
||||||
|
klog.Warningf("error while reading registers from KEC2 %v", err)
|
||||||
|
} else if len(registersKEC2) != 3 {
|
||||||
|
klog.Warningf("expected three registers from modbus slave 2, but got %d", len(registersKEC2))
|
||||||
|
} else {
|
||||||
|
d.mu.Lock()
|
||||||
|
d.daemonState.tempDPBottom = modbusValuesToFloat(registersKEC2[0])
|
||||||
|
d.daemonState.tempDPInlet = modbusValuesToFloat(registersKEC2[1])
|
||||||
|
d.daemonState.tempDPTop = modbusValuesToFloat(registersKEC2[2])
|
||||||
|
d.mu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Switch to slave 3 (KEC1)
|
||||||
|
d.modbusClient.SetUnitId(3)
|
||||||
|
|
||||||
|
// Do a read first to avoid side-effects from the subsequent write to the relay states
|
||||||
|
var digitalInputs [8]bool
|
||||||
|
var digitalInputRegisters []uint16
|
||||||
|
|
||||||
|
digitalInputRegisters, err = d.modbusClient.ReadRegisters(0x81, 8, modbus.HOLDING_REGISTER)
|
||||||
|
if err != nil {
|
||||||
|
numDevicesNotResponding += 1
|
||||||
|
klog.Warningf("error while reading digital inputs from KEC1 %v", err)
|
||||||
|
} else {
|
||||||
|
// Convert MODBUS words into bools
|
||||||
|
for idx, value := range digitalInputRegisters {
|
||||||
|
if value != 0 {
|
||||||
|
digitalInputs[idx] = true
|
||||||
|
} else {
|
||||||
|
digitalInputs[idx] = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// TODO: Input mapping goes here
|
||||||
|
}
|
||||||
|
|
||||||
|
// We must wait between reading and writing to the -KEC1 relay board
|
||||||
|
// because otherwise it chokes and times out the write registers
|
||||||
|
// command.
|
||||||
|
time.Sleep(time.Millisecond * 10)
|
||||||
|
|
||||||
|
// KFA1-KFA8
|
||||||
|
var relayState [8]bool
|
||||||
|
d.mu.Lock()
|
||||||
|
// -KFA1 Roughing Pump (normally closed contact)
|
||||||
|
relayState[0] = !d.daemonState.rpOn
|
||||||
|
// -KFA2 Diffusion Pump
|
||||||
|
relayState[1] = d.daemonState.dpOn
|
||||||
|
// -KFA4 Button Vent
|
||||||
|
relayState[3] = d.daemonState.vent.output
|
||||||
|
// -KFA5 Button Pump-Down
|
||||||
|
relayState[4] = d.daemonState.pumpdown.output
|
||||||
|
// -KFA6 Fake-Pirani Rough (normally closed contact)
|
||||||
|
relayState[5] = !d.aboveRough.output
|
||||||
|
// -KFA7 Fake-Pirani High (normally closed contact)
|
||||||
|
relayState[6] = !d.aboveHigh.output
|
||||||
|
d.mu.Unlock()
|
||||||
|
|
||||||
|
// The KEC1 module uses a non-standard MODBUS interface
|
||||||
|
// instead of coils
|
||||||
|
// 0x0100 is the open command
|
||||||
|
// 0x0200 is the close command
|
||||||
|
// We write 8 words (16-bit) to address 0x01 to update the relays
|
||||||
|
var registerValuesKEC1 [8]uint16
|
||||||
|
// Convert the boolean values to the commands
|
||||||
|
for idx, state := range relayState {
|
||||||
|
if state {
|
||||||
|
registerValuesKEC1[idx] = 0x0100
|
||||||
|
} else {
|
||||||
|
registerValuesKEC1[idx] = 0x0200
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err = d.modbusClient.WriteRegisters(0x01, registerValuesKEC1[:])
|
||||||
|
if err != nil {
|
||||||
|
numDevicesNotResponding += 1
|
||||||
|
klog.Warningf("error while updating registers %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if numDevicesNotResponding >= 4 {
|
||||||
|
klog.Warningf("no device did respond to our request. Probably a network timeout.")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call modbusUpdate every 100 milliseconds
|
||||||
|
func (d *daemon) modbusProcess(ctx context.Context) {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
shouldRestart := d.modbusUpdate()
|
||||||
|
// the modbus library does not reopen the tcp socket in case of
|
||||||
|
// a connection loss.
|
||||||
|
if shouldRestart {
|
||||||
|
klog.Infof("restarting modbus connection...")
|
||||||
|
err := d.modbusRestart()
|
||||||
|
if err != nil {
|
||||||
|
klog.Warningf("failed to restart modbus %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
time.Sleep(time.Millisecond * 100)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -8,22 +8,17 @@ import (
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/simonvetter/modbus"
|
||||||
"k8s.io/klog"
|
"k8s.io/klog"
|
||||||
)
|
)
|
||||||
|
|
||||||
// daemon is the main service of the succdaemon.
|
// daemon is the main service of the succdaemon.
|
||||||
type daemon struct {
|
type daemon struct {
|
||||||
|
modbusClient *modbus.ModbusClient
|
||||||
// adcPirani is the adc implementation returning the voltage of the Pfeiffer
|
// adcPirani is the adc implementation returning the voltage of the Pfeiffer
|
||||||
// Pirani gauge.
|
// Pirani gauge.
|
||||||
adcPirani adc
|
adcPirani adc
|
||||||
|
|
||||||
gpioDiffusionPump gpio
|
|
||||||
gpioRoughingPump gpio
|
|
||||||
gpioBtnPumpDown gpio
|
|
||||||
gpioBtnVent gpio
|
|
||||||
gpioBelowRough gpio
|
|
||||||
gpioBelowHigh gpio
|
|
||||||
|
|
||||||
load atomic.Int64
|
load atomic.Int64
|
||||||
|
|
||||||
// mu guards the state below.
|
// mu guards the state below.
|
||||||
|
@ -55,6 +50,13 @@ type daemonState struct {
|
||||||
pumpdown momentaryOutput
|
pumpdown momentaryOutput
|
||||||
aboveRough thresholdOutput
|
aboveRough thresholdOutput
|
||||||
aboveHigh thresholdOutput
|
aboveHigh thresholdOutput
|
||||||
|
|
||||||
|
tempDPBottom float32
|
||||||
|
tempDPTop float32
|
||||||
|
tempDPInlet float32
|
||||||
|
|
||||||
|
tempSEM float32
|
||||||
|
humiditySEM float32
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *daemonState) vacuumStatus() (rough, high bool) {
|
func (d *daemonState) vacuumStatus() (rough, high bool) {
|
||||||
|
@ -162,33 +164,5 @@ func (d *daemon) processOnce(_ context.Context) error {
|
||||||
d.dpOn = false
|
d.dpOn = false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update relay outputs.
|
|
||||||
for _, rel := range []struct {
|
|
||||||
name string
|
|
||||||
gpio gpio
|
|
||||||
// activeHigh means the relay is active high, ie. a true source will
|
|
||||||
// mean that NO/COM get connected, and a false source means that NC/COM
|
|
||||||
// get connected.
|
|
||||||
activeHigh bool
|
|
||||||
source bool
|
|
||||||
}{
|
|
||||||
{"rp", d.gpioRoughingPump, false, d.rpOn},
|
|
||||||
{"dp", d.gpioDiffusionPump, true, d.dpOn},
|
|
||||||
{"pumpdown", d.gpioBtnPumpDown, true, d.pumpdown.output},
|
|
||||||
{"vent", d.gpioBtnVent, true, d.vent.output},
|
|
||||||
{"rough", d.gpioBelowRough, false, d.aboveRough.output},
|
|
||||||
{"high", d.gpioBelowHigh, false, d.aboveHigh.output},
|
|
||||||
} {
|
|
||||||
val := rel.source
|
|
||||||
if rel.activeHigh {
|
|
||||||
// Invert because the relays go through logical inversion (ie. a
|
|
||||||
// GPIO false is a relay trigger).
|
|
||||||
val = !val
|
|
||||||
}
|
|
||||||
if err := rel.gpio.set(val); err != nil {
|
|
||||||
return fmt.Errorf("when outputting %s: %w", rel.name, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,13 +30,20 @@ type thresholdOutput struct {
|
||||||
debounce time.Time
|
debounce time.Time
|
||||||
// threshold is the setpoint of the block.
|
// threshold is the setpoint of the block.
|
||||||
threshold float64
|
threshold float64
|
||||||
|
// hysteresis around the process setpoint (min/max is threshold +- hysteresis)
|
||||||
|
hysteresis float64
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *thresholdOutput) process(value float64) {
|
func (t *thresholdOutput) process(value float64) {
|
||||||
if time.Now().Before(t.debounce) {
|
if time.Now().Before(t.debounce) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
new := value > t.threshold
|
new := t.output
|
||||||
|
if t.output {
|
||||||
|
new = value > (t.threshold - t.hysteresis)
|
||||||
|
} else {
|
||||||
|
new = value > (t.threshold + t.hysteresis)
|
||||||
|
}
|
||||||
if new != t.output {
|
if new != t.output {
|
||||||
t.output = new
|
t.output = new
|
||||||
t.debounce = time.Now().Add(time.Second * 5)
|
t.debounce = time.Now().Add(time.Second * 5)
|
||||||
|
|
|
@ -27,19 +27,36 @@ func TestMomentaryOutput(t *testing.T) {
|
||||||
func TestThresholdOutput(t *testing.T) {
|
func TestThresholdOutput(t *testing.T) {
|
||||||
to := thresholdOutput{
|
to := thresholdOutput{
|
||||||
threshold: 1,
|
threshold: 1,
|
||||||
|
hysteresis: 0.2,
|
||||||
}
|
}
|
||||||
to.process(0)
|
to.process(0.7)
|
||||||
if to.output {
|
if to.output {
|
||||||
t.Fatalf("output shouldn't have triggered")
|
t.Fatalf("output shouldn't have triggered")
|
||||||
}
|
}
|
||||||
to.process(2)
|
to.process(1)
|
||||||
|
if to.output {
|
||||||
|
t.Fatalf("output shouldn't have triggered")
|
||||||
|
}
|
||||||
|
to.process(1.3)
|
||||||
if !to.output {
|
if !to.output {
|
||||||
t.Fatalf("output should have triggered")
|
t.Fatalf("output should have triggered")
|
||||||
}
|
}
|
||||||
to.process(0)
|
to.process(1)
|
||||||
|
if !to.output {
|
||||||
|
t.Fatalf("output should have triggered")
|
||||||
|
}
|
||||||
|
to.process(0.7)
|
||||||
if !to.output {
|
if !to.output {
|
||||||
t.Fatalf("output should have triggered (in debounce)")
|
t.Fatalf("output should have triggered (in debounce)")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// let debounce timeout pass
|
||||||
|
time.Sleep(time.Second * 6)
|
||||||
|
|
||||||
|
to.process(0.7)
|
||||||
|
if to.output {
|
||||||
|
t.Fatalf("output shouldn't have triggered")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRingbufferInput(t *testing.T) {
|
func TestRingbufferInput(t *testing.T) {
|
||||||
|
|
Loading…
Reference in a new issue