Compare commits

...

6 commits

6 changed files with 185 additions and 174 deletions

View file

@ -36,24 +36,6 @@ func formatVolts(v float32) string {
return fmt.Sprintf("%.4f V", v) 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<sup>%d</sup>", exp)
res += " mbar"
return template.HTML(res)
}
// apiData is the data model served to the user via HTTP/WebSockets // apiData is the data model served to the user via HTTP/WebSockets
type apiData struct { type apiData struct {
// Safety interlocks. // Safety interlocks.
@ -102,7 +84,8 @@ type apiData struct {
// not being served via websockets). // not being served via websockets).
func (s *webServer) apiData(skipSystem bool) *apiData { func (s *webServer) apiData(skipSystem bool) *apiData {
state := s.d.snapshot() state := s.d.snapshot()
volts, mbar := state.pirani() volts := state.piraniVolts100.avg
mbar := state.piraniMbar100.mbar
rough, high := state.vacuumStatus() rough, high := state.vacuumStatus()
var hostname, load string var hostname, load string
@ -125,7 +108,7 @@ func (s *webServer) apiData(skipSystem bool) *apiData {
ad.Safety.Failsafe = state.safety.failsafe ad.Safety.Failsafe = state.safety.failsafe
ad.Safety.HighPressure = state.safety.highPressure ad.Safety.HighPressure = state.safety.highPressure
ad.Pirani.Volts = formatVolts(volts) ad.Pirani.Volts = formatVolts(volts)
ad.Pirani.Mbar = formatMbar(mbar) ad.Pirani.Mbar = formatMbarHTML(mbar)
ad.Pirani.MbarFloat = mbar ad.Pirani.MbarFloat = mbar
ad.Pumps.RPOn = state.rpOn ad.Pumps.RPOn = state.rpOn
ad.Pumps.DPOn = state.dpOn ad.Pumps.DPOn = state.dpOn
@ -182,7 +165,7 @@ func (s *webServer) viewMetrics(w http.ResponseWriter, r *http.Request) {
// library. // library.
// TODO(q3k): serve the rest of the data model // TODO(q3k): serve the rest of the data model
state := s.d.snapshot() 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, "# HELP sem_pressure_mbar Pressure in the SEM chamber, in millibar\n")
fmt.Fprintf(w, "# TYPE sem_pressure_mbar gauge\n") fmt.Fprintf(w, "# TYPE sem_pressure_mbar gauge\n")
fmt.Fprintf(w, "sem_pressure_mbar %f\n", mbar) fmt.Fprintf(w, "sem_pressure_mbar %f\n", mbar)

View file

@ -28,6 +28,8 @@ func main() {
d := daemon{} d := daemon{}
d.daemonState.rpOn = true d.daemonState.rpOn = true
d.daemonState.piraniVolts3.limit = 3
d.daemonState.piraniVolts100.limit = 100
d.aboveRough.threshold = float64(flagPressureThresholdRough) d.aboveRough.threshold = float64(flagPressureThresholdRough)
d.aboveHigh.threshold = float64(flagPressureThresholdHigh) d.aboveHigh.threshold = float64(flagPressureThresholdHigh)

View file

@ -28,42 +28,36 @@ type daemon struct {
daemonState daemonState
} }
// momentaryOutput is an output that can be triggered for 500ms. // daemonState contains all the state of the daemon. A copy of it can be
type momentaryOutput struct { // requested for consumers, eg. the web view.
// output of the block. type daemonState struct {
output bool safety struct {
// scheduledOff is when the block should be outputting false again. // failsafe mode is enabled when the pirani gauge appears to be
scheduledOff time.Time // disconnected, and is disabled only when an atmosphere is read.
} failsafe bool
// highPressure mode is enabled when the pressure reading is above 1e-1
func (m *momentaryOutput) process() { // mbar, locking out the diffusion pump from being enabled.
m.output = m.scheduledOff.After(time.Now()) highPressure bool
}
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)
} }
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. // process runs the pain acquisition and control loop of succd.
@ -96,56 +90,60 @@ func (d *daemon) processOnce(_ context.Context) error {
d.mu.Lock() d.mu.Lock()
defer d.mu.Unlock() defer d.mu.Unlock()
// Process pirani ringbuffer. // Process pirani ringbuffers.
d.adcPiraniVolts = append(d.adcPiraniVolts, v) d.piraniVolts3.process(v)
trim := len(d.adcPiraniVolts) - 100 d.piraniMbar3.process(d.piraniVolts3.avg)
if trim > 0 { d.piraniVolts100.process(v)
d.adcPiraniVolts = d.adcPiraniVolts[trim:] d.piraniMbar100.process(d.piraniVolts100.avg)
}
d.pumpdown.process() d.pumpdown.process()
d.vent.process() d.vent.process()
_, mbar := d.daemonState.pirani() // Run safety checks based on small ringbuffer.
d.aboveRough.process(float64(mbar)) if d.piraniVolts3.saturated() {
d.aboveHigh.process(float64(mbar)) mbar := d.piraniMbar3.mbar
if !d.safety.failsafe && mbar < 4e-6 {
// Check if the pirani gauge is disconnected. Note: this will assume the // Unrealistic result, Pirani probe probably disconnected. Failsafe mode.
// pirani gauge is connected for the first couple of processing runs as if !d.safety.failsafe {
// samples are still being captured. d.safety.failsafe = true
if d.piraniDetection() == piraniDetectionDisconnected { klog.Errorf("Pirani probe seems disconnected; enabling failsafe mode")
// 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 mbar >= 1e-1 { if !d.safety.highPressure && mbar >= 1e-1 {
if !d.safety.highPressure {
d.safety.highPressure = true 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 && mbar < (1e-1)-(1e-2) {
if d.safety.highPressure {
d.safety.highPressure = false 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 { if d.safety.failsafe {
d.aboveRough.output = true d.aboveRough.output = true
d.aboveHigh.output = true d.aboveHigh.output = true
d.dpOn = false d.dpOn = false
} }
if d.safety.highPressure { if d.safety.highPressure {
d.dpOn = false d.dpOn = false
} }

View file

@ -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)
}

View file

@ -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
}

View file

@ -2,6 +2,7 @@ package main
import ( import (
"fmt" "fmt"
"html/template"
"strconv" "strconv"
) )
@ -16,8 +17,25 @@ func (s *ScientificNotationValue) UnmarshalText(text []byte) error {
return nil return nil
} }
func (s *ScientificNotationValue) MarshalText() ([]byte, error) { // formatMbarHTML formats a millibar value using scientific notation and returns
v := float64(*s) // 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<sup>%d</sup>", exp)
res += " mbar"
return template.HTML(res)
}
func formatMbar(v float32) string {
exp := 0 exp := 0
for v < 1 { for v < 1 {
v *= 10 v *= 10
@ -29,5 +47,11 @@ func (s *ScientificNotationValue) MarshalText() ([]byte, error) {
} }
res := fmt.Sprintf("%.3f", v) res := fmt.Sprintf("%.3f", v)
res += fmt.Sprintf("e%d", exp) res += fmt.Sprintf("e%d", exp)
return res
}
func (s *ScientificNotationValue) MarshalText() ([]byte, error) {
v := float32(*s)
res := formatMbar(v)
return []byte(res), nil return []byte(res), nil
} }