mirror of
https://github.com/espressif/esp-idf.git
synced 2026-10-02 03:00:34 +03:00
feat(ble/nimble): add ESL persist, AP recovery, and image APIs
Enable Tag/AP reboot recovery and image restore, plus OTS DATA_WRITE. AP start only brings up PAwR; scan is explicit via restart_scan. Ported onto current master ESL/OTS APIs (ble_esl_state_t, ble_esl_key_material_t, ble_esl_address_t at the public boundary).
This commit is contained in:
@@ -19,6 +19,7 @@
|
|||||||
|
|
||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
#include <stdbool.h>
|
#include <stdbool.h>
|
||||||
|
#include <stddef.h>
|
||||||
#include "esp_err.h"
|
#include "esp_err.h"
|
||||||
#include "ble_esl_common.h"
|
#include "ble_esl_common.h"
|
||||||
|
|
||||||
@@ -318,6 +319,101 @@ esp_err_t ble_esl_set_service_needed(bool flag);
|
|||||||
esp_err_t ble_esl_report_sensor_data(uint8_t sensor_index, uint8_t error_code,
|
esp_err_t ble_esl_report_sensor_data(uint8_t sensor_index, uint8_t error_code,
|
||||||
const uint8_t *data, uint8_t data_len);
|
const uint8_t *data, uint8_t data_len);
|
||||||
|
|
||||||
|
#if CONFIG_BLE_ESL_OTS_SUPPORT
|
||||||
|
/**
|
||||||
|
* @brief Query whether an OTS image slot has a successfully completed write.
|
||||||
|
*
|
||||||
|
* @param[in] image_index Image index (0 .. num_images-1)
|
||||||
|
* @return true if a successful OTS write completed for this slot
|
||||||
|
*/
|
||||||
|
bool ble_esl_image_is_complete(uint8_t image_index);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Atomically snapshot a completed image into a caller buffer.
|
||||||
|
*
|
||||||
|
* Marks the slot incomplete while a concurrent write is in progress so that
|
||||||
|
* Display Image never observes a torn frame.
|
||||||
|
*
|
||||||
|
* @param[in] image_index Image index
|
||||||
|
* @param[out] dst Destination buffer
|
||||||
|
* @param[in] capacity Capacity of dst in bytes
|
||||||
|
* @param[out] out_len Actual copied length (may be NULL)
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* - ESP_OK on success
|
||||||
|
* - ESP_ERR_INVALID_ARG / ESP_ERR_INVALID_STATE / ESP_ERR_INVALID_SIZE
|
||||||
|
*/
|
||||||
|
esp_err_t ble_esl_image_snapshot(uint8_t image_index, uint8_t *dst, size_t capacity,
|
||||||
|
size_t *out_len);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Load image bytes into an OTS object and mark the slot complete.
|
||||||
|
*
|
||||||
|
* Intended for boot-time restore from persistent storage after ble_esl_init()
|
||||||
|
* and before ble_esl_start(). Does not emit BLE_ESL_EVT_IMAGE_WRITE.
|
||||||
|
*
|
||||||
|
* Uses ble_ots_server_set_object_data(), which may generate Object Changed
|
||||||
|
* indications; do not call while a BLE connection is active.
|
||||||
|
*
|
||||||
|
* @param[in] image_index Image index (0 .. num_images-1)
|
||||||
|
* @param[in] data Full image payload
|
||||||
|
* @param[in] len Payload length (1 .. CONFIG_BLE_ESL_MAX_IMAGE_SIZE)
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* - ESP_OK on success
|
||||||
|
* - ESP_ERR_INVALID_ARG / ESP_ERR_INVALID_STATE / ESP_FAIL
|
||||||
|
*/
|
||||||
|
esp_err_t ble_esl_image_restore(uint8_t image_index, const uint8_t *data, size_t len);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Mark all OTS image slots incomplete (RAM-side only).
|
||||||
|
*
|
||||||
|
* Used after Unassociate / Factory Reset so Display cannot read wiped images
|
||||||
|
* in the same boot before the flash erase worker finishes.
|
||||||
|
*/
|
||||||
|
void ble_esl_image_invalidate_all(void);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/* ========================== Persisted Association Snapshot ========================== */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief POD snapshot of TAG association credentials (no NimBLE private types).
|
||||||
|
*
|
||||||
|
* Absolute Time and PAwR sync handles are intentionally omitted.
|
||||||
|
*/
|
||||||
|
typedef struct {
|
||||||
|
ble_esl_address_t esl_address;
|
||||||
|
ble_esl_key_material_t ap_sync_key;
|
||||||
|
ble_esl_key_material_t resp_key;
|
||||||
|
uint8_t peer_addr_type; /*!< BLE address type of bonded AP */
|
||||||
|
uint8_t peer_addr[6]; /*!< Identity address of bonded AP (little-endian) */
|
||||||
|
} ble_esl_persisted_tag_t;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Export a complete association snapshot
|
||||||
|
*
|
||||||
|
* Succeeds only when ESL address, both key materials, bonded peer, and the
|
||||||
|
* three mandatory config bits (address/ap_sync/resp) are valid. Absolute Time
|
||||||
|
* is not required for export.
|
||||||
|
*
|
||||||
|
* @param[out] out Snapshot destination
|
||||||
|
* @return ESP_OK on success; ESP_ERR_INVALID_STATE when incomplete
|
||||||
|
*/
|
||||||
|
esp_err_t ble_esl_export_persisted_tag(ble_esl_persisted_tag_t *out);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Restore association into RAM after ble_esl_init(), before ble_esl_start()
|
||||||
|
*
|
||||||
|
* Performs bare field assignment (does NOT call esl_state_transition). Sets
|
||||||
|
* state to UNSYNCHRONIZED and config_complete to Address|AP Sync|Resp only
|
||||||
|
* (CONFIG_BIT_ABS_TIME cleared). Does not start advertising or timers —
|
||||||
|
* ble_esl_start() must follow to arm unsync_timer and connectable advertising.
|
||||||
|
*
|
||||||
|
* @param[in] info Valid association snapshot
|
||||||
|
* @return ESP_OK on success; ESP_ERR_INVALID_ARG / ESP_ERR_INVALID_STATE on failure
|
||||||
|
*/
|
||||||
|
esp_err_t ble_esl_restore_persisted_tag(const ble_esl_persisted_tag_t *info);
|
||||||
|
|
||||||
#ifdef __cplusplus
|
#ifdef __cplusplus
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -54,6 +54,8 @@ typedef enum {
|
|||||||
/* Command events (ap_command.c) */
|
/* Command events (ap_command.c) */
|
||||||
BLE_ESL_AP_EVT_RESPONSE, /*!< Command response received from ESL */
|
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_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;
|
} ble_esl_ap_evt_t;
|
||||||
|
|
||||||
/* ========================== Response Types ========================== */
|
/* ========================== Response Types ========================== */
|
||||||
@@ -117,7 +119,10 @@ typedef struct {
|
|||||||
int8_t rssi; /*!< Received signal strength (dBm) */
|
int8_t rssi; /*!< Received signal strength (dBm) */
|
||||||
const uint8_t *adv_data; /*!< Raw advertising data (valid during callback) */
|
const uint8_t *adv_data; /*!< Raw advertising data (valid during callback) */
|
||||||
uint16_t adv_data_len; /*!< Advertising data length in octets */
|
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;
|
} ble_esl_ap_scan_result_t;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -198,6 +203,14 @@ typedef struct {
|
|||||||
esp_err_t status; /*!< ESP_OK if all mandatory chars written */
|
esp_err_t status; /*!< ESP_OK if all mandatory chars written */
|
||||||
} ble_esl_ap_configured_t;
|
} 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)
|
* @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 */
|
uint8_t group_id; /*!< Group_ID of the timed-out ESL */
|
||||||
} esl_ap_cmd_timeout_t;
|
} 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 ========================== */
|
/* ========================== Command Parameter Structures ========================== */
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -342,16 +364,17 @@ esp_err_t ble_esl_ap_init(const ble_esl_ap_config_t *config);
|
|||||||
* stopped, the still connected links stay tracked and ESP_FAIL is
|
* stopped, the still connected links stay tracked and ESP_FAIL is
|
||||||
* returned; the caller may retry this function.
|
* 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_FAIL if an active connection could not be terminated
|
||||||
*/
|
*/
|
||||||
esp_err_t ble_esl_ap_deinit(void);
|
esp_err_t ble_esl_ap_deinit(void);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Start AP operation (scanning + PAwR broadcasting)
|
* @brief Start AP operation (PAwR broadcasting; scan idle until scan_start)
|
||||||
*
|
*
|
||||||
* Begins GAP General Discovery for ESLs and starts PAwR broadcasting.
|
* Starts PAwR broadcasting. GAP discovery is not started here; call
|
||||||
* Discovered ESLs are reported via BLE_ESL_AP_EVT_SCAN_RESULT.
|
* ble_esl_ap_start_scan() when the phone sends scan_start.
|
||||||
*
|
*
|
||||||
* @note This function does not automatically initiate connections. The caller must handle
|
* @note This function does not automatically initiate connections. The caller must handle
|
||||||
* scan results and call ble_esl_ap_connect() to establish connections.
|
* scan results and call ble_esl_ap_connect() to establish connections.
|
||||||
@@ -360,6 +383,27 @@ esp_err_t ble_esl_ap_deinit(void);
|
|||||||
*/
|
*/
|
||||||
esp_err_t ble_esl_ap_start(void);
|
esp_err_t ble_esl_ap_start(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Start GAP discovery if it is not already running
|
||||||
|
*
|
||||||
|
* Idempotent: if discovery is already active, returns ESP_OK without
|
||||||
|
* cancelling the current scan. Clears scan_suppressed so discovery can
|
||||||
|
* auto-resume after connections end. PAwR broadcasting is not affected.
|
||||||
|
*
|
||||||
|
* @return ESP_OK on success; ESP_ERR_INVALID_STATE if AP not started
|
||||||
|
*/
|
||||||
|
esp_err_t ble_esl_ap_start_scan(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Stop GAP discovery without affecting PAwR broadcasting
|
||||||
|
*
|
||||||
|
* Sets scan_suppressed so discovery is not auto-resumed after connections
|
||||||
|
* end. Call ble_esl_ap_start_scan() to scan again.
|
||||||
|
*
|
||||||
|
* @return ESP_OK on success; ESP_ERR_INVALID_STATE if AP not started
|
||||||
|
*/
|
||||||
|
esp_err_t ble_esl_ap_stop_scan(void);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Stop AP operation (scanning + PAwR broadcasting)
|
* @brief Stop AP operation (scanning + PAwR broadcasting)
|
||||||
*
|
*
|
||||||
@@ -425,6 +469,14 @@ esp_err_t ble_esl_ap_connect_synced(ble_esl_address_t esl_addr);
|
|||||||
*/
|
*/
|
||||||
esp_err_t ble_esl_ap_disconnect(uint16_t conn_handle);
|
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 ========================== */
|
/* ========================== Public APIs: Lifecycle ========================== */
|
||||||
/* Implemented in ble_esl_ap_lifecycle.c */
|
/* Implemented in ble_esl_ap_lifecycle.c */
|
||||||
|
|
||||||
@@ -477,6 +529,36 @@ esp_err_t ble_esl_ap_transfer_image(const ble_esl_ap_image_transfer_params_t *pa
|
|||||||
*/
|
*/
|
||||||
esp_err_t ble_esl_ap_synchronize(uint16_t conn_handle);
|
esp_err_t ble_esl_ap_synchronize(uint16_t conn_handle);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Return true while Update Complete → PAST → wait-for-disconnect is active
|
||||||
|
*/
|
||||||
|
bool ble_esl_ap_synchronize_in_progress(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Write ESL Current Absolute Time on an encrypted ACL link
|
||||||
|
*
|
||||||
|
* Uses the current boot monotonic clock (esp_timer_get_time()/1000).
|
||||||
|
* Completion is BLE_ESL_AP_EVT_ABS_TIME_WRITTEN (once, if this returns ESP_OK).
|
||||||
|
* Rejected while configure occupies the same connection.
|
||||||
|
*
|
||||||
|
* @param conn_handle Connection handle of the connected ESL
|
||||||
|
* @return ESP_OK if the GATT write was initiated
|
||||||
|
*/
|
||||||
|
esp_err_t ble_esl_ap_write_absolute_time(uint16_t conn_handle);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Persisted association used to seed AP tracking after reboot
|
||||||
|
*/
|
||||||
|
typedef struct __attribute__((packed)) {
|
||||||
|
uint16_t esl_addr;
|
||||||
|
uint8_t ble_addr[6];
|
||||||
|
uint8_t ble_addr_type;
|
||||||
|
ble_esl_key_material_t ap_sync_key;
|
||||||
|
ble_esl_key_material_t resp_key;
|
||||||
|
} ble_esl_ap_persisted_esl_t;
|
||||||
|
|
||||||
|
esp_err_t ble_esl_ap_restore_persisted_esl(const ble_esl_ap_persisted_esl_t *info);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Get the AP's tracked state for a specific ESL
|
* @brief Get the AP's tracked state for a specific ESL
|
||||||
*
|
*
|
||||||
@@ -493,20 +575,38 @@ ble_esl_state_t ble_esl_ap_get_esl_state(ble_esl_address_t esl_addr);
|
|||||||
/**
|
/**
|
||||||
* @brief Send Ping command (opcode 0x00)
|
* @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)
|
* @param group_id Group_ID (0x00–0x7F)
|
||||||
* @return ESP_OK on successful dispatch; error code on failure
|
* @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);
|
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)
|
* @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 group_id Group_ID (0x00–0x7F)
|
||||||
* @param display_index Index of the target display
|
* @param display_index Index of the target display
|
||||||
* @param image_index Index of the stored image to show
|
* @param image_index Index of the stored image to show
|
||||||
@@ -535,9 +635,11 @@ esp_err_t ble_esl_ap_display_timed_image(uint8_t esl_id, uint8_t group_id,
|
|||||||
/**
|
/**
|
||||||
* @brief Send Refresh Display command (opcode 0x11)
|
* @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 group_id Group_ID (0x00–0x7F)
|
||||||
* @param display_index Index of the display to refresh
|
* @param display_index Index of the display to refresh
|
||||||
* @return ESP_OK on successful dispatch; error code on failure
|
* @return ESP_OK on successful dispatch; error code on failure
|
||||||
@@ -549,9 +651,10 @@ esp_err_t ble_esl_ap_refresh_display(uint8_t esl_id, uint8_t group_id,
|
|||||||
* @brief Send LED Control command (opcode 0xB0)
|
* @brief Send LED Control command (opcode 0xB0)
|
||||||
*
|
*
|
||||||
* Immediately control LED color, brightness, and flashing pattern.
|
* 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 group_id Group_ID (0x00–0x7F)
|
||||||
* @param led_index Index of the target LED
|
* @param led_index Index of the target LED
|
||||||
* @param settings Pointer to LED control settings
|
* @param settings Pointer to LED control settings
|
||||||
@@ -593,12 +696,13 @@ esp_err_t ble_esl_ap_read_sensor(uint8_t esl_id, uint8_t group_id,
|
|||||||
uint8_t sensor_index);
|
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,
|
* Disassociates the ESL. Expected response: Basic State. On success,
|
||||||
* the ESL's tracked state transitions to Unassociated.
|
* 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)
|
* @param group_id Group_ID (0x00–0x7F)
|
||||||
* @return ESP_OK on successful dispatch; error code on failure
|
* @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,
|
static esp_err_t dispatch_command(uint8_t esl_id, uint8_t group_id,
|
||||||
uint8_t opcode, const uint8_t *params,
|
uint8_t opcode, const uint8_t *params,
|
||||||
uint8_t params_len, bool ecp_only);
|
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,
|
static esp_err_t build_led_params(uint8_t esl_id, uint8_t led_index,
|
||||||
const esl_ap_led_settings_t *settings,
|
const esl_ap_led_settings_t *settings,
|
||||||
uint8_t *out_params, uint8_t *out_len);
|
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) {
|
if (status != ESP_OK) {
|
||||||
ESP_LOGE(TAG, "ECP write/notification failed: status=0x%X, conn=0x%04X",
|
ESP_LOGE(TAG, "ECP write/notification failed: status=0x%X, conn=0x%04X",
|
||||||
status, conn_handle);
|
status, conn_handle);
|
||||||
|
uint8_t esl_id = ctx->esl_id;
|
||||||
|
uint8_t group_id = ctx->group_id;
|
||||||
free_ecp_ctx(ctx);
|
free_ecp_ctx(ctx);
|
||||||
xSemaphoreGive(s_ecp_ctx_mutex);
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -492,6 +506,13 @@ static void parse_response(uint8_t esl_id, uint8_t group_id,
|
|||||||
esl_id, response.basic_state.service_needed,
|
esl_id, response.basic_state.service_needed,
|
||||||
response.basic_state.synchronized);
|
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 */
|
/* For Unassociate command, transition ESL to Unassociated state */
|
||||||
if (cmd_opcode == BLE_ESL_CMD_UNASSOCIATE) {
|
if (cmd_opcode == BLE_ESL_CMD_UNASSOCIATE) {
|
||||||
uint16_t esl_addr = BLE_ESL_AP_MAKE_ADDR(esl_id, group_id);
|
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 ========================== */
|
/* ========================== 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 */
|
/* Unicast: determine transport based on ESL tracked state */
|
||||||
uint16_t esl_addr = BLE_ESL_AP_MAKE_ADDR(esl_id, group_id);
|
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) {
|
switch (state) {
|
||||||
case BLE_ESL_STATE_CONFIGURING:
|
case BLE_ESL_STATE_CONFIGURING:
|
||||||
case BLE_ESL_STATE_UPDATING: {
|
case BLE_ESL_STATE_UPDATING: {
|
||||||
/* ECP transport (connection-oriented) */
|
if (conn_handle == BLE_ESL_AP_CONN_HANDLE_INVALID) {
|
||||||
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) {
|
|
||||||
ESP_LOGE(TAG, "No active connection for ESL 0x%04X", esl_addr);
|
ESP_LOGE(TAG, "No active connection for ESL 0x%04X", esl_addr);
|
||||||
return ESP_ERR_INVALID_STATE;
|
return ESP_ERR_INVALID_STATE;
|
||||||
}
|
}
|
||||||
|
|
||||||
uint16_t conn_handle = esl->conn_handle;
|
|
||||||
|
|
||||||
xSemaphoreTake(s_ecp_ctx_mutex, portMAX_DELAY);
|
xSemaphoreTake(s_ecp_ctx_mutex, portMAX_DELAY);
|
||||||
|
|
||||||
/* Check if there's already a pending ECP command on this connection */
|
/* 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);
|
xSemaphoreGive(s_ecp_ctx_mutex);
|
||||||
|
|
||||||
/* Write the TLV to ECP characteristic */
|
/* 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,
|
ret = ble_esl_ap_ecp_write(conn_handle, tlv_buf, tlv_len,
|
||||||
ecp_response_cb, ctx);
|
ecp_response_cb, ctx);
|
||||||
if (ret != ESP_OK) {
|
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;
|
return ESP_ERR_INVALID_STATE;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* PAwR transport (connectionless) */
|
return pawr_send_unicast(esl_addr, esl_id, group_id, opcode, params, params_len);
|
||||||
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,
|
case BLE_ESL_STATE_UNSYNCHRONIZED: {
|
||||||
payload_buf, &payload_len);
|
/* PAwR unicast still works for tags on connectable ext adv after AP reboot */
|
||||||
if (ret != ESP_OK) {
|
if (ecp_only) {
|
||||||
ESP_LOGE(TAG, "Payload encode failed: 0x%X", ret);
|
ESP_LOGE(TAG, "Command 0x%02X is ECP-only, cannot send in Unsynchronized state",
|
||||||
return ret;
|
opcode);
|
||||||
|
return ESP_ERR_INVALID_STATE;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Store the command opcode for response correlation */
|
return pawr_send_unicast(esl_addr, esl_id, group_id, opcode, params, params_len);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
case BLE_ESL_STATE_UNASSOCIATED:
|
case BLE_ESL_STATE_UNASSOCIATED:
|
||||||
case BLE_ESL_STATE_UNSYNCHRONIZED:
|
|
||||||
default:
|
default:
|
||||||
ESP_LOGE(TAG, "ESL 0x%04X in state %d — cannot dispatch command",
|
ESP_LOGE(TAG, "ESL 0x%04X in state %d — cannot dispatch command",
|
||||||
esl_addr, state);
|
esl_addr, state);
|
||||||
@@ -1050,24 +1187,28 @@ void ble_esl_ap_command_handle_pawr_response(uint8_t group_id,
|
|||||||
* the correct ESL entry.
|
* the correct ESL entry.
|
||||||
*/
|
*/
|
||||||
uint16_t esl_addr = pawr_lookup_slot(group_id, response_slot);
|
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) {
|
if (esl_addr != ESL_ADDR_INVALID) {
|
||||||
esl = ble_esl_ap_find_esl(esl_addr);
|
ble_esl_ap_tracking_lock();
|
||||||
}
|
ble_esl_ap_esl_entry_t *esl = ble_esl_ap_find_esl(esl_addr);
|
||||||
|
if (esl == NULL) {
|
||||||
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",
|
ESP_LOGW(TAG, "PAwR response from unknown ESL: group=%u slot=%u",
|
||||||
group_id, response_slot);
|
group_id, response_slot);
|
||||||
return;
|
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 */
|
/* Decrypt and parse the PAwR response */
|
||||||
ble_esl_ap_parsed_response_t parsed;
|
ble_esl_ap_parsed_response_t parsed;
|
||||||
esp_err_t ret = ble_esl_ap_pawr_parse_response(esl_addr, response_slot,
|
esp_err_t ret = ble_esl_ap_pawr_parse_response(esl_addr, response_slot,
|
||||||
|
|||||||
@@ -11,15 +11,23 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
#include <assert.h>
|
||||||
#include "esp_log.h"
|
#include "esp_log.h"
|
||||||
#include "esp_err.h"
|
#include "esp_err.h"
|
||||||
|
|
||||||
|
#include "freertos/FreeRTOS.h"
|
||||||
|
#include "freertos/semphr.h"
|
||||||
|
#include "freertos/task.h"
|
||||||
|
|
||||||
#include "nimble/ble.h"
|
#include "nimble/ble.h"
|
||||||
#include "host/ble_hs.h"
|
#include "host/ble_hs.h"
|
||||||
#include "host/ble_gap.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.h"
|
||||||
#include "ble_esl_ap_int.h"
|
#include "ble_esl_ap_int.h"
|
||||||
|
#include "ble_esl_common.h"
|
||||||
|
|
||||||
static const char *TAG = "esl_ap_conn";
|
static const char *TAG = "esl_ap_conn";
|
||||||
|
|
||||||
@@ -33,6 +41,9 @@ static const char *TAG = "esl_ap_conn";
|
|||||||
|
|
||||||
ble_esl_ap_state_t *g_esl_ap = NULL;
|
ble_esl_ap_state_t *g_esl_ap = NULL;
|
||||||
|
|
||||||
|
static SemaphoreHandle_t s_tracking_mutex;
|
||||||
|
static TaskHandle_t s_tracking_holder;
|
||||||
|
|
||||||
/* ========================== Forward Declarations ========================== */
|
/* ========================== Forward Declarations ========================== */
|
||||||
|
|
||||||
static int ble_esl_ap_gap_event(struct ble_gap_event *event, void *arg);
|
static int ble_esl_ap_gap_event(struct ble_gap_event *event, void *arg);
|
||||||
@@ -42,8 +53,46 @@ static void handle_disconnect_event(struct ble_gap_event *event);
|
|||||||
static void handle_enc_change(struct ble_gap_event *event);
|
static void handle_enc_change(struct ble_gap_event *event);
|
||||||
static void handle_pairing_complete(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 void handle_notify_rx(struct ble_gap_event *event);
|
||||||
|
static esp_err_t ble_esl_ap_start_discovery(void);
|
||||||
|
static void ble_esl_ap_maybe_resume_discovery(void);
|
||||||
|
|
||||||
|
|
||||||
|
/* ========================== Tracking mutex ========================== */
|
||||||
|
|
||||||
|
void ble_esl_ap_tracking_lock(void)
|
||||||
|
{
|
||||||
|
assert(s_tracking_mutex != NULL);
|
||||||
|
assert(xTaskGetCurrentTaskHandle() != s_tracking_holder);
|
||||||
|
assert(!ble_esl_ap_pawr_held());
|
||||||
|
assert(!ble_esl_ap_lifecycle_held());
|
||||||
|
xSemaphoreTake(s_tracking_mutex, portMAX_DELAY);
|
||||||
|
s_tracking_holder = xTaskGetCurrentTaskHandle();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ble_esl_ap_tracking_unlock(void)
|
||||||
|
{
|
||||||
|
assert(s_tracking_mutex != NULL);
|
||||||
|
assert(s_tracking_holder == xTaskGetCurrentTaskHandle());
|
||||||
|
s_tracking_holder = NULL;
|
||||||
|
xSemaphoreGive(s_tracking_mutex);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ble_esl_ap_tracking_held(void)
|
||||||
|
{
|
||||||
|
return s_tracking_holder == xTaskGetCurrentTaskHandle();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ble_esl_ap_dispatch_state_evt(const ble_esl_ap_state_evt_snap_t *snap)
|
||||||
|
{
|
||||||
|
if (snap == NULL || !snap->pending) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (g_esl_ap != NULL && g_esl_ap->app_cb != NULL) {
|
||||||
|
ble_esl_ap_state_changed_t evt = snap->evt;
|
||||||
|
g_esl_ap->app_cb(BLE_ESL_AP_EVT_STATE_CHANGED, &evt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* ========================== Helper Functions ========================== */
|
/* ========================== Helper Functions ========================== */
|
||||||
|
|
||||||
ble_esl_ap_conn_t *ble_esl_ap_find_conn(uint16_t conn_handle)
|
ble_esl_ap_conn_t *ble_esl_ap_find_conn(uint16_t conn_handle)
|
||||||
@@ -88,6 +137,7 @@ void ble_esl_ap_free_conn(ble_esl_ap_conn_t *conn)
|
|||||||
|
|
||||||
ble_esl_ap_esl_entry_t *ble_esl_ap_find_esl(uint16_t esl_addr)
|
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) {
|
if (!g_esl_ap) {
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
@@ -103,6 +153,7 @@ ble_esl_ap_esl_entry_t *ble_esl_ap_find_esl(uint16_t esl_addr)
|
|||||||
ble_esl_ap_esl_entry_t *ble_esl_ap_find_esl_by_ble_addr(const uint8_t *addr,
|
ble_esl_ap_esl_entry_t *ble_esl_ap_find_esl_by_ble_addr(const uint8_t *addr,
|
||||||
uint8_t addr_type)
|
uint8_t addr_type)
|
||||||
{
|
{
|
||||||
|
assert(ble_esl_ap_tracking_held());
|
||||||
if (!g_esl_ap || !addr) {
|
if (!g_esl_ap || !addr) {
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
@@ -118,6 +169,7 @@ ble_esl_ap_esl_entry_t *ble_esl_ap_find_esl_by_ble_addr(const uint8_t *addr,
|
|||||||
|
|
||||||
ble_esl_ap_esl_entry_t *ble_esl_ap_alloc_esl(void)
|
ble_esl_ap_esl_entry_t *ble_esl_ap_alloc_esl(void)
|
||||||
{
|
{
|
||||||
|
assert(ble_esl_ap_tracking_held());
|
||||||
if (!g_esl_ap) {
|
if (!g_esl_ap) {
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
@@ -134,7 +186,10 @@ ble_esl_ap_esl_entry_t *ble_esl_ap_alloc_esl(void)
|
|||||||
|
|
||||||
bool ble_esl_ap_is_associated(const uint8_t *addr, uint8_t addr_type)
|
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 ========================== */
|
/* ========================== Public APIs ========================== */
|
||||||
@@ -163,6 +218,15 @@ esp_err_t ble_esl_ap_init(const ble_esl_ap_config_t *config)
|
|||||||
return ESP_ERR_NO_MEM;
|
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->app_cb = config->callback;
|
||||||
g_esl_ap->pawr_config = config->pawr_config;
|
g_esl_ap->pawr_config = config->pawr_config;
|
||||||
g_esl_ap->initialized = true;
|
g_esl_ap->initialized = true;
|
||||||
@@ -185,6 +249,8 @@ esp_err_t ble_esl_ap_init(const ble_esl_ap_config_t *config)
|
|||||||
esp_err_t ret = ble_esl_ap_lifecycle_init();
|
esp_err_t ret = ble_esl_ap_lifecycle_init();
|
||||||
if (ret != ESP_OK) {
|
if (ret != ESP_OK) {
|
||||||
ESP_LOGE(TAG, "Failed to init lifecycle sub-module: %s", esp_err_to_name(ret));
|
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);
|
free(g_esl_ap);
|
||||||
g_esl_ap = NULL;
|
g_esl_ap = NULL;
|
||||||
return ret;
|
return ret;
|
||||||
@@ -194,6 +260,8 @@ esp_err_t ble_esl_ap_init(const ble_esl_ap_config_t *config)
|
|||||||
if (ret != ESP_OK) {
|
if (ret != ESP_OK) {
|
||||||
ESP_LOGE(TAG, "Failed to init PAwR sub-module: %s", esp_err_to_name(ret));
|
ESP_LOGE(TAG, "Failed to init PAwR sub-module: %s", esp_err_to_name(ret));
|
||||||
ble_esl_ap_lifecycle_deinit();
|
ble_esl_ap_lifecycle_deinit();
|
||||||
|
vSemaphoreDelete(s_tracking_mutex);
|
||||||
|
s_tracking_mutex = NULL;
|
||||||
free(g_esl_ap);
|
free(g_esl_ap);
|
||||||
g_esl_ap = NULL;
|
g_esl_ap = NULL;
|
||||||
return ret;
|
return ret;
|
||||||
@@ -204,6 +272,8 @@ esp_err_t ble_esl_ap_init(const ble_esl_ap_config_t *config)
|
|||||||
ESP_LOGE(TAG, "Failed to init command sub-module: %s", esp_err_to_name(ret));
|
ESP_LOGE(TAG, "Failed to init command sub-module: %s", esp_err_to_name(ret));
|
||||||
ble_esl_ap_pawr_deinit();
|
ble_esl_ap_pawr_deinit();
|
||||||
ble_esl_ap_lifecycle_deinit();
|
ble_esl_ap_lifecycle_deinit();
|
||||||
|
vSemaphoreDelete(s_tracking_mutex);
|
||||||
|
s_tracking_mutex = NULL;
|
||||||
free(g_esl_ap);
|
free(g_esl_ap);
|
||||||
g_esl_ap = NULL;
|
g_esl_ap = NULL;
|
||||||
return ret;
|
return ret;
|
||||||
@@ -215,6 +285,8 @@ esp_err_t ble_esl_ap_init(const ble_esl_ap_config_t *config)
|
|||||||
ble_esl_ap_command_deinit();
|
ble_esl_ap_command_deinit();
|
||||||
ble_esl_ap_pawr_deinit();
|
ble_esl_ap_pawr_deinit();
|
||||||
ble_esl_ap_lifecycle_deinit();
|
ble_esl_ap_lifecycle_deinit();
|
||||||
|
vSemaphoreDelete(s_tracking_mutex);
|
||||||
|
s_tracking_mutex = NULL;
|
||||||
free(g_esl_ap);
|
free(g_esl_ap);
|
||||||
g_esl_ap = NULL;
|
g_esl_ap = NULL;
|
||||||
return ret;
|
return ret;
|
||||||
@@ -272,6 +344,11 @@ esp_err_t ble_esl_ap_deinit(void)
|
|||||||
return ESP_ERR_INVALID_STATE;
|
return ESP_ERR_INVALID_STATE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (ble_esl_ap_lifecycle_has_inflight()) {
|
||||||
|
ESP_LOGE(TAG, "deinit refused: configure or Absolute Time still in-flight");
|
||||||
|
return ESP_ERR_INVALID_STATE;
|
||||||
|
}
|
||||||
|
|
||||||
/* Stop scanning and PAwR if active */
|
/* Stop scanning and PAwR if active */
|
||||||
if (g_esl_ap->started) {
|
if (g_esl_ap->started) {
|
||||||
ble_esl_ap_stop();
|
ble_esl_ap_stop();
|
||||||
@@ -335,6 +412,12 @@ esp_err_t ble_esl_ap_deinit(void)
|
|||||||
g_esl_ap = NULL;
|
g_esl_ap = NULL;
|
||||||
free(ap);
|
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");
|
ESP_LOGI(TAG, "ESL AP deinitialized");
|
||||||
return ESP_OK;
|
return ESP_OK;
|
||||||
}
|
}
|
||||||
@@ -349,7 +432,22 @@ esp_err_t ble_esl_ap_start(void)
|
|||||||
return ESP_ERR_INVALID_STATE;
|
return ESP_ERR_INVALID_STATE;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Start GAP General Discovery (scanning) */
|
g_esl_ap->scan_suppressed = true;
|
||||||
|
|
||||||
|
/* Start PAwR broadcasting; GAP discovery waits for explicit scan_start */
|
||||||
|
esp_err_t ret = ble_esl_ap_pawr_start();
|
||||||
|
if (ret != ESP_OK) {
|
||||||
|
ESP_LOGE(TAG, "Failed to start PAwR: %s", esp_err_to_name(ret));
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
g_esl_ap->started = true;
|
||||||
|
ESP_LOGI(TAG, "ESL AP started (PAwR active, scan idle until scan_start)");
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
static esp_err_t ble_esl_ap_start_discovery(void)
|
||||||
|
{
|
||||||
uint8_t own_addr_type;
|
uint8_t own_addr_type;
|
||||||
int rc = ble_hs_id_infer_auto(0, &own_addr_type);
|
int rc = ble_hs_id_infer_auto(0, &own_addr_type);
|
||||||
if (rc != 0) {
|
if (rc != 0) {
|
||||||
@@ -357,10 +455,17 @@ esp_err_t ble_esl_ap_start(void)
|
|||||||
return ESP_FAIL;
|
return ESP_FAIL;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (ble_gap_disc_active()) {
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
struct ble_gap_disc_params disc_params = {0};
|
struct ble_gap_disc_params disc_params = {0};
|
||||||
disc_params.filter_duplicates = 1;
|
/* Disable duplicate filtering so an ONLINE TAG that reboots and resumes
|
||||||
disc_params.passive = 0; /* Active scan for General Discovery */
|
* Unsynchronized advertising is reported again in the same long-lived
|
||||||
disc_params.itvl = 0; /* Use defaults */
|
* 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.window = 0;
|
||||||
disc_params.filter_policy = 0;
|
disc_params.filter_policy = 0;
|
||||||
disc_params.limited = 0;
|
disc_params.limited = 0;
|
||||||
@@ -371,17 +476,49 @@ esp_err_t ble_esl_ap_start(void)
|
|||||||
ESP_LOGE(TAG, "Failed to start GAP discovery; rc=%d", rc);
|
ESP_LOGE(TAG, "Failed to start GAP discovery; rc=%d", rc);
|
||||||
return ESP_FAIL;
|
return ESP_FAIL;
|
||||||
}
|
}
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
/* Start PAwR broadcasting */
|
static void ble_esl_ap_maybe_resume_discovery(void)
|
||||||
esp_err_t ret = ble_esl_ap_pawr_start();
|
{
|
||||||
if (ret != ESP_OK) {
|
if (!g_esl_ap || !g_esl_ap->started || g_esl_ap->scan_suppressed) {
|
||||||
ESP_LOGE(TAG, "Failed to start PAwR: %s", esp_err_to_name(ret));
|
return;
|
||||||
ble_gap_disc_cancel();
|
}
|
||||||
return ret;
|
if (ble_gap_disc_active()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
(void)ble_esl_ap_start_discovery();
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_err_t ble_esl_ap_start_scan(void)
|
||||||
|
{
|
||||||
|
if (!g_esl_ap || !g_esl_ap->initialized || !g_esl_ap->started) {
|
||||||
|
return ESP_ERR_INVALID_STATE;
|
||||||
}
|
}
|
||||||
|
|
||||||
g_esl_ap->started = true;
|
g_esl_ap->scan_suppressed = false;
|
||||||
ESP_LOGI(TAG, "ESL AP started (scanning + PAwR)");
|
esp_err_t ret = ble_esl_ap_start_discovery();
|
||||||
|
if (ret == ESP_OK) {
|
||||||
|
ESP_LOGI(TAG, "GAP discovery started");
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_err_t ble_esl_ap_stop_scan(void)
|
||||||
|
{
|
||||||
|
if (!g_esl_ap || !g_esl_ap->initialized || !g_esl_ap->started) {
|
||||||
|
return ESP_ERR_INVALID_STATE;
|
||||||
|
}
|
||||||
|
|
||||||
|
g_esl_ap->scan_suppressed = true;
|
||||||
|
if (ble_gap_disc_active()) {
|
||||||
|
int rc = ble_gap_disc_cancel();
|
||||||
|
if (rc != 0 && rc != BLE_HS_EALREADY) {
|
||||||
|
ESP_LOGW(TAG, "Failed to cancel discovery; rc=%d", rc);
|
||||||
|
return ESP_FAIL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ESP_LOGI(TAG, "GAP discovery stopped (scan suppressed)");
|
||||||
return ESP_OK;
|
return ESP_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -464,14 +601,8 @@ esp_err_t ble_esl_ap_connect(const uint8_t *addr, uint8_t addr_type)
|
|||||||
ESP_LOGE(TAG, "Failed to initiate connection; rc=%d", rc);
|
ESP_LOGE(TAG, "Failed to initiate connection; rc=%d", rc);
|
||||||
ble_esl_ap_free_conn(conn);
|
ble_esl_ap_free_conn(conn);
|
||||||
|
|
||||||
/* Resume scanning if we were started */
|
/* Resume scanning if started */
|
||||||
if (g_esl_ap->started) {
|
ble_esl_ap_maybe_resume_discovery();
|
||||||
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);
|
|
||||||
}
|
|
||||||
return ESP_FAIL;
|
return ESP_FAIL;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -488,17 +619,29 @@ esp_err_t ble_esl_ap_connect_synced(ble_esl_address_t esl_addr)
|
|||||||
|
|
||||||
uint16_t addr_key = BLE_ESL_AP_ADDR_PACK(esl_addr);
|
uint16_t addr_key = BLE_ESL_AP_ADDR_PACK(esl_addr);
|
||||||
|
|
||||||
/* The ESL must be tracked and currently Synchronized to our PAwR train.
|
/* The ESL must be tracked. The tag side must still be on our PAwR train
|
||||||
* Only a Synchronized ESL is reachable via the Periodic Advertising
|
* (Synchronized at the ESL); AP runtime may be Unsynchronized after reboot
|
||||||
* Connection procedure; in other states use ble_esl_ap_connect(). */
|
* 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);
|
ble_esl_ap_esl_entry_t *esl = ble_esl_ap_find_esl(addr_key);
|
||||||
if (!esl) {
|
if (!esl) {
|
||||||
|
ble_esl_ap_tracking_unlock();
|
||||||
ESP_LOGE(TAG, "connect_synced: ESL 0x%04X not tracked", addr_key);
|
ESP_LOGE(TAG, "connect_synced: ESL 0x%04X not tracked", addr_key);
|
||||||
return ESP_ERR_NOT_FOUND;
|
return ESP_ERR_NOT_FOUND;
|
||||||
}
|
}
|
||||||
if (esl->state != BLE_ESL_STATE_SYNCHRONIZED) {
|
esl_state = esl->state;
|
||||||
ESP_LOGE(TAG, "connect_synced: ESL 0x%04X not Synchronized (state=%d)",
|
memcpy(ble_addr, esl->ble_addr, 6);
|
||||||
addr_key, esl->state);
|
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;
|
return ESP_ERR_INVALID_STATE;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -508,13 +651,13 @@ esp_err_t ble_esl_ap_connect_synced(ble_esl_address_t esl_addr)
|
|||||||
ESP_LOGE(TAG, "connect_synced: max connections reached");
|
ESP_LOGE(TAG, "connect_synced: max connections reached");
|
||||||
return ESP_ERR_NO_MEM;
|
return ESP_ERR_NO_MEM;
|
||||||
}
|
}
|
||||||
memcpy(conn->addr, esl->ble_addr, 6);
|
memcpy(conn->addr, ble_addr, 6);
|
||||||
conn->addr_type = esl->ble_addr_type;
|
conn->addr_type = ble_addr_type;
|
||||||
conn->esl_addr = addr_key;
|
conn->esl_addr = addr_key;
|
||||||
|
|
||||||
ble_addr_t peer_addr;
|
ble_addr_t peer_addr;
|
||||||
peer_addr.type = esl->ble_addr_type;
|
peer_addr.type = ble_addr_type;
|
||||||
memcpy(peer_addr.val, esl->ble_addr, 6);
|
memcpy(peer_addr.val, ble_addr, 6);
|
||||||
|
|
||||||
uint8_t own_addr_type;
|
uint8_t own_addr_type;
|
||||||
int rc = ble_hs_id_infer_auto(0, &own_addr_type);
|
int rc = ble_hs_id_infer_auto(0, &own_addr_type);
|
||||||
@@ -544,14 +687,8 @@ esp_err_t ble_esl_ap_connect_synced(ble_esl_address_t esl_addr)
|
|||||||
ESP_LOGE(TAG, "connect_synced: ble_gap_connect_with_synced failed; rc=%d", rc);
|
ESP_LOGE(TAG, "connect_synced: ble_gap_connect_with_synced failed; rc=%d", rc);
|
||||||
ble_esl_ap_free_conn(conn);
|
ble_esl_ap_free_conn(conn);
|
||||||
|
|
||||||
/* Resume scanning if we were started */
|
/* Resume scanning if started */
|
||||||
if (g_esl_ap->started && !ble_gap_disc_active()) {
|
ble_esl_ap_maybe_resume_discovery();
|
||||||
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);
|
|
||||||
}
|
|
||||||
return ESP_FAIL;
|
return ESP_FAIL;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -580,7 +717,23 @@ esp_err_t ble_esl_ap_disconnect(uint16_t conn_handle)
|
|||||||
return ESP_OK;
|
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)
|
static int ble_esl_ap_gap_event(struct ble_gap_event *event, void *arg)
|
||||||
{
|
{
|
||||||
@@ -638,6 +791,34 @@ static int ble_esl_ap_gap_event(struct ble_gap_event *event, void *arg)
|
|||||||
|
|
||||||
/* ========================== Scan Result Handling ========================== */
|
/* ========================== 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)
|
static void handle_scan_result(struct ble_gap_event *event)
|
||||||
{
|
{
|
||||||
if (!g_esl_ap || !g_esl_ap->app_cb) {
|
if (!g_esl_ap || !g_esl_ap->app_cb) {
|
||||||
@@ -648,6 +829,8 @@ static void handle_scan_result(struct ble_gap_event *event)
|
|||||||
int8_t rssi;
|
int8_t rssi;
|
||||||
const uint8_t *data;
|
const uint8_t *data;
|
||||||
uint8_t length_data;
|
uint8_t length_data;
|
||||||
|
bool is_connectable = false;
|
||||||
|
bool data_complete = true;
|
||||||
|
|
||||||
if (event->type == BLE_GAP_EVENT_EXT_DISC) {
|
if (event->type == BLE_GAP_EVENT_EXT_DISC) {
|
||||||
const struct ble_gap_ext_disc_desc *ext = &event->ext_disc;
|
const struct ble_gap_ext_disc_desc *ext = &event->ext_disc;
|
||||||
@@ -655,15 +838,24 @@ static void handle_scan_result(struct ble_gap_event *event)
|
|||||||
rssi = ext->rssi;
|
rssi = ext->rssi;
|
||||||
data = ext->data;
|
data = ext->data;
|
||||||
length_data = ext->length_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 {
|
} else {
|
||||||
const struct ble_gap_disc_desc *disc = &event->disc;
|
const struct ble_gap_disc_desc *disc = &event->disc;
|
||||||
addr = disc->addr;
|
addr = disc->addr;
|
||||||
rssi = disc->rssi;
|
rssi = disc->rssi;
|
||||||
data = disc->data;
|
data = disc->data;
|
||||||
length_data = disc->length_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 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;
|
ble_esl_ap_scan_result_t result;
|
||||||
memcpy(result.addr, addr.val, 6);
|
memcpy(result.addr, addr.val, 6);
|
||||||
@@ -672,6 +864,9 @@ static void handle_scan_result(struct ble_gap_event *event)
|
|||||||
result.adv_data = data;
|
result.adv_data = data;
|
||||||
result.adv_data_len = length_data;
|
result.adv_data_len = length_data;
|
||||||
result.is_associated = associated;
|
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);
|
g_esl_ap->app_cb(BLE_ESL_AP_EVT_SCAN_RESULT, &result);
|
||||||
}
|
}
|
||||||
@@ -706,16 +901,7 @@ static void handle_connect_event(struct ble_gap_event *event, void *arg)
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Resume scanning if started */
|
/* Resume scanning if started */
|
||||||
if (g_esl_ap->started && !ble_gap_disc_active()) {
|
ble_esl_ap_maybe_resume_discovery();
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -744,16 +930,7 @@ static void handle_connect_event(struct ble_gap_event *event, void *arg)
|
|||||||
ble_gap_terminate(conn_handle, BLE_ERR_REM_USER_CONN_TERM);
|
ble_gap_terminate(conn_handle, BLE_ERR_REM_USER_CONN_TERM);
|
||||||
|
|
||||||
/* Resume scanning if started */
|
/* Resume scanning if started */
|
||||||
if (g_esl_ap->started && !ble_gap_disc_active()) {
|
ble_esl_ap_maybe_resume_discovery();
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -763,16 +940,7 @@ static void handle_connect_event(struct ble_gap_event *event, void *arg)
|
|||||||
ble_gap_terminate(conn_handle, BLE_ERR_REM_USER_CONN_TERM);
|
ble_gap_terminate(conn_handle, BLE_ERR_REM_USER_CONN_TERM);
|
||||||
|
|
||||||
/* Resume scanning if started */
|
/* Resume scanning if started */
|
||||||
if (g_esl_ap->started && !ble_gap_disc_active()) {
|
ble_esl_ap_maybe_resume_discovery();
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -794,16 +962,7 @@ static void handle_connect_event(struct ble_gap_event *event, void *arg)
|
|||||||
ESP_LOGI(TAG, "Initiate encryption; conn_handle=%u", conn_handle);
|
ESP_LOGI(TAG, "Initiate encryption; conn_handle=%u", conn_handle);
|
||||||
|
|
||||||
/* Resume scanning if started */
|
/* Resume scanning if started */
|
||||||
if (g_esl_ap->started && !ble_gap_disc_active()) {
|
ble_esl_ap_maybe_resume_discovery();
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ========================== Disconnect Event Handling ========================== */
|
/* ========================== Disconnect Event Handling ========================== */
|
||||||
@@ -850,21 +1009,13 @@ static void handle_disconnect_event(struct ble_gap_event *event)
|
|||||||
/* Let lifecycle module complete any pending synchronize procedure
|
/* Let lifecycle module complete any pending synchronize procedure
|
||||||
* and handle link-loss state transitions */
|
* and handle link-loss state transitions */
|
||||||
ble_esl_ap_lifecycle_handle_disconnect(conn_handle);
|
ble_esl_ap_lifecycle_handle_disconnect(conn_handle);
|
||||||
|
ble_esl_ap_lifecycle_disconnect_check(conn);
|
||||||
|
|
||||||
/* Free connection slot */
|
/* Free connection slot */
|
||||||
ble_esl_ap_free_conn(conn);
|
ble_esl_ap_free_conn(conn);
|
||||||
|
|
||||||
/* Resume scanning if started */
|
/* Resume scanning if started */
|
||||||
if (g_esl_ap->started && !ble_gap_disc_active()) {
|
ble_esl_ap_maybe_resume_discovery();
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ======================================================================== */
|
/* ======================================================================== */
|
||||||
@@ -893,6 +1044,8 @@ static void handle_enc_change(struct ble_gap_event *event)
|
|||||||
* Advertising Connection procedure) — the ESL has entered the Updating
|
* Advertising Connection procedure) — the ESL has entered the Updating
|
||||||
* state. Track it so ble_esl_ap_configure()/synchronize() see a valid state
|
* state. Track it so ble_esl_ap_configure()/synchronize() see a valid state
|
||||||
* and link-loss recovery routes it back to Unsynchronized. */
|
* 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,
|
ble_esl_ap_esl_entry_t *esl = ble_esl_ap_find_esl_by_ble_addr(conn->addr,
|
||||||
conn->addr_type);
|
conn->addr_type);
|
||||||
if (esl != NULL &&
|
if (esl != NULL &&
|
||||||
@@ -900,8 +1053,12 @@ static void handle_enc_change(struct ble_gap_event *event)
|
|||||||
esl->state == BLE_ESL_STATE_UNSYNCHRONIZED)) {
|
esl->state == BLE_ESL_STATE_UNSYNCHRONIZED)) {
|
||||||
esl->conn_handle = conn_handle;
|
esl->conn_handle = conn_handle;
|
||||||
conn->esl_addr = esl->esl_addr;
|
conn->esl_addr = esl->esl_addr;
|
||||||
ble_esl_ap_update_esl_state(esl->esl_addr, BLE_ESL_STATE_UPDATING);
|
(void)ble_esl_ap_update_esl_state_locked(esl->esl_addr,
|
||||||
|
BLE_ESL_STATE_UPDATING,
|
||||||
|
&snap);
|
||||||
}
|
}
|
||||||
|
ble_esl_ap_tracking_unlock();
|
||||||
|
ble_esl_ap_dispatch_state_evt(&snap);
|
||||||
|
|
||||||
if (conn->disc_done) {
|
if (conn->disc_done) {
|
||||||
ESP_LOGD(TAG, "Encryption change on already-discovered conn_handle=%u; skipping discovery",
|
ESP_LOGD(TAG, "Encryption change on already-discovered conn_handle=%u; skipping discovery",
|
||||||
|
|||||||
@@ -16,8 +16,12 @@
|
|||||||
|
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
|
#include <assert.h>
|
||||||
|
#include <stddef.h>
|
||||||
|
|
||||||
#include "freertos/FreeRTOS.h"
|
#include "freertos/FreeRTOS.h"
|
||||||
|
#include "freertos/semphr.h"
|
||||||
|
#include "freertos/task.h"
|
||||||
#include "esp_log.h"
|
#include "esp_log.h"
|
||||||
#include "esp_timer.h"
|
#include "esp_timer.h"
|
||||||
#include "host/ble_gap.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,
|
_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");
|
"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 ========================== */
|
/* ========================== Internal Context Structures ========================== */
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -57,6 +65,7 @@ typedef struct {
|
|||||||
uint16_t conn_handle;
|
uint16_t conn_handle;
|
||||||
ble_esl_ap_esl_config_t config;
|
ble_esl_ap_esl_config_t config;
|
||||||
uint16_t esl_addr;
|
uint16_t esl_addr;
|
||||||
|
uint32_t lf_generation;
|
||||||
} configure_ctx_t;
|
} configure_ctx_t;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -117,6 +126,14 @@ static void configure_write_abs_time_cb(uint16_t conn_handle, esp_err_t status,
|
|||||||
const uint8_t *data, uint16_t data_len,
|
const uint8_t *data, uint16_t data_len,
|
||||||
void *user_data);
|
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 */
|
/* Read info chain callbacks */
|
||||||
static void read_info_display_cb(uint16_t conn_handle, esp_err_t status,
|
static void read_info_display_cb(uint16_t conn_handle, esp_err_t status,
|
||||||
const uint8_t *data, uint16_t data_len,
|
const uint8_t *data, uint16_t data_len,
|
||||||
@@ -159,12 +176,227 @@ static image_transfer_ctx_t *s_image_ctx = NULL;
|
|||||||
/** Active synchronize context (only one at a time; new requests are rejected while active) */
|
/** Active synchronize context (only one at a time; new requests are rejected while active) */
|
||||||
static synchronize_ctx_t *s_sync_ctx = NULL;
|
static synchronize_ctx_t *s_sync_ctx = NULL;
|
||||||
|
|
||||||
|
static SemaphoreHandle_t s_lifecycle_mutex;
|
||||||
|
static TaskHandle_t s_lifecycle_holder;
|
||||||
|
|
||||||
|
_Static_assert(sizeof(ble_esl_key_material_t) == 24, "sync key packed 24");
|
||||||
|
_Static_assert(sizeof(ble_esl_key_material_t) == 24, "resp key packed 24");
|
||||||
|
_Static_assert(offsetof(ble_esl_ap_persisted_esl_t, ap_sync_key) == 9, "flat key block start");
|
||||||
|
_Static_assert(offsetof(ble_esl_ap_persisted_esl_t, resp_key) == 9 + 24, "resp key follows sync");
|
||||||
|
|
||||||
|
static void lf_lock(void)
|
||||||
|
{
|
||||||
|
assert(s_lifecycle_mutex != NULL);
|
||||||
|
xSemaphoreTake(s_lifecycle_mutex, portMAX_DELAY);
|
||||||
|
s_lifecycle_holder = xTaskGetCurrentTaskHandle();
|
||||||
|
}
|
||||||
|
|
||||||
|
static void lf_unlock(void)
|
||||||
|
{
|
||||||
|
s_lifecycle_holder = NULL;
|
||||||
|
xSemaphoreGive(s_lifecycle_mutex);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ble_esl_ap_lifecycle_held(void)
|
||||||
|
{
|
||||||
|
return s_lifecycle_holder == xTaskGetCurrentTaskHandle();
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool lf_has_inflight_unlocked(void)
|
||||||
|
{
|
||||||
|
if (g_esl_ap == NULL) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (int i = 0; i < CONFIG_BLE_ESL_AP_MAX_CONNECTIONS; i++) {
|
||||||
|
if (g_esl_ap->conns[i].in_use && g_esl_ap->conns[i].lf_op != LF_OP_NONE) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ble_esl_ap_lifecycle_has_inflight(void)
|
||||||
|
{
|
||||||
|
if (s_lifecycle_mutex == NULL) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
lf_lock();
|
||||||
|
bool busy = lf_has_inflight_unlocked();
|
||||||
|
lf_unlock();
|
||||||
|
return busy;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ble_esl_ap_lifecycle_disconnect_check(ble_esl_ap_conn_t *conn)
|
||||||
|
{
|
||||||
|
if (conn == NULL || s_lifecycle_mutex == NULL) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
lf_lock();
|
||||||
|
if (conn->lf_op != LF_OP_NONE) {
|
||||||
|
ESP_LOGW(TAG, "disconnect: leftover lifecycle op=%u conn=%u (GATT should have completed)",
|
||||||
|
conn->lf_op, conn->conn_handle);
|
||||||
|
conn->lf_op = LF_OP_NONE;
|
||||||
|
conn->lf_generation++;
|
||||||
|
}
|
||||||
|
lf_unlock();
|
||||||
|
}
|
||||||
|
|
||||||
|
static esp_err_t lf_occupy(ble_esl_ap_conn_t *conn, uint8_t op)
|
||||||
|
{
|
||||||
|
assert(ble_esl_ap_lifecycle_held());
|
||||||
|
if (conn->lf_op != LF_OP_NONE) {
|
||||||
|
return ESP_ERR_INVALID_STATE;
|
||||||
|
}
|
||||||
|
conn->lf_generation++;
|
||||||
|
conn->lf_op = op;
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void lf_release(ble_esl_ap_conn_t *conn, uint32_t generation)
|
||||||
|
{
|
||||||
|
assert(ble_esl_ap_lifecycle_held());
|
||||||
|
if (conn != NULL && conn->lf_generation == generation) {
|
||||||
|
conn->lf_op = LF_OP_NONE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static esp_err_t write_abs_time_bytes(uint16_t conn_handle,
|
||||||
|
ble_esl_ap_gatt_cb_t cb, void *user_data)
|
||||||
|
{
|
||||||
|
int64_t now_us = esp_timer_get_time();
|
||||||
|
uint32_t abs_time_ms = (uint32_t)(now_us / 1000ULL);
|
||||||
|
uint8_t abs_time_data[4] = {
|
||||||
|
(uint8_t)(abs_time_ms & 0xFF),
|
||||||
|
(uint8_t)((abs_time_ms >> 8) & 0xFF),
|
||||||
|
(uint8_t)((abs_time_ms >> 16) & 0xFF),
|
||||||
|
(uint8_t)((abs_time_ms >> 24) & 0xFF),
|
||||||
|
};
|
||||||
|
return ble_esl_ap_gatt_write(conn_handle, BLE_ESL_CHR_UUID_CURRENT_ABS_TIME,
|
||||||
|
abs_time_data, sizeof(abs_time_data), cb, user_data);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void write_abs_time_gatt_cb(uint16_t conn_handle, esp_err_t status,
|
||||||
|
const uint8_t *data, uint16_t data_len,
|
||||||
|
void *user_data)
|
||||||
|
{
|
||||||
|
(void)data;
|
||||||
|
(void)data_len;
|
||||||
|
write_abs_time_ctx_t *ctx = (write_abs_time_ctx_t *)user_data;
|
||||||
|
if (ctx == NULL) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (g_esl_ap == NULL) {
|
||||||
|
free(ctx);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool emit = false;
|
||||||
|
ble_esl_ap_gatt_cb_t chained = ctx->chained_cb;
|
||||||
|
void *chained_ud = ctx->user_data;
|
||||||
|
|
||||||
|
lf_lock();
|
||||||
|
ble_esl_ap_conn_t *conn = ble_esl_ap_find_conn(conn_handle);
|
||||||
|
if (conn != NULL && conn->lf_generation == ctx->generation) {
|
||||||
|
if (ctx->public_event && conn->lf_op == LF_OP_ABS_TIME) {
|
||||||
|
conn->lf_op = LF_OP_NONE;
|
||||||
|
emit = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lf_unlock();
|
||||||
|
|
||||||
|
if (emit && g_esl_ap->app_cb != NULL) {
|
||||||
|
ble_esl_ap_abs_time_written_t evt = {
|
||||||
|
.conn_handle = conn_handle,
|
||||||
|
.status = status,
|
||||||
|
};
|
||||||
|
g_esl_ap->app_cb(BLE_ESL_AP_EVT_ABS_TIME_WRITTEN, &evt);
|
||||||
|
}
|
||||||
|
if (chained != NULL) {
|
||||||
|
chained(conn_handle, status, NULL, 0, chained_ud);
|
||||||
|
}
|
||||||
|
free(ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_err_t ble_esl_ap_write_absolute_time(uint16_t conn_handle)
|
||||||
|
{
|
||||||
|
if (g_esl_ap == NULL) {
|
||||||
|
return ESP_ERR_INVALID_STATE;
|
||||||
|
}
|
||||||
|
ble_esl_ap_conn_t *conn = ble_esl_ap_find_conn(conn_handle);
|
||||||
|
if (conn == NULL) {
|
||||||
|
return ESP_ERR_NOT_FOUND;
|
||||||
|
}
|
||||||
|
|
||||||
|
write_abs_time_ctx_t *ctx = calloc(1, sizeof(*ctx));
|
||||||
|
if (ctx == NULL) {
|
||||||
|
return ESP_ERR_NO_MEM;
|
||||||
|
}
|
||||||
|
|
||||||
|
lf_lock();
|
||||||
|
esp_err_t occ = lf_occupy(conn, LF_OP_ABS_TIME);
|
||||||
|
uint32_t gen = conn->lf_generation;
|
||||||
|
lf_unlock();
|
||||||
|
if (occ != ESP_OK) {
|
||||||
|
free(ctx);
|
||||||
|
return occ;
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx->conn_handle = conn_handle;
|
||||||
|
ctx->generation = gen;
|
||||||
|
ctx->public_event = true;
|
||||||
|
|
||||||
|
esp_err_t ret = write_abs_time_bytes(conn_handle, write_abs_time_gatt_cb, ctx);
|
||||||
|
if (ret != ESP_OK) {
|
||||||
|
lf_lock();
|
||||||
|
conn = ble_esl_ap_find_conn(conn_handle);
|
||||||
|
lf_release(conn, gen);
|
||||||
|
lf_unlock();
|
||||||
|
free(ctx);
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
static esp_err_t write_abs_time_for_configure(uint16_t conn_handle, uint32_t generation,
|
||||||
|
void *configure_ctx)
|
||||||
|
{
|
||||||
|
write_abs_time_ctx_t *ctx = calloc(1, sizeof(*ctx));
|
||||||
|
if (ctx == NULL) {
|
||||||
|
return ESP_ERR_NO_MEM;
|
||||||
|
}
|
||||||
|
ctx->conn_handle = conn_handle;
|
||||||
|
ctx->generation = generation;
|
||||||
|
ctx->public_event = false;
|
||||||
|
ctx->chained_cb = configure_write_abs_time_cb;
|
||||||
|
ctx->user_data = configure_ctx;
|
||||||
|
esp_err_t ret = write_abs_time_bytes(conn_handle, write_abs_time_gatt_cb, ctx);
|
||||||
|
if (ret != ESP_OK) {
|
||||||
|
free(ctx);
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void configure_release_slot(uint16_t conn_handle, uint32_t generation)
|
||||||
|
{
|
||||||
|
lf_lock();
|
||||||
|
ble_esl_ap_conn_t *conn = ble_esl_ap_find_conn(conn_handle);
|
||||||
|
lf_release(conn, generation);
|
||||||
|
lf_unlock();
|
||||||
|
}
|
||||||
|
|
||||||
/* ========================== Lifecycle Init / Deinit ========================== */
|
/* ========================== Lifecycle Init / Deinit ========================== */
|
||||||
|
|
||||||
esp_err_t ble_esl_ap_lifecycle_init(void)
|
esp_err_t ble_esl_ap_lifecycle_init(void)
|
||||||
{
|
{
|
||||||
assert(g_esl_ap != NULL);
|
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 = {
|
esp_timer_create_args_t timer_args = {
|
||||||
.callback = lifecycle_timeout_cb,
|
.callback = lifecycle_timeout_cb,
|
||||||
.arg = NULL,
|
.arg = NULL,
|
||||||
@@ -176,6 +408,8 @@ esp_err_t ble_esl_ap_lifecycle_init(void)
|
|||||||
esp_err_t ret = esp_timer_create(&timer_args, &g_esl_ap->timeout_timer);
|
esp_err_t ret = esp_timer_create(&timer_args, &g_esl_ap->timeout_timer);
|
||||||
if (ret != ESP_OK) {
|
if (ret != ESP_OK) {
|
||||||
ESP_LOGE(TAG, "Failed to create lifecycle timer: %s", esp_err_to_name(ret));
|
ESP_LOGE(TAG, "Failed to create lifecycle timer: %s", esp_err_to_name(ret));
|
||||||
|
vSemaphoreDelete(s_lifecycle_mutex);
|
||||||
|
s_lifecycle_mutex = NULL;
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -184,6 +418,8 @@ esp_err_t ble_esl_ap_lifecycle_init(void)
|
|||||||
ESP_LOGE(TAG, "Failed to start lifecycle timer: %s", esp_err_to_name(ret));
|
ESP_LOGE(TAG, "Failed to start lifecycle timer: %s", esp_err_to_name(ret));
|
||||||
esp_timer_delete(g_esl_ap->timeout_timer);
|
esp_timer_delete(g_esl_ap->timeout_timer);
|
||||||
g_esl_ap->timeout_timer = NULL;
|
g_esl_ap->timeout_timer = NULL;
|
||||||
|
vSemaphoreDelete(s_lifecycle_mutex);
|
||||||
|
s_lifecycle_mutex = NULL;
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -216,6 +452,12 @@ void ble_esl_ap_lifecycle_deinit(void)
|
|||||||
s_sync_ctx = NULL;
|
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");
|
ESP_LOGI(TAG, "Lifecycle sub-module deinitialized");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -237,7 +479,10 @@ static void lifecycle_timeout_cb(void *arg)
|
|||||||
}
|
}
|
||||||
|
|
||||||
int64_t now_us = esp_timer_get_time();
|
int64_t now_us = esp_timer_get_time();
|
||||||
|
ble_esl_ap_state_evt_snap_t snaps[CONFIG_BLE_ESL_AP_MAX_ESLS];
|
||||||
|
int snap_count = 0;
|
||||||
|
|
||||||
|
ble_esl_ap_tracking_lock();
|
||||||
for (int i = 0; i < CONFIG_BLE_ESL_AP_MAX_ESLS; i++) {
|
for (int i = 0; i < CONFIG_BLE_ESL_AP_MAX_ESLS; i++) {
|
||||||
ble_esl_ap_esl_entry_t *esl = &g_esl_ap->esls[i];
|
ble_esl_ap_esl_entry_t *esl = &g_esl_ap->esls[i];
|
||||||
if (!esl->in_use) {
|
if (!esl->in_use) {
|
||||||
@@ -250,8 +495,14 @@ static void lifecycle_timeout_cb(void *arg)
|
|||||||
(now_us - esl->last_sync_time_us) >= (int64_t)LIFECYCLE_TIMEOUT_US) {
|
(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",
|
ESP_LOGW(TAG, "ESL 0x%04X: sync timeout (60 min), transitioning to Unsynchronized",
|
||||||
esl->esl_addr);
|
esl->esl_addr);
|
||||||
ble_esl_ap_update_esl_state(esl->esl_addr,
|
if (snap_count < CONFIG_BLE_ESL_AP_MAX_ESLS) {
|
||||||
BLE_ESL_STATE_UNSYNCHRONIZED);
|
(void)ble_esl_ap_update_esl_state_locked(esl->esl_addr,
|
||||||
|
BLE_ESL_STATE_UNSYNCHRONIZED,
|
||||||
|
&snaps[snap_count]);
|
||||||
|
if (snaps[snap_count].pending) {
|
||||||
|
snap_count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else if (esl->state == BLE_ESL_STATE_UNSYNCHRONIZED) {
|
} else if (esl->state == BLE_ESL_STATE_UNSYNCHRONIZED) {
|
||||||
/* Check 60-minute reconnect timeout */
|
/* Check 60-minute reconnect timeout */
|
||||||
@@ -259,18 +510,35 @@ static void lifecycle_timeout_cb(void *arg)
|
|||||||
(now_us - esl->unsync_entry_time_us) >= (int64_t)LIFECYCLE_TIMEOUT_US) {
|
(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",
|
ESP_LOGW(TAG, "ESL 0x%04X: unsync timeout (60 min), transitioning to Unassociated",
|
||||||
esl->esl_addr);
|
esl->esl_addr);
|
||||||
ble_esl_ap_update_esl_state(esl->esl_addr,
|
if (snap_count < CONFIG_BLE_ESL_AP_MAX_ESLS) {
|
||||||
BLE_ESL_STATE_UNASSOCIATED);
|
(void)ble_esl_ap_update_esl_state_locked(esl->esl_addr,
|
||||||
|
BLE_ESL_STATE_UNASSOCIATED,
|
||||||
|
&snaps[snap_count]);
|
||||||
|
if (snaps[snap_count].pending) {
|
||||||
|
snap_count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
ble_esl_ap_tracking_unlock();
|
||||||
|
|
||||||
|
for (int i = 0; i < snap_count; i++) {
|
||||||
|
ble_esl_ap_dispatch_state_evt(&snaps[i]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ========================== Update ESL State ========================== */
|
/* ========================== Update ESL State ========================== */
|
||||||
|
|
||||||
esp_err_t ble_esl_ap_update_esl_state(uint16_t esl_addr,
|
esp_err_t ble_esl_ap_update_esl_state_locked(uint16_t esl_addr,
|
||||||
ble_esl_state_t new_state)
|
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) {
|
if (g_esl_ap == NULL) {
|
||||||
return ESP_ERR_INVALID_STATE;
|
return ESP_ERR_INVALID_STATE;
|
||||||
}
|
}
|
||||||
@@ -296,16 +564,12 @@ esp_err_t ble_esl_ap_update_esl_state(uint16_t esl_addr,
|
|||||||
esl->unsync_entry_time_us = 0;
|
esl->unsync_entry_time_us = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Fire state changed event to application */
|
if (snap != NULL) {
|
||||||
ble_esl_ap_state_changed_t evt = {
|
snap->pending = true;
|
||||||
.conn_handle = esl->conn_handle,
|
snap->evt.conn_handle = esl->conn_handle;
|
||||||
.esl_addr = ble_esl_ap_addr_unpack(esl_addr),
|
snap->evt.esl_addr = ble_esl_ap_addr_unpack(esl_addr);
|
||||||
.old_state = old_state,
|
snap->evt.old_state = old_state;
|
||||||
.new_state = new_state,
|
snap->evt.new_state = new_state;
|
||||||
};
|
|
||||||
|
|
||||||
if (g_esl_ap->app_cb != NULL) {
|
|
||||||
g_esl_ap->app_cb(BLE_ESL_AP_EVT_STATE_CHANGED, &evt);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Handle Unassociated cleanup */
|
/* Handle Unassociated cleanup */
|
||||||
@@ -319,6 +583,17 @@ esp_err_t ble_esl_ap_update_esl_state(uint16_t esl_addr,
|
|||||||
return ESP_OK;
|
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 ========================== */
|
/* ========================== Get ESL State ========================== */
|
||||||
|
|
||||||
ble_esl_state_t ble_esl_ap_get_esl_state(ble_esl_address_t esl_addr)
|
ble_esl_state_t ble_esl_ap_get_esl_state(ble_esl_address_t esl_addr)
|
||||||
@@ -327,12 +602,12 @@ ble_esl_state_t ble_esl_ap_get_esl_state(ble_esl_address_t esl_addr)
|
|||||||
return BLE_ESL_STATE_UNASSOCIATED;
|
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));
|
ble_esl_ap_esl_entry_t *esl = ble_esl_ap_find_esl(BLE_ESL_AP_ADDR_PACK(esl_addr));
|
||||||
if (esl == NULL) {
|
ble_esl_state_t state = (esl == NULL) ? BLE_ESL_STATE_UNASSOCIATED
|
||||||
return BLE_ESL_STATE_UNASSOCIATED;
|
: esl->state;
|
||||||
}
|
ble_esl_ap_tracking_unlock();
|
||||||
|
return state;
|
||||||
return esl->state;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ========================== Configure ========================== */
|
/* ========================== Configure ========================== */
|
||||||
@@ -373,14 +648,14 @@ esp_err_t ble_esl_ap_configure(uint16_t conn_handle,
|
|||||||
/* Store ESL address in connection context for cross-reference */
|
/* Store ESL address in connection context for cross-reference */
|
||||||
conn->esl_addr = ctx->esl_addr;
|
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);
|
ble_esl_ap_esl_entry_t *esl = ble_esl_ap_find_esl(ctx->esl_addr);
|
||||||
if (esl == NULL) {
|
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);
|
esl = ble_esl_ap_find_esl_by_ble_addr(conn->addr, conn->addr_type);
|
||||||
if (esl == NULL) {
|
if (esl == NULL) {
|
||||||
esl = ble_esl_ap_alloc_esl();
|
esl = ble_esl_ap_alloc_esl();
|
||||||
if (esl == NULL) {
|
if (esl == NULL) {
|
||||||
|
ble_esl_ap_tracking_unlock();
|
||||||
ESP_LOGE(TAG, "configure: ESL tracking table full");
|
ESP_LOGE(TAG, "configure: ESL tracking table full");
|
||||||
free(ctx);
|
free(ctx);
|
||||||
return ESP_ERR_NO_MEM;
|
return ESP_ERR_NO_MEM;
|
||||||
@@ -388,7 +663,6 @@ esp_err_t ble_esl_ap_configure(uint16_t conn_handle,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Initialize/update the tracking entry */
|
|
||||||
esl->in_use = true;
|
esl->in_use = true;
|
||||||
esl->esl_addr = ctx->esl_addr;
|
esl->esl_addr = ctx->esl_addr;
|
||||||
memcpy(esl->ble_addr, conn->addr, 6);
|
memcpy(esl->ble_addr, conn->addr, 6);
|
||||||
@@ -399,6 +673,17 @@ esp_err_t ble_esl_ap_configure(uint16_t conn_handle,
|
|||||||
|
|
||||||
/* Store key material in the ESL tracking entry */
|
/* Store key material in the ESL tracking entry */
|
||||||
esl->resp_key = config->resp_key;
|
esl->resp_key = config->resp_key;
|
||||||
|
ble_esl_ap_tracking_unlock();
|
||||||
|
|
||||||
|
lf_lock();
|
||||||
|
esp_err_t occ = lf_occupy(conn, LF_OP_CONFIGURE);
|
||||||
|
ctx->lf_generation = conn->lf_generation;
|
||||||
|
lf_unlock();
|
||||||
|
if (occ != ESP_OK) {
|
||||||
|
ble_esl_ap_update_esl_state(ctx->esl_addr, BLE_ESL_STATE_UNASSOCIATED);
|
||||||
|
free(ctx);
|
||||||
|
return occ;
|
||||||
|
}
|
||||||
|
|
||||||
/* Set PAwR sync key (shared across all ESLs) */
|
/* Set PAwR sync key (shared across all ESLs) */
|
||||||
ble_esl_ap_pawr_set_sync_key(&config->ap_sync_key);
|
ble_esl_ap_pawr_set_sync_key(&config->ap_sync_key);
|
||||||
@@ -423,6 +708,7 @@ esp_err_t ble_esl_ap_configure(uint16_t conn_handle,
|
|||||||
if (ret != ESP_OK) {
|
if (ret != ESP_OK) {
|
||||||
ESP_LOGE(TAG, "configure: failed to initiate ESL Address write: %s",
|
ESP_LOGE(TAG, "configure: failed to initiate ESL Address write: %s",
|
||||||
esp_err_to_name(ret));
|
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);
|
ble_esl_ap_update_esl_state(ctx->esl_addr, BLE_ESL_STATE_UNASSOCIATED);
|
||||||
free(ctx);
|
free(ctx);
|
||||||
return ret;
|
return ret;
|
||||||
@@ -461,6 +747,7 @@ static void configure_write_addr_cb(uint16_t conn_handle, esp_err_t status,
|
|||||||
|
|
||||||
fail:
|
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_update_esl_state(ctx->esl_addr, BLE_ESL_STATE_UNASSOCIATED);
|
||||||
ble_esl_ap_configured_t evt = {
|
ble_esl_ap_configured_t evt = {
|
||||||
.conn_handle = conn_handle,
|
.conn_handle = conn_handle,
|
||||||
@@ -501,6 +788,7 @@ static void configure_write_sync_key_cb(uint16_t conn_handle, esp_err_t status,
|
|||||||
|
|
||||||
fail:
|
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_update_esl_state(ctx->esl_addr, BLE_ESL_STATE_UNASSOCIATED);
|
||||||
ble_esl_ap_configured_t evt = {
|
ble_esl_ap_configured_t evt = {
|
||||||
.conn_handle = conn_handle,
|
.conn_handle = conn_handle,
|
||||||
@@ -527,19 +815,7 @@ static void configure_write_resp_key_cb(uint16_t conn_handle, esp_err_t status,
|
|||||||
|
|
||||||
ESP_LOGD(TAG, "configure: Response Key written, writing Absolute Time");
|
ESP_LOGD(TAG, "configure: Response Key written, writing Absolute Time");
|
||||||
|
|
||||||
/* Step 4: Write ESL Current Absolute Time (4 bytes from esp_timer_get_time) */
|
esp_err_t ret = write_abs_time_for_configure(conn_handle, ctx->lf_generation, ctx);
|
||||||
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);
|
|
||||||
if (ret != ESP_OK) {
|
if (ret != ESP_OK) {
|
||||||
ESP_LOGE(TAG, "configure: failed to initiate Absolute Time write: %s",
|
ESP_LOGE(TAG, "configure: failed to initiate Absolute Time write: %s",
|
||||||
esp_err_to_name(ret));
|
esp_err_to_name(ret));
|
||||||
@@ -549,6 +825,7 @@ static void configure_write_resp_key_cb(uint16_t conn_handle, esp_err_t status,
|
|||||||
|
|
||||||
fail:
|
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_update_esl_state(ctx->esl_addr, BLE_ESL_STATE_UNASSOCIATED);
|
||||||
ble_esl_ap_configured_t evt = {
|
ble_esl_ap_configured_t evt = {
|
||||||
.conn_handle = conn_handle,
|
.conn_handle = conn_handle,
|
||||||
@@ -576,12 +853,16 @@ static void configure_write_abs_time_cb(uint16_t conn_handle, esp_err_t status,
|
|||||||
ctx->esl_addr);
|
ctx->esl_addr);
|
||||||
|
|
||||||
/* Mark configuration as complete in the tracking entry */
|
/* 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);
|
ble_esl_ap_esl_entry_t *esl = ble_esl_ap_find_esl(ctx->esl_addr);
|
||||||
if (esl != NULL) {
|
if (esl != NULL) {
|
||||||
esl->config_complete = true;
|
esl->config_complete = true;
|
||||||
}
|
}
|
||||||
|
ble_esl_ap_tracking_unlock();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
configure_release_slot(conn_handle, ctx->lf_generation);
|
||||||
|
|
||||||
/* Fire BLE_ESL_AP_EVT_CONFIGURED */
|
/* Fire BLE_ESL_AP_EVT_CONFIGURED */
|
||||||
ble_esl_ap_configured_t evt = {
|
ble_esl_ap_configured_t evt = {
|
||||||
.conn_handle = conn_handle,
|
.conn_handle = conn_handle,
|
||||||
@@ -1129,6 +1410,11 @@ void ble_esl_ap_lifecycle_handle_ots_event(uint16_t conn_id,
|
|||||||
|
|
||||||
/* ========================== Synchronize ========================== */
|
/* ========================== 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)
|
esp_err_t ble_esl_ap_synchronize(uint16_t conn_handle)
|
||||||
{
|
{
|
||||||
if (g_esl_ap == NULL) {
|
if (g_esl_ap == NULL) {
|
||||||
@@ -1142,12 +1428,15 @@ esp_err_t ble_esl_ap_synchronize(uint16_t conn_handle)
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Need a valid ESL address to proceed */
|
/* 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);
|
ble_esl_ap_esl_entry_t *esl = ble_esl_ap_find_esl(conn->esl_addr);
|
||||||
if (esl == NULL) {
|
if (esl == NULL) {
|
||||||
|
ble_esl_ap_tracking_unlock();
|
||||||
ESP_LOGE(TAG, "synchronize: ESL address 0x%04X not tracked for conn 0x%04X",
|
ESP_LOGE(TAG, "synchronize: ESL address 0x%04X not tracked for conn 0x%04X",
|
||||||
conn->esl_addr, conn_handle);
|
conn->esl_addr, conn_handle);
|
||||||
return ESP_ERR_INVALID_STATE;
|
return ESP_ERR_INVALID_STATE;
|
||||||
}
|
}
|
||||||
|
ble_esl_ap_tracking_unlock();
|
||||||
|
|
||||||
/* Only one synchronize procedure at a time */
|
/* Only one synchronize procedure at a time */
|
||||||
if (s_sync_ctx != NULL) {
|
if (s_sync_ctx != NULL) {
|
||||||
@@ -1296,14 +1585,17 @@ void ble_esl_ap_lifecycle_handle_disconnect(uint16_t conn_handle)
|
|||||||
bool is_sync_disconnect = (s_sync_ctx != NULL &&
|
bool is_sync_disconnect = (s_sync_ctx != NULL &&
|
||||||
s_sync_ctx->conn_handle == conn_handle);
|
s_sync_ctx->conn_handle == conn_handle);
|
||||||
|
|
||||||
|
ble_esl_ap_state_evt_snap_t snaps[CONFIG_BLE_ESL_AP_MAX_ESLS];
|
||||||
|
int snap_count = 0;
|
||||||
|
|
||||||
if (g_esl_ap != NULL) {
|
if (g_esl_ap != NULL) {
|
||||||
|
ble_esl_ap_tracking_lock();
|
||||||
for (int i = 0; i < CONFIG_BLE_ESL_AP_MAX_ESLS; i++) {
|
for (int i = 0; i < CONFIG_BLE_ESL_AP_MAX_ESLS; i++) {
|
||||||
ble_esl_ap_esl_entry_t *esl = &g_esl_ap->esls[i];
|
ble_esl_ap_esl_entry_t *esl = &g_esl_ap->esls[i];
|
||||||
if (!esl->in_use || esl->conn_handle != conn_handle) {
|
if (!esl->in_use || esl->conn_handle != conn_handle) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Clear the connection handle */
|
|
||||||
esl->conn_handle = BLE_ESL_AP_CONN_HANDLE_INVALID;
|
esl->conn_handle = BLE_ESL_AP_CONN_HANDLE_INVALID;
|
||||||
|
|
||||||
if (!is_sync_disconnect) {
|
if (!is_sync_disconnect) {
|
||||||
@@ -1311,23 +1603,45 @@ void ble_esl_ap_lifecycle_handle_disconnect(uint16_t conn_handle)
|
|||||||
if (esl->config_complete) {
|
if (esl->config_complete) {
|
||||||
ESP_LOGI(TAG, "link-loss in Configuring (config complete) for ESL 0x%04X, "
|
ESP_LOGI(TAG, "link-loss in Configuring (config complete) for ESL 0x%04X, "
|
||||||
"transitioning to Unsynchronized", esl->esl_addr);
|
"transitioning to Unsynchronized", esl->esl_addr);
|
||||||
ble_esl_ap_update_esl_state(esl->esl_addr,
|
if (snap_count < CONFIG_BLE_ESL_AP_MAX_ESLS) {
|
||||||
BLE_ESL_STATE_UNSYNCHRONIZED);
|
(void)ble_esl_ap_update_esl_state_locked(esl->esl_addr,
|
||||||
|
BLE_ESL_STATE_UNSYNCHRONIZED,
|
||||||
|
&snaps[snap_count]);
|
||||||
|
if (snaps[snap_count].pending) {
|
||||||
|
snap_count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
ESP_LOGI(TAG, "link-loss in Configuring (config incomplete) for ESL 0x%04X, "
|
ESP_LOGI(TAG, "link-loss in Configuring (config incomplete) for ESL 0x%04X, "
|
||||||
"transitioning to Unassociated", esl->esl_addr);
|
"transitioning to Unassociated", esl->esl_addr);
|
||||||
ble_esl_ap_update_esl_state(esl->esl_addr,
|
if (snap_count < CONFIG_BLE_ESL_AP_MAX_ESLS) {
|
||||||
BLE_ESL_STATE_UNASSOCIATED);
|
(void)ble_esl_ap_update_esl_state_locked(esl->esl_addr,
|
||||||
|
BLE_ESL_STATE_UNASSOCIATED,
|
||||||
|
&snaps[snap_count]);
|
||||||
|
if (snaps[snap_count].pending) {
|
||||||
|
snap_count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else if (esl->state == BLE_ESL_STATE_UPDATING) {
|
} else if (esl->state == BLE_ESL_STATE_UPDATING) {
|
||||||
ESP_LOGI(TAG, "link-loss in Updating for ESL 0x%04X, "
|
ESP_LOGI(TAG, "link-loss in Updating for ESL 0x%04X, "
|
||||||
"transitioning to Unsynchronized", esl->esl_addr);
|
"transitioning to Unsynchronized", esl->esl_addr);
|
||||||
ble_esl_ap_update_esl_state(esl->esl_addr,
|
if (snap_count < CONFIG_BLE_ESL_AP_MAX_ESLS) {
|
||||||
BLE_ESL_STATE_UNSYNCHRONIZED);
|
(void)ble_esl_ap_update_esl_state_locked(esl->esl_addr,
|
||||||
|
BLE_ESL_STATE_UNSYNCHRONIZED,
|
||||||
|
&snaps[snap_count]);
|
||||||
|
if (snaps[snap_count].pending) {
|
||||||
|
snap_count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
ble_esl_ap_tracking_unlock();
|
||||||
|
for (int i = 0; i < snap_count; i++) {
|
||||||
|
ble_esl_ap_dispatch_state_evt(&snaps[i]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Step 2: Handle synchronize procedure completion if this disconnect
|
/* Step 2: Handle synchronize procedure completion if this disconnect
|
||||||
@@ -1387,3 +1701,67 @@ void ble_esl_ap_lifecycle_handle_disconnect(uint16_t conn_handle)
|
|||||||
s_image_ctx = NULL;
|
s_image_ctx = NULL;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ========================== Persisted ESL Restore ========================== */
|
||||||
|
|
||||||
|
esp_err_t ble_esl_ap_restore_persisted_esl(const ble_esl_ap_persisted_esl_t *info)
|
||||||
|
{
|
||||||
|
if (g_esl_ap == NULL || !g_esl_ap->initialized || info == NULL) {
|
||||||
|
return ESP_ERR_INVALID_ARG;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (info->esl_addr == 0) {
|
||||||
|
ESP_LOGI(TAG, "restore: esl_addr=0x0000 (esl_id=0 group_id=0)");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (BLE_ESL_AP_ADDR_ESL_ID(info->esl_addr) == BLE_ESL_BROADCAST_ADDRESS) {
|
||||||
|
ESP_LOGE(TAG, "restore: broadcast esl_id not allowed");
|
||||||
|
return ESP_ERR_INVALID_ARG;
|
||||||
|
}
|
||||||
|
|
||||||
|
ble_esl_ap_tracking_lock();
|
||||||
|
ble_esl_ap_esl_entry_t *esl = ble_esl_ap_find_esl(info->esl_addr);
|
||||||
|
if (esl == NULL) {
|
||||||
|
esl = ble_esl_ap_find_esl_by_ble_addr(info->ble_addr, info->ble_addr_type);
|
||||||
|
}
|
||||||
|
if (esl == NULL) {
|
||||||
|
esl = ble_esl_ap_alloc_esl();
|
||||||
|
}
|
||||||
|
if (esl == NULL) {
|
||||||
|
ble_esl_ap_tracking_unlock();
|
||||||
|
ESP_LOGE(TAG, "restore: ESL tracking table full");
|
||||||
|
return ESP_ERR_NO_MEM;
|
||||||
|
}
|
||||||
|
|
||||||
|
esl->in_use = true;
|
||||||
|
esl->esl_addr = info->esl_addr;
|
||||||
|
memcpy(esl->ble_addr, info->ble_addr, 6);
|
||||||
|
esl->ble_addr_type = info->ble_addr_type;
|
||||||
|
esl->conn_handle = BLE_ESL_AP_CONN_HANDLE_INVALID;
|
||||||
|
esl->config_complete = true;
|
||||||
|
esl->pending_pawr_cmd_opcode = 0;
|
||||||
|
esl->resp_key = info->resp_key;
|
||||||
|
esl->last_sync_time_us = 0;
|
||||||
|
ble_esl_ap_tracking_unlock();
|
||||||
|
|
||||||
|
ble_esl_ap_pawr_set_sync_key(&info->ap_sync_key);
|
||||||
|
|
||||||
|
esp_err_t ret = ble_esl_ap_pawr_set_response_key(info->esl_addr,
|
||||||
|
&info->resp_key);
|
||||||
|
if (ret != ESP_OK) {
|
||||||
|
ESP_LOGW(TAG, "restore: set response key for 0x%04X failed: %s",
|
||||||
|
info->esl_addr, esp_err_to_name(ret));
|
||||||
|
}
|
||||||
|
|
||||||
|
ret = ble_esl_ap_update_esl_state(info->esl_addr,
|
||||||
|
BLE_ESL_STATE_UNSYNCHRONIZED);
|
||||||
|
if (ret != ESP_OK) {
|
||||||
|
ESP_LOGE(TAG, "restore: failed to mark ESL 0x%04X unsynchronized: %s",
|
||||||
|
info->esl_addr, esp_err_to_name(ret));
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
ESP_LOGI(TAG, "Restored ESL 0x%04X from persistence (awaiting resync)",
|
||||||
|
info->esl_addr);
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
|||||||
@@ -22,6 +22,7 @@
|
|||||||
|
|
||||||
#include "freertos/FreeRTOS.h"
|
#include "freertos/FreeRTOS.h"
|
||||||
#include "freertos/semphr.h"
|
#include "freertos/semphr.h"
|
||||||
|
#include "freertos/task.h"
|
||||||
|
|
||||||
#include "nimble/ble.h"
|
#include "nimble/ble.h"
|
||||||
#include "host/ble_hs.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) */
|
/** Mutex protecting the per-subevent pending TX buffers (g_esl_ap->pawr_pending) */
|
||||||
static SemaphoreHandle_t s_pawr_mutex;
|
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 */
|
/** Advertising instance used for PAwR */
|
||||||
#define PAWR_ADV_INSTANCE 0
|
#define PAWR_ADV_INSTANCE 0
|
||||||
@@ -88,6 +108,7 @@ void ble_esl_ap_pawr_deinit(void)
|
|||||||
if (s_pawr_mutex != NULL) {
|
if (s_pawr_mutex != NULL) {
|
||||||
vSemaphoreDelete(s_pawr_mutex);
|
vSemaphoreDelete(s_pawr_mutex);
|
||||||
s_pawr_mutex = NULL;
|
s_pawr_mutex = NULL;
|
||||||
|
s_pawr_holder = NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
ESP_LOGI(TAG, "PAwR sub-module deinitialized");
|
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
|
/* Release pending TX buffers under the lock so the data-request callback
|
||||||
* (host task) never dereferences a freed pointer. */
|
* (host task) never dereferences a freed pointer. */
|
||||||
xSemaphoreTake(s_pawr_mutex, portMAX_DELAY);
|
pawr_lock();
|
||||||
g_esl_ap->pawr_active = false;
|
g_esl_ap->pawr_active = false;
|
||||||
free(g_esl_ap->pawr_pending);
|
free(g_esl_ap->pawr_pending);
|
||||||
g_esl_ap->pawr_pending = NULL;
|
g_esl_ap->pawr_pending = NULL;
|
||||||
xSemaphoreGive(s_pawr_mutex);
|
pawr_unlock();
|
||||||
|
|
||||||
ESP_LOGI(TAG, "PAwR broadcaster stopped");
|
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;
|
return ESP_ERR_INVALID_ARG;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ble_esl_ap_tracking_lock();
|
||||||
ble_esl_ap_esl_entry_t *entry = ble_esl_ap_find_esl(esl_addr);
|
ble_esl_ap_esl_entry_t *entry = ble_esl_ap_find_esl(esl_addr);
|
||||||
if (entry == NULL) {
|
if (entry == NULL) {
|
||||||
|
ble_esl_ap_tracking_unlock();
|
||||||
ESP_LOGE(TAG, "ESL 0x%04x not found for response key", esl_addr);
|
ESP_LOGE(TAG, "ESL 0x%04x not found for response key", esl_addr);
|
||||||
return ESP_ERR_NOT_FOUND;
|
return ESP_ERR_NOT_FOUND;
|
||||||
}
|
}
|
||||||
|
|
||||||
entry->resp_key = *key_mat;
|
entry->resp_key = *key_mat;
|
||||||
|
ble_esl_ap_tracking_unlock();
|
||||||
|
|
||||||
ESP_LOGI(TAG, "Response Key Material set for ESL 0x%04x", esl_addr);
|
ESP_LOGI(TAG, "Response Key Material set for ESL 0x%04x", esl_addr);
|
||||||
return ESP_OK;
|
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
|
/* The Randomizer is read and advanced under the lock: this function is a
|
||||||
* public command path and may be called from any application task. */
|
* 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,
|
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->ap_sync_key.iv,
|
||||||
g_esl_ap->randomizer,
|
g_esl_ap->randomizer,
|
||||||
plaintext,
|
plaintext,
|
||||||
plaintext_len,
|
plaintext_len,
|
||||||
encrypted_payload);
|
encrypted_payload);
|
||||||
xSemaphoreGive(s_pawr_mutex);
|
pawr_unlock();
|
||||||
if (enc_err != ESP_OK) {
|
if (enc_err != ESP_OK) {
|
||||||
ESP_LOGE(TAG, "EAD encrypt failed: 0x%x", enc_err);
|
ESP_LOGE(TAG, "EAD encrypt failed: 0x%x", enc_err);
|
||||||
return 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;
|
return ESP_ERR_INVALID_SIZE;
|
||||||
}
|
}
|
||||||
|
|
||||||
xSemaphoreTake(s_pawr_mutex, portMAX_DELAY);
|
pawr_lock();
|
||||||
if (g_esl_ap->pawr_pending == NULL) {
|
if (g_esl_ap->pawr_pending == NULL) {
|
||||||
xSemaphoreGive(s_pawr_mutex);
|
pawr_unlock();
|
||||||
ESP_LOGE(TAG, "PAwR pending buffers not allocated");
|
ESP_LOGE(TAG, "PAwR pending buffers not allocated");
|
||||||
return ESP_ERR_INVALID_STATE;
|
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->len = pos;
|
||||||
slot->repeats_left = PAWR_TX_REPEATS;
|
slot->repeats_left = PAWR_TX_REPEATS;
|
||||||
slot->valid = true;
|
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)",
|
ESP_LOGD(TAG, "PAwR sync packet queued for group %u (payload_len=%u, ad_len=%u)",
|
||||||
group_id, payload_len, pos);
|
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;
|
uint8_t enc_payload_len = outer_len - 1;
|
||||||
|
|
||||||
/* Step 1: Look up ESL entry by esl_addr */
|
/* 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);
|
ble_esl_ap_esl_entry_t *entry = ble_esl_ap_find_esl(esl_addr);
|
||||||
if (entry == NULL) {
|
if (entry == NULL) {
|
||||||
|
ble_esl_ap_tracking_unlock();
|
||||||
ESP_LOGE(TAG, "ESL 0x%04x not found for response decryption", esl_addr);
|
ESP_LOGE(TAG, "ESL 0x%04x not found for response decryption", esl_addr);
|
||||||
return ESP_ERR_NOT_FOUND;
|
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
|
/* Step 2: Decrypt and verify MIC via ble_esl_ead_decrypt
|
||||||
* Input: enc_payload = [Randomizer(5)] [Ciphertext(N)] [MIC(4)]
|
* 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 */
|
uint8_t plaintext[BLE_ESL_PAYLOAD_MAX_SIZE + 2]; /* inner AD max */
|
||||||
size_t decrypted_len = 0;
|
size_t decrypted_len = 0;
|
||||||
esp_err_t err = ble_esl_ead_decrypt(entry->resp_key.session_key,
|
esp_err_t err = ble_esl_ead_decrypt(resp_key,
|
||||||
entry->resp_key.iv,
|
resp_iv,
|
||||||
enc_payload,
|
enc_payload,
|
||||||
enc_payload_len,
|
enc_payload_len,
|
||||||
plaintext, sizeof(plaintext),
|
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[BLE_ESL_AP_PAWR_MAX_AD_BUF_SIZE];
|
||||||
uint8_t pkt_len = 0;
|
uint8_t pkt_len = 0;
|
||||||
|
|
||||||
xSemaphoreTake(s_pawr_mutex, portMAX_DELAY);
|
pawr_lock();
|
||||||
if (g_esl_ap->pawr_pending != NULL) {
|
if (g_esl_ap->pawr_pending != NULL) {
|
||||||
ble_esl_ap_pawr_pending_t *slot = &g_esl_ap->pawr_pending[sub];
|
ble_esl_ap_pawr_pending_t *slot = &g_esl_ap->pawr_pending[sub];
|
||||||
if (slot->valid && slot->len > 0) {
|
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) */
|
/* Allocate an mbuf (sized to the packet, or empty for idle subevents) */
|
||||||
struct os_mbuf *mbuf = os_msys_get_pkthdr(pkt_len, 0);
|
struct os_mbuf *mbuf = os_msys_get_pkthdr(pkt_len, 0);
|
||||||
|
|||||||
@@ -171,6 +171,10 @@ typedef struct {
|
|||||||
|
|
||||||
/* ESL tracking cross-reference */
|
/* ESL tracking cross-reference */
|
||||||
uint16_t esl_addr; /*!< Assigned ESL Address (valid after configure) */
|
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;
|
} ble_esl_ap_conn_t;
|
||||||
|
|
||||||
/* ========================== Per-ESL Tracking Entry ========================== */
|
/* ========================== Per-ESL Tracking Entry ========================== */
|
||||||
@@ -227,6 +231,7 @@ typedef struct {
|
|||||||
typedef struct {
|
typedef struct {
|
||||||
bool initialized; /*!< Module has been initialized */
|
bool initialized; /*!< Module has been initialized */
|
||||||
bool started; /*!< Scanning + PAwR broadcasting active */
|
bool started; /*!< Scanning + PAwR broadcasting active */
|
||||||
|
bool scan_suppressed; /*!< User/app requested scan stop; do not auto-resume */
|
||||||
|
|
||||||
/* Application callback */
|
/* Application callback */
|
||||||
ble_esl_ap_cb_t app_cb;
|
ble_esl_ap_cb_t app_cb;
|
||||||
@@ -253,6 +258,34 @@ typedef struct {
|
|||||||
/** Global pointer to the AP module state (allocated by init, freed by deinit) */
|
/** Global pointer to the AP module state (allocated by init, freed by deinit) */
|
||||||
extern ble_esl_ap_state_t *g_esl_ap;
|
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 ========================== */
|
/* ========================== Helpers ========================== */
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -266,17 +299,16 @@ ble_esl_ap_conn_t *ble_esl_ap_find_conn(uint16_t conn_handle);
|
|||||||
/**
|
/**
|
||||||
* @brief Find an ESL tracking entry by ESL Address
|
* @brief Find an ESL tracking entry by ESL Address
|
||||||
*
|
*
|
||||||
* @param esl_addr ESL Address (ESL_ID | Group_ID << 8)
|
* Caller must already hold the tracking mutex. The returned pointer is only
|
||||||
* @return Pointer to ESL entry, or NULL if not found
|
* 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);
|
ble_esl_ap_esl_entry_t *ble_esl_ap_find_esl(uint16_t esl_addr);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Find an ESL tracking entry by BLE address
|
* @brief Find an ESL tracking entry by BLE address
|
||||||
*
|
*
|
||||||
* @param addr 6-byte BLE address
|
* Caller must already hold the tracking mutex. See ble_esl_ap_find_esl().
|
||||||
* @param addr_type Address type
|
|
||||||
* @return Pointer to ESL entry, or NULL if not found
|
|
||||||
*/
|
*/
|
||||||
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_find_esl_by_ble_addr(const uint8_t *addr,
|
||||||
uint8_t addr_type);
|
uint8_t addr_type);
|
||||||
@@ -298,7 +330,7 @@ void ble_esl_ap_free_conn(ble_esl_ap_conn_t *conn);
|
|||||||
/**
|
/**
|
||||||
* @brief Allocate a free ESL tracking entry
|
* @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);
|
ble_esl_ap_esl_entry_t *ble_esl_ap_alloc_esl(void);
|
||||||
|
|
||||||
|
|||||||
@@ -26,7 +26,18 @@ extern "C" {
|
|||||||
|
|
||||||
/* ========================== Constants ========================== */
|
/* ========================== 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)
|
#define ESL_STATE_TIMEOUT_US (60ULL * 60ULL * 1000000ULL)
|
||||||
|
|
||||||
/** @brief ECP Procedure Timeout in microseconds */
|
/** @brief ECP Procedure Timeout in microseconds */
|
||||||
|
|||||||
@@ -60,10 +60,18 @@ typedef struct {
|
|||||||
bool ap_sync_key_valid; /* AP sync key written */
|
bool ap_sync_key_valid; /* AP sync key written */
|
||||||
bool resp_key_valid; /* Response key written */
|
bool resp_key_valid; /* Response key written */
|
||||||
bool address_valid; /* ESL address written */
|
bool address_valid; /* ESL address written */
|
||||||
uint16_t pawr_sync_handle; /* Active PAwR periodic sync handle; BLE_HS_CONN_HANDLE_NONE if none */
|
/* Current vs retiring PAwR sync tracking (AP reboot / PAST switch).
|
||||||
|
* Only current_sync_handle drives SYNCHRONIZED/UPDATING sync bookkeeping.
|
||||||
|
* retiring_* tracks a locally terminated old sync so its SYNC_LOST does not
|
||||||
|
* look like a natural loss of the new train. */
|
||||||
|
uint16_t current_sync_handle; /* Valid periodic sync; BLE_HS_CONN_HANDLE_NONE if none */
|
||||||
|
uint32_t sync_generation; /* Bumps each time current_sync_handle is assigned a new value */
|
||||||
|
uint16_t retiring_sync_handle; /* Locally terminated old sync awaiting SYNC_LOST */
|
||||||
|
uint32_t retiring_sync_generation; /* Snapshot of sync_generation when current was retired */
|
||||||
|
bool retiring_local_terminate; /* retiring SYNC_LOST is expected; do not change ESL state */
|
||||||
bool past_received; /* PAST completed in Updating state */
|
bool past_received; /* PAST completed in Updating state */
|
||||||
bool pawr_synced; /* Synchronized to the AP's PAwR train (retained across Updating) */
|
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 update_complete_received; /* Update Complete cmd received in Updating state */
|
||||||
bool deinit_pending; /* ble_esl_deinit() is waiting for disconnect */
|
bool deinit_pending; /* ble_esl_deinit() is waiting for disconnect */
|
||||||
SemaphoreHandle_t deinit_sem; /* Signaled when disconnect completes during deinit */
|
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:
|
* Writable OTS-backed images require a successful write-complete. Read-only
|
||||||
* - The image_writable_mask is NULL: all images are read-only (static/pre-loaded)
|
* preloaded images (writable_mask false / NULL) remain always available.
|
||||||
* 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.
|
|
||||||
*/
|
*/
|
||||||
static bool is_image_available(uint8_t image_index)
|
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) {
|
if (image_index >= config->num_images) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
/* All valid image indices are considered available:
|
|
||||||
* - Read-only images (no writable mask, or mask[i] == false) are
|
#if CONFIG_BLE_ESL_OTS_SUPPORT
|
||||||
* static/pre-loaded and always available.
|
bool writable = true;
|
||||||
* - Writable images (mask[i] == true) are assumed written by the AP. */
|
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;
|
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,
|
static esp_err_t handle_unassociate(const uint8_t *params, uint8_t params_len,
|
||||||
ble_esl_cmd_result_t *result)
|
ble_esl_cmd_result_t *result)
|
||||||
{
|
{
|
||||||
|
ESP_LOGI(TAG, "Unassociate ECP command received");
|
||||||
uint16_t bitmap = 0;
|
uint16_t bitmap = 0;
|
||||||
esp_err_t ret = esl_build_basic_state(&bitmap);
|
esp_err_t ret = esl_build_basic_state(&bitmap);
|
||||||
if (ret != ESP_OK) {
|
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 */
|
ble_ots_obj_id_t ots_obj_ids[CONFIG_BLE_ESL_MAX_IMAGES]; /* reverse map */
|
||||||
uint8_t ots_obj_count;
|
uint8_t ots_obj_count;
|
||||||
bool ots_initialized; /* OTS server init state */
|
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
|
#endif
|
||||||
} esl_gatts_ctx_t;
|
} esl_gatts_ctx_t;
|
||||||
|
|
||||||
@@ -502,7 +505,172 @@ static int esl_gatt_access_cb(uint16_t conn_handle, uint16_t attr_handle,
|
|||||||
|
|
||||||
#if CONFIG_BLE_ESL_OTS_SUPPORT
|
#if CONFIG_BLE_ESL_OTS_SUPPORT
|
||||||
|
|
||||||
/* OTS object-ID reverse map, count and init state live in esl_gatts_ctx_t. */
|
static void esl_image_state_reset(void)
|
||||||
|
{
|
||||||
|
if (s_esl_gatts == NULL) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
memset(s_esl_gatts->image_complete, 0, sizeof(s_esl_gatts->image_complete));
|
||||||
|
memset(s_esl_gatts->image_length, 0, sizeof(s_esl_gatts->image_length));
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool esl_map_ots_obj_to_image_index(ble_ots_obj_id_t obj_id, uint8_t *out_image_index)
|
||||||
|
{
|
||||||
|
if (out_image_index == NULL || s_esl_gatts == NULL) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (uint8_t i = 0; i < s_esl_gatts->ots_obj_count; i++) {
|
||||||
|
if (s_esl_gatts->ots_obj_ids[i] == obj_id) {
|
||||||
|
*out_image_index = i;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Fallback for base+index layout used by AP transfer helpers */
|
||||||
|
if (obj_id >= BLE_ESL_OTS_OBJECT_ID_BASE) {
|
||||||
|
uint64_t diff = obj_id - BLE_ESL_OTS_OBJECT_ID_BASE;
|
||||||
|
if (s_esl_gatts->ctx != NULL && diff < s_esl_gatts->ctx->config.num_images) {
|
||||||
|
*out_image_index = (uint8_t)diff;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ESP_LOGW(TAG, "OTS write: unknown obj_id=0x%06llx", (unsigned long long)obj_id);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void esl_mark_image_incomplete(uint8_t image_index)
|
||||||
|
{
|
||||||
|
if (s_esl_gatts == NULL || s_esl_gatts->image_lock == NULL ||
|
||||||
|
image_index >= CONFIG_BLE_ESL_MAX_IMAGES) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
xSemaphoreTake(s_esl_gatts->image_lock, portMAX_DELAY);
|
||||||
|
s_esl_gatts->image_complete[image_index] = false;
|
||||||
|
xSemaphoreGive(s_esl_gatts->image_lock);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void esl_mark_image_complete(uint8_t image_index, uint32_t length, bool success)
|
||||||
|
{
|
||||||
|
if (s_esl_gatts == NULL || s_esl_gatts->image_lock == NULL ||
|
||||||
|
image_index >= CONFIG_BLE_ESL_MAX_IMAGES) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
xSemaphoreTake(s_esl_gatts->image_lock, portMAX_DELAY);
|
||||||
|
if (success && length > 0) {
|
||||||
|
s_esl_gatts->image_complete[image_index] = true;
|
||||||
|
s_esl_gatts->image_length[image_index] = length;
|
||||||
|
} else {
|
||||||
|
s_esl_gatts->image_complete[image_index] = false;
|
||||||
|
s_esl_gatts->image_length[image_index] = 0;
|
||||||
|
}
|
||||||
|
xSemaphoreGive(s_esl_gatts->image_lock);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ble_esl_image_is_complete(uint8_t image_index)
|
||||||
|
{
|
||||||
|
if (s_esl_gatts == NULL || s_esl_gatts->image_lock == NULL ||
|
||||||
|
image_index >= CONFIG_BLE_ESL_MAX_IMAGES) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (s_esl_gatts->ctx == NULL || image_index >= s_esl_gatts->ctx->config.num_images) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
xSemaphoreTake(s_esl_gatts->image_lock, portMAX_DELAY);
|
||||||
|
bool complete = s_esl_gatts->image_complete[image_index];
|
||||||
|
xSemaphoreGive(s_esl_gatts->image_lock);
|
||||||
|
return complete;
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_err_t ble_esl_image_snapshot(uint8_t image_index, uint8_t *dst, size_t capacity,
|
||||||
|
size_t *out_len)
|
||||||
|
{
|
||||||
|
if (dst == NULL || capacity == 0) {
|
||||||
|
return ESP_ERR_INVALID_ARG;
|
||||||
|
}
|
||||||
|
if (s_esl_gatts == NULL || s_esl_gatts->image_lock == NULL ||
|
||||||
|
!s_esl_gatts->ots_initialized) {
|
||||||
|
return ESP_ERR_INVALID_STATE;
|
||||||
|
}
|
||||||
|
if (image_index >= s_esl_gatts->ots_obj_count ||
|
||||||
|
image_index >= CONFIG_BLE_ESL_MAX_IMAGES) {
|
||||||
|
return ESP_ERR_INVALID_ARG;
|
||||||
|
}
|
||||||
|
|
||||||
|
xSemaphoreTake(s_esl_gatts->image_lock, portMAX_DELAY);
|
||||||
|
if (!s_esl_gatts->image_complete[image_index]) {
|
||||||
|
xSemaphoreGive(s_esl_gatts->image_lock);
|
||||||
|
return ESP_ERR_INVALID_STATE;
|
||||||
|
}
|
||||||
|
uint32_t length = s_esl_gatts->image_length[image_index];
|
||||||
|
if (length == 0 || length > capacity) {
|
||||||
|
xSemaphoreGive(s_esl_gatts->image_lock);
|
||||||
|
return ESP_ERR_INVALID_SIZE;
|
||||||
|
}
|
||||||
|
|
||||||
|
ble_ots_obj_id_t obj_id = s_esl_gatts->ots_obj_ids[image_index];
|
||||||
|
int rc = ble_ots_server_copy_object_data(obj_id, 0, length, dst);
|
||||||
|
bool still_complete = s_esl_gatts->image_complete[image_index];
|
||||||
|
xSemaphoreGive(s_esl_gatts->image_lock);
|
||||||
|
|
||||||
|
if (rc != 0 || !still_complete) {
|
||||||
|
return ESP_ERR_INVALID_STATE;
|
||||||
|
}
|
||||||
|
if (out_len != NULL) {
|
||||||
|
*out_len = length;
|
||||||
|
}
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_err_t ble_esl_image_restore(uint8_t image_index, const uint8_t *data, size_t len)
|
||||||
|
{
|
||||||
|
if (data == NULL || len == 0 || len > CONFIG_BLE_ESL_MAX_IMAGE_SIZE) {
|
||||||
|
return ESP_ERR_INVALID_ARG;
|
||||||
|
}
|
||||||
|
if (s_esl_gatts == NULL || s_esl_gatts->image_lock == NULL ||
|
||||||
|
!s_esl_gatts->ots_initialized || s_esl_gatts->ctx == NULL) {
|
||||||
|
return ESP_ERR_INVALID_STATE;
|
||||||
|
}
|
||||||
|
if (image_index >= s_esl_gatts->ots_obj_count ||
|
||||||
|
image_index >= s_esl_gatts->ctx->config.num_images ||
|
||||||
|
image_index >= CONFIG_BLE_ESL_MAX_IMAGES) {
|
||||||
|
return ESP_ERR_INVALID_ARG;
|
||||||
|
}
|
||||||
|
|
||||||
|
xSemaphoreTake(s_esl_gatts->image_lock, portMAX_DELAY);
|
||||||
|
s_esl_gatts->image_complete[image_index] = false;
|
||||||
|
s_esl_gatts->image_length[image_index] = 0;
|
||||||
|
|
||||||
|
ble_ots_obj_id_t obj_id = s_esl_gatts->ots_obj_ids[image_index];
|
||||||
|
int rc = ble_ots_server_set_object_data(obj_id, data, 0, (uint32_t)len);
|
||||||
|
if (rc != 0) {
|
||||||
|
xSemaphoreGive(s_esl_gatts->image_lock);
|
||||||
|
ESP_LOGE(TAG, "image_restore: set_object_data failed rc=%d index=%u",
|
||||||
|
rc, image_index);
|
||||||
|
return ESP_FAIL;
|
||||||
|
}
|
||||||
|
|
||||||
|
s_esl_gatts->image_complete[image_index] = true;
|
||||||
|
s_esl_gatts->image_length[image_index] = (uint32_t)len;
|
||||||
|
xSemaphoreGive(s_esl_gatts->image_lock);
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ble_esl_image_invalidate_all(void)
|
||||||
|
{
|
||||||
|
if (s_esl_gatts == NULL) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (s_esl_gatts->image_lock == NULL) {
|
||||||
|
esl_image_state_reset();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
xSemaphoreTake(s_esl_gatts->image_lock, portMAX_DELAY);
|
||||||
|
esl_image_state_reset();
|
||||||
|
xSemaphoreGive(s_esl_gatts->image_lock);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief OTS event callback — handles write-complete events,
|
* @brief OTS event callback — handles write-complete events,
|
||||||
@@ -511,41 +679,49 @@ static int esl_gatt_access_cb(uint16_t conn_handle, uint16_t attr_handle,
|
|||||||
static void esl_ots_write_cb(ble_ots_server_event_t event,
|
static void esl_ots_write_cb(ble_ots_server_event_t event,
|
||||||
ble_ots_server_cb_param_t *param)
|
ble_ots_server_cb_param_t *param)
|
||||||
{
|
{
|
||||||
if (event != BLE_OTS_SERVER_EVT_WRITE_COMPLETE || param == NULL) {
|
if (param == NULL) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
ble_ots_obj_id_t obj_id = param->write_complete.object_id;
|
if (event == BLE_OTS_SERVER_EVT_DATA_WRITE) {
|
||||||
uint32_t offset = param->write_complete.offset;
|
uint8_t image_index;
|
||||||
uint32_t length = param->write_complete.bytes_received;
|
if (!esl_map_ots_obj_to_image_index(param->data_write.object_id, &image_index)) {
|
||||||
|
return;
|
||||||
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 (!found) {
|
esl_mark_image_incomplete(image_index);
|
||||||
ESP_LOGW(TAG, "OTS write: unknown obj_id=0x%06llx", (unsigned long long)obj_id);
|
|
||||||
|
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;
|
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 = {
|
ble_esl_cb_param_t cb_param = {
|
||||||
.image_write = {
|
.image_write = {
|
||||||
.image_index = image_index,
|
.image_index = image_index,
|
||||||
.data = NULL,
|
.data = NULL,
|
||||||
.length = length,
|
.length = param->write_complete.bytes_received,
|
||||||
.offset = offset,
|
.offset = param->write_complete.offset,
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
esl_notify_app(BLE_ESL_EVT_IMAGE_WRITE, &cb_param);
|
esl_notify_app(BLE_ESL_EVT_IMAGE_WRITE, &cb_param);
|
||||||
@@ -557,6 +733,13 @@ static esp_err_t esl_setup_ots(const ble_esl_config_t *config)
|
|||||||
return ESP_OK;
|
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;
|
s_esl_gatts->ots_obj_count = 0;
|
||||||
|
|
||||||
ble_ots_server_config_t ots_config = {
|
ble_ots_server_config_t ots_config = {
|
||||||
@@ -633,6 +816,12 @@ static void esl_teardown_ots(void)
|
|||||||
ble_ots_server_deinit();
|
ble_ots_server_deinit();
|
||||||
s_esl_gatts->ots_initialized = false;
|
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 */
|
#endif /* CONFIG_BLE_ESL_OTS_SUPPORT */
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,130 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @file esl_persisted_tag_map.h
|
||||||
|
* @brief Pure helpers for TAG persisted snapshot field mapping (host-testable).
|
||||||
|
*/
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include "ble_esl_common.h"
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifndef CONFIG_BIT_ADDRESS
|
||||||
|
#define CONFIG_BIT_ADDRESS (1 << 0)
|
||||||
|
#define CONFIG_BIT_AP_SYNC_KEY (1 << 1)
|
||||||
|
#define CONFIG_BIT_RESP_KEY (1 << 2)
|
||||||
|
#define CONFIG_BIT_ABS_TIME (1 << 3)
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#define ESL_PERSISTED_CONFIG_MASK \
|
||||||
|
(CONFIG_BIT_ADDRESS | CONFIG_BIT_AP_SYNC_KEY | CONFIG_BIT_RESP_KEY)
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
ble_esl_address_t esl_address;
|
||||||
|
ble_esl_key_material_t ap_sync_key;
|
||||||
|
ble_esl_key_material_t resp_key;
|
||||||
|
uint8_t peer_addr_type;
|
||||||
|
uint8_t peer_addr[6];
|
||||||
|
} esl_persisted_tag_dto_t;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
bool address_valid;
|
||||||
|
bool ap_sync_key_valid;
|
||||||
|
bool resp_key_valid;
|
||||||
|
bool has_bonded_peer;
|
||||||
|
uint8_t config_complete;
|
||||||
|
uint8_t peer_addr_type;
|
||||||
|
uint8_t peer_addr[6];
|
||||||
|
ble_esl_address_t esl_address;
|
||||||
|
ble_esl_key_material_t ap_sync_key;
|
||||||
|
ble_esl_key_material_t resp_key;
|
||||||
|
} esl_persisted_tag_ram_t;
|
||||||
|
|
||||||
|
static inline bool esl_persisted_peer_nonzero(const uint8_t addr[6])
|
||||||
|
{
|
||||||
|
for (int i = 0; i < 6; i++) {
|
||||||
|
if (addr[i] != 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline bool esl_persisted_export_ok(const esl_persisted_tag_ram_t *ram)
|
||||||
|
{
|
||||||
|
if (ram == NULL) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!ram->address_valid || !ram->ap_sync_key_valid || !ram->resp_key_valid ||
|
||||||
|
!ram->has_bonded_peer) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if ((ram->config_complete & ESL_PERSISTED_CONFIG_MASK) != ESL_PERSISTED_CONFIG_MASK) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (ram->esl_address.esl_id == BLE_ESL_BROADCAST_ADDRESS) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!esl_persisted_peer_nonzero(ram->peer_addr)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline bool esl_persisted_fill_dto(const esl_persisted_tag_ram_t *ram,
|
||||||
|
esl_persisted_tag_dto_t *out)
|
||||||
|
{
|
||||||
|
if (!esl_persisted_export_ok(ram) || out == NULL) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
out->esl_address = ram->esl_address;
|
||||||
|
out->ap_sync_key = ram->ap_sync_key;
|
||||||
|
out->resp_key = ram->resp_key;
|
||||||
|
out->peer_addr_type = ram->peer_addr_type;
|
||||||
|
memcpy(out->peer_addr, ram->peer_addr, 6);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply DTO into RAM fields for restore. Does not touch Absolute Time / sync handles.
|
||||||
|
* Sets config_complete to ESL_PERSISTED_CONFIG_MASK only (ABS_TIME cleared).
|
||||||
|
*/
|
||||||
|
static inline bool esl_persisted_apply_dto(const esl_persisted_tag_dto_t *in,
|
||||||
|
esl_persisted_tag_ram_t *ram)
|
||||||
|
{
|
||||||
|
if (in == NULL || ram == NULL) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (in->esl_address.esl_id == BLE_ESL_BROADCAST_ADDRESS) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!esl_persisted_peer_nonzero(in->peer_addr)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
ram->esl_address = in->esl_address;
|
||||||
|
ram->ap_sync_key = in->ap_sync_key;
|
||||||
|
ram->resp_key = in->resp_key;
|
||||||
|
ram->peer_addr_type = in->peer_addr_type;
|
||||||
|
memcpy(ram->peer_addr, in->peer_addr, 6);
|
||||||
|
ram->address_valid = true;
|
||||||
|
ram->ap_sync_key_valid = true;
|
||||||
|
ram->resp_key_valid = true;
|
||||||
|
ram->has_bonded_peer = true;
|
||||||
|
ram->config_complete = (uint8_t)ESL_PERSISTED_CONFIG_MASK;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
@@ -11,6 +11,7 @@
|
|||||||
|
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
#include <assert.h>
|
#include <assert.h>
|
||||||
|
#include <inttypes.h>
|
||||||
|
|
||||||
#include "freertos/FreeRTOS.h"
|
#include "freertos/FreeRTOS.h"
|
||||||
#include "freertos/semphr.h"
|
#include "freertos/semphr.h"
|
||||||
@@ -27,6 +28,8 @@
|
|||||||
#include "ble_esl.h"
|
#include "ble_esl.h"
|
||||||
#include "ble_esl_int.h"
|
#include "ble_esl_int.h"
|
||||||
#include "ble_esl_state_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";
|
static const char *TAG = "esl_state";
|
||||||
|
|
||||||
@@ -43,6 +46,142 @@ static void ecp_timeout_cb(void *arg);
|
|||||||
static esp_err_t esl_start_advertising(void);
|
static esp_err_t esl_start_advertising(void);
|
||||||
static esp_err_t esl_stop_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 bool is_transition_valid(ble_esl_state_t from, ble_esl_state_t to);
|
||||||
|
static void enable_past_reception(uint16_t conn_handle);
|
||||||
|
static void install_current_sync(uint16_t sync_handle);
|
||||||
|
static esp_err_t retire_current_sync(bool rearm_past);
|
||||||
|
static void clear_retiring_sync(void);
|
||||||
|
static void clear_current_sync_bookkeeping(void);
|
||||||
|
|
||||||
|
static void clear_retiring_sync(void)
|
||||||
|
{
|
||||||
|
if (s_ctx == NULL) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
s_ctx->retiring_sync_handle = BLE_HS_CONN_HANDLE_NONE;
|
||||||
|
s_ctx->retiring_sync_generation = 0;
|
||||||
|
s_ctx->retiring_local_terminate = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void clear_current_sync_bookkeeping(void)
|
||||||
|
{
|
||||||
|
if (s_ctx == NULL) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
s_ctx->current_sync_handle = BLE_HS_CONN_HANDLE_NONE;
|
||||||
|
/* sync_generation is monotonic across NONE assignments; do not reset. */
|
||||||
|
}
|
||||||
|
|
||||||
|
static void install_current_sync(uint16_t sync_handle)
|
||||||
|
{
|
||||||
|
if (s_ctx == NULL || sync_handle == BLE_HS_CONN_HANDLE_NONE) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
s_ctx->current_sync_handle = sync_handle;
|
||||||
|
if (s_ctx->sync_generation == UINT32_MAX) {
|
||||||
|
s_ctx->sync_generation = 1;
|
||||||
|
} else {
|
||||||
|
s_ctx->sync_generation++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Move current sync into retiring and terminate it locally.
|
||||||
|
*
|
||||||
|
* @param rearm_past If true, set past_pending so SYNC_LOST re-arms PAST after
|
||||||
|
* the controller frees the reception pool slot.
|
||||||
|
* @return ESP_OK on success, ESP_ERR_INVALID_STATE if another retire is active,
|
||||||
|
* ESP_FAIL if terminate failed (fields rolled back).
|
||||||
|
*/
|
||||||
|
static esp_err_t retire_current_sync(bool rearm_past)
|
||||||
|
{
|
||||||
|
if (s_ctx == NULL) {
|
||||||
|
return ESP_ERR_INVALID_STATE;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (s_ctx->current_sync_handle == BLE_HS_CONN_HANDLE_NONE) {
|
||||||
|
if (rearm_past && s_ctx->conn_handle != BLE_HS_CONN_HANDLE_NONE) {
|
||||||
|
enable_past_reception(s_ctx->conn_handle);
|
||||||
|
}
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (s_ctx->retiring_sync_handle != BLE_HS_CONN_HANDLE_NONE) {
|
||||||
|
ESP_LOGW(TAG, "retire_current_sync: retiring already active (handle=%u)",
|
||||||
|
s_ctx->retiring_sync_handle);
|
||||||
|
return ESP_ERR_INVALID_STATE;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint16_t old_handle = s_ctx->current_sync_handle;
|
||||||
|
uint32_t old_generation = s_ctx->sync_generation;
|
||||||
|
|
||||||
|
s_ctx->retiring_sync_handle = old_handle;
|
||||||
|
s_ctx->retiring_sync_generation = old_generation;
|
||||||
|
s_ctx->retiring_local_terminate = true;
|
||||||
|
s_ctx->current_sync_handle = BLE_HS_CONN_HANDLE_NONE;
|
||||||
|
s_ctx->past_pending = rearm_past;
|
||||||
|
|
||||||
|
int rc = ble_gap_periodic_adv_sync_terminate(old_handle);
|
||||||
|
if (rc != 0 && rc != BLE_HS_EALREADY) {
|
||||||
|
ESP_LOGE(TAG, "retire_current_sync: terminate(%u) failed rc=%d — rolling back",
|
||||||
|
old_handle, rc);
|
||||||
|
s_ctx->current_sync_handle = old_handle;
|
||||||
|
s_ctx->past_pending = false;
|
||||||
|
clear_retiring_sync();
|
||||||
|
return ESP_FAIL;
|
||||||
|
}
|
||||||
|
|
||||||
|
ESP_LOGI(TAG, "Retired sync handle=%u gen=%" PRIu32 " rearm_past=%d",
|
||||||
|
old_handle, old_generation, rearm_past);
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Install a newly received PAST sync, retiring any previous current sync.
|
||||||
|
*/
|
||||||
|
static void adopt_past_sync(uint16_t sync_handle)
|
||||||
|
{
|
||||||
|
if (s_ctx == NULL || sync_handle == BLE_HS_CONN_HANDLE_NONE) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (s_ctx->current_sync_handle != BLE_HS_CONN_HANDLE_NONE &&
|
||||||
|
s_ctx->current_sync_handle != sync_handle) {
|
||||||
|
/* Move old current to retiring, then install the new handle immediately
|
||||||
|
* so a late SYNC_LOST for the old handle cannot affect the new current. */
|
||||||
|
if (s_ctx->retiring_sync_handle != BLE_HS_CONN_HANDLE_NONE) {
|
||||||
|
ESP_LOGW(TAG, "adopt_past_sync: dropping stale retiring handle=%u",
|
||||||
|
s_ctx->retiring_sync_handle);
|
||||||
|
clear_retiring_sync();
|
||||||
|
}
|
||||||
|
|
||||||
|
uint16_t old_handle = s_ctx->current_sync_handle;
|
||||||
|
uint32_t old_generation = s_ctx->sync_generation;
|
||||||
|
s_ctx->retiring_sync_handle = old_handle;
|
||||||
|
s_ctx->retiring_sync_generation = old_generation;
|
||||||
|
s_ctx->retiring_local_terminate = true;
|
||||||
|
s_ctx->current_sync_handle = BLE_HS_CONN_HANDLE_NONE;
|
||||||
|
s_ctx->past_pending = false;
|
||||||
|
|
||||||
|
install_current_sync(sync_handle);
|
||||||
|
|
||||||
|
int rc = ble_gap_periodic_adv_sync_terminate(old_handle);
|
||||||
|
if (rc != 0 && rc != BLE_HS_EALREADY) {
|
||||||
|
ESP_LOGW(TAG, "adopt_past_sync: terminate old handle=%u rc=%d",
|
||||||
|
old_handle, rc);
|
||||||
|
/* New current is already installed; clear retiring bookkeeping so a
|
||||||
|
* missing SYNC_LOST cannot block future retires indefinitely. */
|
||||||
|
clear_retiring_sync();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (s_ctx->current_sync_handle == sync_handle) {
|
||||||
|
/* Same handle reported again — keep generation. */
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
install_current_sync(sync_handle);
|
||||||
|
}
|
||||||
|
|
||||||
/* ========================== State Machine ========================== */
|
/* ========================== State Machine ========================== */
|
||||||
|
|
||||||
@@ -347,22 +486,22 @@ void esl_notify_update_complete(void)
|
|||||||
*/
|
*/
|
||||||
static esp_err_t esl_terminate_pawr_sync(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;
|
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) {
|
if (rc != 0 && rc != BLE_HS_ENOTCONN) {
|
||||||
/* Anything other than ENOTCONN (host no longer knows this sync, e.g. it
|
/* Anything other than ENOTCONN (host no longer knows this sync, e.g. it
|
||||||
* was lost concurrently) means the host still holds the periodic sync.
|
* 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
|
* Keep the handle so the caller can retry, and keep pawr_synced so the
|
||||||
* Basic State Synchronized bit stays truthful. */
|
* Basic State Synchronized bit stays truthful. */
|
||||||
ESP_LOGW(TAG, "Failed to terminate PAwR sync %u: rc=%d",
|
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;
|
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;
|
s_ctx->pawr_synced = false;
|
||||||
return ESP_OK;
|
return ESP_OK;
|
||||||
}
|
}
|
||||||
@@ -643,8 +782,19 @@ static void enable_past_reception(uint16_t conn_handle)
|
|||||||
|
|
||||||
int rc = ble_gap_periodic_adv_sync_receive(conn_handle, &sync_params,
|
int rc = ble_gap_periodic_adv_sync_receive(conn_handle, &sync_params,
|
||||||
esl_gap_event_handler, NULL);
|
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) {
|
if (rc != 0) {
|
||||||
ESP_LOGE(TAG, "ble_gap_periodic_adv_sync_receive failed: rc=%d", rc);
|
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);
|
ESP_LOGI(TAG, "Enable PAST reception: conn_handle=%u", conn_handle);
|
||||||
@@ -802,21 +952,11 @@ static void handle_gap_periodic_transfer(struct ble_gap_event *event)
|
|||||||
uint16_t sync_handle = event->periodic_transfer.sync_handle;
|
uint16_t sync_handle = event->periodic_transfer.sync_handle;
|
||||||
ESP_LOGI(TAG, "PAST received: sync_handle=%d, state=%d", sync_handle, s_ctx->state);
|
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
|
/* Adopt the new train through the unique retire helper so a late SYNC_LOST
|
||||||
* SYNCHRONIZED → UPDATING because ble_gap_periodic_adv_sync_terminate
|
* for the previous handle cannot be mistaken for natural loss of the new
|
||||||
* defers pool-entry freeing and would cause enable_past_reception to fail
|
* current sync (or re-arm PAST incorrectly). */
|
||||||
* with ENOMEM if freed synchronously). */
|
s_ctx->past_pending = false;
|
||||||
if (s_ctx->pawr_sync_handle != BLE_HS_CONN_HANDLE_NONE &&
|
adopt_past_sync(sync_handle);
|
||||||
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;
|
|
||||||
|
|
||||||
/* Enable PAwR subevent reception and response slots for our group/subevent. */
|
/* Enable PAwR subevent reception and response slots for our group/subevent. */
|
||||||
uint8_t group_id = BLE_ESL_ADDR_GROUP_ID(s_ctx->esl_address);
|
uint8_t group_id = BLE_ESL_ADDR_GROUP_ID(s_ctx->esl_address);
|
||||||
@@ -974,18 +1114,61 @@ static int esl_gap_event_handler(struct ble_gap_event *event, void *arg)
|
|||||||
handle_gap_periodic_report(event);
|
handle_gap_periodic_report(event);
|
||||||
return 0;
|
return 0;
|
||||||
|
|
||||||
case BLE_GAP_EVENT_PERIODIC_SYNC_LOST:
|
case BLE_GAP_EVENT_PERIODIC_SYNC_LOST: {
|
||||||
/* Fired asynchronously after ble_gap_periodic_adv_sync_terminate frees
|
uint16_t lost_handle = event->periodic_sync_lost.sync_handle;
|
||||||
* the periodic sync pool slot. If PAST was deferred, arm it now. */
|
esl_sync_lost_ctx_t lost_ctx = {
|
||||||
if (s_ctx->past_pending &&
|
.current_sync_handle = s_ctx->current_sync_handle,
|
||||||
s_ctx->state == BLE_ESL_STATE_UPDATING &&
|
.sync_generation = s_ctx->sync_generation,
|
||||||
s_ctx->conn_handle != BLE_HS_CONN_HANDLE_NONE) {
|
.retiring_sync_handle = s_ctx->retiring_sync_handle,
|
||||||
s_ctx->past_pending = false;
|
.retiring_sync_generation = s_ctx->retiring_sync_generation,
|
||||||
ESP_LOGI(TAG, "SYNC_LOST: re-arming PAST reception (conn=%d)",
|
.retiring_local_terminate = s_ctx->retiring_local_terminate,
|
||||||
s_ctx->conn_handle);
|
};
|
||||||
enable_past_reception(s_ctx->conn_handle);
|
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;
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
case BLE_GAP_EVENT_NOTIFY_TX:
|
case BLE_GAP_EVENT_NOTIFY_TX:
|
||||||
esl_handle_ecp_notify_tx(event->notify_tx.conn_handle,
|
esl_handle_ecp_notify_tx(event->notify_tx.conn_handle,
|
||||||
@@ -1139,7 +1322,9 @@ esp_err_t ble_esl_init(const ble_esl_config_t *config)
|
|||||||
memcpy(&s_ctx->config, config, sizeof(ble_esl_config_t));
|
memcpy(&s_ctx->config, config, sizeof(ble_esl_config_t));
|
||||||
s_ctx->state = BLE_ESL_STATE_UNASSOCIATED;
|
s_ctx->state = BLE_ESL_STATE_UNASSOCIATED;
|
||||||
s_ctx->conn_handle = BLE_HS_CONN_HANDLE_NONE;
|
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;
|
s_ctx->initialized = true;
|
||||||
|
|
||||||
/* Configure security as mandated by the ESL Profile: LE Secure Connections
|
/* Configure security as mandated by the ESL Profile: LE Secure Connections
|
||||||
@@ -1323,7 +1508,7 @@ esp_err_t ble_esl_deinit(void)
|
|||||||
* as the retry for a teardown that ble_esl_stop() could not complete. */
|
* as the retry for a teardown that ble_esl_stop() could not complete. */
|
||||||
if (esl_terminate_pawr_sync() != ESP_OK) {
|
if (esl_terminate_pawr_sync() != ESP_OK) {
|
||||||
ESP_LOGW(TAG, "PAwR sync %u still active while deinitializing",
|
ESP_LOGW(TAG, "PAwR sync %u still active while deinitializing",
|
||||||
s_ctx->pawr_sync_handle);
|
s_ctx->current_sync_handle);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Deinit sub-modules */
|
/* Deinit sub-modules */
|
||||||
@@ -1420,7 +1605,7 @@ esp_err_t ble_esl_stop(void)
|
|||||||
|
|
||||||
if (sync_err != ESP_OK) {
|
if (sync_err != ESP_OK) {
|
||||||
ESP_LOGW(TAG, "ESL stopped, but PAwR sync %u could not be terminated",
|
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;
|
return sync_err;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1451,3 +1636,87 @@ esp_err_t ble_esl_register_cb(ble_esl_cb_t callback)
|
|||||||
ESP_LOGI(TAG, "Application callback registered");
|
ESP_LOGI(TAG, "Application callback registered");
|
||||||
return ESP_OK;
|
return ESP_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
esp_err_t ble_esl_export_persisted_tag(ble_esl_persisted_tag_t *out)
|
||||||
|
{
|
||||||
|
if (out == NULL || s_ctx == NULL || !s_ctx->initialized) {
|
||||||
|
return ESP_ERR_INVALID_ARG;
|
||||||
|
}
|
||||||
|
|
||||||
|
esl_persisted_tag_ram_t ram = {
|
||||||
|
.address_valid = s_ctx->address_valid,
|
||||||
|
.ap_sync_key_valid = s_ctx->ap_sync_key_valid,
|
||||||
|
.resp_key_valid = s_ctx->resp_key_valid,
|
||||||
|
.has_bonded_peer = s_ctx->has_bonded_peer,
|
||||||
|
.config_complete = s_ctx->config_complete,
|
||||||
|
.peer_addr_type = s_ctx->bonded_peer_addr.type,
|
||||||
|
.esl_address = s_ctx->esl_address,
|
||||||
|
.ap_sync_key = s_ctx->ap_sync_key,
|
||||||
|
.resp_key = s_ctx->resp_key,
|
||||||
|
};
|
||||||
|
memcpy(ram.peer_addr, s_ctx->bonded_peer_addr.val, 6);
|
||||||
|
|
||||||
|
esl_persisted_tag_dto_t dto;
|
||||||
|
if (!esl_persisted_fill_dto(&ram, &dto)) {
|
||||||
|
return ESP_ERR_INVALID_STATE;
|
||||||
|
}
|
||||||
|
|
||||||
|
out->esl_address = dto.esl_address;
|
||||||
|
out->ap_sync_key = dto.ap_sync_key;
|
||||||
|
out->resp_key = dto.resp_key;
|
||||||
|
out->peer_addr_type = dto.peer_addr_type;
|
||||||
|
memcpy(out->peer_addr, dto.peer_addr, 6);
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_err_t ble_esl_restore_persisted_tag(const ble_esl_persisted_tag_t *info)
|
||||||
|
{
|
||||||
|
if (info == NULL || s_ctx == NULL || !s_ctx->initialized) {
|
||||||
|
return ESP_ERR_INVALID_ARG;
|
||||||
|
}
|
||||||
|
if (s_ctx->started) {
|
||||||
|
ESP_LOGE(TAG, "restore_persisted_tag must be called before ble_esl_start()");
|
||||||
|
return ESP_ERR_INVALID_STATE;
|
||||||
|
}
|
||||||
|
|
||||||
|
esl_persisted_tag_dto_t dto = {
|
||||||
|
.esl_address = info->esl_address,
|
||||||
|
.ap_sync_key = info->ap_sync_key,
|
||||||
|
.resp_key = info->resp_key,
|
||||||
|
.peer_addr_type = info->peer_addr_type,
|
||||||
|
};
|
||||||
|
memcpy(dto.peer_addr, info->peer_addr, 6);
|
||||||
|
|
||||||
|
esl_persisted_tag_ram_t ram;
|
||||||
|
memset(&ram, 0, sizeof(ram));
|
||||||
|
if (!esl_persisted_apply_dto(&dto, &ram)) {
|
||||||
|
return ESP_ERR_INVALID_ARG;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Bare assignment — do NOT call esl_state_transition(). */
|
||||||
|
s_ctx->esl_address = ram.esl_address;
|
||||||
|
s_ctx->ap_sync_key = ram.ap_sync_key;
|
||||||
|
s_ctx->resp_key = ram.resp_key;
|
||||||
|
s_ctx->address_valid = true;
|
||||||
|
s_ctx->ap_sync_key_valid = true;
|
||||||
|
s_ctx->resp_key_valid = true;
|
||||||
|
s_ctx->has_bonded_peer = true;
|
||||||
|
s_ctx->bonded_peer_addr.type = ram.peer_addr_type;
|
||||||
|
memcpy(s_ctx->bonded_peer_addr.val, ram.peer_addr, 6);
|
||||||
|
s_ctx->config_complete = ram.config_complete; /* ABS_TIME bit clear */
|
||||||
|
s_ctx->abs_time_base = 0;
|
||||||
|
s_ctx->abs_time_offset_us = 0;
|
||||||
|
s_ctx->past_received = false;
|
||||||
|
s_ctx->past_pending = false;
|
||||||
|
s_ctx->pawr_synced = false;
|
||||||
|
s_ctx->update_complete_received = false;
|
||||||
|
s_ctx->current_sync_handle = BLE_HS_CONN_HANDLE_NONE;
|
||||||
|
s_ctx->sync_generation = 0;
|
||||||
|
s_ctx->retiring_sync_handle = BLE_HS_CONN_HANDLE_NONE;
|
||||||
|
s_ctx->retiring_sync_generation = 0;
|
||||||
|
s_ctx->retiring_local_terminate = false;
|
||||||
|
s_ctx->state = BLE_ESL_STATE_UNSYNCHRONIZED;
|
||||||
|
|
||||||
|
ESP_LOGI(TAG, "Restored association, state=UNSYNCHRONIZED");
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,78 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*
|
||||||
|
* Pure classification helpers for PERIODIC_SYNC_LOST (host-testable).
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <stdbool.h>
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifndef ESL_SYNC_HANDLE_NONE
|
||||||
|
#define ESL_SYNC_HANDLE_NONE 0xffffu
|
||||||
|
#endif
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
ESL_SYNC_LOST_CURRENT_NATURAL = 0,
|
||||||
|
ESL_SYNC_LOST_RETIRING_LOCAL,
|
||||||
|
ESL_SYNC_LOST_STALE,
|
||||||
|
} esl_sync_lost_class_t;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
uint16_t current_sync_handle;
|
||||||
|
uint32_t sync_generation;
|
||||||
|
uint16_t retiring_sync_handle;
|
||||||
|
uint32_t retiring_sync_generation;
|
||||||
|
bool retiring_local_terminate;
|
||||||
|
} esl_sync_lost_ctx_t;
|
||||||
|
|
||||||
|
static inline uint32_t esl_sync_lost_generation_hint(const esl_sync_lost_ctx_t *ctx,
|
||||||
|
uint16_t evt_sync_handle)
|
||||||
|
{
|
||||||
|
if (ctx == NULL) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (evt_sync_handle == ctx->current_sync_handle &&
|
||||||
|
ctx->current_sync_handle != ESL_SYNC_HANDLE_NONE) {
|
||||||
|
return ctx->sync_generation;
|
||||||
|
}
|
||||||
|
if (evt_sync_handle == ctx->retiring_sync_handle &&
|
||||||
|
ctx->retiring_sync_handle != ESL_SYNC_HANDLE_NONE) {
|
||||||
|
return ctx->retiring_sync_generation;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline esl_sync_lost_class_t esl_classify_sync_lost(const esl_sync_lost_ctx_t *ctx,
|
||||||
|
uint16_t evt_sync_handle)
|
||||||
|
{
|
||||||
|
if (ctx == NULL) {
|
||||||
|
return ESL_SYNC_LOST_STALE;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (evt_sync_handle == ctx->retiring_sync_handle &&
|
||||||
|
ctx->retiring_sync_handle != ESL_SYNC_HANDLE_NONE &&
|
||||||
|
ctx->retiring_local_terminate) {
|
||||||
|
return ESL_SYNC_LOST_RETIRING_LOCAL;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (evt_sync_handle == ctx->current_sync_handle &&
|
||||||
|
ctx->current_sync_handle != ESL_SYNC_HANDLE_NONE) {
|
||||||
|
if (esl_sync_lost_generation_hint(ctx, evt_sync_handle) == ctx->sync_generation) {
|
||||||
|
return ESL_SYNC_LOST_CURRENT_NATURAL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ESL_SYNC_LOST_STALE;
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
@@ -105,6 +105,7 @@ typedef enum {
|
|||||||
BLE_OTS_SERVER_EVT_EXECUTE, /*!< OACP Execute on current object */
|
BLE_OTS_SERVER_EVT_EXECUTE, /*!< OACP Execute on current object */
|
||||||
BLE_OTS_SERVER_EVT_CHECKSUM_REQUEST, /*!< OACP Calculate Checksum completed */
|
BLE_OTS_SERVER_EVT_CHECKSUM_REQUEST, /*!< OACP Calculate Checksum completed */
|
||||||
BLE_OTS_SERVER_EVT_READ_COMPLETE, /*!< Read transfer completed */
|
BLE_OTS_SERVER_EVT_READ_COMPLETE, /*!< Read transfer completed */
|
||||||
|
BLE_OTS_SERVER_EVT_DATA_WRITE, /*!< Object data chunk received during write transfer */
|
||||||
BLE_OTS_SERVER_EVT_WRITE_COMPLETE, /*!< Write transfer completed */
|
BLE_OTS_SERVER_EVT_WRITE_COMPLETE, /*!< Write transfer completed */
|
||||||
BLE_OTS_SERVER_EVT_METADATA_WRITTEN, /*!< Client wrote a metadata characteristic */
|
BLE_OTS_SERVER_EVT_METADATA_WRITTEN, /*!< Client wrote a metadata characteristic */
|
||||||
} ble_ots_server_event_t;
|
} ble_ots_server_event_t;
|
||||||
@@ -173,6 +174,18 @@ typedef struct {
|
|||||||
ble_ots_server_transfer_status_t status; /*!< Completion status */
|
ble_ots_server_transfer_status_t status; /*!< Completion status */
|
||||||
} ble_ots_server_evt_read_complete_t;
|
} 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.
|
* @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_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_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_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_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_metadata_evt_t metadata_written; /*!< BLE_OTS_SERVER_EVT_METADATA_WRITTEN */
|
||||||
} ble_ots_server_cb_param_t;
|
} 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 offset,
|
||||||
uint32_t length);
|
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.
|
* @brief Trigger Object Changed indication for a server-initiated change.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -547,6 +547,12 @@ ble_ots_server_obj_t *ble_ots_server_obj_db_lookup(ble_ots_obj_id_t object_id)
|
|||||||
*****************************************************************************/
|
*****************************************************************************/
|
||||||
int ble_ots_server_obj_data_read(ble_ots_obj_id_t object_id, uint32_t offset,
|
int ble_ots_server_obj_data_read(ble_ots_obj_id_t object_id, uint32_t offset,
|
||||||
uint32_t length, uint8_t *buf)
|
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;
|
int rc = 0;
|
||||||
|
|
||||||
@@ -564,7 +570,7 @@ int ble_ots_server_obj_data_read(ble_ots_obj_id_t object_id, uint32_t offset,
|
|||||||
rc = BLE_HS_ENOENT;
|
rc = BLE_HS_ENOENT;
|
||||||
} else if (offset + length > obj->current_size) {
|
} else if (offset + length > obj->current_size) {
|
||||||
rc = BLE_HS_EINVAL;
|
rc = BLE_HS_EINVAL;
|
||||||
} else if (obj->data) {
|
} else if (obj->data && length > 0) {
|
||||||
memcpy(buf, obj->data + offset, length);
|
memcpy(buf, obj->data + offset, length);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -407,6 +407,14 @@ void ble_ots_server_otc_receive_cb(uint16_t conn_handle, struct ble_l2cap_chan *
|
|||||||
int write_err = 0;
|
int write_err = 0;
|
||||||
while (cur != NULL && written < data_len) {
|
while (cur != NULL && written < data_len) {
|
||||||
if (cur->om_len > 0) {
|
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,
|
int rc = ble_ots_server_obj_data_write(cs->transfer_object_id,
|
||||||
write_offset + written,
|
write_offset + written,
|
||||||
cur->om_data, cur->om_len);
|
cur->om_data, cur->om_len);
|
||||||
|
|||||||
Reference in New Issue
Block a user