290 lines
		
	
	
	
		
			6.6 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			290 lines
		
	
	
	
		
			6.6 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| 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))
 | |
| 
 | |
|   klog.Infof("rate of change: %f", d.piraniRateOfChange())
 | |
| 
 | |
|   // 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)
 | |
| 
 | |
|   return 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()
 | |
| }
 | 
