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:
@@ -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).
|
||||||
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -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 1 | 1 | `board_input.c` |
|
||||||
| LiPo ADC 2 | 11 | `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.
|
**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 |
|
| 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 |
|
| 7 | ESPNOW_UNICAST_TEST | `cmd_espnow_unicast_test.c` | Link-Test zu Slave |
|
||||||
| 8 | LED_RING | `cmd_led_ring.c` | Ring lokal / ESP-NOW |
|
| 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 |
|
| 17 | OTA_PAYLOAD | `cmd_ota.c` | Chunk ≤200 B |
|
||||||
| 18 | OTA_END | `cmd_ota.c` | Abschluss + ESP-NOW-Verteilung |
|
| 18 | OTA_END | `cmd_ota.c` | Abschluss + ESP-NOW-Verteilung |
|
||||||
| 19 | OTA_STATUS | `cmd_ota.c` | Gerät → Host Status |
|
| 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. 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 |
|
| 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()`.
|
- Inaktive Partition: `esp_ota_get_next_update_partition()`.
|
||||||
- LED-Ring zeigt Fortschritt (blau Schreiben, grün Verteilung).
|
- 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)
|
### 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`
|
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. `ESPNOW_OTA_PAYLOAD` bis 200 B → 4 KiB Buffer auf Slave
|
||||||
|
|||||||
+2
-1
@@ -61,7 +61,7 @@ See [`testdata/README.md`](testdata/README.md) for the JSON schema.
|
|||||||
|
|
||||||
### Web dashboard
|
### 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
|
```bash
|
||||||
go run . serve
|
go run . serve
|
||||||
@@ -110,6 +110,7 @@ go run . -port /dev/ttyUSB0 cache-status
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
go run . -port /dev/ttyUSB0 ota build/powerpod.bin
|
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.
|
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
@@ -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) {
|
func serveOTAUpload(w http.ResponseWriter, r *http.Request, link *managedSerial, hub *wsHub) {
|
||||||
if link.Transport() != hostTransportUART || !link.IsConnected() {
|
if !link.IsConnected() {
|
||||||
writeJSON(w, http.StatusBadRequest, otaAPIResponse{Error: "OTA requires UART host (DIP8=0)"})
|
writeJSON(w, http.StatusBadRequest, otaAPIResponse{Error: "host not connected"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err := r.ParseMultipartForm(otaMaxFirmwareSize); err != nil {
|
if err := r.ParseMultipartForm(otaMaxFirmwareSize); err != nil {
|
||||||
|
|||||||
+79
-4
@@ -26,6 +26,7 @@ type bleLink struct {
|
|||||||
respCh chan []byte
|
respCh chan []byte
|
||||||
streamCh chan []byte
|
streamCh chan []byte
|
||||||
quiet bool
|
quiet bool
|
||||||
|
attMTU int // negotiated ATT MTU (0 if unknown)
|
||||||
}
|
}
|
||||||
|
|
||||||
func openBLE(name string, settle time.Duration) (*bleLink, error) {
|
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]
|
char := chars[0]
|
||||||
|
|
||||||
|
attMTU := 0
|
||||||
if mtu, err := char.GetMTU(); err == nil {
|
if mtu, err := char.GetMTU(); err == nil {
|
||||||
log.Printf("BLE ATT MTU=%d", mtu)
|
attMTU = int(mtu)
|
||||||
if mtu < 203 {
|
log.Printf("BLE ATT MTU=%d", attMTU)
|
||||||
log.Printf("warning: MTU %d < 203 — large frames/stream ticks may fail", mtu)
|
if attMTU < otaMinATTMTU {
|
||||||
|
log.Printf("warning: MTU %d < %d — OTA (200 B chunks) and large stream ticks may fail",
|
||||||
|
attMTU, otaMinATTMTU)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
link := &bleLink{
|
link := &bleLink{
|
||||||
device: device,
|
device: device,
|
||||||
char: char,
|
char: char,
|
||||||
parser: uartframe.NewParser(),
|
parser: uartframe.NewParser(),
|
||||||
respCh: make(chan []byte, 8),
|
respCh: make(chan []byte, 8),
|
||||||
streamCh: make(chan []byte, 64),
|
streamCh: make(chan []byte, 64),
|
||||||
|
attMTU: attMTU,
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := char.EnableNotifications(func(buf []byte) {
|
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")
|
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
@@ -5,7 +5,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
)
|
)
|
||||||
|
|
||||||
func runOTA(sp *serialPort, args []string) error {
|
func runOTA(t otaTransport, args []string) error {
|
||||||
if len(args) < 1 {
|
if len(args) < 1 {
|
||||||
return fmt.Errorf("usage: ota <firmware.bin>")
|
return fmt.Errorf("usage: ota <firmware.bin>")
|
||||||
}
|
}
|
||||||
@@ -14,9 +14,17 @@ func runOTA(sp *serialPort, args []string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
sp.mu.Lock()
|
switch x := t.(type) {
|
||||||
defer sp.mu.Unlock()
|
case *serialPort:
|
||||||
return runOTAOnPortUnlocked(sp, data, func(p OTAProgress) {
|
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 {
|
switch p.Phase {
|
||||||
case "preparing", "ready":
|
case "preparing", "ready":
|
||||||
fmt.Println(p.Message)
|
fmt.Println(p.Message)
|
||||||
|
|||||||
@@ -5,14 +5,19 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
)
|
)
|
||||||
|
|
||||||
func runOtaProgress(sp *serialPort, args []string) error {
|
func runOtaProgress(t otaTransport, args []string) error {
|
||||||
fs := flag.NewFlagSet("ota-progress", flag.ExitOnError)
|
fs := flag.NewFlagSet("ota-progress", flag.ExitOnError)
|
||||||
clientID := fs.Uint("client", 0, "slave client id (0 = all in session)")
|
clientID := fs.Uint("client", 0, "slave client id (0 = all in session)")
|
||||||
if err := fs.Parse(args); err != nil {
|
if err := fs.Parse(args); err != nil {
|
||||||
return err
|
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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -138,7 +138,7 @@ POST /api/ble/scan
|
|||||||
|
|
||||||
Dashboard WebSocket state includes `host_connected`, `host_transport`, `host_endpoint` (aliases `uart_connected` / `serial_port` kept).
|
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)
|
### 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**.
|
Dashboard: Master card → dropdown **ESP Log-Level** with **Lesen** / **Setzen**.
|
||||||
|
|
||||||
### OTA (master UART upload)
|
### OTA (master host upload, UART or BLE)
|
||||||
|
|
||||||
```http
|
```http
|
||||||
POST /api/ota
|
POST /api/ota
|
||||||
Content-Type: multipart/form-data
|
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
|
```json
|
||||||
{"success": true, "bytes_written": 123456, "target_slot": 1}
|
{"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.
|
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
|
### LED ring and battery
|
||||||
|
|
||||||
|
|||||||
+11
-11
@@ -17,7 +17,7 @@ const (
|
|||||||
hostTransportBLE = "ble"
|
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
|
var errHostBusy = errUARTBusy
|
||||||
|
|
||||||
// managedHost is a reconnecting UART or BLE host link for serve/CLI.
|
// managedHost is a reconnecting UART or BLE host link for serve/CLI.
|
||||||
@@ -26,15 +26,15 @@ type managedHost struct {
|
|||||||
baud int
|
baud int
|
||||||
settle time.Duration
|
settle time.Duration
|
||||||
|
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
transport string // none|uart|ble
|
transport string // none|uart|ble
|
||||||
endpoint string // port path or BLE address string
|
endpoint string // port path or BLE address string
|
||||||
bleName string // last connected BLE local name (optional)
|
bleName string // last connected BLE local name (optional)
|
||||||
sp *serialPort
|
sp *serialPort
|
||||||
ble *bleLink
|
ble *bleLink
|
||||||
otaActive bool
|
otaActive bool
|
||||||
pausePoll bool // true while BLE latency tests run (pollers yield)
|
pausePoll bool // true while BLE latency tests run (pollers yield)
|
||||||
uartPort string // remembered UART port for reconnect
|
uartPort string // remembered UART port for reconnect
|
||||||
}
|
}
|
||||||
|
|
||||||
func newManagedHost(baud int) *managedHost {
|
func newManagedHost(baud int) *managedHost {
|
||||||
@@ -279,7 +279,7 @@ func (m *managedHost) recoverAfterMasterRestart() {
|
|||||||
log.Printf("UART reconnect after master restart: %v", err)
|
log.Printf("UART reconnect after master restart: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
flushSerialInput(m.sp)
|
m.sp.flushInput()
|
||||||
log.Printf("UART %s ready after master restart", m.uartPort)
|
log.Printf("UART %s ready after master restart", m.uartPort)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+13
-12
@@ -29,7 +29,7 @@ func usage() {
|
|||||||
fmt.Fprintf(os.Stderr, " ble-stream enable stream + measure BleStreamTick window\n")
|
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, " 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, " 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, " 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, " 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")
|
fmt.Fprintf(os.Stderr, " find-me blink LED ring red/green/blue (3× each, full brightness)\n")
|
||||||
@@ -83,13 +83,6 @@ func main() {
|
|||||||
usage()
|
usage()
|
||||||
os.Exit(2)
|
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
|
var h hostConn
|
||||||
if *useBLE {
|
if *useBLE {
|
||||||
bl, err := openBLE(*bleName, time.Duration(*bleSettle*float64(time.Second)))
|
bl, err := openBLE(*bleName, time.Duration(*bleSettle*float64(time.Second)))
|
||||||
@@ -187,11 +180,19 @@ func main() {
|
|||||||
}
|
}
|
||||||
runErr = runLogLevel(sp, flag.Args()[1:])
|
runErr = runLogLevel(sp, flag.Args()[1:])
|
||||||
case "ota":
|
case "ota":
|
||||||
sp := h.(*serialPort)
|
t, ok := h.(otaTransport)
|
||||||
runErr = runOTA(sp, flag.Args()[1:])
|
if !ok {
|
||||||
|
runErr = fmt.Errorf("ota: unsupported host transport")
|
||||||
|
break
|
||||||
|
}
|
||||||
|
runErr = runOTA(t, flag.Args()[1:])
|
||||||
case "ota-progress", "ota_progress":
|
case "ota-progress", "ota_progress":
|
||||||
sp := h.(*serialPort)
|
t, ok := h.(otaTransport)
|
||||||
runErr = runOtaProgress(sp, flag.Args()[1:])
|
if !ok {
|
||||||
|
runErr = fmt.Errorf("ota-progress: unsupported host transport")
|
||||||
|
break
|
||||||
|
}
|
||||||
|
runErr = runOtaProgress(t, flag.Args()[1:])
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
fmt.Fprintf(os.Stderr, "unknown command %q\n\n", cmd)
|
fmt.Fprintf(os.Stderr, "unknown command %q\n\n", cmd)
|
||||||
|
|||||||
+124
-102
@@ -6,14 +6,14 @@ import (
|
|||||||
|
|
||||||
"google.golang.org/protobuf/proto"
|
"google.golang.org/protobuf/proto"
|
||||||
|
|
||||||
uartframe "powerpod/gotool/uart"
|
|
||||||
"powerpod/gotool/pb"
|
"powerpod/gotool/pb"
|
||||||
|
uartframe "powerpod/gotool/uart"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
otaHostChunkSize = 200
|
otaHostChunkSize = 200
|
||||||
otaFlashBlockSize = 4096
|
otaFlashBlockSize = 4096
|
||||||
otaPrepareTimeout = 120 * time.Second
|
otaPrepareTimeout = 120 * time.Second
|
||||||
otaDefaultTimeout = 15 * time.Second
|
otaDefaultTimeout = 15 * time.Second
|
||||||
otaStatusPollTimeout = 3 * time.Second
|
otaStatusPollTimeout = 3 * time.Second
|
||||||
otaDistReadTimeout = 400 * time.Millisecond
|
otaDistReadTimeout = 400 * time.Millisecond
|
||||||
@@ -21,8 +21,12 @@ const (
|
|||||||
otaDistQueryTimeout = 2 * time.Second
|
otaDistQueryTimeout = 2 * time.Second
|
||||||
otaDistEmitMinInterval = 150 * time.Millisecond
|
otaDistEmitMinInterval = 150 * time.Millisecond
|
||||||
// Pace host chunks so the master UART RX ring is not overrun (~20 frames/block).
|
// Pace host chunks so the master UART RX ring is not overrun (~20 frames/block).
|
||||||
otaHostChunkPace = 3 * time.Millisecond
|
otaHostChunkPaceUART = 3 * time.Millisecond
|
||||||
otaBlockMaxRetries = 3
|
// 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 (
|
const (
|
||||||
@@ -37,6 +41,14 @@ const (
|
|||||||
otaDistTimeout = 45 * time.Minute
|
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.
|
// OtaSlaveDetail is per-slave ESP-NOW OTA state from OTA_SLAVE_PROGRESS.
|
||||||
type OtaSlaveDetail struct {
|
type OtaSlaveDetail struct {
|
||||||
BytesWritten uint32 `json:"bytes_written"`
|
BytesWritten uint32 `json:"bytes_written"`
|
||||||
@@ -47,20 +59,20 @@ type OtaSlaveDetail struct {
|
|||||||
|
|
||||||
// OTAProgress is pushed to the dashboard during web uploads.
|
// OTAProgress is pushed to the dashboard during web uploads.
|
||||||
type OTAProgress struct {
|
type OTAProgress struct {
|
||||||
Type string `json:"type"` // always "ota_progress"
|
Type string `json:"type"` // always "ota_progress"
|
||||||
Phase string `json:"phase"`
|
Phase string `json:"phase"`
|
||||||
Step string `json:"step,omitempty"` // master, slaves
|
Step string `json:"step,omitempty"` // master, slaves
|
||||||
Percent int `json:"percent"`
|
Percent int `json:"percent"`
|
||||||
MasterPercent int `json:"master_percent,omitempty"`
|
MasterPercent int `json:"master_percent,omitempty"`
|
||||||
MasterDone bool `json:"master_done,omitempty"`
|
MasterDone bool `json:"master_done,omitempty"`
|
||||||
Message string `json:"message"`
|
Message string `json:"message"`
|
||||||
MasterMessage string `json:"master_message,omitempty"`
|
MasterMessage string `json:"master_message,omitempty"`
|
||||||
Bytes uint32 `json:"bytes_written,omitempty"`
|
Bytes uint32 `json:"bytes_written,omitempty"`
|
||||||
Slot uint32 `json:"target_slot,omitempty"`
|
Slot uint32 `json:"target_slot,omitempty"`
|
||||||
Slaves uint32 `json:"slaves,omitempty"`
|
Slaves uint32 `json:"slaves,omitempty"`
|
||||||
ImageSize uint32 `json:"image_size,omitempty"`
|
ImageSize uint32 `json:"image_size,omitempty"`
|
||||||
SlaveProgress map[uint32]uint32 `json:"slave_progress,omitempty"` // client_id -> bytes
|
SlaveProgress map[uint32]uint32 `json:"slave_progress,omitempty"` // client_id -> bytes
|
||||||
SlaveDetails map[uint32]OtaSlaveDetail `json:"slave_details,omitempty"`
|
SlaveDetails map[uint32]OtaSlaveDetail `json:"slave_details,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type otaProgressFn func(OTAProgress)
|
type otaProgressFn func(OTAProgress)
|
||||||
@@ -80,27 +92,52 @@ func runOTAUpload(m *managedSerial, firmware []byte, onProgress otaProgressFn) e
|
|||||||
Percent: 0, Message: msg, MasterMessage: msg,
|
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
|
// Hold m.mu for the entire upload so dashboard/API polling cannot interleave.
|
||||||
// dashboard/API polling cannot interleave on the serial port.
|
|
||||||
m.mu.Lock()
|
m.mu.Lock()
|
||||||
if m.otaActive {
|
if m.otaActive {
|
||||||
m.mu.Unlock()
|
m.mu.Unlock()
|
||||||
return errOTAInProgress
|
return errOTAInProgress
|
||||||
}
|
}
|
||||||
m.otaActive = true
|
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.otaActive = false
|
||||||
m.mu.Unlock()
|
m.mu.Unlock()
|
||||||
push("error", err.Error())
|
push("error", err.Error())
|
||||||
return err
|
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 {
|
if err != nil {
|
||||||
m.invalidateLocked(err)
|
m.invalidateLocked(err)
|
||||||
}
|
}
|
||||||
@@ -109,7 +146,7 @@ func runOTAUpload(m *managedSerial, firmware []byte, onProgress otaProgressFn) e
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func runOTAOnPortUnlocked(sp *serialPort, firmware []byte, onProgress otaProgressFn) error {
|
func runOTAUnlocked(t otaTransport, firmware []byte, onProgress otaProgressFn) error {
|
||||||
if len(firmware) == 0 {
|
if len(firmware) == 0 {
|
||||||
return fmt.Errorf("empty firmware")
|
return fmt.Errorf("empty firmware")
|
||||||
}
|
}
|
||||||
@@ -151,16 +188,11 @@ func runOTAOnPortUnlocked(sp *serialPort, firmware []byte, onProgress otaProgres
|
|||||||
onProgress(p)
|
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))
|
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,
|
Type: pb.MessageType_OTA_START,
|
||||||
Payload: &pb.UartMessage_OtaStart{
|
Payload: &pb.UartMessage_OtaStart{
|
||||||
OtaStart: &pb.OtaStartPayload{TotalSize: uint32(imageSize)},
|
OtaStart: &pb.OtaStartPayload{TotalSize: uint32(imageSize)},
|
||||||
@@ -170,13 +202,7 @@ func runOTAOnPortUnlocked(sp *serialPort, firmware []byte, onProgress otaProgres
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := sp.port.SetReadTimeout(otaPrepareTimeout); err != nil {
|
ready, err := waitOtaStatus(t, otaStReady, otaPrepareTimeout, func(msg string) {
|
||||||
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)
|
notify("preparing", otaStepMaster, 2, msg)
|
||||||
})
|
})
|
||||||
if err != nil {
|
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()))
|
notify("ready", otaStepMaster, 5, fmt.Sprintf("Master: Slot %d bereit", ready.GetTargetSlot()))
|
||||||
|
|
||||||
if err := sp.port.SetReadTimeout(otaDefaultTimeout); err != nil {
|
pace := t.chunkPace()
|
||||||
notify("error", "", 0, err.Error())
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
var seq uint32
|
var seq uint32
|
||||||
for offset := 0; offset < imageSize; {
|
for offset := 0; offset < imageSize; {
|
||||||
blockStart := offset
|
blockStart := offset
|
||||||
@@ -208,7 +230,7 @@ func runOTAOnPortUnlocked(sp *serialPort, firmware []byte, onProgress otaProgres
|
|||||||
}
|
}
|
||||||
chunk := firmware[offset : offset+n]
|
chunk := firmware[offset : offset+n]
|
||||||
|
|
||||||
if err := writeUartMessage(sp, &pb.UartMessage{
|
if err := t.writeMsg(&pb.UartMessage{
|
||||||
Type: pb.MessageType_OTA_PAYLOAD,
|
Type: pb.MessageType_OTA_PAYLOAD,
|
||||||
Payload: &pb.UartMessage_OtaPayload{
|
Payload: &pb.UartMessage_OtaPayload{
|
||||||
OtaPayload: &pb.OtaPayload{Seq: seq, Data: chunk},
|
OtaPayload: &pb.OtaPayload{Seq: seq, Data: chunk},
|
||||||
@@ -216,7 +238,7 @@ func runOTAOnPortUnlocked(sp *serialPort, firmware []byte, onProgress otaProgres
|
|||||||
}); err != nil {
|
}); err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
time.Sleep(otaHostChunkPace)
|
time.Sleep(pace)
|
||||||
seq++
|
seq++
|
||||||
offset += n
|
offset += n
|
||||||
bytesInBlock += n
|
bytesInBlock += n
|
||||||
@@ -252,7 +274,7 @@ func runOTAOnPortUnlocked(sp *serialPort, firmware []byte, onProgress otaProgres
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
st, ackErr = waitOtaStatus(sp, otaStBlockAck, otaDefaultTimeout, nil)
|
st, ackErr = waitOtaStatus(t, otaStBlockAck, otaDefaultTimeout, nil)
|
||||||
if ackErr == nil {
|
if ackErr == nil {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -272,10 +294,10 @@ func runOTAOnPortUnlocked(sp *serialPort, firmware []byte, onProgress otaProgres
|
|||||||
}
|
}
|
||||||
|
|
||||||
masterPct = 100
|
masterPct = 100
|
||||||
masterMsg = "Master: UART-Upload abgeschlossen"
|
masterMsg = "Master: Upload abgeschlossen"
|
||||||
notify("uploading", otaStepMaster, 100, masterMsg)
|
notify("uploading", otaStepMaster, 100, masterMsg)
|
||||||
|
|
||||||
if err := writeUartMessage(sp, &pb.UartMessage{
|
if err := t.writeMsg(&pb.UartMessage{
|
||||||
Type: pb.MessageType_OTA_END,
|
Type: pb.MessageType_OTA_END,
|
||||||
Payload: &pb.UartMessage_OtaEnd{
|
Payload: &pb.UartMessage_OtaEnd{
|
||||||
OtaEnd: &pb.OtaEndPayload{},
|
OtaEnd: &pb.OtaEndPayload{},
|
||||||
@@ -314,7 +336,8 @@ func runOTAOnPortUnlocked(sp *serialPort, firmware []byte, onProgress otaProgres
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if slaveCount == 0 {
|
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)",
|
return fmt.Sprintf("ESP-NOW: %d / %d bytes (%d Slaves)",
|
||||||
aggBytes, imageSize, slaveCount), aggBytes, slaveCount
|
aggBytes, imageSize, slaveCount), aggBytes, slaveCount
|
||||||
@@ -334,7 +357,7 @@ func runOTAOnPortUnlocked(sp *serialPort, firmware []byte, onProgress otaProgres
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
lastQuery = time.Now()
|
lastQuery = time.Now()
|
||||||
prog, err := queryOtaSlaveProgressLocked(sp, 0, onDistStatus, otaDistQueryTimeout)
|
prog, err := queryOtaSlaveProgress(t, 0, onDistStatus, otaDistQueryTimeout)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if len(slaveDetails) > 0 {
|
if len(slaveDetails) > 0 {
|
||||||
emitSlaveThrottled(true)
|
emitSlaveThrottled(true)
|
||||||
@@ -356,13 +379,13 @@ func runOTAOnPortUnlocked(sp *serialPort, firmware []byte, onProgress otaProgres
|
|||||||
|
|
||||||
lastQuery = time.Time{} // first query immediately when distribution starts
|
lastQuery = time.Time{} // first query immediately when distribution starts
|
||||||
querySlaveProgress()
|
querySlaveProgress()
|
||||||
st, err := waitOtaComplete(sp, otaDistTimeout, pushSlaveDist, onWaitTick, otaDistReadTimeout)
|
st, err := waitOtaComplete(t, otaDistTimeout, pushSlaveDist, onWaitTick, otaDistReadTimeout)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
notify("error", "", 0, err.Error())
|
notify("error", "", 0, err.Error())
|
||||||
return err
|
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)
|
mergeSlaveProgressResponse(prog, slaveBytes, slaveDetails)
|
||||||
}
|
}
|
||||||
notify("done", "", 100,
|
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.
|
// QueryOtaSlaveProgress queries the master for per-slave ESP-NOW OTA progress.
|
||||||
func QueryOtaSlaveProgress(sp *serialPort, clientID uint32) (*pb.OtaSlaveProgressResponse, error) {
|
func QueryOtaSlaveProgress(t otaTransport, clientID uint32) (*pb.OtaSlaveProgressResponse, error) {
|
||||||
sp.mu.Lock()
|
return queryOtaSlaveProgress(t, clientID, nil, otaDefaultTimeout)
|
||||||
defer sp.mu.Unlock()
|
|
||||||
return queryOtaSlaveProgressLocked(sp, 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) {
|
onStatus func(*pb.OtaStatusPayload), queryTimeout time.Duration) (*pb.OtaSlaveProgressResponse, error) {
|
||||||
req := &pb.UartMessage{
|
req := &pb.UartMessage{
|
||||||
Type: pb.MessageType_OTA_SLAVE_PROGRESS,
|
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
|
return nil, err
|
||||||
}
|
}
|
||||||
if queryTimeout <= 0 {
|
if queryTimeout <= 0 {
|
||||||
queryTimeout = otaDefaultTimeout
|
queryTimeout = otaDefaultTimeout
|
||||||
}
|
}
|
||||||
deadline := time.Now().Add(queryTimeout)
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -434,7 +455,7 @@ func applyDistributingOtaStatus(st *pb.OtaStatusPayload, imageSize int,
|
|||||||
details[id] = d
|
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) {
|
onStatus func(*pb.OtaStatusPayload), readChunk time.Duration) (*pb.UartMessage, error) {
|
||||||
if readChunk <= 0 {
|
if readChunk <= 0 {
|
||||||
readChunk = otaStatusPollTimeout
|
readChunk = otaStatusPollTimeout
|
||||||
@@ -447,10 +468,7 @@ func readUartMessageUntil(sp *serialPort, deadline time.Time, want pb.MessageTyp
|
|||||||
if wait > readChunk {
|
if wait > readChunk {
|
||||||
wait = readChunk
|
wait = readChunk
|
||||||
}
|
}
|
||||||
if err := sp.port.SetReadTimeout(wait); err != nil {
|
payload, err := t.readFrame(wait)
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
payload, err := uartframe.ReadFrame(sp.port, nil, wait)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -505,7 +523,7 @@ func copySlaveMap(m map[uint32]uint32) map[uint32]uint32 {
|
|||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
func waitOtaComplete(sp *serialPort, timeout time.Duration,
|
func waitOtaComplete(t otaTransport, timeout time.Duration,
|
||||||
onDistributing func(*pb.OtaStatusPayload), onInterval func(),
|
onDistributing func(*pb.OtaStatusPayload), onInterval func(),
|
||||||
readTimeout time.Duration) (*pb.OtaStatusPayload, error) {
|
readTimeout time.Duration) (*pb.OtaStatusPayload, error) {
|
||||||
if readTimeout <= 0 {
|
if readTimeout <= 0 {
|
||||||
@@ -520,10 +538,7 @@ func waitOtaComplete(sp *serialPort, timeout time.Duration,
|
|||||||
if readWait > readTimeout {
|
if readWait > readTimeout {
|
||||||
readWait = readTimeout
|
readWait = readTimeout
|
||||||
}
|
}
|
||||||
if err := sp.port.SetReadTimeout(readWait); err != nil {
|
st, err := readOtaStatus(t, readWait)
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
st, err := readOtaStatus(sp)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if onInterval != nil {
|
if onInterval != nil {
|
||||||
onInterval()
|
onInterval()
|
||||||
@@ -548,16 +563,7 @@ func waitOtaComplete(sp *serialPort, timeout time.Duration,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func writeUartMessage(sp *serialPort, msg *pb.UartMessage) error {
|
func waitOtaStatus(t otaTransport, want uint32, timeout time.Duration, onPreparing func(string)) (*pb.OtaStatusPayload, 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) {
|
|
||||||
deadline := time.Now().Add(timeout)
|
deadline := time.Now().Add(timeout)
|
||||||
for {
|
for {
|
||||||
if time.Now().After(deadline) {
|
if time.Now().After(deadline) {
|
||||||
@@ -567,10 +573,7 @@ func waitOtaStatus(sp *serialPort, want uint32, timeout time.Duration, onPrepari
|
|||||||
if readWait > otaStatusPollTimeout {
|
if readWait > otaStatusPollTimeout {
|
||||||
readWait = otaStatusPollTimeout
|
readWait = otaStatusPollTimeout
|
||||||
}
|
}
|
||||||
if err := sp.port.SetReadTimeout(readWait); err != nil {
|
payload, err := t.readFrame(readWait)
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
payload, err := uartframe.ReadFrame(sp.port, nil, readWait)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -595,8 +598,8 @@ func waitOtaStatus(sp *serialPort, want uint32, timeout time.Duration, onPrepari
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func readOtaStatus(sp *serialPort) (*pb.OtaStatusPayload, error) {
|
func readOtaStatus(t otaTransport, timeout time.Duration) (*pb.OtaStatusPayload, error) {
|
||||||
payload, err := uartframe.ReadFrame(sp.port, nil, readTimeout)
|
payload, err := t.readFrame(timeout)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("read response: %w", err)
|
return nil, fmt.Errorf("read response: %w", err)
|
||||||
}
|
}
|
||||||
@@ -623,8 +626,37 @@ func encodeUartMessage(msg *pb.UartMessage) ([]byte, error) {
|
|||||||
return uartframe.EncodeFrame(payload)
|
return uartframe.EncodeFrame(payload)
|
||||||
}
|
}
|
||||||
|
|
||||||
// flushSerialInput drops stale RX bytes (not full frames — avoids ReadFrame blocking).
|
func decodeUartPayload(payload []byte) (*pb.UartMessage, error) {
|
||||||
func flushSerialInput(sp *serialPort) {
|
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 {
|
if sp == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -639,14 +671,4 @@ func flushSerialInput(sp *serialPort) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func decodeUartPayload(payload []byte) (*pb.UartMessage, error) {
|
func (sp *serialPort) chunkPace() time.Duration { return otaHostChunkPaceUART }
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|||||||
+22
-25
@@ -788,23 +788,23 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="col-12" x-show="isUARTHost() || !hostUp()">
|
<section class="col-12">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header">Firmware OTA (A/B)</div>
|
<div class="card-header">Firmware OTA (A/B)</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<p class="text-muted small mb-3">
|
<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.
|
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. Bei BLE-Host ist OTA nicht verfügbar.
|
Während des Uploads pausiert das Live-Polling. BLE braucht ATT MTU ≥ 219.
|
||||||
</p>
|
</p>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<input type="file" class="form-control form-control-sm" accept=".bin,application/octet-stream"
|
<input type="file" class="form-control form-control-sm" accept=".bin,application/octet-stream"
|
||||||
@change="otaFile = $event.target.files[0]"
|
@change="otaFile = $event.target.files[0]"
|
||||||
:disabled="ota.active || busy || !isUARTHost()">
|
:disabled="ota.active || busy || !hostUp()">
|
||||||
</div>
|
</div>
|
||||||
<div class="d-flex flex-wrap gap-2 align-items-center mb-2">
|
<div class="d-flex flex-wrap gap-2 align-items-center mb-2">
|
||||||
<button type="button" class="btn btn-primary btn-sm"
|
<button type="button" class="btn btn-primary btn-sm"
|
||||||
@click="uploadOTA()"
|
@click="uploadOTA()"
|
||||||
:disabled="ota.active || busy || !isUARTHost() || !otaFile">
|
:disabled="ota.active || busy || !hostUp() || !otaFile">
|
||||||
Firmware hochladen
|
Firmware hochladen
|
||||||
</button>
|
</button>
|
||||||
<span class="text-muted small" x-show="otaFile"
|
<span class="text-muted small" x-show="otaFile"
|
||||||
@@ -812,7 +812,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="ota-progress-panel mt-3"
|
<div class="ota-progress-panel mt-3"
|
||||||
x-show="ota.active || ota.phase === 'distributing' || ota.phase === 'done' || ota.phase === 'error'">
|
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">
|
<table class="table table-sm pp-table ota-progress-table mb-3">
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -845,7 +845,8 @@
|
|||||||
Slaves (ESP-NOW)
|
Slaves (ESP-NOW)
|
||||||
<span x-text="'(' + otaSlaveRows().length + ')'"></span>
|
<span x-text="'(' + otaSlaveRows().length + ')'"></span>
|
||||||
</p>
|
</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>
|
x-text="ota.message"></p>
|
||||||
<table class="table table-sm pp-table ota-progress-table mb-2">
|
<table class="table table-sm pp-table ota-progress-table mb-2">
|
||||||
<tbody>
|
<tbody>
|
||||||
@@ -877,9 +878,8 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</template>
|
</template>
|
||||||
<tr x-show="otaSlaveRows().length === 0">
|
<tr x-show="otaSlaveRows().length === 0">
|
||||||
<td colspan="2" class="text-muted small py-3">
|
<td colspan="2" class="text-muted small py-3"
|
||||||
Warte auf Slave-Fortschritt…
|
x-text="otaDistEmptyHint()"></td>
|
||||||
</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
@@ -1422,7 +1422,19 @@
|
|||||||
if (status === 1 || status === 2) return 'bg-secondary';
|
if (status === 1 || status === 2) return 'bg-secondary';
|
||||||
return 'bg-info';
|
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() {
|
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 details = this.mergeSlaveDetails(this.ota.slaveDetails);
|
||||||
const ids = new Set(Object.keys(details).map(Number).filter(id => !Number.isNaN(id)));
|
const ids = new Set(Object.keys(details).map(Number).filter(id => !Number.isNaN(id)));
|
||||||
for (const [k, v] of Object.entries(this.ota.slaveProgress || {})) {
|
for (const [k, v] of Object.entries(this.ota.slaveProgress || {})) {
|
||||||
@@ -1454,21 +1466,6 @@
|
|||||||
bytesLabel: total ? `${bytes} / ${total} bytes` : `${bytes} bytes`
|
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;
|
return rows;
|
||||||
},
|
},
|
||||||
applyOTAProgress(p) {
|
applyOTAProgress(p) {
|
||||||
|
|||||||
+4
-1
@@ -6,6 +6,7 @@
|
|||||||
#include "esp_timer.h"
|
#include "esp_timer.h"
|
||||||
#include "freertos/FreeRTOS.h"
|
#include "freertos/FreeRTOS.h"
|
||||||
#include "freertos/task.h"
|
#include "freertos/task.h"
|
||||||
|
#include "ota_session.h"
|
||||||
#include "pb_encode.h"
|
#include "pb_encode.h"
|
||||||
#include "uart.h"
|
#include "uart.h"
|
||||||
#include "uart_cmd.h"
|
#include "uart_cmd.h"
|
||||||
@@ -146,7 +147,9 @@ static void ble_stream_task(void *param) {
|
|||||||
period = BLE_STREAM_PERIOD_MAX_MS;
|
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;
|
alox_UartMessage msg = alox_UartMessage_init_zero;
|
||||||
uart_cmd_init_response(&msg, alox_MessageType_BLE_STREAM_TICK,
|
uart_cmd_init_response(&msg, alox_MessageType_BLE_STREAM_TICK,
|
||||||
alox_UartMessage_ble_stream_tick_tag);
|
alox_UartMessage_ble_stream_tick_tag);
|
||||||
|
|||||||
+2
-19
@@ -1,5 +1,4 @@
|
|||||||
#include "cmd_ota.h"
|
#include "cmd_ota.h"
|
||||||
#include "app_config.h"
|
|
||||||
#include "led_ring.h"
|
#include "led_ring.h"
|
||||||
#include "ota_espnow.h"
|
#include "ota_espnow.h"
|
||||||
#include "ota_uart.h"
|
#include "ota_uart.h"
|
||||||
@@ -14,6 +13,8 @@ static const char *TAG = "[OTA_CMD]";
|
|||||||
|
|
||||||
#define OTA_PREPARE_STACK 8192
|
#define OTA_PREPARE_STACK 8192
|
||||||
#define OTA_PREPARE_PRIO 5
|
#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_STACK 8192
|
||||||
#define OTA_DIST_PRIO 5
|
#define OTA_DIST_PRIO 5
|
||||||
|
|
||||||
@@ -104,24 +105,10 @@ static void ota_prepare_task(void *param) {
|
|||||||
vTaskDelete(NULL);
|
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) {
|
static void handle_ota_start(const uint8_t *data, size_t len) {
|
||||||
alox_UartMessage uart_msg;
|
alox_UartMessage uart_msg;
|
||||||
alox_OtaStartPayload req = alox_OtaStartPayload_init_zero;
|
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) {
|
if (uart_cmd_decode(data, len, &uart_msg) != ESP_OK) {
|
||||||
send_ota_failed( 2);
|
send_ota_failed( 2);
|
||||||
return;
|
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) {
|
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)data;
|
||||||
(void)len;
|
(void)len;
|
||||||
|
|
||||||
|
|||||||
+105
-14
@@ -28,6 +28,12 @@ static const char *TAG = "[OTA_ESPNOW]";
|
|||||||
#define OTA_ST_SUCCESS 4u
|
#define OTA_ST_SUCCESS 4u
|
||||||
#define OTA_ST_FAILED 5u
|
#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). */
|
/** ESP-NOW OTA receive on slave (blue progress bar). */
|
||||||
#define OTA_LED_ESPNOW_RX_R 0
|
#define OTA_LED_ESPNOW_RX_R 0
|
||||||
#define OTA_LED_ESPNOW_RX_G 0
|
#define OTA_LED_ESPNOW_RX_G 0
|
||||||
@@ -58,6 +64,7 @@ typedef struct {
|
|||||||
static EventGroupHandle_t s_eg;
|
static EventGroupHandle_t s_eg;
|
||||||
static QueueHandle_t s_slave_work_queue;
|
static QueueHandle_t s_slave_work_queue;
|
||||||
static bool s_distribution_active;
|
static bool s_distribution_active;
|
||||||
|
static TaskHandle_t s_slave_prep_task;
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
uint8_t count;
|
uint8_t count;
|
||||||
@@ -70,6 +77,9 @@ typedef struct {
|
|||||||
|
|
||||||
static ota_dist_t s_dist;
|
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 {
|
typedef struct {
|
||||||
uint32_t client_id;
|
uint32_t client_id;
|
||||||
uint32_t bytes_written;
|
uint32_t bytes_written;
|
||||||
@@ -164,14 +174,46 @@ static uint32_t all_target_bits(void) {
|
|||||||
return (1u << s_dist.count) - 1u;
|
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) {
|
static bool wait_target_bits(uint32_t want_bits, uint32_t timeout_ms) {
|
||||||
if (s_eg == NULL || want_bits == 0) {
|
if (s_eg == NULL || want_bits == 0) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
EventBits_t got =
|
|
||||||
xEventGroupWaitBits(s_eg, want_bits, pdTRUE, pdTRUE,
|
const TickType_t start = xTaskGetTickCount();
|
||||||
pdMS_TO_TICKS(timeout_ms));
|
const TickType_t timeout_ticks = pdMS_TO_TICKS(timeout_ms);
|
||||||
return (got & want_bits) == want_bits;
|
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) {
|
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) {
|
static void ota_slave_prepare_task(void *param) {
|
||||||
uint32_t total_size = (uint32_t)(uintptr_t)param;
|
uint32_t total_size = (uint32_t)(uintptr_t)param;
|
||||||
uint8_t master_mac[6];
|
uint8_t master_mac[6];
|
||||||
|
|
||||||
if (!esp_now_comm_get_master_mac(master_mac)) {
|
if (!esp_now_comm_get_master_mac(master_mac)) {
|
||||||
|
s_slave_prep_task = NULL;
|
||||||
vTaskDelete(NULL);
|
vTaskDelete(NULL);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -386,6 +438,7 @@ static void ota_slave_prepare_task(void *param) {
|
|||||||
int slot = ota_uart_prepare(total_size);
|
int slot = ota_uart_prepare(total_size);
|
||||||
if (slot < 0) {
|
if (slot < 0) {
|
||||||
send_slave_status(master_mac, OTA_ST_FAILED, 0, 1);
|
send_slave_status(master_mac, OTA_ST_FAILED, 0, 1);
|
||||||
|
s_slave_prep_task = NULL;
|
||||||
vTaskDelete(NULL);
|
vTaskDelete(NULL);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -393,6 +446,7 @@ static void ota_slave_prepare_task(void *param) {
|
|||||||
send_slave_status(master_mac, OTA_ST_READY, 0, 0);
|
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,
|
led_ring_show_ota_progress(0, total_size, OTA_LED_ESPNOW_RX_R, OTA_LED_ESPNOW_RX_G,
|
||||||
OTA_LED_ESPNOW_RX_B);
|
OTA_LED_ESPNOW_RX_B);
|
||||||
|
s_slave_prep_task = NULL;
|
||||||
vTaskDelete(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);
|
ESP_LOGI(TAG, "ESP-NOW OTA_START (%lu bytes)", (unsigned long)start->total_size);
|
||||||
|
|
||||||
if (ota_uart_is_active()) {
|
/* Duplicate START while prepare task still running — ignore. */
|
||||||
queue_slave_status(master_mac, OTA_ST_FAILED, 0, 4);
|
if (s_slave_prep_task != NULL) {
|
||||||
|
ESP_LOGW(TAG, "OTA prepare already in flight — ignore START");
|
||||||
return;
|
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,
|
if (xTaskCreate(ota_slave_prepare_task, "ota_esp_prep", OTA_ESPNOW_PREPARE_STACK,
|
||||||
(void *)(uintptr_t)start->total_size, OTA_ESPNOW_PREPARE_PRIO,
|
(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);
|
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);
|
status->error);
|
||||||
ESP_LOGW(TAG, "slave %lu OTA failed (err=%lu)",
|
ESP_LOGW(TAG, "slave %lu OTA failed (err=%lu)",
|
||||||
(unsigned long)s_dist.id[idx], (unsigned long)status->error);
|
(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;
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
@@ -527,7 +600,7 @@ static esp_err_t distribute_image(const esp_partition_t *partition,
|
|||||||
uint32_t target_mask = all_target_bits();
|
uint32_t target_mask = all_target_bits();
|
||||||
esp_err_t err;
|
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++) {
|
for (uint8_t i = 0; i < s_dist.count; i++) {
|
||||||
err = esp_now_comm_send_ota_start(s_dist.mac[i], size);
|
err = esp_now_comm_send_ota_start(s_dist.mac[i], size);
|
||||||
if (err != ESP_OK) {
|
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 (!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");
|
ESP_LOGE(TAG, "timeout waiting for slave OTA ready");
|
||||||
prog_end();
|
prog_end();
|
||||||
s_distribution_active = false;
|
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);
|
s_dist.progress.aggregate(0, size, s_dist.count);
|
||||||
}
|
}
|
||||||
|
|
||||||
uint8_t block_buf[OTA_UART_FLASH_BLOCK_SIZE];
|
|
||||||
uint32_t offset = 0;
|
uint32_t offset = 0;
|
||||||
uint32_t seq = 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;
|
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) {
|
if (err != ESP_OK) {
|
||||||
ESP_LOGE(TAG, "partition read @%lu failed: %s", (unsigned long)offset,
|
ESP_LOGE(TAG, "partition read @%lu failed: %s", (unsigned long)offset,
|
||||||
esp_err_to_name(err));
|
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)send_attempt,
|
||||||
(unsigned long)OTA_BLOCK_MAX_RETRIES);
|
(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) {
|
if (err == ESP_OK) {
|
||||||
block_sent = true;
|
block_sent = true;
|
||||||
break;
|
break;
|
||||||
@@ -614,6 +692,13 @@ static esp_err_t distribute_image(const esp_partition_t *partition,
|
|||||||
acked = true;
|
acked = true;
|
||||||
break;
|
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);
|
log_missing_block_acks(s_dist.expected_bytes);
|
||||||
if (attempt >= OTA_BLOCK_MAX_RETRIES) {
|
if (attempt >= OTA_BLOCK_MAX_RETRIES) {
|
||||||
break;
|
break;
|
||||||
@@ -622,9 +707,9 @@ static esp_err_t distribute_image(const esp_partition_t *partition,
|
|||||||
(unsigned long)s_dist.expected_bytes,
|
(unsigned long)s_dist.expected_bytes,
|
||||||
(unsigned long)(attempt + 1),
|
(unsigned long)(attempt + 1),
|
||||||
(unsigned long)OTA_BLOCK_MAX_RETRIES);
|
(unsigned long)OTA_BLOCK_MAX_RETRIES);
|
||||||
xEventGroupClearBits(s_eg, target_mask);
|
xEventGroupClearBits(s_eg, target_mask | OTA_EG_FAIL_BIT);
|
||||||
seq = block_start_seq;
|
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) {
|
if (err != ESP_OK) {
|
||||||
prog_end();
|
prog_end();
|
||||||
s_distribution_active = false;
|
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++) {
|
for (uint8_t i = 0; i < s_dist.count; i++) {
|
||||||
err = esp_now_comm_send_ota_end(s_dist.mac[i]);
|
err = esp_now_comm_send_ota_end(s_dist.mac[i]);
|
||||||
if (err != ESP_OK) {
|
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 (!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");
|
ESP_LOGE(TAG, "timeout waiting for slave OTA success");
|
||||||
prog_end();
|
prog_end();
|
||||||
s_distribution_active = false;
|
s_distribution_active = false;
|
||||||
|
|||||||
@@ -407,7 +407,7 @@ message TapConfigResponse {
|
|||||||
bool enable_triple = 12;
|
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 {
|
message OtaStartPayload {
|
||||||
uint32 total_size = 1;
|
uint32 total_size = 1;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user