Make controller input best-effort and harden OTA rollback.
Accel/tap use fire-and-forget ESP-NOW with a 10 ms FreeRTOS-safe cadence, OTA recovers from seq gaps and auto-reboots after success, and bootloader app rollback confirms after a stable boot. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -39,6 +39,7 @@ idf_component_register(
|
||||
"ota_uart.c"
|
||||
"ota_espnow.c"
|
||||
"ota_session.c"
|
||||
"ota_confirm.c"
|
||||
"client_registry.c"
|
||||
"esp_now_comm.c"
|
||||
"esp_now_core.c"
|
||||
|
||||
+4
-4
@@ -124,7 +124,7 @@ Schema: `proto/esp_now_messages.proto`. Encode/decode: `esp_now_proto.c`. The ES
|
||||
| `ESPNOW_UNICAST_TEST` | Master → slave | `EspNowUnicastTest` (`seq`) |
|
||||
| `ESPNOW_FIND_ME` | Master → slave | `EspNowFindMe` (`client_id` filter) — LED locate sequence |
|
||||
| `ESPNOW_RESTART` | Master → slave | `EspNowRestart` (`client_id` filter) — reboot slave |
|
||||
| `ESPNOW_ACCEL_SAMPLE` | Slave → master | `EspNowAccelSample` (`slave_id`, `x`, `y`, `z` raw LSB) — ~every 16 ms |
|
||||
| `ESPNOW_ACCEL_SAMPLE` | Slave → master | `EspNowAccelSample` (`slave_id`, `x`, `y`, `z` raw LSB) — ~every 8 ms, best-effort |
|
||||
| `ESPNOW_SET_TAP_NOTIFY` | Master → slave | `EspNowTapNotify` (`client_id`, `single`, `double_tap`, `triple`) — which tap kinds to forward |
|
||||
| `ESPNOW_TAP_EVENT` | Slave → master | `EspNowTapEvent` (`client_id`, `kind`) — on BMA456 tap interrupt if notify enabled |
|
||||
| `ESPNOW_BATTERY_REPORT` | Slave → master | `EspNowBatteryReport` (`client_id`, `lipo1/2` mV) — ~every 30 s; cached in `client_registry` |
|
||||
@@ -376,7 +376,7 @@ go run . -port /dev/ttyUSB0 tap-notify -client 16
|
||||
|
||||
### CACHE_STATUS command
|
||||
|
||||
Read **cached** accel and/or tap data on the **master** in one UART round-trip. Slaves send `ESPNOW_ACCEL_SAMPLE` every **16 ms** when streaming; tap events arrive via `ESPNOW_TAP_EVENT` and are held up to **16 ms** (`CLIENT_REGISTRY_TAP_MAX_AGE_MS`). Pending taps are **consumed** on read (like the former `TAP_SNAPSHOT`).
|
||||
Read **cached** accel and/or tap data on the **master** in one UART round-trip. Slaves send `ESPNOW_ACCEL_SAMPLE` every **8 ms** (best-effort) when streaming; tap events arrive via `ESPNOW_TAP_EVENT` and are held up to **8 ms** (`CLIENT_REGISTRY_TAP_MAX_AGE_MS`). Pending taps are **consumed** on read (like the former `TAP_SNAPSHOT`).
|
||||
|
||||
**Request:** framed `1d` (`0x1d`) only — no body (`CacheStatusRequest` empty).
|
||||
|
||||
@@ -390,7 +390,7 @@ Read **cached** accel and/or tap data on the **master** in one UART round-trip.
|
||||
|
||||
Unsubscribed submessages are omitted on the wire (proto3 defaults). The master walks `client_registry` once per request (`cmd/cmd_cache_status.c`).
|
||||
|
||||
Host tools poll this at **16 ms** when live-stream / WebSocket receive is enabled. Tap events stay visible for **2 s** in the UI/API after first sight.
|
||||
Host tools poll this at **~8 ms** when live-stream / WebSocket receive is enabled. Tap events stay visible for **2 s** in the UI/API after first sight.
|
||||
|
||||
```bash
|
||||
go run . -port /dev/ttyUSB0 cache-status
|
||||
@@ -534,7 +534,7 @@ Fields per client: `id`, `mac`, `version`, `available`, `used`, `last_ping`, `la
|
||||
| `client_registry_heartbeat(mac, id, version, …)` | Same as upsert for heartbeats; reactivates inactive clients |
|
||||
| `client_registry_check_timeouts(timeout_ms)` | Mark stale clients inactive (master monitor task) |
|
||||
| `client_registry_count()` / `client_registry_at(i)` | Iterate for UART encoding |
|
||||
| `client_registry_set_tap_notify()` / `client_registry_take_tap()` | Tap notify flags + short-lived tap cache (16 ms) |
|
||||
| `client_registry_set_tap_notify()` / `client_registry_take_tap()` | Tap notify flags + short-lived tap cache (8 ms) |
|
||||
|
||||
Slaves register when the master receives `SLAVE_INFO` on the matching network; `HEARTBEAT` keeps them marked available. The registry **MAC is always the ESP-NOW source address** (`recv_info.src_addr`), not the optional `mac` bytes in the protobuf (used only on the wire for debugging).
|
||||
|
||||
|
||||
+8
-3
@@ -15,9 +15,10 @@
|
||||
|
||||
static const char *TAG = "[BLE_STREAM]";
|
||||
|
||||
#define BLE_STREAM_PERIOD_MIN_MS 8u
|
||||
/* Min matches FreeRTOS tick (HZ=100 → 10 ms); 8 ms would delay 0 ticks. */
|
||||
#define BLE_STREAM_PERIOD_MIN_MS 10u
|
||||
#define BLE_STREAM_PERIOD_MAX_MS 16u
|
||||
#define BLE_STREAM_PERIOD_DEFAULT_MS 16u
|
||||
#define BLE_STREAM_PERIOD_DEFAULT_MS 10u
|
||||
|
||||
static uint32_t s_period_ms = BLE_STREAM_PERIOD_DEFAULT_MS;
|
||||
static TaskHandle_t s_task;
|
||||
@@ -164,7 +165,11 @@ static void ble_stream_task(void *param) {
|
||||
}
|
||||
}
|
||||
|
||||
vTaskDelay(pdMS_TO_TICKS(period));
|
||||
TickType_t delay_ticks = pdMS_TO_TICKS(period);
|
||||
if (delay_ticks < 1) {
|
||||
delay_ticks = 1;
|
||||
}
|
||||
vTaskDelay(delay_ticks);
|
||||
}
|
||||
|
||||
s_task = NULL;
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
/** Clamp and store BLE push period (8–16 ms). */
|
||||
/** Clamp and store BLE push period (10–16 ms; min = FreeRTOS tick). */
|
||||
void ble_stream_set_period_ms(uint32_t period_ms);
|
||||
|
||||
uint32_t ble_stream_period_ms(void);
|
||||
|
||||
@@ -53,7 +53,7 @@ typedef struct {
|
||||
|
||||
#define CLIENT_REGISTRY_DEFAULT_ACCEL_DEADZONE 100u
|
||||
/** Tap events older than this are discarded (matches accel stream interval). */
|
||||
#define CLIENT_REGISTRY_TAP_MAX_AGE_MS 16u
|
||||
#define CLIENT_REGISTRY_TAP_MAX_AGE_MS 10u
|
||||
|
||||
void client_registry_init(void);
|
||||
|
||||
|
||||
@@ -111,8 +111,13 @@ void vCmdDispatcherTask(void *param) {
|
||||
bool handled = false;
|
||||
for (int i = 0; i < handler_count; i++) {
|
||||
if (handlers[i].msg_id == msg.msg_id) {
|
||||
ESP_LOGI(TAG, "trigger command %s (0x%02x)", message_type_name(msg.msg_id),
|
||||
(unsigned)msg.msg_id);
|
||||
if (msg.msg_id == (uint16_t)alox_MessageType_OTA_PAYLOAD) {
|
||||
ESP_LOGD(TAG, "trigger command %s (0x%02x)",
|
||||
message_type_name(msg.msg_id), (unsigned)msg.msg_id);
|
||||
} else {
|
||||
ESP_LOGI(TAG, "trigger command %s (0x%02x)",
|
||||
message_type_name(msg.msg_id), (unsigned)msg.msg_id);
|
||||
}
|
||||
handlers[i].callback(msg.payload, msg.len);
|
||||
handled = true;
|
||||
break;
|
||||
|
||||
+7
-1
@@ -2,6 +2,7 @@
|
||||
#include "led_ring.h"
|
||||
#include "ota_espnow.h"
|
||||
#include "ota_uart.h"
|
||||
#include "pod_reboot.h"
|
||||
#include "uart_cmd.h"
|
||||
#include "esp_log.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
@@ -179,8 +180,10 @@ static void handle_ota_payload(const uint8_t *data, size_t len) {
|
||||
|
||||
ota_feed_result_t r = ota_uart_feed_chunk(req_ptr->seq, req_ptr->data.bytes,
|
||||
req_ptr->data.size);
|
||||
/* Gap: keep session; host block-retries from the last ACK boundary. */
|
||||
if (r == OTA_FEED_SEQ_GAP) {
|
||||
send_ota_failed(16);
|
||||
ESP_LOGW(TAG, "OTA_PAYLOAD seq gap (got %lu) — waiting for host retry",
|
||||
(unsigned long)req_ptr->seq);
|
||||
return;
|
||||
}
|
||||
if (r == OTA_FEED_SEQ_DUP) {
|
||||
@@ -257,6 +260,8 @@ static void ota_distribute_task(void *param) {
|
||||
|
||||
led_ring_ota_success();
|
||||
free(job);
|
||||
/* Host already got SUCCESS; reboot onto the staged image. */
|
||||
pod_schedule_restart();
|
||||
vTaskDelete(NULL);
|
||||
}
|
||||
|
||||
@@ -332,6 +337,7 @@ static void ota_start_espnow_task(void *param) {
|
||||
response.payload.ota_status.error = 0;
|
||||
uart_cmd_send(&response, TAG);
|
||||
led_ring_ota_success();
|
||||
pod_schedule_restart();
|
||||
vTaskDelete(NULL);
|
||||
}
|
||||
|
||||
|
||||
+22
-7
@@ -204,10 +204,11 @@ esp_err_t esp_now_core_send_reliable(const uint8_t *dest_mac,
|
||||
ESPNOW_SEND_RETRY_DELAY_MS, true);
|
||||
}
|
||||
|
||||
esp_err_t esp_now_core_send_fast(const uint8_t *dest_mac,
|
||||
const alox_EspNowMessage *msg) {
|
||||
static esp_err_t send_fast_internal(const uint8_t *dest_mac,
|
||||
const alox_EspNowMessage *msg,
|
||||
bool quiet) {
|
||||
if (s_send_lock == NULL ||
|
||||
xSemaphoreTake(s_send_lock, pdMS_TO_TICKS(5000)) != pdTRUE) {
|
||||
xSemaphoreTake(s_send_lock, pdMS_TO_TICKS(5)) != pdTRUE) {
|
||||
return ESP_ERR_TIMEOUT;
|
||||
}
|
||||
|
||||
@@ -216,14 +217,18 @@ esp_err_t esp_now_core_send_fast(const uint8_t *dest_mac,
|
||||
|
||||
esp_err_t err = esp_now_proto_encode(msg, buf, sizeof(buf), &len);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGW(TAG, "encode failed");
|
||||
if (!quiet) {
|
||||
ESP_LOGW(TAG, "encode failed");
|
||||
}
|
||||
xSemaphoreGive(s_send_lock);
|
||||
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);
|
||||
if (!quiet) {
|
||||
ESP_LOGW(TAG, "encoded len %u > ESP-NOW max %u", (unsigned)len,
|
||||
(unsigned)ESP_NOW_MAX_DATA_LEN);
|
||||
}
|
||||
xSemaphoreGive(s_send_lock);
|
||||
return ESP_ERR_INVALID_SIZE;
|
||||
}
|
||||
@@ -234,7 +239,7 @@ esp_err_t esp_now_core_send_fast(const uint8_t *dest_mac,
|
||||
}
|
||||
|
||||
err = esp_now_send(dest_mac, buf, len);
|
||||
if (err != ESP_OK) {
|
||||
if (err != ESP_OK && !quiet) {
|
||||
ESP_LOGW(TAG, "send type=%u failed: %s", (unsigned)msg->type,
|
||||
esp_err_to_name(err));
|
||||
}
|
||||
@@ -242,6 +247,16 @@ esp_err_t esp_now_core_send_fast(const uint8_t *dest_mac,
|
||||
return err;
|
||||
}
|
||||
|
||||
esp_err_t esp_now_core_send_fast(const uint8_t *dest_mac,
|
||||
const alox_EspNowMessage *msg) {
|
||||
return send_fast_internal(dest_mac, msg, false);
|
||||
}
|
||||
|
||||
esp_err_t esp_now_core_send_best_effort(const uint8_t *dest_mac,
|
||||
const alox_EspNowMessage *msg) {
|
||||
return send_fast_internal(dest_mac, msg, true);
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
@@ -26,6 +26,12 @@ esp_err_t esp_now_core_send(const uint8_t *dest_mac,
|
||||
/** Like send but does not wait for esp_now send-done (lower latency). */
|
||||
esp_err_t esp_now_core_send_fast(const uint8_t *dest_mac,
|
||||
const alox_EspNowMessage *msg);
|
||||
/**
|
||||
* Fire-and-forget without failure logs — for high-rate gaming input
|
||||
* (accel/tap) where drops are expected under load.
|
||||
*/
|
||||
esp_err_t esp_now_core_send_best_effort(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);
|
||||
/** OTA payloads: wait for send-done with extra NO_MEM backoff (queue drain). */
|
||||
|
||||
+15
-5
@@ -23,7 +23,12 @@
|
||||
#define SLAVE_MASTER_LOST_MS (ESPNOW_HEARTBEAT_INTERVAL_MS * 5)
|
||||
/** While master or slave OTA is in progress (discover may be sparse). */
|
||||
#define SLAVE_MASTER_OTA_GRACE_MS 300000u
|
||||
#define ESPNOW_ACCEL_INTERVAL_MS 16
|
||||
/**
|
||||
* Best-effort gaming input cadence (loss OK; send_best_effort).
|
||||
* Must be >= one FreeRTOS tick (CONFIG_FREERTOS_HZ=100 → 10 ms); shorter
|
||||
* values become 0 ticks and assert in vTaskDelayUntil.
|
||||
*/
|
||||
#define ESPNOW_ACCEL_INTERVAL_MS 10
|
||||
#define ESPNOW_BATTERY_INTERVAL_MS 30000
|
||||
#define SLAVE_BATTERY_AFTER_JOIN_MS 150
|
||||
|
||||
@@ -165,7 +170,8 @@ static esp_err_t send_accel_sample(const uint8_t *dest_mac, uint32_t 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);
|
||||
/* Fire-and-forget: controller input prefers low latency over delivery. */
|
||||
return esp_now_core_send_best_effort(dest_mac, &msg);
|
||||
}
|
||||
|
||||
static esp_err_t send_tap_event(const uint8_t *dest_mac, uint32_t slave_id,
|
||||
@@ -175,7 +181,7 @@ static esp_err_t send_tap_event(const uint8_t *dest_mac, uint32_t slave_id,
|
||||
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);
|
||||
return esp_now_core_send_best_effort(dest_mac, &msg);
|
||||
}
|
||||
|
||||
static esp_err_t send_battery_report(const uint8_t *dest_mac,
|
||||
@@ -579,11 +585,15 @@ static void check_master_timeout(void) {
|
||||
static void accel_stream_task(void *param) {
|
||||
(void)param;
|
||||
|
||||
ESP_LOGI(TAG, "accel stream task (interval %u ms)",
|
||||
ESP_LOGI(TAG, "accel stream task (interval %u ms, best-effort)",
|
||||
(unsigned)ESPNOW_ACCEL_INTERVAL_MS);
|
||||
|
||||
TickType_t last_wake = xTaskGetTickCount();
|
||||
const TickType_t period_ticks = pdMS_TO_TICKS(ESPNOW_ACCEL_INTERVAL_MS);
|
||||
/* pdMS_TO_TICKS can be 0 when ms < tick period — DelayUntil asserts. */
|
||||
const TickType_t delay_ticks = period_ticks > 0 ? period_ticks : 1;
|
||||
while (1) {
|
||||
vTaskDelay(pdMS_TO_TICKS(ESPNOW_ACCEL_INTERVAL_MS));
|
||||
vTaskDelayUntil(&last_wake, delay_ticks);
|
||||
if (!s_joined || s_assigned_id == 0 || !s_accel_stream_enabled ||
|
||||
!bma456_is_ready()) {
|
||||
continue;
|
||||
|
||||
+11
-3
@@ -2,6 +2,7 @@
|
||||
#include "cmd_handler.h"
|
||||
#include "esp_log.h"
|
||||
#include "uart.h"
|
||||
#include "uart_messages.pb.h"
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
@@ -87,9 +88,16 @@ void host_rx_bytes(const uint8_t *data, size_t len) {
|
||||
|
||||
for (size_t i = 0; i < len; i++) {
|
||||
if (parse_uart_byte(data[i], &s_rx_packet)) {
|
||||
ESP_LOGI(TAG, "received message cmd=0x%02x len=%u",
|
||||
s_rx_packet.len > 0 ? s_rx_packet.payload[0] : 0,
|
||||
(unsigned)s_rx_packet.len);
|
||||
uint8_t cmd =
|
||||
s_rx_packet.len > 0 ? s_rx_packet.payload[0] : 0;
|
||||
/* OTA payloads are high-rate; avoid INFO spam starving the dispatcher. */
|
||||
if (cmd == (uint8_t)alox_MessageType_OTA_PAYLOAD) {
|
||||
ESP_LOGD(TAG, "received message cmd=0x%02x len=%u", cmd,
|
||||
(unsigned)s_rx_packet.len);
|
||||
} else {
|
||||
ESP_LOGI(TAG, "received message cmd=0x%02x len=%u", cmd,
|
||||
(unsigned)s_rx_packet.len);
|
||||
}
|
||||
host_enqueue_packet(s_rx_packet.payload, s_rx_packet.len);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
#include "ota_confirm.h"
|
||||
#include "esp_log.h"
|
||||
#include "esp_ota_ops.h"
|
||||
#include "esp_partition.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
|
||||
static const char *TAG = "[OTA_CONFIRM]";
|
||||
|
||||
/** Long enough for early crash loops (e.g. assert in a worker task) to reboot
|
||||
* before we cancel rollback; short enough that a follow-up OTA is not blocked
|
||||
* for long (esp_ota_begin fails while PENDING_VERIFY). */
|
||||
#define OTA_CONFIRM_DELAY_MS 8000u
|
||||
|
||||
static void ota_confirm_task(void *param) {
|
||||
(void)param;
|
||||
|
||||
vTaskDelay(pdMS_TO_TICKS(OTA_CONFIRM_DELAY_MS));
|
||||
|
||||
const esp_partition_t *running = esp_ota_get_running_partition();
|
||||
if (running == NULL) {
|
||||
ESP_LOGW(TAG, "no running partition");
|
||||
vTaskDelete(NULL);
|
||||
return;
|
||||
}
|
||||
|
||||
esp_ota_img_states_t state = ESP_OTA_IMG_UNDEFINED;
|
||||
esp_err_t err = esp_ota_get_state_partition(running, &state);
|
||||
if (err != ESP_OK) {
|
||||
/* otadata missing / non-OTA slot — nothing to confirm. */
|
||||
ESP_LOGI(TAG, "ota state unavailable (%s) — skip confirm",
|
||||
esp_err_to_name(err));
|
||||
vTaskDelete(NULL);
|
||||
return;
|
||||
}
|
||||
|
||||
if (state != ESP_OTA_IMG_PENDING_VERIFY) {
|
||||
ESP_LOGI(TAG, "running %s state=%d — no pending verify", running->label,
|
||||
(int)state);
|
||||
vTaskDelete(NULL);
|
||||
return;
|
||||
}
|
||||
|
||||
err = esp_ota_mark_app_valid_cancel_rollback();
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "mark_app_valid failed: %s", esp_err_to_name(err));
|
||||
} else {
|
||||
ESP_LOGI(TAG, "app confirmed valid (%s) — rollback cancelled",
|
||||
running->label);
|
||||
}
|
||||
vTaskDelete(NULL);
|
||||
}
|
||||
|
||||
void ota_confirm_schedule(void) {
|
||||
if (xTaskCreate(ota_confirm_task, "ota_confirm", 3072, NULL, 3, NULL) !=
|
||||
pdPASS) {
|
||||
ESP_LOGE(TAG, "failed to create confirm task — attempting immediate mark");
|
||||
(void)esp_ota_mark_app_valid_cancel_rollback();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
#ifndef OTA_CONFIRM_H
|
||||
#define OTA_CONFIRM_H
|
||||
|
||||
/**
|
||||
* After OTA with BOOTLOADER_APP_ROLLBACK_ENABLE, the new image boots as
|
||||
* ESP_OTA_IMG_PENDING_VERIFY. If we crash/reset before confirming, the
|
||||
* bootloader rolls back to the previous app on the next boot.
|
||||
*
|
||||
* Call once from app_main after init: schedules a delayed confirm so early
|
||||
* panics (task asserts, etc.) still trigger rollback.
|
||||
*/
|
||||
void ota_confirm_schedule(void);
|
||||
|
||||
#endif
|
||||
+18
-7
@@ -10,6 +10,7 @@
|
||||
#include "freertos/event_groups.h"
|
||||
#include "freertos/idf_additions.h"
|
||||
#include "ota_uart.h"
|
||||
#include "pod_reboot.h"
|
||||
#include <string.h>
|
||||
|
||||
static const char *TAG = "[OTA_ESPNOW]";
|
||||
@@ -186,6 +187,10 @@ static bool any_target_failed(void) {
|
||||
/**
|
||||
* Wait until all want_bits are set, or OTA_EG_FAIL_BIT (slave FAILED).
|
||||
* Returns true only when every target bit is set; false on fail or timeout.
|
||||
*
|
||||
* Wait only for *still missing* bits. Waiting on the full mask with "any bit"
|
||||
* busy-spins after the first slave ACK (already-set bits make WaitBits return
|
||||
* immediately) and starves IDLE → task_wdt.
|
||||
*/
|
||||
static bool wait_target_bits(uint32_t want_bits, uint32_t timeout_ms) {
|
||||
if (s_eg == NULL || want_bits == 0) {
|
||||
@@ -194,7 +199,6 @@ static bool wait_target_bits(uint32_t want_bits, uint32_t timeout_ms) {
|
||||
|
||||
const TickType_t start = xTaskGetTickCount();
|
||||
const TickType_t timeout_ticks = pdMS_TO_TICKS(timeout_ms);
|
||||
const EventBits_t wait_mask = want_bits | OTA_EG_FAIL_BIT;
|
||||
|
||||
while (true) {
|
||||
TickType_t elapsed = xTaskGetTickCount() - start;
|
||||
@@ -203,16 +207,19 @@ static bool wait_target_bits(uint32_t want_bits, uint32_t timeout_ms) {
|
||||
}
|
||||
TickType_t remain = timeout_ticks - elapsed;
|
||||
|
||||
EventBits_t got =
|
||||
xEventGroupWaitBits(s_eg, wait_mask, pdFALSE, pdFALSE, remain);
|
||||
if (got & OTA_EG_FAIL_BIT) {
|
||||
EventBits_t have = xEventGroupGetBits(s_eg);
|
||||
if (have & OTA_EG_FAIL_BIT) {
|
||||
xEventGroupClearBits(s_eg, OTA_EG_FAIL_BIT);
|
||||
return false;
|
||||
}
|
||||
if ((got & want_bits) == want_bits) {
|
||||
if ((have & want_bits) == want_bits) {
|
||||
xEventGroupClearBits(s_eg, want_bits);
|
||||
return true;
|
||||
}
|
||||
|
||||
EventBits_t missing = want_bits & ~have;
|
||||
(void)xEventGroupWaitBits(s_eg, missing | OTA_EG_FAIL_BIT, pdFALSE,
|
||||
pdFALSE, remain);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -314,9 +321,10 @@ static void process_slave_payload(const uint8_t master_mac[6],
|
||||
|
||||
ota_feed_result_t r = ota_uart_feed_chunk(payload->seq, payload->data.bytes,
|
||||
payload->data.size);
|
||||
/* Gap: keep session; master block-retries from the last ACK boundary. */
|
||||
if (r == OTA_FEED_SEQ_GAP) {
|
||||
led_ring_ota_failed();
|
||||
send_slave_status(master_mac, OTA_ST_FAILED, ota_uart_bytes_written(), 16);
|
||||
ESP_LOGW(TAG, "ESP-NOW OTA seq gap (got %lu) — waiting for retry",
|
||||
(unsigned long)payload->seq);
|
||||
return;
|
||||
}
|
||||
if (r == OTA_FEED_SEQ_DUP) {
|
||||
@@ -371,6 +379,9 @@ static void process_slave_end(const uint8_t master_mac[6]) {
|
||||
led_ring_ota_success();
|
||||
ESP_LOGI(TAG, "slave OTA success (%lu bytes), reboot to run",
|
||||
(unsigned long)written);
|
||||
/* Boot partition already set in ota_uart_finish(true); must reboot or we
|
||||
* keep running the old image while the master expects the new one. */
|
||||
pod_schedule_restart();
|
||||
}
|
||||
|
||||
static void ota_slave_work_task(void *param) {
|
||||
|
||||
+7
-1
@@ -97,7 +97,13 @@ int ota_uart_prepare(uint32_t total_size) {
|
||||
esp_ota_handle_t handle;
|
||||
esp_err_t err = esp_ota_begin(update_partition, OTA_SIZE_UNKNOWN, &handle);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "esp_ota_begin failed: %s", esp_err_to_name(err));
|
||||
if (err == ESP_ERR_OTA_ROLLBACK_INVALID_STATE) {
|
||||
ESP_LOGE(TAG,
|
||||
"esp_ota_begin blocked: running app still PENDING_VERIFY "
|
||||
"(wait for ota_confirm ~8 s after boot, or let rollback finish)");
|
||||
} else {
|
||||
ESP_LOGE(TAG, "esp_ota_begin failed: %s", esp_err_to_name(err));
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
|
||||
+3
-1
@@ -45,7 +45,9 @@ void ota_uart_abort(void);
|
||||
|
||||
/**
|
||||
* Append up to 200 bytes with strict seq checking (0, 1, 2, …).
|
||||
* Duplicates (seq < expected) return OTA_FEED_SEQ_DUP; gaps return OTA_FEED_SEQ_GAP.
|
||||
* Duplicates (seq < expected) return OTA_FEED_SEQ_DUP.
|
||||
* Gaps (seq > expected) return OTA_FEED_SEQ_GAP without consuming — caller
|
||||
* should keep the session so the host can retry the current flash block.
|
||||
*/
|
||||
ota_feed_result_t ota_uart_feed_chunk(uint32_t seq, const uint8_t *data, size_t len);
|
||||
|
||||
|
||||
+1
-5
@@ -355,10 +355,6 @@ esp_err_t pod_settings_load_id_map(pod_id_map_t *out) {
|
||||
(unsigned)len);
|
||||
return ESP_OK;
|
||||
}
|
||||
if (blob.count > POD_ID_MAP_MAX) {
|
||||
ESP_LOGW(TAG, "id_map count %u invalid", (unsigned)blob.count);
|
||||
return ESP_OK;
|
||||
}
|
||||
if (blob.next_id < POD_CLIENT_ID_MIN) {
|
||||
blob.next_id = (uint8_t)POD_CLIENT_ID_MIN;
|
||||
}
|
||||
@@ -375,7 +371,7 @@ esp_err_t pod_settings_load_id_map(pod_id_map_t *out) {
|
||||
}
|
||||
|
||||
esp_err_t pod_settings_save_id_map(const pod_id_map_t *map) {
|
||||
if (map == NULL || map->count > POD_ID_MAP_MAX) {
|
||||
if (map == NULL) {
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
if (map->next_id < POD_CLIENT_ID_MIN) {
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
#include "cmd_version.h"
|
||||
#include "cmd_ota.h"
|
||||
#include "cmd_ota_slave_progress.h"
|
||||
#include "ota_confirm.h"
|
||||
#include "cmd_led_ring.h"
|
||||
#include "cmd_battery.h"
|
||||
#include "cmd_set_log_level.h"
|
||||
@@ -236,6 +237,10 @@ void app_main(void) {
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "LED ring: UART LED_RING commands only (no local demo loop)");
|
||||
|
||||
/* After OTA: PENDING_VERIFY until confirm; crash before this → rollback. */
|
||||
ota_confirm_schedule();
|
||||
|
||||
while (1) {
|
||||
vTaskDelay(portMAX_DELAY);
|
||||
}
|
||||
|
||||
@@ -99,13 +99,13 @@ typedef struct _alox_EspNowAccelDeadzone {
|
||||
uint32_t client_id; /* 0 = all slaves; otherwise only matching slave_id applies */
|
||||
} alox_EspNowAccelDeadzone;
|
||||
|
||||
/* * Master → slave: enable/disable periodic accel ESP-NOW stream (~16 ms). */
|
||||
/* * Master → slave: enable/disable periodic accel ESP-NOW stream (~10 ms). */
|
||||
typedef struct _alox_EspNowAccelStream {
|
||||
bool enable;
|
||||
uint32_t client_id;
|
||||
} alox_EspNowAccelStream;
|
||||
|
||||
/* * Slave → master: latest BMA456 sample (sent ~every 16 ms). */
|
||||
/* * Slave → master: latest BMA456 sample (best-effort, ~every 10 ms). */
|
||||
typedef struct _alox_EspNowAccelSample {
|
||||
uint32_t slave_id;
|
||||
int32_t x;
|
||||
|
||||
@@ -90,13 +90,13 @@ message EspNowAccelDeadzone {
|
||||
uint32 client_id = 2; // 0 = all slaves; otherwise only matching slave_id applies
|
||||
}
|
||||
|
||||
/** Master → slave: enable/disable periodic accel ESP-NOW stream (~16 ms). */
|
||||
/** Master → slave: enable/disable periodic accel ESP-NOW stream (~10 ms). */
|
||||
message EspNowAccelStream {
|
||||
bool enable = 1;
|
||||
uint32 client_id = 2;
|
||||
}
|
||||
|
||||
/** Slave → master: latest BMA456 sample (sent ~every 16 ms). */
|
||||
/** Slave → master: latest BMA456 sample (best-effort, ~every 10 ms). */
|
||||
message EspNowAccelSample {
|
||||
uint32 slave_id = 1;
|
||||
sint32 x = 2;
|
||||
|
||||
@@ -31,7 +31,7 @@ typedef enum _alox_MessageType {
|
||||
alox_MessageType_ACCEL_STREAM = 25,
|
||||
alox_MessageType_BATTERY_STATUS = 26,
|
||||
alox_MessageType_TAP_NOTIFY = 27,
|
||||
/* * Combined cached accel + tap poll (one UART round-trip, ~16 ms cadence). */
|
||||
/* * Combined cached accel + tap poll (one UART round-trip, ~10 ms cadence). */
|
||||
alox_MessageType_CACHE_STATUS = 29,
|
||||
/* * Host → master → slave → master: timestamp echo round-trip (latency test). */
|
||||
alox_MessageType_ESPNOW_ECHO_PING = 30,
|
||||
@@ -118,9 +118,9 @@ typedef struct _alox_AccelDeadzoneResponse {
|
||||
uint32_t slaves_updated;
|
||||
} alox_AccelDeadzoneResponse;
|
||||
|
||||
/* Host → master: enable/disable slave accel ESP-NOW stream (~16 ms per slave).
|
||||
/* Host → master: enable/disable slave accel ESP-NOW stream (~10 ms per slave, best-effort).
|
||||
write=false: read; write=true: apply. client_id 0 invalid for write (use >0 or all_clients).
|
||||
period_ms (8–16): BLE host push interval for BleStreamTick; ignored on UART (default 16). */
|
||||
period_ms (10–16): BLE host push interval for BleStreamTick; ignored on UART (default 10). */
|
||||
typedef struct _alox_AccelStreamRequest {
|
||||
bool write;
|
||||
bool enable;
|
||||
|
||||
@@ -27,7 +27,7 @@ enum MessageType {
|
||||
BATTERY_STATUS = 26;
|
||||
TAP_NOTIFY = 27;
|
||||
reserved 28;
|
||||
/** Combined cached accel + tap poll (one UART round-trip, ~16 ms cadence). */
|
||||
/** Combined cached accel + tap poll (one UART round-trip, ~10 ms cadence). */
|
||||
CACHE_STATUS = 29;
|
||||
/** Host → master → slave → master: timestamp echo round-trip (latency test). */
|
||||
ESPNOW_ECHO_PING = 30;
|
||||
@@ -149,9 +149,9 @@ message AccelDeadzoneResponse {
|
||||
uint32 slaves_updated = 4;
|
||||
}
|
||||
|
||||
// Host → master: enable/disable slave accel ESP-NOW stream (~16 ms per slave).
|
||||
// Host → master: enable/disable slave accel ESP-NOW stream (~10 ms per slave, best-effort).
|
||||
// write=false: read; write=true: apply. client_id 0 invalid for write (use >0 or all_clients).
|
||||
// period_ms (8–16): BLE host push interval for BleStreamTick; ignored on UART (default 16).
|
||||
// period_ms (10–16): BLE host push interval for BleStreamTick; ignored on UART (default 10).
|
||||
message AccelStreamRequest {
|
||||
bool write = 1;
|
||||
bool enable = 2;
|
||||
|
||||
Reference in New Issue
Block a user