Add per-slave ESP-NOW OTA progress over UART and fix dashboard updates.

Expose OTA_SLAVE_PROGRESS on the master, track per-slave state during
distribution, run ESP-NOW OTA in a background task so the host can poll
while slaves update, and show master/slave progress in the dashboard
with table layout and faster WebSocket refresh during uploads.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
simon 2026-05-19 21:07:46 +02:00
parent 5a948a5c8c
commit a0f4a81a55
23 changed files with 1467 additions and 146 deletions

View File

@ -18,10 +18,6 @@ default:
@echo "Targets: proto_generate gotool-build gotool-clients gotool-version …" @echo "Targets: proto_generate gotool-build gotool-clients gotool-version …"
@echo "Set PORT=$(PORT) (current) for goTool targets." @echo "Set PORT=$(PORT) (current) for goTool targets."
proto_generate_uart:
cd main/proto && python ../../libs/nanopb/generator/nanopb_generator.py \
-I ../../libs/nanopb/generator/proto uart_messages.proto
proto_generate_espnow: proto_generate_espnow:
cd main/proto && python ../../libs/nanopb/generator/nanopb_generator.py \ cd main/proto && python ../../libs/nanopb/generator/nanopb_generator.py \
-I ../../libs/nanopb/generator/proto esp_now_messages.proto -I ../../libs/nanopb/generator/proto esp_now_messages.proto
@ -31,7 +27,15 @@ proto_generate: proto_generate_uart proto_generate_espnow
gotool-proto: gotool-proto:
cd $(GOTOOL_DIR) && protoc --go_out=./pb --go_opt=paths=source_relative \ cd $(GOTOOL_DIR) && protoc --go_out=./pb --go_opt=paths=source_relative \
--go_opt=Muart_messages.proto=powerpod/gotool/pb \ --go_opt=Muart_messages.proto=powerpod/gotool/pb \
-I ../main/proto ../main/proto/uart_messages.proto --go_opt=Mnanopb.proto=powerpod/gotool/pb/nanopb \
-I ../main/proto \
-I ../libs/nanopb/generator/proto \
../main/proto/uart_messages.proto
@sed -i '/powerpod\/gotool\/pb\/nanopb/d' $(GOTOOL_DIR)/pb/uart_messages.pb.go
proto_generate_uart:
cd main/proto && python3 ../../libs/nanopb/generator/nanopb_generator.py \
-I . -I ../../libs/nanopb/generator/proto uart_messages.proto
gotool-tidy: gotool-tidy:
cd $(GOTOOL_DIR) && go mod tidy cd $(GOTOOL_DIR) && go mod tidy

View File

@ -28,6 +28,7 @@ go run . -port /dev/ttyUSB0 clients
| `test` | — | Run an automated scenario (JSON configs under `testdata/`) | | `test` | — | Run an automated scenario (JSON configs under `testdata/`) |
| `serve` | — | Web dashboard at `http://localhost:8080` (WebSocket live updates) | | `serve` | — | Web dashboard at `http://localhost:8080` (WebSocket live updates) |
| `ota` | 1619 | UART firmware upload to master; firmware then pushes to slaves via ESP-NOW | | `ota` | 1619 | UART firmware upload to master; firmware then pushes to slaves via ESP-NOW |
| `ota-progress` | 21 | Query per-slave ESP-NOW OTA progress on the master (`-client N`, default all) |
`clients` requires slaves to have responded to master discover broadcasts first. `clients` requires slaves to have responded to master discover broadcasts first.
@ -72,7 +73,7 @@ HTTP API (used by the web UI): `GET/POST /api/deadzone`, `POST /api/unicast-test
| UI / API | Behaviour | | UI / API | Behaviour |
|----------|-----------| |----------|-----------|
| Firmware OTA card | Same as `ota` CLI; progress via WebSocket `ota_progress` events | | Firmware OTA card | Same as `ota` CLI; WebSocket `ota_progress` with `step` `master` (UART) then `slaves` (ESP-NOW) |
| `POST /api/ota` | Upload `.bin` to master only — slaves are updated by firmware over ESP-NOW after `OTA_END` | | `POST /api/ota` | Upload `.bin` to master only — slaves are updated by firmware over ESP-NOW after `OTA_END` |
```bash ```bash
@ -96,8 +97,9 @@ clients (2):
## Regenerate protobuf ## Regenerate protobuf
From repo root (needs `protoc`, `protoc-gen-go`, and for C also `pip install protobuf`):
```bash ```bash
protoc --go_out=./pb --go_opt=paths=source_relative \ make gotool-proto # Go: goTool/pb/uart_messages.pb.go
--go_opt=Muart_messages.proto=powerpod/gotool/pb \ make proto_generate # C: main/proto/*.pb.h, *.pb.c
-I ../main/proto ../main/proto/uart_messages.proto
``` ```

View File

@ -16,6 +16,14 @@ func (m *managedSerial) getVersion() (*pb.VersionResponse, error) {
return decodeVersionPayload(payload) return decodeVersionPayload(payload)
} }
func (m *managedSerial) getVersionPoll() (*pb.VersionResponse, error) {
payload, err := m.exchangePoll(byte(pb.MessageType_VERSION), "VERSION")
if err != nil {
return nil, err
}
return decodeVersionPayload(payload)
}
func (m *managedSerial) listClients() ([]*pb.ClientInfo, error) { func (m *managedSerial) listClients() ([]*pb.ClientInfo, error) {
payload, err := m.exchange(byte(pb.MessageType_CLIENT_INFO), "CLIENT_INFO") payload, err := m.exchange(byte(pb.MessageType_CLIENT_INFO), "CLIENT_INFO")
if err != nil { if err != nil {
@ -24,9 +32,28 @@ func (m *managedSerial) listClients() ([]*pb.ClientInfo, error) {
return decodeClientsPayload(payload) return decodeClientsPayload(payload)
} }
func (m *managedSerial) listClientsPoll() ([]*pb.ClientInfo, error) {
payload, err := m.exchangePoll(byte(pb.MessageType_CLIENT_INFO), "CLIENT_INFO")
if err != nil {
return nil, err
}
return decodeClientsPayload(payload)
}
func (m *managedSerial) AccelDeadzone(req *pb.AccelDeadzoneRequest) (*pb.AccelDeadzoneResponse, error) { func (m *managedSerial) AccelDeadzone(req *pb.AccelDeadzoneRequest) (*pb.AccelDeadzoneResponse, error) {
return m.accelDeadzoneVia(m.withPort, req)
}
func (m *managedSerial) AccelDeadzonePoll(req *pb.AccelDeadzoneRequest) (*pb.AccelDeadzoneResponse, error) {
return m.accelDeadzoneVia(m.withPortPoll, req)
}
func (m *managedSerial) accelDeadzoneVia(
portFn func(func(*serialPort) error) error,
req *pb.AccelDeadzoneRequest,
) (*pb.AccelDeadzoneResponse, error) {
var resp *pb.AccelDeadzoneResponse var resp *pb.AccelDeadzoneResponse
err := m.withPort(func(sp *serialPort) error { err := portFn(func(sp *serialPort) error {
var e error var e error
resp, e = sp.AccelDeadzone(req) resp, e = sp.AccelDeadzone(req)
return e return e

View File

@ -0,0 +1,28 @@
package main
import (
"flag"
"fmt"
)
func runOtaProgress(sp *serialPort, 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 err != nil {
return err
}
fmt.Printf("active=%v total=%d aggregate=%d slaves=%d\n",
r.GetActive(), r.GetTotalBytes(), r.GetAggregateBytes(), r.GetSlaveCount())
for _, s := range r.GetSlaves() {
fmt.Printf(" slave %d: %d / %d bytes status=%d error=%d\n",
s.GetClientId(), s.GetBytesWritten(), s.GetTotalBytes(),
s.GetStatus(), s.GetError())
}
return nil
}

View File

@ -3,6 +3,7 @@ package main
import ( import (
"encoding/hex" "encoding/hex"
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"log" "log"
"sync" "sync"
@ -105,14 +106,17 @@ func (h *wsHub) broadcastRaw(v any) {
} }
} }
func pollDashboard(link *managedSerial, portName string) DashboardState { func pollDashboard(link *managedSerial, portName string, last *DashboardState) DashboardState {
st := DashboardState{ st := DashboardState{
UpdatedAt: time.Now().Format(time.RFC3339), UpdatedAt: time.Now().Format(time.RFC3339),
SerialPort: portName, SerialPort: portName,
Clients: []ClientView{}, Clients: []ClientView{},
} }
ver, err := link.getVersion() ver, err := link.getVersionPoll()
if errors.Is(err, errUARTBusy) {
return pausedPollState(portName, last)
}
if err != nil { if err != nil {
return disconnectedState(portName, err) return disconnectedState(portName, err)
} }
@ -124,12 +128,15 @@ func pollDashboard(link *managedSerial, portName string) DashboardState {
RunningPartition: ver.GetRunningPartition(), RunningPartition: ver.GetRunningPartition(),
OK: true, OK: true,
} }
if dz, err := readDeadzone(link, 0); err == nil { if dz, err := readDeadzonePoll(link, 0); err == nil {
st.Master.Deadzone = dz st.Master.Deadzone = dz
} }
clients, err := link.listClients() clients, err := link.listClientsPoll()
if err != nil { if err != nil {
if errors.Is(err, errUARTBusy) {
return pausedPollState(portName, last)
}
st.SerialOK = false st.SerialOK = false
st.SerialError = err.Error() st.SerialError = err.Error()
st.UARTConnected = link.IsConnected() st.UARTConnected = link.IsConnected()
@ -146,7 +153,7 @@ func pollDashboard(link *managedSerial, portName string) DashboardState {
LastPing: c.GetLastPing(), LastPing: c.GetLastPing(),
LastSuccessPing: c.GetLastSuccessPing(), LastSuccessPing: c.GetLastSuccessPing(),
} }
if dz, err := readDeadzone(link, c.GetId()); err == nil { if dz, err := readDeadzonePoll(link, c.GetId()); err == nil {
cv.Deadzone = dz cv.Deadzone = dz
} }
st.Clients = append(st.Clients, cv) st.Clients = append(st.Clients, cv)
@ -154,6 +161,18 @@ func pollDashboard(link *managedSerial, portName string) DashboardState {
return st return st
} }
func pausedPollState(portName string, last *DashboardState) DashboardState {
if last != nil && last.UARTConnected {
st := *last
st.UpdatedAt = time.Now().Format(time.RFC3339)
st.SerialPort = portName
st.SerialOK = true
st.SerialError = "Live-Polling pausiert (OTA läuft)"
return st
}
return disconnectedState(portName, errUARTBusy)
}
func readDeadzone(link *managedSerial, clientID uint32) (uint32, error) { func readDeadzone(link *managedSerial, clientID uint32) (uint32, error) {
r, err := link.AccelDeadzone(&pb.AccelDeadzoneRequest{ r, err := link.AccelDeadzone(&pb.AccelDeadzoneRequest{
Write: false, Write: false,
@ -168,6 +187,20 @@ func readDeadzone(link *managedSerial, clientID uint32) (uint32, error) {
return r.GetDeadzone(), nil return r.GetDeadzone(), nil
} }
func readDeadzonePoll(link *managedSerial, clientID uint32) (uint32, error) {
r, err := link.AccelDeadzonePoll(&pb.AccelDeadzoneRequest{
Write: false,
ClientId: clientID,
})
if err != nil {
return 0, err
}
if !r.GetSuccess() {
return 0, fmt.Errorf("deadzone read failed for client %d", clientID)
}
return r.GetDeadzone(), nil
}
func formatMAC(mac []byte) string { func formatMAC(mac []byte) string {
if len(mac) == 0 { if len(mac) == 0 {
return "" return ""
@ -180,8 +213,12 @@ func runPoller(link *managedSerial, portName string, hub *wsHub, interval time.D
defer ticker.Stop() defer ticker.Stop()
uartUp := false uartUp := false
var lastGood DashboardState
publish := func() { publish := func() {
st := pollDashboard(link, portName) st := pollDashboard(link, portName, &lastGood)
if st.UARTConnected && st.SerialOK {
lastGood = st
}
if st.UARTConnected && !uartUp { if st.UARTConnected && !uartUp {
log.Printf("UART %s connected", portName) log.Printf("UART %s connected", portName)
} }

View File

@ -19,7 +19,8 @@ func usage() {
fmt.Fprintf(os.Stderr, " unicast-test send ESP-NOW unicast test to one slave\n") fmt.Fprintf(os.Stderr, " unicast-test send ESP-NOW unicast test to one slave\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 (Bootstrap + WebSocket)\n") fmt.Fprintf(os.Stderr, " serve web dashboard (Bootstrap + WebSocket)\n")
fmt.Fprintf(os.Stderr, " ota UART OTA upload (A/B partitions)\n\n") fmt.Fprintf(os.Stderr, " ota UART OTA upload (A/B partitions)\n")
fmt.Fprintf(os.Stderr, " ota-progress query per-slave ESP-NOW OTA progress on master\n\n")
flag.PrintDefaults() flag.PrintDefaults()
} }
@ -46,7 +47,7 @@ func main() {
os.Exit(2) os.Exit(2)
} }
runErr = runServe(*portName, *baud, flag.Args()[1:]) runErr = runServe(*portName, *baud, flag.Args()[1:])
case "version", "clients", "client-info", "deadzone", "accel-deadzone", "unicast-test", "unicast_test", "ota": case "version", "clients", "client-info", "deadzone", "accel-deadzone", "unicast-test", "unicast_test", "ota", "ota-progress", "ota_progress":
if *portName == "" { if *portName == "" {
fmt.Fprintf(os.Stderr, "command %q requires -port\n\n", cmd) fmt.Fprintf(os.Stderr, "command %q requires -port\n\n", cmd)
usage() usage()
@ -68,6 +69,8 @@ func main() {
runErr = runUnicastTest(sp, flag.Args()[1:]) runErr = runUnicastTest(sp, flag.Args()[1:])
case "ota": case "ota":
runErr = runOTA(sp, flag.Args()[1:]) runErr = runOTA(sp, flag.Args()[1:])
case "ota-progress", "ota_progress":
runErr = runOtaProgress(sp, 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)

View File

@ -16,6 +16,11 @@ const (
otaFlashBlockSize = 4096 otaFlashBlockSize = 4096
otaPrepareTimeout = 120 * time.Second otaPrepareTimeout = 120 * time.Second
otaDefaultTimeout = 15 * time.Second otaDefaultTimeout = 15 * time.Second
otaStatusPollTimeout = 3 * time.Second
otaDistReadTimeout = 400 * time.Millisecond
otaDistQueryInterval = 500 * time.Millisecond
otaDistQueryTimeout = 2 * time.Second
otaDistEmitMinInterval = 150 * time.Millisecond
) )
const ( const (
@ -24,20 +29,45 @@ const (
otaStBlockAck = 3 otaStBlockAck = 3
otaStSuccess = 4 otaStSuccess = 4
otaStFailed = 5 otaStFailed = 5
otaStDistributing = 6
otaDistAggregate = 0
otaDistPerSlave = 1
otaDistTimeout = 45 * time.Minute
) )
// OtaSlaveDetail is per-slave ESP-NOW OTA state from OTA_SLAVE_PROGRESS.
type OtaSlaveDetail struct {
BytesWritten uint32 `json:"bytes_written"`
TotalBytes uint32 `json:"total_bytes"`
Status uint32 `json:"status"`
Error uint32 `json:"error"`
}
// 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"` // preparing, ready, uploading, done, error Phase string `json:"phase"`
Step string `json:"step,omitempty"` // master, slaves
Percent int `json:"percent"` Percent int `json:"percent"`
MasterPercent int `json:"master_percent,omitempty"`
MasterDone bool `json:"master_done,omitempty"`
Message string `json:"message"` Message string `json:"message"`
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"`
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) type otaProgressFn func(OTAProgress)
const (
otaStepMaster = "master"
otaStepSlaves = "slaves"
)
func runOTAUpload(m *managedSerial, firmware []byte, onProgress otaProgressFn) error { func runOTAUpload(m *managedSerial, firmware []byte, onProgress otaProgressFn) error {
m.mu.Lock() m.mu.Lock()
defer m.mu.Unlock() defer m.mu.Unlock()
@ -52,69 +82,95 @@ func runOTAOnPortUnlocked(m *managedSerial, firmware []byte, onProgress otaProgr
if len(firmware) == 0 { if len(firmware) == 0 {
return fmt.Errorf("empty firmware") return fmt.Errorf("empty firmware")
} }
notify := func(phase string, percent int, msg string, extra ...OTAProgress) { imageSize := len(firmware)
masterPct := 0
masterMsg := ""
notify := func(phase, step string, percent int, msg string, extra ...OTAProgress) {
if onProgress == nil { if onProgress == nil {
return return
} }
p := OTAProgress{Type: "ota_progress", Phase: phase, Percent: percent, Message: msg} p := OTAProgress{
Type: "ota_progress", Phase: phase, Step: step,
Percent: percent, Message: msg,
ImageSize: uint32(imageSize),
}
if step == otaStepMaster || phase == "preparing" || phase == "ready" || phase == "uploading" {
masterPct = percent
masterMsg = msg
}
p.MasterPercent = masterPct
p.MasterMessage = masterMsg
if step == otaStepSlaves || phase == "distributing" || phase == "done" {
p.MasterDone = true
}
if len(extra) > 0 { if len(extra) > 0 {
p.Bytes = extra[0].Bytes e := extra[0]
p.Slot = extra[0].Slot p.Bytes = e.Bytes
p.Slot = e.Slot
p.Slaves = e.Slaves
p.SlaveProgress = e.SlaveProgress
p.SlaveDetails = e.SlaveDetails
if e.MasterPercent > 0 {
p.MasterPercent = e.MasterPercent
}
if e.MasterMessage != "" {
p.MasterMessage = e.MasterMessage
}
} }
onProgress(p) onProgress(p)
} }
if m.sp == nil { if m.sp == nil {
if err := m.openLocked(); err != nil { if err := m.openLocked(); err != nil {
notify("error", 0, err.Error()) notify("error", "", 0, err.Error())
return err return err
} }
} }
sp := m.sp sp := m.sp
if err := sp.port.SetReadTimeout(otaPrepareTimeout); err != nil { if err := sp.port.SetReadTimeout(otaPrepareTimeout); err != nil {
notify("error", 0, err.Error()) notify("error", "", 0, err.Error())
return err return err
} }
defer sp.port.SetReadTimeout(readTimeout) defer sp.port.SetReadTimeout(readTimeout)
notify("preparing", 0, fmt.Sprintf("OTA start (%d bytes)…", len(firmware))) notify("preparing", otaStepMaster, 0, fmt.Sprintf("Master: OTA start (%d bytes)…", imageSize))
if err := writeUartMessage(sp, &pb.UartMessage{ if err := writeUartMessage(sp, &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(len(firmware))}, OtaStart: &pb.OtaStartPayload{TotalSize: uint32(imageSize)},
}, },
}, false); err != nil { }, false); err != nil {
notify("error", 0, err.Error()) notify("error", "", 0, err.Error())
return err return err
} }
ready, err := waitOtaStatus(sp, otaStReady, otaPrepareTimeout, func(msg string) { ready, err := waitOtaStatus(sp, otaStReady, otaPrepareTimeout, func(msg string) {
notify("preparing", 2, msg) notify("preparing", otaStepMaster, 2, msg)
}) })
if err != nil { if err != nil {
notify("error", 0, err.Error()) notify("error", "", 0, err.Error())
return err return err
} }
notify("ready", 5, fmt.Sprintf("Ziel-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 { if err := sp.port.SetReadTimeout(otaDefaultTimeout); err != nil {
notify("error", 0, err.Error()) notify("error", "", 0, err.Error())
return err return err
} }
var seq uint32 var seq uint32
for offset := 0; offset < len(firmware); { for offset := 0; offset < imageSize; {
bytesInBlock := 0 bytesInBlock := 0
for bytesInBlock < otaFlashBlockSize && offset < len(firmware) { for bytesInBlock < otaFlashBlockSize && offset < imageSize {
n := otaHostChunkSize n := otaHostChunkSize
room := otaFlashBlockSize - bytesInBlock room := otaFlashBlockSize - bytesInBlock
if n > room { if n > room {
n = room n = room
} }
if offset+n > len(firmware) { if offset+n > imageSize {
n = len(firmware) - offset n = imageSize - offset
} }
chunk := firmware[offset : offset+n] chunk := firmware[offset : offset+n]
@ -124,53 +180,311 @@ func runOTAOnPortUnlocked(m *managedSerial, firmware []byte, onProgress otaProgr
OtaPayload: &pb.OtaPayload{Seq: seq, Data: chunk}, OtaPayload: &pb.OtaPayload{Seq: seq, Data: chunk},
}, },
}, false); err != nil { }, false); err != nil {
notify("error", 0, err.Error()) notify("error", "", 0, err.Error())
return err return err
} }
seq++ seq++
offset += n offset += n
bytesInBlock += n bytesInBlock += n
pct := 5 + (offset * 90 / len(firmware)) pct := offset * 100 / imageSize
notify("uploading", pct, fmt.Sprintf("%d / %d bytes", offset, len(firmware))) if pct > 99 {
pct = 99
}
notify("uploading", otaStepMaster, pct, fmt.Sprintf("Master: %d / %d bytes", offset, imageSize))
} }
if bytesInBlock == otaFlashBlockSize { if bytesInBlock == otaFlashBlockSize {
st, err := waitOtaStatus(sp, otaStBlockAck, otaDefaultTimeout, nil) st, err := waitOtaStatus(sp, otaStBlockAck, otaDefaultTimeout, nil)
if err != nil { if err != nil {
notify("error", 0, err.Error()) notify("error", "", 0, err.Error())
return err return err
} }
pct := 5 + (offset * 90 / len(firmware)) pct := offset * 100 / imageSize
notify("uploading", pct, fmt.Sprintf("Block geschrieben (%d bytes in flash)", st.GetBytesWritten()), if pct > 99 {
pct = 99
}
notify("uploading", otaStepMaster, pct,
fmt.Sprintf("Master: Block geschrieben (%d bytes)", st.GetBytesWritten()),
OTAProgress{Bytes: st.GetBytesWritten()}) OTAProgress{Bytes: st.GetBytesWritten()})
} }
} }
masterPct = 100
masterMsg = "Master: UART-Upload abgeschlossen"
notify("uploading", otaStepMaster, 100, masterMsg)
if err := writeUartMessage(sp, &pb.UartMessage{ if err := writeUartMessage(sp, &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{},
}, },
}, false); err != nil { }, false); err != nil {
notify("error", 0, err.Error()) notify("error", "", 0, err.Error())
return err return err
} }
slaveBytes := make(map[uint32]uint32)
slaveDetails := make(map[uint32]OtaSlaveDetail)
emitSlaveOTA := func(msg string, aggBytes uint32, slaveCount uint32) {
if slaveCount == 0 && len(slaveDetails) > 0 {
slaveCount = uint32(len(slaveDetails))
}
notify("distributing", otaStepSlaves, 0, msg,
OTAProgress{
Bytes: aggBytes, Slaves: slaveCount,
MasterPercent: 100, MasterMessage: masterMsg,
SlaveProgress: copySlaveMap(slaveBytes),
SlaveDetails: copySlaveDetails(slaveDetails),
})
}
onDistStatus := func(st *pb.OtaStatusPayload) {
applyDistributingOtaStatus(st, imageSize, slaveBytes, slaveDetails)
}
var lastEmit, lastQuery time.Time
slaveDistMessage := func() (msg string, aggBytes, slaveCount uint32) {
slaveCount = uint32(len(slaveDetails))
for _, d := range slaveDetails {
if d.BytesWritten > aggBytes {
aggBytes = d.BytesWritten
}
}
if slaveCount == 0 {
return "Keine verfügbaren Slaves — Verteilung übersprungen", 0, 0
}
return fmt.Sprintf("ESP-NOW: %d / %d bytes (%d Slaves)",
aggBytes, imageSize, slaveCount), aggBytes, slaveCount
}
emitSlaveThrottled := func(force bool) {
if !force && time.Since(lastEmit) < otaDistEmitMinInterval {
return
}
lastEmit = time.Now()
msg, agg, n := slaveDistMessage()
emitSlaveOTA(msg, agg, n)
}
querySlaveProgress := func() {
if time.Since(lastQuery) < otaDistQueryInterval {
return
}
lastQuery = time.Now()
prog, err := queryOtaSlaveProgressLocked(sp, 0, onDistStatus, otaDistQueryTimeout)
if err != nil {
if len(slaveDetails) > 0 {
emitSlaveThrottled(true)
}
return
}
mergeSlaveProgressResponse(prog, slaveBytes, slaveDetails)
emitSlaveThrottled(true)
}
pushSlaveDist := func(st *pb.OtaStatusPayload) {
onDistStatus(st)
emitSlaveThrottled(false)
}
onWaitTick := func() {
querySlaveProgress()
}
lastQuery = time.Time{} // first query immediately when distribution starts
querySlaveProgress()
st, err := waitOtaComplete(sp, otaDistTimeout, pushSlaveDist, onWaitTick, otaDistReadTimeout)
if err != nil {
notify("error", "", 0, err.Error())
return err
}
if prog, err := queryOtaSlaveProgressLocked(sp, 0, nil, otaDistQueryTimeout); err == nil {
mergeSlaveProgressResponse(prog, slaveBytes, slaveDetails)
}
notify("done", "", 100,
fmt.Sprintf("Fertig — %d bytes, Boot-Slot %d. Master und Slaves neu starten.",
st.GetBytesWritten(), st.GetTargetSlot()),
OTAProgress{
Bytes: st.GetBytesWritten(), Slot: st.GetTargetSlot(),
MasterPercent: 100, MasterMessage: "Master: OK",
SlaveProgress: copySlaveMap(slaveBytes),
SlaveDetails: copySlaveDetails(slaveDetails),
})
return nil
}
// 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 queryOtaSlaveProgressLocked(sp *serialPort, clientID uint32,
onStatus func(*pb.OtaStatusPayload), queryTimeout time.Duration) (*pb.OtaSlaveProgressResponse, error) {
req := &pb.UartMessage{
Type: pb.MessageType_OTA_SLAVE_PROGRESS,
Payload: &pb.UartMessage_OtaSlaveProgressRequest{
OtaSlaveProgressRequest: &pb.OtaSlaveProgressRequest{
ClientId: clientID,
},
},
}
if err := writeUartMessage(sp, req, false); 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)
if err != nil {
return nil, err
}
r := msg.GetOtaSlaveProgressResponse()
if r == nil {
return nil, fmt.Errorf("missing ota_slave_progress_response")
}
return r, nil
}
func applyDistributingOtaStatus(st *pb.OtaStatusPayload, imageSize int,
slaveBytes map[uint32]uint32, details map[uint32]OtaSlaveDetail) {
if st == nil || st.GetStatus() != otaStDistributing {
return
}
if st.GetError() != otaDistPerSlave {
return
}
id := st.GetTargetSlot()
bw := st.GetBytesWritten()
slaveBytes[id] = bw
d := details[id]
d.BytesWritten = bw
if d.TotalBytes == 0 {
d.TotalBytes = uint32(imageSize)
}
if d.Status == 0 || d.Status == 1 || d.Status == 2 {
d.Status = 3
}
details[id] = d
}
func readUartMessageUntil(sp *serialPort, deadline time.Time, want pb.MessageType,
onStatus func(*pb.OtaStatusPayload), readChunk time.Duration) (*pb.UartMessage, error) {
if readChunk <= 0 {
readChunk = otaStatusPollTimeout
}
for {
if time.Now().After(deadline) {
return nil, fmt.Errorf("timeout waiting for %v", want)
}
wait := time.Until(deadline)
if wait > readChunk {
wait = readChunk
}
if err := sp.port.SetReadTimeout(wait); err != nil {
return nil, err
}
payload, err := uartframe.ReadFrame(sp.port, nil)
if err != nil {
return nil, err
}
msg, err := decodeUartPayload(payload)
if err != nil {
continue
}
if msg.GetType() == pb.MessageType_OTA_STATUS {
if onStatus != nil {
if st := msg.GetOtaStatus(); st != nil {
onStatus(st)
}
}
continue
}
if msg.GetType() == want {
return msg, nil
}
}
}
func mergeSlaveProgressResponse(r *pb.OtaSlaveProgressResponse,
bytesOut map[uint32]uint32, detailsOut map[uint32]OtaSlaveDetail) {
if r == nil {
return
}
for _, s := range r.GetSlaves() {
id := s.GetClientId()
bytesOut[id] = s.GetBytesWritten()
detailsOut[id] = OtaSlaveDetail{
BytesWritten: s.GetBytesWritten(),
TotalBytes: s.GetTotalBytes(),
Status: s.GetStatus(),
Error: s.GetError(),
}
}
}
func copySlaveDetails(m map[uint32]OtaSlaveDetail) map[uint32]OtaSlaveDetail {
out := make(map[uint32]OtaSlaveDetail, len(m))
for k, v := range m {
out[k] = v
}
return out
}
func copySlaveMap(m map[uint32]uint32) map[uint32]uint32 {
out := make(map[uint32]uint32, len(m))
for k, v := range m {
out[k] = v
}
return out
}
func waitOtaComplete(sp *serialPort, timeout time.Duration,
onDistributing func(*pb.OtaStatusPayload), onInterval func(),
readTimeout time.Duration) (*pb.OtaStatusPayload, error) {
if readTimeout <= 0 {
readTimeout = otaStatusPollTimeout
}
deadline := time.Now().Add(timeout)
for {
if time.Now().After(deadline) {
return nil, fmt.Errorf("timeout waiting for OTA success (slave distribution?)")
}
readWait := time.Until(deadline)
if readWait > readTimeout {
readWait = readTimeout
}
if err := sp.port.SetReadTimeout(readWait); err != nil {
return nil, err
}
st, err := readOtaStatus(sp) st, err := readOtaStatus(sp)
if err != nil { if err != nil {
notify("error", 0, err.Error()) if onInterval != nil {
return err onInterval()
}
continue
}
switch st.GetStatus() {
case otaStSuccess:
return st, nil
case otaStFailed:
return nil, fmt.Errorf("OTA failed (error=%d)", st.GetError())
case otaStDistributing:
if onDistributing != nil {
onDistributing(st)
}
if onInterval != nil {
onInterval()
}
default:
// ignore other interim statuses
} }
if st.GetStatus() != otaStSuccess {
err := fmt.Errorf("OTA failed: status=%d error=%d", st.GetStatus(), st.GetError())
notify("error", 0, err.Error())
return err
} }
notify("done", 100, fmt.Sprintf("Erfolg — %d bytes auf Slot %d (Neustart)", st.GetBytesWritten(), st.GetTargetSlot()),
OTAProgress{Bytes: st.GetBytesWritten(), Slot: st.GetTargetSlot()})
return nil
} }
func writeUartMessage(sp *serialPort, msg *pb.UartMessage, logFrame bool) error { func writeUartMessage(sp *serialPort, msg *pb.UartMessage, logFrame bool) error {

View File

@ -37,6 +37,7 @@ const (
MessageType_OTA_END MessageType = 18 MessageType_OTA_END MessageType = 18
MessageType_OTA_STATUS MessageType = 19 MessageType_OTA_STATUS MessageType = 19
MessageType_OTA_START_ESPNOW MessageType = 20 MessageType_OTA_START_ESPNOW MessageType = 20
MessageType_OTA_SLAVE_PROGRESS MessageType = 21
) )
// Enum value maps for MessageType. // Enum value maps for MessageType.
@ -55,6 +56,7 @@ var (
18: "OTA_END", 18: "OTA_END",
19: "OTA_STATUS", 19: "OTA_STATUS",
20: "OTA_START_ESPNOW", 20: "OTA_START_ESPNOW",
21: "OTA_SLAVE_PROGRESS",
} }
MessageType_value = map[string]int32{ MessageType_value = map[string]int32{
"UNKNOWN": 0, "UNKNOWN": 0,
@ -70,6 +72,7 @@ var (
"OTA_END": 18, "OTA_END": 18,
"OTA_STATUS": 19, "OTA_STATUS": 19,
"OTA_START_ESPNOW": 20, "OTA_START_ESPNOW": 20,
"OTA_SLAVE_PROGRESS": 21,
} }
) )
@ -118,6 +121,8 @@ type UartMessage struct {
// *UartMessage_AccelDeadzoneResponse // *UartMessage_AccelDeadzoneResponse
// *UartMessage_EspnowUnicastTestRequest // *UartMessage_EspnowUnicastTestRequest
// *UartMessage_EspnowUnicastTestResponse // *UartMessage_EspnowUnicastTestResponse
// *UartMessage_OtaSlaveProgressRequest
// *UartMessage_OtaSlaveProgressResponse
Payload isUartMessage_Payload `protobuf_oneof:"payload"` Payload isUartMessage_Payload `protobuf_oneof:"payload"`
unknownFields protoimpl.UnknownFields unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache sizeCache protoimpl.SizeCache
@ -284,6 +289,24 @@ func (x *UartMessage) GetEspnowUnicastTestResponse() *EspNowUnicastTestResponse
return nil return nil
} }
func (x *UartMessage) GetOtaSlaveProgressRequest() *OtaSlaveProgressRequest {
if x != nil {
if x, ok := x.Payload.(*UartMessage_OtaSlaveProgressRequest); ok {
return x.OtaSlaveProgressRequest
}
}
return nil
}
func (x *UartMessage) GetOtaSlaveProgressResponse() *OtaSlaveProgressResponse {
if x != nil {
if x, ok := x.Payload.(*UartMessage_OtaSlaveProgressResponse); ok {
return x.OtaSlaveProgressResponse
}
}
return nil
}
type isUartMessage_Payload interface { type isUartMessage_Payload interface {
isUartMessage_Payload() isUartMessage_Payload()
} }
@ -340,6 +363,14 @@ type UartMessage_EspnowUnicastTestResponse struct {
EspnowUnicastTestResponse *EspNowUnicastTestResponse `protobuf:"bytes,14,opt,name=espnow_unicast_test_response,json=espnowUnicastTestResponse,proto3,oneof"` EspnowUnicastTestResponse *EspNowUnicastTestResponse `protobuf:"bytes,14,opt,name=espnow_unicast_test_response,json=espnowUnicastTestResponse,proto3,oneof"`
} }
type UartMessage_OtaSlaveProgressRequest struct {
OtaSlaveProgressRequest *OtaSlaveProgressRequest `protobuf:"bytes,15,opt,name=ota_slave_progress_request,json=otaSlaveProgressRequest,proto3,oneof"`
}
type UartMessage_OtaSlaveProgressResponse struct {
OtaSlaveProgressResponse *OtaSlaveProgressResponse `protobuf:"bytes,16,opt,name=ota_slave_progress_response,json=otaSlaveProgressResponse,proto3,oneof"`
}
func (*UartMessage_AckPayload) isUartMessage_Payload() {} func (*UartMessage_AckPayload) isUartMessage_Payload() {}
func (*UartMessage_EchoPayload) isUartMessage_Payload() {} func (*UartMessage_EchoPayload) isUartMessage_Payload() {}
@ -366,6 +397,10 @@ func (*UartMessage_EspnowUnicastTestRequest) isUartMessage_Payload() {}
func (*UartMessage_EspnowUnicastTestResponse) isUartMessage_Payload() {} func (*UartMessage_EspnowUnicastTestResponse) isUartMessage_Payload() {}
func (*UartMessage_OtaSlaveProgressRequest) isUartMessage_Payload() {}
func (*UartMessage_OtaSlaveProgressResponse) isUartMessage_Payload() {}
type Ack struct { type Ack struct {
state protoimpl.MessageState `protogen:"open.v1"` state protoimpl.MessageState `protogen:"open.v1"`
unknownFields protoimpl.UnknownFields unknownFields protoimpl.UnknownFields
@ -1134,7 +1169,7 @@ func (*OtaEndPayload) Descriptor() ([]byte, []int) {
} }
// Device → host status (also used as ACK after each 4 KiB written). // Device → host status (also used as ACK after each 4 KiB written).
// status: 1=preparing, 2=ready, 3=block_ack, 4=success, 5=failed // status: 1=preparing, 2=ready, 3=block_ack, 4=success, 5=failed, 6=distributing
type OtaStatusPayload struct { type OtaStatusPayload struct {
state protoimpl.MessageState `protogen:"open.v1"` state protoimpl.MessageState `protogen:"open.v1"`
Status uint32 `protobuf:"varint,1,opt,name=status,proto3" json:"status,omitempty"` Status uint32 `protobuf:"varint,1,opt,name=status,proto3" json:"status,omitempty"`
@ -1203,11 +1238,209 @@ func (x *OtaStatusPayload) GetError() uint32 {
return 0 return 0
} }
// Host → master: query ESP-NOW slave OTA progress (client_id 0 = all slaves in session).
type OtaSlaveProgressRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
ClientId uint32 `protobuf:"varint,1,opt,name=client_id,json=clientId,proto3" json:"client_id,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *OtaSlaveProgressRequest) Reset() {
*x = OtaSlaveProgressRequest{}
mi := &file_uart_messages_proto_msgTypes[16]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *OtaSlaveProgressRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*OtaSlaveProgressRequest) ProtoMessage() {}
func (x *OtaSlaveProgressRequest) ProtoReflect() protoreflect.Message {
mi := &file_uart_messages_proto_msgTypes[16]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use OtaSlaveProgressRequest.ProtoReflect.Descriptor instead.
func (*OtaSlaveProgressRequest) Descriptor() ([]byte, []int) {
return file_uart_messages_proto_rawDescGZIP(), []int{16}
}
func (x *OtaSlaveProgressRequest) GetClientId() uint32 {
if x != nil {
return x.ClientId
}
return 0
}
type OtaSlaveProgressEntry struct {
state protoimpl.MessageState `protogen:"open.v1"`
ClientId uint32 `protobuf:"varint,1,opt,name=client_id,json=clientId,proto3" json:"client_id,omitempty"`
BytesWritten uint32 `protobuf:"varint,2,opt,name=bytes_written,json=bytesWritten,proto3" json:"bytes_written,omitempty"`
TotalBytes uint32 `protobuf:"varint,3,opt,name=total_bytes,json=totalBytes,proto3" json:"total_bytes,omitempty"`
// * 0=idle, 1=preparing, 2=ready, 3=distributing, 4=success, 5=failed
Status uint32 `protobuf:"varint,4,opt,name=status,proto3" json:"status,omitempty"`
Error uint32 `protobuf:"varint,5,opt,name=error,proto3" json:"error,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *OtaSlaveProgressEntry) Reset() {
*x = OtaSlaveProgressEntry{}
mi := &file_uart_messages_proto_msgTypes[17]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *OtaSlaveProgressEntry) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*OtaSlaveProgressEntry) ProtoMessage() {}
func (x *OtaSlaveProgressEntry) ProtoReflect() protoreflect.Message {
mi := &file_uart_messages_proto_msgTypes[17]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use OtaSlaveProgressEntry.ProtoReflect.Descriptor instead.
func (*OtaSlaveProgressEntry) Descriptor() ([]byte, []int) {
return file_uart_messages_proto_rawDescGZIP(), []int{17}
}
func (x *OtaSlaveProgressEntry) GetClientId() uint32 {
if x != nil {
return x.ClientId
}
return 0
}
func (x *OtaSlaveProgressEntry) GetBytesWritten() uint32 {
if x != nil {
return x.BytesWritten
}
return 0
}
func (x *OtaSlaveProgressEntry) GetTotalBytes() uint32 {
if x != nil {
return x.TotalBytes
}
return 0
}
func (x *OtaSlaveProgressEntry) GetStatus() uint32 {
if x != nil {
return x.Status
}
return 0
}
func (x *OtaSlaveProgressEntry) GetError() uint32 {
if x != nil {
return x.Error
}
return 0
}
type OtaSlaveProgressResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
Active bool `protobuf:"varint,1,opt,name=active,proto3" json:"active,omitempty"`
TotalBytes uint32 `protobuf:"varint,2,opt,name=total_bytes,json=totalBytes,proto3" json:"total_bytes,omitempty"`
AggregateBytes uint32 `protobuf:"varint,3,opt,name=aggregate_bytes,json=aggregateBytes,proto3" json:"aggregate_bytes,omitempty"`
SlaveCount uint32 `protobuf:"varint,4,opt,name=slave_count,json=slaveCount,proto3" json:"slave_count,omitempty"`
Slaves []*OtaSlaveProgressEntry `protobuf:"bytes,5,rep,name=slaves,proto3" json:"slaves,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *OtaSlaveProgressResponse) Reset() {
*x = OtaSlaveProgressResponse{}
mi := &file_uart_messages_proto_msgTypes[18]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *OtaSlaveProgressResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*OtaSlaveProgressResponse) ProtoMessage() {}
func (x *OtaSlaveProgressResponse) ProtoReflect() protoreflect.Message {
mi := &file_uart_messages_proto_msgTypes[18]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use OtaSlaveProgressResponse.ProtoReflect.Descriptor instead.
func (*OtaSlaveProgressResponse) Descriptor() ([]byte, []int) {
return file_uart_messages_proto_rawDescGZIP(), []int{18}
}
func (x *OtaSlaveProgressResponse) GetActive() bool {
if x != nil {
return x.Active
}
return false
}
func (x *OtaSlaveProgressResponse) GetTotalBytes() uint32 {
if x != nil {
return x.TotalBytes
}
return 0
}
func (x *OtaSlaveProgressResponse) GetAggregateBytes() uint32 {
if x != nil {
return x.AggregateBytes
}
return 0
}
func (x *OtaSlaveProgressResponse) GetSlaveCount() uint32 {
if x != nil {
return x.SlaveCount
}
return 0
}
func (x *OtaSlaveProgressResponse) GetSlaves() []*OtaSlaveProgressEntry {
if x != nil {
return x.Slaves
}
return nil
}
var File_uart_messages_proto protoreflect.FileDescriptor var File_uart_messages_proto protoreflect.FileDescriptor
const file_uart_messages_proto_rawDesc = "" + const file_uart_messages_proto_rawDesc = "" +
"\n" + "\n" +
"\x13uart_messages.proto\x12\x04alox\"\xcc\a\n" + "\x13uart_messages.proto\x12\x04alox\x1a\fnanopb.proto\"\x8b\t\n" +
"\vUartMessage\x12%\n" + "\vUartMessage\x12%\n" +
"\x04type\x18\x01 \x01(\x0e2\x11.alox.MessageTypeR\x04type\x12,\n" + "\x04type\x18\x01 \x01(\x0e2\x11.alox.MessageTypeR\x04type\x12,\n" +
"\vack_payload\x18\x02 \x01(\v2\t.alox.AckH\x00R\n" + "\vack_payload\x18\x02 \x01(\v2\t.alox.AckH\x00R\n" +
@ -1226,7 +1459,9 @@ const file_uart_messages_proto_rawDesc = "" +
"\x16accel_deadzone_request\x18\v \x01(\v2\x1a.alox.AccelDeadzoneRequestH\x00R\x14accelDeadzoneRequest\x12U\n" + "\x16accel_deadzone_request\x18\v \x01(\v2\x1a.alox.AccelDeadzoneRequestH\x00R\x14accelDeadzoneRequest\x12U\n" +
"\x17accel_deadzone_response\x18\f \x01(\v2\x1b.alox.AccelDeadzoneResponseH\x00R\x15accelDeadzoneResponse\x12_\n" + "\x17accel_deadzone_response\x18\f \x01(\v2\x1b.alox.AccelDeadzoneResponseH\x00R\x15accelDeadzoneResponse\x12_\n" +
"\x1bespnow_unicast_test_request\x18\r \x01(\v2\x1e.alox.EspNowUnicastTestRequestH\x00R\x18espnowUnicastTestRequest\x12b\n" + "\x1bespnow_unicast_test_request\x18\r \x01(\v2\x1e.alox.EspNowUnicastTestRequestH\x00R\x18espnowUnicastTestRequest\x12b\n" +
"\x1cespnow_unicast_test_response\x18\x0e \x01(\v2\x1f.alox.EspNowUnicastTestResponseH\x00R\x19espnowUnicastTestResponseB\t\n" + "\x1cespnow_unicast_test_response\x18\x0e \x01(\v2\x1f.alox.EspNowUnicastTestResponseH\x00R\x19espnowUnicastTestResponse\x12\\\n" +
"\x1aota_slave_progress_request\x18\x0f \x01(\v2\x1d.alox.OtaSlaveProgressRequestH\x00R\x17otaSlaveProgressRequest\x12_\n" +
"\x1bota_slave_progress_response\x18\x10 \x01(\v2\x1e.alox.OtaSlaveProgressResponseH\x00R\x18otaSlaveProgressResponseB\t\n" +
"\apayload\"\x05\n" + "\apayload\"\x05\n" +
"\x03Ack\"!\n" + "\x03Ack\"!\n" +
"\vEchoPayload\x12\x12\n" + "\vEchoPayload\x12\x12\n" +
@ -1272,18 +1507,35 @@ const file_uart_messages_proto_rawDesc = "" +
"\x03seq\x18\x02 \x01(\rR\x03seq\"0\n" + "\x03seq\x18\x02 \x01(\rR\x03seq\"0\n" +
"\x0fOtaStartPayload\x12\x1d\n" + "\x0fOtaStartPayload\x12\x1d\n" +
"\n" + "\n" +
"total_size\x18\x01 \x01(\rR\ttotalSize\"2\n" + "total_size\x18\x01 \x01(\rR\ttotalSize\":\n" +
"\n" + "\n" +
"OtaPayload\x12\x10\n" + "OtaPayload\x12\x10\n" +
"\x03seq\x18\x01 \x01(\rR\x03seq\x12\x12\n" + "\x03seq\x18\x01 \x01(\rR\x03seq\x12\x1a\n" +
"\x04data\x18\x02 \x01(\fR\x04data\"\x0f\n" + "\x04data\x18\x02 \x01(\fB\x06\x92?\x03\b\xc8\x01R\x04data\"\x0f\n" +
"\rOtaEndPayload\"\x86\x01\n" + "\rOtaEndPayload\"\x86\x01\n" +
"\x10OtaStatusPayload\x12\x16\n" + "\x10OtaStatusPayload\x12\x16\n" +
"\x06status\x18\x01 \x01(\rR\x06status\x12#\n" + "\x06status\x18\x01 \x01(\rR\x06status\x12#\n" +
"\rbytes_written\x18\x02 \x01(\rR\fbytesWritten\x12\x1f\n" + "\rbytes_written\x18\x02 \x01(\rR\fbytesWritten\x12\x1f\n" +
"\vtarget_slot\x18\x03 \x01(\rR\n" + "\vtarget_slot\x18\x03 \x01(\rR\n" +
"targetSlot\x12\x14\n" + "targetSlot\x12\x14\n" +
"\x05error\x18\x04 \x01(\rR\x05error*\xdd\x01\n" + "\x05error\x18\x04 \x01(\rR\x05error\"6\n" +
"\x17OtaSlaveProgressRequest\x12\x1b\n" +
"\tclient_id\x18\x01 \x01(\rR\bclientId\"\xa8\x01\n" +
"\x15OtaSlaveProgressEntry\x12\x1b\n" +
"\tclient_id\x18\x01 \x01(\rR\bclientId\x12#\n" +
"\rbytes_written\x18\x02 \x01(\rR\fbytesWritten\x12\x1f\n" +
"\vtotal_bytes\x18\x03 \x01(\rR\n" +
"totalBytes\x12\x16\n" +
"\x06status\x18\x04 \x01(\rR\x06status\x12\x14\n" +
"\x05error\x18\x05 \x01(\rR\x05error\"\xd9\x01\n" +
"\x18OtaSlaveProgressResponse\x12\x16\n" +
"\x06active\x18\x01 \x01(\bR\x06active\x12\x1f\n" +
"\vtotal_bytes\x18\x02 \x01(\rR\n" +
"totalBytes\x12'\n" +
"\x0faggregate_bytes\x18\x03 \x01(\rR\x0eaggregateBytes\x12\x1f\n" +
"\vslave_count\x18\x04 \x01(\rR\n" +
"slaveCount\x12:\n" +
"\x06slaves\x18\x05 \x03(\v2\x1b.alox.OtaSlaveProgressEntryB\x05\x92?\x02\x10\x10R\x06slaves*\xf5\x01\n" +
"\vMessageType\x12\v\n" + "\vMessageType\x12\v\n" +
"\aUNKNOWN\x10\x00\x12\a\n" + "\aUNKNOWN\x10\x00\x12\a\n" +
"\x03ACK\x10\x01\x12\b\n" + "\x03ACK\x10\x01\x12\b\n" +
@ -1298,7 +1550,8 @@ const file_uart_messages_proto_rawDesc = "" +
"\aOTA_END\x10\x12\x12\x0e\n" + "\aOTA_END\x10\x12\x12\x0e\n" +
"\n" + "\n" +
"OTA_STATUS\x10\x13\x12\x14\n" + "OTA_STATUS\x10\x13\x12\x14\n" +
"\x10OTA_START_ESPNOW\x10\x14b\x06proto3" "\x10OTA_START_ESPNOW\x10\x14\x12\x16\n" +
"\x12OTA_SLAVE_PROGRESS\x10\x15b\x06proto3"
var ( var (
file_uart_messages_proto_rawDescOnce sync.Once file_uart_messages_proto_rawDescOnce sync.Once
@ -1313,7 +1566,7 @@ func file_uart_messages_proto_rawDescGZIP() []byte {
} }
var file_uart_messages_proto_enumTypes = make([]protoimpl.EnumInfo, 1) var file_uart_messages_proto_enumTypes = make([]protoimpl.EnumInfo, 1)
var file_uart_messages_proto_msgTypes = make([]protoimpl.MessageInfo, 16) var file_uart_messages_proto_msgTypes = make([]protoimpl.MessageInfo, 19)
var file_uart_messages_proto_goTypes = []any{ var file_uart_messages_proto_goTypes = []any{
(MessageType)(0), // 0: alox.MessageType (MessageType)(0), // 0: alox.MessageType
(*UartMessage)(nil), // 1: alox.UartMessage (*UartMessage)(nil), // 1: alox.UartMessage
@ -1332,6 +1585,9 @@ var file_uart_messages_proto_goTypes = []any{
(*OtaPayload)(nil), // 14: alox.OtaPayload (*OtaPayload)(nil), // 14: alox.OtaPayload
(*OtaEndPayload)(nil), // 15: alox.OtaEndPayload (*OtaEndPayload)(nil), // 15: alox.OtaEndPayload
(*OtaStatusPayload)(nil), // 16: alox.OtaStatusPayload (*OtaStatusPayload)(nil), // 16: alox.OtaStatusPayload
(*OtaSlaveProgressRequest)(nil), // 17: alox.OtaSlaveProgressRequest
(*OtaSlaveProgressEntry)(nil), // 18: alox.OtaSlaveProgressEntry
(*OtaSlaveProgressResponse)(nil), // 19: alox.OtaSlaveProgressResponse
} }
var file_uart_messages_proto_depIdxs = []int32{ var file_uart_messages_proto_depIdxs = []int32{
0, // 0: alox.UartMessage.type:type_name -> alox.MessageType 0, // 0: alox.UartMessage.type:type_name -> alox.MessageType
@ -1348,13 +1604,16 @@ var file_uart_messages_proto_depIdxs = []int32{
10, // 11: alox.UartMessage.accel_deadzone_response:type_name -> alox.AccelDeadzoneResponse 10, // 11: alox.UartMessage.accel_deadzone_response:type_name -> alox.AccelDeadzoneResponse
11, // 12: alox.UartMessage.espnow_unicast_test_request:type_name -> alox.EspNowUnicastTestRequest 11, // 12: alox.UartMessage.espnow_unicast_test_request:type_name -> alox.EspNowUnicastTestRequest
12, // 13: alox.UartMessage.espnow_unicast_test_response:type_name -> alox.EspNowUnicastTestResponse 12, // 13: alox.UartMessage.espnow_unicast_test_response:type_name -> alox.EspNowUnicastTestResponse
5, // 14: alox.ClientInfoResponse.clients:type_name -> alox.ClientInfo 17, // 14: alox.UartMessage.ota_slave_progress_request:type_name -> alox.OtaSlaveProgressRequest
7, // 15: alox.ClientInputResponse.clients:type_name -> alox.ClientInput 19, // 15: alox.UartMessage.ota_slave_progress_response:type_name -> alox.OtaSlaveProgressResponse
16, // [16:16] is the sub-list for method output_type 5, // 16: alox.ClientInfoResponse.clients:type_name -> alox.ClientInfo
16, // [16:16] is the sub-list for method input_type 7, // 17: alox.ClientInputResponse.clients:type_name -> alox.ClientInput
16, // [16:16] is the sub-list for extension type_name 18, // 18: alox.OtaSlaveProgressResponse.slaves:type_name -> alox.OtaSlaveProgressEntry
16, // [16:16] is the sub-list for extension extendee 19, // [19:19] is the sub-list for method output_type
0, // [0:16] is the sub-list for field type_name 19, // [19:19] is the sub-list for method input_type
19, // [19:19] is the sub-list for extension type_name
19, // [19:19] is the sub-list for extension extendee
0, // [0:19] is the sub-list for field type_name
} }
func init() { file_uart_messages_proto_init() } func init() { file_uart_messages_proto_init() }
@ -1376,6 +1635,8 @@ func file_uart_messages_proto_init() {
(*UartMessage_AccelDeadzoneResponse)(nil), (*UartMessage_AccelDeadzoneResponse)(nil),
(*UartMessage_EspnowUnicastTestRequest)(nil), (*UartMessage_EspnowUnicastTestRequest)(nil),
(*UartMessage_EspnowUnicastTestResponse)(nil), (*UartMessage_EspnowUnicastTestResponse)(nil),
(*UartMessage_OtaSlaveProgressRequest)(nil),
(*UartMessage_OtaSlaveProgressResponse)(nil),
} }
type x struct{} type x struct{}
out := protoimpl.TypeBuilder{ out := protoimpl.TypeBuilder{
@ -1383,7 +1644,7 @@ func file_uart_messages_proto_init() {
GoPackagePath: reflect.TypeOf(x{}).PkgPath(), GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_uart_messages_proto_rawDesc), len(file_uart_messages_proto_rawDesc)), RawDescriptor: unsafe.Slice(unsafe.StringData(file_uart_messages_proto_rawDesc), len(file_uart_messages_proto_rawDesc)),
NumEnums: 1, NumEnums: 1,
NumMessages: 16, NumMessages: 19,
NumExtensions: 0, NumExtensions: 0,
NumServices: 0, NumServices: 0,
}, },

View File

@ -1,12 +1,16 @@
package main package main
import ( import (
"errors"
"fmt" "fmt"
"log" "log"
"sync" "sync"
"time" "time"
) )
// errUARTBusy is returned when the port is held for OTA (poller should not treat as unplug).
var errUARTBusy = errors.New("uart busy (OTA in progress)")
// managedSerial keeps the UART open and reconnects after I/O failures or unplug. // managedSerial keeps the UART open and reconnects after I/O failures or unplug.
type managedSerial struct { type managedSerial struct {
portName string portName string
@ -69,7 +73,22 @@ func (m *managedSerial) invalidateLocked(reason error) {
} }
func (m *managedSerial) withPort(fn func(*serialPort) error) error { func (m *managedSerial) withPort(fn func(*serialPort) error) error {
return m.withPortLocked(false, fn)
}
// withPortPoll is like withPort but returns errUARTBusy instead of blocking during OTA.
func (m *managedSerial) withPortPoll(fn func(*serialPort) error) error {
return m.withPortLocked(true, fn)
}
func (m *managedSerial) withPortLocked(try bool, fn func(*serialPort) error) error {
if try {
if !m.mu.TryLock() {
return errUARTBusy
}
} else {
m.mu.Lock() m.mu.Lock()
}
defer m.mu.Unlock() defer m.mu.Unlock()
if m.sp == nil { if m.sp == nil {
@ -86,8 +105,19 @@ func (m *managedSerial) withPort(fn func(*serialPort) error) error {
} }
func (m *managedSerial) exchangePayload(payload []byte, cmdName string) ([]byte, error) { func (m *managedSerial) exchangePayload(payload []byte, cmdName string) ([]byte, error) {
return m.exchangePayloadVia(m.withPort, payload, cmdName)
}
func (m *managedSerial) exchangePayloadPoll(payload []byte, cmdName string) ([]byte, error) {
return m.exchangePayloadVia(m.withPortPoll, payload, cmdName)
}
func (m *managedSerial) exchangePayloadVia(
portFn func(func(*serialPort) error) error,
payload []byte, cmdName string,
) ([]byte, error) {
var resp []byte var resp []byte
err := m.withPort(func(sp *serialPort) error { err := portFn(func(sp *serialPort) error {
var e error var e error
resp, e = sp.exchangePayloadLocked(payload, cmdName) resp, e = sp.exchangePayloadLocked(payload, cmdName)
return e return e
@ -96,8 +126,19 @@ func (m *managedSerial) exchangePayload(payload []byte, cmdName string) ([]byte,
} }
func (m *managedSerial) exchange(cmdID byte, cmdName string) ([]byte, error) { func (m *managedSerial) exchange(cmdID byte, cmdName string) ([]byte, error) {
return m.exchangeVia(m.withPort, cmdID, cmdName)
}
func (m *managedSerial) exchangePoll(cmdID byte, cmdName string) ([]byte, error) {
return m.exchangeVia(m.withPortPoll, cmdID, cmdName)
}
func (m *managedSerial) exchangeVia(
portFn func(func(*serialPort) error) error,
cmdID byte, cmdName string,
) ([]byte, error) {
var resp []byte var resp []byte
err := m.withPort(func(sp *serialPort) error { err := portFn(func(sp *serialPort) error {
var e error var e error
resp, e = sp.exchangeLocked(cmdID, cmdName) resp, e = sp.exchangeLocked(cmdID, cmdName)
return e return e

View File

@ -131,6 +131,19 @@
.progress-bar { .progress-bar {
background: #2d6cdf; background: #2d6cdf;
} }
.progress-bar.bg-success { background: #00a86b !important; }
.progress-bar.bg-danger { background: #e35d6a !important; }
.progress-bar.bg-info { background: #2d6cdf !important; }
.progress-bar.bg-secondary { background: #5c6570 !important; }
.ota-progress-table .ota-progress-col {
width: 100%;
}
.ota-progress-table .ota-restart-col {
width: 6.5rem;
white-space: nowrap;
vertical-align: middle !important;
}
.form-control[type="file"]::file-selector-button { .form-control[type="file"]::file-selector-button {
background: var(--pp-border); background: var(--pp-border);
border: none; border: none;
@ -180,7 +193,7 @@
<dl class="row mb-0"> <dl class="row mb-0">
<dt class="col-5 text-muted">Version</dt> <dt class="col-5 text-muted">Version</dt>
<dd class="col-7" x-text="state.master.version"></dd> <dd class="col-7" x-text="state.master.version"></dd>
<dt class="col-5 text-muted">Git</dt> <dt class="col-5 text-muted">Hash</dt>
<dd class="col-7 text-break" x-text="state.master.git_hash"></dd> <dd class="col-7 text-break" x-text="state.master.git_hash"></dd>
<dt class="col-5 text-muted">Partition</dt> <dt class="col-5 text-muted">Partition</dt>
<dd class="col-7" x-text="state.master.running_partition || '—'"></dd> <dd class="col-7" x-text="state.master.running_partition || '—'"></dd>
@ -303,7 +316,7 @@
<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 die inaktive OTA-Partition (wie <code>gotool ota</code>). Lädt eine <code>.bin</code> auf den Master (UART), danach verteilt die Firmware automatisch per ESP-NOW an alle verfügbaren Slaves.
Während des Uploads pausiert das Live-Polling. Während des Uploads pausiert das Live-Polling.
</p> </p>
<div class="mb-3"> <div class="mb-3">
@ -320,17 +333,76 @@
<span class="text-muted small" x-show="otaFile" <span class="text-muted small" x-show="otaFile"
x-text="otaFile ? otaFile.name + ' (' + formatSize(otaFile.size) + ')' : ''"></span> x-text="otaFile ? otaFile.name + ' (' + formatSize(otaFile.size) + ')' : ''"></span>
</div> </div>
<template x-if="ota.active || ota.phase === 'done' || ota.phase === 'error'"> <div class="ota-progress-panel mt-3"
<div class="progress mb-2" style="height: 1.25rem;"> x-show="ota.active || ota.phase === 'distributing' || ota.phase === 'done' || ota.phase === 'error'">
<div class="progress-bar" role="progressbar" <p class="small text-muted mb-2">Master (UART)</p>
:style="'width: ' + ota.percent + '%'" <table class="table table-sm pp-table ota-progress-table mb-3">
:class="ota.phase === 'error' ? 'bg-danger' : (ota.phase === 'done' ? 'bg-success' : '')" <tbody>
x-text="ota.percent + '%'"></div> <tr>
<td class="ota-progress-col">
<div class="d-flex justify-content-between small text-muted mb-1">
<span>Master</span>
<span x-text="otaMasterPct() + '%'"></span>
</div> </div>
<p class="small mb-0" <div class="progress mb-1" style="height: 1.1rem;">
:class="ota.phase === 'error' ? 'text-danger' : (ota.phase === 'done' ? 'text-success' : 'text-muted')" <div class="progress-bar" role="progressbar"
:style="'width: ' + otaMasterPct() + '%'"
:class="otaMasterBarClass()"
x-text="otaMasterPct() + '%'"></div>
</div>
<p class="small text-muted mb-0"
x-text="ota.masterMessage || (ota.step === 'master' ? ota.message : '')"></p>
</td>
<td class="ota-restart-col text-end">
<button type="button" class="btn btn-outline-secondary btn-sm">Restart</button>
</td>
</tr>
</tbody>
</table>
<p class="small text-muted mb-2">
Slaves (ESP-NOW)
<span x-text="'(' + otaSlaveRows().length + ')'"></span>
</p>
<p class="small text-muted mb-2" x-show="ota.message && ota.step === 'slaves'"
x-text="ota.message"></p> x-text="ota.message"></p>
<table class="table table-sm pp-table ota-progress-table mb-2">
<tbody>
<template x-for="row in otaSlaveRows()" :key="'ota-slave-' + row.id">
<tr>
<td class="ota-progress-col">
<div class="d-flex justify-content-between small mb-1">
<span>
Slave <span x-text="row.id"></span>
<span class="text-muted mac ms-1" x-text="row.mac"></span>
<span class="badge bg-secondary ms-1" x-text="row.statusLabel"></span>
</span>
<span x-text="row.percent + '%'"></span>
</div>
<div class="progress mb-1" style="height: 0.85rem;">
<div class="progress-bar" role="progressbar"
:class="row.barClass"
:style="'width: ' + row.percent + '%'"></div>
</div>
<p class="small text-muted mb-0" x-text="row.bytesLabel"></p>
</td>
<td class="ota-restart-col text-end">
<button type="button" class="btn btn-outline-secondary btn-sm">Restart</button>
</td>
</tr>
</template> </template>
<tr x-show="otaSlaveRows().length === 0">
<td colspan="2" class="text-muted small py-3">
Warte auf Slave-Fortschritt…
</td>
</tr>
</tbody>
</table>
<p class="small mb-0" x-show="ota.phase === 'done' || ota.phase === 'error'"
:class="ota.phase === 'error' ? 'text-danger' : 'text-success'"
x-text="ota.message"></p>
</div>
</div> </div>
</div> </div>
</section> </section>
@ -347,7 +419,13 @@
allDz: 100, allDz: 100,
slaveDz: {}, slaveDz: {},
otaFile: null, otaFile: null,
ota: { active: false, phase: '', percent: 0, message: '' }, ota: {
active: false, phase: '', step: '', percent: 0,
masterPercent: 0, masterDone: false, masterMessage: '',
message: '', slaves: 0, imageSize: 0,
slaveProgress: {},
slaveDetails: {}
},
busy: false, busy: false,
configMsg: '', configMsg: '',
configMsgOk: false, configMsgOk: false,
@ -392,20 +470,148 @@
if (n < 1024 * 1024) return (n / 1024).toFixed(1) + ' KiB'; if (n < 1024 * 1024) return (n / 1024).toFixed(1) + ' KiB';
return (n / (1024 * 1024)).toFixed(2) + ' MiB'; return (n / (1024 * 1024)).toFixed(2) + ' MiB';
}, },
otaMasterPct() {
if (this.ota.masterDone || this.ota.step === 'slaves' ||
this.ota.phase === 'distributing' || this.ota.phase === 'done') {
return this.ota.masterPercent >= 100 ? 100 : (this.ota.masterPercent || 100);
}
if (this.ota.step === 'master' || this.ota.phase === 'preparing' ||
this.ota.phase === 'ready' || this.ota.phase === 'uploading') {
return this.ota.masterPercent ?? this.ota.percent ?? 0;
}
return this.ota.masterPercent || 0;
},
otaMasterBarClass() {
if (this.ota.phase === 'error' && this.ota.step === 'master') return 'bg-danger';
if (this.otaMasterPct() >= 100) return 'bg-success';
return '';
},
mergeSlaveDetails(incoming) {
const out = { ...(this.ota.slaveDetails || {}) };
if (!incoming) return out;
for (const [k, v] of Object.entries(incoming)) {
const id = Number(k);
out[id] = { ...(out[id] || {}), ...v };
}
return out;
},
otaSlaveStatusLabel(status) {
const labels = {
0: 'idle', 1: 'vorbereiten', 2: 'bereit', 3: 'lädt',
4: 'fertig', 5: 'fehler'
};
return labels[status] || 'status ' + status;
},
otaSlaveBarClass(status) {
if (status === 4) return 'bg-success';
if (status === 5) return 'bg-danger';
if (status === 1 || status === 2) return 'bg-secondary';
return 'bg-info';
},
otaSlaveRows() {
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 || {})) {
const id = Number(k);
if (!Number.isNaN(id)) {
ids.add(id);
if (!details[id]) {
details[id] = { bytes_written: v, status: 3 };
}
}
}
const rows = [];
for (const id of [...ids].sort((a, b) => a - b)) {
const c = (this.state.clients || []).find(x => x.id === id);
const d = details[id] || {};
const total = d.total_bytes || this.ota.imageSize || this.otaFile?.size || 0;
const bytes = d.bytes_written ?? 0;
const status = d.status ?? 0;
let percent = 0;
if (total > 0) percent = Math.min(100, Math.round(bytes * 100 / total));
if (status === 4) percent = 100;
rows.push({
id,
mac: c?.mac ? this.formatMac(c.mac) : '—',
percent,
status,
statusLabel: this.otaSlaveStatusLabel(status),
barClass: this.otaSlaveBarClass(status),
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) { applyOTAProgress(p) {
this.ota.phase = p.phase || ''; this.ota.phase = p.phase || '';
this.ota.percent = p.percent ?? 0; this.ota.step = p.step || this.ota.step || '';
this.ota.percent = p.percent ?? this.ota.percent;
this.ota.message = p.message || ''; this.ota.message = p.message || '';
if (p.phase === 'preparing' || p.phase === 'ready' || p.phase === 'uploading') { if (p.image_size) this.ota.imageSize = p.image_size;
if (p.master_done) this.ota.masterDone = true;
if (p.step === 'master' || p.phase === 'preparing' || p.phase === 'ready' || p.phase === 'uploading') {
if (p.master_percent != null) this.ota.masterPercent = p.master_percent;
else if (p.percent != null) this.ota.masterPercent = p.percent;
if (p.master_message) this.ota.masterMessage = p.master_message;
else if (p.message) this.ota.masterMessage = p.message;
} else if (p.step === 'slaves' || p.phase === 'distributing' || p.phase === 'done') {
this.ota.masterDone = true;
if (p.master_percent != null) this.ota.masterPercent = p.master_percent;
else if (this.ota.masterPercent < 100) this.ota.masterPercent = 100;
if (p.master_message) this.ota.masterMessage = p.master_message;
}
if (p.slaves != null) this.ota.slaves = p.slaves;
if (p.phase === 'distributing') {
this.ota.step = 'slaves';
}
if (p.slave_details) {
const merged = this.mergeSlaveDetails(p.slave_details);
this.ota.slaveDetails = { ...merged };
if (Object.keys(this.ota.slaveDetails).length > 0) {
this.ota.step = 'slaves';
}
}
if (p.slave_progress) {
if (!this.ota.slaveProgress) this.ota.slaveProgress = {};
for (const [k, v] of Object.entries(p.slave_progress)) {
this.ota.slaveProgress[Number(k)] = v;
}
}
if (p.phase === 'preparing' || p.phase === 'ready' || p.phase === 'uploading' ||
p.phase === 'distributing') {
this.ota.active = true; this.ota.active = true;
} }
if (p.phase === 'done' || p.phase === 'error') { if (p.phase === 'done' || p.phase === 'error') {
this.ota.active = false; this.ota.active = false;
if (p.phase === 'done') {
this.ota.masterPercent = 100;
}
} }
}, },
async uploadOTA() { async uploadOTA() {
if (!this.otaFile) return; if (!this.otaFile) return;
this.ota = { active: true, phase: 'preparing', percent: 0, message: 'Upload startet…' }; this.ota = {
active: true, phase: 'preparing', step: 'master', percent: 0,
masterPercent: 0, masterDone: false, masterMessage: 'Upload startet…',
message: 'Upload startet…', slaves: 0,
imageSize: this.otaFile?.size || 0,
slaveProgress: {}, slaveDetails: {}
};
this.busy = true; this.busy = true;
const form = new FormData(); const form = new FormData();
form.append('firmware', this.otaFile); form.append('firmware', this.otaFile);
@ -414,23 +620,22 @@
const data = await r.json(); const data = await r.json();
if (!r.ok || !data.success) { if (!r.ok || !data.success) {
this.applyOTAProgress({ this.applyOTAProgress({
phase: 'error', phase: 'error', step: '', percent: 0,
percent: 0,
message: data.error || 'OTA fehlgeschlagen' message: data.error || 'OTA fehlgeschlagen'
}); });
return; return;
} }
if (this.ota.phase !== 'done') {
const slot = data.target_slot != null ? 'ota_' + data.target_slot : '?'; const slot = data.target_slot != null ? 'ota_' + data.target_slot : '?';
this.applyOTAProgress({ this.applyOTAProgress({
phase: 'done', phase: 'done', step: '', percent: 100,
percent: 100, message: `OK — ${data.bytes_written} Bytes, Slot ${slot}. Alle Knoten neu starten.`
message: `OK — ${data.bytes_written} Bytes nach ${slot} (Neustart zum Booten)`
}); });
}
} catch (e) { } catch (e) {
this.applyOTAProgress({ phase: 'error', percent: 0, message: String(e) }); this.applyOTAProgress({ phase: 'error', step: '', percent: 0, message: String(e) });
} finally { } finally {
this.busy = false; this.busy = false;
this.ota.active = false;
} }
}, },
flash(msg, ok) { flash(msg, ok) {

View File

@ -20,6 +20,7 @@ idf_component_register(
"cmd_accel_deadzone.c" "cmd_accel_deadzone.c"
"cmd_espnow_unicast_test.c" "cmd_espnow_unicast_test.c"
"cmd_ota.c" "cmd_ota.c"
"cmd_ota_slave_progress.c"
"ota_uart.c" "ota_uart.c"
"ota_espnow.c" "ota_espnow.c"
"client_registry.c" "client_registry.c"

View File

@ -207,6 +207,7 @@ Host and master speak nanopb-encoded `UartMessage` inside UART frames (byte 0 =
| 18 | `OTA_END` | Implemented — flush, `esp_ota_end`, push image to slaves via ESP-NOW, set boot | | 18 | `OTA_END` | Implemented — flush, `esp_ota_end`, push image to slaves via ESP-NOW, set boot |
| 19 | `OTA_STATUS` | Device → host (prepare/ready/block ACK/success/failed) | | 19 | `OTA_STATUS` | Device → host (prepare/ready/block ACK/success/failed) |
| 20 | `OTA_START_ESPNOW` | Implemented — re-distribute staged image to slaves only | | 20 | `OTA_START_ESPNOW` | Implemented — re-distribute staged image to slaves only |
| 21 | `OTA_SLAVE_PROGRESS` | Implemented (`cmd_ota_slave_progress.c`) — query per-slave ESP-NOW OTA progress |
Regenerate C code: Regenerate C code:
@ -260,7 +261,28 @@ Host upload:
go run . -port /dev/ttyUSB0 ota build/powerpod.bin go run . -port /dev/ttyUSB0 ota build/powerpod.bin
``` ```
`OtaStatusPayload.status`: `1` preparing, `2` ready, `3` block_ack, `4` success, `5` failed. `OtaStatusPayload.status`: `1` preparing, `2` ready, `3` block_ack, `4` success, `5` failed, `6` distributing (`bytes_written` = progress, `target_slot` = slave count).
### OTA_SLAVE_PROGRESS command
**Request:** framed `15` (`0x15`) + optional `ota_slave_progress_request` (`client_id`; `0` = all slaves in the current/last distribution session).
**Response:** `ota_slave_progress_response`:
| Field | Meaning |
|-------|---------|
| `active` | ESP-NOW distribution running |
| `total_bytes` | Image size |
| `aggregate_bytes` | Overall bytes sent to all slaves |
| `slave_count` | Number of slaves in session |
| `slaves[]` | Per slave: `client_id`, `bytes_written`, `total_bytes`, `status`, `error` |
Per-slave `status`: `0` idle, `1` preparing, `2` ready, `3` block_ack/distributing, `4` success, `5` failed.
```bash
go run . -port /dev/ttyUSB0 ota-progress
go run . -port /dev/ttyUSB0 ota-progress -client 16
```
### ACCEL_DEADZONE command ### ACCEL_DEADZONE command

View File

@ -40,6 +40,8 @@ static const char *message_type_name(uint16_t id) {
return "OTA_STATUS"; return "OTA_STATUS";
case alox_MessageType_OTA_START_ESPNOW: case alox_MessageType_OTA_START_ESPNOW:
return "OTA_START_ESPNOW"; return "OTA_START_ESPNOW";
case alox_MessageType_OTA_SLAVE_PROGRESS:
return "OTA_SLAVE_PROGRESS";
default: default:
return "UNKNOWN"; return "UNKNOWN";
} }

View File

@ -5,12 +5,20 @@
#include "esp_log.h" #include "esp_log.h"
#include "freertos/FreeRTOS.h" #include "freertos/FreeRTOS.h"
#include "freertos/idf_additions.h" #include "freertos/idf_additions.h"
#include <stdlib.h>
#include <string.h> #include <string.h>
static const char *TAG = "[OTA_CMD]"; 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
#define OTA_DIST_STACK 8192
#define OTA_DIST_PRIO 5
typedef struct {
uint32_t written;
int slot;
} ota_dist_job_t;
static void send_ota_status(ota_uart_status_t status, uint32_t err_code) { static void send_ota_status(ota_uart_status_t status, uint32_t err_code) {
alox_UartMessage response; alox_UartMessage response;
uart_cmd_init_response(&response, alox_MessageType_OTA_STATUS, uart_cmd_init_response(&response, alox_MessageType_OTA_STATUS,
@ -24,6 +32,35 @@ static void send_ota_status(ota_uart_status_t status, uint32_t err_code) {
uart_cmd_send(&response, TAG); uart_cmd_send(&response, TAG);
} }
static void send_ota_distributing(uint32_t kind, uint32_t bytes_done,
uint32_t target_slot) {
alox_UartMessage response;
uart_cmd_init_response(&response, alox_MessageType_OTA_STATUS,
alox_UartMessage_ota_status_tag);
response.payload.ota_status.status = (uint32_t)OTA_UART_ST_DISTRIBUTING;
response.payload.ota_status.bytes_written = bytes_done;
response.payload.ota_status.target_slot = target_slot;
response.payload.ota_status.error = kind;
uart_cmd_send(&response, TAG);
}
static void ota_dist_aggregate(uint32_t bytes_done, uint32_t total_bytes,
uint8_t slave_count) {
(void)total_bytes;
send_ota_distributing(OTA_DIST_AGGREGATE, bytes_done, (uint32_t)slave_count);
}
static void ota_dist_per_slave(uint32_t slave_id, uint32_t bytes_done,
uint32_t total_bytes) {
(void)total_bytes;
send_ota_distributing(OTA_DIST_PER_SLAVE, bytes_done, slave_id);
}
static const ota_espnow_progress_cbs_t s_dist_progress = {
.aggregate = ota_dist_aggregate,
.per_slave = ota_dist_per_slave,
};
static void ota_prepare_task(void *param) { static void ota_prepare_task(void *param) {
uint32_t total_size = (uint32_t)(uintptr_t)param; uint32_t total_size = (uint32_t)(uintptr_t)param;
@ -128,46 +165,54 @@ static void handle_ota_payload(const uint8_t *data, size_t len) {
} }
} }
static esp_err_t finish_master_ota_and_distribute(void) { static void ota_distribute_task(void *param) {
uint32_t written = ota_uart_bytes_written(); ota_dist_job_t *job = (ota_dist_job_t *)param;
int slot = ota_uart_target_slot(); if (job == NULL) {
bool success = false; vTaskDelete(NULL);
esp_err_t err = ota_uart_finish(false, &success); return;
if (err != ESP_OK || !success) {
send_ota_status(OTA_UART_ST_FAILED, (uint32_t)err);
return err;
} }
const esp_partition_t *part = NULL; const esp_partition_t *part = NULL;
uint32_t image_size = 0; uint32_t image_size = 0;
if (!ota_uart_get_staged_image(&part, &image_size)) { if (!ota_uart_get_staged_image(&part, &image_size)) {
send_ota_status(OTA_UART_ST_FAILED, 30); send_ota_status(OTA_UART_ST_FAILED, 30);
return ESP_ERR_INVALID_STATE; free(job);
vTaskDelete(NULL);
return;
} }
err = ota_espnow_distribute(part, image_size); send_ota_distributing(OTA_DIST_AGGREGATE, 0, 0);
esp_err_t err = ota_espnow_distribute(part, image_size, &s_dist_progress);
if (err != ESP_OK) { if (err != ESP_OK) {
ESP_LOGE(TAG, "slave OTA distribution failed: %s", esp_err_to_name(err)); ESP_LOGE(TAG, "slave OTA distribution failed: %s", esp_err_to_name(err));
ota_uart_clear_staged(); ota_uart_clear_staged();
send_ota_status(OTA_UART_ST_FAILED, 31); send_ota_status(OTA_UART_ST_FAILED, 31);
return err; free(job);
vTaskDelete(NULL);
return;
} }
err = ota_uart_apply_boot(); err = ota_uart_apply_boot();
if (err != ESP_OK) { if (err != ESP_OK) {
send_ota_status(OTA_UART_ST_FAILED, (uint32_t)err); send_ota_status(OTA_UART_ST_FAILED, (uint32_t)err);
return err; free(job);
vTaskDelete(NULL);
return;
} }
alox_UartMessage response; alox_UartMessage response;
uart_cmd_init_response(&response, alox_MessageType_OTA_STATUS, uart_cmd_init_response(&response, alox_MessageType_OTA_STATUS,
alox_UartMessage_ota_status_tag); alox_UartMessage_ota_status_tag);
response.payload.ota_status.status = (uint32_t)OTA_UART_ST_SUCCESS; response.payload.ota_status.status = (uint32_t)OTA_UART_ST_SUCCESS;
response.payload.ota_status.bytes_written = written; response.payload.ota_status.bytes_written = job->written;
response.payload.ota_status.target_slot = slot >= 0 ? (uint32_t)slot : 0; response.payload.ota_status.target_slot =
job->slot >= 0 ? (uint32_t)job->slot : 0;
response.payload.ota_status.error = 0; response.payload.ota_status.error = 0;
uart_cmd_send(&response, TAG); uart_cmd_send(&response, TAG);
return ESP_OK;
free(job);
vTaskDelete(NULL);
} }
static void handle_ota_end(const uint8_t *data, size_t len) { static void handle_ota_end(const uint8_t *data, size_t len) {
@ -179,7 +224,28 @@ static void handle_ota_end(const uint8_t *data, size_t len) {
return; return;
} }
(void)finish_master_ota_and_distribute(); ota_dist_job_t *job = calloc(1, sizeof(*job));
if (job == NULL) {
send_ota_status(OTA_UART_ST_FAILED, 21);
return;
}
job->written = ota_uart_bytes_written();
job->slot = ota_uart_target_slot();
bool success = false;
esp_err_t err = ota_uart_finish(false, &success);
if (err != ESP_OK || !success) {
send_ota_status(OTA_UART_ST_FAILED, (uint32_t)err);
free(job);
return;
}
if (xTaskCreate(ota_distribute_task, "ota_dist", OTA_DIST_STACK, job,
OTA_DIST_PRIO, NULL) != pdPASS) {
ESP_LOGE(TAG, "failed to create ota_dist task");
send_ota_status(OTA_UART_ST_FAILED, 22);
free(job);
}
} }
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) {
@ -198,7 +264,7 @@ static void handle_ota_start_espnow(const uint8_t *data, size_t len) {
return; return;
} }
esp_err_t err = ota_espnow_distribute(part, image_size); esp_err_t err = ota_espnow_distribute(part, image_size, &s_dist_progress);
if (err != ESP_OK) { if (err != ESP_OK) {
send_ota_status(OTA_UART_ST_FAILED, 42); send_ota_status(OTA_UART_ST_FAILED, 42);
return; return;

View File

@ -0,0 +1,37 @@
#include "cmd_ota_slave_progress.h"
#include "ota_espnow.h"
#include "uart_cmd.h"
#include "esp_log.h"
static const char *TAG = "[OTA_PROG]";
static void handle_ota_slave_progress(const uint8_t *data, size_t len) {
alox_UartMessage uart_msg;
uint32_t filter = 0;
if (uart_cmd_decode(data, len, &uart_msg) == ESP_OK) {
const alox_OtaSlaveProgressRequest *req =
UART_CMD_REQ(&uart_msg, alox_UartMessage_ota_slave_progress_request_tag,
ota_slave_progress_request);
if (req != NULL) {
filter = req->client_id;
}
}
alox_UartMessage response;
uart_cmd_init_response(
&response, alox_MessageType_OTA_SLAVE_PROGRESS,
alox_UartMessage_ota_slave_progress_response_tag);
ota_espnow_progress_query(filter, &response.payload.ota_slave_progress_response);
ESP_LOGI(TAG, "query client_id=%lu -> %u slave(s) active=%d",
(unsigned long)filter,
(unsigned)response.payload.ota_slave_progress_response.slaves_count,
(int)response.payload.ota_slave_progress_response.active);
uart_cmd_send(&response, TAG);
}
void cmd_ota_slave_progress_register(void) {
uart_cmd_register(alox_MessageType_OTA_SLAVE_PROGRESS,
handle_ota_slave_progress);
}

View File

@ -0,0 +1,6 @@
#ifndef CMD_OTA_SLAVE_PROGRESS_H
#define CMD_OTA_SLAVE_PROGRESS_H
void cmd_ota_slave_progress_register(void);
#endif

View File

@ -36,10 +36,90 @@ typedef struct {
uint8_t mac[OTA_MAX_TARGETS][6]; uint8_t mac[OTA_MAX_TARGETS][6];
uint32_t id[OTA_MAX_TARGETS]; uint32_t id[OTA_MAX_TARGETS];
uint32_t expected_bytes; uint32_t expected_bytes;
uint32_t total_bytes;
ota_espnow_progress_cbs_t progress;
} ota_dist_t; } ota_dist_t;
static ota_dist_t s_dist; static ota_dist_t s_dist;
typedef struct {
uint32_t client_id;
uint32_t bytes_written;
uint32_t status;
uint32_t error;
} ota_prog_entry_t;
static struct {
bool active;
uint32_t total_bytes;
uint32_t aggregate_bytes;
uint8_t count;
ota_prog_entry_t entries[OTA_MAX_TARGETS];
} s_prog;
static void prog_begin(uint32_t total_bytes) {
s_prog.active = true;
s_prog.total_bytes = total_bytes;
s_prog.aggregate_bytes = 0;
s_prog.count = s_dist.count;
for (uint8_t i = 0; i < s_dist.count; i++) {
s_prog.entries[i].client_id = s_dist.id[i];
s_prog.entries[i].bytes_written = 0;
s_prog.entries[i].status = OTA_ST_PREPARING;
s_prog.entries[i].error = 0;
}
}
static void prog_end(void) { s_prog.active = false; }
static void prog_set_aggregate(uint32_t bytes_done) {
s_prog.aggregate_bytes = bytes_done;
}
static void prog_update_idx(int idx, uint32_t status, uint32_t bytes,
uint32_t error) {
if (idx < 0 || idx >= (int)s_prog.count) {
return;
}
ota_prog_entry_t *e = &s_prog.entries[idx];
e->status = status;
if (bytes > e->bytes_written) {
e->bytes_written = bytes;
}
if (error != 0) {
e->error = error;
}
}
void ota_espnow_progress_query(uint32_t filter_client_id,
alox_OtaSlaveProgressResponse *out) {
if (out == NULL) {
return;
}
*out = (alox_OtaSlaveProgressResponse)alox_OtaSlaveProgressResponse_init_zero;
out->active = s_prog.active;
out->total_bytes = s_prog.total_bytes;
out->aggregate_bytes = s_prog.aggregate_bytes;
out->slave_count = s_prog.count;
for (uint8_t i = 0; i < s_prog.count; i++) {
const ota_prog_entry_t *e = &s_prog.entries[i];
if (filter_client_id != 0 && e->client_id != filter_client_id) {
continue;
}
if (out->slaves_count >=
sizeof(out->slaves) / sizeof(out->slaves[0])) {
break;
}
alox_OtaSlaveProgressEntry *dst = &out->slaves[out->slaves_count++];
dst->client_id = e->client_id;
dst->bytes_written = e->bytes_written;
dst->total_bytes = s_prog.total_bytes;
dst->status = e->status;
dst->error = e->error;
}
}
static int find_target_index(const uint8_t mac[6]) { static int find_target_index(const uint8_t mac[6]) {
for (uint8_t i = 0; i < s_dist.count; i++) { for (uint8_t i = 0; i < s_dist.count; i++) {
if (memcmp(s_dist.mac[i], mac, 6) == 0) { if (memcmp(s_dist.mac[i], mac, 6) == 0) {
@ -179,9 +259,15 @@ void ota_espnow_master_on_status(const uint8_t slave_mac[6],
switch (status->status) { switch (status->status) {
case OTA_ST_READY: case OTA_ST_READY:
prog_update_idx(idx, OTA_ST_READY, 0, 0);
xEventGroupSetBits(s_eg, bit); xEventGroupSetBits(s_eg, bit);
break; break;
case OTA_ST_BLOCK_ACK: case OTA_ST_BLOCK_ACK:
prog_update_idx(idx, OTA_ST_BLOCK_ACK, status->bytes_written, 0);
if (s_dist.progress.per_slave != NULL) {
s_dist.progress.per_slave(s_dist.id[idx], status->bytes_written,
s_dist.total_bytes);
}
if (status->bytes_written >= s_dist.expected_bytes) { if (status->bytes_written >= s_dist.expected_bytes) {
xEventGroupSetBits(s_eg, bit); xEventGroupSetBits(s_eg, bit);
} else { } else {
@ -191,9 +277,12 @@ void ota_espnow_master_on_status(const uint8_t slave_mac[6],
} }
break; break;
case OTA_ST_SUCCESS: case OTA_ST_SUCCESS:
prog_update_idx(idx, OTA_ST_SUCCESS, status->bytes_written, 0);
xEventGroupSetBits(s_eg, bit); xEventGroupSetBits(s_eg, bit);
break; break;
case OTA_ST_FAILED: case OTA_ST_FAILED:
prog_update_idx(idx, OTA_ST_FAILED, status->bytes_written,
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);
break; break;
@ -220,7 +309,8 @@ static size_t collect_targets(void) {
} }
static esp_err_t distribute_image(const esp_partition_t *partition, static esp_err_t distribute_image(const esp_partition_t *partition,
uint32_t size) { uint32_t size,
const ota_espnow_progress_cbs_t *progress) {
if (s_eg == NULL) { if (s_eg == NULL) {
s_eg = xEventGroupCreate(); s_eg = xEventGroupCreate();
if (s_eg == NULL) { if (s_eg == NULL) {
@ -228,6 +318,13 @@ static esp_err_t distribute_image(const esp_partition_t *partition,
} }
} }
memset(&s_dist.progress, 0, sizeof(s_dist.progress));
if (progress != NULL) {
s_dist.progress = *progress;
}
s_dist.total_bytes = size;
prog_begin(size);
ESP_LOGI(TAG, "distributing %lu bytes from %s to %u slave(s)", ESP_LOGI(TAG, "distributing %lu bytes from %s to %u slave(s)",
(unsigned long)size, partition->label, (unsigned)s_dist.count); (unsigned long)size, partition->label, (unsigned)s_dist.count);
@ -240,15 +337,22 @@ static esp_err_t distribute_image(const esp_partition_t *partition,
if (err != ESP_OK) { if (err != ESP_OK) {
ESP_LOGW(TAG, "OTA_START to slave %lu failed", ESP_LOGW(TAG, "OTA_START to slave %lu failed",
(unsigned long)s_dist.id[i]); (unsigned long)s_dist.id[i]);
prog_end();
return err; return err;
} }
} }
if (!wait_target_bits(target_mask, OTA_PREPARE_TIMEOUT_MS)) { if (!wait_target_bits(target_mask, OTA_PREPARE_TIMEOUT_MS)) {
ESP_LOGE(TAG, "timeout waiting for slave OTA ready"); ESP_LOGE(TAG, "timeout waiting for slave OTA ready");
prog_end();
return ESP_ERR_TIMEOUT; return ESP_ERR_TIMEOUT;
} }
prog_set_aggregate(0);
if (s_dist.progress.aggregate != NULL) {
s_dist.progress.aggregate(0, size, s_dist.count);
}
uint8_t block_buf[OTA_UART_FLASH_BLOCK_SIZE]; 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;
@ -263,6 +367,7 @@ static esp_err_t distribute_image(const esp_partition_t *partition,
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));
prog_end();
return err; return err;
} }
@ -277,6 +382,7 @@ static esp_err_t distribute_image(const esp_partition_t *partition,
err = esp_now_comm_send_ota_payload(s_dist.mac[i], seq, err = esp_now_comm_send_ota_payload(s_dist.mac[i], seq,
block_buf + sent, chunk); block_buf + sent, chunk);
if (err != ESP_OK) { if (err != ESP_OK) {
prog_end();
return err; return err;
} }
} }
@ -293,6 +399,7 @@ static esp_err_t distribute_image(const esp_partition_t *partition,
if (!wait_target_bits(target_mask, OTA_BLOCK_TIMEOUT_MS)) { if (!wait_target_bits(target_mask, OTA_BLOCK_TIMEOUT_MS)) {
ESP_LOGE(TAG, "timeout block ack @%lu bytes", ESP_LOGE(TAG, "timeout block ack @%lu bytes",
(unsigned long)s_dist.expected_bytes); (unsigned long)s_dist.expected_bytes);
prog_end();
return ESP_ERR_TIMEOUT; return ESP_ERR_TIMEOUT;
} }
ESP_LOGI(TAG, "block ack @%lu/%lu (%lu%%)", ESP_LOGI(TAG, "block ack @%lu/%lu (%lu%%)",
@ -303,34 +410,48 @@ static esp_err_t distribute_image(const esp_partition_t *partition,
(unsigned long)block_len); (unsigned long)block_len);
} }
offset += block_len; offset += block_len;
prog_set_aggregate(offset);
if (s_dist.progress.aggregate != NULL) {
s_dist.progress.aggregate(offset, size, s_dist.count);
}
} }
xEventGroupClearBits(s_eg, target_mask); xEventGroupClearBits(s_eg, target_mask);
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) {
prog_end();
return err; return err;
} }
} }
if (!wait_target_bits(target_mask, OTA_END_TIMEOUT_MS)) { if (!wait_target_bits(target_mask, OTA_END_TIMEOUT_MS)) {
ESP_LOGE(TAG, "timeout waiting for slave OTA success"); ESP_LOGE(TAG, "timeout waiting for slave OTA success");
prog_end();
return ESP_ERR_TIMEOUT; return ESP_ERR_TIMEOUT;
} }
prog_set_aggregate(size);
prog_end();
ESP_LOGI(TAG, "ESP-NOW OTA complete for %u slave(s)", (unsigned)s_dist.count); ESP_LOGI(TAG, "ESP-NOW OTA complete for %u slave(s)", (unsigned)s_dist.count);
return ESP_OK; return ESP_OK;
} }
esp_err_t ota_espnow_distribute(const esp_partition_t *partition, uint32_t size) { esp_err_t ota_espnow_distribute(const esp_partition_t *partition, uint32_t size,
const ota_espnow_progress_cbs_t *progress) {
if (partition == NULL || size == 0) { if (partition == NULL || size == 0) {
return ESP_ERR_INVALID_ARG; return ESP_ERR_INVALID_ARG;
} }
if (collect_targets() == 0) { if (collect_targets() == 0) {
ESP_LOGI(TAG, "no available slaves — skip ESP-NOW OTA"); ESP_LOGI(TAG, "no available slaves — skip ESP-NOW OTA");
memset(&s_prog, 0, sizeof(s_prog));
s_prog.total_bytes = size;
if (progress != NULL && progress->aggregate != NULL) {
progress->aggregate(size, size, 0);
}
return ESP_OK; return ESP_OK;
} }
return distribute_image(partition, size); return distribute_image(partition, size, progress);
} }

View File

@ -4,10 +4,20 @@
#include "esp_err.h" #include "esp_err.h"
#include "esp_now_messages.pb.h" #include "esp_now_messages.pb.h"
#include "esp_partition.h" #include "esp_partition.h"
#include "uart_messages.pb.h"
#include <stdint.h> #include <stdint.h>
typedef struct {
/** bytes_done, total_bytes, number of slaves targeted. */
void (*aggregate)(uint32_t bytes_done, uint32_t total_bytes,
uint8_t slave_count);
/** Per-slave block ACK (slave_id, bytes_written on that slave, total_bytes). */
void (*per_slave)(uint32_t slave_id, uint32_t bytes_done, uint32_t total_bytes);
} ota_espnow_progress_cbs_t;
/** Master: read staged image from partition and push to all available slaves. */ /** Master: read staged image from partition and push to all available slaves. */
esp_err_t ota_espnow_distribute(const esp_partition_t *partition, uint32_t size); esp_err_t ota_espnow_distribute(const esp_partition_t *partition, uint32_t size,
const ota_espnow_progress_cbs_t *progress);
/** Master: handle slave → master OTA status / block ACK. */ /** Master: handle slave → master OTA status / block ACK. */
void ota_espnow_master_on_status(const uint8_t slave_mac[6], void ota_espnow_master_on_status(const uint8_t slave_mac[6],
@ -20,4 +30,11 @@ void ota_espnow_slave_on_payload(const uint8_t master_mac[6],
const alox_EspNowOtaPayload *payload); const alox_EspNowOtaPayload *payload);
void ota_espnow_slave_on_end(const uint8_t master_mac[6]); void ota_espnow_slave_on_end(const uint8_t master_mac[6]);
/**
* Fill UART OtaSlaveProgressResponse from tracked per-slave state.
* filter_client_id 0 = all slaves in the last/current distribution session.
*/
void ota_espnow_progress_query(uint32_t filter_client_id,
alox_OtaSlaveProgressResponse *out);
#endif #endif

View File

@ -17,8 +17,14 @@ typedef enum {
OTA_UART_ST_BLOCK_ACK = 3, OTA_UART_ST_BLOCK_ACK = 3,
OTA_UART_ST_SUCCESS = 4, OTA_UART_ST_SUCCESS = 4,
OTA_UART_ST_FAILED = 5, OTA_UART_ST_FAILED = 5,
/** ESP-NOW slave distribution in progress (see OTA_DIST_* in cmd_ota.c). */
OTA_UART_ST_DISTRIBUTING = 6,
} ota_uart_status_t; } ota_uart_status_t;
/** OtaStatusPayload.error when status == OTA_UART_ST_DISTRIBUTING. */
#define OTA_DIST_AGGREGATE 0u
#define OTA_DIST_PER_SLAVE 1u
typedef enum { typedef enum {
OTA_FEED_OK = 0, OTA_FEED_OK = 0,
OTA_FEED_BLOCK_WRITTEN, OTA_FEED_BLOCK_WRITTEN,

View File

@ -5,6 +5,7 @@
#include "cmd_client_info.h" #include "cmd_client_info.h"
#include "cmd_version.h" #include "cmd_version.h"
#include "cmd_ota.h" #include "cmd_ota.h"
#include "cmd_ota_slave_progress.h"
#include "esp_now_comm.h" #include "esp_now_comm.h"
#include "powerpod.h" #include "powerpod.h"
#include "driver/gpio.h" #include "driver/gpio.h"
@ -168,6 +169,7 @@ void app_main(void) {
cmd_accel_deadzone_register(); cmd_accel_deadzone_register();
cmd_espnow_unicast_test_register(); cmd_espnow_unicast_test_register();
cmd_ota_register(); cmd_ota_register();
cmd_ota_slave_progress_register();
} }
uint8_t current_digit = 10; uint8_t current_digit = 10;

View File

@ -6,7 +6,7 @@
#error Regenerate this file with the current version of nanopb generator. #error Regenerate this file with the current version of nanopb generator.
#endif #endif
PB_BIND(alox_UartMessage, alox_UartMessage, AUTO) PB_BIND(alox_UartMessage, alox_UartMessage, 2)
PB_BIND(alox_Ack, alox_Ack, AUTO) PB_BIND(alox_Ack, alox_Ack, AUTO)
@ -54,6 +54,15 @@ PB_BIND(alox_OtaEndPayload, alox_OtaEndPayload, AUTO)
PB_BIND(alox_OtaStatusPayload, alox_OtaStatusPayload, AUTO) PB_BIND(alox_OtaStatusPayload, alox_OtaStatusPayload, AUTO)
PB_BIND(alox_OtaSlaveProgressRequest, alox_OtaSlaveProgressRequest, AUTO)
PB_BIND(alox_OtaSlaveProgressEntry, alox_OtaSlaveProgressEntry, AUTO)
PB_BIND(alox_OtaSlaveProgressResponse, alox_OtaSlaveProgressResponse, 2)

View File

@ -1,8 +1,8 @@
/* Automatically generated nanopb header */ /* Automatically generated nanopb header */
/* Generated by nanopb-1.0.0-dev */ /* Generated by nanopb-1.0.0-dev */
#ifndef PB_ALOX_MAIN_PROTO_UART_MESSAGES_PB_H_INCLUDED #ifndef PB_ALOX_UART_MESSAGES_PB_H_INCLUDED
#define PB_ALOX_MAIN_PROTO_UART_MESSAGES_PB_H_INCLUDED #define PB_ALOX_UART_MESSAGES_PB_H_INCLUDED
#include <pb.h> #include <pb.h>
#if PB_PROTO_HEADER_VERSION != 40 #if PB_PROTO_HEADER_VERSION != 40
@ -23,7 +23,8 @@ typedef enum _alox_MessageType {
alox_MessageType_OTA_PAYLOAD = 17, alox_MessageType_OTA_PAYLOAD = 17,
alox_MessageType_OTA_END = 18, alox_MessageType_OTA_END = 18,
alox_MessageType_OTA_STATUS = 19, alox_MessageType_OTA_STATUS = 19,
alox_MessageType_OTA_START_ESPNOW = 20 alox_MessageType_OTA_START_ESPNOW = 20,
alox_MessageType_OTA_SLAVE_PROGRESS = 21
} alox_MessageType; } alox_MessageType;
/* Struct definitions */ /* Struct definitions */
@ -99,8 +100,8 @@ typedef struct _alox_OtaStartPayload {
uint32_t total_size; uint32_t total_size;
} alox_OtaStartPayload; } alox_OtaStartPayload;
/* Host → device: firmware chunk (up to 200 bytes); device buffers 4 KiB before flash write. */
typedef PB_BYTES_ARRAY_T(200) alox_OtaPayload_data_t; typedef PB_BYTES_ARRAY_T(200) alox_OtaPayload_data_t;
/* Host → device: firmware chunk (up to 200 bytes); device buffers 4 KiB before flash write. */
typedef struct _alox_OtaPayload { typedef struct _alox_OtaPayload {
uint32_t seq; uint32_t seq;
alox_OtaPayload_data_t data; alox_OtaPayload_data_t data;
@ -112,7 +113,7 @@ typedef struct _alox_OtaEndPayload {
} alox_OtaEndPayload; } alox_OtaEndPayload;
/* Device → host status (also used as ACK after each 4 KiB written). /* Device → host status (also used as ACK after each 4 KiB written).
status: 1=preparing, 2=ready, 3=block_ack, 4=success, 5=failed */ status: 1=preparing, 2=ready, 3=block_ack, 4=success, 5=failed, 6=distributing */
typedef struct _alox_OtaStatusPayload { typedef struct _alox_OtaStatusPayload {
uint32_t status; uint32_t status;
uint32_t bytes_written; uint32_t bytes_written;
@ -120,6 +121,29 @@ typedef struct _alox_OtaStatusPayload {
uint32_t error; uint32_t error;
} alox_OtaStatusPayload; } alox_OtaStatusPayload;
/* Host → master: query ESP-NOW slave OTA progress (client_id 0 = all slaves in session). */
typedef struct _alox_OtaSlaveProgressRequest {
uint32_t client_id;
} alox_OtaSlaveProgressRequest;
typedef struct _alox_OtaSlaveProgressEntry {
uint32_t client_id;
uint32_t bytes_written;
uint32_t total_bytes;
/* * 0=idle, 1=preparing, 2=ready, 3=distributing, 4=success, 5=failed */
uint32_t status;
uint32_t error;
} alox_OtaSlaveProgressEntry;
typedef struct _alox_OtaSlaveProgressResponse {
bool active;
uint32_t total_bytes;
uint32_t aggregate_bytes;
uint32_t slave_count;
pb_size_t slaves_count;
alox_OtaSlaveProgressEntry slaves[16];
} alox_OtaSlaveProgressResponse;
typedef struct _alox_UartMessage { typedef struct _alox_UartMessage {
alox_MessageType type; alox_MessageType type;
pb_size_t which_payload; pb_size_t which_payload;
@ -137,6 +161,8 @@ typedef struct _alox_UartMessage {
alox_AccelDeadzoneResponse accel_deadzone_response; alox_AccelDeadzoneResponse accel_deadzone_response;
alox_EspNowUnicastTestRequest espnow_unicast_test_request; alox_EspNowUnicastTestRequest espnow_unicast_test_request;
alox_EspNowUnicastTestResponse espnow_unicast_test_response; alox_EspNowUnicastTestResponse espnow_unicast_test_response;
alox_OtaSlaveProgressRequest ota_slave_progress_request;
alox_OtaSlaveProgressResponse ota_slave_progress_response;
} payload; } payload;
} alox_UartMessage; } alox_UartMessage;
@ -147,8 +173,8 @@ extern "C" {
/* Helper constants for enums */ /* Helper constants for enums */
#define _alox_MessageType_MIN alox_MessageType_UNKNOWN #define _alox_MessageType_MIN alox_MessageType_UNKNOWN
#define _alox_MessageType_MAX alox_MessageType_OTA_START_ESPNOW #define _alox_MessageType_MAX alox_MessageType_OTA_SLAVE_PROGRESS
#define _alox_MessageType_ARRAYSIZE ((alox_MessageType)(alox_MessageType_OTA_START_ESPNOW+1)) #define _alox_MessageType_ARRAYSIZE ((alox_MessageType)(alox_MessageType_OTA_SLAVE_PROGRESS+1))
#define alox_UartMessage_type_ENUMTYPE alox_MessageType #define alox_UartMessage_type_ENUMTYPE alox_MessageType
@ -168,6 +194,9 @@ extern "C" {
/* Initializer values for message structs */ /* Initializer values for message structs */
#define alox_UartMessage_init_default {_alox_MessageType_MIN, 0, {alox_Ack_init_default}} #define alox_UartMessage_init_default {_alox_MessageType_MIN, 0, {alox_Ack_init_default}}
#define alox_Ack_init_default {0} #define alox_Ack_init_default {0}
@ -185,6 +214,9 @@ extern "C" {
#define alox_OtaPayload_init_default {0, {0, {0}}} #define alox_OtaPayload_init_default {0, {0, {0}}}
#define alox_OtaEndPayload_init_default {0} #define alox_OtaEndPayload_init_default {0}
#define alox_OtaStatusPayload_init_default {0, 0, 0, 0} #define alox_OtaStatusPayload_init_default {0, 0, 0, 0}
#define alox_OtaSlaveProgressRequest_init_default {0}
#define alox_OtaSlaveProgressEntry_init_default {0, 0, 0, 0, 0}
#define alox_OtaSlaveProgressResponse_init_default {0, 0, 0, 0, 0, {alox_OtaSlaveProgressEntry_init_default, alox_OtaSlaveProgressEntry_init_default, alox_OtaSlaveProgressEntry_init_default, alox_OtaSlaveProgressEntry_init_default, alox_OtaSlaveProgressEntry_init_default, alox_OtaSlaveProgressEntry_init_default, alox_OtaSlaveProgressEntry_init_default, alox_OtaSlaveProgressEntry_init_default, alox_OtaSlaveProgressEntry_init_default, alox_OtaSlaveProgressEntry_init_default, alox_OtaSlaveProgressEntry_init_default, alox_OtaSlaveProgressEntry_init_default, alox_OtaSlaveProgressEntry_init_default, alox_OtaSlaveProgressEntry_init_default, alox_OtaSlaveProgressEntry_init_default, alox_OtaSlaveProgressEntry_init_default}}
#define alox_UartMessage_init_zero {_alox_MessageType_MIN, 0, {alox_Ack_init_zero}} #define alox_UartMessage_init_zero {_alox_MessageType_MIN, 0, {alox_Ack_init_zero}}
#define alox_Ack_init_zero {0} #define alox_Ack_init_zero {0}
#define alox_EchoPayload_init_zero {{{NULL}, NULL}} #define alox_EchoPayload_init_zero {{{NULL}, NULL}}
@ -201,6 +233,9 @@ extern "C" {
#define alox_OtaPayload_init_zero {0, {0, {0}}} #define alox_OtaPayload_init_zero {0, {0, {0}}}
#define alox_OtaEndPayload_init_zero {0} #define alox_OtaEndPayload_init_zero {0}
#define alox_OtaStatusPayload_init_zero {0, 0, 0, 0} #define alox_OtaStatusPayload_init_zero {0, 0, 0, 0}
#define alox_OtaSlaveProgressRequest_init_zero {0}
#define alox_OtaSlaveProgressEntry_init_zero {0, 0, 0, 0, 0}
#define alox_OtaSlaveProgressResponse_init_zero {0, 0, 0, 0, 0, {alox_OtaSlaveProgressEntry_init_zero, alox_OtaSlaveProgressEntry_init_zero, alox_OtaSlaveProgressEntry_init_zero, alox_OtaSlaveProgressEntry_init_zero, alox_OtaSlaveProgressEntry_init_zero, alox_OtaSlaveProgressEntry_init_zero, alox_OtaSlaveProgressEntry_init_zero, alox_OtaSlaveProgressEntry_init_zero, alox_OtaSlaveProgressEntry_init_zero, alox_OtaSlaveProgressEntry_init_zero, alox_OtaSlaveProgressEntry_init_zero, alox_OtaSlaveProgressEntry_init_zero, alox_OtaSlaveProgressEntry_init_zero, alox_OtaSlaveProgressEntry_init_zero, alox_OtaSlaveProgressEntry_init_zero, alox_OtaSlaveProgressEntry_init_zero}}
/* Field tags (for use in manual encoding/decoding) */ /* Field tags (for use in manual encoding/decoding) */
#define alox_EchoPayload_data_tag 1 #define alox_EchoPayload_data_tag 1
@ -239,6 +274,17 @@ extern "C" {
#define alox_OtaStatusPayload_bytes_written_tag 2 #define alox_OtaStatusPayload_bytes_written_tag 2
#define alox_OtaStatusPayload_target_slot_tag 3 #define alox_OtaStatusPayload_target_slot_tag 3
#define alox_OtaStatusPayload_error_tag 4 #define alox_OtaStatusPayload_error_tag 4
#define alox_OtaSlaveProgressRequest_client_id_tag 1
#define alox_OtaSlaveProgressEntry_client_id_tag 1
#define alox_OtaSlaveProgressEntry_bytes_written_tag 2
#define alox_OtaSlaveProgressEntry_total_bytes_tag 3
#define alox_OtaSlaveProgressEntry_status_tag 4
#define alox_OtaSlaveProgressEntry_error_tag 5
#define alox_OtaSlaveProgressResponse_active_tag 1
#define alox_OtaSlaveProgressResponse_total_bytes_tag 2
#define alox_OtaSlaveProgressResponse_aggregate_bytes_tag 3
#define alox_OtaSlaveProgressResponse_slave_count_tag 4
#define alox_OtaSlaveProgressResponse_slaves_tag 5
#define alox_UartMessage_type_tag 1 #define alox_UartMessage_type_tag 1
#define alox_UartMessage_ack_payload_tag 2 #define alox_UartMessage_ack_payload_tag 2
#define alox_UartMessage_echo_payload_tag 3 #define alox_UartMessage_echo_payload_tag 3
@ -253,6 +299,8 @@ extern "C" {
#define alox_UartMessage_accel_deadzone_response_tag 12 #define alox_UartMessage_accel_deadzone_response_tag 12
#define alox_UartMessage_espnow_unicast_test_request_tag 13 #define alox_UartMessage_espnow_unicast_test_request_tag 13
#define alox_UartMessage_espnow_unicast_test_response_tag 14 #define alox_UartMessage_espnow_unicast_test_response_tag 14
#define alox_UartMessage_ota_slave_progress_request_tag 15
#define alox_UartMessage_ota_slave_progress_response_tag 16
/* Struct field encoding specification for nanopb */ /* Struct field encoding specification for nanopb */
#define alox_UartMessage_FIELDLIST(X, a) \ #define alox_UartMessage_FIELDLIST(X, a) \
@ -269,7 +317,9 @@ X(a, STATIC, ONEOF, MESSAGE, (payload,ota_status,payload.ota_status), 10)
X(a, STATIC, ONEOF, MESSAGE, (payload,accel_deadzone_request,payload.accel_deadzone_request), 11) \ X(a, STATIC, ONEOF, MESSAGE, (payload,accel_deadzone_request,payload.accel_deadzone_request), 11) \
X(a, STATIC, ONEOF, MESSAGE, (payload,accel_deadzone_response,payload.accel_deadzone_response), 12) \ X(a, STATIC, ONEOF, MESSAGE, (payload,accel_deadzone_response,payload.accel_deadzone_response), 12) \
X(a, STATIC, ONEOF, MESSAGE, (payload,espnow_unicast_test_request,payload.espnow_unicast_test_request), 13) \ X(a, STATIC, ONEOF, MESSAGE, (payload,espnow_unicast_test_request,payload.espnow_unicast_test_request), 13) \
X(a, STATIC, ONEOF, MESSAGE, (payload,espnow_unicast_test_response,payload.espnow_unicast_test_response), 14) X(a, STATIC, ONEOF, MESSAGE, (payload,espnow_unicast_test_response,payload.espnow_unicast_test_response), 14) \
X(a, STATIC, ONEOF, MESSAGE, (payload,ota_slave_progress_request,payload.ota_slave_progress_request), 15) \
X(a, STATIC, ONEOF, MESSAGE, (payload,ota_slave_progress_response,payload.ota_slave_progress_response), 16)
#define alox_UartMessage_CALLBACK NULL #define alox_UartMessage_CALLBACK NULL
#define alox_UartMessage_DEFAULT NULL #define alox_UartMessage_DEFAULT NULL
#define alox_UartMessage_payload_ack_payload_MSGTYPE alox_Ack #define alox_UartMessage_payload_ack_payload_MSGTYPE alox_Ack
@ -285,6 +335,8 @@ X(a, STATIC, ONEOF, MESSAGE, (payload,espnow_unicast_test_response,payload
#define alox_UartMessage_payload_accel_deadzone_response_MSGTYPE alox_AccelDeadzoneResponse #define alox_UartMessage_payload_accel_deadzone_response_MSGTYPE alox_AccelDeadzoneResponse
#define alox_UartMessage_payload_espnow_unicast_test_request_MSGTYPE alox_EspNowUnicastTestRequest #define alox_UartMessage_payload_espnow_unicast_test_request_MSGTYPE alox_EspNowUnicastTestRequest
#define alox_UartMessage_payload_espnow_unicast_test_response_MSGTYPE alox_EspNowUnicastTestResponse #define alox_UartMessage_payload_espnow_unicast_test_response_MSGTYPE alox_EspNowUnicastTestResponse
#define alox_UartMessage_payload_ota_slave_progress_request_MSGTYPE alox_OtaSlaveProgressRequest
#define alox_UartMessage_payload_ota_slave_progress_response_MSGTYPE alox_OtaSlaveProgressResponse
#define alox_Ack_FIELDLIST(X, a) \ #define alox_Ack_FIELDLIST(X, a) \
@ -386,6 +438,30 @@ X(a, STATIC, SINGULAR, UINT32, error, 4)
#define alox_OtaStatusPayload_CALLBACK NULL #define alox_OtaStatusPayload_CALLBACK NULL
#define alox_OtaStatusPayload_DEFAULT NULL #define alox_OtaStatusPayload_DEFAULT NULL
#define alox_OtaSlaveProgressRequest_FIELDLIST(X, a) \
X(a, STATIC, SINGULAR, UINT32, client_id, 1)
#define alox_OtaSlaveProgressRequest_CALLBACK NULL
#define alox_OtaSlaveProgressRequest_DEFAULT NULL
#define alox_OtaSlaveProgressEntry_FIELDLIST(X, a) \
X(a, STATIC, SINGULAR, UINT32, client_id, 1) \
X(a, STATIC, SINGULAR, UINT32, bytes_written, 2) \
X(a, STATIC, SINGULAR, UINT32, total_bytes, 3) \
X(a, STATIC, SINGULAR, UINT32, status, 4) \
X(a, STATIC, SINGULAR, UINT32, error, 5)
#define alox_OtaSlaveProgressEntry_CALLBACK NULL
#define alox_OtaSlaveProgressEntry_DEFAULT NULL
#define alox_OtaSlaveProgressResponse_FIELDLIST(X, a) \
X(a, STATIC, SINGULAR, BOOL, active, 1) \
X(a, STATIC, SINGULAR, UINT32, total_bytes, 2) \
X(a, STATIC, SINGULAR, UINT32, aggregate_bytes, 3) \
X(a, STATIC, SINGULAR, UINT32, slave_count, 4) \
X(a, STATIC, REPEATED, MESSAGE, slaves, 5)
#define alox_OtaSlaveProgressResponse_CALLBACK NULL
#define alox_OtaSlaveProgressResponse_DEFAULT NULL
#define alox_OtaSlaveProgressResponse_slaves_MSGTYPE alox_OtaSlaveProgressEntry
extern const pb_msgdesc_t alox_UartMessage_msg; extern const pb_msgdesc_t alox_UartMessage_msg;
extern const pb_msgdesc_t alox_Ack_msg; extern const pb_msgdesc_t alox_Ack_msg;
extern const pb_msgdesc_t alox_EchoPayload_msg; extern const pb_msgdesc_t alox_EchoPayload_msg;
@ -402,6 +478,9 @@ extern const pb_msgdesc_t alox_OtaStartPayload_msg;
extern const pb_msgdesc_t alox_OtaPayload_msg; extern const pb_msgdesc_t alox_OtaPayload_msg;
extern const pb_msgdesc_t alox_OtaEndPayload_msg; extern const pb_msgdesc_t alox_OtaEndPayload_msg;
extern const pb_msgdesc_t alox_OtaStatusPayload_msg; extern const pb_msgdesc_t alox_OtaStatusPayload_msg;
extern const pb_msgdesc_t alox_OtaSlaveProgressRequest_msg;
extern const pb_msgdesc_t alox_OtaSlaveProgressEntry_msg;
extern const pb_msgdesc_t alox_OtaSlaveProgressResponse_msg;
/* Defines for backwards compatibility with code written before nanopb-0.4.0 */ /* Defines for backwards compatibility with code written before nanopb-0.4.0 */
#define alox_UartMessage_fields &alox_UartMessage_msg #define alox_UartMessage_fields &alox_UartMessage_msg
@ -420,6 +499,9 @@ extern const pb_msgdesc_t alox_OtaStatusPayload_msg;
#define alox_OtaPayload_fields &alox_OtaPayload_msg #define alox_OtaPayload_fields &alox_OtaPayload_msg
#define alox_OtaEndPayload_fields &alox_OtaEndPayload_msg #define alox_OtaEndPayload_fields &alox_OtaEndPayload_msg
#define alox_OtaStatusPayload_fields &alox_OtaStatusPayload_msg #define alox_OtaStatusPayload_fields &alox_OtaStatusPayload_msg
#define alox_OtaSlaveProgressRequest_fields &alox_OtaSlaveProgressRequest_msg
#define alox_OtaSlaveProgressEntry_fields &alox_OtaSlaveProgressEntry_msg
#define alox_OtaSlaveProgressResponse_fields &alox_OtaSlaveProgressResponse_msg
/* Maximum encoded size of messages (where known) */ /* Maximum encoded size of messages (where known) */
/* alox_UartMessage_size depends on runtime parameters */ /* alox_UartMessage_size depends on runtime parameters */
@ -428,8 +510,7 @@ extern const pb_msgdesc_t alox_OtaStatusPayload_msg;
/* alox_ClientInfo_size depends on runtime parameters */ /* alox_ClientInfo_size depends on runtime parameters */
/* alox_ClientInfoResponse_size depends on runtime parameters */ /* alox_ClientInfoResponse_size depends on runtime parameters */
/* alox_ClientInputResponse_size depends on runtime parameters */ /* alox_ClientInputResponse_size depends on runtime parameters */
/* alox_OtaPayload_size depends on runtime parameters */ #define ALOX_UART_MESSAGES_PB_H_MAX_SIZE alox_OtaSlaveProgressResponse_size
#define ALOX_MAIN_PROTO_UART_MESSAGES_PB_H_MAX_SIZE alox_OtaStatusPayload_size
#define alox_AccelDeadzoneRequest_size 16 #define alox_AccelDeadzoneRequest_size 16
#define alox_AccelDeadzoneResponse_size 20 #define alox_AccelDeadzoneResponse_size 20
#define alox_Ack_size 0 #define alox_Ack_size 0
@ -437,6 +518,10 @@ extern const pb_msgdesc_t alox_OtaStatusPayload_msg;
#define alox_EspNowUnicastTestRequest_size 12 #define alox_EspNowUnicastTestRequest_size 12
#define alox_EspNowUnicastTestResponse_size 8 #define alox_EspNowUnicastTestResponse_size 8
#define alox_OtaEndPayload_size 0 #define alox_OtaEndPayload_size 0
#define alox_OtaPayload_size 209
#define alox_OtaSlaveProgressEntry_size 30
#define alox_OtaSlaveProgressRequest_size 6
#define alox_OtaSlaveProgressResponse_size 532
#define alox_OtaStartPayload_size 6 #define alox_OtaStartPayload_size 6
#define alox_OtaStatusPayload_size 24 #define alox_OtaStatusPayload_size 24

View File

@ -18,6 +18,7 @@ enum MessageType {
OTA_END = 18; OTA_END = 18;
OTA_STATUS = 19; OTA_STATUS = 19;
OTA_START_ESPNOW = 20; OTA_START_ESPNOW = 20;
OTA_SLAVE_PROGRESS = 21;
} }
message UartMessage { message UartMessage {
@ -36,6 +37,8 @@ message UartMessage {
AccelDeadzoneResponse accel_deadzone_response = 12; AccelDeadzoneResponse accel_deadzone_response = 12;
EspNowUnicastTestRequest espnow_unicast_test_request = 13; EspNowUnicastTestRequest espnow_unicast_test_request = 13;
EspNowUnicastTestResponse espnow_unicast_test_response = 14; EspNowUnicastTestResponse espnow_unicast_test_response = 14;
OtaSlaveProgressRequest ota_slave_progress_request = 15;
OtaSlaveProgressResponse ota_slave_progress_response = 16;
} }
} }
@ -119,10 +122,32 @@ message OtaPayload {
message OtaEndPayload {} message OtaEndPayload {}
// Device host status (also used as ACK after each 4 KiB written). // Device host status (also used as ACK after each 4 KiB written).
// status: 1=preparing, 2=ready, 3=block_ack, 4=success, 5=failed // status: 1=preparing, 2=ready, 3=block_ack, 4=success, 5=failed, 6=distributing
message OtaStatusPayload { message OtaStatusPayload {
uint32 status = 1; uint32 status = 1;
uint32 bytes_written = 2; uint32 bytes_written = 2;
uint32 target_slot = 3; uint32 target_slot = 3;
uint32 error = 4; uint32 error = 4;
} }
// Host master: query ESP-NOW slave OTA progress (client_id 0 = all slaves in session).
message OtaSlaveProgressRequest {
uint32 client_id = 1;
}
message OtaSlaveProgressEntry {
uint32 client_id = 1;
uint32 bytes_written = 2;
uint32 total_bytes = 3;
/** 0=idle, 1=preparing, 2=ready, 3=distributing, 4=success, 5=failed */
uint32 status = 4;
uint32 error = 5;
}
message OtaSlaveProgressResponse {
bool active = 1;
uint32 total_bytes = 2;
uint32 aggregate_bytes = 3;
uint32 slave_count = 4;
repeated OtaSlaveProgressEntry slaves = 5 [(nanopb).max_count = 16];
}