Compare commits

..

6 commits

Author SHA1 Message Date
Serge Bazanski f66afc0c8f succd: restyle slightly 2024-09-28 08:32:07 +02:00
Serge Bazanski 8f7ec7e141 succd: split out http server, daemon state, daemon controller
This improves the structure of the code, separating the data/control
interface out and then implementing the http interface as a user of this
interface.
2024-09-28 08:10:35 +02:00
Serge Bazanski 3ec6fd1d1b succd: unify html/js data source 2024-09-28 07:54:18 +02:00
Serge Bazanski 776f7a9911 succd: add hysteresis for high pressure safety interlock 2024-09-28 07:36:37 +02:00
Serge Bazanski 239a5c40cc succd: factor out safety status to separate struct 2024-09-28 07:36:37 +02:00
Serge Bazanski 80f482b732 succd: tristate pirani safety detection 2024-09-28 07:31:06 +02:00
6 changed files with 347 additions and 253 deletions

View file

@ -15,6 +15,10 @@ import (
"k8s.io/klog" "k8s.io/klog"
) )
type httpServer struct {
d daemonController
}
var ( var (
//go:embed index.html //go:embed index.html
templateIndexText string templateIndexText string
@ -23,7 +27,7 @@ var (
favicon []byte favicon []byte
) )
func (d *daemon) httpFavicon(w http.ResponseWriter, r *http.Request) { func (s *httpServer) viewFavicon(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "image/png") w.Header().Set("Content-Type", "image/png")
w.Write(favicon) w.Write(favicon)
} }
@ -50,45 +54,103 @@ func formatMbar(v float32) template.HTML {
return template.HTML(res) return template.HTML(res)
} }
// apiData is the data model served to the user via HTTP/WebSockets
type apiData struct {
// Safety interlocks.
Safety struct {
// Failsafe mode enabled - pirani gauge seems disconnected.
Failsafe bool
// HighPressure interlock enabled - pressure too high to run diffusion
// pump.
HighPressure bool
}
// Pirani gauge data.
Pirani struct {
// Volts read from the gauge (0-10).
Volts string
// Mbar read from the gauge, formatted as HTML.
Mbar template.HTML
// MbarFloat read from the gauge.
MbarFloat float32
}
// Pump state.
Pumps struct {
// RPOn means the roughing pump is turned on.
RPOn bool
// DPOn means the diffusion pump is turned on.
DPOn bool
}
// Pressure feedback into evacuation board.
Feedback struct {
// RoughReached is true when the system has reached a rough vacuum
// stage.
RoughReached bool
// HighReached is true when the system has reached a high vacuum stage.
HighReached bool
}
// System junk.
System struct {
// Load of the system.
Load string
// Hostname of the system.
Hostname string
}
}
// apiData returns the user data model for the current state of the system. If
// skipSystem is set, the System subset is ignored (saves system load, and is
// not being served via websockets).
func (s *httpServer) apiData(skipSystem bool) *apiData {
state := s.d.snapshot()
volts, mbar := state.pirani()
rough, high := state.vacuumStatus()
var hostname, load string
var err error
if !skipSystem {
hostname, err = os.Hostname()
if err != nil {
hostname = "unknown"
}
loadB, err := os.ReadFile("/proc/loadavg")
load = "unknown"
if err == nil {
parts := strings.Fields(string(loadB))
load = strings.Join(parts[:3], " ")
}
}
ad := apiData{}
ad.Safety.Failsafe = state.safety.failsafe
ad.Safety.HighPressure = state.safety.highPressure
ad.Pirani.Volts = formatVolts(volts)
ad.Pirani.Mbar = formatMbar(mbar)
ad.Pirani.MbarFloat = mbar
ad.Pumps.RPOn = state.rpOn
ad.Pumps.DPOn = state.dpOn
ad.Feedback.RoughReached = rough
ad.Feedback.HighReached = high
ad.System.Load = load
ad.System.Hostname = hostname
return &ad
}
// httpIndex is the / view. // httpIndex is the / view.
func (d *daemon) httpIndex(w http.ResponseWriter, r *http.Request) { func (s *httpServer) viewIndex(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/" { if r.URL.Path != "/" {
http.NotFound(w, r) http.NotFound(w, r)
return return
} }
volts, mbar := d.pirani() data := s.apiData(false)
rp := d.rpGet() //data.Pirani.Mbar = html.St
dp := d.dpGet() templateIndex.Execute(w, data)
failsafe, highpressure := d.safetyStatusGet()
loadB, err := os.ReadFile("/proc/loadavg")
load := "unknown"
if err == nil {
parts := strings.Fields(string(loadB))
load = strings.Join(parts[:3], " ")
}
hostname, err := os.Hostname()
if err != nil {
hostname = "unknown"
}
templateIndex.Execute(w, map[string]any{
"failsafe": failsafe,
"highpressure": highpressure,
"volts": formatVolts(volts),
"mbar": formatMbar(mbar),
"rp": rp,
"dp": dp,
"hostname": hostname,
"load": load,
})
} }
// httpStream is the websocket clientwards data hose, returning a 10Hz update // httpStream is the websocket clientwards data hose, returning a 10Hz update
// stream of pressure/voltage. // stream of pressure/voltage.
func (d *daemon) httpStream(w http.ResponseWriter, r *http.Request) { func (s *httpServer) viewStream(w http.ResponseWriter, r *http.Request) {
c, err := websocket.Accept(w, r, nil) c, err := websocket.Accept(w, r, nil)
if err != nil { if err != nil {
return return
@ -105,33 +167,7 @@ func (d *daemon) httpStream(w http.ResponseWriter, r *http.Request) {
c.Close(websocket.StatusNormalClosure, "") c.Close(websocket.StatusNormalClosure, "")
return return
case <-t.C: case <-t.C:
// TODO(q3k): don't poll, get notified when new ADC readout is available. v := s.apiData(true)
volts, mbar := d.pirani()
rp := d.rpGet()
dp := d.dpGet()
rough, high := d.vacuumStatusGet()
failsafe, highpressure := d.safetyStatusGet()
v := struct {
Failsafe bool
HighPressure bool
Volts string
Mbar string
MbarFloat float32
RPOn bool
DPOn bool
RoughReached bool
HighReached bool
}{
Failsafe: failsafe,
HighPressure: highpressure,
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 { if err := wsjson.Write(ctx, c, v); err != nil {
klog.Errorf("Websocket write failed: %v", err) klog.Errorf("Websocket write failed: %v", err)
return return
@ -141,39 +177,54 @@ func (d *daemon) httpStream(w http.ResponseWriter, r *http.Request) {
} }
// httpMetrics serves minimalistic Prometheus-compatible metrics. // httpMetrics serves minimalistic Prometheus-compatible metrics.
func (d *daemon) httpMetrics(w http.ResponseWriter, r *http.Request) { func (s *httpServer) viewMetrics(w http.ResponseWriter, r *http.Request) {
// TODO(q3k): also serve Go stuff using the actual Prometheus metrics client // TODO(q3k): also serve Go stuff using the actual Prometheus metrics client
// library. // library.
_, mbar := d.pirani() // TODO(q3k): serve the rest of the data model
state := s.d.snapshot()
_, 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)
} }
func (d *daemon) httpRoughingPumpEnable(w http.ResponseWriter, r *http.Request) { func (s *httpServer) viewRoughingPumpEnable(w http.ResponseWriter, r *http.Request) {
d.rpSet(true) s.d.rpSet(true)
fmt.Fprintf(w, "succ on\n") fmt.Fprintf(w, "succ on\n")
} }
func (d *daemon) httpRoughingPumpDisable(w http.ResponseWriter, r *http.Request) { func (s *httpServer) viewRoughingPumpDisable(w http.ResponseWriter, r *http.Request) {
d.rpSet(false) s.d.rpSet(false)
fmt.Fprintf(w, "succ off\n") fmt.Fprintf(w, "succ off\n")
} }
func (d *daemon) httpDiffusionPumpEnable(w http.ResponseWriter, r *http.Request) { func (s *httpServer) viewDiffusionPumpEnable(w http.ResponseWriter, r *http.Request) {
d.dpSet(true) s.d.dpSet(true)
fmt.Fprintf(w, "deep succ on\n") fmt.Fprintf(w, "deep succ on\n")
} }
func (d *daemon) httpDiffusionPumpDisable(w http.ResponseWriter, r *http.Request) { func (s *httpServer) viewDiffusionPumpDisable(w http.ResponseWriter, r *http.Request) {
d.dpSet(false) s.d.dpSet(false)
fmt.Fprintf(w, "deep succ off\n") fmt.Fprintf(w, "deep succ off\n")
} }
func (d *daemon) httpButtonPumpDown(w http.ResponseWriter, r *http.Request) { func (s *httpServer) viewButtonPumpDown(w http.ResponseWriter, r *http.Request) {
d.pumpDownPress() s.d.pumpDownPress()
} }
func (d *daemon) httpButtonVent(w http.ResponseWriter, r *http.Request) { func (s *httpServer) viewButtonVent(w http.ResponseWriter, r *http.Request) {
d.ventPress() s.d.ventPress()
}
func (s *httpServer) setupViews() {
http.HandleFunc("/", s.viewIndex)
http.HandleFunc("/favicon.png", s.viewFavicon)
http.HandleFunc("/stream", s.viewStream)
http.HandleFunc("/metrics", s.viewMetrics)
http.HandleFunc("/button/vent", s.viewButtonVent)
http.HandleFunc("/button/pumpdown", s.viewButtonPumpDown)
http.HandleFunc("/rp/on", s.viewRoughingPumpEnable)
http.HandleFunc("/rp/off", s.viewRoughingPumpDisable)
http.HandleFunc("/dp/on", s.viewDiffusionPumpEnable)
http.HandleFunc("/dp/off", s.viewDiffusionPumpDisable)
} }

View file

@ -10,6 +10,10 @@ body {
} }
table { table {
font-size: 40px; font-size: 40px;
margin-top: 1em;
}
table.status td {
width: 2em;
} }
th, td { th, td {
background-color: #e8e8e8; background-color: #e8e8e8;
@ -58,50 +62,66 @@ td > span {
<p style="margin-top: 2em; clear: both;"> <p style="margin-top: 2em; clear: both;">
<table> <table>
<tr> <tr>
<th rowspan="2">Pirani Gauge</th>
<th>Voltage</th> <th>Voltage</th>
<td id="volts" colspan="4">{{.volts}}</td> <td id="volts">{{ .Pirani.Volts }}</td>
</tr> </tr>
<tr> <tr>
<th>Pressure</th> <th>Pressure</th>
<td id="mbar" colspan="4">{{.mbar}}</td> <td id="mbar">{{ .Pirani.Mbar }}</td>
</tr> </tr>
</table>
<table class="status">
<tr> <tr>
<th>Thresholds</th> <th>Thresholds</th>
<td>Rough:</td> <th>Rough</th>
<td id="trough">...</td> <td id="trough">{{ if .Feedback.RoughReached }}OK{{ else }}NOK{{ end }}</td>
<td>High:</td> <th>High</th>
<td id="thigh">...</td> <td id="thigh">{{ if .Feedback.HighReached }}OK{{ else }}NOK{{ end }}</td>
</tr> </tr>
<tr> <tr>
<th>Pumps</th> <th>Pumps</th>
<td>RP:</td> <th>RP</th>
<td id="rp">{{ if .rp }}ON{{ else }}OFF{{ end }}</td> <td id="rp">{{ if .Pumps.RPOn }}ON{{ else }}OFF{{ end }}</td>
<td>DP:</td> <th>DP</th>
<td id="dp">{{ if .dp }}ON{{ else }}OFF{{ end }}</td> <td id="dp">{{ if .Pumps.DPOn }}ON{{ else }}OFF{{ end }}</td>
</tr> </tr>
<tr> <tr>
<th>Evac Control</th> <th>Safety</th>
<td colspan="4"> <th style="font-size: 0.7em;">Pirani<br />Failsafe</th>
<td id="failsafe">{{ if .Safety.Failsafe }}ON{{ else }}OFF{{ end }}</td>
<th style="font-size: 0.7em;">DP<br />Lockout</th>
<td id="highpressure">{{ if .Safety.HighPressure }}ON{{ else }}OFF{{ end }}</td>
</tr>
</table>
<table>
<tr>
<th rowspan="3">Control</th>
<th>RP</th>
<td>
<button id="rpon">On</button>
<button id="rpoff">Off</button>
</td>
</tr>
<tr>
<th>DP</th>
<td>
<button id="dpon">On</button>
<button id="dpoff">Off</button>
</td>
</tr>
<tr>
<td colspan="2">
<button id="pd">Pump Down</button> <button id="pd">Pump Down</button>
<button id="vent">Vent</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> </td>
</tr> </tr>
<tr> <tr>
<th>Status</th> <th>Status</th>
<td id="status" colspan="4">OK</td> <td id="status" colspan="4">OK</td>
</tr> </tr>
<tr>
<th>Failsafe</th>
<td id="failsafe" colspan="4">OK</td>
</tr>
<tr>
<th>Diffusion Pump Lockout</th>
<td id="highpressure" colspan="4">OK</td>
</tr>
</table> </table>
</p> </p>
<p style="margin-top: 2em;"> <p style="margin-top: 2em;">
@ -109,7 +129,7 @@ td > span {
</p> </p>
<p style="font-style: italic; font-size: 12px; margin-top: 5em;"> <p style="font-style: italic; font-size: 12px; margin-top: 5em;">
{{.hostname}} | load: {{.load}} | <a href="/debug/pprof">pprof</a> | <a href="/metrics">metrics</a> | ws ping: <span id="ping"></span> {{ .System.Hostname }} | load: {{ .System.Load }} | <a href="/debug/pprof">pprof</a> | <a href="/metrics">metrics</a> | ws ping: <span id="ping"></span>
</p> </p>
<script> <script>
@ -288,50 +308,53 @@ window.addEventListener("load", (_) => {
}); });
socket.addEventListener("message", (event) => { socket.addEventListener("message", (event) => {
const data = JSON.parse(event.data); const data = JSON.parse(event.data);
volts.innerHTML = data.Volts; volts.innerHTML = data.Pirani.Volts;
mbar.innerHTML = data.Mbar; mbar.innerHTML = data.Pirani.Mbar;
failsafe.innerHTML = data.Failsafe ? "ON" : "OFF"; if (data.Safety.Failsafe) {
if (data.Failsafe) { failsafe.innerHTML = "ON";
failsafe.style = "background-color: #f06060"; failsafe.style = "background-color: #f06060";
} else { } else {
failsafe.innerHTML = "OFF";
failsafe.style = "background-color: #60f060"; failsafe.style = "background-color: #60f060";
} }
if (data.HighPressure) { if (data.Safety.HighPressure) {
highpressure.innerHTML = "ON"; highpressure.innerHTML = "ON";
highpressure.style = "background-color: #f06060"; highpressure.style = "background-color: #f06060";
} else { } else {
highpressure.innerHTML = "OFF"; highpressure.innerHTML = "OFF";
highpressure.style = "background-color: #60f060"; highpressure.style = "background-color: #60f060";
} }
rp.innerHTML = data.RPOn ? "ON" : "OFF"; if (data.Pumps.RPOn) {
if (data.RPOn) { rp.innerHTML = "ON";
rp.style = "background-color: #60f060"; rp.style = "background-color: #60f060";
} else { } else {
rp.innerHTML = "OFF";
rp.style = "background-color: #f06060"; rp.style = "background-color: #f06060";
} }
dp.innerHTML = data.DPOn ? "ON" : "OFF"; if (data.Pumps.DPOn) {
if (data.DPOn) { dp.innerHTML = "ON";
dp.style = "background-color: #60f060"; dp.style = "background-color: #60f060";
} else { } else {
dp.innerHTML = "OFF";
dp.style = "background-color: #f06060"; dp.style = "background-color: #f06060";
} }
let t = []; let t = [];
if (data.RoughReached) { if (data.Feedback.RoughReached) {
trough.innerHTML = "OK"; trough.innerHTML = "OK";
trough.style = "background-color: #60f060"; trough.style = "background-color: #60f060";
} else { } else {
trough.innerHTML = "NOK"; trough.innerHTML = "NOK";
trough.style = "background-color: #f06060"; trough.style = "background-color: #f06060";
} }
if (data.HighReached) { if (data.Feedback.HighReached) {
thigh.innerHTML = "OK"; thigh.innerHTML = "OK";
thigh.style = "background-color: #60f060"; thigh.style = "background-color: #60f060";
} else { } else {
thigh.innerHTML = "NOK"; thigh.innerHTML = "NOK";
thigh.style = "background-color: #f06060"; thigh.style = "background-color: #f06060";
} }
historicalPush(data.MbarFloat); historicalPush(data.Pirani.MbarFloat);
ping.innerHTML = Date.now(); ping.innerHTML = Date.now();
}); });
socket.addEventListener("close", (event) => { socket.addEventListener("close", (event) => {

View file

@ -26,9 +26,9 @@ func main() {
ctx, _ := signal.NotifyContext(context.Background(), os.Interrupt) ctx, _ := signal.NotifyContext(context.Background(), os.Interrupt)
d := daemon{ d := daemon{}
rpOn: true, d.daemonState.rpOn = true
}
d.aboveRough.threshold = float64(flagPressureThresholdRough) d.aboveRough.threshold = float64(flagPressureThresholdRough)
d.aboveHigh.threshold = float64(flagPressureThresholdHigh) d.aboveHigh.threshold = float64(flagPressureThresholdHigh)
if flagFake { if flagFake {
@ -65,16 +65,10 @@ func main() {
} }
} }
http.HandleFunc("/", d.httpIndex) httpServer := httpServer{
http.HandleFunc("/favicon.png", d.httpFavicon) d: &d,
http.HandleFunc("/stream", d.httpStream) }
http.HandleFunc("/metrics", d.httpMetrics) httpServer.setupViews()
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) klog.Infof("Listening for HTTP at %s", flagListenHTTP)
go func() { go func() {

View file

@ -4,22 +4,18 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"math"
"sync" "sync"
"time" "time"
"k8s.io/klog" "k8s.io/klog"
) )
// daemon is the main state of the succdaemon. // daemon is the main service of the succdaemon.
type daemon struct { type daemon struct {
// adcPirani is the adc implementation returning the voltage of the Pfeiffer // adcPirani is the adc implementation returning the voltage of the Pfeiffer
// Pirani gauge. // Pirani gauge.
adcPirani adc adcPirani adc
failsafe bool
highPressure bool
gpioDiffusionPump gpio gpioDiffusionPump gpio
gpioRoughingPump gpio gpioRoughingPump gpio
gpioBtnPumpDown gpio gpioBtnPumpDown gpio
@ -27,18 +23,9 @@ type daemon struct {
gpioBelowRough gpio gpioBelowRough gpio
gpioBelowHigh gpio gpioBelowHigh gpio
// mu guards state variables below. // mu guards the state below.
mu sync.RWMutex mu sync.RWMutex
// adcPiraniVolts is a moving window of read ADC values, used to calculate a daemonState
// 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. // momentaryOutput is an output that can be triggered for 500ms.
@ -119,44 +106,47 @@ func (d *daemon) processOnce(_ context.Context) error {
d.pumpdown.process() d.pumpdown.process()
d.vent.process() d.vent.process()
_, mbar := d.piraniUnlocked() _, mbar := d.daemonState.pirani()
d.aboveRough.process(float64(mbar)) d.aboveRough.process(float64(mbar))
d.aboveHigh.process(float64(mbar)) d.aboveHigh.process(float64(mbar))
if d.piraniWireBreakDetection() { // Check if the pirani gauge is disconnected. Note: this will assume the
// pirani gauge is connected for the first couple of processing runs as
// samples are still being captured.
if d.piraniDetection() == piraniDetectionDisconnected {
// Unrealistic result, Pirani probe probably disconnected. Failsafe mode. // Unrealistic result, Pirani probe probably disconnected. Failsafe mode.
if !d.failsafe { if !d.safety.failsafe {
d.failsafe = true d.safety.failsafe = true
klog.Errorf("Pirani probe seems disconnected; enabling failsafe mode") klog.Errorf("Pirani probe seems disconnected; enabling failsafe mode")
} }
} else { } else {
if d.failsafe { if d.safety.failsafe {
if mbar >= 1e2 { if mbar >= 1e2 {
d.failsafe = false d.safety.failsafe = false
klog.Infof("Values are plausible again; quitting failsafe mode") klog.Infof("Values are plausible again; quitting failsafe mode")
} }
} }
} }
if mbar >= 1e-1 { if mbar >= 1e-1 {
if !d.highPressure { if !d.safety.highPressure {
d.highPressure = true d.safety.highPressure = true
klog.Errorf("Pressure is too high; enabling diffusion pump lockout") klog.Errorf("Pressure is too high; enabling diffusion pump lockout")
} }
} else { } else if mbar < (1e-1)-(1e-2) {
if d.highPressure { if d.safety.highPressure {
d.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 for diffusion pump operation; quitting diffusion pump lockout")
} }
} }
if d.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.highPressure { if d.safety.highPressure {
d.dpOn = false d.dpOn = false
} }
@ -190,100 +180,3 @@ func (d *daemon) processOnce(_ context.Context) error {
return nil return nil
} }
func (d *daemon) piraniWireBreakDetection() bool {
if len(d.adcPiraniVolts) < 3 {
return true
}
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)
return mbar < 4e-6
}
// 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
}
func (d *daemon) safetyStatusGet() (failsafe, highPressure bool) {
d.mu.RLock()
defer d.mu.RUnlock()
failsafe = d.failsafe
highPressure = d.highPressure
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()
}

View file

@ -0,0 +1,49 @@
package main
// daemonController is the control/data interface passed on to external system
// controllers, eg. the web interface.
//
// This is a subset of daemon functions limited to a safe, explicit subset.
type daemonController interface {
// snapshot returns an internally consistent copy of the daemon state.
snapshot() *daemonState
// rpSet enables/disables the roughing pump.
rpSet(bool)
// dpSet enables/disables the diffusion pump.
dpSet(bool)
// pumpDownPress simulates a pumpdown button press.
pumpDownPress()
// ventPRess simulates a vent button press.
ventPress()
}
func (d *daemon) snapshot() *daemonState {
d.mu.RLock()
ds := d.daemonState
d.mu.RUnlock()
return &ds
}
func (d *daemon) rpSet(state bool) {
d.mu.Lock()
defer d.mu.Unlock()
d.rpOn = state
}
func (d *daemon) dpSet(state bool) {
d.mu.Lock()
defer d.mu.Unlock()
d.dpOn = state
}
func (d *daemon) pumpDownPress() {
d.mu.Lock()
defer d.mu.Unlock()
d.pumpdown.trigger()
}
func (d *daemon) ventPress() {
d.mu.Lock()
defer d.mu.Unlock()
d.vent.trigger()
}

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