succd: implement rp/dp/vent/pumpdown control
This commit is contained in:
parent
d01263fead
commit
070f45b1bc
|
@ -3,7 +3,11 @@ succd
|
|||
|
||||
A little daemon for monitoring the SEM. This is a temporary solution that runs on a BeagleBone Enhanced (`succbone.lab`) and *SHOULD* be replaced with a proper PLC/SCADA system and general process control in the future.
|
||||
|
||||
Currently it monitors the state of the Pirani gauge via the BBE's builtin ADC.
|
||||
Features:
|
||||
|
||||
1. Monitors the state of the Pirani gauge via the BBE's builtin ADC.
|
||||
2. Allows enabling/disabling the diffusion/roughing pumps (builtin controller always keeps them enabled).
|
||||
3. Allows for simulating vent/pumpdown button presses.
|
||||
|
||||
Accessing at the lab
|
||||
---
|
||||
|
|
92
succbone/succd/gpio.go
Normal file
92
succbone/succd/gpio.go
Normal file
|
@ -0,0 +1,92 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"sync"
|
||||
|
||||
"k8s.io/klog"
|
||||
)
|
||||
|
||||
// gpio is an abstract GPIO output.
|
||||
type gpio interface {
|
||||
// set returns the GPIO value. The meaning of the logic level is
|
||||
// implementation-dependent.
|
||||
set(state bool) error
|
||||
}
|
||||
|
||||
// bbGPIO implements gpio using BeagleBone's built-in GPIO pins.
|
||||
//
|
||||
// The value of a GPIO is logically non-inverted: false is 0V, true is 3.3V.
|
||||
//
|
||||
// The GPIO can be repeatedly set to the same value without performance penalty
|
||||
// - only level changes are actually written to hardware registers.
|
||||
type bbGPIO struct {
|
||||
path string
|
||||
mu sync.Mutex
|
||||
state bool
|
||||
}
|
||||
|
||||
// newBBGPIO returns a BeagleBone GPIO for a given GPIO number.
|
||||
//
|
||||
// See the following for GPIO pin numbers ('GPIO NO.' column):
|
||||
//
|
||||
// https://vadl.github.io/images/bbb/P8Header.png
|
||||
// https://vadl.github.io/images/bbb/P9Header.png
|
||||
func newBBGPIO(num int, value bool) (*bbGPIO, error) {
|
||||
path := fmt.Sprintf("/sys/class/gpio/gpio%d", num)
|
||||
|
||||
if _, err := os.Stat(path); err != nil {
|
||||
return nil, fmt.Errorf("could not access: %w", err)
|
||||
}
|
||||
|
||||
pathDir := path + "/direction"
|
||||
if err := os.WriteFile(pathDir, []byte("out"), 0); err != nil {
|
||||
return nil, fmt.Errorf("could not set direction: %w", err)
|
||||
}
|
||||
|
||||
pathValue := path + "/value"
|
||||
res := &bbGPIO{
|
||||
path: pathValue,
|
||||
}
|
||||
if err := res.set(value); err != nil {
|
||||
return nil, fmt.Errorf("when setting initial value: %w", err)
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (b *bbGPIO) set(state bool) error {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
if state && !b.state {
|
||||
if err := os.WriteFile(b.path, []byte("1"), 0); err != nil {
|
||||
return fmt.Errorf("could not turn on: %w", err)
|
||||
}
|
||||
} else if !state && b.state {
|
||||
if err := os.WriteFile(b.path, []byte("0"), 0); err != nil {
|
||||
return fmt.Errorf("could not turn off: %w", err)
|
||||
}
|
||||
}
|
||||
b.state = state
|
||||
return nil
|
||||
}
|
||||
|
||||
// fakeGPIO implements a GPIO that logs state changes.
|
||||
type fakeGPIO struct {
|
||||
desc string
|
||||
mu sync.Mutex
|
||||
state bool
|
||||
}
|
||||
|
||||
func (b *fakeGPIO) set(state bool) error {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
if state && !b.state {
|
||||
klog.Infof("%s on", b.desc)
|
||||
} else if !state && b.state {
|
||||
klog.Infof("%s off", b.desc)
|
||||
}
|
||||
b.state = state
|
||||
return nil
|
||||
}
|
|
@ -51,6 +51,8 @@ func (d *daemon) httpIndex(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
|
||||
volts, mbar := d.pirani()
|
||||
rp := d.rpGet()
|
||||
dp := d.dpGet()
|
||||
|
||||
loadB, err := os.ReadFile("/proc/loadavg")
|
||||
load := "unknown"
|
||||
|
@ -67,6 +69,8 @@ func (d *daemon) httpIndex(w http.ResponseWriter, r *http.Request) {
|
|||
templateIndex.Execute(w, map[string]any{
|
||||
"volts": formatVolts(volts),
|
||||
"mbar": formatMbar(mbar),
|
||||
"rp": rp,
|
||||
"dp": dp,
|
||||
"hostname": hostname,
|
||||
"load": load,
|
||||
})
|
||||
|
@ -93,14 +97,20 @@ func (d *daemon) httpStream(w http.ResponseWriter, r *http.Request) {
|
|||
case <-t.C:
|
||||
// TODO(q3k): don't poll, get notified when new ADC readout is available.
|
||||
volts, mbar := d.pirani()
|
||||
rp := d.rpGet()
|
||||
dp := d.dpGet()
|
||||
v := struct {
|
||||
Volts string
|
||||
Mbar string
|
||||
MbarFloat float32
|
||||
RPOn bool
|
||||
DPOn bool
|
||||
}{
|
||||
Volts: formatVolts(volts),
|
||||
Mbar: string(formatMbar(mbar)),
|
||||
MbarFloat: mbar,
|
||||
RPOn: rp,
|
||||
DPOn: dp,
|
||||
}
|
||||
if err := wsjson.Write(ctx, c, v); err != nil {
|
||||
klog.Errorf("Websocket write failed: %v", err)
|
||||
|
@ -119,3 +129,31 @@ func (d *daemon) httpMetrics(w http.ResponseWriter, r *http.Request) {
|
|||
fmt.Fprintf(w, "# TYPE sem_pressure_mbar gauge\n")
|
||||
fmt.Fprintf(w, "sem_pressure_mbar %f\n", mbar)
|
||||
}
|
||||
|
||||
func (d *daemon) httpRoughingPumpEnable(w http.ResponseWriter, r *http.Request) {
|
||||
d.rpSet(true)
|
||||
fmt.Fprintf(w, "succ on\n")
|
||||
}
|
||||
|
||||
func (d *daemon) httpRoughingPumpDisable(w http.ResponseWriter, r *http.Request) {
|
||||
d.rpSet(false)
|
||||
fmt.Fprintf(w, "succ off\n")
|
||||
}
|
||||
|
||||
func (d *daemon) httpDiffusionPumpEnable(w http.ResponseWriter, r *http.Request) {
|
||||
d.dpSet(true)
|
||||
fmt.Fprintf(w, "deep succ on\n")
|
||||
}
|
||||
|
||||
func (d *daemon) httpDiffusionPumpDisable(w http.ResponseWriter, r *http.Request) {
|
||||
d.dpSet(false)
|
||||
fmt.Fprintf(w, "deep succ off\n")
|
||||
}
|
||||
|
||||
func (d *daemon) httpButtonPumpDown(w http.ResponseWriter, r *http.Request) {
|
||||
d.pumpDownPress()
|
||||
}
|
||||
|
||||
func (d *daemon) httpButtonVent(w http.ResponseWriter, r *http.Request) {
|
||||
d.ventPress()
|
||||
}
|
||||
|
|
|
@ -29,12 +29,17 @@ h2 {
|
|||
font-style: italic;
|
||||
font-weight: 100;
|
||||
}
|
||||
button {
|
||||
height: 4.5em;
|
||||
padding-left: 1.5em;
|
||||
padding-right: 1.5em;
|
||||
}
|
||||
</style>
|
||||
|
||||
<h1>succd</h1>
|
||||
<h2>nothing more permanent than a temporary solution</h2>
|
||||
|
||||
<p style="margin-top: 5em;">
|
||||
<p style="margin-top: 2em;">
|
||||
<table>
|
||||
<tr>
|
||||
<th>Voltage</th>
|
||||
|
@ -44,6 +49,25 @@ h2 {
|
|||
<th>Pressure</th>
|
||||
<td id="mbar">{{.mbar}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Roughing Pump</th>
|
||||
<td id="rp">{{ if .rp }}ON{{ else }}OFF{{ end }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Diffusion Pump</th>
|
||||
<td id="dp">{{ if .dp }}ON{{ else }}OFF{{ end }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Evac Control</th>
|
||||
<td>
|
||||
<button id="pd">Pump Down</button>
|
||||
<button id="vent">Vent</button>
|
||||
<button id="rpon">RP On</button>
|
||||
<button id="rpoff">RP Off</button>
|
||||
<button id="dpon">DP On</button>
|
||||
<button id="dpoff">DP Off</button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Status</th>
|
||||
<td id="status">OK</td>
|
||||
|
@ -197,6 +221,12 @@ window.addEventListener("load", (_) => {
|
|||
let volts = document.querySelector("#volts");
|
||||
let mbar = document.querySelector("#mbar");
|
||||
let ping = document.querySelector("#ping");
|
||||
|
||||
// Buttons
|
||||
let pd = document.querySelector("#pd");
|
||||
let vent = document.querySelector("#vent");
|
||||
let rpon = document.querySelector("#rpon");
|
||||
let rpoff = document.querySelector("#rpoff");
|
||||
canvas = document.querySelector("#graph").getContext("2d");
|
||||
|
||||
// TODO(q3k): unhardcode this.
|
||||
|
@ -226,6 +256,18 @@ window.addEventListener("load", (_) => {
|
|||
const data = JSON.parse(event.data);
|
||||
volts.innerHTML = data.Volts;
|
||||
mbar.innerHTML = data.Mbar;
|
||||
rp.innerHTML = data.RPOn ? "ON" : "OFF";
|
||||
if (data.RPOn) {
|
||||
rp.style = "background-color: #60f060";
|
||||
} else {
|
||||
rp.style = "background-color: #f06060";
|
||||
}
|
||||
dp.innerHTML = data.DPOn ? "ON" : "OFF";
|
||||
if (data.DPOn) {
|
||||
dp.style = "background-color: #60f060";
|
||||
} else {
|
||||
dp.style = "background-color: #f06060";
|
||||
}
|
||||
historicalPush(data.MbarFloat);
|
||||
ping.innerHTML = Date.now();
|
||||
});
|
||||
|
@ -243,5 +285,24 @@ window.addEventListener("load", (_) => {
|
|||
});
|
||||
};
|
||||
connect();
|
||||
|
||||
pd.addEventListener("click", async (event) => {
|
||||
await fetch("/button/pumpdown");
|
||||
});
|
||||
vent.addEventListener("click", async (event) => {
|
||||
await fetch("/button/vent");
|
||||
});
|
||||
rpon.addEventListener("click", async (event) => {
|
||||
await fetch("/rp/on");
|
||||
});
|
||||
rpoff.addEventListener("click", async (event) => {
|
||||
await fetch("/rp/off");
|
||||
});
|
||||
dpon.addEventListener("click", async (event) => {
|
||||
await fetch("/dp/on");
|
||||
});
|
||||
dpoff.addEventListener("click", async (event) => {
|
||||
await fetch("/dp/off");
|
||||
});
|
||||
});
|
||||
</script>
|
|
@ -21,11 +21,23 @@ type daemon struct {
|
|||
// 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.
|
||||
|
@ -56,12 +68,24 @@ func (d *daemon) processOnce(_ context.Context) error {
|
|||
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:]
|
||||
}
|
||||
d.mu.Unlock()
|
||||
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
|
||||
}
|
||||
|
@ -84,6 +108,52 @@ func (d *daemon) pirani() (volts float32, mbar float32) {
|
|||
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
|
||||
|
@ -96,21 +166,49 @@ func main() {
|
|||
|
||||
ctx, _ := signal.NotifyContext(context.Background(), os.Interrupt)
|
||||
|
||||
d := daemon{}
|
||||
d := daemon{
|
||||
rpOn: true,
|
||||
}
|
||||
if flagFake {
|
||||
klog.Infof("Starting with fake Pirani probe")
|
||||
klog.Infof("Starting with fake peripherals")
|
||||
d.adcPirani = &fakeADC{}
|
||||
d.gpioRoughingPump = &fakeGPIO{desc: "rp"}
|
||||
d.gpioDiffusionPump = &fakeGPIO{desc: "~dp"}
|
||||
d.gpioBtnPumpDown = &fakeGPIO{desc: "~pd"}
|
||||
d.gpioBtnVent = &fakeGPIO{desc: "~vent"}
|
||||
} else {
|
||||
adc, err := newBBADC(0)
|
||||
if err != nil {
|
||||
klog.Exitf("Failed to setup Pirani ADC: %v", err)
|
||||
}
|
||||
d.adcPirani = adc
|
||||
|
||||
for _, c := range []struct {
|
||||
out *gpio
|
||||
num int
|
||||
}{
|
||||
{&d.gpioRoughingPump, 115},
|
||||
{&d.gpioDiffusionPump, 49},
|
||||
{&d.gpioBtnPumpDown, 48},
|
||||
{&d.gpioBtnVent, 60},
|
||||
} {
|
||||
// Relay, active low.
|
||||
*c.out, err = newBBGPIO(c.num, true)
|
||||
if err != nil {
|
||||
klog.Exitf("Failed to setup GPIO: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
http.HandleFunc("/", d.httpIndex)
|
||||
http.HandleFunc("/stream", d.httpStream)
|
||||
http.HandleFunc("/metrics", d.httpMetrics)
|
||||
http.HandleFunc("/button/vent", d.httpButtonVent)
|
||||
http.HandleFunc("/button/pumpdown", d.httpButtonPumpDown)
|
||||
http.HandleFunc("/rp/on", d.httpRoughingPumpEnable)
|
||||
http.HandleFunc("/rp/off", d.httpRoughingPumpDisable)
|
||||
http.HandleFunc("/dp/on", d.httpDiffusionPumpEnable)
|
||||
http.HandleFunc("/dp/off", d.httpDiffusionPumpDisable)
|
||||
|
||||
klog.Infof("Listening for HTTP at %s", flagListenHTTP)
|
||||
go func() {
|
||||
|
|
Loading…
Reference in a new issue