diff --git a/components/bt/common/CMakeLists.txt b/components/bt/common/CMakeLists.txt index dc8a30653b8..91e8c14b379 100644 --- a/components/bt/common/CMakeLists.txt +++ b/components/bt/common/CMakeLists.txt @@ -170,8 +170,10 @@ list(APPEND bt_common_priv_include_dirs if(CONFIG_BLE_LOG_ENABLED) list(APPEND bt_common_srcs "${CMAKE_CURRENT_LIST_DIR}/ble_log/src/ble_log.c" - "${CMAKE_CURRENT_LIST_DIR}/ble_log/src/ble_log_lbm.c" + "${CMAKE_CURRENT_LIST_DIR}/ble_log/src/ble_log_lbm_v2.c" + "${CMAKE_CURRENT_LIST_DIR}/ble_log/src/ble_log_redir.c" "${CMAKE_CURRENT_LIST_DIR}/ble_log/src/ble_log_rt.c" + "${CMAKE_CURRENT_LIST_DIR}/ble_log/src/ble_log_task_registry.c" "${CMAKE_CURRENT_LIST_DIR}/ble_log/src/ble_log_util.c" ) @@ -184,11 +186,6 @@ if(CONFIG_BLE_LOG_ENABLED) "${CMAKE_CURRENT_LIST_DIR}/ble_log/src/internal_include/prph" ) - # Timestamp synchronization extension - if(CONFIG_BLE_LOG_TS_ENABLED) - list(APPEND bt_common_srcs "${CMAKE_CURRENT_LIST_DIR}/ble_log/src/ble_log_ts.c") - endif() - # Peripheral interface implementation if(CONFIG_BLE_LOG_PRPH_DUMMY) list(APPEND bt_common_srcs diff --git a/components/bt/common/ble_log/Kconfig.in b/components/bt/common/ble_log/Kconfig.in index 8888ab70559..ab45d42f07a 100644 --- a/components/bt/common/ble_log/Kconfig.in +++ b/components/bt/common/ble_log/Kconfig.in @@ -1,55 +1,57 @@ config BLE_LOG_ENABLED bool "Enable BT Log Async Output (Dev Only)" select BLE_COMPRESSED_LOG_ENABLE - select BLE_HOST_COMPRESSED_LOG_ENABLE if BT_BLUEDROID_ENABLED + select BLE_HOST_COMPRESSED_LOG_ENABLE if BT_BLUEDROID_ENABLED || BT_NIMBLE_ENABLED select ESP_TIMER_IN_IRAM default n help Enable BT Log Async Output if BLE_LOG_ENABLED - config BLE_LOG_LBM_AUTO_FLUSH - bool "Enable automatic BLE Log LBM buffer flush" - default n + config BLE_LOG_POOL_TRANS_CNT + int "Number of transport buffers in the unified pool" + range 2 32 + default 8 help - Periodically flush partially-filled BLE Log LBM transport buffers - that remain pending, reducing latency for low-volume or - intermittent logging. + Total number of transports shared by ordinary, Link Layer, ISR, + and critical-section log writers. Availability is represented by + 32-bit bitmaps, so the maximum is 32. - config BLE_LOG_LBM_TRANS_BUF_SIZE - int "Total buffer memory per common LBM (bytes)" - default 2048 - help - Total buffer memory allocated for each common pool log buffer - manager (LBM). This memory is divided equally among internal - transport buffers. Must be a multiple of BLE_LOG_TRANS_BUF_CNT - (currently 4). - - The common pool contains: - - BLE_LOG_LBM_ATOMIC_LOCK_TASK_CNT atomic LBMs (task context) - - BLE_LOG_LBM_ATOMIC_LOCK_ISR_CNT atomic LBMs (ISR context) - - 2 spinlock-protected LBMs (one for task, one for ISR fallback) - - Total common pool memory: - (ATOMIC_TASK_CNT + ATOMIC_ISR_CNT + 2) * BLE_LOG_LBM_TRANS_BUF_SIZE - - config BLE_LOG_LBM_ATOMIC_LOCK_TASK_CNT - int "Count of log buffer managers with atomic lock protection for task context" - default 2 - help - BLE Log module will search for an LBM with atomic lock protection first; if - all LBMs with atomic lock protection are unavailable, BLE Log module will - try to use the LBM with spin lock protection. So the more LBMs with atomic - lock protection are created, the better the logging performance will be. - - config BLE_LOG_LBM_ATOMIC_LOCK_ISR_CNT - int "Count of log buffer managers with atomic lock protection for ISR context" + config BLE_LOG_POOL_NON_YIELD_RESERVE_CNT + int "Transport buffers reserved for non-yieldable contexts" + range 1 31 default 1 help - BLE Log module will search for an LBM with atomic lock protection first; if - all LBMs with atomic lock protection are unavailable, BLE Log module will - try to use the LBM with spin lock protection. So the more LBMs with atomic - lock protection are created, the more ISRs can nest. + Buffers that ordinary task writers cannot consume. ISR and + critical-section writers try the shared region first, then this + reserve. The value must remain below BLE_LOG_POOL_TRANS_CNT. + + config BLE_LOG_POOL_TRANS_SIZE + int "Per-transport buffer size (bytes)" + range 640 10240 + default 640 + help + Size of every unified-pool transport. One complete frame must fit + in one transport. SPI builds require a multiple of four so they + can append peripheral-only zero padding to each logical frame. + + config BLE_LOG_TASK_ID_MAX + int "Maximum number of distinct logging tasks" + range 2 32 + default 16 + help + Size of the task-id registry (protocol v8). Every ENCODE record + carries the one-byte id of the task that formatted it; the + registry maps task names to those stable ids. One binding record + per registered entry is broadcast as an INTERNAL frame on every + periodic snapshot window, so a receiver that missed or lost a + binding converges on the next window. A task that starts logging + after the registry is full falls back to the unknown id (0xFF) + and its records are still emitted. + + RAM cost is 16 bytes per entry (256 bytes at the default 16) + plus one dedicated binding transport (320 bytes at 16 entries, + 624 at 32; the broadcast frame must fit in one transport). config BLE_LOG_IS_ESP_CONTROLLER bool "Current BLE Controller is ESP BLE Controller" @@ -77,44 +79,9 @@ if BLE_LOG_ENABLED help Enable BLE Log for Link Layer - if BLE_LOG_LL_ENABLED - config BLE_LOG_LBM_LL_TRANS_BUF_SIZE - int "Total buffer memory per Link Layer LBM (bytes)" - default 2048 - help - Total buffer memory allocated for each Link Layer dedicated - log buffer manager (LBM). This memory is divided equally among - internal transport buffers. Must be a multiple of - BLE_LOG_TRANS_BUF_CNT (currently 4). - - There are 2 Link Layer LBMs without lock protection (each is - accessed from a single context only): - - LL task LBM (Link Layer task context logs) - - LL HCI LBM (Link Layer HCI context logs) - - Total LL pool memory: 2 * BLE_LOG_LBM_LL_TRANS_BUF_SIZE - - config BLE_LOG_LL_HCI_LOG_PAYLOAD_LEN_LIMIT_ENABLED - bool "Enable LL HCI Log Payload Length Limit" - default n - help - Enable length limit for LL HCI Log payload (addr_append). - When enabled, if len_append exceeds the configured limit, - it will be truncated to the maximum length. - - config BLE_LOG_LL_HCI_LOG_PAYLOAD_LEN_LIMIT - int "LL HCI Log Payload Length Limit (bytes)" - depends on BLE_LOG_LL_HCI_LOG_PAYLOAD_LEN_LIMIT_ENABLED - default 32 - help - Maximum length for LL HCI Log payload (len_append). - When the feature is enabled and len_append exceeds this value, - it will be truncated. - endif - config BLE_LOG_HOST_LOG bool "Enable BLE Log for Host" - depends on BT_BLUEDROID_ENABLED + depends on BT_BLUEDROID_ENABLED || BT_NIMBLE_ENABLED default y help Enable BLE Log for the Host stack (Bluedroid or NimBLE). @@ -124,26 +91,30 @@ if BLE_LOG_ENABLED the standard UART logging path. Disable this option to keep BLE Log enabled for the controller/link layer only. - config BLE_LOG_HOST_SIDE_HCI_LOG_ENABLED - bool "Enable BLE Host side HCI Logging" - default y if BLE_LOG_IS_ESP_LEGACY_CONTROLLER + config BLE_LOG_HCI_LOG_ENABLED + bool "Enable BLE HCI Logging" + default y help - Enable HCI packet logging captured on the Host side - (Bluedroid / NimBLE HCI HAL). Useful for correlating - host events with controller-side HCI traces. + Enable HCI packet logging. Bluedroid and legacy VHCI NimBLE + capture on the Host side and suppress duplicate Controller + HCI records. Other configurations, including non-legacy + NimBLE, retain Controller HCI records through the LL callback + and require BLE_LOG_LL_ENABLED. Disabling this option + suppresses HCI records from both capture paths. - config BLE_LOG_TS_ENABLED - bool "Enable BLE Log Timestamp Synchronization (TS)" + config BLE_LOG_TS_SYNC_TOGGLE_IO_ENABLED + bool "Toggle a GPIO on every BLE Log TS sync sample" default n help - Enable BLE Log TS with external logging module. Synchronization is - triggered periodically by an ESP Timer using task dispatch. The - timer does not wake the system from light sleep. + Every periodic Internal Snapshot samples the link-layer, ESP and + OS clocks regardless of this option. Enable this option to build + the GPIO toggle used by external logic analyzers. It starts + disabled and low; ble_log_ts_sync_io_toggle_enable controls its + edges without stopping periodic snapshots. - if BLE_LOG_TS_ENABLED + if BLE_LOG_TS_SYNC_TOGGLE_IO_ENABLED config BLE_LOG_SYNC_IO_NUM int "GPIO number for Timestamp Synchronization (TS) toggle output" - depends on BLE_LOG_TS_ENABLED default 0 help GPIO number for TS toggle output @@ -165,8 +136,11 @@ if BLE_LOG_ENABLED bool "Test only transport" help Test transport that models DMA ownership and optional - link-rate backpressure. Selected by BLE Log test apps - only; not shown in menuconfig. + link-rate backpressure. Intended for the BLE Log test apps + only: outside them nothing drains the transmitted buffers, + so selecting this transport in a production build stalls + log output. The BLE Log test apps select it through their + sdkconfig.defaults. config BLE_LOG_PRPH_SPI_MASTER_DMA bool "Utilize SPI master DMA driver as transport" @@ -240,6 +214,35 @@ if BLE_LOG_ENABLED int default 512 + config BLE_LOG_LBM_TRANS_BUF_SIZE + int + default 2048 + + config BLE_LOG_LBM_ATOMIC_LOCK_TASK_CNT + int + default 2 + + config BLE_LOG_LBM_ATOMIC_LOCK_ISR_CNT + int + default 1 + + config BLE_LOG_LBM_LL_TRANS_BUF_SIZE + int + default 2048 + + config BLE_LOG_LL_HCI_LOG_PAYLOAD_LEN_LIMIT_ENABLED + bool + default n + + config BLE_LOG_LL_HCI_LOG_PAYLOAD_LEN_LIMIT + int + depends on BLE_LOG_LL_HCI_LOG_PAYLOAD_LEN_LIMIT_ENABLED + default 32 + + config BLE_LOG_LBM_AUTO_FLUSH + bool + default n + config BLE_LOG_ENH_STAT_ENABLED bool default y @@ -260,28 +263,31 @@ if BLE_LOG_ENABLED config BLE_LOG_TS_TRIGGER_TIMEOUT_MS int - depends on BLE_LOG_TS_ENABLED default 1000 config BLE_LOG_TS_TRIGGER_CHOICE bool - depends on BLE_LOG_TS_ENABLED default y config BLE_LOG_TS_TRIGGER_ESP_TIMER bool - depends on BLE_LOG_TS_ENABLED default y config BLE_LOG_TS_TRIGGER_TASK_EVENT bool - depends on BLE_LOG_TS_ENABLED default n config BLE_LOG_TS_TRIGGER_ESP_TIMER_ISR_DISPATCH_METHOD bool - depends on BLE_LOG_TS_ENABLED default n + + # Deprecated: renamed to BLE_LOG_HCI_LOG_ENABLED. No prompt, so it + # never appears in menuconfig; kept so sdkconfig files pinning the old + # name keep Host side HCI logging enabled. + config BLE_LOG_HOST_SIDE_HCI_LOG_ENABLED + bool + default n + select BLE_LOG_HCI_LOG_ENABLED endif menu "Legacy SPI Log Output (Deprecated - use BT Log Async Output instead)" diff --git a/components/bt/common/ble_log/README.md b/components/bt/common/ble_log/README.md index eb049112cde..5fe0c793285 100644 --- a/components/bt/common/ble_log/README.md +++ b/components/bt/common/ble_log/README.md @@ -1,495 +1,310 @@ -# BLE Log Module +# BLE Log -A high-performance, modular Bluetooth logging system that provides real-time log capture and asynchronous transmission capabilities for the ESP-IDF Bluetooth stack. +BLE Log is an asynchronous binary logging transport for Bluetooth controller, +Host HCI, application, and compressed logs. -## Table of Contents +## Architecture -- [Overview](#overview) -- [Architecture Design](#architecture-design) -- [Features](#features) -- [Quick Start](#quick-start) -- [Configuration Options](#configuration-options) -- [API Reference](#api-reference) -- [Usage Examples](#usage-examples) -- [Performance & Memory Optimization](#performance--memory-optimization) -- [Troubleshooting](#troubleshooting) -- [Important Notes](#important-notes) - -## Overview - -The BLE Log module is an efficient logging system specifically designed for the ESP-IDF Bluetooth stack, supporting real-time log capture, multi-source log collection, and various transmission methods. This module has been refactored with a modular design, featuring high-concurrency processing capabilities and low-latency characteristics. - -### Main Components - -- **BLE Log Core** (`ble_log.c`): Module core responsible for initialization and coordination of sub-modules -- **Runtime Manager** (`ble_log_rt.c`): One-shot ESP Timer dispatch for log transmission scheduling -- **Log Buffer Manager** (`ble_log_lbm.c`): Log buffer management supporting multiple locking mechanisms -- **Peripheral Interface** (`ble_log_prph_*.c`): Peripheral interface abstraction layer supporting various transmission methods -- **Timestamp Sync** (`ble_log_ts.c`): Timestamp synchronization module -- **Utility** (`ble_log_util.c`): Common utility functions - -## Features - -### Core Functionality - -- **Multi-source Log Collection**: Supports multiple log sources including Link Layer, Host, HCI, UART redirection, etc. -- **High Concurrency Processing**: Uses atomic and spin lock mechanisms for multi-task concurrent writing -- **Real-time Transmission**: Asynchronous transmission through ESP Timer task dispatch -- **Data Integrity**: Checksum mechanism ensures data integrity (always enabled) -- **Multi-buffer Transport**: Each LBM manages multiple transport buffers (default 4) for improved throughput over the legacy ping-pong design -- **Cross-pool Buffer Fallback**: LBM acquire attempts all atomic LBMs before falling back to spinlock LBMs, improving buffer availability under contention - -### Advanced Features - -- **UART Redirection**: When using UART DMA on PORT 0, UART output (including `esp_rom_printf`) is transparently redirected through the async log pipeline -- **Timestamp Synchronization**: Supports timestamp synchronization with external devices (optional) -- **Enhanced Statistics**: Detailed logging statistics including written/lost frame and byte counts (always enabled) -- **Buffer Utilization Reporting**: Per-LBM buffer utilization and inflight peak tracking for diagnostics -- **Link Layer Integration**: Deep integration with ESP-IDF Bluetooth Link Layer -- **Multiple Transmission Methods**: Supports SPI Master DMA, UART DMA, and Dummy transmission - -### Performance Features - -- **IRAM Optimization**: Critical path code runs in IRAM ensuring low latency -- **Lock-free Design**: Most operations use atomic operations reducing lock contention -- **Buffer Reuse**: Intelligent buffer management reduces memory allocation overhead - -## Quick Start - -### 1. Enable Module - -Enable the BLE Log module in `menuconfig`: - -``` -Component config → Bluetooth → Enable BT Log Async Output (Dev Only) +```mermaid +flowchart TD + A[Public and LL writers] --> P[Unified bitmap pool] + C[Compression encoder] --> P + S[Internal snapshot] --> I[Fixed internal transport] + R[UART0 redirection] --> D[Redirection transports] + P --> Q[ESP Timer runtime queue] + I --> Q + D --> Q + Q --> X[Peripheral DMA] ``` -### 2. Basic Configuration +The shared pool contains `CONFIG_BLE_LOG_POOL_TRANS_CNT` transports. The last +`CONFIG_BLE_LOG_POOL_NON_YIELD_RESERVE_CNT` transports are reserved for ISR and +other contexts that cannot yield. Ordinary yieldable task writers, from the +public API and claims to the controller LL task, wait for a shared transport +unless a claim explicitly opts out. Writers on the shared ESP Timer task never +wait for a transport: its callbacks must return so dispatch can progress. +While yieldable, they use shared transports only; ISR and critical-section +writers retain reserve access and fail-fast behavior. + +The Internal Snapshot and UART0 redirection transports are not members of the +bitmap pool. + +Periodic flushing scans the whole bounded pool, not just OPEN bitmap hints: +writers temporarily remove their hints while writing or holding a claim. A +failed try-lock leaves `pending_seal`; the next claim seals the old buffer +before appending, or a later periodic scan seals it when unlocked. This is a +deferred request, not a snapshot barrier. Seal and recycle clear the flag, +but a delayed flusher can set it afterwards; an extra early partial seal is +allowed. Full flush/deinit retain OPEN-only scans after writers drain. + +FREE acquisition requires the lock, FREE state, and atomically removing an +actually published FREE bit. A cached candidate alone cannot bypass the +recycler's state-to-bitmap publication window. UART0 periodic flushing is +independent of producer enable: disable stops new writes, not cached output. + +## Runtime dispatch + +Sealed transports enter one bounded queue. The first submission anchors a +one-shot ESP Timer deadline at 1 ms; later submissions do not move it. Each +callback drains the queue depth captured at entry and schedules another fixed +defer only for arrivals left behind, so it does not continuously monopolize +the shared ESP Timer task. + +## Version 8 frame + +All multi-byte fields use the target's little-endian representation. + +```text +offset size field +0 2 payload length +2 4 frame_meta +6 N payload +6 + N 4 checksum +``` + +`frame_meta` is: + +```text +bits 0..7 source +bits 8..31 sequence number, low 24 bits +``` + +The checksum is `ble_log_fast_checksum()` over the six-byte header and exactly +`payload length` bytes. It excludes the checksum field and peripheral-only DMA +padding. + +Core frame payloads follow the writer entry point, not a single rule: + +- Public write / claim path (`write_hex`, compressed encoder): the API + prepends a `[4-byte low32 esp_timer_get_time() microseconds]` before + the source payload. The timestamp is captured at API entry before any + pool mutex wait. It wraps approximately every 71.6 minutes and must be + unwrapped modulo 2^32 by the receiver. +- LL callback path (`write_hex_ll`, covering the LL_TASK / LL_HCI / + LL_ISR sources): controller payloads are forwarded raw, with no ESP + timestamp prefix — the on-wire layout is exactly what the controller + handed over. + +UART0 redirection payload is likewise the raw console stream with no +timestamp prefix: the redirection stream keeps its own frame sequence, and +its receiver-side arrival time (aggregation delay bounded by the periodic +redirection flush) is the alignment reference against the core timeline. + +### Sources + +The public `ble_log_src_t` ABI is frozen and its values are the on-wire +source IDs of protocol v8 frames: + +```text +0 INTERNAL +1 CUSTOM +2 LL_TASK +3 LL_HCI +4 LL_ISR +5 HOST +6 HCI +7 ENCODE +8 REDIR extension +``` + +Core log sources (`CUSTOM` through `ENCODE`) and Internal Snapshots share one +24-bit Global SN. Core logs consume it after entry validation and gate acceptance, +before buffer contention; lost log attempts leave gaps accounted for by +`lost_frame_cnt`. Snapshots consume it only after acquiring their dedicated +transport, when assembling the frame. Busy/skipped snapshots do not consume a +Global SN and are not included in `lost_frame_cnt`. +Snapshots also carry a separate 24-bit `anchor_count` in their payload: +only snapshot attempts advance this counter, including skipped snapshots. +The periodic task-binding broadcast and REDIR console stream retain their +own separate header sequences (gaps count skipped binding windows or dropped +console batches). `ble_log_init()` resets all sequences and the anchor counter, +and its required `INIT` snapshot starts a new +receiver epoch. They remain continuous through `FLUSH` within that epoch. +Callers must not write until `ble_log_init()` returns, so the `INIT` +snapshot is submitted first. + +`CONFIG_BLE_LOG_HCI_LOG_ENABLED` controls HCI logging. Bluedroid and legacy +VHCI NimBLE retain Host-side capture and suppress duplicate Controller HCI +records. Other configurations, including non-legacy NimBLE, retain Controller +HCI records through the LL callback (requires `CONFIG_BLE_LOG_LL_ENABLED`). +Controller flags preserve their source mapping: `HCI` to `LL_HCI` and +`HCI_UPSTREAM` to `HCI`, with ISR precedence. Controller payloads are forwarded +unchanged. Host-side capture retains its direction bit in HCI payload byte 0 +bit 7. Disabling HCI logging suppresses records from both capture paths. + +## Internal Snapshot + +All BLE Log-owned internal information is emitted as fixed-layout frames +from dedicated transports: the snapshot frame is 179 logical bytes on its +180-byte aligned transport, and the periodic task-binding broadcast has its own +transport sized to the registry (one full binding frame per window; +`CONFIG_BLE_LOG_TASK_ID_MAX` entries of 19 bytes plus a timestamp). + +The snapshot contains: + +- reason flags (`INIT`, `PERIODIC`, `FLUSH`, `TS_VALID`); +- a 24-bit little-endian `anchor_count` (three bytes), immediately after the reason flags; +- the complete 58-byte build, library, chip, and protocol version block; +- the LC clock, ESP Timer, and FreeRTOS tick samples captured together with + the GPIO sync level; +- pool count, reserve count, current inflight count, and inflight peak; +- compact statistics for `CUSTOM` through `ENCODE` (one slot per public + source in that range, `HOST` included). + +The periodic snapshot is an always-on system behavior from initialization +until deinitialization. It cannot be stopped by the TS sync IO control API. +A skipped periodic snapshot advances only the anchor count, not the Global SN. +Anchor counts start at zero for INIT, also advance for FLUSH, and wrap modulo +2^24 on the wire (the atomic runtime counter remains uint32_t). +A gap between anchor counts identifies missed snapshot attempts without +mistaking intervening ordinary logs for missed snapshots. + +Each source statistic contains only: ```c -#include "ble_log.h" +uint32_t written_frame_cnt; +uint32_t lost_frame_cnt; +uint32_t written_bytes_cnt; +``` -void app_main() { - // Initialize BLE Log module - if (!ble_log_init()) { - ESP_LOGE(TAG, "Failed to initialize BLE Log"); - return; - } - - // Write log data - uint8_t data[] = {0x01, 0x02, 0x03, 0x04}; - ble_log_write_hex(BLE_LOG_SRC_CUSTOM, data, sizeof(data)); - - // End session and flush buffers - ble_log_flush(); - - // Cleanup resources - ble_log_deinit(); +Successful counts are incremented only after a complete frame is committed to +a transport; they do not imply confirmed physical TX. `written_bytes_cnt` +counts the full logical frame (six-byte header, payload including any timestamp +prefix, and four-byte checksum), excluding peripheral DMA padding. Rejected or +aborted writes do not add bytes; there is no lost-byte counter. Internal and +REDIR frames are not included in these per-core-source byte counters. All three +counters are uint32 values and wrap modulo 2^32. +Core counts and the pool peak restart after a completed FLUSH; the Global SN +and anchor count remain continuous. A busy periodic Internal +transport is never overwritten; that snapshot is skipped and counted. Required +INIT and FLUSH snapshots use a bounded task-context wait. + +The protocol version remains v8 despite the changed snapshot payload layout and +header sequence semantics. Receivers must be updated together with the producer; +the version byte alone cannot distinguish the old and new layouts. +The snapshot remains a sampling point, +not a confirmed TX boundary: periodic OPEN flushing is best-effort, statistics +are sampled individually while writers can run, and Global SN allocation order +is not submission order. Neither deferred sealing nor the anchor count makes +snapshot differences an exact SN-interval accounting boundary. + +## Public API + +```c +bool ble_log_init(void); +void ble_log_deinit(void); +bool ble_log_enable(bool enable); +void ble_log_flush(void); +bool ble_log_write_hex(ble_log_src_t source, const uint8_t *data, size_t len); +uint8_t *ble_log_claim(ble_log_src_t source, size_t maximum, + uint32_t *handle, bool wait_for_transport); +void ble_log_commit(uint32_t handle, size_t actual_len); +void ble_log_write_hex_ll(uint32_t len, const uint8_t *data, + uint32_t append_len, const uint8_t *append, + uint32_t flags); +bool ble_log_ts_sync_io_toggle_enable(bool enable); +bool ble_log_sync_enable(bool enable); /* compatibility shim */ +``` + +`ble_log_enable()` gates public producers only. Periodic system output remains +active while that gate is closed. + +`ble_log_ts_sync_io_toggle_enable()` controls only the optional external +analyzer GPIO, which starts disabled and low. Disabling it leaves the IO low +after a final falling edge; +periodic clock sampling, OPEN transport flushing, and Internal Snapshots +continue. When the GPIO feature is not built, the call remains a +lifecycle-checked no-op. `ble_log_sync_enable()` is the backward-compatible +name for the same behavior. + +`ble_log_flush()` is an ordinary FreeRTOS task API. Do not call it from an ISR, +critical section, or the shared ESP Timer task. + +A public or LL record that cannot fit completely in one pool transport is +rejected and counted as lost; BLE Log never emits a truncated record. + +## Compression direct write + +Compression encoders write directly into shared-pool storage: + +```c +uint32_t handle; +uint8_t *payload = ble_log_claim(BLE_LOG_SRC_ENCODE, maximum, + &handle, true); +if (payload) { + size_t encoded = encode(payload, maximum); + ble_log_commit(handle, encoded); } ``` -### 3. Link Layer Integration +`ble_log_claim()` reserves the hidden frame header, ESP Timer timestamp, and +checksum. It waits for a shared transport in ordinary yieldable tasks when +`wait_for_transport` is true (writer backpressure); pass false to opt out. +The shared ESP Timer task and non-yieldable contexts fail fast either way. +A busy-pool rejection leaves a Global SN gap and increments the source's loss +counter, just like other pool acquisition failures. +Every successful claim must be committed exactly once before +`ble_log_deinit()`; `ble_log_commit(handle, 0)` cancels it. Handles include a +transport generation so a stale handle cannot commit a later claim in the same +lifecycle. -When `CONFIG_BLE_LOG_LL_ENABLED` is enabled, Link Layer logs are automatically integrated: +Mesh, ISO, Bluedroid, and NimBLE compression no longer allocate three static +payload buffers per channel. Every ENCODE record names its writer: the byte +after the source is a task id from a name-keyed registry. The registry is a +self-contained module (`ble_log_task_registry.c/h`, structured like the +UART redirection writer: a small append-only table shared by every ENCODE +writer; RAM cost 16 bytes per entry, sized by `CONFIG_BLE_LOG_TASK_ID_MAX`). -```c -// Link Layer logs will automatically call this function -void ble_log_write_hex_ll(uint32_t len, const uint8_t *addr, - uint32_t len_append, const uint8_t *addr_append, - uint32_t flag); +The registry broadcast is module-owned system output: every periodic snapshot +window, one INTERNAL frame (`BLE_LOG_INT_SRC_TASK_BINDING`) packs one +fixed-layout record per registered entry, binding each id to its task name. +It rides the registry's own dedicated transport with a sequence of its own +(a window skipped despite a non-empty registry leaves a gap in the binding +sequence, never in the Global SN or snapshot anchor count), is never counted in the +per-source written/lost stats, and never contends with the snapshot +transport or with user records for pool transports. A receiver that joined +late or lost a frame converges on the next window; a record of a new task +keeps its id and is bound by name at the next window. Announcements are +idempotent on the wire and best effort (a busy binding transport — the +previous broadcast still in DMA — skips a window). +Attribution is a property of the record, so concurrent writers to one source +need no serialization and never drop a record on contention. When the registry +is full, a new task degrades to the unknown id (`0xFF`) and its records are +still emitted. + +## Configuration + +| Option | Default | Meaning | +| --- | ---: | --- | +| `CONFIG_BLE_LOG_ENABLED` | n | Enable BLE Log | +| `CONFIG_BLE_LOG_POOL_TRANS_CNT` | 8 | Unified pool transport count, range 2..32 | +| `CONFIG_BLE_LOG_POOL_NON_YIELD_RESERVE_CNT` | 1 | ISR/critical reserve count | +| `CONFIG_BLE_LOG_POOL_TRANS_SIZE` | 640 | Bytes per shared transport; SPI builds require a multiple of four | +| `CONFIG_BLE_LOG_LL_ENABLED` | target dependent | Controller LL logging | +| `CONFIG_BLE_LOG_HCI_LOG_ENABLED` | y | HCI capture from Host or Controller, selected by transport | +| `CONFIG_BLE_LOG_TASK_ID_MAX` | 16 | Task-id registry size, range 2..32; 16 bytes of RAM per entry | +| `CONFIG_BLE_LOG_TS_SYNC_TOGGLE_IO_ENABLED` | n | Build the optional analyzer GPIO toggle | +| `CONFIG_BLE_LOG_TS_ENABLED` | n | Deprecated compatibility entry selecting the GPIO toggle | + +Old multi-LBM sizing options remain hidden only so existing sdkconfig files can +be parsed; they no longer control allocation. + +## Validation apps + +```bash +. ./export.sh +cd components/bt/common/ble_log/test_apps/ble_log_test +idf.py build + +cd ../ble_log_rt_test +idf.py build + +cd ../ble_log_perf_test +idf.py build ``` -## Configuration Options - -### Basic Configuration - -| Configuration | Default | Description | -|---------------|---------|-------------| -| `CONFIG_BLE_LOG_ENABLED` | n | Enable BT Log Async Output | -| `CONFIG_BLE_LOG_LBM_TRANS_BUF_SIZE` | 2048 | Total buffer memory per common LBM (bytes). Divided equally among `BLE_LOG_TRANS_BUF_CNT` (4) internal transport buffers. | -| `CONFIG_BLE_LOG_LBM_ATOMIC_LOCK_TASK_CNT` | 2 | Number of atomic lock LBMs for task context | -| `CONFIG_BLE_LOG_LBM_ATOMIC_LOCK_ISR_CNT` | 1 | Number of atomic lock LBMs for ISR context | - -### Link Layer Configuration - -| Configuration | Default | Description | -|---------------|---------|-------------| -| `CONFIG_BLE_LOG_LL_ENABLED` | y | Enable Link Layer logging | -| `CONFIG_BLE_LOG_LBM_LL_TRANS_BUF_SIZE` | 2048 | Total buffer memory per Link Layer LBM (bytes). Divided equally among `BLE_LOG_TRANS_BUF_CNT` (4) internal transport buffers. | - -### Other Features - -| Configuration | Default | Description | -|---------------|---------|-------------| -| `CONFIG_BLE_LOG_TS_ENABLED` | n | Enable timestamp synchronization | -| `CONFIG_BLE_LOG_HOST_HCI_LOG_ENABLED` | n | Enable BLE Host side HCI logging | - -> **Note**: Payload checksum and enhanced statistics are now always enabled and no longer have separate Kconfig options. - -### Transport Method Configuration - -| Transport | Configuration | Description | -|-----------|---------------|-------------| -| Dummy | `CONFIG_BLE_LOG_PRPH_DUMMY` | Debug dummy transport (default unless `SOC_UHCI_SUPPORTED`) | -| SPI Master DMA | `CONFIG_BLE_LOG_PRPH_SPI_MASTER_DMA` | SPI DMA transport | -| UART DMA | `CONFIG_BLE_LOG_PRPH_UART_DMA` | UART DMA transport (default when `SOC_UHCI_SUPPORTED`). Default baud rate: 3000000. | - -### Deprecated / Removed - -| Module | Status | Notes | -|--------|--------|-------| -| Legacy SPI Log Output | Deprecated | Moved to `deprecated/` directory. Use BT Log Async Output instead. A separate Kconfig menu "Legacy SPI Log Output" is available for backward compatibility. | -| UHCI Out | Removed | The standalone UHCI Out module (`ble_log_uhci_out.c`) has been removed. UART DMA transport under the main BLE Log peripheral interface replaces it. | - -## API Reference - -### Core API - -#### `bool ble_log_init(void)` - -Initialize the BLE Log module. - -**Return Value**: -- `true`: Initialization successful -- `false`: Initialization failed - -**Note**: Must be called before using any other APIs. - -#### `void ble_log_deinit(void)` - -Cleanup the BLE Log module and release all resources. - -**Note**: -- All pending logs will be lost after calling this function -- Peripheral interface will be cleaned up first to avoid DMA transmission issues during memory release -- This function may run concurrently with BLE Log write APIs. New writes are - rejected after shutdown begins, and deinit waits for in-progress writers - before releasing their buffers. - -#### `bool ble_log_write_hex(ble_log_src_t src_code, const uint8_t *addr, size_t len)` - -Write hexadecimal log data. - -**Parameters**: -- `src_code`: Log source code -- `addr`: Data pointer -- `len`: Data length - -**Return Value**: -- `true`: Write successful -- `false`: Write failed (module not initialized or insufficient buffer) - -#### `void ble_log_flush(void)` - -End the current logging session and flush pending logs immediately. - -This API temporarily suspends ordinary log writes, waits for in-progress -writers to exit, emits a final statistics internal frame, flushes pending -transport buffers, resets statistics, and then restores the enable state that -was in effect before the call. - -**Note**: This operation is blocking and must run in an ordinary caller-owned -FreeRTOS task. Do not call it from an ISR or a system callback such as an ESP -Timer callback. If BLE Log was enabled before the call, it remains enabled -after the flush completes. - -#### `void ble_log_dump_to_console(void)` - -Output all buffer contents to console in hexadecimal format for debugging. - -### Log Source Types - -```c -typedef enum { - BLE_LOG_SRC_INTERNAL = 0, // Internal system logs - BLE_LOG_SRC_CUSTOM, // User-defined logs - BLE_LOG_SRC_LL_TASK, // Link Layer task logs - BLE_LOG_SRC_LL_HCI, // Link Layer HCI logs - BLE_LOG_SRC_LL_ISR, // Link Layer interrupt logs - BLE_LOG_SRC_HOST, // Host layer logs - BLE_LOG_SRC_HCI, // HCI layer logs - BLE_LOG_SRC_ENCODE, // Encoding layer logs - BLE_LOG_SRC_REDIR, // UART redirection (PORT 0 only) - BLE_LOG_SRC_MAX, -} ble_log_src_t; -``` - -### HCI Log Macro - -```c -#define ble_log_write_hci(direction, data, len) -``` - -Writes an HCI packet with direction encoding. `direction` is `BLE_LOG_HCI_DOWNSTREAM` (0) or `BLE_LOG_HCI_UPSTREAM` (1). Direction is encoded in the MSB of the first byte (HCI type). - -### Link Layer API (Conditional Compilation) - -#### `void ble_log_write_hex_ll(uint32_t len, const uint8_t *addr, uint32_t len_append, const uint8_t *addr_append, uint32_t flag)` - -Link Layer dedicated log writing interface. - -**Parameters**: -- `len`: Main data length -- `addr`: Main data pointer -- `len_append`: Append data length -- `addr_append`: Append data pointer -- `flag`: Log flag bits - -**Flag Definitions**: -```c -enum { - BLE_LOG_LL_FLAG_CONTINUE = 0, - BLE_LOG_LL_FLAG_END, - BLE_LOG_LL_FLAG_TASK, - BLE_LOG_LL_FLAG_ISR, - BLE_LOG_LL_FLAG_HCI, - BLE_LOG_LL_FLAG_RAW, - BLE_LOG_LL_FLAG_OMDATA, - BLE_LOG_LL_FLAG_HCI_UPSTREAM, -}; -``` - -### Timestamp Synchronization API (Conditional Compilation) - -#### `bool ble_log_sync_enable(bool enable)` - -Enable or disable timestamp synchronization functionality. - -**Parameters**: -- `enable`: true to enable, false to disable - -**Return Value**: -- `true`: Operation successful -- `false`: Operation failed (module not initialized) - -## Usage Examples - -### Example 1: Basic Logging - -```c -#include "ble_log.h" - -void example_basic_logging() { - // Initialize - if (!ble_log_init()) { - printf("BLE Log init failed\n"); - return; - } - - // Log some example data - uint8_t hci_cmd[] = {0x01, 0x03, 0x0C, 0x00}; // HCI Reset Command - ble_log_write_hex(BLE_LOG_SRC_HCI, hci_cmd, sizeof(hci_cmd)); - - uint8_t host_data[] = {0x02, 0x00, 0x20, 0x0B, 0x00, 0x07, 0x00, 0x04, 0x00, 0x10, 0x01, 0x00, 0xFF, 0xFF, 0x00, 0x28}; - ble_log_write_hex(BLE_LOG_SRC_HOST, host_data, sizeof(host_data)); - - // End session and flush buffers - ble_log_flush(); - - // Cleanup - ble_log_deinit(); -} -``` - -### Example 2: ISR Context Logging - -```c -void IRAM_ATTR some_isr_handler() { - uint8_t isr_data[] = {0xDE, 0xAD, 0xBE, 0xEF}; - - // Safe to write logs in ISR context - ble_log_write_hex(BLE_LOG_SRC_LL_ISR, isr_data, sizeof(isr_data)); -} -``` - -### Example 3: Logging with Timestamp Synchronization - -```c -void example_with_timestamp_sync() { - if (!ble_log_init()) { - return; - } - - #if CONFIG_BLE_LOG_TS_ENABLED - // Enable timestamp synchronization - ble_log_sync_enable(true); - #endif - - // Log data... - uint8_t data[] = {0x01, 0x02, 0x03}; - ble_log_write_hex(BLE_LOG_SRC_CUSTOM, data, sizeof(data)); - - // Timestamp information will be automatically included in logs - - ble_log_deinit(); -} -``` - -### Example 4: Performance Testing - -```c -void example_performance_test() { - if (!ble_log_init()) { - return; - } - - uint8_t test_data[100]; - for (int i = 0; i < 100; i++) { - test_data[i] = i; - } - - uint32_t start_time = esp_timer_get_time(); - - // Send 1000 logs - for (int i = 0; i < 1000; i++) { - ble_log_write_hex(BLE_LOG_SRC_CUSTOM, test_data, sizeof(test_data)); - } - - // End session and flush buffers - ble_log_flush(); - uint32_t end_time = esp_timer_get_time(); - - printf("Time to write 1000 logs: %lu us\n", end_time - start_time); - - ble_log_deinit(); -} -``` - -## Performance & Memory Optimization - -### Memory Usage Estimation - -Each LBM's total buffer memory is configured directly via Kconfig. The configured value is divided equally among `BLE_LOG_TRANS_BUF_CNT` (currently 4) internal transport buffers. - -Memory usage under default configuration: - -``` -Common Pool: - LBM count = Atomic Task (2) + Atomic ISR (1) + Spinlock (2) = 5 - Total = 5 × BLE_LOG_LBM_TRANS_BUF_SIZE = 5 × 2048 = 10240 bytes - -Link Layer Pool (when CONFIG_BLE_LOG_LL_ENABLED): - LBM count = 2 (LL task + LL HCI) - Total = 2 × BLE_LOG_LBM_LL_TRANS_BUF_SIZE = 2 × 2048 = 4096 bytes - -Statistics (always enabled): - Total = BLE_LOG_SRC_MAX × sizeof(ble_log_stat_mgr_t) = 9 × 20 = 180 bytes - -UART Redirect (when UART DMA on PORT 0): - Additional BLE_LOG_TRANS_BUF_CNT (4) transport buffers -``` - -### Performance Optimization Recommendations - -1. **Adjust LBM Count**: Adjust atomic lock LBM count based on concurrency requirements -2. **Buffer Size**: Adjust total buffer memory per LBM based on log volume; must be a multiple of `BLE_LOG_TRANS_BUF_CNT` (4) -3. **Transport Method**: Choose optimal transport method based on hardware (UART DMA is default on supported SoCs) - -### Real-time Considerations - -- Critical code paths are marked with `BLE_LOG_IRAM_ATTR` and run in IRAM -- Atomic operations avoid lock contention -- Multi-buffer transport ensures continuous writing even when some buffers are in-flight - -## Troubleshooting - -### Common Issues - -#### 1. Initialization Failure - -**Symptoms**: `ble_log_init()` returns `false` - -**Possible Causes**: -- Insufficient memory -- Peripheral configuration error -- Duplicate initialization - -**Solutions**: -```c -// Check available memory -printf("Free heap: %d bytes\n", esp_get_free_heap_size()); - -// Ensure initialization only happens once -static bool initialized = false; -if (!initialized) { - initialized = ble_log_init(); -} -``` - -#### 2. Log Loss - -**Symptoms**: Some logs don't appear in output - -**Possible Causes**: -- Buffer overflow -- Transmission speed can't keep up with write speed -- Module not properly initialized - -**Solutions**: -```c -// Enhanced statistics are always enabled — check written/lost frame -// and byte counts in the log stream output - -// Increase total buffer memory per LBM -// CONFIG_BLE_LOG_LBM_TRANS_BUF_SIZE=4096 - -// Increase atomic lock LBM count -// CONFIG_BLE_LOG_LBM_ATOMIC_LOCK_TASK_CNT=4 -``` - -#### 3. Performance Issues - -**Symptoms**: System response becomes slow - -**Possible Causes**: -- Transmission bottleneck -- Lock contention - -**Solutions**: -```c -// Use faster transmission method -// CONFIG_BLE_LOG_PRPH_UART_DMA=y (default on SOC_UHCI_SUPPORTED targets) - -// Increase baud rate (default is now 3000000) -// CONFIG_BLE_LOG_PRPH_UART_DMA_BAUD_RATE=3000000 -``` - -### Debugging Techniques - -#### 1. Use Dummy Transport for Debugging - -```c -// Select Dummy transport in menuconfig -// Then use dump function to view buffer contents -ble_log_dump_to_console(); -``` - -#### 2. Check Enhanced Statistics - -```c -// Enhanced statistics are always enabled -// Written/lost frame and byte counts are automatically output to logs -``` - -#### 3. Monitor Memory Usage - -```c -void monitor_memory() { - printf("Free heap before init: %d\n", esp_get_free_heap_size()); - ble_log_init(); - printf("Free heap after init: %d\n", esp_get_free_heap_size()); -} -``` - -## Important Notes - -### Buffer Size Constraints - -- `CONFIG_BLE_LOG_LBM_TRANS_BUF_SIZE` and `CONFIG_BLE_LOG_LBM_LL_TRANS_BUF_SIZE` must be multiples of `BLE_LOG_TRANS_BUF_CNT` (currently 4) -- The per-buffer size (total ÷ 4) must be at least large enough to hold one frame overhead (`BLE_LOG_FRAME_OVERHEAD`) -- `BLE_LOG_TRANS_BUF_CNT` must be a power of 2 - -### Migration from Legacy Modules - -- **UHCI Out**: The standalone `ble_log_uhci_out` module has been removed. Use the UART DMA peripheral transport (`CONFIG_BLE_LOG_PRPH_UART_DMA`) instead. -- **SPI Out**: The legacy SPI log output has been moved to `deprecated/`. A separate Kconfig menu "Legacy SPI Log Output (Deprecated)" is available for backward compatibility, but new projects should use BT Log Async Output with the SPI Master DMA peripheral transport. +`ble_log_test` validates golden v8 bytes, the consolidated Internal Snapshot, +source/HCI metadata and capture selection, pool exhaustion and reserve use, +snapshot busy loss, stale claims, and enable/disable/deinit races. +`ble_log_rt_test` covers batched dispatch, timer behavior, inflight statistics, +and repeated deinit races. diff --git a/components/bt/common/ble_log/extension/log_compression/Kconfig.in b/components/bt/common/ble_log/extension/log_compression/Kconfig.in index 9b2f442a547..4db87fcdc63 100644 --- a/components/bt/common/ble_log/extension/log_compression/Kconfig.in +++ b/components/bt/common/ble_log/extension/log_compression/Kconfig.in @@ -15,6 +15,7 @@ config BLE_COMPRESSED_LOG_ENABLE for installation instructions. if BLE_COMPRESSED_LOG_ENABLE + source "$IDF_PATH/components/bt/common/ble_log/extension/log_compression/profile/mesh/Kconfig.mesh.in" source "$IDF_PATH/components/bt/common/ble_log/extension/log_compression/iso/audio/Kconfig.iso.in" source "$IDF_PATH/components/bt/common/ble_log/extension/log_compression/host/Kconfig.host.in" diff --git a/components/bt/common/ble_log/extension/log_compression/ble_log_compression.c b/components/bt/common/ble_log/extension/log_compression/ble_log_compression.c index 908f80289c1..b86b6f3b275 100644 --- a/components/bt/common/ble_log/extension/log_compression/ble_log_compression.c +++ b/components/bt/common/ble_log/extension/log_compression/ble_log_compression.c @@ -9,14 +9,29 @@ // Private includes #include "freertos/FreeRTOS.h" -#include "freertos/task.h" #include "sdkconfig.h" +#include "ble_log_lbm_v2.h" +#include "ble_log_task_registry.h" #include "ble_log_util.h" #include "log_compression/utils.h" #if CONFIG_BLE_COMPRESSED_LOG_ENABLE -#define BLE_CP_DROP_LOG_PERIOD 256U +#if CONFIG_BLE_MESH_COMPRESSED_LOG_ENABLE +_Static_assert(CONFIG_BLE_MESH_COMPRESSED_LOG_BUFFER_LEN <= + BLE_LOG_MAX_PAYLOAD_LEN - sizeof(uint32_t), + "Mesh compressed log record exceeds one BLE Log transport"); +#endif +#if CONFIG_BLE_ISO_COMPRESSED_LOG_ENABLE +_Static_assert(CONFIG_BLE_ISO_COMPRESSED_LOG_BUFFER_LEN <= + BLE_LOG_MAX_PAYLOAD_LEN - sizeof(uint32_t), + "ISO compressed log record exceeds one BLE Log transport"); +#endif +#if CONFIG_BLE_HOST_COMPRESSED_LOG_ENABLE +_Static_assert(CONFIG_BLE_HOST_COMPRESSED_LOG_BUFFER_LEN <= + BLE_LOG_MAX_PAYLOAD_LEN - sizeof(uint32_t), + "Host compressed log record exceeds one BLE Log transport"); +#endif #define BLE_CP_TRY_PUSH(expr) do { \ if ((expr) != 0) { \ @@ -24,88 +39,30 @@ } \ } while (0) -#define BUF_NAME(name, idx) name##_buffer##idx -#define BUF_MGMT_NAME(name) name##_log_buffer_mgmt - -#define DECL_BUF_OP(name, len, idx) \ - static uint8_t BUF_NAME(name, idx)[len]; - -#define INIT_MAP_OP(name, _, buffer_idx) \ - {.busy = 0, \ - .idx = 0, \ - .buffer = BUF_NAME(name, buffer_idx), \ - .len = sizeof(BUF_NAME(name, buffer_idx))}, - -#define DECLARE_BUFFERS(NAME, BUF_LEN, BUF_CNT) \ - FOR_EACH_IDX(DECL_BUF_OP, NAME, BUF_LEN, GEN_INDEX(BUF_CNT)); - -#define INIT_BUFFER_MGMT(NAME, BUF_CNT) \ - ble_cp_log_buffer_mgmt_t BUF_MGMT_NAME(NAME)[BUF_CNT] = { \ - FOR_EACH_IDX(INIT_MAP_OP, NAME, 0, GEN_INDEX(BUF_CNT)) \ - }; - -#if CONFIG_BLE_MESH_COMPRESSED_LOG_ENABLE -DECLARE_BUFFERS(mesh, CONFIG_BLE_MESH_COMPRESSED_LOG_BUFFER_LEN, LOG_CP_MAX_LOG_BUFFER_USED_SIMU); -INIT_BUFFER_MGMT(mesh, LOG_CP_MAX_LOG_BUFFER_USED_SIMU); -char * mesh_last_task_handle = NULL; -#endif - -#if CONFIG_BLE_ISO_COMPRESSED_LOG_ENABLE -/* The BLE_ISO buffer is shared by every source compiled into the unified - * ISO channel: esp_ble_iso, esp_ble_audio (and future ISO consumers, e.g. - * HID-over-ISO), as well as the AUDIO_LIB runtime callback (prebuilt - * libble_audio.a, source code 5) — they all funnel here. */ -DECLARE_BUFFERS(iso, CONFIG_BLE_ISO_COMPRESSED_LOG_BUFFER_LEN, LOG_CP_MAX_LOG_BUFFER_USED_SIMU); -INIT_BUFFER_MGMT(iso, LOG_CP_MAX_LOG_BUFFER_USED_SIMU); -char * iso_last_task_handle = NULL; -#endif - -#if CONFIG_BLE_HOST_COMPRESSED_LOG_ENABLE && CONFIG_BT_BLUEDROID_ENABLED -DECLARE_BUFFERS(host, CONFIG_BLE_HOST_COMPRESSED_LOG_BUFFER_LEN, LOG_CP_MAX_LOG_BUFFER_USED_SIMU); -INIT_BUFFER_MGMT(host, LOG_CP_MAX_LOG_BUFFER_USED_SIMU); -char * host_last_task_handle = NULL; -#endif - -#if CONFIG_BLE_HOST_COMPRESSED_LOG_ENABLE && CONFIG_BT_NIMBLE_ENABLED -DECLARE_BUFFERS(nimble, CONFIG_BLE_HOST_COMPRESSED_LOG_BUFFER_LEN, LOG_CP_MAX_LOG_BUFFER_USED_SIMU); -INIT_BUFFER_MGMT(nimble, LOG_CP_MAX_LOG_BUFFER_USED_SIMU); -char * nimble_last_task_handle = NULL; -#endif - /* The maximum number of supported parameters is 64 */ #define LOG_HEADER(log_type, info) ((log_type << 6) | (info & 0x3f)) -int ble_compressed_log_cb_get(uint8_t source, ble_cp_log_buffer_mgmt_t **mgmt) +int ble_compressed_log_cb_get(uint8_t source, ble_cp_log_buffer_mgmt_t *mgmt) { - ble_cp_log_buffer_mgmt_t *buffer_mgmt = NULL; - char ** last_handle = NULL; - char * cur_handle = pcTaskGetName(NULL); + uint16_t claim_len = 0; switch (source) { #if CONFIG_BLE_MESH_COMPRESSED_LOG_ENABLE case BLE_COMPRESSED_LOG_OUT_SOURCE_MESH: case BLE_COMPRESSED_LOG_OUT_SOURCE_MESH_LIB: - buffer_mgmt = BUF_MGMT_NAME(mesh); - last_handle = &mesh_last_task_handle; + claim_len = CONFIG_BLE_MESH_COMPRESSED_LOG_BUFFER_LEN; break; #endif #if CONFIG_BLE_ISO_COMPRESSED_LOG_ENABLE case BLE_COMPRESSED_LOG_OUT_SOURCE_ISO: case BLE_COMPRESSED_LOG_OUT_SOURCE_AUDIO_LIB: - buffer_mgmt = BUF_MGMT_NAME(iso); - last_handle = &iso_last_task_handle; + claim_len = CONFIG_BLE_ISO_COMPRESSED_LOG_BUFFER_LEN; break; #endif #if CONFIG_BLE_HOST_COMPRESSED_LOG_ENABLE && (CONFIG_BT_BLUEDROID_ENABLED || CONFIG_BT_NIMBLE_ENABLED) case BLE_COMPRESSED_LOG_OUT_SOURCE_HOST: -#if CONFIG_BT_BLUEDROID_ENABLED - buffer_mgmt = BUF_MGMT_NAME(host); - last_handle = &host_last_task_handle; -#elif CONFIG_BT_NIMBLE_ENABLED - buffer_mgmt = BUF_MGMT_NAME(nimble); - last_handle = &nimble_last_task_handle; -#endif + claim_len = CONFIG_BLE_HOST_COMPRESSED_LOG_BUFFER_LEN; break; #endif default: @@ -113,37 +70,36 @@ int ble_compressed_log_cb_get(uint8_t source, ble_cp_log_buffer_mgmt_t **mgmt) return -1; } - for (int i = 0; i < LOG_CP_MAX_LOG_BUFFER_USED_SIMU; i++) { - if (ble_log_cas_acquire(&(buffer_mgmt[i].busy))) { - *mgmt = &buffer_mgmt[i]; - if (ble_log_cp_push_u8(*mgmt, source) != 0) { - (*mgmt)->idx = 0; - ble_log_cas_release(&((*mgmt)->busy)); - return -1; - } - if (*last_handle == NULL || - *last_handle != cur_handle) { - if (ble_log_cp_push_u8(*mgmt, LOG_HEADER(LOG_TYPE_INFO, LOG_TYPE_INFO_TASK_SWITCH)) != 0) { - (*mgmt)->idx = 0; - ble_log_cas_release(&((*mgmt)->busy)); - return -1; - } - *last_handle = cur_handle; - } - return 0; - } + mgmt->len = claim_len; + mgmt->idx = 0; + mgmt->buffer = ble_log_claim(BLE_LOG_SRC_ENCODE, claim_len, + &mgmt->handle, true); + if (!mgmt->buffer) { + return -1; } - - return -1; + /* Resolve the task id only after the claim succeeded: the claim's + * lifetime reference pins the current registry epoch, so the id + * cannot cross an init/deinit boundary (which reassigns ids from 0). + * Resolving before the claim instead left a window where a full + * deinit/reinit completed and the stale id named a different task. */ + uint8_t task_id = ble_log_task_id_current(); + if (ble_log_cp_push_u8(mgmt, source) != 0 || + ble_log_cp_push_u8(mgmt, task_id) != 0) { + ble_log_commit(mgmt->handle, 0); + return -1; + } + return 0; } -static inline int ble_compressed_log_buffer_free(ble_cp_log_buffer_mgmt_t *mgmt) +static inline int ble_compressed_log_commit(ble_cp_log_buffer_mgmt_t *mgmt) { -#if BLE_LOG_CP_CONTENT_CHECK_ENABLE - memset(mgmt->buffer, BLE_LOG_CP_CONTENT_CHECK_VAL, mgmt->idx); -#endif - mgmt->idx = 0; - ble_log_cas_release(&mgmt->busy); + ble_log_commit(mgmt->handle, mgmt->idx); + return 0; +} + +static inline int ble_compressed_log_abort(ble_cp_log_buffer_mgmt_t *mgmt) +{ + ble_log_commit(mgmt->handle, 0); return 0; } @@ -287,76 +243,75 @@ int ble_log_compressed_hex_print_internal(ble_cp_log_buffer_mgmt_t *mgmt, uint32 int ble_log_compressed_hex_printv(uint8_t source, uint32_t log_index, size_t args_cnt, va_list args) { - ble_cp_log_buffer_mgmt_t *mgmt = NULL; + ble_cp_log_buffer_mgmt_t mgmt; if (ble_compressed_log_cb_get(source, &mgmt)) { return 0; } - if (ble_log_compressed_hex_print_internal(mgmt, log_index, args_cnt, args) != 0) { - ble_compressed_log_buffer_free(mgmt); + if (ble_log_compressed_hex_print_internal(&mgmt, log_index, args_cnt, args) != 0) { + ble_compressed_log_abort(&mgmt); return 0; } - ble_compressed_log_output(source, mgmt->buffer, mgmt->idx); - ble_compressed_log_buffer_free(mgmt); + ble_compressed_log_commit(&mgmt); return 0; } int ble_log_compressed_hex_print(uint8_t source, uint32_t log_index, size_t args_cnt, ...) { - ble_cp_log_buffer_mgmt_t *mgmt = NULL; + ble_cp_log_buffer_mgmt_t mgmt; if (ble_compressed_log_cb_get(source, &mgmt)) { return 0; } if (args_cnt == 0) { - if (ble_log_cp_push_u8(mgmt, LOG_HEADER(LOG_TYPE_HEX_ARGS, 0)) != 0 || - ble_log_cp_push_u16(mgmt, log_index) != 0) { - ble_compressed_log_buffer_free(mgmt); + if (ble_log_cp_push_u8(&mgmt, LOG_HEADER(LOG_TYPE_HEX_ARGS, 0)) != 0 || + ble_log_cp_push_u16(&mgmt, log_index) != 0) { + ble_compressed_log_abort(&mgmt); return 0; } } else { va_list args; va_start(args, args_cnt); - if (ble_log_compressed_hex_print_internal(mgmt, log_index, args_cnt, args) != 0) { + if (ble_log_compressed_hex_print_internal(&mgmt, log_index, args_cnt, args) != 0) { va_end(args); - ble_compressed_log_buffer_free(mgmt); + ble_compressed_log_abort(&mgmt); return 0; } va_end(args); } - ble_compressed_log_output(source, mgmt->buffer, mgmt->idx); - ble_compressed_log_buffer_free(mgmt); + ble_compressed_log_commit(&mgmt); return 0; } int ble_log_compressed_hex_print_buf(uint8_t source, uint32_t log_index, uint8_t buf_idx, const uint8_t *buf, size_t len) { - ble_cp_log_buffer_mgmt_t *mgmt = NULL; + ble_cp_log_buffer_mgmt_t mgmt; if (ble_compressed_log_cb_get(source, &mgmt)) { return 0; } if (buf == NULL) { - ble_log_cp_push_u8(mgmt, LOG_HEADER(LOG_TYPE_INFO, LOG_TYPE_INFO_NULL_BUF)); - ble_log_cp_push_u16(mgmt, log_index); - ble_compressed_log_output(source, mgmt->buffer, mgmt->idx); - ble_compressed_log_buffer_free(mgmt); + if (ble_log_cp_push_u8(&mgmt, LOG_HEADER(LOG_TYPE_INFO, LOG_TYPE_INFO_NULL_BUF)) != 0 || + ble_log_cp_push_u16(&mgmt, log_index) != 0) { + ble_compressed_log_abort(&mgmt); + return 0; + } + ble_compressed_log_commit(&mgmt); return 0; } - if (ble_log_cp_push_u8(mgmt, LOG_HEADER(LOG_TYPE_HEX_BUF, buf_idx)) != 0 || - ble_log_cp_push_u16(mgmt, log_index) != 0 || - ble_log_cp_push_u16(mgmt, (uint16_t)len) != 0 || - ble_log_cp_push_buf(mgmt, buf, (uint16_t)len) != 0) { - ble_compressed_log_buffer_free(mgmt); + if (ble_log_cp_push_u8(&mgmt, LOG_HEADER(LOG_TYPE_HEX_BUF, buf_idx)) != 0 || + ble_log_cp_push_u16(&mgmt, log_index) != 0 || + ble_log_cp_push_u16(&mgmt, (uint16_t)len) != 0 || + ble_log_cp_push_buf(&mgmt, buf, (uint16_t)len) != 0) { + ble_compressed_log_abort(&mgmt); return 0; } - ble_compressed_log_output(source, mgmt->buffer, mgmt->idx); - ble_compressed_log_buffer_free(mgmt); + ble_compressed_log_commit(&mgmt); return 0; } #endif /* CONFIG_BLE_COMPRESSED_LOG_ENABLE */ diff --git a/components/bt/common/ble_log/extension/log_compression/include/log_compression/utils.h b/components/bt/common/ble_log/extension/log_compression/include/log_compression/utils.h index 6197b1d6e0f..969e85e9d94 100644 --- a/components/bt/common/ble_log/extension/log_compression/include/log_compression/utils.h +++ b/components/bt/common/ble_log/extension/log_compression/include/log_compression/utils.h @@ -10,49 +10,6 @@ #include #include -#ifndef CONCAT -#define CONCAT(a, b) a##b -#endif - -#ifndef _CONCAT -#define _CONCAT(a, b) CONCAT(a, b) -#endif - -#define _0 0 -#define _1 1 -#define _2 2 -#define _3 3 -#define _4 4 -#define _5 5 -#define _6 6 -#define _7 7 -#define _8 8 -#define _9 9 - -#define __COUNT_ARGS(_0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _n, X...) _n -#define COUNT_ARGS(X...) __COUNT_ARGS(, ##X, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0) - -#ifndef FOR_EACH_IDX -#define FOR_EACH_IDX(macro, name, len, ...) \ - _CONCAT(_FOR_EACH_, COUNT_ARGS(__VA_ARGS__))(macro, name, len, __VA_ARGS__) -#endif - -#define _FOR_EACH_0(m, n, l, ...) -#define _FOR_EACH_1(m, n, l, i1, ...) m(n, l, i1) -#define _FOR_EACH_2(m, n, l, i1, ...) m(n, l, i1) _FOR_EACH_1(m, n, l, __VA_ARGS__) -#define _FOR_EACH_3(m, n, l, i1, ...) m(n, l, i1) _FOR_EACH_2(m, n, l, __VA_ARGS__) -#define _FOR_EACH_4(m, n, l, i1, ...) m(n, l, i1) _FOR_EACH_3(m, n, l, __VA_ARGS__) -#define _FOR_EACH_5(m, n, l, i1, ...) m(n, l, i1) _FOR_EACH_4(m, n, l, __VA_ARGS__) - -#define _GEN_INDEX_0() -#define _GEN_INDEX_1() _0 -#define _GEN_INDEX_2() _0, _1 -#define _GEN_INDEX_3() _0, _1, _2 -#define _GEN_INDEX_4() _0, _1, _2, _3 -#define _GEN_INDEX_5() _0, _1, _2, _3, _4 -#define GEN_INDEX(n) _CONCAT(_GEN_INDEX_, n)() - - enum { BLE_COMPRESSED_LOG_OUT_SOURCE_HOST, BLE_COMPRESSED_LOG_OUT_SOURCE_MESH, @@ -72,47 +29,30 @@ enum { ARG_SIZE_TYPE_MAX, }; -/* The maximum number of buffers used simultaneously */ -#define LOG_CP_MAX_LOG_BUFFER_USED_SIMU 3 - #define LOG_TYPE_ZERO_ARGS 0 #define LOG_TYPE_HEX_ARGS 1 #define LOG_TYPE_HEX_BUF 2 -/* This type of message is used to update log information, - * such as there is currently a new task log */ +/* Informational in-band records (protocol v8). Task-id bindings used to + * be one of these; they are broadcast as INTERNAL frames on the periodic + * snapshot window now (see ble_log_task_registry.h), keeping the ENCODE + * stream user records only. */ #define LOG_TYPE_INFO 3 -#define LOG_TYPE_INFO_TASK_ID_UPDATE 0 #define LOG_TYPE_INFO_NULL_BUF 1 -#define LOG_TYPE_INFO_TASK_SWITCH 2 typedef struct { - volatile bool busy; - uint8_t *buffer; - uint16_t idx; - uint16_t len; + uint8_t *buffer; /* claim() payload pointer */ + uint16_t idx; /* bytes written so far */ + uint16_t len; /* claimed capacity (max_len) */ + uint32_t handle; /* claim handle for commit/abort */ } ble_cp_log_buffer_mgmt_t; -#define CONTENT_CHECK(idx, buf, except_val, len) -#define LENGTH_CHECK(idx, pbuffer_mgmt) do ( if(unlikely((idx) > (pbuffer_mgmt->len))) assert(0 && "Maximum log buffer length exceeded");) while(0) - -#define BLE_LOG_CP_CONTENT_CHECK_ENABLE 0 -#define BLE_LOG_CP_CONTENT_CHECK_VAL 0x00 - static inline int ble_log_cp_buffer_safe_check(ble_cp_log_buffer_mgmt_t *pbuf_mgmt, uint16_t write_len) { if ((pbuf_mgmt->idx + write_len) > pbuf_mgmt->len) { printf("Maximum length of buffer(%p) idx %d write_len %d exceed\n", pbuf_mgmt, pbuf_mgmt->idx, write_len); return -1; } -#if BLE_LOG_CP_CONTENT_CHECK_ENABLE - for (int i = pbuf_mgmt->idx; i < pbuf_mgmt->idx + write_len; i++) { - if (pbuf_mgmt->buffer[i] != BLE_LOG_CP_CONTENT_CHECK_VAL) { - printf("The value(%02x) in the buffer does not match the expected(%02x)\n", pbuf_mgmt->buffer[i], BLE_LOG_CP_CONTENT_CHECK_VAL); - return -1; - } - } -#endif return 0; } @@ -182,17 +122,4 @@ static inline int ble_log_cp_update_half_byte(ble_cp_log_buffer_mgmt_t *pbuf_mgm return 0; } -static inline int ble_log_cp_buffer_print(ble_cp_log_buffer_mgmt_t *pbuf_mgmt) -{ - for (size_t i = 0; i < pbuf_mgmt->idx; i++) { - printf("%02x ", pbuf_mgmt->buffer[i]); - } - printf("\n"); - return 0; -} - -static inline int ble_compressed_log_output(uint8_t source, uint8_t *data, uint16_t len) -{ - return ble_log_write_hex(BLE_LOG_SRC_ENCODE, data, len); -} #endif /* _BLE_LOG_COMPRESSION_UTILS_H */ diff --git a/components/bt/common/ble_log/include/ble_log.h b/components/bt/common/ble_log/include/ble_log.h index 0a358a34f34..06c4003bd25 100644 --- a/components/bt/common/ble_log/include/ble_log.h +++ b/components/bt/common/ble_log/include/ble_log.h @@ -20,6 +20,8 @@ * The number of BLE Log source code will directly determine the number of statistic manager * memory requirements, keep it as less as possible; it's recommended to use subcode for more * log data structure decoding */ +/* CRITICAL: this enum is a public ABI and must not be reordered or renamed. + * Its values are the base on-wire source IDs of protocol v8 frames. */ typedef enum { /* Internal */ BLE_LOG_SRC_INTERNAL = 0, @@ -45,30 +47,45 @@ typedef enum { #define BLE_LOG_HCI_DOWNSTREAM 0 #define BLE_LOG_HCI_UPSTREAM 1 -/* HCI Log Write Macro - * Encodes direction in MSB of data[0] (HCI type byte) before writing. - * Safe because ble_log_write_hex -> ble_log_lbm_write_trans does synchronous memcpy. - * Parser reads MSB to determine direction; old firmware with MSB=0 defaults to "sent". */ -#define ble_log_write_hci(direction, data, len) do { \ - (data)[0] |= ((direction) << 7); \ - ble_log_write_hex(BLE_LOG_SRC_HCI, (data), (len)); \ - (data)[0] &= 0x7F; \ +/* Encodes HCI direction in payload byte 0 bit 7 for the synchronous copy, + * then restores the complete original HCI type byte. The caller guarantees a + * non-NULL buffer with len > 0. */ +#define ble_log_write_hci(direction, data, len) do { \ + uint8_t *const ble_log_hci_data__ = (data); \ + const uint8_t ble_log_hci_type__ = ble_log_hci_data__[0]; \ + ble_log_hci_data__[0] = (ble_log_hci_type__ & 0x7fU) | \ + ((direction) ? 0x80U : 0U); \ + (void)ble_log_write_hex(BLE_LOG_SRC_HCI, ble_log_hci_data__, \ + (len)); \ + ble_log_hci_data__[0] = ble_log_hci_type__; \ } while (0) /* INTERFACE */ bool ble_log_init(void); void ble_log_deinit(void); +/* Controls public producers only; periodic system output remains active. */ bool ble_log_enable(bool enable); /* Blocking; call only from a caller-owned task, not an ISR or system callback. */ void ble_log_flush(void); +/* Waits for a shared transport in ordinary yieldable tasks. The shared ESP + * Timer task, ISR and critical-section callers fail fast when none is available. */ bool ble_log_write_hex(ble_log_src_t src_code, const uint8_t *addr, size_t len); +/* Same backpressure as ble_log_write_hex(): ordinary yieldable tasks wait + * when wait_for_transport is true; false opts out. The shared ESP Timer + * task and non-yieldable contexts fail fast regardless of this flag. */ +uint8_t *ble_log_claim(ble_log_src_t src_code, size_t max_len, + uint32_t *handle, bool wait_for_transport); +void ble_log_commit(uint32_t handle, size_t actual_len); void ble_log_dump_to_console(void); #if CONFIG_BLE_LOG_LL_ENABLED void ble_log_write_hex_ll(uint32_t len, const uint8_t *addr, uint32_t len_append, const uint8_t *addr_append, uint32_t flag); #endif /* CONFIG_BLE_LOG_LL_ENABLED */ -#if CONFIG_BLE_LOG_TS_ENABLED +/* Task-context only. Controls the optional TS sync IO toggle, which starts + * disabled and low; this is a lifecycle-checked no-op when the toggle is not + * built. Periodic Internal Snapshots remain active in either state. */ +bool ble_log_ts_sync_io_toggle_enable(bool enable); +/* Backward-compatible name for ble_log_ts_sync_io_toggle_enable(). */ bool ble_log_sync_enable(bool enable); -#endif /* CONFIG_BLE_LOG_TS_ENABLED */ #endif /* __BLE_LOG_H__ */ diff --git a/components/bt/common/ble_log/src/ble_log.c b/components/bt/common/ble_log/src/ble_log.c index 6a4e1936515..f74219ab8dc 100644 --- a/components/bt/common/ble_log/src/ble_log.c +++ b/components/bt/common/ble_log/src/ble_log.c @@ -10,14 +10,11 @@ /* INCLUDE */ #include "ble_log.h" #include "ble_log_rt.h" -#include "ble_log_lbm.h" +#include "ble_log_lbm_v2.h" #include "ble_log_prph.h" #include "ble_log_util.h" #include "esp_log.h" #include "esp_system.h" -#if CONFIG_BLE_LOG_TS_ENABLED -#include "ble_log_ts.h" -#endif /* CONFIG_BLE_LOG_TS_ENABLED */ /* VARIABLE */ #define TAG "ble_log" @@ -38,31 +35,29 @@ bool ble_log_init(void) return true; } -#if CONFIG_BLE_LOG_TS_ENABLED - /* Initialize BLE Log TS */ - if (!ble_log_ts_init()) { - goto exit; - } -#endif /* CONFIG_BLE_LOG_TS_ENABLED */ - - /* Initialize BLE Log Runtime */ - if (!ble_log_rt_init()) { - goto exit; - } - - /* Initialize BLE Log LBM */ + /* Allocate pool and dedicated Internal transport before runtime starts. */ if (!ble_log_lbm_init()) { goto exit; } - /* Initialize BLE Log peripheral interface */ if (!ble_log_prph_init(BLE_LOG_TRANS_TOTAL_CNT)) { goto exit; } - /* Initialization done */ + if (!ble_log_rt_init()) { + goto exit; + } + ble_log_inited = true; - ble_log_enable(true); + /* Queue the required INIT snapshot before starting the periodic path or + * opening the public producer gate, so it starts the receiver epoch. + * INIT/FLUSH samples never toggle sync IO. */ + ble_log_ts_info_t ts_info; + ble_log_rt_ts_sample(&ts_info, false); + if (!ble_log_internal_snapshot(BLE_LOG_SNAPSHOT_REASON_INIT, &ts_info, true) || + !ble_log_rt_start_periodic() || !ble_log_enable(true)) { + goto exit; + } esp_err_t ret = esp_register_shutdown_handler(ble_log_shutdown_handler); if (ret == ESP_OK) { shutdown_handler_registered = true; @@ -70,12 +65,6 @@ bool ble_log_init(void) ESP_LOGW(TAG, "Register shutdown handler failed, ret = 0x%x", ret); } - /* Write initialization done log */ - ble_log_info_t ble_log_info = { - .int_src_code = BLE_LOG_INT_SRC_INIT_DONE, - .version = BLE_LOG_VERSION, - }; - ble_log_write_hex(BLE_LOG_SRC_INTERNAL, (const uint8_t *)&ble_log_info, sizeof(ble_log_info_t)); return true; exit: @@ -94,18 +83,26 @@ void ble_log_deinit(void) } } ble_log_inited = false; - ble_log_lbm_close(); + ble_log_lbm_begin_deinit(); - /* CRITICAL — Deinit ordering rationale: + /* Seal and dispatch residual pool and UART0 REDIR data while the runtime + * queue is still alive. The peripheral waits below complete delivery. */ + ble_log_lbm_drain_open_trans(); +#if BLE_LOG_UART_REDIR_ENABLED + (void)ble_log_prph_flush(); +#endif + + /* CRITICAL - Deinit ordering rationale: * * 1. The LBM writer gate is closed before submodule teardown. Writers * already inside the gate keep a reference until they finish; later - * writers are rejected. + * writers are rejected. With writers gone, the pool and REDIR drains + * seal and dispatch every residual frame while runtime is still live. * - * 2. Runtime dispatch must be stopped FIRST to prevent it from sending + * 2. Runtime dispatch is stopped FIRST to prevent it from sending * transports to an already-destroyed peripheral driver. Active - * submissions and callbacks finish before the timers are deleted; - * the queue is then drained and pending transports are discarded. + * submissions and callbacks finish before the timers and queue are + * deleted. * * 3. Peripheral interface is deinitialized SECOND. It waits for DMA * operations started before runtime dispatch stopped, then destroys @@ -113,13 +110,8 @@ void ble_log_deinit(void) * * 4. LBM is deinitialized LAST. At this point all DMA has completed * (ensured by step 3) and all queued transports have been drained - * (ensured by step 2), so freeing the buffers is safe. */ + * (ensured by steps 1 and 2), so freeing the buffers is safe. */ ble_log_rt_deinit(); ble_log_prph_deinit(); ble_log_lbm_deinit(); - -#if CONFIG_BLE_LOG_TS_ENABLED - /* Deinitialize BLE Log TS */ - ble_log_ts_deinit(); -#endif /* CONFIG_BLE_LOG_TS_ENABLED */ } diff --git a/components/bt/common/ble_log/src/ble_log_lbm.c b/components/bt/common/ble_log/src/ble_log_lbm.c deleted file mode 100644 index 3e5d2566094..00000000000 --- a/components/bt/common/ble_log/src/ble_log_lbm.c +++ /dev/null @@ -1,874 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2025-2026 Espressif Systems (Shanghai) CO LTD - * - * SPDX-License-Identifier: Apache-2.0 - */ -/* ------------------------------- */ -/* BLE Log - Log Buffer Management */ -/* ------------------------------- */ - -/* INCLUDE */ -#include "ble_log.h" -#include "ble_log_lbm.h" -#include "ble_log_rt.h" -#include "esp_log.h" - -#if CONFIG_BLE_LOG_LL_ENABLED && CONFIG_SOC_ESP_NIMBLE_CONTROLLER -#if CONFIG_BT_DUAL_MODE_ARCH -#include "ble_mbuf.h" -#define BLE_MBUF_COPY(buf, off, len, dst) ble_mbuf_copydata((struct ble_mbuf *)(buf), off, len, dst) -#else -#include "os/os_mbuf.h" -#define BLE_MBUF_COPY(buf, off, len, dst) os_mbuf_copydata((struct os_mbuf *)(buf), off, len, dst) -#endif // CONFIG_BT_DUAL_MODE_ARCH -#endif /* CONFIG_BLE_LOG_LL_ENABLED && CONFIG_SOC_ESP_NIMBLE_CONTROLLER */ - -/* VARIABLE */ -#define TAG "ble_log" -#define BLE_LOG_LBM_WAIT_TIMEOUT_MS (1000) - -BLE_LOG_STATIC BLE_LOG_DRAM_ATTR volatile uint32_t lbm_ref_count = 0; -BLE_LOG_STATIC BLE_LOG_DRAM_ATTR uint32_t lbm_inited = 0; -BLE_LOG_STATIC BLE_LOG_DRAM_ATTR uint32_t lbm_enabled = 0; -BLE_LOG_STATIC volatile bool flush_in_progress = false; -BLE_LOG_STATIC BLE_LOG_DRAM_ATTR ble_log_lbm_ctx_t *lbm_ctx = NULL; -BLE_LOG_STATIC BLE_LOG_DRAM_ATTR ble_log_stat_mgr_t *stat_mgr_ctx[BLE_LOG_SRC_MAX] = {0}; - -/* PRIVATE FUNCTION DECLARATION */ -BLE_LOG_STATIC -bool ble_log_lbm_acquire_trans(size_t log_len, ble_log_lbm_t **out_lbm, - ble_log_prph_trans_t ***out_trans); -BLE_LOG_STATIC void ble_log_lbm_release(ble_log_lbm_t *lbm); -BLE_LOG_STATIC void ble_log_lbm_submit_trans(ble_log_prph_trans_t **trans); -BLE_LOG_STATIC -ble_log_prph_trans_t **ble_log_lbm_get_trans(ble_log_lbm_t *lbm, size_t log_len); -BLE_LOG_STATIC bool ble_log_lbm_flush_all_trans(void); -BLE_LOG_STATIC void ble_log_lbm_reset_stats(void); -BLE_LOG_STATIC -void ble_log_lbm_write_trans(ble_log_prph_trans_t **trans, ble_log_src_t src_code, - const uint8_t *addr, uint16_t len, - const uint8_t *addr_append, uint16_t len_append, bool omdata); -BLE_LOG_STATIC -bool ble_log_write_hex_core(ble_log_src_t src_code, const uint8_t *addr, size_t len); -#if BLE_LOG_UART_REDIR_ENABLED -BLE_LOG_STATIC -void ble_log_lbm_stream_seal(ble_log_prph_trans_t **trans, ble_log_src_t src_code); -#endif /* BLE_LOG_UART_REDIR_ENABLED */ -BLE_LOG_STATIC void ble_log_stat_mgr_update(ble_log_src_t src_code, uint32_t len, bool lost); - -/* ------------------------- */ -/* PRIVATE INTERFACE */ -/* ------------------------- */ -BLE_LOG_IRAM_ATTR BLE_LOG_STATIC -bool ble_log_lbm_ref_acquire(bool require_enabled) -{ - if (!ble_log_ref_count_try_acquire(&lbm_ref_count, &lbm_inited)) { - return false; - } - if (require_enabled && !BLE_LOG_ATOMIC_LOAD_ACQUIRE(lbm_enabled)) { - BLE_LOG_REF_COUNT_RELEASE(&lbm_ref_count); - return false; - } - return true; -} - -BLE_LOG_IRAM_ATTR BLE_LOG_STATIC -bool ble_log_lbm_acquire_trans(size_t log_len, ble_log_lbm_t **out_lbm, - ble_log_prph_trans_t ***out_trans) -{ - *out_lbm = NULL; - *out_trans = NULL; - - ble_log_lbm_t *lbm; - ble_log_prph_trans_t **trans; - ble_log_lbm_t *atomic_pool; - ble_log_lbm_t *spin_lbm; - int atomic_pool_size; - - if (BLE_LOG_IN_ISR()) { - atomic_pool = lbm_ctx->atomic_pool_isr; - spin_lbm = &(lbm_ctx->spin_isr); - atomic_pool_size = BLE_LOG_LBM_ATOMIC_ISR_CNT; - } else { - atomic_pool = lbm_ctx->atomic_pool_task; - spin_lbm = &(lbm_ctx->spin_task); - atomic_pool_size = BLE_LOG_LBM_ATOMIC_TASK_CNT; - } - - /* Try each atomic LBM: acquire lock, check buffer, fallback on failure */ - for (int i = 0; i < atomic_pool_size; i++) { - lbm = &atomic_pool[i]; - if (ble_log_cas_acquire(&(lbm->atomic_lock))) { - trans = ble_log_lbm_get_trans(lbm, log_len); - if (trans) { - *out_lbm = lbm; - *out_trans = trans; - return true; - } - ble_log_cas_release(&(lbm->atomic_lock)); - } - } - - /* Last resort: spinlock LBM */ - lbm = spin_lbm; - BLE_LOG_ACQUIRE_SPIN_LOCK(&(lbm->spin_lock)); - trans = ble_log_lbm_get_trans(lbm, log_len); - if (trans) { - *out_lbm = lbm; - *out_trans = trans; - return true; - } - BLE_LOG_RELEASE_SPIN_LOCK(&(lbm->spin_lock)); - - return false; -} - -BLE_LOG_IRAM_ATTR BLE_LOG_STATIC -void ble_log_lbm_release(ble_log_lbm_t *lbm) -{ - switch (lbm->lock_type) { - case BLE_LOG_LBM_LOCK_ATOMIC: - ble_log_cas_release(&(lbm->atomic_lock)); - break; - case BLE_LOG_LBM_LOCK_SPIN: - BLE_LOG_RELEASE_SPIN_LOCK(&lbm->spin_lock); - break; - case BLE_LOG_LBM_LOCK_NONE: - default: - break; - } -} - -BLE_LOG_IRAM_ATTR BLE_LOG_STATIC -void ble_log_lbm_submit_trans(ble_log_prph_trans_t **trans) -{ - ble_log_prph_trans_t *submitted = *trans; - BLE_LOG_ATOMIC_STORE_RELAXED(submitted->prph_owned, true); - - ble_log_lbm_t *lbm = (ble_log_lbm_t *)submitted->owner; - uint32_t inflight = __atomic_add_fetch(&lbm->trans_inflight, 1, __ATOMIC_RELAXED); - uint32_t peak = __atomic_load_n(&lbm->trans_inflight_peak, __ATOMIC_RELAXED); - while (inflight > peak && - !__atomic_compare_exchange_n(&lbm->trans_inflight_peak, &peak, inflight, - false, __ATOMIC_RELAXED, __ATOMIC_RELAXED)) { - } - - ble_log_rt_submit_trans(submitted); -} - -BLE_LOG_STATIC bool ble_log_lbm_flush_all_trans(void) -{ - ble_log_lbm_t *lbm; - ble_log_prph_trans_t **trans; - bool in_progress; - TickType_t start_tick = xTaskGetTickCount(); - - /* Queue transports with logs */ - for (int i = 0; i < BLE_LOG_LBM_CNT; i++) { - lbm = &(lbm_ctx->lbm_pool[i]); - int trans_idx = lbm->trans_idx; - for (int j = 0; j < BLE_LOG_TRANS_BUF_CNT; j++) { - trans = &(lbm->trans[trans_idx]); - if (!BLE_LOG_ATOMIC_LOAD_ACQUIRE((*trans)->prph_owned) && (*trans)->pos) { - ble_log_lbm_submit_trans(trans); - } - trans_idx = (trans_idx + 1) & (BLE_LOG_TRANS_BUF_CNT - 1); - } - } - - /* Dispatch anything still waiting on the defer alarm, then - * wait for transportation to finish */ - if (!ble_log_rt_drain()) { - return false; - } - - do { - in_progress = false; - for (int i = 0; i < BLE_LOG_LBM_CNT; i++) { - lbm = &(lbm_ctx->lbm_pool[i]); - for (int j = 0; j < BLE_LOG_TRANS_BUF_CNT; j++) { - trans = &(lbm->trans[j]); - in_progress |= BLE_LOG_ATOMIC_LOAD_ACQUIRE((*trans)->prph_owned); - } - } - if (in_progress) { - if ((xTaskGetTickCount() - start_tick) >= - pdMS_TO_TICKS(BLE_LOG_LBM_WAIT_TIMEOUT_MS)) { - ESP_LOGE(TAG, "Timed out waiting for BLE Log transports"); - return false; - } - vTaskDelay(1); - } - } while (in_progress); - - return true; -} - -BLE_LOG_STATIC void ble_log_lbm_reset_stats(void) -{ - ble_log_lbm_t *lbm; - - for (int i = 0; i < BLE_LOG_SRC_MAX; i++) { - BLE_LOG_MEMSET(stat_mgr_ctx[i], 0, sizeof(ble_log_stat_mgr_t)); - } - - for (int i = 0; i < BLE_LOG_LBM_CNT; i++) { - lbm = &(lbm_ctx->lbm_pool[i]); - __atomic_store_n(&lbm->trans_inflight, 0, __ATOMIC_RELAXED); - __atomic_store_n(&lbm->trans_inflight_peak, 0, __ATOMIC_RELAXED); - } -#if CONFIG_BLE_LOG_PRPH_UART_DMA - ble_log_prph_reset_util_counters(); -#endif -} - -BLE_LOG_IRAM_ATTR BLE_LOG_STATIC -void ble_log_lbm_write_trans(ble_log_prph_trans_t **trans, ble_log_src_t src_code, - const uint8_t *addr, uint16_t len, - const uint8_t *addr_append, uint16_t len_append, bool omdata) -{ - /* Preparation before writing */ - uint8_t *buf = (*trans)->buf + (*trans)->pos; - uint16_t payload_len = len + len_append; - ble_log_stat_mgr_t *stat_mgr = stat_mgr_ctx[src_code]; - uint32_t frame_sn = BLE_LOG_GET_FRAME_SN(&(stat_mgr->frame_sn)); - ble_log_frame_head_t frame_head = { - .length = payload_len, - .frame_meta = BLE_LOG_MAKE_FRAME_META(src_code, frame_sn), - }; - - /* Memory operation */ - BLE_LOG_MEMCPY(buf, &frame_head, BLE_LOG_FRAME_HEAD_LEN); - if (len) { - BLE_LOG_MEMCPY(buf + BLE_LOG_FRAME_HEAD_LEN, addr, len); - } - if (len_append) { -#if CONFIG_BLE_LOG_LL_ENABLED && CONFIG_SOC_ESP_NIMBLE_CONTROLLER - if (omdata) { - BLE_MBUF_COPY(addr_append, 0, len_append, buf + BLE_LOG_FRAME_HEAD_LEN + len); - } - else -#endif /* CONFIG_BLE_LOG_LL_ENABLED && CONFIG_SOC_ESP_NIMBLE_CONTROLLER */ - { - BLE_LOG_MEMCPY(buf + BLE_LOG_FRAME_HEAD_LEN + len, addr_append, len_append); - } - } - - /* Data integrity check */ - uint32_t checksum = ble_log_fast_checksum((const uint8_t *)buf, BLE_LOG_FRAME_HEAD_LEN + payload_len); - BLE_LOG_MEMCPY(buf + BLE_LOG_FRAME_HEAD_LEN + payload_len, &checksum, BLE_LOG_FRAME_TAIL_LEN); - - /* Update peripheral transport */ - (*trans)->pos += payload_len + BLE_LOG_FRAME_OVERHEAD; - - ble_log_stat_mgr_update(src_code, payload_len, false); - - /* Queue trans if full */ - if (BLE_LOG_TRANS_FREE_SPACE((*trans)) <= BLE_LOG_FRAME_OVERHEAD) { - ble_log_lbm_submit_trans(trans); - } -} - -#if BLE_LOG_UART_REDIR_ENABLED -BLE_LOG_IRAM_ATTR BLE_LOG_STATIC -void ble_log_lbm_stream_seal(ble_log_prph_trans_t **trans, ble_log_src_t src_code) -{ - if ((*trans)->pos <= BLE_LOG_FRAME_HEAD_LEN) { - return; - } - - uint16_t payload_len = (*trans)->pos - BLE_LOG_FRAME_HEAD_LEN; - ble_log_stat_mgr_t *stat_mgr = stat_mgr_ctx[src_code]; - uint32_t frame_sn = BLE_LOG_GET_FRAME_SN(&(stat_mgr->frame_sn)); - ble_log_frame_head_t frame_head = { - .length = payload_len, - .frame_meta = BLE_LOG_MAKE_FRAME_META(src_code, frame_sn), - }; - BLE_LOG_MEMCPY((*trans)->buf, &frame_head, BLE_LOG_FRAME_HEAD_LEN); - - uint32_t checksum = ble_log_fast_checksum((*trans)->buf, (*trans)->pos); - BLE_LOG_MEMCPY((*trans)->buf + (*trans)->pos, &checksum, BLE_LOG_FRAME_TAIL_LEN); - (*trans)->pos += BLE_LOG_FRAME_TAIL_LEN; - - ble_log_stat_mgr_update(src_code, payload_len, false); - - ble_log_lbm_submit_trans(trans); -} -#endif /* BLE_LOG_UART_REDIR_ENABLED */ - -BLE_LOG_IRAM_ATTR BLE_LOG_STATIC -void ble_log_stat_mgr_update(ble_log_src_t src_code, uint32_t len, bool lost) -{ - /* Get statistic manager by source code */ - ble_log_stat_mgr_t *stat_mgr = stat_mgr_ctx[src_code]; - - /* Update aligned counters */ - uint32_t bytes_cnt = len + BLE_LOG_FRAME_OVERHEAD; - if (lost) { - BLE_LOG_GET_FRAME_SN(&(stat_mgr->frame_sn)); /* consume SN for loss detection */ - __atomic_fetch_add(&stat_mgr->lost_frame_cnt, 1, __ATOMIC_RELAXED); - __atomic_fetch_add(&stat_mgr->lost_bytes_cnt, bytes_cnt, __ATOMIC_RELAXED); - } else { - __atomic_fetch_add(&stat_mgr->written_frame_cnt, 1, __ATOMIC_RELAXED); - __atomic_fetch_add(&stat_mgr->written_bytes_cnt, bytes_cnt, __ATOMIC_RELAXED); - } -} - -BLE_LOG_IRAM_ATTR BLE_LOG_STATIC -bool ble_log_write_hex_core(ble_log_src_t src_code, const uint8_t *addr, size_t len) -{ - /* Get transport from the best available pool */ - size_t payload_len = len + sizeof(uint32_t); - ble_log_lbm_t *lbm; - ble_log_prph_trans_t **trans; - if (!ble_log_lbm_acquire_trans(payload_len, &lbm, &trans)) { - goto failed; - } - - /* Write transport */ - uint32_t os_ts = pdTICKS_TO_MS(BLE_LOG_IN_ISR()? - xTaskGetTickCountFromISR(): - xTaskGetTickCount()); - ble_log_lbm_write_trans(trans, src_code, (const uint8_t *)&os_ts, - sizeof(uint32_t), addr, len, false); - - /* Release */ - ble_log_lbm_release(lbm); - return true; - -failed: - if (BLE_LOG_ATOMIC_LOAD_RELAXED(lbm_inited)) { - ble_log_stat_mgr_update(src_code, payload_len, true); - } - return false; -} - -/* -------------------------- */ -/* INTERNAL INTERFACE */ -/* -------------------------- */ -/* CRITICAL: - * Recycle a transport back to its LBM pool after the send completes or fails. - * Leaves trans->pos untouched on purpose: a failed send keeps its buffered data - * so the next flush re-queues it (flush re-sends any trans with pos != 0) */ -BLE_LOG_IRAM_ATTR void ble_log_lbm_recycle_trans(ble_log_prph_trans_t *trans) -{ - ble_log_lbm_t *lbm = (ble_log_lbm_t *)trans->owner; - __atomic_fetch_sub(&lbm->trans_inflight, 1, __ATOMIC_RELAXED); - BLE_LOG_ATOMIC_STORE_RELEASE(trans->prph_owned, false); -} - -bool ble_log_lbm_init(void) -{ - /* Avoid double init */ - if (BLE_LOG_ATOMIC_LOAD_ACQUIRE(lbm_inited)) { - return true; - } - - /* Initialize LBM context */ - lbm_ctx = (ble_log_lbm_ctx_t *)BLE_LOG_MALLOC(sizeof(ble_log_lbm_ctx_t)); - if (!lbm_ctx) { - goto exit; - } - BLE_LOG_MEMSET(lbm_ctx, 0, sizeof(ble_log_lbm_ctx_t)); - - /* Initialize peripheral transport for common LBMs */ - ble_log_lbm_t *lbm; - for (int i = 0; i < BLE_LOG_LBM_COMMON_CNT; i++) { - lbm = &(lbm_ctx->lbm_common_pool[i]); - for (int j = 0; j < BLE_LOG_TRANS_BUF_CNT; j++) { - if (!ble_log_prph_trans_init(&(lbm->trans[j]), - BLE_LOG_TRANS_SIZE)) { - goto exit; - } - lbm->trans[j]->owner = (void *)lbm; - } - } - - /* Initialize lock types for atomic pool */ - for (int i = 0; i < BLE_LOG_LBM_ATOMIC_CNT; i++) { - lbm_ctx->atomic_pool[i].lock_type = BLE_LOG_LBM_LOCK_ATOMIC; - } - - /* Initialize lock types for spin pool */ - for (int i = 0; i < BLE_LOG_LBM_SPIN_MAX; i++) { - lbm_ctx->spin_pool[i].lock_type = BLE_LOG_LBM_LOCK_SPIN; - portMUX_INITIALIZE(&(lbm_ctx->spin_pool[i].spin_lock)); - } - -#if CONFIG_BLE_LOG_LL_ENABLED - for (int i = 0; i < BLE_LOG_LBM_LL_MAX; i++) { - lbm = &(lbm_ctx->lbm_ll_pool[i]); - for (int j = 0; j < BLE_LOG_TRANS_BUF_CNT; j++) { - if (!ble_log_prph_trans_init(&(lbm->trans[j]), - BLE_LOG_TRANS_LL_SIZE)) { - goto exit; - } - lbm->trans[j]->owner = (void *)lbm; - } - } - - /* Initialize lock types for LL pool */ - for (int i = 0; i < BLE_LOG_LBM_LL_MAX; i++) { - lbm_ctx->lbm_ll_pool[i].lock_type = BLE_LOG_LBM_LOCK_NONE; - } -#endif /* CONFIG_BLE_LOG_LL_ENABLED */ - - /* Initialize statistic manager context */ - for (int i = 0; i < BLE_LOG_SRC_MAX; i++) { - stat_mgr_ctx[i] = (ble_log_stat_mgr_t *)BLE_LOG_MALLOC(sizeof(ble_log_stat_mgr_t)); - if (!stat_mgr_ctx[i]) { - goto exit; - } - BLE_LOG_MEMSET(stat_mgr_ctx[i], 0, sizeof(ble_log_stat_mgr_t)); - } - - /* Initialization done */ - BLE_LOG_ATOMIC_STORE_RELAXED(lbm_enabled, false); - BLE_LOG_ATOMIC_STORE_RELEASE(lbm_inited, true); - return true; - -exit: - ble_log_lbm_deinit(); - return false; -} - -void ble_log_lbm_close(void) -{ - BLE_LOG_ATOMIC_STORE_SEQ_CST(lbm_inited, false); - BLE_LOG_ATOMIC_STORE_RELEASE(lbm_enabled, false); -} - -void ble_log_lbm_deinit(void) -{ - /* Close before waiting: every LBM entry increments the reference count - * before checking this seq_cst gate. */ - ble_log_lbm_close(); - - /* Disable module and wait for all references to be released */ - while (!ble_log_ref_count_wait(&lbm_ref_count, 0)) { - ESP_LOGE(TAG, "Timed out waiting for BLE Log references during deinit"); - BLE_LOG_ASSERT(false); - } - - /* Release statistic manager context */ - for (int i = 0; i < BLE_LOG_SRC_MAX; i++) { - if (stat_mgr_ctx[i]) { - BLE_LOG_FREE(stat_mgr_ctx[i]); - stat_mgr_ctx[i] = NULL; - } - } - - /* Release LBM */ - if (lbm_ctx) { - /* Release peripheral transport for common pools */ - ble_log_lbm_t *lbm; - for (int i = 0; i < BLE_LOG_LBM_CNT; i++) { - lbm = &(lbm_ctx->lbm_pool[i]); - for (int j = 0; j < BLE_LOG_TRANS_BUF_CNT; j++) { - ble_log_prph_trans_deinit(&(lbm->trans[j])); - } - } - - /* Release LBM context */ - BLE_LOG_FREE(lbm_ctx); - lbm_ctx = NULL; - } -} - -BLE_LOG_IRAM_ATTR BLE_LOG_STATIC -ble_log_prph_trans_t **ble_log_lbm_get_trans(ble_log_lbm_t *lbm, size_t log_len) -{ - /* Check if available buffer can contain incoming log */ - ble_log_prph_trans_t **trans; - for (int i = 0; i < BLE_LOG_TRANS_BUF_CNT; i++) { - trans = &(lbm->trans[lbm->trans_idx]); - if (!BLE_LOG_ATOMIC_LOAD_ACQUIRE((*trans)->prph_owned)) { - /* Return if there's enough free space in current transport */ - if (BLE_LOG_TRANS_FREE_SPACE((*trans)) >= (log_len + BLE_LOG_FRAME_OVERHEAD)) { - return trans; - } - - /* Queue transport if there's insufficient free space */ - if ((*trans)->pos) { - ble_log_lbm_submit_trans(trans); - } - } - - /* Current transport unavailable, switch to the other */ - lbm->trans_idx = (lbm->trans_idx + 1) & (BLE_LOG_TRANS_BUF_CNT - 1); - } - - /* All buffers are unavailable */ - return NULL; -} - -void ble_log_write_enh_stat(void) -{ - if (!ble_log_lbm_ref_acquire(true)) { - return; - } - - /* Snapshot all sources under one critical section so the set of - * counters is mutually consistent, then write outside the lock. */ - ble_log_enh_stat_t snapshots[BLE_LOG_SRC_MAX]; - for (int i = 0; i < BLE_LOG_SRC_MAX; i++) { - snapshots[i].int_src_code = BLE_LOG_INT_SRC_ENH_STAT; - snapshots[i].src_code = i; - } - BLE_LOG_ENTER_CRITICAL(); - for (int i = 0; i < BLE_LOG_SRC_MAX; i++) { - BLE_LOG_MEMCPY(&snapshots[i].written_frame_cnt, - &stat_mgr_ctx[i]->written_frame_cnt, - 4 * sizeof(uint32_t)); - } - BLE_LOG_EXIT_CRITICAL(); - - for (int i = 0; i < BLE_LOG_SRC_MAX; i++) { - ble_log_write_hex(BLE_LOG_SRC_INTERNAL, (const uint8_t *)&snapshots[i], sizeof(ble_log_enh_stat_t)); - } - - BLE_LOG_REF_COUNT_RELEASE(&lbm_ref_count); -} - -#if BLE_LOG_UART_REDIR_ENABLED -/* ------------------------------------------------- */ -/* STREAM WRITE INTERFACE */ -/* */ -/* Stream mode appends raw data into a transport */ -/* buffer with deferred frame encapsulation: */ -/* - Header space is reserved on first write */ -/* - Data is memcpy'd after the reserved header */ -/* - Header and checksum are filled in at seal */ -/* */ -/* get_trans(lbm, 0) reuse safety: */ -/* */ -/* get_trans auto-queues a buffer raw (no seal) when */ -/* free_space < log_len + FRAME_OVERHEAD. With */ -/* log_len = 0, this triggers at free_space < 10. */ -/* */ -/* To prevent unsealed stream data from being sent */ -/* raw, stream_write auto-seals at free_space <= 10. */ -/* This guarantees that any unsealed stream buffer */ -/* seen by get_trans always has free_space > 10, */ -/* so get_trans returns it directly without queuing. */ -/* ------------------------------------------------- */ -BLE_LOG_IRAM_ATTR -void ble_log_lbm_stream_write(ble_log_lbm_t *lbm, ble_log_src_t src_code, - const uint8_t *data, size_t len) -{ - while (len > 0) { - ble_log_prph_trans_t **trans = ble_log_lbm_get_trans(lbm, 0); - if (!trans) { - ble_log_stat_mgr_update(src_code, len, true); - return; - } - - if ((*trans)->pos == 0) { - (*trans)->pos = BLE_LOG_FRAME_HEAD_LEN; - } - - uint16_t available = BLE_LOG_TRANS_FREE_SPACE((*trans)); - if (available <= BLE_LOG_FRAME_TAIL_LEN) { - ble_log_lbm_stream_seal(trans, src_code); - continue; - } - available -= BLE_LOG_FRAME_TAIL_LEN; - - size_t to_write = (len < available) ? len : available; - BLE_LOG_MEMCPY((*trans)->buf + (*trans)->pos, data, to_write); - (*trans)->pos += to_write; - data += to_write; - len -= to_write; - - if (BLE_LOG_TRANS_FREE_SPACE((*trans)) <= BLE_LOG_FRAME_OVERHEAD) { - ble_log_lbm_stream_seal(trans, src_code); - } - } -} - -BLE_LOG_IRAM_ATTR -void ble_log_lbm_stream_flush(ble_log_lbm_t *lbm, ble_log_src_t src_code) -{ - int trans_idx = lbm->trans_idx; - for (int i = 0; i < BLE_LOG_TRANS_BUF_CNT; i++) { - ble_log_prph_trans_t **trans = &(lbm->trans[trans_idx]); - if (!BLE_LOG_ATOMIC_LOAD_ACQUIRE((*trans)->prph_owned) && - (*trans)->pos > BLE_LOG_FRAME_HEAD_LEN) { - ble_log_lbm_stream_seal(trans, src_code); - } - trans_idx = (trans_idx + 1) & (BLE_LOG_TRANS_BUF_CNT - 1); - } -} -#endif /* BLE_LOG_UART_REDIR_ENABLED */ - -BLE_LOG_STATIC void ble_log_emit_buf_util(ble_log_lbm_t *lbm, uint8_t lbm_id) -{ - ble_log_buf_util_t util = { - .int_src_code = BLE_LOG_INT_SRC_BUF_UTIL, - .lbm_id = lbm_id, - .trans_cnt = BLE_LOG_TRANS_BUF_CNT, - .inflight_peak = (uint8_t)__atomic_load_n(&lbm->trans_inflight_peak, - __ATOMIC_RELAXED), - }; - ble_log_write_hex(BLE_LOG_SRC_INTERNAL, - (const uint8_t *)&util, sizeof(ble_log_buf_util_t)); -} - -void ble_log_write_buf_util(void) -{ - if (!ble_log_lbm_ref_acquire(true)) { - return; - } - - ble_log_emit_buf_util(&lbm_ctx->spin_task, - BLE_LOG_BUF_UTIL_MAKE_ID(BLE_LOG_BUF_UTIL_POOL_COMMON_TASK, 0)); - for (int i = 0; i < BLE_LOG_LBM_ATOMIC_TASK_CNT; i++) { - ble_log_emit_buf_util(&lbm_ctx->atomic_pool_task[i], - BLE_LOG_BUF_UTIL_MAKE_ID(BLE_LOG_BUF_UTIL_POOL_COMMON_TASK, 1 + i)); - } - - ble_log_emit_buf_util(&lbm_ctx->spin_isr, - BLE_LOG_BUF_UTIL_MAKE_ID(BLE_LOG_BUF_UTIL_POOL_COMMON_ISR, 0)); - for (int i = 0; i < BLE_LOG_LBM_ATOMIC_ISR_CNT; i++) { - ble_log_emit_buf_util(&lbm_ctx->atomic_pool_isr[i], - BLE_LOG_BUF_UTIL_MAKE_ID(BLE_LOG_BUF_UTIL_POOL_COMMON_ISR, 1 + i)); - } - -#if CONFIG_BLE_LOG_LL_ENABLED - ble_log_emit_buf_util(&lbm_ctx->lbm_ll_task, - BLE_LOG_BUF_UTIL_MAKE_ID(BLE_LOG_BUF_UTIL_POOL_LL, 0)); - ble_log_emit_buf_util(&lbm_ctx->lbm_ll_hci, - BLE_LOG_BUF_UTIL_MAKE_ID(BLE_LOG_BUF_UTIL_POOL_LL, 1)); -#endif - -#if BLE_LOG_UART_REDIR_ENABLED - ble_log_lbm_t *redir_lbm = ble_log_prph_get_redir_lbm(); - if (redir_lbm) { - ble_log_emit_buf_util(redir_lbm, - BLE_LOG_BUF_UTIL_MAKE_ID(BLE_LOG_BUF_UTIL_POOL_REDIR, 0)); - } -#endif - - BLE_LOG_REF_COUNT_RELEASE(&lbm_ref_count); -} - -void ble_log_write_final_stat(void) -{ - if (!ble_log_lbm_ref_acquire(false)) { - return; - } - - ble_log_final_stat_t final_stat; - final_stat.int_src_code = BLE_LOG_INT_SRC_FINAL_STAT; - final_stat.src_cnt = BLE_LOG_SRC_MAX; - - BLE_LOG_ENTER_CRITICAL(); - for (int i = 0; i < BLE_LOG_SRC_MAX; i++) { - ble_log_final_stat_entry_t *entry = &final_stat.entries[i]; - entry->src_code = i; - BLE_LOG_MEMCPY(&entry->written_frame_cnt, - &stat_mgr_ctx[i]->written_frame_cnt, - 4 * sizeof(uint32_t)); - } - BLE_LOG_EXIT_CRITICAL(); - - ble_log_write_internal((const uint8_t *)&final_stat, sizeof(final_stat)); - - BLE_LOG_REF_COUNT_RELEASE(&lbm_ref_count); -} - -/* ------------------------ */ -/* PUBLIC INTERFACE */ -/* ------------------------ */ -bool ble_log_enable(bool enable) -{ - if (!BLE_LOG_ATOMIC_LOAD_ACQUIRE(lbm_inited)) { - return false; - } - BLE_LOG_ATOMIC_STORE_RELEASE(lbm_enabled, enable); - return true; -} - -void ble_log_flush(void) -{ - /* Prevent concurrent flush — two concurrent callers would deadlock on - * the ref_count spin-wait (both hold a ref, both wait for ref_count <= 1). - * Second caller returns immediately instead of deadlocking. */ - if (__atomic_test_and_set(&flush_in_progress, __ATOMIC_ACQUIRE)) { - return; - } - - if (!ble_log_lbm_ref_acquire(false)) { - goto clear; - } - ble_log_write_enh_stat(); - ble_log_write_buf_util(); - - /* Write BLE Log flush log */ - ble_log_info_t ble_log_info = { - .int_src_code = BLE_LOG_INT_SRC_FLUSH, - .version = BLE_LOG_VERSION, - }; - ble_log_write_hex(BLE_LOG_SRC_INTERNAL, (const uint8_t *)&ble_log_info, sizeof(ble_log_info_t)); - - /* Disable module and wait for all other references to release */ - bool lbm_enabled_copy = BLE_LOG_ATOMIC_LOAD_ACQUIRE(lbm_enabled); - - BLE_LOG_ATOMIC_STORE_RELEASE(lbm_enabled, false); - if (!ble_log_ref_count_wait(&lbm_ref_count, 1)) { - ESP_LOGE(TAG, "Timed out waiting for BLE Log writers"); - goto fail; - } - - if (!ble_log_lbm_flush_all_trans()) { - goto fail; - } - - ble_log_write_final_stat(); - - if (!ble_log_lbm_flush_all_trans()) { - goto fail; - } - ble_log_lbm_reset_stats(); - -fail: - /* Resume enable status after a completed or failed flush. */ - BLE_LOG_ATOMIC_STORE_RELEASE(lbm_enabled, lbm_enabled_copy); - BLE_LOG_REF_COUNT_RELEASE(&lbm_ref_count); -clear: - __atomic_clear(&flush_in_progress, __ATOMIC_RELEASE); -} - -BLE_LOG_IRAM_ATTR -bool ble_log_write_hex(ble_log_src_t src_code, const uint8_t *addr, size_t len) -{ - bool ret = false; - - if (!ble_log_lbm_ref_acquire(true)) { - return false; - } - - ret = ble_log_write_hex_core(src_code, addr, len); - BLE_LOG_REF_COUNT_RELEASE(&lbm_ref_count); - return ret; -} - -BLE_LOG_IRAM_ATTR -bool ble_log_write_internal(const uint8_t *addr, size_t len) -{ - bool ret = false; - - if (!ble_log_lbm_ref_acquire(false)) { - return false; - } - - ret = ble_log_write_hex_core(BLE_LOG_SRC_INTERNAL, addr, len); - BLE_LOG_REF_COUNT_RELEASE(&lbm_ref_count); - return ret; -} - -#if CONFIG_BLE_LOG_LL_ENABLED -BLE_LOG_IRAM_ATTR -void ble_log_write_hex_ll(uint32_t len, const uint8_t *addr, - uint32_t len_append, const uint8_t *addr_append, uint32_t flag) -{ - if (!ble_log_lbm_ref_acquire(true)) { - return; - } - - /* Source code shall be determined before LBM determination */ - ble_log_src_t src_code = BLE_LOG_SRC_MAX; - bool use_ll_task = false; - if (flag & BIT(BLE_LOG_LL_FLAG_ISR)) { - src_code = BLE_LOG_SRC_LL_ISR; - } else if (flag & BIT(BLE_LOG_LL_FLAG_HCI)) { - src_code = BLE_LOG_SRC_LL_HCI; - } else if (flag & BIT(BLE_LOG_LL_FLAG_HCI_UPSTREAM)) { - src_code = BLE_LOG_SRC_HCI; - } else { - src_code = BLE_LOG_SRC_LL_TASK; - use_ll_task = true; - } - bool omdata = flag & BIT(BLE_LOG_LL_FLAG_OMDATA); - - /* Determine LBM and get transport */ - ble_log_lbm_t *lbm; - ble_log_prph_trans_t **trans; - size_t payload_len; - - if (BLE_LOG_IN_ISR()) { - /* os_mbuf_copydata is in flash and not safe to call from ISR */ - omdata = false; - - payload_len = len + len_append; - if (!ble_log_lbm_acquire_trans(payload_len, &lbm, &trans)) { - goto failed; - } - } else { - if (use_ll_task) { - lbm = &(lbm_ctx->lbm_ll_task); - } else { - lbm = &(lbm_ctx->lbm_ll_hci); -#if CONFIG_BLE_LOG_LL_HCI_LOG_PAYLOAD_LEN_LIMIT_ENABLED - if (len_append > CONFIG_BLE_LOG_LL_HCI_LOG_PAYLOAD_LEN_LIMIT) { - len_append = CONFIG_BLE_LOG_LL_HCI_LOG_PAYLOAD_LEN_LIMIT; - } -#endif /* CONFIG_BLE_LOG_LL_HCI_LOG_PAYLOAD_LEN_LIMIT_ENABLED */ - } - payload_len = len + len_append; - trans = ble_log_lbm_get_trans(lbm, payload_len); - if (!trans) { - /* LL pools use LOCK_NONE, release is no-op */ - goto failed; - } - } - - /* Write transport */ - ble_log_lbm_write_trans(trans, src_code, addr, len, addr_append, len_append, omdata); - - ble_log_lbm_release(lbm); - BLE_LOG_REF_COUNT_RELEASE(&lbm_ref_count); - return; - -failed: - if (BLE_LOG_ATOMIC_LOAD_RELAXED(lbm_inited)) { - ble_log_stat_mgr_update(src_code, payload_len, true); - } - BLE_LOG_REF_COUNT_RELEASE(&lbm_ref_count); - return; -} -#endif /* CONFIG_BLE_LOG_LL_ENABLED */ - -void ble_log_dump_to_console(void) -{ - if (!ble_log_lbm_ref_acquire(false)) { - return; - } - - int trans_idx; - ble_log_lbm_t *lbm; - ble_log_prph_trans_t *trans; - BLE_LOG_ENTER_CRITICAL(); - BLE_LOG_CONSOLE("[BLE_LOG_DUMP_START:\n"); - for (int i = 0; i < BLE_LOG_LBM_CNT; i++) { - lbm = &(lbm_ctx->lbm_pool[i]); - trans_idx = lbm->trans_idx; - for (int j = 0; j < BLE_LOG_TRANS_BUF_CNT; j++) { - trans = lbm->trans[trans_idx]; - BLE_LOG_FEED_WDT(); - - for (int k = 0; k < trans->size; k++) { - BLE_LOG_CONSOLE("%02x ", trans->buf[k]); - if (!(k & 0xFF)) { - BLE_LOG_FEED_WDT(); - } - } - trans_idx = (trans_idx + 1) & (BLE_LOG_TRANS_BUF_CNT - 1); - } - } - BLE_LOG_CONSOLE("\n:BLE_LOG_DUMP_END]\n\n"); - BLE_LOG_EXIT_CRITICAL(); - - BLE_LOG_REF_COUNT_RELEASE(&lbm_ref_count); - return; -} diff --git a/components/bt/common/ble_log/src/ble_log_lbm_v2.c b/components/bt/common/ble_log/src/ble_log_lbm_v2.c new file mode 100644 index 00000000000..f173c1d0146 --- /dev/null +++ b/components/bt/common/ble_log/src/ble_log_lbm_v2.c @@ -0,0 +1,1380 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ +/* ------------------------------- */ +/* BLE Log - Log Buffer Management */ +/* ------------------------------- */ + +/* INCLUDE */ +#include "ble_log.h" +#include "ble_log_lbm_v2.h" +#include "ble_log_task_registry.h" +#include "ble_log_rt.h" + +#include "esp_timer.h" +#include "esp_chip_info.h" + +#if CONFIG_BLE_LOG_LL_ENABLED && CONFIG_SOC_ESP_NIMBLE_CONTROLLER +#if CONFIG_BT_DUAL_MODE_ARCH +#include "ble_mbuf.h" +#define BLE_MBUF_COPY(buf, off, len, dst) ble_mbuf_copydata((struct ble_mbuf *)(buf), off, len, dst) +#else +#include "os/os_mbuf.h" +#define BLE_MBUF_COPY(buf, off, len, dst) os_mbuf_copydata((struct os_mbuf *)(buf), off, len, dst) +#endif // CONFIG_BT_DUAL_MODE_ARCH +#endif /* CONFIG_BLE_LOG_LL_ENABLED && CONFIG_SOC_ESP_NIMBLE_CONTROLLER */ + +/* hint: private API; keep this declaration in sync with esp_timer_impl.h. */ +extern TaskHandle_t esp_timer_impl_get_timer_task_handle(void); + +/* MACRO */ +#define BLE_LOG_POOL_MASK(count) (0xFFFFFFFFu >> (32 - (count))) +#define BLE_LOG_POOL_ALL_MASK BLE_LOG_POOL_MASK(BLE_LOG_POOL_TRANS_CNT) +#define BLE_LOG_POOL_SHARED_MASK BLE_LOG_POOL_MASK(BLE_LOG_POOL_SHARED_CNT) +#define BLE_LOG_POOL_RESERVE_MASK (BLE_LOG_POOL_ALL_MASK & ~BLE_LOG_POOL_SHARED_MASK) +#define BLE_LOG_POOL_SEM_MAX_COUNT (~(UBaseType_t)0) +#define BLE_LOG_WAIT_TIMEOUT_TICKS pdMS_TO_TICKS(1000) + +/* Single-instruction clock read; a function would add an IRAM call site. */ +#define BLE_LOG_TIMESTAMP_NOW() ((uint32_t)esp_timer_get_time()) + +#if CONFIG_BT_CONTROLLER_ENABLED +#if CONFIG_IDF_TARGET_ESP32 || CONFIG_IDF_TARGET_ESP32C3 || CONFIG_IDF_TARGET_ESP32S3 +extern const char *btdm_controller_get_compile_version(void); +#define BLE_LOG_CONTROLLER_GET_COMMIT() btdm_controller_get_compile_version() +#elif !CONFIG_BT_DUAL_MODE_ARCH || CONFIG_BT_CTRL_BLE_ENABLE +/* BR/EDR-only dual-mode builds do not link the BLE controller lib */ +extern char *ble_controller_get_compile_version(void); +#define BLE_LOG_CONTROLLER_GET_COMMIT() ble_controller_get_compile_version() +#endif +#if CONFIG_BT_DUAL_MODE_ARCH +/* BTDM common lib (dual-mode arch only) */ +extern const char *r_btdm_get_compile_version(void); +#define BLE_LOG_BTDM_COMMON_GET_COMMIT() r_btdm_get_compile_version() +#endif +#endif + +#if CONFIG_BLE_MESH && CONFIG_BLE_MESH_V11_SUPPORT +/* "Bluetooth Mesh v1.1 commit: " */ +extern const char bt_mesh_v11_commit_str[]; +#endif + +#if CONFIG_BT_AUDIO && CONFIG_SOC_BLE_AUDIO_SUPPORTED +extern const char *lib_audio_commit_get(void); +#endif + +_Static_assert(sizeof(ble_log_version_info_t) == 58, + "Unexpected BLE Log version info frame size"); + +/* ------------------------------- */ +/* Global Pool Context */ +/* ------------------------------- */ +typedef struct { + ble_log_prph_trans_t *trans[BLE_LOG_POOL_TRANS_CNT]; + + /* Bitmaps accelerate lookup; cached candidates may be stale. Ownership + * still requires the per-buffer lock and a state check. FREE acceptance + * also requires atomically taking a published FREE bit: state=FREE alone + * can precede the recycler's bitmap publication. OPEN is only a hint. */ + volatile uint32_t free_bitmap; + volatile uint32_t open_bitmap; + + /* Round-robin scan cursors. */ + volatile uint32_t open_cursor; + volatile uint32_t free_cursor; + + /* Peak number of non-free pool buffers. */ + volatile uint32_t inflight_peak; + + /* Number of task writers currently blocked waiting for a buffer. */ + volatile uint32_t waiting_task_count; + + /* Event-only counting semaphore used to wake blocked task writers. + * Not a lock: it carries no ownership and never protects shared data. */ + SemaphoreHandle_t sem; +} ble_log_pool_t; + +/* ------------------------------- */ +/* Claim/Commit Bookkeeping */ +/* ------------------------------- */ +/* State that must survive between ble_log_claim() and ble_log_commit(). It + * lives with the pool, not in ble_log_prph_trans_t: the peripheral transport + * only needs buf/size/pos and the lifecycle state for DMA handoff. */ +typedef struct { + uint8_t src_code; /* source of the pending claim */ + uint16_t max_len; /* maximum committed payload length */ + uint32_t frame_sn; /* Global SN consumed at claim entry */ + uint32_t generation; /* stale-handle detection, wraps 24-bit */ +} ble_log_pool_claim_t; + +/* VARIABLE */ +/* Process-lifetime count: a caller rejected by a closed gate may still owe + * its balancing release when the next LBM epoch starts. Never reset it. */ +BLE_LOG_STATIC BLE_LOG_DRAM_ATTR volatile uint32_t lbm_ref_count = 0; +BLE_LOG_STATIC BLE_LOG_DRAM_ATTR uint32_t lbm_inited = 0; +BLE_LOG_STATIC BLE_LOG_DRAM_ATTR uint32_t lbm_enabled = 0; +BLE_LOG_STATIC volatile bool flush_in_progress = false; +BLE_LOG_STATIC BLE_LOG_DRAM_ATTR ble_log_pool_t g_pool; +BLE_LOG_STATIC BLE_LOG_DRAM_ATTR ble_log_stat_mgr_t stat_mgr_ctx[BLE_LOG_SRC_MAX]; +/* Global SN (core logs and snapshots) and the snapshot-only anchor count; + * task bindings and REDIR retain independent sequences. */ +BLE_LOG_STATIC BLE_LOG_DRAM_ATTR uint32_t g_frame_sn; +BLE_LOG_STATIC BLE_LOG_DRAM_ATTR uint32_t g_anchor_count; +#define BLE_LOG_GET_GLOBAL_SN() BLE_LOG_GET_FRAME_SN(g_frame_sn) +#define BLE_LOG_GET_ANCHOR_COUNT() BLE_LOG_GET_FRAME_SN(g_anchor_count) +BLE_LOG_STATIC BLE_LOG_DRAM_ATTR ble_log_prph_trans_t *internal_trans; +BLE_LOG_STATIC BLE_LOG_DRAM_ATTR ble_log_pool_claim_t pool_claim_ctx[BLE_LOG_POOL_TRANS_CNT]; +BLE_LOG_STATIC ble_log_internal_snapshot_t internal_snapshot; + +#if CONFIG_BLE_LOG_PRPH_TEST +extern void ble_log_test_claim_pre_publish_hook(void) __attribute__((weak)); +extern void ble_log_test_enable_before_lifecycle_lock_hook(void) __attribute__((weak)); +extern void ble_log_test_disable_before_wake_hook(void) __attribute__((weak)); +extern void ble_log_test_claim_locked_hook(void) __attribute__((weak)); +extern void ble_log_test_init_snapshot_before_acquire_hook(void) __attribute__((weak)); +extern void ble_log_test_flush_hint_seen_hook(uint8_t id) __attribute__((weak)); +extern void ble_log_test_flush_stale_locked_hook(void) __attribute__((weak)); +extern void ble_log_test_recycle_pre_bitmap_hook(ble_log_prph_trans_t *trans) __attribute__((weak)); +extern void ble_log_test_flush_drain_between_loads_hook(void) __attribute__((weak)); +extern void ble_log_test_acquire_after_unregister_hook(void) __attribute__((weak)); +#endif + +BLE_LOG_IRAM_ATTR BLE_LOG_STATIC +void ble_log_lbm_disable(void) +{ + BLE_LOG_ATOMIC_STORE_SEQ_CST(lbm_enabled, false); + /* Pairs with the waiter-side fence so either wake_all observes the waiter + * or the waiter observes the disabled flag before blocking. */ + __atomic_thread_fence(__ATOMIC_SEQ_CST); +} + +/* PRIVATE FUNCTION DECLARATION */ +BLE_LOG_STATIC ble_log_prph_trans_t *ble_log_pool_acquire(size_t log_len, + bool use_reserve, + bool wait); +BLE_LOG_STATIC void ble_log_pool_seal_and_send(ble_log_prph_trans_t *trans); +BLE_LOG_STATIC void ble_log_pool_seal_open_trans(void); +BLE_LOG_STATIC void ble_log_pool_write_frame(ble_log_prph_trans_t *trans, + uint32_t frame_sn, + ble_log_src_t src_code, + const uint8_t *prefix, + uint16_t prefix_len, + const uint8_t *addr, uint16_t len, + const uint8_t *addr_append, + uint16_t len_append, bool omdata); +BLE_LOG_STATIC void ble_log_pool_finish_frame(ble_log_prph_trans_t *trans, + uint16_t payload_len, + ble_log_stat_mgr_t *stat_mgr); +BLE_LOG_STATIC void ble_log_stat_mgr_mark_lost(ble_log_src_t src_code); +BLE_LOG_STATIC void ble_log_snapshot_stats(ble_log_source_stat_t *snapshots); +BLE_LOG_STATIC bool ble_log_pool_flush_all_trans(void); +BLE_LOG_STATIC void ble_log_pool_wake_all(void); +BLE_LOG_STATIC void ble_log_internal_version_info_init(void); + +/* ------------------------- */ +/* BITMAP HELPERS */ +/* ------------------------- */ +/* Non-inline: each helper is called from several IRAM sites, and inlining + * would duplicate its body into every call site (IRAM is the scarce + * resource, not the call cost). */ +BLE_LOG_IRAM_ATTR BLE_LOG_STATIC void +ble_log_pool_bitmap_set(volatile uint32_t *bitmap, uint8_t id) +{ + __atomic_fetch_or(bitmap, BIT(id), __ATOMIC_RELEASE); +} + +BLE_LOG_IRAM_ATTR BLE_LOG_STATIC +uint32_t ble_log_pool_bitmap_clear(volatile uint32_t *bitmap, uint8_t id) +{ + /* Clearing publishes no data; ownership is held by lock. For FREE, the + * returned bit also confirms that recycle publication completed. */ + return __atomic_fetch_and(bitmap, ~BIT(id), __ATOMIC_RELAXED); +} + +BLE_LOG_IRAM_ATTR BLE_LOG_STATIC void +ble_log_pool_update_peak(uint32_t free_bitmap) +{ + uint32_t used = BLE_LOG_POOL_TRANS_CNT - + __builtin_popcount(free_bitmap & BLE_LOG_POOL_ALL_MASK); + ble_log_atomic_update_peak(&g_pool.inflight_peak, used); +} + +/* A newly available shared transport satisfies one waiter. The SEQ_CST fence + * pairs with the waiter-side store/fence/load handshake: either the waiter + * observes the bitmap publication or this side observes the waiter count. */ +BLE_LOG_IRAM_ATTR BLE_LOG_STATIC void ble_log_pool_notify_waiter(uint8_t id) +{ + /* Reserve transports never satisfy task waiters. */ + if (!(BLE_LOG_POOL_SHARED_MASK & BIT(id))) { + return; + } + + __atomic_thread_fence(__ATOMIC_SEQ_CST); + + SemaphoreHandle_t sem = g_pool.sem; + /* The fence above provides ordering; the count is only a wake predicate. */ + if (!sem || + BLE_LOG_ATOMIC_LOAD_RELAXED(g_pool.waiting_task_count) == 0) { + return; + } + + if (BLE_LOG_IN_ISR()) { + BaseType_t woken = pdFALSE; + xSemaphoreGiveFromISR(sem, &woken); + if (woken) { + portYIELD_FROM_ISR(woken); + } + } else { + xSemaphoreGive(sem); + } +} + +/* Releases a candidate lock that was taken on a stale bitmap hint without + * changing the transport state. The transport may have become claimable + * while the lock was held (recycle does not take this lock), and a waiter + * whose scan bounced off this lock depends on this release for its wake: + * availability is re-checked only AFTER the release, inside the same + * SC-fenced window as the waiter-count read. SENDING/CLAIMED must not + * mint: unconditional notification would let a registered scanning waiter + * wake itself on a full pool. A FREE transport notifies only when its + * free-bitmap hint is already published: state alone does not make it + * claimable (a preempted recycler sits between the FREE store and the + * bitmap set), and notifying an unadvertised transport lets a waiter + * mint and consume its own wake tokens in a self-sustaining spin. */ +BLE_LOG_IRAM_ATTR BLE_LOG_STATIC void +ble_log_pool_release_candidate(ble_log_prph_trans_t *trans) +{ + BLE_LOG_CAS_RELEASE(&trans->atomic_lock); + __atomic_thread_fence(__ATOMIC_SEQ_CST); + ble_log_trans_state_t st = BLE_LOG_ATOMIC_LOAD_RELAXED(trans->state); + if (st == BLE_LOG_TRANS_STATE_OPEN || + (st == BLE_LOG_TRANS_STATE_FREE && + (BLE_LOG_ATOMIC_LOAD_ACQUIRE(g_pool.free_bitmap) & BIT(trans->id)))) { + ble_log_pool_notify_waiter(trans->id); + } +} + +/* Publish OPEN and release ownership as one operation so notification can + * never be moved before the lock release. */ +BLE_LOG_IRAM_ATTR BLE_LOG_STATIC void +ble_log_pool_publish_open_and_unlock(ble_log_prph_trans_t *trans) +{ + BLE_LOG_ATOMIC_STORE_RELAXED(trans->state, BLE_LOG_TRANS_STATE_OPEN); + /* The following lock release publishes both state and frame data. */ + __atomic_fetch_or(&g_pool.open_bitmap, BIT(trans->id), __ATOMIC_RELAXED); + BLE_LOG_CAS_RELEASE(&trans->atomic_lock); + ble_log_pool_notify_waiter(trans->id); +} + +/* Task context only: release all potentially-blocked task writers. */ +BLE_LOG_STATIC void ble_log_pool_wake_all(void) +{ + if (!g_pool.sem) { + return; + } + uint32_t waiters = BLE_LOG_ATOMIC_LOAD_RELAXED(g_pool.waiting_task_count); + for (uint32_t i = 0; i < waiters; i++) { + (void)xSemaphoreGive(g_pool.sem); + } +} + +/* Waiter registration is one atomic RMW with ACQ_REL on both sides of the + * park: the register must be visible to a concurrent gate close before the + * task blocks, and the unregister must pair with the wake that released + * it. The RMW is a CAS loop plus barrier on Xtensa (several instructions), + * so it lives here once instead of at the four call sites of the IRAM + * acquire path. delta is +1 or -1. */ +BLE_LOG_IRAM_ATTR BLE_LOG_STATIC void +ble_log_pool_waiter_adjust(int delta) +{ + __atomic_add_fetch(&g_pool.waiting_task_count, (uint32_t)delta, + __ATOMIC_ACQ_REL); +} + +/* -------------------------------------- */ +/* UNIFIED TRANSPORT RECYCLE */ +/* -------------------------------------- */ +BLE_LOG_IRAM_ATTR void ble_log_lbm_recycle_trans(ble_log_prph_trans_t *trans) +{ + trans->pos = 0; + /* Clear existing requests. A flusher delayed after a failed try-lock may + * store true after this clear, even in the next lifecycle. That hint may + * cause an extra partial seal; it never grants permission to send a + * buffer still owned by a writer. */ + BLE_LOG_ATOMIC_STORE_RELAXED(trans->pending_seal, false); + + if (trans->owner_kind == BLE_LOG_TRANS_OWNER_INTERNAL) { + BLE_LOG_ATOMIC_STORE_RELEASE(trans->state, BLE_LOG_TRANS_STATE_FREE); + return; + } + + if (trans->owner_kind == BLE_LOG_TRANS_OWNER_REDIR) { +#if BLE_LOG_UART_REDIR_ENABLED + ble_log_redir_t *redir = ble_log_prph_get_redir_lbm(); + if (redir) { + __atomic_sub_fetch(&redir->inflight, 1, __ATOMIC_RELAXED); + } +#endif + BLE_LOG_ATOMIC_STORE_RELEASE(trans->state, BLE_LOG_TRANS_STATE_FREE); + return; + } + + /* Publish FREE state before advertising the pool bitmap hint. */ + BLE_LOG_ATOMIC_STORE_RELEASE(trans->state, BLE_LOG_TRANS_STATE_FREE); +#if CONFIG_BLE_LOG_PRPH_TEST + if (ble_log_test_recycle_pre_bitmap_hook) { + ble_log_test_recycle_pre_bitmap_hook(trans); + } +#endif + ble_log_pool_bitmap_set(&g_pool.free_bitmap, trans->id); + ble_log_pool_notify_waiter(trans->id); +} + +/* -------------------------------------- */ +/* SEAL AND SUBMIT (holds lock) */ +/* -------------------------------------- */ +BLE_LOG_IRAM_ATTR void ble_log_pool_seal_and_send(ble_log_prph_trans_t *trans) +{ + /* Consumes any pending-seal marker: every seal path (capacity, claim, + * periodic flush, full drain) funnels through here while holding the + * lock. */ + BLE_LOG_ATOMIC_STORE_RELAXED(trans->pending_seal, false); + BLE_LOG_ATOMIC_STORE_RELAXED(trans->state, BLE_LOG_TRANS_STATE_SENDING); + /* A claimed buffer is already absent from free_bitmap. Remove any OPEN + * hint before releasing the SENDING buffer to the runtime task. */ + ble_log_pool_bitmap_clear(&g_pool.open_bitmap, trans->id); + + /* Release the lock BEFORE submitting. The buffer is now in no bitmap and + * is SENDING, so no other writer can find it. */ + BLE_LOG_CAS_RELEASE(&trans->atomic_lock); + ble_log_rt_submit_trans(trans); +} + +/* Seals every OPEN transport in the pool. Caller contract: writers are + * gone (producer gate closed and drained), so each lock is uncontended + * and the acquire waits are bounded. */ +BLE_LOG_STATIC void ble_log_pool_seal_open_trans(void) +{ + for (int id = 0; id < BLE_LOG_POOL_TRANS_CNT; id++) { + if (!(BLE_LOG_ATOMIC_LOAD_ACQUIRE(g_pool.open_bitmap) & BIT(id))) { + continue; + } + ble_log_prph_trans_t *trans = g_pool.trans[id]; + while (!BLE_LOG_CAS_ACQUIRE(&trans->atomic_lock)) { + } + if (BLE_LOG_ATOMIC_LOAD_RELAXED(trans->state) == BLE_LOG_TRANS_STATE_OPEN && + trans->pos > 0) { + ble_log_pool_seal_and_send(trans); /* releases the lock */ + } else { + BLE_LOG_CAS_RELEASE(&trans->atomic_lock); + } + } +} + +/* -------------------------------------- */ +/* AVAILABLE BUFFER CLAIM */ +/* -------------------------------------- */ +BLE_LOG_IRAM_ATTR BLE_LOG_STATIC +ble_log_prph_trans_t *ble_log_pool_try_claim_from(volatile uint32_t *bitmap, + volatile uint32_t *cursor, + uint32_t domain_mask, + ble_log_trans_state_t expected_state, + uint32_t frame_len) +{ + uint32_t candidates = BLE_LOG_ATOMIC_LOAD_ACQUIRE(*bitmap) & domain_mask; + if (!candidates) { + return NULL; + } + + uint32_t start = BLE_LOG_ATOMIC_LOAD_RELAXED(*cursor); + + for (uint32_t offset = 0; offset < BLE_LOG_POOL_TRANS_CNT; offset++) { + uint8_t id = (uint8_t)((start + offset) % BLE_LOG_POOL_TRANS_CNT); + if (!(candidates & BIT(id))) { + continue; + } + + ble_log_prph_trans_t *trans = g_pool.trans[id]; + if (!BLE_LOG_CAS_ACQUIRE(&trans->atomic_lock)) { + continue; + } + /* Acceptance load: pairs with the recycler's STORE_RELEASE(FREE), + * which publishes pos/pending_seal without holding this lock (the + * last lock release predates the recycle). The OPEN domain is + * already covered by that lock pairing; the acquire is required + * for FREE acceptance. */ + if (BLE_LOG_ATOMIC_LOAD_ACQUIRE(trans->state) != expected_state) { + /* The bitmap is only a hint; another owner may have changed state. */ + ble_log_pool_release_candidate(trans); + continue; + } + + if (expected_state == BLE_LOG_TRANS_STATE_OPEN && + (BLE_LOG_TRANS_FREE_SPACE(trans) < frame_len || + BLE_LOG_ATOMIC_LOAD_ACQUIRE(trans->pending_seal))) { + /* No room for this frame, or a periodic flush left a + * pending-seal marker while the transport was busy: send the + * buffered frames and scan on for another transport. */ + ble_log_pool_seal_and_send(trans); /* releases the lock */ + continue; + } + + uint32_t previous = ble_log_pool_bitmap_clear(bitmap, id); + if (bitmap == &g_pool.free_bitmap) { + if (!(previous & BIT(id))) { + ble_log_pool_release_candidate(trans); + continue; + } + ble_log_pool_update_peak(previous & ~BIT(id)); + } + /* Keep packing the same OPEN transport instead of scanning the rest + * of the pool before wrapping back to it on the next write. */ + uint32_t next = expected_state == BLE_LOG_TRANS_STATE_OPEN + ? id + : (id + 1u) % BLE_LOG_POOL_TRANS_CNT; + BLE_LOG_ATOMIC_STORE_RELAXED(*cursor, next); + return trans; /* returned locked */ + } + + return NULL; +} + +BLE_LOG_IRAM_ATTR BLE_LOG_STATIC +ble_log_prph_trans_t *ble_log_pool_try_claim_available(uint32_t frame_len, bool use_reserve) +{ + uint32_t open_domain = use_reserve ? BLE_LOG_POOL_ALL_MASK : BLE_LOG_POOL_SHARED_MASK; + + /* The cursor normally points at the OPEN transport used by the previous + * write. Claim it directly; fall back to bitmap scanning on contention, + * stale state, or insufficient space. */ + uint8_t open_id = (uint8_t)BLE_LOG_ATOMIC_LOAD_RELAXED(g_pool.open_cursor); + if (open_domain & BIT(open_id)) { + ble_log_prph_trans_t *open_trans = g_pool.trans[open_id]; + if (BLE_LOG_CAS_ACQUIRE(&open_trans->atomic_lock)) { + if (BLE_LOG_ATOMIC_LOAD_RELAXED(open_trans->state) == BLE_LOG_TRANS_STATE_OPEN) { + /* A pending-seal marker left by a periodic flush that lost + * the lock race sends the buffered frames first; this claim + * continues with another transport below. */ + if (!BLE_LOG_ATOMIC_LOAD_ACQUIRE(open_trans->pending_seal) && + BLE_LOG_TRANS_FREE_SPACE(open_trans) >= frame_len) { +#if CONFIG_BLE_LOG_PRPH_TEST + /* Test point: the transport is locked and still listed + * in open_bitmap, mirroring an in-progress writer for a + * concurrent flush attempt. */ + if (ble_log_test_claim_locked_hook) { + ble_log_test_claim_locked_hook(); + } +#endif + ble_log_pool_bitmap_clear(&g_pool.open_bitmap, open_id); + return open_trans; + } + ble_log_pool_seal_and_send(open_trans); /* releases the lock */ + } else { + ble_log_pool_release_candidate(open_trans); + } + } + } + + /* Prefer partially-filled buffers to preserve packing efficiency. */ + ble_log_prph_trans_t *trans = + ble_log_pool_try_claim_from(&g_pool.open_bitmap, &g_pool.open_cursor, + open_domain, BLE_LOG_TRANS_STATE_OPEN, + frame_len); + if (trans) { + return trans; + } + + trans = ble_log_pool_try_claim_from(&g_pool.free_bitmap, &g_pool.free_cursor, + BLE_LOG_POOL_SHARED_MASK, BLE_LOG_TRANS_STATE_FREE, + 0); + if (!trans && use_reserve) { + trans = ble_log_pool_try_claim_from(&g_pool.free_bitmap, &g_pool.free_cursor, + BLE_LOG_POOL_RESERVE_MASK, BLE_LOG_TRANS_STATE_FREE, + 0); + } + return trans; +} + +/* -------------------------------------- */ +/* ACQUIRE (with task backpressure) */ +/* -------------------------------------- */ +BLE_LOG_IRAM_ATTR BLE_LOG_STATIC +ble_log_prph_trans_t *ble_log_pool_acquire(size_t log_len, + bool use_reserve, + bool wait) +{ + uint32_t frame_len = (uint32_t)log_len + BLE_LOG_FRAME_OVERHEAD; + if (frame_len > BLE_LOG_POOL_TRANS_SIZE) { + return NULL; + } + + for (;;) { + ble_log_prph_trans_t *trans = + ble_log_pool_try_claim_available(frame_len, use_reserve); + /* The shared ESP Timer task cannot wait for its own dispatcher. + * Check its identity only on the yieldable, would-wait path. */ + if (trans || !wait || !BLE_LOG_ATOMIC_LOAD_ACQUIRE(lbm_enabled) || + xTaskGetCurrentTaskHandle() == esp_timer_impl_get_timer_task_handle()) { + return trans; + } + + ble_log_pool_waiter_adjust(1); + __atomic_thread_fence(__ATOMIC_SEQ_CST); + + trans = ble_log_pool_try_claim_available(frame_len, use_reserve); + if (trans) { + ble_log_pool_waiter_adjust(-1); + return trans; + } + if (!BLE_LOG_ATOMIC_LOAD_ACQUIRE(lbm_enabled)) { + ble_log_pool_waiter_adjust(-1); + return NULL; + } + + /* A parked LL task keeps its waiter registration but releases the + * normal writer reference so flush/deinit can close the gate. */ + BLE_LOG_REF_COUNT_RELEASE(&lbm_ref_count); + xSemaphoreTake(g_pool.sem, portMAX_DELAY); + BLE_LOG_REF_COUNT_ACQUIRE_SEQ_CST(&lbm_ref_count); + ble_log_pool_waiter_adjust(-1); +#if CONFIG_BLE_LOG_PRPH_TEST + if (ble_log_test_acquire_after_unregister_hook) { + ble_log_test_acquire_after_unregister_hook(); + } +#endif + + if (!BLE_LOG_ATOMIC_LOAD_ACQUIRE(lbm_enabled)) { + return NULL; + } + } +} + +/* -------------------------------------- */ +/* WRITE ONE FRAME (holds lock) */ +/* -------------------------------------- */ +BLE_LOG_IRAM_ATTR BLE_LOG_STATIC +void ble_log_pool_write_frame(ble_log_prph_trans_t *trans, uint32_t frame_sn, + ble_log_src_t src_code, + const uint8_t *prefix, uint16_t prefix_len, + const uint8_t *addr, uint16_t len, + const uint8_t *addr_append, uint16_t len_append, bool omdata) +{ + uint8_t *buf = trans->buf + trans->pos; + uint16_t payload_len = prefix_len + len + len_append; + ble_log_stat_mgr_t *stat_mgr = &stat_mgr_ctx[src_code]; + ble_log_frame_head_t frame_head = { + .length = payload_len, + .frame_meta = BLE_LOG_MAKE_FRAME_META(src_code, frame_sn), + }; + + /* Memory operation */ + BLE_LOG_MEMCPY(buf, &frame_head, BLE_LOG_FRAME_HEAD_LEN); + uint8_t *payload = buf + BLE_LOG_FRAME_HEAD_LEN; + if (prefix_len) { + BLE_LOG_MEMCPY(payload, prefix, prefix_len); + payload += prefix_len; + } + if (len) { + BLE_LOG_MEMCPY(payload, addr, len); + payload += len; + } + if (len_append) { +#if CONFIG_BLE_LOG_LL_ENABLED && CONFIG_SOC_ESP_NIMBLE_CONTROLLER + if (omdata) { + BLE_MBUF_COPY(addr_append, 0, len_append, payload); + } + else +#endif /* CONFIG_BLE_LOG_LL_ENABLED && CONFIG_SOC_ESP_NIMBLE_CONTROLLER */ + { + BLE_LOG_MEMCPY(payload, addr_append, len_append); + } + } + /* Data integrity check, transport update and completion. */ + ble_log_pool_finish_frame(trans, payload_len, stat_mgr); +} + +/* Completes a frame whose head and payload are already in place at + * trans->pos: writes the checksum tail, advances pos, publishes the + * written-frame stat, then seals or publishes the transport. */ +BLE_LOG_IRAM_ATTR BLE_LOG_STATIC +void ble_log_pool_finish_frame(ble_log_prph_trans_t *trans, uint16_t payload_len, + ble_log_stat_mgr_t *stat_mgr) +{ + uint8_t *buf = trans->buf + trans->pos; + uint32_t checksum = ble_log_fast_checksum(buf, BLE_LOG_FRAME_HEAD_LEN + payload_len); + BLE_LOG_MEMCPY(buf + BLE_LOG_FRAME_HEAD_LEN + payload_len, &checksum, BLE_LOG_FRAME_TAIL_LEN); + + trans->pos += payload_len + BLE_LOG_FRAME_OVERHEAD; + BLE_LOG_ATOMIC_ADD_RELAXED(stat_mgr->counters.written_frame_cnt, 1); + BLE_LOG_ATOMIC_ADD_RELAXED(stat_mgr->counters.written_bytes_cnt, + payload_len + BLE_LOG_FRAME_OVERHEAD); + + /* Completion: seal if nearly full, otherwise publish as OPEN. */ + if (BLE_LOG_TRANS_FREE_SPACE(trans) <= BLE_LOG_FRAME_OVERHEAD) { + ble_log_pool_seal_and_send(trans); /* releases the lock */ + } else { + ble_log_pool_publish_open_and_unlock(trans); + } +} + +/* ---------------------------------------------- */ +/* CLAIM / COMMIT INTERFACE (holds lock) */ +/* ---------------------------------------------- */ + +/* Claim with a caller-chosen wait policy. wait_for_transport=true applies + * backpressure in ordinary yieldable tasks; false returns NULL on a busy + * pool. The shared ESP Timer task never waits, regardless of that policy. + * Non-yieldable contexts retain their reserve and fail-fast behavior. */ +uint8_t *ble_log_claim(ble_log_src_t src_code, size_t max_len, + uint32_t *handle, bool wait_for_transport) +{ + if (!handle || src_code != BLE_LOG_SRC_ENCODE || max_len == 0 || + max_len > UINT16_MAX || BLE_LOG_IN_ISR()) { + return NULL; + } + *handle = UINT32_MAX; + + if (!ble_log_ref_count_try_acquire(&lbm_ref_count, &lbm_enabled)) { + return NULL; + } + + /* Every claim attempt consumes one Global SN at entry; an abandoned or + * failed claim therefore leaves a gap. */ + uint32_t frame_sn = BLE_LOG_GET_GLOBAL_SN(); + + /* The timestamp is the log occurrence time, before pool contention. */ + uint32_t timestamp = BLE_LOG_TIMESTAMP_NOW(); + bool non_yield = !xPortCanYield() || + xTaskGetSchedulerState() != taskSCHEDULER_RUNNING; + size_t payload_capacity = sizeof(timestamp) + max_len; + /* Non-yieldable contexts never wait, whatever the requested policy: + * blocking there would suspend the only context that can free the + * transport. wait_for_transport=false is the lossy fast path for + * yieldable contexts that must not block. */ + ble_log_prph_trans_t *trans = ble_log_pool_acquire( + payload_capacity, non_yield, wait_for_transport && !non_yield); + if (!trans) { + ble_log_stat_mgr_mark_lost(src_code); + BLE_LOG_REF_COUNT_RELEASE(&lbm_ref_count); + return NULL; + } + +#if CONFIG_BLE_LOG_PRPH_TEST + if (ble_log_test_claim_pre_publish_hook) { + ble_log_test_claim_pre_publish_hook(); + } +#endif + ble_log_pool_claim_t *claim = &pool_claim_ctx[trans->id]; + claim->src_code = (uint8_t)src_code; + claim->max_len = (uint16_t)max_len; + claim->frame_sn = frame_sn; + claim->generation = (claim->generation + 1) & 0x00ffffffU; + + uint8_t *payload = trans->buf + trans->pos + BLE_LOG_FRAME_HEAD_LEN; + BLE_LOG_MEMCPY(payload, ×tamp, sizeof(timestamp)); + + /* Publish only after all claim metadata and payload prefix are complete. + * An acquire load in commit then rejects stale generations without ever + * observing a partially initialized claim. */ + BLE_LOG_ATOMIC_STORE_RELEASE(trans->state, BLE_LOG_TRANS_STATE_CLAIMED); + *handle = (claim->generation << 8) | trans->id; + return payload + sizeof(timestamp); +} + +void ble_log_commit(uint32_t handle, size_t actual_len) +{ + uint8_t id = handle & 0xffU; + uint32_t generation = handle >> 8; + if (id >= BLE_LOG_POOL_TRANS_CNT) { + return; + } + ble_log_prph_trans_t *trans = g_pool.trans[id]; + ble_log_pool_claim_t *claim = &pool_claim_ctx[id]; + if (BLE_LOG_ATOMIC_LOAD_ACQUIRE(trans->state) != + BLE_LOG_TRANS_STATE_CLAIMED || + claim->generation != generation) { + return; + } + + ble_log_src_t src_code = (ble_log_src_t)claim->src_code; + if (actual_len == 0 || actual_len > claim->max_len) { + ble_log_stat_mgr_mark_lost(src_code); + if (trans->pos == 0) { + /* Returning straight to FREE: recycle covers pos, pending_seal, + * state, the free-bitmap hint and the waiter wake. The transport + * is in no bitmap while CLAIMED, so releasing the lock first + * keeps it invisible to scanners until recycle publishes it. */ + BLE_LOG_CAS_RELEASE(&trans->atomic_lock); + ble_log_lbm_recycle_trans(trans); + } else { + ble_log_pool_publish_open_and_unlock(trans); + } + BLE_LOG_REF_COUNT_RELEASE(&lbm_ref_count); + return; + } + + uint16_t payload_len = (uint16_t)(sizeof(uint32_t) + actual_len); + ble_log_stat_mgr_t *stat_mgr = &stat_mgr_ctx[src_code]; + ble_log_frame_head_t frame_head = { + .length = payload_len, + .frame_meta = BLE_LOG_MAKE_FRAME_META(src_code, claim->frame_sn), + }; + BLE_LOG_MEMCPY(trans->buf + trans->pos, &frame_head, BLE_LOG_FRAME_HEAD_LEN); + ble_log_pool_finish_frame(trans, payload_len, stat_mgr); + + BLE_LOG_REF_COUNT_RELEASE(&lbm_ref_count); +} + + +BLE_LOG_IRAM_ATTR BLE_LOG_STATIC +void ble_log_stat_mgr_mark_lost(ble_log_src_t src_code) +{ + /* The Global SN was already consumed at API entry; only the per-source + * loss counter is attributed here. */ + ble_log_stat_mgr_t *stat_mgr = &stat_mgr_ctx[src_code]; + BLE_LOG_ATOMIC_ADD_RELAXED(stat_mgr->counters.lost_frame_cnt, 1); +} + +/* -------------------------- */ +/* INTERNAL INTERFACE */ +/* -------------------------- */ +bool ble_log_lbm_init(void) +{ + /* Avoid double init */ + if (BLE_LOG_ATOMIC_LOAD_ACQUIRE(lbm_inited)) { + return true; + } + + BLE_LOG_MEMSET(&g_pool, 0, sizeof(g_pool)); + + /* The semaphore carries wake events rather than buffer ownership, so its + * capacity must accommodate every possible blocked writer. Counting + * semaphores have a zero-sized queue item, so this does not allocate + * storage proportional to the maximum count. */ + g_pool.sem = xSemaphoreCreateCounting(BLE_LOG_POOL_SEM_MAX_COUNT, 0); + if (!g_pool.sem) { + goto exit; + } + + /* Allocate the transport buffers of the global pool. trans_init zeroes + * the storage, so only the non-zero identity fields need setting. */ + for (int id = 0; id < BLE_LOG_POOL_TRANS_CNT; id++) { + if (!ble_log_prph_trans_init(&(g_pool.trans[id]), BLE_LOG_POOL_TRANS_SIZE)) { + goto exit; + } + g_pool.trans[id]->id = (uint8_t)id; + } + + if (!ble_log_prph_trans_init(&internal_trans, BLE_LOG_INTERNAL_TRANS_SIZE)) { + goto exit; + } + internal_trans->id = BLE_LOG_TRANS_ID_NONE; + internal_trans->owner_kind = BLE_LOG_TRANS_OWNER_INTERNAL; + + g_pool.free_bitmap = BLE_LOG_POOL_ALL_MASK; + g_pool.open_bitmap = 0; + BLE_LOG_MEMSET(stat_mgr_ctx, 0, sizeof(stat_mgr_ctx)); + g_frame_sn = 0; + g_anchor_count = 0; + /* Task registry: fresh epoch (registry, sequence and its dedicated + * transport) for the new receiver epoch. */ + if (!ble_log_task_registry_init()) { + goto exit; + } + BLE_LOG_MEMSET(&internal_snapshot, 0, sizeof(internal_snapshot)); + internal_snapshot.int_src_code = BLE_LOG_INT_SRC_SNAPSHOT; + internal_snapshot.pool.trans_cnt = BLE_LOG_POOL_TRANS_CNT; + internal_snapshot.pool.non_yield_reserve_cnt = BLE_LOG_POOL_NON_YIELD_RESERVE_CNT; + ble_log_internal_version_info_init(); + + BLE_LOG_ATOMIC_STORE_RELEASE(lbm_enabled, false); + BLE_LOG_ATOMIC_STORE_RELEASE(lbm_inited, true); + return true; + +exit: + ble_log_lbm_deinit(); + return false; +} + +void ble_log_lbm_begin_deinit(void) +{ + /* Serialize both producer-gate closes with enable and flush restore. */ + BLE_LOG_ENTER_CRITICAL(); + ble_log_lbm_disable(); + BLE_LOG_ATOMIC_STORE_SEQ_CST(lbm_inited, false); + BLE_LOG_EXIT_CRITICAL(); + + /* Close the task-registry gate with the LBM gate: its broadcast is + * system output, so only teardown stops it. The publish path never + * blocks, so this drain is immediate. */ + ble_log_task_registry_begin_deinit(); + + /* Wake any blocked task writers and wait until BOTH the reference count + * and the waiting-task count drain to zero. Blocked writers hold no + * reference while parked, so waiting on ref_count alone could free the + * pool while a woken task still touches it (use-after-free). The two + * counters are read separately: a writer waking between the loads + * re-acquires its reference before unregistering, so a zero waiter + * count alone is not a drain. The SC fence pairs with that acquire: + * once the waiter load has observed the unregister, the re-read below + * must observe the re-acquired reference. */ + TickType_t ticks_waited = 0; + while (true) { + bool drained = false; + if (BLE_LOG_ATOMIC_LOAD_SEQ_CST(lbm_ref_count) == 0) { + if (BLE_LOG_ATOMIC_LOAD_ACQUIRE(g_pool.waiting_task_count) == 0) { + __atomic_thread_fence(__ATOMIC_SEQ_CST); + drained = BLE_LOG_ATOMIC_LOAD_SEQ_CST(lbm_ref_count) == 0; + } + } + if (drained) { + break; + } + ble_log_pool_wake_all(); + vTaskDelay(1); + BLE_LOG_ASSERT(ticks_waited++ < BLE_LOG_WAIT_TIMEOUT_TICKS); + } +} + +__attribute__((noinline)) BLE_LOG_STATIC +void ble_log_snapshot_stats(ble_log_source_stat_t *snapshots) +{ + /* The counter writers (claim/commit/write_hex_ll, some in IRAM/ISR + * context) use relaxed atomics and never take the spinlock, so an + * exclusive section here would exclude nobody. The relaxed loads + * are the whole protection. */ + for (int i = 0; i < BLE_LOG_SRC_CORE_COUNT; i++) { + ble_log_stat_mgr_t *stat_mgr = + &stat_mgr_ctx[BLE_LOG_SRC_CORE_FIRST + i]; + snapshots[i].written_frame_cnt = + BLE_LOG_ATOMIC_LOAD_RELAXED(stat_mgr->counters.written_frame_cnt); + snapshots[i].lost_frame_cnt = + BLE_LOG_ATOMIC_LOAD_RELAXED(stat_mgr->counters.lost_frame_cnt); + snapshots[i].written_bytes_cnt = + BLE_LOG_ATOMIC_LOAD_RELAXED(stat_mgr->counters.written_bytes_cnt); + } +} + +void ble_log_lbm_deinit(void) +{ + ble_log_lbm_begin_deinit(); + + ble_log_task_registry_deinit(); + ble_log_prph_trans_deinit(&internal_trans); + for (int id = 0; id < BLE_LOG_POOL_TRANS_CNT; id++) { + ble_log_prph_trans_deinit(&(g_pool.trans[id])); + } + + if (g_pool.sem) { + vSemaphoreDelete(g_pool.sem); + g_pool.sem = NULL; + } +} + +bool ble_log_lbm_is_enabled(void) +{ + return BLE_LOG_ATOMIC_LOAD_ACQUIRE(lbm_enabled); +} + +/* Copies a NUL-terminated commit string into a fixed-width zero-padded field */ +BLE_LOG_STATIC void ble_log_commit_copy(uint8_t *dst, const char *src, size_t len) +{ + BLE_LOG_MEMCPY(dst, src, strnlen(src, len)); +} + +/* Fills the version-info record carried by internal snapshots, in place: + * no intermediate copy. ble_log_lbm_init zeroes internal_snapshot first. */ +BLE_LOG_STATIC void ble_log_internal_version_info_init(void) +{ + ble_log_version_info_t *vi = &internal_snapshot.version_info; + vi->int_src_code = BLE_LOG_INT_SRC_VERSION_INFO; + vi->version = BLE_LOG_VERSION; +#ifdef BLE_LOG_IDF_COMMIT + BLE_LOG_MEMCPY(vi->idf_commit, BLE_LOG_IDF_COMMIT, + BLE_LOG_IDF_COMMIT_LEN); +#endif +#if CONFIG_BT_CONTROLLER_ENABLED && defined(BLE_LOG_CONTROLLER_GET_COMMIT) + ble_log_commit_copy(vi->controller_commit, + BLE_LOG_CONTROLLER_GET_COMMIT(), BLE_LOG_LIB_COMMIT_LEN); +#endif +#if CONFIG_BT_CONTROLLER_ENABLED && defined(BLE_LOG_BTDM_COMMON_GET_COMMIT) + ble_log_commit_copy(vi->btdm_common_commit, + BLE_LOG_BTDM_COMMON_GET_COMMIT(), BLE_LOG_LIB_COMMIT_LEN); +#endif +#if CONFIG_BLE_MESH && CONFIG_BLE_MESH_V11_SUPPORT + const char *mesh_commit = strrchr(bt_mesh_v11_commit_str, ' '); + if (mesh_commit) { + ble_log_commit_copy(vi->mesh_commit, mesh_commit + 1, + BLE_LOG_LIB_COMMIT_LEN); + } +#endif +#if CONFIG_BT_AUDIO && CONFIG_SOC_BLE_AUDIO_SUPPORTED + ble_log_commit_copy(vi->audio_commit, lib_audio_commit_get(), + BLE_LOG_LIB_COMMIT_LEN); +#endif + esp_chip_info_t chip_info; + esp_chip_info(&chip_info); + vi->chip_model = (uint16_t)chip_info.model; + vi->chip_revision = chip_info.revision; +} + +bool ble_log_internal_snapshot(uint16_t reason_flags, + const ble_log_ts_info_t *ts_info, + bool wait_for_transport) +{ +#if CONFIG_BLE_LOG_PRPH_TEST + if ((reason_flags & BLE_LOG_SNAPSHOT_REASON_INIT) && + ble_log_test_init_snapshot_before_acquire_hook) { + ble_log_test_init_snapshot_before_acquire_hook(); + } +#endif + /* Internal Snapshots are system output and outlive the public producer + * gate. Only LBM teardown stops new snapshots. */ + if (!ble_log_ref_count_try_acquire(&lbm_ref_count, &lbm_inited)) { + return false; + } + if (!internal_trans) { + goto failed; + } + + /* The caller sampled the clocks (with or without a sync IO toggle) + * before any dedicated-buffer drain or wait; the frame timestamp is + * that sample's esp_ts. */ + uint32_t timestamp = ts_info->esp_ts; + TickType_t start_tick = xTaskGetTickCount(); + for (;;) { + if (BLE_LOG_CAS_ACQUIRE(&internal_trans->atomic_lock)) { + /* Acceptance load: pairs with the dedicated transport's + * lock-free recycle publication (pos=0, STORE_RELEASE(FREE)). */ + if (BLE_LOG_ATOMIC_LOAD_ACQUIRE(internal_trans->state) == + BLE_LOG_TRANS_STATE_FREE) { + break; + } + BLE_LOG_CAS_RELEASE(&internal_trans->atomic_lock); + } + if (!wait_for_transport || + (xTaskGetTickCount() - start_tick) >= BLE_LOG_WAIT_TIMEOUT_TICKS) { + goto lost; + } + (void)ble_log_rt_drain(); + vTaskDelay(1); + } + + internal_snapshot.reason_flags = reason_flags; + internal_snapshot.ts.io_level = ts_info->io_level; + internal_snapshot.ts.lc_ts = ts_info->lc_ts; + internal_snapshot.ts.esp_ts = timestamp; + internal_snapshot.ts.os_ts = ts_info->os_ts; + uint32_t free_bitmap = BLE_LOG_ATOMIC_LOAD_ACQUIRE(g_pool.free_bitmap) & + BLE_LOG_POOL_ALL_MASK; + internal_snapshot.pool.inflight = + (uint8_t)(BLE_LOG_POOL_TRANS_CNT - __builtin_popcount(free_bitmap)); + internal_snapshot.pool.inflight_peak = + (uint8_t)BLE_LOG_ATOMIC_LOAD_RELAXED(g_pool.inflight_peak); + ble_log_snapshot_stats(internal_snapshot.stats); + + /* Only a snapshot that acquired its transport consumes a Global SN. + * Busy snapshots are not in the core loss statistics; their gaps belong + * exclusively to anchor_count. The transport lock serializes successful + * snapshots, so no extra critical section is needed for these counters. */ + uint32_t frame_sn = BLE_LOG_GET_GLOBAL_SN(); + uint32_t anchor_count = BLE_LOG_GET_ANCHOR_COUNT(); + for (unsigned i = 0; i < sizeof(internal_snapshot.anchor_count); i++) { + internal_snapshot.anchor_count[i] = (uint8_t)(anchor_count >> (8U * i)); + } + + ble_log_frame_head_t frame_head = { + .length = sizeof(timestamp) + sizeof(internal_snapshot), + .frame_meta = BLE_LOG_MAKE_FRAME_META(BLE_LOG_SRC_INTERNAL, frame_sn), + }; + uint8_t *buf = internal_trans->buf; + BLE_LOG_MEMCPY(buf, &frame_head, sizeof(frame_head)); + BLE_LOG_MEMCPY(buf + BLE_LOG_FRAME_HEAD_LEN, ×tamp, sizeof(timestamp)); + BLE_LOG_MEMCPY(buf + BLE_LOG_FRAME_HEAD_LEN + sizeof(timestamp), + &internal_snapshot, sizeof(internal_snapshot)); + uint32_t checksum = ble_log_fast_checksum( + buf, BLE_LOG_FRAME_HEAD_LEN + frame_head.length); + BLE_LOG_MEMCPY(buf + BLE_LOG_FRAME_HEAD_LEN + frame_head.length, + &checksum, sizeof(checksum)); + + internal_trans->pos = BLE_LOG_INTERNAL_FRAME_LEN; + BLE_LOG_ATOMIC_STORE_RELAXED(internal_trans->state, + BLE_LOG_TRANS_STATE_SENDING); + BLE_LOG_CAS_RELEASE(&internal_trans->atomic_lock); + ble_log_rt_submit_trans(internal_trans); + BLE_LOG_REF_COUNT_RELEASE(&lbm_ref_count); + return true; + +lost: + (void)BLE_LOG_GET_ANCHOR_COUNT(); +failed: + BLE_LOG_REF_COUNT_RELEASE(&lbm_ref_count); + return false; +} + +BLE_LOG_STATIC bool ble_log_pool_flush_all_trans(void) +{ + /* New writes are disabled, so every held lock will be released after its + * current frame copy. Seal every remaining OPEN transport. */ + ble_log_pool_seal_open_trans(); + TickType_t start_tick = xTaskGetTickCount(); + while ((BLE_LOG_ATOMIC_LOAD_ACQUIRE(g_pool.free_bitmap) & + BLE_LOG_POOL_ALL_MASK) != BLE_LOG_POOL_ALL_MASK) { + (void)ble_log_rt_drain(); + if ((xTaskGetTickCount() - start_tick) >= BLE_LOG_WAIT_TIMEOUT_TICKS) { + BLE_LOG_CONSOLE("@EW: Timed out waiting for BLE Log transports\n"); + return false; + } + vTaskDelay(1); + } + + return true; +} + +void ble_log_lbm_flush_open_trans(void) +{ + /* Periodic OPEN flush is system output, independent from the public + * producer gate. LBM teardown remains the lifetime boundary. */ + if (!ble_log_ref_count_try_acquire(&lbm_ref_count, &lbm_inited)) { + return; + } + + for (int id = 0; id < BLE_LOG_POOL_TRANS_CNT; id++) { + /* Scan the bounded pool, including writers absent from OPEN hints. */ + ble_log_prph_trans_t *trans = g_pool.trans[id]; +#if CONFIG_BLE_LOG_PRPH_TEST + /* Preserve the stale-OPEN-hint test's target without filtering the + * production scan. Unrelated FREE candidates must not fire its hooks. */ + bool had_open_hint = BLE_LOG_ATOMIC_LOAD_ACQUIRE(g_pool.open_bitmap) & BIT(id); + if (had_open_hint && ble_log_test_flush_hint_seen_hook) { + ble_log_test_flush_hint_seen_hook((uint8_t)id); + } +#endif + if (!BLE_LOG_CAS_ACQUIRE(&trans->atomic_lock)) { + /* A writer holds the buffer: leave a pending-seal marker for + * the next claim instead of waiting (the flusher never + * competes for a lock). The marker is serviced by the next + * writer on this transport, or by the next periodic pass once + * the lock is uncontended. */ + BLE_LOG_ATOMIC_STORE_RELEASE(trans->pending_seal, true); + continue; + } + if (BLE_LOG_ATOMIC_LOAD_RELAXED(trans->state) == BLE_LOG_TRANS_STATE_OPEN && + trans->pos > 0) { + ble_log_pool_seal_and_send(trans); + } else { +#if CONFIG_BLE_LOG_PRPH_TEST + /* Test point: this scan holds a candidate lock taken on a + * stale hint; the transport state is not OPEN. */ + if (had_open_hint && ble_log_test_flush_stale_locked_hook) { + ble_log_test_flush_stale_locked_hook(); + } +#endif + ble_log_pool_release_candidate(trans); + } + } + + BLE_LOG_REF_COUNT_RELEASE(&lbm_ref_count); +} + +void ble_log_lbm_drain_open_trans(void) +{ + /* Contract (see header): called after ble_log_lbm_begin_deinit() and + * before ble_log_rt_deinit(). The producer gate is closed and writers + * have drained, so no lock is held for longer than one frame copy. */ + ble_log_pool_seal_open_trans(); + /* Hand the sealed buffers to the peripheral before the runtime queue + * is destroyed; the peripheral deinit wait completes the delivery. */ + (void)ble_log_rt_drain(); +} + +/* ------------------------ */ +/* PUBLIC INTERFACE */ +/* ------------------------ */ +bool ble_log_enable(bool enable) +{ +#if CONFIG_BLE_LOG_PRPH_TEST + if (ble_log_test_enable_before_lifecycle_lock_hook) { + ble_log_test_enable_before_lifecycle_lock_hook(); + } +#endif + if (!ble_log_ref_count_try_acquire(&lbm_ref_count, &lbm_inited)) { + return false; + } + + BLE_LOG_ENTER_CRITICAL(); + /* Recheck under the same lifecycle lock used by deinit and flush restore, + * so a racing enable cannot reopen a closed producer gate. */ + if (!BLE_LOG_ATOMIC_LOAD_ACQUIRE(lbm_inited) || flush_in_progress) { + BLE_LOG_EXIT_CRITICAL(); + BLE_LOG_REF_COUNT_RELEASE(&lbm_ref_count); + return false; + } + if (enable) { + BLE_LOG_ATOMIC_STORE_RELEASE(lbm_enabled, true); + } else { + ble_log_lbm_disable(); + } + BLE_LOG_EXIT_CRITICAL(); + + if (!enable) { +#if CONFIG_BLE_LOG_PRPH_TEST + if (ble_log_test_disable_before_wake_hook) { + ble_log_test_disable_before_wake_hook(); + } +#endif + /* Keep the lifetime reference until every waiter wake is complete. */ + ble_log_pool_wake_all(); + } + BLE_LOG_REF_COUNT_RELEASE(&lbm_ref_count); + return true; +} + +void ble_log_flush(void) +{ + BLE_LOG_ENTER_CRITICAL(); + if (flush_in_progress) { + BLE_LOG_EXIT_CRITICAL(); + return; + } + flush_in_progress = true; + BLE_LOG_EXIT_CRITICAL(); + + if (!ble_log_ref_count_try_acquire(&lbm_ref_count, &lbm_inited)) { + goto done; + } + + bool lbm_enabled_copy = BLE_LOG_ATOMIC_LOAD_ACQUIRE(lbm_enabled); + ble_log_lbm_disable(); + TickType_t start_tick = xTaskGetTickCount(); + bool writers_drained = false; + while (!writers_drained) { + if (BLE_LOG_ATOMIC_LOAD_SEQ_CST(lbm_ref_count) <= 1) { +#if CONFIG_BLE_LOG_PRPH_TEST + if (ble_log_test_flush_drain_between_loads_hook) { + ble_log_test_flush_drain_between_loads_hook(); + } +#endif + if (BLE_LOG_ATOMIC_LOAD_ACQUIRE(g_pool.waiting_task_count) == 0) { + /* The refcount and the waiter count are two separate + * loads: a writer waking between them re-acquires its + * reference BEFORE unregistering, so a zero waiter count + * alone is not a drain. The SC fence pairs with that + * re-acquire: once this load has observed the unregister, + * the re-read must observe the re-acquired reference. */ + __atomic_thread_fence(__ATOMIC_SEQ_CST); + writers_drained = + BLE_LOG_ATOMIC_LOAD_SEQ_CST(lbm_ref_count) <= 1; + } + } + if (writers_drained) { + break; + } + ble_log_pool_wake_all(); + if ((xTaskGetTickCount() - start_tick) >= BLE_LOG_WAIT_TIMEOUT_TICKS) { + BLE_LOG_CONSOLE("@EW: Timed out waiting for BLE Log writers\n"); + goto restore; + } + vTaskDelay(1); + } + +#if BLE_LOG_UART_REDIR_ENABLED + if (!ble_log_prph_flush()) { + goto restore; + } +#endif + + if (!ble_log_pool_flush_all_trans()) { + goto restore; + } + + /* One-shot FLUSH snapshot: sample after the pool drained so the clocks + * reflect the flush completion; no sync IO toggle (periodic flow only). */ + ble_log_ts_info_t ts_info; + ble_log_rt_ts_sample(&ts_info, false); + if (!ble_log_internal_snapshot(BLE_LOG_SNAPSHOT_REASON_FLUSH, &ts_info, true)) { + goto restore; + } + + start_tick = xTaskGetTickCount(); + while (BLE_LOG_ATOMIC_LOAD_ACQUIRE(internal_trans->state) != + BLE_LOG_TRANS_STATE_FREE) { + (void)ble_log_rt_drain(); + if ((xTaskGetTickCount() - start_tick) >= BLE_LOG_WAIT_TIMEOUT_TICKS) { + BLE_LOG_CONSOLE("@EW: Timed out waiting for BLE Log Internal Snapshot\n"); + goto restore; + } + vTaskDelay(1); + } + + /* FLUSH is not a segment boundary: reset interval counters while the + * Global SN and snapshot anchor count stay continuous. */ + for (int i = 0; i < BLE_LOG_SRC_MAX; i++) { + BLE_LOG_ATOMIC_STORE_RELAXED(stat_mgr_ctx[i].counters.written_frame_cnt, 0); + BLE_LOG_ATOMIC_STORE_RELAXED(stat_mgr_ctx[i].counters.lost_frame_cnt, 0); + BLE_LOG_ATOMIC_STORE_RELAXED(stat_mgr_ctx[i].counters.written_bytes_cnt, 0); + } + BLE_LOG_ATOMIC_STORE_RELAXED(g_pool.inflight_peak, 0); +#if BLE_LOG_UART_REDIR_ENABLED + ble_log_prph_reset_util_counters(); +#endif + +restore: + BLE_LOG_ENTER_CRITICAL(); + if (BLE_LOG_ATOMIC_LOAD_ACQUIRE(lbm_inited)) { + BLE_LOG_ATOMIC_STORE_RELEASE(lbm_enabled, lbm_enabled_copy); + } + BLE_LOG_EXIT_CRITICAL(); + + BLE_LOG_REF_COUNT_RELEASE(&lbm_ref_count); +done: + BLE_LOG_ENTER_CRITICAL(); + flush_in_progress = false; + BLE_LOG_EXIT_CRITICAL(); +} + +BLE_LOG_IRAM_ATTR +bool ble_log_write_hex(ble_log_src_t src_code, const uint8_t *addr, size_t len) +{ + /* Source IDs are the frozen public enum values; garbage values are + * rejected. A non-NULL buffer with len > 0 is the caller's contract. */ + if (src_code >= BLE_LOG_SRC_MAX || (!addr && len)) { + return false; + } + + if (!ble_log_ref_count_try_acquire(&lbm_ref_count, &lbm_enabled)) { + return false; + } + /* One Global SN per attempt, consumed before the length and pool checks + * so rejected or lost attempts leave a gap. */ + uint32_t frame_sn = BLE_LOG_GET_GLOBAL_SN(); + + size_t max_log_len = BLE_LOG_MAX_PAYLOAD_LEN - sizeof(uint32_t); + if (len > max_log_len) { + goto failed; + } + + bool is_isr = BLE_LOG_IN_ISR(); + bool can_yield = !is_isr && xPortCanYield() && + xTaskGetSchedulerState() == taskSCHEDULER_RUNNING; + uint32_t timestamp = BLE_LOG_TIMESTAMP_NOW(); + size_t payload_len = sizeof(timestamp) + len; + ble_log_prph_trans_t *trans = + ble_log_pool_acquire(payload_len, !can_yield, can_yield); + if (!trans) { + goto failed; + } + + ble_log_pool_write_frame(trans, frame_sn, src_code, + (const uint8_t *)×tamp, sizeof(timestamp), + addr, (uint16_t)len, NULL, 0, false); + + BLE_LOG_REF_COUNT_RELEASE(&lbm_ref_count); + return true; + +failed: + if (BLE_LOG_ATOMIC_LOAD_ACQUIRE(lbm_inited)) { + ble_log_stat_mgr_mark_lost(src_code); + } + BLE_LOG_REF_COUNT_RELEASE(&lbm_ref_count); + return false; +} + +#if CONFIG_BLE_LOG_LL_ENABLED +BLE_LOG_IRAM_ATTR +void ble_log_write_hex_ll(uint32_t len, const uint8_t *addr, + uint32_t len_append, const uint8_t *addr_append, uint32_t flag) +{ +#if !CONFIG_BLE_LOG_HCI_LOG_ENABLED || CONFIG_BT_BLUEDROID_ENABLED || \ + CONFIG_BT_NIMBLE_LEGACY_VHCI_ENABLE + /* Bluedroid and legacy NimBLE already capture HCI on the Host side. */ + if (flag & (BIT(BLE_LOG_LL_FLAG_HCI) | BIT(BLE_LOG_LL_FLAG_HCI_UPSTREAM))) { + return; + } +#endif + if ((!addr && len) || (!addr_append && len_append)) { + return; + } + + if (!ble_log_ref_count_try_acquire(&lbm_ref_count, &lbm_enabled)) { + return; + } + + /* One Global SN per attempt; the entry order it assigns also breaks + * ties between equal-timestamp records from different sources. */ + uint32_t frame_sn = BLE_LOG_GET_GLOBAL_SN(); + + /* Preserve the controller flag-to-source mapping and ISR precedence. */ + ble_log_src_t src_code; + if (flag & BIT(BLE_LOG_LL_FLAG_ISR)) { + src_code = BLE_LOG_SRC_LL_ISR; + } else if (flag & BIT(BLE_LOG_LL_FLAG_HCI)) { + src_code = BLE_LOG_SRC_LL_HCI; + } else if (flag & BIT(BLE_LOG_LL_FLAG_HCI_UPSTREAM)) { + src_code = BLE_LOG_SRC_HCI; + } else { + src_code = BLE_LOG_SRC_LL_TASK; + } + + bool is_isr = BLE_LOG_IN_ISR(); + bool can_yield = !is_isr && xPortCanYield() && + xTaskGetSchedulerState() == taskSCHEDULER_RUNNING; + bool non_yield = !can_yield || (flag & BIT(BLE_LOG_LL_FLAG_ISR)); + bool omdata = flag & BIT(BLE_LOG_LL_FLAG_OMDATA); + if (is_isr) { + /* os_mbuf_copydata is in flash and cannot run from an ISR. */ + omdata = false; + } + + if (len > BLE_LOG_MAX_PAYLOAD_LEN || + len_append > BLE_LOG_MAX_PAYLOAD_LEN - len) { + goto failed; + } + + size_t payload_len = len + len_append; + ble_log_prph_trans_t *trans = + ble_log_pool_acquire(payload_len, non_yield, can_yield && !non_yield); + if (!trans) { + goto failed; + } + + ble_log_pool_write_frame(trans, frame_sn, src_code, + NULL, 0, addr, (uint16_t)len, + addr_append, (uint16_t)len_append, omdata); + + BLE_LOG_REF_COUNT_RELEASE(&lbm_ref_count); + return; + +failed: + if (BLE_LOG_ATOMIC_LOAD_ACQUIRE(lbm_inited)) { + ble_log_stat_mgr_mark_lost(src_code); + } + BLE_LOG_REF_COUNT_RELEASE(&lbm_ref_count); +} +#endif /* CONFIG_BLE_LOG_LL_ENABLED */ + +void ble_log_dump_to_console(void) +{ + if (!ble_log_ref_count_try_acquire(&lbm_ref_count, &lbm_inited)) { + return; + } + + BLE_LOG_ENTER_CRITICAL(); + BLE_LOG_CONSOLE("[BLE_LOG_DUMP_START:\n"); + for (int id = 0; id < BLE_LOG_POOL_TRANS_CNT; id++) { + ble_log_prph_trans_t *trans = g_pool.trans[id]; + BLE_LOG_FEED_WDT(); + for (int k = 0; k < trans->size; k++) { + BLE_LOG_CONSOLE("%02x ", trans->buf[k]); + if (!(k & 0xFF)) { + BLE_LOG_FEED_WDT(); + } + } + } + BLE_LOG_CONSOLE("\n:BLE_LOG_DUMP_END]\n\n"); + BLE_LOG_EXIT_CRITICAL(); + + BLE_LOG_REF_COUNT_RELEASE(&lbm_ref_count); +} diff --git a/components/bt/common/ble_log/src/ble_log_redir.c b/components/bt/common/ble_log/src/ble_log_redir.c new file mode 100644 index 00000000000..009fac42a30 --- /dev/null +++ b/components/bt/common/ble_log/src/ble_log_redir.c @@ -0,0 +1,121 @@ +/* + * SPDX-FileCopyrightText: 2025-2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +/* ------------------------------------------------- */ +/* BLE Log - UART Redirection Stream Writer */ +/* */ +/* Stream mode appends raw data into a transport */ +/* buffer with deferred frame encapsulation. */ +/* Redirection transports are single-writer under */ +/* redir->mutex; their only concurrent mutation is */ +/* the UART tx-done recycle (state SENDING -> FREE), */ +/* so state access uses atomics. */ +/* ------------------------------------------------- */ + +/* INCLUDE */ +#include "ble_log_redir.h" +#include "ble_log_rt.h" + +#if BLE_LOG_UART_REDIR_ENABLED +BLE_LOG_STATIC void ble_log_redir_seal(ble_log_prph_trans_t *trans, ble_log_src_t src_code); + +BLE_LOG_STATIC +ble_log_prph_trans_t *ble_log_redir_get_trans(ble_log_redir_t *redir, + ble_log_src_t src_code) +{ + for (int i = 0; i < BLE_LOG_TRANS_BUF_CNT; i++) { + ble_log_prph_trans_t *trans = redir->trans[redir->trans_idx]; + if (BLE_LOG_ATOMIC_LOAD_ACQUIRE(trans->state) != BLE_LOG_TRANS_STATE_SENDING) { + if (BLE_LOG_TRANS_FREE_SPACE(trans) >= BLE_LOG_FRAME_OVERHEAD) { + return trans; + } + if (trans->pos > BLE_LOG_FRAME_HEAD_LEN) { + ble_log_redir_seal(trans, src_code); + } + } + redir->trans_idx = (redir->trans_idx + 1) & (BLE_LOG_TRANS_BUF_CNT - 1); + } + return NULL; +} + +BLE_LOG_STATIC +void ble_log_redir_seal(ble_log_prph_trans_t *trans, ble_log_src_t src_code) +{ + if (trans->pos <= BLE_LOG_FRAME_HEAD_LEN) { + return; + } + + uint16_t payload_len = trans->pos - BLE_LOG_FRAME_HEAD_LEN; + ble_log_redir_t *redir = ble_log_prph_get_redir_lbm(); + /* REDIR keeps its own stream sequence: a raw console stream is not a + * log attempt, so it never eats Global SNs or fakes loss gaps. The + * stream has no core-stat slot. */ + uint32_t frame_sn = BLE_LOG_GET_FRAME_SN(redir->frame_sn); + ble_log_frame_head_t frame_head = { + .length = payload_len, + .frame_meta = BLE_LOG_MAKE_FRAME_META(src_code, frame_sn), + }; + BLE_LOG_MEMCPY(trans->buf, &frame_head, BLE_LOG_FRAME_HEAD_LEN); + + uint32_t checksum = ble_log_fast_checksum(trans->buf, trans->pos); + BLE_LOG_MEMCPY(trans->buf + trans->pos, &checksum, BLE_LOG_FRAME_TAIL_LEN); + trans->pos += BLE_LOG_FRAME_TAIL_LEN; + + uint32_t infl = __atomic_add_fetch(&redir->inflight, 1, __ATOMIC_RELAXED); + ble_log_atomic_update_peak(&redir->inflight_peak, infl); + + BLE_LOG_ATOMIC_STORE_RELAXED(trans->state, BLE_LOG_TRANS_STATE_SENDING); + ble_log_rt_submit_trans(trans); +} + +void ble_log_lbm_stream_write(ble_log_redir_t *redir, ble_log_src_t src_code, + const uint8_t *data, size_t len) +{ + while (len > 0) { + ble_log_prph_trans_t *trans = ble_log_redir_get_trans(redir, src_code); + if (!trans) { + /* Burn one stream SN so the dropped console batch leaves a + * gap in the REDIR sequence. */ + (void)BLE_LOG_GET_FRAME_SN(redir->frame_sn); + return; + } + + if (trans->pos == 0) { + trans->pos = BLE_LOG_FRAME_HEAD_LEN; + } + + uint16_t available = BLE_LOG_TRANS_FREE_SPACE(trans); + if (available <= BLE_LOG_FRAME_TAIL_LEN) { + ble_log_redir_seal(trans, src_code); + continue; + } + available -= BLE_LOG_FRAME_TAIL_LEN; + + size_t to_write = (len < available) ? len : available; + BLE_LOG_MEMCPY(trans->buf + trans->pos, data, to_write); + trans->pos += to_write; + data += to_write; + len -= to_write; + + if (BLE_LOG_TRANS_FREE_SPACE(trans) <= BLE_LOG_FRAME_OVERHEAD) { + ble_log_redir_seal(trans, src_code); + } + } +} + +void ble_log_lbm_stream_flush(ble_log_redir_t *redir, ble_log_src_t src_code) +{ + int trans_idx = redir->trans_idx; + for (int i = 0; i < BLE_LOG_TRANS_BUF_CNT; i++) { + ble_log_prph_trans_t *trans = redir->trans[trans_idx]; + if (BLE_LOG_ATOMIC_LOAD_ACQUIRE(trans->state) != BLE_LOG_TRANS_STATE_SENDING && + trans->pos > BLE_LOG_FRAME_HEAD_LEN) { + ble_log_redir_seal(trans, src_code); + } + trans_idx = (trans_idx + 1) & (BLE_LOG_TRANS_BUF_CNT - 1); + } +} +#endif /* BLE_LOG_UART_REDIR_ENABLED */ diff --git a/components/bt/common/ble_log/src/ble_log_rt.c b/components/bt/common/ble_log/src/ble_log_rt.c index b085468be0a..9d3d8ea452d 100644 --- a/components/bt/common/ble_log/src/ble_log_rt.c +++ b/components/bt/common/ble_log/src/ble_log_rt.c @@ -11,172 +11,166 @@ /* INCLUDE */ #include "ble_log.h" #include "ble_log_rt.h" -#include "ble_log_lbm.h" +#include "ble_log_lbm_v2.h" +#include "ble_log_task_registry.h" +#include "ble_log_util.h" #include "esp_log.h" #include "esp_timer.h" -#include "esp_chip_info.h" +#if CONFIG_BLE_LOG_LL_ENABLED +#include "esp_bt.h" +#endif +#include "driver/gpio.h" /* MACRO */ #define TAG "ble_log_rt" #define BLE_LOG_RT_DEFER_TIMEOUT_US (1000) -#if CONFIG_BT_CONTROLLER_ENABLED -#if CONFIG_IDF_TARGET_ESP32 || CONFIG_IDF_TARGET_ESP32C3 || CONFIG_IDF_TARGET_ESP32S3 -extern const char *btdm_controller_get_compile_version(void); -#define BLE_LOG_CONTROLLER_GET_COMMIT() btdm_controller_get_compile_version() -#elif !CONFIG_BT_DUAL_MODE_ARCH || CONFIG_BT_CTRL_BLE_ENABLE -/* BR/EDR-only dual-mode builds do not link the BLE controller lib */ -extern char *ble_controller_get_compile_version(void); -#define BLE_LOG_CONTROLLER_GET_COMMIT() ble_controller_get_compile_version() -#endif +/* Link-layer clock sample; 0 when the controller exports no accessor. */ +#if CONFIG_BLE_LOG_LL_ENABLED #if CONFIG_BT_DUAL_MODE_ARCH -/* BTDM common lib (dual-mode arch only) */ -extern const char *r_btdm_get_compile_version(void); -#define BLE_LOG_BTDM_COMMON_GET_COMMIT() r_btdm_get_compile_version() -#endif -#endif +/* Temporary fallback until the dual-mode controller libraries export the + * link-layer timer accessor. A strong library definition overrides it. */ +uint32_t r_sched_timer_getCurrentTimeU32(void) __attribute__((weak)); +uint32_t r_sched_timer_getCurrentTimeU32(void) +{ + return 0; +} +#define BLE_LOG_GET_LC_TS r_sched_timer_getCurrentTimeU32() +/* ESP BLE Controller Gen 2 */ +#elif defined(CONFIG_IDF_TARGET_ESP32H2) || defined(CONFIG_IDF_TARGET_ESP32C6) || defined(CONFIG_IDF_TARGET_ESP32C5) ||\ + defined(CONFIG_IDF_TARGET_ESP32C61) || defined(CONFIG_IDF_TARGET_ESP32H21) +extern uint32_t r_ble_lll_timer_current_tick_get(void); +#define BLE_LOG_GET_LC_TS r_ble_lll_timer_current_tick_get() +/* ESP BLE Controller Gen 1 */ +#elif defined(CONFIG_IDF_TARGET_ESP32C2) +extern uint32_t r_os_cputime_get32(void); +#define BLE_LOG_GET_LC_TS r_os_cputime_get32() +/* Legacy BLE Controller */ +#elif defined(CONFIG_IDF_TARGET_ESP32C3) || defined(CONFIG_IDF_TARGET_ESP32S3) +extern uint32_t lld_read_clock_us(void); +#define BLE_LOG_GET_LC_TS lld_read_clock_us() +#else /* Other targets */ +#define BLE_LOG_GET_LC_TS 0 +#endif /* BLE targets */ +#else /* !CONFIG_BLE_LOG_LL_ENABLED */ +#define BLE_LOG_GET_LC_TS 0 +#endif /* CONFIG_BLE_LOG_LL_ENABLED */ -#if CONFIG_BLE_MESH && CONFIG_BLE_MESH_V11_SUPPORT -/* "Bluetooth Mesh v1.1 commit: " */ -extern const char bt_mesh_v11_commit_str[]; +BLE_LOG_STATIC uint32_t ble_log_rt_lc_ts_get(void) +{ +#if CONFIG_BLE_LOG_LL_ENABLED + /* Legacy accessors dereference controller state. INIT is emitted before + * controller initialization completes, and standalone users may keep the + * controller idle for the entire BLE Log epoch. */ + if (esp_bt_controller_get_status() == ESP_BT_CONTROLLER_STATUS_IDLE) { + return 0; + } #endif - -#if CONFIG_BT_AUDIO && CONFIG_SOC_BLE_AUDIO_SUPPORTED -extern const char *lib_audio_commit_get(void); -#endif - -_Static_assert(sizeof(ble_log_version_info_t) == 58, - "Unexpected BLE Log version info frame size"); + return BLE_LOG_GET_LC_TS; +} /* VARIABLE */ BLE_LOG_STATIC BLE_LOG_DRAM_ATTR uint32_t rt_inited = 0; BLE_LOG_STATIC BLE_LOG_DRAM_ATTR volatile uint32_t rt_ref_count = 0; BLE_LOG_STATIC BLE_LOG_DRAM_ATTR QueueHandle_t rt_queue_handle = NULL; BLE_LOG_STATIC BLE_LOG_DRAM_ATTR esp_timer_handle_t rt_defer_timer = NULL; -BLE_LOG_STATIC uint32_t rt_last_hook_os_ts = 0; -#if CONFIG_BLE_LOG_TS_ENABLED -BLE_LOG_STATIC BLE_LOG_DRAM_ATTR uint32_t rt_ts_enabled = 0; -BLE_LOG_STATIC esp_timer_handle_t rt_ts_timer = NULL; -#endif /* CONFIG_BLE_LOG_TS_ENABLED */ +BLE_LOG_STATIC BLE_LOG_DRAM_ATTR esp_timer_handle_t rt_ts_timer = NULL; +/* Toggle IO phase; stays false when the toggle IO is compiled out. */ +BLE_LOG_STATIC BLE_LOG_DRAM_ATTR bool rt_ts_io_level = false; +#if CONFIG_BLE_LOG_TS_SYNC_TOGGLE_IO_ENABLED +/* Runtime IO control is independent from the always-on periodic snapshot. */ +BLE_LOG_STATIC BLE_LOG_DRAM_ATTR bool rt_ts_io_toggle_enabled = false; +#endif /* PRIVATE FUNCTION DECLARATION */ BLE_LOG_STATIC void ble_log_rt_defer_cb(void *arg); -BLE_LOG_STATIC bool ble_log_rt_dispatch(QueueHandle_t queue, UBaseType_t pending); -BLE_LOG_STATIC void ble_log_rt_run_hook(void); -#if CONFIG_BLE_LOG_TS_ENABLED +BLE_LOG_STATIC void ble_log_rt_dispatch(QueueHandle_t queue, + UBaseType_t pending); BLE_LOG_STATIC void ble_log_rt_ts_trigger(void *arg); -#endif /* CONFIG_BLE_LOG_TS_ENABLED */ /* PRIVATE FUNCTION */ -/* Copies a NUL-terminated commit string into a fixed-width zero-padded field */ -BLE_LOG_STATIC void ble_log_commit_copy(uint8_t *dst, const char *src, size_t len) +/* Captures the link-layer, ESP and OS clocks at one instant. */ +void ble_log_rt_ts_sample(ble_log_ts_info_t *info, bool toggle_io) { - BLE_LOG_MEMCPY(dst, src, strnlen(src, len)); + info->int_src_code = BLE_LOG_INT_SRC_TS; + BLE_LOG_ENTER_CRITICAL(); +#if CONFIG_BLE_LOG_TS_SYNC_TOGGLE_IO_ENABLED + /* The critical section keeps the optional IO edge and clock samples + * adjacent, and serializes runtime IO control with the phase update. */ + if (toggle_io && rt_ts_io_toggle_enabled) { + rt_ts_io_level = !rt_ts_io_level; + gpio_set_level(CONFIG_BLE_LOG_SYNC_IO_NUM, rt_ts_io_level); + } +#endif /* CONFIG_BLE_LOG_TS_SYNC_TOGGLE_IO_ENABLED */ + info->io_level = rt_ts_io_level; + info->lc_ts = ble_log_rt_lc_ts_get(); + info->esp_ts = esp_timer_get_time(); + info->os_ts = pdTICKS_TO_MS(xTaskGetTickCountFromISR()); + BLE_LOG_EXIT_CRITICAL(); } -BLE_LOG_STATIC void ble_log_rt_run_hook(void) -{ - uint32_t now = pdTICKS_TO_MS(xTaskGetTickCount()); - if ((uint32_t)(now - rt_last_hook_os_ts) < BLE_LOG_TS_TRIGGER_TIMEOUT_MS) { - return; - } - rt_last_hook_os_ts = now; - - /* Write version info: BLE Log version, idf commit (build-time), - * linked-in BLE lib commits, chip model/revision (efuse, runtime-only). - * Libs absent from the build leave their fields zero. */ - ble_log_version_info_t version_info = { - .int_src_code = BLE_LOG_INT_SRC_VERSION_INFO, - .version = BLE_LOG_VERSION, - }; -#ifdef BLE_LOG_IDF_COMMIT - BLE_LOG_MEMCPY(version_info.idf_commit, BLE_LOG_IDF_COMMIT, BLE_LOG_IDF_COMMIT_LEN); -#endif -#if CONFIG_BT_CONTROLLER_ENABLED && defined(BLE_LOG_CONTROLLER_GET_COMMIT) - ble_log_commit_copy(version_info.controller_commit, BLE_LOG_CONTROLLER_GET_COMMIT(), - BLE_LOG_LIB_COMMIT_LEN); -#endif -#if CONFIG_BT_CONTROLLER_ENABLED && defined(BLE_LOG_BTDM_COMMON_GET_COMMIT) - ble_log_commit_copy(version_info.btdm_common_commit, BLE_LOG_BTDM_COMMON_GET_COMMIT(), - BLE_LOG_LIB_COMMIT_LEN); -#endif -#if CONFIG_BLE_MESH && CONFIG_BLE_MESH_V11_SUPPORT - /* The hash is the substring after the last space of the lib string */ - const char *mesh_commit = strrchr(bt_mesh_v11_commit_str, ' '); - if (mesh_commit) { - ble_log_commit_copy(version_info.mesh_commit, mesh_commit + 1, - BLE_LOG_LIB_COMMIT_LEN); - } -#endif -#if CONFIG_BT_AUDIO && CONFIG_SOC_BLE_AUDIO_SUPPORTED - ble_log_commit_copy(version_info.audio_commit, lib_audio_commit_get(), - BLE_LOG_LIB_COMMIT_LEN); -#endif - esp_chip_info_t chip_info; - esp_chip_info(&chip_info); - version_info.chip_model = (uint16_t)chip_info.model; - version_info.chip_revision = chip_info.revision; - ble_log_write_hex(BLE_LOG_SRC_INTERNAL, (const uint8_t *)&version_info, - sizeof(version_info)); - - ble_log_write_enh_stat(); - ble_log_write_buf_util(); -} - -BLE_LOG_STATIC bool ble_log_rt_dispatch(QueueHandle_t queue, UBaseType_t pending) +/* Dispatch only the queue depth observed at callback entry. A backend may + * recycle synchronously on queue-full while another core immediately refills + * the runtime queue; an unbounded loop here could starve every other callback + * on the shared ESP timer task. */ +BLE_LOG_STATIC void ble_log_rt_dispatch(QueueHandle_t queue, + UBaseType_t pending) { ble_log_prph_trans_t *trans = NULL; - bool processed = false; while (pending-- && xQueueReceive(queue, &trans, 0) == pdTRUE) { ble_log_prph_send_trans(trans); - processed = true; } - return processed; } BLE_LOG_STATIC void ble_log_rt_defer_cb(void *arg) { (void)arg; + /* Deinit clears the queue only after stop_blocking has waited for this + * callback, so a live gate implies a live queue. */ if (!BLE_LOG_ATOMIC_LOAD_ACQUIRE(rt_inited)) { return; } QueueHandle_t queue = rt_queue_handle; - if (!queue) { - return; - } + ble_log_rt_dispatch(queue, uxQueueMessagesWaiting(queue)); - UBaseType_t pending = uxQueueMessagesWaiting(queue); - if (ble_log_rt_dispatch(queue, pending)) { - ble_log_rt_run_hook(); - } - - pending = uxQueueMessagesWaiting(queue); - if (pending && + /* A submit racing an active one-shot callback may fail to arm it. If the + * bounded batch left work behind, schedule another turn after yielding + * the shared timer task to callbacks that are already due. */ + if (uxQueueMessagesWaiting(queue) && ble_log_ref_count_try_acquire(&rt_ref_count, &rt_inited)) { - (void)esp_timer_start_once(rt_defer_timer, BLE_LOG_RT_DEFER_TIMEOUT_US); + (void)esp_timer_start_once(rt_defer_timer, + BLE_LOG_RT_DEFER_TIMEOUT_US); BLE_LOG_REF_COUNT_RELEASE(&rt_ref_count); } } -#if CONFIG_BLE_LOG_TS_ENABLED BLE_LOG_STATIC void ble_log_rt_ts_trigger(void *arg) { (void)arg; - if (!BLE_LOG_ATOMIC_LOAD_ACQUIRE(rt_inited) || - !BLE_LOG_ATOMIC_LOAD_ACQUIRE(rt_ts_enabled)) { + if (!BLE_LOG_ATOMIC_LOAD_ACQUIRE(rt_inited)) { return; } - ble_log_ts_info_t *ts_info = NULL; - ble_log_ts_info_update(&ts_info); - if (ts_info) { - ble_log_write_hex(BLE_LOG_SRC_INTERNAL, (const uint8_t *)ts_info, sizeof(ble_log_ts_info_t)); - } + ble_log_ts_info_t ts_info; + ble_log_rt_ts_sample(&ts_info, true); + + /* Unified periodic output: best-effort flush of partially-filled OPEN + * transports ahead of the periodic snapshot, so parked frames do not + * wait for the next capacity seal. The task-id binding broadcast + * rides the same window, ahead of the snapshot, on its own dedicated + * transport: a receiver that missed a binding converges on the next + * one, and the snapshot never waits behind it. */ + ble_log_lbm_flush_open_trans(); + ble_log_task_bindings_publish(); + + (void)ble_log_internal_snapshot( + BLE_LOG_SNAPSHOT_REASON_PERIODIC | + BLE_LOG_SNAPSHOT_REASON_TS_VALID, + &ts_info, false); } -#endif /* CONFIG_BLE_LOG_TS_ENABLED */ /* INTERFACE */ bool ble_log_rt_init(void) @@ -185,6 +179,21 @@ bool ble_log_rt_init(void) return true; } + /* Configure the analyzer toggle IO */ +#if CONFIG_BLE_LOG_TS_SYNC_TOGGLE_IO_ENABLED + gpio_config_t sync_io_conf = { + .intr_type = GPIO_INTR_DISABLE, + .mode = GPIO_MODE_OUTPUT, + .pin_bit_mask = BIT64(CONFIG_BLE_LOG_SYNC_IO_NUM), + }; + /* Program the output latch before enabling the driver to avoid a stale + * high level or a high glitch after deinit/reinit. */ + if (gpio_set_level(CONFIG_BLE_LOG_SYNC_IO_NUM, 0) != ESP_OK || + gpio_config(&sync_io_conf) != ESP_OK) { + goto exit; + } +#endif /* CONFIG_BLE_LOG_TS_SYNC_TOGGLE_IO_ENABLED */ + rt_queue_handle = xQueueCreate(BLE_LOG_TRANS_TOTAL_CNT, sizeof(ble_log_prph_trans_t *)); if (!rt_queue_handle) { goto exit; @@ -201,8 +210,13 @@ bool ble_log_rt_init(void) goto exit; } -#if CONFIG_BLE_LOG_TS_ENABLED - BLE_LOG_ATOMIC_STORE_RELAXED(rt_ts_enabled, false); + /* Create the system-periodic path here; the top-level init starts it only + * after queuing INIT, then it runs through deinit. Runtime sync control + * affects only the optional analyzer IO toggle. */ + rt_ts_io_level = false; +#if CONFIG_BLE_LOG_TS_SYNC_TOGGLE_IO_ENABLED + rt_ts_io_toggle_enabled = false; +#endif esp_timer_create_args_t ts_timer_args = { .callback = ble_log_rt_ts_trigger, .arg = NULL, @@ -211,13 +225,10 @@ bool ble_log_rt_init(void) /* Do not wake light sleep or replay every missed periodic callback. */ .skip_unhandled_events = true, }; - if (esp_timer_create(&ts_timer_args, &rt_ts_timer) != ESP_OK || - esp_timer_start_periodic(rt_ts_timer, BLE_LOG_TS_TRIGGER_TIMEOUT_US) != ESP_OK) { + if (esp_timer_create(&ts_timer_args, &rt_ts_timer) != ESP_OK) { goto exit; } -#endif /* CONFIG_BLE_LOG_TS_ENABLED */ - rt_last_hook_os_ts = 0; BLE_LOG_ATOMIC_STORE_RELEASE(rt_inited, true); return true; @@ -226,6 +237,21 @@ exit: return false; } +bool ble_log_rt_start_periodic(void) +{ + bool started = false; + if (!ble_log_ref_count_try_acquire(&rt_ref_count, &rt_inited)) { + return false; + } + if (rt_ts_timer && + esp_timer_start_periodic(rt_ts_timer, + BLE_LOG_TS_TRIGGER_TIMEOUT_US) == ESP_OK) { + started = true; + } + BLE_LOG_REF_COUNT_RELEASE(&rt_ref_count); + return started; +} + void ble_log_rt_deinit(void) { /* Closing gate: seq_cst on both sides (see also submit/drain) so a @@ -236,14 +262,11 @@ void ble_log_rt_deinit(void) ESP_LOGE(TAG, "Timed out waiting for BLE Log runtime references"); BLE_LOG_ASSERT(false); } -#if CONFIG_BLE_LOG_TS_ENABLED - BLE_LOG_ATOMIC_STORE_RELEASE(rt_ts_enabled, false); if (rt_ts_timer) { esp_timer_stop_blocking(rt_ts_timer, portMAX_DELAY); esp_timer_delete(rt_ts_timer); rt_ts_timer = NULL; } -#endif /* CONFIG_BLE_LOG_TS_ENABLED */ if (rt_defer_timer) { esp_timer_stop_blocking(rt_defer_timer, portMAX_DELAY); @@ -254,12 +277,18 @@ void ble_log_rt_deinit(void) if (rt_queue_handle) { ble_log_prph_trans_t *trans = NULL; while (xQueueReceive(rt_queue_handle, &trans, 0) == pdTRUE) { - trans->pos = 0; ble_log_lbm_recycle_trans(trans); } vQueueDelete(rt_queue_handle); rt_queue_handle = NULL; } + + /* Release the toggle IO */ + rt_ts_io_level = false; +#if CONFIG_BLE_LOG_TS_SYNC_TOGGLE_IO_ENABLED + rt_ts_io_toggle_enabled = false; + gpio_reset_pin(CONFIG_BLE_LOG_SYNC_IO_NUM); +#endif /* CONFIG_BLE_LOG_TS_SYNC_TOGGLE_IO_ENABLED */ } bool ble_log_rt_drain(void) @@ -276,7 +305,9 @@ bool ble_log_rt_drain(void) goto exit; } QueueHandle_t queue = rt_queue_handle; - (void)ble_log_rt_dispatch(queue, uxQueueMessagesWaiting(queue)); + while (uxQueueMessagesWaiting(queue)) { + ble_log_rt_dispatch(queue, uxQueueMessagesWaiting(queue)); + } drained = true; exit: @@ -287,13 +318,13 @@ exit: BLE_LOG_IRAM_ATTR void ble_log_rt_submit_trans(ble_log_prph_trans_t *trans) { if (!ble_log_ref_count_try_acquire(&rt_ref_count, &rt_inited)) { + /* No reference was taken, so this path must not release; it stays + * separate from the held-reference failures below. */ ble_log_lbm_recycle_trans(trans); return; } if (!rt_queue_handle) { - BLE_LOG_REF_COUNT_RELEASE(&rt_ref_count); - ble_log_lbm_recycle_trans(trans); - return; + goto fail; } bool in_isr = BLE_LOG_IN_ISR(); @@ -301,24 +332,46 @@ BLE_LOG_IRAM_ATTR void ble_log_rt_submit_trans(ble_log_prph_trans_t *trans) ? xQueueSendFromISR(rt_queue_handle, &trans, NULL) : xQueueSend(rt_queue_handle, &trans, 0); if (queued != pdTRUE) { - BLE_LOG_REF_COUNT_RELEASE(&rt_ref_count); - ble_log_lbm_recycle_trans(trans); - return; + goto fail; } /* An active timer keeps the deadline anchored to the first submission. */ (void)esp_timer_start_once(rt_defer_timer, BLE_LOG_RT_DEFER_TIMEOUT_US); BLE_LOG_REF_COUNT_RELEASE(&rt_ref_count); + return; + +fail: + BLE_LOG_REF_COUNT_RELEASE(&rt_ref_count); + ble_log_lbm_recycle_trans(trans); } -#if CONFIG_BLE_LOG_TS_ENABLED -bool ble_log_sync_enable(bool enable) +bool ble_log_ts_sync_io_toggle_enable(bool enable) { - if (!BLE_LOG_ATOMIC_LOAD_ACQUIRE(rt_inited)) { + if (!ble_log_ref_count_try_acquire(&rt_ref_count, &rt_inited)) { return false; } - BLE_LOG_ATOMIC_STORE_RELEASE(rt_ts_enabled, enable); - ble_log_ts_reset(enable); + +#if CONFIG_BLE_LOG_TS_SYNC_TOGGLE_IO_ENABLED + /* Restart the analyzer phase from low. When disabling an already-low IO, + * drive it high first so the external analyzer still sees a final + * falling edge. The periodic snapshot remains active in either state. */ + BLE_LOG_ENTER_CRITICAL(); + rt_ts_io_toggle_enabled = enable; + if (!enable && !rt_ts_io_level) { + gpio_set_level(CONFIG_BLE_LOG_SYNC_IO_NUM, 1); + } + rt_ts_io_level = false; + gpio_set_level(CONFIG_BLE_LOG_SYNC_IO_NUM, 0); + BLE_LOG_EXIT_CRITICAL(); +#else + (void)enable; +#endif /* CONFIG_BLE_LOG_TS_SYNC_TOGGLE_IO_ENABLED */ + + BLE_LOG_REF_COUNT_RELEASE(&rt_ref_count); return true; } -#endif /* CONFIG_BLE_LOG_TS_ENABLED */ + +bool ble_log_sync_enable(bool enable) +{ + return ble_log_ts_sync_io_toggle_enable(enable); +} diff --git a/components/bt/common/ble_log/src/ble_log_task_registry.c b/components/bt/common/ble_log/src/ble_log_task_registry.c new file mode 100644 index 00000000000..5a1d8130d28 --- /dev/null +++ b/components/bt/common/ble_log/src/ble_log_task_registry.c @@ -0,0 +1,274 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +/* ------------------------------------ */ +/* BLE Log - Task-id Registry */ +/* */ +/* Owns the name-to-id registry, its */ +/* broadcast sequence and its dedicated */ +/* transport; a separate .c/.h pair */ +/* like the UART redirection writer. */ +/* ------------------------------------ */ + +/* INCLUDE */ +#include "ble_log_task_registry.h" +#include "ble_log_rt.h" + +#include "freertos/task.h" +#include "esp_timer.h" + +/* MACRO */ +/* Registry entries are word-compared on the record path. */ +#define BLE_LOG_TASK_NAME_WORDS (BLE_LOG_TASK_NAME_LEN / sizeof(uint32_t)) +/* ble_log_task_lookup() and ble_log_task_register() compare the four name + * words literally; a different name width must rework them. */ +_Static_assert(BLE_LOG_TASK_NAME_WORDS == 4, + "Unexpected BLE Log task name word count"); + +/* VARIABLE */ +/* Plain static variables (NOBITS .bss): the registry has no IRAM path, and zero + * page-image bytes beat DRAM_ATTR placement for a table this size. */ +BLE_LOG_STATIC uint32_t s_task_names[CONFIG_BLE_LOG_TASK_ID_MAX][BLE_LOG_TASK_NAME_WORDS]; +BLE_LOG_STATIC volatile uint8_t s_task_cnt; +BLE_LOG_STATIC ble_log_atomic_lock_t s_task_reg_lock; + +/* The binding broadcast keeps its own 24-bit frame sequence, separate + * from the log/snapshot Global SN (see ble_log_lbm_v2.h): a gap counts a + * broadcast window skipped despite a non-empty registry, never a lost + * snapshot. ble_log_task_registry_init resets it. */ +BLE_LOG_STATIC uint32_t g_task_binding_sn; +#define BLE_LOG_GET_TASK_BINDING_SN() BLE_LOG_GET_FRAME_SN(g_task_binding_sn) + +/* Module lifetime gate, closed by begin_deinit (LBM teardown starts): + * publish is system output beyond the public producer gate, and only + * module teardown stops new broadcasts. The publish path never blocks, + * so the begin_deinit drain is immediate. */ +BLE_LOG_STATIC volatile uint32_t s_registry_ref_count = 0; +BLE_LOG_STATIC uint32_t s_registry_inited = 0; +/* The dedicated single-frame transport. It shares the INTERNAL owner + * kind: recycled by the peripheral tx-done straight back to FREE, with + * no pool bookkeeping. */ +BLE_LOG_STATIC ble_log_prph_trans_t *binding_trans; + +/* ------------------------------- */ +/* Registry Lookup/Insert */ +/* ------------------------------- */ +/* - Entries are append-only: an inserter (holding s_task_reg_lock) + * writes all name words first and publishes the entry by storing the + * new count with RELEASE; readers ACQUIRE-load the count and scan + * only the entries below it, so the read path is lock-free. + * - Names are matched by content, not by TCB pointer, so a recycled + * TCB cannot alias two different tasks onto one id. + * - Names longer than the storage are truncated; tasks sharing the + * truncated prefix share an id (identical names are indistinguishable + * in the decoded log anyway). + * - A full registry, or a contended registration attempt, degrades that + * record to the unknown id (BLE_LOG_TASK_ID_UNKNOWN) and still emits + * it; the task retries the registration with its next record. + * - Bindings are broadcast on every periodic snapshot window, so a + * receiver that joined late or lost a frame converges on the next + * window; broadcasts are idempotent on the wire and a lost one + * self-heals on the next window. A task that starts logging between + * windows emits records tagged with its id; the receiver binds the id + * at the next window. */ + +/* Normalizes a task name to NUL-padded words. Word-level compare + * replaces strcmp on the record path: the first word acts as the hash, + * a miss costs one load per entry, and there is no call overhead. Inline: + * every ENCODE record resolves its writer id, so this runs per record. */ +BLE_LOG_STATIC inline void ble_log_task_name_to_words( + const char *name, uint32_t w[BLE_LOG_TASK_NAME_WORDS]) +{ + BLE_LOG_MEMSET(w, 0, BLE_LOG_TASK_NAME_LEN); + BLE_LOG_MEMCPY(w, name, strnlen(name, BLE_LOG_TASK_NAME_LEN)); +} + +/* Per-record lookup: inline for the same reason as the normalizer above. */ +BLE_LOG_STATIC inline uint8_t ble_log_task_lookup( + const uint32_t w[BLE_LOG_TASK_NAME_WORDS]) +{ + uint8_t cnt = BLE_LOG_ATOMIC_LOAD_ACQUIRE(s_task_cnt); + for (uint8_t i = 0; i < cnt; i++) { + if (s_task_names[i][0] != w[0]) { + continue; + } + if (w[1] == s_task_names[i][1] && w[2] == s_task_names[i][2] && + w[3] == s_task_names[i][3]) { + return i; + } + } + return BLE_LOG_TASK_ID_UNKNOWN; +} + +/* Cold path, once per task lifetime. A single CAS attempt serializes + * inserters: on contention this record degrades to the unknown id + * instead of blocking, and the next record of the task retries. */ +BLE_LOG_STATIC uint8_t ble_log_task_register(const uint32_t w[BLE_LOG_TASK_NAME_WORDS]) +{ + uint8_t id = BLE_LOG_TASK_ID_UNKNOWN; + + if (!BLE_LOG_CAS_ACQUIRE(&s_task_reg_lock)) { + return BLE_LOG_TASK_ID_UNKNOWN; + } + uint8_t cnt = s_task_cnt; + for (uint8_t i = 0; i < cnt; i++) { + if (w[0] == s_task_names[i][0] && w[1] == s_task_names[i][1] && + w[2] == s_task_names[i][2] && w[3] == s_task_names[i][3]) { + BLE_LOG_CAS_RELEASE(&s_task_reg_lock); + return i; /* raced with another first lookup */ + } + } + if (cnt < CONFIG_BLE_LOG_TASK_ID_MAX) { + BLE_LOG_MEMCPY(s_task_names[cnt], w, BLE_LOG_TASK_NAME_LEN); + id = cnt; + BLE_LOG_ATOMIC_STORE_RELEASE(s_task_cnt, cnt + 1); + } + BLE_LOG_CAS_RELEASE(&s_task_reg_lock); + return id; +} + +uint8_t ble_log_task_id_current(void) +{ + const char *cur_name = pcTaskGetName(NULL); + if (!cur_name) { + return BLE_LOG_TASK_ID_UNKNOWN; + } + uint32_t w[BLE_LOG_TASK_NAME_WORDS]; + ble_log_task_name_to_words(cur_name, w); + uint8_t id = ble_log_task_lookup(w); + /* Registration mutates shared state; ISR callers stop at the + * lookup-hit path and read only. */ + if (id == BLE_LOG_TASK_ID_UNKNOWN && !BLE_LOG_IN_ISR()) { + id = ble_log_task_register(w); + } + return id; +} + +/* ----------------------------------- */ +/* Periodic Binding Broadcast */ +/* ----------------------------------- */ +/* One binding frame on the dedicated registry transport, packing every + * registered entry in id order. Best effort, single CAS attempt: a busy + * transport (the previous broadcast still in DMA) skips this window and + * the next one rebroadcasts. The registry is only read here (ACQUIRE + * count, append-only entries), so this races no inserter. */ +BLE_LOG_STATIC bool ble_log_task_binding_send(uint8_t cnt, uint32_t timestamp) +{ + /* Snapshot-sequence semantics: the window SN is consumed before + * contending for the transport, so a skipped window burns it and + * the gap in the binding sequence counts skipped windows. */ + uint32_t frame_sn = BLE_LOG_GET_TASK_BINDING_SN(); + if (!BLE_LOG_CAS_ACQUIRE(&binding_trans->atomic_lock)) { + return false; + } + /* Acceptance load: pairs with the dedicated transport's lock-free + * recycle publication (pos=0, STORE_RELEASE(FREE)). */ + if (BLE_LOG_ATOMIC_LOAD_ACQUIRE(binding_trans->state) != + BLE_LOG_TRANS_STATE_FREE) { + BLE_LOG_CAS_RELEASE(&binding_trans->atomic_lock); + return false; + } + + ble_log_frame_head_t frame_head = { + .length = (uint16_t)(sizeof(uint32_t) + + cnt * sizeof(ble_log_task_binding_t)), + .frame_meta = BLE_LOG_MAKE_FRAME_META(BLE_LOG_SRC_INTERNAL, frame_sn), + }; + uint8_t *buf = binding_trans->buf; + BLE_LOG_MEMCPY(buf, &frame_head, sizeof(frame_head)); + size_t pos = BLE_LOG_FRAME_HEAD_LEN; + BLE_LOG_MEMCPY(buf + pos, ×tamp, sizeof(timestamp)); + pos += sizeof(timestamp); + for (uint8_t i = 0; i < cnt; i++) { + ble_log_task_binding_t binding = { + .int_src_code = BLE_LOG_INT_SRC_TASK_BINDING, + .task_id = i, + }; + BLE_LOG_MEMCPY(binding.task_name, s_task_names[i], + BLE_LOG_TASK_NAME_LEN); + BLE_LOG_MEMCPY(buf + pos, &binding, sizeof(binding)); + pos += sizeof(binding); + } + uint32_t checksum = ble_log_fast_checksum( + buf, BLE_LOG_FRAME_HEAD_LEN + frame_head.length); + BLE_LOG_MEMCPY(buf + BLE_LOG_FRAME_HEAD_LEN + frame_head.length, + &checksum, sizeof(checksum)); + + binding_trans->pos = (uint16_t)(pos + BLE_LOG_FRAME_TAIL_LEN); + BLE_LOG_ATOMIC_STORE_RELAXED(binding_trans->state, + BLE_LOG_TRANS_STATE_SENDING); + BLE_LOG_CAS_RELEASE(&binding_trans->atomic_lock); + ble_log_rt_submit_trans(binding_trans); + return true; +} + +void ble_log_task_bindings_publish(void) +{ + if (!ble_log_ref_count_try_acquire(&s_registry_ref_count, &s_registry_inited)) { + return; + } + /* Empty registry: nothing to broadcast; no window attempt, no SN. */ + uint8_t cnt = BLE_LOG_ATOMIC_LOAD_ACQUIRE(s_task_cnt); + if (cnt != 0) { + uint32_t timestamp = (uint32_t)esp_timer_get_time(); + (void)ble_log_task_binding_send(cnt, timestamp); + } + BLE_LOG_REF_COUNT_RELEASE(&s_registry_ref_count); +} + +/* --------------------------- */ +/* Module Lifetime */ +/* --------------------------- */ +bool ble_log_task_registry_init(void) +{ + if (BLE_LOG_ATOMIC_LOAD_ACQUIRE(s_registry_inited)) { + return true; + } + + /* Fresh epoch: empty registry, no broadcast sequence. init runs + * single-threaded (from ble_log_lbm_init), so the registry state and + * its lock are cleared by value. */ + BLE_LOG_MEMSET(s_task_names, 0, sizeof(s_task_names)); + BLE_LOG_ATOMIC_STORE_RELEASE(s_task_cnt, 0); + s_task_reg_lock = 0; + g_task_binding_sn = 0; + + if (!ble_log_prph_trans_init(&binding_trans, BLE_LOG_TASK_BINDING_TRANS_SIZE)) { + return false; + } + binding_trans->id = BLE_LOG_TRANS_ID_NONE; + binding_trans->owner_kind = BLE_LOG_TRANS_OWNER_INTERNAL; + + BLE_LOG_ATOMIC_STORE_RELEASE(s_registry_inited, true); + return true; +} + +void ble_log_task_registry_begin_deinit(void) +{ + /* Closing gate: pairs with the seq_cst acquire in publish. */ + BLE_LOG_ATOMIC_STORE_SEQ_CST(s_registry_inited, false); + while (!ble_log_ref_count_wait(&s_registry_ref_count, 0)) { + BLE_LOG_ASSERT(false); + } +} + +void ble_log_task_registry_deinit(void) +{ + ble_log_task_registry_begin_deinit(); + ble_log_prph_trans_deinit(&binding_trans); +} + +#if CONFIG_BLE_LOG_PRPH_TEST +/* Test-only: wipes the task registry so test cases stay order-independent. + * Call between cases, with no writers in flight. */ +void ble_log_test_task_registry_reset(void) +{ + if (BLE_LOG_CAS_ACQUIRE(&s_task_reg_lock)) { + BLE_LOG_ATOMIC_STORE_RELEASE(s_task_cnt, 0); + BLE_LOG_CAS_RELEASE(&s_task_reg_lock); + } +} +#endif diff --git a/components/bt/common/ble_log/src/ble_log_ts.c b/components/bt/common/ble_log/src/ble_log_ts.c deleted file mode 100644 index 03f931fe6ce..00000000000 --- a/components/bt/common/ble_log/src/ble_log_ts.c +++ /dev/null @@ -1,92 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2025-2026 Espressif Systems (Shanghai) CO LTD - * - * SPDX-License-Identifier: Apache-2.0 - */ -/* ----------------------------------- */ -/* BLE Log - Timestamp Synchronization */ -/* ----------------------------------- */ - -/* INCLUDE */ -#include "ble_log_ts.h" - -/* VARIABLE */ -BLE_LOG_STATIC BLE_LOG_DRAM_ATTR bool ts_inited = false; -BLE_LOG_STATIC BLE_LOG_DRAM_ATTR ble_log_ts_info_t *ts_info = NULL; - -/* INTERFACE */ -bool ble_log_ts_init(void) -{ - if (ts_inited) { - return true; - } - - /* Initialize toggle IO */ - gpio_config_t sync_io_conf = { - .intr_type = GPIO_INTR_DISABLE, - .mode = GPIO_MODE_OUTPUT, - .pin_bit_mask = BIT64(CONFIG_BLE_LOG_SYNC_IO_NUM), - }; - if (gpio_config(&sync_io_conf) != ESP_OK) { - goto exit; - } - - /* Initialize sync data */ - ts_info = (ble_log_ts_info_t *)BLE_LOG_MALLOC(sizeof(ble_log_ts_info_t)); - if (!ts_info) { - goto exit; - } - BLE_LOG_MEMSET(ts_info, 0, sizeof(ble_log_ts_info_t)); - ts_info->int_src_code = BLE_LOG_INT_SRC_TS; - - ts_inited = true; - return true; - -exit: - ble_log_ts_deinit(); - return false; -} - -void ble_log_ts_deinit(void) -{ - ts_inited = false; - - /* Release sync data */ - if (ts_info) { - BLE_LOG_FREE(ts_info); - ts_info = NULL; - } - - /* Release toggle IO */ - gpio_reset_pin(CONFIG_BLE_LOG_SYNC_IO_NUM); -} - -void ble_log_ts_info_update(ble_log_ts_info_t **info) -{ - if (!ts_inited) { - return; - } - - BLE_LOG_ENTER_CRITICAL(); - ts_info->io_level = !ts_info->io_level; - gpio_set_level(CONFIG_BLE_LOG_SYNC_IO_NUM, ts_info->io_level); - ts_info->lc_ts = BLE_LOG_GET_LC_TS; - ts_info->esp_ts = esp_timer_get_time(); - ts_info->os_ts = pdTICKS_TO_MS(xTaskGetTickCountFromISR()); - BLE_LOG_EXIT_CRITICAL(); - - *info = ts_info; -} - -void ble_log_ts_reset(bool status) -{ - if (!ts_inited) { - return; - } - - if (!status && !ts_info->io_level) { - gpio_set_level(CONFIG_BLE_LOG_SYNC_IO_NUM, 1); - } - ts_info->io_level = 0; - gpio_set_level(CONFIG_BLE_LOG_SYNC_IO_NUM, 0); -} diff --git a/components/bt/common/ble_log/src/ble_log_util.c b/components/bt/common/ble_log/src/ble_log_util.c index b602f3ced41..17a52df6ca6 100644 --- a/components/bt/common/ble_log/src/ble_log_util.c +++ b/components/bt/common/ble_log/src/ble_log_util.c @@ -25,9 +25,8 @@ BLE_LOG_DRAM_ATTR portMUX_TYPE ble_log_spin_lock = portMUX_INITIALIZER_UNLOCKED; BLE_LOG_IRAM_ATTR BLE_LOG_STATIC BLE_LOG_INLINE uint32_t ror32(uint32_t word, uint32_t shift) { - if (unlikely(shift == 0)) { - return word; - } + /* The mask folds shift == 0 into a zero left-shift amount, so the + * expression needs no shift == 0 guard. */ return (word >> shift) | (word << ((32 - shift) & 0x1F)); } @@ -74,12 +73,12 @@ uint32_t ble_log_fast_checksum(const uint8_t *data, size_t len) BLE_LOG_IRAM_ATTR bool ble_log_ref_count_try_acquire(volatile uint32_t *ref_count, - const uint32_t *inited) + const uint32_t *gate) { - /* The seq_cst increment/check pairs with deinit's seq_cst gate close - * before it waits for the reference count. */ + /* The seq_cst increment/check pairs with a seq_cst gate close before the + * owner waits for the reference count. */ BLE_LOG_REF_COUNT_ACQUIRE_SEQ_CST(ref_count); - if (BLE_LOG_ATOMIC_LOAD_SEQ_CST(*inited)) { + if (BLE_LOG_ATOMIC_LOAD_SEQ_CST(*gate)) { return true; } BLE_LOG_REF_COUNT_RELEASE(ref_count); @@ -98,3 +97,13 @@ bool ble_log_ref_count_wait(volatile uint32_t *ref_count, uint32_t max_ref_count } return true; } + +BLE_LOG_IRAM_ATTR +void ble_log_atomic_update_peak(volatile uint32_t *peak, uint32_t value) +{ + uint32_t current = BLE_LOG_ATOMIC_LOAD_RELAXED(*peak); + while (value > current && + !__atomic_compare_exchange_n(peak, ¤t, value, true, + __ATOMIC_RELAXED, __ATOMIC_RELAXED)) { + } +} diff --git a/components/bt/common/ble_log/src/internal_include/ble_log_lbm.h b/components/bt/common/ble_log/src/internal_include/ble_log_lbm.h deleted file mode 100644 index b9f2b9e32d9..00000000000 --- a/components/bt/common/ble_log/src/internal_include/ble_log_lbm.h +++ /dev/null @@ -1,272 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2025-2026 Espressif Systems (Shanghai) CO LTD - * - * SPDX-License-Identifier: Apache-2.0 - */ -#ifndef __BLE_LOG_LBM_H__ -#define __BLE_LOG_LBM_H__ - -/* --------------------------------------- */ -/* BLE Log - Log Buffer Management */ -/* --------------------------------------- */ - -/* ---------------- */ -/* Includes */ -/* ---------------- */ -#include "ble_log.h" -#include "ble_log_prph.h" - -#include "freertos/FreeRTOS.h" - -#if defined(CONFIG_BLE_LOG_PRPH_UART_DMA) && (CONFIG_BLE_LOG_PRPH_UART_DMA_PORT == 0) -#define BLE_LOG_UART_REDIR_ENABLED (1) -#else -#define BLE_LOG_UART_REDIR_ENABLED (0) -#endif -#include "freertos/semphr.h" - -/* ------------------------- */ -/* Log Frame Defines */ -/* ------------------------- */ -typedef struct { - uint16_t length; - uint32_t frame_meta; -} __attribute__((packed)) ble_log_frame_head_t; - -#define BLE_LOG_FRAME_HEAD_LEN (sizeof(ble_log_frame_head_t)) -#define BLE_LOG_FRAME_TAIL_LEN (sizeof(uint32_t)) -#define BLE_LOG_FRAME_OVERHEAD (BLE_LOG_FRAME_HEAD_LEN + BLE_LOG_FRAME_TAIL_LEN) -#define BLE_LOG_MAKE_FRAME_META(src_code, sn) (((src_code) & 0xFF) | ((sn) << 8)) - -/* ---------------------------------- */ -/* Log Buffer Manager Defines */ -/* ---------------------------------- */ -typedef enum { - BLE_LOG_LBM_LOCK_NONE, - BLE_LOG_LBM_LOCK_SPIN, - BLE_LOG_LBM_LOCK_ATOMIC, - BLE_LOG_LBM_LOCK_MUTEX, -} ble_log_lbm_lock_t; - -typedef struct { - int trans_idx; - ble_log_prph_trans_t *trans[BLE_LOG_TRANS_BUF_CNT]; - ble_log_lbm_lock_t lock_type; - union { - /* BLE_LOG_LBM_LOCK_NONE */ - void *none; - /* BLE_LOG_LBM_LOCK_SPIN */ - portMUX_TYPE spin_lock; - /* BLE_LOG_LBM_LOCK_ATOMIC */ - volatile bool atomic_lock; - /* BLE_LOG_LBM_LOCK_MUTEX */ - SemaphoreHandle_t mutex; - }; - uint32_t trans_inflight; - uint32_t trans_inflight_peak; -} ble_log_lbm_t; - -/* --------------------------------------- */ -/* Log Buffer Manager Pool Defines */ -/* --------------------------------------- */ -enum { -#if CONFIG_BLE_LOG_LL_ENABLED - BLE_LOG_LBM_LL_TASK, - BLE_LOG_LBM_LL_HCI, -#endif /* CONFIG_BLE_LOG_LL_ENABLED */ - BLE_LOG_LBM_LL_MAX, -}; - -enum { - BLE_LOG_LBM_SPIN_TASK = 0, - BLE_LOG_LBM_SPIN_ISR, - BLE_LOG_LBM_SPIN_MAX, -}; - -#define BLE_LOG_LBM_ATOMIC_TASK_CNT CONFIG_BLE_LOG_LBM_ATOMIC_LOCK_TASK_CNT -#define BLE_LOG_LBM_ATOMIC_ISR_CNT CONFIG_BLE_LOG_LBM_ATOMIC_LOCK_ISR_CNT -#define BLE_LOG_LBM_ATOMIC_CNT (BLE_LOG_LBM_ATOMIC_TASK_CNT +\ - BLE_LOG_LBM_ATOMIC_ISR_CNT) -#define BLE_LOG_LBM_COMMON_CNT (BLE_LOG_LBM_ATOMIC_CNT + BLE_LOG_LBM_SPIN_MAX) -#define BLE_LOG_LBM_CNT (BLE_LOG_LBM_COMMON_CNT + BLE_LOG_LBM_LL_MAX) - -/* Derived per-buffer size from user-configured total-per-LBM budget */ -#define BLE_LOG_TRANS_SIZE (CONFIG_BLE_LOG_LBM_TRANS_BUF_SIZE / BLE_LOG_TRANS_BUF_CNT) -#define BLE_LOG_TRANS_LL_SIZE (CONFIG_BLE_LOG_LBM_LL_TRANS_BUF_SIZE / BLE_LOG_TRANS_BUF_CNT) - -/* Unified queue depth derivation */ -#define BLE_LOG_TRANS_POOL_CNT (BLE_LOG_LBM_CNT * BLE_LOG_TRANS_BUF_CNT) -#if BLE_LOG_UART_REDIR_ENABLED -#define BLE_LOG_TRANS_REDIR_CNT BLE_LOG_TRANS_BUF_CNT -#else -#define BLE_LOG_TRANS_REDIR_CNT (0) -#endif -#define BLE_LOG_TRANS_TOTAL_CNT (BLE_LOG_TRANS_POOL_CNT + BLE_LOG_TRANS_REDIR_CNT) - -/* ------------------------------------------ */ -/* Log Buffer Manager Context Defines */ -/* ------------------------------------------ */ -typedef struct { - union { - ble_log_lbm_t lbm_pool[BLE_LOG_LBM_CNT]; - struct { - union { - ble_log_lbm_t lbm_common_pool[BLE_LOG_LBM_COMMON_CNT]; - struct { - union { - ble_log_lbm_t spin_pool[BLE_LOG_LBM_SPIN_MAX]; - struct { - ble_log_lbm_t spin_task; - ble_log_lbm_t spin_isr; - }; - }; - union { - ble_log_lbm_t atomic_pool[BLE_LOG_LBM_ATOMIC_CNT]; - struct { - ble_log_lbm_t atomic_pool_task[BLE_LOG_LBM_ATOMIC_TASK_CNT]; - ble_log_lbm_t atomic_pool_isr[BLE_LOG_LBM_ATOMIC_ISR_CNT]; - }; - }; - }; - }; - union { - ble_log_lbm_t lbm_ll_pool[BLE_LOG_LBM_LL_MAX]; -#if CONFIG_BLE_LOG_LL_ENABLED - struct { - ble_log_lbm_t lbm_ll_task; - ble_log_lbm_t lbm_ll_hci; - }; -#endif /* CONFIG_BLE_LOG_LL_ENABLED */ - }; - }; - }; -} ble_log_lbm_ctx_t; - -/* -------------------------------------------- */ -/* Buffer Utilization Reporting Defines */ -/* -------------------------------------------- */ -typedef enum { - BLE_LOG_BUF_UTIL_POOL_COMMON_TASK = 0, - BLE_LOG_BUF_UTIL_POOL_COMMON_ISR = 1, - BLE_LOG_BUF_UTIL_POOL_LL = 2, - BLE_LOG_BUF_UTIL_POOL_REDIR = 3, -} ble_log_buf_util_pool_t; - -typedef struct { - uint8_t int_src_code; - uint8_t lbm_id; - uint8_t trans_cnt; - uint8_t inflight_peak; -} __attribute__((packed)) ble_log_buf_util_t; - -#define BLE_LOG_BUF_UTIL_MAKE_ID(pool, idx) (((pool) << 4) | ((idx) & 0x0F)) -#define BLE_LOG_BUF_UTIL_GET_POOL(id) (((id) >> 4) & 0x0F) -#define BLE_LOG_BUF_UTIL_GET_INDEX(id) ((id) & 0x0F) - -/* ---------------------------------------- */ -/* Enhanced Statistics Data Defines */ -/* ---------------------------------------- */ -typedef struct { - uint8_t int_src_code; - uint8_t src_code; - uint32_t written_frame_cnt; - uint32_t lost_frame_cnt; - uint32_t written_bytes_cnt; - uint32_t lost_bytes_cnt; -} __attribute__((packed)) ble_log_enh_stat_t; - -/* -------------------------------- */ -/* Final Statistics Defines */ -/* -------------------------------- */ -typedef struct { - uint8_t src_code; - uint32_t written_frame_cnt; - uint32_t lost_frame_cnt; - uint32_t written_bytes_cnt; - uint32_t lost_bytes_cnt; -} __attribute__((packed)) ble_log_final_stat_entry_t; - -typedef struct { - uint8_t int_src_code; - uint8_t src_cnt; - ble_log_final_stat_entry_t entries[BLE_LOG_SRC_MAX]; -} __attribute__((packed)) ble_log_final_stat_t; - -#define BLE_LOG_FINAL_STAT_LEN sizeof(ble_log_final_stat_t) - -/* -------------------------------------- */ -/* Log Statistics Manager Context */ -/* -------------------------------------- */ -typedef struct { - uint32_t frame_sn; - /* Aligned live counters — updated by ble_log_stat_mgr_update(), - * snapshot by ble_log_write_enh_stat(). Natural 4-byte alignment - * ensures each load/store compiles to a single l32i/s32i on - * Xtensa/RISC-V, so individual field access is atomic without locks. - * The packed ble_log_enh_stat_t wire format is built on the stack - * only when serializing to UART. */ - uint32_t written_frame_cnt; - uint32_t lost_frame_cnt; - uint32_t written_bytes_cnt; - uint32_t lost_bytes_cnt; -} ble_log_stat_mgr_t; - -#define BLE_LOG_GET_FRAME_SN(VAR) __atomic_fetch_add(VAR, 1, __ATOMIC_RELAXED) - -/* -------------------------- */ -/* Link Layer Defines */ -/* -------------------------- */ -#if CONFIG_BLE_LOG_LL_ENABLED -enum { - BLE_LOG_LL_FLAG_CONTINUE = 0, - BLE_LOG_LL_FLAG_END, - BLE_LOG_LL_FLAG_TASK, - BLE_LOG_LL_FLAG_ISR, - BLE_LOG_LL_FLAG_HCI, - BLE_LOG_LL_FLAG_RAW, - BLE_LOG_LL_FLAG_OMDATA, - BLE_LOG_LL_FLAG_HCI_UPSTREAM, -}; -#endif /* CONFIG_BLE_LOG_LL_ENABLED */ - -/* ------------------------------- */ -/* Compile-Time Guards */ -/* ------------------------------- */ -_Static_assert(CONFIG_BLE_LOG_LBM_TRANS_BUF_SIZE % BLE_LOG_TRANS_BUF_CNT == 0, - "Common LBM total buffer size must be a multiple of BLE_LOG_TRANS_BUF_CNT (4)"); -#if CONFIG_BLE_LOG_LL_ENABLED -_Static_assert(CONFIG_BLE_LOG_LBM_LL_TRANS_BUF_SIZE % BLE_LOG_TRANS_BUF_CNT == 0, - "LL LBM total buffer size must be a multiple of BLE_LOG_TRANS_BUF_CNT (4)"); -#endif -_Static_assert(CONFIG_BLE_LOG_LBM_TRANS_BUF_SIZE / BLE_LOG_TRANS_BUF_CNT >= BLE_LOG_FRAME_OVERHEAD, - "Common LBM per-buffer size too small for a single frame"); -_Static_assert(BLE_LOG_TRANS_SIZE >= BLE_LOG_FINAL_STAT_LEN + sizeof(uint32_t) + BLE_LOG_FRAME_OVERHEAD, - "Common LBM per-buffer size too small for final statistics frame"); -_Static_assert((BLE_LOG_TRANS_BUF_CNT & (BLE_LOG_TRANS_BUF_CNT - 1)) == 0, - "BLE_LOG_TRANS_BUF_CNT must be a power of 2"); -_Static_assert(1 + BLE_LOG_LBM_ATOMIC_TASK_CNT <= 16, - "Common task pool exceeds lbm_id 4-bit index limit (max 15)"); -_Static_assert(1 + BLE_LOG_LBM_ATOMIC_ISR_CNT <= 16, - "Common ISR pool exceeds lbm_id 4-bit index limit (max 15)"); -_Static_assert(BLE_LOG_TRANS_BUF_CNT <= 255, - "BLE_LOG_TRANS_BUF_CNT must fit in uint8_t for ble_log_buf_util_t"); - -/* --------------------------- */ -/* Internal Interfaces */ -/* --------------------------- */ -bool ble_log_lbm_init(void); -void ble_log_lbm_close(void); -void ble_log_lbm_deinit(void); -void ble_log_lbm_recycle_trans(ble_log_prph_trans_t *trans); -bool ble_log_write_internal(const uint8_t *addr, size_t len); -void ble_log_write_final_stat(void); -void ble_log_write_enh_stat(void); -void ble_log_write_buf_util(void); -#if BLE_LOG_UART_REDIR_ENABLED -void ble_log_lbm_stream_write(ble_log_lbm_t *lbm, ble_log_src_t src_code, - const uint8_t *data, size_t len); -void ble_log_lbm_stream_flush(ble_log_lbm_t *lbm, ble_log_src_t src_code); -ble_log_lbm_t *ble_log_prph_get_redir_lbm(void); -#endif - -#endif /* __BLE_LOG_LBM_H__ */ diff --git a/components/bt/common/ble_log/src/internal_include/ble_log_lbm_v2.h b/components/bt/common/ble_log/src/internal_include/ble_log_lbm_v2.h new file mode 100644 index 00000000000..fdace26460d --- /dev/null +++ b/components/bt/common/ble_log/src/internal_include/ble_log_lbm_v2.h @@ -0,0 +1,264 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ +#ifndef __BLE_LOG_LBM_V2_H__ +#define __BLE_LOG_LBM_V2_H__ + +/* -------------------------------------------------- */ +/* BLE Log - Unified Transport Pool (v8) */ +/* -------------------------------------------------- */ +/* Replaces the legacy multi-LBM design (ble_log_lbm.h) with one shared + * transport pool. Written as a new module so the legacy implementation + * stays untouched and reviewable as a pure addition. */ + +/* INCLUDE */ +#include "ble_log.h" +#include "ble_log_prph.h" + +#include "freertos/FreeRTOS.h" +#include "freertos/semphr.h" + +/* ------------------------- */ +/* Log Frame Defines */ +/* ------------------------- */ +typedef struct { + uint16_t length; + uint32_t frame_meta; +} __attribute__((packed)) ble_log_frame_head_t; + +#define BLE_LOG_FRAME_HEAD_LEN (sizeof(ble_log_frame_head_t)) +#define BLE_LOG_FRAME_TAIL_LEN (sizeof(uint32_t)) +#define BLE_LOG_FRAME_OVERHEAD (BLE_LOG_FRAME_HEAD_LEN + BLE_LOG_FRAME_TAIL_LEN) +#define BLE_LOG_MAKE_FRAME_META(src, sn) \ + (((src) & 0xffU) | (((sn) & 0x00ffffffU) << 8)) + +/* ------------------------------------- */ +/* Unified Buffer Pool Defines */ +/* ------------------------------------- */ +#define BLE_LOG_POOL_TRANS_CNT CONFIG_BLE_LOG_POOL_TRANS_CNT +#define BLE_LOG_POOL_NON_YIELD_RESERVE_CNT CONFIG_BLE_LOG_POOL_NON_YIELD_RESERVE_CNT +#define BLE_LOG_POOL_SHARED_CNT (BLE_LOG_POOL_TRANS_CNT - BLE_LOG_POOL_NON_YIELD_RESERVE_CNT) +#define BLE_LOG_POOL_TRANS_SIZE CONFIG_BLE_LOG_POOL_TRANS_SIZE +#define BLE_LOG_TRANS_SIZE BLE_LOG_POOL_TRANS_SIZE +#define BLE_LOG_MAX_PAYLOAD_LEN (BLE_LOG_POOL_TRANS_SIZE - BLE_LOG_FRAME_OVERHEAD) +#define BLE_LOG_TRANS_INTERNAL_CNT (1) +/* Dedicated transport of the task-id registry binding broadcast. */ +#define BLE_LOG_TRANS_TASK_BINDING_CNT (1) + +#if BLE_LOG_UART_REDIR_ENABLED +#define BLE_LOG_TRANS_REDIR_CNT BLE_LOG_TRANS_BUF_CNT +#else +#define BLE_LOG_TRANS_REDIR_CNT (0) +#endif + +#define BLE_LOG_TRANS_TOTAL_CNT \ + (BLE_LOG_POOL_TRANS_CNT + BLE_LOG_TRANS_INTERNAL_CNT + \ + BLE_LOG_TRANS_TASK_BINDING_CNT + BLE_LOG_TRANS_REDIR_CNT) + +/* --------------------------------------- */ +/* Protocol v8 Source ID Space */ +/* --------------------------------------- */ +/* The frozen public ble_log_src_t values are the on-wire and statistic + * source IDs: the frame source byte carries the bare enum value. */ + +/* Statistic slots in the Internal Snapshot: every public source that can + * produce core frames, i.e. CUSTOM through ENCODE. Snapshots share the + * Global SN but have no statistic slot; REDIR is a raw console stream. */ +#define BLE_LOG_SRC_CORE_FIRST BLE_LOG_SRC_CUSTOM +#define BLE_LOG_SRC_CORE_COUNT (BLE_LOG_SRC_ENCODE - BLE_LOG_SRC_CORE_FIRST + 1) + +/* ------------------------------------- */ +/* UART Redirection Manager */ +/* ------------------------------------- */ +#if BLE_LOG_UART_REDIR_ENABLED +typedef struct { + ble_log_prph_trans_t *trans[BLE_LOG_TRANS_BUF_CNT]; + SemaphoreHandle_t mutex; + int trans_idx; + /* The REDIR console stream keeps its own 24-bit frame sequence: it is + * not a log attempt, so it never consumes the Global SN; a gap counts + * a dropped console batch. Zeroed when the manager is created. */ + volatile uint32_t frame_sn; + volatile uint32_t inflight; + volatile uint32_t inflight_peak; +} ble_log_redir_t; +#endif + +/* -------------------------------- */ +/* Compact Core Statistics */ +/* -------------------------------- */ +typedef struct { + uint32_t written_frame_cnt; + uint32_t lost_frame_cnt; + /* Successfully committed logical frame bytes, including head and tail, + * excluding DMA padding. Not a physical-TX acknowledgement. */ + uint32_t written_bytes_cnt; +} __attribute__((packed)) ble_log_source_stat_t; + +/* Runtime per-source counters. The aligned wrapper keeps the embedded + * packed counters word-aligned, so atomic updates stay single-instruction + * (amo/s32c1i) on both toolchains. */ +typedef struct { + ble_log_source_stat_t counters; +} __attribute__((aligned(4))) ble_log_stat_mgr_t; + +#define BLE_LOG_GET_FRAME_SN(VAR) BLE_LOG_ATOMIC_ADD_RELAXED(VAR, 1) + +/* Core logs and INTERNAL snapshots share one 24-bit Global SN. Core logs + * consume it after entry validation and gate acceptance, before contention; + * failed log attempts leave gaps accounted for by lost_frame_cnt. Snapshots + * only consume it after acquiring their dedicated transport. Busy/skipped + * snapshots are not in core loss statistics and advance only the separate + * 24-bit anchor_count, never the Global SN. + * Task-binding broadcasts and REDIR retain independent header sequences. + * ble_log_init() resets every sequence; INIT starts a new receiver epoch. + * FLUSH resets interval statistics, but never the sequences or anchor count. + * Allocation order is not physical TX order; a snapshot is not a drain + * barrier. The wire SN is masked to 24 bits when frame_meta is packed. */ + +/* -------------------------------- */ +/* Internal Snapshot Frame */ +/* -------------------------------- */ +/* Clock sample captured at one instant by the runtime periodic tick. */ +typedef struct { + uint8_t int_src_code; + uint8_t io_level; + uint32_t lc_ts; + uint32_t esp_ts; + uint32_t os_ts; +} __attribute__((packed)) ble_log_ts_info_t; + +#define BLE_LOG_SNAPSHOT_REASON_INIT BIT(0) +#define BLE_LOG_SNAPSHOT_REASON_PERIODIC BIT(1) +#define BLE_LOG_SNAPSHOT_REASON_FLUSH BIT(2) +#define BLE_LOG_SNAPSHOT_REASON_TS_VALID BIT(3) + +typedef struct { + uint8_t int_src_code; + uint16_t reason_flags; + /* Snapshot attempts (INIT/PERIODIC/FLUSH), including skipped points. + * Little-endian low 24 bits; the runtime counter remains uint32_t. */ + uint8_t anchor_count[3]; + ble_log_version_info_t version_info; + + /* Clock samples captured at the same instant, mirroring + * ble_log_ts_info_t (minus its int_src_code). */ + struct { + uint8_t io_level; + uint32_t lc_ts; /* Link-layer clock; 0 when not available */ + uint32_t esp_ts; /* esp_timer_get_time() */ + uint32_t os_ts; /* FreeRTOS tick count in ms */ + } __attribute__((packed)) ts; + + /* Unified pool state at capture time. */ + struct { + uint8_t trans_cnt; + uint8_t non_yield_reserve_cnt; + uint8_t inflight; + uint8_t inflight_peak; + } __attribute__((packed)) pool; + + ble_log_source_stat_t stats[BLE_LOG_SRC_CORE_COUNT]; +} __attribute__((packed)) ble_log_internal_snapshot_t; + +#define BLE_LOG_INTERNAL_FRAME_LEN \ + (BLE_LOG_FRAME_OVERHEAD + sizeof(uint32_t) + sizeof(ble_log_internal_snapshot_t)) +#define BLE_LOG_INTERNAL_TRANS_SIZE \ + ((BLE_LOG_INTERNAL_FRAME_LEN + 3U) & ~3U) + +/* -------------------------- */ +/* Link Layer Defines */ +/* -------------------------- */ +#if CONFIG_BLE_LOG_LL_ENABLED +/* Numeric positions are an ABI with prebuilt controller libraries. */ +enum { + BLE_LOG_LL_FLAG_CONTINUE = 0, + BLE_LOG_LL_FLAG_END = 1, + BLE_LOG_LL_FLAG_TASK = 2, + BLE_LOG_LL_FLAG_ISR = 3, + BLE_LOG_LL_FLAG_HCI = 4, + BLE_LOG_LL_FLAG_RAW = 5, + BLE_LOG_LL_FLAG_OMDATA = 6, + BLE_LOG_LL_FLAG_HCI_UPSTREAM = 7, +}; +#endif + +/* ------------------------------- */ +/* Compile-Time Guards */ +/* ------------------------------- */ +_Static_assert(sizeof(ble_log_frame_head_t) == 6, + "Unexpected BLE Log frame header size"); +_Static_assert(BLE_LOG_POOL_TRANS_CNT >= 2 && BLE_LOG_POOL_TRANS_CNT <= 32, + "BLE_LOG_POOL_TRANS_CNT must be within [2, 32]"); +_Static_assert(BLE_LOG_POOL_NON_YIELD_RESERVE_CNT > 0 && + BLE_LOG_POOL_NON_YIELD_RESERVE_CNT < BLE_LOG_POOL_TRANS_CNT, + "Non-yield reserve must leave at least one shared transport"); +_Static_assert(BLE_LOG_POOL_TRANS_SIZE >= BLE_LOG_FRAME_OVERHEAD + sizeof(uint32_t), + "BLE_LOG_POOL_TRANS_SIZE is too small for a timestamped frame"); +_Static_assert(BLE_LOG_POOL_TRANS_SIZE <= UINT16_MAX, + "BLE_LOG_POOL_TRANS_SIZE exceeds the transport size field"); +_Static_assert(BLE_LOG_POOL_TRANS_SIZE <= 10240, + "BLE_LOG_POOL_TRANS_SIZE exceeds the peripheral transfer limit"); +#if CONFIG_BLE_LOG_PRPH_SPI_MASTER_DMA || CONFIG_BLE_LOG_PRPH_SPI_MASTER_HD +_Static_assert((BLE_LOG_POOL_TRANS_SIZE & 3U) == 0, + "SPI BLE Log transport size must be four-byte aligned"); +#endif +_Static_assert(sizeof(ble_log_version_info_t) == 58, + "Unexpected BLE Log version information size"); +_Static_assert(sizeof(ble_log_source_stat_t) == 12, + "Unexpected BLE Log source statistic size"); +_Static_assert(offsetof(ble_log_stat_mgr_t, counters) == 0 && + sizeof(ble_log_stat_mgr_t) == 12, + "stat manager layout must keep the counters word-aligned"); +_Static_assert(sizeof(ble_log_internal_snapshot_t) == 165, + "Unexpected BLE Log Internal Snapshot size"); +_Static_assert(BLE_LOG_INTERNAL_FRAME_LEN == 179, + "Unexpected BLE Log Internal frame size"); +_Static_assert(BLE_LOG_INTERNAL_TRANS_SIZE >= BLE_LOG_INTERNAL_FRAME_LEN, + "Internal transport is too small"); +_Static_assert((BLE_LOG_TRANS_BUF_CNT & (BLE_LOG_TRANS_BUF_CNT - 1)) == 0, + "BLE_LOG_TRANS_BUF_CNT must be a power of two"); + +/* --------------------------- */ +/* Internal Interfaces */ +/* --------------------------- */ +bool ble_log_lbm_init(void); +void ble_log_lbm_begin_deinit(void); +void ble_log_lbm_deinit(void); +bool ble_log_lbm_is_enabled(void); +/* Best-effort system flush, gated by LBM lifetime rather than producers. */ +void ble_log_lbm_flush_open_trans(void); +/* Deinit drain: seal every OPEN transport and dispatch it to the runtime + * queue. Contract: called after ble_log_lbm_begin_deinit() (producer gate + * closed, writers drained) and before ble_log_rt_deinit(); every transport + * lock is then uncontended. The peripheral deinit wait completes the + * delivery of the dispatched buffers. */ +void ble_log_lbm_drain_open_trans(void); +void ble_log_lbm_recycle_trans(ble_log_prph_trans_t *trans); +/* System output: gated by the LBM lifetime, not ble_log_enable(). A false + * wait_for_transport makes a busy dedicated transport a lossy fast path. */ +bool ble_log_internal_snapshot(uint16_t reason_flags, + const ble_log_ts_info_t *ts_info, + bool wait_for_transport); + +/* The task-id registry (protocol v8 attribution) lives in its own module: + * ble_log_task_registry.c/h, like the UART redirection writer. */ + +/* Claim/commit: the public ble_log_src_t is accepted for API stability, but + * only BLE_LOG_SRC_ENCODE is supported; its frames are stamped with the + * ENCODE source ID on the wire. wait_for_transport=false turns a busy pool + * into a lossy fast path (NULL, as in a non-yieldable context) instead of + * waiting. true requests backpressure in ordinary yieldable tasks; the + * shared ESP Timer task and non-yieldable contexts never wait, regardless + * of the requested policy. */ +uint8_t *ble_log_claim(ble_log_src_t src_code, size_t max_len, + uint32_t *handle, bool wait_for_transport); +void ble_log_commit(uint32_t handle, size_t actual_len); + +#if BLE_LOG_UART_REDIR_ENABLED +ble_log_redir_t *ble_log_prph_get_redir_lbm(void); +#endif + +#endif /* __BLE_LOG_LBM_V2_H__ */ diff --git a/components/bt/common/ble_log/src/internal_include/ble_log_prph.h b/components/bt/common/ble_log/src/internal_include/ble_log_prph.h index e9986c6efb7..3a4eee27b02 100644 --- a/components/bt/common/ble_log/src/internal_include/ble_log_prph.h +++ b/components/bt/common/ble_log/src/internal_include/ble_log_prph.h @@ -10,32 +10,68 @@ /* BLE Log - Peripheral Interface */ /* ------------------------------ */ -/* INCLUDE */ #include "ble_log_util.h" -/* TYPEDEF */ +#if defined(CONFIG_BLE_LOG_PRPH_UART_DMA) && (CONFIG_BLE_LOG_PRPH_UART_DMA_PORT == 0) +#define BLE_LOG_UART_REDIR_ENABLED (1) +#else +#define BLE_LOG_UART_REDIR_ENABLED (0) +#endif + +typedef enum { + BLE_LOG_TRANS_STATE_FREE = 0, + BLE_LOG_TRANS_STATE_OPEN, + BLE_LOG_TRANS_STATE_CLAIMED, + BLE_LOG_TRANS_STATE_SENDING, +} ble_log_trans_state_t; + +typedef enum { + BLE_LOG_TRANS_OWNER_POOL = 0, + BLE_LOG_TRANS_OWNER_INTERNAL, + BLE_LOG_TRANS_OWNER_REDIR, +} ble_log_trans_owner_t; + +#define BLE_LOG_TRANS_ID_NONE (0xff) + typedef struct { - volatile uint32_t prph_owned; + /* Transport lifecycle ownership, shared with the runtime dispatch and + * the peripheral tx-done recycle. Claim/commit bookkeeping deliberately + * lives in the pool (ble_log_lbm_v2.c), not here. */ + volatile ble_log_atomic_lock_t atomic_lock; + volatile uint8_t state; + uint8_t id; + uint8_t owner_kind; + + /* Lazy flush marker, pool-owned: set by the periodic flusher (without + * holding atomic_lock) when the transport was busy at flush time; the + * next claim that takes the lock seals the buffered frames first. + * Cleared by seal_and_send and on recycle, but a delayed flusher may + * store true afterwards. Such a stale hint permits an extra partial + * seal in the next lifecycle, never sending a writer-owned buffer. */ + volatile uint8_t pending_seal; + uint8_t *buf; uint16_t size; uint16_t pos; - /* Peripheral implementation specific context */ + /* Peripheral implementation specific context. */ void *ctx; - - /* Opaque back-reference to owning LBM, set once at init */ - void *owner; } ble_log_prph_trans_t; #define BLE_LOG_TRANS_FREE_SPACE(trans) ((trans)->size - (trans)->pos) #define BLE_LOG_TRANS_BUF_CNT (4) -/* INTERFACE */ bool ble_log_prph_init(size_t trans_cnt); void ble_log_prph_deinit(void); +/* Allocates a transport whose storage is zeroed (every field except size + * starts at its zero value: state FREE, owner POOL, lock and pending_seal + * clear). Callers only need to set the non-zero identity fields. */ bool ble_log_prph_trans_init(ble_log_prph_trans_t **trans, size_t trans_size); void ble_log_prph_trans_deinit(ble_log_prph_trans_t **trans); void ble_log_prph_send_trans(ble_log_prph_trans_t *trans); +#if BLE_LOG_UART_REDIR_ENABLED +bool ble_log_prph_flush(void); void ble_log_prph_reset_util_counters(void); +#endif #endif /* __BLE_LOG_PRPH_H__ */ diff --git a/components/bt/common/ble_log/src/internal_include/ble_log_redir.h b/components/bt/common/ble_log/src/internal_include/ble_log_redir.h new file mode 100644 index 00000000000..94437db6e61 --- /dev/null +++ b/components/bt/common/ble_log/src/internal_include/ble_log_redir.h @@ -0,0 +1,21 @@ +/* + * SPDX-FileCopyrightText: 2025-2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ +#ifndef __BLE_LOG_REDIR_H__ +#define __BLE_LOG_REDIR_H__ + +/* ---------------------------------------------- */ +/* BLE Log - UART Redirection Stream API */ +/* ---------------------------------------------- */ + +#include "ble_log_lbm_v2.h" + +#if BLE_LOG_UART_REDIR_ENABLED +void ble_log_lbm_stream_write(ble_log_redir_t *redir, ble_log_src_t src_code, + const uint8_t *data, size_t len); +void ble_log_lbm_stream_flush(ble_log_redir_t *redir, ble_log_src_t src_code); +#endif /* BLE_LOG_UART_REDIR_ENABLED */ + +#endif /* __BLE_LOG_REDIR_H__ */ diff --git a/components/bt/common/ble_log/src/internal_include/ble_log_rt.h b/components/bt/common/ble_log/src/internal_include/ble_log_rt.h index 28d1018ce55..86347fd05b4 100644 --- a/components/bt/common/ble_log/src/internal_include/ble_log_rt.h +++ b/components/bt/common/ble_log/src/internal_include/ble_log_rt.h @@ -12,7 +12,7 @@ /* INCLUDE */ #include "ble_log_prph.h" -#include "ble_log_ts.h" +#include "ble_log_lbm_v2.h" #include "freertos/FreeRTOS.h" #include "freertos/task.h" @@ -24,8 +24,16 @@ /* INTERFACE */ bool ble_log_rt_init(void); +/* Starts the always-on periodic path after the epoch INIT frame is queued. */ +bool ble_log_rt_start_periodic(void); void ble_log_rt_deinit(void); bool ble_log_rt_drain(void); void ble_log_rt_submit_trans(ble_log_prph_trans_t *trans); +/* Samples the link-layer, ESP and OS clocks at one instant. toggle_io allows + * the periodic path to toggle the sync IO when runtime IO toggling is enabled; + * one-shot snapshots (INIT/FLUSH) always pass false and leave the IO edge + * sequence untouched. io_level always reports the physical IO level. */ +void ble_log_rt_ts_sample(ble_log_ts_info_t *info, bool toggle_io); + #endif /* __BLE_LOG_RT_H__ */ diff --git a/components/bt/common/ble_log/src/internal_include/ble_log_task_registry.h b/components/bt/common/ble_log/src/internal_include/ble_log_task_registry.h new file mode 100644 index 00000000000..8c08f1612bd --- /dev/null +++ b/components/bt/common/ble_log/src/internal_include/ble_log_task_registry.h @@ -0,0 +1,87 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ +#ifndef __BLE_LOG_TASK_REGISTRY_H__ +#define __BLE_LOG_TASK_REGISTRY_H__ + +/* ------------------------------------ */ +/* BLE Log - Task-id Registry */ +/* ------------------------------------ */ +/* Self-contained module (one .c/.h pair, like the UART redirection + * writer): owns the name-to-id registry, its broadcast sequence and its + * dedicated transport, so it never contends with the internal snapshot + * transport or the pool. */ + +/* INCLUDE */ +#include "ble_log_lbm_v2.h" + +/* ------------------------- */ +/* Wire Format (v8) */ +/* ------------------------- */ +/* Writer attribution: every ENCODE record names the task that formatted + * it (one byte after the record source); the registry maps task names to + * stable ids. The name width is a wire property of the binding record + * below. */ +#define BLE_LOG_TASK_ID_UNKNOWN UINT8_C(0xff) +#define BLE_LOG_TASK_NAME_LEN (16U) + +/* The binding record inside an INTERNAL frame: every periodic window + * broadcasts one frame that packs one record per registered entry, in id + * order. The name is NUL-padded to the registry width plus one byte, so + * a decoder can bind the id in place even when the task name fills the + * whole field. */ +typedef struct { + uint8_t int_src_code; + uint8_t task_id; + uint8_t task_name[BLE_LOG_TASK_NAME_LEN + 1]; +} __attribute__((packed)) ble_log_task_binding_t; + +/* One binding frame must fit the dedicated transport in a single frame: + * one submit per window, and the transport is recycled only when the + * peripheral has read it. */ +#define BLE_LOG_TASK_BINDING_FRAME_LEN \ + (BLE_LOG_FRAME_OVERHEAD + sizeof(uint32_t) + \ + CONFIG_BLE_LOG_TASK_ID_MAX * sizeof(ble_log_task_binding_t)) +/* Word-aligned by construction (SPI builds require aligned transfers). */ +#define BLE_LOG_TASK_BINDING_TRANS_SIZE \ + ((BLE_LOG_TASK_BINDING_FRAME_LEN + 3U) & ~3U) + +_Static_assert(sizeof(ble_log_task_binding_t) == 19, + "Unexpected task-binding record layout"); + +/* --------------------------- */ +/* Internal Interfaces */ +/* --------------------------- */ +/* Module lifetime, driven by the LBM layer (ble_log_lbm_init/deinit): + * init allocates the dedicated transport and wipes the registry and its + * sequence for a fresh epoch; begin_deinit closes the gate and waits for + * in-flight publishes; deinit frees the transport. */ +bool ble_log_task_registry_init(void); +void ble_log_task_registry_begin_deinit(void); +void ble_log_task_registry_deinit(void); + +/* Resolves the calling task's name to its stable registry id, registering + * the name on a miss. ISR-safe: an ISR caller reads only (a miss returns + * the unknown id). Every ENCODE record must resolve before its claim so + * the registry already holds the id the record carries. */ +uint8_t ble_log_task_id_current(void); + +/* Periodic window output: broadcasts one INTERNAL frame packing one + * fixed-layout binding record per registered entry, in id order, on the + * dedicated registry transport. Runs on the shared ESP timer task and + * must never block: a busy transport (previous broadcast still in DMA) + * skips this window and the next one rebroadcasts. Best-effort control + * traffic, never counted in the per-source written/lost stats, and + * carried on its own sequence (see ble_log_task_registry.c) so a skipped + * window cannot look like a lost snapshot. */ +void ble_log_task_bindings_publish(void); + +#if CONFIG_BLE_LOG_PRPH_TEST +/* Test-only: wipes the task registry so test cases stay order-independent. + * Call between cases, with no writers in flight. */ +void ble_log_test_task_registry_reset(void); +#endif + +#endif /* __BLE_LOG_TASK_REGISTRY_H__ */ diff --git a/components/bt/common/ble_log/src/internal_include/ble_log_ts.h b/components/bt/common/ble_log/src/internal_include/ble_log_ts.h deleted file mode 100644 index 6a09cd70f1b..00000000000 --- a/components/bt/common/ble_log/src/internal_include/ble_log_ts.h +++ /dev/null @@ -1,58 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2025 Espressif Systems (Shanghai) CO LTD - * - * SPDX-License-Identifier: Apache-2.0 - */ -#ifndef __BLE_LOG_TS_H__ -#define __BLE_LOG_TS_H__ - -/* ----------------------------------- */ -/* BLE Log - Timestamp Synchronization */ -/* ----------------------------------- */ - -/* INCLUDE */ -#include "ble_log_util.h" - -#include "freertos/FreeRTOS.h" -#include "freertos/task.h" -#include "esp_timer.h" -#include "driver/gpio.h" - -/* MACRO */ -#if CONFIG_BLE_LOG_LL_ENABLED -/* ESP BLE Controller Gen 2 */ -#if defined(CONFIG_IDF_TARGET_ESP32H2) || defined(CONFIG_IDF_TARGET_ESP32C6) || defined(CONFIG_IDF_TARGET_ESP32C5) ||\ - defined(CONFIG_IDF_TARGET_ESP32C61) || defined(CONFIG_IDF_TARGET_ESP32H21) || defined(CONFIG_IDF_TARGET_ESP32H4) -extern uint32_t r_ble_lll_timer_current_tick_get(void); -#define BLE_LOG_GET_LC_TS r_ble_lll_timer_current_tick_get() -/* ESP BLE Controller Gen 1 */ -#elif defined(CONFIG_IDF_TARGET_ESP32C2) -extern uint32_t r_os_cputime_get32(void); -#define BLE_LOG_GET_LC_TS r_os_cputime_get32() -/* Legacy BLE Controller */ -#elif defined(CONFIG_IDF_TARGET_ESP32C3) || defined(CONFIG_IDF_TARGET_ESP32S3) -extern uint32_t lld_read_clock_us(void); -#define BLE_LOG_GET_LC_TS lld_read_clock_us() -#else /* Other targets */ -#define BLE_LOG_GET_LC_TS 0 -#endif /* BLE targets */ -#else /* !CONFIG_BLE_LOG_LL_ENABLED */ -#define BLE_LOG_GET_LC_TS 0 -#endif /* CONFIG_BLE_LOG_LL_ENABLED */ - -/* TYPEDEF */ -typedef struct { - uint8_t int_src_code; - uint8_t io_level; - uint32_t lc_ts; - uint32_t esp_ts; - uint32_t os_ts; -} __attribute__((packed)) ble_log_ts_info_t; - -/* INTERFACE */ -bool ble_log_ts_init(void); -void ble_log_ts_deinit(void); -void ble_log_ts_info_update(ble_log_ts_info_t **ts_info); -void ble_log_ts_reset(bool status); - -#endif /* __BLE_LOG_TS_H__ */ diff --git a/components/bt/common/ble_log/src/internal_include/ble_log_util.h b/components/bt/common/ble_log/src/internal_include/ble_log_util.h index 1ce2dbb719a..5e9b2eeca67 100644 --- a/components/bt/common/ble_log/src/internal_include/ble_log_util.h +++ b/components/bt/common/ble_log/src/internal_include/ble_log_util.h @@ -27,9 +27,11 @@ #define BLE_LOG_ATOMIC_LOAD_RELAXED(VAR) __atomic_load_n(&(VAR), __ATOMIC_RELAXED) #define BLE_LOG_ATOMIC_STORE_RELEASE(VAR, VALUE) __atomic_store_n(&(VAR), (VALUE), __ATOMIC_RELEASE) #define BLE_LOG_ATOMIC_STORE_RELAXED(VAR, VALUE) __atomic_store_n(&(VAR), (VALUE), __ATOMIC_RELAXED) +#define BLE_LOG_ATOMIC_ADD_RELAXED(VAR, VALUE) __atomic_fetch_add(&(VAR), (VALUE), __ATOMIC_RELAXED) + +typedef uint32_t ble_log_atomic_lock_t; /* Reference counting macros */ -#define BLE_LOG_REF_COUNT_ACQUIRE(VAR) __atomic_fetch_add(VAR, 1, __ATOMIC_ACQUIRE) #define BLE_LOG_REF_COUNT_RELEASE(VAR) __atomic_fetch_sub(VAR, 1, __ATOMIC_RELEASE) /* Closing gate: pairs an inited-flag store with a reference-count load (and * vice versa) at seq_cst so deinit and a submitter cannot both observe the @@ -76,22 +78,14 @@ extern void esp_panic_handler_feed_wdts(void); #define BLE_LOG_FEED_WDT() esp_panic_handler_feed_wdts() /* INLINE */ -BLE_LOG_IRAM_ATTR static inline -bool ble_log_cas_acquire(volatile bool *cas_lock) -{ - bool expected = false; - return __atomic_compare_exchange_n( - cas_lock, &expected, true, false, __ATOMIC_ACQUIRE, __ATOMIC_RELAXED - ); -} +/* Compare-and-swap lock as macros: single-instruction acquire/release used + * from several IRAM sites; a function would add an IRAM call site each. */ +#define BLE_LOG_CAS_ACQUIRE(cas_lock) \ + (__atomic_exchange_n((cas_lock), 1, __ATOMIC_ACQUIRE) == 0) +#define BLE_LOG_CAS_RELEASE(cas_lock) \ + __atomic_store_n((cas_lock), 0, __ATOMIC_RELEASE) -BLE_LOG_IRAM_ATTR static inline -void ble_log_cas_release(volatile bool *cas_lock) -{ - __atomic_store_n(cas_lock, false, __ATOMIC_RELEASE); -} - -#define BLE_LOG_VERSION (6) +#define BLE_LOG_VERSION (8) #define BLE_LOG_IDF_COMMIT_LEN (12) /* Lib commit hashes are at most 10 hex chars; zero-padded when shorter */ #define BLE_LOG_LIB_COMMIT_LEN (10) @@ -106,6 +100,10 @@ typedef enum { BLE_LOG_INT_SRC_BUF_UTIL, BLE_LOG_INT_SRC_FINAL_STAT, BLE_LOG_INT_SRC_VERSION_INFO, + BLE_LOG_INT_SRC_SNAPSHOT, + /* protocol v8: periodic task-id binding broadcast; its frames carry a + * sequence of their own (a gap counts a skipped broadcast window). */ + BLE_LOG_INT_SRC_TASK_BINDING, BLE_LOG_INT_SRC_MAX, } ble_log_int_src_t; @@ -131,9 +129,14 @@ uint32_t ble_log_fast_checksum(const uint8_t *data, size_t len); /* Acquire a lifetime reference only while the closing gate remains open. */ bool ble_log_ref_count_try_acquire(volatile uint32_t *ref_count, - const uint32_t *inited); + const uint32_t *gate); /* Task-context wait; returns false if the count stays above max for one second. */ bool ble_log_ref_count_wait(volatile uint32_t *ref_count, uint32_t max_ref_count); +/* Monotonic-peak publish: stores value into *peak only when it exceeds the + * current peak. Relaxed atomics are enough - a lost race can only leave the + * recorded peak below a transient maximum, and peaks are diagnostics. */ +void ble_log_atomic_update_peak(volatile uint32_t *peak, uint32_t value); + #endif /* __BLE_LOG_UTIL_H__ */ diff --git a/components/bt/common/ble_log/src/prph/ble_log_prph_dummy.c b/components/bt/common/ble_log/src/prph/ble_log_prph_dummy.c index e00c3b0f1fe..0042581cc49 100644 --- a/components/bt/common/ble_log/src/prph/ble_log_prph_dummy.c +++ b/components/bt/common/ble_log/src/prph/ble_log_prph_dummy.c @@ -9,7 +9,7 @@ /* INCLUDE */ #include "ble_log_prph_dummy.h" -#include "ble_log_lbm.h" +#include "ble_log_lbm_v2.h" /* INTERFACE */ bool ble_log_prph_init(size_t trans_cnt) @@ -82,11 +82,10 @@ void ble_log_prph_trans_deinit(ble_log_prph_trans_t **trans) } /* Dummy transport has no DMA/hardware -- recycle the buffer immediately - * so that ble_log_lbm_get_trans() can reuse it and ble_log_flush() does - * not hang waiting for prph_owned to clear. Real peripherals (UART DMA, + * so that the pool can reuse it and ble_log_flush() does not hang waiting + * for it to drain. Real peripherals (UART DMA, * SPI DMA) do the same work inside their asynchronous tx_done callbacks. */ void ble_log_prph_send_trans(ble_log_prph_trans_t *trans) { - trans->pos = 0; ble_log_lbm_recycle_trans(trans); } diff --git a/components/bt/common/ble_log/src/prph/ble_log_prph_spi_master_dma.c b/components/bt/common/ble_log/src/prph/ble_log_prph_spi_master_dma.c index 7014a58892b..eab253a8b2f 100644 --- a/components/bt/common/ble_log/src/prph/ble_log_prph_spi_master_dma.c +++ b/components/bt/common/ble_log/src/prph/ble_log_prph_spi_master_dma.c @@ -10,7 +10,7 @@ /* INCLUDE */ #include "ble_log_prph_spi_master_dma.h" #include "ble_log_prph_spi_common.h" -#include "ble_log_lbm.h" +#include "ble_log_lbm_v2.h" #include "esp_timer.h" @@ -26,6 +26,7 @@ /* VARIABLE */ BLE_LOG_STATIC bool prph_inited = false; +BLE_LOG_STATIC bool bus_inited = false; BLE_LOG_STATIC spi_device_handle_t dev_handle = NULL; BLE_LOG_STATIC BLE_LOG_DRAM_ATTR uint32_t last_tx_done_ts = 0; @@ -37,16 +38,16 @@ BLE_LOG_STATIC void spi_master_dma_pre_tx_cb(spi_transaction_t *spi_trans); BLE_LOG_SPI_MASTER_DMA_CB_ATTR BLE_LOG_STATIC void spi_master_dma_tx_done_cb(spi_transaction_t *spi_trans) { /* SPI slave performance issue workaround */ - last_tx_done_ts = esp_timer_get_time(); + last_tx_done_ts = (uint32_t)esp_timer_get_time(); /* Recycle transport */ ble_log_prph_trans_t *trans = (ble_log_prph_trans_t *)(spi_trans->user); - trans->pos = 0; ble_log_lbm_recycle_trans(trans); } BLE_LOG_SPI_MASTER_DMA_CB_ATTR BLE_LOG_STATIC void spi_master_dma_pre_tx_cb(spi_transaction_t *spi_trans) { + (void)spi_trans; /* SPI slave performance issue workaround */ while ((esp_timer_get_time() - last_tx_done_ts) < BLE_LOG_SPI_TRANS_ITVL_MIN_US) {} } @@ -74,6 +75,7 @@ bool ble_log_prph_init(size_t trans_cnt) if (spi_bus_initialize(BLE_LOG_SPI_BUS, &bus_config, SPI_DMA_CH_AUTO) != ESP_OK) { goto exit; } + bus_inited = true; spi_device_interface_config_t dev_config = { .clock_speed_hz = SPI_MASTER_FREQ_20M, @@ -108,8 +110,10 @@ void ble_log_prph_deinit(void) dev_handle = NULL; } - /* Note: We don't care if the bus has been inited or not */ - spi_bus_free(BLE_LOG_SPI_BUS); + if (bus_inited) { + spi_bus_free(BLE_LOG_SPI_BUS); + bus_inited = false; + } } bool ble_log_prph_trans_init(ble_log_prph_trans_t **trans, size_t trans_size) diff --git a/components/bt/common/ble_log/src/prph/ble_log_prph_spi_master_hd.c b/components/bt/common/ble_log/src/prph/ble_log_prph_spi_master_hd.c index 04730fe0f22..662be1ec579 100644 --- a/components/bt/common/ble_log/src/prph/ble_log_prph_spi_master_hd.c +++ b/components/bt/common/ble_log/src/prph/ble_log_prph_spi_master_hd.c @@ -10,7 +10,7 @@ /* INCLUDE */ #include "ble_log_prph_spi_master_hd.h" #include "ble_log_prph_spi_common.h" -#include "ble_log_lbm.h" +#include "ble_log_lbm_v2.h" #include "hal/spi_ll.h" #include "hal/spi_types.h" @@ -44,7 +44,6 @@ BLE_LOG_SPI_MASTER_HD_CB_ATTR BLE_LOG_STATIC void spi_master_hd_tx_done_cb(spi_t } } - ctx->trans->pos = 0; ble_log_lbm_recycle_trans(ctx->trans); } @@ -219,9 +218,7 @@ BLE_LOG_IRAM_ATTR void ble_log_prph_send_trans(ble_log_prph_trans_t *trans) if (spi_device_queue_trans(dev_handle, &ctx->end, 0) != ESP_OK) { uint8_t old_status = __atomic_fetch_or(&ctx->status, BLE_LOG_SPI_HD_END_QUEUE_FAILED, __ATOMIC_ACQ_REL); if (old_status & BLE_LOG_SPI_HD_DATA_DONE) { - /* Data already on the wire: drop it from the buffer so the next - * flush does not re-send these bytes (recycle keeps pos on purpose) */ - trans->pos = 0; + /* Data is already on the wire; recycle it without re-sending. */ ble_log_lbm_recycle_trans(trans); } } diff --git a/components/bt/common/ble_log/src/prph/ble_log_prph_test.c b/components/bt/common/ble_log/src/prph/ble_log_prph_test.c index 28f6982901c..9dc034c8bbd 100644 --- a/components/bt/common/ble_log/src/prph/ble_log_prph_test.c +++ b/components/bt/common/ble_log/src/prph/ble_log_prph_test.c @@ -9,7 +9,7 @@ /* INCLUDE */ #include "ble_log_prph_test.h" -#include "ble_log_lbm.h" +#include "ble_log_lbm_v2.h" #include "esp_timer.h" #include "freertos/queue.h" #include "freertos/semphr.h" @@ -155,7 +155,6 @@ void ble_log_prph_send_trans(ble_log_prph_trans_t *trans) uint32_t idx = __atomic_load_n(&s_auto_recycle_idx, __ATOMIC_ACQUIRE); ble_log_prph_test_hook_reg_t reg = s_auto_recycle_slots[idx & 1]; if (reg.hook) { - trans->pos = 0; ble_log_lbm_recycle_trans(trans); reg.hook(reg.ctx); __atomic_fetch_sub(&s_auto_recycle_busy, 1, __ATOMIC_RELEASE); @@ -164,7 +163,6 @@ void ble_log_prph_send_trans(ble_log_prph_trans_t *trans) __atomic_fetch_sub(&s_auto_recycle_busy, 1, __ATOMIC_RELEASE); if (xQueueSend(s_pending_trans, &trans, 0) != pdTRUE) { - trans->pos = 0; ble_log_lbm_recycle_trans(trans); BLE_LOG_ASSERT(false); } @@ -217,7 +215,6 @@ size_t ble_log_prph_test_read(uint8_t *data, size_t len, TickType_t timeout, if (delay_us > 0 && (esp_timer_start_once(s_tx_timer, (uint64_t)delay_us) != ESP_OK || xSemaphoreTake(s_tx_done, portMAX_DELAY) != pdTRUE)) { - trans->pos = 0; ble_log_lbm_recycle_trans(trans); BLE_LOG_ASSERT(false); return 0; @@ -233,7 +230,6 @@ size_t ble_log_prph_test_read(uint8_t *data, size_t len, TickType_t timeout, } size_t copied = len < trans->pos ? len : trans->pos; BLE_LOG_MEMCPY(data, trans->buf, copied); - trans->pos = 0; ble_log_lbm_recycle_trans(trans); if (uxQueueMessagesWaiting(s_pending_trans) == 0) { s_tx_deadline_us = 0; diff --git a/components/bt/common/ble_log/src/prph/ble_log_prph_uart_dma.c b/components/bt/common/ble_log/src/prph/ble_log_prph_uart_dma.c index 8af7851c3bd..854e499aedb 100644 --- a/components/bt/common/ble_log/src/prph/ble_log_prph_uart_dma.c +++ b/components/bt/common/ble_log/src/prph/ble_log_prph_uart_dma.c @@ -11,7 +11,9 @@ /* INCLUDE */ #include "ble_log_prph_uart_dma.h" #include "ble_log.h" -#include "ble_log_lbm.h" +#include "ble_log_lbm_v2.h" +#include "ble_log_redir.h" +#include "ble_log_rt.h" #if BLE_LOG_PRPH_UART_DMA_REDIR @@ -24,10 +26,11 @@ /* MACRO */ #define BLE_LOG_UART_MAX_TRANSFER_SIZE (10240) #define BLE_LOG_UART_RX_BUF_SIZE (256) -/* ponytail: data burst disabled — UHCI enforces burst-size alignment (addr+len) on +/* ponytail: data burst disabled - UHCI enforces burst-size alignment (addr+len) on * uhci_transmit() once GDMA weighted arbitration is enabled, and UART log bandwidth * is baud-rate limited anyway, so burst buys nothing here */ #define BLE_LOG_UART_DMA_BURST_SIZE (1) +#define BLE_LOG_UART_FLUSH_TIMEOUT_TICKS pdMS_TO_TICKS(1000) #if BLE_LOG_PRPH_UART_DMA_REDIR #define BLE_LOG_UART_REDIR_BUF_SIZE (512) #define BLE_LOG_UART_REDIR_FLUSH_PERIOD_US (1000 * 1000) @@ -38,8 +41,9 @@ BLE_LOG_STATIC BLE_LOG_DRAM_ATTR bool prph_inited = false; BLE_LOG_STATIC uhci_controller_handle_t dev_handle = NULL; #if BLE_LOG_PRPH_UART_DMA_REDIR BLE_LOG_STATIC bool uart_driver_inited = false; -BLE_LOG_STATIC ble_log_lbm_t *redir_lbm = NULL; +BLE_LOG_STATIC ble_log_redir_t *redir_lbm = NULL; BLE_LOG_STATIC esp_timer_handle_t redir_flush_timer = NULL; +BLE_LOG_STATIC volatile uint32_t redir_writer_count = 0; #endif /* BLE_LOG_PRPH_UART_DMA_REDIR */ /* PRIVATE FUNCTION DECLARATION */ @@ -56,20 +60,20 @@ BLE_LOG_IRAM_ATTR BLE_LOG_STATIC bool uart_dma_tx_done_cb( /* Recycle transport */ ble_log_prph_trans_ctx_t *uart_trans_ctx = (ble_log_prph_trans_ctx_t *)( - (uint8_t *)edata->buffer - sizeof(ble_log_prph_trans_ctx_t) - ); + (uint8_t *)edata->buffer - sizeof(ble_log_prph_trans_ctx_t) + ); ble_log_prph_trans_t *trans = uart_trans_ctx->trans; - trans->pos = 0; ble_log_lbm_recycle_trans(trans); return true; } #if BLE_LOG_PRPH_UART_DMA_REDIR -BLE_LOG_IRAM_ATTR BLE_LOG_STATIC void esp_timer_cb_flush_log(void *arg) +BLE_LOG_STATIC void esp_timer_cb_flush_log(void *arg) { (void)arg; - if (!prph_inited) { + /* Producer disable stops new writes, not periodic draining of old data. */ + if (!BLE_LOG_ATOMIC_LOAD_ACQUIRE(prph_inited)) { return; } @@ -87,7 +91,7 @@ BLE_LOG_IRAM_ATTR BLE_LOG_STATIC void esp_timer_cb_flush_log(void *arg) bool ble_log_prph_init(size_t trans_cnt) { /* Avoid double init */ - if (prph_inited) { + if (BLE_LOG_ATOMIC_LOAD_ACQUIRE(prph_inited)) { return true; } @@ -99,8 +103,8 @@ bool ble_log_prph_init(size_t trans_cnt) .stop_bits = UART_STOP_BITS_1, }; if ((uart_param_config(CONFIG_BLE_LOG_PRPH_UART_DMA_PORT, &uart_config) != ESP_OK) || - (uart_set_pin(CONFIG_BLE_LOG_PRPH_UART_DMA_PORT, - CONFIG_BLE_LOG_PRPH_UART_DMA_TX_IO_NUM, -1, -1, -1) != ESP_OK)) { + (uart_set_pin(CONFIG_BLE_LOG_PRPH_UART_DMA_PORT, + CONFIG_BLE_LOG_PRPH_UART_DMA_TX_IO_NUM, -1, -1, -1) != ESP_OK)) { goto exit; } @@ -117,19 +121,18 @@ bool ble_log_prph_init(size_t trans_cnt) .on_tx_trans_done = uart_dma_tx_done_cb, }; if ((uhci_new_controller(&uhci_config, &dev_handle) != ESP_OK) || - (uhci_register_event_callbacks(dev_handle, &uhci_cbs, NULL) != ESP_OK)) { + (uhci_register_event_callbacks(dev_handle, &uhci_cbs, NULL) != ESP_OK)) { goto exit; } -/* Redirection is required when utilizing UART port 0 */ + /* Redirection is required when utilizing UART port 0 */ #if BLE_LOG_PRPH_UART_DMA_REDIR - /* Initialize a dedicated LBM for redirection */ - redir_lbm = (ble_log_lbm_t *)BLE_LOG_MALLOC(sizeof(ble_log_lbm_t)); + /* Initialize a dedicated redirection manager (separate from the pool) */ + redir_lbm = (ble_log_redir_t *)BLE_LOG_MALLOC(sizeof(ble_log_redir_t)); if (!redir_lbm) { goto exit; } - BLE_LOG_MEMSET(redir_lbm, 0, sizeof(ble_log_lbm_t)); - redir_lbm->lock_type = BLE_LOG_LBM_LOCK_MUTEX; + BLE_LOG_MEMSET(redir_lbm, 0, sizeof(ble_log_redir_t)); /* Transport initialization */ for (int i = 0; i < BLE_LOG_TRANS_BUF_CNT; i++) { @@ -137,7 +140,9 @@ bool ble_log_prph_init(size_t trans_cnt) BLE_LOG_UART_REDIR_BUF_SIZE)) { goto exit; } - redir_lbm->trans[i]->owner = (void *)redir_lbm; + /* Redirection transports are not part of the global pool. */ + redir_lbm->trans[i]->id = BLE_LOG_TRANS_ID_NONE; + redir_lbm->trans[i]->owner_kind = BLE_LOG_TRANS_OWNER_REDIR; } /* Mutex initialization */ @@ -146,11 +151,13 @@ bool ble_log_prph_init(size_t trans_cnt) goto exit; } - /* Initialize UART driver for redirection */ + /* Initialize UART driver for redirection. */ if (!uart_is_driver_installed(UART_NUM_0)) { - if (uart_driver_install(UART_NUM_0, BLE_LOG_UART_RX_BUF_SIZE, 0, 0, NULL, 0) == ESP_OK) { - uart_driver_inited = true; + if (uart_driver_install(UART_NUM_0, BLE_LOG_UART_RX_BUF_SIZE, + 0, 0, NULL, 0) != ESP_OK) { + goto exit; } + uart_driver_inited = true; } uart_vfs_dev_use_driver(UART_NUM_0); @@ -158,15 +165,19 @@ bool ble_log_prph_init(size_t trans_cnt) esp_timer_create_args_t timer_args = { .callback = esp_timer_cb_flush_log, .dispatch_method = ESP_TIMER_TASK, + .skip_unhandled_events = true, }; if (esp_timer_create(&timer_args, &redir_flush_timer) != ESP_OK) { goto exit; } #endif /* BLE_LOG_PRPH_UART_DMA_REDIR */ - prph_inited = true; + BLE_LOG_ATOMIC_STORE_RELEASE(prph_inited, true); #if BLE_LOG_PRPH_UART_DMA_REDIR - esp_timer_start_periodic(redir_flush_timer, BLE_LOG_UART_REDIR_FLUSH_PERIOD_US); + if (esp_timer_start_periodic(redir_flush_timer, + BLE_LOG_UART_REDIR_FLUSH_PERIOD_US) != ESP_OK) { + goto exit; + } #endif /* BLE_LOG_PRPH_UART_DMA_REDIR */ return true; @@ -178,27 +189,45 @@ exit: void ble_log_prph_deinit(void) { - prph_inited = false; + __atomic_store_n(&prph_inited, false, __ATOMIC_SEQ_CST); #if BLE_LOG_PRPH_UART_DMA_REDIR /* Release flush timer */ if (redir_flush_timer) { - esp_timer_stop(redir_flush_timer); + esp_timer_stop_blocking(redir_flush_timer, portMAX_DELAY); esp_timer_delete(redir_flush_timer); redir_flush_timer = NULL; } - /* Delete UART driver if it's installed by current module */ - if (uart_driver_inited) { - uart_driver_delete(UART_NUM_0); + while (__atomic_load_n(&redir_writer_count, __ATOMIC_SEQ_CST) > 0) { + vTaskDelay(1); } - /* Release redirection LBM */ + /* Flush redirection buffers before waiting for all submitted DMA. */ if (redir_lbm) { if (redir_lbm->mutex) { xSemaphoreTake(redir_lbm->mutex, portMAX_DELAY); ble_log_lbm_stream_flush(redir_lbm, BLE_LOG_SRC_REDIR); xSemaphoreGive(redir_lbm->mutex); + } + } +#endif /* BLE_LOG_PRPH_UART_DMA_REDIR */ + + if (dev_handle) { + uhci_wait_all_tx_transaction_done(dev_handle, portMAX_DELAY); + } + +#if BLE_LOG_PRPH_UART_DMA_REDIR + /* Restore the VFS before deleting a driver installed by this module. */ + if (uart_driver_inited) { + uart_vfs_dev_use_nonblocking(UART_NUM_0); + uart_driver_delete(UART_NUM_0); + uart_driver_inited = false; + } + + /* Release redirection LBM only after DMA callbacks have completed. */ + if (redir_lbm) { + if (redir_lbm->mutex) { vSemaphoreDelete(redir_lbm->mutex); } @@ -214,7 +243,6 @@ void ble_log_prph_deinit(void) #endif /* BLE_LOG_PRPH_UART_DMA_REDIR */ if (dev_handle) { - uhci_wait_all_tx_transaction_done(dev_handle, portMAX_DELAY); uhci_del_controller(dev_handle); dev_handle = NULL; } @@ -277,67 +305,105 @@ void ble_log_prph_trans_deinit(ble_log_prph_trans_t **trans) BLE_LOG_IRAM_ATTR void ble_log_prph_send_trans(ble_log_prph_trans_t *trans) { if (uhci_transmit(dev_handle, trans->buf, trans->pos) != ESP_OK) { + /* The UHCI queue depth matches the transport count and each transport + * occupies at most one slot, so a full queue cannot occur here: this + * is a driver fault. The assert compiles out with NDEBUG; the recycle + * below still covers that case (no tx_done will fire on failure). */ + BLE_LOG_ASSERT(false); ble_log_lbm_recycle_trans(trans); } } /* Redirection is required when utilizing UART port 0 */ #if BLE_LOG_PRPH_UART_DMA_REDIR -BLE_LOG_IRAM_ATTR BLE_LOG_STATIC -void ble_log_redir_uart_tx_chars(const char *src, size_t len) +BLE_LOG_STATIC +bool ble_log_redir_uart_tx_chars(const char *src, size_t len) { + __atomic_add_fetch(&redir_writer_count, 1, __ATOMIC_SEQ_CST); + if (!__atomic_load_n(&prph_inited, __ATOMIC_SEQ_CST) || + !ble_log_lbm_is_enabled()) { + __atomic_sub_fetch(&redir_writer_count, 1, __ATOMIC_SEQ_CST); + return false; + } + if (BLE_LOG_IN_ISR() || xTaskGetSchedulerState() == taskSCHEDULER_SUSPENDED) { - return; + __atomic_sub_fetch(&redir_writer_count, 1, __ATOMIC_SEQ_CST); + return true; } xSemaphoreTake(redir_lbm->mutex, portMAX_DELAY); ble_log_lbm_stream_write(redir_lbm, BLE_LOG_SRC_REDIR, - (const uint8_t *)src, len); + (const uint8_t *)src, len); xSemaphoreGive(redir_lbm->mutex); + __atomic_sub_fetch(&redir_writer_count, 1, __ATOMIC_SEQ_CST); + return true; } int __real_uart_tx_chars(uart_port_t uart_num, const char *buffer, uint32_t len); int __wrap_uart_tx_chars(uart_port_t uart_num, const char *buffer, uint32_t len) { - if (!prph_inited || (uart_num != UART_NUM_0)) { + if ((uart_num != UART_NUM_0) || + !ble_log_redir_uart_tx_chars(buffer, len)) { return __real_uart_tx_chars(uart_num, buffer, len); } - ble_log_redir_uart_tx_chars(buffer, len); return len; } int __real_uart_write_bytes(uart_port_t uart_num, const void *src, size_t size); int __wrap_uart_write_bytes(uart_port_t uart_num, const void *src, size_t size) { - if (!prph_inited || (uart_num != UART_NUM_0)) { + if ((uart_num != UART_NUM_0) || + !ble_log_redir_uart_tx_chars(src, size)) { return __real_uart_write_bytes(uart_num, src, size); } - ble_log_redir_uart_tx_chars(src, size); return size; } int __real_uart_write_bytes_with_break(uart_port_t uart_num, const void *src, size_t size, int brk_len); int __wrap_uart_write_bytes_with_break(uart_port_t uart_num, const void *src, size_t size, int brk_len) { - if (!prph_inited || (uart_num != UART_NUM_0)) { + if ((uart_num != UART_NUM_0) || + !ble_log_redir_uart_tx_chars(src, size)) { return __real_uart_write_bytes_with_break(uart_num, src, size, brk_len); - } else { - (void)brk_len; - return __wrap_uart_write_bytes(uart_num, src, size); } + return size; } -ble_log_lbm_t *ble_log_prph_get_redir_lbm(void) +BLE_LOG_IRAM_ATTR ble_log_redir_t *ble_log_prph_get_redir_lbm(void) { return redir_lbm; } #endif /* BLE_LOG_PRPH_UART_DMA_REDIR */ +#if BLE_LOG_PRPH_UART_DMA_REDIR +bool ble_log_prph_flush(void) +{ + while (__atomic_load_n(&redir_writer_count, __ATOMIC_SEQ_CST) > 0) { + vTaskDelay(1); + } + if (!redir_lbm) { + return true; + } + + xSemaphoreTake(redir_lbm->mutex, portMAX_DELAY); + ble_log_lbm_stream_flush(redir_lbm, BLE_LOG_SRC_REDIR); + xSemaphoreGive(redir_lbm->mutex); + (void)ble_log_rt_drain(); + + TickType_t start_tick = xTaskGetTickCount(); + while (BLE_LOG_ATOMIC_LOAD_ACQUIRE(redir_lbm->inflight) > 0) { + if ((xTaskGetTickCount() - start_tick) >= BLE_LOG_UART_FLUSH_TIMEOUT_TICKS) { + return false; + } + vTaskDelay(1); + } + return true; +} + void ble_log_prph_reset_util_counters(void) { -#if BLE_LOG_PRPH_UART_DMA_REDIR if (redir_lbm) { - __atomic_store_n(&redir_lbm->trans_inflight, 0, __ATOMIC_RELAXED); - __atomic_store_n(&redir_lbm->trans_inflight_peak, 0, __ATOMIC_RELAXED); + uint32_t inflight = BLE_LOG_ATOMIC_LOAD_RELAXED(redir_lbm->inflight); + BLE_LOG_ATOMIC_STORE_RELAXED(redir_lbm->inflight_peak, inflight); } -#endif } +#endif /* BLE_LOG_PRPH_UART_DMA_REDIR */ diff --git a/components/bt/common/ble_log/test_apps/ble_log_perf_test/README.md b/components/bt/common/ble_log/test_apps/ble_log_perf_test/README.md index de7c10477f3..dc505520509 100644 --- a/components/bt/common/ble_log/test_apps/ble_log_perf_test/README.md +++ b/components/bt/common/ble_log/test_apps/ble_log_perf_test/README.md @@ -28,9 +28,12 @@ Runtime dispatch behavior and latency are covered by the sibling - `throughput`: fixed 32B / 64B / 128B / mixed 8-64B payload profiles, each at 2 Mbps, 20 Mbps, and unlimited link. Runs 3 write_hex writers + LL task + LL HCI + compressed writer + 1 kHz ISR writer concurrently. -- `write_hex cycles`: single writer, no link cap, payload 8/32/64/128 B. -- `write_hex drop path cycles`: saturated 2 Mbps link, measures the cost of a - failed (dropped) write. +- `write_hex cycles`: single writer, no link cap, payload 8/32/64/128 B. The + scheduler remains active during each measured call, followed by an unmeasured + one-tick pacing delay so the no-loss profile does not become a saturation test. +- `write_hex drop path cycles`: saturated 2 Mbps link, measures the backpressure + cost of a parked write (wait and wake on transport recycle) without the + no-loss pacing delay. - `write_hex_ll cycles`: payload 8/32/64/128 B; plus a 32+32 B append case. - `compressed write cycles`: workload matrix of the compressed entry points — U32 args (0/1/2/mixed), U64 values (full 8B / leading-zero LZ / zero), diff --git a/components/bt/common/ble_log/test_apps/ble_log_perf_test/main/test_ble_log_main.c b/components/bt/common/ble_log/test_apps/ble_log_perf_test/main/test_ble_log_main.c index 3437fed856c..0e874caf815 100644 --- a/components/bt/common/ble_log/test_apps/ble_log_perf_test/main/test_ble_log_main.c +++ b/components/bt/common/ble_log/test_apps/ble_log_perf_test/main/test_ble_log_main.c @@ -10,7 +10,7 @@ #include "unity_test_runner.h" #include "ble_log.h" -#include "ble_log_lbm.h" +#include "ble_log_lbm_v2.h" #include "test_ble_log_main.h" bool test_ble_log_walk_frames(const uint8_t *data, size_t len, @@ -36,7 +36,7 @@ bool test_ble_log_walk_frames(const uint8_t *data, size_t len, if (observer) { test_ble_log_frame_t frame = { - .src = head.frame_meta & 0xff, + .src = (ble_log_src_t)(head.frame_meta & 0xff), .sn = head.frame_meta >> 8, .payload = data + offset + BLE_LOG_FRAME_HEAD_LEN, .payload_len = head.length, diff --git a/components/bt/common/ble_log/test_apps/ble_log_perf_test/main/test_ble_log_perf.c b/components/bt/common/ble_log/test_apps/ble_log_perf_test/main/test_ble_log_perf.c index 202c3f8e9a4..33f7fb8af86 100644 --- a/components/bt/common/ble_log/test_apps/ble_log_perf_test/main/test_ble_log_perf.c +++ b/components/bt/common/ble_log/test_apps/ble_log_perf_test/main/test_ble_log_perf.c @@ -12,7 +12,7 @@ #include #include "ble_log.h" -#include "ble_log_lbm.h" +#include "ble_log_lbm_v2.h" #include "ble_log_prph_test.h" #include "log_compression/utils.h" #include "esp_cpu.h" @@ -125,7 +125,7 @@ typedef struct { uint32_t ll_flag; /* UINT32_MAX: use ble_log_write_hex */ bool compressed; /* call ble_log_compressed_hex_print instead */ bool record_cycles; - bool isolate_write; /* vTaskSuspendAll around the timed write */ + bool pace_after_write; /* one tick outside a no-loss timed call */ const uint8_t *ll_append; /* write_hex_ll append part */ size_t ll_append_len; const perf_compress_cfg_t *compress_cfg; @@ -174,7 +174,6 @@ typedef struct { bool compress; bool isr; bool measure_cycles; - bool isolate_write; bool expect_no_loss; const uint8_t *ll_append; size_t ll_append_len; @@ -316,8 +315,7 @@ static void print_run_separator(const char *tag, const perf_run_cfg_t *cfg) tag, (unsigned)cfg->run_idx, (unsigned)cfg->run_total, cfg->mode_name, cfg->profile_name); print_link(cfg->bytes_per_second); - printf(" isolate=%s ============\n", - cfg->isolate_write ? "sched" : "none"); + printf(" isolate=none ============\n"); } /* ---------------- */ @@ -349,9 +347,6 @@ static void perf_writer_task(void *arg) size_t payload_len = w->profile[frame_index % w->profile_count]; memcpy(payload, &frame_index, sizeof(frame_index)); - if (w->isolate_write) { - vTaskSuspendAll(); - } uint32_t start_cycles = w->record_cycles ? esp_cpu_get_cycle_count() : 0; bool ok; @@ -427,9 +422,6 @@ static void perf_writer_task(void *arg) if (w->record_cycles) { write_cycles = esp_cpu_get_cycle_count() - start_cycles; } - if (w->isolate_write) { - xTaskResumeAll(); - } if (ok) { w->cycles->ok_frames++; @@ -443,7 +435,14 @@ static void perf_writer_task(void *arg) } } frame_index++; - taskYIELD(); + if (w->pace_after_write) { + /* Cycle profiles measure one production-semantics call, then + * yield enough time for deferred dispatch and sink recycling. + * Throughput and explicit drop profiles remain unpaced. */ + vTaskDelay(1); + } else { + taskYIELD(); + } } xSemaphoreGive(w->done); @@ -485,21 +484,26 @@ static void observe_perf_frame(const test_ble_log_frame_t *frame, void *ctx) sink->frames_by_src[frame->src]++; } - /* Capture enhanced statistics records (written/lost per source). - * Frames carry a 4-byte os_timestamp prefix before the record. */ if (frame->src == BLE_LOG_SRC_INTERNAL && - frame->payload_len == sizeof(uint32_t) + sizeof(ble_log_enh_stat_t) && - frame->payload[sizeof(uint32_t)] == BLE_LOG_INT_SRC_ENH_STAT) { - ble_log_enh_stat_t stat; - memcpy(&stat, frame->payload + sizeof(uint32_t), sizeof(stat)); - if (stat.src_code < BLE_LOG_SRC_MAX) { - sink->stat_written[stat.src_code] = stat.written_frame_cnt; - sink->stat_lost[stat.src_code] = stat.lost_frame_cnt; + frame->payload_len == sizeof(uint32_t) + + sizeof(ble_log_internal_snapshot_t)) { + ble_log_internal_snapshot_t snapshot; + memcpy(&snapshot, frame->payload + sizeof(uint32_t), sizeof(snapshot)); + if (snapshot.int_src_code != BLE_LOG_INT_SRC_SNAPSHOT) { + return; + } + /* Latch the final counters only from the FLUSH snapshot: periodic + * snapshots keep arriving after it and carry interval counters + * that the flush reset; letting them through would overwrite the + * final result the run report and assertions rely on. */ + if (snapshot.reason_flags & BLE_LOG_SNAPSHOT_REASON_FLUSH) { + for (int i = 0; i < BLE_LOG_SRC_CORE_COUNT; i++) { + int src = BLE_LOG_SRC_CORE_FIRST + i; + sink->stat_written[src] = snapshot.stats[i].written_frame_cnt; + sink->stat_lost[src] = snapshot.stats[i].lost_frame_cnt; + } + sink->stat_final_seen = true; } - } else if (frame->src == BLE_LOG_SRC_INTERNAL && - frame->payload_len == sizeof(uint32_t) + sizeof(ble_log_final_stat_t) && - frame->payload[sizeof(uint32_t)] == BLE_LOG_INT_SRC_FINAL_STAT) { - sink->stat_final_seen = true; } } @@ -541,7 +545,7 @@ static void fill_writer(perf_writer_t *w, const char *name, ble_log_src_t src, w->ll_flag = ll_flag; w->compressed = compressed; w->record_cycles = cfg->measure_cycles; - w->isolate_write = cfg->isolate_write; + w->pace_after_write = cfg->measure_cycles && cfg->expect_no_loss; w->ll_append = cfg->ll_append; w->ll_append_len = cfg->ll_append_len; w->compress_cfg = cfg->compress_cfg; @@ -651,8 +655,8 @@ static void run_perf_case(const perf_run_cfg_t *cfg) } if (ll_hci) { - fill_writer(&writers[writer_count], "write_hex_ll_hci", BLE_LOG_SRC_LL_HCI, - BIT(BLE_LOG_LL_FLAG_HCI), false, cfg, &ll_hci_cycles, start); + fill_writer(&writers[writer_count], "write_hex_hci", BLE_LOG_SRC_HCI, + UINT32_MAX, false, cfg, &ll_hci_cycles, start); TEST_ASSERT_EQUAL(pdPASS, xTaskCreatePinnedToCore(perf_writer_task, "perf_hci", PERF_WRITER_STACK_SIZE, @@ -744,6 +748,8 @@ static void run_perf_case(const perf_run_cfg_t *cfg) } sink.stop = true; TEST_ASSERT_EQUAL(pdTRUE, xSemaphoreTake(sink.done, pdMS_TO_TICKS(1000))); + TEST_ASSERT_TRUE_MESSAGE(sink.stat_final_seen, + "Final statistics snapshot was not received"); uint8_t discard[BLE_LOG_TRANS_SIZE]; while (ble_log_prph_test_read(discard, sizeof(discard), 0, 0, NULL)) { @@ -753,12 +759,12 @@ static void run_perf_case(const perf_run_cfg_t *cfg) printf("BLE_LOG_PERF mode=%s profile=%s payload=%uB ", cfg->mode_name, cfg->profile_name, cfg->profile[0]); print_link(cfg->bytes_per_second); - printf(" isolate=%s duration=%" PRIu64 " ms\n", - cfg->isolate_write ? "sched" : "none", elapsed_us / 1000); + printf(" isolate=none duration=%" PRIu64 " ms\n", elapsed_us / 1000); printf("BLE_LOG_PERF flush=%" PRIu32 " cycles\n", flush_cycles); uint64_t ok_total = 0; uint64_t failed_total = 0; + uint64_t custom_failed_total = 0; for (i = 0; i < cfg->custom_writer_count; i++) { char name[32]; snprintf(name, sizeof(name), "write_hex%" PRIu32, i); @@ -769,6 +775,7 @@ static void run_perf_case(const perf_run_cfg_t *cfg) } ok_total += custom_cycles[i].ok_frames; failed_total += custom_cycles[i].failed_frames; + custom_failed_total += custom_cycles[i].failed_frames; } #if CONFIG_BLE_LOG_LL_ENABLED if (ll_task) { @@ -782,9 +789,9 @@ static void run_perf_case(const perf_run_cfg_t *cfg) } if (ll_hci) { if (cfg->measure_cycles) { - print_cycles("write_hex_ll_hci", &ll_hci_cycles, true); + print_cycles("write_hex_hci", &ll_hci_cycles, false); } else { - print_writer_counts("write_hex_ll_hci", &ll_hci_cycles); + print_writer_counts("write_hex_hci", &ll_hci_cycles); } ok_total += ll_hci_cycles.ok_frames; failed_total += ll_hci_cycles.failed_frames; @@ -827,10 +834,12 @@ static void run_perf_case(const perf_run_cfg_t *cfg) } } #endif + uint64_t ll_frames = sink.frames_by_src[BLE_LOG_SRC_LL_TASK] + + sink.frames_by_src[BLE_LOG_SRC_LL_HCI] + + sink.frames_by_src[BLE_LOG_SRC_LL_ISR]; uint64_t sink_frames = sink.frames_by_src[BLE_LOG_SRC_CUSTOM] + - sink.frames_by_src[BLE_LOG_SRC_LL_TASK] + - sink.frames_by_src[BLE_LOG_SRC_LL_HCI] + - sink.frames_by_src[BLE_LOG_SRC_LL_ISR] + + ll_frames + + sink.frames_by_src[BLE_LOG_SRC_HCI] + sink.frames_by_src[BLE_LOG_SRC_ENCODE]; printf("BLE_LOG_PERF total ok=%" PRIu64 " failed=%" PRIu64 " sink_frames=%" PRIu64 " sink_bytes=%" PRIu64 @@ -840,19 +849,21 @@ static void run_perf_case(const perf_run_cfg_t *cfg) if (!cfg->measure_cycles) { print_throughput(sink.transport_bytes, elapsed_us, cfg->bytes_per_second); } - printf("BLE_LOG_PERF stat written=[custom=%" PRIu64 " ll_task=%" PRIu64 - " ll_hci=%" PRIu64 " ll_isr=%" PRIu64 " encode=%" PRIu64 - "] lost=[custom=%" PRIu64 " ll_task=%" PRIu64 " ll_hci=%" PRIu64 - " ll_isr=%" PRIu64 " encode=%" PRIu64 "]\n", + printf("BLE_LOG_PERF stat written=[custom=%" PRIu64 " ll=%" PRIu64 + " hci=%" PRIu64 " encode=%" PRIu64 + "] lost=[custom=%" PRIu64 " ll=%" PRIu64 + " hci=%" PRIu64 " encode=%" PRIu64 "]\n", sink.stat_written[BLE_LOG_SRC_CUSTOM], - sink.stat_written[BLE_LOG_SRC_LL_TASK], - sink.stat_written[BLE_LOG_SRC_LL_HCI], + sink.stat_written[BLE_LOG_SRC_LL_TASK] + + sink.stat_written[BLE_LOG_SRC_LL_HCI] + sink.stat_written[BLE_LOG_SRC_LL_ISR], + sink.stat_written[BLE_LOG_SRC_HCI], sink.stat_written[BLE_LOG_SRC_ENCODE], sink.stat_lost[BLE_LOG_SRC_CUSTOM], - sink.stat_lost[BLE_LOG_SRC_LL_TASK], - sink.stat_lost[BLE_LOG_SRC_LL_HCI], + sink.stat_lost[BLE_LOG_SRC_LL_TASK] + + sink.stat_lost[BLE_LOG_SRC_LL_HCI] + sink.stat_lost[BLE_LOG_SRC_LL_ISR], + sink.stat_lost[BLE_LOG_SRC_HCI], sink.stat_lost[BLE_LOG_SRC_ENCODE]); print_run_separator("END", cfg); @@ -862,13 +873,19 @@ static void run_perf_case(const perf_run_cfg_t *cfg) TEST_ASSERT_GREATER_THAN_UINT64(0, ok_total); if (cfg->expect_no_loss) { TEST_ASSERT_EQUAL_UINT64(0, failed_total); - TEST_ASSERT_EQUAL_UINT64(0, sink.stat_lost[BLE_LOG_SRC_CUSTOM]); - TEST_ASSERT_EQUAL_UINT64(0, sink.stat_lost[BLE_LOG_SRC_LL_TASK]); - TEST_ASSERT_EQUAL_UINT64(0, sink.stat_lost[BLE_LOG_SRC_LL_HCI]); - TEST_ASSERT_EQUAL_UINT64(0, sink.stat_lost[BLE_LOG_SRC_LL_ISR]); - TEST_ASSERT_EQUAL_UINT64(0, sink.stat_lost[BLE_LOG_SRC_ENCODE]); + for (int i = 0; i < BLE_LOG_SRC_CORE_COUNT; i++) { + TEST_ASSERT_EQUAL_UINT64(0, + sink.stat_lost[BLE_LOG_SRC_CORE_FIRST + i]); + } } else if (sink.stat_final_seen) { - TEST_ASSERT_EQUAL_UINT64(failed_total, sink.stat_lost[BLE_LOG_SRC_CUSTOM]); + TEST_ASSERT_EQUAL_UINT64(custom_failed_total, + sink.stat_lost[BLE_LOG_SRC_CUSTOM]); +#if CONFIG_BLE_LOG_LL_ENABLED + if (ll_hci) { + TEST_ASSERT_EQUAL_UINT64(ll_hci_cycles.failed_frames, + sink.stat_lost[BLE_LOG_SRC_HCI]); + } +#endif #if CONFIG_BLE_COMPRESSED_LOG_ENABLE if (compress) { /* Every hex_print call reaches write_hex(ENCODE) exactly once; @@ -934,7 +951,6 @@ static void run_cycle_case(const char *name, const uint16_t *profile, size_t cou .ll_hci = ll_hci, .compress = compress_cfg != NULL, .measure_cycles = true, - .isolate_write = true, .expect_no_loss = true, .compress_cfg = compress_cfg, }; @@ -968,8 +984,9 @@ TEST_CASE("BLE Log write_hex cycles (single writer, link=0)", "[ble_log][perf][c } } -/* Saturated link: most writes fail. Measures the drop-path cost and - * cross-checks client failed counts against the LBM's lost counters. */ +/* Saturated link: yieldable writers park on backpressure instead of dropping; + * measures the park/wake cost and cross-checks client failed counts against + * the LBM's lost counters. */ TEST_CASE("BLE Log write_hex drop path cycles (link=2Mbps)", "[ble_log][perf][cycle][ignore]") { perf_run_cfg_t cfg = { @@ -982,7 +999,6 @@ TEST_CASE("BLE Log write_hex drop path cycles (link=2Mbps)", "[ble_log][perf][cy .bytes_per_second = PERF_LINK_2MBPS_BPS, .custom_writer_count = 1, .measure_cycles = true, - .isolate_write = true, }; run_perf_case(&cfg); } @@ -1011,7 +1027,6 @@ TEST_CASE("BLE Log write_hex_ll append cycles (32+32B)", "[ble_log][perf][cycle] .custom_writer_count = 0, .ll_task = true, .measure_cycles = true, - .isolate_write = true, .expect_no_loss = true, .ll_append = append_buf, .ll_append_len = 32, @@ -1021,6 +1036,88 @@ TEST_CASE("BLE Log write_hex_ll append cycles (32+32B)", "[ble_log][perf][cycle] #endif #if CONFIG_BLE_COMPRESSED_LOG_ENABLE +/* ---------------------- */ +/* Attribution cycles */ +/* ---------------------- */ + +/* Compressed records attribute every record to their writer task by + * normalizing the FreeRTOS task name to NUL-padded words and scanning + * the registry. A hand-rolled copy (no libc calls) was measured on + * ESP32-H2 at -Og against this libc version: the two trade places + * across name lengths (inline wins ~31 cycles on 3-char names, loses + * ~31 on full 16-char names, ties in between) against ~1.6k cycles per + * compressed record - no variant is meaningfully faster. The module + * keeps the libc version because it is three lines; this case records + * the numbers so the decision does not get re-litigated blind. */ +#define NORM_WORDS 4 /* BLE_CP_TASK_NAME_WORDS, private to the module */ +#define NORM_LEN (NORM_WORDS * sizeof(uint32_t)) +#define NORM_ROUNDS 5 +#define NORM_ITERS 20000 + +typedef void (*norm_fn_t)(const char *name, uint32_t w[NORM_WORDS]); + +static void norm_libc(const char *name, uint32_t w[NORM_WORDS]) +{ + memset(w, 0, NORM_LEN); + memcpy(w, name, strnlen(name, NORM_LEN)); +} + +static void norm_inline(const char *name, uint32_t w[NORM_WORDS]) +{ + for (unsigned i = 0; i < NORM_WORDS; i++) { + uint32_t v = 0; + for (unsigned b = 0; b < sizeof(uint32_t); b++) { + uint8_t c = (uint8_t)*name; + if (c == '\0') { + break; + } + v |= (uint32_t)c << (8 * b); + name++; + } + w[i] = v; + } +} + +static volatile uint32_t s_norm_sink; + +static uint32_t norm_bench_once(norm_fn_t fn, const char *name) +{ + uint32_t sink = 0; + uint32_t start = esp_cpu_get_cycle_count(); + for (uint32_t i = 0; i < NORM_ITERS; i++) { + uint32_t w[NORM_WORDS]; + fn(name, w); + sink ^= w[0] ^ w[1] ^ w[2] ^ w[3]; + } + s_norm_sink = sink; /* the copies must stay live */ + return (esp_cpu_get_cycle_count() - start) / NORM_ITERS; +} + +TEST_CASE("BLE Log task-name normalization cycles", "[ble_log][perf][cycle][ignore]") +{ + static const char *const names[] = {"BTU", "NIMBLE_HOST", "0123456789ABCDEF"}; + for (size_t i = 0; i < sizeof(names) / sizeof(names[0]); i++) { + /* both variants must agree on every shape before timing them */ + uint32_t wa[NORM_WORDS], wb[NORM_WORDS]; + norm_libc(names[i], wa); + norm_inline(names[i], wb); + TEST_ASSERT_EQUAL_UINT32_ARRAY(wa, wb, NORM_WORDS); + + uint32_t best_libc = UINT32_MAX; + uint32_t best_inline = UINT32_MAX; + for (int r = 0; r < NORM_ROUNDS; r++) { /* interleaved rounds */ + uint32_t a = norm_bench_once(norm_libc, names[i]); + uint32_t b = norm_bench_once(norm_inline, names[i]); + best_libc = a < best_libc ? a : best_libc; + best_inline = b < best_inline ? b : best_inline; + } + printf("BLE_LOG_PERF norm name=%-16s libc=%4" PRIu32 " inline=%4" PRIu32 + " delta=%+" PRId32 " cycles/call\n", + names[i], best_libc, best_inline, + (int32_t)best_libc - (int32_t)best_inline); + } +} + /* Compressed records are log_index + 0..2 U32 args, not a raw payload * length. One case per arg count splits encode vs downstream write_hex * cost for each workload shape. */ diff --git a/components/bt/common/ble_log/test_apps/ble_log_rt_test/README.md b/components/bt/common/ble_log/test_apps/ble_log_rt_test/README.md index 75af9d61753..7f729f03b65 100644 --- a/components/bt/common/ble_log/test_apps/ble_log_rt_test/README.md +++ b/components/bt/common/ble_log/test_apps/ble_log_rt_test/README.md @@ -19,12 +19,12 @@ the hand-over time, so dispatch latency is measured without a real link. - `runtime` regressions: shared ESP Timer task fairness, periodic timestamp delivery without light-sleep wakeups, ISR-only submission delivery, consumer-independent receipt timing, extra-marker detection, exact 1 ms - first-submission defer scheduling, full task-pool snapshot dispatch, deinit + first-submission defer scheduling, full shared-pool snapshot dispatch, deinit racing submissions (SMP-pinned writer, failure-safe recovery), bounded inflight-peak statistics, and monotonic millisecond waits at both supported tick rates. -The first-deadline and full task-pool regressions require dispatch exclusion +The first-deadline and full shared-pool regressions require dispatch exclusion while enqueueing, so they run on single-core builds only (they are ignored on SMP targets). The deinit-race regression pins its writer to the other core on SMP. diff --git a/components/bt/common/ble_log/test_apps/ble_log_rt_test/main/test_ble_log_main.c b/components/bt/common/ble_log/test_apps/ble_log_rt_test/main/test_ble_log_main.c index bdb96ad8ae9..f8856c86936 100644 --- a/components/bt/common/ble_log/test_apps/ble_log_rt_test/main/test_ble_log_main.c +++ b/components/bt/common/ble_log/test_apps/ble_log_rt_test/main/test_ble_log_main.c @@ -10,7 +10,7 @@ #include "unity_test_runner.h" #include "ble_log.h" -#include "ble_log_lbm.h" +#include "ble_log_lbm_v2.h" #include "ble_log_prph_test.h" #include "test_ble_log_main.h" @@ -37,7 +37,7 @@ bool test_ble_log_walk_frames(const uint8_t *data, size_t len, if (observer) { test_ble_log_frame_t frame = { - .src = head.frame_meta & 0xff, + .src = (ble_log_src_t)(head.frame_meta & 0xff), .sn = head.frame_meta >> 8, .payload = data + offset + BLE_LOG_FRAME_HEAD_LEN, .payload_len = head.length, @@ -51,14 +51,15 @@ bool test_ble_log_walk_frames(const uint8_t *data, size_t len, void setUp(void) { + /* Preserve the external test-system contract: every test starts with the + * optional sync IO disabled and low. Periodic snapshots remain active. */ + (void)ble_log_sync_enable(false); } void tearDown(void) { ble_log_prph_test_set_auto_recycle_hook(NULL, NULL); -#if CONFIG_BLE_LOG_TS_ENABLED (void)ble_log_sync_enable(false); -#endif } void app_main(void) diff --git a/components/bt/common/ble_log/test_apps/ble_log_rt_test/main/test_ble_log_runtime.c b/components/bt/common/ble_log/test_apps/ble_log_rt_test/main/test_ble_log_runtime.c index 1f1791d1c53..87217c87276 100644 --- a/components/bt/common/ble_log/test_apps/ble_log_rt_test/main/test_ble_log_runtime.c +++ b/components/bt/common/ble_log/test_apps/ble_log_rt_test/main/test_ble_log_runtime.c @@ -12,7 +12,7 @@ #include #include "ble_log.h" -#include "ble_log_lbm.h" +#include "ble_log_lbm_v2.h" #include "ble_log_prph_test.h" #include "ble_log_rt.h" #include "esp_timer.h" @@ -24,7 +24,6 @@ #define RT_SAMPLE_COUNT (32) #define RT_BURST_SIZE (4) -#define RT_TASK_POOL_TRANS_COUNT ((BLE_LOG_LBM_ATOMIC_TASK_CNT + 1) * BLE_LOG_TRANS_BUF_CNT) #define RT_READ_TIMEOUT_MS (100) #define RT_QUIET_TIMEOUT_MS (10) #define RT_QUIET_DRAIN_DEADLINE_MS (2000) @@ -37,7 +36,7 @@ #define RT_CONSUMER_DELAY_MS (30) #define RT_RECEIVE_MAX_LATENCY_US (10000) #define RT_BURST_SPAN_MAX_US (500) -#define RT_PEAK_WRITES (8) +#define RT_PEAK_WRITES (BLE_LOG_POOL_SHARED_CNT) #define RT_DEINIT_ROUNDS (200) #define RT_JOIN_TIMEOUT_MS (5000) @@ -52,6 +51,13 @@ typedef struct { uint32_t ts_count; } rt_marker_observer_t; +#if CONFIG_BLE_LOG_TS_SYNC_TOGGLE_IO_ENABLED +typedef struct { + bool found; + uint8_t io_level; +} rt_snapshot_observer_t; +#endif + typedef struct { SemaphoreHandle_t done; uint32_t remaining; @@ -100,9 +106,14 @@ static void observe_runtime_marker(const test_ble_log_frame_t *frame, void *ctx) { rt_marker_observer_t *observer = ctx; if (frame->src == BLE_LOG_SRC_INTERNAL && - frame->payload_len > sizeof(uint32_t) && - frame->payload[sizeof(uint32_t)] == BLE_LOG_INT_SRC_TS) { - observer->ts_count++; + frame->payload_len == sizeof(uint32_t) + + sizeof(ble_log_internal_snapshot_t)) { + ble_log_internal_snapshot_t snapshot; + memcpy(&snapshot, frame->payload + sizeof(uint32_t), sizeof(snapshot)); + if (snapshot.int_src_code == BLE_LOG_INT_SRC_SNAPSHOT && + (snapshot.reason_flags & BLE_LOG_SNAPSHOT_REASON_TS_VALID)) { + observer->ts_count++; + } } if (frame->src != BLE_LOG_SRC_CUSTOM || @@ -118,6 +129,29 @@ static void observe_runtime_marker(const test_ble_log_frame_t *frame, void *ctx) } } +#if CONFIG_BLE_LOG_TS_SYNC_TOGGLE_IO_ENABLED +static void observe_periodic_snapshot(const test_ble_log_frame_t *frame, + void *ctx) +{ + rt_snapshot_observer_t *observer = ctx; + if (observer->found || frame->src != BLE_LOG_SRC_INTERNAL || + frame->payload_len != sizeof(uint32_t) + + sizeof(ble_log_internal_snapshot_t)) { + return; + } + + ble_log_internal_snapshot_t snapshot; + memcpy(&snapshot, frame->payload + sizeof(uint32_t), sizeof(snapshot)); + uint16_t expected = BLE_LOG_SNAPSHOT_REASON_PERIODIC | + BLE_LOG_SNAPSHOT_REASON_TS_VALID; + if (snapshot.int_src_code == BLE_LOG_INT_SRC_SNAPSHOT && + (snapshot.reason_flags & expected) == expected) { + observer->found = true; + observer->io_level = snapshot.ts.io_level; + } +} +#endif + static TickType_t runtime_timeout_ticks(uint32_t timeout_ms) { uint64_t ticks = ((uint64_t)timeout_ms * configTICK_RATE_HZ + 999) / 1000; @@ -155,11 +189,12 @@ static bool write_runtime_marker(uint32_t seq, int64_t *enqueued_at_us) return true; } -static bool read_runtime_marker(uint32_t *seq, uint32_t *ts_count, - int64_t *received_at_us) +static bool read_runtime_marker_with_timeout(uint32_t *seq, uint32_t *ts_count, + int64_t *received_at_us, + uint32_t timeout_ms) { const int64_t deadline_us = esp_timer_get_time() + - (int64_t)RT_READ_TIMEOUT_MS * 1000; + (int64_t)timeout_ms * 1000; uint32_t observed_ts = 0; while (true) { @@ -193,6 +228,13 @@ static bool read_runtime_marker(uint32_t *seq, uint32_t *ts_count, } } +static bool read_runtime_marker(uint32_t *seq, uint32_t *ts_count, + int64_t *received_at_us) +{ + return read_runtime_marker_with_timeout(seq, ts_count, received_at_us, + RT_READ_TIMEOUT_MS); +} + static bool runtime_stream_is_quiet(void) { const int64_t drain_deadline_us = esp_timer_get_time() + @@ -225,6 +267,35 @@ static bool runtime_stream_is_quiet(void) return false; } +#if CONFIG_BLE_LOG_TS_SYNC_TOGGLE_IO_ENABLED +static bool read_periodic_snapshot(uint8_t *io_level) +{ + const int64_t deadline_us = esp_timer_get_time() + + (int64_t)BLE_LOG_TS_TRIGGER_TIMEOUT_US + 1000000; + while (true) { + TickType_t remaining; + if (!runtime_deadline_ticks(deadline_us, &remaining)) { + return false; + } + size_t len = ble_log_prph_test_read(s_capture, sizeof(s_capture), + remaining, 0, NULL); + if (!len) { + continue; + } + + rt_snapshot_observer_t observer = {0}; + if (!test_ble_log_walk_frames(s_capture, len, + observe_periodic_snapshot, &observer)) { + return false; + } + if (observer.found) { + *io_level = observer.io_level; + return true; + } + } +} +#endif + static void refill_runtime_queue(void *arg) { rt_starvation_ctx_t *ctx = arg; @@ -275,18 +346,21 @@ static void observe_buf_util(const test_ble_log_frame_t *frame, void *ctx) { rt_peak_observer_t *observer = ctx; if (frame->src != BLE_LOG_SRC_INTERNAL || - frame->payload_len < sizeof(uint32_t) + sizeof(ble_log_buf_util_t) || - frame->payload[sizeof(uint32_t)] != BLE_LOG_INT_SRC_BUF_UTIL) { + frame->payload_len != sizeof(uint32_t) + + sizeof(ble_log_internal_snapshot_t)) { return; } - ble_log_buf_util_t util; - memcpy(&util, frame->payload + sizeof(uint32_t), sizeof(util)); - observer->buf_util_frames++; - if (util.inflight_peak > observer->max_inflight_peak) { - observer->max_inflight_peak = util.inflight_peak; + ble_log_internal_snapshot_t snapshot; + memcpy(&snapshot, frame->payload + sizeof(uint32_t), sizeof(snapshot)); + if (snapshot.int_src_code != BLE_LOG_INT_SRC_SNAPSHOT) { + return; } - if (util.inflight_peak > util.trans_cnt) { + observer->buf_util_frames++; + if (snapshot.pool.inflight_peak > observer->max_inflight_peak) { + observer->max_inflight_peak = snapshot.pool.inflight_peak; + } + if (snapshot.pool.inflight_peak > snapshot.pool.trans_cnt) { observer->over_limit = true; } } @@ -441,7 +515,6 @@ TEST_CASE("BLE Log ISR-only submission arms runtime dispatch", #endif } -#if CONFIG_BLE_LOG_TS_ENABLED TEST_CASE("BLE Log periodic timestamp skips light sleep wakeups", "[ble_log][runtime][timestamp][ignore]") { @@ -474,7 +547,7 @@ TEST_CASE("BLE Log periodic timestamp skips light sleep wakeups", TEST_ASSERT_TRUE(runtime_stream_is_quiet()); TEST_ASSERT_TRUE(ble_log_sync_enable(true)); for (int i = 0; i < 3; i++) { - vTaskDelay(runtime_timeout_ticks(CONFIG_BLE_LOG_TS_TRIGGER_TIMEOUT_MS)); + vTaskDelay(runtime_timeout_ticks(BLE_LOG_TS_TRIGGER_TIMEOUT_MS)); } TEST_ASSERT_TRUE(ble_log_sync_enable(false)); @@ -489,7 +562,65 @@ TEST_CASE("BLE Log periodic timestamp skips light sleep wakeups", 0, ts_count, "Periodic ESP timer did not emit a timestamp frame"); } + +TEST_CASE("BLE Log sync IO control leaves periodic snapshots running", + "[ble_log][runtime][timestamp][ignore]") +{ +#if !CONFIG_BLE_LOG_TS_SYNC_TOGGLE_IO_ENABLED + TEST_IGNORE_MESSAGE("Requires BLE Log TS sync IO toggle support"); +#else + const uint32_t seq = UINT32_C(0x41000); + uint32_t received_seq; + uint8_t io_level; + TEST_ASSERT_TRUE(ble_log_enable(true)); + + TEST_ASSERT_TRUE(ble_log_ts_sync_io_toggle_enable(false)); + TEST_ASSERT_TRUE(runtime_stream_is_quiet()); + TEST_ASSERT_TRUE_MESSAGE(read_periodic_snapshot(&io_level), + "No periodic clock snapshot while sync IO was disabled"); + TEST_ASSERT_EQUAL_UINT8(0, io_level); + TEST_ASSERT_TRUE(runtime_stream_is_quiet()); + + /* Establish the timer phase above, then park a partial OPEN transport and + * close the public producer gate. The next periodic callback must still + * flush the transport and emit its Internal Snapshot. */ + prepare_payload(seq); + TEST_ASSERT_TRUE(ble_log_write_hex(BLE_LOG_SRC_CUSTOM, s_payload, + sizeof(rt_marker_t))); + TEST_ASSERT_TRUE(ble_log_enable(false)); + bool marker_received = read_runtime_marker_with_timeout( + &received_seq, NULL, NULL, + BLE_LOG_TS_TRIGGER_TIMEOUT_MS + 1000); + bool snapshot_received = read_periodic_snapshot(&io_level); + TEST_ASSERT_TRUE(ble_log_enable(true)); + + TEST_ASSERT_TRUE_MESSAGE( + marker_received, + "Producer disable stopped the periodic OPEN transport flush"); + TEST_ASSERT_EQUAL_UINT32(seq, received_seq); + TEST_ASSERT_TRUE_MESSAGE( + snapshot_received, + "Producer disable stopped periodic Internal Snapshots"); + TEST_ASSERT_EQUAL_UINT8(0, io_level); + + TEST_ASSERT_TRUE(runtime_stream_is_quiet()); + TEST_ASSERT_TRUE(ble_log_ts_sync_io_toggle_enable(true)); + bool high_seen = false; + for (int i = 0; i < 3 && !high_seen; i++) { + TEST_ASSERT_TRUE_MESSAGE(read_periodic_snapshot(&io_level), + "No periodic snapshot while sync IO was enabled"); + high_seen = io_level != 0; + } + TEST_ASSERT_TRUE_MESSAGE(high_seen, "Enabled sync IO never toggled high"); + + /* Exercise the compatibility shim on disable. */ + TEST_ASSERT_TRUE(ble_log_sync_enable(false)); + TEST_ASSERT_TRUE(runtime_stream_is_quiet()); + TEST_ASSERT_TRUE_MESSAGE(read_periodic_snapshot(&io_level), + "Sync IO disable stopped periodic snapshots"); + TEST_ASSERT_EQUAL_UINT8(0, io_level); #endif +} TEST_CASE("BLE Log runtime dispatch yields to other timer callbacks", "[ble_log][runtime][ignore]") @@ -654,7 +785,7 @@ TEST_CASE("BLE Log runtime dispatch latency", "[ble_log][runtime][perf][ignore]" print_latency_stats("burst_last", RT_BURST_SIZE, s_burst_last_latency_us); } -TEST_CASE("BLE Log runtime drains the full task pool in one batch", +TEST_CASE("BLE Log runtime drains the full shared pool in one batch", "[ble_log][runtime][ignore]") { #if !CONFIG_FREERTOS_UNICORE @@ -666,8 +797,8 @@ TEST_CASE("BLE Log runtime drains the full task pool in one batch", TEST_ASSERT_TRUE(ble_log_enable(true)); - /* Flush from this ordinary test task so every task-pool transport is free - * before constructing the callback-entry snapshot. */ + /* Flush from this ordinary test task so every shared-pool transport is + * free before constructing the callback-entry snapshot. */ ble_log_prph_test_set_auto_recycle_hook(noop_callback, NULL); ble_log_flush(); ble_log_prph_test_set_auto_recycle_hook(NULL, NULL); @@ -678,15 +809,15 @@ TEST_CASE("BLE Log runtime drains the full task pool in one batch", * without an artificial item cap. */ bool wrote = true; vTaskSuspendAll(); - for (uint32_t i = 0; i < RT_TASK_POOL_TRANS_COUNT; i++) { + for (uint32_t i = 0; i < BLE_LOG_POOL_SHARED_CNT; i++) { prepare_payload(base_seq + i); wrote = wrote && ble_log_write_hex(BLE_LOG_SRC_CUSTOM, s_payload, sizeof(s_payload)); } (void)xTaskResumeAll(); - TEST_ASSERT_TRUE_MESSAGE(wrote, "Full task-pool enqueue failed"); + TEST_ASSERT_TRUE_MESSAGE(wrote, "Full shared-pool enqueue failed"); - for (uint32_t i = 0; i < RT_TASK_POOL_TRANS_COUNT; i++) { + for (uint32_t i = 0; i < BLE_LOG_POOL_SHARED_CNT; i++) { uint32_t received_seq; int64_t received_at_us; TEST_ASSERT_TRUE_MESSAGE(read_runtime_marker(&received_seq, NULL, @@ -722,10 +853,20 @@ TEST_CASE("BLE Log LBM inflight peak stays bounded under bursts", TEST_ASSERT_TRUE(write_runtime_marker(seq, NULL)); } - /* Snapshot the recorded peaks. A full marker forces the partial BUF_UTIL - * transport to roll over through the normal LBM submission path. */ - ble_log_write_buf_util(); - TEST_ASSERT_TRUE(write_runtime_marker(UINT32_C(0x81000), NULL)); + /* Recycle the burst after its peak has been recorded. A periodic tick may + * also have occupied the dedicated Internal transport, so drain twice. */ + for (int round = 0; round < 2; round++) { + TEST_ASSERT_TRUE(ble_log_rt_drain()); + while (ble_log_prph_test_read(s_capture, sizeof(s_capture), + 0, 0, NULL) > 0) { + } + } + + /* Snapshot the retained peak through the dedicated Internal transport. */ + ble_log_ts_info_t ts_info; + ble_log_rt_ts_sample(&ts_info, false); + TEST_ASSERT_TRUE(ble_log_internal_snapshot( + BLE_LOG_SNAPSHOT_REASON_PERIODIC, &ts_info, true)); TEST_ASSERT_TRUE(ble_log_rt_drain()); const int64_t deadline_us = esp_timer_get_time() + @@ -747,7 +888,7 @@ TEST_CASE("BLE Log LBM inflight peak stays bounded under bursts", TEST_ASSERT_GREATER_THAN_UINT32_MESSAGE( 0, observer.buf_util_frames, - "No BUF_UTIL snapshots observed after flush"); + "No Internal utilization snapshot observed"); TEST_ASSERT_FALSE_MESSAGE(observer.over_limit, "inflight_peak exceeded the LBM transport count"); TEST_ASSERT_GREATER_THAN_UINT32_MESSAGE( @@ -763,6 +904,7 @@ TEST_CASE("BLE Log runtime survives deinit racing submissions", "[ble_log][runtime][ignore]") { rt_deinit_writer_ctx_t *ctx = &s_deinit_race; + TaskHandle_t writer_task = NULL; bool reinit_ok = true; memset(ctx, 0, sizeof(*ctx)); prepare_payload(UINT32_C(0x70000)); @@ -770,13 +912,14 @@ TEST_CASE("BLE Log runtime survives deinit racing submissions", #if CONFIG_FREERTOS_UNICORE BaseType_t task_created = xTaskCreate( deinit_writer_task, "ble_log_deinit_wr", 4096, ctx, - uxTaskPriorityGet(NULL), NULL); + uxTaskPriorityGet(NULL), &writer_task); #else /* Pin the writer away from this core so submissions run concurrently * with deinit instead of alternating at yield points. */ BaseType_t task_created = xTaskCreatePinnedToCore( deinit_writer_task, "ble_log_deinit_wr", 4096, ctx, - uxTaskPriorityGet(NULL), NULL, (xPortGetCoreID() == 0) ? 1 : 0); + uxTaskPriorityGet(NULL), &writer_task, + (xPortGetCoreID() == 0) ? 1 : 0); #endif TEST_ASSERT_EQUAL_MESSAGE(pdPASS, task_created, "Writer task create failed"); @@ -786,9 +929,16 @@ TEST_CASE("BLE Log runtime survives deinit racing submissions", taskYIELD(); } - /* Stop the writer and join with a bound before touching ctx or asserting: - * a unity longjmp past a live writer would leave it on a dead stack. */ + /* Stop the writer and join with a bound before asserting so a Unity + * longjmp cannot leak the writer into later tests. */ __atomic_store_n(&ctx->stop, true, __ATOMIC_RELEASE); + /* A writer parked inside ble_log_claim() cannot observe stop: only the + * deinit waiter wake returns it. Deinit here, before the join, so its + * drain closes the producer gate, wakes any parked writer and waits + * for it to leave the pool API. The vTaskDelete fallback below can + * then never cut a task down inside pool bookkeeping (which would + * leak waiting_task_count and hang the recovery deinit). */ + ble_log_deinit(); const int64_t join_deadline_us = esp_timer_get_time() + (int64_t)RT_JOIN_TIMEOUT_MS * 1000; while (!__atomic_load_n(&ctx->exited, __ATOMIC_ACQUIRE)) { @@ -799,16 +949,20 @@ TEST_CASE("BLE Log runtime survives deinit racing submissions", vTaskDelay(join_ticks); } + bool writer_exited = __atomic_load_n(&ctx->exited, __ATOMIC_ACQUIRE); + if (!writer_exited && writer_task) { + vTaskDelete(writer_task); + } + /* Recover module state before any assertion can abort the test: a * failed re-init leaves the module deinit-ed and tearDown does not - * restore it, which would cascade into every later test. */ - ble_log_deinit(); + * restore it, which would cascade into every later test. Deinit ran + * before the join (see above); only the init half remains. */ bool recovered = ble_log_init(); reinit_ok = reinit_ok && recovered; - TEST_ASSERT_TRUE_MESSAGE( - __atomic_load_n(&ctx->exited, __ATOMIC_ACQUIRE), - "Writer task did not exit after stop"); + TEST_ASSERT_TRUE_MESSAGE(writer_exited, + "Writer task did not exit after stop"); TEST_ASSERT_TRUE_MESSAGE(reinit_ok, "BLE Log re-init failed during the race"); TEST_ASSERT_GREATER_THAN_UINT32_MESSAGE( RT_DEINIT_ROUNDS, ctx->attempts, diff --git a/components/bt/common/ble_log/test_apps/ble_log_rt_test/sdkconfig.defaults b/components/bt/common/ble_log/test_apps/ble_log_rt_test/sdkconfig.defaults index 75545a6db78..bc9a2936bc2 100644 --- a/components/bt/common/ble_log/test_apps/ble_log_rt_test/sdkconfig.defaults +++ b/components/bt/common/ble_log/test_apps/ble_log_rt_test/sdkconfig.defaults @@ -6,7 +6,7 @@ CONFIG_ESP_TIMER_SUPPORTS_ISR_DISPATCH_METHOD=y # Production tick rate; the 1000 Hz regression variant is built from # sdkconfig.defaults.tick_1000. CONFIG_FREERTOS_HZ=100 -CONFIG_BLE_LOG_TS_ENABLED=y +CONFIG_BLE_LOG_TS_SYNC_TOGGLE_IO_ENABLED=y # Keep the production TS/hook cadence (Kconfig default 1000 ms) so the # regressions see the production info/stat/buf-util cadence. CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU0=n diff --git a/components/bt/common/ble_log/test_apps/ble_log_test/README.md b/components/bt/common/ble_log/test_apps/ble_log_test/README.md index 251e6e27533..affd8dae9fe 100644 --- a/components/bt/common/ble_log/test_apps/ble_log_test/README.md +++ b/components/bt/common/ble_log/test_apps/ble_log_test/README.md @@ -7,12 +7,16 @@ | Supported Targets | | ----------------- | -This test app verifies the BLE Log runtime behaviour on target, using the -in-memory test peripheral (`CONFIG_BLE_LOG_PRPH_TEST=y`) to capture the -transport stream written by the runtime dispatch hook. +This app uses the in-memory test peripheral (`CONFIG_BLE_LOG_PRPH_TEST=y`) to +validate the BLE Log transport on target. -Currently covered: +It covers: -- `BLE_LOG_INT_SRC_VERSION_INFO` frame: BLE Log version, ESP-IDF build commit, - controller lib commit, btdm_common lib commit, BLE Mesh and BLE Audio lib - commits, chip model and chip revision +- literal protocol-v8 framing and fixed Internal Snapshot ABI; +- build, library, chip, and protocol versions inside the snapshot; +- task and critical-section writes plus HCI direction encoding; +- direct compression claim/commit, stale handles, and per-source serialization; +- oversized-record rejection, flush sequence continuity, pool exhaustion, and non-yield reserve use; +- shared log/snapshot Global SN, independent 24-bit anchor counts, and snapshot busy/loss behavior; +- successful logical-byte counts for public, claim/commit, and LL writes, plus FLUSH reset; +- enable, disable, parked-writer, and deinit races. diff --git a/components/bt/common/ble_log/test_apps/ble_log_test/main/test_ble_log_main.c b/components/bt/common/ble_log/test_apps/ble_log_test/main/test_ble_log_main.c index 3437fed856c..0d0ec263587 100644 --- a/components/bt/common/ble_log/test_apps/ble_log_test/main/test_ble_log_main.c +++ b/components/bt/common/ble_log/test_apps/ble_log_test/main/test_ble_log_main.c @@ -10,7 +10,7 @@ #include "unity_test_runner.h" #include "ble_log.h" -#include "ble_log_lbm.h" +#include "ble_log_lbm_v2.h" #include "test_ble_log_main.h" bool test_ble_log_walk_frames(const uint8_t *data, size_t len, @@ -36,7 +36,7 @@ bool test_ble_log_walk_frames(const uint8_t *data, size_t len, if (observer) { test_ble_log_frame_t frame = { - .src = head.frame_meta & 0xff, + .src = (ble_log_src_t)(head.frame_meta & 0xff), .sn = head.frame_meta >> 8, .payload = data + offset + BLE_LOG_FRAME_HEAD_LEN, .payload_len = head.length, @@ -50,6 +50,9 @@ bool test_ble_log_walk_frames(const uint8_t *data, size_t len, void setUp(void) { + /* Preserve the external test-system contract: every test starts with the + * optional sync IO disabled and low. Periodic snapshots remain active. */ + (void)ble_log_sync_enable(false); } void tearDown(void) diff --git a/components/bt/common/ble_log/test_apps/ble_log_test/main/test_ble_log_rt.c b/components/bt/common/ble_log/test_apps/ble_log_test/main/test_ble_log_rt.c index 6f1ecb2e182..fef271fa58f 100644 --- a/components/bt/common/ble_log/test_apps/ble_log_test/main/test_ble_log_rt.c +++ b/components/bt/common/ble_log/test_apps/ble_log_test/main/test_ble_log_rt.c @@ -7,26 +7,32 @@ #include #include #include +#include #include #include "esp_chip_info.h" +#include "esp_timer.h" #include "freertos/FreeRTOS.h" #include "freertos/semphr.h" #include "freertos/task.h" #include "unity.h" #include "ble_log.h" +#include "ble_log_lbm_v2.h" #include "ble_log_prph_test.h" #include "ble_log_rt.h" +#include "ble_log_task_registry.h" #include "test_ble_log_main.h" +#if CONFIG_BLE_HOST_COMPRESSED_LOG_ENABLE +#include "log_compression/utils.h" +#endif #if !CONFIG_BLE_LOG_PRPH_TEST #error "BLE Log test app requires CONFIG_BLE_LOG_PRPH_TEST" #endif -/* The runtime dispatch hook is throttled to one pass per - * BLE_LOG_TS_TRIGGER_TIMEOUT_MS; let the window elapse between write bursts - * so a hook pass is guaranteed to run after the settle delay. */ +/* Internal Snapshots use the production periodic cadence; let one full + * window elapse when a test needs to observe the next periodic sample. */ #define TEST_HOOK_SETTLE_MS (BLE_LOG_TS_TRIGGER_TIMEOUT_MS + 100) #define TEST_READ_TIMEOUT_MS (50) #define TEST_MAX_ROUNDS (3) @@ -35,6 +41,8 @@ #define TEST_READ_BUF_SIZE (4096) #define TEST_READER_STACK_SIZE (3072) #define TEST_READER_PRIO (2) +#define TEST_LIFECYCLE_STACK_SIZE (2048) +#define TEST_LIFECYCLE_PRIO (3) typedef struct { size_t version_info_count; @@ -49,6 +57,116 @@ typedef struct { } reader_ctx_t; static uint8_t s_read_buf[TEST_READ_BUF_SIZE]; +static bool s_claim_hook_armed; +static bool s_init_snapshot_hook_armed; +static uint32_t s_stale_claim_handle; +static volatile bool s_locked_hook_armed; +static volatile bool s_enable_hook_armed; +static SemaphoreHandle_t s_enable_hook_entered; +static SemaphoreHandle_t s_enable_hook_continue; +static volatile bool s_disable_hook_armed; +static SemaphoreHandle_t s_disable_hook_entered; +static SemaphoreHandle_t s_disable_hook_continue; + +/* Stale-hint candidate flush (pool lost-wakeup interleaving). */ +static volatile bool s_flush_hint_hook_armed; +static volatile bool s_flush_stale_hook_armed; +static volatile bool s_flush_hint_drained; +static volatile int s_flush_hint_id = -1; +static SemaphoreHandle_t s_stale_writer_start; +static SemaphoreHandle_t s_stale_writer_started; + +void ble_log_test_claim_pre_publish_hook(void); +void ble_log_test_claim_locked_hook(void); +void ble_log_test_init_snapshot_before_acquire_hook(void); +void ble_log_test_enable_before_lifecycle_lock_hook(void); +void ble_log_test_disable_before_wake_hook(void); +void ble_log_test_flush_hint_seen_hook(uint8_t id); +void ble_log_test_flush_stale_locked_hook(void); +#if CONFIG_BLE_HOST_COMPRESSED_LOG_ENABLE +extern int ble_log_compressed_hex_print(uint8_t source, uint32_t log_index, + size_t args_cnt, ...); +#endif + +void ble_log_test_claim_pre_publish_hook(void) +{ + if (s_claim_hook_armed) { + /* A valid length exercises the state check on the still-OPEN + * transport: a deleted check would frame stale claim metadata + * (duplicate SN) on the wire. */ + ble_log_commit(s_stale_claim_handle, 1); + } +} + +void ble_log_test_claim_locked_hook(void) +{ + if (s_locked_hook_armed) { + s_locked_hook_armed = false; + /* Runs while the claiming writer itself holds the OPEN transport + * lock: the flush must skip the busy transport and leave the + * pending-seal marker for the next claim. */ + ble_log_lbm_flush_open_trans(); + } +} + +void ble_log_test_init_snapshot_before_acquire_hook(void) +{ + if (s_init_snapshot_hook_armed) { + vTaskDelay(pdMS_TO_TICKS(TEST_HOOK_SETTLE_MS)); + } +} + +void ble_log_test_enable_before_lifecycle_lock_hook(void) +{ + if (s_enable_hook_armed) { + xSemaphoreGive(s_enable_hook_entered); + xSemaphoreTake(s_enable_hook_continue, portMAX_DELAY); + } +} + +void ble_log_test_disable_before_wake_hook(void) +{ + if (s_disable_hook_armed) { + xSemaphoreGive(s_disable_hook_entered); + xSemaphoreTake(s_disable_hook_continue, portMAX_DELAY); + } +} + +void ble_log_test_flush_hint_seen_hook(uint8_t id) +{ + if (!s_flush_hint_hook_armed) { + return; + } + s_flush_hint_hook_armed = false; + s_flush_hint_id = id; + /* Play the other core between the flusher's hint read and its candidate + * lock: seal, send and recycle the one OPEN transport. The recycle + * notification runs while no writer is registered yet, so only the + * flusher's own candidate release can re-advertise it. drain dispatches + * to the peripheral queue; the peripheral releases on read, and that + * queue holds only the hinted transport (the other shared ones are + * pinned by open claims), so one read recycles exactly it. */ + ble_log_lbm_flush_open_trans(); + s_flush_hint_drained = ble_log_rt_drain(); + if (s_flush_hint_drained) { + s_flush_hint_drained = + ble_log_prph_test_read(s_read_buf, sizeof(s_read_buf), 0, 0, NULL) > 0; + } +} + +void ble_log_test_flush_stale_locked_hook(void) +{ + if (!s_flush_stale_hook_armed) { + return; + } + s_flush_stale_hook_armed = false; + /* The flusher holds the recycled transport's candidate lock. Start the + * writer: with every other shared transport pinned by a claim, its two + * scans bounce off this lock and it parks before this hook returns. */ + xSemaphoreGive(s_stale_writer_start); + (void)xSemaphoreTake(s_stale_writer_started, portMAX_DELAY); + vTaskDelay(pdMS_TO_TICKS(20)); +} /* A commit field is hex characters, zero-padded after a shorter value; * anything else (garbage, non-hex, zeros after data) is invalid. */ @@ -83,18 +201,56 @@ static bool commit_is_zero(const uint8_t *commit, size_t len) static void capture_version_info_frame(const test_ble_log_frame_t *frame, void *ctx) { version_capture_t *capture = ctx; - /* Every frame payload starts with a 4-byte timestamp prefix */ + if (frame->payload_len < sizeof(uint32_t)) { + return; + } const uint8_t *record = frame->payload + sizeof(uint32_t); size_t record_len = frame->payload_len - sizeof(uint32_t); if (frame->src == BLE_LOG_SRC_INTERNAL && - record_len == sizeof(ble_log_version_info_t) && - record[0] == BLE_LOG_INT_SRC_VERSION_INFO) { - memcpy(&capture->version_info, record, sizeof(capture->version_info)); + record_len == sizeof(ble_log_internal_snapshot_t) && + record[0] == BLE_LOG_INT_SRC_SNAPSHOT) { + ble_log_internal_snapshot_t snapshot; + memcpy(&snapshot, record, sizeof(snapshot)); + memcpy(&capture->version_info, &snapshot.version_info, + sizeof(capture->version_info)); capture->version_info_count++; } } +static uint32_t snapshot_anchor_count(const ble_log_internal_snapshot_t *snapshot) +{ + return (uint32_t)snapshot->anchor_count[0] | + ((uint32_t)snapshot->anchor_count[1] << 8) | + ((uint32_t)snapshot->anchor_count[2] << 16); +} + +typedef struct { + bool found; + uint16_t reason_flags; + uint32_t sn; + uint32_t anchor_count; +} first_snapshot_capture_t; + +static void capture_first_snapshot(const test_ble_log_frame_t *frame, void *ctx) +{ + first_snapshot_capture_t *capture = ctx; + if (capture->found || frame->src != BLE_LOG_SRC_INTERNAL || + frame->payload_len != sizeof(uint32_t) + + sizeof(ble_log_internal_snapshot_t)) { + return; + } + + ble_log_internal_snapshot_t snapshot; + memcpy(&snapshot, frame->payload + sizeof(uint32_t), sizeof(snapshot)); + if (snapshot.int_src_code == BLE_LOG_INT_SRC_SNAPSHOT) { + capture->found = true; + capture->reason_flags = snapshot.reason_flags; + capture->sn = frame->sn; + capture->anchor_count = snapshot_anchor_count(&snapshot); + } +} + /* Consumes pending test transports concurrently with the writer: transports * are recycled only once read, and the LBM has a small pool of them. */ static void test_reader_task(void *arg) @@ -114,7 +270,79 @@ static void test_reader_task(void *arg) vTaskDelete(NULL); } -TEST_CASE("BLE Log runtime hook reports build and chip versions", "[ble_log]") +typedef struct { + size_t count; + test_ble_log_frame_t frame; +} golden_capture_t; + +static void capture_golden_frame(const test_ble_log_frame_t *frame, void *ctx) +{ + golden_capture_t *capture = ctx; + capture->frame = *frame; + capture->count++; +} + +TEST_CASE("BLE Log v8 framing matches golden bytes", "[ble_log][wire]") +{ + static const uint8_t golden_frame[] = { + 0x05, 0x00, 0x07, 0xde, 0xc0, 0x00, + 0x78, 0x56, 0x34, 0x12, 0xab, + 0xf1, 0x12, 0xd4, 0x88, + }; + static const uint8_t golden_payload[] = { + 0x78, 0x56, 0x34, 0x12, 0xab, + }; + + TEST_ASSERT_EQUAL_UINT8(8, BLE_LOG_VERSION); + TEST_ASSERT_EQUAL_UINT8(1, BLE_LOG_SRC_CORE_FIRST); + TEST_ASSERT_EQUAL_UINT8(7, BLE_LOG_SRC_CORE_COUNT); + TEST_ASSERT_EQUAL_UINT8(7, BLE_LOG_SRC_ENCODE); + TEST_ASSERT_EQUAL_UINT8(7, BLE_LOG_INT_SRC_VERSION_INFO); + TEST_ASSERT_EQUAL_UINT8(8, BLE_LOG_INT_SRC_SNAPSHOT); + TEST_ASSERT_EQUAL_size_t(12, sizeof(ble_log_source_stat_t)); + TEST_ASSERT_EQUAL_size_t(165, sizeof(ble_log_internal_snapshot_t)); + TEST_ASSERT_EQUAL_size_t( + 3, offsetof(ble_log_internal_snapshot_t, anchor_count)); + TEST_ASSERT_EQUAL_size_t( + 6, offsetof(ble_log_internal_snapshot_t, version_info)); + TEST_ASSERT_EQUAL_size_t( + 65, offsetof(ble_log_internal_snapshot_t, ts.lc_ts)); + TEST_ASSERT_EQUAL_size_t( + 69, offsetof(ble_log_internal_snapshot_t, ts.esp_ts)); + TEST_ASSERT_EQUAL_size_t( + 73, offsetof(ble_log_internal_snapshot_t, ts.os_ts)); + TEST_ASSERT_EQUAL_size_t( + 81, offsetof(ble_log_internal_snapshot_t, stats)); + TEST_ASSERT_EQUAL_UINT32(179, BLE_LOG_INTERNAL_FRAME_LEN); + TEST_ASSERT_EQUAL_UINT32(180, BLE_LOG_INTERNAL_TRANS_SIZE); + const ble_log_internal_snapshot_t anchor_fixture = { + .anchor_count = {0x56, 0x34, 0x12}, + }; + TEST_ASSERT_EQUAL_size_t(3, sizeof(anchor_fixture.anchor_count)); + TEST_ASSERT_EQUAL_HEX32(0x123456, snapshot_anchor_count(&anchor_fixture)); +#if CONFIG_BLE_LOG_LL_ENABLED + TEST_ASSERT_EQUAL_UINT8(4, BLE_LOG_LL_FLAG_HCI); + TEST_ASSERT_EQUAL_UINT8(7, BLE_LOG_LL_FLAG_HCI_UPSTREAM); +#endif + TEST_ASSERT_EQUAL_HEX32( + 0x00c0de07, BLE_LOG_MAKE_FRAME_META(0x07, 0x00c0de)); + TEST_ASSERT_EQUAL_HEX32( + 0x00000007, BLE_LOG_MAKE_FRAME_META(0x07, 0x01000000)); + + golden_capture_t capture = {0}; + TEST_ASSERT_TRUE(test_ble_log_walk_frames(golden_frame, + sizeof(golden_frame), + capture_golden_frame, + &capture)); + TEST_ASSERT_EQUAL_size_t(1, capture.count); + TEST_ASSERT_EQUAL_UINT8(BLE_LOG_SRC_ENCODE, capture.frame.src); + TEST_ASSERT_EQUAL_HEX32(0x00c0de, capture.frame.sn); + TEST_ASSERT_EQUAL_size_t(sizeof(golden_payload), capture.frame.payload_len); + TEST_ASSERT_EQUAL_MEMORY(golden_payload, capture.frame.payload, + sizeof(golden_payload)); +} + +TEST_CASE("BLE Log periodic tick reports build and chip versions", "[ble_log]") { static const uint8_t payload[TEST_PAYLOAD_LEN] = {0}; @@ -127,13 +355,10 @@ TEST_CASE("BLE Log runtime hook reports build and chip versions", "[ble_log]") TEST_READER_STACK_SIZE, &ctx, TEST_READER_PRIO, &reader)); TEST_ASSERT_TRUE(ble_log_enable(true)); + TEST_ASSERT_TRUE(ble_log_sync_enable(true)); - /* Transports are auto-submitted once full, which arms the defer alarm; - * after the throttle window elapses, a hook pass writes the version frame - * into the LBM and a later transport carries it out. ble_log_flush() - * cannot be used here: it disables the module while waiting for the - * transports to drain, so the hook frame written during the flush window - * would be dropped. */ + /* Each periodic tick submits one consolidated Internal Snapshot containing + * the version, statistics, utilization, and optional TS sample. */ for (int round = 0; round < TEST_MAX_ROUNDS && ctx.capture.version_info_count == 0; round++) { vTaskDelay(pdMS_TO_TICKS(TEST_HOOK_SETTLE_MS)); for (int i = 0; i < TEST_WRITES_PER_ROUND; i++) { @@ -185,5 +410,2424 @@ TEST_CASE("BLE Log runtime hook reports build and chip versions", "[ble_log]") TEST_ASSERT_EQUAL_UINT16((uint16_t)chip_info.model, vi->chip_model); TEST_ASSERT_EQUAL_UINT16(chip_info.revision, vi->chip_revision); + (void)ble_log_sync_enable(false); vSemaphoreDelete(ctx.done); } + +TEST_CASE("BLE Log INIT snapshot starts each receiver epoch", "[ble_log][wire]") +{ + ble_log_deinit(); + s_init_snapshot_hook_armed = true; + bool initialized = ble_log_init(); + s_init_snapshot_hook_armed = false; + TEST_ASSERT_TRUE(initialized); + + size_t len = ble_log_prph_test_read( + s_read_buf, sizeof(s_read_buf), pdMS_TO_TICKS(1000), 0, NULL); + TEST_ASSERT_GREATER_THAN_size_t(0, len); + + first_snapshot_capture_t capture = {0}; + TEST_ASSERT_TRUE(test_ble_log_walk_frames( + s_read_buf, len, capture_first_snapshot, &capture)); + TEST_ASSERT_TRUE(capture.found); + TEST_ASSERT_EQUAL_HEX16(BLE_LOG_SNAPSHOT_REASON_INIT, + capture.reason_flags); + TEST_ASSERT_EQUAL_UINT32(0, capture.sn); + TEST_ASSERT_EQUAL_UINT32(0, capture.anchor_count); +} + +TEST_CASE("BLE Log sync IO APIs retain runtime lifecycle checks", "[ble_log]") +{ + ble_log_deinit(); + TEST_ASSERT_FALSE(ble_log_ts_sync_io_toggle_enable(true)); + TEST_ASSERT_FALSE(ble_log_sync_enable(true)); + + TEST_ASSERT_TRUE(ble_log_init()); + TEST_ASSERT_TRUE(ble_log_ts_sync_io_toggle_enable(false)); + TEST_ASSERT_TRUE(ble_log_sync_enable(false)); + TEST_ASSERT_TRUE(ble_log_rt_drain()); + while (ble_log_prph_test_read(s_read_buf, sizeof(s_read_buf), + 0, 0, NULL) > 0) { + } +} + +typedef struct { + bool task_frame; + bool critical_frame; + bool hci_downstream_frame; + bool hci_upstream_frame; + int ll_hci_frame_cnt; + int ll_hci_upstream_frame_cnt; + bool claimed_frame; + bool stale_protected_frame; + int encode_frame_cnt; +} frame_meta_capture_t; + +static void capture_frame_meta(const test_ble_log_frame_t *frame, void *ctx) +{ + frame_meta_capture_t *capture = ctx; + if (frame->src == BLE_LOG_SRC_ENCODE) { + capture->encode_frame_cnt++; + } + if (frame->payload_len < sizeof(uint32_t) + 1) { + return; + } + + uint8_t marker = frame->payload[sizeof(uint32_t)]; + if (frame->src == BLE_LOG_SRC_CUSTOM && marker == 0x11) { + capture->task_frame = true; + } else if (frame->src == BLE_LOG_SRC_CUSTOM && marker == 0x22) { + capture->critical_frame = true; + } else if (frame->src == BLE_LOG_SRC_HCI && marker == 0x01) { + capture->hci_downstream_frame = true; + } else if (frame->src == BLE_LOG_SRC_HCI && marker == 0x82) { + capture->hci_upstream_frame = true; + } else if (frame->src == BLE_LOG_SRC_ENCODE && marker == 0x33) { + capture->claimed_frame = true; + } else if (frame->src == BLE_LOG_SRC_ENCODE && marker == 0x44) { + capture->stale_protected_frame = true; + } else if (marker == 0x51 || marker == 0x52) { + static const uint8_t controller_timestamp[] = {0x78, 0x56, 0x34, 0x12}; + TEST_ASSERT_EQUAL_size_t(sizeof(controller_timestamp) + 1, frame->payload_len); + TEST_ASSERT_EQUAL_MEMORY(controller_timestamp, frame->payload, + sizeof(controller_timestamp)); + if (marker == 0x51) { + TEST_ASSERT_EQUAL_UINT8(BLE_LOG_SRC_LL_HCI, frame->src); + capture->ll_hci_frame_cnt++; + } else { + TEST_ASSERT_EQUAL_UINT8(BLE_LOG_SRC_HCI, frame->src); + capture->ll_hci_upstream_frame_cnt++; + } + } +} + +TEST_CASE("BLE Log writes from critical sections and commits claimed payload", + "[ble_log][lbm]") +{ + const uint8_t task_marker = 0x11; + const uint8_t critical_marker = 0x22; + TEST_ASSERT_TRUE(ble_log_enable(true)); + ble_log_lbm_flush_open_trans(); + for (int round = 0; round < 2; round++) { + TEST_ASSERT_TRUE(ble_log_rt_drain()); + while (ble_log_prph_test_read(s_read_buf, sizeof(s_read_buf), + 0, 0, NULL) > 0) { + } + } + TEST_ASSERT_TRUE(ble_log_write_hex(BLE_LOG_SRC_CUSTOM, + &task_marker, sizeof(task_marker))); + + portMUX_TYPE mux = portMUX_INITIALIZER_UNLOCKED; + portENTER_CRITICAL(&mux); + bool critical_written = ble_log_write_hex(BLE_LOG_SRC_CUSTOM, + &critical_marker, + sizeof(critical_marker)); + portEXIT_CRITICAL(&mux); + TEST_ASSERT_TRUE(critical_written); + + uint8_t hci_type = 0x81; + ble_log_write_hci(BLE_LOG_HCI_DOWNSTREAM, &hci_type, sizeof(hci_type)); + TEST_ASSERT_EQUAL_HEX8(0x81, hci_type); + hci_type = 0x02; + ble_log_write_hci(BLE_LOG_HCI_UPSTREAM, &hci_type, sizeof(hci_type)); + TEST_ASSERT_EQUAL_HEX8(0x02, hci_type); + /* The HCI macro requires one writable type byte. Exercise invalid public + * input at the validating API instead. */ + TEST_ASSERT_FALSE(ble_log_write_hex(BLE_LOG_SRC_HCI, NULL, 1)); + +#if CONFIG_BLE_LOG_LL_ENABLED + /* Controller records keep their existing payload, including timestamp. + * Exercise both contiguous and appended payloads at the real LL entry. */ + static const uint8_t ll_hci_payload[] = {0x78, 0x56, 0x34, 0x12, 0x51}; + const uint8_t ll_hci_upstream = 0x52; + ble_log_write_hex_ll(sizeof(ll_hci_payload), ll_hci_payload, 0, NULL, + BIT(BLE_LOG_LL_FLAG_HCI)); + ble_log_write_hex_ll(sizeof(ll_hci_payload) - 1, ll_hci_payload, + sizeof(ll_hci_upstream), &ll_hci_upstream, + BIT(BLE_LOG_LL_FLAG_HCI_UPSTREAM)); +#endif + + uint32_t handle; + uint8_t *claimed = ble_log_claim(BLE_LOG_SRC_ENCODE, 8, &handle, true); + TEST_ASSERT_NOT_NULL(claimed); + claimed[0] = 0x33; + ble_log_commit(handle, 1); + + /* Invoke the stale commit immediately before the next claim publishes + * CLAIMED. It must see the prior OPEN state and leave the new claim + * intact. */ + s_stale_claim_handle = handle; + s_claim_hook_armed = true; + uint32_t fresh_handle; + claimed = ble_log_claim(BLE_LOG_SRC_ENCODE, 8, &fresh_handle, true); + s_claim_hook_armed = false; + TEST_ASSERT_NOT_NULL(claimed); + TEST_ASSERT_NOT_EQUAL(handle, fresh_handle); + /* The fresh claim is published: the same stale handle must now be + * rejected by the generation check instead. A zero length mirrors a + * stale abort, which would otherwise consume the fresh claim. */ + ble_log_commit(s_stale_claim_handle, 0); + claimed[0] = 0x44; + ble_log_commit(fresh_handle, 1); + + ble_log_lbm_flush_open_trans(); + TEST_ASSERT_TRUE(ble_log_rt_drain()); + + frame_meta_capture_t capture = {0}; + for (int i = 0; i < BLE_LOG_TRANS_TOTAL_CNT; i++) { + size_t len = ble_log_prph_test_read(s_read_buf, sizeof(s_read_buf), + pdMS_TO_TICKS(TEST_READ_TIMEOUT_MS), + 0, NULL); + if (!len) { + break; + } + TEST_ASSERT_TRUE(test_ble_log_walk_frames(s_read_buf, len, + capture_frame_meta, + &capture)); + } + + TEST_ASSERT_TRUE(capture.task_frame); + TEST_ASSERT_TRUE(capture.critical_frame); + TEST_ASSERT_TRUE(capture.hci_downstream_frame); + TEST_ASSERT_TRUE(capture.hci_upstream_frame); +#if CONFIG_BLE_LOG_LL_ENABLED && CONFIG_BLE_LOG_HCI_LOG_ENABLED && \ + !CONFIG_BT_BLUEDROID_ENABLED && !CONFIG_BT_NIMBLE_LEGACY_VHCI_ENABLE + TEST_ASSERT_EQUAL(1, capture.ll_hci_frame_cnt); + TEST_ASSERT_EQUAL(1, capture.ll_hci_upstream_frame_cnt); +#else + /* Disabled HCI or an existing Host capture must suppress duplicates. */ + TEST_ASSERT_EQUAL(0, capture.ll_hci_frame_cnt); + TEST_ASSERT_EQUAL(0, capture.ll_hci_upstream_frame_cnt); +#endif + TEST_ASSERT_TRUE(capture.claimed_frame); + TEST_ASSERT_TRUE(capture.stale_protected_frame); + /* Exactly the two committed claims: a stale commit that slipped past + * the state or generation check would add or consume an ENCODE frame. */ + TEST_ASSERT_EQUAL(2, capture.encode_frame_cnt); +} + +#if CONFIG_BLE_HOST_COMPRESSED_LOG_ENABLE +#define TEST_CP_TASK_UNKNOWN UINT8_C(0xff) +#define TEST_CP_ZERO_ARGS UINT8_C(0x40) +#define TEST_CP_WRITER_RECORDS 8 +#define TEST_CP_INDEX_MAIN_A UINT16_C(0x600) +#define TEST_CP_INDEX_MAIN_B UINT16_C(0x601) +#define TEST_CP_INDEX_BASE_A UINT16_C(0x700) +#define TEST_CP_INDEX_BASE_B UINT16_C(0x710) +#define TEST_CP_INDEX_FILL_A UINT16_C(0x720) +#define TEST_CP_INDEX_FILL_B UINT16_C(0x721) +#define TEST_CP_INDEX_FILLER UINT16_C(0x722) +#define TEST_CP_NAME_MAX 17 + +typedef struct { + /* id-to-name bindings learned from INTERNAL task-binding records */ + char names[CONFIG_BLE_LOG_TASK_ID_MAX][TEST_CP_NAME_MAX]; + uint8_t announce_cnt[CONFIG_BLE_LOG_TASK_ID_MAX]; + /* per-writer record accounting */ + uint8_t main_records; + uint8_t a_records; + uint8_t b_records; + uint8_t filler_records; + uint8_t fill_a_records; + uint8_t fill_b_records; + bool a_id_set; + bool a_id_mismatch; + uint8_t a_id; + bool b_id_set; + bool b_id_mismatch; + uint8_t b_id; + bool main_id_set; + bool main_id_mismatch; + uint8_t main_id; + bool filler_unknown; + bool fill_a_id_set; + uint8_t fill_a_id; + bool fill_b_unknown; +} compression_capture_t; + +typedef struct { + SemaphoreHandle_t token; + SemaphoreHandle_t peer_token; + SemaphoreHandle_t done; + uint16_t index_base; +} compression_writer_ctx_t; + +typedef struct { + SemaphoreHandle_t done; + uint16_t log_index; +} compression_once_ctx_t; + +static void compression_writer_task(void *arg) +{ + compression_writer_ctx_t *ctx = arg; + for (int i = 0; i < TEST_CP_WRITER_RECORDS; i++) { + xSemaphoreTake(ctx->token, portMAX_DELAY); + ble_log_compressed_hex_print(BLE_COMPRESSED_LOG_OUT_SOURCE_HOST, + ctx->index_base + i, 0); + xSemaphoreGive(ctx->peer_token); + } + xSemaphoreGive(ctx->done); + vTaskDelete(NULL); +} + +static void compression_once_task(void *arg) +{ + compression_once_ctx_t *ctx = arg; + ble_log_compressed_hex_print(BLE_COMPRESSED_LOG_OUT_SOURCE_HOST, + ctx->log_index, 0); + xSemaphoreGive(ctx->done); + vTaskDelete(NULL); +} + +/* Walks one ENCODE frame: [ts 4B][source 1B][task_id 1B][header 1B]... + * and one INTERNAL task-binding frame: [ts 4B][int_src 1B][task_id 1B] + * [name 17B] (the periodic registry broadcast). */ +static void capture_compressed_frame(const test_ble_log_frame_t *frame, + void *ctx) +{ + compression_capture_t *capture = ctx; + /* Bindings ride the INTERNAL stream: one frame per periodic window, + * packing one fixed-layout record per registered entry. */ + if (frame->src == BLE_LOG_SRC_INTERNAL && + frame->payload_len >= sizeof(uint32_t) + + sizeof(ble_log_task_binding_t) && + (frame->payload_len - sizeof(uint32_t)) % + sizeof(ble_log_task_binding_t) == 0) { + const uint8_t *record = frame->payload + sizeof(uint32_t); + size_t records = (frame->payload_len - sizeof(uint32_t)) / + sizeof(ble_log_task_binding_t); + for (size_t r = 0; r < records; r++, record += sizeof(ble_log_task_binding_t)) { + if (record[0] != BLE_LOG_INT_SRC_TASK_BINDING) { + continue; + } + uint8_t task_id = record[1]; + const uint8_t *name = record + 2; + if (task_id < CONFIG_BLE_LOG_TASK_ID_MAX) { + capture->announce_cnt[task_id]++; + if (capture->announce_cnt[task_id] == 1) { + memcpy(capture->names[task_id], name, + sizeof(capture->names[task_id])); + } else if (strcmp(capture->names[task_id], + (const char *)name) != 0) { + /* same id re-announced with a different name */ + capture->names[task_id][0] = '\0'; + } + } + } + return; + } + if (frame->src != BLE_LOG_SRC_ENCODE) { + return; + } + const uint8_t *record = frame->payload + sizeof(uint32_t); + size_t record_len = frame->payload_len - sizeof(uint32_t); + if (record_len < 3 || + record[0] != BLE_COMPRESSED_LOG_OUT_SOURCE_HOST) { + return; + } + uint8_t task_id = record[1]; + uint8_t header = record[2]; + + if (header != TEST_CP_ZERO_ARGS || record_len < 5) { + return; + } + uint16_t log_index; + memcpy(&log_index, record + 3, sizeof(log_index)); + + if (log_index == TEST_CP_INDEX_MAIN_A || log_index == TEST_CP_INDEX_MAIN_B) { + capture->main_records++; + if (!capture->main_id_set) { + capture->main_id_set = true; + capture->main_id = task_id; + } else if (capture->main_id != task_id) { + capture->main_id_mismatch = true; + } + } else if (log_index >= TEST_CP_INDEX_BASE_A && + log_index < TEST_CP_INDEX_BASE_A + TEST_CP_WRITER_RECORDS) { + capture->a_records++; + if (!capture->a_id_set) { + capture->a_id_set = true; + capture->a_id = task_id; + } else if (capture->a_id != task_id) { + capture->a_id_mismatch = true; + } + } else if (log_index >= TEST_CP_INDEX_BASE_B && + log_index < TEST_CP_INDEX_BASE_B + TEST_CP_WRITER_RECORDS) { + capture->b_records++; + if (!capture->b_id_set) { + capture->b_id_set = true; + capture->b_id = task_id; + } else if (capture->b_id != task_id) { + capture->b_id_mismatch = true; + } + } else if (log_index >= TEST_CP_INDEX_FILLER && + log_index < TEST_CP_INDEX_FILLER + CONFIG_BLE_LOG_TASK_ID_MAX - 1) { + capture->filler_records++; + if (task_id == TEST_CP_TASK_UNKNOWN) { + capture->filler_unknown = true; + } + } else if (log_index == TEST_CP_INDEX_FILL_A) { + capture->fill_a_records++; + if (task_id != TEST_CP_TASK_UNKNOWN) { + capture->fill_a_id_set = true; + capture->fill_a_id = task_id; + } + } else if (log_index == TEST_CP_INDEX_FILL_B) { + capture->fill_b_records++; + if (task_id == TEST_CP_TASK_UNKNOWN) { + capture->fill_b_unknown = true; + } + } +} + +static void compression_drain_and_capture(compression_capture_t *capture) +{ + ble_log_lbm_flush_open_trans(); + TEST_ASSERT_TRUE(ble_log_rt_drain()); + memset(capture, 0, sizeof(*capture)); + for (int i = 0; i < BLE_LOG_TRANS_TOTAL_CNT; i++) { + size_t len = ble_log_prph_test_read( + s_read_buf, sizeof(s_read_buf), pdMS_TO_TICKS(TEST_READ_TIMEOUT_MS), + 0, NULL); + if (!len) { + break; + } + TEST_ASSERT_TRUE(test_ble_log_walk_frames( + s_read_buf, len, capture_compressed_frame, capture)); + } +} + +static void compression_reset_streams(void) +{ + /* Wipe the task-id registry so neither compression case depends on the + * other's registrations or on test-case order. */ + ble_log_test_task_registry_reset(); + TEST_ASSERT_TRUE(ble_log_enable(true)); + ble_log_lbm_flush_open_trans(); + for (int round = 0; round < 2; round++) { + TEST_ASSERT_TRUE(ble_log_rt_drain()); + while (ble_log_prph_test_read(s_read_buf, sizeof(s_read_buf), + 0, 0, NULL) > 0) { + } + } +} + +static uint8_t compression_announced_id(const compression_capture_t *capture, + const char *name) +{ + for (int i = 0; i < CONFIG_BLE_LOG_TASK_ID_MAX; i++) { + if (capture->announce_cnt[i] > 0 && + strcmp(capture->names[i], name) == 0) { + return (uint8_t)i; + } + } + return TEST_CP_TASK_UNKNOWN; +} + +/* Protocol v8: task attribution is a property of the record. Two writers + * interleave records on one compression source; every record carries its + * writer's id and no record is dropped for contention (there is no source + * lock to contend). Bindings are broadcast on the periodic window only + * (the LBM task-binding publish), so it is driven once before the capture + * below. */ +TEST_CASE("BLE Log attributes compressed records to their writer task", + "[ble_log][compression]") +{ + compression_reset_streams(); + + ble_log_compressed_hex_print(BLE_COMPRESSED_LOG_OUT_SOURCE_HOST, + TEST_CP_INDEX_MAIN_A, 0); + + compression_writer_ctx_t writer_a = { + .token = xSemaphoreCreateBinary(), + .peer_token = NULL, + .done = xSemaphoreCreateBinary(), + .index_base = TEST_CP_INDEX_BASE_A, + }; + compression_writer_ctx_t writer_b = { + .token = xSemaphoreCreateBinary(), + .peer_token = NULL, + .done = xSemaphoreCreateBinary(), + .index_base = TEST_CP_INDEX_BASE_B, + }; + writer_a.peer_token = writer_b.token; + writer_b.peer_token = writer_a.token; + TEST_ASSERT_NOT_NULL(writer_a.token); + TEST_ASSERT_NOT_NULL(writer_b.token); + TEST_ASSERT_NOT_NULL(writer_a.done); + TEST_ASSERT_NOT_NULL(writer_b.done); + + TEST_ASSERT_EQUAL(pdTRUE, + xTaskCreate(compression_writer_task, "cp_wr_a", + TEST_LIFECYCLE_STACK_SIZE, &writer_a, + TEST_LIFECYCLE_PRIO, NULL)); + TEST_ASSERT_EQUAL(pdTRUE, + xTaskCreate(compression_writer_task, "cp_wr_b", + TEST_LIFECYCLE_STACK_SIZE, &writer_b, + TEST_LIFECYCLE_PRIO, NULL)); + + xSemaphoreGive(writer_a.token); + TEST_ASSERT_TRUE(xSemaphoreTake(writer_a.done, pdMS_TO_TICKS(2000))); + TEST_ASSERT_TRUE(xSemaphoreTake(writer_b.done, pdMS_TO_TICKS(2000))); + + /* A second record from the main task must not register a new entry: + * its records keep the registered id. Periodic publish may repeat + * the binding in this window, always with the same name. */ + ble_log_compressed_hex_print(BLE_COMPRESSED_LOG_OUT_SOURCE_HOST, + TEST_CP_INDEX_MAIN_B, 0); + + /* Bindings ride the periodic window only: drive the LBM broadcast + * once so this capture carries every registered writer's binding. */ + ble_log_task_bindings_publish(); + + compression_capture_t capture; + compression_drain_and_capture(&capture); + + const char *main_name = pcTaskGetName(NULL); + TEST_ASSERT_NOT_NULL(main_name); + + /* All records present: contention between interleaved writers no + * longer drops records. */ + TEST_ASSERT_EQUAL_UINT8(TEST_CP_WRITER_RECORDS, capture.a_records); + TEST_ASSERT_EQUAL_UINT8(TEST_CP_WRITER_RECORDS, capture.b_records); + TEST_ASSERT_EQUAL_UINT8(2, capture.main_records); + + /* Every record of a writer carries that writer's announced id. */ + TEST_ASSERT_TRUE(capture.a_id_set); + TEST_ASSERT_FALSE(capture.a_id_mismatch); + TEST_ASSERT_TRUE(capture.b_id_set); + TEST_ASSERT_FALSE(capture.b_id_mismatch); + TEST_ASSERT_TRUE(capture.main_id_set); + TEST_ASSERT_FALSE(capture.main_id_mismatch); + TEST_ASSERT_NOT_EQUAL(TEST_CP_TASK_UNKNOWN, + compression_announced_id(&capture, "cp_wr_a")); + TEST_ASSERT_NOT_EQUAL(TEST_CP_TASK_UNKNOWN, + compression_announced_id(&capture, "cp_wr_b")); + TEST_ASSERT_NOT_EQUAL(TEST_CP_TASK_UNKNOWN, + compression_announced_id(&capture, main_name)); + TEST_ASSERT_EQUAL_UINT8(compression_announced_id(&capture, "cp_wr_a"), + capture.a_id); + TEST_ASSERT_EQUAL_UINT8(compression_announced_id(&capture, "cp_wr_b"), + capture.b_id); + TEST_ASSERT_EQUAL_UINT8(compression_announced_id(&capture, main_name), + capture.main_id); + + /* Every writer is announced at least once by the driven publish (a + * production tick may add another, with the same name), and the + * capture rejects a same-id rename. */ + TEST_ASSERT_TRUE( + capture.announce_cnt[compression_announced_id(&capture, "cp_wr_a")] >= 1); + TEST_ASSERT_TRUE( + capture.announce_cnt[compression_announced_id(&capture, "cp_wr_b")] >= 1); + TEST_ASSERT_TRUE( + capture.announce_cnt[compression_announced_id(&capture, main_name)] >= 1); + + vSemaphoreDelete(writer_a.token); + vSemaphoreDelete(writer_b.token); + vSemaphoreDelete(writer_a.done); + vSemaphoreDelete(writer_b.done); +} + +/* Periodic binding publish: bindings are broadcast on the snapshot window + * (the LBM periodic publish); a task's first record already carries its + * id and is bound by name at the next window. Drive the publish + * synchronously so the window is deterministic: capture the main task's + * record first to learn its id, then the driven publish binds that id to + * the name and announces no other id. */ +TEST_CASE("BLE Log republishes task bindings on the periodic window", + "[ble_log][compression]") +{ + compression_reset_streams(); + + const char *main_name = pcTaskGetName(NULL); + TEST_ASSERT_NOT_NULL(main_name); + + /* First window: the record carries the registered id, but no binding + * is guaranteed yet (a production tick may or may not have landed in + * this window, so no announce assertion is made here). */ + ble_log_compressed_hex_print(BLE_COMPRESSED_LOG_OUT_SOURCE_HOST, + TEST_CP_INDEX_MAIN_A, 0); + compression_capture_t first; + compression_drain_and_capture(&first); + TEST_ASSERT_TRUE(first.main_id_set); + uint8_t main_id = first.main_id; + TEST_ASSERT_NOT_EQUAL(TEST_CP_TASK_UNKNOWN, main_id); + + /* One synchronous publish; a production tick may add another with the + * same id, so only presence and id/name stability are asserted. */ + ble_log_task_bindings_publish(); + compression_capture_t second; + compression_drain_and_capture(&second); + + TEST_ASSERT_TRUE(second.announce_cnt[main_id] >= 1); + TEST_ASSERT_EQUAL_UINT8(main_id, + compression_announced_id(&second, main_name)); + /* Publishing never fabricates bindings: no other id is announced. */ + for (int i = 0; i < CONFIG_BLE_LOG_TASK_ID_MAX; i++) { + if (i != main_id) { + TEST_ASSERT_EQUAL_UINT8(0, second.announce_cnt[i]); + } + } +} + +/* The registry is sized by CONFIG_BLE_LOG_TASK_ID_MAX (4 in this + * app) and wiped between cases, so this case fills it itself: one-shot + * filler tasks consume every slot but the last, cp_fill_a takes the last + * free slot, and cp_fill_b beyond the registry degrades to the unknown id + * (0xFF) and its record is still emitted. Bindings are broadcast on the + * periodic window only, so the publish is driven once before the + * capture. */ +TEST_CASE("BLE Log degrades to the unknown task id when the registry is full", + "[ble_log][compression]") +{ + compression_reset_streams(); + + /* Consume every slot but the last with distinct one-shot tasks. The + * name is copied into the task's TCB, so one buffer is reused. */ + compression_once_ctx_t filler[CONFIG_BLE_LOG_TASK_ID_MAX - 1]; + char filler_name[8]; + for (int i = 0; i < CONFIG_BLE_LOG_TASK_ID_MAX - 1; i++) { + filler[i].done = xSemaphoreCreateBinary(); + TEST_ASSERT_NOT_NULL(filler[i].done); + filler[i].log_index = TEST_CP_INDEX_FILLER + i; + snprintf(filler_name, sizeof(filler_name), "cp_f%d", i); + TEST_ASSERT_EQUAL(pdTRUE, + xTaskCreate(compression_once_task, filler_name, + TEST_LIFECYCLE_STACK_SIZE, &filler[i], + TEST_LIFECYCLE_PRIO, NULL)); + TEST_ASSERT_TRUE(xSemaphoreTake(filler[i].done, pdMS_TO_TICKS(2000))); + } + + compression_once_ctx_t fill_a = { + .done = xSemaphoreCreateBinary(), + .log_index = TEST_CP_INDEX_FILL_A, + }; + compression_once_ctx_t fill_b = { + .done = xSemaphoreCreateBinary(), + .log_index = TEST_CP_INDEX_FILL_B, + }; + TEST_ASSERT_NOT_NULL(fill_a.done); + TEST_ASSERT_NOT_NULL(fill_b.done); + + TEST_ASSERT_EQUAL(pdTRUE, + xTaskCreate(compression_once_task, "cp_fill_a", + TEST_LIFECYCLE_STACK_SIZE, &fill_a, + TEST_LIFECYCLE_PRIO, NULL)); + TEST_ASSERT_TRUE(xSemaphoreTake(fill_a.done, pdMS_TO_TICKS(2000))); + + TEST_ASSERT_EQUAL(pdTRUE, + xTaskCreate(compression_once_task, "cp_fill_b", + TEST_LIFECYCLE_STACK_SIZE, &fill_b, + TEST_LIFECYCLE_PRIO, NULL)); + TEST_ASSERT_TRUE(xSemaphoreTake(fill_b.done, pdMS_TO_TICKS(2000))); + + /* Bindings ride the periodic window only: drive the LBM broadcast + * once so the capture carries every registered name. */ + ble_log_task_bindings_publish(); + + compression_capture_t capture; + compression_drain_and_capture(&capture); + + /* The fillers consumed the first slots: every record emitted with a + * valid id. */ + TEST_ASSERT_EQUAL_UINT8(CONFIG_BLE_LOG_TASK_ID_MAX - 1, + capture.filler_records); + TEST_ASSERT_FALSE(capture.filler_unknown); + + /* The last free slot still works: valid id, bound by the driven + * periodic publish. */ + TEST_ASSERT_EQUAL_UINT8(1, capture.fill_a_records); + TEST_ASSERT_TRUE(capture.fill_a_id_set); + TEST_ASSERT_NOT_EQUAL(TEST_CP_TASK_UNKNOWN, + compression_announced_id(&capture, "cp_fill_a")); + TEST_ASSERT_EQUAL_UINT8(compression_announced_id(&capture, "cp_fill_a"), + capture.fill_a_id); + + /* Beyond the registry: unknown id, no binding, log still emitted. */ + TEST_ASSERT_EQUAL_UINT8(1, capture.fill_b_records); + TEST_ASSERT_TRUE(capture.fill_b_unknown); + TEST_ASSERT_EQUAL_UINT8(TEST_CP_TASK_UNKNOWN, + compression_announced_id(&capture, "cp_fill_b")); + + for (int i = 0; i < CONFIG_BLE_LOG_TASK_ID_MAX - 1; i++) { + vSemaphoreDelete(filler[i].done); + } + vSemaphoreDelete(fill_a.done); + vSemaphoreDelete(fill_b.done); +} +#endif /* CONFIG_BLE_HOST_COMPRESSED_LOG_ENABLE */ + +typedef struct { + ble_log_src_t src; + uint8_t marker; + bool found; + uint32_t sn; +} sequence_capture_t; + +typedef struct { + sequence_capture_t *captures; + size_t count; +} sequence_capture_set_t; + +static void capture_sequence_frame(const test_ble_log_frame_t *frame, void *ctx) +{ + sequence_capture_set_t *set = ctx; + bool is_ll = frame->src == BLE_LOG_SRC_LL_TASK || + frame->src == BLE_LOG_SRC_LL_ISR; + size_t marker_offset = is_ll ? 0 : sizeof(uint32_t); + /* LL payloads already carry the controller's LC timestamp; unlike the + * other sources, they must not gain an ESP timestamp prefix. */ + if (frame->payload_len != marker_offset + 1) { + return; + } + + uint8_t marker = frame->payload[marker_offset]; + for (size_t i = 0; i < set->count; i++) { + sequence_capture_t *capture = &set->captures[i]; + if (frame->src == capture->src && marker == capture->marker) { + capture->found = true; + capture->sn = frame->sn; + } + } +} + +static void read_sequence_frames(sequence_capture_t *captures, size_t count) +{ + sequence_capture_set_t set = { + .captures = captures, + .count = count, + }; + for (int i = 0; i < BLE_LOG_TRANS_TOTAL_CNT; i++) { + size_t len = ble_log_prph_test_read( + s_read_buf, sizeof(s_read_buf), pdMS_TO_TICKS(TEST_READ_TIMEOUT_MS), + 0, NULL); + if (!len) { + break; + } + TEST_ASSERT_TRUE(test_ble_log_walk_frames( + s_read_buf, len, capture_sequence_frame, &set)); + } +} + +static void auto_recycle_noop(void *ctx) +{ + (void)ctx; +} + +TEST_CASE("BLE Log preserves LL payload and rejects oversized records", + "[ble_log][lbm][wire]") +{ + enum { + CUSTOM_BEFORE, + CUSTOM_OVERSIZED, + CUSTOM_AFTER, +#if CONFIG_BLE_LOG_LL_ENABLED + LL_BEFORE, + LL_PRIMARY_OVERSIZED, + LL_APPEND_OVERSIZED, + LL_AFTER, +#endif + SEQUENCE_CAPTURE_COUNT, + }; + static uint8_t custom_oversized_payload[ + BLE_LOG_MAX_PAYLOAD_LEN - sizeof(uint32_t) + 1]; +#if CONFIG_BLE_LOG_LL_ENABLED + static uint8_t ll_oversized_payload[BLE_LOG_MAX_PAYLOAD_LEN + 1]; + static uint8_t ll_oversized_append[BLE_LOG_MAX_PAYLOAD_LEN]; +#endif + const uint8_t custom_before = 0x61; + const uint8_t custom_oversized = 0x62; + const uint8_t custom_after = 0x63; +#if CONFIG_BLE_LOG_LL_ENABLED + const uint8_t ll_before = 0x64; + const uint8_t ll_primary_oversized = 0x65; + const uint8_t ll_append_oversized = 0x66; + const uint8_t ll_after = 0x67; +#endif + sequence_capture_t captures[SEQUENCE_CAPTURE_COUNT] = { + [CUSTOM_BEFORE] = {.src = BLE_LOG_SRC_CUSTOM, .marker = custom_before}, + [CUSTOM_OVERSIZED] = { + .src = BLE_LOG_SRC_CUSTOM, + .marker = custom_oversized, + }, + [CUSTOM_AFTER] = {.src = BLE_LOG_SRC_CUSTOM, .marker = custom_after}, +#if CONFIG_BLE_LOG_LL_ENABLED + [LL_BEFORE] = {.src = BLE_LOG_SRC_LL_TASK, .marker = ll_before}, + [LL_PRIMARY_OVERSIZED] = { + .src = BLE_LOG_SRC_LL_TASK, + .marker = ll_primary_oversized, + }, + [LL_APPEND_OVERSIZED] = { + .src = BLE_LOG_SRC_LL_TASK, + .marker = ll_append_oversized, + }, + [LL_AFTER] = {.src = BLE_LOG_SRC_LL_TASK, .marker = ll_after}, +#endif + }; + + TEST_ASSERT_TRUE(ble_log_enable(true)); + ble_log_lbm_flush_open_trans(); + for (int round = 0; round < 2; round++) { + TEST_ASSERT_TRUE(ble_log_rt_drain()); + while (ble_log_prph_test_read(s_read_buf, sizeof(s_read_buf), + 0, 0, NULL) > 0) { + } + } + + TEST_ASSERT_TRUE(ble_log_write_hex(BLE_LOG_SRC_CUSTOM, + &custom_before, sizeof(custom_before))); + memset(custom_oversized_payload, 0, sizeof(custom_oversized_payload)); + custom_oversized_payload[0] = custom_oversized; + TEST_ASSERT_FALSE(ble_log_write_hex(BLE_LOG_SRC_CUSTOM, + custom_oversized_payload, + sizeof(custom_oversized_payload))); + TEST_ASSERT_TRUE(ble_log_write_hex(BLE_LOG_SRC_CUSTOM, + &custom_after, sizeof(custom_after))); + +#if CONFIG_BLE_LOG_LL_ENABLED + ble_log_write_hex_ll(sizeof(ll_before), &ll_before, 0, NULL, + BIT(BLE_LOG_LL_FLAG_TASK)); + memset(ll_oversized_payload, 0, sizeof(ll_oversized_payload)); + ll_oversized_payload[0] = ll_primary_oversized; + ble_log_write_hex_ll(sizeof(ll_oversized_payload), ll_oversized_payload, + 0, NULL, BIT(BLE_LOG_LL_FLAG_TASK)); + memset(ll_oversized_append, 0, sizeof(ll_oversized_append)); + ble_log_write_hex_ll(sizeof(ll_append_oversized), &ll_append_oversized, + sizeof(ll_oversized_append), ll_oversized_append, + BIT(BLE_LOG_LL_FLAG_TASK)); + ble_log_write_hex_ll(sizeof(ll_after), &ll_after, 0, NULL, + BIT(BLE_LOG_LL_FLAG_TASK)); +#endif + + ble_log_lbm_flush_open_trans(); + TEST_ASSERT_TRUE(ble_log_rt_drain()); + read_sequence_frames(captures, SEQUENCE_CAPTURE_COUNT); + + TEST_ASSERT_TRUE(captures[CUSTOM_BEFORE].found); + TEST_ASSERT_FALSE(captures[CUSTOM_OVERSIZED].found); + TEST_ASSERT_TRUE(captures[CUSTOM_AFTER].found); + /* Periodic snapshots may allocate additional Global SNs between writes. */ + uint32_t custom_delta = (captures[CUSTOM_AFTER].sn - + captures[CUSTOM_BEFORE].sn) & 0x00ffffffU; + TEST_ASSERT_GREATER_OR_EQUAL_UINT32(2, custom_delta); + TEST_ASSERT_LESS_THAN_UINT32(0x00800000U, custom_delta); +#if CONFIG_BLE_LOG_LL_ENABLED + TEST_ASSERT_TRUE(captures[LL_BEFORE].found); + TEST_ASSERT_FALSE(captures[LL_PRIMARY_OVERSIZED].found); + TEST_ASSERT_FALSE(captures[LL_APPEND_OVERSIZED].found); + TEST_ASSERT_TRUE(captures[LL_AFTER].found); + uint32_t ll_delta = (captures[LL_AFTER].sn - captures[LL_BEFORE].sn) & 0x00ffffffU; + TEST_ASSERT_GREATER_OR_EQUAL_UINT32(3, ll_delta); + TEST_ASSERT_LESS_THAN_UINT32(0x00800000U, ll_delta); +#endif +} + +TEST_CASE("BLE Log flush snapshot consumes the continuous Global SN", + "[ble_log][wire]") +{ + const uint8_t before_marker = 0x71; + const uint8_t after_marker = 0x72; + TEST_ASSERT_TRUE(ble_log_enable(true)); + ble_log_lbm_flush_open_trans(); + for (int round = 0; round < 2; round++) { + TEST_ASSERT_TRUE(ble_log_rt_drain()); + while (ble_log_prph_test_read(s_read_buf, sizeof(s_read_buf), + 0, 0, NULL) > 0) { + } + } + + TEST_ASSERT_TRUE(ble_log_write_hex(BLE_LOG_SRC_CUSTOM, + &before_marker, sizeof(before_marker))); + ble_log_lbm_flush_open_trans(); + TEST_ASSERT_TRUE(ble_log_rt_drain()); + sequence_capture_t before = { + .src = BLE_LOG_SRC_CUSTOM, + .marker = before_marker, + }; + read_sequence_frames(&before, 1); + TEST_ASSERT_TRUE(before.found); + + /* The test peripheral normally recycles only when read. Auto-recycle lets + * synchronous flush observe completion without a second reader task. */ + ble_log_prph_test_set_auto_recycle_hook(auto_recycle_noop, NULL); + ble_log_flush(); + ble_log_prph_test_set_auto_recycle_hook(NULL, NULL); + + TEST_ASSERT_TRUE(ble_log_write_hex(BLE_LOG_SRC_CUSTOM, + &after_marker, sizeof(after_marker))); + ble_log_lbm_flush_open_trans(); + TEST_ASSERT_TRUE(ble_log_rt_drain()); + sequence_capture_t after = { + .src = BLE_LOG_SRC_CUSTOM, + .marker = after_marker, + }; + read_sequence_frames(&after, 1); + TEST_ASSERT_TRUE(after.found); + /* At least the FLUSH snapshot consumes a Global SN between the logs; + * periodic snapshots may consume additional numbers during the reads. */ + TEST_ASSERT_GREATER_OR_EQUAL_UINT32( + 2, (after.sn - before.sn) & 0x00ffffffU); + TEST_ASSERT_LESS_THAN_UINT32(0x00800000U, (after.sn - before.sn) & 0x00ffffffU); +} + +/* ------- pending-seal and deinit drain ------- */ + +#define TEST_MARKER_CHUNK_MAX (4) + +typedef struct { + uint8_t markers[TEST_MARKER_CHUNK_MAX]; + int count; +} marker_chunk_capture_t; + +static void capture_custom_marker(const test_ble_log_frame_t *frame, void *ctx) +{ + marker_chunk_capture_t *capture = ctx; + if (frame->src == BLE_LOG_SRC_CUSTOM && + frame->payload_len == sizeof(uint32_t) + 1 && + capture->count < TEST_MARKER_CHUNK_MAX) { + capture->markers[capture->count++] = frame->payload[sizeof(uint32_t)]; + } +} + +/* Reads every pending transport chunk, keeping only the chunks that carry + * CUSTOM marker frames; interleaved snapshot transports are consumed and + * ignored. */ +static int read_marker_chunks(marker_chunk_capture_t *chunks, int max_chunks) +{ + int marker_chunks = 0; + for (int i = 0; i < BLE_LOG_TRANS_TOTAL_CNT; i++) { + size_t len = ble_log_prph_test_read( + s_read_buf, sizeof(s_read_buf), pdMS_TO_TICKS(TEST_READ_TIMEOUT_MS), + 0, NULL); + if (!len) { + break; + } + marker_chunk_capture_t chunk = {0}; + TEST_ASSERT_TRUE(test_ble_log_walk_frames(s_read_buf, len, + capture_custom_marker, + &chunk)); + if (chunk.count && marker_chunks < max_chunks) { + chunks[marker_chunks++] = chunk; + } + } + return marker_chunks; +} + +static void auto_recycle_count(void *ctx) +{ + (*(int *)ctx)++; +} + +TEST_CASE("BLE Log pending-seal marker defers the busy transport flush", + "[ble_log][lbm]") +{ + const uint8_t first_marker = 0xd1; + const uint8_t cursor_marker = 0xd2; + const uint8_t hooked_marker = 0xd3; + const uint8_t post_marker = 0xd4; + + TEST_ASSERT_TRUE(ble_log_enable(true)); + ble_log_lbm_flush_open_trans(); + for (int round = 0; round < 2; round++) { + TEST_ASSERT_TRUE(ble_log_rt_drain()); + while (ble_log_prph_test_read(s_read_buf, sizeof(s_read_buf), + 0, 0, NULL) > 0) { + } + } + + /* Park one frame in an OPEN transport; the claim cursor stays on it. */ + TEST_ASSERT_TRUE(ble_log_write_hex(BLE_LOG_SRC_CUSTOM, + &first_marker, sizeof(first_marker))); + + /* A prior test may leave open_cursor at any recycled transport. Prime + * it through either the direct path or the OPEN bitmap scan before + * arming the direct-path hook. */ + TEST_ASSERT_TRUE(ble_log_write_hex(BLE_LOG_SRC_CUSTOM, + &cursor_marker, sizeof(cursor_marker))); + + /* The armed hook runs a flush while the claiming writer itself holds + * the transport lock: the flush must skip the busy transport and leave + * the pending-seal marker for the NEXT claim. */ + s_locked_hook_armed = true; + bool hooked_write = ble_log_write_hex(BLE_LOG_SRC_CUSTOM, + &hooked_marker, + sizeof(hooked_marker)); + bool hook_ran = !s_locked_hook_armed; + s_locked_hook_armed = false; + TEST_ASSERT_TRUE(hooked_write); + TEST_ASSERT_TRUE(hook_ran); + + /* The next writer must seal the flagged transport first and place its + * own frame in another one. */ + TEST_ASSERT_TRUE(ble_log_write_hex(BLE_LOG_SRC_CUSTOM, + &post_marker, sizeof(post_marker))); + + ble_log_lbm_flush_open_trans(); + TEST_ASSERT_TRUE(ble_log_rt_drain()); + + marker_chunk_capture_t chunks[2] = {0}; + TEST_ASSERT_EQUAL(2, read_marker_chunks(chunks, 2)); + TEST_ASSERT_EQUAL(3, chunks[0].count); + TEST_ASSERT_EQUAL_HEX8(first_marker, chunks[0].markers[0]); + TEST_ASSERT_EQUAL_HEX8(cursor_marker, chunks[0].markers[1]); + TEST_ASSERT_EQUAL_HEX8(hooked_marker, chunks[0].markers[2]); + TEST_ASSERT_EQUAL(1, chunks[1].count); + TEST_ASSERT_EQUAL_HEX8(post_marker, chunks[1].markers[0]); +} + +TEST_CASE("BLE Log deinit drain delivers parked open transports", + "[ble_log][lbm]") +{ + const uint8_t markers[TEST_MARKER_CHUNK_MAX] = {0xe1, 0xe2, 0xe3, 0xe4}; + + TEST_ASSERT_TRUE(ble_log_enable(true)); + ble_log_lbm_flush_open_trans(); + for (int round = 0; round < 2; round++) { + TEST_ASSERT_TRUE(ble_log_rt_drain()); + while (ble_log_prph_test_read(s_read_buf, sizeof(s_read_buf), + 0, 0, NULL) > 0) { + } + } + + /* Sub-capacity burst: frames park in an OPEN transport that no + * capacity seal ever sends. */ + for (int i = 0; i < TEST_MARKER_CHUNK_MAX; i++) { + TEST_ASSERT_TRUE(ble_log_write_hex(BLE_LOG_SRC_CUSTOM, + &markers[i], 1)); + } + + /* Exercise the exact deinit-drain contract: close the producer gate and + * wait for writers before sealing every OPEN transport. */ + ble_log_lbm_begin_deinit(); + ble_log_lbm_drain_open_trans(); + + marker_chunk_capture_t chunks[1] = {0}; + int chunk_count = read_marker_chunks(chunks, 1); + + /* Finish the partially-entered teardown and restore the module before + * assertions so the following Unity case starts from a valid lifetime. */ + ble_log_deinit(); + TEST_ASSERT_TRUE(ble_log_init()); + + TEST_ASSERT_EQUAL(1, chunk_count); + TEST_ASSERT_EQUAL(TEST_MARKER_CHUNK_MAX, chunks[0].count); + for (int i = 0; i < TEST_MARKER_CHUNK_MAX; i++) { + TEST_ASSERT_EQUAL_HEX8(markers[i], chunks[0].markers[i]); + } +} + +TEST_CASE("BLE Log deinit hands residual transports to the peripheral", + "[ble_log][lbm]") +{ + const uint8_t marker = 0xf1; + int recycled = 0; + + TEST_ASSERT_TRUE(ble_log_enable(true)); + ble_log_lbm_flush_open_trans(); + for (int round = 0; round < 2; round++) { + TEST_ASSERT_TRUE(ble_log_rt_drain()); + while (ble_log_prph_test_read(s_read_buf, sizeof(s_read_buf), + 0, 0, NULL) > 0) { + } + } + + TEST_ASSERT_TRUE(ble_log_write_hex(BLE_LOG_SRC_CUSTOM, + &marker, sizeof(marker))); + + /* Auto-recycle turns every dispatched transport into a countable + * event, so the real deinit path is observable without a reader + * task racing the teardown. */ + ble_log_prph_test_set_auto_recycle_hook(auto_recycle_count, &recycled); + ble_log_deinit(); + TEST_ASSERT_GREATER_OR_EQUAL(1, recycled); + TEST_ASSERT_TRUE(ble_log_init()); +} + +typedef struct { + SemaphoreHandle_t started; + SemaphoreHandle_t done; + bool result; +} blocked_writer_ctx_t; + +static void blocked_write_task(void *arg) +{ + blocked_writer_ctx_t *ctx = arg; + static const uint8_t marker = 0x57; + xSemaphoreGive(ctx->started); + ctx->result = ble_log_write_hex(BLE_LOG_SRC_CUSTOM, &marker, + sizeof(marker)); + xSemaphoreGive(ctx->done); + vTaskDelete(NULL); +} + +static void blocked_claim_task(void *arg) +{ + blocked_writer_ctx_t *ctx = arg; + uint32_t handle; + xSemaphoreGive(ctx->started); + uint8_t *payload = ble_log_claim(BLE_LOG_SRC_ENCODE, 1, &handle, true); + ctx->result = payload != NULL; + if (payload) { + payload[0] = 0x58; + } + ble_log_commit(handle, 1); + xSemaphoreGive(ctx->done); + vTaskDelete(NULL); +} + +typedef struct { + bool write_frame; + bool claim_frame; +} blocked_capture_t; + +static void capture_blocked_markers(const test_ble_log_frame_t *frame, void *ctx) +{ + blocked_capture_t *capture = ctx; + if (frame->payload_len != sizeof(uint32_t) + 1) { + return; + } + uint8_t marker = frame->payload[sizeof(uint32_t)]; + if (frame->src == BLE_LOG_SRC_CUSTOM && marker == 0x57) { + capture->write_frame = true; + } else if (frame->src == BLE_LOG_SRC_ENCODE && marker == 0x58) { + capture->claim_frame = true; + } +} + +TEST_CASE("BLE Log task writers wait for a shared transport", "[ble_log][lbm]") +{ + static const uint8_t full_payload[ + BLE_LOG_MAX_PAYLOAD_LEN - sizeof(uint32_t)] = {0}; + + TEST_ASSERT_TRUE(ble_log_enable(true)); + ble_log_lbm_flush_open_trans(); + for (int round = 0; round < 2; round++) { + TEST_ASSERT_TRUE(ble_log_rt_drain()); + while (ble_log_prph_test_read(s_read_buf, sizeof(s_read_buf), + 0, 0, NULL) > 0) { + } + } + + /* Keep every task-usable transport SENDING in the test peripheral. */ + for (int i = 0; i < BLE_LOG_POOL_SHARED_CNT; i++) { + TEST_ASSERT_TRUE(ble_log_write_hex(BLE_LOG_SRC_CUSTOM, full_payload, + sizeof(full_payload))); + } + + blocked_writer_ctx_t writers[2] = {0}; + for (int i = 0; i < 2; i++) { + writers[i].started = xSemaphoreCreateBinary(); + writers[i].done = xSemaphoreCreateBinary(); + TEST_ASSERT_NOT_NULL(writers[i].started); + TEST_ASSERT_NOT_NULL(writers[i].done); + } + TEST_ASSERT_EQUAL(pdTRUE, + xTaskCreate(blocked_write_task, "ble_log_wait1", + TEST_LIFECYCLE_STACK_SIZE, &writers[0], + TEST_LIFECYCLE_PRIO, NULL)); + TEST_ASSERT_EQUAL(pdTRUE, + xTaskCreate(blocked_claim_task, "ble_log_wait2", + TEST_LIFECYCLE_STACK_SIZE, &writers[1], + TEST_LIFECYCLE_PRIO, NULL)); + for (int i = 0; i < 2; i++) { + TEST_ASSERT_TRUE(xSemaphoreTake(writers[i].started, + pdMS_TO_TICKS(1000))); + } + vTaskDelay(1); + /* No shared transport is free: both yieldable writers must be parked. */ + for (int i = 0; i < 2; i++) { + TEST_ASSERT_EQUAL(pdFALSE, xSemaphoreTake(writers[i].done, 0)); + } + + /* Recycling transports wakes the parked writers; neither write is lost. */ + TEST_ASSERT_TRUE(ble_log_rt_drain()); + for (int i = 0; i < 2; i++) { + size_t len = ble_log_prph_test_read(s_read_buf, sizeof(s_read_buf), + pdMS_TO_TICKS(TEST_READ_TIMEOUT_MS), + 0, NULL); + TEST_ASSERT_GREATER_THAN_size_t(0, len); + } + for (int i = 0; i < 2; i++) { + TEST_ASSERT_TRUE(xSemaphoreTake(writers[i].done, pdMS_TO_TICKS(1000))); + TEST_ASSERT_TRUE(writers[i].result); + } + + ble_log_lbm_flush_open_trans(); + TEST_ASSERT_TRUE(ble_log_rt_drain()); + blocked_capture_t capture = {0}; + for (int i = 0; i < BLE_LOG_TRANS_TOTAL_CNT; i++) { + size_t len = ble_log_prph_test_read(s_read_buf, sizeof(s_read_buf), + pdMS_TO_TICKS(TEST_READ_TIMEOUT_MS), + 0, NULL); + if (!len) { + break; + } + TEST_ASSERT_TRUE(test_ble_log_walk_frames(s_read_buf, len, + capture_blocked_markers, + &capture)); + } + TEST_ASSERT_TRUE(capture.write_frame); + TEST_ASSERT_TRUE(capture.claim_frame); + + for (int i = 0; i < 2; i++) { + vSemaphoreDelete(writers[i].started); + vSemaphoreDelete(writers[i].done); + } +} + +typedef struct { + SemaphoreHandle_t done; + bool write_result; +} stale_writer_ctx_t; + +static void stale_candidate_writer_task(void *arg) +{ + stale_writer_ctx_t *ctx = arg; + (void)xSemaphoreTake(s_stale_writer_start, portMAX_DELAY); + xSemaphoreGive(s_stale_writer_started); + const uint8_t marker = 0x61; + ctx->write_result = ble_log_write_hex(BLE_LOG_SRC_CUSTOM, &marker, 1); + xSemaphoreGive(ctx->done); + vTaskDelete(NULL); +} + +TEST_CASE("BLE Log flush re-advertises a stale candidate for parked writers", + "[ble_log][lbm]") +{ + /* Deterministic lost-wakeup interleaving: the periodic flusher reads an + * OPEN hint, the hinted transport is sealed, sent and recycled before + * the flusher takes its candidate lock, and a task writer registers + * and parks while that lock is held. Only the flusher's release can + * then re-advertise the FREE transport; a bare unlock loses the + * writer. */ + TEST_ASSERT_TRUE(ble_log_enable(true)); + ble_log_lbm_flush_open_trans(); + TEST_ASSERT_TRUE(ble_log_rt_drain()); + while (ble_log_prph_test_read(s_read_buf, sizeof(s_read_buf), 0, 0, NULL) > 0) { + } + + /* Pin every shared transport but one with an open claim: claimed + * transports are in no bitmap and never enter the runtime queue. */ + uint32_t pinned[BLE_LOG_POOL_TRANS_CNT] = {0}; + int pinned_cnt = 0; + for (; pinned_cnt < BLE_LOG_POOL_SHARED_CNT - 1; pinned_cnt++) { + uint8_t *payload = ble_log_claim(BLE_LOG_SRC_ENCODE, 1, + &pinned[pinned_cnt], false); + TEST_ASSERT_NOT_NULL(payload); + } + + /* The last shared transport stays OPEN, so the armed flush targets it. */ + const uint8_t marker = 0x61; + TEST_ASSERT_TRUE(ble_log_write_hex(BLE_LOG_SRC_CUSTOM, &marker, 1)); + + stale_writer_ctx_t ctx = {.done = xSemaphoreCreateBinary()}; + s_stale_writer_start = xSemaphoreCreateBinary(); + s_stale_writer_started = xSemaphoreCreateBinary(); + TEST_ASSERT_NOT_NULL(ctx.done); + TEST_ASSERT_NOT_NULL(s_stale_writer_start); + TEST_ASSERT_NOT_NULL(s_stale_writer_started); + TEST_ASSERT_EQUAL(pdTRUE, + xTaskCreate(stale_candidate_writer_task, "ble_log_stale", + TEST_LIFECYCLE_STACK_SIZE, &ctx, + TEST_LIFECYCLE_PRIO, NULL)); + + s_flush_hint_id = -1; + s_flush_hint_drained = false; + s_flush_hint_hook_armed = true; + s_flush_stale_hook_armed = true; + ble_log_lbm_flush_open_trans(); + TEST_ASSERT_TRUE(s_flush_hint_drained); + TEST_ASSERT_TRUE(s_flush_hint_id >= 0); + + bool completed = xSemaphoreTake(ctx.done, pdMS_TO_TICKS(1000)) == pdTRUE; + if (!completed) { + /* Lost wake: un-park the writer, release the pinned claims and + * restore the gate before failing, so later cases stay clean. */ + TEST_ASSERT_TRUE(ble_log_enable(false)); + TEST_ASSERT_EQUAL(pdTRUE, xSemaphoreTake(ctx.done, pdMS_TO_TICKS(1000))); + TEST_ASSERT_FALSE(ctx.write_result); + for (int i = 0; i < pinned_cnt; i++) { + ble_log_commit(pinned[i], 0); + } + TEST_ASSERT_TRUE(ble_log_enable(true)); + TEST_FAIL_MESSAGE("stale-candidate release lost the parked writer's wake"); + } + TEST_ASSERT_TRUE(ctx.write_result); + + for (int i = 0; i < pinned_cnt; i++) { + ble_log_commit(pinned[i], 0); + } + + /* The recovered writer's frame reaches the peripheral. */ + ble_log_lbm_flush_open_trans(); + TEST_ASSERT_TRUE(ble_log_rt_drain()); + TEST_ASSERT_GREATER_THAN_size_t( + 0, ble_log_prph_test_read(s_read_buf, sizeof(s_read_buf), + pdMS_TO_TICKS(TEST_READ_TIMEOUT_MS), 0, NULL)); + + vSemaphoreDelete(ctx.done); + vSemaphoreDelete(s_stale_writer_start); + vSemaphoreDelete(s_stale_writer_started); +} + +typedef struct { + SemaphoreHandle_t done; + uint8_t marker; + bool result; + bool finished; +} conc_writer_ctx_t; + +#define CONC_WRITER_CNT (4) +#define CONC_FRAMES_EACH (96) + +static void conc_writer_task(void *arg) +{ + conc_writer_ctx_t *ctx = arg; + for (uint16_t i = 0; i < CONC_FRAMES_EACH; i++) { + uint8_t payload[3] = {ctx->marker, (uint8_t)(i >> 8), (uint8_t)i}; + if (!ble_log_write_hex(BLE_LOG_SRC_CUSTOM, payload, sizeof(payload))) { + ctx->result = false; + break; + } + } + xSemaphoreGive(ctx->done); + vTaskDelete(NULL); +} + +typedef struct { + size_t frames; + size_t per_marker[CONC_WRITER_CNT]; + uint32_t sn[CONC_WRITER_CNT * CONC_FRAMES_EACH]; + size_t sn_count; +} conc_capture_t; + +static void capture_conc_frame(const test_ble_log_frame_t *frame, void *ctx_) +{ + conc_capture_t *cap = ctx_; + if (frame->src != BLE_LOG_SRC_CUSTOM || frame->payload_len != 4 + 3) { + return; + } + cap->frames++; + uint8_t marker = frame->payload[4]; + for (int w = 0; w < CONC_WRITER_CNT; w++) { + if (marker == 0xa0 + w) { + cap->per_marker[w]++; + } + } + if (cap->sn_count < CONC_WRITER_CNT * CONC_FRAMES_EACH) { + cap->sn[cap->sn_count++] = frame->sn; + } +} + +typedef struct { + SemaphoreHandle_t done; + uint32_t hammer_cnt; + bool taken; +} hammer_ctx_t; + +static void hammer_claim_task(void *arg) +{ + hammer_ctx_t *ctx = arg; + uint32_t committed = 0; + const uint8_t marker = 0xb0; + for (int i = 0; i < 4000; i++) { + uint32_t handle; + uint8_t *payload = ble_log_claim(BLE_LOG_SRC_ENCODE, 1, + &handle, false); + if (payload) { + payload[0] = marker; + ble_log_commit(handle, 1); + committed++; + } + } + ctx->hammer_cnt = committed; + xSemaphoreGive(ctx->done); + vTaskDelete(NULL); +} + +typedef struct { + uint32_t sn[CONC_WRITER_CNT * CONC_FRAMES_EACH + 16 * 1024]; + size_t sn_count; +} hammer_capture_t; + +static void capture_hammer_frame(const test_ble_log_frame_t *frame, void *ctx_) +{ + hammer_capture_t *cap = ctx_; + if (frame->src == BLE_LOG_SRC_ENCODE && + cap->sn_count < CONC_WRITER_CNT * CONC_FRAMES_EACH + 16 * 1024) { + cap->sn[cap->sn_count++] = frame->sn; + } +} + +static size_t count_unique_sn(const uint32_t *sn, size_t n) +{ + size_t unique = 0; + for (size_t i = 0; i < n; i++) { + bool first = true; + for (size_t j = 0; j < i; j++) { + if (sn[j] == sn[i]) { + first = false; + break; + } + } + unique += first; + } + return unique; +} + +TEST_CASE("BLE Log concurrent writers keep frames intact and SNs unique", + "[ble_log][lbm]") +{ + /* Scenario: 4 writers run in parallel across all cores, each emitting + * CONC_FRAMES_EACH marked frames while the pool recycles under them. + * Guards (mutation -> expected failure): + * - per-transport CAS lock weakened to plain test-and-set: two cores + * co-own one transport, frames interleave -> checksum walk fails. + * - Global SN fetch_add weakened to plain increment: concurrent RMWs + * lose updates -> duplicate SNs -> unique-count assertion fails. + * Every completed write must be present exactly once. */ + TEST_ASSERT_TRUE(ble_log_enable(true)); + ble_log_lbm_flush_open_trans(); + for (int round = 0; round < 2; round++) { + TEST_ASSERT_TRUE(ble_log_rt_drain()); + while (ble_log_prph_test_read(s_read_buf, sizeof(s_read_buf), + 0, 0, NULL) > 0) { + } + } + + conc_writer_ctx_t ctx[CONC_WRITER_CNT]; + static conc_capture_t cap; + memset(&cap, 0, sizeof(cap)); + for (int w = 0; w < CONC_WRITER_CNT; w++) { + ctx[w] = (conc_writer_ctx_t){ + .done = xSemaphoreCreateBinary(), + .marker = (uint8_t)(0xa0 + w), + .result = true, + }; + TEST_ASSERT_NOT_NULL(ctx[w].done); + } + for (int w = 0; w < CONC_WRITER_CNT; w++) { + TEST_ASSERT_EQUAL(pdTRUE, xTaskCreatePinnedToCore( + conc_writer_task, "ble_log_conc", + TEST_LIFECYCLE_STACK_SIZE, &ctx[w], + TEST_LIFECYCLE_PRIO, NULL, + w % portNUM_PROCESSORS)); + } + /* Feed the pool while the writers run; the main task is the + * consumer. */ + for (;;) { + bool all_done = true; + for (int w = 0; w < CONC_WRITER_CNT; w++) { + if (!ctx[w].finished) { + if (xSemaphoreTake(ctx[w].done, 0) == pdTRUE) { + ctx[w].finished = true; + TEST_ASSERT_TRUE(ctx[w].result); + vSemaphoreDelete(ctx[w].done); + } else { + all_done = false; + } + } + } + if (all_done) { + break; + } + TEST_ASSERT_TRUE(ble_log_rt_drain()); + size_t len = ble_log_prph_test_read(s_read_buf, sizeof(s_read_buf), + pdMS_TO_TICKS(20), 0, NULL); + if (len) { + TEST_ASSERT_TRUE(test_ble_log_walk_frames( + s_read_buf, len, capture_conc_frame, &cap)); + } + } + + ble_log_lbm_flush_open_trans(); + TEST_ASSERT_TRUE(ble_log_rt_drain()); + for (;;) { + size_t len = ble_log_prph_test_read(s_read_buf, sizeof(s_read_buf), + pdMS_TO_TICKS(TEST_READ_TIMEOUT_MS), + 0, NULL); + if (!len) { + break; + } + TEST_ASSERT_TRUE(test_ble_log_walk_frames(s_read_buf, len, + capture_conc_frame, &cap)); + } + TEST_ASSERT_EQUAL_size_t(CONC_WRITER_CNT * CONC_FRAMES_EACH, cap.frames); + for (int w = 0; w < CONC_WRITER_CNT; w++) { + TEST_ASSERT_EQUAL_size_t(CONC_FRAMES_EACH, cap.per_marker[w]); + } + /* Unique Global SNs: a lost fetch_add update surfaces as two frames + * sharing one SN. */ + TEST_ASSERT_EQUAL_size_t(CONC_WRITER_CNT * CONC_FRAMES_EACH, + count_unique_sn(cap.sn, cap.sn_count)); + + /* Phase 2 - SN hammer: wait=false claims in tight loops across all + * cores. The SN fetch_add window dominates the failing-claim path, so + * a plain (non-atomic) increment loses updates at a high rate. Losses + * are expected and allowed; only SN uniqueness is asserted. */ + enum { HAMMER_TASKS = 4 }; + hammer_ctx_t hctx[HAMMER_TASKS]; + for (int t = 0; t < HAMMER_TASKS; t++) { + hctx[t] = (hammer_ctx_t){.done = xSemaphoreCreateBinary()}; + TEST_ASSERT_NOT_NULL(hctx[t].done); + TEST_ASSERT_EQUAL(pdTRUE, xTaskCreatePinnedToCore( + hammer_claim_task, "ble_log_hammer", + TEST_LIFECYCLE_STACK_SIZE, &hctx[t], + TEST_LIFECYCLE_PRIO, NULL, + t % portNUM_PROCESSORS)); + } + /* Feed the pool while the hammers run. */ + static hammer_capture_t hcap; + memset(&hcap, 0, sizeof(hcap)); + static bool taken[HAMMER_TASKS]; + memset(taken, 0, sizeof(taken)); + for (int finished = 0; finished < HAMMER_TASKS;) { + for (int t = 0; t < HAMMER_TASKS; t++) { + if (!taken[t] && + xSemaphoreTake(hctx[t].done, 0) == pdTRUE) { + taken[t] = true; + finished++; + vSemaphoreDelete(hctx[t].done); + } + } + TEST_ASSERT_TRUE(ble_log_rt_drain()); + size_t len = ble_log_prph_test_read(s_read_buf, sizeof(s_read_buf), + pdMS_TO_TICKS(2), 0, NULL); + if (len) { + TEST_ASSERT_TRUE(test_ble_log_walk_frames( + s_read_buf, len, capture_hammer_frame, &hcap)); + } + } + ble_log_lbm_flush_open_trans(); + TEST_ASSERT_TRUE(ble_log_rt_drain()); + for (;;) { + size_t len = ble_log_prph_test_read(s_read_buf, sizeof(s_read_buf), + pdMS_TO_TICKS(TEST_READ_TIMEOUT_MS), + 0, NULL); + if (!len) { + break; + } + TEST_ASSERT_TRUE(test_ble_log_walk_frames(s_read_buf, len, + capture_hammer_frame, &hcap)); + } + TEST_ASSERT_GREATER_THAN_size_t(0, hcap.sn_count); + TEST_ASSERT_EQUAL_size_t(hcap.sn_count, + count_unique_sn(hcap.sn, hcap.sn_count)); +} + +typedef struct { + SemaphoreHandle_t done; + bool result; +} soak_writer_ctx_t; + +static void soak_writer_task(void *arg) +{ + soak_writer_ctx_t *ctx = arg; + const uint8_t marker = 0x5c; + ctx->result = ble_log_write_hex(BLE_LOG_SRC_CUSTOM, &marker, 1); + xSemaphoreGive(ctx->done); + vTaskDelete(NULL); +} + +TEST_CASE("BLE Log waiter handshake survives continuous park/wake turnover", + "[ble_log][lbm]") +{ + /* Scenario: each round fills every shared transport, parks two writers, + * then recycles transports one at a time. Each single recycle is the + * last-availability event for exactly one waiter: if the register/ + * publish SC-fence handshake is weakened, a waiter can miss the final + * notification and stay parked while capacity exists. + * Mutation (drop the SEQ_CST fences in the waiter/notify handshake) + * -> probabilistic: some round times out. This is a soak: it raises + * the hit probability, it cannot make the window deterministic. */ + TEST_ASSERT_TRUE(ble_log_enable(true)); + static const uint8_t full[ + BLE_LOG_MAX_PAYLOAD_LEN - sizeof(uint32_t)] = {0}; + + for (int round = 0; round < 64; round++) { + ble_log_lbm_flush_open_trans(); + TEST_ASSERT_TRUE(ble_log_rt_drain()); + while (ble_log_prph_test_read(s_read_buf, sizeof(s_read_buf), + 0, 0, NULL) > 0) { + } + for (int i = 0; i < BLE_LOG_POOL_SHARED_CNT; i++) { + TEST_ASSERT_TRUE(ble_log_write_hex(BLE_LOG_SRC_CUSTOM, full, + sizeof(full))); + } + soak_writer_ctx_t w[2]; + for (int i = 0; i < 2; i++) { + w[i] = (soak_writer_ctx_t){ + .done = xSemaphoreCreateBinary(), + .result = false, + }; + TEST_ASSERT_NOT_NULL(w[i].done); + TEST_ASSERT_EQUAL(pdTRUE, xTaskCreatePinnedToCore( + soak_writer_task, "ble_log_soak", + TEST_LIFECYCLE_STACK_SIZE, &w[i], + TEST_LIFECYCLE_PRIO, NULL, + i % portNUM_PROCESSORS)); + } + /* Let both writers scan twice and park. */ + vTaskDelay(pdMS_TO_TICKS(20)); + /* Recycle exactly one transport at a time: each read is one + * waiter's last chance to be notified. */ + TEST_ASSERT_TRUE(ble_log_rt_drain()); + for (int i = 0; i < 2; i++) { + size_t len = ble_log_prph_test_read(s_read_buf, sizeof(s_read_buf), + pdMS_TO_TICKS(TEST_READ_TIMEOUT_MS), + 0, NULL); + TEST_ASSERT_GREATER_THAN_size_t(0, len); + } + for (int i = 0; i < 2; i++) { + TEST_ASSERT_TRUE(xSemaphoreTake(w[i].done, pdMS_TO_TICKS(2000))); + TEST_ASSERT_TRUE(w[i].result); + vSemaphoreDelete(w[i].done); + } + } +} + +typedef struct { + SemaphoreHandle_t done; + bool write_result; + bool claim_result; +} timer_writer_ctx_t; + +static void timer_writer_cb(void *arg) +{ + timer_writer_ctx_t *ctx = arg; + const uint8_t marker = 0x59; + ctx->write_result = ble_log_write_hex(BLE_LOG_SRC_CUSTOM, &marker, 1); + uint32_t handle; + uint8_t *payload = ble_log_claim(BLE_LOG_SRC_ENCODE, 1, &handle, true); + ctx->claim_result = payload != NULL; + if (payload) { + payload[0] = marker; + ble_log_commit(handle, 1); + } +#if CONFIG_BLE_LOG_LL_ENABLED + ble_log_write_hex_ll(1, &marker, 0, NULL, BIT(BLE_LOG_LL_FLAG_TASK)); +#endif +#if CONFIG_BLE_HOST_COMPRESSED_LOG_ENABLE + ble_log_compressed_hex_print(BLE_COMPRESSED_LOG_OUT_SOURCE_HOST, 0x730, 0); +#endif + xSemaphoreGive(ctx->done); +} + +TEST_CASE("BLE Log ESP Timer writers never wait for shared transports", + "[ble_log][lbm]") +{ + static const uint8_t full_payload[ + BLE_LOG_MAX_PAYLOAD_LEN - sizeof(uint32_t)] = {0}; + TEST_ASSERT_TRUE(ble_log_enable(true)); + ble_log_lbm_flush_open_trans(); + TEST_ASSERT_TRUE(ble_log_rt_drain()); + while (ble_log_prph_test_read(s_read_buf, sizeof(s_read_buf), 0, 0, NULL) > 0) { + } + /* The test peripheral holds these transports until explicitly read. + * Reserve capacity remains available but must not be used by this callback. */ + for (int i = 0; i < BLE_LOG_POOL_SHARED_CNT; i++) { + TEST_ASSERT_TRUE(ble_log_write_hex(BLE_LOG_SRC_CUSTOM, full_payload, + sizeof(full_payload))); + } + + timer_writer_ctx_t ctx = {.done = xSemaphoreCreateBinary()}; + TEST_ASSERT_NOT_NULL(ctx.done); + esp_timer_handle_t timer; + const esp_timer_create_args_t args = { + .callback = timer_writer_cb, + .arg = &ctx, + .dispatch_method = ESP_TIMER_TASK, + .name = "ble_log_writer", + }; + TEST_ESP_OK(esp_timer_create(&args, &timer)); + + bool returned = true; + bool results_match = true; + /* First fire with shared capacity exhausted, then again after recycle: + * both callbacks must return, but only the second may write/claim. */ + for (int attempt = 0; attempt < 2; attempt++) { + TEST_ESP_OK(esp_timer_start_once(timer, 1)); + bool completed = xSemaphoreTake(ctx.done, pdMS_TO_TICKS(1000)) == pdTRUE; + returned &= completed; + if (!completed) { + /* A regressed writer may be parked. Wake it before waiting for + * callback completion; never delete a task inside a pool API. */ + TEST_ASSERT_TRUE(ble_log_enable(false)); + } + TEST_ESP_OK(esp_timer_stop_blocking(timer, portMAX_DELAY)); + (void)xSemaphoreTake(ctx.done, 0); + results_match &= ctx.write_result == (attempt != 0) && + ctx.claim_result == (attempt != 0); + + ble_log_lbm_flush_open_trans(); + TEST_ASSERT_TRUE(ble_log_rt_drain()); + while (ble_log_prph_test_read(s_read_buf, sizeof(s_read_buf), 0, 0, NULL) > 0) { + } + TEST_ASSERT_TRUE(ble_log_enable(true)); + } + TEST_ESP_OK(esp_timer_delete(timer)); + vSemaphoreDelete(ctx.done); + TEST_ASSERT_TRUE_MESSAGE(returned, "ESP Timer writer waited for a shared transport"); + TEST_ASSERT_TRUE(results_match); +} + +#define SNAPSHOT_CAPTURE_MAX 8 + +typedef struct { + uint32_t sn[SNAPSHOT_CAPTURE_MAX]; + ble_log_internal_snapshot_t snapshot[SNAPSHOT_CAPTURE_MAX]; + int count; +} snapshot_capture_t; + +static void capture_snapshot_loss(const test_ble_log_frame_t *frame, void *ctx) +{ + snapshot_capture_t *capture = ctx; + if (frame->src != BLE_LOG_SRC_INTERNAL || + frame->payload_len != sizeof(uint32_t) + + sizeof(ble_log_internal_snapshot_t)) { + return; + } + + ble_log_internal_snapshot_t snapshot; + memcpy(&snapshot, frame->payload + sizeof(uint32_t), sizeof(snapshot)); + if (snapshot.int_src_code == BLE_LOG_INT_SRC_SNAPSHOT && + (snapshot.reason_flags & BLE_LOG_SNAPSHOT_REASON_PERIODIC) && + capture->count < SNAPSHOT_CAPTURE_MAX) { + capture->sn[capture->count] = frame->sn; + capture->snapshot[capture->count++] = snapshot; + } +} + +TEST_CASE("BLE Log periodic snapshot ignores producer gate and fails fast when busy", + "[ble_log][lbm]") +{ + ble_log_ts_info_t ts_info; + ble_log_rt_ts_sample(&ts_info, false); + + TEST_ASSERT_TRUE(ble_log_enable(true)); + ble_log_lbm_flush_open_trans(); + for (int round = 0; round < 2; round++) { + TEST_ASSERT_TRUE(ble_log_rt_drain()); + while (ble_log_prph_test_read(s_read_buf, sizeof(s_read_buf), + 0, 0, NULL) > 0) { + } + } + + TEST_ASSERT_TRUE(ble_log_enable(false)); + bool submitted = false; + for (int attempt = 0; attempt < TEST_MAX_ROUNDS && !submitted; attempt++) { + TEST_ASSERT_TRUE(ble_log_rt_drain()); + while (ble_log_prph_test_read(s_read_buf, sizeof(s_read_buf), + 0, 0, NULL) > 0) { + } + submitted = ble_log_internal_snapshot( + BLE_LOG_SNAPSHOT_REASON_PERIODIC, &ts_info, false); + } + TEST_ASSERT_TRUE_MESSAGE(submitted, + "Periodic timer kept the Internal transport busy"); + TEST_ASSERT_FALSE(ble_log_internal_snapshot( + BLE_LOG_SNAPSHOT_REASON_PERIODIC, &ts_info, false)); + TEST_ASSERT_TRUE(ble_log_enable(true)); + TEST_ASSERT_TRUE(ble_log_rt_drain()); + + snapshot_capture_t capture = {0}; + size_t first_len = ble_log_prph_test_read( + s_read_buf, sizeof(s_read_buf), pdMS_TO_TICKS(TEST_READ_TIMEOUT_MS), + 0, NULL); + TEST_ASSERT_GREATER_THAN_size_t(0, first_len); + TEST_ASSERT_TRUE(test_ble_log_walk_frames( + s_read_buf, first_len, capture_snapshot_loss, &capture)); + + /* Interleave successful and rejected log attempts with snapshots. These + * consume Global SNs but must not consume snapshot anchor counts. */ + static const uint8_t marker[] = {0xa5, 0x5a, 0x33}; + static const uint8_t oversized[BLE_LOG_MAX_PAYLOAD_LEN] = {0}; + TEST_ASSERT_TRUE(ble_log_write_hex(BLE_LOG_SRC_CUSTOM, marker, sizeof(marker))); + TEST_ASSERT_FALSE(ble_log_write_hex(BLE_LOG_SRC_CUSTOM, oversized, sizeof(oversized))); + uint32_t log_attempts = 2; + uint32_t handle; + uint8_t *claimed = ble_log_claim(BLE_LOG_SRC_ENCODE, sizeof(marker), &handle, true); + TEST_ASSERT_NOT_NULL(claimed); + memcpy(claimed, marker, sizeof(marker)); + ble_log_commit(handle, sizeof(marker)); + claimed = ble_log_claim(BLE_LOG_SRC_ENCODE, sizeof(marker), &handle, true); + TEST_ASSERT_NOT_NULL(claimed); + ble_log_commit(handle, 0); /* An aborted claim contributes no written bytes. */ + log_attempts += 2; +#if CONFIG_BLE_LOG_LL_ENABLED + ble_log_write_hex_ll(sizeof(marker), marker, 0, NULL, BIT(BLE_LOG_LL_FLAG_TASK)); + log_attempts++; +#endif + + TEST_ASSERT_TRUE(ble_log_internal_snapshot( + BLE_LOG_SNAPSHOT_REASON_PERIODIC, &ts_info, false)); + TEST_ASSERT_TRUE(ble_log_rt_drain()); + + for (int i = 0; i < BLE_LOG_TRANS_TOTAL_CNT; i++) { + size_t len = ble_log_prph_test_read( + s_read_buf, sizeof(s_read_buf), pdMS_TO_TICKS(TEST_READ_TIMEOUT_MS), + 0, NULL); + if (!len) { + break; + } + TEST_ASSERT_TRUE(test_ble_log_walk_frames( + s_read_buf, len, capture_snapshot_loss, &capture)); + } + /* Busy snapshots only advance anchor_count. Each captured snapshot and + * each ordinary log attempt consumes one Global SN; skipped snapshots + * must not create unaccounted gaps in that sequence. */ + TEST_ASSERT_GREATER_OR_EQUAL_INT(2, capture.count); + bool gap_seen = false; + for (int i = 1; i < capture.count; i++) { + if (((snapshot_anchor_count(&capture.snapshot[i]) - + snapshot_anchor_count(&capture.snapshot[i - 1])) & 0x00ffffffU) >= 2) { + gap_seen = true; + } + } + TEST_ASSERT_TRUE(gap_seen); + int last = capture.count - 1; + TEST_ASSERT_EQUAL_UINT32( + ((uint32_t)last + log_attempts) & 0x00ffffffU, + (capture.sn[last] - capture.sn[0]) & 0x00ffffffU); + const ble_log_source_stat_t *before_stat = &capture.snapshot[0].stats[0]; + const ble_log_source_stat_t *after_stat = &capture.snapshot[last].stats[0]; + TEST_ASSERT_EQUAL_UINT32(1, after_stat->written_frame_cnt - before_stat->written_frame_cnt); + TEST_ASSERT_EQUAL_UINT32(1, after_stat->lost_frame_cnt - before_stat->lost_frame_cnt); + TEST_ASSERT_EQUAL_UINT32(BLE_LOG_FRAME_OVERHEAD + sizeof(uint32_t) + sizeof(marker), + after_stat->written_bytes_cnt - before_stat->written_bytes_cnt); + unsigned encode_slot = BLE_LOG_SRC_ENCODE - BLE_LOG_SRC_CORE_FIRST; + before_stat = &capture.snapshot[0].stats[encode_slot]; + after_stat = &capture.snapshot[last].stats[encode_slot]; + TEST_ASSERT_EQUAL_UINT32(1, after_stat->written_frame_cnt - before_stat->written_frame_cnt); + TEST_ASSERT_EQUAL_UINT32(1, after_stat->lost_frame_cnt - before_stat->lost_frame_cnt); + TEST_ASSERT_EQUAL_UINT32(BLE_LOG_FRAME_OVERHEAD + sizeof(uint32_t) + sizeof(marker), + after_stat->written_bytes_cnt - before_stat->written_bytes_cnt); +#if CONFIG_BLE_LOG_LL_ENABLED + unsigned ll_slot = BLE_LOG_SRC_LL_TASK - BLE_LOG_SRC_CORE_FIRST; + TEST_ASSERT_EQUAL_UINT32(BLE_LOG_FRAME_OVERHEAD + sizeof(marker), + capture.snapshot[last].stats[ll_slot].written_bytes_cnt - + capture.snapshot[0].stats[ll_slot].written_bytes_cnt); +#endif + + uint32_t previous_anchor = snapshot_anchor_count(&capture.snapshot[last]); + uint32_t previous_sn = capture.sn[last]; + ble_log_prph_test_set_auto_recycle_hook(auto_recycle_noop, NULL); + ble_log_flush(); + ble_log_prph_test_set_auto_recycle_hook(NULL, NULL); + capture.count = 0; + TEST_ASSERT_TRUE(ble_log_internal_snapshot( + BLE_LOG_SNAPSHOT_REASON_PERIODIC, &ts_info, true)); + TEST_ASSERT_TRUE(ble_log_rt_drain()); + for (int i = 0; i < BLE_LOG_TRANS_TOTAL_CNT && !capture.count; i++) { + size_t len = ble_log_prph_test_read(s_read_buf, sizeof(s_read_buf), + pdMS_TO_TICKS(TEST_READ_TIMEOUT_MS), 0, NULL); + TEST_ASSERT_GREATER_THAN_size_t(0, len); + TEST_ASSERT_TRUE(test_ble_log_walk_frames(s_read_buf, len, capture_snapshot_loss, &capture)); + } + TEST_ASSERT_GREATER_THAN_INT(0, capture.count); + TEST_ASSERT_EQUAL_UINT32(0, capture.snapshot[0].stats[0].written_frame_cnt); + TEST_ASSERT_EQUAL_UINT32(0, capture.snapshot[0].stats[0].lost_frame_cnt); + TEST_ASSERT_EQUAL_UINT32(0, capture.snapshot[0].stats[0].written_bytes_cnt); + TEST_ASSERT_EQUAL_UINT32(0, capture.snapshot[0].stats[encode_slot].written_bytes_cnt); + uint32_t anchor_delta = (snapshot_anchor_count(&capture.snapshot[0]) - + previous_anchor) & 0x00ffffffU; + uint32_t sn_delta = (capture.sn[0] - previous_sn) & 0x00ffffffU; + TEST_ASSERT_GREATER_OR_EQUAL_UINT32(2, anchor_delta); /* FLUSH + next sample */ + TEST_ASSERT_LESS_THAN_UINT32(0x00800000U, anchor_delta); + TEST_ASSERT_GREATER_OR_EQUAL_UINT32(2, sn_delta); + TEST_ASSERT_LESS_OR_EQUAL_UINT32(anchor_delta, sn_delta); +} + +#if CONFIG_BLE_LOG_LL_ENABLED +typedef struct { + SemaphoreHandle_t started; + SemaphoreHandle_t done; +} waiter_ctx_t; + +typedef struct { + SemaphoreHandle_t done; + bool result; +} enable_ctx_t; + +typedef struct { + SemaphoreHandle_t started; + SemaphoreHandle_t done; +} deinit_ctx_t; + +static void blocked_ll_writer_task(void *arg) +{ + waiter_ctx_t *ctx = arg; + static const uint8_t marker = 0x55; + xSemaphoreGive(ctx->started); + ble_log_write_hex_ll(sizeof(marker), &marker, 0, NULL, + BIT(BLE_LOG_LL_FLAG_TASK)); + xSemaphoreGive(ctx->done); + vTaskDelete(NULL); +} + +static void racing_enable_task(void *arg) +{ + enable_ctx_t *ctx = arg; + ctx->result = ble_log_enable(true); + xSemaphoreGive(ctx->done); + vTaskDelete(NULL); +} + +static void racing_disable_task(void *arg) +{ + enable_ctx_t *ctx = arg; + ctx->result = ble_log_enable(false); + xSemaphoreGive(ctx->done); + vTaskDelete(NULL); +} + +static void lifecycle_deinit_task(void *arg) +{ + deinit_ctx_t *ctx = arg; + xSemaphoreGive(ctx->started); + ble_log_deinit(); + xSemaphoreGive(ctx->done); + vTaskDelete(NULL); +} + +TEST_CASE("BLE Log deinit closes a parked LL writer before racing enable", + "[ble_log][lbm]") +{ + static const uint8_t full_payload[ + BLE_LOG_MAX_PAYLOAD_LEN - sizeof(uint32_t)] = {0}; + + TEST_ASSERT_TRUE(ble_log_enable(true)); + ble_log_lbm_flush_open_trans(); + TEST_ASSERT_TRUE(ble_log_rt_drain()); + while (ble_log_prph_test_read(s_read_buf, sizeof(s_read_buf), 0, 0, NULL) > 0) { + } + TEST_ASSERT_TRUE(ble_log_rt_drain()); + while (ble_log_prph_test_read(s_read_buf, sizeof(s_read_buf), 0, 0, NULL) > 0) { + } + + /* Keep every task-usable transport SENDING in the test peripheral. */ + for (int i = 0; i < BLE_LOG_POOL_SHARED_CNT; i++) { + TEST_ASSERT_TRUE(ble_log_write_hex(BLE_LOG_SRC_CUSTOM, full_payload, + sizeof(full_payload))); + } + static const uint8_t reserve_marker = 0x56; + + portMUX_TYPE reserve_mux = portMUX_INITIALIZER_UNLOCKED; + portENTER_CRITICAL(&reserve_mux); + bool reserve_written = ble_log_write_hex(BLE_LOG_SRC_CUSTOM, + &reserve_marker, + sizeof(reserve_marker)); + portEXIT_CRITICAL(&reserve_mux); + TEST_ASSERT_TRUE(reserve_written); + + waiter_ctx_t waiter = { + .started = xSemaphoreCreateBinary(), + .done = xSemaphoreCreateBinary(), + }; + TEST_ASSERT_NOT_NULL(waiter.started); + TEST_ASSERT_NOT_NULL(waiter.done); + TEST_ASSERT_EQUAL(pdTRUE, + xTaskCreate(blocked_ll_writer_task, "ble_log_wait", + TEST_LIFECYCLE_STACK_SIZE, &waiter, + TEST_LIFECYCLE_PRIO, NULL)); + TEST_ASSERT_TRUE(xSemaphoreTake(waiter.started, pdMS_TO_TICKS(1000))); + vTaskDelay(1); + TEST_ASSERT_EQUAL(pdFALSE, xSemaphoreTake(waiter.done, 0)); + + s_enable_hook_entered = xSemaphoreCreateBinary(); + s_enable_hook_continue = xSemaphoreCreateBinary(); + TEST_ASSERT_NOT_NULL(s_enable_hook_entered); + TEST_ASSERT_NOT_NULL(s_enable_hook_continue); + s_enable_hook_armed = true; + + enable_ctx_t enabler = { + .done = xSemaphoreCreateBinary(), + }; + TEST_ASSERT_NOT_NULL(enabler.done); + TEST_ASSERT_EQUAL(pdTRUE, + xTaskCreate(racing_enable_task, "ble_log_enable", + TEST_LIFECYCLE_STACK_SIZE, &enabler, + TEST_LIFECYCLE_PRIO, NULL)); + TEST_ASSERT_TRUE(xSemaphoreTake(s_enable_hook_entered, + pdMS_TO_TICKS(1000))); + + /* The enable task is paused immediately before the lifecycle lock. Close + * and destroy the module, then let enable perform its locked recheck. */ + ble_log_deinit(); + TEST_ASSERT_TRUE(xSemaphoreTake(waiter.done, pdMS_TO_TICKS(1000))); + xSemaphoreGive(s_enable_hook_continue); + TEST_ASSERT_TRUE(xSemaphoreTake(enabler.done, pdMS_TO_TICKS(1000))); + s_enable_hook_armed = false; + TEST_ASSERT_FALSE(enabler.result); + + vSemaphoreDelete(waiter.started); + vSemaphoreDelete(waiter.done); + vSemaphoreDelete(enabler.done); + vSemaphoreDelete(s_enable_hook_entered); + vSemaphoreDelete(s_enable_hook_continue); + s_enable_hook_entered = NULL; + s_enable_hook_continue = NULL; + + TEST_ASSERT_TRUE(ble_log_init()); +} + +TEST_CASE("BLE Log disable keeps waiter semaphore alive during deinit", + "[ble_log][lbm]") +{ + TEST_ASSERT_TRUE(ble_log_enable(true)); + s_disable_hook_entered = xSemaphoreCreateBinary(); + s_disable_hook_continue = xSemaphoreCreateBinary(); + TEST_ASSERT_NOT_NULL(s_disable_hook_entered); + TEST_ASSERT_NOT_NULL(s_disable_hook_continue); + s_disable_hook_armed = true; + + enable_ctx_t disabler = { + .done = xSemaphoreCreateBinary(), + }; + deinit_ctx_t deinit = { + .started = xSemaphoreCreateBinary(), + .done = xSemaphoreCreateBinary(), + }; + TEST_ASSERT_NOT_NULL(disabler.done); + TEST_ASSERT_NOT_NULL(deinit.started); + TEST_ASSERT_NOT_NULL(deinit.done); + + TEST_ASSERT_EQUAL(pdTRUE, + xTaskCreate(racing_disable_task, "ble_log_disable", + TEST_LIFECYCLE_STACK_SIZE, &disabler, + TEST_LIFECYCLE_PRIO, NULL)); + TEST_ASSERT_TRUE(xSemaphoreTake(s_disable_hook_entered, + pdMS_TO_TICKS(1000))); + TEST_ASSERT_EQUAL(pdTRUE, + xTaskCreate(lifecycle_deinit_task, "ble_log_deinit", + TEST_LIFECYCLE_STACK_SIZE, &deinit, + TEST_LIFECYCLE_PRIO, NULL)); + TEST_ASSERT_TRUE(xSemaphoreTake(deinit.started, pdMS_TO_TICKS(1000))); + TEST_ASSERT_EQUAL(pdFALSE, + xSemaphoreTake(deinit.done, pdMS_TO_TICKS(20))); + + xSemaphoreGive(s_disable_hook_continue); + TEST_ASSERT_TRUE(xSemaphoreTake(disabler.done, pdMS_TO_TICKS(1000))); + TEST_ASSERT_TRUE(xSemaphoreTake(deinit.done, pdMS_TO_TICKS(1000))); + s_disable_hook_armed = false; + TEST_ASSERT_TRUE(disabler.result); + + vSemaphoreDelete(disabler.done); + vSemaphoreDelete(deinit.started); + vSemaphoreDelete(deinit.done); + vSemaphoreDelete(s_disable_hook_entered); + vSemaphoreDelete(s_disable_hook_continue); + s_disable_hook_entered = NULL; + s_disable_hook_continue = NULL; + + TEST_ASSERT_TRUE(ble_log_init()); +} +#endif /* CONFIG_BLE_LOG_LL_ENABLED */ + +/* ------------------------------------------------------------------ */ +/* Review blocker repro: pool waiter self-wake spin. */ +/* A cancelled claim publishes FREE state before the free-bitmap hint; */ +/* a waiter scanning that transport from the stale open-cursor hint */ +/* must PARK. Unconditional notification on the unlocked FREE state */ +/* lets the waiter mint and consume its own wake tokens in a tight */ +/* loop: the writer never parks, and the publisher suspended inside */ +/* the recycle window can never run to publish the bitmap. */ +/* ------------------------------------------------------------------ */ +static volatile bool s_spin_hook_armed; +static SemaphoreHandle_t s_spin_writer_start; +static SemaphoreHandle_t s_spin_writer_entered; +static SemaphoreHandle_t s_spin_canceller_hold; + +typedef struct { + SemaphoreHandle_t done; + volatile bool result; +} spin_writer_ctx_t; + +static void spin_writer_task(void *arg) +{ + spin_writer_ctx_t *ctx = arg; + (void)xSemaphoreTake(s_spin_writer_start, portMAX_DELAY); + xSemaphoreGive(s_spin_writer_entered); + const uint8_t marker = 0xC1; + ctx->result = ble_log_write_hex(BLE_LOG_SRC_CUSTOM, &marker, 1); + xSemaphoreGive(ctx->done); + vTaskDelete(NULL); +} + +typedef struct { + SemaphoreHandle_t done; + uint32_t handle; +} spin_cancel_ctx_t; + +static void spin_canceller_task(void *arg) +{ + spin_cancel_ctx_t *ctx = arg; + s_spin_hook_armed = true; + ble_log_commit(ctx->handle, 0); + xSemaphoreGive(ctx->done); + vTaskDelete(NULL); +} + +void ble_log_test_recycle_pre_bitmap_hook(ble_log_prph_trans_t *trans); + +void ble_log_test_recycle_pre_bitmap_hook(ble_log_prph_trans_t *trans) +{ + (void)trans; + if (!s_spin_hook_armed) { + return; + } + s_spin_hook_armed = false; + /* Deterministic preemption between the FREE publication and the + * free-bitmap hint: release the waiting writer, then stay suspended + * exactly like a publisher preempted in this window. */ + xSemaphoreGive(s_spin_writer_start); + (void)xSemaphoreTake(s_spin_canceller_hold, portMAX_DELAY); +} + +typedef struct { + volatile bool stop; + volatile uint32_t loops; +} canary_ctx_t; + +static void spin_canary_task(void *arg) +{ + canary_ctx_t *ctx = arg; + while (!ctx->stop) { + ctx->loops++; + vTaskDelay(1); + } + vTaskDelete(NULL); +} + +TEST_CASE("BLE Log pool waiter parks instead of self-waking on an unpublished FREE", + "[ble_log][lbm][repro]") +{ + canary_ctx_t canary = {0}; + spin_writer_ctx_t wctx = {.done = xSemaphoreCreateBinary()}; + spin_cancel_ctx_t cctx = {.done = xSemaphoreCreateBinary()}; + s_spin_writer_start = xSemaphoreCreateBinary(); + s_spin_writer_entered = xSemaphoreCreateBinary(); + s_spin_canceller_hold = xSemaphoreCreateBinary(); + TEST_ASSERT_NOT_NULL(wctx.done); + TEST_ASSERT_NOT_NULL(cctx.done); + TEST_ASSERT_NOT_NULL(s_spin_writer_start); + TEST_ASSERT_NOT_NULL(s_spin_writer_entered); + TEST_ASSERT_NOT_NULL(s_spin_canceller_hold); + + /* Fresh pool: the first shared transport must land on id 0 so the + * stale open-cursor hint (still 0 from init) points at it. */ + ble_log_deinit(); + TEST_ASSERT_TRUE(ble_log_init()); + TEST_ASSERT_TRUE(ble_log_enable(true)); + ble_log_lbm_flush_open_trans(); + for (int round = 0; round < 2; round++) { + TEST_ASSERT_TRUE(ble_log_rt_drain()); + while (ble_log_prph_test_read(s_read_buf, sizeof(s_read_buf), + 0, 0, NULL) > 0) { + } + } + + TEST_ASSERT_NOT_NULL(ble_log_claim(BLE_LOG_SRC_ENCODE, 1, + &cctx.handle, false)); + TEST_ASSERT_EQUAL(0, cctx.handle & 0xff); + uint32_t pinned[BLE_LOG_POOL_TRANS_CNT] = {0}; + int pinned_cnt = 0; + for (; pinned_cnt < BLE_LOG_POOL_SHARED_CNT - 1; pinned_cnt++) { + TEST_ASSERT_NOT_NULL(ble_log_claim(BLE_LOG_SRC_ENCODE, 1, + &pinned[pinned_cnt], false)); + } + + /* The canary shares the writer's core: it only progresses while the + * writer is parked; a spinning writer monopolizes the core. */ + TEST_ASSERT_EQUAL(pdTRUE, + xTaskCreatePinnedToCore(spin_canary_task, "ble_log_canary", + TEST_LIFECYCLE_STACK_SIZE, &canary, + 1, NULL, 1)); + vTaskDelay(pdMS_TO_TICKS(20)); + uint32_t canary_base = canary.loops; + TEST_ASSERT_GREATER_THAN(0, canary_base); + + TEST_ASSERT_EQUAL(pdTRUE, + xTaskCreatePinnedToCore(spin_writer_task, "ble_log_spin", + TEST_LIFECYCLE_STACK_SIZE, &wctx, + TEST_LIFECYCLE_PRIO + 1, NULL, 1)); + TEST_ASSERT_EQUAL(pdTRUE, + xTaskCreatePinnedToCore(spin_canceller_task, "ble_log_cancel", + TEST_LIFECYCLE_STACK_SIZE, &cctx, + TEST_LIFECYCLE_PRIO - 1, NULL, 0)); + + TEST_ASSERT_TRUE(xSemaphoreTake(s_spin_writer_entered, pdMS_TO_TICKS(1000))); + /* Let the writer reach its steady state (parked or spinning), then + * sample the canary across a second window: a parked writer leaves + * the core to the canary; a spinning writer monopolizes it. */ + vTaskDelay(pdMS_TO_TICKS(30)); + uint32_t canary_settled = canary.loops; + vTaskDelay(pdMS_TO_TICKS(100)); + uint32_t canary_now = canary.loops; + bool completed_early = xSemaphoreTake(wctx.done, 0) == pdTRUE; + printf("B1 sample: writer completed_early=%d canary %u -> %u during hold (core1: %s)\n", + completed_early, (unsigned)canary_settled, (unsigned)canary_now, + canary_now == canary_settled ? "MONOPOLIZED by spinning writer" : "progressing, writer parked"); + + /* Let the suspended publisher finish the recycle and observe the + * recovery: the writer must complete once the bitmap is published. */ + xSemaphoreGive(s_spin_canceller_hold); + TEST_ASSERT_TRUE(xSemaphoreTake(cctx.done, pdMS_TO_TICKS(1000))); + TEST_ASSERT_TRUE(xSemaphoreTake(wctx.done, pdMS_TO_TICKS(1000))); + TEST_ASSERT_TRUE(wctx.result); + + for (int i = 0; i < pinned_cnt; i++) { + ble_log_commit(pinned[i], 0); + } + ble_log_lbm_flush_open_trans(); + TEST_ASSERT_TRUE(ble_log_rt_drain()); + while (ble_log_prph_test_read(s_read_buf, sizeof(s_read_buf), + 0, 0, NULL) > 0) { + } + + canary.stop = true; + vTaskDelay(pdMS_TO_TICKS(5)); + vSemaphoreDelete(wctx.done); + vSemaphoreDelete(cctx.done); + vSemaphoreDelete(s_spin_writer_start); + vSemaphoreDelete(s_spin_writer_entered); + vSemaphoreDelete(s_spin_canceller_hold); + s_spin_writer_start = NULL; + s_spin_writer_entered = NULL; + s_spin_canceller_hold = NULL; + + /* Contract: while its only exit is suspended, the waiter must be + * PARKED (canary progresses across the hold window), never spinning + * on self-minted tokens (canary frozen = core monopolized). */ + TEST_ASSERT_GREATER_THAN(canary_settled, canary_now); +} + +/* ------------------------------------------------------------------ */ +/* Review blocker repro: flush drain straddle. */ +/* The drain loop reads the writer refcount and the waiter count as */ +/* two separate loads. A writer waking between them is in-flight (ref */ +/* re-acquired, waiter unregistered, gate not yet checked) when the */ +/* drain concludes; flush then snapshots and re-enables producers */ +/* while the pre-existing writer still owns its reference. */ +/* ------------------------------------------------------------------ */ +static volatile bool s_straddle_acquire_armed; +static volatile bool s_straddle_flush_armed; +static volatile int s_straddle_flush_evals; +static volatile bool s_straddle_hog_stop; +static volatile bool s_straddle_writer_frozen_at_return; +static volatile bool s_straddle_writer_result; +static volatile int64_t s_straddle_flush_us; +static SemaphoreHandle_t s_straddle_freeze; +static SemaphoreHandle_t s_straddle_inflight; +static SemaphoreHandle_t s_straddle_flush_start; +static SemaphoreHandle_t s_straddle_flush_done; +static SemaphoreHandle_t s_straddle_writer_started; +static SemaphoreHandle_t s_straddle_writer_done; +static SemaphoreHandle_t s_straddle_reader_start; + +void ble_log_test_acquire_after_unregister_hook(void); +void ble_log_test_flush_drain_between_loads_hook(void); +static void straddle_hog_task(void *arg); + +void ble_log_test_acquire_after_unregister_hook(void) +{ + if (!s_straddle_acquire_armed) { + return; + } + s_straddle_acquire_armed = false; + /* The writer is in-flight: reference re-acquired, waiter + * unregistered, producer gate not yet observed. Any preemption of + * this task leaves exactly this state behind. */ + xSemaphoreGive(s_straddle_inflight); + (void)xSemaphoreTake(s_straddle_freeze, portMAX_DELAY); +} + +void ble_log_test_flush_drain_between_loads_hook(void) +{ + if (!s_straddle_flush_armed) { + return; + } + switch (++s_straddle_flush_evals) { + case 1: + /* Evaluation #1, before this iteration's wake_all: start a hog on + * the writer's core (idle: the writer is parked) so the wake token + * minted moments later stays unconsumed. */ + (void)xTaskCreatePinnedToCore(straddle_hog_task, "ble_log_strh", + TEST_LIFECYCLE_STACK_SIZE, NULL, + TEST_LIFECYCLE_PRIO + 2, NULL, 0); + break; + case 2: + /* Evaluation #2, between the two condition loads: stop the hog so + * the parked writer consumes the wake and runs its real post-take + * handoff steps (re-acquire + unregister) inside this window. + * Only now may the link recycle transports: the writer is + * in-flight and unregistered, so recycles mint no wake tokens. */ + s_straddle_flush_armed = false; + s_straddle_hog_stop = true; + if (xSemaphoreTake(s_straddle_inflight, pdMS_TO_TICKS(2000)) == pdTRUE) { + xSemaphoreGive(s_straddle_reader_start); + } + break; + default: + break; + } +} + +static void straddle_writer_task(void *arg) +{ + (void)arg; + xSemaphoreGive(s_straddle_writer_started); + const uint8_t marker = 0xD2; + s_straddle_writer_result = ble_log_write_hex(BLE_LOG_SRC_CUSTOM, &marker, 1); + xSemaphoreGive(s_straddle_writer_done); + vTaskDelete(NULL); +} + +static void straddle_flusher_task(void *arg) +{ + (void)arg; + (void)xSemaphoreTake(s_straddle_flush_start, portMAX_DELAY); + int64_t t0 = esp_timer_get_time(); + ble_log_flush(); + s_straddle_flush_us = esp_timer_get_time() - t0; + s_straddle_writer_frozen_at_return = !s_straddle_acquire_armed; + xSemaphoreGive(s_straddle_flush_done); + vTaskDelete(NULL); +} + +static void straddle_hog_task(void *arg) +{ + (void)arg; + while (!s_straddle_hog_stop) { + } + vTaskDelete(NULL); +} + +static volatile bool s_straddle_reader_stop; + +static void straddle_reader_task(void *arg) +{ + (void)arg; + /* Recycle transports the way a real link does, so flush_all_trans + * completes and the measured flush duration reflects the writer + * drain rather than the transport wait. Blocked until the drain has + * handed the writer off: recycling earlier would wake the parked + * writer before the straddle point. */ + (void)xSemaphoreTake(s_straddle_reader_start, portMAX_DELAY); + while (!s_straddle_reader_stop) { + (void)ble_log_prph_test_read(s_read_buf, sizeof(s_read_buf), + pdMS_TO_TICKS(10), 0, NULL); + } + vTaskDelete(NULL); +} + +TEST_CASE("BLE Log flush drain does not straddle an in-flight parked writer", + "[ble_log][lbm][repro]") +{ + static const uint8_t full_payload[ + BLE_LOG_MAX_PAYLOAD_LEN - sizeof(uint32_t)] = {0}; + + s_straddle_freeze = xSemaphoreCreateBinary(); + s_straddle_inflight = xSemaphoreCreateBinary(); + s_straddle_flush_start = xSemaphoreCreateBinary(); + s_straddle_flush_done = xSemaphoreCreateBinary(); + s_straddle_writer_started = xSemaphoreCreateBinary(); + s_straddle_writer_done = xSemaphoreCreateBinary(); + s_straddle_reader_start = xSemaphoreCreateBinary(); + TEST_ASSERT_NOT_NULL(s_straddle_freeze); + TEST_ASSERT_NOT_NULL(s_straddle_inflight); + TEST_ASSERT_NOT_NULL(s_straddle_flush_start); + TEST_ASSERT_NOT_NULL(s_straddle_flush_done); + TEST_ASSERT_NOT_NULL(s_straddle_writer_started); + TEST_ASSERT_NOT_NULL(s_straddle_writer_done); + TEST_ASSERT_NOT_NULL(s_straddle_reader_start); + + TEST_ASSERT_TRUE(ble_log_enable(true)); + ble_log_lbm_flush_open_trans(); + for (int round = 0; round < 2; round++) { + TEST_ASSERT_TRUE(ble_log_rt_drain()); + while (ble_log_prph_test_read(s_read_buf, sizeof(s_read_buf), + 0, 0, NULL) > 0) { + } + } + + /* Keep every task-usable transport SENDING in the test peripheral. */ + for (int i = 0; i < BLE_LOG_POOL_SHARED_CNT; i++) { + TEST_ASSERT_TRUE(ble_log_write_hex(BLE_LOG_SRC_CUSTOM, full_payload, + sizeof(full_payload))); + } + TEST_ASSERT_TRUE(ble_log_rt_drain()); + + /* The writer parks on the full shared pool. */ + TEST_ASSERT_EQUAL(pdTRUE, + xTaskCreatePinnedToCore(straddle_writer_task, "ble_log_strw", + TEST_LIFECYCLE_STACK_SIZE, NULL, + TEST_LIFECYCLE_PRIO, NULL, 0)); + TEST_ASSERT_TRUE(xSemaphoreTake(s_straddle_writer_started, pdMS_TO_TICKS(1000))); + vTaskDelay(pdMS_TO_TICKS(10)); + + /* The eval#1 hook starts the hog on the writer's core so the first + * wake stays unconsumed; the eval#2 hook stops it exactly between + * the two condition loads. */ + s_straddle_flush_evals = 0; + s_straddle_flush_armed = true; + s_straddle_acquire_armed = true; + s_straddle_hog_stop = false; + s_straddle_reader_stop = false; + s_straddle_writer_frozen_at_return = false; + TEST_ASSERT_EQUAL(pdTRUE, + xTaskCreate(straddle_reader_task, "ble_log_strr", + TEST_READER_STACK_SIZE, NULL, + TEST_READER_PRIO, NULL)); + TEST_ASSERT_EQUAL(pdTRUE, + xTaskCreatePinnedToCore(straddle_flusher_task, "ble_log_strf", + TEST_READER_STACK_SIZE, NULL, + TEST_READER_PRIO + 2, NULL, 1)); + xSemaphoreGive(s_straddle_flush_start); + + TEST_ASSERT_TRUE(xSemaphoreTake(s_straddle_flush_done, pdMS_TO_TICKS(10000))); + int64_t flush_ms = s_straddle_flush_us / 1000; + bool writer_done_early = xSemaphoreTake(s_straddle_writer_done, 0) == pdTRUE; + printf("B2 sample: flush returned in %lld ms, writer in-flight at return=%d, writer already done=%d\n", + (long long)flush_ms, s_straddle_writer_frozen_at_return, + writer_done_early); + + /* Transports are already claimable (the reader kept the link busy); + * release the frozen writer: on the straddle it observes the + * re-enabled gate and completes its pre-flush write after flush + * already returned. */ + xSemaphoreGive(s_straddle_freeze); + TEST_ASSERT_TRUE(xSemaphoreTake(s_straddle_writer_done, pdMS_TO_TICKS(1000))); + printf("B2 outcome: frozen writer passed the re-enabled gate and wrote=%d\n", + s_straddle_writer_result); + + s_straddle_reader_stop = true; + vTaskDelay(pdMS_TO_TICKS(30)); + ble_log_lbm_flush_open_trans(); + TEST_ASSERT_TRUE(ble_log_rt_drain()); + while (ble_log_prph_test_read(s_read_buf, sizeof(s_read_buf), + 0, 0, NULL) > 0) { + } + + vSemaphoreDelete(s_straddle_freeze); + vSemaphoreDelete(s_straddle_inflight); + vSemaphoreDelete(s_straddle_flush_start); + vSemaphoreDelete(s_straddle_flush_done); + vSemaphoreDelete(s_straddle_writer_started); + vSemaphoreDelete(s_straddle_writer_done); + vSemaphoreDelete(s_straddle_reader_start); + s_straddle_freeze = NULL; + s_straddle_inflight = NULL; + s_straddle_flush_start = NULL; + s_straddle_flush_done = NULL; + s_straddle_writer_started = NULL; + s_straddle_writer_done = NULL; + s_straddle_reader_start = NULL; + + /* Contract: when a writer was in-flight at flush return, flush must + * have held the drain for the full documented timeout (1s) instead + * of concluding between the two counter loads. */ + if (s_straddle_writer_frozen_at_return) { + TEST_ASSERT_GREATER_OR_EQUAL(950, (int)flush_ms); + } +} diff --git a/components/bt/common/ble_log/test_apps/ble_log_test/sdkconfig.defaults b/components/bt/common/ble_log/test_apps/ble_log_test/sdkconfig.defaults index 7bbad451380..6f03c3e16dd 100644 --- a/components/bt/common/ble_log/test_apps/ble_log_test/sdkconfig.defaults +++ b/components/bt/common/ble_log/test_apps/ble_log_test/sdkconfig.defaults @@ -4,3 +4,8 @@ CONFIG_BLE_LOG_PRPH_TEST=y CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU0=n CONFIG_UNITY_ENABLE_64BIT=y CONFIG_BLE_MESH=y +# Exercise the compressed-log encoders and the task-id registry (protocol v8). +CONFIG_BLE_COMPRESSED_LOG_ENABLE=y +CONFIG_BLE_HOST_COMPRESSED_LOG_ENABLE=y +# Small registry so the table-full degradation is reachable on target. +CONFIG_BLE_LOG_TASK_ID_MAX=4 diff --git a/components/bt/host/bluedroid/api/esp_bluedroid_hci.c b/components/bt/host/bluedroid/api/esp_bluedroid_hci.c index 486af894490..142328fefb1 100644 --- a/components/bt/host/bluedroid/api/esp_bluedroid_hci.c +++ b/components/bt/host/bluedroid/api/esp_bluedroid_hci.c @@ -99,9 +99,9 @@ void hci_host_send_packet(uint8_t *data, uint16_t len) #if CONFIG_BT_BLE_LOG_SPI_OUT_HCI_ENABLED ble_log_spi_out_hci_write(BLE_LOG_SPI_OUT_SOURCE_HCI_DOWNSTREAM, data, len); #endif // CONFIG_BT_BLE_LOG_SPI_OUT_HCI_ENABLED -#if CONFIG_BLE_LOG_HOST_SIDE_HCI_LOG_ENABLED +#if CONFIG_BLE_LOG_HCI_LOG_ENABLED ble_log_write_hci(BLE_LOG_HCI_DOWNSTREAM, data, len); -#endif /* CONFIG_BLE_LOG_HOST_SIDE_HCI_LOG_ENABLED */ +#endif /* CONFIG_BLE_LOG_HCI_LOG_ENABLED */ #if (BT_CONTROLLER_INCLUDED == TRUE) esp_vhci_host_send_packet(data, len); #else /* BT_CONTROLLER_INCLUDED == TRUE */ diff --git a/components/bt/host/bluedroid/hci/hci_hal_h4.c b/components/bt/host/bluedroid/hci/hci_hal_h4.c index d24f558b153..b1fe9473aed 100644 --- a/components/bt/host/bluedroid/hci/hci_hal_h4.c +++ b/components/bt/host/bluedroid/hci/hci_hal_h4.c @@ -619,9 +619,9 @@ static int host_recv_pkt_cb(uint8_t *data, uint16_t len) #if CONFIG_BT_BLE_LOG_SPI_OUT_HCI_ENABLED ble_log_spi_out_hci_write(BLE_LOG_SPI_OUT_SOURCE_HCI_UPSTREAM, data, len); #endif // CONFIG_BT_BLE_LOG_SPI_OUT_HCI_ENABLED -#if CONFIG_BLE_LOG_HOST_SIDE_HCI_LOG_ENABLED +#if CONFIG_BLE_LOG_HCI_LOG_ENABLED ble_log_write_hci(BLE_LOG_HCI_UPSTREAM, data, len); -#endif /* CONFIG_BLE_LOG_HOST_SIDE_HCI_LOG_ENABLED */ +#endif /* CONFIG_BLE_LOG_HCI_LOG_ENABLED */ //Target has packet to host, malloc new buffer for packet BT_HDR *pkt = NULL; #if (BLE_42_SCAN_EN == TRUE) diff --git a/components/bt/host/nimble/esp-hci/src/esp_nimble_hci.c b/components/bt/host/nimble/esp-hci/src/esp_nimble_hci.c index c3c3549b36c..eb0055efd92 100644 --- a/components/bt/host/nimble/esp-hci/src/esp_nimble_hci.c +++ b/components/bt/host/nimble/esp-hci/src/esp_nimble_hci.c @@ -87,9 +87,9 @@ void esp_vhci_host_send_packet_wrapper(uint8_t *data, uint16_t len) #if CONFIG_BT_BLE_LOG_SPI_OUT_HCI_ENABLED ble_log_spi_out_hci_write(BLE_LOG_SPI_OUT_SOURCE_HCI_DOWNSTREAM, data, len); #endif // CONFIG_BT_BLE_LOG_SPI_OUT_HCI_ENABLED -#if CONFIG_BLE_LOG_HOST_SIDE_HCI_LOG_ENABLED +#if CONFIG_BLE_LOG_HCI_LOG_ENABLED ble_log_write_hci(BLE_LOG_HCI_DOWNSTREAM, data, len); -#endif /* CONFIG_BLE_LOG_HOST_SIDE_HCI_LOG_ENABLED */ +#endif /* CONFIG_BLE_LOG_HCI_LOG_ENABLED */ esp_vhci_host_send_packet(data, len); } @@ -274,9 +274,9 @@ static int host_rcv_pkt(uint8_t *data, uint16_t len) #if CONFIG_BT_BLE_LOG_SPI_OUT_HCI_ENABLED ble_log_spi_out_hci_write(BLE_LOG_SPI_OUT_SOURCE_HCI_UPSTREAM, data, len); #endif // CONFIG_BT_BLE_LOG_SPI_OUT_HCI_ENABLED -#if CONFIG_BLE_LOG_HOST_SIDE_HCI_LOG_ENABLED +#if CONFIG_BLE_LOG_HCI_LOG_ENABLED ble_log_write_hci(BLE_LOG_HCI_UPSTREAM, data, len); -#endif /* CONFIG_BLE_LOG_HOST_SIDE_HCI_LOG_ENABLED */ +#endif /* CONFIG_BLE_LOG_HCI_LOG_ENABLED */ bt_record_hci_data(data, len); diff --git a/components/bt/sdkconfig.rename b/components/bt/sdkconfig.rename index 12bd0e9d5e5..8fab0a89c2f 100644 --- a/components/bt/sdkconfig.rename +++ b/components/bt/sdkconfig.rename @@ -286,3 +286,7 @@ CONFIG_BT_NIMBLE_HCI_EVT_LO_BUF_COUNT CONFIG_BT_NIMBLE_TRA CONFIG_BT_NIMBLE_COEX_PHY_CODED_TX_RX_TLIM_EN CONFIG_BT_LE_COEX_PHY_CODED_TX_RX_TLIM_EN CONFIG_BT_NIMBLE_COEX_PHY_CODED_TX_RX_TLIM_DIS CONFIG_BT_LE_COEX_PHY_CODED_TX_RX_TLIM_DIS + +# BLE Log: periodic Internal Snapshots are always enabled now; the legacy +# TS switch only gates the analyzer toggle IO. +CONFIG_BLE_LOG_TS_ENABLED CONFIG_BLE_LOG_TS_SYNC_TOGGLE_IO_ENABLED