package main import ( "embed" "errors" "flag" "fmt" "io/fs" "log" "net/http" "time" "github.com/gorilla/websocket" ) //go:embed webui/* var webUI embed.FS var wsUpgrader = websocket.Upgrader{ CheckOrigin: func(r *http.Request) bool { return true }, } type serveOpts struct { portName string baud int useBLE bool bleName string settle time.Duration } func runServe(opts serveOpts, args []string) error { serveFlags := flag.NewFlagSet("serve", flag.ExitOnError) addr := serveFlags.String("addr", ":8080", "dashboard HTTP listen address") apiAddr := serveFlags.String("api-addr", ":8081", "external API HTTP listen address (empty to disable)") accelInterval := serveFlags.Duration("accel-interval", defaultAccelStreamInterval, "accel WebSocket sample period on API server") interval := serveFlags.Duration("interval", 2*time.Second, "host poll interval") if err := serveFlags.Parse(args); err != nil { return err } if opts.useBLE && opts.portName != "" { return fmt.Errorf("use either -port or -ble, not both") } link := newManagedHost(opts.baud) link.quiet = true link.settle = opts.settle if opts.settle <= 0 { link.settle = time.Second } switch { case opts.portName != "": if err := link.ConnectUART(opts.portName, opts.baud); err != nil { log.Printf("initial UART connect failed (UI can retry): %v", err) } case opts.useBLE: name := opts.bleName if name == "" { name = bleDefaultName } if err := link.ConnectBLE("", name); err != nil { log.Printf("initial BLE connect failed (UI can scan/retry): %v", err) } } hub := newWSHub() streamCtl := newAccelStreamCtl() tapCtl := newTapNotifyCtl() stop := make(chan struct{}) var dashSrv *http.Server var apiSrv *http.Server registerShutdown(func() { close(stop) shutdownHTTPServer(dashSrv) shutdownAPIServer(apiSrv) if err := link.Close(); err != nil { log.Printf("host close: %v", err) } }) go runPoller(link, link.Endpoint(), hub, streamCtl, tapCtl, *interval, stop) go runBatteryPoller(link, hub, 5*time.Second, stop) go runCacheStatusDashboardPoller(link, hub, *accelInterval, stop) go runBLEStreamDashboardPoller(link, hub, streamCtl, stop) if *apiAddr != "" { apiSrv = runAPIServer(link.Endpoint(), link, *apiAddr, *accelInterval, hub, streamCtl, tapCtl, stop) } mux := http.NewServeMux() mountServeAPI(mux, link, hub, streamCtl, tapCtl) mux.HandleFunc("/ws", func(w http.ResponseWriter, r *http.Request) { conn, err := wsUpgrader.Upgrade(w, r, nil) if err != nil { log.Printf("websocket upgrade: %v", err) return } hub.register(conn) defer hub.unregister(conn) defer conn.Close() for { if _, _, err := conn.ReadMessage(); err != nil { return } } }) ui, err := fs.Sub(webUI, "webui") if err != nil { return err } mux.Handle("/", http.FileServer(http.FS(ui))) log.Printf("dashboard http://localhost%s (transport=%s endpoint=%q poll %s, live-stream %s)", *addr, link.Transport(), link.Endpoint(), interval.String(), accelInterval.String()) if *apiAddr == "" { log.Printf("external API disabled (-api-addr \"\")") } else { log.Printf("external API http://localhost%s ws://localhost%s/ws", *apiAddr, *apiAddr) } dashSrv = &http.Server{Addr: *addr, Handler: mux} go func() { if err := dashSrv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { log.Printf("dashboard server: %v", err) } }() waitForShutdown() return nil }