Rahix
a16dc6b73e
Add counter metrics that count the total operating time for the roughing pump and the diffusion pump.
270 lines
8.1 KiB
Go
270 lines
8.1 KiB
Go
package main
|
|
|
|
import (
|
|
_ "embed"
|
|
"fmt"
|
|
"html/template"
|
|
"net/http"
|
|
_ "net/http/pprof"
|
|
"os"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/coder/websocket"
|
|
"github.com/coder/websocket/wsjson"
|
|
"k8s.io/klog"
|
|
)
|
|
|
|
type webServer struct {
|
|
d daemonController
|
|
}
|
|
|
|
var (
|
|
//go:embed index.html
|
|
templateIndexText string
|
|
templateIndex = template.Must(template.New("index").Parse(templateIndexText))
|
|
//go:embed succd.png
|
|
favicon []byte
|
|
)
|
|
|
|
func (s *webServer) viewFavicon(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "image/png")
|
|
w.Write(favicon)
|
|
}
|
|
|
|
func formatVolts(v float32) string {
|
|
return fmt.Sprintf("%.4f V", v)
|
|
}
|
|
|
|
// 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
|
|
}
|
|
// LoopLoad is a percentage expressing how busy the processing loop is; 100
|
|
// is full utilization; >100 is lag.
|
|
LoopLoad int64
|
|
// 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 *webServer) apiData(skipSystem bool) *apiData {
|
|
state := s.d.snapshot()
|
|
volts := state.piraniVolts100.avg
|
|
mbar := state.piraniMbar100.mbar
|
|
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 = formatMbarHTML(mbar)
|
|
ad.Pirani.MbarFloat = mbar
|
|
ad.Pumps.RPOn = state.rpOn
|
|
ad.Pumps.DPOn = state.dpOn
|
|
ad.Feedback.RoughReached = rough
|
|
ad.Feedback.HighReached = high
|
|
ad.LoopLoad = s.d.loopLoad()
|
|
ad.System.Load = load
|
|
ad.System.Hostname = hostname
|
|
return &ad
|
|
}
|
|
|
|
// httpIndex is the / view.
|
|
func (s *webServer) viewIndex(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Path != "/" {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
|
|
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 (s *webServer) viewStream(w http.ResponseWriter, r *http.Request) {
|
|
c, err := websocket.Accept(w, r, nil)
|
|
if err != nil {
|
|
return
|
|
}
|
|
defer c.CloseNow()
|
|
|
|
t := time.NewTicker(time.Second / 10)
|
|
defer t.Stop()
|
|
|
|
ctx := c.CloseRead(r.Context())
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
c.Close(websocket.StatusNormalClosure, "")
|
|
return
|
|
case <-t.C:
|
|
v := s.apiData(true)
|
|
if err := wsjson.Write(ctx, c, v); err != nil {
|
|
klog.Errorf("Websocket write failed: %v", err)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func boolToFloat(b bool) float32 {
|
|
if b {
|
|
return 1.0
|
|
} else {
|
|
return 0.0
|
|
}
|
|
}
|
|
|
|
// httpMetrics serves minimalistic Prometheus-compatible metrics.
|
|
func (s *webServer) viewMetrics(w http.ResponseWriter, r *http.Request) {
|
|
// TODO(q3k): also serve Go stuff using the actual Prometheus metrics client
|
|
// library.
|
|
// TODO(q3k): serve the rest of the data model
|
|
state := s.d.snapshot()
|
|
|
|
// sem_pressure_mbar is meant to represent the fused pressure value
|
|
// from all data sources once we have more vacuum sensors in the
|
|
// system. sem_pirani_mbar is just the reading from the pirani gauge.
|
|
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", state.piraniMbar100.mbar)
|
|
|
|
fmt.Fprintf(w, "# HELP sem_pirani_mbar Pressure reading by the Pirani gauge, in millibar\n")
|
|
fmt.Fprintf(w, "# TYPE sem_pirani_mbar gauge\n")
|
|
fmt.Fprintf(w, "sem_pirani_mbar %f\n", state.piraniMbar100.mbar)
|
|
|
|
fmt.Fprintf(w, "# HELP sem_pirani_volts Voltage output from the Pirani gauge, in volts\n")
|
|
fmt.Fprintf(w, "# TYPE sem_pirani_volts gauge\n")
|
|
fmt.Fprintf(w, "sem_pirani_volts %f\n", state.piraniVolts100.avg)
|
|
|
|
fmt.Fprintf(w, "# HELP sem_pirani_failsafe Whether pirani gauge failsafe mode is active (boolean)\n")
|
|
fmt.Fprintf(w, "# TYPE sem_pirani_failsafe gauge\n")
|
|
fmt.Fprintf(w, "sem_pirani_failsafe %f\n", boolToFloat(state.safety.failsafe))
|
|
|
|
fmt.Fprintf(w, "# HELP sem_dp_lockout Whether diffusion pump lockout is active (boolean)\n")
|
|
fmt.Fprintf(w, "# TYPE sem_dp_lockout gauge\n")
|
|
fmt.Fprintf(w, "sem_dp_lockout %f\n", boolToFloat(state.safety.highPressure))
|
|
|
|
fmt.Fprintf(w, "# HELP sem_dp_running Whether the diffusion pump is running (boolean)\n")
|
|
fmt.Fprintf(w, "# TYPE sem_dp_running gauge\n")
|
|
fmt.Fprintf(w, "sem_dp_running %f\n", boolToFloat(state.dpOn))
|
|
|
|
fmt.Fprintf(w, "# HELP sem_rp_running Whether the roughing pump is running (boolean)\n")
|
|
fmt.Fprintf(w, "# TYPE sem_rp_running gauge\n")
|
|
fmt.Fprintf(w, "sem_rp_running %f\n", boolToFloat(state.rpOn))
|
|
|
|
rough, high := state.vacuumStatus()
|
|
fmt.Fprintf(w, "# HELP sem_vacuum_rough_reached Whether the rough vacuum threshold has been passed (boolean)\n")
|
|
fmt.Fprintf(w, "# TYPE sem_vacuum_rough_reached gauge\n")
|
|
fmt.Fprintf(w, "sem_vacuum_rough_reached %f\n", boolToFloat(rough))
|
|
|
|
fmt.Fprintf(w, "# HELP sem_vacuum_high_reached Whether the high vacuum threshold has been passed (boolean)\n")
|
|
fmt.Fprintf(w, "# TYPE sem_vacuum_high_reached gauge\n")
|
|
fmt.Fprintf(w, "sem_vacuum_high_reached %f\n", boolToFloat(high))
|
|
|
|
fmt.Fprintf(w, "# HELP sem_rp_operating_seconds_total Operating time of the roughing pump, in seconds\n")
|
|
fmt.Fprintf(w, "# TYPE sem_rp_operating_seconds_total counter\n")
|
|
fmt.Fprintf(w, "sem_rp_operating_seconds_total %f\n", state.rpOperatingTime.Seconds())
|
|
|
|
fmt.Fprintf(w, "# HELP sem_dp_operating_seconds_total Operating time of the diffusion pump, in seconds\n")
|
|
fmt.Fprintf(w, "# TYPE sem_dp_operating_seconds_total counter\n")
|
|
fmt.Fprintf(w, "sem_dp_operating_seconds_total %f\n", state.dpOperatingTime.Seconds())
|
|
}
|
|
|
|
func (s *webServer) viewRoughingPumpEnable(w http.ResponseWriter, r *http.Request) {
|
|
s.d.rpSet(true)
|
|
fmt.Fprintf(w, "succ on\n")
|
|
}
|
|
|
|
func (s *webServer) viewRoughingPumpDisable(w http.ResponseWriter, r *http.Request) {
|
|
s.d.rpSet(false)
|
|
fmt.Fprintf(w, "succ off\n")
|
|
}
|
|
|
|
func (s *webServer) viewDiffusionPumpEnable(w http.ResponseWriter, r *http.Request) {
|
|
s.d.dpSet(true)
|
|
fmt.Fprintf(w, "deep succ on\n")
|
|
}
|
|
|
|
func (s *webServer) viewDiffusionPumpDisable(w http.ResponseWriter, r *http.Request) {
|
|
s.d.dpSet(false)
|
|
fmt.Fprintf(w, "deep succ off\n")
|
|
}
|
|
|
|
func (s *webServer) viewButtonPumpDown(w http.ResponseWriter, r *http.Request) {
|
|
s.d.pumpDownPress()
|
|
}
|
|
|
|
func (s *webServer) viewButtonVent(w http.ResponseWriter, r *http.Request) {
|
|
s.d.ventPress()
|
|
}
|
|
|
|
func (s *webServer) 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)
|
|
}
|