diff --git a/succbone/succd/README.md b/succbone/succd/README.md index ea4f231..4d1b077 100644 --- a/succbone/succd/README.md +++ b/succbone/succd/README.md @@ -9,20 +9,6 @@ Features: 2. Allows enabling/disabling the diffusion/roughing pumps (builtin controller always keeps them enabled). 3. Allows for simulating vent/pumpdown button presses. -Pins ---- - -| P9 pin number | Relay number | Relay board / channel number | GPIO number | Function | -|---------------|----------------|------------------------------|-------------|-------------------| -| 27 | -KFA1 | 1 / 4 | 115 | Roughing Pump | -| 23 | -KFA2 | 1 / 3 | 49 | Diffusion Pump | -| n/c | -KFA3 (broken) | 1 / 2 | n/c | | -| 12 | -KFA4 | 1 / 1 | 60 | Vent Button | -| 15 | -KFA5 | 2 / 4 | 48 | Pump Down Button | -| 11 | -KFA6 | 2 / 3 | 30 | Rough Vacuum Out | -| 42 | -KFA7 | 2 / 2 | 7 | High Vacuum Out | -| n/c | -KFA8 | 2 / 1 | n/c | | - Accessing at the lab --- diff --git a/succbone/succd/http.go b/succbone/succd/http.go index 0be9c76..5822425 100644 --- a/succbone/succd/http.go +++ b/succbone/succd/http.go @@ -19,15 +19,8 @@ var ( //go:embed index.html templateIndexText string templateIndex = template.Must(template.New("index").Parse(templateIndexText)) - //go:embed succd.png - favicon []byte ) -func (d *daemon) httpFavicon(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "image/png") - w.Write(favicon) -} - func formatVolts(v float32) string { return fmt.Sprintf("%.4f V", v) } @@ -106,23 +99,18 @@ 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 - RoughReached bool - HighReached bool + Volts string + Mbar string + MbarFloat float32 + RPOn bool + DPOn bool }{ - Volts: formatVolts(volts), - Mbar: string(formatMbar(mbar)), - MbarFloat: mbar, - RPOn: rp, - DPOn: dp, - RoughReached: rough, - HighReached: high, + Volts: formatVolts(volts), + Mbar: string(formatMbar(mbar)), + MbarFloat: mbar, + RPOn: rp, + DPOn: dp, } 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 c764d76..14fb9dd 100644 --- a/succbone/succd/index.html +++ b/succbone/succd/index.html @@ -2,7 +2,6 @@ succd - - -

succd

nothing more permanent than a temporary solution

-

+

- + - + - - - - - - - - - - - - + + + + + + - - +
Voltage{{.volts}}{{.volts}}
Pressure{{.mbar}}{{.mbar}}
ThresholdsRough:...High:...
PumpsRP:{{ if .rp }}ON{{ else }}OFF{{ end }}DP:{{ if .dp }}ON{{ else }}OFF{{ end }}Roughing Pump{{ if .rp }}ON{{ else }}OFF{{ end }}
Diffusion Pump{{ if .dp }}ON{{ else }}OFF{{ end }}
Evac Control + @@ -92,7 +70,7 @@ td > span {
StatusOKOK

@@ -243,8 +221,6 @@ 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"); @@ -292,22 +268,6 @@ 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 32e5c17..913f29e 100644 --- a/succbone/succd/main.go +++ b/succbone/succd/main.go @@ -2,26 +2,166 @@ 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 - flagPressureThresholdRough = ScientificNotationValue(1e-1) - flagPressureThresholdHigh = ScientificNotationValue(1e-4) + 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.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) @@ -29,8 +169,6 @@ 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{} @@ -38,8 +176,6 @@ 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 { @@ -55,9 +191,8 @@ 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) @@ -66,7 +201,6 @@ func main() { } http.HandleFunc("/", d.httpIndex) - http.HandleFunc("/favicon.png", d.httpFavicon) http.HandleFunc("/stream", d.httpStream) http.HandleFunc("/metrics", d.httpMetrics) http.HandleFunc("/button/vent", d.httpButtonVent) diff --git a/succbone/succd/process.go b/succbone/succd/process.go deleted file mode 100644 index b38cf25..0000000 --- a/succbone/succd/process.go +++ /dev/null @@ -1,225 +0,0 @@ -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 deleted file mode 100644 index d2df8d8..0000000 --- a/succbone/succd/scientific.go +++ /dev/null @@ -1,33 +0,0 @@ -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 -} diff --git a/succbone/succd/succd.png b/succbone/succd/succd.png deleted file mode 100644 index b355107..0000000 --- a/succbone/succd/succd.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:9fbdd257c1025d341d6b96e647188e89b0dc4ef34b6964e860d0089100ee137d -size 16310 diff --git a/succbone/succd/succd.svg b/succbone/succd/succd.svg deleted file mode 100644 index c7946d7..0000000 --- a/succbone/succd/succd.svg +++ /dev/null @@ -1,86 +0,0 @@ - - - -succbone