diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index f9b43bf..aa965a1 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -315,7 +315,7 @@ Alle Master-Sends laufen über `send_message()` / `send_message_ex()`: | Nachricht | Task / Trigger | Master-Ziel | |-----------|----------------|-------------| -| `ESPNOW_ACCEL_SAMPLE` | `slave_accel_stream_task` 10 ms, 1 ms slots via Discover `master_time_us` | `client_registry_update_accel()` | +| `ESPNOW_ACCEL_SAMPLE` | `accel_stream_task` poll 10 ms + deadzone (nur bei Änderung), 1 ms slots via Discover `master_time_us` | `client_registry_update_accel()` | | `ESPNOW_TAP_EVENT` | BMA456-IRQ → `on_bma456_tap` | `client_registry_update_tap()` | | `ESPNOW_BATTERY_REPORT` | Heartbeat + nach Join | `client_registry_update_battery()` | | `ESPNOW_SLAVE_INFO` / `HEARTBEAT` | `slave_tx_task` / Heartbeat-Task | `client_registry_heartbeat()` | @@ -366,7 +366,7 @@ flowchart TB | `espnow_mon` | Master | 1 s Timeout + Master-Battery | | `espnow_stx` | Slave | Queue: SLAVE_INFO, Battery nach Join | | `espnow_hb` | Slave | 1 s Heartbeat + 30 s Battery | -| `espnow_accel` | Slave | 10 ms Accel in ID-Slot (1 ms) wenn Stream an | +| `espnow_accel` | Slave | Accel poll 10 ms / TX bei Deadzone-Änderung, ID-Slot 1 ms wenn Stream an | | `bma456_poll` | Beide | 10 Hz (`bosch456.c`) | | `vTaskLedRing` | Beide | LED-Ring-Befehle | diff --git a/docs/DOCUMENTATION.md b/docs/DOCUMENTATION.md index 8fa7fbc..6dcd324 100644 --- a/docs/DOCUMENTATION.md +++ b/docs/DOCUMENTATION.md @@ -177,7 +177,7 @@ UART_CMD_REQ(&uart_msg, alox_UartMessage_accel_deadzone_request_tag, accel_deadz | `ESPNOW_HEARTBEAT` | S→M | `heartbeat` | Keepalive 1 s | | `ESPNOW_SET_ACCEL_DEADZONE` | M→S | `accel_deadzone` | Deadzone LSB | | `ESPNOW_SET_TAP_CONFIG` | M→S | `tap_config` | Multitap-Parameter + NVS | -| `ESPNOW_SET_ACCEL_STREAM` | M→S | `accel_stream` | Stream ~10 ms (best-effort) | +| `ESPNOW_SET_ACCEL_STREAM` | M→S | `accel_stream` | Stream poll ~10 ms, TX on deadzone change | | `ESPNOW_ACCEL_SAMPLE` | S→M | `accel_sample` | x/y/z Roh-LSB | | `ESPNOW_SET_TAP_NOTIFY` | M→S | `tap_notify` | Tap-Arten filtern | | `ESPNOW_TAP_EVENT` | S→M | `tap_event` | kind 1/2/3 | @@ -199,7 +199,7 @@ UART_CMD_REQ(&uart_msg, alox_UartMessage_accel_deadzone_request_tag, accel_deadz | Heartbeat | 1000 ms | | Client-Timeout (Master) | 3 × Heartbeat = 3 s → `available=false` | | Master-Verlust (Slave) | 5 s ohne Discover | -| Accel-Stream | 10 ms (best-effort, kein ACK-Wait; = FreeRTOS-Tick) | +| Accel-Stream | Poll 10 ms; TX nur bei Deadzone-Änderung (best-effort) | | Batterie-Report | 30 s (+ einmal 150 ms nach Join) | | Echo-Ping Timeout (Master) | 500 ms (`ESPNOW_ECHO_PING_TIMEOUT_MS`) | @@ -262,7 +262,7 @@ Nur auf dem **Master** registriert (`powerpod.c`). IDs aus `MessageType` in `uar - Optional `period_ms` (10–16): BLE-Host-Push-Intervall für `BLE_STREAM_TICK` (UART ignoriert; Default 10). - Master setzt `client_registry_set_accel_stream` und sendet `ESPNOW_SET_ACCEL_STREAM`. -- Slave-Task `slave_accel_stream_task` sendet `ESPNOW_ACCEL_SAMPLE` alle 10 ms (best-effort / `send_best_effort`). +- Slave-Task `accel_stream_task` pollt alle 10 ms; sendet `ESPNOW_ACCEL_SAMPLE` nur wenn eine Achse die Accel-Deadzone seit dem letzten gesendeten Sample überschreitet (best-effort / `send_best_effort`; Baseline-Reset beim Stream-Enable). ### 7.5 TAP_NOTIFY (27) @@ -371,6 +371,7 @@ Implementierung: `ota_uart.c`, Steuerung `cmd/cmd_ota.c`. Gleiches Protokoll üb - Inaktive Partition: `esp_ota_get_next_update_partition()`. - LED-Ring zeigt Fortschritt (blau Schreiben, grün Verteilung). - BLE: ATT MTU ≥ ~219 (preferred 517); große Antworten werden über mehrere Notifies gestückelt; `BLE_STREAM_TICK` pausiert während `ota_session_busy()`. +- **BLE+ESP-NOW Coex:** Tag `wifi` auf `ESP_LOG_NONE` (WiFi-Task-Stack + Error-Logs → Overflow/Panic); während ESP-NOW-Dist `OTA_STATUS` DISTRIBUTING max. ~4/s und kürzere BLE-Notify-Retries. - **App-Rollback** (`CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE`): neues Image bootet als `PENDING_VERIFY`. `ota_confirm` markiert nach ~8 s stabilem Lauf `mark_app_valid`; Crash/Reset davor → Bootloader rollt auf die vorherige App zurück. Während `PENDING_VERIFY` ist ein neues `OTA_START` blockiert. ### 8.2 ESP-NOW-OTA (Master → Slaves) @@ -382,7 +383,7 @@ Implementierung: `ota_uart.c`, Steuerung `cmd/cmd_ota.c`. Gleiches Protokoll üb 3. `ESPNOW_OTA_END` → Boot-Partition setzen + automatischer Reboot 4. Slave antwortet `ESPNOW_OTA_STATUS` (mit `send_message_ex` + Semaphore auf Master) -Nach Erfolg: Master und Slaves starten automatisch neu (Rollback-Confirm siehe 8.1). +Während Distribution: keine Client-Timeouts in der Registry (Heartbeats werden im recv_cb verworfen). Nach Erfolg: Master und Slaves starten automatisch neu (Rollback-Confirm siehe 8.1). ### 8.3 OTA_SLAVE_PROGRESS (21) diff --git a/main/ble_host.c b/main/ble_host.c index 5c4e8f8..906984c 100644 --- a/main/ble_host.c +++ b/main/ble_host.c @@ -3,6 +3,7 @@ #include "esp_timer.h" #include "host_transport.h" #include "nvs_flash.h" +#include "ota_espnow.h" #include "freertos/FreeRTOS.h" #include "freertos/semphr.h" #include "freertos/task.h" @@ -26,9 +27,12 @@ static const char *DEVICE_NAME = "Powerpod"; /** Prefer large ATT MTU so CLIENT_INFO / BATTERY fit in fewer notify chunks. */ #define BLE_PREFERRED_ATT_MTU 517 #define BLE_NOTIFY_CHUNK_RETRIES 24 +/** During ESP-NOW OTA dist, fail notify sooner so WiFi/ESP-NOW keeps airtime. */ +#define BLE_NOTIFY_CHUNK_RETRIES_OTA_DIST 4 #define BLE_NOTIFY_CHUNK_RETRY_MS 5 /** Pace multi-chunk notifies so the central parser sees contiguous frame bytes. */ #define BLE_NOTIFY_CHUNK_GAP_MS 8 +#define BLE_NOTIFY_CHUNK_GAP_OTA_DIST_MS 2 /* * Nordic UART style: @@ -73,7 +77,10 @@ uint16_t ble_host_max_payload(void) { * One ATT notification (≤ MTU-3). Retries on transient NimBLE buffer pressure. */ static esp_err_t ble_notify_chunk(const uint8_t *data, uint16_t len) { - for (int attempt = 0; attempt < BLE_NOTIFY_CHUNK_RETRIES; attempt++) { + const int max_retries = ota_espnow_distribution_active() + ? BLE_NOTIFY_CHUNK_RETRIES_OTA_DIST + : BLE_NOTIFY_CHUNK_RETRIES; + for (int attempt = 0; attempt < max_retries; attempt++) { struct os_mbuf *om = ble_hs_mbuf_from_flat(data, len); if (om == NULL) { vTaskDelay(pdMS_TO_TICKS(BLE_NOTIFY_CHUNK_RETRY_MS)); @@ -140,7 +147,10 @@ static esp_err_t ble_host_tx(const uint8_t *frame, size_t frame_len) { } off += n; if (off < frame_len) { - vTaskDelay(pdMS_TO_TICKS(BLE_NOTIFY_CHUNK_GAP_MS)); + const uint32_t gap_ms = ota_espnow_distribution_active() + ? BLE_NOTIFY_CHUNK_GAP_OTA_DIST_MS + : BLE_NOTIFY_CHUNK_GAP_MS; + vTaskDelay(pdMS_TO_TICKS(gap_ms)); } } diff --git a/main/bosch456.c b/main/bosch456.c index b3d1cdc..39d9a77 100644 --- a/main/bosch456.c +++ b/main/bosch456.c @@ -2,7 +2,7 @@ * BMA456H integration for Powerpod (ESP-IDF I2C master + Bosch SensorAPI). * * Polls accelerometer at 10 Hz; tap events arrive on BMA456_INT_GPIO. - * Accel logging is filtered in software (deadzone); slaves stream samples via ESP-NOW. + * Accel log + ESP-NOW stream share a software deadzone filter (emit on change). */ #include "bosch456.h" @@ -33,6 +33,10 @@ static int16_t s_last_x; static int16_t s_last_y; static int16_t s_last_z; static bool s_have_last_sample; +static int16_t s_stream_last_x; +static int16_t s_stream_last_y; +static int16_t s_stream_last_z; +static bool s_stream_have_last; static volatile bool s_int_pending; static SemaphoreHandle_t s_accel_mutex; @@ -119,6 +123,7 @@ bool bma456_is_ready(void) { return s_bma456_ready; } void bma456_set_accel_deadzone(uint32_t deadzone_lsb) { s_accel_deadzone = deadzone_lsb; s_have_last_sample = false; + s_stream_have_last = false; if (s_bma456_ready) { ESP_LOGI(TAG, "accel deadzone %lu LSB", (unsigned long)deadzone_lsb); } @@ -126,6 +131,23 @@ void bma456_set_accel_deadzone(uint32_t deadzone_lsb) { uint32_t bma456_get_accel_deadzone(void) { return s_accel_deadzone; } +void bma456_accel_stream_reset_baseline(void) { s_stream_have_last = false; } + +bool bma456_accel_stream_take_if_changed(int16_t x, int16_t y, int16_t z) { + if (s_stream_have_last) { + if (axis_delta(x, s_stream_last_x) <= (int16_t)s_accel_deadzone && + axis_delta(y, s_stream_last_y) <= (int16_t)s_accel_deadzone && + axis_delta(z, s_stream_last_z) <= (int16_t)s_accel_deadzone) { + return false; + } + } + s_stream_last_x = x; + s_stream_last_y = y; + s_stream_last_z = z; + s_stream_have_last = true; + return true; +} + bool bma456_tap_config_valid(const bma456_tap_config_t *cfg) { if (cfg == NULL) { return false; diff --git a/main/bosch456.h b/main/bosch456.h index b46424b..86c1d8f 100644 --- a/main/bosch456.h +++ b/main/bosch456.h @@ -19,7 +19,7 @@ /** Sensor interrupt line → ESP32 GPIO (active high, rising edge). */ #define BMA456_INT_GPIO 10 -/** Software filter: log accel only when |axis - last| > deadzone (raw LSB). */ +/** Software filter: emit accel only when |axis - last| > deadzone (raw LSB). */ #define BMA456_DEFAULT_ACCEL_DEADZONE 100u /** @@ -81,6 +81,16 @@ bool bma456_is_ready(void); void bma456_set_accel_deadzone(uint32_t deadzone_lsb); uint32_t bma456_get_accel_deadzone(void); +/** + * ESP-NOW stream filter: true if sample exceeds deadzone vs last streamed + * sample (or no baseline yet). On true, commits (x,y,z) as stream baseline. + * Independent of the log filter baseline. + */ +bool bma456_accel_stream_take_if_changed(int16_t x, int16_t y, int16_t z); + +/** Force next stream take to accept (call when accel stream is enabled). */ +void bma456_accel_stream_reset_baseline(void); + /** Log accel when any axis moved more than deadzone since last reported sample. */ void bma456_report_accel_if_changed(int16_t x, int16_t y, int16_t z); diff --git a/main/cmd/cmd_ota.c b/main/cmd/cmd_ota.c index f64a634..15ff8cb 100644 --- a/main/cmd/cmd_ota.c +++ b/main/cmd/cmd_ota.c @@ -5,8 +5,10 @@ #include "pod_reboot.h" #include "uart_cmd.h" #include "esp_log.h" +#include "esp_timer.h" #include "freertos/FreeRTOS.h" #include "freertos/idf_additions.h" +#include #include #include @@ -32,6 +34,14 @@ typedef struct { uint32_t written; int slot; } ota_dist_job_t; + +/** Cap host DISTRIBUTING pushes (BLE notifies compete with ESP-NOW OTA TX). */ +#define OTA_DIST_NOTIFY_MIN_US (250 * 1000ull) + +static int64_t s_last_dist_agg_us; +static int64_t s_last_dist_slave_us; +static uint32_t s_last_dist_agg_bytes = UINT32_MAX; + static void send_ota_status(ota_uart_status_t status, uint32_t err_code) { alox_UartMessage response; uart_cmd_init_response(&response, alox_MessageType_OTA_STATUS, @@ -64,15 +74,33 @@ static void send_ota_distributing(uint32_t kind, uint32_t bytes_done, static void ota_dist_aggregate(uint32_t bytes_done, uint32_t total_bytes, uint8_t slave_count) { - (void)slave_count; led_ring_show_ota_progress(bytes_done, total_bytes, OTA_LED_ESPNOW_TX_R, OTA_LED_ESPNOW_TX_G, OTA_LED_ESPNOW_TX_B); + + const int64_t now = esp_timer_get_time(); + const bool first = (s_last_dist_agg_bytes == UINT32_MAX); + const bool boundary = + (bytes_done == 0) || (total_bytes > 0 && bytes_done >= total_bytes); + const bool paced = + (now - s_last_dist_agg_us) >= (int64_t)OTA_DIST_NOTIFY_MIN_US; + if (!first && !boundary && !paced) { + return; + } + s_last_dist_agg_us = now; + s_last_dist_agg_bytes = bytes_done; send_ota_distributing(OTA_DIST_AGGREGATE, bytes_done, (uint32_t)slave_count); } static void ota_dist_per_slave(uint32_t slave_id, uint32_t bytes_done, uint32_t total_bytes) { (void)total_bytes; + /* Host also polls OTA_SLAVE_PROGRESS; keep per-slave pushes rare on BLE. */ + const int64_t now = esp_timer_get_time(); + if ((now - s_last_dist_slave_us) < (int64_t)OTA_DIST_NOTIFY_MIN_US && + bytes_done > 0) { + return; + } + s_last_dist_slave_us = now; send_ota_distributing(OTA_DIST_PER_SLAVE, bytes_done, slave_id); } @@ -225,6 +253,9 @@ static void ota_distribute_task(void *param) { led_ring_show_ota_progress(0, image_size, OTA_LED_ESPNOW_TX_R, OTA_LED_ESPNOW_TX_G, OTA_LED_ESPNOW_TX_B); + s_last_dist_agg_us = 0; + s_last_dist_slave_us = 0; + s_last_dist_agg_bytes = UINT32_MAX; send_ota_distributing(OTA_DIST_AGGREGATE, 0, 0); esp_err_t err = ota_espnow_distribute(part, image_size, &s_dist_progress); @@ -315,6 +346,10 @@ static void ota_start_espnow_task(void *param) { return; } + s_last_dist_agg_us = 0; + s_last_dist_slave_us = 0; + s_last_dist_agg_bytes = UINT32_MAX; + esp_err_t err = ota_espnow_distribute(part, image_size, &s_dist_progress); if (err != ESP_OK) { send_ota_failed(42); diff --git a/main/esp_now_core.c b/main/esp_now_core.c index 015b782..d236840 100644 --- a/main/esp_now_core.c +++ b/main/esp_now_core.c @@ -280,6 +280,14 @@ esp_err_t esp_now_core_init_radio(uint8_t channel) { ESP_ERROR_CHECK(esp_wifi_set_ps(WIFI_PS_NONE)); ESP_ERROR_CHECK(esp_wifi_set_channel(channel, WIFI_SECOND_CHAN_NONE)); + /* WiFi task stack is tiny; ERROR logs from pp/coex (e.g. under BLE+ESP-NOW + * OTA) call esp_log_write on that stack and can overflow → panic reboot. + * Mute after esp_wifi_init (which resets the wifi tag to default level). */ + esp_log_level_set("wifi", ESP_LOG_NONE); + esp_log_level_set("wifi_init", ESP_LOG_WARN); + esp_log_level_set("coex", ESP_LOG_WARN); + esp_log_level_set("NimBLE", ESP_LOG_WARN); + ESP_ERROR_CHECK(esp_read_mac(s_own_mac, ESP_MAC_WIFI_STA)); s_wifi_channel = channel; return ESP_OK; diff --git a/main/esp_now_master.c b/main/esp_now_master.c index 0b457af..6458c3b 100644 --- a/main/esp_now_master.c +++ b/main/esp_now_master.c @@ -780,7 +780,11 @@ static void monitor_task(void *param) { while (1) { vTaskDelay(pdMS_TO_TICKS(ESPNOW_HEARTBEAT_INTERVAL_MS)); - client_registry_check_timeouts(ESPNOW_CLIENT_TIMEOUT_MS); + /* During ESP-NOW OTA dist the recv_cb drops heartbeats; do not wipe + * targets from the registry while they are still being updated. */ + if (!ota_espnow_distribution_active()) { + 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) { diff --git a/main/esp_now_slave.c b/main/esp_now_slave.c index 8e5dd3c..a7bf67d 100644 --- a/main/esp_now_slave.c +++ b/main/esp_now_slave.c @@ -26,7 +26,8 @@ /** While master or slave OTA is in progress (discover may be sparse). */ #define SLAVE_MASTER_OTA_GRACE_MS 300000u /** - * Accel stream cadence (10 ms frame). TX path is selectable (INPUT_SEND_MODE). + * Accel stream poll cadence (10 ms frame). TX only when sample exceeds the + * BMA456 deadzone vs last sent sample. TX path is selectable (INPUT_SEND_MODE). * With master Discover.master_time_us, slaves phase into 1 ms slots: * offset_ms = (assigned_id - 1) % 8. Slot wait uses esp_timer (FreeRTOS tick * is 10 ms — too coarse for intra-frame offsets). @@ -516,6 +517,9 @@ static void handle_accel_stream(const uint8_t *master_mac, return; } s_accel_stream_enabled = cfg->enable; + if (cfg->enable) { + bma456_accel_stream_reset_baseline(); + } 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)", @@ -743,7 +747,8 @@ static void accel_stream_task(void *param) { (void)param; ESP_LOGI(TAG, - "accel stream task (interval %u ms, %u×%llu us slots, mode selectable)", + "accel stream task (poll %u ms, deadzone TX, %u×%llu us slots, " + "mode selectable)", (unsigned)ESPNOW_ACCEL_INTERVAL_MS, (unsigned)ESPNOW_ACCEL_SLOT_COUNT, (unsigned long long)ESPNOW_ACCEL_SLOT_US); @@ -759,6 +764,9 @@ static void accel_stream_task(void *param) { if (bma456_read_accel(&x, &y, &z) != ESP_OK) { continue; } + if (!bma456_accel_stream_take_if_changed(x, y, z)) { + continue; + } (void)send_accel_sample(s_master_mac, s_assigned_id, x, y, z); } }