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:
2026-11-01 22:16:28 +01:00
co-authored by Cursor
parent ae8ef40820
commit 178ab46cec
35 changed files with 236 additions and 79 deletions
+1
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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);
+1 -1
View File
@@ -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);
+7 -2
View File
@@ -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
View File
@@ -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
View File
@@ -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);
+6
View File
@@ -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
View File
@@ -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
View File
@@ -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);
}
}
+60
View File
@@ -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();
}
}
+14
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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) {
+5
View File
@@ -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);
}
+2 -2
View File
@@ -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;
+2 -2
View File
@@ -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;
+3 -3
View File
@@ -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;
+3 -3
View File
@@ -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;