Enable host OTA over BLE and harden ESP-NOW slave distribution.

Allow the same staged firmware upload path on BLE as UART, fix distribute stack/event-group crashes, and recover sticky slave OTA sessions instead of failing with err=4.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-11-01 20:42:14 +01:00
co-authored by Cursor
parent 567dab6d0a
commit ae8ef40820
16 changed files with 400 additions and 208 deletions
+1 -1
View File
@@ -109,7 +109,7 @@ sequenceDiagram
└─────────────────────────────────────────────────────────────┘
```
**Host UART vs BLE:** Nur einer aktiv (DIP8). Gleiches Framing (`0xAA`…`0xCC`) und dieselbe `cmd_queue`. BLE: NUS-ähnliche GATT-Char, Write=RX, Notify=TX. `gotool serve` verbindet UART oder BLE über Dashboard/REST/WS (`managedHost`); **OTA nur UART**. Bei BLE-Host und aktivem Accel-Stream pusht `ble_stream` periodisch `BLE_STREAM_TICK` (8–16 ms) mit `esp_timestamp_us` (Live-Pfad bevorzugt Tick statt CACHE_STATUS-Poll).
**Host UART vs BLE:** Nur einer aktiv (DIP8). Gleiches Framing (`0xAA`…`0xCC`) und dieselbe `cmd_queue`. BLE: NUS-ähnliche GATT-Char, Write=RX, Notify=TX. `gotool serve` verbindet UART oder BLE über Dashboard/REST/WS (`managedHost`); **OTA** über UART oder BLE (ATT MTU ≥ ~219). Bei BLE-Host und aktivem Accel-Stream pusht `ble_stream` periodisch `BLE_STREAM_TICK` (8–16 ms; pausiert während OTA) mit `esp_timestamp_us` (Live-Pfad bevorzugt Tick statt CACHE_STATUS-Poll).
---
+6 -5
View File
@@ -55,7 +55,7 @@ Zielbild für den Host: Befehle an den Master senden; der Master steuert Slaves
| LiPo ADC 1 | 1 | `board_input.c` |
| LiPo ADC 2 | 11 | `board_input.c` |
**Host-Protokoll (UART oder BLE):** Gleiches Framing (`0xAA`…`0xCC`) + nanopb. **UART:** `UART_NUM_1`, **921600** Baud, 8N1. **BLE:** Gerät `Powerpod`, NUS-UUIDs `6e400001`/`6e400002` (Write=RX, Notify=TX). goTool CLI: `-port …` oder `-ble` / `-ble-name Powerpod`. **`gotool serve`** startet ohne Pflicht-Gerät; Dashboard/REST/WS verbinden UART oder BLE dynamisch (`-port`/`-ble` nur optionale Vorbelegung). **OTA nur UART** (DIP8=0). Bei BLE + Accel-Stream: periodische `BLE_STREAM_TICK`-Notifies (`period_ms` 8–16) speisen Live-Accel im Dashboard/API.
**Host-Protokoll (UART oder BLE):** Gleiches Framing (`0xAA`…`0xCC`) + nanopb. **UART:** `UART_NUM_1`, **921600** Baud, 8N1. **BLE:** Gerät `Powerpod`, NUS-UUIDs `6e400001`/`6e400002` (Write=RX, Notify=TX). goTool CLI: `-port …` oder `-ble` / `-ble-name Powerpod`. **`gotool serve`** startet ohne Pflicht-Gerät; Dashboard/REST/WS verbinden UART oder BLE dynamisch (`-port`/`-ble` nur optionale Vorbelegung). **OTA** über UART oder BLE (DIP8); BLE braucht ATT MTU ≥ ~219 für 200-B-Chunks. Bei BLE + Accel-Stream: periodische `BLE_STREAM_TICK`-Notifies (`period_ms` 8–16; während OTA pausiert).
**ESP-IDF-Log (Debug):** `esp_log_*` geht auf **UART0** (Standard-Konsole, typisch USB am Dev-Board, **115200** Baud, `CONFIG_ESP_CONSOLE_UART_NUM=0`). Das ist **getrennt** vom Host-UART1 — goTool liest keine `esp_log`-Ausgabe. Ohne angeschlossenes Debug-Kabel werden aktivierte Logs trotzdem formatiert und an UART0 gesendet (CPU-Overhead bleibt); nur `ESP_LOG_NONE` / Level-Filter vermeiden die Arbeit.
@@ -226,7 +226,7 @@ Nur auf dem **Master** registriert (`powerpod.c`). IDs aus `MessageType` in `uar
| 6 | ACCEL_DEADZONE | `cmd_accel_deadzone.c` | Get/Set Deadzone lokal + ESP-NOW |
| 7 | ESPNOW_UNICAST_TEST | `cmd_espnow_unicast_test.c` | Link-Test zu Slave |
| 8 | LED_RING | `cmd_led_ring.c` | Ring lokal / ESP-NOW |
| 16 | OTA_START | `cmd_ota.c` | UART-OTA beginnen |
| 16 | OTA_START | `cmd_ota.c` | Host-OTA beginnen (UART oder BLE) |
| 17 | OTA_PAYLOAD | `cmd_ota.c` | Chunk ≤200 B |
| 18 | OTA_END | `cmd_ota.c` | Abschluss + ESP-NOW-Verteilung |
| 19 | OTA_STATUS | `cmd_ota.c` | Gerät → Host Status |
@@ -358,9 +358,9 @@ go run . -port /dev/ttyUSB0 log-level -set -level 3
## 8. OTA
### 8.1 UART-OTA (nur Master)
### 8.1 Host-OTA (nur Master, UART oder BLE)
Implementierung: `ota_uart.c`, Steuerung `cmd/cmd_ota.c`.
Implementierung: `ota_uart.c`, Steuerung `cmd/cmd_ota.c`. Gleiches Protokoll über DIP8-Host (UART oder BLE NUS).
| Phase | Host → Master | Master → Host |
|-------|---------------|---------------|
@@ -370,10 +370,11 @@ Implementierung: `ota_uart.c`, Steuerung `cmd/cmd_ota.c`.
- Inaktive Partition: `esp_ota_get_next_update_partition()`.
- LED-Ring zeigt Fortschritt (blau Schreiben, grün Verteilung).
- BLE: ATT MTU ≥ ~219 (preferred 247); `BLE_STREAM_TICK` pausiert während `ota_session_busy()`.
### 8.2 ESP-NOW-OTA (Master → Slaves)
`ota_espnow.c` — gleiche Status-Codes wie UART.
`ota_espnow.c` — gleiche Status-Codes wie Host-OTA.
1. `ESPNOW_OTA_START` + `total_size` → Slave `ota_espnow_slave_on_start`
2. `ESPNOW_OTA_PAYLOAD` bis 200 B → 4 KiB Buffer auf Slave
+2 -1
View File
@@ -61,7 +61,7 @@ See [`testdata/README.md`](testdata/README.md) for the JSON schema.
### Web dashboard
Starts without a required host link. Connect UART or BLE from the UI (or pre-connect with `-port` / `-ble`). State is pushed to the browser via WebSocket (Alpine.js + Bootstrap 5). OTA remains UART-only.
Starts without a required host link. Connect UART or BLE from the UI (or pre-connect with `-port` / `-ble`). State is pushed to the browser via WebSocket (Alpine.js + Bootstrap 5). OTA works over UART or BLE (BLE needs ATT MTU ≥ 219).
```bash
go run . serve
@@ -110,6 +110,7 @@ go run . -port /dev/ttyUSB0 cache-status
```bash
go run . -port /dev/ttyUSB0 ota build/powerpod.bin
go run . -ble ota build/powerpod.bin
```
Waits for **ready** after start (~30 s erase), sends 200-byte `OTA_PAYLOAD` frames, reads **block_ack** every 4 KiB, then `OTA_END`. The master then distributes to all available slaves (no extra host traffic); **success** is reported only when that finishes. Allow several minutes for large images. Reboot master and slaves to boot the new firmware.
+2 -2
View File
@@ -167,8 +167,8 @@ func mountServeAPI(mux *http.ServeMux, link *managedSerial, hub *wsHub, streamCt
}
func serveOTAUpload(w http.ResponseWriter, r *http.Request, link *managedSerial, hub *wsHub) {
if link.Transport() != hostTransportUART || !link.IsConnected() {
writeJSON(w, http.StatusBadRequest, otaAPIResponse{Error: "OTA requires UART host (DIP8=0)"})
if !link.IsConnected() {
writeJSON(w, http.StatusBadRequest, otaAPIResponse{Error: "host not connected"})
return
}
if err := r.ParseMultipartForm(otaMaxFirmwareSize); err != nil {
+79 -4
View File
@@ -26,6 +26,7 @@ type bleLink struct {
respCh chan []byte
streamCh chan []byte
quiet bool
attMTU int // negotiated ATT MTU (0 if unknown)
}
func openBLE(name string, settle time.Duration) (*bleLink, error) {
@@ -125,19 +126,23 @@ func connectBLEDevice(addr bluetooth.Address, settle time.Duration) (*bleLink, e
}
char := chars[0]
attMTU := 0
if mtu, err := char.GetMTU(); err == nil {
log.Printf("BLE ATT MTU=%d", mtu)
if mtu < 203 {
log.Printf("warning: MTU %d < 203 — large frames/stream ticks may fail", mtu)
attMTU = int(mtu)
log.Printf("BLE ATT MTU=%d", attMTU)
if attMTU < otaMinATTMTU {
log.Printf("warning: MTU %d < %d — OTA (200 B chunks) and large stream ticks may fail",
attMTU, otaMinATTMTU)
}
}
link := &bleLink{
device: device,
device: device,
char: char,
parser: uartframe.NewParser(),
respCh: make(chan []byte, 8),
streamCh: make(chan []byte, 64),
attMTU: attMTU,
}
if err := char.EnableNotifications(func(buf []byte) {
@@ -277,3 +282,73 @@ func (b *bleLink) NextStreamTick(timeout time.Duration) ([]byte, error) {
return nil, fmt.Errorf("BLE stream tick timeout")
}
}
// ensureOTAMTU rejects OTA when the negotiated ATT MTU cannot carry 200 B chunks.
func (b *bleLink) ensureOTAMTU() error {
mtu := b.attMTU
if mtu <= 0 {
if v, err := b.char.GetMTU(); err == nil {
mtu = int(v)
b.attMTU = mtu
}
}
if mtu < otaMinATTMTU {
return fmt.Errorf("BLE ATT MTU %d < %d required for OTA (negotiate larger MTU)", mtu, otaMinATTMTU)
}
maxWrite := mtu - 3
if maxWrite < 216 {
return fmt.Errorf("BLE max write %d < 216 needed for 200 B OTA frames", maxWrite)
}
return nil
}
func (b *bleLink) writeMsg(msg *pb.UartMessage) error {
b.mu.Lock()
defer b.mu.Unlock()
frame, err := encodeUartMessage(msg)
if err != nil {
return err
}
if b.attMTU > 0 && len(frame) > b.attMTU-3 {
return fmt.Errorf("OTA frame %d exceeds BLE max write %d (MTU=%d)",
len(frame), b.attMTU-3, b.attMTU)
}
if _, err := b.char.WriteWithoutResponse(frame); err != nil {
return fmt.Errorf("BLE write: %w", err)
}
return nil
}
func (b *bleLink) readFrame(timeout time.Duration) ([]byte, error) {
if timeout <= 0 {
timeout = readTimeout
}
timer := time.NewTimer(timeout)
defer timer.Stop()
select {
case p := <-b.respCh:
return p, nil
case <-timer.C:
return nil, fmt.Errorf("BLE read timeout")
}
}
func (b *bleLink) flushInput() {
for {
select {
case <-b.respCh:
default:
goto streams
}
}
streams:
for {
select {
case <-b.streamCh:
default:
return
}
}
}
func (b *bleLink) chunkPace() time.Duration { return otaHostChunkPaceBLE }
+12 -4
View File
@@ -5,7 +5,7 @@ import (
"os"
)
func runOTA(sp *serialPort, args []string) error {
func runOTA(t otaTransport, args []string) error {
if len(args) < 1 {
return fmt.Errorf("usage: ota <firmware.bin>")
}
@@ -14,9 +14,17 @@ func runOTA(sp *serialPort, args []string) error {
return err
}
sp.mu.Lock()
defer sp.mu.Unlock()
return runOTAOnPortUnlocked(sp, data, func(p OTAProgress) {
switch x := t.(type) {
case *serialPort:
x.mu.Lock()
defer x.mu.Unlock()
case *bleLink:
if err := x.ensureOTAMTU(); err != nil {
return err
}
}
return runOTAUnlocked(t, data, func(p OTAProgress) {
switch p.Phase {
case "preparing", "ready":
fmt.Println(p.Message)
+7 -2
View File
@@ -5,14 +5,19 @@ import (
"fmt"
)
func runOtaProgress(sp *serialPort, args []string) error {
func runOtaProgress(t otaTransport, args []string) error {
fs := flag.NewFlagSet("ota-progress", flag.ExitOnError)
clientID := fs.Uint("client", 0, "slave client id (0 = all in session)")
if err := fs.Parse(args); err != nil {
return err
}
r, err := QueryOtaSlaveProgress(sp, uint32(*clientID))
if sp, ok := t.(*serialPort); ok {
sp.mu.Lock()
defer sp.mu.Unlock()
}
r, err := QueryOtaSlaveProgress(t, uint32(*clientID))
if err != nil {
return err
}
+9 -4
View File
@@ -138,7 +138,7 @@ POST /api/ble/scan
Dashboard WebSocket state includes `host_connected`, `host_transport`, `host_endpoint` (aliases `uart_connected` / `serial_port` kept).
**OTA** (`POST /api/ota`) returns **400** unless `transport==uart` and connected.
**OTA** (`POST /api/ota`) returns **400** unless a host (UART or BLE) is connected.
### BLE latency tests (BLE host only)
@@ -400,14 +400,14 @@ go run . -port /dev/ttyUSB0 log-level -set -level 3
Dashboard: Master card → dropdown **ESP Log-Level** with **Lesen** / **Setzen**.
### OTA (master UART upload)
### OTA (master host upload, UART or BLE)
```http
POST /api/ota
Content-Type: multipart/form-data
```
Form field **`firmware`**: binary image, max **2 MiB**.
Form field **`firmware`**: binary image, max **2 MiB**. Requires connected host (`transport` uart or ble). BLE needs ATT MTU ≥ 219 for 200 B chunks.
```json
{"success": true, "bytes_written": 123456, "target_slot": 1}
@@ -415,7 +415,12 @@ Form field **`firmware`**: binary image, max **2 MiB**.
Firmware distributes to slaves over ESP-NOW after `OTA_END`. Progress also appears on dashboard WebSocket as `ota_progress` messages.
CLI equivalent: `go run . -port /dev/ttyUSB0 ota build/powerpod.bin`
CLI equivalent:
```bash
go run . -port /dev/ttyUSB0 ota build/powerpod.bin
go run . -ble ota build/powerpod.bin
```
### LED ring and battery
+11 -11
View File
@@ -17,7 +17,7 @@ const (
hostTransportBLE = "ble"
)
// errHostBusy is returned when the link is held for OTA (UART only).
// errHostBusy is returned when the link is held for OTA.
var errHostBusy = errUARTBusy
// managedHost is a reconnecting UART or BLE host link for serve/CLI.
@@ -26,15 +26,15 @@ type managedHost struct {
baud int
settle time.Duration
mu sync.Mutex
transport string // none|uart|ble
endpoint string // port path or BLE address string
bleName string // last connected BLE local name (optional)
sp *serialPort
ble *bleLink
otaActive bool
pausePoll bool // true while BLE latency tests run (pollers yield)
uartPort string // remembered UART port for reconnect
mu sync.Mutex
transport string // none|uart|ble
endpoint string // port path or BLE address string
bleName string // last connected BLE local name (optional)
sp *serialPort
ble *bleLink
otaActive bool
pausePoll bool // true while BLE latency tests run (pollers yield)
uartPort string // remembered UART port for reconnect
}
func newManagedHost(baud int) *managedHost {
@@ -279,7 +279,7 @@ func (m *managedHost) recoverAfterMasterRestart() {
log.Printf("UART reconnect after master restart: %v", err)
return
}
flushSerialInput(m.sp)
m.sp.flushInput()
log.Printf("UART %s ready after master restart", m.uartPort)
}
}
+13 -12
View File
@@ -29,7 +29,7 @@ func usage() {
fmt.Fprintf(os.Stderr, " ble-stream enable stream + measure BleStreamTick window\n")
fmt.Fprintf(os.Stderr, " test run automated scenario (see testdata/)\n")
fmt.Fprintf(os.Stderr, " serve web dashboard (optional -port / -ble; connect from UI)\n")
fmt.Fprintf(os.Stderr, " ota UART OTA upload (A/B partitions; UART host only)\n")
fmt.Fprintf(os.Stderr, " ota host OTA upload (A/B partitions; UART or BLE)\n")
fmt.Fprintf(os.Stderr, " ota-progress query per-slave ESP-NOW OTA progress on master\n")
fmt.Fprintf(os.Stderr, " led-ring set LED ring progress bar (0–100%%, rgb, intensity)\n")
fmt.Fprintf(os.Stderr, " find-me blink LED ring red/green/blue (3× each, full brightness)\n")
@@ -83,13 +83,6 @@ func main() {
usage()
os.Exit(2)
}
if cmd == "ota" || cmd == "ota-progress" || cmd == "ota_progress" {
if *useBLE {
fmt.Fprintf(os.Stderr, "OTA requires UART host (DIP8=0, -port)\n")
os.Exit(2)
}
}
var h hostConn
if *useBLE {
bl, err := openBLE(*bleName, time.Duration(*bleSettle*float64(time.Second)))
@@ -187,11 +180,19 @@ func main() {
}
runErr = runLogLevel(sp, flag.Args()[1:])
case "ota":
sp := h.(*serialPort)
runErr = runOTA(sp, flag.Args()[1:])
t, ok := h.(otaTransport)
if !ok {
runErr = fmt.Errorf("ota: unsupported host transport")
break
}
runErr = runOTA(t, flag.Args()[1:])
case "ota-progress", "ota_progress":
sp := h.(*serialPort)
runErr = runOtaProgress(sp, flag.Args()[1:])
t, ok := h.(otaTransport)
if !ok {
runErr = fmt.Errorf("ota-progress: unsupported host transport")
break
}
runErr = runOtaProgress(t, flag.Args()[1:])
}
default:
fmt.Fprintf(os.Stderr, "unknown command %q\n\n", cmd)
+124 -102
View File
@@ -6,14 +6,14 @@ import (
"google.golang.org/protobuf/proto"
uartframe "powerpod/gotool/uart"
"powerpod/gotool/pb"
uartframe "powerpod/gotool/uart"
)
const (
otaHostChunkSize = 200
otaFlashBlockSize = 4096
otaPrepareTimeout = 120 * time.Second
otaHostChunkSize = 200
otaFlashBlockSize = 4096
otaPrepareTimeout = 120 * time.Second
otaDefaultTimeout = 15 * time.Second
otaStatusPollTimeout = 3 * time.Second
otaDistReadTimeout = 400 * time.Millisecond
@@ -21,8 +21,12 @@ const (
otaDistQueryTimeout = 2 * time.Second
otaDistEmitMinInterval = 150 * time.Millisecond
// Pace host chunks so the master UART RX ring is not overrun (~20 frames/block).
otaHostChunkPace = 3 * time.Millisecond
otaBlockMaxRetries = 3
otaHostChunkPaceUART = 3 * time.Millisecond
// Slightly slower for BLE WriteWithoutResponse flow control.
otaHostChunkPaceBLE = 6 * time.Millisecond
otaBlockMaxRetries = 3
// 200 B OTA_PAYLOAD → ~216 B framed; needs ATT MTU >= 219.
otaMinATTMTU = 219
)
const (
@@ -37,6 +41,14 @@ const (
otaDistTimeout = 45 * time.Minute
)
// otaTransport is the host IO used by the OTA protocol (UART or BLE).
type otaTransport interface {
writeMsg(msg *pb.UartMessage) error
readFrame(timeout time.Duration) ([]byte, error)
flushInput()
chunkPace() time.Duration
}
// OtaSlaveDetail is per-slave ESP-NOW OTA state from OTA_SLAVE_PROGRESS.
type OtaSlaveDetail struct {
BytesWritten uint32 `json:"bytes_written"`
@@ -47,20 +59,20 @@ type OtaSlaveDetail struct {
// OTAProgress is pushed to the dashboard during web uploads.
type OTAProgress struct {
Type string `json:"type"` // always "ota_progress"
Phase string `json:"phase"`
Step string `json:"step,omitempty"` // master, slaves
Percent int `json:"percent"`
MasterPercent int `json:"master_percent,omitempty"`
MasterDone bool `json:"master_done,omitempty"`
Message string `json:"message"`
MasterMessage string `json:"master_message,omitempty"`
Bytes uint32 `json:"bytes_written,omitempty"`
Slot uint32 `json:"target_slot,omitempty"`
Slaves uint32 `json:"slaves,omitempty"`
ImageSize uint32 `json:"image_size,omitempty"`
SlaveProgress map[uint32]uint32 `json:"slave_progress,omitempty"` // client_id -> bytes
SlaveDetails map[uint32]OtaSlaveDetail `json:"slave_details,omitempty"`
Type string `json:"type"` // always "ota_progress"
Phase string `json:"phase"`
Step string `json:"step,omitempty"` // master, slaves
Percent int `json:"percent"`
MasterPercent int `json:"master_percent,omitempty"`
MasterDone bool `json:"master_done,omitempty"`
Message string `json:"message"`
MasterMessage string `json:"master_message,omitempty"`
Bytes uint32 `json:"bytes_written,omitempty"`
Slot uint32 `json:"target_slot,omitempty"`
Slaves uint32 `json:"slaves,omitempty"`
ImageSize uint32 `json:"image_size,omitempty"`
SlaveProgress map[uint32]uint32 `json:"slave_progress,omitempty"` // client_id -> bytes
SlaveDetails map[uint32]OtaSlaveDetail `json:"slave_details,omitempty"`
}
type otaProgressFn func(OTAProgress)
@@ -80,27 +92,52 @@ func runOTAUpload(m *managedSerial, firmware []byte, onProgress otaProgressFn) e
Percent: 0, Message: msg, MasterMessage: msg,
})
}
push("preparing", "UART wird vorbereitet…")
push("preparing", "Host wird für OTA 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.
// Hold m.mu for the entire upload so dashboard/API polling cannot interleave.
m.mu.Lock()
if m.otaActive {
m.mu.Unlock()
return errOTAInProgress
}
m.otaActive = true
if m.sp == nil {
if err := m.openLocked(); err != nil {
var t otaTransport
switch m.transport {
case hostTransportBLE:
if m.ble == nil {
m.otaActive = false
m.mu.Unlock()
err := fmt.Errorf("BLE not connected")
push("error", err.Error())
return err
}
if err := m.ble.ensureOTAMTU(); err != nil {
m.otaActive = false
m.mu.Unlock()
push("error", err.Error())
return err
}
t = m.ble
case hostTransportUART, hostTransportNone:
if m.sp == nil {
if err := m.openLocked(); err != nil {
m.otaActive = false
m.mu.Unlock()
push("error", err.Error())
return err
}
}
t = m.sp
default:
m.otaActive = false
m.mu.Unlock()
err := fmt.Errorf("not connected")
push("error", err.Error())
return err
}
sp := m.sp
err := runOTAOnPortUnlocked(sp, firmware, onProgress)
err := runOTAUnlocked(t, firmware, onProgress)
if err != nil {
m.invalidateLocked(err)
}
@@ -109,7 +146,7 @@ func runOTAUpload(m *managedSerial, firmware []byte, onProgress otaProgressFn) e
return err
}
func runOTAOnPortUnlocked(sp *serialPort, firmware []byte, onProgress otaProgressFn) error {
func runOTAUnlocked(t otaTransport, firmware []byte, onProgress otaProgressFn) error {
if len(firmware) == 0 {
return fmt.Errorf("empty firmware")
}
@@ -151,16 +188,11 @@ func runOTAOnPortUnlocked(sp *serialPort, firmware []byte, onProgress otaProgres
onProgress(p)
}
if err := sp.port.SetReadTimeout(readTimeout); err != nil {
notify("error", "", 0, err.Error())
return err
}
notify("preparing", otaStepMaster, 0, fmt.Sprintf("Master: OTA start (%d bytes)…", imageSize))
flushSerialInput(sp)
t.flushInput()
if err := writeUartMessage(sp, &pb.UartMessage{
if err := t.writeMsg(&pb.UartMessage{
Type: pb.MessageType_OTA_START,
Payload: &pb.UartMessage_OtaStart{
OtaStart: &pb.OtaStartPayload{TotalSize: uint32(imageSize)},
@@ -170,13 +202,7 @@ func runOTAOnPortUnlocked(sp *serialPort, firmware []byte, onProgress otaProgres
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) {
ready, err := waitOtaStatus(t, otaStReady, otaPrepareTimeout, func(msg string) {
notify("preparing", otaStepMaster, 2, msg)
})
if err != nil {
@@ -185,11 +211,7 @@ func runOTAOnPortUnlocked(sp *serialPort, firmware []byte, onProgress otaProgres
}
notify("ready", otaStepMaster, 5, fmt.Sprintf("Master: Slot %d bereit", ready.GetTargetSlot()))
if err := sp.port.SetReadTimeout(otaDefaultTimeout); err != nil {
notify("error", "", 0, err.Error())
return err
}
pace := t.chunkPace()
var seq uint32
for offset := 0; offset < imageSize; {
blockStart := offset
@@ -208,7 +230,7 @@ func runOTAOnPortUnlocked(sp *serialPort, firmware []byte, onProgress otaProgres
}
chunk := firmware[offset : offset+n]
if err := writeUartMessage(sp, &pb.UartMessage{
if err := t.writeMsg(&pb.UartMessage{
Type: pb.MessageType_OTA_PAYLOAD,
Payload: &pb.UartMessage_OtaPayload{
OtaPayload: &pb.OtaPayload{Seq: seq, Data: chunk},
@@ -216,7 +238,7 @@ func runOTAOnPortUnlocked(sp *serialPort, firmware []byte, onProgress otaProgres
}); err != nil {
return false, err
}
time.Sleep(otaHostChunkPace)
time.Sleep(pace)
seq++
offset += n
bytesInBlock += n
@@ -252,7 +274,7 @@ func runOTAOnPortUnlocked(sp *serialPort, firmware []byte, onProgress otaProgres
return err
}
}
st, ackErr = waitOtaStatus(sp, otaStBlockAck, otaDefaultTimeout, nil)
st, ackErr = waitOtaStatus(t, otaStBlockAck, otaDefaultTimeout, nil)
if ackErr == nil {
break
}
@@ -272,10 +294,10 @@ func runOTAOnPortUnlocked(sp *serialPort, firmware []byte, onProgress otaProgres
}
masterPct = 100
masterMsg = "Master: UART-Upload abgeschlossen"
masterMsg = "Master: Upload abgeschlossen"
notify("uploading", otaStepMaster, 100, masterMsg)
if err := writeUartMessage(sp, &pb.UartMessage{
if err := t.writeMsg(&pb.UartMessage{
Type: pb.MessageType_OTA_END,
Payload: &pb.UartMessage_OtaEnd{
OtaEnd: &pb.OtaEndPayload{},
@@ -314,7 +336,8 @@ func runOTAOnPortUnlocked(sp *serialPort, firmware []byte, onProgress otaProgres
}
}
if slaveCount == 0 {
return "Keine verfügbaren Slaves — Verteilung übersprungen", 0, 0
// Do not claim "skipped" mid-flight — details often arrive after the first query.
return "ESP-NOW: warte auf Slave-Fortschritt…", 0, 0
}
return fmt.Sprintf("ESP-NOW: %d / %d bytes (%d Slaves)",
aggBytes, imageSize, slaveCount), aggBytes, slaveCount
@@ -334,7 +357,7 @@ func runOTAOnPortUnlocked(sp *serialPort, firmware []byte, onProgress otaProgres
return
}
lastQuery = time.Now()
prog, err := queryOtaSlaveProgressLocked(sp, 0, onDistStatus, otaDistQueryTimeout)
prog, err := queryOtaSlaveProgress(t, 0, onDistStatus, otaDistQueryTimeout)
if err != nil {
if len(slaveDetails) > 0 {
emitSlaveThrottled(true)
@@ -356,13 +379,13 @@ func runOTAOnPortUnlocked(sp *serialPort, firmware []byte, onProgress otaProgres
lastQuery = time.Time{} // first query immediately when distribution starts
querySlaveProgress()
st, err := waitOtaComplete(sp, otaDistTimeout, pushSlaveDist, onWaitTick, otaDistReadTimeout)
st, err := waitOtaComplete(t, otaDistTimeout, pushSlaveDist, onWaitTick, otaDistReadTimeout)
if err != nil {
notify("error", "", 0, err.Error())
return err
}
if prog, err := queryOtaSlaveProgressLocked(sp, 0, nil, otaDistQueryTimeout); err == nil {
if prog, err := queryOtaSlaveProgress(t, 0, nil, otaDistQueryTimeout); err == nil {
mergeSlaveProgressResponse(prog, slaveBytes, slaveDetails)
}
notify("done", "", 100,
@@ -378,13 +401,11 @@ func runOTAOnPortUnlocked(sp *serialPort, firmware []byte, onProgress otaProgres
}
// QueryOtaSlaveProgress queries the master for per-slave ESP-NOW OTA progress.
func QueryOtaSlaveProgress(sp *serialPort, clientID uint32) (*pb.OtaSlaveProgressResponse, error) {
sp.mu.Lock()
defer sp.mu.Unlock()
return queryOtaSlaveProgressLocked(sp, clientID, nil, otaDefaultTimeout)
func QueryOtaSlaveProgress(t otaTransport, clientID uint32) (*pb.OtaSlaveProgressResponse, error) {
return queryOtaSlaveProgress(t, clientID, nil, otaDefaultTimeout)
}
func queryOtaSlaveProgressLocked(sp *serialPort, clientID uint32,
func queryOtaSlaveProgress(t otaTransport, clientID uint32,
onStatus func(*pb.OtaStatusPayload), queryTimeout time.Duration) (*pb.OtaSlaveProgressResponse, error) {
req := &pb.UartMessage{
Type: pb.MessageType_OTA_SLAVE_PROGRESS,
@@ -394,14 +415,14 @@ func queryOtaSlaveProgressLocked(sp *serialPort, clientID uint32,
},
},
}
if err := writeUartMessage(sp, req); err != nil {
if err := t.writeMsg(req); err != nil {
return nil, err
}
if queryTimeout <= 0 {
queryTimeout = otaDefaultTimeout
}
deadline := time.Now().Add(queryTimeout)
msg, err := readUartMessageUntil(sp, deadline, pb.MessageType_OTA_SLAVE_PROGRESS, onStatus, otaDistReadTimeout)
msg, err := readUartMessageUntil(t, deadline, pb.MessageType_OTA_SLAVE_PROGRESS, onStatus, otaDistReadTimeout)
if err != nil {
return nil, err
}
@@ -434,7 +455,7 @@ func applyDistributingOtaStatus(st *pb.OtaStatusPayload, imageSize int,
details[id] = d
}
func readUartMessageUntil(sp *serialPort, deadline time.Time, want pb.MessageType,
func readUartMessageUntil(t otaTransport, deadline time.Time, want pb.MessageType,
onStatus func(*pb.OtaStatusPayload), readChunk time.Duration) (*pb.UartMessage, error) {
if readChunk <= 0 {
readChunk = otaStatusPollTimeout
@@ -447,10 +468,7 @@ func readUartMessageUntil(sp *serialPort, deadline time.Time, want pb.MessageTyp
if wait > readChunk {
wait = readChunk
}
if err := sp.port.SetReadTimeout(wait); err != nil {
return nil, err
}
payload, err := uartframe.ReadFrame(sp.port, nil, wait)
payload, err := t.readFrame(wait)
if err != nil {
return nil, err
}
@@ -505,7 +523,7 @@ func copySlaveMap(m map[uint32]uint32) map[uint32]uint32 {
return out
}
func waitOtaComplete(sp *serialPort, timeout time.Duration,
func waitOtaComplete(t otaTransport, timeout time.Duration,
onDistributing func(*pb.OtaStatusPayload), onInterval func(),
readTimeout time.Duration) (*pb.OtaStatusPayload, error) {
if readTimeout <= 0 {
@@ -520,10 +538,7 @@ func waitOtaComplete(sp *serialPort, timeout time.Duration,
if readWait > readTimeout {
readWait = readTimeout
}
if err := sp.port.SetReadTimeout(readWait); err != nil {
return nil, err
}
st, err := readOtaStatus(sp)
st, err := readOtaStatus(t, readWait)
if err != nil {
if onInterval != nil {
onInterval()
@@ -548,16 +563,7 @@ func waitOtaComplete(sp *serialPort, timeout time.Duration,
}
}
func writeUartMessage(sp *serialPort, msg *pb.UartMessage) error {
frame, err := encodeUartMessage(msg)
if err != nil {
return err
}
_, err = sp.port.Write(frame)
return err
}
func waitOtaStatus(sp *serialPort, want uint32, timeout time.Duration, onPreparing func(string)) (*pb.OtaStatusPayload, error) {
func waitOtaStatus(t otaTransport, want uint32, timeout time.Duration, onPreparing func(string)) (*pb.OtaStatusPayload, error) {
deadline := time.Now().Add(timeout)
for {
if time.Now().After(deadline) {
@@ -567,10 +573,7 @@ func waitOtaStatus(sp *serialPort, want uint32, timeout time.Duration, onPrepari
if readWait > otaStatusPollTimeout {
readWait = otaStatusPollTimeout
}
if err := sp.port.SetReadTimeout(readWait); err != nil {
return nil, err
}
payload, err := uartframe.ReadFrame(sp.port, nil, readWait)
payload, err := t.readFrame(readWait)
if err != nil {
continue
}
@@ -595,8 +598,8 @@ func waitOtaStatus(sp *serialPort, want uint32, timeout time.Duration, onPrepari
}
}
func readOtaStatus(sp *serialPort) (*pb.OtaStatusPayload, error) {
payload, err := uartframe.ReadFrame(sp.port, nil, readTimeout)
func readOtaStatus(t otaTransport, timeout time.Duration) (*pb.OtaStatusPayload, error) {
payload, err := t.readFrame(timeout)
if err != nil {
return nil, fmt.Errorf("read response: %w", err)
}
@@ -623,8 +626,37 @@ 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) {
func decodeUartPayload(payload []byte) (*pb.UartMessage, error) {
if len(payload) == 0 {
return nil, fmt.Errorf("empty response")
}
var msg pb.UartMessage
if err := proto.Unmarshal(payload[1:], &msg); err != nil {
return nil, err
}
msg.Type = pb.MessageType(payload[0])
return &msg, nil
}
// --- UART otaTransport ---
func (sp *serialPort) writeMsg(msg *pb.UartMessage) error {
frame, err := encodeUartMessage(msg)
if err != nil {
return err
}
_, err = sp.port.Write(frame)
return err
}
func (sp *serialPort) readFrame(timeout time.Duration) ([]byte, error) {
if err := sp.port.SetReadTimeout(timeout); err != nil {
return nil, err
}
return uartframe.ReadFrame(sp.port, nil, timeout)
}
func (sp *serialPort) flushInput() {
if sp == nil {
return
}
@@ -639,14 +671,4 @@ func flushSerialInput(sp *serialPort) {
}
}
func decodeUartPayload(payload []byte) (*pb.UartMessage, error) {
if len(payload) == 0 {
return nil, fmt.Errorf("empty response")
}
var msg pb.UartMessage
if err := proto.Unmarshal(payload[1:], &msg); err != nil {
return nil, err
}
msg.Type = pb.MessageType(payload[0])
return &msg, nil
}
func (sp *serialPort) chunkPace() time.Duration { return otaHostChunkPaceUART }
+22 -25
View File
@@ -788,23 +788,23 @@
</div>
</section>
<section class="col-12" x-show="isUARTHost() || !hostUp()">
<section class="col-12">
<div class="card">
<div class="card-header">Firmware OTA (A/B)</div>
<div class="card-body">
<p class="text-muted small mb-3">
Lädt eine <code>.bin</code> auf den Master (nur UART-Host, DIP8=0), danach verteilt die Firmware automatisch per ESP-NOW an alle verfügbaren Slaves.
Während des Uploads pausiert das Live-Polling. Bei BLE-Host ist OTA nicht verfügbar.
Lädt eine <code>.bin</code> auf den Master (UART oder BLE), danach verteilt die Firmware automatisch per ESP-NOW an alle verfügbaren Slaves.
Während des Uploads pausiert das Live-Polling. BLE braucht ATT MTU ≥ 219.
</p>
<div class="mb-3">
<input type="file" class="form-control form-control-sm" accept=".bin,application/octet-stream"
@change="otaFile = $event.target.files[0]"
:disabled="ota.active || busy || !isUARTHost()">
:disabled="ota.active || busy || !hostUp()">
</div>
<div class="d-flex flex-wrap gap-2 align-items-center mb-2">
<button type="button" class="btn btn-primary btn-sm"
@click="uploadOTA()"
:disabled="ota.active || busy || !isUARTHost() || !otaFile">
:disabled="ota.active || busy || !hostUp() || !otaFile">
Firmware hochladen
</button>
<span class="text-muted small" x-show="otaFile"
@@ -812,7 +812,7 @@
</div>
<div class="ota-progress-panel mt-3"
x-show="ota.active || ota.phase === 'distributing' || ota.phase === 'done' || ota.phase === 'error'">
<p class="small text-muted mb-2">Master (UART)</p>
<p class="small text-muted mb-2">Master (Host)</p>
<table class="table table-sm pp-table ota-progress-table mb-3">
<tbody>
<tr>
@@ -845,7 +845,8 @@
Slaves (ESP-NOW)
<span x-text="'(' + otaSlaveRows().length + ')'"></span>
</p>
<p class="small text-muted mb-2" x-show="ota.message && ota.step === 'slaves'"
<p class="small text-muted mb-2"
x-show="ota.step === 'slaves' && otaSlaveRows().length > 0 && ota.message"
x-text="ota.message"></p>
<table class="table table-sm pp-table ota-progress-table mb-2">
<tbody>
@@ -877,9 +878,8 @@
</tr>
</template>
<tr x-show="otaSlaveRows().length === 0">
<td colspan="2" class="text-muted small py-3">
Warte auf Slave-Fortschritt…
</td>
<td colspan="2" class="text-muted small py-3"
x-text="otaDistEmptyHint()"></td>
</tr>
</tbody>
</table>
@@ -1422,7 +1422,19 @@
if (status === 1 || status === 2) return 'bg-secondary';
return 'bg-info';
},
otaDistEmptyHint() {
const msg = this.ota.message || '';
if (/übersprungen/i.test(msg)) {
return msg;
}
if (this.ota.phase === 'done') {
return 'Keine Slave-Verteilung gemeldet.';
}
return 'Warte auf Slave-Fortschritt…';
},
otaSlaveRows() {
// Only real OTA progress — do not invent placeholder rows from the
// client list (that contradicted "keine Slaves / übersprungen").
const details = this.mergeSlaveDetails(this.ota.slaveDetails);
const ids = new Set(Object.keys(details).map(Number).filter(id => !Number.isNaN(id)));
for (const [k, v] of Object.entries(this.ota.slaveProgress || {})) {
@@ -1454,21 +1466,6 @@
bytesLabel: total ? `${bytes} / ${total} bytes` : `${bytes} bytes`
});
}
if (rows.length === 0 && (this.ota.phase === 'distributing' || this.ota.step === 'slaves')) {
const targets = (this.state.clients || []).filter(c => c.available);
const limit = this.ota.slaves > 0 ? this.ota.slaves : targets.length;
for (const c of targets.slice(0, limit)) {
rows.push({
id: c.id,
mac: c.mac ? this.formatMac(c.mac) : '—',
percent: 0,
status: 1,
statusLabel: this.otaSlaveStatusLabel(1),
barClass: this.otaSlaveBarClass(1),
bytesLabel: '—'
});
}
}
return rows;
},
applyOTAProgress(p) {
+4 -1
View File
@@ -6,6 +6,7 @@
#include "esp_timer.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "ota_session.h"
#include "pb_encode.h"
#include "uart.h"
#include "uart_cmd.h"
@@ -146,7 +147,9 @@ static void ble_stream_task(void *param) {
period = BLE_STREAM_PERIOD_MAX_MS;
}
if (ble_host_ready() && any_accel_stream()) {
/* Pause ticks during host OTA / ESP-NOW distribute so ATT bandwidth
* stays available for OTA_STATUS notifies. */
if (ble_host_ready() && any_accel_stream() && !ota_session_busy()) {
alox_UartMessage msg = alox_UartMessage_init_zero;
uart_cmd_init_response(&msg, alox_MessageType_BLE_STREAM_TICK,
alox_UartMessage_ble_stream_tick_tag);
+2 -19
View File
@@ -1,5 +1,4 @@
#include "cmd_ota.h"
#include "app_config.h"
#include "led_ring.h"
#include "ota_espnow.h"
#include "ota_uart.h"
@@ -14,6 +13,8 @@ static const char *TAG = "[OTA_CMD]";
#define OTA_PREPARE_STACK 8192
#define OTA_PREPARE_PRIO 5
/* Block buffer is static .bss; stack is for nested EspNowMessage + encode buf +
* esp_now_send path. 4 KiB overflowed; 8 KiB matches the pre-crash budget. */
#define OTA_DIST_STACK 8192
#define OTA_DIST_PRIO 5
@@ -104,24 +105,10 @@ static void ota_prepare_task(void *param) {
vTaskDelete(NULL);
}
static bool ota_host_is_uart(void) {
const app_config_t *cfg = app_config_get();
if (cfg != NULL && cfg->host_ble) {
ESP_LOGW(TAG, "OTA requires UART host (DIP8=0); BLE host active");
return false;
}
return true;
}
static void handle_ota_start(const uint8_t *data, size_t len) {
alox_UartMessage uart_msg;
alox_OtaStartPayload req = alox_OtaStartPayload_init_zero;
if (!ota_host_is_uart()) {
send_ota_failed(20);
return;
}
if (uart_cmd_decode(data, len, &uart_msg) != ESP_OK) {
send_ota_failed( 2);
return;
@@ -349,10 +336,6 @@ static void ota_start_espnow_task(void *param) {
}
static void handle_ota_start_espnow(const uint8_t *data, size_t len) {
if (!ota_host_is_uart()) {
send_ota_failed(20);
return;
}
(void)data;
(void)len;
+105 -14
View File
@@ -28,6 +28,12 @@ static const char *TAG = "[OTA_ESPNOW]";
#define OTA_ST_SUCCESS 4u
#define OTA_ST_FAILED 5u
/**
* Event-group fail bit. FreeRTOS reserves the top 8 bits (assert if used);
* targets use bits 0..CLIENT_REGISTRY_MAX-1 (max 16).
*/
#define OTA_EG_FAIL_BIT (1u << 16)
/** ESP-NOW OTA receive on slave (blue progress bar). */
#define OTA_LED_ESPNOW_RX_R 0
#define OTA_LED_ESPNOW_RX_G 0
@@ -58,6 +64,7 @@ typedef struct {
static EventGroupHandle_t s_eg;
static QueueHandle_t s_slave_work_queue;
static bool s_distribution_active;
static TaskHandle_t s_slave_prep_task;
typedef struct {
uint8_t count;
@@ -70,6 +77,9 @@ typedef struct {
static ota_dist_t s_dist;
/** Always-reserved flash block for ESP-NOW distribute (not on task stack). */
static uint8_t s_ota_block_buf[OTA_UART_FLASH_BLOCK_SIZE];
typedef struct {
uint32_t client_id;
uint32_t bytes_written;
@@ -164,14 +174,46 @@ static uint32_t all_target_bits(void) {
return (1u << s_dist.count) - 1u;
}
static bool any_target_failed(void) {
for (uint8_t i = 0; i < s_prog.count; i++) {
if (s_prog.entries[i].status == OTA_ST_FAILED) {
return true;
}
}
return false;
}
/**
* Wait until all want_bits are set, or OTA_EG_FAIL_BIT (slave FAILED).
* Returns true only when every target bit is set; false on fail or timeout.
*/
static bool wait_target_bits(uint32_t want_bits, uint32_t timeout_ms) {
if (s_eg == NULL || want_bits == 0) {
return false;
}
EventBits_t got =
xEventGroupWaitBits(s_eg, want_bits, pdTRUE, pdTRUE,
pdMS_TO_TICKS(timeout_ms));
return (got & want_bits) == want_bits;
const TickType_t start = xTaskGetTickCount();
const TickType_t timeout_ticks = pdMS_TO_TICKS(timeout_ms);
const EventBits_t wait_mask = want_bits | OTA_EG_FAIL_BIT;
while (true) {
TickType_t elapsed = xTaskGetTickCount() - start;
if (elapsed >= timeout_ticks) {
return false;
}
TickType_t remain = timeout_ticks - elapsed;
EventBits_t got =
xEventGroupWaitBits(s_eg, wait_mask, pdFALSE, pdFALSE, remain);
if (got & OTA_EG_FAIL_BIT) {
xEventGroupClearBits(s_eg, OTA_EG_FAIL_BIT);
return false;
}
if ((got & want_bits) == want_bits) {
xEventGroupClearBits(s_eg, want_bits);
return true;
}
}
}
static uint32_t block_ack_timeout_ms(void) {
@@ -372,11 +414,21 @@ void ota_espnow_slave_init(void) {
}
}
static void drain_slave_work_queue(void) {
if (s_slave_work_queue == NULL) {
return;
}
ota_slave_work_t junk;
while (xQueueReceive(s_slave_work_queue, &junk, 0) == pdTRUE) {
}
}
static void ota_slave_prepare_task(void *param) {
uint32_t total_size = (uint32_t)(uintptr_t)param;
uint8_t master_mac[6];
if (!esp_now_comm_get_master_mac(master_mac)) {
s_slave_prep_task = NULL;
vTaskDelete(NULL);
return;
}
@@ -386,6 +438,7 @@ static void ota_slave_prepare_task(void *param) {
int slot = ota_uart_prepare(total_size);
if (slot < 0) {
send_slave_status(master_mac, OTA_ST_FAILED, 0, 1);
s_slave_prep_task = NULL;
vTaskDelete(NULL);
return;
}
@@ -393,6 +446,7 @@ static void ota_slave_prepare_task(void *param) {
send_slave_status(master_mac, OTA_ST_READY, 0, 0);
led_ring_show_ota_progress(0, total_size, OTA_LED_ESPNOW_RX_R, OTA_LED_ESPNOW_RX_G,
OTA_LED_ESPNOW_RX_B);
s_slave_prep_task = NULL;
vTaskDelete(NULL);
}
@@ -404,14 +458,31 @@ void ota_espnow_slave_on_start(const uint8_t master_mac[6],
ESP_LOGI(TAG, "ESP-NOW OTA_START (%lu bytes)", (unsigned long)start->total_size);
if (ota_uart_is_active()) {
queue_slave_status(master_mac, OTA_ST_FAILED, 0, 4);
/* Duplicate START while prepare task still running — ignore. */
if (s_slave_prep_task != NULL) {
ESP_LOGW(TAG, "OTA prepare already in flight — ignore START");
return;
}
if (ota_uart_is_active()) {
/* Same image, nothing written yet: sticky READY after master timeout /
* duplicate START — re-ACK instead of failing with err=4. */
if (ota_uart_total_size() == start->total_size &&
ota_uart_bytes_received() == 0) {
ESP_LOGI(TAG, "OTA already prepared for %lu bytes — re-send READY",
(unsigned long)start->total_size);
queue_slave_status(master_mac, OTA_ST_READY, 0, 0);
return;
}
ESP_LOGW(TAG, "aborting sticky OTA session for new START");
ota_uart_abort();
drain_slave_work_queue();
}
if (xTaskCreate(ota_slave_prepare_task, "ota_esp_prep", OTA_ESPNOW_PREPARE_STACK,
(void *)(uintptr_t)start->total_size, OTA_ESPNOW_PREPARE_PRIO,
NULL) != pdPASS) {
&s_slave_prep_task) != pdPASS) {
s_slave_prep_task = NULL;
queue_slave_status(master_mac, OTA_ST_FAILED, 0, 5);
}
}
@@ -479,6 +550,8 @@ void ota_espnow_master_on_status(const uint8_t slave_mac[6],
status->error);
ESP_LOGW(TAG, "slave %lu OTA failed (err=%lu)",
(unsigned long)s_dist.id[idx], (unsigned long)status->error);
/* Wake wait_target_bits so distribute aborts instead of waiting for READY. */
xEventGroupSetBits(s_eg, OTA_EG_FAIL_BIT);
break;
default:
break;
@@ -527,7 +600,7 @@ static esp_err_t distribute_image(const esp_partition_t *partition,
uint32_t target_mask = all_target_bits();
esp_err_t err;
xEventGroupClearBits(s_eg, target_mask);
xEventGroupClearBits(s_eg, target_mask | OTA_EG_FAIL_BIT);
for (uint8_t i = 0; i < s_dist.count; i++) {
err = esp_now_comm_send_ota_start(s_dist.mac[i], size);
if (err != ESP_OK) {
@@ -540,6 +613,12 @@ static esp_err_t distribute_image(const esp_partition_t *partition,
}
if (!wait_target_bits(target_mask, OTA_PREPARE_TIMEOUT_MS)) {
if (any_target_failed()) {
ESP_LOGE(TAG, "slave OTA failed during prepare");
prog_end();
s_distribution_active = false;
return ESP_FAIL;
}
ESP_LOGE(TAG, "timeout waiting for slave OTA ready");
prog_end();
s_distribution_active = false;
@@ -551,7 +630,6 @@ static esp_err_t distribute_image(const esp_partition_t *partition,
s_dist.progress.aggregate(0, size, s_dist.count);
}
uint8_t block_buf[OTA_UART_FLASH_BLOCK_SIZE];
uint32_t offset = 0;
uint32_t seq = 0;
@@ -561,7 +639,7 @@ static esp_err_t distribute_image(const esp_partition_t *partition,
block_len = OTA_UART_FLASH_BLOCK_SIZE;
}
err = esp_partition_read(partition, offset, block_buf, block_len);
err = esp_partition_read(partition, offset, s_ota_block_buf, block_len);
if (err != ESP_OK) {
ESP_LOGE(TAG, "partition read @%lu failed: %s", (unsigned long)offset,
esp_err_to_name(err));
@@ -591,7 +669,7 @@ static esp_err_t distribute_image(const esp_partition_t *partition,
(unsigned long)send_attempt,
(unsigned long)OTA_BLOCK_MAX_RETRIES);
}
err = send_block_payloads(block_buf, block_len, &seq);
err = send_block_payloads(s_ota_block_buf, block_len, &seq);
if (err == ESP_OK) {
block_sent = true;
break;
@@ -614,6 +692,13 @@ static esp_err_t distribute_image(const esp_partition_t *partition,
acked = true;
break;
}
if (any_target_failed()) {
ESP_LOGE(TAG, "slave OTA failed during block @%lu",
(unsigned long)s_dist.expected_bytes);
prog_end();
s_distribution_active = false;
return ESP_FAIL;
}
log_missing_block_acks(s_dist.expected_bytes);
if (attempt >= OTA_BLOCK_MAX_RETRIES) {
break;
@@ -622,9 +707,9 @@ static esp_err_t distribute_image(const esp_partition_t *partition,
(unsigned long)s_dist.expected_bytes,
(unsigned long)(attempt + 1),
(unsigned long)OTA_BLOCK_MAX_RETRIES);
xEventGroupClearBits(s_eg, target_mask);
xEventGroupClearBits(s_eg, target_mask | OTA_EG_FAIL_BIT);
seq = block_start_seq;
err = send_block_payloads(block_buf, block_len, &seq);
err = send_block_payloads(s_ota_block_buf, block_len, &seq);
if (err != ESP_OK) {
prog_end();
s_distribution_active = false;
@@ -653,7 +738,7 @@ static esp_err_t distribute_image(const esp_partition_t *partition,
}
}
xEventGroupClearBits(s_eg, target_mask);
xEventGroupClearBits(s_eg, target_mask | OTA_EG_FAIL_BIT);
for (uint8_t i = 0; i < s_dist.count; i++) {
err = esp_now_comm_send_ota_end(s_dist.mac[i]);
if (err != ESP_OK) {
@@ -664,6 +749,12 @@ static esp_err_t distribute_image(const esp_partition_t *partition,
}
if (!wait_target_bits(target_mask, OTA_END_TIMEOUT_MS)) {
if (any_target_failed()) {
ESP_LOGE(TAG, "slave OTA failed during end");
prog_end();
s_distribution_active = false;
return ESP_FAIL;
}
ESP_LOGE(TAG, "timeout waiting for slave OTA success");
prog_end();
s_distribution_active = false;
+1 -1
View File
@@ -407,7 +407,7 @@ message TapConfigResponse {
bool enable_triple = 12;
}
// Host → device: begin UART OTA (erase inactive OTA slot; device replies OTA_STATUS).
// Host → device: begin host OTA over UART or BLE (erase inactive OTA slot; device replies OTA_STATUS).
message OtaStartPayload {
uint32 total_size = 1;
}