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,122 @@
|
||||
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{},
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user