jeol-t330a/succbone/succd/process.go
Serge Bazanski 8f7ec7e141 succd: split out http server, daemon state, daemon controller
This improves the structure of the code, separating the data/control
interface out and then implementing the http interface as a user of this
interface.
2024-09-28 08:10:35 +02:00

183 lines
4.4 KiB
Go

package main
import (
"context"
"errors"
"fmt"
"sync"
"time"
"k8s.io/klog"
)
// daemon is the main service of the succdaemon.
type daemon struct {
// 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
// mu guards the state below.
mu sync.RWMutex
daemonState
}
// momentaryOutput is an output that can be triggered for 500ms.
type momentaryOutput struct {
// output of the block.
output bool
// scheduledOff is when the block should be outputting false again.
scheduledOff time.Time
}
func (m *momentaryOutput) process() {
m.output = m.scheduledOff.After(time.Now())
}
func (m *momentaryOutput) trigger() {
m.scheduledOff = time.Now().Add(time.Millisecond * 500)
}
// thresholdOutput outputs true if a given value is above a setpoint/threshold.
// It contains debounce logic for processing noisy analog signals.
type thresholdOutput struct {
// output of the block.
output bool
// debounce is when the debouncer should be inactive again.
debounce time.Time
// threshold is the setpoint of the block.
threshold float64
}
func (t *thresholdOutput) process(value float64) {
if time.Now().Before(t.debounce) {
return
}
new := value > t.threshold
if new != t.output {
t.output = new
t.debounce = time.Now().Add(time.Second * 5)
}
}
// process runs the pain acquisition and control loop of succd.
func (d *daemon) process(ctx context.Context) {
ticker := time.NewTicker(time.Millisecond * 100)
defer ticker.Stop()
for {
select {
case <-ticker.C:
if err := d.processOnce(ctx); err != nil {
if errors.Is(err, ctx.Err()) {
return
} else {
klog.Errorf("Processing error: %v", err)
time.Sleep(time.Second * 10)
}
}
case <-ctx.Done():
return
}
}
}
// processOnce runs the main loop step of succd.
func (d *daemon) processOnce(_ context.Context) error {
v, err := d.adcPirani.Read()
if err != nil {
return fmt.Errorf("when reading ADC: %w", err)
}
d.mu.Lock()
defer d.mu.Unlock()
// Process pirani ringbuffer.
d.adcPiraniVolts = append(d.adcPiraniVolts, v)
trim := len(d.adcPiraniVolts) - 100
if trim > 0 {
d.adcPiraniVolts = d.adcPiraniVolts[trim:]
}
d.pumpdown.process()
d.vent.process()
_, mbar := d.daemonState.pirani()
d.aboveRough.process(float64(mbar))
d.aboveHigh.process(float64(mbar))
// Check if the pirani gauge is disconnected. Note: this will assume the
// pirani gauge is connected for the first couple of processing runs as
// samples are still being captured.
if d.piraniDetection() == piraniDetectionDisconnected {
// Unrealistic result, Pirani probe probably disconnected. Failsafe mode.
if !d.safety.failsafe {
d.safety.failsafe = true
klog.Errorf("Pirani probe seems disconnected; enabling failsafe mode")
}
} else {
if d.safety.failsafe {
if mbar >= 1e2 {
d.safety.failsafe = false
klog.Infof("Values are plausible again; quitting failsafe mode")
}
}
}
if mbar >= 1e-1 {
if !d.safety.highPressure {
d.safety.highPressure = true
klog.Errorf("Pressure is too high; enabling diffusion pump lockout")
}
} else if mbar < (1e-1)-(1e-2) {
if d.safety.highPressure {
d.safety.highPressure = false
klog.Infof("Pressure is low enough for diffusion pump operation; quitting diffusion pump lockout")
}
}
if d.safety.failsafe {
d.aboveRough.output = true
d.aboveHigh.output = true
d.dpOn = false
}
if d.safety.highPressure {
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
}