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:
2026-05-19 00:04:57 +02:00
co-authored by Cursor
parent 0299ba44fd
commit c4696657a7
10 changed files with 652 additions and 13 deletions
+28 -5
View File
@@ -3,6 +3,7 @@ package main
import (
"fmt"
"log"
"sync"
"time"
"go.bug.st/serial"
@@ -12,7 +13,9 @@ import (
const readTimeout = 3 * time.Second
type serialPort struct {
port serial.Port
port serial.Port
mu sync.Mutex
quiet bool
}
func openSerial(portName string, baud int) (*serialPort, error) {
@@ -39,6 +42,12 @@ func (s *serialPort) Close() error {
}
func (s *serialPort) exchangePayload(payload []byte, cmdName string) ([]byte, error) {
s.mu.Lock()
defer s.mu.Unlock()
return s.exchangePayloadLocked(payload, cmdName)
}
func (s *serialPort) exchangePayloadLocked(payload []byte, cmdName string) ([]byte, error) {
if len(payload) == 0 {
return nil, fmt.Errorf("empty payload")
}
@@ -47,7 +56,9 @@ func (s *serialPort) exchangePayload(payload []byte, cmdName string) ([]byte, er
return nil, fmt.Errorf("encode frame: %w", err)
}
log.Printf("sending %s command (%d bytes): % x", cmdName, len(frame), frame)
if !s.quiet {
log.Printf("sending %s command (%d bytes): % x", cmdName, len(frame), frame)
}
if _, err := s.port.Write(frame); err != nil {
return nil, fmt.Errorf("write: %w", err)
}
@@ -57,7 +68,9 @@ func (s *serialPort) exchangePayload(payload []byte, cmdName string) ([]byte, er
return nil, fmt.Errorf("read response: %w", err)
}
log.Printf("response payload (%d bytes): % x", len(respPayload), respPayload)
if !s.quiet {
log.Printf("response payload (%d bytes): % x", len(respPayload), respPayload)
}
if len(respPayload) == 0 {
return nil, fmt.Errorf("empty response payload")
}
@@ -65,12 +78,20 @@ func (s *serialPort) exchangePayload(payload []byte, cmdName string) ([]byte, er
}
func (s *serialPort) exchange(cmdID byte, cmdName string) ([]byte, error) {
s.mu.Lock()
defer s.mu.Unlock()
return s.exchangeLocked(cmdID, cmdName)
}
func (s *serialPort) exchangeLocked(cmdID byte, cmdName string) ([]byte, error) {
frame, err := uartframe.EncodeFrame([]byte{cmdID})
if err != nil {
return nil, fmt.Errorf("encode frame: %w", err)
}
log.Printf("sending %s command (%d bytes): % x", cmdName, len(frame), frame)
if !s.quiet {
log.Printf("sending %s command (%d bytes): % x", cmdName, len(frame), frame)
}
if _, err := s.port.Write(frame); err != nil {
return nil, fmt.Errorf("write: %w", err)
}
@@ -80,7 +101,9 @@ func (s *serialPort) exchange(cmdID byte, cmdName string) ([]byte, error) {
return nil, fmt.Errorf("read response: %w", err)
}
log.Printf("response payload (%d bytes): % x", len(payload), payload)
if !s.quiet {
log.Printf("response payload (%d bytes): % x", len(payload), payload)
}
if len(payload) == 0 {
return nil, fmt.Errorf("empty response payload")
}