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 {
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 |
@@ -96,11 +96,11 @@ td > span {
Failsafe |
- OK |
+ {{ if .Safety.Failsafe }}ON{{ else }}OFF{{ end }} |
Diffusion Pump Lockout |
- OK |
+ {{ 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: …