From 8f7ec7e1419b41ba975432b73c931e9d7426f884 Mon Sep 17 00:00:00 2001 From: Serge Bazanski Date: Sat, 28 Sep 2024 08:10:33 +0200 Subject: [PATCH] succd: split out http server, daemon state, daemon controller This improves the structure of the code, separating the data/control interface out and then implementing the http interface as a user of this interface. --- succbone/succd/http.go | 75 ++++++++------ succbone/succd/main.go | 20 ++-- succbone/succd/process.go | 142 +-------------------------- succbone/succd/process_controller.go | 49 +++++++++ succbone/succd/process_state.go | 84 ++++++++++++++++ 5 files changed, 190 insertions(+), 180 deletions(-) create mode 100644 succbone/succd/process_controller.go create mode 100644 succbone/succd/process_state.go diff --git a/succbone/succd/http.go b/succbone/succd/http.go index d32d274..3d38823 100644 --- a/succbone/succd/http.go +++ b/succbone/succd/http.go @@ -15,6 +15,10 @@ import ( "k8s.io/klog" ) +type httpServer struct { + d daemonController +} + var ( //go:embed index.html templateIndexText string @@ -23,7 +27,7 @@ var ( favicon []byte ) -func (d *daemon) httpFavicon(w http.ResponseWriter, r *http.Request) { +func (s *httpServer) viewFavicon(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "image/png") w.Write(favicon) } @@ -96,12 +100,10 @@ type apiData struct { // apiData returns the user data model for the current state of the system. If // skipSystem is set, the System subset is ignored (saves system load, and is // not being served via websockets). -func (d *daemon) apiData(skipSystem bool) *apiData { - volts, mbar := d.pirani() - rp := d.rpGet() - dp := d.dpGet() - rough, high := d.vacuumStatusGet() - safety := d.safetyStatusGet() +func (s *httpServer) apiData(skipSystem bool) *apiData { + state := s.d.snapshot() + volts, mbar := state.pirani() + rough, high := state.vacuumStatus() var hostname, load string var err error @@ -120,13 +122,13 @@ func (d *daemon) apiData(skipSystem bool) *apiData { } ad := apiData{} - ad.Safety.Failsafe = safety.failsafe - ad.Safety.HighPressure = safety.highPressure + ad.Safety.Failsafe = state.safety.failsafe + ad.Safety.HighPressure = state.safety.highPressure ad.Pirani.Volts = formatVolts(volts) ad.Pirani.Mbar = formatMbar(mbar) ad.Pirani.MbarFloat = mbar - ad.Pumps.RPOn = rp - ad.Pumps.DPOn = dp + ad.Pumps.RPOn = state.rpOn + ad.Pumps.DPOn = state.dpOn ad.Feedback.RoughReached = rough ad.Feedback.HighReached = high ad.System.Load = load @@ -135,20 +137,20 @@ func (d *daemon) apiData(skipSystem bool) *apiData { } // httpIndex is the / view. -func (d *daemon) httpIndex(w http.ResponseWriter, r *http.Request) { +func (s *httpServer) viewIndex(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/" { http.NotFound(w, r) return } - data := d.apiData(false) + data := s.apiData(false) //data.Pirani.Mbar = html.St templateIndex.Execute(w, data) } // httpStream is the websocket clientwards data hose, returning a 10Hz update // stream of pressure/voltage. -func (d *daemon) httpStream(w http.ResponseWriter, r *http.Request) { +func (s *httpServer) viewStream(w http.ResponseWriter, r *http.Request) { c, err := websocket.Accept(w, r, nil) if err != nil { return @@ -165,7 +167,7 @@ func (d *daemon) httpStream(w http.ResponseWriter, r *http.Request) { c.Close(websocket.StatusNormalClosure, "") return case <-t.C: - v := d.apiData(true) + v := s.apiData(true) if err := wsjson.Write(ctx, c, v); err != nil { klog.Errorf("Websocket write failed: %v", err) return @@ -175,39 +177,54 @@ func (d *daemon) httpStream(w http.ResponseWriter, r *http.Request) { } // httpMetrics serves minimalistic Prometheus-compatible metrics. -func (d *daemon) httpMetrics(w http.ResponseWriter, r *http.Request) { +func (s *httpServer) viewMetrics(w http.ResponseWriter, r *http.Request) { // TODO(q3k): also serve Go stuff using the actual Prometheus metrics client // library. - _, mbar := d.pirani() + // TODO(q3k): serve the rest of the data model + state := s.d.snapshot() + _, mbar := state.pirani() fmt.Fprintf(w, "# HELP sem_pressure_mbar Pressure in the SEM chamber, in millibar\n") fmt.Fprintf(w, "# TYPE sem_pressure_mbar gauge\n") fmt.Fprintf(w, "sem_pressure_mbar %f\n", mbar) } -func (d *daemon) httpRoughingPumpEnable(w http.ResponseWriter, r *http.Request) { - d.rpSet(true) +func (s *httpServer) viewRoughingPumpEnable(w http.ResponseWriter, r *http.Request) { + s.d.rpSet(true) fmt.Fprintf(w, "succ on\n") } -func (d *daemon) httpRoughingPumpDisable(w http.ResponseWriter, r *http.Request) { - d.rpSet(false) +func (s *httpServer) viewRoughingPumpDisable(w http.ResponseWriter, r *http.Request) { + s.d.rpSet(false) fmt.Fprintf(w, "succ off\n") } -func (d *daemon) httpDiffusionPumpEnable(w http.ResponseWriter, r *http.Request) { - d.dpSet(true) +func (s *httpServer) viewDiffusionPumpEnable(w http.ResponseWriter, r *http.Request) { + s.d.dpSet(true) fmt.Fprintf(w, "deep succ on\n") } -func (d *daemon) httpDiffusionPumpDisable(w http.ResponseWriter, r *http.Request) { - d.dpSet(false) +func (s *httpServer) viewDiffusionPumpDisable(w http.ResponseWriter, r *http.Request) { + s.d.dpSet(false) fmt.Fprintf(w, "deep succ off\n") } -func (d *daemon) httpButtonPumpDown(w http.ResponseWriter, r *http.Request) { - d.pumpDownPress() +func (s *httpServer) viewButtonPumpDown(w http.ResponseWriter, r *http.Request) { + s.d.pumpDownPress() } -func (d *daemon) httpButtonVent(w http.ResponseWriter, r *http.Request) { - d.ventPress() +func (s *httpServer) viewButtonVent(w http.ResponseWriter, r *http.Request) { + s.d.ventPress() +} + +func (s *httpServer) setupViews() { + http.HandleFunc("/", s.viewIndex) + http.HandleFunc("/favicon.png", s.viewFavicon) + http.HandleFunc("/stream", s.viewStream) + http.HandleFunc("/metrics", s.viewMetrics) + http.HandleFunc("/button/vent", s.viewButtonVent) + http.HandleFunc("/button/pumpdown", s.viewButtonPumpDown) + http.HandleFunc("/rp/on", s.viewRoughingPumpEnable) + http.HandleFunc("/rp/off", s.viewRoughingPumpDisable) + http.HandleFunc("/dp/on", s.viewDiffusionPumpEnable) + http.HandleFunc("/dp/off", s.viewDiffusionPumpDisable) } diff --git a/succbone/succd/main.go b/succbone/succd/main.go index 32e5c17..c82d936 100644 --- a/succbone/succd/main.go +++ b/succbone/succd/main.go @@ -26,9 +26,9 @@ func main() { ctx, _ := signal.NotifyContext(context.Background(), os.Interrupt) - d := daemon{ - rpOn: true, - } + d := daemon{} + d.daemonState.rpOn = true + d.aboveRough.threshold = float64(flagPressureThresholdRough) d.aboveHigh.threshold = float64(flagPressureThresholdHigh) if flagFake { @@ -65,16 +65,10 @@ 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) - 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) + httpServer := httpServer{ + d: &d, + } + httpServer.setupViews() klog.Infof("Listening for HTTP at %s", flagListenHTTP) go func() { diff --git a/succbone/succd/process.go b/succbone/succd/process.go index 31bc1fa..bd3f85e 100644 --- a/succbone/succd/process.go +++ b/succbone/succd/process.go @@ -4,21 +4,18 @@ import ( "context" "errors" "fmt" - "math" "sync" "time" "k8s.io/klog" ) -// daemon is the main state of the succdaemon. +// daemon is the main service of the succdaemon. type daemon struct { // adcPirani is the adc implementation returning the voltage of the Pfeiffer // Pirani gauge. adcPirani adc - safety safetyStatus - gpioDiffusionPump gpio gpioRoughingPump gpio gpioBtnPumpDown gpio @@ -26,27 +23,9 @@ type daemon struct { gpioBelowRough gpio gpioBelowHigh gpio - // mu guards state variables below. + // mu guards the state 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 -} - -type safetyStatus 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 + daemonState } // momentaryOutput is an output that can be triggered for 500ms. @@ -127,7 +106,7 @@ func (d *daemon) processOnce(_ context.Context) error { d.pumpdown.process() d.vent.process() - _, mbar := d.piraniUnlocked() + _, mbar := d.daemonState.pirani() d.aboveRough.process(float64(mbar)) d.aboveHigh.process(float64(mbar)) @@ -201,116 +180,3 @@ func (d *daemon) processOnce(_ context.Context) error { return nil } - -type piraniDetection uint - -const ( - // piraniDetectionUnknown means the system isn't yet sure whether the pirani - // gauge is connected. - piraniDetectionUnknown piraniDetection = iota - // piraniDetectionConnected means the system assumes the pirani gauge is - // connected. - piraniDetectionConnected = iota - // piraniDetectionDisconnected means the system assumes the pirani gauge is - // disconnected. - piraniDetectionDisconnected = iota -) - -// piraniDetection guesses whether the pirani gauge is connected. -func (d *daemon) piraniDetection() piraniDetection { - if len(d.adcPiraniVolts) < 3 { - return piraniDetectionUnknown - } - volts := float32(0.0) - for _, v := range d.adcPiraniVolts[len(d.adcPiraniVolts)-3:] { - volts += v - } - volts /= 3.0 - - bar := math.Pow(10.0, float64(volts)-8.5) - mbar := float32(bar * 1000.0) - - if mbar < 4e-6 { - return piraniDetectionDisconnected - } - return piraniDetectionConnected -} - -// 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 -} - -func (d *daemon) safetyStatusGet() safetyStatus { - d.mu.RLock() - defer d.mu.RUnlock() - return d.safety -} - -// 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/process_controller.go b/succbone/succd/process_controller.go new file mode 100644 index 0000000..2a228f5 --- /dev/null +++ b/succbone/succd/process_controller.go @@ -0,0 +1,49 @@ +package main + +// daemonController is the control/data interface passed on to external system +// controllers, eg. the web interface. +// +// This is a subset of daemon functions limited to a safe, explicit subset. +type daemonController interface { + // snapshot returns an internally consistent copy of the daemon state. + snapshot() *daemonState + // rpSet enables/disables the roughing pump. + rpSet(bool) + // dpSet enables/disables the diffusion pump. + dpSet(bool) + // pumpDownPress simulates a pumpdown button press. + pumpDownPress() + // ventPRess simulates a vent button press. + ventPress() +} + +func (d *daemon) snapshot() *daemonState { + d.mu.RLock() + ds := d.daemonState + d.mu.RUnlock() + return &ds +} + +func (d *daemon) rpSet(state bool) { + d.mu.Lock() + defer d.mu.Unlock() + d.rpOn = state +} + +func (d *daemon) dpSet(state bool) { + d.mu.Lock() + defer d.mu.Unlock() + d.dpOn = state +} + +func (d *daemon) pumpDownPress() { + d.mu.Lock() + defer d.mu.Unlock() + d.pumpdown.trigger() +} + +func (d *daemon) ventPress() { + d.mu.Lock() + defer d.mu.Unlock() + d.vent.trigger() +} diff --git a/succbone/succd/process_state.go b/succbone/succd/process_state.go new file mode 100644 index 0000000..0bbb09e --- /dev/null +++ b/succbone/succd/process_state.go @@ -0,0 +1,84 @@ +package main + +import "math" + +// 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 safetyStatus + + // 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 +} + +type safetyStatus 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 +} + +type piraniDetection uint + +const ( + // piraniDetectionUnknown means the system isn't yet sure whether the pirani + // gauge is connected. + piraniDetectionUnknown piraniDetection = iota + // piraniDetectionConnected means the system assumes the pirani gauge is + // connected. + piraniDetectionConnected = iota + // piraniDetectionDisconnected means the system assumes the pirani gauge is + // disconnected. + piraniDetectionDisconnected = iota +) + +// piraniDetection guesses whether the pirani gauge is connected. +func (d *daemonState) piraniDetection() piraniDetection { + if len(d.adcPiraniVolts) < 3 { + return piraniDetectionUnknown + } + volts := float32(0.0) + for _, v := range d.adcPiraniVolts[len(d.adcPiraniVolts)-3:] { + volts += v + } + volts /= 3.0 + + bar := math.Pow(10.0, float64(volts)-8.5) + mbar := float32(bar * 1000.0) + + if mbar < 4e-6 { + return piraniDetectionDisconnected + } + return piraniDetectionConnected +} + +func (d *daemonState) pirani() (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 +} + +func (d *daemonState) vacuumStatus() (rough, high bool) { + rough = !d.aboveRough.output + high = !d.aboveHigh.output + return +}