From 0eea27a876926b33a193e7a4098b576b2ef4d234 Mon Sep 17 00:00:00 2001 From: simon Date: Sun, 31 May 2026 16:35:18 +0200 Subject: [PATCH] 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 --- docs/ARCHITECTURE.md | 42 +- docs/DOCUMENTATION.md | 13 +- goTool/api_serve.go | 7 +- goTool/cmd_ota.go | 3 +- goTool/dashboard.go | 56 +- goTool/ota_upload.go | 102 ++- goTool/serial_link.go | 23 +- goTool/uart/frame.go | 3 +- goTool/webui/index.html | 17 +- main/CMakeLists.txt | 4 + main/README.md | 10 +- main/cmd/cmd_accel_stream.c | 14 +- main/cmd/cmd_battery.c | 28 +- main/cmd/cmd_handler.c | 29 +- main/cmd/cmd_handler.h | 1 - main/cmd/cmd_ota.c | 62 +- main/cmd/cmd_ota_slave_progress.c | 22 +- main/cmd/cmd_tap_notify.c | 14 +- main/esp_now_comm.c | 1257 +---------------------------- main/esp_now_core.c | 172 ++++ main/esp_now_core.h | 32 + main/esp_now_master.c | 484 +++++++++++ main/esp_now_master.h | 11 + main/esp_now_slave.c | 579 +++++++++++++ main/esp_now_slave.h | 14 + main/ota_espnow.c | 201 ++++- main/ota_espnow.h | 6 + main/ota_session.c | 25 + main/ota_session.h | 13 + 29 files changed, 1828 insertions(+), 1416 deletions(-) create mode 100644 main/esp_now_core.c create mode 100644 main/esp_now_core.h create mode 100644 main/esp_now_master.c create mode 100644 main/esp_now_master.h create mode 100644 main/esp_now_slave.c create mode 100644 main/esp_now_slave.h create mode 100644 main/ota_session.c create mode 100644 main/ota_session.h diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index d7c19b4..e43d0fa 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -41,7 +41,7 @@ flowchart TB | Rolle | UART | ESP-NOW | Zentrale Datenhaltung | |-------|------|---------|------------------------| | **Master** | Ja — einziger Befehlseingang von außen | Discover (Broadcast), Unicast zu Slaves | `client_registry.c` | -| **Slave** | Nein | Antwort auf Discover, Heartbeat, Events zum Master | Lokaler Zustand in `esp_now_comm.c` | +| **Slave** | Nein | Antwort auf Discover, Heartbeat, Events zum Master | `esp_now_slave.c` | **Einstieg:** `main/powerpod.c` → `app_main()`. @@ -158,11 +158,26 @@ flowchart LR | Protobuf + Framing | `main/uart_proto.c` | `uart_send_uart_message()` | | XOR-Rahmen | `main/uart.c` | `uart_send_framed()` | -### 4.3 Interner Befehlspost (ohne UART) +### 4.3 OTA-Sperre (keine parallelen Befehle) -`msg_post(id, data, len)` in `cmd_handler.c` legt dieselbe `generic_msg_t`-Struktur in `cmd_queue` — für zukünftige Firmware-interne Quellen (z. B. ESP-NOW → PC-Pfad). Aktuell ist **UART der einzige Produktiv-Eingang**. +Während einer OTA-Session (`ota_uart_is_active()` oder `ota_espnow_distribution_active()`) lehnt der Dispatcher alle UART-Befehle ab **außer**: -### 4.4 Bridge-Muster: UART-Befehl → ESP-NOW +- `OTA_START`, `OTA_PAYLOAD`, `OTA_END`, `OTA_START_ESPNOW`, `OTA_SLAVE_PROGRESS` + +Implementierung: `ota_session.c`, Prüfung in `vCmdDispatcherTask` vor Handler-Aufruf. + +Auf dem **Slave** verarbeitet `espnow_recv_cb` während `ota_uart_is_active()` nur noch OTA-Nachrichten vom joined Master (kein Discover, Stream, LED, …). + +Auf dem **Master** während ESP-NOW-Verteilung nur noch `ESPNOW_OTA_STATUS` aus dem recv_cb. + +### 4.4 UART-Request-Decode (strikt) + +| Regel | Beispiele | +|-------|-----------| +| Leerer protobuf-Body (`len == 0`) erlaubt | `VERSION`, `CLIENT_INFO`, `CACHE_STATUS`, `BATTERY_STATUS` (Defaults) | +| `len > 0` → Decode muss gelingen, `which_payload` muss passen | `ACCEL_STREAM`, `TAP_NOTIFY`, `RESTART`, OTA, … | + +### 4.5 Bridge-Muster: UART-Befehl → ESP-NOW Viele Master-Handler folgen demselben Muster: @@ -237,13 +252,12 @@ Queue-Größe Master: **64** Einträge (`powerpod.c`); volle Queue → Warnung, ### 6.1 Stack-Initialisierung -`esp_now_comm_init()` (`main/esp_now_comm.c`): +`esp_now_comm_init()` (`esp_now_comm.c`) → `esp_now_core` (WiFi/Radio/Send) + Rolle: -1. `client_registry_init()` -2. WiFi STA, Kanal = `network` (1–13) -3. `esp_now_init()`, `esp_now_register_recv_cb(espnow_recv_cb)` -4. **Master:** Broadcast-Peer, Tasks `espnow_disc` (500 ms), `espnow_mon` (1 s) -5. **Slave:** `slave_tx_task`, `slave_heartbeat_task`, `slave_accel_stream_task` (16 ms) +1. `client_registry_init()` (Master) +2. `esp_now_init()`, recv_cb leitet an `esp_now_master_on_recv` / `esp_now_slave_on_recv` +3. **Master** (`esp_now_master.c`): `espnow_disc`, `espnow_mon` +4. **Slave** (`esp_now_slave.c`): `espnow_stx`, `espnow_hb`, `espnow_accel`, `ota_slave_work` **Codec:** Rohes Paket = ein nanopb `EspNowMessage` — **kein** zusätzliches Framing (`main/esp_now_proto.c`). @@ -276,6 +290,10 @@ sequenceDiagram **Master-Verlust:** Slave setzt Join zurück, wenn **5 s** kein Discover vom gleichen Master (`SLAVE_MASTER_LOST_MS`). +**Join-Policy:** Master→Slave-Steuerung (Stream, Tap, LED, OTA, `UNICAST_TEST`, `SET_ACCEL_DEADZONE`, …) nur bei `s_slave_joined` und `src_addr == s_master_mac`. Während `ota_uart_is_active()` auf dem Slave verarbeitet der recv_cb nur OTA vom joined Master. + +**Slave OTA:** Payload/End/Status-Sends laufen über `ota_slave_work_task` (Queue), nicht im ESP-NOW-recv_cb. + ### 6.3 Master → Slave (Unicast) Alle Master-Sends laufen über `send_message()` / `send_message_ex()`: @@ -361,7 +379,7 @@ flowchart TB | UART Handler-Boilerplate | `main/uart_cmd.c`, `main/uart_cmd.h` | Decode, Register, Send | | Command Dispatch | `main/cmd/cmd_handler.c` | Queue, Dispatcher | | UART Commands | `main/cmd/cmd_*.c` | Pro Befehl ein Modul | -| ESP-NOW Kern | `main/esp_now_comm.c`, `.h` | WiFi, Tasks, recv/send, Slave-Join | +| ESP-NOW | `esp_now_comm.c` Init/Router; `esp_now_core.c` Send/Peer; `esp_now_master.c` / `esp_now_slave.c` Rollenlogik | | ESP-NOW Codec | `main/esp_now_proto.c` | nanopb encode/decode | | Registry | `main/client_registry.c` | Slave-Tabelle + Caches | | BMA456 | `main/bosch456.c` | Sensor, Tap, Deadzone-Filter | @@ -386,7 +404,7 @@ Regenerierung: `make proto_generate` (siehe `DOCUMENTATION.md`). ## 11. Design-Entscheidungen -1. **Eine Queue für alle Commands** — einheitliches Modell für UART und künftige interne Quellen. +1. **Eine Queue für UART-Commands** — einziger Eingang vom Host; während OTA nur OTA-Befehle. 2. **Kein ESP-NOW-Send im `recv_cb`** — Slave antwortet auf Discover über `slave_tx_task` (Deadlock-/Stack-Risiko vermeiden). 3. **Registry-MAC = ESP-NOW-Quelladresse** — Protobuf-MAC ist optional/informativ. 4. **Gleiches Binary** — Konfiguration nur Hardware (DIP + Expander); reduziert Release-Komplexität. diff --git a/docs/DOCUMENTATION.md b/docs/DOCUMENTATION.md index e2853a3..471ca95 100644 --- a/docs/DOCUMENTATION.md +++ b/docs/DOCUMENTATION.md @@ -140,8 +140,9 @@ Implementierung: `uart.c` — `parse_uart_byte()`, `uart_send_framed()`. 1. `uart_read_task` parst Frames → `uart_enqueue_packet` 2. `generic_msg_t` → `cmd_queue` 3. `vCmdDispatcherTask` → registrierter `msg_callback_t` -4. Handler: `uart_cmd_decode(data, len, &msg)` — `data` ist **nur** protobuf-Teil -5. Antwort: `uart_cmd_init_response()` + Felder setzen + `uart_cmd_send()` +4. Handler: `uart_cmd_decode(data, len, &msg)` — `data` ist **nur** protobuf-Teil; bei `len > 0` strikt (Decode/`which_payload`-Fehler → Fehlerantwort) +5. `ota_session_uart_cmd_allowed()` — während OTA nur OTA-Befehle +6. Antwort: `uart_cmd_init_response()` + Felder setzen + `uart_cmd_send()` Hilfsmakro für Request-Felder: @@ -404,7 +405,10 @@ Includes in generierten `.pb.c` müssen `"uart_messages.pb.h"` heißen (nicht `m | Datei | Rolle | |-------|--------| -| `esp_now_comm.c` | WiFi, ESP-NOW, alle Rollen-Tasks | +| `esp_now_comm.c` | Init, recv-Router | +| `esp_now_core.c` | WiFi, Peer, gemeinsamer Send | +| `esp_now_master.c` | Master-Tasks, Registry, Unicast send | +| `esp_now_slave.c` | Join, Heartbeat, Accel/Tap send | | `esp_now_proto.c` | nanopb für EspNowMessage | | `client_registry.c` | Slave-Tabelle + Telemetrie-Cache | @@ -436,7 +440,8 @@ Includes in generierten `.pb.c` müssen `"uart_messages.pb.h"` heißen (nicht `m | `pod_settings.c` | NVS | | `pod_reboot.c` | Verzögerter Restart | | `ota_uart.c` | Flash-Puffer UART-OTA | -| `ota_espnow.c` | OTA an Slaves | +| `ota_espnow.c` | OTA Master/Slave; Slave-Work-Queue | +| `ota_session.c` | OTA-Sperre für UART-Dispatcher | ### 14.5 Protobuf diff --git a/goTool/api_serve.go b/goTool/api_serve.go index 052b2fe..0ba7d18 100644 --- a/goTool/api_serve.go +++ b/goTool/api_serve.go @@ -2,6 +2,7 @@ package main import ( "encoding/json" + "errors" "fmt" "io" "net/http" @@ -146,7 +147,11 @@ func serveOTAUpload(w http.ResponseWriter, r *http.Request, link *managedSerial, if hub != nil { hub.broadcastRaw(OTAProgress{Type: "ota_progress", Phase: "error", Message: err.Error()}) } - writeJSON(w, http.StatusServiceUnavailable, otaAPIResponse{Error: err.Error()}) + status := http.StatusServiceUnavailable + if errors.Is(err, errOTAInProgress) { + status = http.StatusConflict + } + writeJSON(w, status, otaAPIResponse{Error: err.Error()}) return } writeJSON(w, http.StatusOK, otaAPIResponse{ diff --git a/goTool/cmd_ota.go b/goTool/cmd_ota.go index f82196a..8e61100 100644 --- a/goTool/cmd_ota.go +++ b/goTool/cmd_ota.go @@ -16,8 +16,7 @@ func runOTA(sp *serialPort, args []string) error { sp.mu.Lock() defer sp.mu.Unlock() - m := &managedSerial{quiet: false, sp: sp} - return runOTAOnPortUnlocked(m, data, func(p OTAProgress) { + return runOTAOnPortUnlocked(sp, data, func(p OTAProgress) { switch p.Phase { case "preparing", "ready": fmt.Println(p.Message) diff --git a/goTool/dashboard.go b/goTool/dashboard.go index e5c0910..d2f529c 100644 --- a/goTool/dashboard.go +++ b/goTool/dashboard.go @@ -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())) diff --git a/goTool/ota_upload.go b/goTool/ota_upload.go index d3089e4..8dfcba6 100644 --- a/goTool/ota_upload.go +++ b/goTool/ota_upload.go @@ -2,7 +2,6 @@ package main import ( "fmt" - "log" "time" "google.golang.org/protobuf/proto" @@ -69,16 +68,45 @@ const ( ) func runOTAUpload(m *managedSerial, firmware []byte, onProgress otaProgressFn) error { + push := func(phase, msg string) { + if onProgress == nil { + return + } + onProgress(OTAProgress{ + Type: "ota_progress", Phase: phase, Step: otaStepMaster, + Percent: 0, Message: msg, MasterMessage: msg, + }) + } + push("preparing", "UART wird vorbereitet…") + + // Block until the UART is free, then hold m.mu for the entire upload so + // dashboard/API polling cannot interleave on the serial port. m.mu.Lock() - defer m.mu.Unlock() - err := runOTAOnPortUnlocked(m, firmware, onProgress) + if m.otaActive { + m.mu.Unlock() + return errOTAInProgress + } + m.otaActive = true + if m.sp == nil { + if err := m.openLocked(); err != nil { + m.otaActive = false + m.mu.Unlock() + push("error", err.Error()) + return err + } + } + sp := m.sp + + err := runOTAOnPortUnlocked(sp, firmware, onProgress) if err != nil { m.invalidateLocked(err) } + m.otaActive = false + m.mu.Unlock() return err } -func runOTAOnPortUnlocked(m *managedSerial, firmware []byte, onProgress otaProgressFn) error { +func runOTAOnPortUnlocked(sp *serialPort, firmware []byte, onProgress otaProgressFn) error { if len(firmware) == 0 { return fmt.Errorf("empty firmware") } @@ -120,32 +148,31 @@ func runOTAOnPortUnlocked(m *managedSerial, firmware []byte, onProgress otaProgr onProgress(p) } - if m.sp == nil { - if err := m.openLocked(); err != nil { - notify("error", "", 0, err.Error()) - return err - } - } - - sp := m.sp - if err := sp.port.SetReadTimeout(otaPrepareTimeout); err != nil { + if err := sp.port.SetReadTimeout(readTimeout); err != nil { notify("error", "", 0, err.Error()) return err } - defer sp.port.SetReadTimeout(readTimeout) notify("preparing", otaStepMaster, 0, fmt.Sprintf("Master: OTA start (%d bytes)…", imageSize)) + flushSerialInput(sp) + if err := writeUartMessage(sp, &pb.UartMessage{ Type: pb.MessageType_OTA_START, Payload: &pb.UartMessage_OtaStart{ OtaStart: &pb.OtaStartPayload{TotalSize: uint32(imageSize)}, }, - }, false); err != nil { + }); err != nil { notify("error", "", 0, err.Error()) return err } + if err := sp.port.SetReadTimeout(otaPrepareTimeout); err != nil { + notify("error", "", 0, err.Error()) + return err + } + defer func() { _ = sp.port.SetReadTimeout(readTimeout) }() + ready, err := waitOtaStatus(sp, otaStReady, otaPrepareTimeout, func(msg string) { notify("preparing", otaStepMaster, 2, msg) }) @@ -179,7 +206,7 @@ func runOTAOnPortUnlocked(m *managedSerial, firmware []byte, onProgress otaProgr Payload: &pb.UartMessage_OtaPayload{ OtaPayload: &pb.OtaPayload{Seq: seq, Data: chunk}, }, - }, false); err != nil { + }); err != nil { notify("error", "", 0, err.Error()) return err } @@ -219,7 +246,7 @@ func runOTAOnPortUnlocked(m *managedSerial, firmware []byte, onProgress otaProgr Payload: &pb.UartMessage_OtaEnd{ OtaEnd: &pb.OtaEndPayload{}, }, - }, false); err != nil { + }); err != nil { notify("error", "", 0, err.Error()) return err } @@ -333,7 +360,7 @@ func queryOtaSlaveProgressLocked(sp *serialPort, clientID uint32, }, }, } - if err := writeUartMessage(sp, req, false); err != nil { + if err := writeUartMessage(sp, req); err != nil { return nil, err } if queryTimeout <= 0 { @@ -487,14 +514,11 @@ func waitOtaComplete(sp *serialPort, timeout time.Duration, } } -func writeUartMessage(sp *serialPort, msg *pb.UartMessage, logFrame bool) error { +func writeUartMessage(sp *serialPort, msg *pb.UartMessage) error { frame, err := encodeUartMessage(msg) if err != nil { return err } - if logFrame { - log.Printf("sending %s (%d frame bytes)", msg.Type, len(frame)) - } _, err = sp.port.Write(frame) return err } @@ -505,12 +529,24 @@ func waitOtaStatus(sp *serialPort, want uint32, timeout time.Duration, onPrepari if time.Now().After(deadline) { return nil, fmt.Errorf("timeout waiting for OTA status %d", want) } - if err := sp.port.SetReadTimeout(time.Until(deadline)); err != nil { + readWait := time.Until(deadline) + if readWait > otaStatusPollTimeout { + readWait = otaStatusPollTimeout + } + if err := sp.port.SetReadTimeout(readWait); err != nil { return nil, err } - st, err := readOtaStatus(sp) + payload, err := uartframe.ReadFrame(sp.port, nil) if err != nil { - return nil, err + continue + } + msg, err := decodeUartPayload(payload) + if err != nil || msg.GetType() != pb.MessageType_OTA_STATUS { + continue + } + st := msg.GetOtaStatus() + if st == nil { + continue } switch st.GetStatus() { case want: @@ -553,6 +589,22 @@ func encodeUartMessage(msg *pb.UartMessage) ([]byte, error) { return uartframe.EncodeFrame(payload) } +// flushSerialInput drops stale RX bytes (not full frames — avoids ReadFrame blocking). +func flushSerialInput(sp *serialPort) { + if sp == nil { + return + } + _ = sp.port.SetReadTimeout(10 * time.Millisecond) + buf := make([]byte, 256) + deadline := time.Now().Add(50 * time.Millisecond) + for time.Now().Before(deadline) { + n, err := sp.port.Read(buf) + if n == 0 || err != nil { + return + } + } +} + func decodeUartPayload(payload []byte) (*pb.UartMessage, error) { if len(payload) == 0 { return nil, fmt.Errorf("empty response") diff --git a/goTool/serial_link.go b/goTool/serial_link.go index 6d905db..2cdcd69 100644 --- a/goTool/serial_link.go +++ b/goTool/serial_link.go @@ -11,14 +11,18 @@ import ( // 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)") +// errOTAInProgress is returned when a second OTA upload is attempted while one is running. +var errOTAInProgress = errors.New("OTA upload already in progress") + // managedSerial keeps the UART open and reconnects after I/O failures or unplug. type managedSerial struct { portName string baud int quiet bool - mu sync.Mutex - sp *serialPort + mu sync.Mutex + sp *serialPort + otaActive bool // UART held for firmware upload; poll/API must not interleave } func newManagedSerial(portName string, baud int) *managedSerial { @@ -76,20 +80,17 @@ func (m *managedSerial) withPort(fn func(*serialPort) error) error { return m.withPortLocked(false, fn) } -// withPortPoll is like withPort but returns errUARTBusy instead of blocking during OTA. +// withPortPoll is like withPort but returns errUARTBusy during OTA (no TryLock race). 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() - } +func (m *managedSerial) withPortLocked(poll bool, fn func(*serialPort) error) error { + m.mu.Lock() defer m.mu.Unlock() + if m.otaActive { + return errUARTBusy + } if m.sp == nil { if err := m.openLocked(); err != nil { diff --git a/goTool/uart/frame.go b/goTool/uart/frame.go index 8849e25..63c3795 100644 --- a/goTool/uart/frame.go +++ b/goTool/uart/frame.go @@ -9,7 +9,8 @@ import ( const ( StartMarker = 0xAA StopMarker = 0xCC - MaxPayload = 252 + // Must match main/uart.h MAX_PAYLOAD_SIZE (MAX_BUF_SIZE - 4). + MaxPayload = 248 ) var ( diff --git a/goTool/webui/index.html b/goTool/webui/index.html index fc216c4..070241f 100644 --- a/goTool/webui/index.html +++ b/goTool/webui/index.html @@ -938,8 +938,21 @@ return rows; }, applyOTAProgress(p) { - this.ota.phase = p.phase || ''; - this.ota.step = p.step || this.ota.step || ''; + const prevPhase = this.ota.phase; + const prevStep = this.ota.step; + if (p.phase) { + // Ignore out-of-order master upload updates after distribution started. + if (!(p.phase === 'uploading' && prevPhase === 'distributing')) { + this.ota.phase = p.phase; + } + } + if (p.step) { + if (!(p.step === 'master' && (prevStep === 'slaves' || prevPhase === 'distributing'))) { + this.ota.step = p.step; + } + } else if (!this.ota.step) { + this.ota.step = ''; + } this.ota.percent = p.percent ?? this.ota.percent; this.ota.message = p.message || ''; if (p.image_size) this.ota.imageSize = p.image_size; diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index bfdc377..86b9d9e 100644 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -31,8 +31,12 @@ idf_component_register( "cmd/cmd_ota_slave_progress.c" "ota_uart.c" "ota_espnow.c" + "ota_session.c" "client_registry.c" "esp_now_comm.c" + "esp_now_core.c" + "esp_now_master.c" + "esp_now_slave.c" "esp_now_proto.c" "bosch456.c" "board_input.c" diff --git a/main/README.md b/main/README.md index 9602156..4fec2fd 100644 --- a/main/README.md +++ b/main/README.md @@ -178,7 +178,7 @@ Logging: ## Command handler -Generic dispatch for host commands (UART today; `msg_post()` for in-firmware sources later). +Generic dispatch for host commands over UART only. ``` UART → generic_msg_t queue → vCmdDispatcherTask → registered handler @@ -188,7 +188,8 @@ UART → generic_msg_t queue → vCmdDispatcherTask → registered handler |-----|-------------| | `init_cmdHandler(queue)` | Start dispatcher task (priority 5) | | `msg_register_handler(id, cb)` | Register callback; max 32 handlers | -| `msg_post(id, data, len)` | Enqueue from firmware (e.g. future ESP-NOW → PC path) | + +During an OTA session (`ota_session_busy()`), the dispatcher rejects all UART commands except OTA_* and `OTA_SLAVE_PROGRESS` (see `ota_session.c`). ```c typedef void (*msg_callback_t)(const uint8_t *data, size_t len); @@ -519,7 +520,10 @@ Target: ESP32-S3. Close serial monitor on the UART adapter port before running ` | `powerpod.c` | `app_main`, DIP/network config, init order | | `powerpod.h` | Pin defines | | `app_config.h` | `app_config_t` | -| `esp_now_comm.c/h` | WiFi, ESP-NOW, discover / slave info / OTA send | +| `esp_now_comm.c/h` | ESP-NOW init and recv router | +| `esp_now_core.c/h` | Shared WiFi, peer, send | +| `esp_now_master.c/h` | Master discover, monitor, unicast | +| `esp_now_slave.c/h` | Slave join, heartbeat, telemetry | | `ota_uart.c/h` | Shared 4 KiB OTA flash buffer (UART + ESP-NOW) | | `ota_espnow.c/h` | Master: distribute staged image to slaves | | `cmd/cmd_ota.c/h` | UART OTA command handlers (master only) | diff --git a/main/cmd/cmd_accel_stream.c b/main/cmd/cmd_accel_stream.c index 0d3501e..9cefb1a 100644 --- a/main/cmd/cmd_accel_stream.c +++ b/main/cmd/cmd_accel_stream.c @@ -33,12 +33,20 @@ static void handle_accel_stream(const uint8_t *data, size_t len) { alox_UartMessage uart_msg; alox_AccelStreamRequest req = alox_AccelStreamRequest_init_zero; - if (uart_cmd_decode(data, len, &uart_msg) == ESP_OK) { + if (len > 0) { + if (uart_cmd_decode(data, len, &uart_msg) != ESP_OK) { + ESP_LOGW(TAG, "decode failed"); + reply(false, 0, false, 0); + return; + } const alox_AccelStreamRequest *req_ptr = UART_CMD_REQ( &uart_msg, alox_UartMessage_accel_stream_request_tag, accel_stream_request); - if (req_ptr != NULL) { - req = *req_ptr; + if (req_ptr == NULL) { + ESP_LOGW(TAG, "missing accel_stream_request"); + reply(false, 0, false, 0); + return; } + req = *req_ptr; } if (req.write) { diff --git a/main/cmd/cmd_battery.c b/main/cmd/cmd_battery.c index 9fb2608..47c1131 100644 --- a/main/cmd/cmd_battery.c +++ b/main/cmd/cmd_battery.c @@ -67,14 +67,28 @@ static void handle_battery_status(const uint8_t *data, size_t len) { if (len > 0) { alox_UartMessage uart_msg; - if (uart_cmd_decode(data, len, &uart_msg) == ESP_OK) { - const alox_BatteryStatusRequest *req_ptr = UART_CMD_REQ( - &uart_msg, alox_UartMessage_battery_status_request_tag, - battery_status_request); - if (req_ptr != NULL) { - req = *req_ptr; - } + if (uart_cmd_decode(data, len, &uart_msg) != ESP_OK) { + ESP_LOGW(TAG, "decode failed"); + alox_UartMessage response; + uart_cmd_init_response(&response, alox_MessageType_BATTERY_STATUS, + alox_UartMessage_battery_status_response_tag); + response.payload.battery_status_response.success = false; + uart_cmd_send(&response, TAG); + return; } + const alox_BatteryStatusRequest *req_ptr = UART_CMD_REQ( + &uart_msg, alox_UartMessage_battery_status_request_tag, + battery_status_request); + if (req_ptr == NULL) { + ESP_LOGW(TAG, "missing battery_status_request"); + alox_UartMessage response; + uart_cmd_init_response(&response, alox_MessageType_BATTERY_STATUS, + alox_UartMessage_battery_status_response_tag); + response.payload.battery_status_response.success = false; + uart_cmd_send(&response, TAG); + return; + } + req = *req_ptr; } alox_UartMessage response; diff --git a/main/cmd/cmd_handler.c b/main/cmd/cmd_handler.c index d04b2eb..e22aa7a 100644 --- a/main/cmd/cmd_handler.c +++ b/main/cmd/cmd_handler.c @@ -1,4 +1,5 @@ #include "cmd_handler.h" +#include "ota_session.h" #include "esp_err.h" #include "esp_log.h" #include "freertos/FreeRTOS.h" @@ -88,33 +89,19 @@ esp_err_t msg_register_handler(uint16_t id, msg_callback_t cb) { return ESP_OK; } -esp_err_t msg_post(uint16_t id, const uint8_t *data, size_t len) { - if (cmd_queue == NULL) { - return ESP_ERR_INVALID_STATE; - } - - generic_msg_t msg = {.msg_id = id, .len = len, .payload = NULL}; - if (len > 0) { - msg.payload = malloc(len); - if (msg.payload == NULL) { - return ESP_ERR_NO_MEM; - } - memcpy(msg.payload, data, len); - } - - if (xQueueSend(cmd_queue, &msg, pdMS_TO_TICKS(100)) != pdPASS) { - free(msg.payload); - return ESP_ERR_TIMEOUT; - } - return ESP_OK; -} - void vCmdDispatcherTask(void *param) { (void)param; generic_msg_t msg; while (1) { if (xQueueReceive(cmd_queue, &msg, portMAX_DELAY) == pdPASS) { + if (!ota_session_uart_cmd_allowed(msg.msg_id)) { + ESP_LOGW(TAG, "reject %s (0x%02x) during OTA session", + message_type_name(msg.msg_id), (unsigned)msg.msg_id); + free(msg.payload); + continue; + } + bool handled = false; for (int i = 0; i < handler_count; i++) { if (handlers[i].msg_id == msg.msg_id) { diff --git a/main/cmd/cmd_handler.h b/main/cmd/cmd_handler.h index f26bc6e..ef9b22f 100644 --- a/main/cmd/cmd_handler.h +++ b/main/cmd/cmd_handler.h @@ -21,6 +21,5 @@ void init_cmdHandler(QueueHandle_t queue); void vCmdDispatcherTask(void *param); esp_err_t msg_register_handler(uint16_t id, msg_callback_t cb); -esp_err_t msg_post(uint16_t id, const uint8_t *data, size_t len); #endif diff --git a/main/cmd/cmd_ota.c b/main/cmd/cmd_ota.c index c3a236b..eae4a3d 100644 --- a/main/cmd/cmd_ota.c +++ b/main/cmd/cmd_ota.c @@ -81,8 +81,6 @@ static const ota_espnow_progress_cbs_t s_dist_progress = { static void ota_prepare_task(void *param) { uint32_t total_size = (uint32_t)(uintptr_t)param; - send_ota_status(OTA_UART_ST_PREPARING, 0); - int slot = ota_uart_prepare(total_size); if (slot < 0) { send_ota_failed(1); @@ -116,27 +114,32 @@ static void handle_ota_start(const uint8_t *data, size_t len) { const alox_OtaStartPayload *req_ptr = UART_CMD_REQ(&uart_msg, alox_UartMessage_ota_start_tag, ota_start); - if (req_ptr != NULL) { - req = *req_ptr; + if (req_ptr == NULL) { + ESP_LOGW(TAG, "OTA_START: missing ota_start payload"); + send_ota_failed(3); + return; } + req = *req_ptr; if (req.total_size == 0) { ESP_LOGW(TAG, "OTA_START: total_size required"); - send_ota_failed( 3); + send_ota_failed(3); return; } if (ota_uart_is_active()) { ESP_LOGW(TAG, "OTA_START while session active"); - send_ota_failed( 4); + send_ota_failed(4); return; } + send_ota_status(OTA_UART_ST_PREPARING, 0); + if (xTaskCreate(ota_prepare_task, "ota_prepare", OTA_PREPARE_STACK, (void *)(uintptr_t)req.total_size, OTA_PREPARE_PRIO, NULL) != pdPASS) { ESP_LOGE(TAG, "failed to create ota_prepare task"); - send_ota_failed( 5); + send_ota_failed(5); } } @@ -293,31 +296,28 @@ static void handle_ota_end(const uint8_t *data, size_t len) { } } -static void handle_ota_start_espnow(const uint8_t *data, size_t len) { - (void)data; - (void)len; - - if (ota_uart_is_active()) { - send_ota_failed( 40); - return; - } +static void ota_start_espnow_task(void *param) { + (void)param; const esp_partition_t *part = NULL; uint32_t image_size = 0; if (!ota_uart_get_staged_image(&part, &image_size)) { - send_ota_failed( 41); + send_ota_failed(41); + vTaskDelete(NULL); return; } - esp_err_t err = ota_espnow_distribute(part, image_size, &s_dist_progress); + esp_err_t err = ota_espnow_distribute(part, image_size, &s_dist_progress); if (err != ESP_OK) { - send_ota_failed( 42); + send_ota_failed(42); + vTaskDelete(NULL); return; } err = ota_uart_apply_boot(); if (err != ESP_OK) { - send_ota_failed( (uint32_t)err); + send_ota_failed((uint32_t)err); + vTaskDelete(NULL); return; } @@ -329,6 +329,30 @@ static void handle_ota_start_espnow(const uint8_t *data, size_t len) { response.payload.ota_status.error = 0; uart_cmd_send(&response, TAG); led_ring_ota_success(); + vTaskDelete(NULL); +} + +static void handle_ota_start_espnow(const uint8_t *data, size_t len) { + (void)data; + (void)len; + + if (ota_uart_is_active()) { + send_ota_failed(40); + return; + } + + const esp_partition_t *part = NULL; + uint32_t image_size = 0; + if (!ota_uart_get_staged_image(&part, &image_size)) { + send_ota_failed(41); + return; + } + + if (xTaskCreate(ota_start_espnow_task, "ota_espnow", OTA_DIST_STACK, NULL, + OTA_DIST_PRIO, NULL) != pdPASS) { + ESP_LOGE(TAG, "failed to create ota_start_espnow task"); + send_ota_failed(43); + } } void cmd_ota_register(void) { diff --git a/main/cmd/cmd_ota_slave_progress.c b/main/cmd/cmd_ota_slave_progress.c index 4968590..b1776b2 100644 --- a/main/cmd/cmd_ota_slave_progress.c +++ b/main/cmd/cmd_ota_slave_progress.c @@ -9,13 +9,29 @@ 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) { + if (len > 0) { + if (uart_cmd_decode(data, len, &uart_msg) != ESP_OK) { + ESP_LOGW(TAG, "decode failed"); + alox_UartMessage response; + uart_cmd_init_response( + &response, alox_MessageType_OTA_SLAVE_PROGRESS, + alox_UartMessage_ota_slave_progress_response_tag); + uart_cmd_send(&response, TAG); + return; + } 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; + if (req == NULL) { + ESP_LOGW(TAG, "missing ota_slave_progress_request"); + alox_UartMessage response; + uart_cmd_init_response( + &response, alox_MessageType_OTA_SLAVE_PROGRESS, + alox_UartMessage_ota_slave_progress_response_tag); + uart_cmd_send(&response, TAG); + return; } + filter = req->client_id; } alox_UartMessage response; diff --git a/main/cmd/cmd_tap_notify.c b/main/cmd/cmd_tap_notify.c index 41646ee..be0489d 100644 --- a/main/cmd/cmd_tap_notify.c +++ b/main/cmd/cmd_tap_notify.c @@ -39,12 +39,20 @@ static void handle_tap_notify(const uint8_t *data, size_t len) { alox_UartMessage uart_msg; alox_TapNotifyRequest req = alox_TapNotifyRequest_init_zero; - if (uart_cmd_decode(data, len, &uart_msg) == ESP_OK) { + if (len > 0) { + if (uart_cmd_decode(data, len, &uart_msg) != ESP_OK) { + ESP_LOGW(TAG, "decode failed"); + reply(0, false, 0, false, false, false); + return; + } const alox_TapNotifyRequest *req_ptr = UART_CMD_REQ( &uart_msg, alox_UartMessage_tap_notify_request_tag, tap_notify_request); - if (req_ptr != NULL) { - req = *req_ptr; + if (req_ptr == NULL) { + ESP_LOGW(TAG, "missing tap_notify_request"); + reply(0, false, 0, false, false, false); + return; } + req = *req_ptr; } if (req.write) { diff --git a/main/esp_now_comm.c b/main/esp_now_comm.c index 53393f8..c56f6e9 100644 --- a/main/esp_now_comm.c +++ b/main/esp_now_comm.c @@ -1,1197 +1,23 @@ -#include "bosch456.h" -#include "client_registry.h" #include "esp_now_comm.h" -#include "board_input.h" -#include "cmd_led_ring.h" -#include "led_ring.h" -#include "ota_espnow.h" -#include "pod_reboot.h" -#include "pod_settings.h" -#include "esp_now_proto.h" +#include "client_registry.h" +#include "esp_now_core.h" +#include "esp_now_master.h" +#include "esp_now_slave.h" #include "esp_err.h" -#include "esp_event.h" #include "esp_log.h" -#include "esp_mac.h" -#include "esp_netif.h" #include "esp_now.h" -#include "esp_wifi.h" -#include "freertos/FreeRTOS.h" -#include "freertos/idf_additions.h" -#include "ota_uart.h" #include #include -#ifndef POWERPOD_FW_VERSION -#define POWERPOD_FW_VERSION 1u -#endif - -#define ESPNOW_DISCOVER_INTERVAL_MS 500 -#define ESPNOW_HEARTBEAT_INTERVAL_MS 1000 -#define ESPNOW_HEARTBEAT_MISS_COUNT 3 -#define ESPNOW_CLIENT_TIMEOUT_MS \ - (ESPNOW_HEARTBEAT_INTERVAL_MS * ESPNOW_HEARTBEAT_MISS_COUNT) -#define SLAVE_MASTER_LOST_MS (ESPNOW_HEARTBEAT_INTERVAL_MS * 5) -#define ESPNOW_ACCEL_INTERVAL_MS 16 - -static const uint8_t ESPNOW_BCAST[ESP_NOW_ETH_ALEN] = {0xff, 0xff, 0xff, - 0xff, 0xff, 0xff}; - static const char *TAG = "[ESPNOW]"; -static app_config_t s_config; -static uint8_t s_wifi_channel; -static uint8_t s_own_mac[ESP_NOW_ETH_ALEN]; -static bool s_slave_joined; -static bool s_accel_stream_enabled; -static bool s_tap_notify_single; -static bool s_tap_notify_double; -static bool s_tap_notify_triple; -static uint8_t s_master_mac[ESP_NOW_ETH_ALEN]; -static uint32_t s_last_discover_ms; - -static SemaphoreHandle_t s_send_done; -static bool s_send_cb_ready; - -#define ESPNOW_BATTERY_INTERVAL_MS 30000 -#define SLAVE_BATTERY_AFTER_JOIN_MS 150 - -typedef enum { - SLAVE_TX_SLAVE_INFO = 1, - SLAVE_TX_BATTERY, -} slave_tx_op_t; - -static QueueHandle_t s_slave_tx_queue; - -static uint32_t now_ms(void) { - return (uint32_t)(xTaskGetTickCount() * portTICK_PERIOD_MS); -} - -static uint8_t network_to_channel(uint8_t network) { - if (network < 1 || network > 13) { - return 1; - } - return network; -} - -static bool mac_equal(const uint8_t *a, const uint8_t *b) { - return memcmp(a, b, ESP_NOW_ETH_ALEN) == 0; -} - -static void mac_to_str(const uint8_t *mac, char *out, size_t out_len) { - snprintf(out, out_len, "%02x:%02x:%02x:%02x:%02x:%02x", mac[0], mac[1], - mac[2], mac[3], mac[4], mac[5]); -} - -static esp_err_t ensure_peer(const uint8_t *mac) { - if (esp_now_is_peer_exist(mac)) { - return ESP_OK; - } - - esp_now_peer_info_t peer = {0}; - memcpy(peer.peer_addr, mac, ESP_NOW_ETH_ALEN); - peer.channel = s_wifi_channel; - peer.ifidx = WIFI_IF_STA; - peer.encrypt = false; - - esp_err_t err = esp_now_add_peer(&peer); - if (err != ESP_OK) { - ESP_LOGW(TAG, "add peer failed: %s", esp_err_to_name(err)); - } - return err; -} - -static esp_err_t ensure_broadcast_peer(void) { return ensure_peer(ESPNOW_BCAST); } - -static esp_err_t send_message_ex(const uint8_t *dest_mac, - const alox_EspNowMessage *msg, bool wait_done); -static void slave_send_battery_report_to_master(void); - -static void fill_presence(alox_EspNowSlavePresence *presence) { - presence->network = s_config.network; - presence->version = POWERPOD_FW_VERSION; - presence->slave_id = s_own_mac[5]; - presence->available = true; - presence->used = false; - esp_now_proto_setup_presence_encode(presence, s_own_mac); -} - -static void espnow_send_done_cb(const esp_now_send_info_t *tx_info, - esp_now_send_status_t status) { - (void)tx_info; - (void)status; - if (s_send_done != NULL) { - xSemaphoreGive(s_send_done); - } -} - -static esp_err_t send_message(const uint8_t *dest_mac, - const alox_EspNowMessage *msg) { - return send_message_ex(dest_mac, msg, false); -} - -static esp_err_t send_accel_sample(const uint8_t *dest_mac, uint32_t slave_id, - int16_t x, int16_t y, int16_t z) { - alox_EspNowMessage msg = alox_EspNowMessage_init_zero; - msg.type = alox_EspNowMessageType_ESPNOW_ACCEL_SAMPLE; - msg.which_payload = alox_EspNowMessage_accel_sample_tag; - msg.payload.accel_sample.slave_id = slave_id; - msg.payload.accel_sample.x = x; - msg.payload.accel_sample.y = y; - msg.payload.accel_sample.z = z; - return send_message(dest_mac, &msg); -} - -static esp_err_t send_tap_event(const uint8_t *dest_mac, uint32_t slave_id, - uint32_t kind) { - alox_EspNowMessage msg = alox_EspNowMessage_init_zero; - msg.type = alox_EspNowMessageType_ESPNOW_TAP_EVENT; - msg.which_payload = alox_EspNowMessage_tap_event_tag; - msg.payload.tap_event.slave_id = slave_id; - msg.payload.tap_event.kind = kind; - return send_message(dest_mac, &msg); -} - -static esp_err_t send_tap_notify(const uint8_t *dest_mac, uint32_t client_id, - bool single, bool double_tap, bool triple) { - alox_EspNowMessage msg = alox_EspNowMessage_init_zero; - msg.type = alox_EspNowMessageType_ESPNOW_SET_TAP_NOTIFY; - msg.which_payload = alox_EspNowMessage_tap_notify_tag; - msg.payload.tap_notify.client_id = client_id; - msg.payload.tap_notify.single = single; - msg.payload.tap_notify.double_tap = double_tap; - msg.payload.tap_notify.triple = triple; - return send_message(dest_mac, &msg); -} - -static esp_err_t send_message_ex(const uint8_t *dest_mac, - const alox_EspNowMessage *msg, bool wait_done) { - uint8_t buf[ESPNOW_PB_MAX_SIZE]; - size_t len = 0; - - esp_err_t err = esp_now_proto_encode(msg, buf, sizeof(buf), &len); - if (err != ESP_OK) { - ESP_LOGW(TAG, "encode failed"); - return err; - } - - if (len > ESP_NOW_MAX_DATA_LEN) { - ESP_LOGW(TAG, "encoded len %u > ESP-NOW max %u", (unsigned)len, - (unsigned)ESP_NOW_MAX_DATA_LEN); - return ESP_ERR_INVALID_SIZE; - } - - if (ensure_peer(dest_mac) != ESP_OK) { - return ESP_FAIL; - } - - if (wait_done && s_send_cb_ready && s_send_done != NULL) { - xSemaphoreTake(s_send_done, 0); - } - - err = esp_now_send(dest_mac, buf, len); - if (err != ESP_OK) { - ESP_LOGW(TAG, "send type=%u failed: %s", (unsigned)msg->type, - esp_err_to_name(err)); - return err; - } - - if (wait_done && s_send_cb_ready && s_send_done != NULL) { - if (xSemaphoreTake(s_send_done, pdMS_TO_TICKS(50)) != pdTRUE) { - ESP_LOGW(TAG, "send type=%u done timeout", (unsigned)msg->type); - } - } - return err; -} - -static esp_err_t send_accel_stream(const uint8_t *dest_mac, uint32_t client_id, - bool enable) { - alox_EspNowMessage msg = alox_EspNowMessage_init_zero; - - msg.type = alox_EspNowMessageType_ESPNOW_SET_ACCEL_STREAM; - msg.which_payload = alox_EspNowMessage_accel_stream_tag; - msg.payload.accel_stream.enable = enable; - msg.payload.accel_stream.client_id = client_id; - - return send_message(dest_mac, &msg); -} - -static esp_err_t send_accel_deadzone(const uint8_t *dest_mac, uint32_t client_id, - uint32_t deadzone) { - alox_EspNowMessage msg = alox_EspNowMessage_init_zero; - - msg.type = alox_EspNowMessageType_ESPNOW_SET_ACCEL_DEADZONE; - msg.which_payload = alox_EspNowMessage_accel_deadzone_tag; - msg.payload.accel_deadzone.deadzone = deadzone; - msg.payload.accel_deadzone.client_id = client_id; - - return send_message(dest_mac, &msg); -} - -static esp_err_t send_unicast_test(const uint8_t *dest_mac, uint32_t seq) { - alox_EspNowMessage msg = alox_EspNowMessage_init_zero; - - msg.type = alox_EspNowMessageType_ESPNOW_UNICAST_TEST; - msg.which_payload = alox_EspNowMessage_unicast_test_tag; - msg.payload.unicast_test.seq = seq; - - return send_message(dest_mac, &msg); -} - -static esp_err_t send_find_me(const uint8_t *dest_mac, uint32_t client_id) { - alox_EspNowMessage msg = alox_EspNowMessage_init_zero; - - msg.type = alox_EspNowMessageType_ESPNOW_FIND_ME; - msg.which_payload = alox_EspNowMessage_find_me_tag; - msg.payload.find_me.client_id = client_id; - - return send_message(dest_mac, &msg); -} - -static esp_err_t send_battery_report(const uint8_t *dest_mac, - const alox_EspNowBatteryReport *report) { - if (report == NULL) { - return ESP_ERR_INVALID_ARG; - } - - alox_EspNowMessage msg = alox_EspNowMessage_init_zero; - - msg.type = alox_EspNowMessageType_ESPNOW_BATTERY_REPORT; - msg.which_payload = alox_EspNowMessage_battery_report_tag; - msg.payload.battery_report = *report; - - return send_message(dest_mac, &msg); -} - -static esp_err_t send_led_ring(const uint8_t *dest_mac, uint32_t client_id, - const alox_LedRingProgressRequest *req) { - if (req == NULL) { - return ESP_ERR_INVALID_ARG; - } - - alox_EspNowMessage msg = alox_EspNowMessage_init_zero; - - msg.type = alox_EspNowMessageType_ESPNOW_LED_RING; - msg.which_payload = alox_EspNowMessage_led_ring_tag; - msg.payload.led_ring.client_id = client_id; - msg.payload.led_ring.mode = req->mode; - msg.payload.led_ring.progress = req->progress; - msg.payload.led_ring.digit = req->digit; - msg.payload.led_ring.r = req->r; - msg.payload.led_ring.g = req->g; - msg.payload.led_ring.b = req->b; - msg.payload.led_ring.intensity = req->intensity; - msg.payload.led_ring.blink_ms = req->blink_ms; - msg.payload.led_ring.blink_count = req->blink_count; - - return send_message(dest_mac, &msg); -} - -static esp_err_t send_restart(const uint8_t *dest_mac, uint32_t client_id) { - alox_EspNowMessage msg = alox_EspNowMessage_init_zero; - - msg.type = alox_EspNowMessageType_ESPNOW_RESTART; - msg.which_payload = alox_EspNowMessage_restart_tag; - msg.payload.restart.client_id = client_id; - - return send_message(dest_mac, &msg); -} - -static esp_err_t send_ota_start(const uint8_t *dest_mac, uint32_t total_size) { - alox_EspNowMessage msg = alox_EspNowMessage_init_zero; - - msg.type = alox_EspNowMessageType_ESPNOW_OTA_START; - msg.which_payload = alox_EspNowMessage_ota_start_tag; - msg.payload.ota_start.total_size = total_size; - - return send_message_ex(dest_mac, &msg, true); -} - -static esp_err_t send_ota_payload(const uint8_t *dest_mac, uint32_t seq, - const uint8_t *data, size_t len) { - if (data == NULL || len == 0 || len > OTA_UART_HOST_CHUNK_SIZE) { - return ESP_ERR_INVALID_ARG; - } - - alox_EspNowMessage msg = alox_EspNowMessage_init_zero; - - msg.type = alox_EspNowMessageType_ESPNOW_OTA_PAYLOAD; - msg.which_payload = alox_EspNowMessage_ota_payload_tag; - msg.payload.ota_payload.seq = seq; - msg.payload.ota_payload.data.size = len; - memcpy(msg.payload.ota_payload.data.bytes, data, len); - - return send_message_ex(dest_mac, &msg, true); -} - -static esp_err_t send_ota_end(const uint8_t *dest_mac) { - alox_EspNowMessage msg = alox_EspNowMessage_init_zero; - - msg.type = alox_EspNowMessageType_ESPNOW_OTA_END; - msg.which_payload = alox_EspNowMessage_ota_end_tag; - - return send_message_ex(dest_mac, &msg, true); -} - -static esp_err_t send_ota_status(const uint8_t *dest_mac, uint32_t status, - uint32_t bytes_written, uint32_t error) { - alox_EspNowMessage msg = alox_EspNowMessage_init_zero; - - msg.type = alox_EspNowMessageType_ESPNOW_OTA_STATUS; - msg.which_payload = alox_EspNowMessage_ota_status_tag; - msg.payload.ota_status.status = status; - msg.payload.ota_status.bytes_written = bytes_written; - msg.payload.ota_status.error = error; - - return send_message_ex(dest_mac, &msg, true); -} - -esp_err_t esp_now_comm_send_ota_start(const uint8_t mac[CLIENT_MAC_LEN], - uint32_t total_size) { - if (mac == NULL || !s_config.master) { - return ESP_ERR_INVALID_STATE; - } - return send_ota_start(mac, total_size); -} - -esp_err_t esp_now_comm_send_ota_payload(const uint8_t mac[CLIENT_MAC_LEN], - uint32_t seq, const uint8_t *data, - size_t len) { - if (mac == NULL || !s_config.master) { - return ESP_ERR_INVALID_STATE; - } - return send_ota_payload(mac, seq, data, len); -} - -esp_err_t esp_now_comm_send_ota_end(const uint8_t mac[CLIENT_MAC_LEN]) { - if (mac == NULL || !s_config.master) { - return ESP_ERR_INVALID_STATE; - } - return send_ota_end(mac); -} - -esp_err_t esp_now_comm_send_ota_status(const uint8_t master_mac[CLIENT_MAC_LEN], - uint32_t status, uint32_t bytes_written, - uint32_t error) { - if (master_mac == NULL || s_config.master) { - return ESP_ERR_INVALID_STATE; - } - return send_ota_status(master_mac, status, bytes_written, error); -} - -bool esp_now_comm_get_master_mac(uint8_t mac_out[CLIENT_MAC_LEN]) { - if (mac_out == NULL || s_config.master || !s_slave_joined) { - return false; - } - memcpy(mac_out, s_master_mac, CLIENT_MAC_LEN); - return true; -} - -esp_err_t esp_now_comm_send_restart(const uint8_t mac[CLIENT_MAC_LEN], - uint32_t client_id) { - if (mac == NULL || !s_config.master) { - return ESP_ERR_INVALID_STATE; - } - - char mac_str[18]; - mac_to_str(mac, mac_str, sizeof(mac_str)); - esp_err_t err = send_restart(mac, client_id); - if (err == ESP_OK) { - ESP_LOGI(TAG, "unicast RESTART to %s client_id=%lu", mac_str, - (unsigned long)client_id); - } else { - ESP_LOGW(TAG, "unicast RESTART to %s failed: %s", mac_str, - esp_err_to_name(err)); - } - return err; -} - -esp_err_t esp_now_comm_send_find_me(const uint8_t mac[CLIENT_MAC_LEN], - uint32_t client_id) { - if (mac == NULL || !s_config.master) { - return ESP_ERR_INVALID_STATE; - } - - char mac_str[18]; - mac_to_str(mac, mac_str, sizeof(mac_str)); - esp_err_t err = send_find_me(mac, client_id); - if (err == ESP_OK) { - ESP_LOGI(TAG, "unicast FIND_ME to %s client_id=%lu", mac_str, - (unsigned long)client_id); - } else { - ESP_LOGW(TAG, "unicast FIND_ME to %s failed: %s", mac_str, - esp_err_to_name(err)); - } - return err; -} - -esp_err_t esp_now_comm_send_led_ring(const uint8_t mac[CLIENT_MAC_LEN], - uint32_t client_id, - const alox_LedRingProgressRequest *req) { - if (mac == NULL || !s_config.master || req == NULL) { - return ESP_ERR_INVALID_STATE; - } - - char mac_str[18]; - mac_to_str(mac, mac_str, sizeof(mac_str)); - esp_err_t err = send_led_ring(mac, client_id, req); - if (err == ESP_OK) { - ESP_LOGI(TAG, "unicast LED_RING mode %lu to %s client_id=%lu", - (unsigned long)req->mode, mac_str, (unsigned long)client_id); - } else { - ESP_LOGW(TAG, "unicast LED_RING to %s failed: %s", mac_str, - esp_err_to_name(err)); - } - return err; -} - -esp_err_t esp_now_comm_send_unicast_test(const uint8_t mac[CLIENT_MAC_LEN], - uint32_t seq) { - if (mac == NULL || !s_config.master) { - return ESP_ERR_INVALID_STATE; - } - - char mac_str[18]; - mac_to_str(mac, mac_str, sizeof(mac_str)); - esp_err_t err = send_unicast_test(mac, seq); - if (err == ESP_OK) { - ESP_LOGI(TAG, "unicast TEST to %s seq=%lu", mac_str, (unsigned long)seq); - } else { - ESP_LOGW(TAG, "unicast TEST to %s failed: %s", mac_str, esp_err_to_name(err)); - } - return err; -} - -esp_err_t esp_now_comm_send_accel_stream(const uint8_t mac[CLIENT_MAC_LEN], - uint32_t client_id, bool enable) { - if (mac == NULL || !s_config.master) { - return ESP_ERR_INVALID_STATE; - } - - char mac_str[18]; - mac_to_str(mac, mac_str, sizeof(mac_str)); - esp_err_t err = send_accel_stream(mac, client_id, enable); - if (err == ESP_OK) { - ESP_LOGI(TAG, "unicast SET_ACCEL_STREAM to %s: %s client_id=%lu", mac_str, - enable ? "on" : "off", (unsigned long)client_id); - } else { - ESP_LOGW(TAG, "unicast SET_ACCEL_STREAM to %s failed: %s", mac_str, - esp_err_to_name(err)); - } - return err; -} - -esp_err_t esp_now_comm_send_tap_notify(const uint8_t mac[CLIENT_MAC_LEN], - uint32_t client_id, bool single, - bool double_tap, bool triple) { - if (mac == NULL || !s_config.master) { - return ESP_ERR_INVALID_STATE; - } - - char mac_str[18]; - mac_to_str(mac, mac_str, sizeof(mac_str)); - esp_err_t err = - send_tap_notify(mac, client_id, single, double_tap, triple); - if (err == ESP_OK) { - ESP_LOGI(TAG, - "unicast SET_TAP_NOTIFY to %s: single=%d double=%d triple=%d " - "client_id=%lu", - mac_str, single, double_tap, triple, (unsigned long)client_id); - } else { - ESP_LOGW(TAG, "unicast SET_TAP_NOTIFY to %s failed: %s", mac_str, - esp_err_to_name(err)); - } - return err; -} - -esp_err_t esp_now_comm_send_accel_deadzone(const uint8_t mac[CLIENT_MAC_LEN], - uint32_t client_id, uint32_t deadzone) { - if (mac == NULL || !s_config.master) { - return ESP_ERR_INVALID_STATE; - } - - char mac_str[18]; - mac_to_str(mac, mac_str, sizeof(mac_str)); - esp_err_t err = send_accel_deadzone(mac, client_id, deadzone); - if (err == ESP_OK) { - ESP_LOGI(TAG, "unicast SET_ACCEL_DEADZONE to %s: deadzone=%lu client_id=%lu", - mac_str, (unsigned long)deadzone, (unsigned long)client_id); - } else { - ESP_LOGW(TAG, "unicast SET_ACCEL_DEADZONE to %s failed: %s", mac_str, - esp_err_to_name(err)); - } - return err; -} - -static void send_presence(const uint8_t *dest_mac, - alox_EspNowMessageType type) { - alox_EspNowMessage msg = alox_EspNowMessage_init_zero; - alox_EspNowSlavePresence *presence = NULL; - - msg.type = type; - if (type == alox_EspNowMessageType_ESPNOW_SLAVE_INFO) { - msg.which_payload = alox_EspNowMessage_slave_info_tag; - presence = &msg.payload.slave_info; - } else { - msg.which_payload = alox_EspNowMessage_heartbeat_tag; - presence = &msg.payload.heartbeat; - } - - fill_presence(presence); - send_message(dest_mac, &msg); -} - -static void slave_reset_join(void) { - s_slave_joined = false; - s_accel_stream_enabled = false; - memset(s_master_mac, 0, sizeof(s_master_mac)); - s_last_discover_ms = 0; - if (s_slave_tx_queue != NULL) { - xQueueReset(s_slave_tx_queue); - } -} - -static void slave_queue_tx(slave_tx_op_t op) { - if (s_slave_tx_queue == NULL) { - return; - } - if (xQueueSend(s_slave_tx_queue, &op, 0) != pdTRUE) { - ESP_LOGW(TAG, "slave tx queue full (op=%d)", (int)op); - } -} - -static void slave_tx_task(void *param) { - (void)param; - slave_tx_op_t op; - - ESP_LOGI(TAG, "slave tx task ready"); - - while (1) { - if (xQueueReceive(s_slave_tx_queue, &op, portMAX_DELAY) != pdTRUE) { - continue; - } - - if (!s_slave_joined) { - continue; - } - - switch (op) { - case SLAVE_TX_SLAVE_INFO: - send_presence(s_master_mac, alox_EspNowMessageType_ESPNOW_SLAVE_INFO); - break; - case SLAVE_TX_BATTERY: - vTaskDelay(pdMS_TO_TICKS(SLAVE_BATTERY_AFTER_JOIN_MS)); - slave_send_battery_report_to_master(); - break; - default: - break; - } - } -} - -static void handle_slave_unicast_test(const uint8_t *master_mac, - const alox_EspNowUnicastTest *test) { - char mac_str[18]; - mac_to_str(master_mac, mac_str, sizeof(mac_str)); - - ESP_LOGI(TAG, "UNICAST TEST OK from master %s seq=%lu (joined=%d)", - mac_str, (unsigned long)test->seq, (int)s_slave_joined); -} - -static void handle_slave_restart(const uint8_t *master_mac, - const alox_EspNowRestart *req) { - uint32_t my_id = s_own_mac[5]; - - if (req->client_id != 0 && req->client_id != my_id) { - return; - } - - if (s_slave_joined && !mac_equal(master_mac, s_master_mac)) { - return; - } - - char mac_str[18]; - mac_to_str(master_mac, mac_str, sizeof(mac_str)); - ESP_LOGI(TAG, "RESTART from master %s (id=%lu)", mac_str, (unsigned long)my_id); - pod_schedule_restart(); -} - -static void slave_send_battery_report_to_master(void) { - if (!s_slave_joined) { - return; - } - - board_lipo_reading_t reading; - board_input_read_lipo(&reading); - - alox_EspNowBatteryReport report = alox_EspNowBatteryReport_init_zero; - report.client_id = s_own_mac[5]; - report.lipo1_valid = reading.lipo1_valid; - report.lipo2_valid = reading.lipo2_valid; - report.lipo1_mv = reading.lipo1_mv; - report.lipo2_mv = reading.lipo2_mv; - - esp_err_t err = send_battery_report(s_master_mac, &report); - if (err != ESP_OK) { - ESP_LOGW(TAG, "battery report send failed id=%lu: %s", - (unsigned long)report.client_id, esp_err_to_name(err)); - } else { - ESP_LOGI(TAG, "battery report sent id=%lu L1=%s %lu mV L2=%s %lu mV", - (unsigned long)report.client_id, - report.lipo1_valid ? "ok" : "n/a", - (unsigned long)report.lipo1_mv, - report.lipo2_valid ? "ok" : "n/a", - (unsigned long)report.lipo2_mv); - } -} - -static void handle_slave_battery_query(const uint8_t *master_mac, - const alox_EspNowBatteryQuery *query) { - uint32_t my_id = s_own_mac[5]; - - if (query->client_id != 0 && query->client_id != my_id) { - return; - } - - if (s_slave_joined && !mac_equal(master_mac, s_master_mac)) { - return; - } - - slave_send_battery_report_to_master(); -} - -static void handle_master_battery_report(const uint8_t *mac, - const alox_EspNowBatteryReport *report) { - if (report == NULL || mac == NULL) { - return; - } - - esp_err_t err = client_registry_update_battery( - mac, report->client_id, report->lipo1_valid, report->lipo1_mv, - report->lipo2_valid, report->lipo2_mv); - if (err == ESP_ERR_NOT_FOUND) { - ESP_LOGW(TAG, "battery report from unregistered slave id=%lu", - (unsigned long)report->client_id); - return; - } - if (err != ESP_OK) { - ESP_LOGW(TAG, "battery report id=%lu rejected: %s", - (unsigned long)report->client_id, esp_err_to_name(err)); - return; - } - - ESP_LOGI(TAG, "battery cached id=%lu L1=%s %lu mV L2=%s %lu mV", - (unsigned long)report->client_id, - report->lipo1_valid ? "ok" : "n/a", - (unsigned long)report->lipo1_mv, report->lipo2_valid ? "ok" : "n/a", - (unsigned long)report->lipo2_mv); -} - -static void handle_slave_led_ring(const uint8_t *master_mac, - const alox_EspNowLedRing *msg) { - uint32_t my_id = s_own_mac[5]; - - if (msg->client_id != 0 && msg->client_id != my_id) { - return; - } - - if (s_slave_joined && !mac_equal(master_mac, s_master_mac)) { - return; - } - - alox_LedRingProgressRequest req = alox_LedRingProgressRequest_init_zero; - req.mode = msg->mode; - req.progress = msg->progress; - req.digit = msg->digit; - req.r = msg->r; - req.g = msg->g; - req.b = msg->b; - req.intensity = msg->intensity; - req.blink_ms = msg->blink_ms; - req.blink_count = msg->blink_count; - - char mac_str[18]; - mac_to_str(master_mac, mac_str, sizeof(mac_str)); - ESP_LOGI(TAG, "LED_RING mode %lu from master %s (id=%lu)", - (unsigned long)req.mode, mac_str, (unsigned long)my_id); - cmd_led_ring_apply(&req); -} - -static void handle_slave_find_me(const uint8_t *master_mac, - const alox_EspNowFindMe *req) { - uint32_t my_id = s_own_mac[5]; - - if (req->client_id != 0 && req->client_id != my_id) { - return; - } - - if (s_slave_joined && !mac_equal(master_mac, s_master_mac)) { - return; - } - - char mac_str[18]; - mac_to_str(master_mac, mac_str, sizeof(mac_str)); - ESP_LOGI(TAG, "FIND_ME from master %s (id=%lu)", mac_str, (unsigned long)my_id); - led_ring_find_me(); -} - -static void handle_slave_accel_stream(const uint8_t *master_mac, - const alox_EspNowAccelStream *cfg) { - uint32_t my_id = s_own_mac[5]; - - if (cfg->client_id != 0 && cfg->client_id != my_id) { - return; - } - - if (s_slave_joined && !mac_equal(master_mac, s_master_mac)) { - return; - } - - s_accel_stream_enabled = cfg->enable; - char mac_str[18]; - mac_to_str(master_mac, mac_str, sizeof(mac_str)); - ESP_LOGI(TAG, "accel stream %s from master %s (id=%lu)", - cfg->enable ? "on" : "off", mac_str, (unsigned long)my_id); -} - -static void handle_slave_tap_notify(const uint8_t *master_mac, - const alox_EspNowTapNotify *cfg) { - uint32_t my_id = s_own_mac[5]; - - if (cfg->client_id != 0 && cfg->client_id != my_id) { - return; - } - - if (s_slave_joined && !mac_equal(master_mac, s_master_mac)) { - return; - } - - s_tap_notify_single = cfg->single; - s_tap_notify_double = cfg->double_tap; - s_tap_notify_triple = cfg->triple; - - char mac_str[18]; - mac_to_str(master_mac, mac_str, sizeof(mac_str)); - ESP_LOGI(TAG, - "tap notify single=%d double=%d triple=%d from master %s (id=%lu)", - cfg->single, cfg->double_tap, cfg->triple, mac_str, - (unsigned long)my_id); -} - -static void handle_slave_accel_deadzone(const uint8_t *master_mac, - const alox_EspNowAccelDeadzone *cfg) { - uint32_t my_id = s_own_mac[5]; - - if (cfg->client_id != 0 && cfg->client_id != my_id) { - return; - } - - if (s_slave_joined && !mac_equal(master_mac, s_master_mac)) { - return; - } - - char mac_str[18]; - mac_to_str(master_mac, mac_str, sizeof(mac_str)); - - ESP_LOGI(TAG, - "accel deadzone from master %s: %lu LSB id=%lu (sensor %s)", - mac_str, (unsigned long)cfg->deadzone, (unsigned long)my_id, - bma456_is_ready() ? "ok" : "not installed"); - - bma456_set_accel_deadzone(cfg->deadzone); - if (pod_settings_save_accel_deadzone(cfg->deadzone) != ESP_OK) { - ESP_LOGW(TAG, "slave deadzone %lu applied but not saved to NVS", - (unsigned long)cfg->deadzone); - } -} - -static void handle_master_accel_sample(const uint8_t mac[CLIENT_MAC_LEN], - const alox_EspNowAccelSample *sample) { - if (sample == NULL) { - return; - } - - esp_err_t err = client_registry_update_accel( - mac, sample->slave_id, (int16_t)sample->x, (int16_t)sample->y, - (int16_t)sample->z); - if (err == ESP_ERR_NOT_FOUND) { - return; - } - if (err != ESP_OK) { - ESP_LOGW(TAG, "accel sample id mismatch from %02x:…:%02x", mac[0], mac[5]); - } -} - -static void handle_master_tap_event(const uint8_t mac[CLIENT_MAC_LEN], - const alox_EspNowTapEvent *event) { - if (event == NULL) { - return; - } - - esp_err_t err = - client_registry_update_tap(mac, event->slave_id, event->kind); - if (err == ESP_ERR_NOT_FOUND) { - return; - } - if (err != ESP_OK) { - ESP_LOGW(TAG, "tap event id=%lu kind=%lu rejected from %02x:…:%02x", - (unsigned long)event->slave_id, (unsigned long)event->kind, mac[0], - mac[5]); - } -} - -static void handle_client_presence(const alox_EspNowSlavePresence *presence, - const uint8_t mac[CLIENT_MAC_LEN]) { - if (presence->network != s_config.network) { - return; - } - - ensure_peer(mac); - - bool is_new = false; - bool reactivated = false; - esp_err_t err = client_registry_heartbeat( - mac, presence->slave_id, presence->version, presence->used, &is_new, - &reactivated); - if (err != ESP_OK) { - ESP_LOGW(TAG, "client registry full"); - return; - } - - char mac_str[18]; - mac_to_str(mac, mac_str, sizeof(mac_str)); - if (is_new) { - ESP_LOGI(TAG, "client registered id=%lu mac=%s ver=%lu", - (unsigned long)presence->slave_id, mac_str, - (unsigned long)presence->version); - } else if (reactivated) { - ESP_LOGI(TAG, "client reconnected id=%lu mac=%s", - (unsigned long)presence->slave_id, mac_str); - } -} - -static void handle_discover(const uint8_t *sender_mac, - const alox_EspNowDiscover *discover) { - if (discover->network != s_config.network) { - return; - } - - uint32_t now = now_ms(); - - if (s_slave_joined) { - if (!mac_equal(sender_mac, s_master_mac)) { - return; - } - if ((now - s_last_discover_ms) <= SLAVE_MASTER_LOST_MS) { - s_last_discover_ms = now; - return; - } - ESP_LOGW(TAG, "master lost, rejoining"); - slave_reset_join(); - } - - memcpy(s_master_mac, sender_mac, ESP_NOW_ETH_ALEN); - s_slave_joined = true; - s_last_discover_ms = now; - ensure_peer(sender_mac); - - char mac_str[18]; - mac_to_str(sender_mac, mac_str, sizeof(mac_str)); - ESP_LOGI(TAG, "joined network %u, master %s", (unsigned)discover->network, - mac_str); - - /* Do not esp_now_send from recv callback — defer to slave_tx_task. */ - slave_queue_tx(SLAVE_TX_SLAVE_INFO); - slave_queue_tx(SLAVE_TX_BATTERY); -} - -static void slave_check_master_timeout(void) { - if (!s_slave_joined) { - return; - } - - uint32_t now = now_ms(); - if (s_last_discover_ms == 0) { - return; - } - - if ((now - s_last_discover_ms) > SLAVE_MASTER_LOST_MS) { - ESP_LOGW(TAG, "no master discover for %u ms, reconnecting", - (unsigned)(now - s_last_discover_ms)); - slave_reset_join(); - } -} - -static void slave_accel_stream_task(void *param) { - (void)param; - - ESP_LOGI(TAG, "slave accel stream task (interval %u ms)", - (unsigned)ESPNOW_ACCEL_INTERVAL_MS); - - while (1) { - vTaskDelay(pdMS_TO_TICKS(ESPNOW_ACCEL_INTERVAL_MS)); - - if (!s_slave_joined || !s_accel_stream_enabled || !bma456_is_ready()) { - continue; - } - - int16_t x = 0; - int16_t y = 0; - int16_t z = 0; - if (bma456_read_accel(&x, &y, &z) != ESP_OK) { - continue; - } - - (void)send_accel_sample(s_master_mac, s_own_mac[5], x, y, z); - } -} - -static void on_bma456_tap(bma456_tap_kind_t kind, void *ctx) { - (void)ctx; - - if (!s_slave_joined) { - return; - } - - bool enabled = false; - switch (kind) { - case BMA456_TAP_SINGLE: - enabled = s_tap_notify_single; - break; - case BMA456_TAP_DOUBLE: - enabled = s_tap_notify_double; - break; - case BMA456_TAP_TRIPLE: - enabled = s_tap_notify_triple; - break; - default: - return; - } - if (!enabled) { - return; - } - - (void)send_tap_event(s_master_mac, s_own_mac[5], (uint32_t)kind); -} - -static void slave_heartbeat_task(void *param) { - (void)param; - uint32_t last_battery_ms = 0; - - ESP_LOGI(TAG, "slave heartbeat task (interval %u ms)", - (unsigned)ESPNOW_HEARTBEAT_INTERVAL_MS); - - while (1) { - vTaskDelay(pdMS_TO_TICKS(ESPNOW_HEARTBEAT_INTERVAL_MS)); - - slave_check_master_timeout(); - - if (!s_slave_joined) { - last_battery_ms = 0; - continue; - } - - send_presence(s_master_mac, alox_EspNowMessageType_ESPNOW_HEARTBEAT); - - uint32_t now = now_ms(); - if (last_battery_ms == 0 || - (now - last_battery_ms) >= ESPNOW_BATTERY_INTERVAL_MS) { - slave_send_battery_report_to_master(); - last_battery_ms = now; - } - } -} - -static void master_monitor_task(void *param) { - (void)param; - uint32_t last_local_battery_ms = 0; - - ESP_LOGI(TAG, "master monitor task (timeout %u ms)", - (unsigned)ESPNOW_CLIENT_TIMEOUT_MS); - - board_lipo_reading_t reading; - board_input_read_lipo(&reading); - client_registry_set_master_battery(&reading); - last_local_battery_ms = now_ms(); - - while (1) { - vTaskDelay(pdMS_TO_TICKS(ESPNOW_HEARTBEAT_INTERVAL_MS)); - client_registry_check_timeouts(ESPNOW_CLIENT_TIMEOUT_MS); - - uint32_t t = now_ms(); - if (t - last_local_battery_ms >= ESPNOW_BATTERY_INTERVAL_MS) { - board_input_read_lipo(&reading); - client_registry_set_master_battery(&reading); - last_local_battery_ms = t; - } - } -} - static void espnow_recv_cb(const esp_now_recv_info_t *info, const uint8_t *data, int len) { - if (info == NULL || data == NULL || len <= 0) { - return; + if (esp_now_core_is_master()) { + esp_now_master_on_recv(info, data, len); + } else { + esp_now_slave_on_recv(info, data, len); } - - if (!s_config.master) { - alox_EspNowMessage msg = alox_EspNowMessage_init_zero; - - if (esp_now_proto_decode(data, (size_t)len, &msg) != ESP_OK) { - ESP_LOGW(TAG, "slave: ESP-NOW decode failed (%d bytes)", len); - return; - } - - if (s_slave_joined && mac_equal(info->src_addr, s_master_mac)) { - ensure_peer(info->src_addr); - } - - switch (msg.which_payload) { - case alox_EspNowMessage_discover_tag: - handle_discover(info->src_addr, &msg.payload.discover); - break; - case alox_EspNowMessage_unicast_test_tag: - handle_slave_unicast_test(info->src_addr, &msg.payload.unicast_test); - break; - case alox_EspNowMessage_accel_deadzone_tag: - handle_slave_accel_deadzone(info->src_addr, &msg.payload.accel_deadzone); - break; - case alox_EspNowMessage_accel_stream_tag: - if (!s_slave_joined || !mac_equal(info->src_addr, s_master_mac)) { - break; - } - handle_slave_accel_stream(info->src_addr, &msg.payload.accel_stream); - break; - case alox_EspNowMessage_tap_notify_tag: - if (!s_slave_joined || !mac_equal(info->src_addr, s_master_mac)) { - break; - } - handle_slave_tap_notify(info->src_addr, &msg.payload.tap_notify); - break; - case alox_EspNowMessage_battery_query_tag: - if (!s_slave_joined || !mac_equal(info->src_addr, s_master_mac)) { - break; - } - handle_slave_battery_query(info->src_addr, &msg.payload.battery_query); - break; - case alox_EspNowMessage_led_ring_tag: - if (!s_slave_joined || !mac_equal(info->src_addr, s_master_mac)) { - break; - } - handle_slave_led_ring(info->src_addr, &msg.payload.led_ring); - break; - case alox_EspNowMessage_find_me_tag: - if (!s_slave_joined || !mac_equal(info->src_addr, s_master_mac)) { - break; - } - handle_slave_find_me(info->src_addr, &msg.payload.find_me); - break; - case alox_EspNowMessage_restart_tag: - if (!s_slave_joined || !mac_equal(info->src_addr, s_master_mac)) { - break; - } - handle_slave_restart(info->src_addr, &msg.payload.restart); - break; - case alox_EspNowMessage_ota_start_tag: - case alox_EspNowMessage_ota_payload_tag: - case alox_EspNowMessage_ota_end_tag: - if (!s_slave_joined || !mac_equal(info->src_addr, s_master_mac)) { - break; - } - if (msg.which_payload == alox_EspNowMessage_ota_start_tag) { - ota_espnow_slave_on_start(info->src_addr, &msg.payload.ota_start); - } else if (msg.which_payload == alox_EspNowMessage_ota_payload_tag) { - ota_espnow_slave_on_payload(info->src_addr, &msg.payload.ota_payload); - } else { - ota_espnow_slave_on_end(info->src_addr); - } - break; - default: - ESP_LOGW(TAG, "slave: unhandled ESP-NOW which=%u type=%u", msg.which_payload, - (unsigned)msg.type); - break; - } - return; - } - - alox_EspNowMessage msg = alox_EspNowMessage_init_zero; - - if (esp_now_proto_decode(data, (size_t)len, &msg) != ESP_OK) { - ESP_LOGW(TAG, "master: ESP-NOW decode failed (%d bytes)", len); - return; - } - - if (msg.which_payload == alox_EspNowMessage_ota_status_tag) { - ensure_peer(info->src_addr); - ota_espnow_master_on_status(info->src_addr, &msg.payload.ota_status); - return; - } - - if (msg.which_payload == alox_EspNowMessage_accel_sample_tag) { - ensure_peer(info->src_addr); - handle_master_accel_sample(info->src_addr, &msg.payload.accel_sample); - return; - } - - if (msg.which_payload == alox_EspNowMessage_tap_event_tag) { - ensure_peer(info->src_addr); - handle_master_tap_event(info->src_addr, &msg.payload.tap_event); - return; - } - - if (msg.which_payload == alox_EspNowMessage_battery_report_tag) { - ensure_peer(info->src_addr); - handle_master_battery_report(info->src_addr, &msg.payload.battery_report); - return; - } - - if (msg.type == alox_EspNowMessageType_ESPNOW_BATTERY_REPORT && - msg.which_payload != alox_EspNowMessage_battery_report_tag) { - ESP_LOGW(TAG, "master: BATTERY_REPORT type but which=%u", msg.which_payload); - } - - const alox_EspNowSlavePresence *presence = esp_now_proto_get_presence(&msg); - if (presence != NULL) { - /* Registry key is the ESP-NOW sender MAC, not the optional protobuf mac field. */ - ensure_peer(info->src_addr); - handle_client_presence(presence, info->src_addr); - } -} - -static void master_discover_task(void *param) { - (void)param; - - alox_EspNowMessage msg = alox_EspNowMessage_init_zero; - msg.type = alox_EspNowMessageType_ESPNOW_DISCOVER; - msg.which_payload = alox_EspNowMessage_discover_tag; - msg.payload.discover.network = s_config.network; - - ESP_LOGI(TAG, "master discover task on network %u ch %u", - (unsigned)s_config.network, (unsigned)s_wifi_channel); - - while (1) { - send_message(ESPNOW_BCAST, &msg); - vTaskDelay(pdMS_TO_TICKS(ESPNOW_DISCOVER_INTERVAL_MS)); - } -} - -static esp_err_t init_wifi_stack(uint8_t channel) { - ESP_ERROR_CHECK(esp_netif_init()); - ESP_ERROR_CHECK(esp_event_loop_create_default()); - - wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT(); - ESP_ERROR_CHECK(esp_wifi_init(&cfg)); - - wifi_config_t wifi_config = {0}; - wifi_config.sta.channel = channel; - wifi_config.sta.scan_method = WIFI_ALL_CHANNEL_SCAN; - wifi_config.sta.sort_method = WIFI_CONNECT_AP_BY_SIGNAL; - - ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA)); - ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &wifi_config)); - ESP_ERROR_CHECK(esp_wifi_start()); - ESP_ERROR_CHECK(esp_wifi_set_ps(WIFI_PS_NONE)); - ESP_ERROR_CHECK(esp_wifi_set_channel(channel, WIFI_SECOND_CHAN_NONE)); - - return ESP_OK; } esp_err_t esp_now_comm_init(const app_config_t *config) { @@ -1199,72 +25,27 @@ esp_err_t esp_now_comm_init(const app_config_t *config) { return ESP_ERR_INVALID_ARG; } - memset(&s_config, 0, sizeof(s_config)); - memcpy(&s_config, config, sizeof(s_config)); + esp_now_core_store_config(config); client_registry_init(); - slave_reset_join(); - s_wifi_channel = network_to_channel(config->network); - ESP_ERROR_CHECK(esp_read_mac(s_own_mac, ESP_MAC_WIFI_STA)); - - char mac_str[18]; - mac_to_str(s_own_mac, mac_str, sizeof(mac_str)); - ESP_LOGI(TAG, "role=%s network=%u channel=%u mac=%s", - config->master ? "master" : "slave", (unsigned)config->network, - (unsigned)s_wifi_channel, mac_str); - - esp_err_t err = init_wifi_stack(s_wifi_channel); + esp_err_t err = esp_now_core_init_radio(esp_now_core_wifi_channel()); if (err != ESP_OK) { ESP_LOGE(TAG, "wifi init failed: %s", esp_err_to_name(err)); return err; } + char mac_str[18]; + esp_now_core_mac_to_str(esp_now_core_own_mac(), mac_str, sizeof(mac_str)); + ESP_LOGI(TAG, "role=%s network=%u channel=%u mac=%s", + config->master ? "master" : "slave", (unsigned)config->network, + (unsigned)esp_now_core_wifi_channel(), mac_str); + ESP_ERROR_CHECK(esp_now_init()); ESP_ERROR_CHECK(esp_now_register_recv_cb(espnow_recv_cb)); - - s_send_done = xSemaphoreCreateBinary(); - if (s_send_done != NULL && - esp_now_register_send_cb(espnow_send_done_cb) == ESP_OK) { - s_send_cb_ready = true; - } else { - ESP_LOGW(TAG, "ESP-NOW send-done callback unavailable (OTA may drop packets)"); - } + esp_now_core_init_send_done(); if (config->master) { - ESP_ERROR_CHECK(ensure_broadcast_peer()); - if (xTaskCreate(master_discover_task, "espnow_disc", 4096, NULL, 4, - NULL) != pdPASS) { - ESP_LOGE(TAG, "failed to create discover task"); - return ESP_FAIL; - } - if (xTaskCreate(master_monitor_task, "espnow_mon", 4096, NULL, 4, NULL) != - pdPASS) { - ESP_LOGE(TAG, "failed to create monitor task"); - return ESP_FAIL; - } - } else { - s_slave_tx_queue = xQueueCreate(4, sizeof(slave_tx_op_t)); - if (s_slave_tx_queue == NULL) { - ESP_LOGE(TAG, "failed to create slave tx queue"); - return ESP_ERR_NO_MEM; - } - if (xTaskCreate(slave_tx_task, "espnow_stx", 4096, NULL, 5, NULL) != - pdPASS) { - ESP_LOGE(TAG, "failed to create slave tx task"); - return ESP_FAIL; - } - if (xTaskCreate(slave_heartbeat_task, "espnow_hb", 4096, NULL, 4, NULL) != - pdPASS) { - ESP_LOGE(TAG, "failed to create heartbeat task"); - return ESP_FAIL; - } - if (xTaskCreate(slave_accel_stream_task, "espnow_accel", 4096, NULL, 5, - NULL) != pdPASS) { - ESP_LOGE(TAG, "failed to create accel stream task"); - return ESP_FAIL; - } - bma456_set_tap_handler(on_bma456_tap, NULL); + return esp_now_master_start(); } - - return ESP_OK; + return esp_now_slave_start(); } diff --git a/main/esp_now_core.c b/main/esp_now_core.c new file mode 100644 index 0000000..744d279 --- /dev/null +++ b/main/esp_now_core.c @@ -0,0 +1,172 @@ +#include "esp_now_core.h" +#include "esp_now_proto.h" +#include "esp_err.h" +#include "esp_event.h" +#include "esp_log.h" +#include "esp_mac.h" +#include "esp_netif.h" +#include "esp_now.h" +#include "esp_wifi.h" +#include "freertos/FreeRTOS.h" +#include "freertos/idf_additions.h" +#include +#include + +static const char *TAG = "[ESPNOW_CORE]"; + +static const uint8_t ESPNOW_BCAST[ESP_NOW_ETH_ALEN] = {0xff, 0xff, 0xff, + 0xff, 0xff, 0xff}; + +static app_config_t s_config; +static uint8_t s_wifi_channel; +static uint8_t s_own_mac[ESP_NOW_ETH_ALEN]; +static SemaphoreHandle_t s_send_done; +static bool s_send_cb_ready; + +static uint8_t network_to_channel(uint8_t network) { + if (network < 1 || network > 13) { + return 1; + } + return network; +} + +static void espnow_send_done_cb(const esp_now_send_info_t *tx_info, + esp_now_send_status_t status) { + (void)tx_info; + (void)status; + if (s_send_done != NULL) { + xSemaphoreGive(s_send_done); + } +} + +void esp_now_core_store_config(const app_config_t *config) { + if (config == NULL) { + return; + } + memset(&s_config, 0, sizeof(s_config)); + memcpy(&s_config, config, sizeof(s_config)); + s_wifi_channel = network_to_channel(config->network); +} + +const app_config_t *esp_now_core_get_config(void) { return &s_config; } + +bool esp_now_core_is_master(void) { return s_config.master; } + +uint8_t esp_now_core_network(void) { return s_config.network; } + +uint8_t esp_now_core_wifi_channel(void) { return s_wifi_channel; } + +const uint8_t *esp_now_core_own_mac(void) { return s_own_mac; } + +uint32_t esp_now_core_now_ms(void) { + return (uint32_t)(xTaskGetTickCount() * portTICK_PERIOD_MS); +} + +bool esp_now_core_mac_equal(const uint8_t *a, const uint8_t *b) { + return memcmp(a, b, ESP_NOW_ETH_ALEN) == 0; +} + +void esp_now_core_mac_to_str(const uint8_t *mac, char *out, size_t out_len) { + snprintf(out, out_len, "%02x:%02x:%02x:%02x:%02x:%02x", mac[0], mac[1], + mac[2], mac[3], mac[4], mac[5]); +} + +esp_err_t esp_now_core_ensure_peer(const uint8_t *mac) { + if (esp_now_is_peer_exist(mac)) { + return ESP_OK; + } + + esp_now_peer_info_t peer = {0}; + memcpy(peer.peer_addr, mac, ESP_NOW_ETH_ALEN); + peer.channel = s_wifi_channel; + peer.ifidx = WIFI_IF_STA; + peer.encrypt = false; + + esp_err_t err = esp_now_add_peer(&peer); + if (err != ESP_OK) { + ESP_LOGW(TAG, "add peer failed: %s", esp_err_to_name(err)); + } + return err; +} + +esp_err_t esp_now_core_ensure_broadcast_peer(void) { + return esp_now_core_ensure_peer(ESPNOW_BCAST); +} + +esp_err_t esp_now_core_send_wait(const uint8_t *dest_mac, + const alox_EspNowMessage *msg) { + uint8_t buf[ESPNOW_PB_MAX_SIZE]; + size_t len = 0; + + esp_err_t err = esp_now_proto_encode(msg, buf, sizeof(buf), &len); + if (err != ESP_OK) { + ESP_LOGW(TAG, "encode failed"); + return err; + } + + if (len > ESP_NOW_MAX_DATA_LEN) { + ESP_LOGW(TAG, "encoded len %u > ESP-NOW max %u", (unsigned)len, + (unsigned)ESP_NOW_MAX_DATA_LEN); + return ESP_ERR_INVALID_SIZE; + } + + if (esp_now_core_ensure_peer(dest_mac) != ESP_OK) { + return ESP_FAIL; + } + + if (s_send_cb_ready && s_send_done != NULL) { + xSemaphoreTake(s_send_done, 0); + } + + err = esp_now_send(dest_mac, buf, len); + if (err != ESP_OK) { + ESP_LOGW(TAG, "send type=%u failed: %s", (unsigned)msg->type, + esp_err_to_name(err)); + return err; + } + + if (s_send_cb_ready && s_send_done != NULL) { + if (xSemaphoreTake(s_send_done, pdMS_TO_TICKS(50)) != pdTRUE) { + ESP_LOGW(TAG, "send type=%u done timeout", (unsigned)msg->type); + } + } + return err; +} + +esp_err_t esp_now_core_send(const uint8_t *dest_mac, + const alox_EspNowMessage *msg) { + return esp_now_core_send_wait(dest_mac, msg); +} + +esp_err_t esp_now_core_init_radio(uint8_t channel) { + ESP_ERROR_CHECK(esp_netif_init()); + ESP_ERROR_CHECK(esp_event_loop_create_default()); + + wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT(); + ESP_ERROR_CHECK(esp_wifi_init(&cfg)); + + wifi_config_t wifi_config = {0}; + wifi_config.sta.channel = channel; + wifi_config.sta.scan_method = WIFI_ALL_CHANNEL_SCAN; + wifi_config.sta.sort_method = WIFI_CONNECT_AP_BY_SIGNAL; + + ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA)); + ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &wifi_config)); + ESP_ERROR_CHECK(esp_wifi_start()); + ESP_ERROR_CHECK(esp_wifi_set_ps(WIFI_PS_NONE)); + ESP_ERROR_CHECK(esp_wifi_set_channel(channel, WIFI_SECOND_CHAN_NONE)); + + ESP_ERROR_CHECK(esp_read_mac(s_own_mac, ESP_MAC_WIFI_STA)); + s_wifi_channel = channel; + return ESP_OK; +} + +void esp_now_core_init_send_done(void) { + s_send_done = xSemaphoreCreateBinary(); + if (s_send_done != NULL && + esp_now_register_send_cb(espnow_send_done_cb) == ESP_OK) { + s_send_cb_ready = true; + } else { + ESP_LOGW(TAG, "send-done callback unavailable (OTA may drop packets)"); + } +} diff --git a/main/esp_now_core.h b/main/esp_now_core.h new file mode 100644 index 0000000..f459ecc --- /dev/null +++ b/main/esp_now_core.h @@ -0,0 +1,32 @@ +#ifndef ESP_NOW_CORE_H +#define ESP_NOW_CORE_H + +#include "app_config.h" +#include "esp_err.h" +#include "esp_now_messages.pb.h" +#include +#include + +void esp_now_core_store_config(const app_config_t *config); +const app_config_t *esp_now_core_get_config(void); +bool esp_now_core_is_master(void); +uint8_t esp_now_core_network(void); +uint8_t esp_now_core_wifi_channel(void); +const uint8_t *esp_now_core_own_mac(void); + +uint32_t esp_now_core_now_ms(void); +bool esp_now_core_mac_equal(const uint8_t *a, const uint8_t *b); +void esp_now_core_mac_to_str(const uint8_t *mac, char *out, size_t out_len); + +esp_err_t esp_now_core_ensure_peer(const uint8_t *mac); +esp_err_t esp_now_core_ensure_broadcast_peer(void); + +esp_err_t esp_now_core_send(const uint8_t *dest_mac, + const alox_EspNowMessage *msg); +esp_err_t esp_now_core_send_wait(const uint8_t *dest_mac, + const alox_EspNowMessage *msg); + +esp_err_t esp_now_core_init_radio(uint8_t channel); +void esp_now_core_init_send_done(void); + +#endif diff --git a/main/esp_now_master.c b/main/esp_now_master.c new file mode 100644 index 0000000..e5cf2cf --- /dev/null +++ b/main/esp_now_master.c @@ -0,0 +1,484 @@ +#include "esp_now_master.h" +#include "client_registry.h" +#include "esp_now_comm.h" +#include "esp_now_core.h" +#include "esp_now_proto.h" +#include "board_input.h" +#include "ota_espnow.h" +#include "ota_uart.h" +#include "esp_log.h" +#include "freertos/FreeRTOS.h" +#include "freertos/idf_additions.h" +#include + +static const uint8_t ESPNOW_BCAST[ESP_NOW_ETH_ALEN] = {0xff, 0xff, 0xff, + 0xff, 0xff, 0xff}; + +#define ESPNOW_DISCOVER_INTERVAL_MS 500 +#define ESPNOW_HEARTBEAT_INTERVAL_MS 1000 +#define ESPNOW_HEARTBEAT_MISS_COUNT 3 +#define ESPNOW_CLIENT_TIMEOUT_MS \ + (ESPNOW_HEARTBEAT_INTERVAL_MS * ESPNOW_HEARTBEAT_MISS_COUNT) +#define ESPNOW_BATTERY_INTERVAL_MS 30000 + +static const char *TAG = "[ESPNOW_M]"; + +static esp_err_t send_accel_stream(const uint8_t *dest_mac, uint32_t client_id, + bool enable) { + alox_EspNowMessage msg = alox_EspNowMessage_init_zero; + msg.type = alox_EspNowMessageType_ESPNOW_SET_ACCEL_STREAM; + msg.which_payload = alox_EspNowMessage_accel_stream_tag; + msg.payload.accel_stream.enable = enable; + msg.payload.accel_stream.client_id = client_id; + return esp_now_core_send(dest_mac, &msg); +} + +static esp_err_t send_accel_deadzone(const uint8_t *dest_mac, uint32_t client_id, + uint32_t deadzone) { + alox_EspNowMessage msg = alox_EspNowMessage_init_zero; + msg.type = alox_EspNowMessageType_ESPNOW_SET_ACCEL_DEADZONE; + msg.which_payload = alox_EspNowMessage_accel_deadzone_tag; + msg.payload.accel_deadzone.deadzone = deadzone; + msg.payload.accel_deadzone.client_id = client_id; + return esp_now_core_send(dest_mac, &msg); +} + +static esp_err_t send_unicast_test(const uint8_t *dest_mac, uint32_t seq) { + alox_EspNowMessage msg = alox_EspNowMessage_init_zero; + msg.type = alox_EspNowMessageType_ESPNOW_UNICAST_TEST; + msg.which_payload = alox_EspNowMessage_unicast_test_tag; + msg.payload.unicast_test.seq = seq; + return esp_now_core_send(dest_mac, &msg); +} + +static esp_err_t send_find_me(const uint8_t *dest_mac, uint32_t client_id) { + alox_EspNowMessage msg = alox_EspNowMessage_init_zero; + msg.type = alox_EspNowMessageType_ESPNOW_FIND_ME; + msg.which_payload = alox_EspNowMessage_find_me_tag; + msg.payload.find_me.client_id = client_id; + return esp_now_core_send(dest_mac, &msg); +} + +static esp_err_t send_led_ring(const uint8_t *dest_mac, uint32_t client_id, + const alox_LedRingProgressRequest *req) { + if (req == NULL) { + return ESP_ERR_INVALID_ARG; + } + alox_EspNowMessage msg = alox_EspNowMessage_init_zero; + msg.type = alox_EspNowMessageType_ESPNOW_LED_RING; + msg.which_payload = alox_EspNowMessage_led_ring_tag; + msg.payload.led_ring.client_id = client_id; + msg.payload.led_ring.mode = req->mode; + msg.payload.led_ring.progress = req->progress; + msg.payload.led_ring.digit = req->digit; + msg.payload.led_ring.r = req->r; + msg.payload.led_ring.g = req->g; + msg.payload.led_ring.b = req->b; + msg.payload.led_ring.intensity = req->intensity; + msg.payload.led_ring.blink_ms = req->blink_ms; + msg.payload.led_ring.blink_count = req->blink_count; + return esp_now_core_send(dest_mac, &msg); +} + +static esp_err_t send_restart(const uint8_t *dest_mac, uint32_t client_id) { + alox_EspNowMessage msg = alox_EspNowMessage_init_zero; + msg.type = alox_EspNowMessageType_ESPNOW_RESTART; + msg.which_payload = alox_EspNowMessage_restart_tag; + msg.payload.restart.client_id = client_id; + return esp_now_core_send(dest_mac, &msg); +} + +static esp_err_t send_tap_notify(const uint8_t *dest_mac, uint32_t client_id, + bool single, bool double_tap, bool triple) { + alox_EspNowMessage msg = alox_EspNowMessage_init_zero; + msg.type = alox_EspNowMessageType_ESPNOW_SET_TAP_NOTIFY; + msg.which_payload = alox_EspNowMessage_tap_notify_tag; + msg.payload.tap_notify.client_id = client_id; + msg.payload.tap_notify.single = single; + msg.payload.tap_notify.double_tap = double_tap; + msg.payload.tap_notify.triple = triple; + return esp_now_core_send(dest_mac, &msg); +} + +static esp_err_t send_ota_start(const uint8_t *dest_mac, uint32_t total_size) { + alox_EspNowMessage msg = alox_EspNowMessage_init_zero; + msg.type = alox_EspNowMessageType_ESPNOW_OTA_START; + msg.which_payload = alox_EspNowMessage_ota_start_tag; + msg.payload.ota_start.total_size = total_size; + return esp_now_core_send_wait(dest_mac, &msg); +} + +static esp_err_t send_ota_payload(const uint8_t *dest_mac, uint32_t seq, + const uint8_t *data, size_t len) { + if (data == NULL || len == 0 || len > OTA_UART_HOST_CHUNK_SIZE) { + return ESP_ERR_INVALID_ARG; + } + alox_EspNowMessage msg = alox_EspNowMessage_init_zero; + msg.type = alox_EspNowMessageType_ESPNOW_OTA_PAYLOAD; + msg.which_payload = alox_EspNowMessage_ota_payload_tag; + msg.payload.ota_payload.seq = seq; + msg.payload.ota_payload.data.size = len; + memcpy(msg.payload.ota_payload.data.bytes, data, len); + return esp_now_core_send_wait(dest_mac, &msg); +} + +static esp_err_t send_ota_end(const uint8_t *dest_mac) { + alox_EspNowMessage msg = alox_EspNowMessage_init_zero; + msg.type = alox_EspNowMessageType_ESPNOW_OTA_END; + msg.which_payload = alox_EspNowMessage_ota_end_tag; + return esp_now_core_send_wait(dest_mac, &msg); +} + +esp_err_t esp_now_comm_send_ota_start(const uint8_t mac[CLIENT_MAC_LEN], + uint32_t total_size) { + if (mac == NULL || !esp_now_core_is_master()) { + return ESP_ERR_INVALID_STATE; + } + return send_ota_start(mac, total_size); +} + +esp_err_t esp_now_comm_send_ota_payload(const uint8_t mac[CLIENT_MAC_LEN], + uint32_t seq, const uint8_t *data, + size_t len) { + if (mac == NULL || !esp_now_core_is_master()) { + return ESP_ERR_INVALID_STATE; + } + return send_ota_payload(mac, seq, data, len); +} + +esp_err_t esp_now_comm_send_ota_end(const uint8_t mac[CLIENT_MAC_LEN]) { + if (mac == NULL || !esp_now_core_is_master()) { + return ESP_ERR_INVALID_STATE; + } + return send_ota_end(mac); +} + +esp_err_t esp_now_comm_send_restart(const uint8_t mac[CLIENT_MAC_LEN], + uint32_t client_id) { + if (mac == NULL || !esp_now_core_is_master()) { + return ESP_ERR_INVALID_STATE; + } + char mac_str[18]; + esp_now_core_mac_to_str(mac, mac_str, sizeof(mac_str)); + esp_err_t err = send_restart(mac, client_id); + if (err == ESP_OK) { + ESP_LOGI(TAG, "unicast RESTART to %s client_id=%lu", mac_str, + (unsigned long)client_id); + } else { + ESP_LOGW(TAG, "unicast RESTART to %s failed: %s", mac_str, + esp_err_to_name(err)); + } + return err; +} + +esp_err_t esp_now_comm_send_find_me(const uint8_t mac[CLIENT_MAC_LEN], + uint32_t client_id) { + if (mac == NULL || !esp_now_core_is_master()) { + return ESP_ERR_INVALID_STATE; + } + char mac_str[18]; + esp_now_core_mac_to_str(mac, mac_str, sizeof(mac_str)); + esp_err_t err = send_find_me(mac, client_id); + if (err == ESP_OK) { + ESP_LOGI(TAG, "unicast FIND_ME to %s client_id=%lu", mac_str, + (unsigned long)client_id); + } else { + ESP_LOGW(TAG, "unicast FIND_ME to %s failed: %s", mac_str, + esp_err_to_name(err)); + } + return err; +} + +esp_err_t esp_now_comm_send_led_ring(const uint8_t mac[CLIENT_MAC_LEN], + uint32_t client_id, + const alox_LedRingProgressRequest *req) { + if (mac == NULL || !esp_now_core_is_master() || req == NULL) { + return ESP_ERR_INVALID_STATE; + } + char mac_str[18]; + esp_now_core_mac_to_str(mac, mac_str, sizeof(mac_str)); + esp_err_t err = send_led_ring(mac, client_id, req); + if (err == ESP_OK) { + ESP_LOGI(TAG, "unicast LED_RING mode %lu to %s client_id=%lu", + (unsigned long)req->mode, mac_str, (unsigned long)client_id); + } else { + ESP_LOGW(TAG, "unicast LED_RING to %s failed: %s", mac_str, + esp_err_to_name(err)); + } + return err; +} + +esp_err_t esp_now_comm_send_unicast_test(const uint8_t mac[CLIENT_MAC_LEN], + uint32_t seq) { + if (mac == NULL || !esp_now_core_is_master()) { + return ESP_ERR_INVALID_STATE; + } + char mac_str[18]; + esp_now_core_mac_to_str(mac, mac_str, sizeof(mac_str)); + esp_err_t err = send_unicast_test(mac, seq); + if (err == ESP_OK) { + ESP_LOGI(TAG, "unicast TEST to %s seq=%lu", mac_str, (unsigned long)seq); + } else { + ESP_LOGW(TAG, "unicast TEST to %s failed: %s", mac_str, esp_err_to_name(err)); + } + return err; +} + +esp_err_t esp_now_comm_send_accel_stream(const uint8_t mac[CLIENT_MAC_LEN], + uint32_t client_id, bool enable) { + if (mac == NULL || !esp_now_core_is_master()) { + return ESP_ERR_INVALID_STATE; + } + char mac_str[18]; + esp_now_core_mac_to_str(mac, mac_str, sizeof(mac_str)); + esp_err_t err = send_accel_stream(mac, client_id, enable); + if (err == ESP_OK) { + ESP_LOGI(TAG, "unicast SET_ACCEL_STREAM to %s: %s client_id=%lu", mac_str, + enable ? "on" : "off", (unsigned long)client_id); + } else { + ESP_LOGW(TAG, "unicast SET_ACCEL_STREAM to %s failed: %s", mac_str, + esp_err_to_name(err)); + } + return err; +} + +esp_err_t esp_now_comm_send_tap_notify(const uint8_t mac[CLIENT_MAC_LEN], + uint32_t client_id, bool single, + bool double_tap, bool triple) { + if (mac == NULL || !esp_now_core_is_master()) { + return ESP_ERR_INVALID_STATE; + } + char mac_str[18]; + esp_now_core_mac_to_str(mac, mac_str, sizeof(mac_str)); + esp_err_t err = + send_tap_notify(mac, client_id, single, double_tap, triple); + if (err == ESP_OK) { + ESP_LOGI(TAG, + "unicast SET_TAP_NOTIFY to %s: single=%d double=%d triple=%d " + "client_id=%lu", + mac_str, single, double_tap, triple, (unsigned long)client_id); + } else { + ESP_LOGW(TAG, "unicast SET_TAP_NOTIFY to %s failed: %s", mac_str, + esp_err_to_name(err)); + } + return err; +} + +esp_err_t esp_now_comm_send_accel_deadzone(const uint8_t mac[CLIENT_MAC_LEN], + uint32_t client_id, + uint32_t deadzone) { + if (mac == NULL || !esp_now_core_is_master()) { + return ESP_ERR_INVALID_STATE; + } + char mac_str[18]; + esp_now_core_mac_to_str(mac, mac_str, sizeof(mac_str)); + esp_err_t err = send_accel_deadzone(mac, client_id, deadzone); + if (err == ESP_OK) { + ESP_LOGI(TAG, "unicast SET_ACCEL_DEADZONE to %s: deadzone=%lu client_id=%lu", + mac_str, (unsigned long)deadzone, (unsigned long)client_id); + } else { + ESP_LOGW(TAG, "unicast SET_ACCEL_DEADZONE to %s failed: %s", mac_str, + esp_err_to_name(err)); + } + return err; +} + +static void handle_accel_sample(const uint8_t mac[CLIENT_MAC_LEN], + const alox_EspNowAccelSample *sample) { + if (sample == NULL) { + return; + } + esp_err_t err = client_registry_update_accel( + mac, sample->slave_id, (int16_t)sample->x, (int16_t)sample->y, + (int16_t)sample->z); + if (err == ESP_ERR_NOT_FOUND) { + return; + } + if (err != ESP_OK) { + ESP_LOGW(TAG, "accel sample id mismatch from %02x:…:%02x", mac[0], mac[5]); + } +} + +static void handle_tap_event(const uint8_t mac[CLIENT_MAC_LEN], + const alox_EspNowTapEvent *event) { + if (event == NULL) { + return; + } + esp_err_t err = + client_registry_update_tap(mac, event->slave_id, event->kind); + if (err == ESP_ERR_NOT_FOUND) { + return; + } + if (err != ESP_OK) { + ESP_LOGW(TAG, "tap event id=%lu kind=%lu rejected from %02x:…:%02x", + (unsigned long)event->slave_id, (unsigned long)event->kind, mac[0], + mac[5]); + } +} + +static void handle_battery_report(const uint8_t mac[CLIENT_MAC_LEN], + const alox_EspNowBatteryReport *report) { + if (report == NULL) { + return; + } + esp_err_t err = client_registry_update_battery( + mac, report->client_id, report->lipo1_valid, report->lipo1_mv, + report->lipo2_valid, report->lipo2_mv); + if (err == ESP_ERR_NOT_FOUND) { + ESP_LOGW(TAG, "battery report from unregistered slave id=%lu", + (unsigned long)report->client_id); + return; + } + if (err != ESP_OK) { + ESP_LOGW(TAG, "battery report id=%lu rejected: %s", + (unsigned long)report->client_id, esp_err_to_name(err)); + return; + } + ESP_LOGI(TAG, "battery cached id=%lu L1=%s %lu mV L2=%s %lu mV", + (unsigned long)report->client_id, + report->lipo1_valid ? "ok" : "n/a", + (unsigned long)report->lipo1_mv, report->lipo2_valid ? "ok" : "n/a", + (unsigned long)report->lipo2_mv); +} + +static void handle_client_presence(const alox_EspNowSlavePresence *presence, + const uint8_t mac[CLIENT_MAC_LEN]) { + if (presence->network != esp_now_core_network()) { + return; + } + + esp_now_core_ensure_peer(mac); + + bool is_new = false; + bool reactivated = false; + esp_err_t err = client_registry_heartbeat( + mac, presence->slave_id, presence->version, presence->used, &is_new, + &reactivated); + if (err != ESP_OK) { + ESP_LOGW(TAG, "client registry full"); + return; + } + + char mac_str[18]; + esp_now_core_mac_to_str(mac, mac_str, sizeof(mac_str)); + if (is_new) { + ESP_LOGI(TAG, "client registered id=%lu mac=%s ver=%lu", + (unsigned long)presence->slave_id, mac_str, + (unsigned long)presence->version); + } else if (reactivated) { + ESP_LOGI(TAG, "client reconnected id=%lu mac=%s", + (unsigned long)presence->slave_id, mac_str); + } +} + +void esp_now_master_on_recv(const esp_now_recv_info_t *info, const uint8_t *data, + int len) { + if (info == NULL || data == NULL || len <= 0) { + return; + } + + alox_EspNowMessage msg = alox_EspNowMessage_init_zero; + if (esp_now_proto_decode(data, (size_t)len, &msg) != ESP_OK) { + ESP_LOGW(TAG, "decode failed (%d bytes)", len); + return; + } + + if (ota_espnow_distribution_active()) { + if (msg.which_payload == alox_EspNowMessage_ota_status_tag) { + esp_now_core_ensure_peer(info->src_addr); + ota_espnow_master_on_status(info->src_addr, &msg.payload.ota_status); + } + return; + } + + if (msg.which_payload == alox_EspNowMessage_ota_status_tag) { + esp_now_core_ensure_peer(info->src_addr); + ota_espnow_master_on_status(info->src_addr, &msg.payload.ota_status); + return; + } + + if (msg.which_payload == alox_EspNowMessage_accel_sample_tag) { + esp_now_core_ensure_peer(info->src_addr); + handle_accel_sample(info->src_addr, &msg.payload.accel_sample); + return; + } + + if (msg.which_payload == alox_EspNowMessage_tap_event_tag) { + esp_now_core_ensure_peer(info->src_addr); + handle_tap_event(info->src_addr, &msg.payload.tap_event); + return; + } + + if (msg.which_payload == alox_EspNowMessage_battery_report_tag) { + esp_now_core_ensure_peer(info->src_addr); + handle_battery_report(info->src_addr, &msg.payload.battery_report); + return; + } + + if (msg.type == alox_EspNowMessageType_ESPNOW_BATTERY_REPORT && + msg.which_payload != alox_EspNowMessage_battery_report_tag) { + ESP_LOGW(TAG, "BATTERY_REPORT type but which=%u", msg.which_payload); + } + + const alox_EspNowSlavePresence *presence = esp_now_proto_get_presence(&msg); + if (presence != NULL) { + esp_now_core_ensure_peer(info->src_addr); + handle_client_presence(presence, info->src_addr); + } +} + +static void discover_task(void *param) { + (void)param; + + alox_EspNowMessage msg = alox_EspNowMessage_init_zero; + msg.type = alox_EspNowMessageType_ESPNOW_DISCOVER; + msg.which_payload = alox_EspNowMessage_discover_tag; + msg.payload.discover.network = esp_now_core_network(); + + ESP_LOGI(TAG, "discover on network %u ch %u", (unsigned)esp_now_core_network(), + (unsigned)esp_now_core_wifi_channel()); + + while (1) { + esp_now_core_send(ESPNOW_BCAST, &msg); + vTaskDelay(pdMS_TO_TICKS(ESPNOW_DISCOVER_INTERVAL_MS)); + } +} + +static void monitor_task(void *param) { + (void)param; + uint32_t last_local_battery_ms = 0; + + ESP_LOGI(TAG, "monitor (client timeout %u ms)", + (unsigned)ESPNOW_CLIENT_TIMEOUT_MS); + + board_lipo_reading_t reading; + board_input_read_lipo(&reading); + client_registry_set_master_battery(&reading); + last_local_battery_ms = esp_now_core_now_ms(); + + while (1) { + vTaskDelay(pdMS_TO_TICKS(ESPNOW_HEARTBEAT_INTERVAL_MS)); + client_registry_check_timeouts(ESPNOW_CLIENT_TIMEOUT_MS); + + uint32_t t = esp_now_core_now_ms(); + if (t - last_local_battery_ms >= ESPNOW_BATTERY_INTERVAL_MS) { + board_input_read_lipo(&reading); + client_registry_set_master_battery(&reading); + last_local_battery_ms = t; + } + } +} + +esp_err_t esp_now_master_start(void) { + ESP_ERROR_CHECK(esp_now_core_ensure_broadcast_peer()); + + if (xTaskCreate(discover_task, "espnow_disc", 4096, NULL, 4, NULL) != pdPASS) { + ESP_LOGE(TAG, "failed to create discover task"); + return ESP_FAIL; + } + if (xTaskCreate(monitor_task, "espnow_mon", 4096, NULL, 4, NULL) != pdPASS) { + ESP_LOGE(TAG, "failed to create monitor task"); + return ESP_FAIL; + } + return ESP_OK; +} diff --git a/main/esp_now_master.h b/main/esp_now_master.h new file mode 100644 index 0000000..a06074e --- /dev/null +++ b/main/esp_now_master.h @@ -0,0 +1,11 @@ +#ifndef ESP_NOW_MASTER_H +#define ESP_NOW_MASTER_H + +#include "esp_err.h" +#include "esp_now.h" + +esp_err_t esp_now_master_start(void); +void esp_now_master_on_recv(const esp_now_recv_info_t *info, const uint8_t *data, + int len); + +#endif diff --git a/main/esp_now_slave.c b/main/esp_now_slave.c new file mode 100644 index 0000000..4fee659 --- /dev/null +++ b/main/esp_now_slave.c @@ -0,0 +1,579 @@ +#include "esp_now_slave.h" +#include "bosch456.h" +#include "cmd_led_ring.h" +#include "esp_now_comm.h" +#include "esp_now_core.h" +#include "esp_now_proto.h" +#include "board_input.h" +#include "led_ring.h" +#include "ota_espnow.h" +#include "ota_uart.h" +#include "pod_reboot.h" +#include "pod_settings.h" +#include "esp_log.h" +#include "freertos/FreeRTOS.h" +#include "freertos/idf_additions.h" +#include + +#ifndef POWERPOD_FW_VERSION +#define POWERPOD_FW_VERSION 1u +#endif + +#define ESPNOW_HEARTBEAT_INTERVAL_MS 1000 +#define SLAVE_MASTER_LOST_MS (ESPNOW_HEARTBEAT_INTERVAL_MS * 5) +#define ESPNOW_ACCEL_INTERVAL_MS 16 +#define ESPNOW_BATTERY_INTERVAL_MS 30000 +#define SLAVE_BATTERY_AFTER_JOIN_MS 150 + +static const char *TAG = "[ESPNOW_S]"; + +static bool s_joined; +static bool s_accel_stream_enabled; +static bool s_tap_notify_single; +static bool s_tap_notify_double; +static bool s_tap_notify_triple; +static uint8_t s_master_mac[ESP_NOW_ETH_ALEN]; +static uint32_t s_last_discover_ms; + +typedef enum { + SLAVE_TX_SLAVE_INFO = 1, + SLAVE_TX_BATTERY, +} slave_tx_op_t; + +static QueueHandle_t s_tx_queue; + +static bool from_joined_master(const uint8_t *master_mac) { + return s_joined && esp_now_core_mac_equal(master_mac, s_master_mac); +} + +static void fill_presence(alox_EspNowSlavePresence *presence) { + const uint8_t *own = esp_now_core_own_mac(); + presence->network = esp_now_core_network(); + presence->version = POWERPOD_FW_VERSION; + presence->slave_id = own[5]; + presence->available = true; + presence->used = false; + esp_now_proto_setup_presence_encode(presence, own); +} + +static void send_presence(const uint8_t *dest_mac, alox_EspNowMessageType type) { + alox_EspNowMessage msg = alox_EspNowMessage_init_zero; + alox_EspNowSlavePresence *presence = NULL; + + msg.type = type; + if (type == alox_EspNowMessageType_ESPNOW_SLAVE_INFO) { + msg.which_payload = alox_EspNowMessage_slave_info_tag; + presence = &msg.payload.slave_info; + } else { + msg.which_payload = alox_EspNowMessage_heartbeat_tag; + presence = &msg.payload.heartbeat; + } + fill_presence(presence); + esp_now_core_send(dest_mac, &msg); +} + +static esp_err_t send_accel_sample(const uint8_t *dest_mac, uint32_t slave_id, + int16_t x, int16_t y, int16_t z) { + alox_EspNowMessage msg = alox_EspNowMessage_init_zero; + msg.type = alox_EspNowMessageType_ESPNOW_ACCEL_SAMPLE; + msg.which_payload = alox_EspNowMessage_accel_sample_tag; + msg.payload.accel_sample.slave_id = slave_id; + msg.payload.accel_sample.x = x; + msg.payload.accel_sample.y = y; + msg.payload.accel_sample.z = z; + return esp_now_core_send(dest_mac, &msg); +} + +static esp_err_t send_tap_event(const uint8_t *dest_mac, uint32_t slave_id, + uint32_t kind) { + alox_EspNowMessage msg = alox_EspNowMessage_init_zero; + msg.type = alox_EspNowMessageType_ESPNOW_TAP_EVENT; + msg.which_payload = alox_EspNowMessage_tap_event_tag; + msg.payload.tap_event.slave_id = slave_id; + msg.payload.tap_event.kind = kind; + return esp_now_core_send(dest_mac, &msg); +} + +static esp_err_t send_battery_report(const uint8_t *dest_mac, + const alox_EspNowBatteryReport *report) { + if (report == NULL) { + return ESP_ERR_INVALID_ARG; + } + alox_EspNowMessage msg = alox_EspNowMessage_init_zero; + msg.type = alox_EspNowMessageType_ESPNOW_BATTERY_REPORT; + msg.which_payload = alox_EspNowMessage_battery_report_tag; + msg.payload.battery_report = *report; + return esp_now_core_send(dest_mac, &msg); +} + +static void reset_join(void) { + s_joined = false; + s_accel_stream_enabled = false; + memset(s_master_mac, 0, sizeof(s_master_mac)); + s_last_discover_ms = 0; + if (s_tx_queue != NULL) { + xQueueReset(s_tx_queue); + } +} + +static void queue_tx(slave_tx_op_t op) { + if (s_tx_queue == NULL) { + return; + } + if (xQueueSend(s_tx_queue, &op, 0) != pdTRUE) { + ESP_LOGW(TAG, "tx queue full (op=%d)", (int)op); + } +} + +static void send_battery_to_master(void) { + if (!s_joined) { + return; + } + + board_lipo_reading_t reading; + board_input_read_lipo(&reading); + + alox_EspNowBatteryReport report = alox_EspNowBatteryReport_init_zero; + report.client_id = esp_now_core_own_mac()[5]; + report.lipo1_valid = reading.lipo1_valid; + report.lipo2_valid = reading.lipo2_valid; + report.lipo1_mv = reading.lipo1_mv; + report.lipo2_mv = reading.lipo2_mv; + + esp_err_t err = send_battery_report(s_master_mac, &report); + if (err != ESP_OK) { + ESP_LOGW(TAG, "battery report send failed id=%lu: %s", + (unsigned long)report.client_id, esp_err_to_name(err)); + } else { + ESP_LOGI(TAG, "battery report sent id=%lu L1=%s %lu mV L2=%s %lu mV", + (unsigned long)report.client_id, + report.lipo1_valid ? "ok" : "n/a", + (unsigned long)report.lipo1_mv, + report.lipo2_valid ? "ok" : "n/a", + (unsigned long)report.lipo2_mv); + } +} + +esp_err_t esp_now_comm_send_ota_status(const uint8_t master_mac[CLIENT_MAC_LEN], + uint32_t status, uint32_t bytes_written, + uint32_t error) { + if (master_mac == NULL || esp_now_core_is_master()) { + return ESP_ERR_INVALID_STATE; + } + alox_EspNowMessage msg = alox_EspNowMessage_init_zero; + msg.type = alox_EspNowMessageType_ESPNOW_OTA_STATUS; + msg.which_payload = alox_EspNowMessage_ota_status_tag; + msg.payload.ota_status.status = status; + msg.payload.ota_status.bytes_written = bytes_written; + msg.payload.ota_status.error = error; + return esp_now_core_send_wait(master_mac, &msg); +} + +bool esp_now_comm_get_master_mac(uint8_t mac_out[CLIENT_MAC_LEN]) { + return esp_now_slave_get_master_mac(mac_out); +} + +bool esp_now_slave_get_master_mac(uint8_t mac_out[CLIENT_MAC_LEN]) { + if (mac_out == NULL || !s_joined) { + return false; + } + memcpy(mac_out, s_master_mac, CLIENT_MAC_LEN); + return true; +} + +static void tx_task(void *param) { + (void)param; + slave_tx_op_t op; + + ESP_LOGI(TAG, "deferred tx task ready"); + + while (1) { + if (xQueueReceive(s_tx_queue, &op, portMAX_DELAY) != pdTRUE) { + continue; + } + if (!s_joined) { + continue; + } + switch (op) { + case SLAVE_TX_SLAVE_INFO: + send_presence(s_master_mac, alox_EspNowMessageType_ESPNOW_SLAVE_INFO); + break; + case SLAVE_TX_BATTERY: + vTaskDelay(pdMS_TO_TICKS(SLAVE_BATTERY_AFTER_JOIN_MS)); + send_battery_to_master(); + break; + default: + break; + } + } +} + +static void handle_unicast_test(const uint8_t *master_mac, + const alox_EspNowUnicastTest *test) { + char mac_str[18]; + esp_now_core_mac_to_str(master_mac, mac_str, sizeof(mac_str)); + ESP_LOGI(TAG, "UNICAST TEST OK from master %s seq=%lu (joined=%d)", mac_str, + (unsigned long)test->seq, (int)s_joined); +} + +static void handle_restart(const uint8_t *master_mac, + const alox_EspNowRestart *req) { + const uint8_t *own = esp_now_core_own_mac(); + uint32_t my_id = own[5]; + + if (req->client_id != 0 && req->client_id != my_id) { + return; + } + if (s_joined && !esp_now_core_mac_equal(master_mac, s_master_mac)) { + return; + } + + char mac_str[18]; + esp_now_core_mac_to_str(master_mac, mac_str, sizeof(mac_str)); + ESP_LOGI(TAG, "RESTART from master %s (id=%lu)", mac_str, (unsigned long)my_id); + pod_schedule_restart(); +} + +static void handle_battery_query(const uint8_t *master_mac, + const alox_EspNowBatteryQuery *query) { + uint32_t my_id = esp_now_core_own_mac()[5]; + if (query->client_id != 0 && query->client_id != my_id) { + return; + } + if (s_joined && !esp_now_core_mac_equal(master_mac, s_master_mac)) { + return; + } + send_battery_to_master(); +} + +static void handle_led_ring(const uint8_t *master_mac, + const alox_EspNowLedRing *msg) { + uint32_t my_id = esp_now_core_own_mac()[5]; + if (msg->client_id != 0 && msg->client_id != my_id) { + return; + } + if (s_joined && !esp_now_core_mac_equal(master_mac, s_master_mac)) { + return; + } + + alox_LedRingProgressRequest req = alox_LedRingProgressRequest_init_zero; + req.mode = msg->mode; + req.progress = msg->progress; + req.digit = msg->digit; + req.r = msg->r; + req.g = msg->g; + req.b = msg->b; + req.intensity = msg->intensity; + req.blink_ms = msg->blink_ms; + req.blink_count = msg->blink_count; + + char mac_str[18]; + esp_now_core_mac_to_str(master_mac, mac_str, sizeof(mac_str)); + ESP_LOGI(TAG, "LED_RING mode %lu from master %s (id=%lu)", + (unsigned long)req.mode, mac_str, (unsigned long)my_id); + cmd_led_ring_apply(&req); +} + +static void handle_find_me(const uint8_t *master_mac, const alox_EspNowFindMe *req) { + uint32_t my_id = esp_now_core_own_mac()[5]; + if (req->client_id != 0 && req->client_id != my_id) { + return; + } + if (s_joined && !esp_now_core_mac_equal(master_mac, s_master_mac)) { + return; + } + char mac_str[18]; + esp_now_core_mac_to_str(master_mac, mac_str, sizeof(mac_str)); + ESP_LOGI(TAG, "FIND_ME from master %s (id=%lu)", mac_str, (unsigned long)my_id); + led_ring_find_me(); +} + +static void handle_accel_stream(const uint8_t *master_mac, + const alox_EspNowAccelStream *cfg) { + uint32_t my_id = esp_now_core_own_mac()[5]; + if (cfg->client_id != 0 && cfg->client_id != my_id) { + return; + } + if (s_joined && !esp_now_core_mac_equal(master_mac, s_master_mac)) { + return; + } + s_accel_stream_enabled = cfg->enable; + char mac_str[18]; + esp_now_core_mac_to_str(master_mac, mac_str, sizeof(mac_str)); + ESP_LOGI(TAG, "accel stream %s from master %s (id=%lu)", + cfg->enable ? "on" : "off", mac_str, (unsigned long)my_id); +} + +static void handle_tap_notify(const uint8_t *master_mac, + const alox_EspNowTapNotify *cfg) { + uint32_t my_id = esp_now_core_own_mac()[5]; + if (cfg->client_id != 0 && cfg->client_id != my_id) { + return; + } + if (s_joined && !esp_now_core_mac_equal(master_mac, s_master_mac)) { + return; + } + s_tap_notify_single = cfg->single; + s_tap_notify_double = cfg->double_tap; + s_tap_notify_triple = cfg->triple; + char mac_str[18]; + esp_now_core_mac_to_str(master_mac, mac_str, sizeof(mac_str)); + ESP_LOGI(TAG, + "tap notify single=%d double=%d triple=%d from master %s (id=%lu)", + cfg->single, cfg->double_tap, cfg->triple, mac_str, + (unsigned long)my_id); +} + +static void handle_accel_deadzone(const uint8_t *master_mac, + const alox_EspNowAccelDeadzone *cfg) { + uint32_t my_id = esp_now_core_own_mac()[5]; + if (cfg->client_id != 0 && cfg->client_id != my_id) { + return; + } + if (s_joined && !esp_now_core_mac_equal(master_mac, s_master_mac)) { + return; + } + char mac_str[18]; + esp_now_core_mac_to_str(master_mac, mac_str, sizeof(mac_str)); + ESP_LOGI(TAG, + "accel deadzone from master %s: %lu LSB id=%lu (sensor %s)", mac_str, + (unsigned long)cfg->deadzone, (unsigned long)my_id, + bma456_is_ready() ? "ok" : "not installed"); + bma456_set_accel_deadzone(cfg->deadzone); + if (pod_settings_save_accel_deadzone(cfg->deadzone) != ESP_OK) { + ESP_LOGW(TAG, "deadzone %lu applied but not saved to NVS", + (unsigned long)cfg->deadzone); + } +} + +static void handle_discover(const uint8_t *sender_mac, + const alox_EspNowDiscover *discover) { + if (discover->network != esp_now_core_network()) { + return; + } + + uint32_t now = esp_now_core_now_ms(); + + if (s_joined) { + if (!esp_now_core_mac_equal(sender_mac, s_master_mac)) { + return; + } + if ((now - s_last_discover_ms) <= SLAVE_MASTER_LOST_MS) { + s_last_discover_ms = now; + return; + } + ESP_LOGW(TAG, "master lost, rejoining"); + reset_join(); + } + + memcpy(s_master_mac, sender_mac, ESP_NOW_ETH_ALEN); + s_joined = true; + s_last_discover_ms = now; + esp_now_core_ensure_peer(sender_mac); + + char mac_str[18]; + esp_now_core_mac_to_str(sender_mac, mac_str, sizeof(mac_str)); + ESP_LOGI(TAG, "joined network %u, master %s", (unsigned)discover->network, + mac_str); + + queue_tx(SLAVE_TX_SLAVE_INFO); + queue_tx(SLAVE_TX_BATTERY); +} + +static void check_master_timeout(void) { + if (!s_joined || s_last_discover_ms == 0) { + return; + } + uint32_t now = esp_now_core_now_ms(); + if ((now - s_last_discover_ms) > SLAVE_MASTER_LOST_MS) { + ESP_LOGW(TAG, "no master discover for %u ms, reconnecting", + (unsigned)(now - s_last_discover_ms)); + reset_join(); + } +} + +static void accel_stream_task(void *param) { + (void)param; + const uint8_t *own = esp_now_core_own_mac(); + + ESP_LOGI(TAG, "accel stream task (interval %u ms)", + (unsigned)ESPNOW_ACCEL_INTERVAL_MS); + + while (1) { + vTaskDelay(pdMS_TO_TICKS(ESPNOW_ACCEL_INTERVAL_MS)); + if (!s_joined || !s_accel_stream_enabled || !bma456_is_ready()) { + continue; + } + int16_t x = 0; + int16_t y = 0; + int16_t z = 0; + if (bma456_read_accel(&x, &y, &z) != ESP_OK) { + continue; + } + (void)send_accel_sample(s_master_mac, own[5], x, y, z); + } +} + +static void on_bma456_tap(bma456_tap_kind_t kind, void *ctx) { + (void)ctx; + if (!s_joined) { + return; + } + bool enabled = false; + switch (kind) { + case BMA456_TAP_SINGLE: + enabled = s_tap_notify_single; + break; + case BMA456_TAP_DOUBLE: + enabled = s_tap_notify_double; + break; + case BMA456_TAP_TRIPLE: + enabled = s_tap_notify_triple; + break; + default: + return; + } + if (!enabled) { + return; + } + (void)send_tap_event(s_master_mac, esp_now_core_own_mac()[5], (uint32_t)kind); +} + +static void heartbeat_task(void *param) { + (void)param; + uint32_t last_battery_ms = 0; + + ESP_LOGI(TAG, "heartbeat task (interval %u ms)", + (unsigned)ESPNOW_HEARTBEAT_INTERVAL_MS); + + while (1) { + vTaskDelay(pdMS_TO_TICKS(ESPNOW_HEARTBEAT_INTERVAL_MS)); + check_master_timeout(); + if (!s_joined) { + last_battery_ms = 0; + continue; + } + send_presence(s_master_mac, alox_EspNowMessageType_ESPNOW_HEARTBEAT); + uint32_t now = esp_now_core_now_ms(); + if (last_battery_ms == 0 || + (now - last_battery_ms) >= ESPNOW_BATTERY_INTERVAL_MS) { + send_battery_to_master(); + last_battery_ms = now; + } + } +} + +void esp_now_slave_on_recv(const esp_now_recv_info_t *info, const uint8_t *data, + int len) { + if (info == NULL || data == NULL || len <= 0) { + return; + } + + alox_EspNowMessage msg = alox_EspNowMessage_init_zero; + if (esp_now_proto_decode(data, (size_t)len, &msg) != ESP_OK) { + ESP_LOGW(TAG, "decode failed (%d bytes)", len); + return; + } + + if (from_joined_master(info->src_addr)) { + esp_now_core_ensure_peer(info->src_addr); + } + + if (ota_uart_is_active()) { + switch (msg.which_payload) { + case alox_EspNowMessage_ota_start_tag: + case alox_EspNowMessage_ota_payload_tag: + case alox_EspNowMessage_ota_end_tag: + if (!from_joined_master(info->src_addr)) { + break; + } + if (msg.which_payload == alox_EspNowMessage_ota_start_tag) { + ota_espnow_slave_on_start(info->src_addr, &msg.payload.ota_start); + } else if (msg.which_payload == alox_EspNowMessage_ota_payload_tag) { + ota_espnow_slave_on_payload(info->src_addr, &msg.payload.ota_payload); + } else { + ota_espnow_slave_on_end(info->src_addr); + } + break; + default: + break; + } + return; + } + + switch (msg.which_payload) { + case alox_EspNowMessage_discover_tag: + handle_discover(info->src_addr, &msg.payload.discover); + break; + case alox_EspNowMessage_unicast_test_tag: + if (from_joined_master(info->src_addr)) { + handle_unicast_test(info->src_addr, &msg.payload.unicast_test); + } + break; + case alox_EspNowMessage_accel_deadzone_tag: + if (from_joined_master(info->src_addr)) { + handle_accel_deadzone(info->src_addr, &msg.payload.accel_deadzone); + } + break; + case alox_EspNowMessage_accel_stream_tag: + if (from_joined_master(info->src_addr)) { + handle_accel_stream(info->src_addr, &msg.payload.accel_stream); + } + break; + case alox_EspNowMessage_tap_notify_tag: + if (from_joined_master(info->src_addr)) { + handle_tap_notify(info->src_addr, &msg.payload.tap_notify); + } + break; + case alox_EspNowMessage_battery_query_tag: + if (from_joined_master(info->src_addr)) { + handle_battery_query(info->src_addr, &msg.payload.battery_query); + } + break; + case alox_EspNowMessage_led_ring_tag: + if (from_joined_master(info->src_addr)) { + handle_led_ring(info->src_addr, &msg.payload.led_ring); + } + break; + case alox_EspNowMessage_find_me_tag: + if (from_joined_master(info->src_addr)) { + handle_find_me(info->src_addr, &msg.payload.find_me); + } + break; + case alox_EspNowMessage_restart_tag: + if (from_joined_master(info->src_addr)) { + handle_restart(info->src_addr, &msg.payload.restart); + } + break; + default: + ESP_LOGW(TAG, "unhandled which=%u type=%u", msg.which_payload, + (unsigned)msg.type); + break; + } +} + +esp_err_t esp_now_slave_start(void) { + reset_join(); + + s_tx_queue = xQueueCreate(4, sizeof(slave_tx_op_t)); + if (s_tx_queue == NULL) { + ESP_LOGE(TAG, "failed to create tx queue"); + return ESP_ERR_NO_MEM; + } + if (xTaskCreate(tx_task, "espnow_stx", 4096, NULL, 5, NULL) != pdPASS) { + ESP_LOGE(TAG, "failed to create tx task"); + return ESP_FAIL; + } + if (xTaskCreate(heartbeat_task, "espnow_hb", 4096, NULL, 4, NULL) != pdPASS) { + ESP_LOGE(TAG, "failed to create heartbeat task"); + return ESP_FAIL; + } + if (xTaskCreate(accel_stream_task, "espnow_accel", 4096, NULL, 5, NULL) != + pdPASS) { + ESP_LOGE(TAG, "failed to create accel stream task"); + return ESP_FAIL; + } + ota_espnow_slave_init(); + bma456_set_tap_handler(on_bma456_tap, NULL); + return ESP_OK; +} diff --git a/main/esp_now_slave.h b/main/esp_now_slave.h new file mode 100644 index 0000000..e8210b2 --- /dev/null +++ b/main/esp_now_slave.h @@ -0,0 +1,14 @@ +#ifndef ESP_NOW_SLAVE_H +#define ESP_NOW_SLAVE_H + +#include "client_registry.h" +#include "esp_err.h" +#include "esp_now.h" + +esp_err_t esp_now_slave_start(void); +void esp_now_slave_on_recv(const esp_now_recv_info_t *info, const uint8_t *data, + int len); + +bool esp_now_slave_get_master_mac(uint8_t mac_out[CLIENT_MAC_LEN]); + +#endif diff --git a/main/ota_espnow.c b/main/ota_espnow.c index 3ebdb18..37b0665 100644 --- a/main/ota_espnow.c +++ b/main/ota_espnow.c @@ -35,7 +35,28 @@ static const char *TAG = "[OTA_ESPNOW]"; #define OTA_MAX_TARGETS CLIENT_REGISTRY_MAX +#define OTA_SLAVE_WORK_QUEUE_LEN 12 +#define OTA_SLAVE_WORK_STACK 8192 +#define OTA_SLAVE_WORK_PRIO 5 + +typedef enum { + OTA_SLAVE_WORK_STATUS = 1, + OTA_SLAVE_WORK_PAYLOAD, + OTA_SLAVE_WORK_END, +} ota_slave_work_op_t; + +typedef struct { + ota_slave_work_op_t op; + uint8_t master_mac[6]; + uint32_t status; + uint32_t bytes_written; + uint32_t error; + alox_EspNowOtaPayload payload; +} ota_slave_work_t; + static EventGroupHandle_t s_eg; +static QueueHandle_t s_slave_work_queue; +static bool s_distribution_active; typedef struct { uint8_t count; @@ -152,57 +173,38 @@ static bool wait_target_bits(uint32_t want_bits, uint32_t timeout_ms) { return (got & want_bits) == want_bits; } +bool ota_espnow_distribution_active(void) { return s_distribution_active; } + static void send_slave_status(const uint8_t master_mac[6], uint32_t status, uint32_t bytes_written, uint32_t error) { esp_now_comm_send_ota_status(master_mac, status, bytes_written, error); } -static void ota_slave_prepare_task(void *param) { - uint32_t total_size = (uint32_t)(uintptr_t)param; - uint8_t master_mac[6]; - - if (!esp_now_comm_get_master_mac(master_mac)) { - vTaskDelete(NULL); - return; +static bool queue_slave_work(const ota_slave_work_t *work) { + if (work == NULL || s_slave_work_queue == NULL) { + return false; } - - send_slave_status(master_mac, OTA_ST_PREPARING, 0, 0); - - int slot = ota_uart_prepare(total_size); - if (slot < 0) { - send_slave_status(master_mac, OTA_ST_FAILED, 0, 1); - vTaskDelete(NULL); - return; + if (xQueueSend(s_slave_work_queue, work, 0) != pdTRUE) { + ESP_LOGW(TAG, "slave OTA work queue full (op=%d)", (int)work->op); + return false; } - - send_slave_status(master_mac, OTA_ST_READY, 0, 0); - led_ring_show_ota_progress(0, total_size, OTA_LED_ESPNOW_RX_R, OTA_LED_ESPNOW_RX_G, - OTA_LED_ESPNOW_RX_B); - vTaskDelete(NULL); + return true; } -void ota_espnow_slave_on_start(const uint8_t master_mac[6], - const alox_EspNowOtaStart *start) { - if (start == NULL || start->total_size == 0) { - return; - } - - ESP_LOGI(TAG, "ESP-NOW OTA_START (%lu bytes)", (unsigned long)start->total_size); - - if (ota_uart_is_active()) { - send_slave_status(master_mac, OTA_ST_FAILED, 0, 4); - return; - } - - if (xTaskCreate(ota_slave_prepare_task, "ota_esp_prep", OTA_ESPNOW_PREPARE_STACK, - (void *)(uintptr_t)start->total_size, OTA_ESPNOW_PREPARE_PRIO, - NULL) != pdPASS) { - send_slave_status(master_mac, OTA_ST_FAILED, 0, 5); - } +static void queue_slave_status(const uint8_t master_mac[6], uint32_t status, + uint32_t bytes_written, uint32_t error) { + ota_slave_work_t work = { + .op = OTA_SLAVE_WORK_STATUS, + .status = status, + .bytes_written = bytes_written, + .error = error, + }; + memcpy(work.master_mac, master_mac, 6); + (void)queue_slave_work(&work); } -void ota_espnow_slave_on_payload(const uint8_t master_mac[6], - const alox_EspNowOtaPayload *payload) { +static void process_slave_payload(const uint8_t master_mac[6], + const alox_EspNowOtaPayload *payload) { if (payload == NULL || payload->data.size == 0) { send_slave_status(master_mac, OTA_ST_FAILED, 0, 11); return; @@ -246,7 +248,7 @@ void ota_espnow_slave_on_payload(const uint8_t master_mac[6], } } -void ota_espnow_slave_on_end(const uint8_t master_mac[6]) { +static void process_slave_end(const uint8_t master_mac[6]) { ESP_LOGI(TAG, "ESP-NOW OTA_END"); if (!ota_uart_is_active()) { send_slave_status(master_mac, OTA_ST_FAILED, 0, 20); @@ -268,6 +270,113 @@ void ota_espnow_slave_on_end(const uint8_t master_mac[6]) { (unsigned long)written); } +static void ota_slave_work_task(void *param) { + (void)param; + ota_slave_work_t work; + + while (1) { + if (xQueueReceive(s_slave_work_queue, &work, portMAX_DELAY) != pdTRUE) { + continue; + } + + switch (work.op) { + case OTA_SLAVE_WORK_STATUS: + send_slave_status(work.master_mac, work.status, work.bytes_written, + work.error); + break; + case OTA_SLAVE_WORK_PAYLOAD: + process_slave_payload(work.master_mac, &work.payload); + break; + case OTA_SLAVE_WORK_END: + process_slave_end(work.master_mac); + break; + default: + break; + } + } +} + +void ota_espnow_slave_init(void) { + if (s_slave_work_queue != NULL) { + return; + } + s_slave_work_queue = xQueueCreate(OTA_SLAVE_WORK_QUEUE_LEN, sizeof(ota_slave_work_t)); + if (s_slave_work_queue == NULL) { + ESP_LOGE(TAG, "failed to create slave OTA work queue"); + return; + } + if (xTaskCreate(ota_slave_work_task, "ota_slave_wrk", OTA_SLAVE_WORK_STACK, NULL, + OTA_SLAVE_WORK_PRIO, NULL) != pdPASS) { + ESP_LOGE(TAG, "failed to create slave OTA work task"); + } +} + +static void ota_slave_prepare_task(void *param) { + uint32_t total_size = (uint32_t)(uintptr_t)param; + uint8_t master_mac[6]; + + if (!esp_now_comm_get_master_mac(master_mac)) { + vTaskDelete(NULL); + return; + } + + send_slave_status(master_mac, OTA_ST_PREPARING, 0, 0); + + int slot = ota_uart_prepare(total_size); + if (slot < 0) { + send_slave_status(master_mac, OTA_ST_FAILED, 0, 1); + vTaskDelete(NULL); + return; + } + + send_slave_status(master_mac, OTA_ST_READY, 0, 0); + led_ring_show_ota_progress(0, total_size, OTA_LED_ESPNOW_RX_R, OTA_LED_ESPNOW_RX_G, + OTA_LED_ESPNOW_RX_B); + vTaskDelete(NULL); +} + +void ota_espnow_slave_on_start(const uint8_t master_mac[6], + const alox_EspNowOtaStart *start) { + if (start == NULL || start->total_size == 0) { + return; + } + + ESP_LOGI(TAG, "ESP-NOW OTA_START (%lu bytes)", (unsigned long)start->total_size); + + if (ota_uart_is_active()) { + queue_slave_status(master_mac, OTA_ST_FAILED, 0, 4); + return; + } + + if (xTaskCreate(ota_slave_prepare_task, "ota_esp_prep", OTA_ESPNOW_PREPARE_STACK, + (void *)(uintptr_t)start->total_size, OTA_ESPNOW_PREPARE_PRIO, + NULL) != pdPASS) { + queue_slave_status(master_mac, OTA_ST_FAILED, 0, 5); + } +} + +void ota_espnow_slave_on_payload(const uint8_t master_mac[6], + const alox_EspNowOtaPayload *payload) { + if (payload == NULL) { + queue_slave_status(master_mac, OTA_ST_FAILED, 0, 11); + return; + } + + ota_slave_work_t work = {.op = OTA_SLAVE_WORK_PAYLOAD, .payload = *payload}; + memcpy(work.master_mac, master_mac, 6); + if (!queue_slave_work(&work)) { + queue_slave_status(master_mac, OTA_ST_FAILED, 0, 14); + } +} + +void ota_espnow_slave_on_end(const uint8_t master_mac[6]) { + ota_slave_work_t work = {.op = OTA_SLAVE_WORK_END}; + memcpy(work.master_mac, master_mac, 6); + if (!queue_slave_work(&work)) { + queue_slave_status(master_mac, OTA_ST_FAILED, 0, 15); + } +} + void ota_espnow_master_on_status(const uint8_t slave_mac[6], const alox_EspNowOtaStatus *status) { if (status == NULL || s_eg == NULL) { @@ -342,6 +451,8 @@ static esp_err_t distribute_image(const esp_partition_t *partition, } } + s_distribution_active = true; + memset(&s_dist.progress, 0, sizeof(s_dist.progress)); if (progress != NULL) { s_dist.progress = *progress; @@ -362,6 +473,7 @@ static esp_err_t distribute_image(const esp_partition_t *partition, ESP_LOGW(TAG, "OTA_START to slave %lu failed", (unsigned long)s_dist.id[i]); prog_end(); + s_distribution_active = false; return err; } } @@ -369,6 +481,7 @@ static esp_err_t distribute_image(const esp_partition_t *partition, if (!wait_target_bits(target_mask, OTA_PREPARE_TIMEOUT_MS)) { ESP_LOGE(TAG, "timeout waiting for slave OTA ready"); prog_end(); + s_distribution_active = false; return ESP_ERR_TIMEOUT; } @@ -392,6 +505,7 @@ static esp_err_t distribute_image(const esp_partition_t *partition, ESP_LOGE(TAG, "partition read @%lu failed: %s", (unsigned long)offset, esp_err_to_name(err)); prog_end(); + s_distribution_active = false; return err; } @@ -407,6 +521,7 @@ static esp_err_t distribute_image(const esp_partition_t *partition, block_buf + sent, chunk); if (err != ESP_OK) { prog_end(); + s_distribution_active = false; return err; } } @@ -424,6 +539,7 @@ static esp_err_t distribute_image(const esp_partition_t *partition, ESP_LOGE(TAG, "timeout block ack @%lu bytes", (unsigned long)s_dist.expected_bytes); prog_end(); + s_distribution_active = false; return ESP_ERR_TIMEOUT; } ESP_LOGI(TAG, "block ack @%lu/%lu (%lu%%)", @@ -445,6 +561,7 @@ static esp_err_t distribute_image(const esp_partition_t *partition, err = esp_now_comm_send_ota_end(s_dist.mac[i]); if (err != ESP_OK) { prog_end(); + s_distribution_active = false; return err; } } @@ -452,11 +569,13 @@ static esp_err_t distribute_image(const esp_partition_t *partition, if (!wait_target_bits(target_mask, OTA_END_TIMEOUT_MS)) { ESP_LOGE(TAG, "timeout waiting for slave OTA success"); prog_end(); + s_distribution_active = false; return ESP_ERR_TIMEOUT; } prog_set_aggregate(size); prog_end(); + s_distribution_active = false; ESP_LOGI(TAG, "ESP-NOW OTA complete for %u slave(s)", (unsigned)s_dist.count); return ESP_OK; } diff --git a/main/ota_espnow.h b/main/ota_espnow.h index 0ed5fcc..f598ab7 100644 --- a/main/ota_espnow.h +++ b/main/ota_espnow.h @@ -37,4 +37,10 @@ void ota_espnow_slave_on_end(const uint8_t master_mac[6]); void ota_espnow_progress_query(uint32_t filter_client_id, alox_OtaSlaveProgressResponse *out); +/** True while master is pushing a staged image to slaves. */ +bool ota_espnow_distribution_active(void); + +/** Slave: work queue for OTA (no esp_now_send from recv callback). */ +void ota_espnow_slave_init(void); + #endif diff --git a/main/ota_session.c b/main/ota_session.c new file mode 100644 index 0000000..e8d1303 --- /dev/null +++ b/main/ota_session.c @@ -0,0 +1,25 @@ +#include "ota_session.h" +#include "ota_espnow.h" +#include "ota_uart.h" +#include "uart_messages.pb.h" + +bool ota_session_busy(void) { + return ota_uart_is_active() || ota_espnow_distribution_active(); +} + +bool ota_session_uart_cmd_allowed(uint16_t msg_id) { + if (!ota_session_busy()) { + return true; + } + + switch ((alox_MessageType)msg_id) { + case alox_MessageType_OTA_START: + case alox_MessageType_OTA_PAYLOAD: + case alox_MessageType_OTA_END: + case alox_MessageType_OTA_START_ESPNOW: + case alox_MessageType_OTA_SLAVE_PROGRESS: + return true; + default: + return false; + } +} diff --git a/main/ota_session.h b/main/ota_session.h new file mode 100644 index 0000000..3b0dd94 --- /dev/null +++ b/main/ota_session.h @@ -0,0 +1,13 @@ +#ifndef OTA_SESSION_H +#define OTA_SESSION_H + +#include +#include + +/** UART upload or ESP-NOW slave distribution in progress. */ +bool ota_session_busy(void); + +/** During OTA only UART OTA-related commands are accepted on the master. */ +bool ota_session_uart_cmd_allowed(uint16_t msg_id); + +#endif