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 } // Temperature state. Temperatures struct { DPBottom float32 DPTop float32 DPInlet float32 SEM float32 } // Humidity state. Humidity struct { SEM float32 } // 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.Temperatures.DPBottom = state.tempDPBottom ad.Temperatures.DPTop = state.tempDPTop ad.Temperatures.DPInlet = state.tempDPInlet ad.Temperatures.SEM = state.tempSEM ad.Humidity.SEM = state.humiditySEM 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 } } } } // 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() mbar := state.piraniMbar100.mbar 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 (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) }