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 httpServer 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 *httpServer) 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) } // formatMbar formats a millibar value using scientific notation and returns a // HTML fragment (for superscript support). func formatMbar(v float32) template.HTML { exp := 0 for v < 1 { v *= 10 exp -= 1 } for v >= 10 { v /= 10 exp += 1 } res := fmt.Sprintf("%.3f", v) res += fmt.Sprintf(" x 10%d", exp) res += " mbar" return template.HTML(res) } // 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 } // 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 *httpServer) apiData(skipSystem bool) *apiData { state := s.d.snapshot() volts, mbar := state.pirani() 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 = formatMbar(mbar) ad.Pirani.MbarFloat = mbar ad.Pumps.RPOn = state.rpOn ad.Pumps.DPOn = state.dpOn ad.Feedback.RoughReached = rough ad.Feedback.HighReached = high ad.System.Load = load ad.System.Hostname = hostname return &ad } // httpIndex is the / view. func (s *httpServer) 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 *httpServer) 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 *httpServer) 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.pirani() 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 *httpServer) viewRoughingPumpEnable(w http.ResponseWriter, r *http.Request) { s.d.rpSet(true) fmt.Fprintf(w, "succ on\n") } func (s *httpServer) viewRoughingPumpDisable(w http.ResponseWriter, r *http.Request) { s.d.rpSet(false) fmt.Fprintf(w, "succ off\n") } func (s *httpServer) viewDiffusionPumpEnable(w http.ResponseWriter, r *http.Request) { s.d.dpSet(true) fmt.Fprintf(w, "deep succ on\n") } func (s *httpServer) viewDiffusionPumpDisable(w http.ResponseWriter, r *http.Request) { s.d.dpSet(false) fmt.Fprintf(w, "deep succ off\n") } func (s *httpServer) viewButtonPumpDown(w http.ResponseWriter, r *http.Request) { s.d.pumpDownPress() } func (s *httpServer) viewButtonVent(w http.ResponseWriter, r *http.Request) { s.d.ventPress() } func (s *httpServer) 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) }