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.
This commit is contained in:
parent
3ec6fd1d1b
commit
8f7ec7e141
|
@ -15,6 +15,10 @@ import (
|
|||
"k8s.io/klog"
|
||||
)
|
||||
|
||||
type httpServer struct {
|
||||
d daemonController
|
||||
}
|
||||
|
||||
var (
|
||||
//go:embed index.html
|
||||
templateIndexText string
|
||||
|
@ -23,7 +27,7 @@ var (
|
|||
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.Write(favicon)
|
||||
}
|
||||
|
@ -96,12 +100,10 @@ type apiData struct {
|
|||
// 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 (d *daemon) apiData(skipSystem bool) *apiData {
|
||||
volts, mbar := d.pirani()
|
||||
rp := d.rpGet()
|
||||
dp := d.dpGet()
|
||||
rough, high := d.vacuumStatusGet()
|
||||
safety := d.safetyStatusGet()
|
||||
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
|
||||
|
@ -120,13 +122,13 @@ func (d *daemon) apiData(skipSystem bool) *apiData {
|
|||
}
|
||||
|
||||
ad := apiData{}
|
||||
ad.Safety.Failsafe = safety.failsafe
|
||||
ad.Safety.HighPressure = safety.highPressure
|
||||
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 = rp
|
||||
ad.Pumps.DPOn = dp
|
||||
ad.Pumps.RPOn = state.rpOn
|
||||
ad.Pumps.DPOn = state.dpOn
|
||||
ad.Feedback.RoughReached = rough
|
||||
ad.Feedback.HighReached = high
|
||||
ad.System.Load = load
|
||||
|
@ -135,20 +137,20 @@ func (d *daemon) apiData(skipSystem bool) *apiData {
|
|||
}
|
||||
|
||||
// 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 != "/" {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
data := d.apiData(false)
|
||||
data := s.apiData(false)
|
||||
//data.Pirani.Mbar = html.St
|
||||
templateIndex.Execute(w, data)
|
||||
}
|
||||
|
||||
// httpStream is the websocket clientwards data hose, returning a 10Hz update
|
||||
// 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)
|
||||
if err != nil {
|
||||
return
|
||||
|
@ -165,7 +167,7 @@ func (d *daemon) httpStream(w http.ResponseWriter, r *http.Request) {
|
|||
c.Close(websocket.StatusNormalClosure, "")
|
||||
return
|
||||
case <-t.C:
|
||||
v := d.apiData(true)
|
||||
v := s.apiData(true)
|
||||
if err := wsjson.Write(ctx, c, v); err != nil {
|
||||
klog.Errorf("Websocket write failed: %v", err)
|
||||
return
|
||||
|
@ -175,39 +177,54 @@ func (d *daemon) httpStream(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
|
||||
// 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
|
||||
// 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, "# 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)
|
||||
func (s *httpServer) viewRoughingPumpEnable(w http.ResponseWriter, r *http.Request) {
|
||||
s.d.rpSet(true)
|
||||
fmt.Fprintf(w, "succ on\n")
|
||||
}
|
||||
|
||||
func (d *daemon) httpRoughingPumpDisable(w http.ResponseWriter, r *http.Request) {
|
||||
d.rpSet(false)
|
||||
func (s *httpServer) viewRoughingPumpDisable(w http.ResponseWriter, r *http.Request) {
|
||||
s.d.rpSet(false)
|
||||
fmt.Fprintf(w, "succ off\n")
|
||||
}
|
||||
|
||||
func (d *daemon) httpDiffusionPumpEnable(w http.ResponseWriter, r *http.Request) {
|
||||
d.dpSet(true)
|
||||
func (s *httpServer) viewDiffusionPumpEnable(w http.ResponseWriter, r *http.Request) {
|
||||
s.d.dpSet(true)
|
||||
fmt.Fprintf(w, "deep succ on\n")
|
||||
}
|
||||
|
||||
func (d *daemon) httpDiffusionPumpDisable(w http.ResponseWriter, r *http.Request) {
|
||||
d.dpSet(false)
|
||||
func (s *httpServer) viewDiffusionPumpDisable(w http.ResponseWriter, r *http.Request) {
|
||||
s.d.dpSet(false)
|
||||
fmt.Fprintf(w, "deep succ off\n")
|
||||
}
|
||||
|
||||
func (d *daemon) httpButtonPumpDown(w http.ResponseWriter, r *http.Request) {
|
||||
d.pumpDownPress()
|
||||
func (s *httpServer) viewButtonPumpDown(w http.ResponseWriter, r *http.Request) {
|
||||
s.d.pumpDownPress()
|
||||
}
|
||||
|
||||
func (d *daemon) httpButtonVent(w http.ResponseWriter, r *http.Request) {
|
||||
d.ventPress()
|
||||
func (s *httpServer) viewButtonVent(w http.ResponseWriter, r *http.Request) {
|
||||
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)
|
||||
}
|
||||
|
|
|
@ -26,9 +26,9 @@ func main() {
|
|||
|
||||
ctx, _ := signal.NotifyContext(context.Background(), os.Interrupt)
|
||||
|
||||
d := daemon{
|
||||
rpOn: true,
|
||||
}
|
||||
d := daemon{}
|
||||
d.daemonState.rpOn = true
|
||||
|
||||
d.aboveRough.threshold = float64(flagPressureThresholdRough)
|
||||
d.aboveHigh.threshold = float64(flagPressureThresholdHigh)
|
||||
if flagFake {
|
||||
|
@ -65,16 +65,10 @@ func main() {
|
|||
}
|
||||
}
|
||||
|
||||
http.HandleFunc("/", d.httpIndex)
|
||||
http.HandleFunc("/favicon.png", d.httpFavicon)
|
||||
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)
|
||||
httpServer := httpServer{
|
||||
d: &d,
|
||||
}
|
||||
httpServer.setupViews()
|
||||
|
||||
klog.Infof("Listening for HTTP at %s", flagListenHTTP)
|
||||
go func() {
|
||||
|
|
|
@ -4,21 +4,18 @@ import (
|
|||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"k8s.io/klog"
|
||||
)
|
||||
|
||||
// daemon is the main state of the succdaemon.
|
||||
// daemon is the main service of the succdaemon.
|
||||
type daemon struct {
|
||||
// adcPirani is the adc implementation returning the voltage of the Pfeiffer
|
||||
// Pirani gauge.
|
||||
adcPirani adc
|
||||
|
||||
safety safetyStatus
|
||||
|
||||
gpioDiffusionPump gpio
|
||||
gpioRoughingPump gpio
|
||||
gpioBtnPumpDown gpio
|
||||
|
@ -26,27 +23,9 @@ type daemon struct {
|
|||
gpioBelowRough gpio
|
||||
gpioBelowHigh gpio
|
||||
|
||||
// mu guards state variables below.
|
||||
// mu guards the state 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
|
||||
}
|
||||
|
||||
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
|
||||
daemonState
|
||||
}
|
||||
|
||||
// momentaryOutput is an output that can be triggered for 500ms.
|
||||
|
@ -127,7 +106,7 @@ func (d *daemon) processOnce(_ context.Context) error {
|
|||
d.pumpdown.process()
|
||||
d.vent.process()
|
||||
|
||||
_, mbar := d.piraniUnlocked()
|
||||
_, mbar := d.daemonState.pirani()
|
||||
d.aboveRough.process(float64(mbar))
|
||||
d.aboveHigh.process(float64(mbar))
|
||||
|
||||
|
@ -201,116 +180,3 @@ func (d *daemon) processOnce(_ context.Context) error {
|
|||
|
||||
return nil
|
||||
}
|
||||
|
||||
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 *daemon) 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
|
||||
}
|
||||
|
||||
// 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() safetyStatus {
|
||||
d.mu.RLock()
|
||||
defer d.mu.RUnlock()
|
||||
return d.safety
|
||||
}
|
||||
|
||||
// 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()
|
||||
}
|
||||
|
|
49
succbone/succd/process_controller.go
Normal file
49
succbone/succd/process_controller.go
Normal 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()
|
||||
}
|
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
|
||||
}
|
Loading…
Reference in a new issue