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:
2026-05-19 21:07:46 +02:00
co-authored by Cursor
parent 5a948a5c8c
commit a0f4a81a55
23 changed files with 1467 additions and 146 deletions
+43 -6
View File
@@ -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)
}