diff --git a/succbone/succd/http.go b/succbone/succd/http.go index 43bee92..3d38823 100644 --- a/succbone/succd/http.go +++ b/succbone/succd/http.go @@ -15,6 +15,10 @@ import ( "k8s.io/klog" ) +type httpServer struct { + d daemonController +} + var ( //go:embed index.html templateIndexText string @@ -23,7 +27,7 @@ var ( favicon []byte ) -func (d *daemon) httpFavicon(w http.ResponseWriter, r *http.Request) { +func (s *httpServer) viewFavicon(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "image/png") w.Write(favicon) } @@ -50,45 +54,103 @@ func formatMbar(v float32) template.HTML { 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 (d *daemon) httpIndex(w http.ResponseWriter, r *http.Request) { +func (s *httpServer) viewIndex(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/" { http.NotFound(w, r) return } - volts, mbar := d.pirani() - rp := d.rpGet() - dp := d.dpGet() - failsafe, highpressure := d.safetyStatusGet() - - loadB, err := os.ReadFile("/proc/loadavg") - load := "unknown" - if err == nil { - parts := strings.Fields(string(loadB)) - load = strings.Join(parts[:3], " ") - } - - hostname, err := os.Hostname() - if err != nil { - hostname = "unknown" - } - - templateIndex.Execute(w, map[string]any{ - "failsafe": failsafe, - "highpressure": highpressure, - "volts": formatVolts(volts), - "mbar": formatMbar(mbar), - "rp": rp, - "dp": dp, - "hostname": hostname, - "load": load, - }) + 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 (d *daemon) httpStream(w http.ResponseWriter, r *http.Request) { +func (s *httpServer) viewStream(w http.ResponseWriter, r *http.Request) { c, err := websocket.Accept(w, r, nil) if err != nil { return @@ -105,33 +167,7 @@ func (d *daemon) httpStream(w http.ResponseWriter, r *http.Request) { c.Close(websocket.StatusNormalClosure, "") return case <-t.C: - // TODO(q3k): don't poll, get notified when new ADC readout is available. - volts, mbar := d.pirani() - rp := d.rpGet() - dp := d.dpGet() - rough, high := d.vacuumStatusGet() - failsafe, highpressure := d.safetyStatusGet() - v := struct { - Failsafe bool - HighPressure bool - Volts string - Mbar string - MbarFloat float32 - RPOn bool - DPOn bool - RoughReached bool - HighReached bool - }{ - Failsafe: failsafe, - HighPressure: highpressure, - Volts: formatVolts(volts), - Mbar: string(formatMbar(mbar)), - MbarFloat: mbar, - RPOn: rp, - DPOn: dp, - RoughReached: rough, - HighReached: high, - } + v := s.apiData(true) if err := wsjson.Write(ctx, c, v); err != nil { klog.Errorf("Websocket write failed: %v", err) return @@ -141,39 +177,54 @@ func (d *daemon) httpStream(w http.ResponseWriter, r *http.Request) { } // httpMetrics serves minimalistic Prometheus-compatible metrics. -func (d *daemon) httpMetrics(w http.ResponseWriter, r *http.Request) { +func (s *httpServer) viewMetrics(w http.ResponseWriter, r *http.Request) { // TODO(q3k): also serve Go stuff using the actual Prometheus metrics client // library. - _, mbar := d.pirani() + // 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 (d *daemon) httpRoughingPumpEnable(w http.ResponseWriter, r *http.Request) { - d.rpSet(true) +func (s *httpServer) viewRoughingPumpEnable(w http.ResponseWriter, r *http.Request) { + s.d.rpSet(true) fmt.Fprintf(w, "succ on\n") } -func (d *daemon) httpRoughingPumpDisable(w http.ResponseWriter, r *http.Request) { - d.rpSet(false) +func (s *httpServer) viewRoughingPumpDisable(w http.ResponseWriter, r *http.Request) { + s.d.rpSet(false) fmt.Fprintf(w, "succ off\n") } -func (d *daemon) httpDiffusionPumpEnable(w http.ResponseWriter, r *http.Request) { - d.dpSet(true) +func (s *httpServer) viewDiffusionPumpEnable(w http.ResponseWriter, r *http.Request) { + s.d.dpSet(true) fmt.Fprintf(w, "deep succ on\n") } -func (d *daemon) httpDiffusionPumpDisable(w http.ResponseWriter, r *http.Request) { - d.dpSet(false) +func (s *httpServer) viewDiffusionPumpDisable(w http.ResponseWriter, r *http.Request) { + s.d.dpSet(false) fmt.Fprintf(w, "deep succ off\n") } -func (d *daemon) httpButtonPumpDown(w http.ResponseWriter, r *http.Request) { - d.pumpDownPress() +func (s *httpServer) viewButtonPumpDown(w http.ResponseWriter, r *http.Request) { + s.d.pumpDownPress() } -func (d *daemon) httpButtonVent(w http.ResponseWriter, r *http.Request) { - d.ventPress() +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) } diff --git a/succbone/succd/index.html b/succbone/succd/index.html index 4d758be..08acab3 100644 --- a/succbone/succd/index.html +++ b/succbone/succd/index.html @@ -10,6 +10,10 @@ body { } table { font-size: 40px; + margin-top: 1em; +} +table.status td { + width: 2em; } th, td { background-color: #e8e8e8; @@ -58,50 +62,66 @@ td > span {
Pirani Gauge | Voltage | -{{.volts}} | +{{ .Pirani.Volts }} | |||
---|---|---|---|---|---|---|
Pressure | -{{.mbar}} | +{{ .Pirani.Mbar }} |
Thresholds | -Rough: | -... | -High: | -... | +Rough | +{{ if .Feedback.RoughReached }}OK{{ else }}NOK{{ end }} | +High | +{{ if .Feedback.HighReached }}OK{{ else }}NOK{{ end }} | |
---|---|---|---|---|---|---|---|---|---|
Pumps | -RP: | -{{ if .rp }}ON{{ else }}OFF{{ end }} | -DP: | -{{ if .dp }}ON{{ else }}OFF{{ end }} | +RP | +{{ if .Pumps.RPOn }}ON{{ else }}OFF{{ end }} | +DP | +{{ if .Pumps.DPOn }}ON{{ else }}OFF{{ end }} | |
Evac Control | -+ | Safety | +Pirani Failsafe |
+ {{ if .Safety.Failsafe }}ON{{ else }}OFF{{ end }} | +DP Lockout |
+ {{ if .Safety.HighPressure }}ON{{ else }}OFF{{ end }} | +
Control | +RP | ++ + + | +||
---|---|---|---|---|
DP | ++ + + | +|||
- - - - | ||||
Status | OK | |||
Failsafe | -OK | -|||
Diffusion Pump Lockout | -OK | -
@@ -109,7 +129,7 @@ td > span {
- {{.hostname}} | load: {{.load}} | pprof | metrics | ws ping: … + {{ .System.Hostname }} | load: {{ .System.Load }} | pprof | metrics | ws ping: …