Compare commits
No commits in common. "960be9cd236d42f553b02b10f551a56c9e559ed7" and "42c9ae2fa70452b887da48e6951f6647e3898aba" have entirely different histories.
960be9cd23
...
42c9ae2fa7
|
@ -36,6 +36,24 @@ 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.
|
||||||
|
@ -84,8 +102,7 @@ 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 := state.piraniVolts100.avg
|
volts, mbar := state.pirani()
|
||||||
mbar := state.piraniMbar100.mbar
|
|
||||||
rough, high := state.vacuumStatus()
|
rough, high := state.vacuumStatus()
|
||||||
|
|
||||||
var hostname, load string
|
var hostname, load string
|
||||||
|
@ -108,7 +125,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 = formatMbarHTML(mbar)
|
ad.Pirani.Mbar = formatMbar(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
|
||||||
|
@ -165,7 +182,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.piraniMbar100.mbar
|
_, mbar := state.pirani()
|
||||||
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)
|
||||||
|
|
|
@ -28,8 +28,6 @@ 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)
|
||||||
|
|
|
@ -28,36 +28,42 @@ type daemon struct {
|
||||||
daemonState
|
daemonState
|
||||||
}
|
}
|
||||||
|
|
||||||
// daemonState contains all the state of the daemon. A copy of it can be
|
// momentaryOutput is an output that can be triggered for 500ms.
|
||||||
// requested for consumers, eg. the web view.
|
type momentaryOutput struct {
|
||||||
type daemonState struct {
|
// output of the block.
|
||||||
safety struct {
|
output bool
|
||||||
// failsafe mode is enabled when the pirani gauge appears to be
|
// scheduledOff is when the block should be outputting false again.
|
||||||
// disconnected, and is disabled only when an atmosphere is read.
|
scheduledOff time.Time
|
||||||
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) {
|
func (m *momentaryOutput) process() {
|
||||||
rough = !d.aboveRough.output
|
m.output = m.scheduledOff.After(time.Now())
|
||||||
high = !d.aboveHigh.output
|
}
|
||||||
return
|
|
||||||
|
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.
|
// process runs the pain acquisition and control loop of succd.
|
||||||
|
@ -90,60 +96,56 @@ func (d *daemon) processOnce(_ context.Context) error {
|
||||||
d.mu.Lock()
|
d.mu.Lock()
|
||||||
defer d.mu.Unlock()
|
defer d.mu.Unlock()
|
||||||
|
|
||||||
// Process pirani ringbuffers.
|
// Process pirani ringbuffer.
|
||||||
d.piraniVolts3.process(v)
|
d.adcPiraniVolts = append(d.adcPiraniVolts, v)
|
||||||
d.piraniMbar3.process(d.piraniVolts3.avg)
|
trim := len(d.adcPiraniVolts) - 100
|
||||||
d.piraniVolts100.process(v)
|
if trim > 0 {
|
||||||
d.piraniMbar100.process(d.piraniVolts100.avg)
|
d.adcPiraniVolts = d.adcPiraniVolts[trim:]
|
||||||
|
}
|
||||||
|
|
||||||
d.pumpdown.process()
|
d.pumpdown.process()
|
||||||
d.vent.process()
|
d.vent.process()
|
||||||
|
|
||||||
// Run safety checks based on small ringbuffer.
|
_, mbar := d.daemonState.pirani()
|
||||||
if d.piraniVolts3.saturated() {
|
d.aboveRough.process(float64(mbar))
|
||||||
mbar := d.piraniMbar3.mbar
|
d.aboveHigh.process(float64(mbar))
|
||||||
if !d.safety.failsafe && mbar < 4e-6 {
|
|
||||||
// Unrealistic result, Pirani probe probably disconnected. Failsafe mode.
|
// Check if the pirani gauge is disconnected. Note: this will assume the
|
||||||
if !d.safety.failsafe {
|
// pirani gauge is connected for the first couple of processing runs as
|
||||||
d.safety.failsafe = true
|
// samples are still being captured.
|
||||||
klog.Errorf("Pirani probe seems disconnected; enabling failsafe mode")
|
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
|
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
|
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 {
|
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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
|
||||||
}
|
|
84
succbone/succd/process_state.go
Normal file
84
succbone/succd/process_state.go
Normal file
|
@ -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
|
||||||
|
}
|
|
@ -2,7 +2,6 @@ package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"html/template"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -17,25 +16,8 @@ func (s *ScientificNotationValue) UnmarshalText(text []byte) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// formatMbarHTML formats a millibar value using scientific notation and returns
|
func (s *ScientificNotationValue) MarshalText() ([]byte, error) {
|
||||||
// a HTML fragment (for superscript support).
|
v := float64(*s)
|
||||||
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
|
||||||
|
@ -47,11 +29,5 @@ func formatMbar(v float32) string {
|
||||||
}
|
}
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue