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 }