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

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

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
simon 2026-05-31 16:35:18 +02:00
parent 0cbc4d0644
commit 0eea27a876
29 changed files with 1828 additions and 1416 deletions

View File

@ -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` (113)
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.

View File

@ -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

View File

@ -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{

View File

@ -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)

View File

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

View File

@ -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 {
if err := sp.port.SetReadTimeout(readTimeout); err != nil {
notify("error", "", 0, err.Error())
return err
}
}
sp := m.sp
if err := sp.port.SetReadTimeout(otaPrepareTimeout); 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")

View File

@ -11,6 +11,9 @@ 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
@ -19,6 +22,7 @@ type managedSerial struct {
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() {
func (m *managedSerial) withPortLocked(poll bool, fn func(*serialPort) error) error {
m.mu.Lock()
defer m.mu.Unlock()
if m.otaActive {
return errUARTBusy
}
} else {
m.mu.Lock()
}
defer m.mu.Unlock()
if m.sp == nil {
if err := m.openLocked(); err != nil {

View File

@ -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 (

View File

@ -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;

View File

@ -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"

View File

@ -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) |

View File

@ -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) {

View File

@ -67,15 +67,29 @@ 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) {
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) {
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;
uart_cmd_init_response(&response, alox_MessageType_BATTERY_STATUS,

View File

@ -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) {

View File

@ -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

View File

@ -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);
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) {

View File

@ -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;

View File

@ -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) {

File diff suppressed because it is too large Load Diff

172
main/esp_now_core.c Normal file
View File

@ -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 <stdio.h>
#include <string.h>
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)");
}
}

32
main/esp_now_core.h Normal file
View File

@ -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 <stdbool.h>
#include <stdint.h>
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

484
main/esp_now_master.c Normal file
View File

@ -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 <string.h>
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;
}

11
main/esp_now_master.h Normal file
View File

@ -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

579
main/esp_now_slave.c Normal file
View File

@ -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 <string.h>
#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;
}

14
main/esp_now_slave.h Normal file
View File

@ -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

View File

@ -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,56 +173,37 @@ 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],
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);
@ -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;
}

View File

@ -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

25
main/ota_session.c Normal file
View File

@ -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;
}
}

13
main/ota_session.h Normal file
View File

@ -0,0 +1,13 @@
#ifndef OTA_SESSION_H
#define OTA_SESSION_H
#include <stdbool.h>
#include <stdint.h>
/** 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