Compare commits

..

4 commits

Author SHA1 Message Date
Rahix e4fa961ef0 succd: Only show voltage on hover
All checks were successful
/ test (push) Successful in 10s
/ test (pull_request) Successful in 10s
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
Rahix 314dd72aa6 succd: Optimize color usage for HP HMI
In the process automation world, there is a trend to move away from
colorful user-interfaces, towards more "boring" colorschemes.  The
argument is about situational awareness - by only using colors to
highlight abnormal situations, they become instantly recognizable to the
operators.

This design philosophy is outlined by the ISA-101 [1] under the name
"High Performance HMI".  While it covers much more than just colors, I
think this is the most important part and the one that is most
applicable for our usecase.

So let's do a bit of HP HMI - reduce colors usage such that only
important information is highlighted.

[1]: https://www.isa.org/standards-and-publications/isa-standards/isa-standards-committees/isa101
2024-10-05 02:52:51 +02:00
Rahix 276551064f succd: Fix mobile layout
Use smaller font sizes for mobile devices so the full interface fits on
a single screen (mostly).
2024-10-05 02:52:48 +02:00
Rahix 4f74e92c45 succd: Fix layout on large screens
Make sure the grid cannot grow too large on big screens.  Also slightly
adjust the breakpoint to avoid some weird artifacts.
2024-10-05 02:40:20 +02:00
13 changed files with 82 additions and 392 deletions

View file

@ -11,8 +11,4 @@ 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)

Binary file not shown.

BIN
succbone/panel.pdf (Stored with Git LFS)

Binary file not shown.

BIN
succbone/panel.qet (Stored with Git LFS)

Binary file not shown.

BIN
succbone/succbone-din-mount.FCStd (Stored with Git LFS)

Binary file not shown.

BIN
succbone/succbone-din-mount.stl (Stored with Git LFS)

Binary file not shown.

View file

@ -4,8 +4,5 @@ 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

View file

@ -1,9 +1,5 @@
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=

View file

@ -62,17 +62,6 @@ 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
@ -126,11 +115,6 @@ 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()
@ -179,80 +163,16 @@ 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", state.piraniMbar100.mbar) fmt.Fprintf(w, "sem_pressure_mbar %f\n", 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) {

View file

@ -9,19 +9,19 @@ body {
padding: 2em; padding: 2em;
} }
table { table {
font-size: 30px; font-size: 40px;
} }
table.status td { table.status td {
width: 2em; width: 2em;
} }
th, td { th, td {
background-color: #e8e8e8; background-color: #e8e8e8;
padding: 0.3em; padding: 0.4em;
} }
th { th {
font-weight: 100; font-weight: 100;
text-align: right; text-align: right;
font-size: 25px; font-size: 30px;
} }
td { td {
text-align: left; text-align: left;
@ -34,7 +34,7 @@ h2 {
font-weight: 100; font-weight: 100;
} }
button { button {
height: 3.3em; height: 4.5em;
padding-left: 1.5em; padding-left: 1.5em;
padding-right: 1.5em; padding-right: 1.5em;
} }
@ -76,7 +76,6 @@ td > span {
.has-hidden:hover .hidden-text { .has-hidden:hover .hidden-text {
display: block; display: block;
}
@media only screen and (max-width: 700px) { @media only screen and (max-width: 700px) {
body { body {
@ -121,6 +120,19 @@ td > span {
</tr> </tr>
</table> </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> <table>
<tr> <tr>
<th rowspan="3">Control</th> <th rowspan="3">Control</th>
@ -151,41 +163,6 @@ td > span {
</tr> </tr>
</table> </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"> <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>
</div> </div>
@ -339,11 +316,6 @@ 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");
@ -416,30 +388,6 @@ window.addEventListener("load", (_) => {
dp.style = colors.default; dp.style = colors.default;
} }
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;
tempDPTop.innerHTML = data.Temperatures.DPTop + "&nbsp;°C";
tempDPTop.style = (data.Temperatures.DPTop > 30) ?
colors.highlightCaution : colors.default;
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;
}
let t = []; let t = [];
if (data.Feedback.RoughReached) { if (data.Feedback.RoughReached) {
trough.innerHTML = "OK"; trough.innerHTML = "OK";
@ -462,15 +410,6 @@ window.addEventListener("load", (_) => {
socket.addEventListener("close", (event) => { socket.addEventListener("close", (event) => {
status.innerHTML = "Offline"; status.innerHTML = "Offline";
status.style = colors.highlightFault; 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...");
} }

View file

@ -14,7 +14,7 @@ var (
flagFake bool flagFake bool
flagListenHTTP string flagListenHTTP string
flagPressureThresholdRough = ScientificNotationValue(1e-1) flagPressureThresholdRough = ScientificNotationValue(1e-1)
flagPressureThresholdRoughHysteresis = ScientificNotationValue(3e-2) flagPressureThresholdRoughHysteresis = ScientificNotationValue(2e-2)
flagPressureThresholdHigh = ScientificNotationValue(1e-4) flagPressureThresholdHigh = ScientificNotationValue(1e-4)
flagPressureThresholdHighHysteresis = ScientificNotationValue(2e-5) flagPressureThresholdHighHysteresis = ScientificNotationValue(2e-5)
) )
@ -35,12 +35,6 @@ 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.aboveRough.hysteresis = float64(flagPressureThresholdRoughHysteresis)
d.aboveHigh.threshold = float64(flagPressureThresholdHigh) d.aboveHigh.threshold = float64(flagPressureThresholdHigh)
@ -48,6 +42,12 @@ func main() {
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 {
@ -55,9 +55,21 @@ func main() {
} }
d.adcPirani = adc d.adcPirani = adc
err = d.modbusConnect() for _, c := range []struct {
if err != nil { out *gpio
klog.Exitf("Failed to connect to modbus %v", err) 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 {
klog.Exitf("Failed to setup GPIO: %v", err)
}
} }
} }
@ -74,9 +86,5 @@ func main() {
}() }()
go d.process(ctx) go d.process(ctx)
if !flagFake {
go d.modbusProcess(ctx)
}
<-ctx.Done() <-ctx.Done()
} }

View file

@ -1,186 +0,0 @@
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)
}
}
}

View file

@ -8,17 +8,22 @@ 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.
@ -50,13 +55,6 @@ 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) {
@ -164,5 +162,33 @@ 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
} }