Merge branch 'feat/ble_esl_persist_and_ots_speedup' into 'master'

feat(ble/nimble): add ESL persist, AP recovery, and image APIs

See merge request espressif/esp-idf!52374
This commit is contained in:
Island
2026-09-14 15:48:31 +08:00
19 changed files with 2184 additions and 391 deletions
@@ -19,6 +19,7 @@
#include <stdint.h>
#include <stdbool.h>
#include <stddef.h>
#include "esp_err.h"
#include "ble_esl_common.h"
@@ -95,7 +96,7 @@ typedef struct {
*/
typedef enum {
BLE_ESL_EVT_STATE_CHANGED = 0, /*!< State transition occurred */
BLE_ESL_EVT_IMAGE_WRITE, /*!< Image data received via OTS */
BLE_ESL_EVT_IMAGE_WRITE, /*!< OTS image chunk or write completion */
BLE_ESL_EVT_DISPLAY_IMAGE, /*!< Display a stored image */
BLE_ESL_EVT_REFRESH_DISPLAY, /*!< Refresh the current display image */
BLE_ESL_EVT_SENSOR_READ, /*!< Read sensor data request */
@@ -116,11 +117,16 @@ typedef struct {
/**
* @brief Event data for BLE_ESL_EVT_IMAGE_WRITE
*
* Fired once per OTS fragment and once when the write transfer ends:
* - `data != NULL`: one fragment; the pointer is valid only during the callback
* - `data == NULL && length > 0`: write succeeded; `length` is the total size
* - `data == NULL && length == 0`: write failed or empty; the slot is incomplete
*/
typedef struct {
uint8_t image_index; /*!< Image storage index (0 to Max_Image_Index) */
const uint8_t *data; /*!< Pointer to received image data */
uint32_t length; /*!< Length of image data in bytes */
const uint8_t *data; /*!< Fragment bytes, or NULL on completion */
uint32_t length; /*!< Fragment size, total size, or 0 on failure */
uint32_t offset; /*!< Write offset within the image object */
} ble_esl_image_write_evt_param_t;
@@ -318,6 +324,103 @@ esp_err_t ble_esl_set_service_needed(bool flag);
esp_err_t ble_esl_report_sensor_data(uint8_t sensor_index, uint8_t error_code,
const uint8_t *data, uint8_t data_len);
#if CONFIG_BLE_ESL_OTS_SUPPORT
/**
* @brief Query whether an OTS image slot has a successfully completed write.
*
* @param[in] image_index Image index (0 .. num_images-1)
* @return true if a successful OTS write completed for this slot
*/
bool ble_esl_image_is_complete(uint8_t image_index);
/**
* @brief Atomically snapshot a completed image into a caller buffer.
*
* Copies only when the slot is marked complete. Concurrent OTS writes
* mark the slot incomplete via BLE_OTS_SERVER_EVT_DATA_WRITE so Display
* Image never observes a torn frame. This function does not change the
* complete flag itself.
*
* @param[in] image_index Image index
* @param[out] dst Destination buffer
* @param[in] capacity Capacity of dst in bytes
* @param[out] out_len Actual copied length (may be NULL)
*
* @return
* - ESP_OK on success
* - ESP_ERR_INVALID_ARG / ESP_ERR_INVALID_STATE / ESP_ERR_INVALID_SIZE
*/
esp_err_t ble_esl_image_snapshot(uint8_t image_index, uint8_t *dst, size_t capacity,
size_t *out_len);
/**
* @brief Load image bytes into an OTS object and mark the slot complete.
*
* Intended for boot-time restore from persistent storage after ble_esl_init()
* and before ble_esl_start(). Does not emit BLE_ESL_EVT_IMAGE_WRITE.
*
* Uses ble_ots_server_set_object_data(), which may generate Object Changed
* indications; do not call while a BLE connection is active.
*
* @param[in] image_index Image index (0 .. num_images-1)
* @param[in] data Full image payload
* @param[in] len Payload length (1 .. CONFIG_BLE_ESL_MAX_IMAGE_SIZE)
*
* @return
* - ESP_OK on success
* - ESP_ERR_INVALID_ARG / ESP_ERR_INVALID_STATE / ESP_FAIL
*/
esp_err_t ble_esl_image_restore(uint8_t image_index, const uint8_t *data, size_t len);
/**
* @brief Mark all OTS image slots incomplete (RAM-side only).
*
* Used after Unassociate / Factory Reset so Display cannot read wiped images
* in the same boot before the flash erase worker finishes.
*/
void ble_esl_image_invalidate_all(void);
#endif
/* ========================== Persisted Association Snapshot ========================== */
/**
* @brief POD snapshot of TAG association credentials (no NimBLE private types).
*
* Absolute Time and PAwR sync handles are intentionally omitted.
*/
typedef struct {
ble_esl_address_t esl_address;
ble_esl_key_material_t ap_sync_key;
ble_esl_key_material_t resp_key;
uint8_t peer_addr_type; /*!< BLE address type of bonded AP */
uint8_t peer_addr[6]; /*!< Identity address of bonded AP (little-endian) */
} ble_esl_persisted_tag_t;
/**
* @brief Export a complete association snapshot
*
* Succeeds only when ESL address, both key materials, bonded peer, and the
* three mandatory config bits (address/ap_sync/resp) are valid. Absolute Time
* is not required for export.
*
* @param[out] out Snapshot destination
* @return ESP_OK on success; ESP_ERR_INVALID_STATE when incomplete
*/
esp_err_t ble_esl_export_persisted_tag(ble_esl_persisted_tag_t *out);
/**
* @brief Restore association into RAM after ble_esl_init(), before ble_esl_start()
*
* Performs bare field assignment (does NOT call esl_state_transition). Sets
* state to UNSYNCHRONIZED and config_complete to Address|AP Sync|Resp only
* (Absolute Time bit cleared). Does not start advertising or timers —
* ble_esl_start() must follow to arm unsync_timer and connectable advertising.
*
* @param[in] info Valid association snapshot
* @return ESP_OK on success; ESP_ERR_INVALID_ARG / ESP_ERR_INVALID_STATE on failure
*/
esp_err_t ble_esl_restore_persisted_tag(const ble_esl_persisted_tag_t *info);
#ifdef __cplusplus
}
#endif
@@ -54,6 +54,8 @@ typedef enum {
/* Command events (ap_command.c) */
BLE_ESL_AP_EVT_RESPONSE, /*!< Command response received from ESL */
BLE_ESL_AP_EVT_CMD_TIMEOUT, /*!< ECP procedure timeout (30 seconds) */
BLE_ESL_AP_EVT_CMD_FAILED, /*!< ECP write/notification failed */
BLE_ESL_AP_EVT_ABS_TIME_WRITTEN, /*!< Public Absolute Time write complete */
} ble_esl_ap_evt_t;
/* ========================== Response Types ========================== */
@@ -104,6 +106,7 @@ typedef struct {
typedef struct {
ble_esl_ap_cb_t callback; /*!< Application event callback (must not be NULL) */
ble_esl_ap_pawr_config_t pawr_config; /*!< PAwR timing parameters */
ble_esl_key_material_t ap_sync_key; /*!< PAwR train Sync Key (one per AP, not per ESL) */
} ble_esl_ap_config_t;
/* ========================== Connection Event Structures ========================== */
@@ -117,7 +120,10 @@ typedef struct {
int8_t rssi; /*!< Received signal strength (dBm) */
const uint8_t *adv_data; /*!< Raw advertising data (valid during callback) */
uint16_t adv_data_len; /*!< Advertising data length in octets */
bool is_associated; /*!< true = Unsynchronized (known), false = Unassociated */
bool is_associated; /*!< true = address present in AP ESL table (not TAG state) */
bool is_connectable; /*!< true when report is connectable advertising */
bool data_complete; /*!< true when advertising data is complete (not truncated) */
bool advertises_esl_service; /*!< true when complete AD contains ESL Service UUID 0x1857 */
} ble_esl_ap_scan_result_t;
/**
@@ -154,7 +160,6 @@ typedef struct {
typedef struct {
uint8_t esl_id; /*!< ESL_ID to assign (0x00–0xFE) */
uint8_t group_id; /*!< Group_ID to assign (0x00–0x7F) */
ble_esl_key_material_t ap_sync_key; /*!< AP Sync Key Material (session key + IV) */
ble_esl_key_material_t resp_key; /*!< ESL Response Key Material (session key + IV) */
} ble_esl_ap_esl_config_t;
@@ -198,6 +203,14 @@ typedef struct {
esp_err_t status; /*!< ESP_OK if all mandatory chars written */
} ble_esl_ap_configured_t;
/**
* @brief Absolute Time write event (BLE_ESL_AP_EVT_ABS_TIME_WRITTEN)
*/
typedef struct {
uint16_t conn_handle; /*!< Connection handle */
esp_err_t status; /*!< GATT write status */
} ble_esl_ap_abs_time_written_t;
/**
* @brief Image transferred event data (BLE_ESL_AP_EVT_IMAGE_TRANSFERRED)
*/
@@ -302,6 +315,15 @@ typedef struct {
uint8_t group_id; /*!< Group_ID of the timed-out ESL */
} esl_ap_cmd_timeout_t;
/**
* @brief Command transport failure event data (BLE_ESL_AP_EVT_CMD_FAILED)
*/
typedef struct {
uint8_t esl_id; /*!< ESL_ID of the failed command target */
uint8_t group_id; /*!< Group_ID of the failed command target */
esp_err_t status; /*!< ECP transport status */
} esl_ap_cmd_failed_t;
/* ========================== Command Parameter Structures ========================== */
/**
@@ -326,13 +348,25 @@ typedef struct {
* Registers the application callback, stores PAwR timing parameters,
* and allocates internal resources. Must be called before any other API.
*
* @param config Pointer to AP configuration (callback + PAwR timing)
* @param config Pointer to AP configuration (callback, PAwR timing, AP Sync Key)
* @return ESP_OK on success; ESP_ERR_INVALID_ARG if config/callback is NULL or
* num_subevents is outside 1–128;
* ESP_ERR_INVALID_STATE if already initialized
*/
esp_err_t ble_esl_ap_init(const ble_esl_ap_config_t *config);
/**
* @brief Set or replace the AP-wide PAwR Sync Key Material
*
* One key encrypts the whole PAwR train. ble_esl_ap_init() installs
* config->ap_sync_key; call this to rekey or restore a persisted network
* key. Does not rewrite already-configured ESLs over GATT.
*
* @param key AP Sync Key Material (session key + IV)
* @return ESP_OK on success; ESP_ERR_INVALID_ARG / ESP_ERR_INVALID_STATE
*/
esp_err_t ble_esl_ap_set_sync_key(const ble_esl_key_material_t *key);
/**
* @brief Deinitialize the AP module
*
@@ -342,32 +376,54 @@ esp_err_t ble_esl_ap_init(const ble_esl_ap_config_t *config);
* stopped, the still connected links stay tracked and ESP_FAIL is
* returned; the caller may retry this function.
*
* @return ESP_OK on success; ESP_ERR_INVALID_STATE if not initialized;
* @return ESP_OK on success; ESP_ERR_INVALID_STATE if not initialized
* or if a configure / Absolute Time write is still in-flight;
* ESP_FAIL if an active connection could not be terminated
*/
esp_err_t ble_esl_ap_deinit(void);
/**
* @brief Start AP operation (scanning + PAwR broadcasting)
* @brief Start PAwR broadcasting
*
* Begins GAP General Discovery for ESLs and starts PAwR broadcasting.
* Discovered ESLs are reported via BLE_ESL_AP_EVT_SCAN_RESULT.
* Does not start GAP discovery. Call ble_esl_ap_start_scan() when the
* application wants to discover ESLs. Existing ACL connections are not
* affected.
*
* @note This function does not automatically initiate connections. The caller must handle
* scan results and call ble_esl_ap_connect() to establish connections.
*
* @return ESP_OK on success; ESP_ERR_INVALID_STATE if not initialized or already started
* @return ESP_OK on success; ESP_ERR_INVALID_STATE if not initialized
* or PAwR is already started
*/
esp_err_t ble_esl_ap_start(void);
esp_err_t ble_esl_ap_start_pawr(void);
/**
* @brief Stop AP operation (scanning + PAwR broadcasting)
* @brief Stop PAwR broadcasting
*
* Existing ACL connections are not affected.
* Does not stop GAP discovery. Call ble_esl_ap_stop_scan() first if
* discovery is running. Existing ACL connections are not affected.
*
* @return ESP_OK on success; ESP_ERR_INVALID_STATE if not started
* @return ESP_OK on success; ESP_ERR_INVALID_STATE if PAwR is not started
*/
esp_err_t ble_esl_ap_stop(void);
esp_err_t ble_esl_ap_stop_pawr(void);
/**
* @brief Start GAP discovery if it is not already running
*
* Idempotent: if discovery is already active, returns ESP_OK without
* cancelling the current scan. Clears scan_suppressed so discovery can
* auto-resume after connections end. PAwR broadcasting is not affected.
*
* @return ESP_OK on success; ESP_ERR_INVALID_STATE if PAwR is not started
*/
esp_err_t ble_esl_ap_start_scan(void);
/**
* @brief Stop GAP discovery without affecting PAwR broadcasting
*
* Sets scan_suppressed so discovery is not auto-resumed after connections
* end. Call ble_esl_ap_start_scan() to scan again.
*
* @return ESP_OK on success; ESP_ERR_INVALID_STATE if PAwR is not started
*/
esp_err_t ble_esl_ap_stop_scan(void);
/**
* @brief Initiate ACL connection to an ESL
@@ -425,18 +481,27 @@ esp_err_t ble_esl_ap_connect_synced(ble_esl_address_t esl_addr);
*/
esp_err_t ble_esl_ap_disconnect(uint16_t conn_handle);
/**
* @brief Cancel pending Tag connects and terminate active Tag ACL links.
*
* Does not complete in-flight configure / Absolute Time; GATT failure
* callbacks still deliver those events.
*/
esp_err_t ble_esl_ap_abort_tag_connections(void);
/* ========================== Public APIs: Lifecycle ========================== */
/* Implemented in ble_esl_ap_lifecycle.c */
/**
* @brief Write ESL configuration characteristics
*
* Writes ESL Address, AP Sync Key Material, ESL Response Key Material, and
* ESL Current Absolute Time in sequence. Async — result via
* Writes ESL Address, the AP-wide Sync Key already installed by
* ble_esl_ap_init() / ble_esl_ap_set_sync_key(), ESL Response Key Material,
* and ESL Current Absolute Time in sequence. Async — result via
* BLE_ESL_AP_EVT_CONFIGURED.
*
* @param conn_handle Connection handle of the connected ESL
* @param config Pointer to configuration parameters
* @param config Pointer to per-ESL parameters (address + response key)
* @return ESP_OK if operation initiated; error code on failure
*/
esp_err_t ble_esl_ap_configure(uint16_t conn_handle,
@@ -477,6 +542,44 @@ esp_err_t ble_esl_ap_transfer_image(const ble_esl_ap_image_transfer_params_t *pa
*/
esp_err_t ble_esl_ap_synchronize(uint16_t conn_handle);
/**
* @brief Return true while Update Complete → PAST → wait-for-disconnect is active
*/
bool ble_esl_ap_synchronize_in_progress(void);
/**
* @brief Write ESL Current Absolute Time on an encrypted ACL link
*
* @p abs_time_ms is the ESL 32-bit millisecond counter (wrapping), not a
* POSIX/RTC timestamp. The application owns the epoch (boot monotonic,
* persisted network time, SNTP-derived, etc.). Completion is
* BLE_ESL_AP_EVT_ABS_TIME_WRITTEN (once, if this returns ESP_OK).
* Rejected while configure occupies the same connection.
*
* @param conn_handle Connection handle of the connected ESL
* @param abs_time_ms Absolute time in milliseconds to write to the ESL
* @return ESP_OK if the GATT write was initiated
*/
esp_err_t ble_esl_ap_write_absolute_time(uint16_t conn_handle, uint32_t abs_time_ms);
/**
* @brief In-memory association snapshot used to seed AP tracking after reboot
*
* Not an NVS/wire blob. Call after ble_esl_ap_init(); may be invoked
* multiple times (once per ESL), including after ble_esl_ap_start_pawr().
* Key material is not checked for all-zeros — the application must
* quarantine corrupt records. The AP Sync Key is AP-wide: restore it
* once via ble_esl_ap_init() / ble_esl_ap_set_sync_key(), not per ESL.
*/
typedef struct {
ble_esl_address_t esl_address; /*!< ESL Address (ESL_ID + Group_ID) */
uint8_t ble_addr[6]; /*!< Identity address of the ESL */
uint8_t ble_addr_type; /*!< BLE address type */
ble_esl_key_material_t resp_key; /*!< Per-ESL Response Key Material */
} ble_esl_ap_persisted_esl_t;
esp_err_t ble_esl_ap_restore_persisted_esl(const ble_esl_ap_persisted_esl_t *info);
/**
* @brief Get the AP's tracked state for a specific ESL
*
@@ -493,20 +596,38 @@ ble_esl_state_t ble_esl_ap_get_esl_state(ble_esl_address_t esl_addr);
/**
* @brief Send Ping command (opcode 0x00)
*
* Verifies ESL reachability. Expected response: Basic State.
* Unicast: verifies ESL reachability; expected response Basic State.
* Broadcast (`esl_id` 0xFF): queues a PAwR TLV for all ESLs in `group_id`;
* no Tag response is produced.
*
* @param esl_id Target ESL_ID (0x00–0xFE, or 0xFF for broadcast)
* @param esl_id Target ESL_ID (0x00–0xFE, or 0xFF for group broadcast)
* @param group_id Group_ID (0x00–0x7F)
* @return ESP_OK on successful dispatch; error code on failure
*/
esp_err_t ble_esl_ap_ping(uint8_t esl_id, uint8_t group_id);
/**
* @brief Send a PAwR-only command (atomic state check + transmit)
*
* Never uses ECP. Unicast is allowed only when the ESL is Synchronized or
* Unsynchronized. Opcode must be on the PAwR allowlist; vendor opcodes are
* rejected (use ble_esl_ap_vendor_command()).
*
* @param params TLV parameter bytes; params[0] must equal esl_id
* @param params_len Must equal BLE_ESL_TLV_PARAMS_LEN(opcode)
*/
esp_err_t ble_esl_ap_pawr_command(uint8_t esl_id, uint8_t group_id,
uint8_t opcode, const uint8_t *params,
uint8_t params_len);
/**
* @brief Send Display Image command (opcode 0x20)
*
* Immediately display a stored image. Expected response: Display State.
* Immediately display a stored image.
* Unicast: expected response Display State.
* Broadcast (`esl_id` 0xFF): queues PAwR for the group; no Tag response.
*
* @param esl_id Target ESL_ID (0x00–0xFE, or 0xFF for broadcast)
* @param esl_id Target ESL_ID (0x00–0xFE, or 0xFF for group broadcast)
* @param group_id Group_ID (0x00–0x7F)
* @param display_index Index of the target display
* @param image_index Index of the stored image to show
@@ -535,9 +656,11 @@ esp_err_t ble_esl_ap_display_timed_image(uint8_t esl_id, uint8_t group_id,
/**
* @brief Send Refresh Display command (opcode 0x11)
*
* Refresh current display without changing image. Expected response: Display State.
* Refresh current display without changing image.
* Unicast: expected response Display State.
* Broadcast (`esl_id` 0xFF): queues PAwR for the group; no Tag response.
*
* @param esl_id Target ESL_ID (0x00–0xFE, or 0xFF for broadcast)
* @param esl_id Target ESL_ID (0x00–0xFE, or 0xFF for group broadcast)
* @param group_id Group_ID (0x00–0x7F)
* @param display_index Index of the display to refresh
* @return ESP_OK on successful dispatch; error code on failure
@@ -549,9 +672,10 @@ esp_err_t ble_esl_ap_refresh_display(uint8_t esl_id, uint8_t group_id,
* @brief Send LED Control command (opcode 0xB0)
*
* Immediately control LED color, brightness, and flashing pattern.
* Expected response: LED State.
* Unicast: expected response LED State.
* Broadcast (`esl_id` 0xFF): queues PAwR for the group; no Tag response.
*
* @param esl_id Target ESL_ID (0x00–0xFE, or 0xFF for broadcast)
* @param esl_id Target ESL_ID (0x00–0xFE, or 0xFF for group broadcast)
* @param group_id Group_ID (0x00–0x7F)
* @param led_index Index of the target LED
* @param settings Pointer to LED control settings
@@ -593,12 +717,13 @@ esp_err_t ble_esl_ap_read_sensor(uint8_t esl_id, uint8_t group_id,
uint8_t sensor_index);
/**
* @brief Send Unassociate from AP command (opcode 0x01)
* @brief Send Unassociate from AP command (opcode 0x01) — ECP only
*
* Disassociates the ESL. Expected response: Basic State. On success,
* the ESL's tracked state transitions to Unassociated.
* Broadcast (`esl_id` 0xFF) is rejected with ESP_ERR_INVALID_ARG.
*
* @param esl_id Target ESL_ID (0x00–0xFE, or 0xFF for broadcast)
* @param esl_id Target ESL_ID (0x00–0xFE)
* @param group_id Group_ID (0x00–0x7F)
* @return ESP_OK on successful dispatch; error code on failure
*/
@@ -154,6 +154,9 @@ static void parse_response(uint8_t esl_id, uint8_t group_id,
static esp_err_t dispatch_command(uint8_t esl_id, uint8_t group_id,
uint8_t opcode, const uint8_t *params,
uint8_t params_len, bool ecp_only);
static esp_err_t pawr_send_unicast(uint16_t esl_addr, uint8_t esl_id, uint8_t group_id,
uint8_t opcode, const uint8_t *params,
uint8_t params_len);
static esp_err_t build_led_params(uint8_t esl_id, uint8_t led_index,
const esl_ap_led_settings_t *settings,
uint8_t *out_params, uint8_t *out_len);
@@ -386,8 +389,19 @@ static void ecp_response_cb(uint16_t conn_handle, esp_err_t status,
if (status != ESP_OK) {
ESP_LOGE(TAG, "ECP write/notification failed: status=0x%X, conn=0x%04X",
status, conn_handle);
uint8_t esl_id = ctx->esl_id;
uint8_t group_id = ctx->group_id;
free_ecp_ctx(ctx);
xSemaphoreGive(s_ecp_ctx_mutex);
if (g_esl_ap != NULL && g_esl_ap->app_cb != NULL) {
esl_ap_cmd_failed_t failed_evt = {
.esl_id = esl_id,
.group_id = group_id,
.status = status,
};
g_esl_ap->app_cb(BLE_ESL_AP_EVT_CMD_FAILED, &failed_evt);
}
return;
}
@@ -492,6 +506,13 @@ static void parse_response(uint8_t esl_id, uint8_t group_id,
esl_id, response.basic_state.service_needed,
response.basic_state.synchronized);
/* Basic State after Ping must NOT auto-promote ESL runtime to
* SYNCHRONIZED. Product ONLINE is owned exclusively by the recovery
* coordinator after a generation-validated confirmation Ping. PAST
* success may still move ESL runtime to SYNCHRONIZED via the
* synchronize path; that is separate from product availability. */
(void)cmd_opcode;
/* For Unassociate command, transition ESL to Unassociated state */
if (cmd_opcode == BLE_ESL_CMD_UNASSOCIATE) {
uint16_t esl_addr = BLE_ESL_AP_MAKE_ADDR(esl_id, group_id);
@@ -550,6 +571,128 @@ static void parse_response(uint8_t esl_id, uint8_t group_id,
}
}
/* ========================== PAwR Unicast Helper ========================== */
static esp_err_t pawr_send_unicast(uint16_t esl_addr, uint8_t esl_id, uint8_t group_id,
uint8_t opcode, const uint8_t *params,
uint8_t params_len)
{
uint8_t tlv_buf[BLE_ESL_TLV_MAX_SIZE];
uint8_t tlv_len = 0;
esp_err_t ret = ble_esl_tlv_encode(opcode, params, params_len,
tlv_buf, &tlv_len);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "TLV encode failed: 0x%X", ret);
return ret;
}
uint8_t payload_buf[BLE_ESL_PAYLOAD_MAX_SIZE];
uint8_t payload_len = 0;
const uint8_t *tlv_ptrs[] = { tlv_buf };
const uint8_t tlv_lens[] = { tlv_len };
ret = ble_esl_payload_encode(group_id, tlv_ptrs, tlv_lens, 1,
payload_buf, &payload_len);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "Payload encode failed: 0x%X", ret);
return ret;
}
ble_esl_ap_tracking_lock();
ble_esl_ap_esl_entry_t *esl = ble_esl_ap_find_esl(esl_addr);
if (esl != NULL) {
esl->pending_pawr_cmd_opcode = opcode;
}
ble_esl_ap_tracking_unlock();
pawr_record_slot_mapping(group_id, esl_id);
ret = ble_esl_ap_pawr_send(group_id, payload_buf, payload_len);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "PAwR send failed: 0x%X", ret);
}
return ret;
}
static bool pawr_opcode_allowed(uint8_t opcode)
{
switch (opcode) {
case BLE_ESL_CMD_PING:
case BLE_ESL_CMD_SERVICE_RESET:
case BLE_ESL_CMD_READ_SENSOR:
case BLE_ESL_CMD_REFRESH_DISPLAY:
case BLE_ESL_CMD_DISPLAY_IMAGE:
case BLE_ESL_CMD_DISPLAY_TIMED_IMAGE:
case BLE_ESL_CMD_LED_CONTROL:
case BLE_ESL_CMD_LED_TIMED_CONTROL:
return true;
default:
return false;
}
}
esp_err_t ble_esl_ap_pawr_command(uint8_t esl_id, uint8_t group_id,
uint8_t opcode, const uint8_t *params,
uint8_t params_len)
{
if (g_esl_ap == NULL || !g_esl_ap->initialized) {
return ESP_ERR_INVALID_STATE;
}
if (group_id > BLE_ESL_GROUP_ID_MAX) {
return ESP_ERR_INVALID_ARG;
}
if (BLE_ESL_TLV_TAG(opcode) == BLE_ESL_CMD_VENDOR_TAG) {
return ESP_ERR_NOT_SUPPORTED;
}
if (!pawr_opcode_allowed(opcode)) {
return ESP_ERR_NOT_SUPPORTED;
}
if (params == NULL || params_len != BLE_ESL_TLV_PARAMS_LEN(opcode) ||
params[0] != esl_id) {
return ESP_ERR_INVALID_ARG;
}
uint8_t tlv_buf[BLE_ESL_TLV_MAX_SIZE];
uint8_t tlv_len = 0;
esp_err_t ret = ble_esl_tlv_encode(opcode, params, params_len,
tlv_buf, &tlv_len);
if (ret != ESP_OK) {
return ret;
}
uint8_t payload_buf[BLE_ESL_PAYLOAD_MAX_SIZE];
uint8_t payload_len = 0;
const uint8_t *tlv_ptrs[] = { tlv_buf };
const uint8_t tlv_lens[] = { tlv_len };
ret = ble_esl_payload_encode(group_id, tlv_ptrs, tlv_lens, 1,
payload_buf, &payload_len);
if (ret != ESP_OK) {
return ret;
}
if (esl_id == BLE_ESL_BROADCAST_ADDRESS) {
return ble_esl_ap_pawr_send(group_id, payload_buf, payload_len);
}
uint16_t esl_addr = BLE_ESL_AP_MAKE_ADDR(esl_id, group_id);
ble_esl_ap_tracking_lock();
ble_esl_ap_esl_entry_t *esl = ble_esl_ap_find_esl(esl_addr);
if (esl == NULL) {
ble_esl_ap_tracking_unlock();
return ESP_ERR_NOT_FOUND;
}
if (esl->state != BLE_ESL_STATE_SYNCHRONIZED &&
esl->state != BLE_ESL_STATE_UNSYNCHRONIZED) {
ble_esl_ap_tracking_unlock();
return ESP_ERR_INVALID_STATE;
}
esl->pending_pawr_cmd_opcode = opcode;
pawr_record_slot_mapping(group_id, esl_id);
ret = ble_esl_ap_pawr_send(group_id, payload_buf, payload_len);
ble_esl_ap_tracking_unlock();
return ret;
}
/* ========================== Transport Dispatch ========================== */
/**
@@ -615,20 +758,28 @@ static esp_err_t dispatch_command(uint8_t esl_id, uint8_t group_id,
/* Unicast: determine transport based on ESL tracked state */
uint16_t esl_addr = BLE_ESL_AP_MAKE_ADDR(esl_id, group_id);
ble_esl_state_t state = ble_esl_ap_get_esl_state(ble_esl_addr_make(esl_id, group_id));
uint16_t conn_handle = BLE_ESL_AP_CONN_HANDLE_INVALID;
ble_esl_state_t state;
ble_esl_ap_tracking_lock();
ble_esl_ap_esl_entry_t *esl = ble_esl_ap_find_esl(esl_addr);
if (esl == NULL) {
ble_esl_ap_tracking_unlock();
ESP_LOGE(TAG, "ESL 0x%04X not tracked — cannot dispatch command", esl_addr);
return ESP_ERR_INVALID_STATE;
}
state = esl->state;
conn_handle = esl->conn_handle;
ble_esl_ap_tracking_unlock();
switch (state) {
case BLE_ESL_STATE_CONFIGURING:
case BLE_ESL_STATE_UPDATING: {
/* ECP transport (connection-oriented) */
ble_esl_ap_esl_entry_t *esl = ble_esl_ap_find_esl(esl_addr);
if (esl == NULL || esl->conn_handle == BLE_ESL_AP_CONN_HANDLE_INVALID) {
if (conn_handle == BLE_ESL_AP_CONN_HANDLE_INVALID) {
ESP_LOGE(TAG, "No active connection for ESL 0x%04X", esl_addr);
return ESP_ERR_INVALID_STATE;
}
uint16_t conn_handle = esl->conn_handle;
xSemaphoreTake(s_ecp_ctx_mutex, portMAX_DELAY);
/* Check if there's already a pending ECP command on this connection */
@@ -682,6 +833,8 @@ static esp_err_t dispatch_command(uint8_t esl_id, uint8_t group_id,
xSemaphoreGive(s_ecp_ctx_mutex);
/* Write the TLV to ECP characteristic */
ESP_LOGI(TAG, "ECP write opcode=0x%02X conn=%u esl_id=%u group=%u tlv_len=%u",
opcode, conn_handle, esl_id, group_id, tlv_len);
ret = ble_esl_ap_ecp_write(conn_handle, tlv_buf, tlv_len,
ecp_response_cb, ctx);
if (ret != ESP_OK) {
@@ -703,37 +856,21 @@ static esp_err_t dispatch_command(uint8_t esl_id, uint8_t group_id,
return ESP_ERR_INVALID_STATE;
}
/* PAwR transport (connectionless) */
uint8_t payload_buf[BLE_ESL_PAYLOAD_MAX_SIZE];
uint8_t payload_len = 0;
const uint8_t *tlv_ptrs[] = { tlv_buf };
const uint8_t tlv_lens[] = { tlv_len };
return pawr_send_unicast(esl_addr, esl_id, group_id, opcode, params, params_len);
}
ret = ble_esl_payload_encode(group_id, tlv_ptrs, tlv_lens, 1,
payload_buf, &payload_len);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "Payload encode failed: 0x%X", ret);
return ret;
case BLE_ESL_STATE_UNSYNCHRONIZED: {
/* PAwR unicast still works for tags on connectable ext adv after AP reboot */
if (ecp_only) {
ESP_LOGE(TAG, "Command 0x%02X is ECP-only, cannot send in Unsynchronized state",
opcode);
return ESP_ERR_INVALID_STATE;
}
/* Store the command opcode for response correlation */
ble_esl_ap_esl_entry_t *esl = ble_esl_ap_find_esl(esl_addr);
if (esl != NULL) {
esl->pending_pawr_cmd_opcode = opcode;
}
/* Record the response slot mapping for this group (Issue 9) */
pawr_record_slot_mapping(group_id, esl_id);
ret = ble_esl_ap_pawr_send(group_id, payload_buf, payload_len);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "PAwR send failed: 0x%X", ret);
}
return ret;
return pawr_send_unicast(esl_addr, esl_id, group_id, opcode, params, params_len);
}
case BLE_ESL_STATE_UNASSOCIATED:
case BLE_ESL_STATE_UNSYNCHRONIZED:
default:
ESP_LOGE(TAG, "ESL 0x%04X in state %d — cannot dispatch command",
esl_addr, state);
@@ -1050,24 +1187,28 @@ void ble_esl_ap_command_handle_pawr_response(uint8_t group_id,
* the correct ESL entry.
*/
uint16_t esl_addr = pawr_lookup_slot(group_id, response_slot);
ble_esl_ap_esl_entry_t *esl = NULL;
uint8_t cmd_opcode = 0;
uint8_t esl_id = 0;
if (esl_addr != ESL_ADDR_INVALID) {
esl = ble_esl_ap_find_esl(esl_addr);
}
if (esl == NULL) {
ble_esl_ap_tracking_lock();
ble_esl_ap_esl_entry_t *esl = ble_esl_ap_find_esl(esl_addr);
if (esl == NULL) {
ble_esl_ap_tracking_unlock();
ESP_LOGW(TAG, "PAwR response from unknown ESL: group=%u slot=%u",
group_id, response_slot);
return;
}
esl_addr = esl->esl_addr;
esl_id = BLE_ESL_AP_ADDR_ESL_ID(esl_addr);
cmd_opcode = esl->pending_pawr_cmd_opcode;
esl->pending_pawr_cmd_opcode = 0;
ble_esl_ap_tracking_unlock();
} else {
ESP_LOGW(TAG, "PAwR response from unknown ESL: group=%u slot=%u",
group_id, response_slot);
return;
}
esl_addr = esl->esl_addr;
uint8_t esl_id = BLE_ESL_AP_ADDR_ESL_ID(esl_addr);
/* Retrieve and clear the stored command opcode for response correlation */
uint8_t cmd_opcode = esl->pending_pawr_cmd_opcode;
esl->pending_pawr_cmd_opcode = 0;
/* Decrypt and parse the PAwR response */
ble_esl_ap_parsed_response_t parsed;
esp_err_t ret = ble_esl_ap_pawr_parse_response(esl_addr, response_slot,
@@ -11,15 +11,23 @@
*/
#include <string.h>
#include <assert.h>
#include "esp_log.h"
#include "esp_err.h"
#include "freertos/FreeRTOS.h"
#include "freertos/semphr.h"
#include "freertos/task.h"
#include "nimble/ble.h"
#include "host/ble_hs.h"
#include "host/ble_gap.h"
#include "host/ble_hs_adv.h"
#include "nimble/hci_common.h"
#include "ble_esl_ap.h"
#include "ble_esl_ap_int.h"
#include "ble_esl_common.h"
static const char *TAG = "esl_ap_conn";
@@ -29,10 +37,25 @@ static const char *TAG = "esl_ap_conn";
/** Connection-establishment timeout for the PAwR connection procedure (ms) */
#define PAWR_CONNECT_TIMEOUT_MS 30000
/* Bulk OTS transfers need a low-latency, full-length 2M ACL link.
* One 1024-byte CoC SDU is ~5 DLE PDUs. With max_ce_len=0 the controller
* typically sends one PDU per event, which caps a 15 ms link at ~17 KB/s
* (the rate measured on 460800-byte badge frames). A long CE lets one
* event carry the whole SDU; NimBLE CoC still only holds one TX SDU. */
#define ESL_CONN_ITVL_MIN 6 /* 7.5 ms */
#define ESL_CONN_ITVL_MAX 12 /* 15 ms */
#define ESL_CONN_TIMEOUT 400 /* 4 s */
#define ESL_CONN_CE_LEN_MAX 0xffff
#define ESL_LL_TX_OCTETS 251
#define ESL_LL_TX_TIME 2120
/* ========================== Global State ========================== */
ble_esl_ap_state_t *g_esl_ap = NULL;
static SemaphoreHandle_t s_tracking_mutex;
static TaskHandle_t s_tracking_holder;
/* ========================== Forward Declarations ========================== */
static int ble_esl_ap_gap_event(struct ble_gap_event *event, void *arg);
@@ -42,7 +65,77 @@ static void handle_disconnect_event(struct ble_gap_event *event);
static void handle_enc_change(struct ble_gap_event *event);
static void handle_pairing_complete(struct ble_gap_event *event);
static void handle_notify_rx(struct ble_gap_event *event);
static esp_err_t ble_esl_ap_start_discovery(void);
static void ble_esl_ap_maybe_resume_discovery(void);
static void request_fast_esl_link(uint16_t conn_handle)
{
struct ble_gap_upd_params params = {
.itvl_min = ESL_CONN_ITVL_MIN,
.itvl_max = ESL_CONN_ITVL_MAX,
.latency = 0,
.supervision_timeout = ESL_CONN_TIMEOUT,
.min_ce_len = 0,
.max_ce_len = ESL_CONN_CE_LEN_MAX,
};
int rc = ble_gap_update_params(conn_handle, &params);
if (rc != 0 && rc != BLE_HS_EALREADY) {
ESP_LOGW(TAG, "Fast interval request failed; conn=%u rc=%d",
conn_handle, rc);
}
rc = ble_gap_set_data_len(conn_handle, ESL_LL_TX_OCTETS, ESL_LL_TX_TIME);
if (rc != 0 && rc != BLE_HS_EALREADY) {
ESP_LOGW(TAG, "Data length request failed; conn=%u rc=%d",
conn_handle, rc);
}
rc = ble_gap_set_prefered_le_phy(conn_handle,
BLE_HCI_LE_PHY_2M_PREF_MASK,
BLE_HCI_LE_PHY_2M_PREF_MASK, 0);
if (rc != 0 && rc != BLE_HS_EALREADY) {
ESP_LOGW(TAG, "2M PHY request failed; conn=%u rc=%d",
conn_handle, rc);
}
}
/* ========================== Tracking mutex ========================== */
void ble_esl_ap_tracking_lock(void)
{
assert(s_tracking_mutex != NULL);
assert(xTaskGetCurrentTaskHandle() != s_tracking_holder);
assert(!ble_esl_ap_pawr_held());
assert(!ble_esl_ap_lifecycle_held());
xSemaphoreTake(s_tracking_mutex, portMAX_DELAY);
s_tracking_holder = xTaskGetCurrentTaskHandle();
}
void ble_esl_ap_tracking_unlock(void)
{
assert(s_tracking_mutex != NULL);
assert(s_tracking_holder == xTaskGetCurrentTaskHandle());
s_tracking_holder = NULL;
xSemaphoreGive(s_tracking_mutex);
}
bool ble_esl_ap_tracking_held(void)
{
return s_tracking_holder == xTaskGetCurrentTaskHandle();
}
void ble_esl_ap_dispatch_state_evt(const ble_esl_ap_state_evt_snap_t *snap)
{
if (snap == NULL || !snap->pending) {
return;
}
if (g_esl_ap != NULL && g_esl_ap->app_cb != NULL) {
ble_esl_ap_state_changed_t evt = snap->evt;
g_esl_ap->app_cb(BLE_ESL_AP_EVT_STATE_CHANGED, &evt);
}
}
/* ========================== Helper Functions ========================== */
@@ -88,6 +181,7 @@ void ble_esl_ap_free_conn(ble_esl_ap_conn_t *conn)
ble_esl_ap_esl_entry_t *ble_esl_ap_find_esl(uint16_t esl_addr)
{
assert(ble_esl_ap_tracking_held());
if (!g_esl_ap) {
return NULL;
}
@@ -103,6 +197,7 @@ ble_esl_ap_esl_entry_t *ble_esl_ap_find_esl(uint16_t esl_addr)
ble_esl_ap_esl_entry_t *ble_esl_ap_find_esl_by_ble_addr(const uint8_t *addr,
uint8_t addr_type)
{
assert(ble_esl_ap_tracking_held());
if (!g_esl_ap || !addr) {
return NULL;
}
@@ -118,6 +213,7 @@ ble_esl_ap_esl_entry_t *ble_esl_ap_find_esl_by_ble_addr(const uint8_t *addr,
ble_esl_ap_esl_entry_t *ble_esl_ap_alloc_esl(void)
{
assert(ble_esl_ap_tracking_held());
if (!g_esl_ap) {
return NULL;
}
@@ -134,7 +230,10 @@ ble_esl_ap_esl_entry_t *ble_esl_ap_alloc_esl(void)
bool ble_esl_ap_is_associated(const uint8_t *addr, uint8_t addr_type)
{
return (ble_esl_ap_find_esl_by_ble_addr(addr, addr_type) != NULL);
ble_esl_ap_tracking_lock();
bool found = (ble_esl_ap_find_esl_by_ble_addr(addr, addr_type) != NULL);
ble_esl_ap_tracking_unlock();
return found;
}
/* ========================== Public APIs ========================== */
@@ -163,10 +262,21 @@ esp_err_t ble_esl_ap_init(const ble_esl_ap_config_t *config)
return ESP_ERR_NO_MEM;
}
s_tracking_mutex = xSemaphoreCreateMutex();
if (s_tracking_mutex == NULL) {
ESP_LOGE(TAG, "Failed to create tracking mutex");
free(g_esl_ap);
g_esl_ap = NULL;
return ESP_ERR_NO_MEM;
}
s_tracking_holder = NULL;
g_esl_ap->app_cb = config->callback;
g_esl_ap->pawr_config = config->pawr_config;
g_esl_ap->initialized = true;
g_esl_ap->started = false;
ble_esl_ap_pawr_set_sync_key(&config->ap_sync_key);
g_esl_ap->ap_sync_key_valid = true;
g_esl_ap->pawr_started = false;
g_esl_ap->pawr_active = false;
/* Initialize connection table */
@@ -185,6 +295,8 @@ esp_err_t ble_esl_ap_init(const ble_esl_ap_config_t *config)
esp_err_t ret = ble_esl_ap_lifecycle_init();
if (ret != ESP_OK) {
ESP_LOGE(TAG, "Failed to init lifecycle sub-module: %s", esp_err_to_name(ret));
vSemaphoreDelete(s_tracking_mutex);
s_tracking_mutex = NULL;
free(g_esl_ap);
g_esl_ap = NULL;
return ret;
@@ -194,6 +306,8 @@ esp_err_t ble_esl_ap_init(const ble_esl_ap_config_t *config)
if (ret != ESP_OK) {
ESP_LOGE(TAG, "Failed to init PAwR sub-module: %s", esp_err_to_name(ret));
ble_esl_ap_lifecycle_deinit();
vSemaphoreDelete(s_tracking_mutex);
s_tracking_mutex = NULL;
free(g_esl_ap);
g_esl_ap = NULL;
return ret;
@@ -204,6 +318,8 @@ esp_err_t ble_esl_ap_init(const ble_esl_ap_config_t *config)
ESP_LOGE(TAG, "Failed to init command sub-module: %s", esp_err_to_name(ret));
ble_esl_ap_pawr_deinit();
ble_esl_ap_lifecycle_deinit();
vSemaphoreDelete(s_tracking_mutex);
s_tracking_mutex = NULL;
free(g_esl_ap);
g_esl_ap = NULL;
return ret;
@@ -215,6 +331,8 @@ esp_err_t ble_esl_ap_init(const ble_esl_ap_config_t *config)
ble_esl_ap_command_deinit();
ble_esl_ap_pawr_deinit();
ble_esl_ap_lifecycle_deinit();
vSemaphoreDelete(s_tracking_mutex);
s_tracking_mutex = NULL;
free(g_esl_ap);
g_esl_ap = NULL;
return ret;
@@ -224,6 +342,19 @@ esp_err_t ble_esl_ap_init(const ble_esl_ap_config_t *config)
return ESP_OK;
}
esp_err_t ble_esl_ap_set_sync_key(const ble_esl_key_material_t *key)
{
if (g_esl_ap == NULL || !g_esl_ap->initialized) {
return ESP_ERR_INVALID_STATE;
}
if (key == NULL) {
return ESP_ERR_INVALID_ARG;
}
ble_esl_ap_pawr_set_sync_key(key);
g_esl_ap->ap_sync_key_valid = true;
return ESP_OK;
}
/**
* @brief Run the disconnect cleanup for one established link during deinit
*
@@ -272,9 +403,15 @@ esp_err_t ble_esl_ap_deinit(void)
return ESP_ERR_INVALID_STATE;
}
/* Stop scanning and PAwR if active */
if (g_esl_ap->started) {
ble_esl_ap_stop();
if (ble_esl_ap_lifecycle_has_inflight()) {
ESP_LOGE(TAG, "deinit refused: configure or Absolute Time still in-flight");
return ESP_ERR_INVALID_STATE;
}
/* Stop discovery then PAwR if the public start_pawr() path ran */
if (g_esl_ap->pawr_started) {
(void)ble_esl_ap_stop_scan();
(void)ble_esl_ap_stop_pawr();
}
/* Mark the module as no longer operational before touching connections:
@@ -335,21 +472,41 @@ esp_err_t ble_esl_ap_deinit(void)
g_esl_ap = NULL;
free(ap);
if (s_tracking_mutex != NULL) {
vSemaphoreDelete(s_tracking_mutex);
s_tracking_mutex = NULL;
s_tracking_holder = NULL;
}
ESP_LOGI(TAG, "ESL AP deinitialized");
return ESP_OK;
}
esp_err_t ble_esl_ap_start(void)
esp_err_t ble_esl_ap_start_pawr(void)
{
if (!g_esl_ap || !g_esl_ap->initialized) {
return ESP_ERR_INVALID_STATE;
}
if (g_esl_ap->started) {
ESP_LOGW(TAG, "Already started");
if (g_esl_ap->pawr_started) {
ESP_LOGW(TAG, "PAwR already started");
return ESP_ERR_INVALID_STATE;
}
/* Start GAP General Discovery (scanning) */
g_esl_ap->scan_suppressed = true;
esp_err_t ret = ble_esl_ap_pawr_start();
if (ret != ESP_OK) {
ESP_LOGE(TAG, "Failed to start PAwR: %s", esp_err_to_name(ret));
return ret;
}
g_esl_ap->pawr_started = true;
ESP_LOGI(TAG, "PAwR started (scan idle until start_scan)");
return ESP_OK;
}
static esp_err_t ble_esl_ap_start_discovery(void)
{
uint8_t own_addr_type;
int rc = ble_hs_id_infer_auto(0, &own_addr_type);
if (rc != 0) {
@@ -357,10 +514,17 @@ esp_err_t ble_esl_ap_start(void)
return ESP_FAIL;
}
if (ble_gap_disc_active()) {
return ESP_OK;
}
struct ble_gap_disc_params disc_params = {0};
disc_params.filter_duplicates = 1;
disc_params.passive = 0; /* Active scan for General Discovery */
disc_params.itvl = 0; /* Use defaults */
/* Disable duplicate filtering so an ONLINE TAG that reboots and resumes
* Unsynchronized advertising is reported again in the same long-lived
* discovery session (filter_duplicates=1 would suppress the re-advertise). */
disc_params.filter_duplicates = 0;
disc_params.passive = 0;
disc_params.itvl = 0;
disc_params.window = 0;
disc_params.filter_policy = 0;
disc_params.limited = 0;
@@ -371,45 +535,65 @@ esp_err_t ble_esl_ap_start(void)
ESP_LOGE(TAG, "Failed to start GAP discovery; rc=%d", rc);
return ESP_FAIL;
}
/* Start PAwR broadcasting */
esp_err_t ret = ble_esl_ap_pawr_start();
if (ret != ESP_OK) {
ESP_LOGE(TAG, "Failed to start PAwR: %s", esp_err_to_name(ret));
ble_gap_disc_cancel();
return ret;
}
g_esl_ap->started = true;
ESP_LOGI(TAG, "ESL AP started (scanning + PAwR)");
return ESP_OK;
}
esp_err_t ble_esl_ap_stop(void)
static void ble_esl_ap_maybe_resume_discovery(void)
{
if (!g_esl_ap || !g_esl_ap->initialized || !g_esl_ap->started) {
if (!g_esl_ap || !g_esl_ap->pawr_started || g_esl_ap->scan_suppressed) {
return;
}
if (ble_gap_disc_active()) {
return;
}
(void)ble_esl_ap_start_discovery();
}
esp_err_t ble_esl_ap_start_scan(void)
{
if (!g_esl_ap || !g_esl_ap->initialized || !g_esl_ap->pawr_started) {
return ESP_ERR_INVALID_STATE;
}
/* Cancel any pending (not yet established) connection attempts */
ble_gap_conn_cancel();
g_esl_ap->scan_suppressed = false;
esp_err_t ret = ble_esl_ap_start_discovery();
if (ret == ESP_OK) {
ESP_LOGI(TAG, "GAP discovery started");
}
return ret;
}
/* Stop scanning */
esp_err_t ble_esl_ap_stop_scan(void)
{
if (!g_esl_ap || !g_esl_ap->initialized || !g_esl_ap->pawr_started) {
return ESP_ERR_INVALID_STATE;
}
g_esl_ap->scan_suppressed = true;
if (ble_gap_disc_active()) {
int rc = ble_gap_disc_cancel();
if (rc != 0 && rc != BLE_HS_EALREADY) {
ESP_LOGW(TAG, "Failed to cancel discovery; rc=%d", rc);
return ESP_FAIL;
}
}
ESP_LOGI(TAG, "GAP discovery stopped (scan suppressed)");
return ESP_OK;
}
esp_err_t ble_esl_ap_stop_pawr(void)
{
if (!g_esl_ap || !g_esl_ap->initialized || !g_esl_ap->pawr_started) {
return ESP_ERR_INVALID_STATE;
}
/* Stop PAwR broadcasting */
esp_err_t ret = ble_esl_ap_pawr_stop();
if (ret != ESP_OK) {
ESP_LOGW(TAG, "Failed to stop PAwR: %s", esp_err_to_name(ret));
}
g_esl_ap->started = false;
ESP_LOGI(TAG, "ESL AP stopped");
g_esl_ap->pawr_started = false;
ESP_LOGI(TAG, "PAwR stopped");
return ESP_OK;
}
@@ -464,14 +648,8 @@ esp_err_t ble_esl_ap_connect(const uint8_t *addr, uint8_t addr_type)
ESP_LOGE(TAG, "Failed to initiate connection; rc=%d", rc);
ble_esl_ap_free_conn(conn);
/* Resume scanning if we were started */
if (g_esl_ap->started) {
struct ble_gap_disc_params disc_params = {0};
disc_params.filter_duplicates = 1;
disc_params.passive = 0;
ble_gap_disc(own_addr_type, BLE_HS_FOREVER, &disc_params,
ble_esl_ap_gap_event, NULL);
}
/* Resume scanning if started */
ble_esl_ap_maybe_resume_discovery();
return ESP_FAIL;
}
@@ -488,17 +666,29 @@ esp_err_t ble_esl_ap_connect_synced(ble_esl_address_t esl_addr)
uint16_t addr_key = BLE_ESL_AP_ADDR_PACK(esl_addr);
/* The ESL must be tracked and currently Synchronized to our PAwR train.
* Only a Synchronized ESL is reachable via the Periodic Advertising
* Connection procedure; in other states use ble_esl_ap_connect(). */
/* The ESL must be tracked. The tag side must still be on our PAwR train
* (Synchronized at the ESL); AP runtime may be Unsynchronized after reboot
* while the tag is still reachable via Periodic Advertising Connection. */
uint8_t ble_addr[6];
uint8_t ble_addr_type;
ble_esl_state_t esl_state;
ble_esl_ap_tracking_lock();
ble_esl_ap_esl_entry_t *esl = ble_esl_ap_find_esl(addr_key);
if (!esl) {
ble_esl_ap_tracking_unlock();
ESP_LOGE(TAG, "connect_synced: ESL 0x%04X not tracked", addr_key);
return ESP_ERR_NOT_FOUND;
}
if (esl->state != BLE_ESL_STATE_SYNCHRONIZED) {
ESP_LOGE(TAG, "connect_synced: ESL 0x%04X not Synchronized (state=%d)",
addr_key, esl->state);
esl_state = esl->state;
memcpy(ble_addr, esl->ble_addr, 6);
ble_addr_type = esl->ble_addr_type;
ble_esl_ap_tracking_unlock();
if (esl_state != BLE_ESL_STATE_SYNCHRONIZED &&
esl_state != BLE_ESL_STATE_UNSYNCHRONIZED) {
ESP_LOGE(TAG, "connect_synced: ESL 0x%04X bad state for PAwR connect (state=%d)",
addr_key, esl_state);
return ESP_ERR_INVALID_STATE;
}
@@ -508,13 +698,13 @@ esp_err_t ble_esl_ap_connect_synced(ble_esl_address_t esl_addr)
ESP_LOGE(TAG, "connect_synced: max connections reached");
return ESP_ERR_NO_MEM;
}
memcpy(conn->addr, esl->ble_addr, 6);
conn->addr_type = esl->ble_addr_type;
memcpy(conn->addr, ble_addr, 6);
conn->addr_type = ble_addr_type;
conn->esl_addr = addr_key;
ble_addr_t peer_addr;
peer_addr.type = esl->ble_addr_type;
memcpy(peer_addr.val, esl->ble_addr, 6);
peer_addr.type = ble_addr_type;
memcpy(peer_addr.val, ble_addr, 6);
uint8_t own_addr_type;
int rc = ble_hs_id_infer_auto(0, &own_addr_type);
@@ -544,14 +734,8 @@ esp_err_t ble_esl_ap_connect_synced(ble_esl_address_t esl_addr)
ESP_LOGE(TAG, "connect_synced: ble_gap_connect_with_synced failed; rc=%d", rc);
ble_esl_ap_free_conn(conn);
/* Resume scanning if we were started */
if (g_esl_ap->started && !ble_gap_disc_active()) {
struct ble_gap_disc_params disc_params = {0};
disc_params.filter_duplicates = 1;
disc_params.passive = 0;
ble_gap_disc(own_addr_type, BLE_HS_FOREVER, &disc_params,
ble_esl_ap_gap_event, NULL);
}
/* Resume scanning if started */
ble_esl_ap_maybe_resume_discovery();
return ESP_FAIL;
}
@@ -580,7 +764,23 @@ esp_err_t ble_esl_ap_disconnect(uint16_t conn_handle)
return ESP_OK;
}
/* ========================== GAP Event Handler ========================== */
esp_err_t ble_esl_ap_abort_tag_connections(void)
{
if (!g_esl_ap || !g_esl_ap->initialized) {
return ESP_ERR_INVALID_STATE;
}
ble_gap_conn_cancel();
for (int i = 0; i < CONFIG_BLE_ESL_AP_MAX_CONNECTIONS; i++) {
ble_esl_ap_conn_t *conn = &g_esl_ap->conns[i];
if (conn->in_use && conn->conn_handle != BLE_ESL_AP_CONN_HANDLE_INVALID) {
(void)ble_esl_ap_disconnect(conn->conn_handle);
}
}
return ESP_OK;
}
static int ble_esl_ap_gap_event(struct ble_gap_event *event, void *arg)
{
@@ -631,6 +831,28 @@ static int ble_esl_ap_gap_event(struct ble_gap_event *event, void *arg)
event->mtu.conn_handle, event->mtu.value);
return 0;
case BLE_GAP_EVENT_CONN_UPDATE: {
struct ble_gap_conn_desc desc;
if (ble_gap_conn_find(event->conn_update.conn_handle, &desc) == 0) {
ESP_LOGI(TAG, "ESL link updated: conn=%u interval=%u (%u ms) latency=%u status=%d",
event->conn_update.conn_handle, desc.conn_itvl,
desc.conn_itvl * 125 / 100, desc.conn_latency,
event->conn_update.status);
}
return 0;
}
case BLE_GAP_EVENT_PHY_UPDATE_COMPLETE:
ESP_LOGI(TAG, "ESL PHY updated: conn=%u tx=%u rx=%u status=%d",
event->phy_updated.conn_handle, event->phy_updated.tx_phy,
event->phy_updated.rx_phy, event->phy_updated.status);
return 0;
case BLE_GAP_EVENT_DATA_LEN_CHG:
ESP_LOGI(TAG, "ESL data length updated: conn=%u",
event->data_len_chg.conn_handle);
return 0;
default:
return 0;
}
@@ -638,6 +860,34 @@ static int ble_esl_ap_gap_event(struct ble_gap_event *event, void *arg)
/* ========================== Scan Result Handling ========================== */
static bool adv_contains_esl_uuid16(const uint8_t *data, uint8_t len)
{
if (data == NULL || len < 4) {
return false;
}
uint8_t i = 0;
while (i + 1 < len) {
uint8_t field_len = data[i];
if (field_len == 0 || (uint16_t)i + 1u + field_len > len) {
break;
}
uint8_t type = data[i + 1];
if (type == BLE_HS_ADV_TYPE_INCOMP_UUIDS16 ||
type == BLE_HS_ADV_TYPE_COMP_UUIDS16) {
const uint8_t *uuids = &data[i + 2];
uint8_t uuid_bytes = (uint8_t)(field_len - 1);
for (uint8_t off = 0; off + 1 < uuid_bytes; off += 2) {
uint16_t uuid = (uint16_t)uuids[off] | ((uint16_t)uuids[off + 1] << 8);
if (uuid == BLE_ESL_SVC_UUID) {
return true;
}
}
}
i = (uint8_t)(i + 1 + field_len);
}
return false;
}
static void handle_scan_result(struct ble_gap_event *event)
{
if (!g_esl_ap || !g_esl_ap->app_cb) {
@@ -648,6 +898,8 @@ static void handle_scan_result(struct ble_gap_event *event)
int8_t rssi;
const uint8_t *data;
uint8_t length_data;
bool is_connectable = false;
bool data_complete = true;
if (event->type == BLE_GAP_EVENT_EXT_DISC) {
const struct ble_gap_ext_disc_desc *ext = &event->ext_disc;
@@ -655,15 +907,24 @@ static void handle_scan_result(struct ble_gap_event *event)
rssi = ext->rssi;
data = ext->data;
length_data = ext->length_data;
is_connectable = (ext->props & BLE_HCI_ADV_CONN_MASK) != 0;
data_complete = (ext->data_status == BLE_GAP_EXT_ADV_DATA_STATUS_COMPLETE);
} else {
const struct ble_gap_disc_desc *disc = &event->disc;
addr = disc->addr;
rssi = disc->rssi;
data = disc->data;
length_data = disc->length_data;
is_connectable = (disc->event_type == BLE_HCI_ADV_RPT_EVTYPE_ADV_IND ||
disc->event_type == BLE_HCI_ADV_RPT_EVTYPE_DIR_IND);
data_complete = true;
}
bool associated = ble_esl_ap_is_associated(addr.val, addr.type);
bool advertises_esl = false;
if (data_complete) {
advertises_esl = adv_contains_esl_uuid16(data, length_data);
}
ble_esl_ap_scan_result_t result;
memcpy(result.addr, addr.val, 6);
@@ -672,6 +933,9 @@ static void handle_scan_result(struct ble_gap_event *event)
result.adv_data = data;
result.adv_data_len = length_data;
result.is_associated = associated;
result.is_connectable = is_connectable;
result.data_complete = data_complete;
result.advertises_esl_service = advertises_esl;
g_esl_ap->app_cb(BLE_ESL_AP_EVT_SCAN_RESULT, &result);
}
@@ -706,16 +970,7 @@ static void handle_connect_event(struct ble_gap_event *event, void *arg)
}
/* Resume scanning if started */
if (g_esl_ap->started && !ble_gap_disc_active()) {
uint8_t own_addr_type;
if (ble_hs_id_infer_auto(0, &own_addr_type) == 0) {
struct ble_gap_disc_params disc_params = {0};
disc_params.filter_duplicates = 1;
disc_params.passive = 0;
ble_gap_disc(own_addr_type, BLE_HS_FOREVER, &disc_params,
ble_esl_ap_gap_event, NULL);
}
}
ble_esl_ap_maybe_resume_discovery();
return;
}
@@ -744,16 +999,7 @@ static void handle_connect_event(struct ble_gap_event *event, void *arg)
ble_gap_terminate(conn_handle, BLE_ERR_REM_USER_CONN_TERM);
/* Resume scanning if started */
if (g_esl_ap->started && !ble_gap_disc_active()) {
uint8_t own_addr_type;
if (ble_hs_id_infer_auto(0, &own_addr_type) == 0) {
struct ble_gap_disc_params disc_params = {0};
disc_params.filter_duplicates = 1;
disc_params.passive = 0;
ble_gap_disc(own_addr_type, BLE_HS_FOREVER, &disc_params,
ble_esl_ap_gap_event, NULL);
}
}
ble_esl_ap_maybe_resume_discovery();
return;
}
@@ -763,16 +1009,7 @@ static void handle_connect_event(struct ble_gap_event *event, void *arg)
ble_gap_terminate(conn_handle, BLE_ERR_REM_USER_CONN_TERM);
/* Resume scanning if started */
if (g_esl_ap->started && !ble_gap_disc_active()) {
uint8_t own_addr_type;
if (ble_hs_id_infer_auto(0, &own_addr_type) == 0) {
struct ble_gap_disc_params disc_params = {0};
disc_params.filter_duplicates = 1;
disc_params.passive = 0;
ble_gap_disc(own_addr_type, BLE_HS_FOREVER, &disc_params,
ble_esl_ap_gap_event, NULL);
}
}
ble_esl_ap_maybe_resume_discovery();
return;
}
@@ -794,16 +1031,7 @@ static void handle_connect_event(struct ble_gap_event *event, void *arg)
ESP_LOGI(TAG, "Initiate encryption; conn_handle=%u", conn_handle);
/* Resume scanning if started */
if (g_esl_ap->started && !ble_gap_disc_active()) {
uint8_t own_addr_type;
if (ble_hs_id_infer_auto(0, &own_addr_type) == 0) {
struct ble_gap_disc_params disc_params = {0};
disc_params.filter_duplicates = 1;
disc_params.passive = 0;
ble_gap_disc(own_addr_type, BLE_HS_FOREVER, &disc_params,
ble_esl_ap_gap_event, NULL);
}
}
ble_esl_ap_maybe_resume_discovery();
}
/* ========================== Disconnect Event Handling ========================== */
@@ -850,21 +1078,13 @@ static void handle_disconnect_event(struct ble_gap_event *event)
/* Let lifecycle module complete any pending synchronize procedure
* and handle link-loss state transitions */
ble_esl_ap_lifecycle_handle_disconnect(conn_handle);
ble_esl_ap_lifecycle_disconnect_check(conn);
/* Free connection slot */
ble_esl_ap_free_conn(conn);
/* Resume scanning if started */
if (g_esl_ap->started && !ble_gap_disc_active()) {
uint8_t own_addr_type;
if (ble_hs_id_infer_auto(0, &own_addr_type) == 0) {
struct ble_gap_disc_params disc_params = {0};
disc_params.filter_duplicates = 1;
disc_params.passive = 0;
ble_gap_disc(own_addr_type, BLE_HS_FOREVER, &disc_params,
ble_esl_ap_gap_event, NULL);
}
}
ble_esl_ap_maybe_resume_discovery();
}
/* ======================================================================== */
@@ -893,6 +1113,8 @@ static void handle_enc_change(struct ble_gap_event *event)
* Advertising Connection procedure) — the ESL has entered the Updating
* state. Track it so ble_esl_ap_configure()/synchronize() see a valid state
* and link-loss recovery routes it back to Unsynchronized. */
ble_esl_ap_state_evt_snap_t snap = { 0 };
ble_esl_ap_tracking_lock();
ble_esl_ap_esl_entry_t *esl = ble_esl_ap_find_esl_by_ble_addr(conn->addr,
conn->addr_type);
if (esl != NULL &&
@@ -900,8 +1122,14 @@ static void handle_enc_change(struct ble_gap_event *event)
esl->state == BLE_ESL_STATE_UNSYNCHRONIZED)) {
esl->conn_handle = conn_handle;
conn->esl_addr = esl->esl_addr;
ble_esl_ap_update_esl_state(esl->esl_addr, BLE_ESL_STATE_UPDATING);
(void)ble_esl_ap_update_esl_state_locked(esl->esl_addr,
BLE_ESL_STATE_UPDATING,
&snap);
}
ble_esl_ap_tracking_unlock();
ble_esl_ap_dispatch_state_evt(&snap);
request_fast_esl_link(conn_handle);
if (conn->disc_done) {
ESP_LOGD(TAG, "Encryption change on already-discovered conn_handle=%u; skipping discovery",
@@ -16,8 +16,12 @@
#include <string.h>
#include <stdlib.h>
#include <assert.h>
#include <stddef.h>
#include "freertos/FreeRTOS.h"
#include "freertos/semphr.h"
#include "freertos/task.h"
#include "esp_log.h"
#include "esp_timer.h"
#include "host/ble_gap.h"
@@ -48,6 +52,10 @@ static const char *TAG = "esl_ap_lifecycle";
_Static_assert(sizeof(ble_esl_key_material_t) == BLE_ESL_KEY_MATERIAL_SIZE,
"ble_esl_key_material_t must match the on-air Key Material layout");
#define LF_OP_NONE 0
#define LF_OP_CONFIGURE 1
#define LF_OP_ABS_TIME 2
/* ========================== Internal Context Structures ========================== */
/**
@@ -56,7 +64,9 @@ _Static_assert(sizeof(ble_esl_key_material_t) == BLE_ESL_KEY_MATERIAL_SIZE,
typedef struct {
uint16_t conn_handle;
ble_esl_ap_esl_config_t config;
ble_esl_key_material_t ap_sync_key; /*!< Snapshot of the AP-wide sync key */
uint16_t esl_addr;
uint32_t lf_generation;
} configure_ctx_t;
/**
@@ -117,6 +127,14 @@ static void configure_write_abs_time_cb(uint16_t conn_handle, esp_err_t status,
const uint8_t *data, uint16_t data_len,
void *user_data);
typedef struct {
uint16_t conn_handle;
uint32_t generation;
bool public_event;
void *user_data;
ble_esl_ap_gatt_cb_t chained_cb;
} write_abs_time_ctx_t;
/* Read info chain callbacks */
static void read_info_display_cb(uint16_t conn_handle, esp_err_t status,
const uint8_t *data, uint16_t data_len,
@@ -155,16 +173,234 @@ void ble_esl_ap_lifecycle_handle_ots_event(uint16_t conn_id,
/** Active image transfer context (one at a time) */
static image_transfer_ctx_t *s_image_ctx = NULL;
static bool s_image_pumping;
static bool s_image_done;
/** Active synchronize context (only one at a time; new requests are rejected while active) */
static synchronize_ctx_t *s_sync_ctx = NULL;
static SemaphoreHandle_t s_lifecycle_mutex;
static TaskHandle_t s_lifecycle_holder;
_Static_assert(sizeof(ble_esl_key_material_t) == 24, "sync key packed 24");
_Static_assert(sizeof(ble_esl_key_material_t) == 24, "resp key packed 24");
/* In-memory compactness only — not an NVS/wire layout. */
_Static_assert(offsetof(ble_esl_ap_persisted_esl_t, resp_key) == 9, "resp key follows identity");
static void lf_lock(void)
{
assert(s_lifecycle_mutex != NULL);
xSemaphoreTake(s_lifecycle_mutex, portMAX_DELAY);
s_lifecycle_holder = xTaskGetCurrentTaskHandle();
}
static void lf_unlock(void)
{
s_lifecycle_holder = NULL;
xSemaphoreGive(s_lifecycle_mutex);
}
bool ble_esl_ap_lifecycle_held(void)
{
return s_lifecycle_holder == xTaskGetCurrentTaskHandle();
}
static bool lf_has_inflight_unlocked(void)
{
if (g_esl_ap == NULL) {
return false;
}
for (int i = 0; i < CONFIG_BLE_ESL_AP_MAX_CONNECTIONS; i++) {
if (g_esl_ap->conns[i].in_use && g_esl_ap->conns[i].lf_op != LF_OP_NONE) {
return true;
}
}
return false;
}
bool ble_esl_ap_lifecycle_has_inflight(void)
{
if (s_lifecycle_mutex == NULL) {
return false;
}
lf_lock();
bool busy = lf_has_inflight_unlocked();
lf_unlock();
return busy;
}
void ble_esl_ap_lifecycle_disconnect_check(ble_esl_ap_conn_t *conn)
{
if (conn == NULL || s_lifecycle_mutex == NULL) {
return;
}
lf_lock();
if (conn->lf_op != LF_OP_NONE) {
ESP_LOGW(TAG, "disconnect: leftover lifecycle op=%u conn=%u (GATT should have completed)",
conn->lf_op, conn->conn_handle);
conn->lf_op = LF_OP_NONE;
conn->lf_generation++;
}
lf_unlock();
}
static esp_err_t lf_occupy(ble_esl_ap_conn_t *conn, uint8_t op)
{
assert(ble_esl_ap_lifecycle_held());
if (conn->lf_op != LF_OP_NONE) {
return ESP_ERR_INVALID_STATE;
}
conn->lf_generation++;
conn->lf_op = op;
return ESP_OK;
}
static void lf_release(ble_esl_ap_conn_t *conn, uint32_t generation)
{
assert(ble_esl_ap_lifecycle_held());
if (conn != NULL && conn->lf_generation == generation) {
conn->lf_op = LF_OP_NONE;
}
}
static esp_err_t write_abs_time_bytes(uint16_t conn_handle, uint32_t abs_time_ms,
ble_esl_ap_gatt_cb_t cb, void *user_data)
{
uint8_t abs_time_data[4] = {
(uint8_t)(abs_time_ms & 0xFF),
(uint8_t)((abs_time_ms >> 8) & 0xFF),
(uint8_t)((abs_time_ms >> 16) & 0xFF),
(uint8_t)((abs_time_ms >> 24) & 0xFF),
};
return ble_esl_ap_gatt_write(conn_handle, BLE_ESL_CHR_UUID_CURRENT_ABS_TIME,
abs_time_data, sizeof(abs_time_data), cb, user_data);
}
static void write_abs_time_gatt_cb(uint16_t conn_handle, esp_err_t status,
const uint8_t *data, uint16_t data_len,
void *user_data)
{
(void)data;
(void)data_len;
write_abs_time_ctx_t *ctx = (write_abs_time_ctx_t *)user_data;
if (ctx == NULL) {
return;
}
if (g_esl_ap == NULL) {
free(ctx);
return;
}
bool emit = false;
ble_esl_ap_gatt_cb_t chained = ctx->chained_cb;
void *chained_ud = ctx->user_data;
lf_lock();
ble_esl_ap_conn_t *conn = ble_esl_ap_find_conn(conn_handle);
if (conn != NULL && conn->lf_generation == ctx->generation) {
if (ctx->public_event && conn->lf_op == LF_OP_ABS_TIME) {
conn->lf_op = LF_OP_NONE;
emit = true;
}
}
lf_unlock();
if (emit && g_esl_ap->app_cb != NULL) {
ble_esl_ap_abs_time_written_t evt = {
.conn_handle = conn_handle,
.status = status,
};
g_esl_ap->app_cb(BLE_ESL_AP_EVT_ABS_TIME_WRITTEN, &evt);
}
if (chained != NULL) {
chained(conn_handle, status, NULL, 0, chained_ud);
}
free(ctx);
}
esp_err_t ble_esl_ap_write_absolute_time(uint16_t conn_handle, uint32_t abs_time_ms)
{
if (g_esl_ap == NULL) {
return ESP_ERR_INVALID_STATE;
}
ble_esl_ap_conn_t *conn = ble_esl_ap_find_conn(conn_handle);
if (conn == NULL) {
return ESP_ERR_NOT_FOUND;
}
write_abs_time_ctx_t *ctx = calloc(1, sizeof(*ctx));
if (ctx == NULL) {
return ESP_ERR_NO_MEM;
}
lf_lock();
esp_err_t occ = lf_occupy(conn, LF_OP_ABS_TIME);
uint32_t gen = conn->lf_generation;
lf_unlock();
if (occ != ESP_OK) {
free(ctx);
return occ;
}
ctx->conn_handle = conn_handle;
ctx->generation = gen;
ctx->public_event = true;
esp_err_t ret = write_abs_time_bytes(conn_handle, abs_time_ms,
write_abs_time_gatt_cb, ctx);
if (ret != ESP_OK) {
lf_lock();
conn = ble_esl_ap_find_conn(conn_handle);
lf_release(conn, gen);
lf_unlock();
free(ctx);
}
return ret;
}
static esp_err_t write_abs_time_for_configure(uint16_t conn_handle, uint32_t generation,
void *configure_ctx)
{
write_abs_time_ctx_t *ctx = calloc(1, sizeof(*ctx));
if (ctx == NULL) {
return ESP_ERR_NO_MEM;
}
ctx->conn_handle = conn_handle;
ctx->generation = generation;
ctx->public_event = false;
ctx->chained_cb = configure_write_abs_time_cb;
ctx->user_data = configure_ctx;
uint32_t abs_time_ms = (uint32_t)(esp_timer_get_time() / 1000ULL);
esp_err_t ret = write_abs_time_bytes(conn_handle, abs_time_ms,
write_abs_time_gatt_cb, ctx);
if (ret != ESP_OK) {
free(ctx);
}
return ret;
}
static void configure_release_slot(uint16_t conn_handle, uint32_t generation)
{
lf_lock();
ble_esl_ap_conn_t *conn = ble_esl_ap_find_conn(conn_handle);
lf_release(conn, generation);
lf_unlock();
}
/* ========================== Lifecycle Init / Deinit ========================== */
esp_err_t ble_esl_ap_lifecycle_init(void)
{
assert(g_esl_ap != NULL);
if (s_lifecycle_mutex == NULL) {
s_lifecycle_mutex = xSemaphoreCreateMutex();
if (s_lifecycle_mutex == NULL) {
return ESP_ERR_NO_MEM;
}
}
s_lifecycle_holder = NULL;
esp_timer_create_args_t timer_args = {
.callback = lifecycle_timeout_cb,
.arg = NULL,
@@ -176,6 +412,8 @@ esp_err_t ble_esl_ap_lifecycle_init(void)
esp_err_t ret = esp_timer_create(&timer_args, &g_esl_ap->timeout_timer);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "Failed to create lifecycle timer: %s", esp_err_to_name(ret));
vSemaphoreDelete(s_lifecycle_mutex);
s_lifecycle_mutex = NULL;
return ret;
}
@@ -184,6 +422,8 @@ esp_err_t ble_esl_ap_lifecycle_init(void)
ESP_LOGE(TAG, "Failed to start lifecycle timer: %s", esp_err_to_name(ret));
esp_timer_delete(g_esl_ap->timeout_timer);
g_esl_ap->timeout_timer = NULL;
vSemaphoreDelete(s_lifecycle_mutex);
s_lifecycle_mutex = NULL;
return ret;
}
@@ -216,6 +456,12 @@ void ble_esl_ap_lifecycle_deinit(void)
s_sync_ctx = NULL;
}
if (s_lifecycle_mutex != NULL) {
vSemaphoreDelete(s_lifecycle_mutex);
s_lifecycle_mutex = NULL;
s_lifecycle_holder = NULL;
}
ESP_LOGI(TAG, "Lifecycle sub-module deinitialized");
}
@@ -237,40 +483,61 @@ static void lifecycle_timeout_cb(void *arg)
}
int64_t now_us = esp_timer_get_time();
int i = 0;
for (int i = 0; i < CONFIG_BLE_ESL_AP_MAX_ESLS; i++) {
ble_esl_ap_esl_entry_t *esl = &g_esl_ap->esls[i];
if (!esl->in_use) {
continue;
}
while (i < CONFIG_BLE_ESL_AP_MAX_ESLS) {
ble_esl_ap_state_evt_snap_t snap = { 0 };
if (esl->state == BLE_ESL_STATE_SYNCHRONIZED) {
/* Check 60-minute sync timeout */
if (esl->last_sync_time_us != 0 &&
(now_us - esl->last_sync_time_us) >= (int64_t)LIFECYCLE_TIMEOUT_US) {
ESP_LOGW(TAG, "ESL 0x%04X: sync timeout (60 min), transitioning to Unsynchronized",
esl->esl_addr);
ble_esl_ap_update_esl_state(esl->esl_addr,
BLE_ESL_STATE_UNSYNCHRONIZED);
ble_esl_ap_tracking_lock();
for (; i < CONFIG_BLE_ESL_AP_MAX_ESLS; i++) {
ble_esl_ap_esl_entry_t *esl = &g_esl_ap->esls[i];
if (!esl->in_use) {
continue;
}
} else if (esl->state == BLE_ESL_STATE_UNSYNCHRONIZED) {
/* Check 60-minute reconnect timeout */
if (esl->unsync_entry_time_us != 0 &&
(now_us - esl->unsync_entry_time_us) >= (int64_t)LIFECYCLE_TIMEOUT_US) {
ESP_LOGW(TAG, "ESL 0x%04X: unsync timeout (60 min), transitioning to Unassociated",
esl->esl_addr);
ble_esl_ap_update_esl_state(esl->esl_addr,
BLE_ESL_STATE_UNASSOCIATED);
if (esl->state == BLE_ESL_STATE_SYNCHRONIZED) {
/* Check 60-minute sync timeout */
if (esl->last_sync_time_us != 0 &&
(now_us - esl->last_sync_time_us) >= (int64_t)LIFECYCLE_TIMEOUT_US) {
ESP_LOGW(TAG, "ESL 0x%04X: sync timeout (60 min), transitioning to Unsynchronized",
esl->esl_addr);
(void)ble_esl_ap_update_esl_state_locked(esl->esl_addr,
BLE_ESL_STATE_UNSYNCHRONIZED,
&snap);
i++;
break;
}
} else if (esl->state == BLE_ESL_STATE_UNSYNCHRONIZED) {
/* Check 60-minute reconnect timeout */
if (esl->unsync_entry_time_us != 0 &&
(now_us - esl->unsync_entry_time_us) >= (int64_t)LIFECYCLE_TIMEOUT_US) {
ESP_LOGW(TAG, "ESL 0x%04X: unsync timeout (60 min), transitioning to Unassociated",
esl->esl_addr);
(void)ble_esl_ap_update_esl_state_locked(esl->esl_addr,
BLE_ESL_STATE_UNASSOCIATED,
&snap);
i++;
break;
}
}
}
ble_esl_ap_tracking_unlock();
ble_esl_ap_dispatch_state_evt(&snap);
}
}
/* ========================== Update ESL State ========================== */
esp_err_t ble_esl_ap_update_esl_state(uint16_t esl_addr,
ble_esl_state_t new_state)
esp_err_t ble_esl_ap_update_esl_state_locked(uint16_t esl_addr,
ble_esl_state_t new_state,
ble_esl_ap_state_evt_snap_t *snap)
{
assert(ble_esl_ap_tracking_held());
if (snap != NULL) {
snap->pending = false;
}
if (g_esl_ap == NULL) {
return ESP_ERR_INVALID_STATE;
}
@@ -296,16 +563,12 @@ esp_err_t ble_esl_ap_update_esl_state(uint16_t esl_addr,
esl->unsync_entry_time_us = 0;
}
/* Fire state changed event to application */
ble_esl_ap_state_changed_t evt = {
.conn_handle = esl->conn_handle,
.esl_addr = ble_esl_ap_addr_unpack(esl_addr),
.old_state = old_state,
.new_state = new_state,
};
if (g_esl_ap->app_cb != NULL) {
g_esl_ap->app_cb(BLE_ESL_AP_EVT_STATE_CHANGED, &evt);
if (snap != NULL) {
snap->pending = true;
snap->evt.conn_handle = esl->conn_handle;
snap->evt.esl_addr = ble_esl_ap_addr_unpack(esl_addr);
snap->evt.old_state = old_state;
snap->evt.new_state = new_state;
}
/* Handle Unassociated cleanup */
@@ -319,6 +582,17 @@ esp_err_t ble_esl_ap_update_esl_state(uint16_t esl_addr,
return ESP_OK;
}
esp_err_t ble_esl_ap_update_esl_state(uint16_t esl_addr,
ble_esl_state_t new_state)
{
ble_esl_ap_state_evt_snap_t snap = { 0 };
ble_esl_ap_tracking_lock();
esp_err_t ret = ble_esl_ap_update_esl_state_locked(esl_addr, new_state, &snap);
ble_esl_ap_tracking_unlock();
ble_esl_ap_dispatch_state_evt(&snap);
return ret;
}
/* ========================== Get ESL State ========================== */
ble_esl_state_t ble_esl_ap_get_esl_state(ble_esl_address_t esl_addr)
@@ -327,12 +601,12 @@ ble_esl_state_t ble_esl_ap_get_esl_state(ble_esl_address_t esl_addr)
return BLE_ESL_STATE_UNASSOCIATED;
}
ble_esl_ap_tracking_lock();
ble_esl_ap_esl_entry_t *esl = ble_esl_ap_find_esl(BLE_ESL_AP_ADDR_PACK(esl_addr));
if (esl == NULL) {
return BLE_ESL_STATE_UNASSOCIATED;
}
return esl->state;
ble_esl_state_t state = (esl == NULL) ? BLE_ESL_STATE_UNASSOCIATED
: esl->state;
ble_esl_ap_tracking_unlock();
return state;
}
/* ========================== Configure ========================== */
@@ -353,6 +627,11 @@ esp_err_t ble_esl_ap_configure(uint16_t conn_handle,
return ESP_ERR_INVALID_ARG;
}
if (!g_esl_ap->ap_sync_key_valid) {
ESP_LOGE(TAG, "configure: AP Sync Key not set");
return ESP_ERR_INVALID_STATE;
}
/* Validate connection handle */
ble_esl_ap_conn_t *conn = ble_esl_ap_find_conn(conn_handle);
if (conn == NULL) {
@@ -368,19 +647,20 @@ esp_err_t ble_esl_ap_configure(uint16_t conn_handle,
ctx->conn_handle = conn_handle;
memcpy(&ctx->config, config, sizeof(ble_esl_ap_esl_config_t));
ctx->ap_sync_key = g_esl_ap->ap_sync_key;
ctx->esl_addr = BLE_ESL_AP_MAKE_ADDR(config->esl_id, config->group_id);
/* Store ESL address in connection context for cross-reference */
conn->esl_addr = ctx->esl_addr;
/* Find or create ESL tracking entry */
ble_esl_ap_tracking_lock();
ble_esl_ap_esl_entry_t *esl = ble_esl_ap_find_esl(ctx->esl_addr);
if (esl == NULL) {
/* Try to find by BLE address (may already exist from a prior association) */
esl = ble_esl_ap_find_esl_by_ble_addr(conn->addr, conn->addr_type);
if (esl == NULL) {
esl = ble_esl_ap_alloc_esl();
if (esl == NULL) {
ble_esl_ap_tracking_unlock();
ESP_LOGE(TAG, "configure: ESL tracking table full");
free(ctx);
return ESP_ERR_NO_MEM;
@@ -388,7 +668,6 @@ esp_err_t ble_esl_ap_configure(uint16_t conn_handle,
}
}
/* Initialize/update the tracking entry */
esl->in_use = true;
esl->esl_addr = ctx->esl_addr;
memcpy(esl->ble_addr, conn->addr, 6);
@@ -399,9 +678,17 @@ esp_err_t ble_esl_ap_configure(uint16_t conn_handle,
/* Store key material in the ESL tracking entry */
esl->resp_key = config->resp_key;
ble_esl_ap_tracking_unlock();
/* Set PAwR sync key (shared across all ESLs) */
ble_esl_ap_pawr_set_sync_key(&config->ap_sync_key);
lf_lock();
esp_err_t occ = lf_occupy(conn, LF_OP_CONFIGURE);
ctx->lf_generation = conn->lf_generation;
lf_unlock();
if (occ != ESP_OK) {
ble_esl_ap_update_esl_state(ctx->esl_addr, BLE_ESL_STATE_UNASSOCIATED);
free(ctx);
return occ;
}
/* Set per-ESL response key */
esp_err_t ret = ble_esl_ap_pawr_set_response_key(ctx->esl_addr,
@@ -423,6 +710,7 @@ esp_err_t ble_esl_ap_configure(uint16_t conn_handle,
if (ret != ESP_OK) {
ESP_LOGE(TAG, "configure: failed to initiate ESL Address write: %s",
esp_err_to_name(ret));
configure_release_slot(conn_handle, ctx->lf_generation);
ble_esl_ap_update_esl_state(ctx->esl_addr, BLE_ESL_STATE_UNASSOCIATED);
free(ctx);
return ret;
@@ -449,8 +737,8 @@ static void configure_write_addr_cb(uint16_t conn_handle, esp_err_t status,
/* Step 2: Write AP Sync Key Material (24 bytes: 16-byte key + 8-byte IV) */
esp_err_t ret = ble_esl_ap_gatt_write(conn_handle, BLE_ESL_CHR_UUID_AP_SYNC_KEY,
(const uint8_t *)&ctx->config.ap_sync_key,
sizeof(ctx->config.ap_sync_key),
(const uint8_t *)&ctx->ap_sync_key,
sizeof(ctx->ap_sync_key),
configure_write_sync_key_cb, ctx);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "configure: failed to initiate AP Sync Key write: %s",
@@ -461,6 +749,7 @@ static void configure_write_addr_cb(uint16_t conn_handle, esp_err_t status,
fail:
{
configure_release_slot(conn_handle, ctx->lf_generation);
ble_esl_ap_update_esl_state(ctx->esl_addr, BLE_ESL_STATE_UNASSOCIATED);
ble_esl_ap_configured_t evt = {
.conn_handle = conn_handle,
@@ -501,6 +790,7 @@ static void configure_write_sync_key_cb(uint16_t conn_handle, esp_err_t status,
fail:
{
configure_release_slot(conn_handle, ctx->lf_generation);
ble_esl_ap_update_esl_state(ctx->esl_addr, BLE_ESL_STATE_UNASSOCIATED);
ble_esl_ap_configured_t evt = {
.conn_handle = conn_handle,
@@ -527,19 +817,7 @@ static void configure_write_resp_key_cb(uint16_t conn_handle, esp_err_t status,
ESP_LOGD(TAG, "configure: Response Key written, writing Absolute Time");
/* Step 4: Write ESL Current Absolute Time (4 bytes from esp_timer_get_time) */
int64_t now_us = esp_timer_get_time();
/* Convert microseconds to milliseconds for ESL Absolute Time */
uint32_t abs_time_ms = (uint32_t)(now_us / 1000ULL);
uint8_t abs_time_data[4];
abs_time_data[0] = (uint8_t)(abs_time_ms & 0xFF);
abs_time_data[1] = (uint8_t)((abs_time_ms >> 8) & 0xFF);
abs_time_data[2] = (uint8_t)((abs_time_ms >> 16) & 0xFF);
abs_time_data[3] = (uint8_t)((abs_time_ms >> 24) & 0xFF);
esp_err_t ret = ble_esl_ap_gatt_write(conn_handle, BLE_ESL_CHR_UUID_CURRENT_ABS_TIME,
abs_time_data, sizeof(abs_time_data),
configure_write_abs_time_cb, ctx);
esp_err_t ret = write_abs_time_for_configure(conn_handle, ctx->lf_generation, ctx);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "configure: failed to initiate Absolute Time write: %s",
esp_err_to_name(ret));
@@ -549,6 +827,7 @@ static void configure_write_resp_key_cb(uint16_t conn_handle, esp_err_t status,
fail:
{
configure_release_slot(conn_handle, ctx->lf_generation);
ble_esl_ap_update_esl_state(ctx->esl_addr, BLE_ESL_STATE_UNASSOCIATED);
ble_esl_ap_configured_t evt = {
.conn_handle = conn_handle,
@@ -576,12 +855,16 @@ static void configure_write_abs_time_cb(uint16_t conn_handle, esp_err_t status,
ctx->esl_addr);
/* Mark configuration as complete in the tracking entry */
ble_esl_ap_tracking_lock();
ble_esl_ap_esl_entry_t *esl = ble_esl_ap_find_esl(ctx->esl_addr);
if (esl != NULL) {
esl->config_complete = true;
}
ble_esl_ap_tracking_unlock();
}
configure_release_slot(conn_handle, ctx->lf_generation);
/* Fire BLE_ESL_AP_EVT_CONFIGURED */
ble_esl_ap_configured_t evt = {
.conn_handle = conn_handle,
@@ -911,6 +1194,8 @@ esp_err_t ble_esl_ap_transfer_image(const ble_esl_ap_image_transfer_params_t *pa
s_image_ctx->data_len = params->data_len;
s_image_ctx->truncate = params->truncate;
s_image_ctx->otc_mtu = 256; /* Default L2CAP OTC MTU; updated on CHANNEL_OPEN */
s_image_pumping = false;
s_image_done = false;
/* Step 1: Select the target object via OLCP Go To (over GATT).
* Per the OTS spec, the Current Object must be selected *before* the
@@ -974,6 +1259,46 @@ static void image_transfer_finish(uint16_t conn_id, esp_err_t status)
free(s_image_ctx);
s_image_ctx = NULL;
s_image_pumping = false;
s_image_done = false;
}
/* NimBLE CoC TX has a single SDU slot. Keep filling it until the stack is
* busy or stalled; DATA_SENT / TX_UNSTALLED refill. Do not finish from the
* nested DATA_SENT that send_data() dispatches on success. */
static int image_transfer_pump(uint16_t conn_id)
{
if (s_image_ctx == NULL || s_image_pumping) {
return 0;
}
s_image_pumping = true;
int rc = 0;
for (int burst = 0; burst < 4; burst++) {
uint32_t offset = s_image_ctx->sent_offset;
if (offset >= s_image_ctx->data_len) {
break;
}
uint32_t left = s_image_ctx->data_len - offset;
uint16_t chunk = (left > s_image_ctx->otc_mtu) ?
s_image_ctx->otc_mtu : (uint16_t)left;
rc = ble_ots_client_send_data(conn_id, s_image_ctx->data + offset, chunk);
if (rc == BLE_HS_EBUSY) {
rc = 0;
break;
}
if (rc != 0) {
break;
}
s_image_ctx->sent_offset += chunk;
}
s_image_pumping = false;
if (rc == 0 && s_image_done) {
ESP_LOGI(TAG, "transfer_image: image transfer complete for index %d",
s_image_ctx->image_index);
image_transfer_finish(conn_id, ESP_OK);
}
return rc;
}
/**
@@ -1056,15 +1381,11 @@ void ble_esl_ap_lifecycle_handle_ots_event(uint16_t conn_id,
case BLE_OTS_CLIENT_EVT_OACP_RESPONSE: {
const ble_ots_client_oacp_response_t *oacp = (const ble_ots_client_oacp_response_t *)param;
if (oacp->request_opcode == 0x06 && oacp->result_code == 0x01) {
/* OACP Write success — send the first chunk of data.
* One send_data() call becomes one L2CAP SDU, so it must not
* exceed the negotiated OTC MTU (max SDU size) or ble_l2cap_send
* rejects it with BLE_HS_EBADDATA. Chunk by otc_mtu. */
/* OACP Write success — fill the CoC TX slot. One send_data() call
* is one L2CAP SDU and must stay within otc_mtu. */
ESP_LOGD(TAG, "transfer_image: OACP Write accepted, sending data");
uint16_t chunk_len = (s_image_ctx->data_len > s_image_ctx->otc_mtu) ?
s_image_ctx->otc_mtu : (uint16_t)s_image_ctx->data_len;
s_image_ctx->sent_offset = chunk_len;
int rc = ble_ots_client_send_data(conn_id, s_image_ctx->data, chunk_len);
s_image_done = false;
int rc = image_transfer_pump(conn_id);
if (rc != 0) {
ESP_LOGE(TAG, "transfer_image: send_data failed; rc=%d", rc);
image_transfer_finish(conn_id, ESP_FAIL);
@@ -1080,26 +1401,19 @@ void ble_esl_ap_lifecycle_handle_ots_event(uint16_t conn_id,
case BLE_OTS_CLIENT_EVT_DATA_SENT: {
const ble_ots_client_data_sent_t *sent = (const ble_ots_client_data_sent_t *)param;
if (sent->remaining == 0) {
/* Transfer complete */
ESP_LOGI(TAG, "transfer_image: image transfer complete for index %d",
s_image_ctx->image_index);
image_transfer_finish(conn_id, ESP_OK);
} else {
/* Send the next chunk, capped at the negotiated OTC MTU (one SDU). */
uint32_t offset = s_image_ctx->data_len - sent->remaining;
uint16_t chunk_len = (sent->remaining > s_image_ctx->otc_mtu) ?
s_image_ctx->otc_mtu : (uint16_t)sent->remaining;
s_image_ctx->sent_offset = offset + chunk_len;
ESP_LOGD(TAG, "transfer_image: sending next chunk at offset %lu, %u bytes "
"(%lu remaining)", (unsigned long)offset, chunk_len,
(unsigned long)sent->remaining);
int rc = ble_ots_client_send_data(conn_id,
s_image_ctx->data + offset,
chunk_len);
if (rc != 0) {
ESP_LOGE(TAG, "transfer_image: send_data failed for chunk; rc=%d", rc);
image_transfer_finish(conn_id, ESP_FAIL);
if (s_image_pumping) {
s_image_done = true;
} else {
ESP_LOGI(TAG, "transfer_image: image transfer complete for index %d",
s_image_ctx->image_index);
image_transfer_finish(conn_id, ESP_OK);
}
break;
}
int rc = image_transfer_pump(conn_id);
if (rc != 0) {
ESP_LOGE(TAG, "transfer_image: send_data failed for chunk; rc=%d", rc);
image_transfer_finish(conn_id, ESP_FAIL);
}
break;
}
@@ -1129,6 +1443,11 @@ void ble_esl_ap_lifecycle_handle_ots_event(uint16_t conn_id,
/* ========================== Synchronize ========================== */
bool ble_esl_ap_synchronize_in_progress(void)
{
return s_sync_ctx != NULL;
}
esp_err_t ble_esl_ap_synchronize(uint16_t conn_handle)
{
if (g_esl_ap == NULL) {
@@ -1142,12 +1461,15 @@ esp_err_t ble_esl_ap_synchronize(uint16_t conn_handle)
}
/* Need a valid ESL address to proceed */
ble_esl_ap_tracking_lock();
ble_esl_ap_esl_entry_t *esl = ble_esl_ap_find_esl(conn->esl_addr);
if (esl == NULL) {
ble_esl_ap_tracking_unlock();
ESP_LOGE(TAG, "synchronize: ESL address 0x%04X not tracked for conn 0x%04X",
conn->esl_addr, conn_handle);
return ESP_ERR_INVALID_STATE;
}
ble_esl_ap_tracking_unlock();
/* Only one synchronize procedure at a time */
if (s_sync_ctx != NULL) {
@@ -1296,14 +1618,16 @@ void ble_esl_ap_lifecycle_handle_disconnect(uint16_t conn_handle)
bool is_sync_disconnect = (s_sync_ctx != NULL &&
s_sync_ctx->conn_handle == conn_handle);
ble_esl_ap_state_evt_snap_t snap = { 0 };
if (g_esl_ap != NULL) {
ble_esl_ap_tracking_lock();
for (int i = 0; i < CONFIG_BLE_ESL_AP_MAX_ESLS; i++) {
ble_esl_ap_esl_entry_t *esl = &g_esl_ap->esls[i];
if (!esl->in_use || esl->conn_handle != conn_handle) {
continue;
}
/* Clear the connection handle */
esl->conn_handle = BLE_ESL_AP_CONN_HANDLE_INVALID;
if (!is_sync_disconnect) {
@@ -1311,23 +1635,28 @@ void ble_esl_ap_lifecycle_handle_disconnect(uint16_t conn_handle)
if (esl->config_complete) {
ESP_LOGI(TAG, "link-loss in Configuring (config complete) for ESL 0x%04X, "
"transitioning to Unsynchronized", esl->esl_addr);
ble_esl_ap_update_esl_state(esl->esl_addr,
BLE_ESL_STATE_UNSYNCHRONIZED);
(void)ble_esl_ap_update_esl_state_locked(esl->esl_addr,
BLE_ESL_STATE_UNSYNCHRONIZED,
&snap);
} else {
ESP_LOGI(TAG, "link-loss in Configuring (config incomplete) for ESL 0x%04X, "
"transitioning to Unassociated", esl->esl_addr);
ble_esl_ap_update_esl_state(esl->esl_addr,
BLE_ESL_STATE_UNASSOCIATED);
(void)ble_esl_ap_update_esl_state_locked(esl->esl_addr,
BLE_ESL_STATE_UNASSOCIATED,
&snap);
}
} else if (esl->state == BLE_ESL_STATE_UPDATING) {
ESP_LOGI(TAG, "link-loss in Updating for ESL 0x%04X, "
"transitioning to Unsynchronized", esl->esl_addr);
ble_esl_ap_update_esl_state(esl->esl_addr,
BLE_ESL_STATE_UNSYNCHRONIZED);
(void)ble_esl_ap_update_esl_state_locked(esl->esl_addr,
BLE_ESL_STATE_UNSYNCHRONIZED,
&snap);
}
}
break;
}
ble_esl_ap_tracking_unlock();
ble_esl_ap_dispatch_state_evt(&snap);
}
/* Step 2: Handle synchronize procedure completion if this disconnect
@@ -1387,3 +1716,81 @@ void ble_esl_ap_lifecycle_handle_disconnect(uint16_t conn_handle)
s_image_ctx = NULL;
}
}
/* ========================== Persisted ESL Restore ========================== */
esp_err_t ble_esl_ap_restore_persisted_esl(const ble_esl_ap_persisted_esl_t *info)
{
if (g_esl_ap == NULL || !g_esl_ap->initialized || info == NULL) {
return ESP_ERR_INVALID_ARG;
}
uint16_t esl_addr = BLE_ESL_AP_ADDR_PACK(info->esl_address);
if (esl_addr == 0) {
ESP_LOGI(TAG, "restore: esl_addr=0x0000 (esl_id=0 group_id=0)");
}
if (info->esl_address.esl_id == BLE_ESL_BROADCAST_ADDRESS) {
ESP_LOGE(TAG, "restore: broadcast esl_id not allowed");
return ESP_ERR_INVALID_ARG;
}
if (!g_esl_ap->ap_sync_key_valid) {
ESP_LOGE(TAG, "restore: AP Sync Key not set");
return ESP_ERR_INVALID_STATE;
}
ble_esl_ap_tracking_lock();
ble_esl_ap_esl_entry_t *esl = ble_esl_ap_find_esl(esl_addr);
if (esl == NULL) {
esl = ble_esl_ap_find_esl_by_ble_addr(info->ble_addr, info->ble_addr_type);
}
bool created = false;
ble_esl_ap_esl_entry_t backup = { 0 };
if (esl == NULL) {
esl = ble_esl_ap_alloc_esl();
created = true;
} else {
backup = *esl;
}
if (esl == NULL) {
ble_esl_ap_tracking_unlock();
ESP_LOGE(TAG, "restore: ESL tracking table full");
return ESP_ERR_NO_MEM;
}
esl->in_use = true;
esl->esl_addr = esl_addr;
memcpy(esl->ble_addr, info->ble_addr, 6);
esl->ble_addr_type = info->ble_addr_type;
esl->conn_handle = BLE_ESL_AP_CONN_HANDLE_INVALID;
esl->config_complete = true;
esl->pending_pawr_cmd_opcode = 0;
esl->resp_key = info->resp_key;
esl->last_sync_time_us = 0;
ble_esl_ap_state_evt_snap_t snap = { 0 };
esp_err_t ret = ble_esl_ap_update_esl_state_locked(esl_addr,
BLE_ESL_STATE_UNSYNCHRONIZED,
&snap);
if (ret != ESP_OK) {
if (created) {
memset(esl, 0, sizeof(*esl));
esl->in_use = false;
esl->conn_handle = BLE_ESL_AP_CONN_HANDLE_INVALID;
} else {
*esl = backup;
}
ble_esl_ap_tracking_unlock();
ESP_LOGE(TAG, "restore: failed to mark ESL 0x%04X unsynchronized: %s",
esl_addr, esp_err_to_name(ret));
return ret;
}
ble_esl_ap_tracking_unlock();
ble_esl_ap_dispatch_state_evt(&snap);
ESP_LOGI(TAG, "Restored ESL 0x%04X from persistence (awaiting resync)",
esl_addr);
return ESP_OK;
}
@@ -22,6 +22,7 @@
#include "freertos/FreeRTOS.h"
#include "freertos/semphr.h"
#include "freertos/task.h"
#include "nimble/ble.h"
#include "host/ble_hs.h"
@@ -37,6 +38,25 @@ static const char *TAG = "esl_ap_pawr";
/** Mutex protecting the per-subevent pending TX buffers (g_esl_ap->pawr_pending) */
static SemaphoreHandle_t s_pawr_mutex;
static TaskHandle_t s_pawr_holder;
static void pawr_lock(void)
{
assert(s_pawr_mutex != NULL);
xSemaphoreTake(s_pawr_mutex, portMAX_DELAY);
s_pawr_holder = xTaskGetCurrentTaskHandle();
}
static void pawr_unlock(void)
{
s_pawr_holder = NULL;
xSemaphoreGive(s_pawr_mutex);
}
bool ble_esl_ap_pawr_held(void)
{
return s_pawr_holder == xTaskGetCurrentTaskHandle();
}
/** Advertising instance used for PAwR */
#define PAWR_ADV_INSTANCE 0
@@ -88,6 +108,7 @@ void ble_esl_ap_pawr_deinit(void)
if (s_pawr_mutex != NULL) {
vSemaphoreDelete(s_pawr_mutex);
s_pawr_mutex = NULL;
s_pawr_holder = NULL;
}
ESP_LOGI(TAG, "PAwR sub-module deinitialized");
@@ -216,11 +237,11 @@ esp_err_t ble_esl_ap_pawr_stop(void)
/* Release pending TX buffers under the lock so the data-request callback
* (host task) never dereferences a freed pointer. */
xSemaphoreTake(s_pawr_mutex, portMAX_DELAY);
pawr_lock();
g_esl_ap->pawr_active = false;
free(g_esl_ap->pawr_pending);
g_esl_ap->pawr_pending = NULL;
xSemaphoreGive(s_pawr_mutex);
pawr_unlock();
ESP_LOGI(TAG, "PAwR broadcaster stopped");
@@ -248,13 +269,16 @@ esp_err_t ble_esl_ap_pawr_set_response_key(uint16_t esl_addr,
return ESP_ERR_INVALID_ARG;
}
ble_esl_ap_tracking_lock();
ble_esl_ap_esl_entry_t *entry = ble_esl_ap_find_esl(esl_addr);
if (entry == NULL) {
ble_esl_ap_tracking_unlock();
ESP_LOGE(TAG, "ESL 0x%04x not found for response key", esl_addr);
return ESP_ERR_NOT_FOUND;
}
entry->resp_key = *key_mat;
ble_esl_ap_tracking_unlock();
ESP_LOGI(TAG, "Response Key Material set for ESL 0x%04x", esl_addr);
return ESP_OK;
@@ -304,14 +328,14 @@ esp_err_t ble_esl_ap_pawr_send(uint8_t group_id, const uint8_t *payload,
/* The Randomizer is read and advanced under the lock: this function is a
* public command path and may be called from any application task. */
xSemaphoreTake(s_pawr_mutex, portMAX_DELAY);
pawr_lock();
esp_err_t enc_err = ble_esl_ead_encrypt(g_esl_ap->ap_sync_key.session_key,
g_esl_ap->ap_sync_key.iv,
g_esl_ap->randomizer,
plaintext,
plaintext_len,
encrypted_payload);
xSemaphoreGive(s_pawr_mutex);
pawr_unlock();
if (enc_err != ESP_OK) {
ESP_LOGE(TAG, "EAD encrypt failed: 0x%x", enc_err);
return enc_err;
@@ -349,9 +373,9 @@ esp_err_t ble_esl_ap_pawr_send(uint8_t group_id, const uint8_t *payload,
return ESP_ERR_INVALID_SIZE;
}
xSemaphoreTake(s_pawr_mutex, portMAX_DELAY);
pawr_lock();
if (g_esl_ap->pawr_pending == NULL) {
xSemaphoreGive(s_pawr_mutex);
pawr_unlock();
ESP_LOGE(TAG, "PAwR pending buffers not allocated");
return ESP_ERR_INVALID_STATE;
}
@@ -360,7 +384,7 @@ esp_err_t ble_esl_ap_pawr_send(uint8_t group_id, const uint8_t *payload,
slot->len = pos;
slot->repeats_left = PAWR_TX_REPEATS;
slot->valid = true;
xSemaphoreGive(s_pawr_mutex);
pawr_unlock();
ESP_LOGD(TAG, "PAwR sync packet queued for group %u (payload_len=%u, ad_len=%u)",
group_id, payload_len, pos);
@@ -412,11 +436,19 @@ esp_err_t ble_esl_ap_pawr_parse_response(uint16_t esl_addr,
uint8_t enc_payload_len = outer_len - 1;
/* Step 1: Look up ESL entry by esl_addr */
uint8_t resp_key[BLE_ESL_SESSION_KEY_SIZE];
uint8_t resp_iv[BLE_ESL_IV_SIZE];
ble_esl_ap_tracking_lock();
ble_esl_ap_esl_entry_t *entry = ble_esl_ap_find_esl(esl_addr);
if (entry == NULL) {
ble_esl_ap_tracking_unlock();
ESP_LOGE(TAG, "ESL 0x%04x not found for response decryption", esl_addr);
return ESP_ERR_NOT_FOUND;
}
memcpy(resp_key, entry->resp_key.session_key, BLE_ESL_SESSION_KEY_SIZE);
memcpy(resp_iv, entry->resp_key.iv, BLE_ESL_IV_SIZE);
ble_esl_ap_tracking_unlock();
/* Step 2: Decrypt and verify MIC via ble_esl_ead_decrypt
* Input: enc_payload = [Randomizer(5)] [Ciphertext(N)] [MIC(4)]
@@ -424,8 +456,8 @@ esp_err_t ble_esl_ap_pawr_parse_response(uint16_t esl_addr,
*/
uint8_t plaintext[BLE_ESL_PAYLOAD_MAX_SIZE + 2]; /* inner AD max */
size_t decrypted_len = 0;
esp_err_t err = ble_esl_ead_decrypt(entry->resp_key.session_key,
entry->resp_key.iv,
esp_err_t err = ble_esl_ead_decrypt(resp_key,
resp_iv,
enc_payload,
enc_payload_len,
plaintext, sizeof(plaintext),
@@ -566,7 +598,7 @@ static void pawr_handle_subev_data_req(struct ble_gap_event *event)
uint8_t pkt[BLE_ESL_AP_PAWR_MAX_AD_BUF_SIZE];
uint8_t pkt_len = 0;
xSemaphoreTake(s_pawr_mutex, portMAX_DELAY);
pawr_lock();
if (g_esl_ap->pawr_pending != NULL) {
ble_esl_ap_pawr_pending_t *slot = &g_esl_ap->pawr_pending[sub];
if (slot->valid && slot->len > 0) {
@@ -578,7 +610,7 @@ static void pawr_handle_subev_data_req(struct ble_gap_event *event)
}
}
}
xSemaphoreGive(s_pawr_mutex);
pawr_unlock();
/* Allocate an mbuf (sized to the packet, or empty for idle subevents) */
struct os_mbuf *mbuf = os_msys_get_pkthdr(pkt_len, 0);
@@ -171,6 +171,10 @@ typedef struct {
/* ESL tracking cross-reference */
uint16_t esl_addr; /*!< Assigned ESL Address (valid after configure) */
/* Per-connection lifecycle slot (protected by lifecycle mutex) */
uint32_t lf_generation;
uint8_t lf_op; /*!< 0=none, 1=configure, 2=abs-time */
} ble_esl_ap_conn_t;
/* ========================== Per-ESL Tracking Entry ========================== */
@@ -226,7 +230,8 @@ typedef struct {
*/
typedef struct {
bool initialized; /*!< Module has been initialized */
bool started; /*!< Scanning + PAwR broadcasting active */
bool pawr_started; /*!< ble_esl_ap_start_pawr() has succeeded */
bool scan_suppressed; /*!< User/app requested scan stop; do not auto-resume */
/* Application callback */
ble_esl_ap_cb_t app_cb;
@@ -242,6 +247,7 @@ typedef struct {
/* PAwR broadcaster state */
ble_esl_key_material_t ap_sync_key; /*!< AP Sync Key Material */
bool ap_sync_key_valid; /*!< Sync key installed via init/setter */
uint8_t randomizer[BLE_ESL_RANDOMIZER_SIZE]; /*!< Current AP Randomizer (5 octets, LE) */
bool pawr_active; /*!< PAwR broadcaster is running */
ble_esl_ap_pawr_pending_t *pawr_pending; /*!< Per-subevent pending TX buffers (num_subevents entries) */
@@ -253,6 +259,34 @@ typedef struct {
/** Global pointer to the AP module state (allocated by init, freed by deinit) */
extern ble_esl_ap_state_t *g_esl_ap;
/**
* @brief Snapshot of BLE_ESL_AP_EVT_STATE_CHANGED for dispatch after unlock
*/
typedef struct {
bool pending;
ble_esl_ap_state_changed_t evt;
} ble_esl_ap_state_evt_snap_t;
void ble_esl_ap_tracking_lock(void);
void ble_esl_ap_tracking_unlock(void);
bool ble_esl_ap_tracking_held(void);
bool ble_esl_ap_pawr_held(void);
bool ble_esl_ap_lifecycle_held(void);
bool ble_esl_ap_lifecycle_has_inflight(void);
void ble_esl_ap_lifecycle_disconnect_check(ble_esl_ap_conn_t *conn);
void ble_esl_ap_dispatch_state_evt(const ble_esl_ap_state_evt_snap_t *snap);
/**
* @brief Update ESL state; caller must already hold the tracking mutex.
*
* Does not invoke app_cb. Fills @p snap when a state-changed event should be
* delivered after unlock.
*/
esp_err_t ble_esl_ap_update_esl_state_locked(uint16_t esl_addr,
ble_esl_state_t new_state,
ble_esl_ap_state_evt_snap_t *snap);
/* ========================== Helpers ========================== */
/**
@@ -266,17 +300,16 @@ ble_esl_ap_conn_t *ble_esl_ap_find_conn(uint16_t conn_handle);
/**
* @brief Find an ESL tracking entry by ESL Address
*
* @param esl_addr ESL Address (ESL_ID | Group_ID << 8)
* @return Pointer to ESL entry, or NULL if not found
* Caller must already hold the tracking mutex. The returned pointer is only
* valid until the matching unlock. Do not call this as an internally locking
* lookup.
*/
ble_esl_ap_esl_entry_t *ble_esl_ap_find_esl(uint16_t esl_addr);
/**
* @brief Find an ESL tracking entry by BLE address
*
* @param addr 6-byte BLE address
* @param addr_type Address type
* @return Pointer to ESL entry, or NULL if not found
* Caller must already hold the tracking mutex. See ble_esl_ap_find_esl().
*/
ble_esl_ap_esl_entry_t *ble_esl_ap_find_esl_by_ble_addr(const uint8_t *addr,
uint8_t addr_type);
@@ -298,7 +331,7 @@ void ble_esl_ap_free_conn(ble_esl_ap_conn_t *conn);
/**
* @brief Allocate a free ESL tracking entry
*
* @return Pointer to free entry, or NULL if table is full
* Caller must already hold the tracking mutex.
*/
ble_esl_ap_esl_entry_t *ble_esl_ap_alloc_esl(void);
@@ -26,7 +26,18 @@ extern "C" {
/* ========================== Constants ========================== */
/** @brief Sync/Unsync state timeout: 60 minutes in microseconds (for esp_timer) */
/**
* @brief Sync / Unsynchronized state timeout (microseconds for esp_timer).
*
* Default: 60 minutes. This matches the Bluetooth SIG ESL Service Specification
* mandatory timeout used for:
* - SYNCHRONIZED → UNSYNCHRONIZED (no valid PAwR payload / sync lost fallback)
* - UNSYNCHRONIZED → UNASSOCIATED (clear config and bond)
*
* This is a protocol-compliance parameter, not a product tuning knob. Do not
* expose a production Kconfig override. Test-only profiles may shorten it, but
* those builds must not be used for certification or mass production.
*/
#define ESL_STATE_TIMEOUT_US (60ULL * 60ULL * 1000000ULL)
/** @brief ECP Procedure Timeout in microseconds */
@@ -30,12 +30,12 @@ extern "C" {
/* ========================== Configuration Bitmask ========================== */
#define CONFIG_BIT_ADDRESS (1 << 0)
#define CONFIG_BIT_AP_SYNC_KEY (1 << 1)
#define CONFIG_BIT_RESP_KEY (1 << 2)
#define CONFIG_BIT_ABS_TIME (1 << 3)
#define CONFIG_COMPLETE_MASK (CONFIG_BIT_ADDRESS | CONFIG_BIT_AP_SYNC_KEY | \
CONFIG_BIT_RESP_KEY | CONFIG_BIT_ABS_TIME)
#define ESL_CONFIG_BIT_ADDRESS (1 << 0)
#define ESL_CONFIG_BIT_AP_SYNC_KEY (1 << 1)
#define ESL_CONFIG_BIT_RESP_KEY (1 << 2)
#define ESL_CONFIG_BIT_ABS_TIME (1 << 3)
#define ESL_CONFIG_COMPLETE_MASK (ESL_CONFIG_BIT_ADDRESS | ESL_CONFIG_BIT_AP_SYNC_KEY | \
ESL_CONFIG_BIT_RESP_KEY | ESL_CONFIG_BIT_ABS_TIME)
/* ========================== Internal State Context ========================== */
typedef struct {
@@ -60,10 +60,16 @@ typedef struct {
bool ap_sync_key_valid; /* AP sync key written */
bool resp_key_valid; /* Response key written */
bool address_valid; /* ESL address written */
uint16_t pawr_sync_handle; /* Active PAwR periodic sync handle; BLE_HS_CONN_HANDLE_NONE if none */
/* Current vs retiring PAwR sync tracking (AP reboot / PAST switch).
* Only current_sync_handle drives SYNCHRONIZED/UPDATING sync bookkeeping.
* retiring_* tracks a locally terminated old sync so its SYNC_LOST does not
* look like a natural loss of the new train. */
uint16_t current_sync_handle; /* Valid periodic sync; BLE_HS_CONN_HANDLE_NONE if none */
uint16_t retiring_sync_handle; /* Locally terminated old sync awaiting SYNC_LOST */
bool retiring_local_terminate; /* retiring SYNC_LOST is expected; do not change ESL state */
bool past_received; /* PAST completed in Updating state */
bool pawr_synced; /* Synchronized to the AP's PAwR train (retained across Updating) */
bool past_pending; /* PAST re-arm deferred until SYNC_LOST frees pool slot */
bool past_pending; /* Re-arm PAST only after retiring SYNC_LOST frees pool slot */
bool update_complete_received; /* Update Complete cmd received in Updating state */
bool deinit_pending; /* ble_esl_deinit() is waiting for disconnect */
SemaphoreHandle_t deinit_sem; /* Signaled when disconnect completes during deinit */
@@ -100,17 +100,10 @@ static void build_display_state_response(ble_esl_cmd_result_t *result,
}
/**
* @brief Check if an image slot contains valid data
* @brief Check if an image slot contains valid data.
*
* An image is considered available if:
* - The image_writable_mask is NULL: all images are read-only (static/pre-loaded)
* and therefore always available.
* - The image_writable_mask entry is false: the image is read-only (protected)
* and therefore always available.
* - The image_writable_mask entry is true: the image is writable and assumed
* to have been written by the AP.
*
* In other words, any valid image index within range is considered available.
* Writable OTS-backed images require a successful write-complete. Read-only
* preloaded images (writable_mask false / NULL) remain always available.
*/
static bool is_image_available(uint8_t image_index)
{
@@ -121,10 +114,16 @@ static bool is_image_available(uint8_t image_index)
if (image_index >= config->num_images) {
return false;
}
/* All valid image indices are considered available:
* - Read-only images (no writable mask, or mask[i] == false) are
* static/pre-loaded and always available.
* - Writable images (mask[i] == true) are assumed written by the AP. */
#if CONFIG_BLE_ESL_OTS_SUPPORT
bool writable = true;
if (config->image_writable_mask != NULL) {
writable = config->image_writable_mask[image_index];
}
if (writable) {
return ble_esl_image_is_complete(image_index);
}
#endif
return true;
}
@@ -161,6 +161,7 @@ static esp_err_t handle_ping(const uint8_t *params, uint8_t params_len,
static esp_err_t handle_unassociate(const uint8_t *params, uint8_t params_len,
ble_esl_cmd_result_t *result)
{
ESP_LOGI(TAG, "Unassociate ECP command received");
uint16_t bitmap = 0;
esp_err_t ret = esl_build_basic_state(&bitmap);
if (ret != ESP_OK) {
@@ -93,6 +93,9 @@ typedef struct {
ble_ots_obj_id_t ots_obj_ids[CONFIG_BLE_ESL_MAX_IMAGES]; /* reverse map */
uint8_t ots_obj_count;
bool ots_initialized; /* OTS server init state */
bool image_complete[CONFIG_BLE_ESL_MAX_IMAGES];
uint32_t image_length[CONFIG_BLE_ESL_MAX_IMAGES];
SemaphoreHandle_t image_lock;
#endif
} esl_gatts_ctx_t;
@@ -238,7 +241,7 @@ static int handle_write_esl_address(struct os_mbuf *om)
s_esl_gatts->ctx->esl_address = addr;
s_esl_gatts->ctx->address_valid = true;
s_esl_gatts->ctx->config_complete |= CONFIG_BIT_ADDRESS;
s_esl_gatts->ctx->config_complete |= ESL_CONFIG_BIT_ADDRESS;
ESP_LOGI(TAG, "ESL Address set: id=0x%02x group=0x%02x",
addr.esl_id, BLE_ESL_ADDR_GROUP_ID(addr));
return 0;
@@ -258,7 +261,7 @@ static int handle_write_ap_sync_key(struct os_mbuf *om)
}
s_esl_gatts->ctx->ap_sync_key_valid = true;
s_esl_gatts->ctx->config_complete |= CONFIG_BIT_AP_SYNC_KEY;
s_esl_gatts->ctx->config_complete |= ESL_CONFIG_BIT_AP_SYNC_KEY;
ESP_LOGI(TAG, "AP Sync Key Material written");
return 0;
}
@@ -277,7 +280,7 @@ static int handle_write_resp_key(struct os_mbuf *om)
}
s_esl_gatts->ctx->resp_key_valid = true;
s_esl_gatts->ctx->config_complete |= CONFIG_BIT_RESP_KEY;
s_esl_gatts->ctx->config_complete |= ESL_CONFIG_BIT_RESP_KEY;
ESP_LOGI(TAG, "ESL Response Key Material written");
return 0;
}
@@ -297,7 +300,7 @@ static int handle_write_abs_time(struct os_mbuf *om)
s_esl_gatts->ctx->abs_time_base = time_val;
s_esl_gatts->ctx->abs_time_offset_us = esp_timer_get_time();
s_esl_gatts->ctx->config_complete |= CONFIG_BIT_ABS_TIME;
s_esl_gatts->ctx->config_complete |= ESL_CONFIG_BIT_ABS_TIME;
ESP_LOGI(TAG, "ESL Absolute Time set: %" PRIu32 " ms", time_val);
return 0;
}
@@ -502,7 +505,179 @@ static int esl_gatt_access_cb(uint16_t conn_handle, uint16_t attr_handle,
#if CONFIG_BLE_ESL_OTS_SUPPORT
/* OTS object-ID reverse map, count and init state live in esl_gatts_ctx_t. */
static void esl_image_state_reset(void)
{
if (s_esl_gatts == NULL) {
return;
}
memset(s_esl_gatts->image_complete, 0, sizeof(s_esl_gatts->image_complete));
memset(s_esl_gatts->image_length, 0, sizeof(s_esl_gatts->image_length));
}
static bool esl_map_ots_obj_to_image_index(ble_ots_obj_id_t obj_id, uint8_t *out_image_index)
{
if (out_image_index == NULL || s_esl_gatts == NULL) {
return false;
}
for (uint8_t i = 0; i < s_esl_gatts->ots_obj_count; i++) {
if (s_esl_gatts->ots_obj_ids[i] == obj_id) {
*out_image_index = i;
return true;
}
}
/* Fallback for base+index layout used by AP transfer helpers */
if (obj_id >= BLE_ESL_OTS_OBJECT_ID_BASE) {
uint64_t diff = obj_id - BLE_ESL_OTS_OBJECT_ID_BASE;
if (s_esl_gatts->ctx != NULL && diff < s_esl_gatts->ctx->config.num_images) {
*out_image_index = (uint8_t)diff;
return true;
}
}
ESP_LOGW(TAG, "OTS write: unknown obj_id=0x%06llx", (unsigned long long)obj_id);
return false;
}
static void esl_mark_image_incomplete(uint8_t image_index)
{
if (s_esl_gatts == NULL || s_esl_gatts->image_lock == NULL ||
image_index >= CONFIG_BLE_ESL_MAX_IMAGES) {
return;
}
xSemaphoreTake(s_esl_gatts->image_lock, portMAX_DELAY);
s_esl_gatts->image_complete[image_index] = false;
xSemaphoreGive(s_esl_gatts->image_lock);
}
static void esl_mark_image_complete(uint8_t image_index, uint32_t length, bool success)
{
if (s_esl_gatts == NULL || s_esl_gatts->image_lock == NULL ||
image_index >= CONFIG_BLE_ESL_MAX_IMAGES) {
return;
}
xSemaphoreTake(s_esl_gatts->image_lock, portMAX_DELAY);
if (success && length > 0) {
s_esl_gatts->image_complete[image_index] = true;
s_esl_gatts->image_length[image_index] = length;
} else {
s_esl_gatts->image_complete[image_index] = false;
s_esl_gatts->image_length[image_index] = 0;
}
xSemaphoreGive(s_esl_gatts->image_lock);
}
bool ble_esl_image_is_complete(uint8_t image_index)
{
if (s_esl_gatts == NULL || s_esl_gatts->image_lock == NULL ||
image_index >= CONFIG_BLE_ESL_MAX_IMAGES) {
return false;
}
if (s_esl_gatts->ctx == NULL || image_index >= s_esl_gatts->ctx->config.num_images) {
return false;
}
xSemaphoreTake(s_esl_gatts->image_lock, portMAX_DELAY);
bool complete = s_esl_gatts->image_complete[image_index];
xSemaphoreGive(s_esl_gatts->image_lock);
return complete;
}
esp_err_t ble_esl_image_snapshot(uint8_t image_index, uint8_t *dst, size_t capacity,
size_t *out_len)
{
if (dst == NULL || capacity == 0) {
return ESP_ERR_INVALID_ARG;
}
if (s_esl_gatts == NULL || s_esl_gatts->image_lock == NULL ||
!s_esl_gatts->ots_initialized) {
return ESP_ERR_INVALID_STATE;
}
if (image_index >= s_esl_gatts->ots_obj_count ||
image_index >= CONFIG_BLE_ESL_MAX_IMAGES) {
return ESP_ERR_INVALID_ARG;
}
/* Never call OTS APIs while holding image_lock: the write callback
* takes image_lock with the OTS mutex already held (ABBA). */
xSemaphoreTake(s_esl_gatts->image_lock, portMAX_DELAY);
if (!s_esl_gatts->image_complete[image_index]) {
xSemaphoreGive(s_esl_gatts->image_lock);
return ESP_ERR_INVALID_STATE;
}
uint32_t length = s_esl_gatts->image_length[image_index];
if (length == 0 || length > capacity) {
xSemaphoreGive(s_esl_gatts->image_lock);
return ESP_ERR_INVALID_SIZE;
}
ble_ots_obj_id_t obj_id = s_esl_gatts->ots_obj_ids[image_index];
xSemaphoreGive(s_esl_gatts->image_lock);
int rc = ble_ots_server_copy_object_data(obj_id, 0, length, dst);
xSemaphoreTake(s_esl_gatts->image_lock, portMAX_DELAY);
bool still_valid = s_esl_gatts->image_complete[image_index] &&
(s_esl_gatts->image_length[image_index] == length);
xSemaphoreGive(s_esl_gatts->image_lock);
if (rc != 0 || !still_valid) {
return ESP_ERR_INVALID_STATE;
}
if (out_len != NULL) {
*out_len = length;
}
return ESP_OK;
}
esp_err_t ble_esl_image_restore(uint8_t image_index, const uint8_t *data, size_t len)
{
if (data == NULL || len == 0 || len > CONFIG_BLE_ESL_MAX_IMAGE_SIZE) {
return ESP_ERR_INVALID_ARG;
}
if (s_esl_gatts == NULL || s_esl_gatts->image_lock == NULL ||
!s_esl_gatts->ots_initialized || s_esl_gatts->ctx == NULL) {
return ESP_ERR_INVALID_STATE;
}
if (image_index >= s_esl_gatts->ots_obj_count ||
image_index >= s_esl_gatts->ctx->config.num_images ||
image_index >= CONFIG_BLE_ESL_MAX_IMAGES) {
return ESP_ERR_INVALID_ARG;
}
xSemaphoreTake(s_esl_gatts->image_lock, portMAX_DELAY);
s_esl_gatts->image_complete[image_index] = false;
s_esl_gatts->image_length[image_index] = 0;
ble_ots_obj_id_t obj_id = s_esl_gatts->ots_obj_ids[image_index];
xSemaphoreGive(s_esl_gatts->image_lock);
int rc = ble_ots_server_set_object_data(obj_id, data, 0, (uint32_t)len);
if (rc != 0) {
ESP_LOGE(TAG, "image_restore: set_object_data failed rc=%d index=%u",
rc, image_index);
return ESP_FAIL;
}
xSemaphoreTake(s_esl_gatts->image_lock, portMAX_DELAY);
s_esl_gatts->image_complete[image_index] = true;
s_esl_gatts->image_length[image_index] = (uint32_t)len;
xSemaphoreGive(s_esl_gatts->image_lock);
return ESP_OK;
}
void ble_esl_image_invalidate_all(void)
{
if (s_esl_gatts == NULL) {
return;
}
if (s_esl_gatts->image_lock == NULL) {
esl_image_state_reset();
return;
}
xSemaphoreTake(s_esl_gatts->image_lock, portMAX_DELAY);
esl_image_state_reset();
xSemaphoreGive(s_esl_gatts->image_lock);
}
/**
* @brief OTS event callback — handles write-complete events,
@@ -511,41 +686,49 @@ static int esl_gatt_access_cb(uint16_t conn_handle, uint16_t attr_handle,
static void esl_ots_write_cb(ble_ots_server_event_t event,
ble_ots_server_cb_param_t *param)
{
if (event != BLE_OTS_SERVER_EVT_WRITE_COMPLETE || param == NULL) {
if (param == NULL) {
return;
}
ble_ots_obj_id_t obj_id = param->write_complete.object_id;
uint32_t offset = param->write_complete.offset;
uint32_t length = param->write_complete.bytes_received;
if (s_esl_gatts == NULL || s_esl_gatts->ctx == NULL) {
return;
}
/* Map obj_id back to image_index through the reverse map built by
* esl_setup_ots(). The OTS server assigns IDs from its own monotonic
* counter, so they are not guaranteed to start at
* BLE_ESL_OTS_OBJECT_ID_BASE nor to be contiguous. */
uint8_t image_index = 0;
bool found = false;
for (uint8_t i = 0; i < s_esl_gatts->ots_obj_count; i++) {
if (s_esl_gatts->ots_obj_ids[i] == obj_id) {
image_index = i;
found = true;
break;
if (event == BLE_OTS_SERVER_EVT_DATA_WRITE) {
uint8_t image_index;
if (!esl_map_ots_obj_to_image_index(param->data_write.object_id, &image_index)) {
return;
}
}
if (!found) {
ESP_LOGW(TAG, "OTS write: unknown obj_id=0x%06llx", (unsigned long long)obj_id);
esl_mark_image_incomplete(image_index);
ble_esl_cb_param_t cb_param = {
.image_write = {
.image_index = image_index,
.data = param->data_write.data,
.length = param->data_write.data_len,
.offset = param->data_write.offset,
}
};
esl_notify_app(BLE_ESL_EVT_IMAGE_WRITE, &cb_param);
return;
}
if (event != BLE_OTS_SERVER_EVT_WRITE_COMPLETE) {
return;
}
uint8_t image_index;
if (!esl_map_ots_obj_to_image_index(param->write_complete.object_id, &image_index)) {
return;
}
bool success = (param->write_complete.status == BLE_OTS_TRANSFER_SUCCESS) &&
(param->write_complete.bytes_received > 0);
esl_mark_image_complete(image_index, param->write_complete.bytes_received, success);
ble_esl_cb_param_t cb_param = {
.image_write = {
.image_index = image_index,
.data = NULL,
.length = length,
.offset = offset,
.length = success ? param->write_complete.bytes_received : 0,
.offset = param->write_complete.offset,
}
};
esl_notify_app(BLE_ESL_EVT_IMAGE_WRITE, &cb_param);
@@ -557,6 +740,13 @@ static esp_err_t esl_setup_ots(const ble_esl_config_t *config)
return ESP_OK;
}
if (s_esl_gatts->image_lock == NULL) {
s_esl_gatts->image_lock = xSemaphoreCreateMutex();
if (s_esl_gatts->image_lock == NULL) {
return ESP_ERR_NO_MEM;
}
}
esl_image_state_reset();
s_esl_gatts->ots_obj_count = 0;
ble_ots_server_config_t ots_config = {
@@ -633,6 +823,12 @@ static void esl_teardown_ots(void)
ble_ots_server_deinit();
s_esl_gatts->ots_initialized = false;
}
esl_image_state_reset();
s_esl_gatts->ots_obj_count = 0;
if (s_esl_gatts->image_lock != NULL) {
vSemaphoreDelete(s_esl_gatts->image_lock);
s_esl_gatts->image_lock = NULL;
}
}
#endif /* CONFIG_BLE_ESL_OTS_SUPPORT */
@@ -0,0 +1,125 @@
/*
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
/**
* @file esl_persisted_tag_map.h
* @brief Helpers for TAG persisted snapshot field mapping.
*/
#pragma once
#include <stdint.h>
#include <stdbool.h>
#include <string.h>
#include "ble_esl_common.h"
#include "ble_esl_state_int.h"
#ifdef __cplusplus
extern "C" {
#endif
/* Persist does not store Absolute Time; restore must leave that bit clear. */
#define ESL_PERSISTED_CONFIG_MASK \
(ESL_CONFIG_BIT_ADDRESS | ESL_CONFIG_BIT_AP_SYNC_KEY | ESL_CONFIG_BIT_RESP_KEY)
typedef struct {
ble_esl_address_t esl_address;
ble_esl_key_material_t ap_sync_key;
ble_esl_key_material_t resp_key;
uint8_t peer_addr_type;
uint8_t peer_addr[6];
} esl_persisted_tag_dto_t;
typedef struct {
bool address_valid;
bool ap_sync_key_valid;
bool resp_key_valid;
bool has_bonded_peer;
uint8_t config_complete;
uint8_t peer_addr_type;
uint8_t peer_addr[6];
ble_esl_address_t esl_address;
ble_esl_key_material_t ap_sync_key;
ble_esl_key_material_t resp_key;
} esl_persisted_tag_ram_t;
static inline bool esl_persisted_peer_nonzero(const uint8_t addr[6])
{
for (int i = 0; i < 6; i++) {
if (addr[i] != 0) {
return true;
}
}
return false;
}
static inline bool esl_persisted_export_ok(const esl_persisted_tag_ram_t *ram)
{
if (ram == NULL) {
return false;
}
if (!ram->address_valid || !ram->ap_sync_key_valid || !ram->resp_key_valid ||
!ram->has_bonded_peer) {
return false;
}
if ((ram->config_complete & ESL_PERSISTED_CONFIG_MASK) != ESL_PERSISTED_CONFIG_MASK) {
return false;
}
if (ram->esl_address.esl_id == BLE_ESL_BROADCAST_ADDRESS) {
return false;
}
if (!esl_persisted_peer_nonzero(ram->peer_addr)) {
return false;
}
return true;
}
static inline bool esl_persisted_fill_dto(const esl_persisted_tag_ram_t *ram,
esl_persisted_tag_dto_t *out)
{
if (!esl_persisted_export_ok(ram) || out == NULL) {
return false;
}
out->esl_address = ram->esl_address;
out->ap_sync_key = ram->ap_sync_key;
out->resp_key = ram->resp_key;
out->peer_addr_type = ram->peer_addr_type;
memcpy(out->peer_addr, ram->peer_addr, 6);
return true;
}
/**
* Apply DTO into RAM fields for restore. Does not touch Absolute Time / sync handles.
* Sets config_complete to ESL_PERSISTED_CONFIG_MASK only (ABS_TIME cleared).
*/
static inline bool esl_persisted_apply_dto(const esl_persisted_tag_dto_t *in,
esl_persisted_tag_ram_t *ram)
{
if (in == NULL || ram == NULL) {
return false;
}
if (in->esl_address.esl_id == BLE_ESL_BROADCAST_ADDRESS) {
return false;
}
if (!esl_persisted_peer_nonzero(in->peer_addr)) {
return false;
}
ram->esl_address = in->esl_address;
ram->ap_sync_key = in->ap_sync_key;
ram->resp_key = in->resp_key;
ram->peer_addr_type = in->peer_addr_type;
memcpy(ram->peer_addr, in->peer_addr, 6);
ram->address_valid = true;
ram->ap_sync_key_valid = true;
ram->resp_key_valid = true;
ram->has_bonded_peer = true;
ram->config_complete = (uint8_t)ESL_PERSISTED_CONFIG_MASK;
return true;
}
#ifdef __cplusplus
}
#endif
@@ -27,6 +27,8 @@
#include "ble_esl.h"
#include "ble_esl_int.h"
#include "ble_esl_state_int.h"
#include "esl_sync_lost.h"
#include "esl_persisted_tag_map.h"
static const char *TAG = "esl_state";
@@ -43,6 +45,131 @@ static void ecp_timeout_cb(void *arg);
static esp_err_t esl_start_advertising(void);
static esp_err_t esl_stop_advertising(void);
static bool is_transition_valid(ble_esl_state_t from, ble_esl_state_t to);
static void enable_past_reception(uint16_t conn_handle);
static void install_current_sync(uint16_t sync_handle);
static esp_err_t retire_current_sync(bool rearm_past);
static void clear_retiring_sync(void);
static void clear_current_sync_bookkeeping(void);
static void clear_retiring_sync(void)
{
if (s_ctx == NULL) {
return;
}
s_ctx->retiring_sync_handle = BLE_HS_CONN_HANDLE_NONE;
s_ctx->retiring_local_terminate = false;
}
static void clear_current_sync_bookkeeping(void)
{
if (s_ctx == NULL) {
return;
}
s_ctx->current_sync_handle = BLE_HS_CONN_HANDLE_NONE;
}
static void install_current_sync(uint16_t sync_handle)
{
if (s_ctx == NULL || sync_handle == BLE_HS_CONN_HANDLE_NONE) {
return;
}
s_ctx->current_sync_handle = sync_handle;
}
/**
* @brief Move current sync into retiring and terminate it locally.
*
* @param rearm_past If true, set past_pending so SYNC_LOST re-arms PAST after
* the controller frees the reception pool slot.
* @return ESP_OK on success, ESP_ERR_INVALID_STATE if another retire is active,
* ESP_FAIL if terminate failed (fields rolled back).
*/
static esp_err_t retire_current_sync(bool rearm_past)
{
if (s_ctx == NULL) {
return ESP_ERR_INVALID_STATE;
}
if (s_ctx->current_sync_handle == BLE_HS_CONN_HANDLE_NONE) {
if (rearm_past && s_ctx->conn_handle != BLE_HS_CONN_HANDLE_NONE) {
enable_past_reception(s_ctx->conn_handle);
}
return ESP_OK;
}
if (s_ctx->retiring_sync_handle != BLE_HS_CONN_HANDLE_NONE) {
ESP_LOGW(TAG, "retire_current_sync: retiring already active (handle=%u)",
s_ctx->retiring_sync_handle);
return ESP_ERR_INVALID_STATE;
}
uint16_t old_handle = s_ctx->current_sync_handle;
s_ctx->retiring_sync_handle = old_handle;
s_ctx->retiring_local_terminate = true;
s_ctx->current_sync_handle = BLE_HS_CONN_HANDLE_NONE;
s_ctx->past_pending = rearm_past;
int rc = ble_gap_periodic_adv_sync_terminate(old_handle);
if (rc != 0 && rc != BLE_HS_EALREADY) {
ESP_LOGE(TAG, "retire_current_sync: terminate(%u) failed rc=%d — rolling back",
old_handle, rc);
s_ctx->current_sync_handle = old_handle;
s_ctx->past_pending = false;
clear_retiring_sync();
return ESP_FAIL;
}
ESP_LOGI(TAG, "Retired sync handle=%u rearm_past=%d",
old_handle, rearm_past);
return ESP_OK;
}
/**
* @brief Install a newly received PAST sync, retiring any previous current sync.
*/
static void adopt_past_sync(uint16_t sync_handle)
{
if (s_ctx == NULL || sync_handle == BLE_HS_CONN_HANDLE_NONE) {
return;
}
if (s_ctx->current_sync_handle != BLE_HS_CONN_HANDLE_NONE &&
s_ctx->current_sync_handle != sync_handle) {
/* Move old current to retiring, then install the new handle immediately
* so a late SYNC_LOST for the old handle cannot affect the new current. */
if (s_ctx->retiring_sync_handle != BLE_HS_CONN_HANDLE_NONE) {
ESP_LOGW(TAG, "adopt_past_sync: dropping stale retiring handle=%u",
s_ctx->retiring_sync_handle);
clear_retiring_sync();
}
uint16_t old_handle = s_ctx->current_sync_handle;
s_ctx->retiring_sync_handle = old_handle;
s_ctx->retiring_local_terminate = true;
s_ctx->current_sync_handle = BLE_HS_CONN_HANDLE_NONE;
s_ctx->past_pending = false;
install_current_sync(sync_handle);
int rc = ble_gap_periodic_adv_sync_terminate(old_handle);
if (rc != 0 && rc != BLE_HS_EALREADY) {
ESP_LOGW(TAG, "adopt_past_sync: terminate old handle=%u rc=%d",
old_handle, rc);
/* New current is already installed; clear retiring bookkeeping so a
* missing SYNC_LOST cannot block future retires indefinitely. */
clear_retiring_sync();
}
return;
}
if (s_ctx->current_sync_handle == sync_handle) {
/* Same handle reported again. */
return;
}
install_current_sync(sync_handle);
}
/* ========================== State Machine ========================== */
@@ -316,7 +443,7 @@ void esl_notify_update_complete(void)
case BLE_ESL_STATE_CONFIGURING:
/* First provisioning also requires all mandatory configuration writes. */
if (s_ctx->past_received &&
(s_ctx->config_complete & CONFIG_COMPLETE_MASK) == CONFIG_COMPLETE_MASK) {
(s_ctx->config_complete & ESL_CONFIG_COMPLETE_MASK) == ESL_CONFIG_COMPLETE_MASK) {
esl_state_transition(BLE_ESL_STATE_SYNCHRONIZED);
} else {
ESP_LOGI(TAG, "Update Complete received in Configuring — waiting for PAST / config");
@@ -347,22 +474,22 @@ void esl_notify_update_complete(void)
*/
static esp_err_t esl_terminate_pawr_sync(void)
{
if (s_ctx == NULL || s_ctx->pawr_sync_handle == BLE_HS_CONN_HANDLE_NONE) {
if (s_ctx == NULL || s_ctx->current_sync_handle == BLE_HS_CONN_HANDLE_NONE) {
return ESP_OK;
}
int rc = ble_gap_periodic_adv_sync_terminate(s_ctx->pawr_sync_handle);
int rc = ble_gap_periodic_adv_sync_terminate(s_ctx->current_sync_handle);
if (rc != 0 && rc != BLE_HS_ENOTCONN) {
/* Anything other than ENOTCONN (host no longer knows this sync, e.g. it
* was lost concurrently) means the host still holds the periodic sync.
* Keep the handle so the caller can retry, and keep pawr_synced so the
* Basic State Synchronized bit stays truthful. */
ESP_LOGW(TAG, "Failed to terminate PAwR sync %u: rc=%d",
s_ctx->pawr_sync_handle, rc);
s_ctx->current_sync_handle, rc);
return ESP_FAIL;
}
s_ctx->pawr_sync_handle = BLE_HS_CONN_HANDLE_NONE;
s_ctx->current_sync_handle = BLE_HS_CONN_HANDLE_NONE;
s_ctx->pawr_synced = false;
return ESP_OK;
}
@@ -609,7 +736,7 @@ static void handle_gap_disconnect(struct ble_gap_event *event)
switch (s_ctx->state) {
case BLE_ESL_STATE_CONFIGURING:
if ((s_ctx->config_complete & CONFIG_COMPLETE_MASK) == CONFIG_COMPLETE_MASK) {
if ((s_ctx->config_complete & ESL_CONFIG_COMPLETE_MASK) == ESL_CONFIG_COMPLETE_MASK) {
/* Configuration complete — go to Unsynchronized */
esl_state_transition(BLE_ESL_STATE_UNSYNCHRONIZED);
} else {
@@ -643,8 +770,19 @@ static void enable_past_reception(uint16_t conn_handle)
int rc = ble_gap_periodic_adv_sync_receive(conn_handle, &sync_params,
esl_gap_event_handler, NULL);
if (rc == BLE_HS_ENOMEM &&
s_ctx != NULL &&
s_ctx->current_sync_handle != BLE_HS_CONN_HANDLE_NONE) {
/* Controller PAST reception pool is full while an old sync is still
* held. Retire it and re-arm PAST only after the deferred SYNC_LOST. */
ESP_LOGW(TAG, "PAST receive ENOMEM — retiring current sync before re-arm");
if (retire_current_sync(true) == ESP_OK) {
return;
}
}
if (rc != 0) {
ESP_LOGE(TAG, "ble_gap_periodic_adv_sync_receive failed: rc=%d", rc);
return;
}
ESP_LOGI(TAG, "Enable PAST reception: conn_handle=%u", conn_handle);
@@ -802,21 +940,11 @@ static void handle_gap_periodic_transfer(struct ble_gap_event *event)
uint16_t sync_handle = event->periodic_transfer.sync_handle;
ESP_LOGI(TAG, "PAST received: sync_handle=%d, state=%d", sync_handle, s_ctx->state);
/* Terminate the previous PAwR sync if one was still alive (kept during
* SYNCHRONIZED → UPDATING because ble_gap_periodic_adv_sync_terminate
* defers pool-entry freeing and would cause enable_past_reception to fail
* with ENOMEM if freed synchronously). */
if (s_ctx->pawr_sync_handle != BLE_HS_CONN_HANDLE_NONE &&
s_ctx->pawr_sync_handle != sync_handle) {
int term_rc = ble_gap_periodic_adv_sync_terminate(s_ctx->pawr_sync_handle);
if (term_rc != 0 && term_rc != BLE_HS_ENOTCONN) {
/* Not fatal: the new sync from PAST supersedes the old one, which is
* dropped when the host tears down its stale entry. */
ESP_LOGW(TAG, "Failed to terminate stale PAwR sync %u: rc=%d",
s_ctx->pawr_sync_handle, term_rc);
}
}
s_ctx->pawr_sync_handle = sync_handle;
/* Adopt the new train through the unique retire helper so a late SYNC_LOST
* for the previous handle cannot be mistaken for natural loss of the new
* current sync (or re-arm PAST incorrectly). */
s_ctx->past_pending = false;
adopt_past_sync(sync_handle);
/* Enable PAwR subevent reception and response slots for our group/subevent. */
uint8_t group_id = BLE_ESL_ADDR_GROUP_ID(s_ctx->esl_address);
@@ -837,7 +965,7 @@ static void handle_gap_periodic_transfer(struct ble_gap_event *event)
* advance the state machine before provisioning finishes. */
s_ctx->past_received = true;
if (s_ctx->update_complete_received &&
(s_ctx->config_complete & CONFIG_COMPLETE_MASK) == CONFIG_COMPLETE_MASK) {
(s_ctx->config_complete & ESL_CONFIG_COMPLETE_MASK) == ESL_CONFIG_COMPLETE_MASK) {
esl_state_transition(BLE_ESL_STATE_SYNCHRONIZED);
} else {
ESP_LOGI(TAG, "PAST received in Configuring — waiting for Update Complete / config");
@@ -974,18 +1102,59 @@ static int esl_gap_event_handler(struct ble_gap_event *event, void *arg)
handle_gap_periodic_report(event);
return 0;
case BLE_GAP_EVENT_PERIODIC_SYNC_LOST:
/* Fired asynchronously after ble_gap_periodic_adv_sync_terminate frees
* the periodic sync pool slot. If PAST was deferred, arm it now. */
if (s_ctx->past_pending &&
s_ctx->state == BLE_ESL_STATE_UPDATING &&
s_ctx->conn_handle != BLE_HS_CONN_HANDLE_NONE) {
s_ctx->past_pending = false;
ESP_LOGI(TAG, "SYNC_LOST: re-arming PAST reception (conn=%d)",
s_ctx->conn_handle);
enable_past_reception(s_ctx->conn_handle);
case BLE_GAP_EVENT_PERIODIC_SYNC_LOST: {
uint16_t lost_handle = event->periodic_sync_lost.sync_handle;
esl_sync_lost_ctx_t lost_ctx = {
.current_sync_handle = s_ctx->current_sync_handle,
.retiring_sync_handle = s_ctx->retiring_sync_handle,
.retiring_local_terminate = s_ctx->retiring_local_terminate,
};
esl_sync_lost_class_t kind = esl_classify_sync_lost(&lost_ctx, lost_handle);
ESP_LOGI(TAG, "SYNC_LOST: handle=%u class=%d state=%d",
lost_handle, (int)kind, s_ctx->state);
switch (kind) {
case ESL_SYNC_LOST_RETIRING_LOCAL: {
bool rearm = s_ctx->past_pending;
clear_retiring_sync();
if (rearm &&
s_ctx->state == BLE_ESL_STATE_UPDATING &&
s_ctx->conn_handle != BLE_HS_CONN_HANDLE_NONE) {
s_ctx->past_pending = false;
ESP_LOGI(TAG, "SYNC_LOST(retiring): re-arming PAST (conn=%d)",
s_ctx->conn_handle);
enable_past_reception(s_ctx->conn_handle);
} else {
s_ctx->past_pending = false;
}
break;
}
case ESL_SYNC_LOST_CURRENT_NATURAL:
clear_current_sync_bookkeeping();
if (s_ctx->state == BLE_ESL_STATE_SYNCHRONIZED) {
/* Natural loss of the active train: become discoverable for
* AP reboot recovery (ACL + PAST). */
esl_state_transition(BLE_ESL_STATE_UNSYNCHRONIZED);
} else if (s_ctx->state == BLE_ESL_STATE_UPDATING) {
/* Keep UPDATING while ACL is alive; disconnect path will move
* to UNSYNCHRONIZED if PAST never completes. */
ESP_LOGI(TAG, "SYNC_LOST(current) in UPDATING — cleared handle only");
} else {
ESP_LOGI(TAG, "SYNC_LOST(current) in state=%d — bookkeeping only",
s_ctx->state);
}
break;
case ESL_SYNC_LOST_STALE:
default:
/* Includes CONFIGURING-era late events and reused handle numbers.
* Never release current/retiring, never change state, never re-arm. */
ESP_LOGD(TAG, "SYNC_LOST stale handle=%u — ignored", lost_handle);
break;
}
return 0;
}
case BLE_GAP_EVENT_NOTIFY_TX:
esl_handle_ecp_notify_tx(event->notify_tx.conn_handle,
@@ -1139,7 +1308,9 @@ esp_err_t ble_esl_init(const ble_esl_config_t *config)
memcpy(&s_ctx->config, config, sizeof(ble_esl_config_t));
s_ctx->state = BLE_ESL_STATE_UNASSOCIATED;
s_ctx->conn_handle = BLE_HS_CONN_HANDLE_NONE;
s_ctx->pawr_sync_handle = BLE_HS_CONN_HANDLE_NONE;
s_ctx->current_sync_handle = BLE_HS_CONN_HANDLE_NONE;
s_ctx->retiring_sync_handle = BLE_HS_CONN_HANDLE_NONE;
s_ctx->pawr_synced = false;
s_ctx->initialized = true;
/* Configure security as mandated by the ESL Profile: LE Secure Connections
@@ -1323,7 +1494,7 @@ esp_err_t ble_esl_deinit(void)
* as the retry for a teardown that ble_esl_stop() could not complete. */
if (esl_terminate_pawr_sync() != ESP_OK) {
ESP_LOGW(TAG, "PAwR sync %u still active while deinitializing",
s_ctx->pawr_sync_handle);
s_ctx->current_sync_handle);
}
/* Deinit sub-modules */
@@ -1420,7 +1591,7 @@ esp_err_t ble_esl_stop(void)
if (sync_err != ESP_OK) {
ESP_LOGW(TAG, "ESL stopped, but PAwR sync %u could not be terminated",
s_ctx->pawr_sync_handle);
s_ctx->current_sync_handle);
return sync_err;
}
@@ -1451,3 +1622,85 @@ esp_err_t ble_esl_register_cb(ble_esl_cb_t callback)
ESP_LOGI(TAG, "Application callback registered");
return ESP_OK;
}
esp_err_t ble_esl_export_persisted_tag(ble_esl_persisted_tag_t *out)
{
if (out == NULL || s_ctx == NULL || !s_ctx->initialized) {
return ESP_ERR_INVALID_ARG;
}
esl_persisted_tag_ram_t ram = {
.address_valid = s_ctx->address_valid,
.ap_sync_key_valid = s_ctx->ap_sync_key_valid,
.resp_key_valid = s_ctx->resp_key_valid,
.has_bonded_peer = s_ctx->has_bonded_peer,
.config_complete = s_ctx->config_complete,
.peer_addr_type = s_ctx->bonded_peer_addr.type,
.esl_address = s_ctx->esl_address,
.ap_sync_key = s_ctx->ap_sync_key,
.resp_key = s_ctx->resp_key,
};
memcpy(ram.peer_addr, s_ctx->bonded_peer_addr.val, 6);
esl_persisted_tag_dto_t dto;
if (!esl_persisted_fill_dto(&ram, &dto)) {
return ESP_ERR_INVALID_STATE;
}
out->esl_address = dto.esl_address;
out->ap_sync_key = dto.ap_sync_key;
out->resp_key = dto.resp_key;
out->peer_addr_type = dto.peer_addr_type;
memcpy(out->peer_addr, dto.peer_addr, 6);
return ESP_OK;
}
esp_err_t ble_esl_restore_persisted_tag(const ble_esl_persisted_tag_t *info)
{
if (info == NULL || s_ctx == NULL || !s_ctx->initialized) {
return ESP_ERR_INVALID_ARG;
}
if (s_ctx->started) {
ESP_LOGE(TAG, "restore_persisted_tag must be called before ble_esl_start()");
return ESP_ERR_INVALID_STATE;
}
esl_persisted_tag_dto_t dto = {
.esl_address = info->esl_address,
.ap_sync_key = info->ap_sync_key,
.resp_key = info->resp_key,
.peer_addr_type = info->peer_addr_type,
};
memcpy(dto.peer_addr, info->peer_addr, 6);
esl_persisted_tag_ram_t ram;
memset(&ram, 0, sizeof(ram));
if (!esl_persisted_apply_dto(&dto, &ram)) {
return ESP_ERR_INVALID_ARG;
}
/* Bare assignment — do NOT call esl_state_transition(). */
s_ctx->esl_address = ram.esl_address;
s_ctx->ap_sync_key = ram.ap_sync_key;
s_ctx->resp_key = ram.resp_key;
s_ctx->address_valid = true;
s_ctx->ap_sync_key_valid = true;
s_ctx->resp_key_valid = true;
s_ctx->has_bonded_peer = true;
s_ctx->bonded_peer_addr.type = ram.peer_addr_type;
memcpy(s_ctx->bonded_peer_addr.val, ram.peer_addr, 6);
s_ctx->config_complete = ram.config_complete; /* ABS_TIME bit clear */
s_ctx->abs_time_base = 0;
s_ctx->abs_time_offset_us = 0;
s_ctx->past_received = false;
s_ctx->past_pending = false;
s_ctx->pawr_synced = false;
s_ctx->update_complete_received = false;
s_ctx->current_sync_handle = BLE_HS_CONN_HANDLE_NONE;
s_ctx->retiring_sync_handle = BLE_HS_CONN_HANDLE_NONE;
s_ctx->retiring_local_terminate = false;
s_ctx->state = BLE_ESL_STATE_UNSYNCHRONIZED;
ESP_LOGI(TAG, "Restored association, state=UNSYNCHRONIZED");
return ESP_OK;
}
@@ -0,0 +1,61 @@
/*
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*
* Pure classification helpers for PERIODIC_SYNC_LOST (host-testable).
*
* SYNC_LOST carries only a handle. Handle reuse cannot be detected here, so
* classification uses current vs retiring handles plus the local-terminate
* flag — not a generation counter.
*/
#pragma once
#include <stdint.h>
#include <stdbool.h>
#ifdef __cplusplus
extern "C" {
#endif
#ifndef ESL_SYNC_HANDLE_NONE
#define ESL_SYNC_HANDLE_NONE 0xffffu
#endif
typedef enum {
ESL_SYNC_LOST_CURRENT_NATURAL = 0,
ESL_SYNC_LOST_RETIRING_LOCAL,
ESL_SYNC_LOST_STALE,
} esl_sync_lost_class_t;
typedef struct {
uint16_t current_sync_handle;
uint16_t retiring_sync_handle;
bool retiring_local_terminate;
} esl_sync_lost_ctx_t;
static inline esl_sync_lost_class_t esl_classify_sync_lost(const esl_sync_lost_ctx_t *ctx,
uint16_t evt_sync_handle)
{
if (ctx == NULL) {
return ESL_SYNC_LOST_STALE;
}
if (evt_sync_handle == ctx->retiring_sync_handle &&
ctx->retiring_sync_handle != ESL_SYNC_HANDLE_NONE &&
ctx->retiring_local_terminate) {
return ESL_SYNC_LOST_RETIRING_LOCAL;
}
if (evt_sync_handle == ctx->current_sync_handle &&
ctx->current_sync_handle != ESL_SYNC_HANDLE_NONE) {
return ESL_SYNC_LOST_CURRENT_NATURAL;
}
return ESL_SYNC_LOST_STALE;
}
#ifdef __cplusplus
}
#endif
@@ -107,6 +107,7 @@ typedef enum {
BLE_OTS_SERVER_EVT_READ_COMPLETE, /*!< Read transfer completed */
BLE_OTS_SERVER_EVT_WRITE_COMPLETE, /*!< Write transfer completed */
BLE_OTS_SERVER_EVT_METADATA_WRITTEN, /*!< Client wrote a metadata characteristic */
BLE_OTS_SERVER_EVT_DATA_WRITE, /*!< Object data chunk received during write transfer */
} ble_ots_server_event_t;
/*****************************************************************************
@@ -173,6 +174,18 @@ typedef struct {
ble_ots_server_transfer_status_t status; /*!< Completion status */
} ble_ots_server_evt_read_complete_t;
/**
* @brief Event data for BLE_OTS_SERVER_EVT_DATA_WRITE.
*
* The @p data pointer is valid only for the duration of the callback.
*/
typedef struct {
ble_ots_obj_id_t object_id; /*!< Object ID being written */
uint32_t offset; /*!< Byte offset within the object */
const uint8_t *data; /*!< Pointer to received chunk data */
uint16_t data_len; /*!< Length of chunk data in octets */
} ble_ots_server_evt_data_write_t;
/**
* @brief Event data for BLE_OTS_SERVER_EVT_WRITE_COMPLETE.
*/
@@ -209,6 +222,7 @@ typedef union {
ble_ots_server_oacp_execute_evt_t execute; /*!< BLE_OTS_SERVER_EVT_EXECUTE */
ble_ots_server_oacp_checksum_evt_t checksum; /*!< BLE_OTS_SERVER_EVT_CHECKSUM_REQUEST */
ble_ots_server_evt_read_complete_t read_complete; /*!< BLE_OTS_SERVER_EVT_READ_COMPLETE */
ble_ots_server_evt_data_write_t data_write; /*!< BLE_OTS_SERVER_EVT_DATA_WRITE */
ble_ots_server_evt_write_complete_t write_complete; /*!< BLE_OTS_SERVER_EVT_WRITE_COMPLETE */
ble_ots_server_metadata_evt_t metadata_written; /*!< BLE_OTS_SERVER_EVT_METADATA_WRITTEN */
} ble_ots_server_cb_param_t;
@@ -323,6 +337,22 @@ int ble_ots_server_set_object_data(ble_ots_obj_id_t object_id,
uint32_t offset,
uint32_t length);
/**
* @brief Copy object content into a caller-owned buffer under the OTS mutex.
*
* Performs an atomic snapshot of [offset, offset+length) against the object's
* current_size. Suitable for application reads that must not race with OACP
* writes.
*
* @param object_id Object ID of the source object
* @param offset Byte offset within the object
* @param length Number of octets to copy
* @param buf Destination buffer (must be >= length octets)
* @return 0 on success, non-zero on failure (ENOENT / EINVAL)
*/
int ble_ots_server_copy_object_data(ble_ots_obj_id_t object_id, uint32_t offset,
uint32_t length, uint8_t *buf);
/**
* @brief Trigger Object Changed indication for a server-initiated change.
*
@@ -25,7 +25,7 @@ static const char *TAG = "ots_client_transfer";
* Constants
*****************************************************************************/
#define PSM_OTS 0x0025
#define OTC_MTU 256
#define OTC_MTU 1024
#define OACP_RESPONSE_OPCODE 0x60
@@ -68,7 +68,8 @@ void ble_ots_server_unlock(void)
/*****************************************************************************
* L2CAP OTC MTU
*****************************************************************************/
#define OTS_L2CAP_COC_MTU 256
#define OTS_L2CAP_COC_MTU 1024
#define OTS_L2CAP_RX_WIN_MAX 8
/*****************************************************************************
* Directory Listing Object name
@@ -547,6 +548,12 @@ ble_ots_server_obj_t *ble_ots_server_obj_db_lookup(ble_ots_obj_id_t object_id)
*****************************************************************************/
int ble_ots_server_obj_data_read(ble_ots_obj_id_t object_id, uint32_t offset,
uint32_t length, uint8_t *buf)
{
return ble_ots_server_copy_object_data(object_id, offset, length, buf);
}
int ble_ots_server_copy_object_data(ble_ots_obj_id_t object_id, uint32_t offset,
uint32_t length, uint8_t *buf)
{
int rc = 0;
@@ -564,7 +571,7 @@ int ble_ots_server_obj_data_read(ble_ots_obj_id_t object_id, uint32_t offset,
rc = BLE_HS_ENOENT;
} else if (offset + length > obj->current_size) {
rc = BLE_HS_EINVAL;
} else if (obj->data) {
} else if (obj->data && length > 0) {
memcpy(buf, obj->data + offset, length);
}
@@ -1509,13 +1516,40 @@ static int ots_l2cap_event_handle(struct ble_l2cap_event *event)
return BLE_HS_ENOMEM;
}
/* Provide an SDU receive buffer */
struct os_mbuf *sdu_rx = ble_hs_mbuf_from_flat(NULL, 0);
if (!sdu_rx) {
ESP_LOGE(TAG, "L2CAP accept: failed to allocate SDU rx buffer");
return BLE_HS_ENOMEM;
/* Pre-post the configured receive window. Each buffer contributes one
* peer credit; posting only one (the Kconfig default) is stop-and-wait.
* Raise CONFIG_BT_NIMBLE_L2CAP_COC_SDU_BUFF_COUNT and msys block size
* to actually pipeline a 1024-byte CoC write. Allocate all SDUs first
* so a later ENOMEM does not leave a half-posted window. */
int win = CONFIG_BT_NIMBLE_L2CAP_COC_SDU_BUFF_COUNT;
if (win < 1) {
win = 1;
} else if (win > OTS_L2CAP_RX_WIN_MAX) {
win = OTS_L2CAP_RX_WIN_MAX;
}
struct os_mbuf *sdu_bufs[OTS_L2CAP_RX_WIN_MAX];
int got = 0;
for (; got < win; got++) {
sdu_bufs[got] = os_msys_get_pkthdr(OTS_L2CAP_COC_MTU, 0);
if (!sdu_bufs[got]) {
ESP_LOGE(TAG, "L2CAP accept: failed to allocate SDU rx buffer %d", got);
while (got-- > 0) {
os_mbuf_free_chain(sdu_bufs[got]);
}
return BLE_HS_ENOMEM;
}
}
for (int i = 0; i < win; i++) {
int rc = ble_l2cap_recv_ready(event->accept.chan, sdu_bufs[i]);
if (rc != 0) {
ESP_LOGE(TAG, "L2CAP accept: recv_ready failed rc=%d", rc);
os_mbuf_free_chain(sdu_bufs[i]);
for (int j = i + 1; j < win; j++) {
os_mbuf_free_chain(sdu_bufs[j]);
}
return rc;
}
}
ble_l2cap_recv_ready(event->accept.chan, sdu_rx);
return 0;
}
@@ -1558,7 +1592,7 @@ static int ots_l2cap_event_handle(struct ble_l2cap_event *event)
event->receive.sdu_rx);
/* Provide a new SDU rx buffer for next receive */
struct os_mbuf *sdu_rx = ble_hs_mbuf_from_flat(NULL, 0);
struct os_mbuf *sdu_rx = os_msys_get_pkthdr(OTS_L2CAP_COC_MTU, 0);
if (sdu_rx) {
ble_l2cap_recv_ready(event->receive.chan, sdu_rx);
}
@@ -407,6 +407,14 @@ void ble_ots_server_otc_receive_cb(uint16_t conn_handle, struct ble_l2cap_chan *
int write_err = 0;
while (cur != NULL && written < data_len) {
if (cur->om_len > 0) {
ble_ots_server_cb_param_t data_param;
memset(&data_param, 0, sizeof(data_param));
data_param.data_write.object_id = cs->transfer_object_id;
data_param.data_write.offset = write_offset + written;
data_param.data_write.data = cur->om_data;
data_param.data_write.data_len = cur->om_len;
ble_ots_server_dispatch_event(BLE_OTS_SERVER_EVT_DATA_WRITE, &data_param);
int rc = ble_ots_server_obj_data_write(cs->transfer_object_id,
write_offset + written,
cur->om_data, cur->om_len);