package main import ( "encoding/hex" "encoding/json" "errors" "fmt" "log" "sync" "time" "github.com/gorilla/websocket" "powerpod/gotool/pb" ) type MasterView struct { Version uint32 `json:"version"` GitHash string `json:"git_hash"` RunningPartition string `json:"running_partition,omitempty"` Deadzone uint32 `json:"deadzone,omitempty"` OK bool `json:"ok"` Error string `json:"error,omitempty"` } type ClientView struct { ID uint32 `json:"id"` MAC string `json:"mac"` Version uint32 `json:"version"` Deadzone uint32 `json:"deadzone,omitempty"` Available bool `json:"available"` Used bool `json:"used"` LastPing uint32 `json:"last_ping"` LastSuccessPing uint32 `json:"last_success_ping"` } type DashboardState struct { UpdatedAt string `json:"updated_at"` SerialPort string `json:"serial_port"` UARTConnected bool `json:"uart_connected"` SerialOK bool `json:"serial_ok"` SerialError string `json:"serial_error,omitempty"` Master MasterView `json:"master"` Clients []ClientView `json:"clients"` } type wsHub struct { mu sync.RWMutex clients map[*websocket.Conn]struct{} state DashboardState } func newWSHub() *wsHub { return &wsHub{clients: make(map[*websocket.Conn]struct{})} } func (h *wsHub) setState(st DashboardState) { h.mu.Lock() h.state = st conns := make([]*websocket.Conn, 0, len(h.clients)) for c := range h.clients { conns = append(conns, c) } h.mu.Unlock() data, err := json.Marshal(st) if err != nil { return } for _, c := range conns { _ = c.WriteMessage(websocket.TextMessage, data) } } func (h *wsHub) register(c *websocket.Conn) { h.mu.Lock() h.clients[c] = struct{}{} snap := h.state h.mu.Unlock() if data, err := json.Marshal(snap); err == nil { _ = c.WriteMessage(websocket.TextMessage, data) } } func (h *wsHub) unregister(c *websocket.Conn) { h.mu.Lock() delete(h.clients, c) h.mu.Unlock() } func (h *wsHub) broadcastRaw(v any) { h.mu.RLock() conns := make([]*websocket.Conn, 0, len(h.clients)) for c := range h.clients { conns = append(conns, c) } h.mu.RUnlock() data, err := json.Marshal(v) if err != nil { return } for _, c := range conns { _ = c.WriteMessage(websocket.TextMessage, data) } } func pollDashboard(link *managedSerial, portName string, last *DashboardState) DashboardState { st := DashboardState{ UpdatedAt: time.Now().Format(time.RFC3339), SerialPort: portName, Clients: []ClientView{}, } ver, err := link.getVersionPoll() if errors.Is(err, errUARTBusy) { return pausedPollState(portName, last) } if err != nil { return disconnectedState(portName, err) } st.UARTConnected = true st.SerialOK = true st.Master = MasterView{ Version: ver.GetVersion(), GitHash: ver.GetGitHash(), RunningPartition: ver.GetRunningPartition(), OK: true, } if dz, err := readDeadzonePoll(link, 0); err == nil { st.Master.Deadzone = dz } clients, err := link.listClientsPoll() if err != nil { if errors.Is(err, errUARTBusy) { return pausedPollState(portName, last) } st.SerialOK = false st.SerialError = err.Error() st.UARTConnected = link.IsConnected() return st } for _, c := range clients { cv := ClientView{ ID: c.GetId(), MAC: formatMAC(c.GetMac()), Version: c.GetVersion(), Available: c.GetAvailable(), Used: c.GetUsed(), LastPing: c.GetLastPing(), LastSuccessPing: c.GetLastSuccessPing(), } if dz, err := readDeadzonePoll(link, c.GetId()); err == nil { cv.Deadzone = dz } st.Clients = append(st.Clients, cv) } return st } func pausedPollState(portName string, last *DashboardState) DashboardState { if last != nil && last.UARTConnected { st := *last st.UpdatedAt = time.Now().Format(time.RFC3339) st.SerialPort = portName st.SerialOK = true st.SerialError = "Live-Polling pausiert (OTA läuft)" return st } return disconnectedState(portName, errUARTBusy) } func readDeadzone(link *managedSerial, clientID uint32) (uint32, error) { r, err := link.AccelDeadzone(&pb.AccelDeadzoneRequest{ Write: false, ClientId: clientID, }) if err != nil { return 0, err } if !r.GetSuccess() { return 0, fmt.Errorf("deadzone read failed for client %d", clientID) } return r.GetDeadzone(), nil } func readDeadzonePoll(link *managedSerial, clientID uint32) (uint32, error) { r, err := link.AccelDeadzonePoll(&pb.AccelDeadzoneRequest{ Write: false, ClientId: clientID, }) if err != nil { return 0, err } if !r.GetSuccess() { return 0, fmt.Errorf("deadzone read failed for client %d", clientID) } return r.GetDeadzone(), nil } func formatMAC(mac []byte) string { if len(mac) == 0 { return "" } return hex.EncodeToString(mac) } func runPoller(link *managedSerial, portName string, hub *wsHub, interval time.Duration, stop <-chan struct{}) { ticker := time.NewTicker(interval) defer ticker.Stop() uartUp := false var lastGood DashboardState publish := func() { st := pollDashboard(link, portName, &lastGood) if st.UARTConnected && st.SerialOK { lastGood = st } if st.UARTConnected && !uartUp { log.Printf("UART %s connected", portName) } uartUp = st.UARTConnected hub.setState(st) } publish() for { select { case <-stop: return case <-ticker.C: publish() } } }