package main import ( "context" "errors" "fmt" "sync" "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 load atomic.Int64 // 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 piraniMbar100 pfeifferVoltsToMbar piraniVolts3 ringbufferInput piraniMbar3 pfeifferVoltsToMbar rpOn bool dpOn bool vent momentaryOutput pumpdown momentaryOutput aboveRough thresholdOutput aboveHigh thresholdOutput tempDPBottom float32 tempDPTop float32 tempDPInlet float32 tempSEM float32 humiditySEM float32 } 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) { var lastRun time.Time hz := 100 period := time.Second / time.Duration(hz) // Extra grace period for GC pauses and other non-realtime system jitter. periodGrace := period + time.Millisecond*5 ticker := time.NewTicker(period) defer ticker.Stop() for { select { case <-ticker.C: now := time.Now() if elapsed := now.Sub(lastRun); !lastRun.IsZero() && elapsed > periodGrace { klog.Warningf("Processing loop lag: took %s, want %s", elapsed, period) } lastRun = now 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) } } runtime := time.Since(lastRun) load := int64(100 * runtime / period) d.load.Store(load) 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.piraniMbar3.process(d.piraniVolts3.avg) d.piraniVolts100.process(v) d.piraniMbar100.process(d.piraniVolts100.avg) d.pumpdown.process() d.vent.process() // Run safety checks based on small ringbuffer. if d.piraniVolts3.saturated() { mbar := d.piraniMbar3.mbar if !d.safety.failsafe && mbar < 4e-6 { // Unrealistic result, Pirani probe probably disconnected. Failsafe mode. d.safety.failsafe = true klog.Errorf("SAFETY: Pirani probe seems disconnected; enabling failsafe mode") } if d.safety.failsafe && mbar > 1e2 { d.safety.failsafe = false klog.Infof("SAFETY: Pirani probe value (%s) is plausible again; quitting failsafe mode", formatMbar(mbar)) } if !d.safety.highPressure && mbar >= 1e-1 { d.safety.highPressure = true klog.Warningf("SAFETY: Pressure is too high (%s mbar); enabling diffusion pump lockout", formatMbar(mbar)) } if d.safety.highPressure && mbar < (1e-1)-(1e-2) { d.safety.highPressure = false klog.Infof("SAFETY: Pressure is low enough (%s mbar) for diffusion pump operation; quitting diffusion pump lockout", formatMbar(mbar)) } } else { d.safety.highPressure = true } // Control threhold/feedback values based on main pirani ringbuffer, failing // safe if not enough data is present. if d.piraniVolts100.saturated() { mbar := d.piraniMbar100.mbar d.aboveRough.process(float64(mbar)) d.aboveHigh.process(float64(mbar)) } else { d.aboveRough.output = true d.aboveHigh.output = true } // Apply safety overrides. if d.safety.failsafe { d.aboveRough.output = true d.aboveHigh.output = true d.dpOn = false } if d.safety.highPressure { d.dpOn = false } return nil }