package main import ( "context" "errors" "fmt" "math" "sync" "time" "k8s.io/klog" ) // daemon is the main state of the succdaemon. type daemon struct { // adcPirani is the adc implementation returning the voltage of the Pfeiffer // Pirani gauge. adcPirani adc failsafe bool highPressure bool gpioDiffusionPump gpio gpioRoughingPump gpio gpioBtnPumpDown gpio gpioBtnVent gpio gpioBelowRough gpio gpioBelowHigh gpio // mu guards state variables below. mu sync.RWMutex // adcPiraniVolts is a moving window of read ADC values, used to calculate a // moving average. adcPiraniVolts []float32 rpOn bool dpOn bool vent momentaryOutput pumpdown momentaryOutput aboveRough thresholdOutput aboveHigh thresholdOutput } // 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.piraniUnlocked() d.aboveRough.process(float64(mbar)) d.aboveHigh.process(float64(mbar)) // max rate of 1.0 in 500 ms because ringbuffer holds 5 values if d.piraniRateOfChange() > 2.0 { if !d.failsafe { d.failsafe = true klog.Errorf("Pressure changed too fast; entering failsafe mode") } } else if mbar < 4e-6 { // Unrealistic result, Pirani probe probably disconnected. Failsafe mode. if !d.failsafe { d.failsafe = true klog.Errorf("Pirani probe seems disconnected; enabling failsafe mode") } } else { if d.failsafe { d.failsafe = false klog.Infof("Values are plausible again; quitting failsafe mode") } } if mbar >= 1e-1 { if !d.highPressure { d.highPressure = true klog.Errorf("Pressure is too high; enabling diffusion pump lockout") } } else { if d.highPressure { d.highPressure = false klog.Infof("Pressure is low enough for diffusion pump operation; quitting diffusion pump lockout") } } if d.failsafe { d.aboveRough.output = true d.aboveHigh.output = true d.dpOn = false } if d.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 } func (d *daemon) piraniRateOfChange() float32 { max := d.adcPiraniVolts[0] min := d.adcPiraniVolts[0] for _, val := range d.adcPiraniVolts { if val < min { min = val } if val > max { max = val } } max_bar = math.Pow(10.0, float64(max)-8.5); max_mbar = float32(max_bar * 1000.0) min_bar = math.Pow(10.0, float64(min)-8.5); min_mbar = float32(min_bar * 1000.0) (max_mbar - min_mbar) } // pirani returns the Pirani gauge voltage and pressure. func (d *daemon) pirani() (volts float32, mbar float32) { d.mu.RLock() volts, mbar = d.piraniUnlocked() d.mu.RUnlock() return } func (d *daemon) piraniUnlocked() (volts float32, mbar float32) { volts = 0.0 for _, v := range d.adcPiraniVolts { volts += v } if len(d.adcPiraniVolts) != 0 { volts /= float32(len(d.adcPiraniVolts)) } // Per Pirani probe docs. bar := math.Pow(10.0, float64(volts)-8.5) mbar = float32(bar * 1000.0) return } // rpSet enables/disables the roughing pump. func (d *daemon) rpSet(state bool) { d.mu.Lock() defer d.mu.Unlock() d.rpOn = state } // rpGet returns whether the roughing pump is enabled/disabled. func (d *daemon) rpGet() bool { d.mu.RLock() defer d.mu.RUnlock() return d.rpOn } // dpSet enables/disables the diffusion pump. func (d *daemon) dpSet(state bool) { d.mu.Lock() defer d.mu.Unlock() d.dpOn = state } // dpGet returns whether the diffusion pump is enabled/disabled. func (d *daemon) dpGet() bool { d.mu.RLock() defer d.mu.RUnlock() return d.dpOn } func (d *daemon) vacuumStatusGet() (rough, high bool) { d.mu.RLock() defer d.mu.RUnlock() rough = !d.aboveRough.output high = !d.aboveHigh.output return } // pumpDownPressed toggles the pump down relay for 500ms. func (d *daemon) pumpDownPress() { d.mu.Lock() defer d.mu.Unlock() d.pumpdown.trigger() } // ventPress toggles the vent relay for 500ms. func (d *daemon) ventPress() { d.mu.Lock() defer d.mu.Unlock() d.vent.trigger() }