diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 1bdcaa3..f9b43bf 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -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). --- diff --git a/docs/DOCUMENTATION.md b/docs/DOCUMENTATION.md index 426fe8c..8fa7fbc 100644 --- a/docs/DOCUMENTATION.md +++ b/docs/DOCUMENTATION.md @@ -370,7 +370,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 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()`. - **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) diff --git a/goTool/ble_link.go b/goTool/ble_link.go index cebbf82..e1100a3 100644 --- a/goTool/ble_link.go +++ b/goTool/ble_link.go @@ -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) } } diff --git a/main/ble_host.c b/main/ble_host.c index 246d0c1..5c4e8f8 100644 --- a/main/ble_host.c +++ b/main/ble_host.c @@ -4,6 +4,7 @@ #include "host_transport.h" #include "nvs_flash.h" #include "freertos/FreeRTOS.h" +#include "freertos/semphr.h" #include "freertos/task.h" #include "nimble/nimble_port.h" @@ -22,6 +23,12 @@ 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 +#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 /* * Nordic UART style: @@ -43,6 +50,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 +69,83 @@ 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) { + for (int attempt = 0; attempt < BLE_NOTIFY_CHUNK_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) { + vTaskDelay(pdMS_TO_TICKS(BLE_NOTIFY_CHUNK_GAP_MS)); + } } - return ESP_OK; + + xSemaphoreGive(s_tx_mu); + return result; } static void ble_conn_tune_timer_cb(void *arg) { @@ -365,6 +428,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 +473,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); diff --git a/main/ble_host.h b/main/ble_host.h index db2b66b..79cb964 100644 --- a/main/ble_host.h +++ b/main/ble_host.h @@ -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 diff --git a/main/ble_stream.c b/main/ble_stream.c index 9147486..d3c74fc 100644 --- a/main/ble_stream.c +++ b/main/ble_stream.c @@ -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 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 + UART_FRAME_OVERHEAD; notify must fit ATT MTU-3. */ - size_t frame_len = payload_len + UART_FRAME_OVERHEAD; - 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); } } diff --git a/main/uart_cmd.c b/main/uart_cmd.c index a3caee5..eb31e8a 100644 --- a/main/uart_cmd.c +++ b/main/uart_cmd.c @@ -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; }