Fix web OTA upload and isolate OTA sessions across firmware and goTool.

Split ESP-NOW into core/master/slave modules, block non-OTA UART traffic during updates, and hold the host serial port exclusively so dashboard polling cannot interleave with firmware uploads.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-05-31 16:35:18 +02:00
co-authored by Cursor
parent 0cbc4d0644
commit 0eea27a876
29 changed files with 1828 additions and 1416 deletions
+77 -25
View File
@@ -2,7 +2,6 @@ package main
import (
"fmt"
"log"
"time"
"google.golang.org/protobuf/proto"
@@ -69,16 +68,45 @@ const (
)
func runOTAUpload(m *managedSerial, firmware []byte, onProgress otaProgressFn) error {
push := func(phase, msg string) {
if onProgress == nil {
return
}
onProgress(OTAProgress{
Type: "ota_progress", Phase: phase, Step: otaStepMaster,
Percent: 0, Message: msg, MasterMessage: msg,
})
}
push("preparing", "UART wird vorbereitet…")
// Block until the UART is free, then hold m.mu for the entire upload so
// dashboard/API polling cannot interleave on the serial port.
m.mu.Lock()
defer m.mu.Unlock()
err := runOTAOnPortUnlocked(m, firmware, onProgress)
if m.otaActive {
m.mu.Unlock()
return errOTAInProgress
}
m.otaActive = true
if m.sp == nil {
if err := m.openLocked(); err != nil {
m.otaActive = false
m.mu.Unlock()
push("error", err.Error())
return err
}
}
sp := m.sp
err := runOTAOnPortUnlocked(sp, firmware, onProgress)
if err != nil {
m.invalidateLocked(err)
}
m.otaActive = false
m.mu.Unlock()
return err
}
func runOTAOnPortUnlocked(m *managedSerial, firmware []byte, onProgress otaProgressFn) error {
func runOTAOnPortUnlocked(sp *serialPort, firmware []byte, onProgress otaProgressFn) error {
if len(firmware) == 0 {
return fmt.Errorf("empty firmware")
}
@@ -120,32 +148,31 @@ func runOTAOnPortUnlocked(m *managedSerial, firmware []byte, onProgress otaProgr
onProgress(p)
}
if m.sp == nil {
if err := m.openLocked(); err != nil {
notify("error", "", 0, err.Error())
return err
}
}
sp := m.sp
if err := sp.port.SetReadTimeout(otaPrepareTimeout); err != nil {
if err := sp.port.SetReadTimeout(readTimeout); err != nil {
notify("error", "", 0, err.Error())
return err
}
defer sp.port.SetReadTimeout(readTimeout)
notify("preparing", otaStepMaster, 0, fmt.Sprintf("Master: OTA start (%d bytes)…", imageSize))
flushSerialInput(sp)
if err := writeUartMessage(sp, &pb.UartMessage{
Type: pb.MessageType_OTA_START,
Payload: &pb.UartMessage_OtaStart{
OtaStart: &pb.OtaStartPayload{TotalSize: uint32(imageSize)},
},
}, false); err != nil {
}); err != nil {
notify("error", "", 0, err.Error())
return err
}
if err := sp.port.SetReadTimeout(otaPrepareTimeout); err != nil {
notify("error", "", 0, err.Error())
return err
}
defer func() { _ = sp.port.SetReadTimeout(readTimeout) }()
ready, err := waitOtaStatus(sp, otaStReady, otaPrepareTimeout, func(msg string) {
notify("preparing", otaStepMaster, 2, msg)
})
@@ -179,7 +206,7 @@ func runOTAOnPortUnlocked(m *managedSerial, firmware []byte, onProgress otaProgr
Payload: &pb.UartMessage_OtaPayload{
OtaPayload: &pb.OtaPayload{Seq: seq, Data: chunk},
},
}, false); err != nil {
}); err != nil {
notify("error", "", 0, err.Error())
return err
}
@@ -219,7 +246,7 @@ func runOTAOnPortUnlocked(m *managedSerial, firmware []byte, onProgress otaProgr
Payload: &pb.UartMessage_OtaEnd{
OtaEnd: &pb.OtaEndPayload{},
},
}, false); err != nil {
}); err != nil {
notify("error", "", 0, err.Error())
return err
}
@@ -333,7 +360,7 @@ func queryOtaSlaveProgressLocked(sp *serialPort, clientID uint32,
},
},
}
if err := writeUartMessage(sp, req, false); err != nil {
if err := writeUartMessage(sp, req); err != nil {
return nil, err
}
if queryTimeout <= 0 {
@@ -487,14 +514,11 @@ func waitOtaComplete(sp *serialPort, timeout time.Duration,
}
}
func writeUartMessage(sp *serialPort, msg *pb.UartMessage, logFrame bool) error {
func writeUartMessage(sp *serialPort, msg *pb.UartMessage) error {
frame, err := encodeUartMessage(msg)
if err != nil {
return err
}
if logFrame {
log.Printf("sending %s (%d frame bytes)", msg.Type, len(frame))
}
_, err = sp.port.Write(frame)
return err
}
@@ -505,12 +529,24 @@ func waitOtaStatus(sp *serialPort, want uint32, timeout time.Duration, onPrepari
if time.Now().After(deadline) {
return nil, fmt.Errorf("timeout waiting for OTA status %d", want)
}
if err := sp.port.SetReadTimeout(time.Until(deadline)); err != nil {
readWait := time.Until(deadline)
if readWait > otaStatusPollTimeout {
readWait = otaStatusPollTimeout
}
if err := sp.port.SetReadTimeout(readWait); err != nil {
return nil, err
}
st, err := readOtaStatus(sp)
payload, err := uartframe.ReadFrame(sp.port, nil)
if err != nil {
return nil, err
continue
}
msg, err := decodeUartPayload(payload)
if err != nil || msg.GetType() != pb.MessageType_OTA_STATUS {
continue
}
st := msg.GetOtaStatus()
if st == nil {
continue
}
switch st.GetStatus() {
case want:
@@ -553,6 +589,22 @@ func encodeUartMessage(msg *pb.UartMessage) ([]byte, error) {
return uartframe.EncodeFrame(payload)
}
// flushSerialInput drops stale RX bytes (not full frames — avoids ReadFrame blocking).
func flushSerialInput(sp *serialPort) {
if sp == nil {
return
}
_ = sp.port.SetReadTimeout(10 * time.Millisecond)
buf := make([]byte, 256)
deadline := time.Now().Add(50 * time.Millisecond)
for time.Now().Before(deadline) {
n, err := sp.port.Read(buf)
if n == 0 || err != nil {
return
}
}
}
func decodeUartPayload(payload []byte) (*pb.UartMessage, error) {
if len(payload) == 0 {
return nil, fmt.Errorf("empty response")