Add goTool web dashboard with UART auto-reconnect.
Serve polls the master over UART and pushes live state via WebSocket; reopens the serial port when the device is unplugged and comes back. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,156 @@
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user