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