Rahix
0aba323779
For more detailed monitoring, let's export all process values that are exposed to the web API as prometheus metrics.
262 lines
7.7 KiB
Go
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)
|
|
}
|