From bd1599b333b1adaf1baa341db075f0ac53dad3cb Mon Sep 17 00:00:00 2001 From: jiminxiang Date: Thu, 3 Sep 2026 16:08:30 +0800 Subject: [PATCH] feat(ble/nimble): add ESL persist, AP recovery, and image APIs Enable Tag/AP reboot recovery and image restore, plus OTS DATA_WRITE. AP start only brings up PAwR; scan is explicit via restart_scan. Ported onto current master ESL/OTS APIs (ble_esl_state_t, ble_esl_key_material_t, ble_esl_address_t at the public boundary). --- .../nimble/ble_esl/include/ble_esl.h | 96 ++++ .../nimble/ble_esl/include/ble_esl_ap.h | 134 ++++- .../nimble/ble_esl/src/ap/ap_command.c | 227 +++++++-- .../nimble/ble_esl/src/ap/ap_connection.c | 339 +++++++++---- .../nimble/ble_esl/src/ap/ap_lifecycle.c | 466 ++++++++++++++++-- .../nimble/ble_esl/src/ap/ap_pawr.c | 54 +- .../nimble/ble_esl/src/ap/ble_esl_ap_int.h | 44 +- .../nimble/ble_esl/src/esl/ble_esl_int.h | 13 +- .../ble_esl/src/esl/ble_esl_state_int.h | 12 +- .../nimble/ble_esl/src/esl/esl_cmd_display.c | 27 +- .../ble_esl/src/esl/esl_cmd_lifecycle.c | 1 + .../nimble/ble_esl/src/esl/esl_gatts.c | 239 ++++++++- .../ble_esl/src/esl/esl_persisted_tag_map.h | 130 +++++ .../nimble/ble_esl/src/esl/esl_state.c | 333 +++++++++++-- .../nimble/ble_esl/src/esl/esl_sync_lost.h | 78 +++ .../nimble/ble_ots/include/ble_ots_server.h | 30 ++ .../ble_ots/src/server/ots_server_init.c | 8 +- .../src/server/ots_server_oacp_transfer.c | 8 + 18 files changed, 1954 insertions(+), 285 deletions(-) create mode 100644 components/bt/ble_profiles/nimble/ble_esl/src/esl/esl_persisted_tag_map.h create mode 100644 components/bt/ble_profiles/nimble/ble_esl/src/esl/esl_sync_lost.h diff --git a/components/bt/ble_profiles/nimble/ble_esl/include/ble_esl.h b/components/bt/ble_profiles/nimble/ble_esl/include/ble_esl.h index 8135ac4bd43..84440533ff3 100644 --- a/components/bt/ble_profiles/nimble/ble_esl/include/ble_esl.h +++ b/components/bt/ble_profiles/nimble/ble_esl/include/ble_esl.h @@ -19,6 +19,7 @@ #include #include +#include #include "esp_err.h" #include "ble_esl_common.h" @@ -318,6 +319,101 @@ 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. + * + * Marks the slot incomplete while a concurrent write is in progress so that + * Display Image never observes a torn frame. + * + * @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 + * (CONFIG_BIT_ABS_TIME 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 diff --git a/components/bt/ble_profiles/nimble/ble_esl/include/ble_esl_ap.h b/components/bt/ble_profiles/nimble/ble_esl/include/ble_esl_ap.h index c9da5071d7b..ddf1cea9b5f 100644 --- a/components/bt/ble_profiles/nimble/ble_esl/include/ble_esl_ap.h +++ b/components/bt/ble_profiles/nimble/ble_esl/include/ble_esl_ap.h @@ -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 ========================== */ @@ -117,7 +119,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; /** @@ -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 ========================== */ /** @@ -342,16 +364,17 @@ 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 AP operation (PAwR broadcasting; scan idle until scan_start) * - * Begins GAP General Discovery for ESLs and starts PAwR broadcasting. - * Discovered ESLs are reported via BLE_ESL_AP_EVT_SCAN_RESULT. + * Starts PAwR broadcasting. GAP discovery is not started here; call + * ble_esl_ap_start_scan() when the phone sends scan_start. * * @note This function does not automatically initiate connections. The caller must handle * scan results and call ble_esl_ap_connect() to establish connections. @@ -360,6 +383,27 @@ esp_err_t ble_esl_ap_deinit(void); */ esp_err_t ble_esl_ap_start(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 AP 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 AP not started + */ +esp_err_t ble_esl_ap_stop_scan(void); + /** * @brief Stop AP operation (scanning + PAwR broadcasting) * @@ -425,6 +469,14 @@ 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 */ @@ -477,6 +529,36 @@ 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 + * + * Uses the current boot monotonic clock (esp_timer_get_time()/1000). + * 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 + * @return ESP_OK if the GATT write was initiated + */ +esp_err_t ble_esl_ap_write_absolute_time(uint16_t conn_handle); + +/** + * @brief Persisted association used to seed AP tracking after reboot + */ +typedef struct __attribute__((packed)) { + uint16_t esl_addr; + uint8_t ble_addr[6]; + uint8_t ble_addr_type; + ble_esl_key_material_t ap_sync_key; + ble_esl_key_material_t resp_key; +} 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 +575,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 +635,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 +651,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 +696,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 */ diff --git a/components/bt/ble_profiles/nimble/ble_esl/src/ap/ap_command.c b/components/bt/ble_profiles/nimble/ble_esl/src/ap/ap_command.c index 0f603470cae..924f84c3fa4 100644 --- a/components/bt/ble_profiles/nimble/ble_esl/src/ap/ap_command.c +++ b/components/bt/ble_profiles/nimble/ble_esl/src/ap/ap_command.c @@ -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, diff --git a/components/bt/ble_profiles/nimble/ble_esl/src/ap/ap_connection.c b/components/bt/ble_profiles/nimble/ble_esl/src/ap/ap_connection.c index 5c99dbaf03b..d9448619b53 100644 --- a/components/bt/ble_profiles/nimble/ble_esl/src/ap/ap_connection.c +++ b/components/bt/ble_profiles/nimble/ble_esl/src/ap/ap_connection.c @@ -11,15 +11,23 @@ */ #include +#include #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"; @@ -33,6 +41,9 @@ static const char *TAG = "esl_ap_conn"; 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,8 +53,46 @@ 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); +/* ========================== 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 ========================== */ ble_esl_ap_conn_t *ble_esl_ap_find_conn(uint16_t conn_handle) @@ -88,6 +137,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 +153,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 +169,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 +186,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,6 +218,15 @@ 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; @@ -185,6 +249,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 +260,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 +272,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 +285,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; @@ -272,6 +344,11 @@ esp_err_t ble_esl_ap_deinit(void) return ESP_ERR_INVALID_STATE; } + 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 scanning and PAwR if active */ if (g_esl_ap->started) { ble_esl_ap_stop(); @@ -335,6 +412,12 @@ 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; } @@ -349,7 +432,22 @@ esp_err_t ble_esl_ap_start(void) return ESP_ERR_INVALID_STATE; } - /* Start GAP General Discovery (scanning) */ + g_esl_ap->scan_suppressed = true; + + /* Start PAwR broadcasting; GAP discovery waits for explicit scan_start */ + 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->started = true; + ESP_LOGI(TAG, "ESL AP started (PAwR active, scan idle until scan_start)"); + 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 +455,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,17 +476,49 @@ esp_err_t ble_esl_ap_start(void) ESP_LOGE(TAG, "Failed to start GAP discovery; rc=%d", rc); return ESP_FAIL; } + return ESP_OK; +} - /* 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; +static void ble_esl_ap_maybe_resume_discovery(void) +{ + if (!g_esl_ap || !g_esl_ap->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->started) { + return ESP_ERR_INVALID_STATE; } - g_esl_ap->started = true; - ESP_LOGI(TAG, "ESL AP started (scanning + PAwR)"); + 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; +} + +esp_err_t ble_esl_ap_stop_scan(void) +{ + if (!g_esl_ap || !g_esl_ap->initialized || !g_esl_ap->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; } @@ -464,14 +601,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 +619,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 +651,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 +687,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 +717,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) { @@ -638,6 +791,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 +829,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 +838,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 +864,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 +901,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 +930,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 +940,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 +962,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 +1009,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 +1044,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 +1053,12 @@ 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); if (conn->disc_done) { ESP_LOGD(TAG, "Encryption change on already-discovered conn_handle=%u; skipping discovery", diff --git a/components/bt/ble_profiles/nimble/ble_esl/src/ap/ap_lifecycle.c b/components/bt/ble_profiles/nimble/ble_esl/src/ap/ap_lifecycle.c index 6e146dcead8..5592a39e988 100644 --- a/components/bt/ble_profiles/nimble/ble_esl/src/ap/ap_lifecycle.c +++ b/components/bt/ble_profiles/nimble/ble_esl/src/ap/ap_lifecycle.c @@ -16,8 +16,12 @@ #include #include +#include +#include #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 ========================== */ /** @@ -57,6 +65,7 @@ typedef struct { uint16_t conn_handle; ble_esl_ap_esl_config_t config; uint16_t esl_addr; + uint32_t lf_generation; } configure_ctx_t; /** @@ -117,6 +126,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, @@ -159,12 +176,227 @@ static image_transfer_ctx_t *s_image_ctx = NULL; /** 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"); +_Static_assert(offsetof(ble_esl_ap_persisted_esl_t, ap_sync_key) == 9, "flat key block start"); +_Static_assert(offsetof(ble_esl_ap_persisted_esl_t, resp_key) == 9 + 24, "resp key follows sync"); + +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, + ble_esl_ap_gatt_cb_t cb, void *user_data) +{ + int64_t now_us = esp_timer_get_time(); + uint32_t abs_time_ms = (uint32_t)(now_us / 1000ULL); + 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) +{ + 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, 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; + esp_err_t ret = write_abs_time_bytes(conn_handle, 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 +408,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 +418,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 +452,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,7 +479,10 @@ static void lifecycle_timeout_cb(void *arg) } int64_t now_us = esp_timer_get_time(); + ble_esl_ap_state_evt_snap_t snaps[CONFIG_BLE_ESL_AP_MAX_ESLS]; + int snap_count = 0; + 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) { @@ -250,8 +495,14 @@ static void lifecycle_timeout_cb(void *arg) (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); + if (snap_count < CONFIG_BLE_ESL_AP_MAX_ESLS) { + (void)ble_esl_ap_update_esl_state_locked(esl->esl_addr, + BLE_ESL_STATE_UNSYNCHRONIZED, + &snaps[snap_count]); + if (snaps[snap_count].pending) { + snap_count++; + } + } } } else if (esl->state == BLE_ESL_STATE_UNSYNCHRONIZED) { /* Check 60-minute reconnect timeout */ @@ -259,18 +510,35 @@ static void lifecycle_timeout_cb(void *arg) (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 (snap_count < CONFIG_BLE_ESL_AP_MAX_ESLS) { + (void)ble_esl_ap_update_esl_state_locked(esl->esl_addr, + BLE_ESL_STATE_UNASSOCIATED, + &snaps[snap_count]); + if (snaps[snap_count].pending) { + snap_count++; + } + } } } } + ble_esl_ap_tracking_unlock(); + + for (int i = 0; i < snap_count; i++) { + ble_esl_ap_dispatch_state_evt(&snaps[i]); + } } /* ========================== 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 +564,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 +583,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 +602,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 ========================== */ @@ -373,14 +648,14 @@ esp_err_t ble_esl_ap_configure(uint16_t conn_handle, /* 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 +663,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,6 +673,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(); + + 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 PAwR sync key (shared across all ESLs) */ ble_esl_ap_pawr_set_sync_key(&config->ap_sync_key); @@ -423,6 +708,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; @@ -461,6 +747,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 +788,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 +815,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 +825,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 +853,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, @@ -1129,6 +1410,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 +1428,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 +1585,17 @@ 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 snaps[CONFIG_BLE_ESL_AP_MAX_ESLS]; + int snap_count = 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 +1603,45 @@ 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); + if (snap_count < CONFIG_BLE_ESL_AP_MAX_ESLS) { + (void)ble_esl_ap_update_esl_state_locked(esl->esl_addr, + BLE_ESL_STATE_UNSYNCHRONIZED, + &snaps[snap_count]); + if (snaps[snap_count].pending) { + snap_count++; + } + } } 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); + if (snap_count < CONFIG_BLE_ESL_AP_MAX_ESLS) { + (void)ble_esl_ap_update_esl_state_locked(esl->esl_addr, + BLE_ESL_STATE_UNASSOCIATED, + &snaps[snap_count]); + if (snaps[snap_count].pending) { + snap_count++; + } + } } } 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); + if (snap_count < CONFIG_BLE_ESL_AP_MAX_ESLS) { + (void)ble_esl_ap_update_esl_state_locked(esl->esl_addr, + BLE_ESL_STATE_UNSYNCHRONIZED, + &snaps[snap_count]); + if (snaps[snap_count].pending) { + snap_count++; + } + } } } break; } + ble_esl_ap_tracking_unlock(); + for (int i = 0; i < snap_count; i++) { + ble_esl_ap_dispatch_state_evt(&snaps[i]); + } } /* Step 2: Handle synchronize procedure completion if this disconnect @@ -1387,3 +1701,67 @@ 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; + } + + if (info->esl_addr == 0) { + ESP_LOGI(TAG, "restore: esl_addr=0x0000 (esl_id=0 group_id=0)"); + } + + if (BLE_ESL_AP_ADDR_ESL_ID(info->esl_addr) == BLE_ESL_BROADCAST_ADDRESS) { + ESP_LOGE(TAG, "restore: broadcast esl_id not allowed"); + return ESP_ERR_INVALID_ARG; + } + + ble_esl_ap_tracking_lock(); + ble_esl_ap_esl_entry_t *esl = ble_esl_ap_find_esl(info->esl_addr); + if (esl == NULL) { + esl = ble_esl_ap_find_esl_by_ble_addr(info->ble_addr, info->ble_addr_type); + } + if (esl == NULL) { + esl = ble_esl_ap_alloc_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 = info->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_tracking_unlock(); + + ble_esl_ap_pawr_set_sync_key(&info->ap_sync_key); + + esp_err_t ret = ble_esl_ap_pawr_set_response_key(info->esl_addr, + &info->resp_key); + if (ret != ESP_OK) { + ESP_LOGW(TAG, "restore: set response key for 0x%04X failed: %s", + info->esl_addr, esp_err_to_name(ret)); + } + + ret = ble_esl_ap_update_esl_state(info->esl_addr, + BLE_ESL_STATE_UNSYNCHRONIZED); + if (ret != ESP_OK) { + ESP_LOGE(TAG, "restore: failed to mark ESL 0x%04X unsynchronized: %s", + info->esl_addr, esp_err_to_name(ret)); + return ret; + } + + ESP_LOGI(TAG, "Restored ESL 0x%04X from persistence (awaiting resync)", + info->esl_addr); + return ESP_OK; +} diff --git a/components/bt/ble_profiles/nimble/ble_esl/src/ap/ap_pawr.c b/components/bt/ble_profiles/nimble/ble_esl/src/ap/ap_pawr.c index 74d7c55bc3d..dc3f0c70bf9 100644 --- a/components/bt/ble_profiles/nimble/ble_esl/src/ap/ap_pawr.c +++ b/components/bt/ble_profiles/nimble/ble_esl/src/ap/ap_pawr.c @@ -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); diff --git a/components/bt/ble_profiles/nimble/ble_esl/src/ap/ble_esl_ap_int.h b/components/bt/ble_profiles/nimble/ble_esl/src/ap/ble_esl_ap_int.h index 027c0afbce0..d97cce8ff91 100644 --- a/components/bt/ble_profiles/nimble/ble_esl/src/ap/ble_esl_ap_int.h +++ b/components/bt/ble_profiles/nimble/ble_esl/src/ap/ble_esl_ap_int.h @@ -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 ========================== */ @@ -227,6 +231,7 @@ typedef struct { typedef struct { bool initialized; /*!< Module has been initialized */ bool started; /*!< Scanning + PAwR broadcasting active */ + bool scan_suppressed; /*!< User/app requested scan stop; do not auto-resume */ /* Application callback */ ble_esl_ap_cb_t app_cb; @@ -253,6 +258,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 +299,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 +330,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); diff --git a/components/bt/ble_profiles/nimble/ble_esl/src/esl/ble_esl_int.h b/components/bt/ble_profiles/nimble/ble_esl/src/esl/ble_esl_int.h index c93055b0d11..1e96aea2052 100644 --- a/components/bt/ble_profiles/nimble/ble_esl/src/esl/ble_esl_int.h +++ b/components/bt/ble_profiles/nimble/ble_esl/src/esl/ble_esl_int.h @@ -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 */ diff --git a/components/bt/ble_profiles/nimble/ble_esl/src/esl/ble_esl_state_int.h b/components/bt/ble_profiles/nimble/ble_esl/src/esl/ble_esl_state_int.h index 92f2fdda63b..20b6574e0f9 100644 --- a/components/bt/ble_profiles/nimble/ble_esl/src/esl/ble_esl_state_int.h +++ b/components/bt/ble_profiles/nimble/ble_esl/src/esl/ble_esl_state_int.h @@ -60,10 +60,18 @@ 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 */ + uint32_t sync_generation; /* Bumps each time current_sync_handle is assigned a new value */ + uint16_t retiring_sync_handle; /* Locally terminated old sync awaiting SYNC_LOST */ + uint32_t retiring_sync_generation; /* Snapshot of sync_generation when current was retired */ + 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 */ diff --git a/components/bt/ble_profiles/nimble/ble_esl/src/esl/esl_cmd_display.c b/components/bt/ble_profiles/nimble/ble_esl/src/esl/esl_cmd_display.c index 4e7028e905b..3c5dc7861f9 100644 --- a/components/bt/ble_profiles/nimble/ble_esl/src/esl/esl_cmd_display.c +++ b/components/bt/ble_profiles/nimble/ble_esl/src/esl/esl_cmd_display.c @@ -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; } diff --git a/components/bt/ble_profiles/nimble/ble_esl/src/esl/esl_cmd_lifecycle.c b/components/bt/ble_profiles/nimble/ble_esl/src/esl/esl_cmd_lifecycle.c index 5e88dec7b4b..9963685b0e3 100644 --- a/components/bt/ble_profiles/nimble/ble_esl/src/esl/esl_cmd_lifecycle.c +++ b/components/bt/ble_profiles/nimble/ble_esl/src/esl/esl_cmd_lifecycle.c @@ -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) { diff --git a/components/bt/ble_profiles/nimble/ble_esl/src/esl/esl_gatts.c b/components/bt/ble_profiles/nimble/ble_esl/src/esl/esl_gatts.c index 935a1f241a0..e52e91bf5b1 100644 --- a/components/bt/ble_profiles/nimble/ble_esl/src/esl/esl_gatts.c +++ b/components/bt/ble_profiles/nimble/ble_esl/src/esl/esl_gatts.c @@ -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; @@ -502,7 +505,172 @@ 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; + } + + 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]; + int rc = ble_ots_server_copy_object_data(obj_id, 0, length, dst); + bool still_complete = s_esl_gatts->image_complete[image_index]; + xSemaphoreGive(s_esl_gatts->image_lock); + + if (rc != 0 || !still_complete) { + 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]; + int rc = ble_ots_server_set_object_data(obj_id, data, 0, (uint32_t)len); + if (rc != 0) { + xSemaphoreGive(s_esl_gatts->image_lock); + ESP_LOGE(TAG, "image_restore: set_object_data failed rc=%d index=%u", + rc, image_index); + return ESP_FAIL; + } + + 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 +679,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 = param->write_complete.bytes_received, + .offset = param->write_complete.offset, } }; esl_notify_app(BLE_ESL_EVT_IMAGE_WRITE, &cb_param); @@ -557,6 +733,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 +816,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 */ diff --git a/components/bt/ble_profiles/nimble/ble_esl/src/esl/esl_persisted_tag_map.h b/components/bt/ble_profiles/nimble/ble_esl/src/esl/esl_persisted_tag_map.h new file mode 100644 index 00000000000..721a2eff344 --- /dev/null +++ b/components/bt/ble_profiles/nimble/ble_esl/src/esl/esl_persisted_tag_map.h @@ -0,0 +1,130 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * @file esl_persisted_tag_map.h + * @brief Pure helpers for TAG persisted snapshot field mapping (host-testable). + */ +#pragma once + +#include +#include +#include +#include "ble_esl_common.h" + +#ifdef __cplusplus +extern "C" { +#endif + +#ifndef CONFIG_BIT_ADDRESS +#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) +#endif + +#define ESL_PERSISTED_CONFIG_MASK \ + (CONFIG_BIT_ADDRESS | CONFIG_BIT_AP_SYNC_KEY | 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 diff --git a/components/bt/ble_profiles/nimble/ble_esl/src/esl/esl_state.c b/components/bt/ble_profiles/nimble/ble_esl/src/esl/esl_state.c index 5832bfa1299..14be6340611 100644 --- a/components/bt/ble_profiles/nimble/ble_esl/src/esl/esl_state.c +++ b/components/bt/ble_profiles/nimble/ble_esl/src/esl/esl_state.c @@ -11,6 +11,7 @@ #include #include +#include #include "freertos/FreeRTOS.h" #include "freertos/semphr.h" @@ -27,6 +28,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 +46,142 @@ 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_sync_generation = 0; + 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; + /* sync_generation is monotonic across NONE assignments; do not reset. */ +} + +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; + if (s_ctx->sync_generation == UINT32_MAX) { + s_ctx->sync_generation = 1; + } else { + s_ctx->sync_generation++; + } +} + +/** + * @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; + uint32_t old_generation = s_ctx->sync_generation; + + s_ctx->retiring_sync_handle = old_handle; + s_ctx->retiring_sync_generation = old_generation; + 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 gen=%" PRIu32 " rearm_past=%d", + old_handle, old_generation, 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; + uint32_t old_generation = s_ctx->sync_generation; + s_ctx->retiring_sync_handle = old_handle; + s_ctx->retiring_sync_generation = old_generation; + 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 — keep generation. */ + return; + } + + install_current_sync(sync_handle); +} /* ========================== State Machine ========================== */ @@ -347,22 +486,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; } @@ -643,8 +782,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 +952,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); @@ -974,18 +1114,61 @@ 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, + .sync_generation = s_ctx->sync_generation, + .retiring_sync_handle = s_ctx->retiring_sync_handle, + .retiring_sync_generation = s_ctx->retiring_sync_generation, + .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 +1322,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 +1508,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 +1605,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 +1636,87 @@ 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->sync_generation = 0; + s_ctx->retiring_sync_handle = BLE_HS_CONN_HANDLE_NONE; + s_ctx->retiring_sync_generation = 0; + s_ctx->retiring_local_terminate = false; + s_ctx->state = BLE_ESL_STATE_UNSYNCHRONIZED; + + ESP_LOGI(TAG, "Restored association, state=UNSYNCHRONIZED"); + return ESP_OK; +} diff --git a/components/bt/ble_profiles/nimble/ble_esl/src/esl/esl_sync_lost.h b/components/bt/ble_profiles/nimble/ble_esl/src/esl/esl_sync_lost.h new file mode 100644 index 00000000000..1beb6c6f65a --- /dev/null +++ b/components/bt/ble_profiles/nimble/ble_esl/src/esl/esl_sync_lost.h @@ -0,0 +1,78 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + * + * Pure classification helpers for PERIODIC_SYNC_LOST (host-testable). + */ + +#pragma once + +#include +#include + +#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; + uint32_t sync_generation; + uint16_t retiring_sync_handle; + uint32_t retiring_sync_generation; + bool retiring_local_terminate; +} esl_sync_lost_ctx_t; + +static inline uint32_t esl_sync_lost_generation_hint(const esl_sync_lost_ctx_t *ctx, + uint16_t evt_sync_handle) +{ + if (ctx == NULL) { + return 0; + } + if (evt_sync_handle == ctx->current_sync_handle && + ctx->current_sync_handle != ESL_SYNC_HANDLE_NONE) { + return ctx->sync_generation; + } + if (evt_sync_handle == ctx->retiring_sync_handle && + ctx->retiring_sync_handle != ESL_SYNC_HANDLE_NONE) { + return ctx->retiring_sync_generation; + } + return 0; +} + +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) { + if (esl_sync_lost_generation_hint(ctx, evt_sync_handle) == ctx->sync_generation) { + return ESL_SYNC_LOST_CURRENT_NATURAL; + } + } + + return ESL_SYNC_LOST_STALE; +} + +#ifdef __cplusplus +} +#endif diff --git a/components/bt/ble_profiles/nimble/ble_ots/include/ble_ots_server.h b/components/bt/ble_profiles/nimble/ble_ots/include/ble_ots_server.h index af0c5a76388..4a59b06a4c7 100644 --- a/components/bt/ble_profiles/nimble/ble_ots/include/ble_ots_server.h +++ b/components/bt/ble_profiles/nimble/ble_ots/include/ble_ots_server.h @@ -105,6 +105,7 @@ typedef enum { BLE_OTS_SERVER_EVT_EXECUTE, /*!< OACP Execute on current object */ BLE_OTS_SERVER_EVT_CHECKSUM_REQUEST, /*!< OACP Calculate Checksum completed */ BLE_OTS_SERVER_EVT_READ_COMPLETE, /*!< Read transfer completed */ + BLE_OTS_SERVER_EVT_DATA_WRITE, /*!< Object data chunk received during write transfer */ BLE_OTS_SERVER_EVT_WRITE_COMPLETE, /*!< Write transfer completed */ BLE_OTS_SERVER_EVT_METADATA_WRITTEN, /*!< Client wrote a metadata characteristic */ } 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. * diff --git a/components/bt/ble_profiles/nimble/ble_ots/src/server/ots_server_init.c b/components/bt/ble_profiles/nimble/ble_ots/src/server/ots_server_init.c index d5ab4993dc1..d0855f2ab8c 100644 --- a/components/bt/ble_profiles/nimble/ble_ots/src/server/ots_server_init.c +++ b/components/bt/ble_profiles/nimble/ble_ots/src/server/ots_server_init.c @@ -547,6 +547,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 +570,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); } diff --git a/components/bt/ble_profiles/nimble/ble_ots/src/server/ots_server_oacp_transfer.c b/components/bt/ble_profiles/nimble/ble_ots/src/server/ots_server_oacp_transfer.c index a4bc4f2bfce..2b5c37c15a5 100644 --- a/components/bt/ble_profiles/nimble/ble_ots/src/server/ots_server_oacp_transfer.c +++ b/components/bt/ble_profiles/nimble/ble_ots/src/server/ots_server_oacp_transfer.c @@ -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);