succd: implement rp/dp/vent/pumpdown control

This commit is contained in:
Serge Bazanski 2024-09-25 23:28:57 +02:00
parent d01263fead
commit 070f45b1bc
5 changed files with 298 additions and 5 deletions

View file

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

View file

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

View file

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

View file

@ -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() {