diff --git a/Makefile b/Makefile index 22668e1..b229cab 100644 --- a/Makefile +++ b/Makefile @@ -18,10 +18,6 @@ default: @echo "Targets: proto_generate gotool-build gotool-clients gotool-version …" @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: cd main/proto && python ../../libs/nanopb/generator/nanopb_generator.py \ -I ../../libs/nanopb/generator/proto esp_now_messages.proto @@ -31,7 +27,15 @@ proto_generate: proto_generate_uart proto_generate_espnow gotool-proto: cd $(GOTOOL_DIR) && protoc --go_out=./pb --go_opt=paths=source_relative \ --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: cd $(GOTOOL_DIR) && go mod tidy diff --git a/goTool/README.md b/goTool/README.md index 591e978..c01668b 100644 --- a/goTool/README.md +++ b/goTool/README.md @@ -28,6 +28,7 @@ go run . -port /dev/ttyUSB0 clients | `test` | — | Run an automated scenario (JSON configs under `testdata/`) | | `serve` | — | Web dashboard at `http://localhost:8080` (WebSocket live updates) | | `ota` | 16–19 | 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. @@ -72,7 +73,7 @@ HTTP API (used by the web UI): `GET/POST /api/deadzone`, `POST /api/unicast-test | 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` | ```bash @@ -96,8 +97,9 @@ clients (2): ## Regenerate protobuf +From repo root (needs `protoc`, `protoc-gen-go`, and for C also `pip install protobuf`): + ```bash -protoc --go_out=./pb --go_opt=paths=source_relative \ - --go_opt=Muart_messages.proto=powerpod/gotool/pb \ - -I ../main/proto ../main/proto/uart_messages.proto +make gotool-proto # Go: goTool/pb/uart_messages.pb.go +make proto_generate # C: main/proto/*.pb.h, *.pb.c ``` diff --git a/goTool/client_api.go b/goTool/client_api.go index e9cc4b6..39fdd02 100644 --- a/goTool/client_api.go +++ b/goTool/client_api.go @@ -16,6 +16,14 @@ func (m *managedSerial) getVersion() (*pb.VersionResponse, error) { 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) { payload, err := m.exchange(byte(pb.MessageType_CLIENT_INFO), "CLIENT_INFO") if err != nil { @@ -24,9 +32,28 @@ func (m *managedSerial) listClients() ([]*pb.ClientInfo, error) { 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) { + 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 - err := m.withPort(func(sp *serialPort) error { + err := portFn(func(sp *serialPort) error { var e error resp, e = sp.AccelDeadzone(req) return e diff --git a/goTool/cmd_ota_progress.go b/goTool/cmd_ota_progress.go new file mode 100644 index 0000000..708a3d1 --- /dev/null +++ b/goTool/cmd_ota_progress.go @@ -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 +} diff --git a/goTool/dashboard.go b/goTool/dashboard.go index 1fb8bd3..2477286 100644 --- a/goTool/dashboard.go +++ b/goTool/dashboard.go @@ -3,6 +3,7 @@ package main import ( "encoding/hex" "encoding/json" + "errors" "fmt" "log" "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{ UpdatedAt: time.Now().Format(time.RFC3339), SerialPort: portName, Clients: []ClientView{}, } - ver, err := link.getVersion() + ver, err := link.getVersionPoll() + if errors.Is(err, errUARTBusy) { + return pausedPollState(portName, last) + } if err != nil { return disconnectedState(portName, err) } @@ -124,12 +128,15 @@ func pollDashboard(link *managedSerial, portName string) DashboardState { RunningPartition: ver.GetRunningPartition(), OK: true, } - if dz, err := readDeadzone(link, 0); err == nil { + if dz, err := readDeadzonePoll(link, 0); err == nil { st.Master.Deadzone = dz } - clients, err := link.listClients() + clients, err := link.listClientsPoll() if err != nil { + if errors.Is(err, errUARTBusy) { + return pausedPollState(portName, last) + } st.SerialOK = false st.SerialError = err.Error() st.UARTConnected = link.IsConnected() @@ -146,7 +153,7 @@ func pollDashboard(link *managedSerial, portName string) DashboardState { LastPing: c.GetLastPing(), LastSuccessPing: c.GetLastSuccessPing(), } - if dz, err := readDeadzone(link, c.GetId()); err == nil { + if dz, err := readDeadzonePoll(link, c.GetId()); err == nil { cv.Deadzone = dz } st.Clients = append(st.Clients, cv) @@ -154,6 +161,18 @@ func pollDashboard(link *managedSerial, portName string) DashboardState { 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) { r, err := link.AccelDeadzone(&pb.AccelDeadzoneRequest{ Write: false, @@ -168,6 +187,20 @@ func readDeadzone(link *managedSerial, clientID uint32) (uint32, error) { 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 { if len(mac) == 0 { return "" @@ -180,8 +213,12 @@ func runPoller(link *managedSerial, portName string, hub *wsHub, interval time.D defer ticker.Stop() uartUp := false + var lastGood DashboardState publish := func() { - st := pollDashboard(link, portName) + st := pollDashboard(link, portName, &lastGood) + if st.UARTConnected && st.SerialOK { + lastGood = st + } if st.UARTConnected && !uartUp { log.Printf("UART %s connected", portName) } diff --git a/goTool/main.go b/goTool/main.go index fee006d..b10b7a1 100644 --- a/goTool/main.go +++ b/goTool/main.go @@ -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, " test run automated scenario (see testdata/)\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() } @@ -46,7 +47,7 @@ func main() { os.Exit(2) } 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 == "" { fmt.Fprintf(os.Stderr, "command %q requires -port\n\n", cmd) usage() @@ -68,6 +69,8 @@ func main() { runErr = runUnicastTest(sp, flag.Args()[1:]) case "ota": runErr = runOTA(sp, flag.Args()[1:]) + case "ota-progress", "ota_progress": + runErr = runOtaProgress(sp, flag.Args()[1:]) } default: fmt.Fprintf(os.Stderr, "unknown command %q\n\n", cmd) diff --git a/goTool/ota_upload.go b/goTool/ota_upload.go index b0e5707..d3089e4 100644 --- a/goTool/ota_upload.go +++ b/goTool/ota_upload.go @@ -12,32 +12,62 @@ import ( ) const ( - otaHostChunkSize = 200 - otaFlashBlockSize = 4096 - otaPrepareTimeout = 120 * time.Second - otaDefaultTimeout = 15 * time.Second + otaHostChunkSize = 200 + otaFlashBlockSize = 4096 + otaPrepareTimeout = 120 * 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 ( - otaStPreparing = 1 - otaStReady = 2 - otaStBlockAck = 3 - otaStSuccess = 4 - otaStFailed = 5 + otaStPreparing = 1 + otaStReady = 2 + otaStBlockAck = 3 + otaStSuccess = 4 + 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. type OTAProgress struct { - Type string `json:"type"` // always "ota_progress" - Phase string `json:"phase"` // preparing, ready, uploading, done, error - Percent int `json:"percent"` - Message string `json:"message"` - Bytes uint32 `json:"bytes_written,omitempty"` - Slot uint32 `json:"target_slot,omitempty"` + Type string `json:"type"` // always "ota_progress" + Phase string `json:"phase"` + Step string `json:"step,omitempty"` // master, slaves + Percent int `json:"percent"` + MasterPercent int `json:"master_percent,omitempty"` + MasterDone bool `json:"master_done,omitempty"` + Message string `json:"message"` + MasterMessage string `json:"master_message,omitempty"` + Bytes uint32 `json:"bytes_written,omitempty"` + Slot uint32 `json:"target_slot,omitempty"` + Slaves uint32 `json:"slaves,omitempty"` + ImageSize uint32 `json:"image_size,omitempty"` + SlaveProgress map[uint32]uint32 `json:"slave_progress,omitempty"` // client_id -> bytes + SlaveDetails map[uint32]OtaSlaveDetail `json:"slave_details,omitempty"` } type otaProgressFn func(OTAProgress) +const ( + otaStepMaster = "master" + otaStepSlaves = "slaves" +) + func runOTAUpload(m *managedSerial, firmware []byte, onProgress otaProgressFn) error { m.mu.Lock() defer m.mu.Unlock() @@ -52,69 +82,95 @@ func runOTAOnPortUnlocked(m *managedSerial, firmware []byte, onProgress otaProgr if len(firmware) == 0 { 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 { 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 { - p.Bytes = extra[0].Bytes - p.Slot = extra[0].Slot + e := extra[0] + 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) } if m.sp == nil { if err := m.openLocked(); err != nil { - notify("error", 0, err.Error()) + notify("error", "", 0, err.Error()) return err } } sp := m.sp if err := sp.port.SetReadTimeout(otaPrepareTimeout); err != nil { - notify("error", 0, err.Error()) + notify("error", "", 0, err.Error()) return err } 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{ Type: pb.MessageType_OTA_START, Payload: &pb.UartMessage_OtaStart{ - OtaStart: &pb.OtaStartPayload{TotalSize: uint32(len(firmware))}, + OtaStart: &pb.OtaStartPayload{TotalSize: uint32(imageSize)}, }, }, false); err != nil { - notify("error", 0, err.Error()) + notify("error", "", 0, err.Error()) return err } ready, err := waitOtaStatus(sp, otaStReady, otaPrepareTimeout, func(msg string) { - notify("preparing", 2, msg) + notify("preparing", otaStepMaster, 2, msg) }) if err != nil { - notify("error", 0, err.Error()) + notify("error", "", 0, err.Error()) 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 { - notify("error", 0, err.Error()) + notify("error", "", 0, err.Error()) return err } var seq uint32 - for offset := 0; offset < len(firmware); { + for offset := 0; offset < imageSize; { bytesInBlock := 0 - for bytesInBlock < otaFlashBlockSize && offset < len(firmware) { + for bytesInBlock < otaFlashBlockSize && offset < imageSize { n := otaHostChunkSize room := otaFlashBlockSize - bytesInBlock if n > room { n = room } - if offset+n > len(firmware) { - n = len(firmware) - offset + if offset+n > imageSize { + n = imageSize - offset } chunk := firmware[offset : offset+n] @@ -124,55 +180,313 @@ func runOTAOnPortUnlocked(m *managedSerial, firmware []byte, onProgress otaProgr OtaPayload: &pb.OtaPayload{Seq: seq, Data: chunk}, }, }, false); err != nil { - notify("error", 0, err.Error()) + notify("error", "", 0, err.Error()) return err } seq++ offset += n bytesInBlock += n - pct := 5 + (offset * 90 / len(firmware)) - notify("uploading", pct, fmt.Sprintf("%d / %d bytes", offset, len(firmware))) + pct := offset * 100 / imageSize + if pct > 99 { + pct = 99 + } + notify("uploading", otaStepMaster, pct, fmt.Sprintf("Master: %d / %d bytes", offset, imageSize)) } if bytesInBlock == otaFlashBlockSize { st, err := waitOtaStatus(sp, otaStBlockAck, otaDefaultTimeout, nil) if err != nil { - notify("error", 0, err.Error()) + notify("error", "", 0, err.Error()) return err } - pct := 5 + (offset * 90 / len(firmware)) - notify("uploading", pct, fmt.Sprintf("Block geschrieben (%d bytes in flash)", st.GetBytesWritten()), + pct := offset * 100 / imageSize + if pct > 99 { + pct = 99 + } + notify("uploading", otaStepMaster, pct, + fmt.Sprintf("Master: Block geschrieben (%d 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{ Type: pb.MessageType_OTA_END, Payload: &pb.UartMessage_OtaEnd{ OtaEnd: &pb.OtaEndPayload{}, }, }, false); err != nil { - notify("error", 0, err.Error()) + notify("error", "", 0, err.Error()) return err } - st, err := readOtaStatus(sp) + 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 st.GetStatus() != otaStSuccess { - err := fmt.Errorf("OTA failed: status=%d error=%d", st.GetStatus(), st.GetError()) - notify("error", 0, err.Error()) + 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()}) + 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) + if err != nil { + if onInterval != nil { + 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 + } + } +} + func writeUartMessage(sp *serialPort, msg *pb.UartMessage, logFrame bool) error { frame, err := encodeUartMessage(msg) if err != nil { diff --git a/goTool/pb/uart_messages.pb.go b/goTool/pb/uart_messages.pb.go index a34769f..f738ee7 100644 --- a/goTool/pb/uart_messages.pb.go +++ b/goTool/pb/uart_messages.pb.go @@ -37,6 +37,7 @@ const ( MessageType_OTA_END MessageType = 18 MessageType_OTA_STATUS MessageType = 19 MessageType_OTA_START_ESPNOW MessageType = 20 + MessageType_OTA_SLAVE_PROGRESS MessageType = 21 ) // Enum value maps for MessageType. @@ -55,6 +56,7 @@ var ( 18: "OTA_END", 19: "OTA_STATUS", 20: "OTA_START_ESPNOW", + 21: "OTA_SLAVE_PROGRESS", } MessageType_value = map[string]int32{ "UNKNOWN": 0, @@ -70,6 +72,7 @@ var ( "OTA_END": 18, "OTA_STATUS": 19, "OTA_START_ESPNOW": 20, + "OTA_SLAVE_PROGRESS": 21, } ) @@ -118,6 +121,8 @@ type UartMessage struct { // *UartMessage_AccelDeadzoneResponse // *UartMessage_EspnowUnicastTestRequest // *UartMessage_EspnowUnicastTestResponse + // *UartMessage_OtaSlaveProgressRequest + // *UartMessage_OtaSlaveProgressResponse Payload isUartMessage_Payload `protobuf_oneof:"payload"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache @@ -284,6 +289,24 @@ func (x *UartMessage) GetEspnowUnicastTestResponse() *EspNowUnicastTestResponse 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 { 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"` } +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_EchoPayload) isUartMessage_Payload() {} @@ -366,6 +397,10 @@ func (*UartMessage_EspnowUnicastTestRequest) isUartMessage_Payload() {} func (*UartMessage_EspnowUnicastTestResponse) isUartMessage_Payload() {} +func (*UartMessage_OtaSlaveProgressRequest) isUartMessage_Payload() {} + +func (*UartMessage_OtaSlaveProgressResponse) isUartMessage_Payload() {} + type Ack struct { state protoimpl.MessageState `protogen:"open.v1"` unknownFields protoimpl.UnknownFields @@ -1134,7 +1169,7 @@ func (*OtaEndPayload) Descriptor() ([]byte, []int) { } // 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 { state protoimpl.MessageState `protogen:"open.v1"` Status uint32 `protobuf:"varint,1,opt,name=status,proto3" json:"status,omitempty"` @@ -1203,11 +1238,209 @@ func (x *OtaStatusPayload) GetError() uint32 { 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 const file_uart_messages_proto_rawDesc = "" + "\n" + - "\x13uart_messages.proto\x12\x04alox\"\xcc\a\n" + + "\x13uart_messages.proto\x12\x04alox\x1a\fnanopb.proto\"\x8b\t\n" + "\vUartMessage\x12%\n" + "\x04type\x18\x01 \x01(\x0e2\x11.alox.MessageTypeR\x04type\x12,\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" + "\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" + - "\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" + "\x03Ack\"!\n" + "\vEchoPayload\x12\x12\n" + @@ -1272,18 +1507,35 @@ const file_uart_messages_proto_rawDesc = "" + "\x03seq\x18\x02 \x01(\rR\x03seq\"0\n" + "\x0fOtaStartPayload\x12\x1d\n" + "\n" + - "total_size\x18\x01 \x01(\rR\ttotalSize\"2\n" + + "total_size\x18\x01 \x01(\rR\ttotalSize\":\n" + "\n" + "OtaPayload\x12\x10\n" + - "\x03seq\x18\x01 \x01(\rR\x03seq\x12\x12\n" + - "\x04data\x18\x02 \x01(\fR\x04data\"\x0f\n" + + "\x03seq\x18\x01 \x01(\rR\x03seq\x12\x1a\n" + + "\x04data\x18\x02 \x01(\fB\x06\x92?\x03\b\xc8\x01R\x04data\"\x0f\n" + "\rOtaEndPayload\"\x86\x01\n" + "\x10OtaStatusPayload\x12\x16\n" + "\x06status\x18\x01 \x01(\rR\x06status\x12#\n" + "\rbytes_written\x18\x02 \x01(\rR\fbytesWritten\x12\x1f\n" + "\vtarget_slot\x18\x03 \x01(\rR\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" + "\aUNKNOWN\x10\x00\x12\a\n" + "\x03ACK\x10\x01\x12\b\n" + @@ -1298,7 +1550,8 @@ const file_uart_messages_proto_rawDesc = "" + "\aOTA_END\x10\x12\x12\x0e\n" + "\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 ( 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_msgTypes = make([]protoimpl.MessageInfo, 16) +var file_uart_messages_proto_msgTypes = make([]protoimpl.MessageInfo, 19) var file_uart_messages_proto_goTypes = []any{ (MessageType)(0), // 0: alox.MessageType (*UartMessage)(nil), // 1: alox.UartMessage @@ -1332,6 +1585,9 @@ var file_uart_messages_proto_goTypes = []any{ (*OtaPayload)(nil), // 14: alox.OtaPayload (*OtaEndPayload)(nil), // 15: alox.OtaEndPayload (*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{ 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 11, // 12: alox.UartMessage.espnow_unicast_test_request:type_name -> alox.EspNowUnicastTestRequest 12, // 13: alox.UartMessage.espnow_unicast_test_response:type_name -> alox.EspNowUnicastTestResponse - 5, // 14: alox.ClientInfoResponse.clients:type_name -> alox.ClientInfo - 7, // 15: alox.ClientInputResponse.clients:type_name -> alox.ClientInput - 16, // [16:16] is the sub-list for method output_type - 16, // [16:16] is the sub-list for method input_type - 16, // [16:16] is the sub-list for extension type_name - 16, // [16:16] is the sub-list for extension extendee - 0, // [0:16] is the sub-list for field type_name + 17, // 14: alox.UartMessage.ota_slave_progress_request:type_name -> alox.OtaSlaveProgressRequest + 19, // 15: alox.UartMessage.ota_slave_progress_response:type_name -> alox.OtaSlaveProgressResponse + 5, // 16: alox.ClientInfoResponse.clients:type_name -> alox.ClientInfo + 7, // 17: alox.ClientInputResponse.clients:type_name -> alox.ClientInput + 18, // 18: alox.OtaSlaveProgressResponse.slaves:type_name -> alox.OtaSlaveProgressEntry + 19, // [19:19] is the sub-list for method output_type + 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() } @@ -1376,6 +1635,8 @@ func file_uart_messages_proto_init() { (*UartMessage_AccelDeadzoneResponse)(nil), (*UartMessage_EspnowUnicastTestRequest)(nil), (*UartMessage_EspnowUnicastTestResponse)(nil), + (*UartMessage_OtaSlaveProgressRequest)(nil), + (*UartMessage_OtaSlaveProgressResponse)(nil), } type x struct{} out := protoimpl.TypeBuilder{ @@ -1383,7 +1644,7 @@ func file_uart_messages_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_uart_messages_proto_rawDesc), len(file_uart_messages_proto_rawDesc)), NumEnums: 1, - NumMessages: 16, + NumMessages: 19, NumExtensions: 0, NumServices: 0, }, diff --git a/goTool/serial_link.go b/goTool/serial_link.go index 2e6ec50..2179367 100644 --- a/goTool/serial_link.go +++ b/goTool/serial_link.go @@ -1,12 +1,16 @@ package main import ( + "errors" "fmt" "log" "sync" "time" ) +// errUARTBusy is returned when the port is held for OTA (poller should not treat as unplug). +var errUARTBusy = errors.New("uart busy (OTA in progress)") + // managedSerial keeps the UART open and reconnects after I/O failures or unplug. type managedSerial struct { portName string @@ -69,7 +73,22 @@ func (m *managedSerial) invalidateLocked(reason error) { } func (m *managedSerial) withPort(fn func(*serialPort) error) error { - m.mu.Lock() + 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() + } defer m.mu.Unlock() 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) { + return m.exchangePayloadVia(m.withPort, payload, cmdName) +} + +func (m *managedSerial) exchangePayloadPoll(payload []byte, cmdName string) ([]byte, error) { + return m.exchangePayloadVia(m.withPortPoll, payload, cmdName) +} + +func (m *managedSerial) exchangePayloadVia( + portFn func(func(*serialPort) error) error, + payload []byte, cmdName string, +) ([]byte, error) { var resp []byte - err := m.withPort(func(sp *serialPort) error { + err := portFn(func(sp *serialPort) error { var e error resp, e = sp.exchangePayloadLocked(payload, cmdName) 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) { + return m.exchangeVia(m.withPort, cmdID, cmdName) +} + +func (m *managedSerial) exchangePoll(cmdID byte, cmdName string) ([]byte, error) { + return m.exchangeVia(m.withPortPoll, cmdID, cmdName) +} + +func (m *managedSerial) exchangeVia( + portFn func(func(*serialPort) error) error, + cmdID byte, cmdName string, +) ([]byte, error) { var resp []byte - err := m.withPort(func(sp *serialPort) error { + err := portFn(func(sp *serialPort) error { var e error resp, e = sp.exchangeLocked(cmdID, cmdName) return e diff --git a/goTool/webui/index.html b/goTool/webui/index.html index 9481c6d..a18f3bf 100644 --- a/goTool/webui/index.html +++ b/goTool/webui/index.html @@ -131,6 +131,19 @@ .progress-bar { 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 { background: var(--pp-border); border: none; @@ -180,7 +193,7 @@
Version
-
Git
+
Hash
Partition
@@ -303,7 +316,7 @@
Firmware OTA (A/B)

- Lädt eine .bin auf die inaktive OTA-Partition (wie gotool ota). + Lädt eine .bin 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.

@@ -320,17 +333,76 @@
- + + + + + + + +
+ Warte auf Slave-Fortschritt… +
+ +

+
@@ -347,7 +419,13 @@ allDz: 100, slaveDz: {}, 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, configMsg: '', configMsgOk: false, @@ -392,20 +470,148 @@ if (n < 1024 * 1024) return (n / 1024).toFixed(1) + ' KiB'; 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) { 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 || ''; - 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; } if (p.phase === 'done' || p.phase === 'error') { this.ota.active = false; + if (p.phase === 'done') { + this.ota.masterPercent = 100; + } } }, async uploadOTA() { 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; const form = new FormData(); form.append('firmware', this.otaFile); @@ -414,23 +620,22 @@ const data = await r.json(); if (!r.ok || !data.success) { this.applyOTAProgress({ - phase: 'error', - percent: 0, + phase: 'error', step: '', percent: 0, message: data.error || 'OTA fehlgeschlagen' }); return; } - const slot = data.target_slot != null ? 'ota_' + data.target_slot : '?'; - this.applyOTAProgress({ - phase: 'done', - percent: 100, - message: `OK — ${data.bytes_written} Bytes nach ${slot} (Neustart zum Booten)` - }); + if (this.ota.phase !== 'done') { + const slot = data.target_slot != null ? 'ota_' + data.target_slot : '?'; + this.applyOTAProgress({ + phase: 'done', step: '', percent: 100, + message: `OK — ${data.bytes_written} Bytes, Slot ${slot}. Alle Knoten neu starten.` + }); + } } catch (e) { - this.applyOTAProgress({ phase: 'error', percent: 0, message: String(e) }); + this.applyOTAProgress({ phase: 'error', step: '', percent: 0, message: String(e) }); } finally { this.busy = false; - this.ota.active = false; } }, flash(msg, ok) { diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index 6611d13..b62d67c 100644 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -20,6 +20,7 @@ idf_component_register( "cmd_accel_deadzone.c" "cmd_espnow_unicast_test.c" "cmd_ota.c" + "cmd_ota_slave_progress.c" "ota_uart.c" "ota_espnow.c" "client_registry.c" diff --git a/main/README.md b/main/README.md index 0e19bdc..19fae5d 100644 --- a/main/README.md +++ b/main/README.md @@ -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 | | 19 | `OTA_STATUS` | Device → host (prepare/ready/block ACK/success/failed) | | 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: @@ -260,7 +261,28 @@ Host upload: 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 diff --git a/main/cmd_handler.c b/main/cmd_handler.c index 26e4bf8..b259177 100644 --- a/main/cmd_handler.c +++ b/main/cmd_handler.c @@ -40,6 +40,8 @@ static const char *message_type_name(uint16_t id) { return "OTA_STATUS"; case alox_MessageType_OTA_START_ESPNOW: return "OTA_START_ESPNOW"; + case alox_MessageType_OTA_SLAVE_PROGRESS: + return "OTA_SLAVE_PROGRESS"; default: return "UNKNOWN"; } diff --git a/main/cmd_ota.c b/main/cmd_ota.c index 2e9e4eb..8db3091 100644 --- a/main/cmd_ota.c +++ b/main/cmd_ota.c @@ -5,12 +5,20 @@ #include "esp_log.h" #include "freertos/FreeRTOS.h" #include "freertos/idf_additions.h" +#include #include static const char *TAG = "[OTA_CMD]"; #define OTA_PREPARE_STACK 8192 #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) { alox_UartMessage response; 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); } +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) { 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) { - uint32_t written = ota_uart_bytes_written(); - int 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); - return err; +static void ota_distribute_task(void *param) { + ota_dist_job_t *job = (ota_dist_job_t *)param; + if (job == NULL) { + vTaskDelete(NULL); + return; } const esp_partition_t *part = NULL; uint32_t image_size = 0; if (!ota_uart_get_staged_image(&part, &image_size)) { 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) { ESP_LOGE(TAG, "slave OTA distribution failed: %s", esp_err_to_name(err)); ota_uart_clear_staged(); send_ota_status(OTA_UART_ST_FAILED, 31); - return err; + free(job); + vTaskDelete(NULL); + return; } err = ota_uart_apply_boot(); if (err != ESP_OK) { send_ota_status(OTA_UART_ST_FAILED, (uint32_t)err); - return err; + free(job); + vTaskDelete(NULL); + return; } 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_SUCCESS; - response.payload.ota_status.bytes_written = written; - response.payload.ota_status.target_slot = slot >= 0 ? (uint32_t)slot : 0; + response.payload.ota_status.bytes_written = job->written; + response.payload.ota_status.target_slot = + job->slot >= 0 ? (uint32_t)job->slot : 0; response.payload.ota_status.error = 0; uart_cmd_send(&response, TAG); - return ESP_OK; + + free(job); + vTaskDelete(NULL); } 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; } - (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) { @@ -198,7 +264,7 @@ static void handle_ota_start_espnow(const uint8_t *data, size_t len) { 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) { send_ota_status(OTA_UART_ST_FAILED, 42); return; diff --git a/main/cmd_ota_slave_progress.c b/main/cmd_ota_slave_progress.c new file mode 100644 index 0000000..4968590 --- /dev/null +++ b/main/cmd_ota_slave_progress.c @@ -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); +} diff --git a/main/cmd_ota_slave_progress.h b/main/cmd_ota_slave_progress.h new file mode 100644 index 0000000..5c12798 --- /dev/null +++ b/main/cmd_ota_slave_progress.h @@ -0,0 +1,6 @@ +#ifndef CMD_OTA_SLAVE_PROGRESS_H +#define CMD_OTA_SLAVE_PROGRESS_H + +void cmd_ota_slave_progress_register(void); + +#endif diff --git a/main/ota_espnow.c b/main/ota_espnow.c index 8477dcf..d800d0b 100644 --- a/main/ota_espnow.c +++ b/main/ota_espnow.c @@ -36,10 +36,90 @@ typedef struct { uint8_t mac[OTA_MAX_TARGETS][6]; uint32_t id[OTA_MAX_TARGETS]; uint32_t expected_bytes; + uint32_t total_bytes; + ota_espnow_progress_cbs_t progress; } ota_dist_t; 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]) { for (uint8_t i = 0; i < s_dist.count; i++) { 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) { case OTA_ST_READY: + prog_update_idx(idx, OTA_ST_READY, 0, 0); xEventGroupSetBits(s_eg, bit); break; 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) { xEventGroupSetBits(s_eg, bit); } else { @@ -191,9 +277,12 @@ void ota_espnow_master_on_status(const uint8_t slave_mac[6], } break; case OTA_ST_SUCCESS: + prog_update_idx(idx, OTA_ST_SUCCESS, status->bytes_written, 0); xEventGroupSetBits(s_eg, bit); break; 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)", (unsigned long)s_dist.id[idx], (unsigned long)status->error); break; @@ -220,7 +309,8 @@ static size_t collect_targets(void) { } 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) { s_eg = xEventGroupCreate(); 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)", (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) { ESP_LOGW(TAG, "OTA_START to slave %lu failed", (unsigned long)s_dist.id[i]); + prog_end(); return err; } } if (!wait_target_bits(target_mask, OTA_PREPARE_TIMEOUT_MS)) { ESP_LOGE(TAG, "timeout waiting for slave OTA ready"); + prog_end(); 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]; uint32_t offset = 0; uint32_t seq = 0; @@ -263,6 +367,7 @@ static esp_err_t distribute_image(const esp_partition_t *partition, if (err != ESP_OK) { ESP_LOGE(TAG, "partition read @%lu failed: %s", (unsigned long)offset, esp_err_to_name(err)); + prog_end(); 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, block_buf + sent, chunk); if (err != ESP_OK) { + prog_end(); 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)) { ESP_LOGE(TAG, "timeout block ack @%lu bytes", (unsigned long)s_dist.expected_bytes); + prog_end(); return ESP_ERR_TIMEOUT; } 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); } 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); for (uint8_t i = 0; i < s_dist.count; i++) { err = esp_now_comm_send_ota_end(s_dist.mac[i]); if (err != ESP_OK) { + prog_end(); return err; } } if (!wait_target_bits(target_mask, OTA_END_TIMEOUT_MS)) { ESP_LOGE(TAG, "timeout waiting for slave OTA success"); + prog_end(); 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); 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) { return ESP_ERR_INVALID_ARG; } if (collect_targets() == 0) { 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 distribute_image(partition, size); + return distribute_image(partition, size, progress); } diff --git a/main/ota_espnow.h b/main/ota_espnow.h index e896814..0ed5fcc 100644 --- a/main/ota_espnow.h +++ b/main/ota_espnow.h @@ -4,10 +4,20 @@ #include "esp_err.h" #include "esp_now_messages.pb.h" #include "esp_partition.h" +#include "uart_messages.pb.h" #include +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. */ -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. */ 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); 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 diff --git a/main/ota_uart.h b/main/ota_uart.h index 66754bf..83106e4 100644 --- a/main/ota_uart.h +++ b/main/ota_uart.h @@ -17,8 +17,14 @@ typedef enum { OTA_UART_ST_BLOCK_ACK = 3, OTA_UART_ST_SUCCESS = 4, 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; +/** OtaStatusPayload.error when status == OTA_UART_ST_DISTRIBUTING. */ +#define OTA_DIST_AGGREGATE 0u +#define OTA_DIST_PER_SLAVE 1u + typedef enum { OTA_FEED_OK = 0, OTA_FEED_BLOCK_WRITTEN, diff --git a/main/powerpod.c b/main/powerpod.c index 40bc42e..d3cdd56 100644 --- a/main/powerpod.c +++ b/main/powerpod.c @@ -5,6 +5,7 @@ #include "cmd_client_info.h" #include "cmd_version.h" #include "cmd_ota.h" +#include "cmd_ota_slave_progress.h" #include "esp_now_comm.h" #include "powerpod.h" #include "driver/gpio.h" @@ -168,6 +169,7 @@ void app_main(void) { cmd_accel_deadzone_register(); cmd_espnow_unicast_test_register(); cmd_ota_register(); + cmd_ota_slave_progress_register(); } uint8_t current_digit = 10; diff --git a/main/proto/uart_messages.pb.c b/main/proto/uart_messages.pb.c index 2c812d0..cc3f4cb 100644 --- a/main/proto/uart_messages.pb.c +++ b/main/proto/uart_messages.pb.c @@ -6,7 +6,7 @@ #error Regenerate this file with the current version of nanopb generator. #endif -PB_BIND(alox_UartMessage, alox_UartMessage, AUTO) +PB_BIND(alox_UartMessage, alox_UartMessage, 2) 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_OtaSlaveProgressRequest, alox_OtaSlaveProgressRequest, AUTO) + + +PB_BIND(alox_OtaSlaveProgressEntry, alox_OtaSlaveProgressEntry, AUTO) + + +PB_BIND(alox_OtaSlaveProgressResponse, alox_OtaSlaveProgressResponse, 2) + + diff --git a/main/proto/uart_messages.pb.h b/main/proto/uart_messages.pb.h index f821b79..da7f3c3 100644 --- a/main/proto/uart_messages.pb.h +++ b/main/proto/uart_messages.pb.h @@ -1,8 +1,8 @@ /* Automatically generated nanopb header */ /* Generated by nanopb-1.0.0-dev */ -#ifndef PB_ALOX_MAIN_PROTO_UART_MESSAGES_PB_H_INCLUDED -#define PB_ALOX_MAIN_PROTO_UART_MESSAGES_PB_H_INCLUDED +#ifndef PB_ALOX_UART_MESSAGES_PB_H_INCLUDED +#define PB_ALOX_UART_MESSAGES_PB_H_INCLUDED #include #if PB_PROTO_HEADER_VERSION != 40 @@ -23,7 +23,8 @@ typedef enum _alox_MessageType { alox_MessageType_OTA_PAYLOAD = 17, alox_MessageType_OTA_END = 18, 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; /* Struct definitions */ @@ -99,8 +100,8 @@ typedef struct _alox_OtaStartPayload { uint32_t total_size; } 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; +/* Host → device: firmware chunk (up to 200 bytes); device buffers 4 KiB before flash write. */ typedef struct _alox_OtaPayload { uint32_t seq; alox_OtaPayload_data_t data; @@ -112,7 +113,7 @@ typedef struct _alox_OtaEndPayload { } alox_OtaEndPayload; /* 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 { uint32_t status; uint32_t bytes_written; @@ -120,6 +121,29 @@ typedef struct _alox_OtaStatusPayload { uint32_t error; } 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 { alox_MessageType type; pb_size_t which_payload; @@ -137,6 +161,8 @@ typedef struct _alox_UartMessage { alox_AccelDeadzoneResponse accel_deadzone_response; alox_EspNowUnicastTestRequest espnow_unicast_test_request; alox_EspNowUnicastTestResponse espnow_unicast_test_response; + alox_OtaSlaveProgressRequest ota_slave_progress_request; + alox_OtaSlaveProgressResponse ota_slave_progress_response; } payload; } alox_UartMessage; @@ -147,8 +173,8 @@ extern "C" { /* Helper constants for enums */ #define _alox_MessageType_MIN alox_MessageType_UNKNOWN -#define _alox_MessageType_MAX alox_MessageType_OTA_START_ESPNOW -#define _alox_MessageType_ARRAYSIZE ((alox_MessageType)(alox_MessageType_OTA_START_ESPNOW+1)) +#define _alox_MessageType_MAX alox_MessageType_OTA_SLAVE_PROGRESS +#define _alox_MessageType_ARRAYSIZE ((alox_MessageType)(alox_MessageType_OTA_SLAVE_PROGRESS+1)) #define alox_UartMessage_type_ENUMTYPE alox_MessageType @@ -168,6 +194,9 @@ extern "C" { + + + /* Initializer values for message structs */ #define alox_UartMessage_init_default {_alox_MessageType_MIN, 0, {alox_Ack_init_default}} #define alox_Ack_init_default {0} @@ -185,6 +214,9 @@ extern "C" { #define alox_OtaPayload_init_default {0, {0, {0}}} #define alox_OtaEndPayload_init_default {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_Ack_init_zero {0} #define alox_EchoPayload_init_zero {{{NULL}, NULL}} @@ -201,6 +233,9 @@ extern "C" { #define alox_OtaPayload_init_zero {0, {0, {0}}} #define alox_OtaEndPayload_init_zero {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) */ #define alox_EchoPayload_data_tag 1 @@ -239,6 +274,17 @@ extern "C" { #define alox_OtaStatusPayload_bytes_written_tag 2 #define alox_OtaStatusPayload_target_slot_tag 3 #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_ack_payload_tag 2 #define alox_UartMessage_echo_payload_tag 3 @@ -253,6 +299,8 @@ extern "C" { #define alox_UartMessage_accel_deadzone_response_tag 12 #define alox_UartMessage_espnow_unicast_test_request_tag 13 #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 */ #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_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_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_DEFAULT NULL #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_espnow_unicast_test_request_MSGTYPE alox_EspNowUnicastTestRequest #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) \ @@ -386,6 +438,30 @@ X(a, STATIC, SINGULAR, UINT32, error, 4) #define alox_OtaStatusPayload_CALLBACK 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_Ack_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_OtaEndPayload_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 */ #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_OtaEndPayload_fields &alox_OtaEndPayload_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) */ /* 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_ClientInfoResponse_size depends on runtime parameters */ /* alox_ClientInputResponse_size depends on runtime parameters */ -/* alox_OtaPayload_size depends on runtime parameters */ -#define ALOX_MAIN_PROTO_UART_MESSAGES_PB_H_MAX_SIZE alox_OtaStatusPayload_size +#define ALOX_UART_MESSAGES_PB_H_MAX_SIZE alox_OtaSlaveProgressResponse_size #define alox_AccelDeadzoneRequest_size 16 #define alox_AccelDeadzoneResponse_size 20 #define alox_Ack_size 0 @@ -437,6 +518,10 @@ extern const pb_msgdesc_t alox_OtaStatusPayload_msg; #define alox_EspNowUnicastTestRequest_size 12 #define alox_EspNowUnicastTestResponse_size 8 #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_OtaStatusPayload_size 24 diff --git a/main/proto/uart_messages.proto b/main/proto/uart_messages.proto index 7737828..da19a47 100644 --- a/main/proto/uart_messages.proto +++ b/main/proto/uart_messages.proto @@ -18,6 +18,7 @@ enum MessageType { OTA_END = 18; OTA_STATUS = 19; OTA_START_ESPNOW = 20; + OTA_SLAVE_PROGRESS = 21; } message UartMessage { @@ -36,6 +37,8 @@ message UartMessage { AccelDeadzoneResponse accel_deadzone_response = 12; EspNowUnicastTestRequest espnow_unicast_test_request = 13; 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 {} // 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 { uint32 status = 1; uint32 bytes_written = 2; uint32 target_slot = 3; 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]; +}