diff --git a/succbone/succd/http.go b/succbone/succd/http.go index 3e24873..0be9c76 100644 --- a/succbone/succd/http.go +++ b/succbone/succd/http.go @@ -106,18 +106,23 @@ func (d *daemon) httpStream(w http.ResponseWriter, r *http.Request) { volts, mbar := d.pirani() rp := d.rpGet() dp := d.dpGet() + rough, high := d.vacuumStatusGet() v := struct { - Volts string - Mbar string - MbarFloat float32 - RPOn bool - DPOn bool + Volts string + Mbar string + MbarFloat float32 + RPOn bool + DPOn bool + RoughReached bool + HighReached bool }{ - Volts: formatVolts(volts), - Mbar: string(formatMbar(mbar)), - MbarFloat: mbar, - RPOn: rp, - DPOn: dp, + Volts: formatVolts(volts), + Mbar: string(formatMbar(mbar)), + MbarFloat: mbar, + RPOn: rp, + DPOn: dp, + RoughReached: rough, + HighReached: high, } if err := wsjson.Write(ctx, c, v); err != nil { klog.Errorf("Websocket write failed: %v", err) diff --git a/succbone/succd/index.html b/succbone/succd/index.html index 363ce94..c764d76 100644 --- a/succbone/succd/index.html +++ b/succbone/succd/index.html @@ -36,6 +36,10 @@ button { padding-right: 1.5em; } +td > span { + padding: 0.2em; +} + .logo { float: left; margin-right: 2em; @@ -55,23 +59,29 @@ button { - + - + + + + + + + + - - - - - - + + + + + - - +
Voltage{{.volts}}{{.volts}}
Pressure{{.mbar}}{{.mbar}}
ThresholdsRough:...High:...
Roughing Pump{{ if .rp }}ON{{ else }}OFF{{ end }}
Diffusion Pump{{ if .dp }}ON{{ else }}OFF{{ end }}PumpsRP:{{ if .rp }}ON{{ else }}OFF{{ end }}DP:{{ if .dp }}ON{{ else }}OFF{{ end }}
Evac Control + @@ -82,7 +92,7 @@ button {
StatusOKOK

@@ -233,6 +243,8 @@ window.addEventListener("load", (_) => { let volts = document.querySelector("#volts"); let mbar = document.querySelector("#mbar"); let ping = document.querySelector("#ping"); + let trough = document.querySelector("#trough"); + let thigh = document.querySelector("#thigh"); // Buttons let pd = document.querySelector("#pd"); @@ -280,6 +292,22 @@ window.addEventListener("load", (_) => { } else { dp.style = "background-color: #f06060"; } + + let t = []; + if (data.RoughReached) { + trough.innerHTML = "OK"; + trough.style = "background-color: #60f060"; + } else { + trough.innerHTML = "NOK"; + trough.style = "background-color: #f06060"; + } + if (data.HighReached) { + thigh.innerHTML = "OK"; + thigh.style = "background-color: #60f060"; + } else { + thigh.innerHTML = "NOK"; + thigh.style = "background-color: #f06060"; + } historicalPush(data.MbarFloat); ping.innerHTML = Date.now(); }); diff --git a/succbone/succd/main.go b/succbone/succd/main.go index 85581c1..32e5c17 100644 --- a/succbone/succd/main.go +++ b/succbone/succd/main.go @@ -2,166 +2,26 @@ 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 + flagFake bool + flagListenHTTP string + flagPressureThresholdRough = ScientificNotationValue(1e-1) + flagPressureThresholdHigh = ScientificNotationValue(1e-4) ) 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.TextVar(&flagPressureThresholdRough, "pressure_threshold_rough", &flagPressureThresholdRough, "Threshold for opening up diffusion pump (mbar)") + flag.TextVar(&flagPressureThresholdHigh, "pressure_threshold_high", &flagPressureThresholdHigh, "Threshold for enabling high voltage circuits (mbar)") flag.Parse() ctx, _ := signal.NotifyContext(context.Background(), os.Interrupt) @@ -169,6 +29,8 @@ func main() { d := daemon{ rpOn: true, } + d.aboveRough.threshold = float64(flagPressureThresholdRough) + d.aboveHigh.threshold = float64(flagPressureThresholdHigh) if flagFake { klog.Infof("Starting with fake peripherals") d.adcPirani = &fakeADC{} @@ -176,6 +38,8 @@ func main() { d.gpioDiffusionPump = &fakeGPIO{desc: "~dp"} d.gpioBtnPumpDown = &fakeGPIO{desc: "~pd"} d.gpioBtnVent = &fakeGPIO{desc: "~vent"} + d.gpioBelowRough = &fakeGPIO{desc: "~rough"} + d.gpioBelowHigh = &fakeGPIO{desc: "~high"} } else { adc, err := newBBADC(0) if err != nil { @@ -191,8 +55,9 @@ func main() { {&d.gpioDiffusionPump, 49}, {&d.gpioBtnPumpDown, 48}, {&d.gpioBtnVent, 60}, + {&d.gpioBelowRough, 30}, + {&d.gpioBelowHigh, 7}, } { - // Relay, active low. *c.out, err = newBBGPIO(c.num, true) if err != nil { klog.Exitf("Failed to setup GPIO: %v", err) diff --git a/succbone/succd/process.go b/succbone/succd/process.go new file mode 100644 index 0000000..b38cf25 --- /dev/null +++ b/succbone/succd/process.go @@ -0,0 +1,225 @@ +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 + + 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)) + + // 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 +} + +// 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() +} diff --git a/succbone/succd/scientific.go b/succbone/succd/scientific.go new file mode 100644 index 0000000..d2df8d8 --- /dev/null +++ b/succbone/succd/scientific.go @@ -0,0 +1,33 @@ +package main + +import ( + "fmt" + "strconv" +) + +type ScientificNotationValue float64 + +func (s *ScientificNotationValue) UnmarshalText(text []byte) error { + f, err := strconv.ParseFloat(string(text), 64) + if err != nil { + return err + } + *s = ScientificNotationValue(f) + return nil +} + +func (s *ScientificNotationValue) MarshalText() ([]byte, error) { + v := float64(*s) + exp := 0 + for v < 1 { + v *= 10 + exp -= 1 + } + for v >= 10 { + v /= 10 + exp += 1 + } + res := fmt.Sprintf("%.3f", v) + res += fmt.Sprintf("e%d", exp) + return []byte(res), nil +}