diff --git a/succbone/succd/http.go b/succbone/succd/http.go
index 506236b..61df8a8 100644
--- a/succbone/succd/http.go
+++ b/succbone/succd/http.go
@@ -36,24 +36,6 @@ func formatVolts(v float32) string {
return fmt.Sprintf("%.4f V", v)
}
-// formatMbar formats a millibar value using scientific notation and returns a
-// HTML fragment (for superscript support).
-func formatMbar(v float32) template.HTML {
- exp := 0
- for v < 1 {
- v *= 10
- exp -= 1
- }
- for v >= 10 {
- v /= 10
- exp += 1
- }
- res := fmt.Sprintf("%.3f", v)
- res += fmt.Sprintf(" x 10%d", exp)
- res += " mbar"
- return template.HTML(res)
-}
-
// apiData is the data model served to the user via HTTP/WebSockets
type apiData struct {
// Safety interlocks.
@@ -102,7 +84,8 @@ type apiData struct {
// not being served via websockets).
func (s *webServer) apiData(skipSystem bool) *apiData {
state := s.d.snapshot()
- volts, mbar := state.pirani()
+ volts := state.piraniVolts100.avg
+ mbar := state.piraniMbar100.mbar
rough, high := state.vacuumStatus()
var hostname, load string
@@ -125,7 +108,7 @@ func (s *webServer) apiData(skipSystem bool) *apiData {
ad.Safety.Failsafe = state.safety.failsafe
ad.Safety.HighPressure = state.safety.highPressure
ad.Pirani.Volts = formatVolts(volts)
- ad.Pirani.Mbar = formatMbar(mbar)
+ ad.Pirani.Mbar = formatMbarHTML(mbar)
ad.Pirani.MbarFloat = mbar
ad.Pumps.RPOn = state.rpOn
ad.Pumps.DPOn = state.dpOn
@@ -182,7 +165,7 @@ func (s *webServer) viewMetrics(w http.ResponseWriter, r *http.Request) {
// library.
// TODO(q3k): serve the rest of the data model
state := s.d.snapshot()
- _, mbar := state.pirani()
+ mbar := state.piraniMbar100.mbar
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)
diff --git a/succbone/succd/main.go b/succbone/succd/main.go
index 99635ad..23427b6 100644
--- a/succbone/succd/main.go
+++ b/succbone/succd/main.go
@@ -28,6 +28,8 @@ func main() {
d := daemon{}
d.daemonState.rpOn = true
+ d.daemonState.piraniVolts3.limit = 3
+ d.daemonState.piraniVolts100.limit = 100
d.aboveRough.threshold = float64(flagPressureThresholdRough)
d.aboveHigh.threshold = float64(flagPressureThresholdHigh)
diff --git a/succbone/succd/process.go b/succbone/succd/process.go
index bd3f85e..550792b 100644
--- a/succbone/succd/process.go
+++ b/succbone/succd/process.go
@@ -28,42 +28,36 @@ type daemon struct {
daemonState
}
-// 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)
+// 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 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
}
+
+ piraniVolts100 ringbufferInput
+ piraniMbar100 pfeifferVoltsToMbar
+ piraniVolts3 ringbufferInput
+ piraniMbar3 pfeifferVoltsToMbar
+
+ rpOn bool
+ dpOn bool
+
+ vent momentaryOutput
+ pumpdown momentaryOutput
+ aboveRough thresholdOutput
+ aboveHigh thresholdOutput
+}
+
+func (d *daemonState) vacuumStatus() (rough, high bool) {
+ rough = !d.aboveRough.output
+ high = !d.aboveHigh.output
+ return
}
// process runs the pain acquisition and control loop of succd.
@@ -96,56 +90,60 @@ func (d *daemon) processOnce(_ context.Context) error {
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:]
- }
+ // Process pirani ringbuffers.
+ d.piraniVolts3.process(v)
+ d.piraniMbar3.process(d.piraniVolts3.avg)
+ d.piraniVolts100.process(v)
+ d.piraniMbar100.process(d.piraniVolts100.avg)
d.pumpdown.process()
d.vent.process()
- _, mbar := d.daemonState.pirani()
- d.aboveRough.process(float64(mbar))
- d.aboveHigh.process(float64(mbar))
-
- // 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.safety.failsafe {
- d.safety.failsafe = true
- klog.Errorf("Pirani probe seems disconnected; enabling failsafe mode")
- }
- } else {
- if d.safety.failsafe {
- if mbar >= 1e2 {
- d.safety.failsafe = false
- klog.Infof("Values are plausible again; quitting failsafe mode")
+ // Run safety checks based on small ringbuffer.
+ if d.piraniVolts3.saturated() {
+ mbar := d.piraniMbar3.mbar
+ if !d.safety.failsafe && mbar < 4e-6 {
+ // Unrealistic result, Pirani probe probably disconnected. Failsafe mode.
+ if !d.safety.failsafe {
+ d.safety.failsafe = true
+ klog.Errorf("Pirani probe seems disconnected; enabling failsafe mode")
}
}
- }
+ if d.safety.failsafe && mbar > 1e2 {
+ d.safety.failsafe = false
+ klog.Infof("Pirani probe value (%s) is plausible again; quitting failsafe mode", formatMbar(mbar))
+ }
- if mbar >= 1e-1 {
- if !d.safety.highPressure {
+ if !d.safety.highPressure && mbar >= 1e-1 {
d.safety.highPressure = true
- klog.Errorf("Pressure is too high; enabling diffusion pump lockout")
+ klog.Warningf("Pressure is too high (%s mbar); enabling diffusion pump lockout", formatMbar(mbar))
}
- } else if mbar < (1e-1)-(1e-2) {
- if d.safety.highPressure {
+ if d.safety.highPressure && mbar < (1e-1)-(1e-2) {
d.safety.highPressure = false
- klog.Infof("Pressure is low enough for diffusion pump operation; quitting diffusion pump lockout")
+ klog.Infof("Pressure is low enough (%s mbar) for diffusion pump operation; quitting diffusion pump lockout", formatMbar(mbar))
}
+ } else {
+ d.safety.failsafe = true
+ d.safety.highPressure = true
}
+ // Control threhold/feedback values based on main pirani ringbuffer, failing
+ // safe if not enough data is present.
+ if d.piraniVolts100.saturated() {
+ mbar := d.piraniMbar100.mbar
+ d.aboveRough.process(float64(mbar))
+ d.aboveHigh.process(float64(mbar))
+ } else {
+ d.aboveRough.output = true
+ d.aboveHigh.output = true
+ }
+
+ // Apply safety overrides.
if d.safety.failsafe {
d.aboveRough.output = true
d.aboveHigh.output = true
d.dpOn = false
}
-
if d.safety.highPressure {
d.dpOn = false
}
diff --git a/succbone/succd/process_blocks.go b/succbone/succd/process_blocks.go
new file mode 100644
index 0000000..0b37193
--- /dev/null
+++ b/succbone/succd/process_blocks.go
@@ -0,0 +1,88 @@
+package main
+
+import (
+ "math"
+ "time"
+)
+
+// 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)
+ }
+}
+
+// ringbufferInput accumulates analog data up to limit samples, and calculates
+// an average.
+type ringbufferInput struct {
+ data []float32
+ limit uint
+ // avg is the mean average of the samples in data, or 0.0 if no data is
+ // present yet. This is the main output of the block.
+ avg float32
+}
+
+func (r *ringbufferInput) process(input float32) {
+ // TODO(q3k): use actual ringbuffer
+ // TODO(q3k): optimize average calculation
+ // TODO(q3k): precalculate value in mbar
+ r.data = append(r.data, input)
+ trim := len(r.data) - int(r.limit)
+ if trim > 0 {
+ r.data = r.data[trim:]
+ }
+ avg := float32(0.0)
+ for _, v := range r.data {
+ avg += v
+ }
+ if len(r.data) != 0 {
+ avg /= float32(len(r.data))
+ }
+ r.avg = avg
+}
+
+// saturated returns true if the number of samples is at the configured limit.
+func (r *ringbufferInput) saturated() bool {
+ return len(r.data) >= int(r.limit)
+}
+
+type pfeifferVoltsToMbar struct {
+ mbar float32
+}
+
+func (p *pfeifferVoltsToMbar) process(volts float32) {
+ // Per Pirani probe docs.
+ bar := math.Pow(10.0, float64(volts)-8.5)
+ p.mbar = float32(bar * 1000.0)
+}
diff --git a/succbone/succd/process_state.go b/succbone/succd/process_state.go
deleted file mode 100644
index 0bbb09e..0000000
--- a/succbone/succd/process_state.go
+++ /dev/null
@@ -1,84 +0,0 @@
-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
-}
diff --git a/succbone/succd/scientific.go b/succbone/succd/scientific.go
index d2df8d8..a15f273 100644
--- a/succbone/succd/scientific.go
+++ b/succbone/succd/scientific.go
@@ -2,6 +2,7 @@ package main
import (
"fmt"
+ "html/template"
"strconv"
)
@@ -16,8 +17,25 @@ func (s *ScientificNotationValue) UnmarshalText(text []byte) error {
return nil
}
-func (s *ScientificNotationValue) MarshalText() ([]byte, error) {
- v := float64(*s)
+// formatMbarHTML formats a millibar value using scientific notation and returns
+// a HTML fragment (for superscript support).
+func formatMbarHTML(v float32) template.HTML {
+ exp := 0
+ for v < 1 {
+ v *= 10
+ exp -= 1
+ }
+ for v >= 10 {
+ v /= 10
+ exp += 1
+ }
+ res := fmt.Sprintf("%.3f", v)
+ res += fmt.Sprintf(" x 10%d", exp)
+ res += " mbar"
+ return template.HTML(res)
+}
+
+func formatMbar(v float32) string {
exp := 0
for v < 1 {
v *= 10
@@ -29,5 +47,11 @@ func (s *ScientificNotationValue) MarshalText() ([]byte, error) {
}
res := fmt.Sprintf("%.3f", v)
res += fmt.Sprintf("e%d", exp)
+ return res
+}
+
+func (s *ScientificNotationValue) MarshalText() ([]byte, error) {
+ v := float32(*s)
+ res := formatMbar(v)
return []byte(res), nil
}