Fix UART hang after master restart from the dashboard.

ReadFrame now returns on serial timeout instead of looping forever, and
serve closes and reopens the port after a master reboot so polling and
API commands can resume.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
simon 2026-05-31 16:45:57 +02:00
parent e4ce18edd8
commit ab1844ac32
5 changed files with 44 additions and 7 deletions

View File

@ -417,9 +417,16 @@ func (m *managedSerial) FindMe(clientID uint32) error {
}
func (m *managedSerial) Restart(clientID uint32) error {
return m.withPort(func(sp *serialPort) error {
err := m.withPort(func(sp *serialPort) error {
return runRestartClient(sp, clientID)
})
if err != nil {
return err
}
if clientID == 0 {
m.recoverAfterMasterRestart()
}
return nil
}
func (s *serialPort) ledRingProgress(req *pb.LedRingProgressRequest) (*pb.LedRingProgressResponse, error) {

View File

@ -416,7 +416,7 @@ func readUartMessageUntil(sp *serialPort, deadline time.Time, want pb.MessageTyp
if err := sp.port.SetReadTimeout(wait); err != nil {
return nil, err
}
payload, err := uartframe.ReadFrame(sp.port, nil)
payload, err := uartframe.ReadFrame(sp.port, nil, wait)
if err != nil {
return nil, err
}
@ -536,7 +536,7 @@ func waitOtaStatus(sp *serialPort, want uint32, timeout time.Duration, onPrepari
if err := sp.port.SetReadTimeout(readWait); err != nil {
return nil, err
}
payload, err := uartframe.ReadFrame(sp.port, nil)
payload, err := uartframe.ReadFrame(sp.port, nil, readWait)
if err != nil {
continue
}
@ -562,7 +562,7 @@ func waitOtaStatus(sp *serialPort, want uint32, timeout time.Duration, onPrepari
}
func readOtaStatus(sp *serialPort) (*pb.OtaStatusPayload, error) {
payload, err := uartframe.ReadFrame(sp.port, nil)
payload, err := uartframe.ReadFrame(sp.port, nil, readTimeout)
if err != nil {
return nil, fmt.Errorf("read response: %w", err)
}

View File

@ -105,6 +105,26 @@ func (m *managedSerial) withPortLocked(poll bool, fn func(*serialPort) error) er
return err
}
func (m *managedSerial) recoverAfterMasterRestart() {
const bootWait = 6 * time.Second
m.mu.Lock()
m.closeLocked()
m.mu.Unlock()
log.Printf("UART: master restart — waiting %s for boot", bootWait)
time.Sleep(bootWait)
m.mu.Lock()
defer m.mu.Unlock()
if err := m.openLocked(); err != nil {
log.Printf("UART reconnect after master restart: %v", err)
return
}
flushSerialInput(m.sp)
log.Printf("UART %s ready after master restart", m.portName)
}
func (m *managedSerial) exchangePayload(payload []byte, cmdName string) ([]byte, error) {
return m.exchangePayloadVia(m.withPort, payload, cmdName)
}

View File

@ -80,7 +80,7 @@ func (s *serialPort) exchangePayloadLocked(payload []byte, cmdName string, timeo
}
defer func() { _ = s.port.SetReadTimeout(readTimeout) }()
respPayload, err := uartframe.ReadFrame(s.port, nil)
respPayload, err := uartframe.ReadFrame(s.port, nil, timeout)
if err != nil {
return nil, fmt.Errorf("read response: %w", err)
}
@ -113,7 +113,7 @@ func (s *serialPort) exchangeLocked(cmdID byte, cmdName string) ([]byte, error)
return nil, fmt.Errorf("write: %w", err)
}
payload, err := uartframe.ReadFrame(s.port, nil)
payload, err := uartframe.ReadFrame(s.port, nil, readTimeout)
if err != nil {
return nil, fmt.Errorf("read response: %w", err)
}

View File

@ -4,6 +4,7 @@ import (
"errors"
"fmt"
"io"
"time"
)
const (
@ -98,13 +99,22 @@ func (p *Parser) Feed(b byte) (payload []byte, ok bool, err error) {
}
// ReadFrame reads bytes from r until one full frame is parsed or an error occurs.
func ReadFrame(r io.Reader, buf []byte) ([]byte, error) {
// maxWait bounds total wait time; zero means no limit (serial read timeouts retry forever).
func ReadFrame(r io.Reader, buf []byte, maxWait time.Duration) ([]byte, error) {
if buf == nil {
buf = make([]byte, 256)
}
parser := NewParser()
var deadline time.Time
if maxWait > 0 {
deadline = time.Now().Add(maxWait)
}
for {
if !deadline.IsZero() && !time.Now().Before(deadline) {
return nil, ErrTimeout
}
n, err := r.Read(buf)
if n > 0 {
for i := 0; i < n; i++ {