diff --git a/succbone/succd/http.go b/succbone/succd/http.go index 61df8a8..506236b 100644 --- a/succbone/succd/http.go +++ b/succbone/succd/http.go @@ -36,6 +36,24 @@ 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. @@ -84,8 +102,7 @@ type apiData struct { // not being served via websockets). func (s *webServer) apiData(skipSystem bool) *apiData { state := s.d.snapshot() - volts := state.piraniVolts100.avg - mbar := state.piraniMbar100.mbar + volts, mbar := state.pirani() rough, high := state.vacuumStatus() var hostname, load string @@ -108,7 +125,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 = formatMbarHTML(mbar) + ad.Pirani.Mbar = formatMbar(mbar) ad.Pirani.MbarFloat = mbar ad.Pumps.RPOn = state.rpOn ad.Pumps.DPOn = state.dpOn @@ -165,7 +182,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.piraniMbar100.mbar + _, 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) diff --git a/succbone/succd/main.go b/succbone/succd/main.go index 23427b6..99635ad 100644 --- a/succbone/succd/main.go +++ b/succbone/succd/main.go @@ -28,8 +28,6 @@ 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 550792b..bd3f85e 100644 --- a/succbone/succd/process.go +++ b/succbone/succd/process.go @@ -28,36 +28,42 @@ type daemon struct { daemonState } -// 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 +// 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 (d *daemonState) vacuumStatus() (rough, high bool) { - rough = !d.aboveRough.output - high = !d.aboveHigh.output - return +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) + } } // process runs the pain acquisition and control loop of succd. @@ -90,60 +96,56 @@ func (d *daemon) processOnce(_ context.Context) error { d.mu.Lock() defer d.mu.Unlock() - // Process pirani ringbuffers. - d.piraniVolts3.process(v) - d.piraniMbar3.process(d.piraniVolts3.avg) - d.piraniVolts100.process(v) - d.piraniMbar100.process(d.piraniVolts100.avg) + // Process pirani ringbuffer. + d.adcPiraniVolts = append(d.adcPiraniVolts, v) + trim := len(d.adcPiraniVolts) - 100 + if trim > 0 { + d.adcPiraniVolts = d.adcPiraniVolts[trim:] + } d.pumpdown.process() d.vent.process() - // 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") + _, 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") } } - 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 !d.safety.highPressure && mbar >= 1e-1 { + if mbar >= 1e-1 { + if !d.safety.highPressure { d.safety.highPressure = true - klog.Warningf("Pressure is too high (%s mbar); enabling diffusion pump lockout", formatMbar(mbar)) + klog.Errorf("Pressure is too high; enabling diffusion pump lockout") } - if d.safety.highPressure && mbar < (1e-1)-(1e-2) { + } else if mbar < (1e-1)-(1e-2) { + if d.safety.highPressure { d.safety.highPressure = false - klog.Infof("Pressure is low enough (%s mbar) for diffusion pump operation; quitting diffusion pump lockout", formatMbar(mbar)) + klog.Infof("Pressure is low enough for diffusion pump operation; quitting diffusion pump lockout") } - } 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 deleted file mode 100644 index 0b37193..0000000 --- a/succbone/succd/process_blocks.go +++ /dev/null @@ -1,88 +0,0 @@ -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 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 +} diff --git a/succbone/succd/scientific.go b/succbone/succd/scientific.go index a15f273..d2df8d8 100644 --- a/succbone/succd/scientific.go +++ b/succbone/succd/scientific.go @@ -2,7 +2,6 @@ package main import ( "fmt" - "html/template" "strconv" ) @@ -17,25 +16,8 @@ func (s *ScientificNotationValue) UnmarshalText(text []byte) error { return nil } -// 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 { +func (s *ScientificNotationValue) MarshalText() ([]byte, error) { + v := float64(*s) exp := 0 for v < 1 { v *= 10 @@ -47,11 +29,5 @@ func formatMbar(v float32) string { } 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 }