jeol-t330a/succbone/succd/process.go

207 lines
4.9 KiB
Go
Raw Normal View History

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)
}
}
// ringbufferInput accumulates analog data up to limit samples, and calculates
// an average.
type ringbufferInput struct {
data []float32
limit uint
avg float32
}
func (r *ringbufferInput) process(input float32) {
// TODO(q3k): use actual ringbuffer
// TODO(q3k): optimize average calculation
// TODO(q3k): precalculate value in mbar
r.data = append(r.data, input)
trim := len(r.data) - int(r.limit)
if trim > 0 {
r.data = r.data[trim:]
}
avg := float32(0.0)
for _, v := range r.data {
avg += v
}
if len(r.data) != 0 {
avg /= float32(len(r.data))
}
r.avg = avg
}
// 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 ringbuffers.
d.piraniVolts3.process(v)
d.piraniVolts100.process(v)
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 {
2024-09-27 00:24:19 +00:00
// Unrealistic result, Pirani probe probably disconnected. Failsafe mode.
if !d.safety.failsafe {
d.safety.failsafe = true
2024-09-27 00:24:19 +00:00
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")
2024-09-27 00:24:19 +00:00
}
}
if d.safety.failsafe {
2024-09-27 00:24:19 +00:00
d.aboveRough.output = true
d.aboveHigh.output = true
d.dpOn = false
}
if d.safety.highPressure {
d.dpOn = false
2024-09-27 00:24:19 +00:00
}
// 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
}