package main import ( "encoding/hex" "encoding/json" "log" "sync" "time" "github.com/gorilla/websocket" ) type MasterView struct { Version uint32 `json:"version"` GitHash string `json:"git_hash"` OK bool `json:"ok"` Error string `json:"error,omitempty"` } type ClientView struct { ID uint32 `json:"id"` MAC string `json:"mac"` Version uint32 `json:"version"` 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 pollDashboard(link *managedSerial, portName string) DashboardState { st := DashboardState{ UpdatedAt: time.Now().Format(time.RFC3339), SerialPort: portName, Clients: []ClientView{}, } ver, err := link.getVersion() if err != nil { return disconnectedState(portName, err) } st.UARTConnected = true st.SerialOK = true st.Master = MasterView{ Version: ver.GetVersion(), GitHash: ver.GetGitHash(), OK: true, } clients, err := link.listClients() if err != nil { st.SerialOK = false st.SerialError = err.Error() st.UARTConnected = link.IsConnected() return st } for _, c := range clients { st.Clients = append(st.Clients, ClientView{ ID: c.GetId(), MAC: formatMAC(c.GetMac()), Version: c.GetVersion(), Available: c.GetAvailable(), Used: c.GetUsed(), LastPing: c.GetLastPing(), LastSuccessPing: c.GetLastSuccessPing(), }) } return st } 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 publish := func() { st := pollDashboard(link, portName) 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() } } }