package main import ( "context" "errors" "flag" "fmt" "math" "net/http" "os" "os/signal" "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 gpioDiffusionPump gpio gpioRoughingPump gpio gpioBtnPumpDown gpio gpioBtnVent 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 // ventScheduled and pumpdownScheduled are timers which expire when the // vent/pumpdown relays should be deactivated. This allows these outputs to // be controlled momentarily. ventScheduled time.Time pumpdownScheduled time.Time } // 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() d.adcPiraniVolts = append(d.adcPiraniVolts, v) trim := len(d.adcPiraniVolts) - 100 if trim > 0 { d.adcPiraniVolts = d.adcPiraniVolts[trim:] } if err := d.gpioRoughingPump.set(d.rpOn); err != nil { return fmt.Errorf("when configuring RP: %w", err) } if err := d.gpioDiffusionPump.set(!d.dpOn); err != nil { return fmt.Errorf("when configuring RP: %w", err) } if err := d.gpioBtnPumpDown.set(!d.pumpdownScheduled.After(time.Now())); err != nil { return fmt.Errorf("when configuring pumpdown: %w", err) } if err := d.gpioBtnVent.set(!d.ventScheduled.After(time.Now())); err != nil { return fmt.Errorf("when configuring vent: %w", err) } return nil } // pirani returns the Pirani gauge voltage and pressure. func (d *daemon) pirani() (volts float32, mbar float32) { d.mu.RLock() volts = 0.0 for _, v := range d.adcPiraniVolts { volts += v } if len(d.adcPiraniVolts) != 0 { volts /= float32(len(d.adcPiraniVolts)) } d.mu.RUnlock() // 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 } // pumpDownPressed toggles the pump down relay for 500ms. func (d *daemon) pumpDownPress() { d.mu.Lock() defer d.mu.Unlock() if d.pumpdownScheduled.Before(time.Now()) { d.pumpdownScheduled = time.Now().Add(500 * time.Millisecond) } } // ventPress toggles the vent relay for 500ms. func (d *daemon) ventPress() { d.mu.Lock() defer d.mu.Unlock() if d.ventScheduled.Before(time.Now()) { d.ventScheduled = time.Now().Add(500 * time.Millisecond) } } var ( flagFake bool flagListenHTTP string ) func main() { flag.BoolVar(&flagFake, "fake", false, "Enable fake mode which allows to run succd for tests outside the succbone") flag.StringVar(&flagListenHTTP, "listen_http", ":8080", "Address at which to listen for HTTP requests") flag.Parse() ctx, _ := signal.NotifyContext(context.Background(), os.Interrupt) d := daemon{ rpOn: true, } if flagFake { klog.Infof("Starting with fake peripherals") d.adcPirani = &fakeADC{} d.gpioRoughingPump = &fakeGPIO{desc: "rp"} d.gpioDiffusionPump = &fakeGPIO{desc: "~dp"} d.gpioBtnPumpDown = &fakeGPIO{desc: "~pd"} d.gpioBtnVent = &fakeGPIO{desc: "~vent"} } else { adc, err := newBBADC(0) if err != nil { klog.Exitf("Failed to setup Pirani ADC: %v", err) } d.adcPirani = adc for _, c := range []struct { out *gpio num int }{ {&d.gpioRoughingPump, 115}, {&d.gpioDiffusionPump, 49}, {&d.gpioBtnPumpDown, 48}, {&d.gpioBtnVent, 60}, } { // Relay, active low. *c.out, err = newBBGPIO(c.num, true) if err != nil { klog.Exitf("Failed to setup GPIO: %v", err) } } } http.HandleFunc("/", d.httpIndex) http.HandleFunc("/stream", d.httpStream) http.HandleFunc("/metrics", d.httpMetrics) http.HandleFunc("/button/vent", d.httpButtonVent) http.HandleFunc("/button/pumpdown", d.httpButtonPumpDown) http.HandleFunc("/rp/on", d.httpRoughingPumpEnable) http.HandleFunc("/rp/off", d.httpRoughingPumpDisable) http.HandleFunc("/dp/on", d.httpDiffusionPumpEnable) http.HandleFunc("/dp/off", d.httpDiffusionPumpDisable) klog.Infof("Listening for HTTP at %s", flagListenHTTP) go func() { if err := http.ListenAndServe(flagListenHTTP, nil); err != nil { klog.Errorf("HTTP listen failed: %v", err) } }() go d.process(ctx) <-ctx.Done() }