diff --git a/succbone/succd/http.go b/succbone/succd/http.go
index 3e24873..0be9c76 100644
--- a/succbone/succd/http.go
+++ b/succbone/succd/http.go
@@ -106,18 +106,23 @@ func (d *daemon) httpStream(w http.ResponseWriter, r *http.Request) {
volts, mbar := d.pirani()
rp := d.rpGet()
dp := d.dpGet()
+ rough, high := d.vacuumStatusGet()
v := struct {
- Volts string
- Mbar string
- MbarFloat float32
- RPOn bool
- DPOn bool
+ Volts string
+ Mbar string
+ MbarFloat float32
+ RPOn bool
+ DPOn bool
+ RoughReached bool
+ HighReached bool
}{
- Volts: formatVolts(volts),
- Mbar: string(formatMbar(mbar)),
- MbarFloat: mbar,
- RPOn: rp,
- DPOn: dp,
+ Volts: formatVolts(volts),
+ Mbar: string(formatMbar(mbar)),
+ MbarFloat: mbar,
+ RPOn: rp,
+ DPOn: dp,
+ RoughReached: rough,
+ HighReached: high,
}
if err := wsjson.Write(ctx, c, v); err != nil {
klog.Errorf("Websocket write failed: %v", err)
diff --git a/succbone/succd/index.html b/succbone/succd/index.html
index 363ce94..c764d76 100644
--- a/succbone/succd/index.html
+++ b/succbone/succd/index.html
@@ -36,6 +36,10 @@ button {
padding-right: 1.5em;
}
+td > span {
+ padding: 0.2em;
+}
+
.logo {
float: left;
margin-right: 2em;
@@ -55,23 +59,29 @@ button {
Voltage |
- {{.volts}} |
+ {{.volts}} |
Pressure |
- {{.mbar}} |
+ {{.mbar}} |
+
+ Thresholds |
+ Rough: |
+ ... |
+ High: |
+ ... |
+
- Roughing Pump |
- {{ if .rp }}ON{{ else }}OFF{{ end }} |
-
-
- Diffusion Pump |
- {{ if .dp }}ON{{ else }}OFF{{ end }} |
+ Pumps |
+ RP: |
+ {{ if .rp }}ON{{ else }}OFF{{ end }} |
+ DP: |
+ {{ if .dp }}ON{{ else }}OFF{{ end }} |
Evac Control |
-
+ |
@@ -82,7 +92,7 @@ button {
|
Status |
- OK |
+ OK |
@@ -233,6 +243,8 @@ window.addEventListener("load", (_) => {
let volts = document.querySelector("#volts");
let mbar = document.querySelector("#mbar");
let ping = document.querySelector("#ping");
+ let trough = document.querySelector("#trough");
+ let thigh = document.querySelector("#thigh");
// Buttons
let pd = document.querySelector("#pd");
@@ -280,6 +292,22 @@ window.addEventListener("load", (_) => {
} else {
dp.style = "background-color: #f06060";
}
+
+ let t = [];
+ if (data.RoughReached) {
+ trough.innerHTML = "OK";
+ trough.style = "background-color: #60f060";
+ } else {
+ trough.innerHTML = "NOK";
+ trough.style = "background-color: #f06060";
+ }
+ if (data.HighReached) {
+ thigh.innerHTML = "OK";
+ thigh.style = "background-color: #60f060";
+ } else {
+ thigh.innerHTML = "NOK";
+ thigh.style = "background-color: #f06060";
+ }
historicalPush(data.MbarFloat);
ping.innerHTML = Date.now();
});
diff --git a/succbone/succd/main.go b/succbone/succd/main.go
index 85581c1..32e5c17 100644
--- a/succbone/succd/main.go
+++ b/succbone/succd/main.go
@@ -2,166 +2,26 @@ package main
import (
"context"
- "errors"
"flag"
- "fmt"
- "math"
"net/http"
"os"
"os/signal"
- "sync"
- "time"
"k8s.io/klog"
)
-// daemon is the main state of the succdaemon.
-type daemon struct {
- // adcPirani is the adc implementation returning the voltage of the Pfeiffer
- // Pirani gauge.
- adcPirani adc
-
- gpioDiffusionPump gpio
- gpioRoughingPump gpio
- gpioBtnPumpDown gpio
- gpioBtnVent gpio
-
- // mu guards state variables below.
- mu sync.RWMutex
- // adcPiraniVolts is a moving window of read ADC values, used to calculate a
- // moving average.
- adcPiraniVolts []float32
- rpOn bool
- dpOn bool
- // ventScheduled and pumpdownScheduled are timers which expire when the
- // vent/pumpdown relays should be deactivated. This allows these outputs to
- // be controlled momentarily.
- ventScheduled time.Time
- pumpdownScheduled time.Time
-}
-
-// process runs the pain acquisition and control loop of succd.
-func (d *daemon) process(ctx context.Context) {
- ticker := time.NewTicker(time.Millisecond * 100)
- defer ticker.Stop()
- for {
- select {
- case <-ticker.C:
- if err := d.processOnce(ctx); err != nil {
- if errors.Is(err, ctx.Err()) {
- return
- } else {
- klog.Errorf("Processing error: %v", err)
- time.Sleep(time.Second * 10)
- }
- }
- case <-ctx.Done():
- return
- }
- }
-}
-
-// processOnce runs the main loop step of succd.
-func (d *daemon) processOnce(_ context.Context) error {
- v, err := d.adcPirani.Read()
- if err != nil {
- return fmt.Errorf("when reading ADC: %w", err)
- }
- d.mu.Lock()
- defer d.mu.Unlock()
- d.adcPiraniVolts = append(d.adcPiraniVolts, v)
- trim := len(d.adcPiraniVolts) - 100
- if trim > 0 {
- d.adcPiraniVolts = d.adcPiraniVolts[trim:]
- }
- if err := d.gpioRoughingPump.set(d.rpOn); err != nil {
- return fmt.Errorf("when configuring RP: %w", err)
- }
- if err := d.gpioDiffusionPump.set(!d.dpOn); err != nil {
- return fmt.Errorf("when configuring RP: %w", err)
- }
- if err := d.gpioBtnPumpDown.set(!d.pumpdownScheduled.After(time.Now())); err != nil {
- return fmt.Errorf("when configuring pumpdown: %w", err)
- }
- if err := d.gpioBtnVent.set(!d.ventScheduled.After(time.Now())); err != nil {
- return fmt.Errorf("when configuring vent: %w", err)
- }
-
- return nil
-}
-
-// pirani returns the Pirani gauge voltage and pressure.
-func (d *daemon) pirani() (volts float32, mbar float32) {
- d.mu.RLock()
- volts = 0.0
- for _, v := range d.adcPiraniVolts {
- volts += v
- }
- if len(d.adcPiraniVolts) != 0 {
- volts /= float32(len(d.adcPiraniVolts))
- }
- d.mu.RUnlock()
-
- // Per Pirani probe docs.
- bar := math.Pow(10.0, float64(volts)-8.5)
- mbar = float32(bar * 1000.0)
- return
-}
-
-// rpSet enables/disables the roughing pump.
-func (d *daemon) rpSet(state bool) {
- d.mu.Lock()
- defer d.mu.Unlock()
- d.rpOn = state
-}
-
-// rpGet returns whether the roughing pump is enabled/disabled.
-func (d *daemon) rpGet() bool {
- d.mu.RLock()
- defer d.mu.RUnlock()
- return d.rpOn
-}
-
-// dpSet enables/disables the diffusion pump.
-func (d *daemon) dpSet(state bool) {
- d.mu.Lock()
- defer d.mu.Unlock()
- d.dpOn = state
-}
-
-// dpGet returns whether the diffusion pump is enabled/disabled.
-func (d *daemon) dpGet() bool {
- d.mu.RLock()
- defer d.mu.RUnlock()
- return d.dpOn
-}
-
-// pumpDownPressed toggles the pump down relay for 500ms.
-func (d *daemon) pumpDownPress() {
- d.mu.Lock()
- defer d.mu.Unlock()
- if d.pumpdownScheduled.Before(time.Now()) {
- d.pumpdownScheduled = time.Now().Add(500 * time.Millisecond)
- }
-}
-
-// ventPress toggles the vent relay for 500ms.
-func (d *daemon) ventPress() {
- d.mu.Lock()
- defer d.mu.Unlock()
- if d.ventScheduled.Before(time.Now()) {
- d.ventScheduled = time.Now().Add(500 * time.Millisecond)
- }
-}
-
var (
- flagFake bool
- flagListenHTTP string
+ flagFake bool
+ flagListenHTTP string
+ flagPressureThresholdRough = ScientificNotationValue(1e-1)
+ flagPressureThresholdHigh = ScientificNotationValue(1e-4)
)
func main() {
flag.BoolVar(&flagFake, "fake", false, "Enable fake mode which allows to run succd for tests outside the succbone")
flag.StringVar(&flagListenHTTP, "listen_http", ":8080", "Address at which to listen for HTTP requests")
+ flag.TextVar(&flagPressureThresholdRough, "pressure_threshold_rough", &flagPressureThresholdRough, "Threshold for opening up diffusion pump (mbar)")
+ flag.TextVar(&flagPressureThresholdHigh, "pressure_threshold_high", &flagPressureThresholdHigh, "Threshold for enabling high voltage circuits (mbar)")
flag.Parse()
ctx, _ := signal.NotifyContext(context.Background(), os.Interrupt)
@@ -169,6 +29,8 @@ func main() {
d := daemon{
rpOn: true,
}
+ d.aboveRough.threshold = float64(flagPressureThresholdRough)
+ d.aboveHigh.threshold = float64(flagPressureThresholdHigh)
if flagFake {
klog.Infof("Starting with fake peripherals")
d.adcPirani = &fakeADC{}
@@ -176,6 +38,8 @@ func main() {
d.gpioDiffusionPump = &fakeGPIO{desc: "~dp"}
d.gpioBtnPumpDown = &fakeGPIO{desc: "~pd"}
d.gpioBtnVent = &fakeGPIO{desc: "~vent"}
+ d.gpioBelowRough = &fakeGPIO{desc: "~rough"}
+ d.gpioBelowHigh = &fakeGPIO{desc: "~high"}
} else {
adc, err := newBBADC(0)
if err != nil {
@@ -191,8 +55,9 @@ func main() {
{&d.gpioDiffusionPump, 49},
{&d.gpioBtnPumpDown, 48},
{&d.gpioBtnVent, 60},
+ {&d.gpioBelowRough, 30},
+ {&d.gpioBelowHigh, 7},
} {
- // Relay, active low.
*c.out, err = newBBGPIO(c.num, true)
if err != nil {
klog.Exitf("Failed to setup GPIO: %v", err)
diff --git a/succbone/succd/process.go b/succbone/succd/process.go
new file mode 100644
index 0000000..b38cf25
--- /dev/null
+++ b/succbone/succd/process.go
@@ -0,0 +1,225 @@
+package main
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "math"
+ "sync"
+ "time"
+
+ "k8s.io/klog"
+)
+
+// daemon is the main state of the succdaemon.
+type daemon struct {
+ // adcPirani is the adc implementation returning the voltage of the Pfeiffer
+ // Pirani gauge.
+ adcPirani adc
+
+ gpioDiffusionPump gpio
+ gpioRoughingPump gpio
+ gpioBtnPumpDown gpio
+ gpioBtnVent gpio
+ gpioBelowRough gpio
+ gpioBelowHigh gpio
+
+ // mu guards state variables below.
+ mu sync.RWMutex
+ // 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
+}
+
+// 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)
+ }
+}
+
+// process runs the pain acquisition and control loop of succd.
+func (d *daemon) process(ctx context.Context) {
+ ticker := time.NewTicker(time.Millisecond * 100)
+ defer ticker.Stop()
+ for {
+ select {
+ case <-ticker.C:
+ if err := d.processOnce(ctx); err != nil {
+ if errors.Is(err, ctx.Err()) {
+ return
+ } else {
+ klog.Errorf("Processing error: %v", err)
+ time.Sleep(time.Second * 10)
+ }
+ }
+ case <-ctx.Done():
+ return
+ }
+ }
+}
+
+// processOnce runs the main loop step of succd.
+func (d *daemon) processOnce(_ context.Context) error {
+ v, err := d.adcPirani.Read()
+ if err != nil {
+ return fmt.Errorf("when reading ADC: %w", err)
+ }
+ 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:]
+ }
+
+ d.pumpdown.process()
+ d.vent.process()
+
+ _, mbar := d.piraniUnlocked()
+ d.aboveRough.process(float64(mbar))
+ d.aboveHigh.process(float64(mbar))
+
+ // Update relay outputs.
+ for _, rel := range []struct {
+ name string
+ gpio gpio
+ // activeHigh means the relay is active high, ie. a true source will
+ // mean that NO/COM get connected, and a false source means that NC/COM
+ // get connected.
+ activeHigh bool
+ source bool
+ }{
+ {"rp", d.gpioRoughingPump, false, d.rpOn},
+ {"dp", d.gpioDiffusionPump, true, d.dpOn},
+ {"pumpdown", d.gpioBtnPumpDown, true, d.pumpdown.output},
+ {"vent", d.gpioBtnVent, true, d.vent.output},
+ {"rough", d.gpioBelowRough, false, d.aboveRough.output},
+ {"high", d.gpioBelowHigh, false, d.aboveHigh.output},
+ } {
+ val := rel.source
+ if rel.activeHigh {
+ // Invert because the relays go through logical inversion (ie. a
+ // GPIO false is a relay trigger).
+ val = !val
+ }
+ if err := rel.gpio.set(val); err != nil {
+ return fmt.Errorf("when outputting %s: %w", rel.name, err)
+ }
+ }
+
+ return nil
+}
+
+// pirani returns the Pirani gauge voltage and pressure.
+func (d *daemon) pirani() (volts float32, mbar float32) {
+ d.mu.RLock()
+ volts, mbar = d.piraniUnlocked()
+ d.mu.RUnlock()
+ return
+}
+
+func (d *daemon) piraniUnlocked() (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
+}
+
+// rpSet enables/disables the roughing pump.
+func (d *daemon) rpSet(state bool) {
+ d.mu.Lock()
+ defer d.mu.Unlock()
+ d.rpOn = state
+}
+
+// rpGet returns whether the roughing pump is enabled/disabled.
+func (d *daemon) rpGet() bool {
+ d.mu.RLock()
+ defer d.mu.RUnlock()
+ return d.rpOn
+}
+
+// dpSet enables/disables the diffusion pump.
+func (d *daemon) dpSet(state bool) {
+ d.mu.Lock()
+ defer d.mu.Unlock()
+ d.dpOn = state
+}
+
+// dpGet returns whether the diffusion pump is enabled/disabled.
+func (d *daemon) dpGet() bool {
+ d.mu.RLock()
+ defer d.mu.RUnlock()
+ return d.dpOn
+}
+
+func (d *daemon) vacuumStatusGet() (rough, high bool) {
+ d.mu.RLock()
+ defer d.mu.RUnlock()
+ rough = !d.aboveRough.output
+ high = !d.aboveHigh.output
+ return
+}
+
+// pumpDownPressed toggles the pump down relay for 500ms.
+func (d *daemon) pumpDownPress() {
+ d.mu.Lock()
+ defer d.mu.Unlock()
+ d.pumpdown.trigger()
+}
+
+// ventPress toggles the vent relay for 500ms.
+func (d *daemon) ventPress() {
+ d.mu.Lock()
+ defer d.mu.Unlock()
+ d.vent.trigger()
+}
diff --git a/succbone/succd/scientific.go b/succbone/succd/scientific.go
new file mode 100644
index 0000000..d2df8d8
--- /dev/null
+++ b/succbone/succd/scientific.go
@@ -0,0 +1,33 @@
+package main
+
+import (
+ "fmt"
+ "strconv"
+)
+
+type ScientificNotationValue float64
+
+func (s *ScientificNotationValue) UnmarshalText(text []byte) error {
+ f, err := strconv.ParseFloat(string(text), 64)
+ if err != nil {
+ return err
+ }
+ *s = ScientificNotationValue(f)
+ return nil
+}
+
+func (s *ScientificNotationValue) MarshalText() ([]byte, error) {
+ v := float64(*s)
+ exp := 0
+ for v < 1 {
+ v *= 10
+ exp -= 1
+ }
+ for v >= 10 {
+ v /= 10
+ exp += 1
+ }
+ res := fmt.Sprintf("%.3f", v)
+ res += fmt.Sprintf("e%d", exp)
+ return []byte(res), nil
+}