Compare commits
6 commits
260f77a2a8
...
4ac1b5eb32
Author | SHA1 | Date | |
---|---|---|---|
Rahix | 4ac1b5eb32 | ||
Rahix | c02d24414f | ||
2e6a3be100 | |||
4edabc5c56 | |||
Rahix | 52231a9e9c | ||
Rahix | 0aba323779 |
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.
|
@ -4,5 +4,8 @@ go 1.22.3
|
|||
|
||||
require (
|
||||
github.com/coder/websocket v1.8.12
|
||||
github.com/simonvetter/modbus v1.6.3
|
||||
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/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs=
|
||||
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/go.mod h1:4Bi6QPql/J/LkTDqv7R/cd3hPo4k2DG6Ptcz060Ez5I=
|
||||
|
|
|
@ -62,6 +62,17 @@ type apiData struct {
|
|||
// DPOn means the diffusion pump is turned on.
|
||||
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.
|
||||
Feedback struct {
|
||||
// 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.Pumps.RPOn = state.rpOn
|
||||
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.HighReached = high
|
||||
ad.LoopLoad = s.d.loopLoad()
|
||||
|
@ -163,16 +179,60 @@ 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.
|
||||
func (s *webServer) viewMetrics(w http.ResponseWriter, r *http.Request) {
|
||||
// TODO(q3k): also serve Go stuff using the actual Prometheus metrics client
|
||||
// library.
|
||||
// TODO(q3k): serve the rest of the data model
|
||||
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, "# 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))
|
||||
}
|
||||
|
||||
func (s *webServer) viewRoughingPumpEnable(w http.ResponseWriter, r *http.Request) {
|
||||
|
|
|
@ -9,19 +9,19 @@ body {
|
|||
padding: 2em;
|
||||
}
|
||||
table {
|
||||
font-size: 40px;
|
||||
font-size: 30px;
|
||||
}
|
||||
table.status td {
|
||||
width: 2em;
|
||||
}
|
||||
th, td {
|
||||
background-color: #e8e8e8;
|
||||
padding: 0.4em;
|
||||
padding: 0.3em;
|
||||
}
|
||||
th {
|
||||
font-weight: 100;
|
||||
text-align: right;
|
||||
font-size: 30px;
|
||||
font-size: 25px;
|
||||
}
|
||||
td {
|
||||
text-align: left;
|
||||
|
@ -34,7 +34,7 @@ h2 {
|
|||
font-weight: 100;
|
||||
}
|
||||
button {
|
||||
height: 4.5em;
|
||||
height: 3.3em;
|
||||
padding-left: 1.5em;
|
||||
padding-right: 1.5em;
|
||||
}
|
||||
|
@ -76,6 +76,7 @@ td > span {
|
|||
|
||||
.has-hidden:hover .hidden-text {
|
||||
display: block;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 700px) {
|
||||
body {
|
||||
|
@ -120,19 +121,6 @@ td > span {
|
|||
</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>
|
||||
|
@ -163,6 +151,41 @@ td > span {
|
|||
</tr>
|
||||
</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 }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>DP Top</th>
|
||||
<td id="temp-dp-top">{{ .Temperatures.DPTop }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>DP Inlet</th>
|
||||
<td id="temp-dp-inlet">{{ .Temperatures.DPInlet }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>SEM Environment</th>
|
||||
<td id="temp-sem">{{ .Temperatures.SEM }}</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>
|
||||
|
@ -316,6 +339,11 @@ window.addEventListener("load", (_) => {
|
|||
let trough = document.querySelector("#trough");
|
||||
let thigh = document.querySelector("#thigh");
|
||||
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
|
||||
let pd = document.querySelector("#pd");
|
||||
|
@ -388,6 +416,30 @@ window.addEventListener("load", (_) => {
|
|||
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 = [];
|
||||
if (data.Feedback.RoughReached) {
|
||||
trough.innerHTML = "OK";
|
||||
|
@ -413,7 +465,7 @@ window.addEventListener("load", (_) => {
|
|||
|
||||
// Indicate all process values as unknown
|
||||
|
||||
[failsafe, highpressure, rp, dp, trough, thigh, volts, mbar].forEach((el) => {
|
||||
[failsafe, highpressure, rp, dp, trough, thigh, volts, mbar, tempDPBottom, tempDPTop, tempDPInlet].forEach((el) => {
|
||||
if (!el.innerHTML.includes("??")) {
|
||||
el.innerHTML += "??";
|
||||
}
|
||||
|
|
|
@ -35,6 +35,12 @@ func main() {
|
|||
d.daemonState.piraniVolts3.limit = 3
|
||||
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.hysteresis = float64(flagPressureThresholdRoughHysteresis)
|
||||
d.aboveHigh.threshold = float64(flagPressureThresholdHigh)
|
||||
|
@ -55,6 +61,11 @@ func main() {
|
|||
}
|
||||
d.adcPirani = adc
|
||||
|
||||
err = d.modbusConnect()
|
||||
if err != nil {
|
||||
klog.Exitf("Failed to connect to modbus %v", err)
|
||||
}
|
||||
|
||||
for _, c := range []struct {
|
||||
out *gpio
|
||||
num int
|
||||
|
@ -86,5 +97,9 @@ func main() {
|
|||
}()
|
||||
|
||||
go d.process(ctx)
|
||||
if !flagFake {
|
||||
go d.modbusProcess(ctx)
|
||||
}
|
||||
|
||||
<-ctx.Done()
|
||||
}
|
||||
|
|
101
succbone/succd/modbus.go
Normal file
101
succbone/succd/modbus.go
Normal file
|
@ -0,0 +1,101 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/simonvetter/modbus"
|
||||
)
|
||||
|
||||
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: 5 * time.Second,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Connect to modbus client
|
||||
err = d.modbusClient.Open()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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
|
||||
func (d *daemon) modbusUpdate() error {
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
|
||||
var err error
|
||||
var registers []uint16 // temperature, humidity
|
||||
|
||||
// Switch to slave 1
|
||||
d.modbusClient.SetUnitId(1)
|
||||
|
||||
// Read temperature and humidity
|
||||
registers, err = d.modbusClient.ReadRegisters(1, 2, modbus.INPUT_REGISTER)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(registers) != 2 {
|
||||
msg := fmt.Sprintf("Expected two registers from modbus slave 1, but got %d", len(registers))
|
||||
return errors.New(msg)
|
||||
}
|
||||
|
||||
d.daemonState.tempSEM = modbusValuesToFloat(registers[0])
|
||||
d.daemonState.humiditySEM = modbusValuesToFloat(registers[1])
|
||||
|
||||
// Switch to slave 2
|
||||
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"
|
||||
registers, err = d.modbusClient.ReadRegisters(0, 3, modbus.HOLDING_REGISTER)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(registers) != 3 {
|
||||
msg := fmt.Sprintf("Expected three registers from modbus slave 2, but got %d", len(registers))
|
||||
return errors.New(msg)
|
||||
}
|
||||
|
||||
d.daemonState.tempDPBottom = modbusValuesToFloat(registers[0])
|
||||
d.daemonState.tempDPInlet = modbusValuesToFloat(registers[1])
|
||||
d.daemonState.tempDPTop = modbusValuesToFloat(registers[2])
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Call modbusUpdate every second
|
||||
func (d *daemon) modbusProcess(ctx context.Context) {
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
default:
|
||||
d.modbusUpdate()
|
||||
time.Sleep(time.Second * 1)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -8,11 +8,13 @@ import (
|
|||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/simonvetter/modbus"
|
||||
"k8s.io/klog"
|
||||
)
|
||||
|
||||
// daemon is the main service of the succdaemon.
|
||||
type daemon struct {
|
||||
modbusClient *modbus.ModbusClient
|
||||
// adcPirani is the adc implementation returning the voltage of the Pfeiffer
|
||||
// Pirani gauge.
|
||||
adcPirani adc
|
||||
|
@ -55,6 +57,13 @@ type daemonState struct {
|
|||
pumpdown momentaryOutput
|
||||
aboveRough thresholdOutput
|
||||
aboveHigh thresholdOutput
|
||||
|
||||
tempDPBottom float32
|
||||
tempDPTop float32
|
||||
tempDPInlet float32
|
||||
|
||||
tempSEM float32
|
||||
humiditySEM float32
|
||||
}
|
||||
|
||||
func (d *daemonState) vacuumStatus() (rough, high bool) {
|
||||
|
|
Loading…
Reference in a new issue