Serve starts without a required port; REST/WS/UI can scan and switch transports, with BLE live ticks and UART-only OTA. Co-authored-by: Cursor <cursoragent@cursor.com>
134 lines
3.4 KiB
Go
134 lines
3.4 KiB
Go
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
|
|
}
|