diff --git a/succbone/succd/go.mod b/succbone/succd/go.mod index d35e107..574985d 100644 --- a/succbone/succd/go.mod +++ b/succbone/succd/go.mod @@ -4,8 +4,5 @@ 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 diff --git a/succbone/succd/go.sum b/succbone/succd/go.sum index ab9ca54..c8b5195 100644 --- a/succbone/succd/go.sum +++ b/succbone/succd/go.sum @@ -1,9 +1,5 @@ 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= diff --git a/succbone/succd/http.go b/succbone/succd/http.go index 01eab8e..f1503e7 100644 --- a/succbone/succd/http.go +++ b/succbone/succd/http.go @@ -62,17 +62,6 @@ 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 @@ -126,11 +115,6 @@ 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() @@ -233,26 +217,6 @@ func (s *webServer) viewMetrics(w http.ResponseWriter, r *http.Request) { 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) { diff --git a/succbone/succd/index.html b/succbone/succd/index.html index 37f0849..abd5695 100644 --- a/succbone/succd/index.html +++ b/succbone/succd/index.html @@ -9,19 +9,19 @@ body { padding: 2em; } table { - font-size: 30px; + font-size: 40px; } table.status td { width: 2em; } th, td { background-color: #e8e8e8; - padding: 0.3em; + padding: 0.4em; } th { font-weight: 100; text-align: right; - font-size: 25px; + font-size: 30px; } td { text-align: left; @@ -34,7 +34,7 @@ h2 { font-weight: 100; } button { - height: 3.3em; + height: 4.5em; padding-left: 1.5em; padding-right: 1.5em; } @@ -76,7 +76,6 @@ td > span { .has-hidden:hover .hidden-text { display: block; -} @media only screen and (max-width: 700px) { body { @@ -121,6 +120,19 @@ td > span { + + + + + +
Pirani Pressure +
{{ .Pirani.Mbar }}
+
+ Voltage: {{ .Pirani.Volts }} +
+
+ + @@ -151,41 +163,6 @@ td > span {
Control
- - - - - - - - - - - - - - - - - - - - - - - - - - - -
Pirani Pressure -
{{ .Pirani.Mbar }}
-
- Voltage: {{ .Pirani.Volts }} -
-
TemperaturesDP Bottom{{ .Temperatures.DPBottom }} °C
DP Top{{ .Temperatures.DPTop }} °C
DP Inlet{{ .Temperatures.DPInlet }} °C
SEM Environment{{ .Temperatures.SEM }} °C
HumiditySEM Environment{{ .Humidity.SEM }}%
- -
@@ -339,11 +316,6 @@ 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"); @@ -416,30 +388,6 @@ 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"; @@ -465,7 +413,7 @@ window.addEventListener("load", (_) => { // Indicate all process values as unknown - [failsafe, highpressure, rp, dp, trough, thigh, volts, mbar, tempDPBottom, tempDPTop, tempDPInlet].forEach((el) => { + [failsafe, highpressure, rp, dp, trough, thigh, volts, mbar].forEach((el) => { if (!el.innerHTML.includes("??")) { el.innerHTML += "??"; } diff --git a/succbone/succd/main.go b/succbone/succd/main.go index 52a10db..64663a8 100644 --- a/succbone/succd/main.go +++ b/succbone/succd/main.go @@ -35,12 +35,6 @@ 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) @@ -48,6 +42,12 @@ func main() { if flagFake { klog.Infof("Starting with fake peripherals") 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 { adc, err := newBBADC(0) if err != nil { @@ -55,9 +55,21 @@ 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 + }{ + {&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) - if !flagFake { - go d.modbusProcess(ctx) - } - <-ctx.Done() } diff --git a/succbone/succd/modbus.go b/succbone/succd/modbus.go deleted file mode 100644 index 4aebd03..0000000 --- a/succbone/succd/modbus.go +++ /dev/null @@ -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) - } - } -} diff --git a/succbone/succd/process.go b/succbone/succd/process.go index 7504b19..de99e84 100644 --- a/succbone/succd/process.go +++ b/succbone/succd/process.go @@ -8,17 +8,22 @@ 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 + gpioDiffusionPump gpio + gpioRoughingPump gpio + gpioBtnPumpDown gpio + gpioBtnVent gpio + gpioBelowRough gpio + gpioBelowHigh gpio + load atomic.Int64 // mu guards the state below. @@ -50,13 +55,6 @@ 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) { @@ -164,5 +162,33 @@ func (d *daemon) processOnce(_ context.Context) error { 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 }