2024-09-12 01:02:57 +00:00
|
|
|
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"
|
|
|
|
)
|
|
|
|
|
2024-09-28 07:19:14 +00:00
|
|
|
type webServer struct {
|
2024-09-28 06:10:33 +00:00
|
|
|
d daemonController
|
|
|
|
}
|
|
|
|
|
2024-09-12 01:02:57 +00:00
|
|
|
var (
|
|
|
|
//go:embed index.html
|
|
|
|
templateIndexText string
|
|
|
|
templateIndex = template.Must(template.New("index").Parse(templateIndexText))
|
2024-09-27 00:10:39 +00:00
|
|
|
//go:embed succd.png
|
|
|
|
favicon []byte
|
2024-09-12 01:02:57 +00:00
|
|
|
)
|
|
|
|
|
2024-09-28 07:19:14 +00:00
|
|
|
func (s *webServer) viewFavicon(w http.ResponseWriter, r *http.Request) {
|
2024-09-27 00:10:39 +00:00
|
|
|
w.Header().Set("Content-Type", "image/png")
|
|
|
|
w.Write(favicon)
|
|
|
|
}
|
|
|
|
|
2024-09-12 01:02:57 +00:00
|
|
|
func formatVolts(v float32) string {
|
2024-09-12 01:18:31 +00:00
|
|
|
return fmt.Sprintf("%.4f V", v)
|
2024-09-12 01:02:57 +00:00
|
|
|
}
|
|
|
|
|
2024-09-28 05:49:03 +00:00
|
|
|
// 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
|
2024-09-12 01:02:57 +00:00
|
|
|
}
|
2024-09-28 05:49:03 +00:00
|
|
|
// 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
|
|
|
|
}
|
2024-09-28 12:26:24 +00:00
|
|
|
// LoopLoad is a percentage expressing how busy the processing loop is; 100
|
|
|
|
// is full utilization; >100 is lag.
|
|
|
|
LoopLoad int64
|
2024-09-28 05:49:03 +00:00
|
|
|
// System junk.
|
|
|
|
System struct {
|
|
|
|
// Load of the system.
|
|
|
|
Load string
|
|
|
|
// Hostname of the system.
|
|
|
|
Hostname string
|
|
|
|
}
|
|
|
|
}
|
2024-09-12 01:02:57 +00:00
|
|
|
|
2024-09-28 05:49:03 +00:00
|
|
|
// 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).
|
2024-09-28 07:19:14 +00:00
|
|
|
func (s *webServer) apiData(skipSystem bool) *apiData {
|
2024-09-28 06:10:33 +00:00
|
|
|
state := s.d.snapshot()
|
2024-09-28 08:17:05 +00:00
|
|
|
volts := state.piraniVolts100.avg
|
|
|
|
mbar := state.piraniMbar100.mbar
|
2024-09-28 06:10:33 +00:00
|
|
|
rough, high := state.vacuumStatus()
|
2024-09-12 01:02:57 +00:00
|
|
|
|
2024-09-28 05:49:03 +00:00
|
|
|
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], " ")
|
|
|
|
}
|
2024-09-12 01:02:57 +00:00
|
|
|
}
|
|
|
|
|
2024-09-28 05:49:03 +00:00
|
|
|
ad := apiData{}
|
2024-09-28 06:10:33 +00:00
|
|
|
ad.Safety.Failsafe = state.safety.failsafe
|
|
|
|
ad.Safety.HighPressure = state.safety.highPressure
|
2024-09-28 05:49:03 +00:00
|
|
|
ad.Pirani.Volts = formatVolts(volts)
|
2024-09-28 07:46:18 +00:00
|
|
|
ad.Pirani.Mbar = formatMbarHTML(mbar)
|
2024-09-28 05:49:03 +00:00
|
|
|
ad.Pirani.MbarFloat = mbar
|
2024-09-28 06:10:33 +00:00
|
|
|
ad.Pumps.RPOn = state.rpOn
|
|
|
|
ad.Pumps.DPOn = state.dpOn
|
2024-09-28 05:49:03 +00:00
|
|
|
ad.Feedback.RoughReached = rough
|
|
|
|
ad.Feedback.HighReached = high
|
2024-09-28 12:26:24 +00:00
|
|
|
ad.LoopLoad = s.d.loopLoad()
|
2024-09-28 05:49:03 +00:00
|
|
|
ad.System.Load = load
|
|
|
|
ad.System.Hostname = hostname
|
|
|
|
return &ad
|
|
|
|
}
|
|
|
|
|
|
|
|
// httpIndex is the / view.
|
2024-09-28 07:19:14 +00:00
|
|
|
func (s *webServer) viewIndex(w http.ResponseWriter, r *http.Request) {
|
2024-09-28 05:49:03 +00:00
|
|
|
if r.URL.Path != "/" {
|
|
|
|
http.NotFound(w, r)
|
|
|
|
return
|
2024-09-12 01:02:57 +00:00
|
|
|
}
|
|
|
|
|
2024-09-28 06:10:33 +00:00
|
|
|
data := s.apiData(false)
|
2024-09-28 05:49:03 +00:00
|
|
|
//data.Pirani.Mbar = html.St
|
|
|
|
templateIndex.Execute(w, data)
|
2024-09-12 01:02:57 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// httpStream is the websocket clientwards data hose, returning a 10Hz update
|
|
|
|
// stream of pressure/voltage.
|
2024-09-28 07:19:14 +00:00
|
|
|
func (s *webServer) viewStream(w http.ResponseWriter, r *http.Request) {
|
2024-09-12 01:02:57 +00:00
|
|
|
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:
|
2024-09-28 06:10:33 +00:00
|
|
|
v := s.apiData(true)
|
2024-09-12 01:02:57 +00:00
|
|
|
if err := wsjson.Write(ctx, c, v); err != nil {
|
|
|
|
klog.Errorf("Websocket write failed: %v", err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// httpMetrics serves minimalistic Prometheus-compatible metrics.
|
2024-09-28 07:19:14 +00:00
|
|
|
func (s *webServer) viewMetrics(w http.ResponseWriter, r *http.Request) {
|
2024-09-12 01:02:57 +00:00
|
|
|
// TODO(q3k): also serve Go stuff using the actual Prometheus metrics client
|
|
|
|
// library.
|
2024-09-28 06:10:33 +00:00
|
|
|
// TODO(q3k): serve the rest of the data model
|
|
|
|
state := s.d.snapshot()
|
2024-09-28 08:17:05 +00:00
|
|
|
mbar := state.piraniMbar100.mbar
|
2024-10-26 17:42:35 +00:00
|
|
|
rpOn := state.rpOn
|
|
|
|
dpOn := state.dpOn
|
|
|
|
pumpDownStatus := s.d.gpioPumpDownStatus()
|
|
|
|
ventStatus := s.d.gpioVentStatus()
|
|
|
|
belowRoughStatus := s.d.gpioBelowRoughStatus()
|
|
|
|
belowHighStatus := s.d.gpioBelowHighStatus()
|
|
|
|
|
2024-09-12 01:02:57 +00:00
|
|
|
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)
|
2024-10-26 17:42:35 +00:00
|
|
|
fmt.Fprintf(w, "# TYPE sem_rp_on gauge\n")
|
|
|
|
fmt.Fprintf(w, "sem_rp_on %t\n", rpOn)
|
|
|
|
fmt.Fprintf(w, "# TYPE sem_dp_on gauge\n")
|
|
|
|
fmt.Fprintf(w, "sem_dp_on %t\n", dpOn)
|
|
|
|
fmt.Fprintf(w, "# TYPE sem_gpio_pump_down_status gauge\n")
|
|
|
|
fmt.Fprintf(w, "sem_gpio_pump_down_status %t\n", pumpDownStatus)
|
|
|
|
fmt.Fprintf(w, "# TYPE sem_gpio_vent_status gauge\n")
|
|
|
|
fmt.Fprintf(w, "sem_gpio_vent_status %t\n", ventStatus)
|
|
|
|
fmt.Fprintf(w, "# TYPE sem_gpio_below_rough_status gauge\n")
|
|
|
|
fmt.Fprintf(w, "sem_gpio_below_rough_status %t\n", belowRoughStatus)
|
|
|
|
fmt.Fprintf(w, "# TYPE sem_gpio_below_high_status gauge\n")
|
|
|
|
fmt.Fprintf(w, "sem_gpio_below_high_status %t\n", belowHighStatus)
|
2024-09-12 01:02:57 +00:00
|
|
|
}
|
2024-09-25 21:28:57 +00:00
|
|
|
|
2024-09-28 07:19:14 +00:00
|
|
|
func (s *webServer) viewRoughingPumpEnable(w http.ResponseWriter, r *http.Request) {
|
2024-09-28 06:10:33 +00:00
|
|
|
s.d.rpSet(true)
|
2024-09-25 21:28:57 +00:00
|
|
|
fmt.Fprintf(w, "succ on\n")
|
|
|
|
}
|
|
|
|
|
2024-09-28 07:19:14 +00:00
|
|
|
func (s *webServer) viewRoughingPumpDisable(w http.ResponseWriter, r *http.Request) {
|
2024-09-28 06:10:33 +00:00
|
|
|
s.d.rpSet(false)
|
2024-09-25 21:28:57 +00:00
|
|
|
fmt.Fprintf(w, "succ off\n")
|
|
|
|
}
|
|
|
|
|
2024-09-28 07:19:14 +00:00
|
|
|
func (s *webServer) viewDiffusionPumpEnable(w http.ResponseWriter, r *http.Request) {
|
2024-09-28 06:10:33 +00:00
|
|
|
s.d.dpSet(true)
|
2024-09-25 21:28:57 +00:00
|
|
|
fmt.Fprintf(w, "deep succ on\n")
|
|
|
|
}
|
|
|
|
|
2024-09-28 07:19:14 +00:00
|
|
|
func (s *webServer) viewDiffusionPumpDisable(w http.ResponseWriter, r *http.Request) {
|
2024-09-28 06:10:33 +00:00
|
|
|
s.d.dpSet(false)
|
2024-09-25 21:28:57 +00:00
|
|
|
fmt.Fprintf(w, "deep succ off\n")
|
|
|
|
}
|
|
|
|
|
2024-09-28 07:19:14 +00:00
|
|
|
func (s *webServer) viewButtonPumpDown(w http.ResponseWriter, r *http.Request) {
|
2024-09-28 06:10:33 +00:00
|
|
|
s.d.pumpDownPress()
|
|
|
|
}
|
|
|
|
|
2024-09-28 07:19:14 +00:00
|
|
|
func (s *webServer) viewButtonVent(w http.ResponseWriter, r *http.Request) {
|
2024-09-28 06:10:33 +00:00
|
|
|
s.d.ventPress()
|
2024-09-25 21:28:57 +00:00
|
|
|
}
|
|
|
|
|
2024-09-28 07:19:14 +00:00
|
|
|
func (s *webServer) setupViews() {
|
2024-09-28 06:10:33 +00:00
|
|
|
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)
|
2024-09-25 21:28:57 +00:00
|
|
|
}
|