From 80f482b732d0fea89887ee49e2625d350aa2bf92 Mon Sep 17 00:00:00 2001 From: Serge Bazanski Date: Sat, 28 Sep 2024 07:31:06 +0200 Subject: [PATCH 1/6] succd: tristate pirani safety detection --- succbone/succd/process.go | 39 ++++++++++++++++++++++++++++++--------- 1 file changed, 30 insertions(+), 9 deletions(-) diff --git a/succbone/succd/process.go b/succbone/succd/process.go index 5fb8a7f..7d408ba 100644 --- a/succbone/succd/process.go +++ b/succbone/succd/process.go @@ -123,7 +123,10 @@ func (d *daemon) processOnce(_ context.Context) error { d.aboveRough.process(float64(mbar)) d.aboveHigh.process(float64(mbar)) - if d.piraniWireBreakDetection() { + // Check if the pirani gauge is disconnected. Note: this will assume the + // pirani gauge is connected for the first couple of processing runs as + // samples are still being captured. + if d.piraniDetection() == piraniDetectionDisconnected { // Unrealistic result, Pirani probe probably disconnected. Failsafe mode. if !d.failsafe { d.failsafe = true @@ -191,9 +194,24 @@ func (d *daemon) processOnce(_ context.Context) error { return nil } -func (d *daemon) piraniWireBreakDetection() 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 *daemon) piraniDetection() piraniDetection { if len(d.adcPiraniVolts) < 3 { - return true + return piraniDetectionUnknown } volts := float32(0.0) for _, v := range d.adcPiraniVolts[len(d.adcPiraniVolts)-3:] { @@ -204,7 +222,10 @@ func (d *daemon) piraniWireBreakDetection() bool { bar := math.Pow(10.0, float64(volts)-8.5) mbar := float32(bar * 1000.0) - return mbar < 4e-6 + if mbar < 4e-6 { + return piraniDetectionDisconnected + } + return piraniDetectionConnected } // pirani returns the Pirani gauge voltage and pressure. @@ -267,11 +288,11 @@ func (d *daemon) vacuumStatusGet() (rough, high bool) { } func (d *daemon) safetyStatusGet() (failsafe, highPressure bool) { - d.mu.RLock() - defer d.mu.RUnlock() - failsafe = d.failsafe - highPressure = d.highPressure - return + d.mu.RLock() + defer d.mu.RUnlock() + failsafe = d.failsafe + highPressure = d.highPressure + return } // pumpDownPressed toggles the pump down relay for 500ms. From 239a5c40cc1543f58dfb3436b5b67285392c4dde Mon Sep 17 00:00:00 2001 From: Serge Bazanski Date: Sat, 28 Sep 2024 07:35:45 +0200 Subject: [PATCH 2/6] succd: factor out safety status to separate struct --- succbone/succd/http.go | 16 ++++++++-------- succbone/succd/process.go | 38 ++++++++++++++++++++++---------------- 2 files changed, 30 insertions(+), 24 deletions(-) diff --git a/succbone/succd/http.go b/succbone/succd/http.go index 43bee92..fd39c87 100644 --- a/succbone/succd/http.go +++ b/succbone/succd/http.go @@ -60,7 +60,7 @@ func (d *daemon) httpIndex(w http.ResponseWriter, r *http.Request) { volts, mbar := d.pirani() rp := d.rpGet() dp := d.dpGet() - failsafe, highpressure := d.safetyStatusGet() + safety := d.safetyStatusGet() loadB, err := os.ReadFile("/proc/loadavg") load := "unknown" @@ -75,8 +75,8 @@ func (d *daemon) httpIndex(w http.ResponseWriter, r *http.Request) { } templateIndex.Execute(w, map[string]any{ - "failsafe": failsafe, - "highpressure": highpressure, + "failsafe": safety.failsafe, + "highpressure": safety.highPressure, "volts": formatVolts(volts), "mbar": formatMbar(mbar), "rp": rp, @@ -110,10 +110,10 @@ func (d *daemon) httpStream(w http.ResponseWriter, r *http.Request) { rp := d.rpGet() dp := d.dpGet() rough, high := d.vacuumStatusGet() - failsafe, highpressure := d.safetyStatusGet() + safety := d.safetyStatusGet() v := struct { - Failsafe bool - HighPressure bool + Failsafe bool + HighPressure bool Volts string Mbar string MbarFloat float32 @@ -122,8 +122,8 @@ func (d *daemon) httpStream(w http.ResponseWriter, r *http.Request) { RoughReached bool HighReached bool }{ - Failsafe: failsafe, - HighPressure: highpressure, + Failsafe: safety.failsafe, + HighPressure: safety.highPressure, Volts: formatVolts(volts), Mbar: string(formatMbar(mbar)), MbarFloat: mbar, diff --git a/succbone/succd/process.go b/succbone/succd/process.go index 7d408ba..313af1d 100644 --- a/succbone/succd/process.go +++ b/succbone/succd/process.go @@ -17,8 +17,7 @@ type daemon struct { // Pirani gauge. adcPirani adc - failsafe bool - highPressure bool + safety safetyStatus gpioDiffusionPump gpio gpioRoughingPump gpio @@ -41,6 +40,15 @@ type daemon struct { 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 +} + // momentaryOutput is an output that can be triggered for 500ms. type momentaryOutput struct { // output of the block. @@ -128,38 +136,38 @@ func (d *daemon) processOnce(_ context.Context) error { // samples are still being captured. if d.piraniDetection() == piraniDetectionDisconnected { // Unrealistic result, Pirani probe probably disconnected. Failsafe mode. - if !d.failsafe { - d.failsafe = true + if !d.safety.failsafe { + d.safety.failsafe = true klog.Errorf("Pirani probe seems disconnected; enabling failsafe mode") } } else { - if d.failsafe { + if d.safety.failsafe { if mbar >= 1e2 { - d.failsafe = false + d.safety.failsafe = false klog.Infof("Values are plausible again; quitting failsafe mode") } } } if mbar >= 1e-1 { - if !d.highPressure { - d.highPressure = true + if !d.safety.highPressure { + d.safety.highPressure = true klog.Errorf("Pressure is too high; enabling diffusion pump lockout") } } else { - if d.highPressure { - d.highPressure = false + if d.safety.highPressure { + d.safety.highPressure = false klog.Infof("Pressure is low enough for diffusion pump operation; quitting diffusion pump lockout") } } - if d.failsafe { + if d.safety.failsafe { d.aboveRough.output = true d.aboveHigh.output = true d.dpOn = false } - if d.highPressure { + if d.safety.highPressure { d.dpOn = false } @@ -287,12 +295,10 @@ func (d *daemon) vacuumStatusGet() (rough, high bool) { return } -func (d *daemon) safetyStatusGet() (failsafe, highPressure bool) { +func (d *daemon) safetyStatusGet() safetyStatus { d.mu.RLock() defer d.mu.RUnlock() - failsafe = d.failsafe - highPressure = d.highPressure - return + return d.safety } // pumpDownPressed toggles the pump down relay for 500ms. From 776f7a9911c11f392b4b0d8cbe6d044a7b98b19b Mon Sep 17 00:00:00 2001 From: Serge Bazanski Date: Sat, 28 Sep 2024 07:36:02 +0200 Subject: [PATCH 3/6] succd: add hysteresis for high pressure safety interlock --- succbone/succd/process.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/succbone/succd/process.go b/succbone/succd/process.go index 313af1d..31bc1fa 100644 --- a/succbone/succd/process.go +++ b/succbone/succd/process.go @@ -154,7 +154,7 @@ func (d *daemon) processOnce(_ context.Context) error { d.safety.highPressure = true klog.Errorf("Pressure is too high; enabling diffusion pump lockout") } - } else { + } else if mbar < (1e-1)-(1e-2) { if d.safety.highPressure { d.safety.highPressure = false klog.Infof("Pressure is low enough for diffusion pump operation; quitting diffusion pump lockout") From 3ec6fd1d1bed8a09769fe55f89692a997aa6c7cc Mon Sep 17 00:00:00 2001 From: Serge Bazanski Date: Sat, 28 Sep 2024 07:49:03 +0200 Subject: [PATCH 4/6] succd: unify html/js data source --- succbone/succd/http.go | 142 +++++++++++++++++++++++--------------- succbone/succd/index.html | 57 +++++++-------- 2 files changed, 118 insertions(+), 81 deletions(-) diff --git a/succbone/succd/http.go b/succbone/succd/http.go index fd39c87..d32d274 100644 --- a/succbone/succd/http.go +++ b/succbone/succd/http.go @@ -50,6 +50,90 @@ func formatMbar(v float32) template.HTML { return template.HTML(res) } +// apiData is the data model served to the user via HTTP/WebSockets +type apiData struct { + // Safety interlocks. + Safety struct { + // Failsafe mode enabled - pirani gauge seems disconnected. + Failsafe bool + // HighPressure interlock enabled - pressure too high to run diffusion + // pump. + HighPressure bool + } + // Pirani gauge data. + Pirani struct { + // Volts read from the gauge (0-10). + Volts string + // Mbar read from the gauge, formatted as HTML. + Mbar template.HTML + // MbarFloat read from the gauge. + MbarFloat float32 + } + // Pump state. + Pumps struct { + // RPOn means the roughing pump is turned on. + RPOn bool + // DPOn means the diffusion pump is turned on. + DPOn bool + } + // Pressure feedback into evacuation board. + Feedback struct { + // RoughReached is true when the system has reached a rough vacuum + // stage. + RoughReached bool + // HighReached is true when the system has reached a high vacuum stage. + HighReached bool + } + // System junk. + System struct { + // Load of the system. + Load string + // Hostname of the system. + Hostname string + } +} + +// 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() + + var hostname, load string + var err error + + if !skipSystem { + hostname, err = os.Hostname() + if err != nil { + hostname = "unknown" + } + loadB, err := os.ReadFile("/proc/loadavg") + load = "unknown" + if err == nil { + parts := strings.Fields(string(loadB)) + load = strings.Join(parts[:3], " ") + } + } + + ad := apiData{} + ad.Safety.Failsafe = safety.failsafe + ad.Safety.HighPressure = 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.Feedback.RoughReached = rough + ad.Feedback.HighReached = high + ad.System.Load = load + ad.System.Hostname = hostname + return &ad +} + // httpIndex is the / view. func (d *daemon) httpIndex(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/" { @@ -57,33 +141,9 @@ func (d *daemon) httpIndex(w http.ResponseWriter, r *http.Request) { return } - volts, mbar := d.pirani() - rp := d.rpGet() - dp := d.dpGet() - safety := d.safetyStatusGet() - - loadB, err := os.ReadFile("/proc/loadavg") - load := "unknown" - if err == nil { - parts := strings.Fields(string(loadB)) - load = strings.Join(parts[:3], " ") - } - - hostname, err := os.Hostname() - if err != nil { - hostname = "unknown" - } - - templateIndex.Execute(w, map[string]any{ - "failsafe": safety.failsafe, - "highpressure": safety.highPressure, - "volts": formatVolts(volts), - "mbar": formatMbar(mbar), - "rp": rp, - "dp": dp, - "hostname": hostname, - "load": load, - }) + data := d.apiData(false) + //data.Pirani.Mbar = html.St + templateIndex.Execute(w, data) } // httpStream is the websocket clientwards data hose, returning a 10Hz update @@ -105,33 +165,7 @@ func (d *daemon) httpStream(w http.ResponseWriter, r *http.Request) { c.Close(websocket.StatusNormalClosure, "") return case <-t.C: - // TODO(q3k): don't poll, get notified when new ADC readout is available. - volts, mbar := d.pirani() - rp := d.rpGet() - dp := d.dpGet() - rough, high := d.vacuumStatusGet() - safety := d.safetyStatusGet() - v := struct { - Failsafe bool - HighPressure bool - Volts string - Mbar string - MbarFloat float32 - RPOn bool - DPOn bool - RoughReached bool - HighReached bool - }{ - Failsafe: safety.failsafe, - HighPressure: safety.highPressure, - Volts: formatVolts(volts), - Mbar: string(formatMbar(mbar)), - MbarFloat: mbar, - RPOn: rp, - DPOn: dp, - RoughReached: rough, - HighReached: high, - } + v := d.apiData(true) if err := wsjson.Write(ctx, c, v); err != nil { klog.Errorf("Websocket write failed: %v", err) return diff --git a/succbone/succd/index.html b/succbone/succd/index.html index 4d758be..e12c73a 100644 --- a/succbone/succd/index.html +++ b/succbone/succd/index.html @@ -59,25 +59,25 @@ td > span { - + - + - + - + - + - + @@ -96,11 +96,11 @@ td > span { - + - +
Voltage{{.volts}}{{ .Pirani.Volts }}
Pressure{{.mbar}}{{ .Pirani.Mbar }}
Thresholds Rough:...{{ if .Feedback.RoughReached }}OK{{ else }}NOK{{ end }} High:...{{ if .Feedback.HighReached }}OK{{ else }}NOK{{ end }}
Pumps RP:{{ if .rp }}ON{{ else }}OFF{{ end }}{{ if .Pumps.RPOn }}ON{{ else }}OFF{{ end }} DP:{{ if .dp }}ON{{ else }}OFF{{ end }}{{ if .Pumps.DPOn }}ON{{ else }}OFF{{ end }}
Evac Control
FailsafeOK{{ if .Safety.Failsafe }}ON{{ else }}OFF{{ end }}
Diffusion Pump LockoutOK{{ if .Safety.HighPressure }}ON{{ else }}OFF{{ end }}

@@ -109,7 +109,7 @@ td > span {

- {{.hostname}} | load: {{.load}} | pprof | metrics | ws ping: + {{ .System.Hostname }} | load: {{ .System.Load }} | pprof | metrics | ws ping: