mirror of
https://github.com/espressif/esp-idf.git
synced 2026-10-01 10:40:47 +03:00
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).
This commit is contained in:
@@ -19,6 +19,7 @@
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#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
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -154,6 +154,9 @@ static void parse_response(uint8_t esl_id, uint8_t group_id,
|
||||
static esp_err_t dispatch_command(uint8_t esl_id, uint8_t group_id,
|
||||
uint8_t opcode, const uint8_t *params,
|
||||
uint8_t params_len, bool ecp_only);
|
||||
static esp_err_t pawr_send_unicast(uint16_t esl_addr, uint8_t esl_id, uint8_t group_id,
|
||||
uint8_t opcode, const uint8_t *params,
|
||||
uint8_t params_len);
|
||||
static esp_err_t build_led_params(uint8_t esl_id, uint8_t led_index,
|
||||
const esl_ap_led_settings_t *settings,
|
||||
uint8_t *out_params, uint8_t *out_len);
|
||||
@@ -386,8 +389,19 @@ static void ecp_response_cb(uint16_t conn_handle, esp_err_t status,
|
||||
if (status != ESP_OK) {
|
||||
ESP_LOGE(TAG, "ECP write/notification failed: status=0x%X, conn=0x%04X",
|
||||
status, conn_handle);
|
||||
uint8_t esl_id = ctx->esl_id;
|
||||
uint8_t group_id = ctx->group_id;
|
||||
free_ecp_ctx(ctx);
|
||||
xSemaphoreGive(s_ecp_ctx_mutex);
|
||||
|
||||
if (g_esl_ap != NULL && g_esl_ap->app_cb != NULL) {
|
||||
esl_ap_cmd_failed_t failed_evt = {
|
||||
.esl_id = esl_id,
|
||||
.group_id = group_id,
|
||||
.status = status,
|
||||
};
|
||||
g_esl_ap->app_cb(BLE_ESL_AP_EVT_CMD_FAILED, &failed_evt);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -492,6 +506,13 @@ static void parse_response(uint8_t esl_id, uint8_t group_id,
|
||||
esl_id, response.basic_state.service_needed,
|
||||
response.basic_state.synchronized);
|
||||
|
||||
/* Basic State after Ping must NOT auto-promote ESL runtime to
|
||||
* SYNCHRONIZED. Product ONLINE is owned exclusively by the recovery
|
||||
* coordinator after a generation-validated confirmation Ping. PAST
|
||||
* success may still move ESL runtime to SYNCHRONIZED via the
|
||||
* synchronize path; that is separate from product availability. */
|
||||
(void)cmd_opcode;
|
||||
|
||||
/* For Unassociate command, transition ESL to Unassociated state */
|
||||
if (cmd_opcode == BLE_ESL_CMD_UNASSOCIATE) {
|
||||
uint16_t esl_addr = BLE_ESL_AP_MAKE_ADDR(esl_id, group_id);
|
||||
@@ -550,6 +571,128 @@ static void parse_response(uint8_t esl_id, uint8_t group_id,
|
||||
}
|
||||
}
|
||||
|
||||
/* ========================== PAwR Unicast Helper ========================== */
|
||||
|
||||
static esp_err_t pawr_send_unicast(uint16_t esl_addr, uint8_t esl_id, uint8_t group_id,
|
||||
uint8_t opcode, const uint8_t *params,
|
||||
uint8_t params_len)
|
||||
{
|
||||
uint8_t tlv_buf[BLE_ESL_TLV_MAX_SIZE];
|
||||
uint8_t tlv_len = 0;
|
||||
esp_err_t ret = ble_esl_tlv_encode(opcode, params, params_len,
|
||||
tlv_buf, &tlv_len);
|
||||
if (ret != ESP_OK) {
|
||||
ESP_LOGE(TAG, "TLV encode failed: 0x%X", ret);
|
||||
return ret;
|
||||
}
|
||||
|
||||
uint8_t payload_buf[BLE_ESL_PAYLOAD_MAX_SIZE];
|
||||
uint8_t payload_len = 0;
|
||||
const uint8_t *tlv_ptrs[] = { tlv_buf };
|
||||
const uint8_t tlv_lens[] = { tlv_len };
|
||||
|
||||
ret = ble_esl_payload_encode(group_id, tlv_ptrs, tlv_lens, 1,
|
||||
payload_buf, &payload_len);
|
||||
if (ret != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Payload encode failed: 0x%X", ret);
|
||||
return ret;
|
||||
}
|
||||
|
||||
ble_esl_ap_tracking_lock();
|
||||
ble_esl_ap_esl_entry_t *esl = ble_esl_ap_find_esl(esl_addr);
|
||||
if (esl != NULL) {
|
||||
esl->pending_pawr_cmd_opcode = opcode;
|
||||
}
|
||||
ble_esl_ap_tracking_unlock();
|
||||
|
||||
pawr_record_slot_mapping(group_id, esl_id);
|
||||
|
||||
ret = ble_esl_ap_pawr_send(group_id, payload_buf, payload_len);
|
||||
if (ret != ESP_OK) {
|
||||
ESP_LOGE(TAG, "PAwR send failed: 0x%X", ret);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
static bool pawr_opcode_allowed(uint8_t opcode)
|
||||
{
|
||||
switch (opcode) {
|
||||
case BLE_ESL_CMD_PING:
|
||||
case BLE_ESL_CMD_SERVICE_RESET:
|
||||
case BLE_ESL_CMD_READ_SENSOR:
|
||||
case BLE_ESL_CMD_REFRESH_DISPLAY:
|
||||
case BLE_ESL_CMD_DISPLAY_IMAGE:
|
||||
case BLE_ESL_CMD_DISPLAY_TIMED_IMAGE:
|
||||
case BLE_ESL_CMD_LED_CONTROL:
|
||||
case BLE_ESL_CMD_LED_TIMED_CONTROL:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
esp_err_t ble_esl_ap_pawr_command(uint8_t esl_id, uint8_t group_id,
|
||||
uint8_t opcode, const uint8_t *params,
|
||||
uint8_t params_len)
|
||||
{
|
||||
if (g_esl_ap == NULL || !g_esl_ap->initialized) {
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
if (group_id > BLE_ESL_GROUP_ID_MAX) {
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
if (BLE_ESL_TLV_TAG(opcode) == BLE_ESL_CMD_VENDOR_TAG) {
|
||||
return ESP_ERR_NOT_SUPPORTED;
|
||||
}
|
||||
if (!pawr_opcode_allowed(opcode)) {
|
||||
return ESP_ERR_NOT_SUPPORTED;
|
||||
}
|
||||
if (params == NULL || params_len != BLE_ESL_TLV_PARAMS_LEN(opcode) ||
|
||||
params[0] != esl_id) {
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
|
||||
uint8_t tlv_buf[BLE_ESL_TLV_MAX_SIZE];
|
||||
uint8_t tlv_len = 0;
|
||||
esp_err_t ret = ble_esl_tlv_encode(opcode, params, params_len,
|
||||
tlv_buf, &tlv_len);
|
||||
if (ret != ESP_OK) {
|
||||
return ret;
|
||||
}
|
||||
|
||||
uint8_t payload_buf[BLE_ESL_PAYLOAD_MAX_SIZE];
|
||||
uint8_t payload_len = 0;
|
||||
const uint8_t *tlv_ptrs[] = { tlv_buf };
|
||||
const uint8_t tlv_lens[] = { tlv_len };
|
||||
ret = ble_esl_payload_encode(group_id, tlv_ptrs, tlv_lens, 1,
|
||||
payload_buf, &payload_len);
|
||||
if (ret != ESP_OK) {
|
||||
return ret;
|
||||
}
|
||||
|
||||
if (esl_id == BLE_ESL_BROADCAST_ADDRESS) {
|
||||
return ble_esl_ap_pawr_send(group_id, payload_buf, payload_len);
|
||||
}
|
||||
|
||||
uint16_t esl_addr = BLE_ESL_AP_MAKE_ADDR(esl_id, group_id);
|
||||
ble_esl_ap_tracking_lock();
|
||||
ble_esl_ap_esl_entry_t *esl = ble_esl_ap_find_esl(esl_addr);
|
||||
if (esl == NULL) {
|
||||
ble_esl_ap_tracking_unlock();
|
||||
return ESP_ERR_NOT_FOUND;
|
||||
}
|
||||
if (esl->state != BLE_ESL_STATE_SYNCHRONIZED &&
|
||||
esl->state != BLE_ESL_STATE_UNSYNCHRONIZED) {
|
||||
ble_esl_ap_tracking_unlock();
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
esl->pending_pawr_cmd_opcode = opcode;
|
||||
pawr_record_slot_mapping(group_id, esl_id);
|
||||
ret = ble_esl_ap_pawr_send(group_id, payload_buf, payload_len);
|
||||
ble_esl_ap_tracking_unlock();
|
||||
return ret;
|
||||
}
|
||||
|
||||
/* ========================== Transport Dispatch ========================== */
|
||||
|
||||
/**
|
||||
@@ -615,20 +758,28 @@ static esp_err_t dispatch_command(uint8_t esl_id, uint8_t group_id,
|
||||
|
||||
/* Unicast: determine transport based on ESL tracked state */
|
||||
uint16_t esl_addr = BLE_ESL_AP_MAKE_ADDR(esl_id, group_id);
|
||||
ble_esl_state_t state = ble_esl_ap_get_esl_state(ble_esl_addr_make(esl_id, group_id));
|
||||
uint16_t conn_handle = BLE_ESL_AP_CONN_HANDLE_INVALID;
|
||||
ble_esl_state_t state;
|
||||
|
||||
ble_esl_ap_tracking_lock();
|
||||
ble_esl_ap_esl_entry_t *esl = ble_esl_ap_find_esl(esl_addr);
|
||||
if (esl == NULL) {
|
||||
ble_esl_ap_tracking_unlock();
|
||||
ESP_LOGE(TAG, "ESL 0x%04X not tracked — cannot dispatch command", esl_addr);
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
state = esl->state;
|
||||
conn_handle = esl->conn_handle;
|
||||
ble_esl_ap_tracking_unlock();
|
||||
|
||||
switch (state) {
|
||||
case BLE_ESL_STATE_CONFIGURING:
|
||||
case BLE_ESL_STATE_UPDATING: {
|
||||
/* ECP transport (connection-oriented) */
|
||||
ble_esl_ap_esl_entry_t *esl = ble_esl_ap_find_esl(esl_addr);
|
||||
if (esl == NULL || esl->conn_handle == BLE_ESL_AP_CONN_HANDLE_INVALID) {
|
||||
if (conn_handle == BLE_ESL_AP_CONN_HANDLE_INVALID) {
|
||||
ESP_LOGE(TAG, "No active connection for ESL 0x%04X", esl_addr);
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
|
||||
uint16_t conn_handle = esl->conn_handle;
|
||||
|
||||
xSemaphoreTake(s_ecp_ctx_mutex, portMAX_DELAY);
|
||||
|
||||
/* Check if there's already a pending ECP command on this connection */
|
||||
@@ -682,6 +833,8 @@ static esp_err_t dispatch_command(uint8_t esl_id, uint8_t group_id,
|
||||
xSemaphoreGive(s_ecp_ctx_mutex);
|
||||
|
||||
/* Write the TLV to ECP characteristic */
|
||||
ESP_LOGI(TAG, "ECP write opcode=0x%02X conn=%u esl_id=%u group=%u tlv_len=%u",
|
||||
opcode, conn_handle, esl_id, group_id, tlv_len);
|
||||
ret = ble_esl_ap_ecp_write(conn_handle, tlv_buf, tlv_len,
|
||||
ecp_response_cb, ctx);
|
||||
if (ret != ESP_OK) {
|
||||
@@ -703,37 +856,21 @@ static esp_err_t dispatch_command(uint8_t esl_id, uint8_t group_id,
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
|
||||
/* PAwR transport (connectionless) */
|
||||
uint8_t payload_buf[BLE_ESL_PAYLOAD_MAX_SIZE];
|
||||
uint8_t payload_len = 0;
|
||||
const uint8_t *tlv_ptrs[] = { tlv_buf };
|
||||
const uint8_t tlv_lens[] = { tlv_len };
|
||||
return pawr_send_unicast(esl_addr, esl_id, group_id, opcode, params, params_len);
|
||||
}
|
||||
|
||||
ret = ble_esl_payload_encode(group_id, tlv_ptrs, tlv_lens, 1,
|
||||
payload_buf, &payload_len);
|
||||
if (ret != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Payload encode failed: 0x%X", ret);
|
||||
return ret;
|
||||
case BLE_ESL_STATE_UNSYNCHRONIZED: {
|
||||
/* PAwR unicast still works for tags on connectable ext adv after AP reboot */
|
||||
if (ecp_only) {
|
||||
ESP_LOGE(TAG, "Command 0x%02X is ECP-only, cannot send in Unsynchronized state",
|
||||
opcode);
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
|
||||
/* Store the command opcode for response correlation */
|
||||
ble_esl_ap_esl_entry_t *esl = ble_esl_ap_find_esl(esl_addr);
|
||||
if (esl != NULL) {
|
||||
esl->pending_pawr_cmd_opcode = opcode;
|
||||
}
|
||||
|
||||
/* Record the response slot mapping for this group (Issue 9) */
|
||||
pawr_record_slot_mapping(group_id, esl_id);
|
||||
|
||||
ret = ble_esl_ap_pawr_send(group_id, payload_buf, payload_len);
|
||||
if (ret != ESP_OK) {
|
||||
ESP_LOGE(TAG, "PAwR send failed: 0x%X", ret);
|
||||
}
|
||||
return ret;
|
||||
return pawr_send_unicast(esl_addr, esl_id, group_id, opcode, params, params_len);
|
||||
}
|
||||
|
||||
case BLE_ESL_STATE_UNASSOCIATED:
|
||||
case BLE_ESL_STATE_UNSYNCHRONIZED:
|
||||
default:
|
||||
ESP_LOGE(TAG, "ESL 0x%04X in state %d — cannot dispatch command",
|
||||
esl_addr, state);
|
||||
@@ -1050,24 +1187,28 @@ void ble_esl_ap_command_handle_pawr_response(uint8_t group_id,
|
||||
* the correct ESL entry.
|
||||
*/
|
||||
uint16_t esl_addr = pawr_lookup_slot(group_id, response_slot);
|
||||
ble_esl_ap_esl_entry_t *esl = NULL;
|
||||
uint8_t cmd_opcode = 0;
|
||||
uint8_t esl_id = 0;
|
||||
if (esl_addr != ESL_ADDR_INVALID) {
|
||||
esl = ble_esl_ap_find_esl(esl_addr);
|
||||
}
|
||||
|
||||
if (esl == NULL) {
|
||||
ble_esl_ap_tracking_lock();
|
||||
ble_esl_ap_esl_entry_t *esl = ble_esl_ap_find_esl(esl_addr);
|
||||
if (esl == NULL) {
|
||||
ble_esl_ap_tracking_unlock();
|
||||
ESP_LOGW(TAG, "PAwR response from unknown ESL: group=%u slot=%u",
|
||||
group_id, response_slot);
|
||||
return;
|
||||
}
|
||||
esl_addr = esl->esl_addr;
|
||||
esl_id = BLE_ESL_AP_ADDR_ESL_ID(esl_addr);
|
||||
cmd_opcode = esl->pending_pawr_cmd_opcode;
|
||||
esl->pending_pawr_cmd_opcode = 0;
|
||||
ble_esl_ap_tracking_unlock();
|
||||
} else {
|
||||
ESP_LOGW(TAG, "PAwR response from unknown ESL: group=%u slot=%u",
|
||||
group_id, response_slot);
|
||||
return;
|
||||
}
|
||||
|
||||
esl_addr = esl->esl_addr;
|
||||
uint8_t esl_id = BLE_ESL_AP_ADDR_ESL_ID(esl_addr);
|
||||
|
||||
/* Retrieve and clear the stored command opcode for response correlation */
|
||||
uint8_t cmd_opcode = esl->pending_pawr_cmd_opcode;
|
||||
esl->pending_pawr_cmd_opcode = 0;
|
||||
|
||||
/* Decrypt and parse the PAwR response */
|
||||
ble_esl_ap_parsed_response_t parsed;
|
||||
esp_err_t ret = ble_esl_ap_pawr_parse_response(esl_addr, response_slot,
|
||||
|
||||
@@ -11,15 +11,23 @@
|
||||
*/
|
||||
|
||||
#include <string.h>
|
||||
#include <assert.h>
|
||||
#include "esp_log.h"
|
||||
#include "esp_err.h"
|
||||
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/semphr.h"
|
||||
#include "freertos/task.h"
|
||||
|
||||
#include "nimble/ble.h"
|
||||
#include "host/ble_hs.h"
|
||||
#include "host/ble_gap.h"
|
||||
#include "host/ble_hs_adv.h"
|
||||
#include "nimble/hci_common.h"
|
||||
|
||||
#include "ble_esl_ap.h"
|
||||
#include "ble_esl_ap_int.h"
|
||||
#include "ble_esl_common.h"
|
||||
|
||||
static const char *TAG = "esl_ap_conn";
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -16,8 +16,12 @@
|
||||
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
#include <assert.h>
|
||||
#include <stddef.h>
|
||||
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/semphr.h"
|
||||
#include "freertos/task.h"
|
||||
#include "esp_log.h"
|
||||
#include "esp_timer.h"
|
||||
#include "host/ble_gap.h"
|
||||
@@ -48,6 +52,10 @@ static const char *TAG = "esl_ap_lifecycle";
|
||||
_Static_assert(sizeof(ble_esl_key_material_t) == BLE_ESL_KEY_MATERIAL_SIZE,
|
||||
"ble_esl_key_material_t must match the on-air Key Material layout");
|
||||
|
||||
#define LF_OP_NONE 0
|
||||
#define LF_OP_CONFIGURE 1
|
||||
#define LF_OP_ABS_TIME 2
|
||||
|
||||
/* ========================== Internal Context Structures ========================== */
|
||||
|
||||
/**
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/semphr.h"
|
||||
#include "freertos/task.h"
|
||||
|
||||
#include "nimble/ble.h"
|
||||
#include "host/ble_hs.h"
|
||||
@@ -37,6 +38,25 @@ static const char *TAG = "esl_ap_pawr";
|
||||
|
||||
/** Mutex protecting the per-subevent pending TX buffers (g_esl_ap->pawr_pending) */
|
||||
static SemaphoreHandle_t s_pawr_mutex;
|
||||
static TaskHandle_t s_pawr_holder;
|
||||
|
||||
static void pawr_lock(void)
|
||||
{
|
||||
assert(s_pawr_mutex != NULL);
|
||||
xSemaphoreTake(s_pawr_mutex, portMAX_DELAY);
|
||||
s_pawr_holder = xTaskGetCurrentTaskHandle();
|
||||
}
|
||||
|
||||
static void pawr_unlock(void)
|
||||
{
|
||||
s_pawr_holder = NULL;
|
||||
xSemaphoreGive(s_pawr_mutex);
|
||||
}
|
||||
|
||||
bool ble_esl_ap_pawr_held(void)
|
||||
{
|
||||
return s_pawr_holder == xTaskGetCurrentTaskHandle();
|
||||
}
|
||||
|
||||
/** Advertising instance used for PAwR */
|
||||
#define PAWR_ADV_INSTANCE 0
|
||||
@@ -88,6 +108,7 @@ void ble_esl_ap_pawr_deinit(void)
|
||||
if (s_pawr_mutex != NULL) {
|
||||
vSemaphoreDelete(s_pawr_mutex);
|
||||
s_pawr_mutex = NULL;
|
||||
s_pawr_holder = NULL;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "PAwR sub-module deinitialized");
|
||||
@@ -216,11 +237,11 @@ esp_err_t ble_esl_ap_pawr_stop(void)
|
||||
|
||||
/* Release pending TX buffers under the lock so the data-request callback
|
||||
* (host task) never dereferences a freed pointer. */
|
||||
xSemaphoreTake(s_pawr_mutex, portMAX_DELAY);
|
||||
pawr_lock();
|
||||
g_esl_ap->pawr_active = false;
|
||||
free(g_esl_ap->pawr_pending);
|
||||
g_esl_ap->pawr_pending = NULL;
|
||||
xSemaphoreGive(s_pawr_mutex);
|
||||
pawr_unlock();
|
||||
|
||||
ESP_LOGI(TAG, "PAwR broadcaster stopped");
|
||||
|
||||
@@ -248,13 +269,16 @@ esp_err_t ble_esl_ap_pawr_set_response_key(uint16_t esl_addr,
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
|
||||
ble_esl_ap_tracking_lock();
|
||||
ble_esl_ap_esl_entry_t *entry = ble_esl_ap_find_esl(esl_addr);
|
||||
if (entry == NULL) {
|
||||
ble_esl_ap_tracking_unlock();
|
||||
ESP_LOGE(TAG, "ESL 0x%04x not found for response key", esl_addr);
|
||||
return ESP_ERR_NOT_FOUND;
|
||||
}
|
||||
|
||||
entry->resp_key = *key_mat;
|
||||
ble_esl_ap_tracking_unlock();
|
||||
|
||||
ESP_LOGI(TAG, "Response Key Material set for ESL 0x%04x", esl_addr);
|
||||
return ESP_OK;
|
||||
@@ -304,14 +328,14 @@ esp_err_t ble_esl_ap_pawr_send(uint8_t group_id, const uint8_t *payload,
|
||||
|
||||
/* The Randomizer is read and advanced under the lock: this function is a
|
||||
* public command path and may be called from any application task. */
|
||||
xSemaphoreTake(s_pawr_mutex, portMAX_DELAY);
|
||||
pawr_lock();
|
||||
esp_err_t enc_err = ble_esl_ead_encrypt(g_esl_ap->ap_sync_key.session_key,
|
||||
g_esl_ap->ap_sync_key.iv,
|
||||
g_esl_ap->randomizer,
|
||||
plaintext,
|
||||
plaintext_len,
|
||||
encrypted_payload);
|
||||
xSemaphoreGive(s_pawr_mutex);
|
||||
pawr_unlock();
|
||||
if (enc_err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "EAD encrypt failed: 0x%x", enc_err);
|
||||
return enc_err;
|
||||
@@ -349,9 +373,9 @@ esp_err_t ble_esl_ap_pawr_send(uint8_t group_id, const uint8_t *payload,
|
||||
return ESP_ERR_INVALID_SIZE;
|
||||
}
|
||||
|
||||
xSemaphoreTake(s_pawr_mutex, portMAX_DELAY);
|
||||
pawr_lock();
|
||||
if (g_esl_ap->pawr_pending == NULL) {
|
||||
xSemaphoreGive(s_pawr_mutex);
|
||||
pawr_unlock();
|
||||
ESP_LOGE(TAG, "PAwR pending buffers not allocated");
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
@@ -360,7 +384,7 @@ esp_err_t ble_esl_ap_pawr_send(uint8_t group_id, const uint8_t *payload,
|
||||
slot->len = pos;
|
||||
slot->repeats_left = PAWR_TX_REPEATS;
|
||||
slot->valid = true;
|
||||
xSemaphoreGive(s_pawr_mutex);
|
||||
pawr_unlock();
|
||||
|
||||
ESP_LOGD(TAG, "PAwR sync packet queued for group %u (payload_len=%u, ad_len=%u)",
|
||||
group_id, payload_len, pos);
|
||||
@@ -412,11 +436,19 @@ esp_err_t ble_esl_ap_pawr_parse_response(uint16_t esl_addr,
|
||||
uint8_t enc_payload_len = outer_len - 1;
|
||||
|
||||
/* Step 1: Look up ESL entry by esl_addr */
|
||||
uint8_t resp_key[BLE_ESL_SESSION_KEY_SIZE];
|
||||
uint8_t resp_iv[BLE_ESL_IV_SIZE];
|
||||
|
||||
ble_esl_ap_tracking_lock();
|
||||
ble_esl_ap_esl_entry_t *entry = ble_esl_ap_find_esl(esl_addr);
|
||||
if (entry == NULL) {
|
||||
ble_esl_ap_tracking_unlock();
|
||||
ESP_LOGE(TAG, "ESL 0x%04x not found for response decryption", esl_addr);
|
||||
return ESP_ERR_NOT_FOUND;
|
||||
}
|
||||
memcpy(resp_key, entry->resp_key.session_key, BLE_ESL_SESSION_KEY_SIZE);
|
||||
memcpy(resp_iv, entry->resp_key.iv, BLE_ESL_IV_SIZE);
|
||||
ble_esl_ap_tracking_unlock();
|
||||
|
||||
/* Step 2: Decrypt and verify MIC via ble_esl_ead_decrypt
|
||||
* Input: enc_payload = [Randomizer(5)] [Ciphertext(N)] [MIC(4)]
|
||||
@@ -424,8 +456,8 @@ esp_err_t ble_esl_ap_pawr_parse_response(uint16_t esl_addr,
|
||||
*/
|
||||
uint8_t plaintext[BLE_ESL_PAYLOAD_MAX_SIZE + 2]; /* inner AD max */
|
||||
size_t decrypted_len = 0;
|
||||
esp_err_t err = ble_esl_ead_decrypt(entry->resp_key.session_key,
|
||||
entry->resp_key.iv,
|
||||
esp_err_t err = ble_esl_ead_decrypt(resp_key,
|
||||
resp_iv,
|
||||
enc_payload,
|
||||
enc_payload_len,
|
||||
plaintext, sizeof(plaintext),
|
||||
@@ -566,7 +598,7 @@ static void pawr_handle_subev_data_req(struct ble_gap_event *event)
|
||||
uint8_t pkt[BLE_ESL_AP_PAWR_MAX_AD_BUF_SIZE];
|
||||
uint8_t pkt_len = 0;
|
||||
|
||||
xSemaphoreTake(s_pawr_mutex, portMAX_DELAY);
|
||||
pawr_lock();
|
||||
if (g_esl_ap->pawr_pending != NULL) {
|
||||
ble_esl_ap_pawr_pending_t *slot = &g_esl_ap->pawr_pending[sub];
|
||||
if (slot->valid && slot->len > 0) {
|
||||
@@ -578,7 +610,7 @@ static void pawr_handle_subev_data_req(struct ble_gap_event *event)
|
||||
}
|
||||
}
|
||||
}
|
||||
xSemaphoreGive(s_pawr_mutex);
|
||||
pawr_unlock();
|
||||
|
||||
/* Allocate an mbuf (sized to the packet, or empty for idle subevents) */
|
||||
struct os_mbuf *mbuf = os_msys_get_pkthdr(pkt_len, 0);
|
||||
|
||||
@@ -171,6 +171,10 @@ typedef struct {
|
||||
|
||||
/* ESL tracking cross-reference */
|
||||
uint16_t esl_addr; /*!< Assigned ESL Address (valid after configure) */
|
||||
|
||||
/* Per-connection lifecycle slot (protected by lifecycle mutex) */
|
||||
uint32_t lf_generation;
|
||||
uint8_t lf_op; /*!< 0=none, 1=configure, 2=abs-time */
|
||||
} ble_esl_ap_conn_t;
|
||||
|
||||
/* ========================== Per-ESL Tracking Entry ========================== */
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -100,17 +100,10 @@ static void build_display_state_response(ble_esl_cmd_result_t *result,
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Check if an image slot contains valid data
|
||||
* @brief Check if an image slot contains valid data.
|
||||
*
|
||||
* An image is considered available if:
|
||||
* - The image_writable_mask is NULL: all images are read-only (static/pre-loaded)
|
||||
* and therefore always available.
|
||||
* - The image_writable_mask entry is false: the image is read-only (protected)
|
||||
* and therefore always available.
|
||||
* - The image_writable_mask entry is true: the image is writable and assumed
|
||||
* to have been written by the AP.
|
||||
*
|
||||
* In other words, any valid image index within range is considered available.
|
||||
* Writable OTS-backed images require a successful write-complete. Read-only
|
||||
* preloaded images (writable_mask false / NULL) remain always available.
|
||||
*/
|
||||
static bool is_image_available(uint8_t image_index)
|
||||
{
|
||||
@@ -121,10 +114,16 @@ static bool is_image_available(uint8_t image_index)
|
||||
if (image_index >= config->num_images) {
|
||||
return false;
|
||||
}
|
||||
/* All valid image indices are considered available:
|
||||
* - Read-only images (no writable mask, or mask[i] == false) are
|
||||
* static/pre-loaded and always available.
|
||||
* - Writable images (mask[i] == true) are assumed written by the AP. */
|
||||
|
||||
#if CONFIG_BLE_ESL_OTS_SUPPORT
|
||||
bool writable = true;
|
||||
if (config->image_writable_mask != NULL) {
|
||||
writable = config->image_writable_mask[image_index];
|
||||
}
|
||||
if (writable) {
|
||||
return ble_esl_image_is_complete(image_index);
|
||||
}
|
||||
#endif
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -161,6 +161,7 @@ static esp_err_t handle_ping(const uint8_t *params, uint8_t params_len,
|
||||
static esp_err_t handle_unassociate(const uint8_t *params, uint8_t params_len,
|
||||
ble_esl_cmd_result_t *result)
|
||||
{
|
||||
ESP_LOGI(TAG, "Unassociate ECP command received");
|
||||
uint16_t bitmap = 0;
|
||||
esp_err_t ret = esl_build_basic_state(&bitmap);
|
||||
if (ret != ESP_OK) {
|
||||
|
||||
@@ -93,6 +93,9 @@ typedef struct {
|
||||
ble_ots_obj_id_t ots_obj_ids[CONFIG_BLE_ESL_MAX_IMAGES]; /* reverse map */
|
||||
uint8_t ots_obj_count;
|
||||
bool ots_initialized; /* OTS server init state */
|
||||
bool image_complete[CONFIG_BLE_ESL_MAX_IMAGES];
|
||||
uint32_t image_length[CONFIG_BLE_ESL_MAX_IMAGES];
|
||||
SemaphoreHandle_t image_lock;
|
||||
#endif
|
||||
} esl_gatts_ctx_t;
|
||||
|
||||
@@ -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 */
|
||||
|
||||
|
||||
@@ -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 <stdint.h>
|
||||
#include <stdbool.h>
|
||||
#include <string.h>
|
||||
#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
|
||||
@@ -11,6 +11,7 @@
|
||||
|
||||
#include <string.h>
|
||||
#include <assert.h>
|
||||
#include <inttypes.h>
|
||||
|
||||
#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;
|
||||
}
|
||||
|
||||
@@ -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 <stdint.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#ifndef ESL_SYNC_HANDLE_NONE
|
||||
#define ESL_SYNC_HANDLE_NONE 0xffffu
|
||||
#endif
|
||||
|
||||
typedef enum {
|
||||
ESL_SYNC_LOST_CURRENT_NATURAL = 0,
|
||||
ESL_SYNC_LOST_RETIRING_LOCAL,
|
||||
ESL_SYNC_LOST_STALE,
|
||||
} esl_sync_lost_class_t;
|
||||
|
||||
typedef struct {
|
||||
uint16_t current_sync_handle;
|
||||
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
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user