package main import ( "errors" "fmt" "log" "sync" "time" ) // errUARTBusy is returned when the port is held for OTA (poller should not treat as unplug). var errUARTBusy = errors.New("uart busy (OTA in progress)") // errOTAInProgress is returned when a second OTA upload is attempted while one is running. var errOTAInProgress = errors.New("OTA upload already in progress") // 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 otaActive bool // UART held for firmware upload; poll/API must not interleave } 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 { return m.withPortLocked(false, fn) } // withPortPoll is like withPort but returns errUARTBusy during OTA (no TryLock race). func (m *managedSerial) withPortPoll(fn func(*serialPort) error) error { return m.withPortLocked(true, fn) } func (m *managedSerial) withPortLocked(poll bool, fn func(*serialPort) error) error { m.mu.Lock() defer m.mu.Unlock() if m.otaActive { return errUARTBusy } 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) 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) } func (m *managedSerial) exchangePayloadPoll(payload []byte, cmdName string) ([]byte, error) { return m.exchangePayloadVia(m.withPortPoll, payload, cmdName) } func (m *managedSerial) exchangePayloadVia( portFn func(func(*serialPort) error) error, payload []byte, cmdName string, ) ([]byte, error) { var resp []byte err := portFn(func(sp *serialPort) error { var e error resp, e = sp.exchangePayloadLocked(payload, cmdName, readTimeout) return e }) return resp, err } func (m *managedSerial) exchange(cmdID byte, cmdName string) ([]byte, error) { return m.exchangeVia(m.withPort, cmdID, cmdName) } func (m *managedSerial) exchangePoll(cmdID byte, cmdName string) ([]byte, error) { return m.exchangeVia(m.withPortPoll, cmdID, cmdName) } func (m *managedSerial) exchangeVia( portFn func(func(*serialPort) error) error, cmdID byte, cmdName string, ) ([]byte, error) { var resp []byte err := portFn(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{}, } }