jeol-t330a/succbone/succd/http.go
Rahix 0aba323779
All checks were successful
/ test (pull_request) Successful in 10s
/ test (push) Successful in 9s
succd: Export all process values as prometheus metrics
For more detailed monitoring, let's export all process values that are
exposed to the web API as prometheus metrics.
2024-10-07 07:42:42 +02:00

262 lines
7.7 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_active Whether pirani gauge failsafe mode is active (boolean)\n")
fmt.Fprintf(w, "# TYPE sem_pirani_failsafe_active gauge\n")
fmt.Fprintf(w, "sem_pirani_failsafe_active %f\n", boolToFloat(state.safety.failsafe))
fmt.Fprintf(w, "# HELP sem_dp_lockout_active Whether diffusion pump lockout is active (boolean)\n")
fmt.Fprintf(w, "# TYPE sem_dp_lockout_active gauge\n")
fmt.Fprintf(w, "sem_dp_lockout_active %f\n", boolToFloat(state.safety.highPressure))
fmt.Fprintf(w, "# HELP sem_pump_diffusion_running Whether the diffusion pump is running (boolean)\n")
fmt.Fprintf(w, "# TYPE sem_pump_diffusion_running gauge\n")
fmt.Fprintf(w, "sem_pump_diffusion_running %f\n", boolToFloat(state.dpOn))
fmt.Fprintf(w, "# HELP sem_pump_roughing_running Whether the roughing pump is running (boolean)\n")
fmt.Fprintf(w, "# TYPE sem_pump_roughing_running gauge\n")
fmt.Fprintf(w, "sem_pump_roughing_running %f\n", boolToFloat(state.rpOn))
rough, high := state.vacuumStatus()
fmt.Fprintf(w, "# HELP sem_vacuum_rough_reached Whether a rough vacuum has been reached (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 a high vacuum has been reached (boolean)\n")
fmt.Fprintf(w, "# TYPE sem_vacuum_high_reached gauge\n")
fmt.Fprintf(w, "sem_vacuum_high_reached %f\n", boolToFloat(high))
}
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)
}