powerpods/goTool/serial_link.go
simon c4696657a7 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>
2026-05-19 00:04:57 +02:00

123 lines
2.3 KiB
Go

package main
import (
"fmt"
"log"
"sync"
"time"
)
// managedSerial keeps the UART open and reconnects after I/O failures or unplug.
type managedSerial struct {
portName string
baud int
quiet bool
mu sync.Mutex
sp *serialPort
}
func newManagedSerial(portName string, baud int) *managedSerial {
return &managedSerial{
portName: portName,
baud: baud,
}
}
func (m *managedSerial) Close() error {
m.mu.Lock()
defer m.mu.Unlock()
return m.closeLocked()
}
func (m *managedSerial) IsConnected() bool {
m.mu.Lock()
defer m.mu.Unlock()
return m.sp != nil
}
func (m *managedSerial) openLocked() error {
sp, err := openSerial(m.portName, m.baud)
if err != nil {
return err
}
sp.quiet = m.quiet
m.sp = sp
if !m.quiet {
log.Printf("UART %s connected (%d baud)", m.portName, m.baud)
}
return nil
}
func (m *managedSerial) closeLocked() error {
if m.sp == nil {
return nil
}
err := m.sp.port.Close()
m.sp = nil
return err
}
func (m *managedSerial) invalidateLocked(reason error) {
if m.sp == nil {
return
}
if !m.quiet {
log.Printf("UART %s disconnected: %v", m.portName, reason)
}
_ = m.closeLocked()
}
func (m *managedSerial) withPort(fn func(*serialPort) error) error {
m.mu.Lock()
defer m.mu.Unlock()
if m.sp == nil {
if err := m.openLocked(); err != nil {
return fmt.Errorf("%s: %w", m.portName, err)
}
}
err := fn(m.sp)
if err != nil {
m.invalidateLocked(err)
}
return err
}
func (m *managedSerial) exchangePayload(payload []byte, cmdName string) ([]byte, error) {
var resp []byte
err := m.withPort(func(sp *serialPort) error {
var e error
resp, e = sp.exchangePayloadLocked(payload, cmdName)
return e
})
return resp, err
}
func (m *managedSerial) exchange(cmdID byte, cmdName string) ([]byte, error) {
var resp []byte
err := m.withPort(func(sp *serialPort) error {
var e error
resp, e = sp.exchangeLocked(cmdID, cmdName)
return e
})
return resp, err
}
func disconnectedState(portName string, err error) DashboardState {
msg := "UART disconnected"
if err != nil {
msg = err.Error()
}
return DashboardState{
UpdatedAt: time.Now().Format(time.RFC3339),
SerialPort: portName,
UARTConnected: false,
SerialOK: false,
SerialError: msg,
Master: MasterView{OK: false, Error: msg},
Clients: []ClientView{},
}
}