Fix web OTA upload and isolate OTA sessions across firmware and goTool.

Split ESP-NOW into core/master/slave modules, block non-OTA UART traffic during updates, and hold the host serial port exclusively so dashboard polling cannot interleave with firmware uploads.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-05-31 16:35:18 +02:00
co-authored by Cursor
parent 0cbc4d0644
commit 0eea27a876
29 changed files with 1828 additions and 1416 deletions
+37 -19
View File
@@ -101,7 +101,7 @@ func (h *wsHub) setState(st DashboardState) {
return
}
for _, c := range conns {
_ = c.WriteMessage(websocket.TextMessage, data)
h.writeJSON(c, data)
}
}
@@ -112,7 +112,7 @@ func (h *wsHub) register(c *websocket.Conn) {
h.mu.Unlock()
if data, err := json.Marshal(snap); err == nil {
_ = c.WriteMessage(websocket.TextMessage, data)
h.writeJSON(c, data)
}
}
@@ -122,6 +122,30 @@ func (h *wsHub) unregister(c *websocket.Conn) {
h.mu.Unlock()
}
// writeJSON sends to one client; removes it on error or panic (closed connection).
func (h *wsHub) writeJSON(c *websocket.Conn, data []byte) {
defer func() {
if recover() != nil {
h.unregister(c)
}
}()
if err := c.WriteMessage(websocket.TextMessage, data); err != nil {
h.unregister(c)
}
}
func (h *wsHub) broadcastJSON(data []byte) {
h.mu.RLock()
conns := make([]*websocket.Conn, 0, len(h.clients))
for c := range h.clients {
conns = append(conns, c)
}
h.mu.RUnlock()
for _, c := range conns {
h.writeJSON(c, data)
}
}
func applyAccelSamples(clients []ClientView, samples []*pb.AccelSample) []ClientView {
if len(samples) == 0 {
return clients
@@ -338,7 +362,7 @@ func (h *wsHub) patchClientAccelStream(clientID uint32, enabled bool) {
return
}
for _, c := range conns {
_ = c.WriteMessage(websocket.TextMessage, data)
h.writeJSON(c, data)
}
}
@@ -397,7 +421,7 @@ func (h *wsHub) patchLiveStream(enabled bool) {
return
}
for _, c := range conns {
_ = c.WriteMessage(websocket.TextMessage, data)
h.writeJSON(c, data)
}
}
@@ -430,7 +454,7 @@ func (h *wsHub) patchClientTapNotify(clientID uint32, single, doubleTap, triple
return
}
for _, c := range conns {
_ = c.WriteMessage(websocket.TextMessage, data)
h.writeJSON(c, data)
}
}
@@ -455,7 +479,7 @@ func (h *wsHub) mergeAccel(samples []*pb.AccelSample) {
return
}
for _, c := range conns {
_ = c.WriteMessage(websocket.TextMessage, data)
h.writeJSON(c, data)
}
}
@@ -479,25 +503,16 @@ func (h *wsHub) mergeTap(events []*pb.TapEvent) {
return
}
for _, c := range conns {
_ = c.WriteMessage(websocket.TextMessage, data)
h.writeJSON(c, data)
}
}
func (h *wsHub) broadcastRaw(v any) {
h.mu.RLock()
conns := make([]*websocket.Conn, 0, len(h.clients))
for c := range h.clients {
conns = append(conns, c)
}
h.mu.RUnlock()
data, err := json.Marshal(v)
if err != nil {
return
}
for _, c := range conns {
_ = c.WriteMessage(websocket.TextMessage, data)
}
h.broadcastJSON(data)
}
func pollDashboard(link *managedSerial, portName string, last *DashboardState, streamCtl *accelStreamCtl, tapCtl *tapNotifyCtl) DashboardState {
@@ -575,6 +590,9 @@ func pollDashboard(link *managedSerial, portName string, last *DashboardState, s
func applyBatteryToState(link *managedSerial, st *DashboardState) {
bat, err := link.BatteryStatusPoll(&pb.BatteryStatusRequest{AllClients: true})
if errors.Is(err, errUARTBusy) {
return
}
if err != nil {
log.Printf("battery poll: %v", err)
return
@@ -602,7 +620,7 @@ func (h *wsHub) mergeBattery(samples []batterySampleJSON) {
return
}
for _, c := range conns {
_ = c.WriteMessage(websocket.TextMessage, data)
h.writeJSON(c, data)
}
}
@@ -619,7 +637,7 @@ func runBatteryPoller(link *managedSerial, hub *wsHub, interval time.Duration, s
continue
}
bat, err := link.BatteryStatusPoll(&pb.BatteryStatusRequest{AllClients: true})
if err != nil {
if errors.Is(err, errUARTBusy) || err != nil {
continue
}
hub.mergeBattery(batterySamplesFromPB(bat.GetSamples()))