package main import ( "context" "errors" "fmt" "math" "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 } // daemonState contains all the state of the daemon. A copy of it can be // requested for consumers, eg. the web view. type daemonState struct { safety struct { // failsafe mode is enabled when the pirani gauge appears to be // disconnected, and is disabled only when an atmosphere is read. failsafe bool // highPressure mode is enabled when the pressure reading is above 1e-1 // mbar, locking out the diffusion pump from being enabled. highPressure bool } piraniVolts100 ringbufferInput piraniVolts3 ringbufferInput rpOn bool dpOn bool vent momentaryOutput pumpdown momentaryOutput aboveRough thresholdOutput aboveHigh thresholdOutput } type piraniDetection uint const ( // piraniDetectionUnknown means the system isn't yet sure whether the pirani // gauge is connected. piraniDetectionUnknown piraniDetection = iota // piraniDetectionConnected means the system assumes the pirani gauge is // connected. piraniDetectionConnected = iota // piraniDetectionDisconnected means the system assumes the pirani gauge is // disconnected. piraniDetectionDisconnected = iota ) func piraniVoltsToMbar(v float32) float32 { // Per Pirani probe docs. bar := math.Pow(10.0, float64(v)-8.5) return float32(bar * 1000.0) } // piraniDetection guesses whether the pirani gauge is connected. func (d *daemonState) piraniDetection() piraniDetection { if !d.piraniVolts3.saturated() { return piraniDetectionUnknown } mbar := piraniVoltsToMbar(d.piraniVolts3.avg) if mbar < 4e-6 { return piraniDetectionDisconnected } return piraniDetectionConnected } func (d *daemonState) pirani() (volts float32, mbar float32) { volts = d.piraniVolts100.avg mbar = piraniVoltsToMbar(volts) return } func (d *daemonState) vacuumStatus() (rough, high bool) { rough = !d.aboveRough.output high = !d.aboveHigh.output return } // 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 { // 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.piraniDetection() == piraniDetectionConnected { if d.safety.failsafe { if mbar >= 1e2 { d.safety.failsafe = false klog.Infof("Pirani probe value (%s) is plausible again; quitting failsafe mode", formatMbar(mbar)) } } } if mbar >= 1e-1 { if !d.safety.highPressure { d.safety.highPressure = true klog.Warningf("Pressure is too high (%s mbar); enabling diffusion pump lockout", formatMbar(mbar)) } } else if mbar < (1e-1)-(1e-2) { if d.safety.highPressure { d.safety.highPressure = false klog.Infof("Pressure is low enough (%s mbar) for diffusion pump operation; quitting diffusion pump lockout", formatMbar(mbar)) } } 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 }