Compare commits

...
3 Commits
Author SHA1 Message Date
skrueckenandCursor 8f90dfe82f Send ESP-NOW accel only on deadzone change and harden BLE+WiFi OTA coex.
Quiet pods no longer spam 100 Hz samples; mute wifi logs and throttle BLE
notifies during slave distribution so the wifi task stack does not panic.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-11-02 00:55:50 +01:00
skrueckenandCursor 141176a824 Send large BLE host frames across multiple notifies.
Prefer ATT MTU 517 and chunk oversized responses so CLIENT_INFO/BATTERY work with many pods; reset the goTool parser after timeouts so partial frames do not poison the next exchange.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-11-01 23:53:40 +01:00
skrueckenandCursor 9046677460 Raise host frame payload limit so multi-client UART replies fit.
Use a uint16 LE length and 1024-byte max payload so CLIENT_INFO and BATTERY_STATUS no longer fail encoding past ~10 connected pods.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-11-01 23:43:33 +01:00
21 changed files with 294 additions and 91 deletions
+5 -5
View File
@@ -109,7 +109,7 @@ sequenceDiagram
└─────────────────────────────────────────────────────────────┘
```
**Host UART vs BLE:** Nur einer aktiv (DIP8). Gleiches Framing (`0xAA`…`0xCC`) und dieselbe `cmd_queue`. BLE: NUS-ähnliche GATT-Char, Write=RX, Notify=TX. `gotool serve` verbindet UART oder BLE über Dashboard/REST/WS (`managedHost`); **OTA** über UART oder BLE (ATT MTU ≥ ~219). Bei BLE-Host und aktivem Accel-Stream pusht `ble_stream` periodisch `BLE_STREAM_TICK` (10–16 ms, Default 10; pausiert während OTA) mit `esp_timestamp_us` (Live-Pfad bevorzugt Tick statt CACHE_STATUS-Poll).
**Host UART vs BLE:** Nur einer aktiv (DIP8). Gleiches Framing (`0xAA`…`0xCC`) und dieselbe `cmd_queue`. BLE: NUS-ähnliche GATT-Char, Write=RX, Notify=TX; Frames größer als ATT MTU-3 werden in aufeinanderfolgende Notifies gestückelt (Host-Parser reassembliert). `gotool serve` verbindet UART oder BLE über Dashboard/REST/WS (`managedHost`); **OTA** über UART oder BLE (ATT MTU ≥ ~219, preferred 517). Bei BLE-Host und aktivem Accel-Stream pusht `ble_stream` periodisch `BLE_STREAM_TICK` (10–16 ms, Default 10; pausiert während OTA) mit `esp_timestamp_us` (Live-Pfad bevorzugt Tick statt CACHE_STATUS-Poll).
---
@@ -225,12 +225,12 @@ Unabhängig von Protobuf — reines Bytestream-Framing:
| Feld | Wert |
|------|------|
| Start | `0xAA` |
| Länge | 1 Byte (1–252) |
| Länge | 2 Bytes little-endian (1–1024) |
| Payload | `length` Bytes |
| Prüfsumme | XOR aller Payload-Bytes |
| Stopp | `0xCC` |
**Parameter:** `main/uart.h` — `UART_NUM_1`, `921600` Baud, TX GPIO **2**, RX GPIO **3**, `MAX_PAYLOAD_SIZE` 248.
**Parameter:** `main/uart.h` — `UART_NUM_1`, `921600` Baud, TX GPIO **2**, RX GPIO **3**, `MAX_PAYLOAD_SIZE` 1024.
### 5.2 Nutzlast (Anwendung)
@@ -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 |
+10 -9
View File
@@ -124,10 +124,10 @@ idf.py -p /dev/ttyUSB0 flash monitor
| Byte | Inhalt |
|------|--------|
| 0 | `0xAA` Start |
| 1 | Länge N (1–252) |
| 2…N+1 | Payload |
| N+2 | XOR-Checksum über Payload |
| N+3 | `0xCC` Stopp |
| 1–2 | Länge N little-endian (1–1024) |
| 3…N+2 | Payload |
| N+3 | XOR-Checksum über Payload |
| N+4 | `0xCC` Stopp |
Implementierung: `uart.c` — `parse_uart_byte()`, `uart_send_framed()`.
@@ -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)
@@ -370,7 +370,8 @@ 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 247); `BLE_STREAM_TICK` pausiert während `ota_session_busy()`.
- 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)
+1 -1
View File
@@ -287,7 +287,7 @@ Entspricht `main/README.md`:
| Byte | Inhalt |
|------|--------|
| Frame | `0xAA`, Länge, Payload, XOR-Checksum, `0xCC` |
| Frame | `0xAA`, Länge (uint16 LE), Payload, XOR-Checksum, `0xCC` |
| Payload[0] | `MessageType` (z.B. `22` = FIND_ME) |
| Payload[1…] | Nanopb-`UartMessage` |
+13
View File
@@ -22,6 +22,7 @@ type bleLink struct {
device bluetooth.Device
char bluetooth.DeviceCharacteristic
mu sync.Mutex
parserMu sync.Mutex
parser *uartframe.Parser
respCh chan []byte
streamCh chan []byte
@@ -162,6 +163,8 @@ func connectBLEDevice(addr bluetooth.Address, settle time.Duration) (*bleLink, e
}
func (b *bleLink) onNotify(buf []byte) {
b.parserMu.Lock()
defer b.parserMu.Unlock()
for _, by := range buf {
payload, ok, err := b.parser.Feed(by)
if err != nil || !ok {
@@ -195,6 +198,12 @@ func (b *bleLink) onNotify(buf []byte) {
}
}
func (b *bleLink) resetParser() {
b.parserMu.Lock()
b.parser = uartframe.NewParser()
b.parserMu.Unlock()
}
func (b *bleLink) Close() error {
return b.device.Disconnect()
}
@@ -243,6 +252,9 @@ sent:
for {
remain := time.Until(deadline)
if remain <= 0 {
// Drop a half-parsed frame (e.g. aborted multi-notify) so the next
// exchange does not treat a new 0xAA as payload data.
b.resetParser()
return nil, fmt.Errorf("BLE read timeout for %s (check DIP8=BLE / -ble)", cmdName)
}
timer := time.NewTimer(remain)
@@ -264,6 +276,7 @@ sent:
}
return resp, nil
case <-timer.C:
b.resetParser()
return nil, fmt.Errorf("BLE read timeout for %s (check DIP8=BLE / -ble)", cmdName)
}
}
+21 -11
View File
@@ -10,8 +10,10 @@ import (
const (
StartMarker = 0xAA
StopMarker = 0xCC
// Must match main/uart.h MAX_PAYLOAD_SIZE (MAX_BUF_SIZE - 4).
MaxPayload = 248
// Must match main/uart.h MAX_PAYLOAD_SIZE.
MaxPayload = 1024
// AA | len_lo | len_hi | payload | xor | CC
FrameOverhead = 5
)
var (
@@ -19,14 +21,15 @@ var (
ErrTimeout = errors.New("read timeout")
)
// EncodeFrame builds a framed packet: 0xAA len payload xor(payload) 0xCC.
// EncodeFrame builds a framed packet: 0xAA len_lo len_hi payload xor(payload) 0xCC.
func EncodeFrame(payload []byte) ([]byte, error) {
if len(payload) == 0 || len(payload) > MaxPayload {
return nil, fmt.Errorf("payload length %d out of range 1..%d", len(payload), MaxPayload)
}
frame := make([]byte, 0, 4+len(payload))
frame = append(frame, StartMarker, byte(len(payload)))
n := len(payload)
frame := make([]byte, 0, FrameOverhead+n)
frame = append(frame, StartMarker, byte(n&0xff), byte((n>>8)&0xff))
var checksum byte
for _, b := range payload {
frame = append(frame, b)
@@ -39,6 +42,7 @@ func EncodeFrame(payload []byte) ([]byte, error) {
type Parser struct {
state int
len int
lenLo byte
payload []byte
index int
checksum byte
@@ -46,7 +50,8 @@ type Parser struct {
const (
stateStart = iota
stateLen
stateLenLo
stateLenHi
stateData
stateChecksum
stateStop
@@ -63,14 +68,19 @@ func (p *Parser) Feed(b byte) (payload []byte, ok bool, err error) {
if b == StartMarker {
p.index = 0
p.checksum = 0
p.state = stateLen
p.state = stateLenLo
}
case stateLen:
if b == 0 || b > MaxPayload {
case stateLenLo:
p.lenLo = b
p.state = stateLenHi
case stateLenHi:
n := int(p.lenLo) | (int(b) << 8)
if n == 0 || n > MaxPayload {
p.state = stateStart
} else {
p.len = int(b)
p.len = n
p.payload = make([]byte, p.len)
p.index = 0
p.state = stateData
}
case stateData:
@@ -102,7 +112,7 @@ func (p *Parser) Feed(b byte) (payload []byte, ok bool, err error) {
// maxWait bounds total wait time; zero means no limit (serial read timeouts retry forever).
func ReadFrame(r io.Reader, buf []byte, maxWait time.Duration) ([]byte, error) {
if buf == nil {
buf = make([]byte, 256)
buf = make([]byte, 1024)
}
parser := NewParser()
+2 -2
View File
@@ -166,7 +166,7 @@ Hardware: **UART1**, **921600** baud, **TX = GPIO3**, **RX = GPIO2** (adapter TX
| Field | Value |
|-------|--------|
| Start | `0xAA` |
| Length | 1 byte, payload size 1–252 |
| Length | 2 bytes little-endian, payload size 1–1024 |
| Payload | `length` bytes |
| Checksum | XOR of all payload bytes |
| Stop | `0xCC` |
@@ -178,7 +178,7 @@ Hardware: **UART1**, **921600** baud, **TX = GPIO3**, **RX = GPIO2** (adapter TX
| 0 | Command ID (`MessageType` / `msg_id`) |
| 1… | Arguments (handler receives bytes after ID only) |
Example VERSION request: single-byte payload `03` → frame `AA 01 03 03 CC`.
Example VERSION request: single-byte payload `03` → frame `AA 01 00 03 03 CC`.
Logging:
+94 -13
View File
@@ -3,7 +3,9 @@
#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"
#include "nimble/nimble_port.h"
@@ -22,6 +24,15 @@ static const char *DEVICE_NAME = "Powerpod";
#define BLE_CONN_ITVL_MIN_UNITS 6 /* 7.5 ms */
#define BLE_CONN_ITVL_MAX_UNITS 6
#define BLE_CONN_TUNE_MAX_TRIES 6
/** 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:
@@ -43,6 +54,7 @@ static bool notify_enabled;
static uint16_t att_mtu = 23;
static esp_timer_handle_t conn_tune_timer;
static uint8_t conn_tune_tries;
static SemaphoreHandle_t s_tx_mu;
static int ble_gap_event(struct ble_gap_event *event, void *arg);
static void ble_request_fast_conn_params(uint16_t handle);
@@ -61,28 +73,89 @@ uint16_t ble_host_max_payload(void) {
return (uint16_t)(att_mtu - 3);
}
static esp_err_t ble_host_tx(const uint8_t *frame, size_t frame_len) {
struct os_mbuf *om;
int rc;
/**
* 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) {
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));
continue;
}
int rc = ble_gatts_notify_custom(conn_handle, ble_chr_val_handle, om);
if (rc == 0) {
return ESP_OK;
}
/* Ownership stays with us on failure. */
os_mbuf_free_chain(om);
if (!ble_host_ready()) {
return ESP_ERR_INVALID_STATE;
}
ESP_LOGD(TAG, "notify chunk rc=%d attempt=%d len=%u", rc, attempt,
(unsigned)len);
vTaskDelay(pdMS_TO_TICKS(BLE_NOTIFY_CHUNK_RETRY_MS));
}
ESP_LOGW(TAG, "notify chunk gave up after retries len=%u", (unsigned)len);
return ESP_ERR_NO_MEM;
}
/**
* Send a full host frame. If larger than ATT MTU-3, split across consecutive
* notifies; goTool feeds every notify into the same byte parser, so the frame
* reassembles without a protocol change. Mutex keeps stream ticks from
* interleaving mid-frame.
*/
static esp_err_t ble_host_tx(const uint8_t *frame, size_t frame_len) {
if (!ble_host_ready()) {
return ESP_ERR_INVALID_STATE;
}
if (frame_len == 0 || frame_len > ble_host_max_payload()) {
if (frame == NULL || frame_len == 0) {
return ESP_ERR_INVALID_ARG;
}
uint16_t max_chunk = ble_host_max_payload();
if (max_chunk == 0) {
return ESP_ERR_INVALID_SIZE;
}
om = ble_hs_mbuf_from_flat(frame, (uint16_t)frame_len);
if (om == NULL) {
return ESP_ERR_NO_MEM;
if (s_tx_mu == NULL ||
xSemaphoreTake(s_tx_mu, pdMS_TO_TICKS(2000)) != pdTRUE) {
return ESP_ERR_TIMEOUT;
}
rc = ble_gatts_notify_custom(conn_handle, ble_chr_val_handle, om);
if (rc != 0) {
os_mbuf_free_chain(om);
return ESP_FAIL;
esp_err_t result = ESP_OK;
size_t off = 0;
while (off < frame_len) {
if (!ble_host_ready()) {
result = ESP_ERR_INVALID_STATE;
break;
}
size_t remain = frame_len - off;
uint16_t n =
(uint16_t)(remain > max_chunk ? max_chunk : remain);
result = ble_notify_chunk(&frame[off], n);
if (result != ESP_OK) {
ESP_LOGW(TAG, "TX abort at %u/%u (chunk %u, mtu_pay %u)",
(unsigned)off, (unsigned)frame_len, (unsigned)n,
(unsigned)max_chunk);
break;
}
off += n;
if (off < frame_len) {
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));
}
}
return ESP_OK;
xSemaphoreGive(s_tx_mu);
return result;
}
static void ble_conn_tune_timer_cb(void *arg) {
@@ -365,6 +438,12 @@ esp_err_t init_ble_host(QueueHandle_t cmd_queue) {
host_transport_set_cmd_queue(cmd_queue);
host_transport_set_tx(ble_host_tx);
s_tx_mu = xSemaphoreCreateMutex();
if (s_tx_mu == NULL) {
ESP_LOGE(TAG, "failed to create TX mutex");
return ESP_ERR_NO_MEM;
}
/* NVS may already be init via pod_settings; ignore ALREADY_INIT. */
ret = nvs_flash_init();
if (ret == ESP_ERR_NVS_NO_FREE_PAGES ||
@@ -404,9 +483,11 @@ esp_err_t init_ble_host(QueueHandle_t cmd_queue) {
return ESP_FAIL;
}
rc = ble_att_set_preferred_mtu(247);
rc = ble_att_set_preferred_mtu(BLE_PREFERRED_ATT_MTU);
if (rc != 0) {
ESP_LOGW(TAG, "preferred MTU set failed: %d", rc);
} else {
ESP_LOGI(TAG, "preferred ATT MTU %u", (unsigned)BLE_PREFERRED_ATT_MTU);
}
nimble_port_freertos_init(ble_host_task);
+1 -1
View File
@@ -16,7 +16,7 @@ bool ble_host_ready(void);
/** Negotiated ATT MTU (default 23 until exchange). */
uint16_t ble_host_att_mtu(void);
/** Max notify/write payload bytes (ATT MTU - 3). */
/** Max bytes per ATT notify/write (ATT MTU - 3). Frames may span multiple notifies. */
uint16_t ble_host_max_payload(void);
#endif
+13 -26
View File
@@ -7,10 +7,9 @@
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "ota_session.h"
#include "pb_encode.h"
#include "uart.h"
#include "uart_cmd.h"
#include "uart_messages.pb.h"
#include "uart_proto.h"
#include <string.h>
static const char *TAG = "[BLE_STREAM]";
@@ -102,34 +101,22 @@ static bool any_accel_stream(void) {
}
static esp_err_t send_tick_truncated(alox_UartMessage *msg) {
uint8_t pb_buf[MAX_PAYLOAD_SIZE];
pb_ostream_t stream = pb_ostream_from_buffer(pb_buf, sizeof(pb_buf));
/*
* ble_host_tx splits frames larger than ATT MTU-3 across notifies. Only
* shrink the tick if it cannot encode into MAX_PAYLOAD_SIZE.
*/
while (true) {
stream = pb_ostream_from_buffer(pb_buf, sizeof(pb_buf));
if (!pb_encode(&stream, alox_UartMessage_fields, msg)) {
return ESP_FAIL;
esp_err_t err = uart_send_uart_message(msg);
if (err == ESP_OK) {
return ESP_OK;
}
size_t payload_len = stream.bytes_written + 1;
uint16_t max_pay = ble_host_max_payload();
/* Framed size = payload + 4; notify must fit ATT MTU-3. */
size_t frame_len = payload_len + 4;
if (payload_len <= MAX_PAYLOAD_SIZE && frame_len <= max_pay) {
uint8_t payload[MAX_PAYLOAD_SIZE];
payload[0] = (uint8_t)msg->type;
memcpy(&payload[1], pb_buf, stream.bytes_written);
return uart_send_framed(payload, payload_len);
}
if (msg->payload.ble_stream_tick.clients_count == 0) {
ESP_LOGW(TAG, "tick too large even with 0 clients (need %u max %u)",
(unsigned)frame_len, (unsigned)max_pay);
return ESP_ERR_NO_MEM;
ESP_LOGW(TAG, "tick too large even with 0 clients: %s",
esp_err_to_name(err));
return err;
}
msg->payload.ble_stream_tick.clients_count--;
ESP_LOGW(TAG, "truncating tick clients to %u for MTU",
ESP_LOGW(TAG, "truncating tick clients to %u for encode buffer",
(unsigned)msg->payload.ble_stream_tick.clients_count);
}
}
@@ -197,7 +184,7 @@ void ble_stream_update(void) {
if (want && !s_running) {
s_running = true;
if (xTaskCreate(ble_stream_task, "ble_stream", 4096, NULL, 5, &s_task) !=
if (xTaskCreate(ble_stream_task, "ble_stream", 8192, NULL, 5, &s_task) !=
pdPASS) {
s_running = false;
s_task = NULL;
+23 -1
View File
@@ -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;
+11 -1
View File
@@ -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);
+36 -1
View File
@@ -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 <stdint.h>
#include <stdlib.h>
#include <string.h>
@@ -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);
+8
View File
@@ -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;
+5 -1
View File
@@ -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) {
+10 -2
View File
@@ -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);
}
}
+5 -4
View File
@@ -22,14 +22,14 @@ void host_transport_set_cmd_queue(QueueHandle_t cmd_queue) {
QueueHandle_t host_transport_cmd_queue(void) { return s_cmd_queue; }
bool host_enqueue_packet(const uint8_t *payload, uint8_t len) {
bool host_enqueue_packet(const uint8_t *payload, size_t len) {
if (s_cmd_queue == NULL || payload == NULL || len == 0) {
return false;
}
generic_msg_t msg = {
.msg_id = payload[0],
.len = len > 1 ? (size_t)(len - 1) : 0,
.len = len > 1 ? (len - 1) : 0,
.payload = NULL,
};
@@ -63,10 +63,11 @@ esp_err_t host_send_framed(const uint8_t *payload, size_t len) {
checksum ^= payload[i];
}
uint8_t frame[4 + MAX_PAYLOAD_SIZE];
uint8_t frame[UART_FRAME_OVERHEAD + MAX_PAYLOAD_SIZE];
size_t pos = 0;
frame[pos++] = START_MARKER;
frame[pos++] = (uint8_t)len;
frame[pos++] = (uint8_t)(len & 0xff);
frame[pos++] = (uint8_t)((len >> 8) & 0xff);
memcpy(&frame[pos], payload, len);
pos += len;
frame[pos++] = checksum;
+2 -2
View File
@@ -19,7 +19,7 @@ void host_transport_set_cmd_queue(QueueHandle_t cmd_queue);
QueueHandle_t host_transport_cmd_queue(void);
/**
* Build AA|len|payload|xor|CC and send via the registered TX backend.
* Build AA|len_lo|len_hi|payload|xor|CC and send via the registered TX backend.
* Same semantics as uart_send_framed.
*/
esp_err_t host_send_framed(const uint8_t *payload, size_t len);
@@ -31,6 +31,6 @@ esp_err_t host_send_framed(const uint8_t *payload, size_t len);
void host_rx_bytes(const uint8_t *data, size_t len);
/** Enqueue a completed uart_packet_t (payload[0]=msg type). */
bool host_enqueue_packet(const uint8_t *payload, uint8_t len);
bool host_enqueue_packet(const uint8_t *payload, size_t len);
#endif
+13 -4
View File
@@ -81,18 +81,27 @@ bool parse_uart_byte(uint8_t byte, uart_packet_t *p) {
if (byte == START_MARKER) {
p->index = 0;
p->checksum = 0;
p->state = STATE_LEN;
p->len = 0;
p->state = STATE_LEN_LO;
}
break;
case STATE_LEN:
if (byte > MAX_PAYLOAD_SIZE || byte == 0) {
case STATE_LEN_LO:
p->len = byte;
p->state = STATE_LEN_HI;
break;
case STATE_LEN_HI: {
uint16_t len = (uint16_t)(p->len | ((uint16_t)byte << 8));
if (len == 0 || len > MAX_PAYLOAD_SIZE) {
p->state = STATE_START;
} else {
p->len = byte;
p->len = len;
p->index = 0;
p->state = STATE_DATA;
}
break;
}
case STATE_DATA:
p->payload[p->index++] = byte;
+11 -6
View File
@@ -22,13 +22,18 @@
#define UART_DRIVER_TX_BUF_SIZE 4096
#define START_MARKER 0xAA
#define STOP_MARKER 0xCC
#define MAX_BUF_SIZE 252
#define MAX_PAYLOAD_SIZE \
MAX_BUF_SIZE - 4 // Buffer overhead, Start, Len, CRC, End
/** Frame: AA | len_lo | len_hi | payload[len] | xor | CC */
#define UART_FRAME_OVERHEAD 5
/**
* Max application payload (type byte + protobuf). Sized for CLIENT_REGISTRY_MAX
* aggregations (CLIENT_INFO / BATTERY_STATUS with 16+ samples).
*/
#define MAX_PAYLOAD_SIZE 1024
typedef enum {
STATE_START,
STATE_LEN,
STATE_LEN_LO,
STATE_LEN_HI,
STATE_DATA,
STATE_CHECKSUM,
STATE_STOP
@@ -36,8 +41,8 @@ typedef enum {
typedef struct {
uint8_t payload[MAX_PAYLOAD_SIZE];
uint8_t len;
uint8_t index;
uint16_t len;
uint16_t index;
uint8_t checksum;
parser_state_t state;
} uart_packet_t;
+2 -1
View File
@@ -42,7 +42,8 @@ void uart_cmd_init_response(alox_UartMessage *msg, alox_MessageType type,
esp_err_t uart_cmd_send(const alox_UartMessage *msg, const char *log_tag) {
esp_err_t err = uart_send_uart_message(msg);
if (err != ESP_OK && log_tag != NULL) {
ESP_LOGE(log_tag, "failed to send UART response");
ESP_LOGE(log_tag, "failed to send UART response: %s",
esp_err_to_name(err));
}
return err;
}
+8
View File
@@ -1,18 +1,26 @@
#include "uart_proto.h"
#include "esp_log.h"
#include "pb_encode.h"
#include "uart.h"
#include <string.h>
static const char *TAG = "[UART_PROTO]";
esp_err_t uart_send_uart_message(const alox_UartMessage *msg) {
uint8_t pb_buf[MAX_PAYLOAD_SIZE];
pb_ostream_t stream = pb_ostream_from_buffer(pb_buf, sizeof(pb_buf));
if (!pb_encode(&stream, alox_UartMessage_fields, msg)) {
ESP_LOGE(TAG, "pb_encode failed type=%u (%s)", (unsigned)msg->type,
PB_GET_ERROR(&stream));
return ESP_FAIL;
}
uint8_t payload[MAX_PAYLOAD_SIZE];
if (stream.bytes_written + 1 > sizeof(payload)) {
ESP_LOGE(TAG, "payload too large type=%u need=%u max=%u",
(unsigned)msg->type, (unsigned)(stream.bytes_written + 1),
(unsigned)sizeof(payload));
return ESP_ERR_NO_MEM;
}