mirror of
https://github.com/espressif/esp-idf.git
synced 2026-09-22 13:01:16 +03:00
feat(ble/nimble): support ble esl profile
This commit is contained in:
@@ -16,5 +16,10 @@ add_subdirectory(ble_ots)
|
||||
list(APPEND _srcs ${ble_ots_srcs})
|
||||
list(APPEND _include_dirs ${ble_ots_include_dirs})
|
||||
|
||||
# Electronic Shelf Label (pulls in OTS via Kconfig select)
|
||||
add_subdirectory(ble_esl)
|
||||
list(APPEND _srcs ${ble_esl_srcs})
|
||||
list(APPEND _include_dirs ${ble_esl_include_dirs})
|
||||
|
||||
set(nimble_profiles_srcs "${_srcs}" PARENT_SCOPE)
|
||||
set(nimble_profiles_include_dirs "${_include_dirs}" PARENT_SCOPE)
|
||||
|
||||
@@ -7,3 +7,6 @@
|
||||
|
||||
# Object Transfer Service
|
||||
source "$IDF_PATH/components/bt/ble_profiles/nimble/ble_ots/Kconfig.in"
|
||||
|
||||
# Electronic Shelf Label
|
||||
source "$IDF_PATH/components/bt/ble_profiles/nimble/ble_esl/Kconfig.in"
|
||||
|
||||
44
components/bt/ble_profiles/nimble/ble_esl/CMakeLists.txt
Normal file
44
components/bt/ble_profiles/nimble/ble_esl/CMakeLists.txt
Normal file
@@ -0,0 +1,44 @@
|
||||
# Electronic Shelf Label (ESL) profile core for the NimBLE host.
|
||||
#
|
||||
# This directory is not a standalone IDF component: it is pulled in by
|
||||
# components/bt/ble_profiles/nimble/CMakeLists.txt via add_subdirectory() and
|
||||
# exports its sources and public include directory to the parent scope, which
|
||||
# forwards them to the bt component.
|
||||
#
|
||||
|
||||
set(ble_esl_srcs "" PARENT_SCOPE)
|
||||
set(ble_esl_include_dirs "" PARENT_SCOPE)
|
||||
|
||||
if(NOT CONFIG_BLE_ESL_ENABLED)
|
||||
return()
|
||||
endif()
|
||||
|
||||
set(_srcs "${CMAKE_CURRENT_LIST_DIR}/src/common/esl_common.c")
|
||||
|
||||
# The role-private headers (src/<role>/*_int.h) sit next to the sources that
|
||||
# include them, so only the public include/ has to be exported.
|
||||
set(_include_dirs "${CMAKE_CURRENT_LIST_DIR}/include")
|
||||
|
||||
if(CONFIG_BLE_ESL_TAG_ROLE)
|
||||
list(APPEND _srcs
|
||||
"${CMAKE_CURRENT_LIST_DIR}/src/esl/esl_state.c"
|
||||
"${CMAKE_CURRENT_LIST_DIR}/src/esl/esl_cmd_display.c"
|
||||
"${CMAKE_CURRENT_LIST_DIR}/src/esl/esl_cmd_led.c"
|
||||
"${CMAKE_CURRENT_LIST_DIR}/src/esl/esl_cmd_lifecycle.c"
|
||||
"${CMAKE_CURRENT_LIST_DIR}/src/esl/esl_pawr.c"
|
||||
"${CMAKE_CURRENT_LIST_DIR}/src/esl/esl_gatts.c"
|
||||
)
|
||||
endif()
|
||||
|
||||
if(CONFIG_BLE_ESL_AP_ROLE)
|
||||
list(APPEND _srcs
|
||||
"${CMAKE_CURRENT_LIST_DIR}/src/ap/ap_connection.c"
|
||||
"${CMAKE_CURRENT_LIST_DIR}/src/ap/ap_pawr.c"
|
||||
"${CMAKE_CURRENT_LIST_DIR}/src/ap/ap_lifecycle.c"
|
||||
"${CMAKE_CURRENT_LIST_DIR}/src/ap/ap_command.c"
|
||||
"${CMAKE_CURRENT_LIST_DIR}/src/ap/ap_gattc.c"
|
||||
)
|
||||
endif()
|
||||
|
||||
set(ble_esl_srcs "${_srcs}" PARENT_SCOPE)
|
||||
set(ble_esl_include_dirs "${_include_dirs}" PARENT_SCOPE)
|
||||
146
components/bt/ble_profiles/nimble/ble_esl/Kconfig.in
Normal file
146
components/bt/ble_profiles/nimble/ble_esl/Kconfig.in
Normal file
@@ -0,0 +1,146 @@
|
||||
menu "Electronic Shelf Label (ESL) (EXPERIMENTAL)"
|
||||
|
||||
config BLE_ESL_ENABLED
|
||||
bool "Enable Electronic Shelf Label (ESL) (EXPERIMENTAL)"
|
||||
depends on BT_NIMBLE_ENABLED
|
||||
depends on IDF_EXPERIMENTAL_FEATURES
|
||||
default n
|
||||
help
|
||||
Build the Electronic Shelf Label profile core on top of the NimBLE
|
||||
host. The Tag and Access Point roles are selected separately below,
|
||||
so a device only pays for the role it actually implements.
|
||||
|
||||
Both roles exchange commands over Periodic Advertising with
|
||||
Responses (PAwR). Display images are transferred over the Object
|
||||
Transfer Service, which is selected automatically when needed.
|
||||
|
||||
Note: this profile is experimental. Its public API, Kconfig option
|
||||
names and event layout may change without notice in future releases,
|
||||
and it is not recommended for production use yet. Enable
|
||||
IDF_EXPERIMENTAL_FEATURES to make this option selectable.
|
||||
|
||||
config BLE_ESL_TAG_ROLE
|
||||
bool "Enable ESL Tag role"
|
||||
depends on BLE_ESL_ENABLED
|
||||
default n
|
||||
help
|
||||
Enable the Electronic Shelf Label (ESL) Tag role.
|
||||
When enabled, the device can operate as an ESL tag that
|
||||
receives commands from an Access Point over PAwR.
|
||||
|
||||
config BLE_ESL_AP_ROLE
|
||||
bool "Enable ESL Access Point role"
|
||||
depends on BLE_ESL_ENABLED
|
||||
default n
|
||||
select BLE_OTS_ENABLED
|
||||
select BLE_OTS_CLIENT_ENABLED
|
||||
help
|
||||
Enable the Electronic Shelf Label (ESL) Access Point role.
|
||||
When enabled, the device can operate as an AP that manages
|
||||
and synchronises ESL tags via PAwR. The AP always pulls in
|
||||
the OTS client, which it uses to push images to the tags.
|
||||
|
||||
menu "Access Point Configuration"
|
||||
depends on BLE_ESL_AP_ROLE
|
||||
|
||||
config BLE_ESL_AP_MAX_CONNECTIONS
|
||||
int "Maximum simultaneous ACL connections"
|
||||
default 8
|
||||
range 1 BT_NIMBLE_MAX_CONNECTIONS
|
||||
help
|
||||
Maximum number of simultaneous ACL connections to ESL
|
||||
devices. Increasing this value allows the AP to configure
|
||||
more tags in parallel but consumes additional memory and
|
||||
radio time. Must not exceed the NimBLE connection limit.
|
||||
|
||||
config BLE_ESL_AP_MAX_ESLS
|
||||
int "Maximum number of tracked ESLs"
|
||||
default 100
|
||||
range 1 65535
|
||||
help
|
||||
Maximum number of ESL tags tracked across all states
|
||||
(unassociated, synchronising, synchronised, etc.).
|
||||
Increase this if the deployment manages a large number
|
||||
of tags. Each tracked ESL consumes a small amount of
|
||||
heap memory.
|
||||
|
||||
endmenu
|
||||
|
||||
menu "Tag Configuration"
|
||||
depends on BLE_ESL_TAG_ROLE
|
||||
|
||||
config BLE_ESL_MAX_DISPLAYS
|
||||
int "Maximum number of displays"
|
||||
default 1
|
||||
range 0 255
|
||||
help
|
||||
Maximum number of display devices exposed by the ESL tag.
|
||||
Set this to match the actual number of e-ink or other
|
||||
displays attached to the hardware.
|
||||
|
||||
config BLE_ESL_MAX_IMAGES
|
||||
int "Maximum number of stored images"
|
||||
default 4
|
||||
range 1 255
|
||||
help
|
||||
Maximum number of images that can be stored on the tag.
|
||||
This determines the number of OTS objects available for
|
||||
image transfer. Increasing this value requires more
|
||||
storage space (flash or RAM).
|
||||
|
||||
config BLE_ESL_MAX_LEDS
|
||||
int "Maximum number of LEDs"
|
||||
default 1
|
||||
range 0 255
|
||||
help
|
||||
Maximum number of LEDs exposed by the ESL tag. Set this
|
||||
to match the actual number of controllable LEDs on the
|
||||
hardware.
|
||||
|
||||
config BLE_ESL_MAX_SENSORS
|
||||
int "Maximum number of sensors"
|
||||
default 1
|
||||
range 0 255
|
||||
help
|
||||
Maximum number of sensors exposed by the ESL tag (e.g.
|
||||
temperature, battery voltage). Set this to match the
|
||||
actual number of sensors available on the hardware.
|
||||
|
||||
config BLE_ESL_OTS_SUPPORT
|
||||
bool "Enable OTP Object Server for image storage"
|
||||
default y
|
||||
select BLE_OTS_ENABLED
|
||||
select BLE_OTS_SERVER_ENABLED
|
||||
help
|
||||
Enable the Object Transfer Protocol (OTP) server so the
|
||||
AP can push display images to this tag over GATT. Disable
|
||||
this only if the tag has no display or images are
|
||||
provisioned by other means.
|
||||
|
||||
config BLE_ESL_MAX_IMAGE_SIZE
|
||||
int "Maximum image size (bytes)"
|
||||
depends on BLE_ESL_OTS_SUPPORT
|
||||
default 4096
|
||||
range 1 4194304
|
||||
help
|
||||
Maximum size in octets reserved for each stored image
|
||||
object (its OTS Allocated Size). An OACP Write is rejected
|
||||
with Invalid Parameter when Offset + Length exceeds this
|
||||
value, since the tag does not support the OACP Append
|
||||
feature. Set this to the largest image the display can
|
||||
hold. The underlying data buffer is still allocated on
|
||||
demand, so this value only sets the accepted write limit.
|
||||
|
||||
config BLE_ESL_VENDOR_SPECIFIC
|
||||
bool "Enable vendor-specific opcode support"
|
||||
default n
|
||||
help
|
||||
Enable handling of vendor-specific ESL opcodes. When
|
||||
enabled, the tag advertises a PnP ID in the Device
|
||||
Information Service (DIS) and accepts vendor-defined
|
||||
commands. Enable this if custom, manufacturer-specific
|
||||
ESL operations are required.
|
||||
|
||||
endmenu
|
||||
|
||||
endmenu
|
||||
325
components/bt/ble_profiles/nimble/ble_esl/include/ble_esl.h
Normal file
325
components/bt/ble_profiles/nimble/ble_esl/include/ble_esl.h
Normal file
@@ -0,0 +1,325 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
/**
|
||||
* @file ble_esl.h
|
||||
* @brief Electronic Shelf Label (ESL) Profile — ESL Role (Server / Peripheral)
|
||||
*
|
||||
* Public API for the ESL component. Manages the five-state ESL state machine,
|
||||
* ESL Service GATT database, command processing (ECP and PAwR), display/LED/sensor
|
||||
* element management, OTP Object Server for image storage, and LE Secure Connections
|
||||
* security requirements.
|
||||
*/
|
||||
|
||||
#ifndef BLE_ESL_H
|
||||
#define BLE_ESL_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
#include "esp_err.h"
|
||||
#include "ble_esl_common.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* ========================== Kconfig Defaults ========================== */
|
||||
|
||||
#ifndef CONFIG_BLE_ESL_MAX_DISPLAYS
|
||||
#define CONFIG_BLE_ESL_MAX_DISPLAYS 1 /*!< Maximum number of displays */
|
||||
#endif
|
||||
|
||||
#ifndef CONFIG_BLE_ESL_MAX_IMAGES
|
||||
#define CONFIG_BLE_ESL_MAX_IMAGES 4 /*!< Maximum number of stored images */
|
||||
#endif
|
||||
|
||||
#ifndef CONFIG_BLE_ESL_MAX_LEDS
|
||||
#define CONFIG_BLE_ESL_MAX_LEDS 1 /*!< Maximum number of LEDs */
|
||||
#endif
|
||||
|
||||
#ifndef CONFIG_BLE_ESL_MAX_SENSORS
|
||||
#define CONFIG_BLE_ESL_MAX_SENSORS 1 /*!< Maximum number of sensors */
|
||||
#endif
|
||||
|
||||
#ifndef CONFIG_BLE_ESL_OTS_SUPPORT
|
||||
#define CONFIG_BLE_ESL_OTS_SUPPORT 1 /*!< Enable OTS Object Server for image storage */
|
||||
#endif
|
||||
|
||||
#ifndef CONFIG_BLE_ESL_MAX_IMAGE_SIZE
|
||||
#define CONFIG_BLE_ESL_MAX_IMAGE_SIZE 4096 /*!< Allocated Size reserved per image object (bytes) */
|
||||
#endif
|
||||
|
||||
#ifndef CONFIG_BLE_ESL_VENDOR_SPECIFIC
|
||||
#define CONFIG_BLE_ESL_VENDOR_SPECIFIC 0 /*!< Enable vendor-specific opcode support */
|
||||
#endif
|
||||
|
||||
/* ========================== Configuration ========================== */
|
||||
|
||||
/**
|
||||
* @brief ESL component configuration
|
||||
*
|
||||
* Passed to ble_esl_init() to configure element counts, display/LED/sensor
|
||||
* information, image storage, and vendor-specific support.
|
||||
*
|
||||
* @note The array members carry no explicit length field (except @c sensor_info,
|
||||
* which is paired with @c sensor_info_len). Their length is implied by the
|
||||
* matching count member and CANNOT be validated by ble_esl_init() — the
|
||||
* caller MUST size each array as documented below:
|
||||
* - @c display_info : num_displays entries
|
||||
* - @c image_writable_mask : num_images entries
|
||||
* - @c led_info : num_leds bytes
|
||||
* All arrays must stay valid for the lifetime of the ESL component, as
|
||||
* only the pointers are stored.
|
||||
*/
|
||||
typedef struct {
|
||||
uint8_t num_displays; /*!< Number of displays supported (0 to CONFIG_BLE_ESL_MAX_DISPLAYS) */
|
||||
uint8_t num_images; /*!< Max storable images, i.e. Max_Image_Index + 1 (0 to CONFIG_BLE_ESL_MAX_IMAGES) */
|
||||
uint8_t num_leds; /*!< Number of LEDs supported (0 to CONFIG_BLE_ESL_MAX_LEDS) */
|
||||
uint8_t num_sensors; /*!< Number of sensors supported (0 to CONFIG_BLE_ESL_MAX_SENSORS) */
|
||||
const ble_esl_display_info_t *display_info; /*!< Display Data array (num_displays entries), NULL if 0 displays */
|
||||
const bool *image_writable_mask; /*!< Per-image writable flag array (num_images entries); NULL means every image is read-only (static/pre-loaded) */
|
||||
const uint8_t *led_info; /*!< Pre-built LED Information array (1 byte each), NULL if 0 leds */
|
||||
const uint8_t *sensor_info; /*!< Concatenated Sensor Information structures, NULL if 0 sensors */
|
||||
uint16_t sensor_info_len; /*!< Total byte length of sensor_info array */
|
||||
bool vendor_specific; /*!< True to register DIS PnP ID characteristic */
|
||||
const uint8_t *pnp_id; /*!< PnP ID value (7 octets), required when vendor_specific */
|
||||
} ble_esl_config_t;
|
||||
|
||||
/* ========================== Event Types ========================== */
|
||||
|
||||
/**
|
||||
* @brief ESL event types dispatched through the application callback
|
||||
*/
|
||||
typedef enum {
|
||||
BLE_ESL_EVT_STATE_CHANGED = 0, /*!< State transition occurred */
|
||||
BLE_ESL_EVT_IMAGE_WRITE, /*!< Image data received via OTS */
|
||||
BLE_ESL_EVT_DISPLAY_IMAGE, /*!< Display a stored image */
|
||||
BLE_ESL_EVT_REFRESH_DISPLAY, /*!< Refresh the current display image */
|
||||
BLE_ESL_EVT_SENSOR_READ, /*!< Read sensor data request */
|
||||
BLE_ESL_EVT_LED_CONTROL, /*!< LED control request */
|
||||
BLE_ESL_EVT_FACTORY_RESET, /*!< Factory reset initiated */
|
||||
BLE_ESL_EVT_UNASSOCIATE, /*!< Unassociation initiated */
|
||||
} ble_esl_event_t;
|
||||
|
||||
/* ========================== Event Data Structures ========================== */
|
||||
|
||||
/**
|
||||
* @brief Event data for BLE_ESL_EVT_STATE_CHANGED
|
||||
*/
|
||||
typedef struct {
|
||||
ble_esl_state_t prev_state; /*!< Previous ESL state */
|
||||
ble_esl_state_t new_state; /*!< New ESL state */
|
||||
} ble_esl_state_changed_evt_param_t;
|
||||
|
||||
/**
|
||||
* @brief Event data for BLE_ESL_EVT_IMAGE_WRITE
|
||||
*/
|
||||
typedef struct {
|
||||
uint8_t image_index; /*!< Image storage index (0 to Max_Image_Index) */
|
||||
const uint8_t *data; /*!< Pointer to received image data */
|
||||
uint32_t length; /*!< Length of image data in bytes */
|
||||
uint32_t offset; /*!< Write offset within the image object */
|
||||
} ble_esl_image_write_evt_param_t;
|
||||
|
||||
/**
|
||||
* @brief Event data for BLE_ESL_EVT_DISPLAY_IMAGE
|
||||
*/
|
||||
typedef struct {
|
||||
uint8_t display_index; /*!< Index of the target display (0-based) */
|
||||
uint8_t image_index; /*!< Index of the stored image to display (0-based) */
|
||||
} ble_esl_display_image_evt_param_t;
|
||||
|
||||
/**
|
||||
* @brief Event data for BLE_ESL_EVT_REFRESH_DISPLAY
|
||||
*/
|
||||
typedef struct {
|
||||
uint8_t display_index; /*!< Index of the display to refresh (0-based) */
|
||||
} ble_esl_refresh_display_evt_param_t;
|
||||
|
||||
/**
|
||||
* @brief Event data for BLE_ESL_EVT_SENSOR_READ
|
||||
*/
|
||||
typedef struct {
|
||||
uint8_t sensor_index; /*!< Index of the sensor to read (0-based) */
|
||||
} ble_esl_sensor_read_evt_param_t;
|
||||
|
||||
/**
|
||||
* @brief Event data for BLE_ESL_EVT_LED_CONTROL
|
||||
*
|
||||
* Carries all LED control parameters to the application callback.
|
||||
*/
|
||||
typedef struct {
|
||||
uint8_t led_index; /*!< Target LED index (0-based) */
|
||||
uint8_t color_red; /*!< 2-bit red component (0–3) */
|
||||
uint8_t color_green; /*!< 2-bit green component (0–3) */
|
||||
uint8_t color_blue; /*!< 2-bit blue component (0–3) */
|
||||
uint8_t brightness; /*!< 2-bit brightness (0=25%, 1=50%, 2=75%, 3=100%) */
|
||||
uint64_t pattern; /*!< 40-bit flashing pattern (lower 40 bits) */
|
||||
uint8_t bit_off_period; /*!< Off-time per bit = value × 2 ms */
|
||||
uint8_t bit_on_period; /*!< On-time per bit = value × 2 ms */
|
||||
uint8_t repeat_type; /*!< 0 = count, 1 = time duration */
|
||||
uint16_t repeats_duration; /*!< 15-bit repetition count or duration (seconds) */
|
||||
bool is_off; /*!< True = LED should be turned off */
|
||||
} ble_esl_led_control_evt_param_t;
|
||||
|
||||
/**
|
||||
* @brief Union of all ESL event data structures
|
||||
*
|
||||
* Discriminated by the ble_esl_event_t argument in the callback.
|
||||
* FACTORY_RESET and UNASSOCIATE events carry no data (param is NULL).
|
||||
*/
|
||||
typedef union {
|
||||
ble_esl_state_changed_evt_param_t state_changed; /*!< BLE_ESL_EVT_STATE_CHANGED */
|
||||
ble_esl_image_write_evt_param_t image_write; /*!< BLE_ESL_EVT_IMAGE_WRITE */
|
||||
ble_esl_display_image_evt_param_t display_image; /*!< BLE_ESL_EVT_DISPLAY_IMAGE */
|
||||
ble_esl_refresh_display_evt_param_t refresh_display; /*!< BLE_ESL_EVT_REFRESH_DISPLAY */
|
||||
ble_esl_sensor_read_evt_param_t sensor_read; /*!< BLE_ESL_EVT_SENSOR_READ */
|
||||
ble_esl_led_control_evt_param_t led_control; /*!< BLE_ESL_EVT_LED_CONTROL */
|
||||
} ble_esl_cb_param_t;
|
||||
|
||||
/**
|
||||
* @brief ESL application event callback type
|
||||
*
|
||||
* @param event Event type identifier
|
||||
* @param param Pointer to event-specific data (may be NULL for events with no data)
|
||||
*/
|
||||
typedef void (*ble_esl_cb_t)(ble_esl_event_t event, ble_esl_cb_param_t *param);
|
||||
|
||||
/* ========================== Public APIs ========================== */
|
||||
|
||||
/**
|
||||
* @brief Initialize the ESL component
|
||||
*
|
||||
* Registers the ESL Service GATT database, initializes the OTS Object Server
|
||||
* (if enabled and images > 0), registers DIS PnP ID if vendor_specific is true,
|
||||
* and sets the state machine to Unassociated. Does not start advertising.
|
||||
*
|
||||
* @note This function modifies the GLOBAL NimBLE Security Manager configuration
|
||||
* (@c ble_hs_cfg.sm_io_cap, @c sm_bonding, @c sm_mitm, @c sm_sc,
|
||||
* @c sm_our_key_dist, @c sm_their_key_dist) to the settings the ESL Profile
|
||||
* mandates: LE Secure Connections, bonding, No Input No Output, no MITM.
|
||||
* These settings are host-wide and therefore affect all other profiles and
|
||||
* connections. The previous values are not saved and ble_esl_deinit() does
|
||||
* not restore them. An application requiring different SM settings must
|
||||
* apply them after this function returns.
|
||||
*
|
||||
* @param[in] config Pointer to ESL configuration structure
|
||||
*
|
||||
* @return
|
||||
* - ESP_OK on success
|
||||
* - ESP_ERR_INVALID_ARG if config is invalid
|
||||
* - ESP_ERR_NO_MEM on allocation failure
|
||||
* - ESP_ERR_INVALID_STATE if already initialized
|
||||
*/
|
||||
esp_err_t ble_esl_init(const ble_esl_config_t *config);
|
||||
|
||||
/**
|
||||
* @brief Deinitialize the ESL component
|
||||
*
|
||||
* Stops advertising, disconnects, removes the GATT database, tears down OTS
|
||||
* objects, releases all allocated resources, and resets internal state.
|
||||
*
|
||||
* @note The global NimBLE Security Manager settings applied by ble_esl_init()
|
||||
* are left in place; reconfigure @c ble_hs_cfg explicitly if needed.
|
||||
*
|
||||
* @return
|
||||
* - ESP_OK on success
|
||||
* - ESP_ERR_INVALID_STATE if not initialized
|
||||
*/
|
||||
esp_err_t ble_esl_deinit(void);
|
||||
|
||||
/**
|
||||
* @brief Start ESL operation
|
||||
*
|
||||
* Enters the GAP advertising mode appropriate for the current state.
|
||||
* Must be called after ble_esl_init().
|
||||
*
|
||||
* @return
|
||||
* - ESP_OK on success
|
||||
* - ESP_ERR_INVALID_STATE if not initialized or already started
|
||||
*/
|
||||
esp_err_t ble_esl_start(void);
|
||||
|
||||
/**
|
||||
* @brief Stop ESL operation
|
||||
*
|
||||
* Stops advertising, disconnects, and stops PAwR synchronization if active.
|
||||
* The ESL state is preserved.
|
||||
*
|
||||
* @return
|
||||
* - ESP_OK on success
|
||||
* - ESP_ERR_INVALID_STATE if not started
|
||||
* - ESP_FAIL if the PAwR sync could not be terminated; the ESL is still
|
||||
* stopped (advertising off, timers disarmed) and the sync is torn down on
|
||||
* the next state transition or on ble_esl_deinit()
|
||||
*/
|
||||
esp_err_t ble_esl_stop(void);
|
||||
|
||||
/**
|
||||
* @brief Register the application event callback
|
||||
*
|
||||
* All ESL events are dispatched through this single callback.
|
||||
* Must be called after ble_esl_init() and before ble_esl_start().
|
||||
*
|
||||
* @param[in] callback Application event callback function
|
||||
*
|
||||
* @return
|
||||
* - ESP_OK on success
|
||||
* - ESP_ERR_INVALID_ARG if callback is NULL
|
||||
* - ESP_ERR_INVALID_STATE if not initialized
|
||||
*/
|
||||
esp_err_t ble_esl_register_cb(ble_esl_cb_t callback);
|
||||
|
||||
/**
|
||||
* @brief Get the current ESL state
|
||||
*
|
||||
* Synchronous. Returns a snapshot of the state machine; the state is only
|
||||
* changed from the NimBLE host task, so the value may already be stale when
|
||||
* read from another task. Use BLE_ESL_EVT_STATE_CHANGED to react to changes.
|
||||
*
|
||||
* @return Current ESL state; BLE_ESL_STATE_UNASSOCIATED if not initialized
|
||||
*/
|
||||
ble_esl_state_t ble_esl_get_state(void);
|
||||
|
||||
/**
|
||||
* @brief Set or clear the Service Needed flag
|
||||
*
|
||||
* Controls bit 0 of the Basic State bitmap. Set when the application detects
|
||||
* a condition requiring AP attention (e.g., low battery, display fault).
|
||||
*
|
||||
* @param[in] flag True to set Service Needed, false to clear it
|
||||
*
|
||||
* @return
|
||||
* - ESP_OK on success
|
||||
* - ESP_ERR_INVALID_STATE if not initialized
|
||||
*/
|
||||
esp_err_t ble_esl_set_service_needed(bool flag);
|
||||
|
||||
/**
|
||||
* @brief Report sensor data in response to a BLE_ESL_EVT_SENSOR_READ event
|
||||
*
|
||||
* Must be called once per BLE_ESL_EVT_SENSOR_READ event. On success
|
||||
* (error_code = 0), a Sensor Value response is sent. On failure, an Error
|
||||
* response with the given error code is sent.
|
||||
*
|
||||
* @param[in] sensor_index Index of the sensor (must match the event's sensor_index)
|
||||
* @param[in] error_code 0 = success; non-zero = ESL error code
|
||||
* @param[in] data Pointer to sensor data (ignored when error_code != 0)
|
||||
* @param[in] data_len Length of sensor data (1–15, ignored when error_code != 0)
|
||||
*
|
||||
* @return
|
||||
* - ESP_OK on success
|
||||
* - ESP_ERR_INVALID_ARG if data is NULL or data_len invalid when error_code = 0
|
||||
* - ESP_ERR_INVALID_STATE if no sensor read is pending for sensor_index
|
||||
*/
|
||||
esp_err_t ble_esl_report_sensor_data(uint8_t sensor_index, uint8_t error_code,
|
||||
const uint8_t *data, uint8_t data_len);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* BLE_ESL_H */
|
||||
651
components/bt/ble_profiles/nimble/ble_esl/include/ble_esl_ap.h
Normal file
651
components/bt/ble_profiles/nimble/ble_esl/include/ble_esl_ap.h
Normal file
@@ -0,0 +1,651 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
/**
|
||||
* @file ble_esl_ap.h
|
||||
* @brief Electronic Shelf Label (ESL) Profile — Access Point (AP) Public API
|
||||
*
|
||||
* Implements the GATT Client / GAP Central role of the ESL Profile v1.0.1.
|
||||
* Manages ESL scanning, connection, configuration, PAwR broadcasting,
|
||||
* command dispatch, and per-ESL lifecycle state tracking.
|
||||
*/
|
||||
|
||||
#ifndef BLE_ESL_AP_H
|
||||
#define BLE_ESL_AP_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
#include "esp_err.h"
|
||||
#include "ble_esl_common.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* ========================== Kconfig Defaults ========================== */
|
||||
|
||||
#ifndef CONFIG_BLE_ESL_AP_MAX_CONNECTIONS
|
||||
#define CONFIG_BLE_ESL_AP_MAX_CONNECTIONS 8 /*!< Max simultaneous ACL connections */
|
||||
#endif
|
||||
|
||||
#ifndef CONFIG_BLE_ESL_AP_MAX_ESLS
|
||||
#define CONFIG_BLE_ESL_AP_MAX_ESLS 100 /*!< Max tracked ESLs across all states */
|
||||
#endif
|
||||
|
||||
/* ========================== Event Types ========================== */
|
||||
|
||||
/**
|
||||
* @brief AP event types delivered through the application callback
|
||||
*/
|
||||
typedef enum {
|
||||
/* Connection events (ap_connection.c) */
|
||||
BLE_ESL_AP_EVT_SCAN_RESULT = 0, /*!< ESL discovered during scanning */
|
||||
BLE_ESL_AP_EVT_CONNECTED, /*!< ACL connected + service discovery done */
|
||||
BLE_ESL_AP_EVT_DISCONNECTED, /*!< ACL disconnected or connection failed */
|
||||
/* Lifecycle events (ap_lifecycle.c) */
|
||||
BLE_ESL_AP_EVT_ESL_INFO, /*!< ESL info characteristics read complete */
|
||||
BLE_ESL_AP_EVT_CONFIGURED, /*!< ESL configuration write complete */
|
||||
BLE_ESL_AP_EVT_IMAGE_TRANSFERRED, /*!< Image transfer via OTS complete */
|
||||
BLE_ESL_AP_EVT_SYNCHRONIZED, /*!< ESL transitioned to Synchronized state */
|
||||
BLE_ESL_AP_EVT_STATE_CHANGED, /*!< ESL tracked state changed */
|
||||
/* Command events (ap_command.c) */
|
||||
BLE_ESL_AP_EVT_RESPONSE, /*!< Command response received from ESL */
|
||||
BLE_ESL_AP_EVT_CMD_TIMEOUT, /*!< ECP procedure timeout (30 seconds) */
|
||||
} ble_esl_ap_evt_t;
|
||||
|
||||
/* ========================== Response Types ========================== */
|
||||
|
||||
/**
|
||||
* @brief ESL response type identifiers
|
||||
*/
|
||||
typedef enum {
|
||||
ESL_AP_RESP_ERROR = 0, /*!< Error Response (opcode 0x00) */
|
||||
ESL_AP_RESP_LED_STATE, /*!< LED State Response (opcode 0x01) */
|
||||
ESL_AP_RESP_BASIC_STATE, /*!< Basic State Response (opcode 0x10) */
|
||||
ESL_AP_RESP_DISPLAY_STATE, /*!< Display State Response (opcode 0x11) */
|
||||
ESL_AP_RESP_SENSOR_VALUE, /*!< Sensor Value Response (opcode 0x_E) */
|
||||
ESL_AP_RESP_VENDOR, /*!< Vendor-specific Response (opcode 0x_F) */
|
||||
} esl_ap_response_type_t;
|
||||
|
||||
/* ========================== Callback Type ========================== */
|
||||
|
||||
/**
|
||||
* @brief Application event callback type
|
||||
*
|
||||
* All AP events are delivered through a single callback. The application
|
||||
* casts @p param to the appropriate struct based on @p event.
|
||||
*
|
||||
* @param event Event type from ble_esl_ap_evt_t
|
||||
* @param param Pointer to event-specific data structure
|
||||
*/
|
||||
typedef void (*ble_esl_ap_cb_t)(ble_esl_ap_evt_t event, void *param);
|
||||
|
||||
/* ========================== Configuration Structures ========================== */
|
||||
|
||||
/**
|
||||
* @brief PAwR timing configuration
|
||||
*/
|
||||
typedef struct {
|
||||
uint16_t periodic_adv_interval_min; /*!< Min PA interval (units: 1.25 ms) */
|
||||
uint16_t periodic_adv_interval_max; /*!< Max PA interval (units: 1.25 ms) */
|
||||
uint8_t num_subevents; /*!< Subevents per interval (1–128) */
|
||||
uint8_t subevent_interval; /*!< Interval between subevents (units: 1.25 ms) */
|
||||
uint8_t response_slot_delay; /*!< Delay to first response slot (units: 1.25 ms) */
|
||||
uint8_t response_slot_spacing; /*!< Response slot spacing (units: 0.125 ms) */
|
||||
uint8_t num_response_slots; /*!< Response slots per subevent (1–255) */
|
||||
} ble_esl_ap_pawr_config_t;
|
||||
|
||||
/**
|
||||
* @brief AP initialization configuration
|
||||
*/
|
||||
typedef struct {
|
||||
ble_esl_ap_cb_t callback; /*!< Application event callback (must not be NULL) */
|
||||
ble_esl_ap_pawr_config_t pawr_config; /*!< PAwR timing parameters */
|
||||
} ble_esl_ap_config_t;
|
||||
|
||||
/* ========================== Connection Event Structures ========================== */
|
||||
|
||||
/**
|
||||
* @brief Scan result event data (BLE_ESL_AP_EVT_SCAN_RESULT)
|
||||
*/
|
||||
typedef struct {
|
||||
uint8_t addr[6]; /*!< BLE address (little-endian) */
|
||||
uint8_t addr_type; /*!< Address type (0 = public, 1 = random) */
|
||||
int8_t rssi; /*!< Received signal strength (dBm) */
|
||||
const uint8_t *adv_data; /*!< Raw advertising data (valid during callback) */
|
||||
uint16_t adv_data_len; /*!< Advertising data length in octets */
|
||||
bool is_associated; /*!< true = Unsynchronized (known), false = Unassociated */
|
||||
} ble_esl_ap_scan_result_t;
|
||||
|
||||
/**
|
||||
* @brief Connected event data (BLE_ESL_AP_EVT_CONNECTED)
|
||||
*
|
||||
* Populated after service and characteristic discovery completes.
|
||||
*/
|
||||
typedef struct {
|
||||
uint16_t conn_handle; /*!< ACL connection handle */
|
||||
uint8_t addr[6]; /*!< BLE address of connected ESL */
|
||||
uint8_t addr_type; /*!< Address type */
|
||||
bool has_display_info; /*!< ESL Display Information (0x2BFA) discovered */
|
||||
bool has_image_info; /*!< ESL Image Information (0x2BFB) discovered */
|
||||
bool has_sensor_info; /*!< ESL Sensor Information (0x2BFC) discovered */
|
||||
bool has_led_info; /*!< ESL LED Information (0x2BFD) discovered */
|
||||
bool has_dis; /*!< Device Information Service discovered */
|
||||
bool has_pnp_id; /*!< PnP ID (0x2A50) discovered within DIS */
|
||||
} ble_esl_ap_conn_info_t;
|
||||
|
||||
/**
|
||||
* @brief Disconnected event data (BLE_ESL_AP_EVT_DISCONNECTED)
|
||||
*/
|
||||
typedef struct {
|
||||
uint16_t conn_handle; /*!< ACL connection handle */
|
||||
uint8_t addr[6]; /*!< BLE address of the ESL */
|
||||
uint8_t reason; /*!< HCI disconnection reason code */
|
||||
} ble_esl_ap_disconn_info_t;
|
||||
|
||||
/* ========================== Lifecycle Structures ========================== */
|
||||
|
||||
/**
|
||||
* @brief ESL configuration parameters for ble_esl_ap_configure()
|
||||
*/
|
||||
typedef struct {
|
||||
uint8_t esl_id; /*!< ESL_ID to assign (0x00–0xFE) */
|
||||
uint8_t group_id; /*!< Group_ID to assign (0x00–0x7F) */
|
||||
ble_esl_key_material_t ap_sync_key; /*!< AP Sync Key Material (session key + IV) */
|
||||
ble_esl_key_material_t resp_key; /*!< ESL Response Key Material (session key + IV) */
|
||||
} ble_esl_ap_esl_config_t;
|
||||
|
||||
/**
|
||||
* @brief ESL info event data (BLE_ESL_AP_EVT_ESL_INFO)
|
||||
*
|
||||
* Populated by ble_esl_ap_read_info(). All pointers are valid only during the
|
||||
* callback invocation.
|
||||
*/
|
||||
typedef struct {
|
||||
uint16_t conn_handle; /*!< Connection handle */
|
||||
esp_err_t status; /*!< Overall read status */
|
||||
uint8_t num_displays; /*!< Number of displays (0 if char absent) */
|
||||
const ble_esl_display_info_t *displays; /*!< Display info array (5 octets each) */
|
||||
uint8_t max_image_index; /*!< Max Image_Index (0xFF if char absent) */
|
||||
uint8_t num_sensors; /*!< Number of sensors (0 if char absent) */
|
||||
const uint8_t *sensors; /*!< Raw sensor info data */
|
||||
uint16_t sensors_len; /*!< Sensor info buffer length */
|
||||
uint8_t num_leds; /*!< Number of LEDs (0 if char absent) */
|
||||
const uint8_t *leds; /*!< LED info octets (1 per LED) */
|
||||
bool has_pnp_id; /*!< PnP ID was successfully read */
|
||||
uint8_t pnp_id[7]; /*!< PnP ID value (valid if has_pnp_id) */
|
||||
} ble_esl_ap_esl_info_t;
|
||||
|
||||
/**
|
||||
* @brief Image transfer parameters for ble_esl_ap_transfer_image()
|
||||
*/
|
||||
typedef struct {
|
||||
uint16_t conn_handle; /*!< Connection handle */
|
||||
uint8_t image_index; /*!< Target Image_Index on the ESL */
|
||||
const uint8_t *data; /*!< Pointer to image data buffer */
|
||||
uint32_t data_len; /*!< Image data length in octets */
|
||||
bool truncate; /*!< If true, set Truncate bit in OTS Mode */
|
||||
} ble_esl_ap_image_transfer_params_t;
|
||||
|
||||
/**
|
||||
* @brief Configured event data (BLE_ESL_AP_EVT_CONFIGURED)
|
||||
*/
|
||||
typedef struct {
|
||||
uint16_t conn_handle; /*!< Connection handle */
|
||||
esp_err_t status; /*!< ESP_OK if all mandatory chars written */
|
||||
} ble_esl_ap_configured_t;
|
||||
|
||||
/**
|
||||
* @brief Image transferred event data (BLE_ESL_AP_EVT_IMAGE_TRANSFERRED)
|
||||
*/
|
||||
typedef struct {
|
||||
uint16_t conn_handle; /*!< Connection handle */
|
||||
uint8_t image_index; /*!< Image index that was transferred */
|
||||
esp_err_t status; /*!< ESP_OK on success */
|
||||
} ble_esl_ap_image_transferred_t;
|
||||
|
||||
/**
|
||||
* @brief Synchronized event data (BLE_ESL_AP_EVT_SYNCHRONIZED)
|
||||
*/
|
||||
typedef struct {
|
||||
ble_esl_address_t esl_addr; /*!< ESL Address (ESL_ID + Group_ID) */
|
||||
esp_err_t status; /*!< ESP_OK on success */
|
||||
} ble_esl_ap_synchronized_t;
|
||||
|
||||
/**
|
||||
* @brief State changed event data (BLE_ESL_AP_EVT_STATE_CHANGED)
|
||||
*/
|
||||
typedef struct {
|
||||
uint16_t conn_handle; /*!< Connection handle (0xFFFF if disconnected) */
|
||||
ble_esl_address_t esl_addr; /*!< ESL Address (ESL_ID + Group_ID) */
|
||||
ble_esl_state_t old_state; /*!< Previous state */
|
||||
ble_esl_state_t new_state; /*!< New state */
|
||||
} ble_esl_ap_state_changed_t;
|
||||
|
||||
/* ========================== Response Data Structures ========================== */
|
||||
|
||||
/**
|
||||
* @brief Parsed 16-bit Basic State bitmap
|
||||
*/
|
||||
typedef struct {
|
||||
bool service_needed; /*!< Bit 0: ESL needs service */
|
||||
bool synchronized; /*!< Bit 1: ESL is synchronized to AP */
|
||||
bool active_led; /*!< Bit 2: One or more LEDs active */
|
||||
bool pending_led_update; /*!< Bit 3: Timed LED command pending */
|
||||
bool pending_display_update; /*!< Bit 4: Timed display command pending */
|
||||
} esl_ap_basic_state_t;
|
||||
|
||||
/**
|
||||
* @brief Error response info
|
||||
*/
|
||||
typedef struct {
|
||||
uint8_t error_code; /*!< ESL error code from BLE_ESL_ERR_* */
|
||||
} esl_ap_error_info_t;
|
||||
|
||||
/**
|
||||
* @brief LED state response info
|
||||
*/
|
||||
typedef struct {
|
||||
uint8_t led_index; /*!< Echo of the commanded LED_Index */
|
||||
} esl_ap_led_state_info_t;
|
||||
|
||||
/**
|
||||
* @brief Display state response info
|
||||
*/
|
||||
typedef struct {
|
||||
uint8_t display_index; /*!< Echo of commanded Display_Index */
|
||||
uint8_t image_index; /*!< Image currently shown on the display */
|
||||
} esl_ap_display_state_info_t;
|
||||
|
||||
/**
|
||||
* @brief Sensor value response info
|
||||
*/
|
||||
typedef struct {
|
||||
uint8_t sensor_index; /*!< Echo of commanded Sensor_Index */
|
||||
const uint8_t *data; /*!< Raw sensor data (valid during callback) */
|
||||
uint8_t data_len; /*!< Sensor data length (0–15 octets) */
|
||||
} esl_ap_sensor_value_info_t;
|
||||
|
||||
/**
|
||||
* @brief Vendor-specific response info
|
||||
*/
|
||||
typedef struct {
|
||||
const uint8_t *data; /*!< Vendor data (valid during callback) */
|
||||
uint8_t data_len; /*!< Data length (1–16 octets) */
|
||||
} esl_ap_vendor_response_info_t;
|
||||
|
||||
/**
|
||||
* @brief Command response delivered via BLE_ESL_AP_EVT_RESPONSE
|
||||
*/
|
||||
typedef struct {
|
||||
uint8_t esl_id; /*!< Responding ESL_ID (0x00–0xFE) */
|
||||
uint8_t group_id; /*!< Responding Group_ID (0x00–0x7F) */
|
||||
esl_ap_response_type_t type; /*!< Response type selector */
|
||||
union {
|
||||
esl_ap_error_info_t error; /*!< Valid when type == ESL_AP_RESP_ERROR */
|
||||
esl_ap_led_state_info_t led_state; /*!< Valid when type == ESL_AP_RESP_LED_STATE */
|
||||
esl_ap_basic_state_t basic_state; /*!< Valid when type == ESL_AP_RESP_BASIC_STATE */
|
||||
esl_ap_display_state_info_t display_state; /*!< Valid when type == ESL_AP_RESP_DISPLAY_STATE */
|
||||
esl_ap_sensor_value_info_t sensor_value; /*!< Valid when type == ESL_AP_RESP_SENSOR_VALUE */
|
||||
esl_ap_vendor_response_info_t vendor; /*!< Valid when type == ESL_AP_RESP_VENDOR */
|
||||
};
|
||||
} esl_ap_response_t;
|
||||
|
||||
/**
|
||||
* @brief Command timeout event data (BLE_ESL_AP_EVT_CMD_TIMEOUT)
|
||||
*/
|
||||
typedef struct {
|
||||
uint8_t esl_id; /*!< ESL_ID of the timed-out ESL */
|
||||
uint8_t group_id; /*!< Group_ID of the timed-out ESL */
|
||||
} esl_ap_cmd_timeout_t;
|
||||
|
||||
/* ========================== Command Parameter Structures ========================== */
|
||||
|
||||
/**
|
||||
* @brief LED control settings for ble_esl_ap_led_control() and ble_esl_ap_led_timed_control()
|
||||
*/
|
||||
typedef struct {
|
||||
uint8_t color_red; /*!< 2-bit red component (0–3) */
|
||||
uint8_t color_green; /*!< 2-bit green component (0–3) */
|
||||
uint8_t color_blue; /*!< 2-bit blue component (0–3) */
|
||||
uint8_t brightness; /*!< 2-bit brightness (0=25%, 1=50%, 2=75%, 3=100%) */
|
||||
uint8_t flashing_pattern[7]; /*!< 56-bit (7-byte) flashing pattern */
|
||||
uint8_t repeat_type; /*!< 0 = count-based, 1 = duration-based */
|
||||
uint16_t repeats_duration; /*!< 15-bit repeat count or duration (seconds) */
|
||||
} esl_ap_led_settings_t;
|
||||
|
||||
/* ========================== Public APIs: Connection ========================== */
|
||||
/* Implemented in ble_esl_ap_connection.c */
|
||||
|
||||
/**
|
||||
* @brief Initialize the AP module
|
||||
*
|
||||
* Registers the application callback, stores PAwR timing parameters,
|
||||
* and allocates internal resources. Must be called before any other API.
|
||||
*
|
||||
* @param config Pointer to AP configuration (callback + PAwR timing)
|
||||
* @return ESP_OK on success; ESP_ERR_INVALID_ARG if config/callback is NULL or
|
||||
* num_subevents is outside 1–128;
|
||||
* ESP_ERR_INVALID_STATE if already initialized
|
||||
*/
|
||||
esp_err_t ble_esl_ap_init(const ble_esl_ap_config_t *config);
|
||||
|
||||
/**
|
||||
* @brief Deinitialize the AP module
|
||||
*
|
||||
* Disconnects all ESLs, stops scanning/PAwR, and frees all resources.
|
||||
*
|
||||
* @note If a connection cannot be terminated, scanning and PAwR are left
|
||||
* stopped, the still connected links stay tracked and ESP_FAIL is
|
||||
* returned; the caller may retry this function.
|
||||
*
|
||||
* @return ESP_OK on success; ESP_ERR_INVALID_STATE if not initialized;
|
||||
* ESP_FAIL if an active connection could not be terminated
|
||||
*/
|
||||
esp_err_t ble_esl_ap_deinit(void);
|
||||
|
||||
/**
|
||||
* @brief Start AP operation (scanning + PAwR broadcasting)
|
||||
*
|
||||
* Begins GAP General Discovery for ESLs and starts PAwR broadcasting.
|
||||
* Discovered ESLs are reported via BLE_ESL_AP_EVT_SCAN_RESULT.
|
||||
*
|
||||
* @note This function does not automatically initiate connections. The caller must handle
|
||||
* scan results and call ble_esl_ap_connect() to establish connections.
|
||||
*
|
||||
* @return ESP_OK on success; ESP_ERR_INVALID_STATE if not initialized or already started
|
||||
*/
|
||||
esp_err_t ble_esl_ap_start(void);
|
||||
|
||||
/**
|
||||
* @brief Stop AP operation (scanning + PAwR broadcasting)
|
||||
*
|
||||
* Existing ACL connections are not affected.
|
||||
*
|
||||
* @return ESP_OK on success; ESP_ERR_INVALID_STATE if not started
|
||||
*/
|
||||
esp_err_t ble_esl_ap_stop(void);
|
||||
|
||||
/**
|
||||
* @brief Initiate ACL connection to an ESL
|
||||
*
|
||||
* Asynchronous — returns immediately.
|
||||
* Result is delivered via BLE_ESL_AP_EVT_CONNECTED on success or
|
||||
* BLE_ESL_AP_EVT_DISCONNECTED on any failure along the way.
|
||||
*
|
||||
* @note This function performs the following steps automatically in sequence:
|
||||
* 1. Initiated ACL connection with peer
|
||||
* 2. Initiated SMP pairing and encryption
|
||||
* 3. Discovery service, characteristic and descriptor (ESL Service, optional DIS, OTS)
|
||||
* 4. Subscribes to indications or notifications
|
||||
*
|
||||
* @param addr 6-byte BLE address of the target ESL
|
||||
* @param addr_type Address type (0 = public, 1 = random)
|
||||
* @return ESP_OK if connection initiation accepted; ESP_ERR_INVALID_ARG if
|
||||
* addr NULL; ESP_ERR_NO_MEM if max connections reached
|
||||
*/
|
||||
esp_err_t ble_esl_ap_connect(const uint8_t *addr, uint8_t addr_type);
|
||||
|
||||
/**
|
||||
* @brief Connect to a Synchronized ESL through the PAwR train
|
||||
*
|
||||
* Implements the Periodic Advertising Connection procedure (Core [Vol 3],
|
||||
* Part C, §9.5.5): the AP sends an AUX_CONNECT_REQ in the ESL's PAwR subevent
|
||||
* (== its Group_ID) to pull an already-Synchronized ESL — which is not
|
||||
* advertising — into an ACL connection without waiting for it to become
|
||||
* connectable. Encryption is restored from the bond's stored LTK, after which
|
||||
* the ESL enters the Updating state.
|
||||
*
|
||||
* Use this to move an ESL from Synchronized to Updating (e.g. for image
|
||||
* transfer or reconfiguration). The ESL must be tracked and currently in the
|
||||
* Synchronized state. Asynchronous — result delivered via
|
||||
* BLE_ESL_AP_EVT_CONNECTED on success (after service discovery) or
|
||||
* BLE_ESL_AP_EVT_DISCONNECTED on failure, mirroring ble_esl_ap_connect().
|
||||
*
|
||||
* @note Requires controller + host support for PAwR connection establishment
|
||||
* (CONFIG_BT_NIMBLE_PERIODIC_ADV_WITH_RESPONSES).
|
||||
*
|
||||
* @param esl_addr ESL Address of the target ESL (see ble_esl_addr_make())
|
||||
* @return ESP_OK if the connection was initiated; ESP_ERR_NOT_FOUND if the ESL
|
||||
* is not tracked; ESP_ERR_INVALID_STATE if it is not Synchronized;
|
||||
* ESP_ERR_NO_MEM if the connection table is full
|
||||
*/
|
||||
esp_err_t ble_esl_ap_connect_synced(ble_esl_address_t esl_addr);
|
||||
|
||||
/**
|
||||
* @brief Disconnect from a connected ESL
|
||||
*
|
||||
* Asynchronous — BLE_ESL_AP_EVT_DISCONNECTED delivered on completion.
|
||||
*
|
||||
* @param conn_handle ACL connection handle
|
||||
* @return ESP_OK if disconnect initiated; ESP_ERR_NOT_FOUND if handle unknown
|
||||
*/
|
||||
esp_err_t ble_esl_ap_disconnect(uint16_t conn_handle);
|
||||
|
||||
/* ========================== Public APIs: Lifecycle ========================== */
|
||||
/* Implemented in ble_esl_ap_lifecycle.c */
|
||||
|
||||
/**
|
||||
* @brief Write ESL configuration characteristics
|
||||
*
|
||||
* Writes ESL Address, AP Sync Key Material, ESL Response Key Material, and
|
||||
* ESL Current Absolute Time in sequence. Async — result via
|
||||
* BLE_ESL_AP_EVT_CONFIGURED.
|
||||
*
|
||||
* @param conn_handle Connection handle of the connected ESL
|
||||
* @param config Pointer to configuration parameters
|
||||
* @return ESP_OK if operation initiated; error code on failure
|
||||
*/
|
||||
esp_err_t ble_esl_ap_configure(uint16_t conn_handle,
|
||||
const ble_esl_ap_esl_config_t *config);
|
||||
|
||||
/**
|
||||
* @brief Read ESL information characteristics
|
||||
*
|
||||
* Reads Display Info, Image Info, Sensor Info, LED Info, and PnP ID
|
||||
* (insofar as present). Async — result via BLE_ESL_AP_EVT_ESL_INFO.
|
||||
*
|
||||
* @param conn_handle Connection handle of the connected ESL
|
||||
* @return ESP_OK if operation initiated; error code on failure
|
||||
*/
|
||||
esp_err_t ble_esl_ap_read_info(uint16_t conn_handle);
|
||||
|
||||
/**
|
||||
* @brief Transfer image data to an ESL via OTS
|
||||
*
|
||||
* Uses OTS Go To + Write Object Contents. Object_ID is derived from
|
||||
* image_index as 0x000000000100 + image_index. Async — result via
|
||||
* BLE_ESL_AP_EVT_IMAGE_TRANSFERRED.
|
||||
*
|
||||
* @param params Pointer to image transfer parameters
|
||||
* @return ESP_OK if operation initiated; error code on failure
|
||||
*/
|
||||
esp_err_t ble_esl_ap_transfer_image(const ble_esl_ap_image_transfer_params_t *params);
|
||||
|
||||
/**
|
||||
* @brief Transition a connected ESL to the Synchronized state
|
||||
*
|
||||
* Sends Update Complete command via ECP, then initiates PAST. ESL
|
||||
* synchronizes to PAwR and disconnects. Async — result via
|
||||
* BLE_ESL_AP_EVT_SYNCHRONIZED.
|
||||
*
|
||||
* @param conn_handle Connection handle of the connected ESL
|
||||
* @return ESP_OK if operation initiated; error code on failure
|
||||
*/
|
||||
esp_err_t ble_esl_ap_synchronize(uint16_t conn_handle);
|
||||
|
||||
/**
|
||||
* @brief Get the AP's tracked state for a specific ESL
|
||||
*
|
||||
* Synchronous. Returns BLE_ESL_STATE_UNASSOCIATED if not found.
|
||||
*
|
||||
* @param esl_addr ESL Address of the ESL (see ble_esl_addr_make())
|
||||
* @return Current tracked state
|
||||
*/
|
||||
ble_esl_state_t ble_esl_ap_get_esl_state(ble_esl_address_t esl_addr);
|
||||
|
||||
/* ========================== Public APIs: Command ========================== */
|
||||
/* Implemented in ble_esl_ap_command.c */
|
||||
|
||||
/**
|
||||
* @brief Send Ping command (opcode 0x00)
|
||||
*
|
||||
* Verifies ESL reachability. Expected response: Basic State.
|
||||
*
|
||||
* @param esl_id Target ESL_ID (0x00–0xFE, or 0xFF for broadcast)
|
||||
* @param group_id Group_ID (0x00–0x7F)
|
||||
* @return ESP_OK on successful dispatch; error code on failure
|
||||
*/
|
||||
esp_err_t ble_esl_ap_ping(uint8_t esl_id, uint8_t group_id);
|
||||
|
||||
/**
|
||||
* @brief Send Display Image command (opcode 0x20)
|
||||
*
|
||||
* Immediately display a stored image. Expected response: Display State.
|
||||
*
|
||||
* @param esl_id Target ESL_ID (0x00–0xFE, or 0xFF for broadcast)
|
||||
* @param group_id Group_ID (0x00–0x7F)
|
||||
* @param display_index Index of the target display
|
||||
* @param image_index Index of the stored image to show
|
||||
* @return ESP_OK on successful dispatch; error code on failure
|
||||
*/
|
||||
esp_err_t ble_esl_ap_display_image(uint8_t esl_id, uint8_t group_id,
|
||||
uint8_t display_index, uint8_t image_index);
|
||||
|
||||
/**
|
||||
* @brief Send Display Timed Image command (opcode 0x60)
|
||||
*
|
||||
* Schedule an image display at a future time. 0x00000000 cancels pending.
|
||||
* Expected response: Display State.
|
||||
*
|
||||
* @param esl_id Target ESL_ID (0x00–0xFE, or 0xFF for broadcast)
|
||||
* @param group_id Group_ID (0x00–0x7F)
|
||||
* @param display_index Index of the target display
|
||||
* @param image_index Index of the stored image
|
||||
* @param absolute_time Scheduled time in ms (0 = cancel)
|
||||
* @return ESP_OK on successful dispatch; error code on failure
|
||||
*/
|
||||
esp_err_t ble_esl_ap_display_timed_image(uint8_t esl_id, uint8_t group_id,
|
||||
uint8_t display_index, uint8_t image_index,
|
||||
uint32_t absolute_time);
|
||||
|
||||
/**
|
||||
* @brief Send Refresh Display command (opcode 0x11)
|
||||
*
|
||||
* Refresh current display without changing image. Expected response: Display State.
|
||||
*
|
||||
* @param esl_id Target ESL_ID (0x00–0xFE, or 0xFF for broadcast)
|
||||
* @param group_id Group_ID (0x00–0x7F)
|
||||
* @param display_index Index of the display to refresh
|
||||
* @return ESP_OK on successful dispatch; error code on failure
|
||||
*/
|
||||
esp_err_t ble_esl_ap_refresh_display(uint8_t esl_id, uint8_t group_id,
|
||||
uint8_t display_index);
|
||||
|
||||
/**
|
||||
* @brief Send LED Control command (opcode 0xB0)
|
||||
*
|
||||
* Immediately control LED color, brightness, and flashing pattern.
|
||||
* Expected response: LED State.
|
||||
*
|
||||
* @param esl_id Target ESL_ID (0x00–0xFE, or 0xFF for broadcast)
|
||||
* @param group_id Group_ID (0x00–0x7F)
|
||||
* @param led_index Index of the target LED
|
||||
* @param settings Pointer to LED control settings
|
||||
* @return ESP_OK on successful dispatch; error code on failure
|
||||
*/
|
||||
esp_err_t ble_esl_ap_led_control(uint8_t esl_id, uint8_t group_id,
|
||||
uint8_t led_index,
|
||||
const esl_ap_led_settings_t *settings);
|
||||
|
||||
/**
|
||||
* @brief Send LED Timed Control command (opcode 0xF0)
|
||||
*
|
||||
* Schedule LED control at a future time. 0x00000000 cancels pending.
|
||||
* Expected response: LED State.
|
||||
*
|
||||
* @param esl_id Target ESL_ID (0x00–0xFE, or 0xFF for broadcast)
|
||||
* @param group_id Group_ID (0x00–0x7F)
|
||||
* @param led_index Index of the target LED
|
||||
* @param settings Pointer to LED control settings
|
||||
* @param absolute_time Scheduled time in ms (0 = cancel)
|
||||
* @return ESP_OK on successful dispatch; error code on failure
|
||||
*/
|
||||
esp_err_t ble_esl_ap_led_timed_control(uint8_t esl_id, uint8_t group_id,
|
||||
uint8_t led_index,
|
||||
const esl_ap_led_settings_t *settings,
|
||||
uint32_t absolute_time);
|
||||
|
||||
/**
|
||||
* @brief Send Read Sensor Data command (opcode 0x10)
|
||||
*
|
||||
* Request sensor data. Expected response: Sensor Value.
|
||||
*
|
||||
* @param esl_id Target ESL_ID (0x00–0xFE, or 0xFF for broadcast)
|
||||
* @param group_id Group_ID (0x00–0x7F)
|
||||
* @param sensor_index Index of the sensor to read
|
||||
* @return ESP_OK on successful dispatch; error code on failure
|
||||
*/
|
||||
esp_err_t ble_esl_ap_read_sensor(uint8_t esl_id, uint8_t group_id,
|
||||
uint8_t sensor_index);
|
||||
|
||||
/**
|
||||
* @brief Send Unassociate from AP command (opcode 0x01)
|
||||
*
|
||||
* Disassociates the ESL. Expected response: Basic State. On success,
|
||||
* the ESL's tracked state transitions to Unassociated.
|
||||
*
|
||||
* @param esl_id Target ESL_ID (0x00–0xFE, or 0xFF for broadcast)
|
||||
* @param group_id Group_ID (0x00–0x7F)
|
||||
* @return ESP_OK on successful dispatch; error code on failure
|
||||
*/
|
||||
esp_err_t ble_esl_ap_unassociate(uint8_t esl_id, uint8_t group_id);
|
||||
|
||||
/**
|
||||
* @brief Send Service Reset command (opcode 0x02)
|
||||
*
|
||||
* Clears the Service Needed flag. Expected response: Basic State.
|
||||
*
|
||||
* @param esl_id Target ESL_ID (0x00–0xFE, or 0xFF for broadcast)
|
||||
* @param group_id Group_ID (0x00–0x7F)
|
||||
* @return ESP_OK on successful dispatch; error code on failure
|
||||
*/
|
||||
esp_err_t ble_esl_ap_service_reset(uint8_t esl_id, uint8_t group_id);
|
||||
|
||||
/**
|
||||
* @brief Send Factory Reset command (opcode 0x03) — ECP only
|
||||
*
|
||||
* Connection-oriented only; returns ESP_ERR_INVALID_STATE if ESL is
|
||||
* in Synchronized state. No response expected; ESL disconnects after.
|
||||
*
|
||||
* @param esl_id Target ESL_ID (0x00–0xFE)
|
||||
* @param group_id Group_ID (0x00–0x7F)
|
||||
* @return ESP_OK on successful dispatch; error code on failure
|
||||
*/
|
||||
esp_err_t ble_esl_ap_factory_reset(uint8_t esl_id, uint8_t group_id);
|
||||
|
||||
/**
|
||||
* @brief Send vendor-specific command (Tag nibble = 0xF)
|
||||
*
|
||||
* Opcode constructed as (params_len << 4) | 0x0F: the Length nibble is derived
|
||||
* internally from params_len, since ESL_ID is prepended as the first parameter
|
||||
* byte and the Length nibble encodes (parameter count - 1).
|
||||
*
|
||||
* @param esl_id Target ESL_ID (0x00–0xFE, or 0xFF for broadcast)
|
||||
* @param group_id Group_ID (0x00–0x7F)
|
||||
* @param params Vendor-specific parameter bytes (ESL_ID prepended internally),
|
||||
* may be NULL only when params_len is 0
|
||||
* @param params_len Length of params (0x00–0x0F)
|
||||
* @return ESP_OK on successful dispatch; error code on failure
|
||||
*/
|
||||
esp_err_t ble_esl_ap_vendor_command(uint8_t esl_id, uint8_t group_id,
|
||||
const uint8_t *params, uint8_t params_len);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* BLE_ESL_AP_H */
|
||||
@@ -0,0 +1,529 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
/**
|
||||
* @file ble_esl_common.h
|
||||
* @brief Electronic Shelf Label (ESL) Service v1.0.1 — Common Protocol Definitions
|
||||
*
|
||||
* Shared protocol artifacts used by both AP (Access Point) and ESL roles:
|
||||
* service/characteristic UUIDs, TLV wire format, command/response opcodes,
|
||||
* error codes, data format structures, and cross-layer encode/decode utilities.
|
||||
*
|
||||
* All multi-byte fields are little-endian. Bit fields within a byte use
|
||||
* LSB-first numbering unless stated otherwise.
|
||||
*/
|
||||
|
||||
#ifndef BLE_ESL_COMMON_H
|
||||
#define BLE_ESL_COMMON_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stddef.h>
|
||||
#include "esp_err.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* ========================== ESL States ========================== */
|
||||
|
||||
/**
|
||||
* @brief ESL state enumeration
|
||||
*/
|
||||
typedef enum {
|
||||
BLE_ESL_STATE_UNASSOCIATED = 0, /*!< ESL not yet configured by an AP */
|
||||
BLE_ESL_STATE_CONFIGURING = 1, /*!< ESL connected and being configured for the first time */
|
||||
BLE_ESL_STATE_SYNCHRONIZED = 2, /*!< ESL receiving commands via PAwR periodic advertising */
|
||||
BLE_ESL_STATE_UPDATING = 3, /*!< ESL connected for reconfiguration / image transfer */
|
||||
BLE_ESL_STATE_UNSYNCHRONIZED = 4, /*!< ESL associated but not connected and not synchronized */
|
||||
} ble_esl_state_t;
|
||||
|
||||
/* ========================== Service & Characteristic UUIDs ========================== */
|
||||
|
||||
#define BLE_ESL_SVC_UUID 0x1857 /*!< Electronic Shelf Label Service UUID */
|
||||
|
||||
#define BLE_ESL_CHR_UUID_ESL_ADDRESS 0x2BF6 /*!< ESL Address characteristic UUID */
|
||||
#define BLE_ESL_CHR_UUID_AP_SYNC_KEY 0x2BF7 /*!< AP Sync Key Material characteristic UUID */
|
||||
#define BLE_ESL_CHR_UUID_RESP_KEY 0x2BF8 /*!< ESL Response Key Material characteristic UUID */
|
||||
#define BLE_ESL_CHR_UUID_CURRENT_ABS_TIME 0x2BF9 /*!< ESL Current Absolute Time characteristic UUID */
|
||||
#define BLE_ESL_CHR_UUID_DISPLAY_INFO 0x2BFA /*!< ESL Display Information characteristic UUID */
|
||||
#define BLE_ESL_CHR_UUID_IMAGE_INFO 0x2BFB /*!< ESL Image Information characteristic UUID */
|
||||
#define BLE_ESL_CHR_UUID_SENSOR_INFO 0x2BFC /*!< ESL Sensor Information characteristic UUID */
|
||||
#define BLE_ESL_CHR_UUID_LED_INFO 0x2BFD /*!< ESL LED Information characteristic UUID */
|
||||
#define BLE_ESL_CHR_UUID_ECP 0x2BFE /*!< ESL Control Point (ECP) characteristic UUID */
|
||||
|
||||
/* ========================== Command Opcodes ========================== */
|
||||
|
||||
#define BLE_ESL_CMD_PING 0x00 /*!< Ping command */
|
||||
#define BLE_ESL_CMD_UNASSOCIATE 0x01 /*!< Unassociate from AP command */
|
||||
#define BLE_ESL_CMD_SERVICE_RESET 0x02 /*!< Service Reset command */
|
||||
#define BLE_ESL_CMD_FACTORY_RESET 0x03 /*!< Factory Reset command */
|
||||
#define BLE_ESL_CMD_UPDATE_COMPLETE 0x04 /*!< Update Complete command */
|
||||
#define BLE_ESL_CMD_READ_SENSOR 0x10 /*!< Read Sensor Data command */
|
||||
#define BLE_ESL_CMD_REFRESH_DISPLAY 0x11 /*!< Refresh Display command */
|
||||
#define BLE_ESL_CMD_DISPLAY_IMAGE 0x20 /*!< Display Image command */
|
||||
#define BLE_ESL_CMD_DISPLAY_TIMED_IMAGE 0x60 /*!< Display Timed Image command */
|
||||
#define BLE_ESL_CMD_LED_CONTROL 0xB0 /*!< LED Control command */
|
||||
#define BLE_ESL_CMD_LED_TIMED_CONTROL 0xF0 /*!< LED Timed Control command */
|
||||
|
||||
/** @brief Vendor-specific command tag nibble. Any opcode with Tag=0xF is vendor-specific. */
|
||||
#define BLE_ESL_CMD_VENDOR_TAG 0x0F
|
||||
|
||||
/** @brief ESL Broadcast Address (addresses all ESLs in a group) */
|
||||
#define BLE_ESL_BROADCAST_ADDRESS 0xFF
|
||||
|
||||
/* ========================== Response Opcodes ========================== */
|
||||
|
||||
#define BLE_ESL_RESP_ERROR 0x00 /*!< Error Response */
|
||||
#define BLE_ESL_RESP_LED_STATE 0x01 /*!< LED State Response */
|
||||
#define BLE_ESL_RESP_BASIC_STATE 0x10 /*!< Basic State Response */
|
||||
#define BLE_ESL_RESP_DISPLAY_STATE 0x11 /*!< Display State Response */
|
||||
|
||||
/** @brief Sensor Value Response tag nibble (0xE). Opcode = (Length << 4) | 0x0E */
|
||||
#define BLE_ESL_RESP_SENSOR_VALUE_TAG 0x0E
|
||||
|
||||
/** @brief Vendor-specific Response tag nibble (0xF). Opcode = (Length << 4) | 0x0F */
|
||||
#define BLE_ESL_RESP_VENDOR_TAG 0x0F
|
||||
|
||||
/* ========================== Error Codes ========================== */
|
||||
|
||||
#define BLE_ESL_ERR_UNSPECIFIED 0x01 /*!< Unspecified Error */
|
||||
#define BLE_ESL_ERR_INVALID_OPCODE 0x02 /*!< Invalid Opcode */
|
||||
#define BLE_ESL_ERR_INVALID_STATE 0x03 /*!< Invalid State */
|
||||
#define BLE_ESL_ERR_INVALID_IMAGE_INDEX 0x04 /*!< Invalid Image_Index */
|
||||
#define BLE_ESL_ERR_IMAGE_NOT_AVAILABLE 0x05 /*!< Image Not Available */
|
||||
#define BLE_ESL_ERR_INVALID_PARAMS 0x06 /*!< Invalid Parameter(s) */
|
||||
#define BLE_ESL_ERR_CAPACITY_LIMIT 0x07 /*!< Capacity Limit */
|
||||
#define BLE_ESL_ERR_INSUFFICIENT_BATTERY 0x08 /*!< Insufficient Battery */
|
||||
#define BLE_ESL_ERR_INSUFFICIENT_RESOURCES 0x09 /*!< Insufficient Resources */
|
||||
#define BLE_ESL_ERR_RETRY 0x0A /*!< Retry */
|
||||
#define BLE_ESL_ERR_QUEUE_FULL 0x0B /*!< Queue Full */
|
||||
#define BLE_ESL_ERR_IMPLAUSIBLE_ABS_TIME 0x0C /*!< Implausible Absolute Time */
|
||||
|
||||
/* ========================== Basic State Bitmap Bits ========================== */
|
||||
|
||||
#define BLE_ESL_BASIC_STATE_SERVICE_NEEDED (1 << 0) /*!< Bit 0: Service Needed */
|
||||
#define BLE_ESL_BASIC_STATE_SYNCHRONIZED (1 << 1) /*!< Bit 1: Synchronized */
|
||||
#define BLE_ESL_BASIC_STATE_ACTIVE_LED (1 << 2) /*!< Bit 2: Active LED */
|
||||
#define BLE_ESL_BASIC_STATE_PENDING_LED_UPDATE (1 << 3) /*!< Bit 3: Pending LED Update */
|
||||
#define BLE_ESL_BASIC_STATE_PENDING_DISP_UPDATE (1 << 4) /*!< Bit 4: Pending Display Update */
|
||||
|
||||
/* ========================== TLV Format Helpers ========================== */
|
||||
|
||||
/** @brief Extract the Tag nibble (bits [3:0]) from an opcode byte */
|
||||
#define BLE_ESL_TLV_TAG(opcode) ((opcode) & 0x0F)
|
||||
|
||||
/** @brief Extract the Length nibble (bits [7:4]) from an opcode byte */
|
||||
#define BLE_ESL_TLV_LENGTH(opcode) (((opcode) >> 4) & 0x0F)
|
||||
|
||||
/** @brief Compute the parameter byte count from an opcode: Length + 1 */
|
||||
#define BLE_ESL_TLV_PARAMS_LEN(opcode) (BLE_ESL_TLV_LENGTH(opcode) + 1)
|
||||
|
||||
/** @brief Compute the total TLV size from an opcode: Length + 2 */
|
||||
#define BLE_ESL_TLV_TOTAL_LEN(opcode) (BLE_ESL_TLV_LENGTH(opcode) + 2)
|
||||
|
||||
/** @brief Build an opcode byte from tag and length nibbles */
|
||||
#define BLE_ESL_TLV_OPCODE(tag, length) ((uint8_t)(((length) << 4) | ((tag) & 0x0F)))
|
||||
|
||||
/** @brief Maximum TLV total size (Length nibble = 0xF → 15+2 = 17) */
|
||||
#define BLE_ESL_TLV_MAX_SIZE 17
|
||||
|
||||
/** @brief Minimum TLV total size (Length nibble = 0x0 → 0+2 = 2) */
|
||||
#define BLE_ESL_TLV_MIN_SIZE 2
|
||||
|
||||
/* ========================== ESL Payload ========================== */
|
||||
|
||||
/** @brief Maximum ESL Payload size in octets */
|
||||
#define BLE_ESL_PAYLOAD_MAX_SIZE 48
|
||||
|
||||
/** @brief Maximum Group_ID value (7-bit field) */
|
||||
#define BLE_ESL_GROUP_ID_MAX 0x7F
|
||||
|
||||
/* ========================== Advertising Data Types ========================== */
|
||||
|
||||
#define BLE_ESL_AD_TYPE_ENCRYPTED_DATA 0x31 /*!< Encrypted Data AD type */
|
||||
#define BLE_ESL_AD_TYPE_ESL 0x34 /*!< ESL data AD type */
|
||||
|
||||
/* ========================== ECP Timeout ========================== */
|
||||
|
||||
/** @brief ECP Procedure Timeout in seconds */
|
||||
#define BLE_ESL_ECP_TIMEOUT_SEC 30
|
||||
|
||||
/* ========================== Display Type Enumeration ========================== */
|
||||
|
||||
#define BLE_ESL_DISPLAY_TYPE_BLACK_WHITE 0x01 /*!< Black White */
|
||||
#define BLE_ESL_DISPLAY_TYPE_THREE_GRAY 0x02 /*!< Three Gray Scale */
|
||||
#define BLE_ESL_DISPLAY_TYPE_FOUR_GRAY 0x03 /*!< Four Gray Scale */
|
||||
#define BLE_ESL_DISPLAY_TYPE_EIGHT_GRAY 0x04 /*!< Eight Gray Scale */
|
||||
#define BLE_ESL_DISPLAY_TYPE_SIXTEEN_GRAY 0x05 /*!< Sixteen Gray Scale */
|
||||
#define BLE_ESL_DISPLAY_TYPE_RED_BLACK_WHITE 0x06 /*!< Red Black White */
|
||||
#define BLE_ESL_DISPLAY_TYPE_YELLOW_BLACK_WHITE 0x07 /*!< Yellow Black White */
|
||||
#define BLE_ESL_DISPLAY_TYPE_RED_YELLOW_BLACK_WHITE 0x08 /*!< Red Yellow Black White */
|
||||
#define BLE_ESL_DISPLAY_TYPE_SEVEN_COLOR 0x09 /*!< Seven Color */
|
||||
#define BLE_ESL_DISPLAY_TYPE_SIXTEEN_COLOR 0x0A /*!< Sixteen Color */
|
||||
#define BLE_ESL_DISPLAY_TYPE_FULL_RGB 0x0B /*!< Full RGB */
|
||||
|
||||
/* ========================== LED Information Bit Fields ========================== */
|
||||
|
||||
#define BLE_ESL_LED_INFO_RED_MASK 0x03 /*!< Bits [1:0]: Red component */
|
||||
#define BLE_ESL_LED_INFO_RED_SHIFT 0
|
||||
#define BLE_ESL_LED_INFO_GREEN_MASK 0x0C /*!< Bits [3:2]: Green component */
|
||||
#define BLE_ESL_LED_INFO_GREEN_SHIFT 2
|
||||
#define BLE_ESL_LED_INFO_BLUE_MASK 0x30 /*!< Bits [5:4]: Blue component */
|
||||
#define BLE_ESL_LED_INFO_BLUE_SHIFT 4
|
||||
#define BLE_ESL_LED_INFO_TYPE_MASK 0xC0 /*!< Bits [7:6]: LED type */
|
||||
#define BLE_ESL_LED_INFO_TYPE_SHIFT 6
|
||||
|
||||
/** @brief LED Type: sRGB multi-color */
|
||||
#define BLE_ESL_LED_TYPE_SRGB 0x00
|
||||
/** @brief LED Type: Monochrome single-color */
|
||||
#define BLE_ESL_LED_TYPE_MONOCHROME 0x01
|
||||
|
||||
/* ========================== Sensor Information Sizes ========================== */
|
||||
|
||||
/** @brief Sensor Information short format size indicator */
|
||||
#define BLE_ESL_SENSOR_INFO_SIZE_SHORT 0x00
|
||||
/** @brief Sensor Information long format size indicator */
|
||||
#define BLE_ESL_SENSOR_INFO_SIZE_LONG 0x01
|
||||
|
||||
/** @brief Sensor Information short format total octets */
|
||||
#define BLE_ESL_SENSOR_INFO_SHORT_LEN 3
|
||||
/** @brief Sensor Information long format total octets */
|
||||
#define BLE_ESL_SENSOR_INFO_LONG_LEN 5
|
||||
|
||||
/* ========================== Key Material Size ========================== */
|
||||
|
||||
/** @brief Key Material total size (Session_Key 16 + IV 8) */
|
||||
#define BLE_ESL_KEY_MATERIAL_SIZE 24
|
||||
/** @brief Session Key size in octets */
|
||||
#define BLE_ESL_SESSION_KEY_SIZE 16
|
||||
/** @brief IV size in octets */
|
||||
#define BLE_ESL_IV_SIZE 8
|
||||
|
||||
/* ========================== Encryption Parameters ========================== */
|
||||
|
||||
/** @brief CCM Nonce size (Randomizer 5 + IV 8) */
|
||||
#define BLE_ESL_CCM_NONCE_SIZE 13
|
||||
/** @brief Randomizer size in octets */
|
||||
#define BLE_ESL_RANDOMIZER_SIZE 5
|
||||
/** @brief MIC size in octets */
|
||||
#define BLE_ESL_MIC_SIZE 4
|
||||
|
||||
/* ========================== OTS Object ID ========================== */
|
||||
|
||||
/** @brief Base Object ID for ESL images (48-bit): Object_ID = base + Image_Index */
|
||||
#define BLE_ESL_OTS_OBJECT_ID_BASE 0x000000000100ULL
|
||||
|
||||
/* ========================== Implausible Time Threshold ========================== */
|
||||
|
||||
/** @brief Maximum future time considered plausible: 48 days in ms */
|
||||
#define BLE_ESL_IMPLAUSIBLE_TIME_MS 4147200000UL
|
||||
|
||||
/* ========================== Data Structures ========================== */
|
||||
|
||||
/**
|
||||
* @brief ESL Address (2 octets)
|
||||
*
|
||||
* ESL_ID (bits 0-7) + Group_ID (bits 8-14) + RFU (bit 15)
|
||||
*/
|
||||
typedef struct __attribute__((packed)) {
|
||||
uint8_t esl_id; /*!< ESL identifier (0x00-0xFE valid, 0xFF = broadcast) */
|
||||
uint8_t group_id_rfu; /*!< Group_ID in bits [6:0], RFU in bit 7 (shall be 0) */
|
||||
} ble_esl_address_t;
|
||||
|
||||
/**
|
||||
* @brief Key Material (24 octets)
|
||||
*
|
||||
* 128-bit AES session key + 64-bit initialization vector.
|
||||
* Shared layout of the AP Sync Key Material and the ESL Response Key Material.
|
||||
*/
|
||||
typedef struct __attribute__((packed)) {
|
||||
uint8_t session_key[BLE_ESL_SESSION_KEY_SIZE]; /*!< 128-bit AES session key */
|
||||
uint8_t iv[BLE_ESL_IV_SIZE]; /*!< 64-bit initialization vector */
|
||||
} ble_esl_key_material_t;
|
||||
|
||||
/**
|
||||
* @brief ESL Current Absolute Time (4 octets)
|
||||
*/
|
||||
typedef uint32_t ble_esl_abs_time_t;
|
||||
|
||||
/**
|
||||
* @brief Display Data Structure (5 octets per display)
|
||||
*/
|
||||
typedef struct __attribute__((packed)) {
|
||||
uint16_t width; /*!< Display width in pixels */
|
||||
uint16_t height; /*!< Display height in pixels */
|
||||
uint8_t display_type; /*!< Display type enumeration */
|
||||
} ble_esl_display_info_t;
|
||||
|
||||
/**
|
||||
* @brief Image Information (1 octet)
|
||||
*/
|
||||
typedef struct __attribute__((packed)) {
|
||||
uint8_t max_image_index; /*!< Numerically highest Image_Index supported */
|
||||
} ble_esl_image_info_t;
|
||||
|
||||
/**
|
||||
* @brief Sensor Information — Short format (3 octets)
|
||||
*/
|
||||
typedef struct __attribute__((packed)) {
|
||||
uint8_t size; /*!< 0x00 = short format */
|
||||
uint16_t sensor_type; /*!< 16-bit Property ID from Mesh Device Properties */
|
||||
} ble_esl_sensor_info_short_t;
|
||||
|
||||
/**
|
||||
* @brief Sensor Information — Long format (5 octets)
|
||||
*/
|
||||
typedef struct __attribute__((packed)) {
|
||||
uint8_t size; /*!< 0x01 = long format */
|
||||
uint16_t company_id; /*!< Bluetooth SIG assigned Company ID (bits 0-15 of Sensor_Type) */
|
||||
uint16_t sensor_code; /*!< Vendor-assigned sensor type code (bits 16-31 of Sensor_Type) */
|
||||
} ble_esl_sensor_info_long_t;
|
||||
|
||||
/**
|
||||
* @brief Flashing Pattern (56 bits = 7 octets)
|
||||
*
|
||||
* Pattern (40 bits) + Bit_Off_Period (8 bits) + Bit_On_Period (8 bits)
|
||||
* Stored in little-endian byte order.
|
||||
*/
|
||||
typedef struct __attribute__((packed)) {
|
||||
uint8_t pattern[5]; /*!< 40-bit on/off pattern (MSB examined first) */
|
||||
uint8_t bit_off_period; /*!< Off-time per bit = value × 2 ms (2-510 ms, 0 invalid) */
|
||||
uint8_t bit_on_period; /*!< On-time per bit = value × 2 ms (2-510 ms, 0 invalid) */
|
||||
} ble_esl_flashing_pattern_t;
|
||||
|
||||
/**
|
||||
* @brief LED Control Settings
|
||||
*
|
||||
* Contains color, brightness, flashing pattern, and repeat configuration.
|
||||
* Total: 10 octets (1 byte color+brightness + 7 bytes flashing + 2 bytes repeat)
|
||||
*/
|
||||
typedef struct __attribute__((packed)) {
|
||||
uint8_t color_brightness; /*!< Red[1:0], Green[3:2], Blue[5:4], Brightness[7:6] */
|
||||
ble_esl_flashing_pattern_t flashing; /*!< 56-bit flashing pattern (7 octets) */
|
||||
uint16_t repeat; /*!< Repeat_Type (bit 0) + Repeats_Duration (bits 15:1) */
|
||||
} ble_esl_led_control_t;
|
||||
|
||||
/* ========================== LED Control Helpers ========================== */
|
||||
|
||||
/** @brief Extract Red component (2 bits) from color_brightness byte */
|
||||
#define BLE_ESL_LED_CTRL_RED(cb) ((cb) & 0x03)
|
||||
/** @brief Extract Green component (2 bits) from color_brightness byte */
|
||||
#define BLE_ESL_LED_CTRL_GREEN(cb) (((cb) >> 2) & 0x03)
|
||||
/** @brief Extract Blue component (2 bits) from color_brightness byte */
|
||||
#define BLE_ESL_LED_CTRL_BLUE(cb) (((cb) >> 4) & 0x03)
|
||||
/** @brief Extract Brightness (2 bits) from color_brightness byte */
|
||||
#define BLE_ESL_LED_CTRL_BRIGHTNESS(cb) (((cb) >> 6) & 0x03)
|
||||
|
||||
/** @brief Build color_brightness byte from components */
|
||||
#define BLE_ESL_LED_CTRL_COLOR_BRIGHTNESS(r, g, b, bright) \
|
||||
((uint8_t)(((r) & 0x03) | (((g) & 0x03) << 2) | (((b) & 0x03) << 4) | (((bright) & 0x03) << 6)))
|
||||
|
||||
/** @brief Extract Repeat_Type (bit 0) from repeat field */
|
||||
#define BLE_ESL_LED_CTRL_REPEAT_TYPE(rep) ((rep) & 0x01)
|
||||
/** @brief Extract Repeats_Duration (bits 15:1) from repeat field */
|
||||
#define BLE_ESL_LED_CTRL_REPEATS_DURATION(rep) (((rep) >> 1) & 0x7FFF)
|
||||
|
||||
/** @brief Build repeat field from type and duration */
|
||||
#define BLE_ESL_LED_CTRL_REPEAT(type, duration) \
|
||||
((uint16_t)(((type) & 0x01) | (((duration) & 0x7FFF) << 1)))
|
||||
|
||||
/* ========================== ESL Address Helpers ========================== */
|
||||
|
||||
/** @brief Extract Group_ID (7 bits) from group_id_rfu byte */
|
||||
#define BLE_ESL_ADDR_GROUP_ID(addr) ((addr).group_id_rfu & 0x7F)
|
||||
|
||||
/** @brief Set Group_ID in an ble_esl_address_t (clears RFU bit) */
|
||||
#define BLE_ESL_ADDR_SET_GROUP_ID(addr, gid) \
|
||||
do { (addr).group_id_rfu = (uint8_t)((gid) & 0x7F); } while (0)
|
||||
|
||||
/**
|
||||
* @brief Build an ESL Address from its ESL_ID and Group_ID components
|
||||
*
|
||||
* @param esl_id ESL identifier (0x00-0xFE valid, 0xFF = broadcast)
|
||||
* @param group_id Group identifier (0x00-0x7F); the RFU bit is always cleared
|
||||
* @return Assembled ESL Address
|
||||
*/
|
||||
static inline ble_esl_address_t ble_esl_addr_make(uint8_t esl_id, uint8_t group_id)
|
||||
{
|
||||
ble_esl_address_t addr = {
|
||||
.esl_id = esl_id,
|
||||
.group_id_rfu = (uint8_t)(group_id & 0x7F),
|
||||
};
|
||||
return addr;
|
||||
}
|
||||
|
||||
/* ========================== Cross-Layer Utility Functions ========================== */
|
||||
|
||||
/**
|
||||
* @brief Encode a single command or response into TLV wire format
|
||||
*
|
||||
* @param[in] opcode TLV opcode byte (Tag in bits [3:0], Length in bits [7:4])
|
||||
* @param[in] params Pointer to parameter bytes (Length+1 octets)
|
||||
* @param[in] params_len Length of params in octets (must equal (opcode >> 4) + 1)
|
||||
* @param[out] out_buf Output buffer to receive the encoded TLV (min size: params_len + 1)
|
||||
* @param[out] out_len On output, set to total TLV size written (params_len + 1)
|
||||
*
|
||||
* @return
|
||||
* - ESP_OK on success
|
||||
* - ESP_ERR_INVALID_ARG if params_len does not match opcode Length nibble,
|
||||
* or if any pointer argument is NULL
|
||||
*/
|
||||
esp_err_t ble_esl_tlv_encode(uint8_t opcode, const uint8_t *params,
|
||||
uint8_t params_len, uint8_t *out_buf,
|
||||
uint8_t *out_len);
|
||||
|
||||
/**
|
||||
* @brief Decode a single TLV element from a byte buffer
|
||||
*
|
||||
* @param[in] in_buf Input buffer containing the TLV
|
||||
* @param[in] in_len Length of input buffer
|
||||
* @param[out] opcode On output, the opcode byte
|
||||
* @param[out] params On output, pointer into in_buf at the start of parameters
|
||||
* @param[out] params_len On output, parameter length in octets
|
||||
*
|
||||
* @return
|
||||
* - ESP_OK on success
|
||||
* - ESP_ERR_INVALID_SIZE if buffer too short for the TLV
|
||||
* - ESP_ERR_INVALID_ARG if any pointer argument is NULL or TLV is malformed
|
||||
*/
|
||||
esp_err_t ble_esl_tlv_decode(const uint8_t *in_buf, uint8_t in_len,
|
||||
uint8_t *opcode, const uint8_t **params,
|
||||
uint8_t *params_len);
|
||||
|
||||
/**
|
||||
* @brief Pack Group_ID header and pre-encoded TLVs into an ESL Payload buffer
|
||||
*
|
||||
* Used by the AP to build command sync packets (max 48 octets).
|
||||
*
|
||||
* @param[in] group_id 7-bit Group_ID value (0x00–0x7F)
|
||||
* @param[in] tlvs Array of pointers to pre-encoded TLV buffers
|
||||
* @param[in] tlv_lens Array of TLV lengths (one per TLV)
|
||||
* @param[in] tlv_count Number of TLVs to pack
|
||||
* @param[out] out_buf Output buffer (min 48 octets recommended)
|
||||
* @param[out] out_len On output, total payload size written
|
||||
*
|
||||
* @return
|
||||
* - ESP_OK on success
|
||||
* - ESP_ERR_INVALID_SIZE if total exceeds 48 octets
|
||||
* - ESP_ERR_INVALID_ARG if group_id > 0x7F or any pointer argument is NULL
|
||||
*/
|
||||
esp_err_t ble_esl_payload_encode(uint8_t group_id, const uint8_t *tlvs[],
|
||||
const uint8_t tlv_lens[], uint8_t tlv_count,
|
||||
uint8_t *out_buf, uint8_t *out_len);
|
||||
|
||||
/**
|
||||
* @brief Unpack an ESL Payload into Group_ID and individual TLV elements
|
||||
*
|
||||
* Used by the ESL to parse received command payloads.
|
||||
*
|
||||
* @param[in] in_buf Input ESL Payload buffer
|
||||
* @param[in] in_len Input buffer length
|
||||
* @param[out] group_id On output, the 7-bit Group_ID
|
||||
* @param[out] tlv_count On output, number of TLVs found
|
||||
* @param[out] tlv_offsets Array of offsets into in_buf where each TLV starts
|
||||
* @param[out] tlv_lens Array of total lengths of each TLV
|
||||
* @param[in] max_tlvs Maximum number of TLVs that can be reported (array size)
|
||||
*
|
||||
* @return
|
||||
* - ESP_OK on success
|
||||
* - ESP_ERR_INVALID_SIZE if payload too short or malformed
|
||||
* - ESP_ERR_INVALID_ARG if any pointer argument is NULL
|
||||
* - ESP_ERR_NO_MEM if more TLVs found than max_tlvs
|
||||
*/
|
||||
esp_err_t ble_esl_payload_decode(const uint8_t *in_buf, uint8_t in_len,
|
||||
uint8_t *group_id, uint8_t *tlv_count,
|
||||
uint8_t *tlv_offsets, uint8_t *tlv_lens,
|
||||
uint8_t max_tlvs);
|
||||
|
||||
/* ========================== Encrypted Advertising Data ========================== */
|
||||
|
||||
/**
|
||||
* @brief PAwR traffic direction, selects the CCM nonce directionBit
|
||||
*
|
||||
* The directionBit is the MSB of the last Randomizer octet (Supplement to the
|
||||
* Bluetooth Core Specification v11, Part A, 1.23.3).
|
||||
*/
|
||||
typedef enum {
|
||||
BLE_ESL_EAD_DIR_AP_TO_ESL = 0, /*!< Sync packets sent by the AP (directionBit = 0) */
|
||||
BLE_ESL_EAD_DIR_ESL_TO_AP = 1, /*!< Responses sent by the ESL (directionBit = 1) */
|
||||
} ble_esl_ead_direction_t;
|
||||
|
||||
/**
|
||||
* @brief Seed a Randomizer for one PAwR direction
|
||||
*
|
||||
* Fills the Randomizer with random octets and sets its directionBit to match
|
||||
* @p direction. Call once per key material lifetime; afterwards
|
||||
* ble_esl_ead_encrypt() increments the Randomizer for every packet, so it must
|
||||
* not be re-seeded while the same session key and IV remain in use.
|
||||
*
|
||||
* @param[out] randomizer Randomizer buffer of BLE_ESL_RANDOMIZER_SIZE octets
|
||||
* @param[in] direction Direction the Randomizer is used for
|
||||
*/
|
||||
void ble_esl_ead_randomizer_init(uint8_t *randomizer,
|
||||
ble_esl_ead_direction_t direction);
|
||||
|
||||
/**
|
||||
* @brief Encrypt an ESL payload as Encrypted Advertising Data (AES-CCM)
|
||||
*
|
||||
* On success the Randomizer is incremented as a little-endian counter with its
|
||||
* directionBit preserved, so every packet encrypted with the same key material
|
||||
* uses a fresh nonce.
|
||||
*
|
||||
* @param[in] session_key Session key (BLE_ESL_SESSION_KEY_SIZE octets)
|
||||
* @param[in] iv Initialization vector (BLE_ESL_IV_SIZE octets)
|
||||
* @param[in,out] randomizer Randomizer (BLE_ESL_RANDOMIZER_SIZE octets),
|
||||
* seeded by ble_esl_ead_randomizer_init()
|
||||
* @param[in] payload Plaintext (inner AD structure) to encrypt
|
||||
* @param[in] payload_len Size of @p payload in octets
|
||||
* @param[out] encrypted_payload Output buffer receiving
|
||||
* [Randomizer(5)][Ciphertext(payload_len)][MIC(4)]
|
||||
*
|
||||
* @return
|
||||
* - ESP_OK on success
|
||||
* - ESP_ERR_INVALID_ARG if any pointer argument is NULL or payload_len is 0
|
||||
* - ESP_FAIL if the AES-CCM operation fails
|
||||
*/
|
||||
esp_err_t ble_esl_ead_encrypt(const uint8_t *session_key, const uint8_t *iv,
|
||||
uint8_t *randomizer, const uint8_t *payload,
|
||||
size_t payload_len, uint8_t *encrypted_payload);
|
||||
|
||||
/**
|
||||
* @brief Decrypt Encrypted Advertising Data into an ESL payload (AES-CCM)
|
||||
*
|
||||
* Counterpart of ble_esl_ead_encrypt(). The Randomizer is taken from the first
|
||||
* BLE_ESL_RANDOMIZER_SIZE octets of @p encrypted_payload, so no Randomizer
|
||||
* state has to be kept for the receive direction. The MIC is verified before
|
||||
* the plaintext is reported as valid.
|
||||
*
|
||||
* @param[in] session_key Session key (BLE_ESL_SESSION_KEY_SIZE octets)
|
||||
* @param[in] iv Initialization vector (BLE_ESL_IV_SIZE octets)
|
||||
* @param[in] encrypted_payload Encrypted Advertising Data value, i.e.
|
||||
* [Randomizer(5)][Ciphertext(N)][MIC(4)],
|
||||
* without the outer AD length and type octets
|
||||
* @param[in] encrypted_payload_len Size of @p encrypted_payload in octets
|
||||
* @param[out] payload Buffer receiving the plaintext
|
||||
* (inner AD structure)
|
||||
* @param[in] payload_size Capacity of @p payload in octets
|
||||
* @param[out] payload_len Number of plaintext octets written
|
||||
*
|
||||
* @return
|
||||
* - ESP_OK on success
|
||||
* - ESP_ERR_INVALID_ARG if any pointer argument is NULL
|
||||
* - ESP_ERR_INVALID_SIZE if @p encrypted_payload_len is too short to hold a
|
||||
* Randomizer, a MIC and at least one ciphertext octet, or if @p payload
|
||||
* is too small for the plaintext
|
||||
* - ESP_ERR_INVALID_RESPONSE if MIC verification fails
|
||||
*/
|
||||
esp_err_t ble_esl_ead_decrypt(const uint8_t *session_key, const uint8_t *iv,
|
||||
const uint8_t *encrypted_payload,
|
||||
size_t encrypted_payload_len, uint8_t *payload,
|
||||
size_t payload_size, size_t *payload_len);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* BLE_ESL_COMMON_H */
|
||||
1104
components/bt/ble_profiles/nimble/ble_esl/src/ap/ap_command.c
Normal file
1104
components/bt/ble_profiles/nimble/ble_esl/src/ap/ap_command.c
Normal file
File diff suppressed because it is too large
Load Diff
979
components/bt/ble_profiles/nimble/ble_esl/src/ap/ap_connection.c
Normal file
979
components/bt/ble_profiles/nimble/ble_esl/src/ap/ap_connection.c
Normal file
@@ -0,0 +1,979 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
/**
|
||||
* @file ble_esl_ap_connection.c
|
||||
* @brief ESL AP — Connection management, service/characteristic discovery,
|
||||
* and internal GATT read/write/ECP helpers.
|
||||
*/
|
||||
|
||||
#include <string.h>
|
||||
#include "esp_log.h"
|
||||
#include "esp_err.h"
|
||||
|
||||
#include "nimble/ble.h"
|
||||
#include "host/ble_hs.h"
|
||||
#include "host/ble_gap.h"
|
||||
|
||||
#include "ble_esl_ap.h"
|
||||
#include "ble_esl_ap_int.h"
|
||||
|
||||
static const char *TAG = "esl_ap_conn";
|
||||
|
||||
/** PAwR advertising instance (shared with ap_pawr.c / ap_lifecycle.c) */
|
||||
#define PAWR_ADV_INSTANCE 0
|
||||
|
||||
/** Connection-establishment timeout for the PAwR connection procedure (ms) */
|
||||
#define PAWR_CONNECT_TIMEOUT_MS 30000
|
||||
|
||||
/* ========================== Global State ========================== */
|
||||
|
||||
ble_esl_ap_state_t *g_esl_ap = NULL;
|
||||
|
||||
/* ========================== Forward Declarations ========================== */
|
||||
|
||||
static int ble_esl_ap_gap_event(struct ble_gap_event *event, void *arg);
|
||||
static void handle_scan_result(struct ble_gap_event *event);
|
||||
static void handle_connect_event(struct ble_gap_event *event, void *arg);
|
||||
static void handle_disconnect_event(struct ble_gap_event *event);
|
||||
static void handle_enc_change(struct ble_gap_event *event);
|
||||
static void handle_pairing_complete(struct ble_gap_event *event);
|
||||
static void handle_notify_rx(struct ble_gap_event *event);
|
||||
|
||||
|
||||
/* ========================== Helper Functions ========================== */
|
||||
|
||||
ble_esl_ap_conn_t *ble_esl_ap_find_conn(uint16_t conn_handle)
|
||||
{
|
||||
if (!g_esl_ap) {
|
||||
return NULL;
|
||||
}
|
||||
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].conn_handle == conn_handle) {
|
||||
return &g_esl_ap->conns[i];
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
ble_esl_ap_conn_t *ble_esl_ap_alloc_conn(void)
|
||||
{
|
||||
if (!g_esl_ap) {
|
||||
return NULL;
|
||||
}
|
||||
for (int i = 0; i < CONFIG_BLE_ESL_AP_MAX_CONNECTIONS; i++) {
|
||||
if (!g_esl_ap->conns[i].in_use) {
|
||||
memset(&g_esl_ap->conns[i], 0, sizeof(ble_esl_ap_conn_t));
|
||||
g_esl_ap->conns[i].in_use = true;
|
||||
g_esl_ap->conns[i].conn_handle = BLE_ESL_AP_CONN_HANDLE_INVALID;
|
||||
g_esl_ap->conns[i].esl_addr = BLE_ESL_AP_CONN_HANDLE_INVALID;
|
||||
return &g_esl_ap->conns[i];
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
void ble_esl_ap_free_conn(ble_esl_ap_conn_t *conn)
|
||||
{
|
||||
if (conn) {
|
||||
memset(conn, 0, sizeof(ble_esl_ap_conn_t));
|
||||
conn->in_use = false;
|
||||
conn->conn_handle = BLE_ESL_AP_CONN_HANDLE_INVALID;
|
||||
}
|
||||
}
|
||||
|
||||
ble_esl_ap_esl_entry_t *ble_esl_ap_find_esl(uint16_t esl_addr)
|
||||
{
|
||||
if (!g_esl_ap) {
|
||||
return NULL;
|
||||
}
|
||||
for (int i = 0; i < CONFIG_BLE_ESL_AP_MAX_ESLS; i++) {
|
||||
if (g_esl_ap->esls[i].in_use &&
|
||||
g_esl_ap->esls[i].esl_addr == esl_addr) {
|
||||
return &g_esl_ap->esls[i];
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
ble_esl_ap_esl_entry_t *ble_esl_ap_find_esl_by_ble_addr(const uint8_t *addr,
|
||||
uint8_t addr_type)
|
||||
{
|
||||
if (!g_esl_ap || !addr) {
|
||||
return NULL;
|
||||
}
|
||||
for (int i = 0; i < CONFIG_BLE_ESL_AP_MAX_ESLS; i++) {
|
||||
if (g_esl_ap->esls[i].in_use &&
|
||||
g_esl_ap->esls[i].ble_addr_type == addr_type &&
|
||||
memcmp(g_esl_ap->esls[i].ble_addr, addr, 6) == 0) {
|
||||
return &g_esl_ap->esls[i];
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
ble_esl_ap_esl_entry_t *ble_esl_ap_alloc_esl(void)
|
||||
{
|
||||
if (!g_esl_ap) {
|
||||
return NULL;
|
||||
}
|
||||
for (int i = 0; i < CONFIG_BLE_ESL_AP_MAX_ESLS; i++) {
|
||||
if (!g_esl_ap->esls[i].in_use) {
|
||||
memset(&g_esl_ap->esls[i], 0, sizeof(ble_esl_ap_esl_entry_t));
|
||||
g_esl_ap->esls[i].in_use = true;
|
||||
g_esl_ap->esls[i].conn_handle = BLE_ESL_AP_CONN_HANDLE_INVALID;
|
||||
return &g_esl_ap->esls[i];
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
/* ========================== Public APIs ========================== */
|
||||
|
||||
esp_err_t ble_esl_ap_init(const ble_esl_ap_config_t *config)
|
||||
{
|
||||
if (!config || !config->callback) {
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
/* Zero subevents would leave the PAwR train with nothing to transmit in and
|
||||
* is used as a divisor when servicing subevent data requests. */
|
||||
if (config->pawr_config.num_subevents == 0 ||
|
||||
config->pawr_config.num_subevents > BLE_ESL_AP_PAWR_MAX_SUBEVENTS) {
|
||||
ESP_LOGE(TAG, "num_subevents %u out of range (1..%d)",
|
||||
config->pawr_config.num_subevents, BLE_ESL_AP_PAWR_MAX_SUBEVENTS);
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
if (g_esl_ap) {
|
||||
ESP_LOGE(TAG, "Already initialized");
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
|
||||
g_esl_ap = calloc(1, sizeof(ble_esl_ap_state_t));
|
||||
if (!g_esl_ap) {
|
||||
ESP_LOGE(TAG, "Failed to allocate AP state");
|
||||
return ESP_ERR_NO_MEM;
|
||||
}
|
||||
|
||||
g_esl_ap->app_cb = config->callback;
|
||||
g_esl_ap->pawr_config = config->pawr_config;
|
||||
g_esl_ap->initialized = true;
|
||||
g_esl_ap->started = false;
|
||||
g_esl_ap->pawr_active = false;
|
||||
|
||||
/* Initialize connection table */
|
||||
for (int i = 0; i < CONFIG_BLE_ESL_AP_MAX_CONNECTIONS; i++) {
|
||||
g_esl_ap->conns[i].in_use = false;
|
||||
g_esl_ap->conns[i].conn_handle = BLE_ESL_AP_CONN_HANDLE_INVALID;
|
||||
}
|
||||
|
||||
/* Initialize ESL tracking table */
|
||||
for (int i = 0; i < CONFIG_BLE_ESL_AP_MAX_ESLS; i++) {
|
||||
g_esl_ap->esls[i].in_use = false;
|
||||
g_esl_ap->esls[i].conn_handle = BLE_ESL_AP_CONN_HANDLE_INVALID;
|
||||
}
|
||||
|
||||
/* Initialize sub-modules */
|
||||
esp_err_t ret = ble_esl_ap_lifecycle_init();
|
||||
if (ret != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to init lifecycle sub-module: %s", esp_err_to_name(ret));
|
||||
free(g_esl_ap);
|
||||
g_esl_ap = NULL;
|
||||
return ret;
|
||||
}
|
||||
|
||||
ret = ble_esl_ap_pawr_init();
|
||||
if (ret != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to init PAwR sub-module: %s", esp_err_to_name(ret));
|
||||
ble_esl_ap_lifecycle_deinit();
|
||||
free(g_esl_ap);
|
||||
g_esl_ap = NULL;
|
||||
return ret;
|
||||
}
|
||||
|
||||
ret = ble_esl_ap_command_init();
|
||||
if (ret != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to init command sub-module: %s", esp_err_to_name(ret));
|
||||
ble_esl_ap_pawr_deinit();
|
||||
ble_esl_ap_lifecycle_deinit();
|
||||
free(g_esl_ap);
|
||||
g_esl_ap = NULL;
|
||||
return ret;
|
||||
}
|
||||
|
||||
ret = ble_esl_ap_gattc_init();
|
||||
if (ret != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to init GATT client: %s", esp_err_to_name(ret));
|
||||
ble_esl_ap_command_deinit();
|
||||
ble_esl_ap_pawr_deinit();
|
||||
ble_esl_ap_lifecycle_deinit();
|
||||
free(g_esl_ap);
|
||||
g_esl_ap = NULL;
|
||||
return ret;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "ESL AP initialized");
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Run the disconnect cleanup for one established link during deinit
|
||||
*
|
||||
* ble_gap_terminate() is asynchronous, so the BLE_GAP_EVENT_DISCONNECT for a
|
||||
* link torn down by ble_esl_ap_deinit() is delivered after the module state has
|
||||
* been freed and is therefore dropped by the GAP event handler. The per-link
|
||||
* cleanup normally performed by handle_disconnect_event() must consequently be
|
||||
* done synchronously here.
|
||||
*
|
||||
* @param conn Connection context with a valid conn_handle
|
||||
*/
|
||||
static void deinit_teardown_conn(ble_esl_ap_conn_t *conn)
|
||||
{
|
||||
uint16_t conn_handle = conn->conn_handle;
|
||||
|
||||
/* Fire disconnected event */
|
||||
if (g_esl_ap->app_cb) {
|
||||
ble_esl_ap_disconn_info_t info;
|
||||
info.conn_handle = conn_handle;
|
||||
memcpy(info.addr, conn->addr, 6);
|
||||
info.reason = BLE_ERR_REM_USER_CONN_TERM;
|
||||
g_esl_ap->app_cb(BLE_ESL_AP_EVT_DISCONNECTED, &info);
|
||||
}
|
||||
|
||||
/* Invoke any pending ECP callback with error before clearing the slot */
|
||||
if (conn->ecp_pending && conn->ecp_cb) {
|
||||
ble_esl_ap_gatt_cb_t cb = conn->ecp_cb;
|
||||
void *user_data = conn->ecp_user_data;
|
||||
conn->ecp_pending = false;
|
||||
conn->ecp_cb = NULL;
|
||||
conn->ecp_user_data = NULL;
|
||||
cb(conn_handle, ESP_FAIL, NULL, 0, user_data);
|
||||
}
|
||||
|
||||
/* Clean up any pending ECP command context and timer in the command module */
|
||||
ble_esl_ap_command_cleanup_conn(conn_handle);
|
||||
|
||||
/* Let lifecycle module complete any pending synchronize procedure
|
||||
* and handle link-loss state transitions */
|
||||
ble_esl_ap_lifecycle_handle_disconnect(conn_handle);
|
||||
}
|
||||
|
||||
esp_err_t ble_esl_ap_deinit(void)
|
||||
{
|
||||
if (!g_esl_ap || !g_esl_ap->initialized) {
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
|
||||
/* Stop scanning and PAwR if active */
|
||||
if (g_esl_ap->started) {
|
||||
ble_esl_ap_stop();
|
||||
}
|
||||
|
||||
/* Mark the module as no longer operational before touching connections:
|
||||
* from here on the GAP event handler drops every asynchronously delivered
|
||||
* event, so no callback can dereference the state (nor the conns[] slot
|
||||
* passed as GAP cb_arg) while it is being torn down and freed below. */
|
||||
g_esl_ap->initialized = false;
|
||||
|
||||
/* Cancel any pending (not yet established) connection attempts */
|
||||
ble_gap_conn_cancel();
|
||||
|
||||
/* Disconnect all active connections and free every slot synchronously */
|
||||
esp_err_t ret = ESP_OK;
|
||||
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) {
|
||||
continue;
|
||||
}
|
||||
if (conn->conn_handle != BLE_ESL_AP_CONN_HANDLE_INVALID) {
|
||||
int rc = ble_gap_terminate(conn->conn_handle, BLE_ERR_REM_USER_CONN_TERM);
|
||||
/* ENOTCONN (link already gone), EALREADY (termination already in
|
||||
* progress) and EDISABLED (host stopped, so no link survives) all
|
||||
* mean the link is on its way out: cleaning the slot up is safe.
|
||||
* Any other error means the controller link is still up — keep the
|
||||
* slot so the link stays tracked and abort the deinit below. */
|
||||
if (rc != 0 && rc != BLE_HS_ENOTCONN && rc != BLE_HS_EALREADY &&
|
||||
rc != BLE_HS_EDISABLED) {
|
||||
ESP_LOGE(TAG, "Failed to terminate conn_handle=%u; rc=%d",
|
||||
conn->conn_handle, rc);
|
||||
ret = ESP_FAIL;
|
||||
continue;
|
||||
}
|
||||
deinit_teardown_conn(conn);
|
||||
}
|
||||
ble_esl_ap_free_conn(conn);
|
||||
}
|
||||
|
||||
if (ret != ESP_OK) {
|
||||
/* At least one link could not be terminated. Tearing the module down
|
||||
* now would leak it: the application would be told the link is gone
|
||||
* while the controller keeps it up, and the later GAP events would be
|
||||
* dropped. Return to the initialized (stopped) state instead so the
|
||||
* remaining links stay tracked and the caller can retry deinit. */
|
||||
g_esl_ap->initialized = true;
|
||||
ESP_LOGE(TAG, "Deinit aborted: connection termination failed");
|
||||
return ret;
|
||||
}
|
||||
|
||||
/* Deinitialize sub-modules */
|
||||
ble_esl_ap_gattc_deinit();
|
||||
ble_esl_ap_command_deinit();
|
||||
ble_esl_ap_pawr_deinit();
|
||||
ble_esl_ap_lifecycle_deinit();
|
||||
|
||||
/* Free global state — set pointer to NULL first to prevent
|
||||
* asynchronous callbacks from dereferencing freed memory */
|
||||
ble_esl_ap_state_t *ap = g_esl_ap;
|
||||
g_esl_ap = NULL;
|
||||
free(ap);
|
||||
|
||||
ESP_LOGI(TAG, "ESL AP deinitialized");
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t ble_esl_ap_start(void)
|
||||
{
|
||||
if (!g_esl_ap || !g_esl_ap->initialized) {
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
if (g_esl_ap->started) {
|
||||
ESP_LOGW(TAG, "Already started");
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
|
||||
/* Start GAP General Discovery (scanning) */
|
||||
uint8_t own_addr_type;
|
||||
int rc = ble_hs_id_infer_auto(0, &own_addr_type);
|
||||
if (rc != 0) {
|
||||
ESP_LOGE(TAG, "Failed to determine address type; rc=%d", rc);
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
struct ble_gap_disc_params disc_params = {0};
|
||||
disc_params.filter_duplicates = 1;
|
||||
disc_params.passive = 0; /* Active scan for General Discovery */
|
||||
disc_params.itvl = 0; /* Use defaults */
|
||||
disc_params.window = 0;
|
||||
disc_params.filter_policy = 0;
|
||||
disc_params.limited = 0;
|
||||
|
||||
rc = ble_gap_disc(own_addr_type, BLE_HS_FOREVER, &disc_params,
|
||||
ble_esl_ap_gap_event, NULL);
|
||||
if (rc != 0) {
|
||||
ESP_LOGE(TAG, "Failed to start GAP discovery; rc=%d", rc);
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
/* Start PAwR broadcasting */
|
||||
esp_err_t ret = ble_esl_ap_pawr_start();
|
||||
if (ret != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to start PAwR: %s", esp_err_to_name(ret));
|
||||
ble_gap_disc_cancel();
|
||||
return ret;
|
||||
}
|
||||
|
||||
g_esl_ap->started = true;
|
||||
ESP_LOGI(TAG, "ESL AP started (scanning + PAwR)");
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t ble_esl_ap_stop(void)
|
||||
{
|
||||
if (!g_esl_ap || !g_esl_ap->initialized || !g_esl_ap->started) {
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
|
||||
/* Cancel any pending (not yet established) connection attempts */
|
||||
ble_gap_conn_cancel();
|
||||
|
||||
/* Stop scanning */
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/* Stop PAwR broadcasting */
|
||||
esp_err_t ret = ble_esl_ap_pawr_stop();
|
||||
if (ret != ESP_OK) {
|
||||
ESP_LOGW(TAG, "Failed to stop PAwR: %s", esp_err_to_name(ret));
|
||||
}
|
||||
|
||||
g_esl_ap->started = false;
|
||||
ESP_LOGI(TAG, "ESL AP stopped");
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t ble_esl_ap_connect(const uint8_t *addr, uint8_t addr_type)
|
||||
{
|
||||
if (!addr) {
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
if (!g_esl_ap || !g_esl_ap->initialized) {
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
|
||||
/* Check if we have a free connection slot */
|
||||
ble_esl_ap_conn_t *conn = ble_esl_ap_alloc_conn();
|
||||
if (!conn) {
|
||||
ESP_LOGE(TAG, "Max connections reached");
|
||||
return ESP_ERR_NO_MEM;
|
||||
}
|
||||
|
||||
/* Store address in the pre-allocated slot */
|
||||
memcpy(conn->addr, addr, 6);
|
||||
conn->addr_type = addr_type;
|
||||
|
||||
/* Build peer address */
|
||||
ble_addr_t peer_addr;
|
||||
peer_addr.type = addr_type;
|
||||
memcpy(peer_addr.val, addr, 6);
|
||||
|
||||
uint8_t own_addr_type;
|
||||
int rc = ble_hs_id_infer_auto(0, &own_addr_type);
|
||||
if (rc != 0) {
|
||||
ESP_LOGE(TAG, "Failed to determine address type; rc=%d", rc);
|
||||
ble_esl_ap_free_conn(conn);
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
/* Stop scanning before connecting (NimBLE requirement unless
|
||||
* BLE_HOST_ALLOW_CONNECT_WITH_SCAN is enabled) */
|
||||
if (ble_gap_disc_active()) {
|
||||
rc = ble_gap_disc_cancel();
|
||||
if (rc != 0 && rc != BLE_HS_EALREADY) {
|
||||
ESP_LOGW(TAG, "Failed to cancel scan before connect; rc=%d", rc);
|
||||
}
|
||||
}
|
||||
|
||||
/* Initiate connection with default parameters, 30s timeout.
|
||||
* Pass conn as cb_arg so the GAP event handler can directly
|
||||
* identify the connection slot without guessing. */
|
||||
rc = ble_gap_connect(own_addr_type, &peer_addr, 30000, NULL,
|
||||
ble_esl_ap_gap_event, conn);
|
||||
if (rc != 0) {
|
||||
ESP_LOGE(TAG, "Failed to initiate connection; rc=%d", rc);
|
||||
ble_esl_ap_free_conn(conn);
|
||||
|
||||
/* Resume scanning if we were started */
|
||||
if (g_esl_ap->started) {
|
||||
struct ble_gap_disc_params disc_params = {0};
|
||||
disc_params.filter_duplicates = 1;
|
||||
disc_params.passive = 0;
|
||||
ble_gap_disc(own_addr_type, BLE_HS_FOREVER, &disc_params,
|
||||
ble_esl_ap_gap_event, NULL);
|
||||
}
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Connection initiated to %02x:%02x:%02x:%02x:%02x:%02x",
|
||||
addr[5], addr[4], addr[3], addr[2], addr[1], addr[0]);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t ble_esl_ap_connect_synced(ble_esl_address_t esl_addr)
|
||||
{
|
||||
if (!g_esl_ap || !g_esl_ap->initialized) {
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
|
||||
uint16_t addr_key = BLE_ESL_AP_ADDR_PACK(esl_addr);
|
||||
|
||||
/* The ESL must be tracked and currently Synchronized to our PAwR train.
|
||||
* Only a Synchronized ESL is reachable via the Periodic Advertising
|
||||
* Connection procedure; in other states use ble_esl_ap_connect(). */
|
||||
ble_esl_ap_esl_entry_t *esl = ble_esl_ap_find_esl(addr_key);
|
||||
if (!esl) {
|
||||
ESP_LOGE(TAG, "connect_synced: ESL 0x%04X not tracked", addr_key);
|
||||
return ESP_ERR_NOT_FOUND;
|
||||
}
|
||||
if (esl->state != BLE_ESL_STATE_SYNCHRONIZED) {
|
||||
ESP_LOGE(TAG, "connect_synced: ESL 0x%04X not Synchronized (state=%d)",
|
||||
addr_key, esl->state);
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
|
||||
/* Reserve a connection slot; the GAP event handler identifies it via cb_arg. */
|
||||
ble_esl_ap_conn_t *conn = ble_esl_ap_alloc_conn();
|
||||
if (!conn) {
|
||||
ESP_LOGE(TAG, "connect_synced: max connections reached");
|
||||
return ESP_ERR_NO_MEM;
|
||||
}
|
||||
memcpy(conn->addr, esl->ble_addr, 6);
|
||||
conn->addr_type = esl->ble_addr_type;
|
||||
conn->esl_addr = addr_key;
|
||||
|
||||
ble_addr_t peer_addr;
|
||||
peer_addr.type = esl->ble_addr_type;
|
||||
memcpy(peer_addr.val, esl->ble_addr, 6);
|
||||
|
||||
uint8_t own_addr_type;
|
||||
int rc = ble_hs_id_infer_auto(0, &own_addr_type);
|
||||
if (rc != 0) {
|
||||
ESP_LOGE(TAG, "connect_synced: failed to determine address type; rc=%d", rc);
|
||||
ble_esl_ap_free_conn(conn);
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
/* Initiating a connection is not possible while scanning is in progress. */
|
||||
if (ble_gap_disc_active()) {
|
||||
rc = ble_gap_disc_cancel();
|
||||
if (rc != 0 && rc != BLE_HS_EALREADY) {
|
||||
ESP_LOGW(TAG, "connect_synced: failed to cancel scan; rc=%d", rc);
|
||||
}
|
||||
}
|
||||
|
||||
/* Periodic Advertising Connection procedure: send the AUX_CONNECT_REQ from
|
||||
* the subevent this ESL responds in — its Group_ID — on our own PAwR train
|
||||
* (PAWR_ADV_INSTANCE). NULL phy params request controller defaults. */
|
||||
uint8_t subevent = BLE_ESL_ADDR_GROUP_ID(esl_addr);
|
||||
rc = ble_gap_connect_with_synced(own_addr_type, PAWR_ADV_INSTANCE, subevent,
|
||||
&peer_addr, PAWR_CONNECT_TIMEOUT_MS,
|
||||
BLE_GAP_LE_PHY_1M_MASK, NULL, NULL, NULL,
|
||||
ble_esl_ap_gap_event, conn);
|
||||
if (rc != 0) {
|
||||
ESP_LOGE(TAG, "connect_synced: ble_gap_connect_with_synced failed; rc=%d", rc);
|
||||
ble_esl_ap_free_conn(conn);
|
||||
|
||||
/* Resume scanning if we were started */
|
||||
if (g_esl_ap->started && !ble_gap_disc_active()) {
|
||||
struct ble_gap_disc_params disc_params = {0};
|
||||
disc_params.filter_duplicates = 1;
|
||||
disc_params.passive = 0;
|
||||
ble_gap_disc(own_addr_type, BLE_HS_FOREVER, &disc_params,
|
||||
ble_esl_ap_gap_event, NULL);
|
||||
}
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "connect_synced: PAwR connection initiated to ESL 0x%04X (subevent=%u)",
|
||||
addr_key, subevent);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t ble_esl_ap_disconnect(uint16_t conn_handle)
|
||||
{
|
||||
if (!g_esl_ap || !g_esl_ap->initialized) {
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
|
||||
ble_esl_ap_conn_t *conn = ble_esl_ap_find_conn(conn_handle);
|
||||
if (!conn) {
|
||||
return ESP_ERR_NOT_FOUND;
|
||||
}
|
||||
|
||||
int rc = ble_gap_terminate(conn_handle, BLE_ERR_REM_USER_CONN_TERM);
|
||||
if (rc != 0) {
|
||||
ESP_LOGE(TAG, "Failed to terminate connection; rc=%d", rc);
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
/* ========================== GAP Event Handler ========================== */
|
||||
|
||||
static int ble_esl_ap_gap_event(struct ble_gap_event *event, void *arg)
|
||||
{
|
||||
/* ble_esl_ap_deinit() clears `initialized` before tearing the module down,
|
||||
* so any event delivered from that point on must be dropped: the state —
|
||||
* including the conns[] slot passed as `arg` — is being freed. */
|
||||
if (!g_esl_ap || !g_esl_ap->initialized) {
|
||||
if (event->type == BLE_GAP_EVENT_CONNECT && event->connect.status == 0) {
|
||||
/* A link came up while shutting down; drop it so it is not leaked */
|
||||
ble_gap_terminate(event->connect.conn_handle,
|
||||
BLE_ERR_REM_USER_CONN_TERM);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
switch (event->type) {
|
||||
case BLE_GAP_EVENT_DISC:
|
||||
case BLE_GAP_EVENT_EXT_DISC:
|
||||
handle_scan_result(event);
|
||||
return 0;
|
||||
|
||||
case BLE_GAP_EVENT_CONNECT:
|
||||
handle_connect_event(event, arg);
|
||||
return 0;
|
||||
|
||||
case BLE_GAP_EVENT_DISCONNECT:
|
||||
handle_disconnect_event(event);
|
||||
return 0;
|
||||
|
||||
case BLE_GAP_EVENT_ENC_CHANGE:
|
||||
handle_enc_change(event);
|
||||
return 0;
|
||||
|
||||
case BLE_GAP_EVENT_PARING_COMPLETE:
|
||||
handle_pairing_complete(event);
|
||||
return 0;
|
||||
|
||||
case BLE_GAP_EVENT_NOTIFY_RX:
|
||||
handle_notify_rx(event);
|
||||
return 0;
|
||||
|
||||
case BLE_GAP_EVENT_DISC_COMPLETE:
|
||||
ESP_LOGI(TAG, "Discovery complete; reason=%d", event->disc_complete.reason);
|
||||
return 0;
|
||||
|
||||
case BLE_GAP_EVENT_MTU:
|
||||
ESP_LOGD(TAG, "MTU update; conn_handle=%d mtu=%d",
|
||||
event->mtu.conn_handle, event->mtu.value);
|
||||
return 0;
|
||||
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* ========================== Scan Result Handling ========================== */
|
||||
|
||||
static void handle_scan_result(struct ble_gap_event *event)
|
||||
{
|
||||
if (!g_esl_ap || !g_esl_ap->app_cb) {
|
||||
return;
|
||||
}
|
||||
|
||||
ble_addr_t addr;
|
||||
int8_t rssi;
|
||||
const uint8_t *data;
|
||||
uint8_t length_data;
|
||||
|
||||
if (event->type == BLE_GAP_EVENT_EXT_DISC) {
|
||||
const struct ble_gap_ext_disc_desc *ext = &event->ext_disc;
|
||||
addr = ext->addr;
|
||||
rssi = ext->rssi;
|
||||
data = ext->data;
|
||||
length_data = ext->length_data;
|
||||
} else {
|
||||
const struct ble_gap_disc_desc *disc = &event->disc;
|
||||
addr = disc->addr;
|
||||
rssi = disc->rssi;
|
||||
data = disc->data;
|
||||
length_data = disc->length_data;
|
||||
}
|
||||
|
||||
bool associated = ble_esl_ap_is_associated(addr.val, addr.type);
|
||||
|
||||
ble_esl_ap_scan_result_t result;
|
||||
memcpy(result.addr, addr.val, 6);
|
||||
result.addr_type = addr.type;
|
||||
result.rssi = rssi;
|
||||
result.adv_data = data;
|
||||
result.adv_data_len = length_data;
|
||||
result.is_associated = associated;
|
||||
|
||||
g_esl_ap->app_cb(BLE_ESL_AP_EVT_SCAN_RESULT, &result);
|
||||
}
|
||||
|
||||
/* ========================== Connect Event Handling ========================== */
|
||||
|
||||
static void handle_connect_event(struct ble_gap_event *event, void *arg)
|
||||
{
|
||||
if (!g_esl_ap) {
|
||||
return;
|
||||
}
|
||||
|
||||
uint16_t conn_handle = event->connect.conn_handle;
|
||||
ble_esl_ap_conn_t *conn = (ble_esl_ap_conn_t *)arg;
|
||||
|
||||
if (event->connect.status != 0) {
|
||||
/* Connection failed */
|
||||
ESP_LOGE(TAG, "Connection failed; status=%d", event->connect.status);
|
||||
|
||||
/* Use the conn pointer passed as cb_arg to directly identify the slot */
|
||||
if (conn && conn->in_use &&
|
||||
conn->conn_handle == BLE_ESL_AP_CONN_HANDLE_INVALID) {
|
||||
/* Fire disconnected event */
|
||||
if (g_esl_ap->app_cb) {
|
||||
ble_esl_ap_disconn_info_t info;
|
||||
info.conn_handle = BLE_ESL_AP_CONN_HANDLE_INVALID;
|
||||
memcpy(info.addr, conn->addr, 6);
|
||||
info.reason = (uint8_t)event->connect.status;
|
||||
g_esl_ap->app_cb(BLE_ESL_AP_EVT_DISCONNECTED, &info);
|
||||
}
|
||||
ble_esl_ap_free_conn(conn);
|
||||
}
|
||||
|
||||
/* Resume scanning if started */
|
||||
if (g_esl_ap->started && !ble_gap_disc_active()) {
|
||||
uint8_t own_addr_type;
|
||||
if (ble_hs_id_infer_auto(0, &own_addr_type) == 0) {
|
||||
struct ble_gap_disc_params disc_params = {0};
|
||||
disc_params.filter_duplicates = 1;
|
||||
disc_params.passive = 0;
|
||||
ble_gap_disc(own_addr_type, BLE_HS_FOREVER, &disc_params,
|
||||
ble_esl_ap_gap_event, NULL);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
/* Connection established successfully */
|
||||
ESP_LOGI(TAG, "Connection established; conn_handle=%d", conn_handle);
|
||||
|
||||
/* Get connection descriptor to retrieve peer identity address */
|
||||
struct ble_gap_conn_desc desc;
|
||||
int rc = ble_gap_conn_find(conn_handle, &desc);
|
||||
if (rc != 0) {
|
||||
ESP_LOGE(TAG, "Failed to find connection desc; rc=%d", rc);
|
||||
|
||||
/* Free the pre-allocated connection slot to prevent a leak */
|
||||
if (conn && conn->in_use &&
|
||||
conn->conn_handle == BLE_ESL_AP_CONN_HANDLE_INVALID) {
|
||||
if (g_esl_ap->app_cb) {
|
||||
ble_esl_ap_disconn_info_t info;
|
||||
info.conn_handle = BLE_ESL_AP_CONN_HANDLE_INVALID;
|
||||
memcpy(info.addr, conn->addr, 6);
|
||||
info.reason = BLE_ERR_UNSPECIFIED;
|
||||
g_esl_ap->app_cb(BLE_ESL_AP_EVT_DISCONNECTED, &info);
|
||||
}
|
||||
ble_esl_ap_free_conn(conn);
|
||||
}
|
||||
|
||||
ble_gap_terminate(conn_handle, BLE_ERR_REM_USER_CONN_TERM);
|
||||
|
||||
/* Resume scanning if started */
|
||||
if (g_esl_ap->started && !ble_gap_disc_active()) {
|
||||
uint8_t own_addr_type;
|
||||
if (ble_hs_id_infer_auto(0, &own_addr_type) == 0) {
|
||||
struct ble_gap_disc_params disc_params = {0};
|
||||
disc_params.filter_duplicates = 1;
|
||||
disc_params.passive = 0;
|
||||
ble_gap_disc(own_addr_type, BLE_HS_FOREVER, &disc_params,
|
||||
ble_esl_ap_gap_event, NULL);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!conn || !conn->in_use ||
|
||||
conn->conn_handle != BLE_ESL_AP_CONN_HANDLE_INVALID) {
|
||||
ESP_LOGE(TAG, "No valid pre-allocated conn slot for handle=%d", conn_handle);
|
||||
ble_gap_terminate(conn_handle, BLE_ERR_REM_USER_CONN_TERM);
|
||||
|
||||
/* Resume scanning if started */
|
||||
if (g_esl_ap->started && !ble_gap_disc_active()) {
|
||||
uint8_t own_addr_type;
|
||||
if (ble_hs_id_infer_auto(0, &own_addr_type) == 0) {
|
||||
struct ble_gap_disc_params disc_params = {0};
|
||||
disc_params.filter_duplicates = 1;
|
||||
disc_params.passive = 0;
|
||||
ble_gap_disc(own_addr_type, BLE_HS_FOREVER, &disc_params,
|
||||
ble_esl_ap_gap_event, NULL);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
/* Populate connection context using the stable identity address
|
||||
* (peer_id_addr) instead of the temporary OTA address (peer_ota_addr)
|
||||
* to ensure correct ESL tracking across reconnections with RPAs. */
|
||||
conn->conn_handle = conn_handle;
|
||||
memcpy(conn->addr, desc.peer_id_addr.val, 6);
|
||||
conn->addr_type = desc.peer_id_addr.type;
|
||||
memset(&conn->handles, 0, sizeof(ble_esl_ap_char_handles_t));
|
||||
conn->ecp_pending = false;
|
||||
|
||||
/* Start security */
|
||||
rc = ble_gap_security_initiate(conn_handle);
|
||||
if (rc != 0 && rc != BLE_HS_EALREADY) {
|
||||
ESP_LOGW(TAG, "Failed to initiate encryption; conn_handle=%u rc=%d",
|
||||
conn_handle, rc);
|
||||
}
|
||||
ESP_LOGI(TAG, "Initiate encryption; conn_handle=%u", conn_handle);
|
||||
|
||||
/* Resume scanning if started */
|
||||
if (g_esl_ap->started && !ble_gap_disc_active()) {
|
||||
uint8_t own_addr_type;
|
||||
if (ble_hs_id_infer_auto(0, &own_addr_type) == 0) {
|
||||
struct ble_gap_disc_params disc_params = {0};
|
||||
disc_params.filter_duplicates = 1;
|
||||
disc_params.passive = 0;
|
||||
ble_gap_disc(own_addr_type, BLE_HS_FOREVER, &disc_params,
|
||||
ble_esl_ap_gap_event, NULL);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ========================== Disconnect Event Handling ========================== */
|
||||
|
||||
static void handle_disconnect_event(struct ble_gap_event *event)
|
||||
{
|
||||
if (!g_esl_ap) {
|
||||
return;
|
||||
}
|
||||
|
||||
uint16_t conn_handle = event->disconnect.conn.conn_handle;
|
||||
int reason = event->disconnect.reason;
|
||||
|
||||
ESP_LOGI(TAG, "Disconnected; conn_handle=%d reason=%d", conn_handle, reason);
|
||||
|
||||
ble_esl_ap_conn_t *conn = ble_esl_ap_find_conn(conn_handle);
|
||||
if (!conn) {
|
||||
ESP_LOGW(TAG, "Disconnect for unknown conn_handle=%d", conn_handle);
|
||||
return;
|
||||
}
|
||||
|
||||
/* Fire disconnected event */
|
||||
if (g_esl_ap->app_cb) {
|
||||
ble_esl_ap_disconn_info_t info;
|
||||
info.conn_handle = conn_handle;
|
||||
memcpy(info.addr, conn->addr, 6);
|
||||
info.reason = (uint8_t)reason;
|
||||
g_esl_ap->app_cb(BLE_ESL_AP_EVT_DISCONNECTED, &info);
|
||||
}
|
||||
|
||||
/* Invoke any pending ECP callback with error before clearing the slot */
|
||||
if (conn->ecp_pending && conn->ecp_cb) {
|
||||
ble_esl_ap_gatt_cb_t cb = conn->ecp_cb;
|
||||
void *user_data = conn->ecp_user_data;
|
||||
conn->ecp_pending = false;
|
||||
conn->ecp_cb = NULL;
|
||||
conn->ecp_user_data = NULL;
|
||||
cb(conn_handle, ESP_FAIL, NULL, 0, user_data);
|
||||
}
|
||||
|
||||
/* Clean up any pending ECP command context and timer in the command module */
|
||||
ble_esl_ap_command_cleanup_conn(conn_handle);
|
||||
|
||||
/* Let lifecycle module complete any pending synchronize procedure
|
||||
* and handle link-loss state transitions */
|
||||
ble_esl_ap_lifecycle_handle_disconnect(conn_handle);
|
||||
|
||||
/* Free connection slot */
|
||||
ble_esl_ap_free_conn(conn);
|
||||
|
||||
/* Resume scanning if started */
|
||||
if (g_esl_ap->started && !ble_gap_disc_active()) {
|
||||
uint8_t own_addr_type;
|
||||
if (ble_hs_id_infer_auto(0, &own_addr_type) == 0) {
|
||||
struct ble_gap_disc_params disc_params = {0};
|
||||
disc_params.filter_duplicates = 1;
|
||||
disc_params.passive = 0;
|
||||
ble_gap_disc(own_addr_type, BLE_HS_FOREVER, &disc_params,
|
||||
ble_esl_ap_gap_event, NULL);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ======================================================================== */
|
||||
/* Handle encryption change */
|
||||
/* ======================================================================== */
|
||||
static void handle_enc_change(struct ble_gap_event *event)
|
||||
{
|
||||
uint16_t conn_handle = event->enc_change.conn_handle;
|
||||
uint8_t status = event->enc_change.status;
|
||||
ESP_LOGI(TAG, "Encryption change; conn_handle=%u status=%d",
|
||||
conn_handle, status);
|
||||
|
||||
if (status != 0) {
|
||||
ESP_LOGW(TAG, "Encryption failed for conn_handle=%u; terminating connection", conn_handle);
|
||||
ble_gap_terminate(conn_handle, BLE_ERR_AUTH_FAIL);
|
||||
return;
|
||||
}
|
||||
|
||||
ble_esl_ap_conn_t *conn = ble_esl_ap_find_conn(conn_handle);
|
||||
if (!conn) {
|
||||
return;
|
||||
}
|
||||
|
||||
/* If the encrypted link is to an already-associated ESL — reconnected from
|
||||
* Unsynchronized (connectable advertising) or from Synchronized (Periodic
|
||||
* Advertising Connection procedure) — the ESL has entered the Updating
|
||||
* state. Track it so ble_esl_ap_configure()/synchronize() see a valid state
|
||||
* and link-loss recovery routes it back to Unsynchronized. */
|
||||
ble_esl_ap_esl_entry_t *esl = ble_esl_ap_find_esl_by_ble_addr(conn->addr,
|
||||
conn->addr_type);
|
||||
if (esl != NULL &&
|
||||
(esl->state == BLE_ESL_STATE_SYNCHRONIZED ||
|
||||
esl->state == BLE_ESL_STATE_UNSYNCHRONIZED)) {
|
||||
esl->conn_handle = conn_handle;
|
||||
conn->esl_addr = esl->esl_addr;
|
||||
ble_esl_ap_update_esl_state(esl->esl_addr, BLE_ESL_STATE_UPDATING);
|
||||
}
|
||||
|
||||
if (conn->disc_done) {
|
||||
ESP_LOGD(TAG, "Encryption change on already-discovered conn_handle=%u; skipping discovery",
|
||||
conn_handle);
|
||||
return;
|
||||
}
|
||||
|
||||
/* Start discovery */
|
||||
ble_esl_ap_start_svc_discovery(conn_handle);
|
||||
}
|
||||
|
||||
/* ======================================================================== */
|
||||
/* Handle pairing complete */
|
||||
/* ======================================================================== */
|
||||
static void handle_pairing_complete(struct ble_gap_event *event)
|
||||
{
|
||||
uint16_t conn_handle = event->pairing_complete.conn_handle;
|
||||
uint8_t status = event->pairing_complete.status;
|
||||
ESP_LOGI(TAG, "Pairing complete; conn_handle=%u status=%d",
|
||||
conn_handle, status);
|
||||
}
|
||||
|
||||
/* ========================== Notification Handling ========================== */
|
||||
|
||||
static void handle_notify_rx(struct ble_gap_event *event)
|
||||
{
|
||||
if (!g_esl_ap) {
|
||||
return;
|
||||
}
|
||||
|
||||
uint16_t conn_handle = event->notify_rx.conn_handle;
|
||||
uint16_t attr_handle = event->notify_rx.attr_handle;
|
||||
|
||||
ble_esl_ap_conn_t *conn = ble_esl_ap_find_conn(conn_handle);
|
||||
if (!conn) {
|
||||
return;
|
||||
}
|
||||
|
||||
/* Check if this is an ECP notification */
|
||||
if (attr_handle == conn->handles.ecp_handle && conn->ecp_pending) {
|
||||
/* Extract notification data from mbuf */
|
||||
uint16_t data_len = OS_MBUF_PKTLEN(event->notify_rx.om);
|
||||
uint8_t *data = NULL;
|
||||
uint8_t buf[BLE_ESL_AP_READ_BUF_MAX];
|
||||
esp_err_t status = ESP_OK;
|
||||
|
||||
if (data_len > 0 && data_len <= sizeof(buf)) {
|
||||
int rc = os_mbuf_copydata(event->notify_rx.om, 0, data_len, buf);
|
||||
if (rc == 0) {
|
||||
data = buf;
|
||||
} else {
|
||||
data_len = 0;
|
||||
}
|
||||
} else if (data_len > sizeof(buf)) {
|
||||
ESP_LOGW(TAG, "ECP notification too large: %u > %zu", data_len, sizeof(buf));
|
||||
data_len = 0;
|
||||
status = ESP_FAIL;
|
||||
}
|
||||
|
||||
/* Clear pending state and invoke callback */
|
||||
conn->ecp_pending = false;
|
||||
ble_esl_ap_gatt_cb_t cb = conn->ecp_cb;
|
||||
void *user_data = conn->ecp_user_data;
|
||||
conn->ecp_cb = NULL;
|
||||
conn->ecp_user_data = NULL;
|
||||
|
||||
if (cb) {
|
||||
cb(conn_handle, status, data, data_len, user_data);
|
||||
} else if (data && data_len > 0) {
|
||||
/* Route to command module only when no registered callback handled it,
|
||||
* to avoid double-processing and spurious 'no pending command' warnings */
|
||||
ble_esl_ap_command_handle_ecp_notification(conn_handle, data, data_len);
|
||||
}
|
||||
}
|
||||
}
|
||||
932
components/bt/ble_profiles/nimble/ble_esl/src/ap/ap_gattc.c
Normal file
932
components/bt/ble_profiles/nimble/ble_esl/src/ap/ap_gattc.c
Normal file
@@ -0,0 +1,932 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
/**
|
||||
* @file ap_gattc.c
|
||||
* @brief ESL AP — GATT client: service/characteristic discovery and
|
||||
* internal GATT read/write/ECP helpers.
|
||||
*/
|
||||
|
||||
#include <string.h>
|
||||
#include "esp_log.h"
|
||||
#include "esp_err.h"
|
||||
|
||||
#include "nimble/ble.h"
|
||||
#include "host/ble_hs.h"
|
||||
#include "host/ble_gap.h"
|
||||
#include "host/ble_gatt.h"
|
||||
#include "host/ble_uuid.h"
|
||||
#include "host/util/util.h"
|
||||
|
||||
#include "ble_esl_ap.h"
|
||||
#include "ble_esl_ap_int.h"
|
||||
#include "ble_ots_client.h"
|
||||
|
||||
static const char *TAG = "esl_ap_gattc";
|
||||
|
||||
/* ========================== Forward Declarations ========================== */
|
||||
/* Discovery state machine steps */
|
||||
static int disc_svc_cb(uint16_t conn_handle, const struct ble_gatt_error *error,
|
||||
const struct ble_gatt_svc *service, void *arg);
|
||||
static void disc_start_esl_chr_discovery(uint16_t conn_handle);
|
||||
static int disc_esl_chr_cb(uint16_t conn_handle, const struct ble_gatt_error *error,
|
||||
const struct ble_gatt_chr *chr, void *arg);
|
||||
static void disc_start_dis_chr_discovery(uint16_t conn_handle);
|
||||
static int disc_dis_chr_cb(uint16_t conn_handle, const struct ble_gatt_error *error,
|
||||
const struct ble_gatt_chr *chr, void *arg);
|
||||
static void disc_start_ecp_dsc_discovery(uint16_t conn_handle);
|
||||
static int disc_ecp_dsc_cb(uint16_t conn_handle, const struct ble_gatt_error *error,
|
||||
uint16_t chr_val_handle, const struct ble_gatt_dsc *dsc,
|
||||
void *arg);
|
||||
static void disc_enable_ecp_notifications(uint16_t conn_handle);
|
||||
static int disc_cccd_write_cb(uint16_t conn_handle,
|
||||
const struct ble_gatt_error *error,
|
||||
struct ble_gatt_attr *attr, void *arg);
|
||||
static void disc_start_ots(uint16_t conn_handle);
|
||||
static void disc_complete(uint16_t conn_handle, bool success);
|
||||
|
||||
/* GATT operation callbacks */
|
||||
static int gatt_read_cb(uint16_t conn_handle, const struct ble_gatt_error *error,
|
||||
struct ble_gatt_attr *attr, void *arg);
|
||||
static int gatt_read_long_cb(uint16_t conn_handle, const struct ble_gatt_error *error,
|
||||
struct ble_gatt_attr *attr, void *arg);
|
||||
static int gatt_write_cb(uint16_t conn_handle, const struct ble_gatt_error *error,
|
||||
struct ble_gatt_attr *attr, void *arg);
|
||||
static int ecp_write_cb(uint16_t conn_handle, const struct ble_gatt_error *error,
|
||||
struct ble_gatt_attr *attr, void *arg);
|
||||
|
||||
/* Characteristic handle resolver */
|
||||
static uint16_t resolve_char_handle(ble_esl_ap_conn_t *conn, uint16_t char_uuid);
|
||||
|
||||
/* ========================== OTS Client Callback ========================== */
|
||||
|
||||
static void ble_esl_ap_ots_event_cb(uint16_t conn_id,
|
||||
ble_ots_client_event_t event,
|
||||
const void *param)
|
||||
{
|
||||
if (event == BLE_OTS_CLIENT_EVT_DISCOVER_COMPLETE) {
|
||||
const ble_ots_client_discover_complete_t *d = param;
|
||||
ESP_LOGI(TAG, "OTS discovery %s; conn_id=%d",
|
||||
d->status == 0 ? "complete" : "failed", conn_id);
|
||||
disc_complete(conn_id, d->status == 0);
|
||||
return;
|
||||
}
|
||||
|
||||
ble_esl_ap_lifecycle_handle_ots_event(conn_id, (int)event, param);
|
||||
}
|
||||
|
||||
/* ========================== Service Discovery State Machine ========================== */
|
||||
|
||||
/**
|
||||
* @brief Step 1: Discover all primary services
|
||||
*/
|
||||
void ble_esl_ap_start_svc_discovery(uint16_t conn_handle)
|
||||
{
|
||||
ESP_LOGD(TAG, "Starting service discovery; conn_handle=%d", conn_handle);
|
||||
|
||||
int rc = ble_gattc_disc_all_svcs(conn_handle, disc_svc_cb, NULL);
|
||||
if (rc != 0) {
|
||||
ESP_LOGE(TAG, "Failed to start service discovery; rc=%d", rc);
|
||||
disc_complete(conn_handle, false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Service discovery callback — collects ESL Service and DIS handles
|
||||
*/
|
||||
static int disc_svc_cb(uint16_t conn_handle, const struct ble_gatt_error *error,
|
||||
const struct ble_gatt_svc *service, void *arg)
|
||||
{
|
||||
ble_esl_ap_conn_t *conn = ble_esl_ap_find_conn(conn_handle);
|
||||
if (!conn) {
|
||||
return BLE_HS_ENOTCONN;
|
||||
}
|
||||
|
||||
if (error->status == 0 && service != NULL) {
|
||||
uint16_t uuid16 = ble_uuid_u16(&service->uuid.u);
|
||||
|
||||
if (uuid16 == BLE_ESL_SVC_UUID) {
|
||||
conn->handles.esl_svc_start = service->start_handle;
|
||||
conn->handles.esl_svc_end = service->end_handle;
|
||||
ESP_LOGD(TAG, "Found ESL Service: start=%d end=%d",
|
||||
service->start_handle, service->end_handle);
|
||||
} else if (uuid16 == BLE_ESL_AP_DIS_UUID) {
|
||||
conn->handles.dis_svc_start = service->start_handle;
|
||||
conn->handles.dis_svc_end = service->end_handle;
|
||||
ESP_LOGD(TAG, "Found DIS: start=%d end=%d",
|
||||
service->start_handle, service->end_handle);
|
||||
} else if (uuid16 == BLE_OTS_UUID_OTS_SERVICE) {
|
||||
conn->has_ots = true;
|
||||
ESP_LOGD(TAG, "Found OTS Service: start=%d end=%d",
|
||||
service->start_handle, service->end_handle);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (error->status == BLE_HS_EDONE) {
|
||||
/* Service discovery complete — check mandatory ESL Service */
|
||||
if (conn->handles.esl_svc_start == 0) {
|
||||
ESP_LOGE(TAG, "ESL Service not found; disconnecting");
|
||||
disc_complete(conn_handle, false);
|
||||
return 0;
|
||||
}
|
||||
/* Proceed to ESL characteristic discovery */
|
||||
disc_start_esl_chr_discovery(conn_handle);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Error during discovery */
|
||||
ESP_LOGE(TAG, "Service discovery error; status=%d", error->status);
|
||||
disc_complete(conn_handle, false);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Step 2: Discover all characteristics within the ESL Service
|
||||
*/
|
||||
static void disc_start_esl_chr_discovery(uint16_t conn_handle)
|
||||
{
|
||||
ble_esl_ap_conn_t *conn = ble_esl_ap_find_conn(conn_handle);
|
||||
if (!conn) {
|
||||
return;
|
||||
}
|
||||
|
||||
ESP_LOGD(TAG, "Discovering ESL Service characteristics; conn_handle=%d", conn_handle);
|
||||
|
||||
int rc = ble_gattc_disc_all_chrs(conn_handle,
|
||||
conn->handles.esl_svc_start,
|
||||
conn->handles.esl_svc_end,
|
||||
disc_esl_chr_cb, NULL);
|
||||
if (rc != 0) {
|
||||
ESP_LOGE(TAG, "Failed to start ESL chr discovery; rc=%d", rc);
|
||||
disc_complete(conn_handle, false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief ESL Service characteristic discovery callback
|
||||
*/
|
||||
static int disc_esl_chr_cb(uint16_t conn_handle, const struct ble_gatt_error *error,
|
||||
const struct ble_gatt_chr *chr, void *arg)
|
||||
{
|
||||
ble_esl_ap_conn_t *conn = ble_esl_ap_find_conn(conn_handle);
|
||||
if (!conn) {
|
||||
return BLE_HS_ENOTCONN;
|
||||
}
|
||||
|
||||
if (error->status == 0 && chr != NULL) {
|
||||
uint16_t uuid16 = ble_uuid_u16(&chr->uuid.u);
|
||||
|
||||
switch (uuid16) {
|
||||
case BLE_ESL_CHR_UUID_ESL_ADDRESS:
|
||||
conn->handles.esl_addr_handle = chr->val_handle;
|
||||
break;
|
||||
case BLE_ESL_CHR_UUID_AP_SYNC_KEY:
|
||||
conn->handles.ap_sync_key_handle = chr->val_handle;
|
||||
break;
|
||||
case BLE_ESL_CHR_UUID_RESP_KEY:
|
||||
conn->handles.resp_key_handle = chr->val_handle;
|
||||
break;
|
||||
case BLE_ESL_CHR_UUID_CURRENT_ABS_TIME:
|
||||
conn->handles.abs_time_handle = chr->val_handle;
|
||||
break;
|
||||
case BLE_ESL_CHR_UUID_DISPLAY_INFO:
|
||||
conn->handles.display_info_handle = chr->val_handle;
|
||||
break;
|
||||
case BLE_ESL_CHR_UUID_IMAGE_INFO:
|
||||
conn->handles.image_info_handle = chr->val_handle;
|
||||
break;
|
||||
case BLE_ESL_CHR_UUID_SENSOR_INFO:
|
||||
conn->handles.sensor_info_handle = chr->val_handle;
|
||||
break;
|
||||
case BLE_ESL_CHR_UUID_LED_INFO:
|
||||
conn->handles.led_info_handle = chr->val_handle;
|
||||
break;
|
||||
case BLE_ESL_CHR_UUID_ECP:
|
||||
conn->handles.ecp_handle = chr->val_handle;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (error->status == BLE_HS_EDONE) {
|
||||
/* Verify mandatory characteristics */
|
||||
if (conn->handles.esl_addr_handle == 0 ||
|
||||
conn->handles.ap_sync_key_handle == 0 ||
|
||||
conn->handles.resp_key_handle == 0 ||
|
||||
conn->handles.abs_time_handle == 0 ||
|
||||
conn->handles.ecp_handle == 0) {
|
||||
ESP_LOGE(TAG, "Missing mandatory ESL characteristics; disconnecting");
|
||||
disc_complete(conn_handle, false);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Proceed to DIS characteristic discovery or ECP descriptor discovery */
|
||||
if (conn->handles.dis_svc_start != 0) {
|
||||
disc_start_dis_chr_discovery(conn_handle);
|
||||
} else {
|
||||
disc_start_ecp_dsc_discovery(conn_handle);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
ESP_LOGE(TAG, "ESL chr discovery error; status=%d", error->status);
|
||||
disc_complete(conn_handle, false);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Step 3: Discover DIS characteristics (PnP ID)
|
||||
*/
|
||||
static void disc_start_dis_chr_discovery(uint16_t conn_handle)
|
||||
{
|
||||
ble_esl_ap_conn_t *conn = ble_esl_ap_find_conn(conn_handle);
|
||||
if (!conn) {
|
||||
return;
|
||||
}
|
||||
|
||||
ESP_LOGD(TAG, "Discovering DIS characteristics; conn_handle=%d", conn_handle);
|
||||
|
||||
int rc = ble_gattc_disc_all_chrs(conn_handle,
|
||||
conn->handles.dis_svc_start,
|
||||
conn->handles.dis_svc_end,
|
||||
disc_dis_chr_cb, NULL);
|
||||
if (rc != 0) {
|
||||
ESP_LOGE(TAG, "Failed to start DIS chr discovery; rc=%d", rc);
|
||||
/* DIS is optional; proceed to ECP descriptor discovery */
|
||||
disc_start_ecp_dsc_discovery(conn_handle);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief DIS characteristic discovery callback
|
||||
*/
|
||||
static int disc_dis_chr_cb(uint16_t conn_handle, const struct ble_gatt_error *error,
|
||||
const struct ble_gatt_chr *chr, void *arg)
|
||||
{
|
||||
ble_esl_ap_conn_t *conn = ble_esl_ap_find_conn(conn_handle);
|
||||
if (!conn) {
|
||||
return BLE_HS_ENOTCONN;
|
||||
}
|
||||
|
||||
if (error->status == 0 && chr != NULL) {
|
||||
uint16_t uuid16 = ble_uuid_u16(&chr->uuid.u);
|
||||
if (uuid16 == BLE_ESL_AP_PNP_ID_UUID) {
|
||||
conn->handles.pnp_id_handle = chr->val_handle;
|
||||
ESP_LOGD(TAG, "Found PnP ID characteristic; handle=%d", chr->val_handle);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (error->status == BLE_HS_EDONE) {
|
||||
/* Proceed to ECP descriptor discovery */
|
||||
disc_start_ecp_dsc_discovery(conn_handle);
|
||||
return 0;
|
||||
}
|
||||
|
||||
ESP_LOGW(TAG, "DIS chr discovery error; status=%d", error->status);
|
||||
/* DIS is optional; proceed anyway */
|
||||
disc_start_ecp_dsc_discovery(conn_handle);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Step 4: Discover ECP CCCD descriptor
|
||||
*/
|
||||
static void disc_start_ecp_dsc_discovery(uint16_t conn_handle)
|
||||
{
|
||||
ble_esl_ap_conn_t *conn = ble_esl_ap_find_conn(conn_handle);
|
||||
if (!conn) {
|
||||
return;
|
||||
}
|
||||
|
||||
ESP_LOGD(TAG, "Discovering ECP descriptors; conn_handle=%d", conn_handle);
|
||||
|
||||
/* Discover descriptors between ECP value handle and ESL service end handle.
|
||||
* ble_gattc_disc_all_dscs expects the characteristic value handle as
|
||||
* start_handle; it internally starts the Find Information Request at
|
||||
* start_handle + 1, so passing ecp_handle ensures the CCCD at
|
||||
* ecp_handle + 1 is found. */
|
||||
uint16_t start = conn->handles.ecp_handle;
|
||||
uint16_t end = conn->handles.esl_svc_end;
|
||||
|
||||
if (start >= end) {
|
||||
ESP_LOGE(TAG, "Invalid ECP descriptor range");
|
||||
disc_complete(conn_handle, false);
|
||||
return;
|
||||
}
|
||||
|
||||
int rc = ble_gattc_disc_all_dscs(conn_handle, start, end,
|
||||
disc_ecp_dsc_cb, NULL);
|
||||
if (rc != 0) {
|
||||
ESP_LOGE(TAG, "Failed to start ECP dsc discovery; rc=%d", rc);
|
||||
disc_complete(conn_handle, false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief ECP descriptor discovery callback — looks for CCCD
|
||||
*/
|
||||
static int disc_ecp_dsc_cb(uint16_t conn_handle, const struct ble_gatt_error *error,
|
||||
uint16_t chr_val_handle, const struct ble_gatt_dsc *dsc,
|
||||
void *arg)
|
||||
{
|
||||
ble_esl_ap_conn_t *conn = ble_esl_ap_find_conn(conn_handle);
|
||||
if (!conn) {
|
||||
return BLE_HS_ENOTCONN;
|
||||
}
|
||||
|
||||
if (error->status == 0 && dsc != NULL) {
|
||||
uint16_t uuid16 = ble_uuid_u16(&dsc->uuid.u);
|
||||
if (uuid16 == BLE_GATT_DSC_CLT_CFG_UUID16) {
|
||||
conn->handles.ecp_cccd_handle = dsc->handle;
|
||||
ESP_LOGD(TAG, "Found ECP CCCD; handle=%d", dsc->handle);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (error->status == BLE_HS_EDONE) {
|
||||
if (conn->handles.ecp_cccd_handle == 0) {
|
||||
ESP_LOGE(TAG, "ECP CCCD not found; disconnecting");
|
||||
disc_complete(conn_handle, false);
|
||||
return 0;
|
||||
}
|
||||
/* Enable notifications on ECP */
|
||||
disc_enable_ecp_notifications(conn_handle);
|
||||
return 0;
|
||||
}
|
||||
|
||||
ESP_LOGE(TAG, "ECP dsc discovery error; status=%d", error->status);
|
||||
disc_complete(conn_handle, false);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Step 5: Write CCCD to enable ECP notifications
|
||||
*/
|
||||
static void disc_enable_ecp_notifications(uint16_t conn_handle)
|
||||
{
|
||||
ble_esl_ap_conn_t *conn = ble_esl_ap_find_conn(conn_handle);
|
||||
if (!conn) {
|
||||
return;
|
||||
}
|
||||
|
||||
ESP_LOGD(TAG, "Enabling ECP notifications; conn_handle=%d", conn_handle);
|
||||
|
||||
uint8_t value[2] = {1, 0}; /* Enable notifications */
|
||||
int rc = ble_gattc_write_flat(conn_handle, conn->handles.ecp_cccd_handle,
|
||||
value, sizeof(value),
|
||||
disc_cccd_write_cb, NULL);
|
||||
if (rc != 0) {
|
||||
ESP_LOGE(TAG, "Failed to write ECP CCCD; rc=%d", rc);
|
||||
disc_complete(conn_handle, false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief CCCD write completion callback
|
||||
*/
|
||||
static int disc_cccd_write_cb(uint16_t conn_handle,
|
||||
const struct ble_gatt_error *error,
|
||||
struct ble_gatt_attr *attr, void *arg)
|
||||
{
|
||||
if (error->status != 0) {
|
||||
ESP_LOGE(TAG, "CCCD write failed; status=%d", error->status);
|
||||
disc_complete(conn_handle, false);
|
||||
return 0;
|
||||
}
|
||||
|
||||
ESP_LOGD(TAG, "ECP notifications enabled; conn_handle=%d", conn_handle);
|
||||
disc_start_ots(conn_handle);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Step 6: Start OTS service discovery if the tag exposes OTS,
|
||||
* otherwise go straight to disc_complete.
|
||||
*
|
||||
* BLE_OTS_CLIENT_EVT_DISCOVER_COMPLETE (intercepted in ble_esl_ap_ots_event_cb)
|
||||
* will call disc_complete once OTS discovery finishes.
|
||||
*/
|
||||
static void disc_start_ots(uint16_t conn_handle)
|
||||
{
|
||||
ble_esl_ap_conn_t *conn = ble_esl_ap_find_conn(conn_handle);
|
||||
if (!conn) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!conn->has_ots) {
|
||||
disc_complete(conn_handle, true);
|
||||
return;
|
||||
}
|
||||
|
||||
ESP_LOGD(TAG, "Starting OTS discovery; conn_handle=%d", conn_handle);
|
||||
int rc = ble_ots_client_discover_service(conn_handle);
|
||||
if (rc != 0) {
|
||||
ESP_LOGE(TAG, "Failed to start OTS discovery; rc=%d", rc);
|
||||
disc_complete(conn_handle, false);
|
||||
}
|
||||
/* On success: wait for BLE_OTS_CLIENT_EVT_DISCOVER_COMPLETE */
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Discovery complete — fire connected or disconnected event
|
||||
*/
|
||||
static void disc_complete(uint16_t conn_handle, bool success)
|
||||
{
|
||||
ble_esl_ap_conn_t *conn = ble_esl_ap_find_conn(conn_handle);
|
||||
if (!conn) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!success) {
|
||||
/* Disconnect and let the disconnect handler fire the event */
|
||||
ESP_LOGW(TAG, "Discovery failed; disconnecting conn_handle=%d", conn_handle);
|
||||
ble_gap_terminate(conn_handle, BLE_ERR_REM_USER_CONN_TERM);
|
||||
return;
|
||||
}
|
||||
|
||||
conn->disc_done = true;
|
||||
|
||||
/* Build connected event info */
|
||||
if (g_esl_ap && g_esl_ap->app_cb) {
|
||||
ble_esl_ap_conn_info_t info;
|
||||
memset(&info, 0, sizeof(info));
|
||||
info.conn_handle = conn_handle;
|
||||
memcpy(info.addr, conn->addr, 6);
|
||||
info.addr_type = conn->addr_type;
|
||||
info.has_display_info = (conn->handles.display_info_handle != 0);
|
||||
info.has_image_info = (conn->handles.image_info_handle != 0);
|
||||
info.has_sensor_info = (conn->handles.sensor_info_handle != 0);
|
||||
info.has_led_info = (conn->handles.led_info_handle != 0);
|
||||
info.has_dis = (conn->handles.dis_svc_start != 0);
|
||||
info.has_pnp_id = (conn->handles.pnp_id_handle != 0);
|
||||
|
||||
ESP_LOGI(TAG, "Discovery complete; conn_handle=%d display=%d image=%d "
|
||||
"sensor=%d led=%d dis=%d pnp=%d",
|
||||
conn_handle, info.has_display_info, info.has_image_info,
|
||||
info.has_sensor_info, info.has_led_info, info.has_dis,
|
||||
info.has_pnp_id);
|
||||
|
||||
g_esl_ap->app_cb(BLE_ESL_AP_EVT_CONNECTED, &info);
|
||||
}
|
||||
}
|
||||
|
||||
/* ========================== Characteristic Handle Resolver ========================== */
|
||||
|
||||
/**
|
||||
* @brief Resolve a 16-bit characteristic UUID to its discovered attribute handle
|
||||
*
|
||||
* @param conn Connection context
|
||||
* @param char_uuid 16-bit UUID
|
||||
* @return Attribute handle, or 0 if not found
|
||||
*/
|
||||
static uint16_t resolve_char_handle(ble_esl_ap_conn_t *conn, uint16_t char_uuid)
|
||||
{
|
||||
if (!conn) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
switch (char_uuid) {
|
||||
case BLE_ESL_CHR_UUID_ESL_ADDRESS:
|
||||
return conn->handles.esl_addr_handle;
|
||||
case BLE_ESL_CHR_UUID_AP_SYNC_KEY:
|
||||
return conn->handles.ap_sync_key_handle;
|
||||
case BLE_ESL_CHR_UUID_RESP_KEY:
|
||||
return conn->handles.resp_key_handle;
|
||||
case BLE_ESL_CHR_UUID_CURRENT_ABS_TIME:
|
||||
return conn->handles.abs_time_handle;
|
||||
case BLE_ESL_CHR_UUID_DISPLAY_INFO:
|
||||
return conn->handles.display_info_handle;
|
||||
case BLE_ESL_CHR_UUID_IMAGE_INFO:
|
||||
return conn->handles.image_info_handle;
|
||||
case BLE_ESL_CHR_UUID_SENSOR_INFO:
|
||||
return conn->handles.sensor_info_handle;
|
||||
case BLE_ESL_CHR_UUID_LED_INFO:
|
||||
return conn->handles.led_info_handle;
|
||||
case BLE_ESL_CHR_UUID_ECP:
|
||||
return conn->handles.ecp_handle;
|
||||
case BLE_ESL_AP_PNP_ID_UUID:
|
||||
return conn->handles.pnp_id_handle;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* ========================== Internal GATT Helpers ========================== */
|
||||
|
||||
/**
|
||||
* @brief Context passed through GATT read callbacks
|
||||
*/
|
||||
typedef struct {
|
||||
ble_esl_ap_gatt_cb_t cb;
|
||||
void *user_data;
|
||||
uint16_t attr_handle;
|
||||
bool use_read_long;
|
||||
} gatt_read_ctx_t;
|
||||
|
||||
/**
|
||||
* @brief Callback for ble_gattc_read (single-shot read)
|
||||
*
|
||||
* If the read value fills the entire ATT_MTU-1, we switch to Read Long
|
||||
* to get the complete value.
|
||||
*/
|
||||
static int gatt_read_cb(uint16_t conn_handle, const struct ble_gatt_error *error,
|
||||
struct ble_gatt_attr *attr, void *arg)
|
||||
{
|
||||
gatt_read_ctx_t *ctx = (gatt_read_ctx_t *)arg;
|
||||
if (!ctx) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
ble_esl_ap_conn_t *conn = ble_esl_ap_find_conn(conn_handle);
|
||||
|
||||
if (error->status != 0) {
|
||||
/* Read failed */
|
||||
if (conn) {
|
||||
conn->read_pending = false;
|
||||
}
|
||||
if (ctx->cb) {
|
||||
ctx->cb(conn_handle, ESP_FAIL, NULL, 0, ctx->user_data);
|
||||
}
|
||||
free(ctx);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Copy data from mbuf */
|
||||
uint16_t data_len = OS_MBUF_PKTLEN(attr->om);
|
||||
|
||||
if (conn && data_len > 0) {
|
||||
uint16_t copy_len = data_len;
|
||||
if (copy_len > BLE_ESL_AP_READ_BUF_MAX) {
|
||||
copy_len = BLE_ESL_AP_READ_BUF_MAX;
|
||||
}
|
||||
int rc = os_mbuf_copydata(attr->om, 0, copy_len, conn->read_buf);
|
||||
if (rc != 0) {
|
||||
conn->read_pending = false;
|
||||
if (ctx->cb) {
|
||||
ctx->cb(conn_handle, ESP_FAIL, NULL, 0, ctx->user_data);
|
||||
}
|
||||
free(ctx);
|
||||
return 0;
|
||||
}
|
||||
conn->read_offset = copy_len;
|
||||
|
||||
/* Check if we need Read Long: only if the data fills exactly
|
||||
* ATT_MTU-1 bytes (indicating more data may be available) and
|
||||
* we still have buffer space. Use the negotiated MTU dynamically. */
|
||||
uint16_t mtu = ble_att_mtu(conn_handle);
|
||||
if (mtu > 0 && data_len == (mtu - 1) &&
|
||||
conn->read_offset < BLE_ESL_AP_READ_BUF_MAX) {
|
||||
/* Switch to Read Long to get the rest */
|
||||
int rl_rc = ble_gattc_read_long(conn_handle, ctx->attr_handle,
|
||||
copy_len, gatt_read_long_cb, ctx);
|
||||
if (rl_rc == 0) {
|
||||
return 0; /* ctx ownership transferred to read_long_cb */
|
||||
}
|
||||
/* If Read Long fails to start, deliver what we have */
|
||||
}
|
||||
|
||||
/* Deliver the data we have */
|
||||
conn->read_pending = false;
|
||||
if (ctx->cb) {
|
||||
ctx->cb(conn_handle, ESP_OK, conn->read_buf, conn->read_offset,
|
||||
ctx->user_data);
|
||||
}
|
||||
} else {
|
||||
/* No data or no connection */
|
||||
if (conn) {
|
||||
conn->read_pending = false;
|
||||
}
|
||||
if (ctx->cb) {
|
||||
ctx->cb(conn_handle, ESP_OK, NULL, 0, ctx->user_data);
|
||||
}
|
||||
}
|
||||
|
||||
free(ctx);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Callback for ble_gattc_read_long — called multiple times per fragment,
|
||||
* then once with error->status == BLE_HS_EDONE on completion.
|
||||
*/
|
||||
static int gatt_read_long_cb(uint16_t conn_handle, const struct ble_gatt_error *error,
|
||||
struct ble_gatt_attr *attr, void *arg)
|
||||
{
|
||||
gatt_read_ctx_t *ctx = (gatt_read_ctx_t *)arg;
|
||||
if (!ctx) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
ble_esl_ap_conn_t *conn = ble_esl_ap_find_conn(conn_handle);
|
||||
|
||||
if (error->status == 0 && attr != NULL) {
|
||||
/* Fragment received — append to read buffer */
|
||||
if (conn) {
|
||||
uint16_t frag_len = OS_MBUF_PKTLEN(attr->om);
|
||||
uint16_t space = BLE_ESL_AP_READ_BUF_MAX - conn->read_offset;
|
||||
uint16_t copy_len = (frag_len < space) ? frag_len : space;
|
||||
|
||||
if (copy_len > 0) {
|
||||
int rc = os_mbuf_copydata(attr->om, 0, copy_len,
|
||||
conn->read_buf + conn->read_offset);
|
||||
if (rc == 0) {
|
||||
conn->read_offset += copy_len;
|
||||
}
|
||||
}
|
||||
}
|
||||
return 0; /* More fragments may follow */
|
||||
}
|
||||
|
||||
if (error->status == BLE_HS_EDONE) {
|
||||
/* Read Long complete — deliver full value */
|
||||
if (conn) {
|
||||
conn->read_pending = false;
|
||||
}
|
||||
if (ctx->cb && conn) {
|
||||
ctx->cb(conn_handle, ESP_OK, conn->read_buf, conn->read_offset,
|
||||
ctx->user_data);
|
||||
} else if (ctx->cb) {
|
||||
/* Connection lost — report failure */
|
||||
ctx->cb(conn_handle, ESP_FAIL, NULL, 0, ctx->user_data);
|
||||
}
|
||||
free(ctx);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Error — clear read_pending */
|
||||
if (conn) {
|
||||
conn->read_pending = false;
|
||||
}
|
||||
if (conn && conn->read_offset > 0 &&
|
||||
(error->status == BLE_HS_ATT_ERR(BLE_ATT_ERR_INVALID_OFFSET) ||
|
||||
error->status == BLE_HS_ATT_ERR(BLE_ATT_ERR_ATTR_NOT_LONG))) {
|
||||
/* ATT error indicates end-of-attribute — deliver partial data as complete */
|
||||
if (ctx->cb) {
|
||||
ctx->cb(conn_handle, ESP_OK, conn->read_buf, conn->read_offset,
|
||||
ctx->user_data);
|
||||
}
|
||||
} else {
|
||||
/* Real GATT error (timeout, disconnect, etc.) — propagate failure */
|
||||
if (ctx->cb) {
|
||||
ctx->cb(conn_handle, ESP_FAIL, NULL, 0, ctx->user_data);
|
||||
}
|
||||
}
|
||||
free(ctx);
|
||||
return 0;
|
||||
}
|
||||
|
||||
esp_err_t ble_esl_ap_gatt_read(uint16_t conn_handle, uint16_t char_uuid,
|
||||
ble_esl_ap_gatt_cb_t cb, void *user_data)
|
||||
{
|
||||
if (!g_esl_ap || !g_esl_ap->initialized) {
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
|
||||
ble_esl_ap_conn_t *conn = ble_esl_ap_find_conn(conn_handle);
|
||||
if (!conn) {
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
|
||||
uint16_t attr_handle = resolve_char_handle(conn, char_uuid);
|
||||
if (attr_handle == 0) {
|
||||
return ESP_ERR_NOT_FOUND;
|
||||
}
|
||||
|
||||
/* Prevent concurrent GATT reads on the same connection */
|
||||
if (conn->read_pending) {
|
||||
ESP_LOGW(TAG, "GATT read already in progress on conn_handle=%d", conn_handle);
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
|
||||
/* Allocate read context */
|
||||
gatt_read_ctx_t *ctx = calloc(1, sizeof(gatt_read_ctx_t));
|
||||
if (!ctx) {
|
||||
return ESP_ERR_NO_MEM;
|
||||
}
|
||||
ctx->cb = cb;
|
||||
ctx->user_data = user_data;
|
||||
ctx->attr_handle = attr_handle;
|
||||
ctx->use_read_long = false;
|
||||
|
||||
/* Mark read as in progress and reset read buffer */
|
||||
conn->read_pending = true;
|
||||
conn->read_offset = 0;
|
||||
|
||||
/* For characteristics known to potentially exceed ATT_MTU-1,
|
||||
* use Read Long directly */
|
||||
if (char_uuid == BLE_ESL_CHR_UUID_DISPLAY_INFO ||
|
||||
char_uuid == BLE_ESL_CHR_UUID_SENSOR_INFO ||
|
||||
char_uuid == BLE_ESL_CHR_UUID_LED_INFO) {
|
||||
ctx->use_read_long = true;
|
||||
int rc = ble_gattc_read_long(conn_handle, attr_handle, 0,
|
||||
gatt_read_long_cb, ctx);
|
||||
if (rc != 0) {
|
||||
ESP_LOGE(TAG, "Failed to start read long; rc=%d", rc);
|
||||
conn->read_pending = false;
|
||||
free(ctx);
|
||||
return ESP_FAIL;
|
||||
}
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
/* Standard single-shot read */
|
||||
int rc = ble_gattc_read(conn_handle, attr_handle, gatt_read_cb, ctx);
|
||||
if (rc != 0) {
|
||||
ESP_LOGE(TAG, "Failed to start read; rc=%d", rc);
|
||||
conn->read_pending = false;
|
||||
free(ctx);
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Context passed through GATT write callbacks
|
||||
*/
|
||||
typedef struct {
|
||||
ble_esl_ap_gatt_cb_t cb;
|
||||
void *user_data;
|
||||
} gatt_write_ctx_t;
|
||||
|
||||
/**
|
||||
* @brief Callback for ble_gattc_write_flat
|
||||
*/
|
||||
static int gatt_write_cb(uint16_t conn_handle, const struct ble_gatt_error *error,
|
||||
struct ble_gatt_attr *attr, void *arg)
|
||||
{
|
||||
gatt_write_ctx_t *ctx = (gatt_write_ctx_t *)arg;
|
||||
if (!ctx) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
esp_err_t status = (error->status == 0) ? ESP_OK : ESP_FAIL;
|
||||
|
||||
if (ctx->cb) {
|
||||
ctx->cb(conn_handle, status, NULL, 0, ctx->user_data);
|
||||
}
|
||||
|
||||
free(ctx);
|
||||
return 0;
|
||||
}
|
||||
|
||||
esp_err_t ble_esl_ap_gatt_write(uint16_t conn_handle, uint16_t char_uuid,
|
||||
const uint8_t *data, uint16_t data_len,
|
||||
ble_esl_ap_gatt_cb_t cb, void *user_data)
|
||||
{
|
||||
if (!g_esl_ap || !g_esl_ap->initialized) {
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
|
||||
ble_esl_ap_conn_t *conn = ble_esl_ap_find_conn(conn_handle);
|
||||
if (!conn) {
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
|
||||
uint16_t attr_handle = resolve_char_handle(conn, char_uuid);
|
||||
if (attr_handle == 0) {
|
||||
return ESP_ERR_NOT_FOUND;
|
||||
}
|
||||
|
||||
/* Allocate write context */
|
||||
gatt_write_ctx_t *ctx = calloc(1, sizeof(gatt_write_ctx_t));
|
||||
if (!ctx) {
|
||||
return ESP_ERR_NO_MEM;
|
||||
}
|
||||
ctx->cb = cb;
|
||||
ctx->user_data = user_data;
|
||||
|
||||
/* A single Write Request carries at most ATT_MTU - 3 bytes of value
|
||||
* (1 byte opcode + 2 byte handle). If the payload exceeds that, fall back
|
||||
* to the Write Long procedure. Use the negotiated MTU dynamically. */
|
||||
uint16_t mtu = ble_att_mtu(conn_handle);
|
||||
if (mtu < 3) {
|
||||
/* ble_att_mtu() returns 0 when the ATT channel is gone (e.g. the peer
|
||||
* disconnected while this write was being prepared). Bail out instead
|
||||
* of deriving a bogus payload limit from it. */
|
||||
ESP_LOGE(TAG, "Invalid MTU for write; conn_handle=%d mtu=%d", conn_handle, mtu);
|
||||
free(ctx);
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
|
||||
int rc;
|
||||
if (data_len > (mtu - 3)) {
|
||||
/* Write Long — payload must be carried in an mbuf */
|
||||
struct os_mbuf *om = ble_hs_mbuf_from_flat(data, data_len);
|
||||
if (!om) {
|
||||
ESP_LOGE(TAG, "Failed to allocate mbuf for write long");
|
||||
free(ctx);
|
||||
return ESP_ERR_NO_MEM;
|
||||
}
|
||||
rc = ble_gattc_write_long(conn_handle, attr_handle, 0, om,
|
||||
gatt_write_cb, ctx);
|
||||
if (rc != 0) {
|
||||
ESP_LOGE(TAG, "Failed to start write long; rc=%d", rc);
|
||||
/* om is consumed by ble_gattc_write_long regardless of outcome */
|
||||
free(ctx);
|
||||
return ESP_FAIL;
|
||||
}
|
||||
} else {
|
||||
rc = ble_gattc_write_flat(conn_handle, attr_handle,
|
||||
data, data_len,
|
||||
gatt_write_cb, ctx);
|
||||
if (rc != 0) {
|
||||
ESP_LOGE(TAG, "Failed to start write; rc=%d", rc);
|
||||
free(ctx);
|
||||
return ESP_FAIL;
|
||||
}
|
||||
}
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Callback for ECP write completion
|
||||
*
|
||||
* After the write completes, we wait for the ECP notification.
|
||||
* If the write itself fails, invoke the ECP callback with error.
|
||||
*/
|
||||
static int ecp_write_cb(uint16_t conn_handle, const struct ble_gatt_error *error,
|
||||
struct ble_gatt_attr *attr, void *arg)
|
||||
{
|
||||
(void)arg;
|
||||
|
||||
if (error->status != 0) {
|
||||
ESP_LOGE(TAG, "ECP write failed; status=%d", error->status);
|
||||
|
||||
ble_esl_ap_conn_t *conn = ble_esl_ap_find_conn(conn_handle);
|
||||
if (conn && conn->ecp_pending) {
|
||||
conn->ecp_pending = false;
|
||||
ble_esl_ap_gatt_cb_t cb = conn->ecp_cb;
|
||||
void *user_data = conn->ecp_user_data;
|
||||
conn->ecp_cb = NULL;
|
||||
conn->ecp_user_data = NULL;
|
||||
|
||||
if (cb) {
|
||||
cb(conn_handle, ESP_FAIL, NULL, 0, user_data);
|
||||
}
|
||||
}
|
||||
}
|
||||
/* On success, we wait for the notification (handled in handle_notify_rx) */
|
||||
return 0;
|
||||
}
|
||||
|
||||
esp_err_t ble_esl_ap_ecp_write(uint16_t conn_handle, const uint8_t *tlv_data,
|
||||
uint8_t tlv_len, ble_esl_ap_gatt_cb_t cb,
|
||||
void *user_data)
|
||||
{
|
||||
if (!g_esl_ap || !g_esl_ap->initialized) {
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
|
||||
ble_esl_ap_conn_t *conn = ble_esl_ap_find_conn(conn_handle);
|
||||
if (!conn) {
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
|
||||
if (conn->handles.ecp_handle == 0) {
|
||||
return ESP_ERR_NOT_FOUND;
|
||||
}
|
||||
|
||||
if (conn->ecp_pending) {
|
||||
ESP_LOGW(TAG, "ECP operation already pending on conn_handle=%d", conn_handle);
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
|
||||
/* Set up ECP pending state */
|
||||
conn->ecp_pending = true;
|
||||
conn->ecp_cb = cb;
|
||||
conn->ecp_user_data = user_data;
|
||||
|
||||
/* Write to ECP characteristic */
|
||||
int rc = ble_gattc_write_flat(conn_handle, conn->handles.ecp_handle,
|
||||
tlv_data, tlv_len,
|
||||
ecp_write_cb, NULL);
|
||||
if (rc != 0) {
|
||||
ESP_LOGE(TAG, "Failed to write ECP; rc=%d", rc);
|
||||
conn->ecp_pending = false;
|
||||
conn->ecp_cb = NULL;
|
||||
conn->ecp_user_data = NULL;
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t ble_esl_ap_gattc_init(void)
|
||||
{
|
||||
int rc = ble_ots_client_init(ble_esl_ap_ots_event_cb);
|
||||
return (rc == 0) ? ESP_OK : ESP_FAIL;
|
||||
}
|
||||
|
||||
void ble_esl_ap_gattc_deinit(void)
|
||||
{
|
||||
ble_ots_client_deinit();
|
||||
}
|
||||
1389
components/bt/ble_profiles/nimble/ble_esl/src/ap/ap_lifecycle.c
Normal file
1389
components/bt/ble_profiles/nimble/ble_esl/src/ap/ap_lifecycle.c
Normal file
File diff suppressed because it is too large
Load Diff
639
components/bt/ble_profiles/nimble/ble_esl/src/ap/ap_pawr.c
Normal file
639
components/bt/ble_profiles/nimble/ble_esl/src/ap/ap_pawr.c
Normal file
@@ -0,0 +1,639 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
/**
|
||||
* @file ble_esl_ap_pawr.c
|
||||
* @brief ESL AP PAwR broadcasting and AES-CCM encryption/decryption
|
||||
*
|
||||
* Implements PAwR broadcaster lifecycle (init/deinit/start/stop),
|
||||
* encrypted sync packet assembly (AP → ESL), and encrypted response
|
||||
* packet parsing (ESL → AP) using AES-CCM with per-direction key material.
|
||||
*/
|
||||
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
#include <assert.h>
|
||||
|
||||
#include "esp_log.h"
|
||||
#include "esp_err.h"
|
||||
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/semphr.h"
|
||||
|
||||
#include "nimble/ble.h"
|
||||
#include "host/ble_hs.h"
|
||||
#include "host/ble_gap.h"
|
||||
#include "os/os_mbuf.h"
|
||||
|
||||
#include "host/ble_ead.h"
|
||||
|
||||
#include "ble_esl_ap_int.h"
|
||||
#include "ble_esl_common.h"
|
||||
|
||||
static const char *TAG = "esl_ap_pawr";
|
||||
|
||||
/** Mutex protecting the per-subevent pending TX buffers (g_esl_ap->pawr_pending) */
|
||||
static SemaphoreHandle_t s_pawr_mutex;
|
||||
|
||||
/** Advertising instance used for PAwR */
|
||||
#define PAWR_ADV_INSTANCE 0
|
||||
|
||||
/**
|
||||
* Number of consecutive PAwR events a queued sync packet is transmitted before
|
||||
* its pending slot is cleared. >1 improves delivery reliability at the cost of
|
||||
* the ESL seeing (and responding to) the same command multiple times.
|
||||
* 1 == transmit exactly once.
|
||||
*/
|
||||
#define PAWR_TX_REPEATS 3
|
||||
|
||||
/* ========================== Forward Declarations ========================== */
|
||||
|
||||
static int pawr_gap_event_cb(struct ble_gap_event *event, void *arg);
|
||||
static void pawr_handle_subev_data_req(struct ble_gap_event *event);
|
||||
static void pawr_handle_subev_response(struct ble_gap_event *event);
|
||||
|
||||
/* ========================== PAwR Init / Deinit ========================== */
|
||||
|
||||
esp_err_t ble_esl_ap_pawr_init(void)
|
||||
{
|
||||
assert(g_esl_ap != NULL);
|
||||
|
||||
ble_esl_ead_randomizer_init(g_esl_ap->randomizer, BLE_ESL_EAD_DIR_AP_TO_ESL);
|
||||
g_esl_ap->pawr_active = false;
|
||||
g_esl_ap->pawr_pending = NULL;
|
||||
|
||||
if (s_pawr_mutex == NULL) {
|
||||
s_pawr_mutex = xSemaphoreCreateMutex();
|
||||
if (s_pawr_mutex == NULL) {
|
||||
ESP_LOGE(TAG, "Failed to create PAwR mutex");
|
||||
return ESP_ERR_NO_MEM;
|
||||
}
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "PAwR sub-module initialized");
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
void ble_esl_ap_pawr_deinit(void)
|
||||
{
|
||||
assert(g_esl_ap != NULL);
|
||||
|
||||
if (g_esl_ap->pawr_active) {
|
||||
ble_esl_ap_pawr_stop();
|
||||
}
|
||||
|
||||
if (s_pawr_mutex != NULL) {
|
||||
vSemaphoreDelete(s_pawr_mutex);
|
||||
s_pawr_mutex = NULL;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "PAwR sub-module deinitialized");
|
||||
}
|
||||
|
||||
/* ========================== PAwR Start / Stop ========================== */
|
||||
|
||||
esp_err_t ble_esl_ap_pawr_start(void)
|
||||
{
|
||||
assert(g_esl_ap != NULL);
|
||||
|
||||
int rc;
|
||||
|
||||
if (g_esl_ap->pawr_active) {
|
||||
ESP_LOGW(TAG, "PAwR already active");
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
|
||||
/* Step 1: Configure extended advertising (non-connectable, non-scannable) */
|
||||
struct ble_gap_ext_adv_params ext_params;
|
||||
memset(&ext_params, 0, sizeof(ext_params));
|
||||
ext_params.connectable = 0;
|
||||
ext_params.scannable = 0;
|
||||
ext_params.legacy_pdu = 0;
|
||||
ext_params.anonymous = 0;
|
||||
ext_params.include_tx_power = 0;
|
||||
ext_params.itvl_min = 0;
|
||||
ext_params.itvl_max = 0;
|
||||
ext_params.primary_phy = BLE_HCI_LE_PHY_1M;
|
||||
ext_params.secondary_phy = BLE_HCI_LE_PHY_1M;
|
||||
ext_params.own_addr_type = BLE_OWN_ADDR_PUBLIC;
|
||||
ext_params.sid = 0;
|
||||
ext_params.tx_power = 127; /* Host selects max */
|
||||
|
||||
int8_t selected_tx_power = 0;
|
||||
rc = ble_gap_ext_adv_configure(PAWR_ADV_INSTANCE, &ext_params,
|
||||
&selected_tx_power,
|
||||
pawr_gap_event_cb, NULL);
|
||||
if (rc != 0) {
|
||||
ESP_LOGE(TAG, "ext_adv_configure failed: %d", rc);
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
/* Step 2: Configure periodic advertising with PAwR parameters */
|
||||
struct ble_gap_periodic_adv_params pparams;
|
||||
memset(&pparams, 0, sizeof(pparams));
|
||||
pparams.include_tx_power = 0;
|
||||
pparams.itvl_min = g_esl_ap->pawr_config.periodic_adv_interval_min;
|
||||
pparams.itvl_max = g_esl_ap->pawr_config.periodic_adv_interval_max;
|
||||
pparams.num_subevents = g_esl_ap->pawr_config.num_subevents;
|
||||
pparams.subevent_interval = g_esl_ap->pawr_config.subevent_interval;
|
||||
pparams.response_slot_delay = g_esl_ap->pawr_config.response_slot_delay;
|
||||
pparams.response_slot_spacing = g_esl_ap->pawr_config.response_slot_spacing;
|
||||
pparams.num_response_slots = g_esl_ap->pawr_config.num_response_slots;
|
||||
|
||||
rc = ble_gap_periodic_adv_configure(PAWR_ADV_INSTANCE, &pparams);
|
||||
if (rc != 0) {
|
||||
ESP_LOGE(TAG, "periodic_adv_configure failed: %d", rc);
|
||||
/* Roll back the instance configured in Step 1, otherwise it stays
|
||||
* configured in the host and a retry may be rejected. */
|
||||
ble_gap_ext_adv_remove(PAWR_ADV_INSTANCE);
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
/* Step 3: Start periodic advertising */
|
||||
rc = ble_gap_periodic_adv_start(PAWR_ADV_INSTANCE);
|
||||
if (rc != 0) {
|
||||
ESP_LOGE(TAG, "periodic_adv_start failed: %d", rc);
|
||||
ble_gap_ext_adv_remove(PAWR_ADV_INSTANCE);
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
/* Step 4: Start extended advertising (required for periodic adv train) */
|
||||
rc = ble_gap_ext_adv_start(PAWR_ADV_INSTANCE, 0, 0);
|
||||
if (rc != 0) {
|
||||
ESP_LOGE(TAG, "ext_adv_start failed: %d", rc);
|
||||
ble_gap_periodic_adv_stop(PAWR_ADV_INSTANCE);
|
||||
ble_gap_ext_adv_remove(PAWR_ADV_INSTANCE);
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
/* Step 5: Allocate one pending TX slot per subevent. PAwR is pull-based:
|
||||
* ble_esl_ap_pawr_send() stores encrypted packets here and the
|
||||
* BLE_GAP_EVENT_PER_SUBEV_DATA_REQ callback drains them into subevents. */
|
||||
uint8_t num_subevents = g_esl_ap->pawr_config.num_subevents;
|
||||
g_esl_ap->pawr_pending = calloc(num_subevents,
|
||||
sizeof(ble_esl_ap_pawr_pending_t));
|
||||
if (g_esl_ap->pawr_pending == NULL) {
|
||||
ESP_LOGE(TAG, "Failed to allocate %u pending TX buffers", num_subevents);
|
||||
ble_gap_ext_adv_stop(PAWR_ADV_INSTANCE);
|
||||
ble_gap_periodic_adv_stop(PAWR_ADV_INSTANCE);
|
||||
ble_gap_ext_adv_remove(PAWR_ADV_INSTANCE);
|
||||
return ESP_ERR_NO_MEM;
|
||||
}
|
||||
|
||||
g_esl_ap->pawr_active = true;
|
||||
ESP_LOGI(TAG, "PAwR broadcaster started (subevents=%u, resp_slots=%u)",
|
||||
g_esl_ap->pawr_config.num_subevents,
|
||||
g_esl_ap->pawr_config.num_response_slots);
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t ble_esl_ap_pawr_stop(void)
|
||||
{
|
||||
assert(g_esl_ap != NULL);
|
||||
|
||||
if (!g_esl_ap->pawr_active) {
|
||||
ESP_LOGW(TAG, "PAwR not active");
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
|
||||
int rc;
|
||||
|
||||
/* Stop periodic advertising */
|
||||
rc = ble_gap_periodic_adv_stop(PAWR_ADV_INSTANCE);
|
||||
if (rc != 0) {
|
||||
ESP_LOGW(TAG, "periodic_adv_stop failed: %d", rc);
|
||||
}
|
||||
|
||||
/* Stop extended advertising */
|
||||
rc = ble_gap_ext_adv_stop(PAWR_ADV_INSTANCE);
|
||||
if (rc != 0) {
|
||||
ESP_LOGW(TAG, "ext_adv_stop failed: %d", rc);
|
||||
}
|
||||
|
||||
/* Release pending TX buffers under the lock so the data-request callback
|
||||
* (host task) never dereferences a freed pointer. */
|
||||
xSemaphoreTake(s_pawr_mutex, portMAX_DELAY);
|
||||
g_esl_ap->pawr_active = false;
|
||||
free(g_esl_ap->pawr_pending);
|
||||
g_esl_ap->pawr_pending = NULL;
|
||||
xSemaphoreGive(s_pawr_mutex);
|
||||
|
||||
ESP_LOGI(TAG, "PAwR broadcaster stopped");
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
/* ========================== Key Material ========================== */
|
||||
|
||||
void ble_esl_ap_pawr_set_sync_key(const ble_esl_key_material_t *key_mat)
|
||||
{
|
||||
assert(g_esl_ap != NULL);
|
||||
assert(key_mat != NULL);
|
||||
|
||||
g_esl_ap->ap_sync_key = *key_mat;
|
||||
|
||||
ESP_LOGI(TAG, "AP Sync Key Material set");
|
||||
}
|
||||
|
||||
esp_err_t ble_esl_ap_pawr_set_response_key(uint16_t esl_addr,
|
||||
const ble_esl_key_material_t *key_mat)
|
||||
{
|
||||
assert(g_esl_ap != NULL);
|
||||
|
||||
if (key_mat == NULL) {
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
|
||||
ble_esl_ap_esl_entry_t *entry = ble_esl_ap_find_esl(esl_addr);
|
||||
if (entry == NULL) {
|
||||
ESP_LOGE(TAG, "ESL 0x%04x not found for response key", esl_addr);
|
||||
return ESP_ERR_NOT_FOUND;
|
||||
}
|
||||
|
||||
entry->resp_key = *key_mat;
|
||||
|
||||
ESP_LOGI(TAG, "Response Key Material set for ESL 0x%04x", esl_addr);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
/* ========================== PAwR Send (Encrypt & Transmit) ========================== */
|
||||
|
||||
esp_err_t ble_esl_ap_pawr_send(uint8_t group_id, const uint8_t *payload,
|
||||
uint8_t payload_len)
|
||||
{
|
||||
assert(g_esl_ap != NULL);
|
||||
|
||||
/* Validate arguments */
|
||||
if (group_id > BLE_ESL_GROUP_ID_MAX) {
|
||||
ESP_LOGE(TAG, "Invalid group_id: 0x%02x", group_id);
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
if (payload == NULL || payload_len == 0 || payload_len > BLE_ESL_PAYLOAD_MAX_SIZE) {
|
||||
ESP_LOGE(TAG, "Invalid payload (ptr=%p, len=%u)", payload, payload_len);
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
if (!g_esl_ap->pawr_active) {
|
||||
ESP_LOGE(TAG, "PAwR broadcaster not active");
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
|
||||
/*
|
||||
* Step 1: Build inner AD structure (plaintext)
|
||||
* [len_byte] [0x34] [ESL Payload]
|
||||
* len_byte = 1 (tag) + payload_len
|
||||
*/
|
||||
uint8_t inner_ad_len = 1 + payload_len; /* ESL Tag + ESL Payload */
|
||||
uint8_t plaintext_len = 1 + inner_ad_len; /* Len byte + inner_ad_len */
|
||||
uint8_t plaintext[1 + 1 + BLE_ESL_PAYLOAD_MAX_SIZE]; /* len + tag + payload */
|
||||
|
||||
plaintext[0] = inner_ad_len; /* Len byte */
|
||||
plaintext[1] = BLE_ESL_AD_TYPE_ESL; /* ESL Tag 0x34 */
|
||||
memcpy(&plaintext[2], payload, payload_len); /* ESL Payload */
|
||||
|
||||
/*
|
||||
* Step 2: Encrypt with the AP Sync Key Material (AES-CCM)
|
||||
* Input: plaintext (inner AD)
|
||||
* Output: encrypted_payload = [Randomizer(5)] [Ciphertext(N)] [MIC(4)]
|
||||
*/
|
||||
uint8_t enc_payload_len = BLE_EAD_ENCRYPTED_PAYLOAD_SIZE(plaintext_len);
|
||||
uint8_t encrypted_payload[BLE_EAD_ENCRYPTED_PAYLOAD_SIZE(1 + 1 + BLE_ESL_PAYLOAD_MAX_SIZE)];
|
||||
|
||||
/* The Randomizer is read and advanced under the lock: this function is a
|
||||
* public command path and may be called from any application task. */
|
||||
xSemaphoreTake(s_pawr_mutex, portMAX_DELAY);
|
||||
esp_err_t enc_err = ble_esl_ead_encrypt(g_esl_ap->ap_sync_key.session_key,
|
||||
g_esl_ap->ap_sync_key.iv,
|
||||
g_esl_ap->randomizer,
|
||||
plaintext,
|
||||
plaintext_len,
|
||||
encrypted_payload);
|
||||
xSemaphoreGive(s_pawr_mutex);
|
||||
if (enc_err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "EAD encrypt failed: 0x%x", enc_err);
|
||||
return enc_err;
|
||||
}
|
||||
|
||||
/*
|
||||
* Step 3: Build outer AD structure (Encrypted Data)
|
||||
* [total_len] [0x31] [encrypted_payload]
|
||||
* total_len = 1 (ED tag) + enc_payload_len
|
||||
*/
|
||||
uint8_t ed_content_len = 1 + enc_payload_len;
|
||||
uint8_t outer_ad[BLE_ESL_AP_PAWR_MAX_AD_BUF_SIZE];
|
||||
uint8_t pos = 0;
|
||||
|
||||
outer_ad[pos++] = ed_content_len; /* Len */
|
||||
outer_ad[pos++] = BLE_ESL_AD_TYPE_ENCRYPTED_DATA; /* ED Tag 0x31 */
|
||||
memcpy(&outer_ad[pos], encrypted_payload, enc_payload_len); /* Randomizer + Ciphertext + MIC */
|
||||
pos += enc_payload_len;
|
||||
|
||||
/*
|
||||
* Step 4: Queue the encrypted packet for the target subevent (= group_id).
|
||||
*
|
||||
* PAwR subevent data can only be handed to the controller in response to a
|
||||
* BLE_GAP_EVENT_PER_SUBEV_DATA_REQ event; pushing it here directly makes the
|
||||
* controller reject the HCI command with Command Disallowed (0x0C). So we
|
||||
* store it in the pending slot and let pawr_handle_subev_data_req() drain it.
|
||||
*/
|
||||
if (group_id >= g_esl_ap->pawr_config.num_subevents) {
|
||||
ESP_LOGE(TAG, "group_id %u exceeds configured subevents (%u)",
|
||||
group_id, g_esl_ap->pawr_config.num_subevents);
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
if (pos > BLE_ESL_AP_PAWR_MAX_AD_BUF_SIZE) {
|
||||
ESP_LOGE(TAG, "Outer AD too large: %u", pos);
|
||||
return ESP_ERR_INVALID_SIZE;
|
||||
}
|
||||
|
||||
xSemaphoreTake(s_pawr_mutex, portMAX_DELAY);
|
||||
if (g_esl_ap->pawr_pending == NULL) {
|
||||
xSemaphoreGive(s_pawr_mutex);
|
||||
ESP_LOGE(TAG, "PAwR pending buffers not allocated");
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
ble_esl_ap_pawr_pending_t *slot = &g_esl_ap->pawr_pending[group_id];
|
||||
memcpy(slot->data, outer_ad, pos);
|
||||
slot->len = pos;
|
||||
slot->repeats_left = PAWR_TX_REPEATS;
|
||||
slot->valid = true;
|
||||
xSemaphoreGive(s_pawr_mutex);
|
||||
|
||||
ESP_LOGD(TAG, "PAwR sync packet queued for group %u (payload_len=%u, ad_len=%u)",
|
||||
group_id, payload_len, pos);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
/* ========================== PAwR Parse Response (Decrypt) ========================== */
|
||||
|
||||
esp_err_t ble_esl_ap_pawr_parse_response(uint16_t esl_addr,
|
||||
uint8_t response_slot,
|
||||
const uint8_t *enc_data,
|
||||
uint8_t enc_data_len,
|
||||
ble_esl_ap_parsed_response_t *out_response)
|
||||
{
|
||||
assert(g_esl_ap != NULL);
|
||||
|
||||
/* Validate arguments */
|
||||
if (enc_data == NULL || out_response == NULL) {
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
|
||||
/*
|
||||
* PAwR response data is a raw AD structure:
|
||||
* [AD Len(1)] [AD Type 0x31(1)] [Randomizer(5)] [Ciphertext(N)] [MIC(4)]
|
||||
* ble_esl_ead_decrypt() expects only the AD value (Randomizer..MIC), so the
|
||||
* outer length + type octets must be parsed off first (mirrors the ESL
|
||||
* receive path in handle_gap_periodic_report).
|
||||
*
|
||||
* Minimum enc_data_len:
|
||||
* 2 (AD hdr) + 5 (randomizer) + 4 (MIC) + 1 (min ciphertext) = 12
|
||||
*/
|
||||
if (enc_data_len < (2 + BLE_ESL_RANDOMIZER_SIZE + BLE_ESL_MIC_SIZE + 1)) {
|
||||
ESP_LOGE(TAG, "enc_data too short: %u", enc_data_len);
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
|
||||
uint8_t outer_len = enc_data[0]; /* AD type octet + encrypted payload */
|
||||
uint8_t outer_type = enc_data[1];
|
||||
if (outer_type != BLE_ESL_AD_TYPE_ENCRYPTED_DATA ||
|
||||
outer_len < 1 + BLE_ESL_RANDOMIZER_SIZE + BLE_ESL_MIC_SIZE ||
|
||||
(uint16_t)(outer_len + 1) > enc_data_len) {
|
||||
ESP_LOGE(TAG, "Not a valid Encrypted Data AD (type=0x%02x len=%u)",
|
||||
outer_type, outer_len);
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
|
||||
/* Strip the outer AD length + type octets to get the EAD payload. */
|
||||
const uint8_t *enc_payload = &enc_data[2];
|
||||
uint8_t enc_payload_len = outer_len - 1;
|
||||
|
||||
/* Step 1: Look up ESL entry by esl_addr */
|
||||
ble_esl_ap_esl_entry_t *entry = ble_esl_ap_find_esl(esl_addr);
|
||||
if (entry == NULL) {
|
||||
ESP_LOGE(TAG, "ESL 0x%04x not found for response decryption", esl_addr);
|
||||
return ESP_ERR_NOT_FOUND;
|
||||
}
|
||||
|
||||
/* Step 2: Decrypt and verify MIC via ble_esl_ead_decrypt
|
||||
* Input: enc_payload = [Randomizer(5)] [Ciphertext(N)] [MIC(4)]
|
||||
* Output: plaintext (inner AD), ciphertext_len octets
|
||||
*/
|
||||
uint8_t plaintext[BLE_ESL_PAYLOAD_MAX_SIZE + 2]; /* inner AD max */
|
||||
size_t decrypted_len = 0;
|
||||
esp_err_t err = ble_esl_ead_decrypt(entry->resp_key.session_key,
|
||||
entry->resp_key.iv,
|
||||
enc_payload,
|
||||
enc_payload_len,
|
||||
plaintext, sizeof(plaintext),
|
||||
&decrypted_len);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGW(TAG, "Response decryption failed for ESL 0x%04x (err=%d)",
|
||||
esl_addr, (int)err);
|
||||
return err;
|
||||
}
|
||||
|
||||
uint8_t ciphertext_len = (uint8_t)decrypted_len;
|
||||
|
||||
/*
|
||||
* Step 3: Strip inner AD wrapper
|
||||
* plaintext[0] = Len (length of ESL Tag + response payload)
|
||||
* plaintext[1] = ESL Tag (must be 0x34)
|
||||
* plaintext[2..] = response TLVs
|
||||
*/
|
||||
if (ciphertext_len < 2) {
|
||||
ESP_LOGE(TAG, "Decrypted data too short for inner AD");
|
||||
return ESP_ERR_INVALID_SIZE;
|
||||
}
|
||||
|
||||
uint8_t inner_len = plaintext[0];
|
||||
uint8_t inner_tag = plaintext[1];
|
||||
|
||||
if (inner_tag != BLE_ESL_AD_TYPE_ESL) {
|
||||
ESP_LOGE(TAG, "Invalid inner AD tag: 0x%02x (expected 0x34)", inner_tag);
|
||||
return ESP_ERR_INVALID_SIZE;
|
||||
}
|
||||
|
||||
/* inner_len = 1 (tag) + response_payload_len */
|
||||
if (inner_len < 1 || (1 + inner_len) > ciphertext_len) {
|
||||
ESP_LOGE(TAG, "Inner AD length mismatch: inner_len=%u, ciphertext_len=%u",
|
||||
inner_len, ciphertext_len);
|
||||
return ESP_ERR_INVALID_SIZE;
|
||||
}
|
||||
|
||||
uint8_t response_payload_len = inner_len - 1; /* subtract ESL Tag */
|
||||
|
||||
/* Step 4: Copy decrypted data into owned buffer and populate output structure */
|
||||
memcpy(out_response->payload_buf, plaintext, ciphertext_len);
|
||||
out_response->payload = &out_response->payload_buf[2];
|
||||
uint8_t *response_payload = out_response->payload;
|
||||
|
||||
out_response->esl_addr = esl_addr;
|
||||
out_response->response_slot = response_slot;
|
||||
out_response->payload_len = response_payload_len;
|
||||
out_response->auth_success = true;
|
||||
|
||||
/* Step 5: Count TLVs by iterating with ble_esl_tlv_decode */
|
||||
uint8_t tlv_count = 0;
|
||||
uint8_t offset = 0;
|
||||
|
||||
while (offset < response_payload_len) {
|
||||
uint8_t opcode;
|
||||
const uint8_t *params;
|
||||
uint8_t params_len;
|
||||
|
||||
esp_err_t dec_rc = ble_esl_tlv_decode(&response_payload[offset],
|
||||
response_payload_len - offset,
|
||||
&opcode, ¶ms, ¶ms_len);
|
||||
if (dec_rc != ESP_OK) {
|
||||
ESP_LOGW(TAG, "TLV decode failed at offset %u: 0x%x", offset, dec_rc);
|
||||
break;
|
||||
}
|
||||
|
||||
uint8_t tlv_total = BLE_ESL_TLV_TOTAL_LEN(opcode);
|
||||
offset += tlv_total;
|
||||
tlv_count++;
|
||||
}
|
||||
|
||||
out_response->tlv_count = tlv_count;
|
||||
|
||||
ESP_LOGD(TAG, "Parsed response from ESL 0x%04x: slot=%u, tlvs=%u, len=%u",
|
||||
esl_addr, response_slot, tlv_count, response_payload_len);
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
/* ========================== GAP Event Handling ========================== */
|
||||
|
||||
/**
|
||||
* @brief GAP event callback for PAwR-related events
|
||||
*
|
||||
* Handles subevent data requests and subevent responses.
|
||||
*/
|
||||
static int pawr_gap_event_cb(struct ble_gap_event *event, void *arg)
|
||||
{
|
||||
switch (event->type) {
|
||||
case BLE_GAP_EVENT_PER_SUBEV_DATA_REQ:
|
||||
pawr_handle_subev_data_req(event);
|
||||
return 0;
|
||||
|
||||
case BLE_GAP_EVENT_PER_SUBEV_RESP:
|
||||
pawr_handle_subev_response(event);
|
||||
return 0;
|
||||
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Handle subevent data request from the controller
|
||||
*
|
||||
* The controller requests subevent data before transmission. For PAwR with ESL,
|
||||
* the actual data is set via ble_esl_ap_pawr_send() when commands are queued.
|
||||
* Here we provide empty data for subevents that have no pending commands.
|
||||
*/
|
||||
static void pawr_handle_subev_data_req(struct ble_gap_event *event)
|
||||
{
|
||||
uint8_t start = event->periodic_adv_subev_data_req.subevent_start;
|
||||
uint8_t count = event->periodic_adv_subev_data_req.subevent_data_count;
|
||||
uint8_t adv_handle = event->periodic_adv_subev_data_req.adv_handle;
|
||||
|
||||
if (count == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
/*
|
||||
* Allocate params array on stack. For each requested subevent we supply the
|
||||
* queued sync packet (if any) or an empty mbuf; the NimBLE stack rejects
|
||||
* NULL data pointers. Note: ble_gap_set_periodic_adv_subev_data always frees
|
||||
* the mbufs.
|
||||
*/
|
||||
struct ble_gap_set_periodic_adv_subev_data_params sub_params[count];
|
||||
memset(sub_params, 0, sizeof(sub_params));
|
||||
|
||||
for (uint8_t i = 0; i < count; i++) {
|
||||
uint8_t sub = (start + i) % g_esl_ap->pawr_config.num_subevents;
|
||||
sub_params[i].subevent = sub;
|
||||
sub_params[i].response_slot_start = 0;
|
||||
sub_params[i].response_slot_count = g_esl_ap->pawr_config.num_response_slots;
|
||||
|
||||
/* Snapshot this subevent's pending slot under the lock, then release it
|
||||
* before touching the mbuf pool / HCI. */
|
||||
uint8_t pkt[BLE_ESL_AP_PAWR_MAX_AD_BUF_SIZE];
|
||||
uint8_t pkt_len = 0;
|
||||
|
||||
xSemaphoreTake(s_pawr_mutex, portMAX_DELAY);
|
||||
if (g_esl_ap->pawr_pending != NULL) {
|
||||
ble_esl_ap_pawr_pending_t *slot = &g_esl_ap->pawr_pending[sub];
|
||||
if (slot->valid && slot->len > 0) {
|
||||
pkt_len = slot->len;
|
||||
memcpy(pkt, slot->data, pkt_len);
|
||||
if (--slot->repeats_left == 0) {
|
||||
slot->valid = false;
|
||||
slot->len = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
xSemaphoreGive(s_pawr_mutex);
|
||||
|
||||
/* Allocate an mbuf (sized to the packet, or empty for idle subevents) */
|
||||
struct os_mbuf *mbuf = os_msys_get_pkthdr(pkt_len, 0);
|
||||
if (mbuf == NULL) {
|
||||
ESP_LOGE(TAG, "Failed to allocate mbuf for subevent %u", sub);
|
||||
/* Free already-allocated mbufs before returning */
|
||||
for (uint8_t j = 0; j < i; j++) {
|
||||
os_mbuf_free_chain(sub_params[j].data);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (pkt_len > 0 && os_mbuf_append(mbuf, pkt, pkt_len) != 0) {
|
||||
ESP_LOGE(TAG, "os_mbuf_append failed for subevent %u", sub);
|
||||
os_mbuf_free_chain(mbuf);
|
||||
for (uint8_t j = 0; j < i; j++) {
|
||||
os_mbuf_free_chain(sub_params[j].data);
|
||||
}
|
||||
return;
|
||||
}
|
||||
sub_params[i].data = mbuf;
|
||||
}
|
||||
|
||||
/* ble_gap_set_periodic_adv_subev_data always frees the mbufs */
|
||||
int rc = ble_gap_set_periodic_adv_subev_data(adv_handle, count, sub_params);
|
||||
if (rc != 0) {
|
||||
ESP_LOGW(TAG, "set_periodic_adv_subev_data failed: %d", rc);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Handle a PAwR response received in a response slot
|
||||
*
|
||||
* Extracts the response data and forwards it to the command module
|
||||
* for decryption and dispatch.
|
||||
*/
|
||||
static void pawr_handle_subev_response(struct ble_gap_event *event)
|
||||
{
|
||||
const struct ble_gap_periodic_adv_response *resp = &event->periodic_adv_response;
|
||||
|
||||
/* Only process complete data */
|
||||
if (resp->data_status != BLE_GAP_PER_ADV_DATA_STATUS_COMPLETE) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (resp->data == NULL || resp->data_length == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
uint8_t group_id = resp->subevent;
|
||||
uint8_t response_slot = resp->response_slot;
|
||||
|
||||
ESP_LOGD(TAG, "PAwR response: subevent=%u, slot=%u, len=%u",
|
||||
group_id, response_slot, resp->data_length);
|
||||
|
||||
/* Forward to command module for decryption and dispatch */
|
||||
ble_esl_ap_command_handle_pawr_response(group_id, response_slot,
|
||||
resp->data, resp->data_length);
|
||||
}
|
||||
@@ -0,0 +1,584 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
/**
|
||||
* @file ble_esl_ap_int.h
|
||||
* @brief ESL AP module — Internal header shared across source files
|
||||
*
|
||||
* Defines internal data structures, per-connection context, per-ESL tracking
|
||||
* entry, and cross-module function declarations used by:
|
||||
* - ap_connection.c
|
||||
* - ap_gattc.c
|
||||
* - ap_lifecycle.c
|
||||
* - ap_command.c
|
||||
* - ap_pawr.c
|
||||
*/
|
||||
|
||||
#ifndef BLE_ESL_AP_INT_H
|
||||
#define BLE_ESL_AP_INT_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
#include "esp_err.h"
|
||||
#include "esp_timer.h"
|
||||
#include "ble_esl_ap.h"
|
||||
#include "ble_esl_common.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* ========================== PAwR Response Structures ========================== */
|
||||
|
||||
/**
|
||||
* @brief Decrypted and parsed PAwR response from a single ESL
|
||||
*/
|
||||
typedef struct {
|
||||
uint16_t esl_addr; /*!< Full ESL Address (ESL_ID + Group_ID) */
|
||||
uint8_t response_slot; /*!< Response slot number in which received */
|
||||
uint8_t tlv_count; /*!< Number of response TLVs parsed */
|
||||
uint8_t payload_buf[BLE_ESL_PAYLOAD_MAX_SIZE + 2]; /*!< Owned buffer for decrypted inner AD (len + tag + payload); payload points into this at offset 2 */
|
||||
uint8_t *payload; /*!< Pointer to decrypted response payload (TLVs) */
|
||||
uint8_t payload_len; /*!< Length of decrypted response payload */
|
||||
bool auth_success; /*!< True if MIC authenticated successfully */
|
||||
} ble_esl_ap_parsed_response_t;
|
||||
|
||||
/* ========================== Constants ========================== */
|
||||
|
||||
/** Invalid/unassigned connection handle sentinel */
|
||||
#define BLE_ESL_AP_CONN_HANDLE_INVALID 0xFFFF
|
||||
|
||||
/** Maximum number of PAwR subevents a periodic advertising train may use
|
||||
* (Core Spec: Num_Subevents range 0x01-0x80) */
|
||||
#define BLE_ESL_AP_PAWR_MAX_SUBEVENTS 128
|
||||
|
||||
/** DIS UUID */
|
||||
#define BLE_ESL_AP_DIS_UUID 0x180A
|
||||
|
||||
/** PnP ID UUID */
|
||||
#define BLE_ESL_AP_PNP_ID_UUID 0x2A50
|
||||
|
||||
/** PnP ID characteristic value length */
|
||||
#define BLE_ESL_AP_PNP_ID_LEN 7
|
||||
|
||||
/** Maximum characteristic value for Read Long reassembly buffer */
|
||||
#define BLE_ESL_AP_READ_BUF_MAX 256
|
||||
|
||||
/*
|
||||
* Internally the AP stores an ESL Address packed into a single 16-bit key
|
||||
* (ESL_ID | Group_ID << 8) so that tracking entries can be compared and looked
|
||||
* up with a plain integer comparison. It carries exactly the same information
|
||||
* as the public ble_esl_address_t; use the helpers below to convert between
|
||||
* both forms at the public API boundary.
|
||||
*/
|
||||
|
||||
/** ESL Address helper: build 16-bit ESL Address key from esl_id and group_id */
|
||||
#define BLE_ESL_AP_MAKE_ADDR(esl_id, group_id) \
|
||||
((uint16_t)((uint8_t)(esl_id) | (((uint8_t)(group_id) & 0x7F) << 8)))
|
||||
|
||||
/** Extract ESL_ID from 16-bit ESL Address key */
|
||||
#define BLE_ESL_AP_ADDR_ESL_ID(addr) ((uint8_t)((addr) & 0xFF))
|
||||
|
||||
/** Extract Group_ID from 16-bit ESL Address key */
|
||||
#define BLE_ESL_AP_ADDR_GROUP_ID(addr) ((uint8_t)(((addr) >> 8) & 0x7F))
|
||||
|
||||
/** Pack a public ble_esl_address_t into the internal 16-bit ESL Address key */
|
||||
#define BLE_ESL_AP_ADDR_PACK(addr) \
|
||||
BLE_ESL_AP_MAKE_ADDR((addr).esl_id, BLE_ESL_ADDR_GROUP_ID(addr))
|
||||
|
||||
/**
|
||||
* @brief Unpack an internal 16-bit ESL Address key into a ble_esl_address_t
|
||||
*
|
||||
* @param esl_addr 16-bit ESL Address key (ESL_ID | Group_ID << 8)
|
||||
* @return Equivalent public ESL Address structure
|
||||
*/
|
||||
static inline ble_esl_address_t ble_esl_ap_addr_unpack(uint16_t esl_addr)
|
||||
{
|
||||
return ble_esl_addr_make(BLE_ESL_AP_ADDR_ESL_ID(esl_addr),
|
||||
BLE_ESL_AP_ADDR_GROUP_ID(esl_addr));
|
||||
}
|
||||
|
||||
/* ========================== Internal Callback Type ========================== */
|
||||
|
||||
/**
|
||||
* @brief GATT operation completion callback (internal)
|
||||
*
|
||||
* @param conn_handle ACL connection handle
|
||||
* @param status ESP_OK on success
|
||||
* @param data Read value / ECP notification payload (NULL for writes)
|
||||
* @param data_len Length of data (0 for writes)
|
||||
* @param user_data Opaque pointer from the caller
|
||||
*/
|
||||
typedef void (*ble_esl_ap_gatt_cb_t)(uint16_t conn_handle, esp_err_t status,
|
||||
const uint8_t *data, uint16_t data_len,
|
||||
void *user_data);
|
||||
|
||||
/* ========================== Per-Connection Characteristic Handles ========================== */
|
||||
|
||||
/**
|
||||
* @brief Discovered characteristic handle cache for one ESL connection
|
||||
*/
|
||||
typedef struct {
|
||||
/* ESL Service characteristics */
|
||||
uint16_t esl_svc_start; /*!< ESL Service start handle */
|
||||
uint16_t esl_svc_end; /*!< ESL Service end handle */
|
||||
uint16_t esl_addr_handle; /*!< ESL Address (0x2BF6) value handle */
|
||||
uint16_t ap_sync_key_handle; /*!< AP Sync Key Material (0x2BF7) value handle */
|
||||
uint16_t resp_key_handle; /*!< ESL Response Key Material (0x2BF8) value handle */
|
||||
uint16_t abs_time_handle; /*!< ESL Current Absolute Time (0x2BF9) value handle */
|
||||
uint16_t display_info_handle;/*!< ESL Display Information (0x2BFA) value handle, 0 if absent */
|
||||
uint16_t image_info_handle; /*!< ESL Image Information (0x2BFB) value handle, 0 if absent */
|
||||
uint16_t sensor_info_handle; /*!< ESL Sensor Information (0x2BFC) value handle, 0 if absent */
|
||||
uint16_t led_info_handle; /*!< ESL LED Information (0x2BFD) value handle, 0 if absent */
|
||||
uint16_t ecp_handle; /*!< ESL Control Point (0x2BFE) value handle */
|
||||
uint16_t ecp_cccd_handle; /*!< ECP CCCD handle */
|
||||
/* DIS characteristics */
|
||||
uint16_t dis_svc_start; /*!< DIS start handle, 0 if absent */
|
||||
uint16_t dis_svc_end; /*!< DIS end handle, 0 if absent */
|
||||
uint16_t pnp_id_handle; /*!< PnP ID (0x2A50) value handle, 0 if absent */
|
||||
} ble_esl_ap_char_handles_t;
|
||||
|
||||
/* ========================== Per-Connection Context ========================== */
|
||||
|
||||
/**
|
||||
* @brief Per-connection context for a connected ESL
|
||||
*/
|
||||
typedef struct {
|
||||
bool in_use; /*!< Entry is active */
|
||||
uint16_t conn_handle; /*!< ACL connection handle */
|
||||
uint8_t addr[6]; /*!< BLE device address */
|
||||
uint8_t addr_type; /*!< Address type (0=public, 1=random) */
|
||||
ble_esl_ap_char_handles_t handles; /*!< Discovered characteristic handles */
|
||||
|
||||
/* ECP state */
|
||||
bool ecp_pending; /*!< ECP write in progress (waiting for notification) */
|
||||
ble_esl_ap_gatt_cb_t ecp_cb; /*!< ECP notification callback */
|
||||
void *ecp_user_data; /*!< ECP callback user data */
|
||||
|
||||
/* GATT read/write state */
|
||||
bool read_pending; /*!< A GATT read is in progress */
|
||||
ble_esl_ap_gatt_cb_t gatt_cb; /*!< Active GATT read/write callback */
|
||||
void *gatt_user_data; /*!< GATT callback user data */
|
||||
uint8_t read_buf[BLE_ESL_AP_READ_BUF_MAX]; /*!< Read Long reassembly buffer */
|
||||
uint16_t read_offset; /*!< Current offset in read_buf */
|
||||
|
||||
/* OTS capability flag (set during service discovery) */
|
||||
bool has_ots; /*!< OTS service was found on this connection */
|
||||
bool disc_done; /*!< Service discovery completed successfully */
|
||||
|
||||
/* ESL tracking cross-reference */
|
||||
uint16_t esl_addr; /*!< Assigned ESL Address (valid after configure) */
|
||||
} ble_esl_ap_conn_t;
|
||||
|
||||
/* ========================== Per-ESL Tracking Entry ========================== */
|
||||
|
||||
/**
|
||||
* @brief Per-ESL tracking entry in the AP's ESL tracking table
|
||||
*/
|
||||
typedef struct {
|
||||
bool in_use; /*!< Entry is active */
|
||||
uint16_t esl_addr; /*!< Assigned ESL Address (ESL_ID | Group_ID << 8) */
|
||||
uint8_t ble_addr[6]; /*!< BLE device address */
|
||||
uint8_t ble_addr_type; /*!< BLE address type */
|
||||
ble_esl_state_t state; /*!< Current tracked state */
|
||||
bool config_complete; /*!< All 4 mandatory chars written successfully */
|
||||
int64_t last_sync_time_us; /*!< Last successful PAwR exchange timestamp (us) */
|
||||
int64_t unsync_entry_time_us; /*!< Time ESL entered Unsynchronized state (us) */
|
||||
uint16_t conn_handle; /*!< Active connection handle (INVALID if not connected) */
|
||||
uint8_t pending_pawr_cmd_opcode; /*!< Last command opcode sent to this ESL via PAwR, for response correlation */
|
||||
|
||||
/* Key material for PAwR response decryption */
|
||||
ble_esl_key_material_t resp_key; /*!< ESL Response Key Material */
|
||||
} ble_esl_ap_esl_entry_t;
|
||||
|
||||
/* ========================== PAwR Pending TX Buffer ========================== */
|
||||
|
||||
/**
|
||||
* Maximum outer AD structure size for one PAwR subevent:
|
||||
* 1 (len) + 1 (ED tag 0x31) + 5 (randomizer) + (2 + 48) (inner AD) + 4 (MIC) = 61
|
||||
* Rounded up to a generous buffer.
|
||||
*/
|
||||
#define BLE_ESL_AP_PAWR_MAX_AD_BUF_SIZE 80
|
||||
|
||||
/**
|
||||
* @brief Pending subevent data for the PAwR pull model.
|
||||
*
|
||||
* PAwR subevent data may only be handed to the controller in response to a
|
||||
* BLE_GAP_EVENT_PER_SUBEV_DATA_REQ event. ble_esl_ap_pawr_send() therefore does
|
||||
* not push data directly; it encrypts the packet and stores it here, and the
|
||||
* data-request callback drains it into the requested subevent. One entry per
|
||||
* subevent (indexed by group_id); allocated by ble_esl_ap_pawr_start().
|
||||
*/
|
||||
typedef struct {
|
||||
uint8_t data[BLE_ESL_AP_PAWR_MAX_AD_BUF_SIZE]; /*!< Encrypted outer AD bytes */
|
||||
uint8_t len; /*!< Valid byte count in data (0 = none) */
|
||||
uint8_t repeats_left; /*!< Remaining PAwR events to transmit */
|
||||
bool valid; /*!< Slot holds a packet awaiting TX */
|
||||
} ble_esl_ap_pawr_pending_t;
|
||||
|
||||
/* ========================== Module Global State ========================== */
|
||||
|
||||
/**
|
||||
* @brief Top-level AP module state (allocated dynamically by init, freed by deinit)
|
||||
*/
|
||||
typedef struct {
|
||||
bool initialized; /*!< Module has been initialized */
|
||||
bool started; /*!< Scanning + PAwR broadcasting active */
|
||||
|
||||
/* Application callback */
|
||||
ble_esl_ap_cb_t app_cb;
|
||||
|
||||
/* PAwR configuration */
|
||||
ble_esl_ap_pawr_config_t pawr_config;
|
||||
|
||||
/* Connection table */
|
||||
ble_esl_ap_conn_t conns[CONFIG_BLE_ESL_AP_MAX_CONNECTIONS];
|
||||
|
||||
/* ESL tracking table */
|
||||
ble_esl_ap_esl_entry_t esls[CONFIG_BLE_ESL_AP_MAX_ESLS];
|
||||
|
||||
/* PAwR broadcaster state */
|
||||
ble_esl_key_material_t ap_sync_key; /*!< AP Sync Key Material */
|
||||
uint8_t randomizer[BLE_ESL_RANDOMIZER_SIZE]; /*!< Current AP Randomizer (5 octets, LE) */
|
||||
bool pawr_active; /*!< PAwR broadcaster is running */
|
||||
ble_esl_ap_pawr_pending_t *pawr_pending; /*!< Per-subevent pending TX buffers (num_subevents entries) */
|
||||
|
||||
/* Timeout monitoring timer */
|
||||
esp_timer_handle_t timeout_timer; /*!< Periodic timer for 60-min checks */
|
||||
} ble_esl_ap_state_t;
|
||||
|
||||
/** Global pointer to the AP module state (allocated by init, freed by deinit) */
|
||||
extern ble_esl_ap_state_t *g_esl_ap;
|
||||
|
||||
/* ========================== Helpers ========================== */
|
||||
|
||||
/**
|
||||
* @brief Find a connection context by connection handle
|
||||
*
|
||||
* @param conn_handle ACL connection handle
|
||||
* @return Pointer to connection context, or NULL if not found
|
||||
*/
|
||||
ble_esl_ap_conn_t *ble_esl_ap_find_conn(uint16_t conn_handle);
|
||||
|
||||
/**
|
||||
* @brief Find an ESL tracking entry by ESL Address
|
||||
*
|
||||
* @param esl_addr ESL Address (ESL_ID | Group_ID << 8)
|
||||
* @return Pointer to ESL entry, or NULL if not found
|
||||
*/
|
||||
ble_esl_ap_esl_entry_t *ble_esl_ap_find_esl(uint16_t esl_addr);
|
||||
|
||||
/**
|
||||
* @brief Find an ESL tracking entry by BLE address
|
||||
*
|
||||
* @param addr 6-byte BLE address
|
||||
* @param addr_type Address type
|
||||
* @return Pointer to ESL entry, or NULL if not found
|
||||
*/
|
||||
ble_esl_ap_esl_entry_t *ble_esl_ap_find_esl_by_ble_addr(const uint8_t *addr,
|
||||
uint8_t addr_type);
|
||||
|
||||
/**
|
||||
* @brief Allocate a free connection context slot
|
||||
*
|
||||
* @return Pointer to free slot, or NULL if all slots in use
|
||||
*/
|
||||
ble_esl_ap_conn_t *ble_esl_ap_alloc_conn(void);
|
||||
|
||||
/**
|
||||
* @brief Free a connection context slot
|
||||
*
|
||||
* @param conn Pointer to connection context
|
||||
*/
|
||||
void ble_esl_ap_free_conn(ble_esl_ap_conn_t *conn);
|
||||
|
||||
/**
|
||||
* @brief Allocate a free ESL tracking entry
|
||||
*
|
||||
* @return Pointer to free entry, or NULL if table is full
|
||||
*/
|
||||
ble_esl_ap_esl_entry_t *ble_esl_ap_alloc_esl(void);
|
||||
|
||||
/**
|
||||
* @brief Check if a BLE address is in the associated-address list
|
||||
*
|
||||
* Used during scanning to determine Unassociated vs Unsynchronized.
|
||||
*
|
||||
* @param addr 6-byte BLE address
|
||||
* @param addr_type Address type
|
||||
* @return true if the address is tracked (associated), false otherwise
|
||||
*/
|
||||
bool ble_esl_ap_is_associated(const uint8_t *addr, uint8_t addr_type);
|
||||
|
||||
/* ---- ap_gattc.c ---- */
|
||||
|
||||
/**
|
||||
* @brief Initialize the GATT client sub-module (OTS client registration)
|
||||
*
|
||||
* Called from ble_esl_ap_init().
|
||||
*
|
||||
* @return ESP_OK on success
|
||||
*/
|
||||
esp_err_t ble_esl_ap_gattc_init(void);
|
||||
|
||||
/**
|
||||
* @brief Deinitialize the GATT client sub-module
|
||||
*
|
||||
* Called from ble_esl_ap_deinit().
|
||||
*/
|
||||
void ble_esl_ap_gattc_deinit(void);
|
||||
|
||||
/**
|
||||
* @brief Start primary service discovery on a newly encrypted connection
|
||||
*
|
||||
* Triggers the full discovery state machine: services → ESL characteristics →
|
||||
* DIS characteristics → ECP CCCD → OTS → disc_complete.
|
||||
*
|
||||
* @param conn_handle ACL connection handle
|
||||
*/
|
||||
void ble_esl_ap_start_svc_discovery(uint16_t conn_handle);
|
||||
|
||||
/**
|
||||
* @brief Read a GATT characteristic value from a connected ESL
|
||||
*
|
||||
* Uses Read Long when value > (ATT_MTU − 1). Callback receives full value.
|
||||
*
|
||||
* @param conn_handle ACL connection handle
|
||||
* @param char_uuid 16-bit UUID of the characteristic
|
||||
* @param cb Completion callback
|
||||
* @param user_data Opaque pointer forwarded to cb
|
||||
* @return ESP_OK if read initiated; ESP_ERR_NOT_FOUND if char not discovered
|
||||
*/
|
||||
esp_err_t ble_esl_ap_gatt_read(uint16_t conn_handle, uint16_t char_uuid,
|
||||
ble_esl_ap_gatt_cb_t cb, void *user_data);
|
||||
|
||||
/**
|
||||
* @brief Write a GATT characteristic value to a connected ESL
|
||||
*
|
||||
* Uses GATT Write Characteristic Value sub-procedure.
|
||||
*
|
||||
* @param conn_handle ACL connection handle
|
||||
* @param char_uuid 16-bit UUID of the characteristic
|
||||
* @param data Value to write
|
||||
* @param data_len Length in octets
|
||||
* @param cb Completion callback (data/data_len = NULL/0)
|
||||
* @param user_data Opaque pointer forwarded to cb
|
||||
* @return ESP_OK if write initiated; ESP_ERR_NOT_FOUND if char not discovered
|
||||
*/
|
||||
esp_err_t ble_esl_ap_gatt_write(uint16_t conn_handle, uint16_t char_uuid,
|
||||
const uint8_t *data, uint16_t data_len,
|
||||
ble_esl_ap_gatt_cb_t cb, void *user_data);
|
||||
|
||||
/**
|
||||
* @brief Write a TLV command to the ECP characteristic and wait for notification
|
||||
*
|
||||
* Only one outstanding ECP write per connection. Notification payload
|
||||
* delivered via cb(data, data_len). 30s timeout managed by caller.
|
||||
*
|
||||
* @param conn_handle ACL connection handle
|
||||
* @param tlv_data TLV-encoded command
|
||||
* @param tlv_len TLV data length
|
||||
* @param cb Notification callback
|
||||
* @param user_data Opaque pointer forwarded to cb
|
||||
* @return ESP_OK if write initiated; ESP_ERR_INVALID_STATE if ECP busy
|
||||
*/
|
||||
esp_err_t ble_esl_ap_ecp_write(uint16_t conn_handle, const uint8_t *tlv_data,
|
||||
uint8_t tlv_len, ble_esl_ap_gatt_cb_t cb,
|
||||
void *user_data);
|
||||
|
||||
/* ---- ble_esl_ap_lifecycle.c ---- */
|
||||
|
||||
/**
|
||||
* @brief Update per-ESL tracked state and fire BLE_ESL_AP_EVT_STATE_CHANGED
|
||||
*
|
||||
* @param esl_addr ESL Address (ESL_ID | Group_ID << 8)
|
||||
* @param new_state New state to record
|
||||
* @return ESP_OK on success; ESP_ERR_NOT_FOUND if ESL not tracked
|
||||
*/
|
||||
esp_err_t ble_esl_ap_update_esl_state(uint16_t esl_addr,
|
||||
ble_esl_state_t new_state);
|
||||
|
||||
/**
|
||||
* @brief Initialize the lifecycle sub-module (timers, etc.)
|
||||
*
|
||||
* Called from ble_esl_ap_init().
|
||||
*
|
||||
* @return ESP_OK on success
|
||||
*/
|
||||
esp_err_t ble_esl_ap_lifecycle_init(void);
|
||||
|
||||
/**
|
||||
* @brief Deinitialize the lifecycle sub-module
|
||||
*
|
||||
* Called from ble_esl_ap_deinit().
|
||||
*/
|
||||
void ble_esl_ap_lifecycle_deinit(void);
|
||||
|
||||
/**
|
||||
* @brief Handle disconnect event for lifecycle tracking
|
||||
*
|
||||
* Called by connection module when an ESL disconnects, to complete
|
||||
* any pending synchronize procedure and update tracked state.
|
||||
*
|
||||
* @param conn_handle Connection handle of the disconnected ESL
|
||||
*/
|
||||
void ble_esl_ap_lifecycle_handle_disconnect(uint16_t conn_handle);
|
||||
|
||||
/**
|
||||
* @brief Route OTS client events to lifecycle for image transfer tracking
|
||||
*
|
||||
* Must only be called from the OTS client callback context. Internally
|
||||
* casts @p event to ble_ots_client_event_t.
|
||||
*
|
||||
* @param conn_id Connection identifier
|
||||
* @param event OTS client event code (ble_ots_client_event_t)
|
||||
* @param param Event-specific parameter
|
||||
*/
|
||||
void ble_esl_ap_lifecycle_handle_ots_event(uint16_t conn_id,
|
||||
int event,
|
||||
const void *param);
|
||||
|
||||
/* ---- ap_pawr.c ---- */
|
||||
|
||||
/**
|
||||
* @brief Initialize the PAwR sub-module
|
||||
*
|
||||
* Called from ble_esl_ap_init().
|
||||
*
|
||||
* @return ESP_OK on success
|
||||
*/
|
||||
esp_err_t ble_esl_ap_pawr_init(void);
|
||||
|
||||
/**
|
||||
* @brief Deinitialize the PAwR sub-module
|
||||
*
|
||||
* Called from ble_esl_ap_deinit().
|
||||
*/
|
||||
void ble_esl_ap_pawr_deinit(void);
|
||||
|
||||
/**
|
||||
* @brief Start PAwR broadcasting
|
||||
*
|
||||
* Configures and enables the PAwR broadcaster using stored pawr_config.
|
||||
*
|
||||
* @return ESP_OK on success
|
||||
*/
|
||||
esp_err_t ble_esl_ap_pawr_start(void);
|
||||
|
||||
/**
|
||||
* @brief Stop PAwR broadcasting
|
||||
*
|
||||
* @return ESP_OK on success
|
||||
*/
|
||||
esp_err_t ble_esl_ap_pawr_stop(void);
|
||||
|
||||
/**
|
||||
* @brief Set the AP Sync Key Material used for encrypting sync packets
|
||||
*
|
||||
* @param key_mat AP Sync Key Material (session key + IV)
|
||||
*/
|
||||
void ble_esl_ap_pawr_set_sync_key(const ble_esl_key_material_t *key_mat);
|
||||
|
||||
/**
|
||||
* @brief Store per-ESL Response Key Material for decrypting responses
|
||||
*
|
||||
* @param esl_addr ESL Address
|
||||
* @param key_mat ESL Response Key Material (session key + IV)
|
||||
* @return ESP_OK on success; ESP_ERR_NOT_FOUND if ESL not tracked
|
||||
*/
|
||||
esp_err_t ble_esl_ap_pawr_set_response_key(uint16_t esl_addr,
|
||||
const ble_esl_key_material_t *key_mat);
|
||||
|
||||
/**
|
||||
* @brief Assemble and transmit an encrypted PAwR sync packet
|
||||
*
|
||||
* @param group_id Target Group_ID (0x00–0x7F)
|
||||
* @param payload Pre-assembled ESL Payload (from ble_esl_payload_encode())
|
||||
* @param payload_len ESL Payload length (1–48)
|
||||
* @return ESP_OK on success; error code on failure
|
||||
*/
|
||||
esp_err_t ble_esl_ap_pawr_send(uint8_t group_id, const uint8_t *payload,
|
||||
uint8_t payload_len);
|
||||
|
||||
/**
|
||||
* @brief Decrypt and parse a PAwR response packet
|
||||
*
|
||||
* @param esl_addr ESL Address of the responding ESL
|
||||
* @param response_slot Response slot number
|
||||
* @param enc_data Raw Encrypted Data AD payload
|
||||
* @param enc_data_len Length of enc_data
|
||||
* @param out_response Output parsed response
|
||||
* @return ESP_OK on success; ESP_ERR_INVALID_RESPONSE on MIC failure
|
||||
*/
|
||||
esp_err_t ble_esl_ap_pawr_parse_response(uint16_t esl_addr,
|
||||
uint8_t response_slot,
|
||||
const uint8_t *enc_data,
|
||||
uint8_t enc_data_len,
|
||||
ble_esl_ap_parsed_response_t *out_response);
|
||||
|
||||
/* ---- ap_command.c ---- */
|
||||
|
||||
/**
|
||||
* @brief Initialize the command sub-module
|
||||
*
|
||||
* Called from ble_esl_ap_init().
|
||||
*
|
||||
* @return ESP_OK on success
|
||||
*/
|
||||
esp_err_t ble_esl_ap_command_init(void);
|
||||
|
||||
/**
|
||||
* @brief Deinitialize the command sub-module
|
||||
*
|
||||
* Called from ble_esl_ap_deinit().
|
||||
*/
|
||||
void ble_esl_ap_command_deinit(void);
|
||||
|
||||
/**
|
||||
* @brief Handle an ECP notification received on a connection
|
||||
*
|
||||
* Called by the connection module when an ECP notification arrives.
|
||||
* Routes to the pending ECP callback.
|
||||
*
|
||||
* @param conn_handle Connection handle
|
||||
* @param data Notification payload (response TLV)
|
||||
* @param data_len Payload length
|
||||
*/
|
||||
void ble_esl_ap_command_handle_ecp_notification(uint16_t conn_handle,
|
||||
const uint8_t *data,
|
||||
uint16_t data_len);
|
||||
|
||||
/**
|
||||
* @brief Clean up any pending ECP command context for a disconnected connection
|
||||
*
|
||||
* Finds the ECP command context associated with the given connection handle,
|
||||
* stops its timer, and frees it. Called from the disconnect handler to prevent
|
||||
* stale contexts and timer leaks.
|
||||
*
|
||||
* @param conn_handle Connection handle of the disconnected connection
|
||||
*/
|
||||
void ble_esl_ap_command_cleanup_conn(uint16_t conn_handle);
|
||||
|
||||
/**
|
||||
* @brief Handle a PAwR response slot data reception
|
||||
*
|
||||
* Called by the PAwR module when response data is received in a slot.
|
||||
*
|
||||
* @param group_id Group_ID (subevent number)
|
||||
* @param response_slot Response slot number
|
||||
* @param data Raw received data (Encrypted Data AD)
|
||||
* @param data_len Data length
|
||||
*/
|
||||
void ble_esl_ap_command_handle_pawr_response(uint8_t group_id,
|
||||
uint8_t response_slot,
|
||||
const uint8_t *data,
|
||||
uint8_t data_len);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* BLE_ESL_AP_INT_H */
|
||||
@@ -0,0 +1,361 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
/**
|
||||
* @file ble_esl_common.c
|
||||
* @brief Implementation of ESL common protocol TLV and payload encode/decode utilities
|
||||
*
|
||||
* Protocol layer shared by both roles: TLV / ESL Payload encode and decode,
|
||||
* plus the Encrypted Advertising Data (AES-CCM) transmit and receive helpers
|
||||
* used by the AP sync and ESL response PAwR paths.
|
||||
*/
|
||||
|
||||
#include <string.h>
|
||||
#include <assert.h>
|
||||
#include "esp_log.h"
|
||||
#include "esp_random.h"
|
||||
#include "host/ble_aes_ccm.h"
|
||||
#include "ble_esl_common.h"
|
||||
|
||||
static const char *TAG = "ble_esl_common";
|
||||
|
||||
/** Bit position of the directionBit within the last Randomizer octet */
|
||||
#define ESL_EAD_DIRECTION_BIT 7
|
||||
/** Mask of the directionBit within the last Randomizer octet */
|
||||
#define ESL_EAD_DIRECTION_BIT_MASK (1U << ESL_EAD_DIRECTION_BIT)
|
||||
|
||||
/**
|
||||
* Additional authenticated data of the Encrypted Advertising Data AD type
|
||||
* (Supplement to the Bluetooth Core Specification v11, Part A, 1.23.3)
|
||||
*/
|
||||
static const uint8_t s_ead_aad[] = {0xEA};
|
||||
|
||||
/* ========================== Forward Declarations ========================== */
|
||||
|
||||
static void esl_ead_increment_randomizer(uint8_t *randomizer);
|
||||
|
||||
esp_err_t ble_esl_tlv_encode(uint8_t opcode, const uint8_t *params,
|
||||
uint8_t params_len, uint8_t *out_buf,
|
||||
uint8_t *out_len)
|
||||
{
|
||||
/* Validate pointer arguments */
|
||||
if (params == NULL || out_buf == NULL || out_len == NULL) {
|
||||
ESP_LOGE(TAG, "tlv_encode: NULL argument");
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
|
||||
/* Validate that params_len matches the Length nibble in the opcode */
|
||||
uint8_t expected_params_len = BLE_ESL_TLV_PARAMS_LEN(opcode);
|
||||
if (params_len != expected_params_len) {
|
||||
ESP_LOGE(TAG, "tlv_encode: params_len %u does not match opcode 0x%02X "
|
||||
"(expected %u)", params_len, opcode, expected_params_len);
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
|
||||
/* Write the opcode byte */
|
||||
out_buf[0] = opcode;
|
||||
|
||||
/* Copy parameter bytes after the opcode */
|
||||
memcpy(&out_buf[1], params, params_len);
|
||||
|
||||
/* Total TLV size = 1 (opcode) + params_len */
|
||||
*out_len = (uint8_t)(1 + params_len);
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t ble_esl_tlv_decode(const uint8_t *in_buf, uint8_t in_len,
|
||||
uint8_t *opcode, const uint8_t **params,
|
||||
uint8_t *params_len)
|
||||
{
|
||||
/* Validate pointer arguments */
|
||||
if (in_buf == NULL || opcode == NULL || params == NULL || params_len == NULL) {
|
||||
ESP_LOGE(TAG, "tlv_decode: NULL argument");
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
|
||||
/* Need at least the minimum TLV size (opcode + 1 param byte) */
|
||||
if (in_len < BLE_ESL_TLV_MIN_SIZE) {
|
||||
ESP_LOGE(TAG, "tlv_decode: buffer too short (%u < %u)",
|
||||
in_len, BLE_ESL_TLV_MIN_SIZE);
|
||||
return ESP_ERR_INVALID_SIZE;
|
||||
}
|
||||
|
||||
/* Extract the opcode */
|
||||
uint8_t opc = in_buf[0];
|
||||
uint8_t expected_total = BLE_ESL_TLV_TOTAL_LEN(opc);
|
||||
|
||||
/* Verify the buffer contains enough data for the full TLV */
|
||||
if (in_len < expected_total) {
|
||||
ESP_LOGE(TAG, "tlv_decode: buffer too short for TLV "
|
||||
"(have %u, need %u for opcode 0x%02X)",
|
||||
in_len, expected_total, opc);
|
||||
return ESP_ERR_INVALID_SIZE;
|
||||
}
|
||||
|
||||
*opcode = opc;
|
||||
*params = &in_buf[1];
|
||||
*params_len = BLE_ESL_TLV_PARAMS_LEN(opc);
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t ble_esl_payload_encode(uint8_t group_id, const uint8_t *tlvs[],
|
||||
const uint8_t tlv_lens[], uint8_t tlv_count,
|
||||
uint8_t *out_buf, uint8_t *out_len)
|
||||
{
|
||||
/* Validate pointer arguments */
|
||||
if (out_buf == NULL || out_len == NULL) {
|
||||
ESP_LOGE(TAG, "payload_encode: NULL output argument");
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
|
||||
if (tlv_count == 0) {
|
||||
ESP_LOGE(TAG, "payload_encode: tlv_count must be greater than 0");
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
|
||||
if (tlvs == NULL || tlv_lens == NULL) {
|
||||
ESP_LOGE(TAG, "payload_encode: NULL TLV argument");
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
|
||||
/* Validate Group_ID range (7-bit) */
|
||||
if (group_id > BLE_ESL_GROUP_ID_MAX) {
|
||||
ESP_LOGE(TAG, "payload_encode: group_id 0x%02X exceeds max 0x%02X",
|
||||
group_id, BLE_ESL_GROUP_ID_MAX);
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
|
||||
/* Calculate total payload size: 1 (Group_ID header) + sum of all TLV lengths */
|
||||
uint16_t total = 1; /* Group_ID header byte */
|
||||
for (uint8_t i = 0; i < tlv_count; i++) {
|
||||
total += tlv_lens[i];
|
||||
}
|
||||
|
||||
if (total > BLE_ESL_PAYLOAD_MAX_SIZE) {
|
||||
ESP_LOGE(TAG, "payload_encode: total size %u exceeds max %u",
|
||||
total, BLE_ESL_PAYLOAD_MAX_SIZE);
|
||||
return ESP_ERR_INVALID_SIZE;
|
||||
}
|
||||
|
||||
/* Write Group_ID header byte: Group_ID in bits [6:0], RFU bit 7 = 0 */
|
||||
out_buf[0] = group_id & 0x7F;
|
||||
|
||||
/* Concatenate all TLV buffers */
|
||||
uint8_t offset = 1;
|
||||
for (uint8_t i = 0; i < tlv_count; i++) {
|
||||
if (tlvs[i] == NULL) {
|
||||
ESP_LOGE(TAG, "payload_encode: tlvs[%u] is NULL", i);
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
if (tlv_lens[i] < BLE_ESL_TLV_MIN_SIZE || tlv_lens[i] > BLE_ESL_TLV_MAX_SIZE) {
|
||||
ESP_LOGE(TAG, "payload_encode: tlv_lens[%u] = %u out of valid range [%u, %u]",
|
||||
i, tlv_lens[i], BLE_ESL_TLV_MIN_SIZE, BLE_ESL_TLV_MAX_SIZE);
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
memcpy(&out_buf[offset], tlvs[i], tlv_lens[i]);
|
||||
offset += tlv_lens[i];
|
||||
}
|
||||
|
||||
*out_len = offset;
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t ble_esl_payload_decode(const uint8_t *in_buf, uint8_t in_len,
|
||||
uint8_t *group_id, uint8_t *tlv_count,
|
||||
uint8_t *tlv_offsets, uint8_t *tlv_lens,
|
||||
uint8_t max_tlvs)
|
||||
{
|
||||
/* Validate pointer arguments */
|
||||
if (in_buf == NULL || group_id == NULL || tlv_count == NULL ||
|
||||
tlv_offsets == NULL || tlv_lens == NULL) {
|
||||
ESP_LOGE(TAG, "payload_decode: NULL argument");
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
|
||||
/* Need at least 1 byte for Group_ID header + minimum TLV (2 bytes) */
|
||||
if (in_len < 1 + BLE_ESL_TLV_MIN_SIZE) {
|
||||
ESP_LOGE(TAG, "payload_decode: payload too short (%u bytes)", in_len);
|
||||
return ESP_ERR_INVALID_SIZE;
|
||||
}
|
||||
|
||||
/* Extract Group_ID (bits [6:0] of first byte) */
|
||||
*group_id = in_buf[0] & 0x7F;
|
||||
|
||||
/* Parse concatenated TLVs starting after the Group_ID header */
|
||||
uint8_t offset = 1;
|
||||
uint8_t count = 0;
|
||||
|
||||
while (offset < in_len) {
|
||||
/* Need at least a minimum-size TLV in the remaining buffer */
|
||||
if ((uint16_t)offset + BLE_ESL_TLV_MIN_SIZE > in_len) {
|
||||
break;
|
||||
}
|
||||
|
||||
uint8_t opc = in_buf[offset];
|
||||
uint8_t tlv_total = BLE_ESL_TLV_TOTAL_LEN(opc);
|
||||
|
||||
/* Verify the TLV fits within the remaining buffer */
|
||||
if ((uint16_t)offset + tlv_total > in_len) {
|
||||
ESP_LOGE(TAG, "payload_decode: malformed TLV at offset %u "
|
||||
"(need %u bytes, have %u remaining)",
|
||||
offset, tlv_total, in_len - offset);
|
||||
return ESP_ERR_INVALID_SIZE;
|
||||
}
|
||||
|
||||
/* Check if we have room to report this TLV */
|
||||
if (count >= max_tlvs) {
|
||||
ESP_LOGE(TAG, "payload_decode: found more TLVs than max_tlvs (%u)",
|
||||
max_tlvs);
|
||||
*tlv_count = count;
|
||||
return ESP_ERR_NO_MEM;
|
||||
}
|
||||
|
||||
/* Record this TLV's offset and length */
|
||||
tlv_offsets[count] = offset;
|
||||
tlv_lens[count] = tlv_total;
|
||||
count++;
|
||||
|
||||
offset += tlv_total;
|
||||
}
|
||||
|
||||
/* If no TLVs were found, the payload is malformed */
|
||||
if (count == 0) {
|
||||
ESP_LOGE(TAG, "payload_decode: no TLVs found in payload");
|
||||
return ESP_ERR_INVALID_SIZE;
|
||||
}
|
||||
|
||||
*tlv_count = count;
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
void ble_esl_ead_randomizer_init(uint8_t *randomizer,
|
||||
ble_esl_ead_direction_t direction)
|
||||
{
|
||||
assert(randomizer != NULL);
|
||||
|
||||
esp_fill_random(randomizer, BLE_ESL_RANDOMIZER_SIZE);
|
||||
|
||||
uint8_t *last = &randomizer[BLE_ESL_RANDOMIZER_SIZE - 1];
|
||||
if (direction == BLE_ESL_EAD_DIR_ESL_TO_AP) {
|
||||
*last |= ESL_EAD_DIRECTION_BIT_MASK;
|
||||
} else {
|
||||
*last &= (uint8_t)~ESL_EAD_DIRECTION_BIT_MASK;
|
||||
}
|
||||
}
|
||||
|
||||
esp_err_t ble_esl_ead_encrypt(const uint8_t *session_key, const uint8_t *iv,
|
||||
uint8_t *randomizer, const uint8_t *payload,
|
||||
size_t payload_len, uint8_t *encrypted_payload)
|
||||
{
|
||||
/* Validate arguments */
|
||||
if (session_key == NULL || iv == NULL || randomizer == NULL ||
|
||||
payload == NULL || payload_len == 0 || encrypted_payload == NULL) {
|
||||
ESP_LOGE(TAG, "ead_encrypt: invalid argument");
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
|
||||
/*
|
||||
* Nonce is the concatenation of Randomizer and IV. The Randomizer is also
|
||||
* transmitted in clear ahead of the ciphertext so the peer can rebuild the
|
||||
* same nonce.
|
||||
*/
|
||||
uint8_t nonce[BLE_ESL_CCM_NONCE_SIZE];
|
||||
|
||||
memcpy(nonce, randomizer, BLE_ESL_RANDOMIZER_SIZE);
|
||||
memcpy(&nonce[BLE_ESL_RANDOMIZER_SIZE], iv, BLE_ESL_IV_SIZE);
|
||||
memcpy(encrypted_payload, randomizer, BLE_ESL_RANDOMIZER_SIZE);
|
||||
|
||||
int rc = ble_aes_ccm_encrypt(session_key, nonce, payload, payload_len,
|
||||
s_ead_aad, sizeof(s_ead_aad),
|
||||
&encrypted_payload[BLE_ESL_RANDOMIZER_SIZE],
|
||||
BLE_ESL_MIC_SIZE);
|
||||
if (rc != 0) {
|
||||
ESP_LOGE(TAG, "ead_encrypt: AES-CCM failed: %d", rc);
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
esl_ead_increment_randomizer(randomizer);
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t ble_esl_ead_decrypt(const uint8_t *session_key, const uint8_t *iv,
|
||||
const uint8_t *encrypted_payload,
|
||||
size_t encrypted_payload_len, uint8_t *payload,
|
||||
size_t payload_size, size_t *payload_len)
|
||||
{
|
||||
/* Validate arguments */
|
||||
if (session_key == NULL || iv == NULL || encrypted_payload == NULL ||
|
||||
payload == NULL || payload_len == NULL) {
|
||||
ESP_LOGE(TAG, "ead_decrypt: invalid argument");
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
|
||||
/*
|
||||
* The plaintext must hold at least one AD structure, so the encrypted
|
||||
* payload carries at least one ciphertext octet on top of the Randomizer
|
||||
* and the MIC (CSS v11, Part A, 1.23.2).
|
||||
*/
|
||||
if (encrypted_payload_len < BLE_ESL_RANDOMIZER_SIZE + BLE_ESL_MIC_SIZE + 1) {
|
||||
ESP_LOGE(TAG, "ead_decrypt: encrypted payload too short: %u",
|
||||
(unsigned)encrypted_payload_len);
|
||||
return ESP_ERR_INVALID_SIZE;
|
||||
}
|
||||
|
||||
size_t plaintext_len = encrypted_payload_len -
|
||||
(BLE_ESL_RANDOMIZER_SIZE + BLE_ESL_MIC_SIZE);
|
||||
if (plaintext_len > payload_size) {
|
||||
ESP_LOGE(TAG, "ead_decrypt: output buffer too small (%u < %u)",
|
||||
(unsigned)payload_size, (unsigned)plaintext_len);
|
||||
return ESP_ERR_INVALID_SIZE;
|
||||
}
|
||||
|
||||
/*
|
||||
* Rebuild the nonce from the Randomizer sent in clear ahead of the
|
||||
* ciphertext and the locally stored IV.
|
||||
*/
|
||||
uint8_t nonce[BLE_ESL_CCM_NONCE_SIZE];
|
||||
|
||||
memcpy(nonce, encrypted_payload, BLE_ESL_RANDOMIZER_SIZE);
|
||||
memcpy(&nonce[BLE_ESL_RANDOMIZER_SIZE], iv, BLE_ESL_IV_SIZE);
|
||||
|
||||
int rc = ble_aes_ccm_decrypt(session_key, nonce,
|
||||
&encrypted_payload[BLE_ESL_RANDOMIZER_SIZE],
|
||||
plaintext_len, s_ead_aad, sizeof(s_ead_aad),
|
||||
payload, BLE_ESL_MIC_SIZE);
|
||||
if (rc != 0) {
|
||||
ESP_LOGW(TAG, "ead_decrypt: MIC authentication failed: %d", rc);
|
||||
return ESP_ERR_INVALID_RESPONSE;
|
||||
}
|
||||
|
||||
*payload_len = plaintext_len;
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
static void esl_ead_increment_randomizer(uint8_t *randomizer)
|
||||
{
|
||||
uint8_t direction_bit = randomizer[BLE_ESL_RANDOMIZER_SIZE - 1] &
|
||||
ESL_EAD_DIRECTION_BIT_MASK;
|
||||
|
||||
for (uint8_t i = 0; i < BLE_ESL_RANDOMIZER_SIZE; i++) {
|
||||
if (i == BLE_ESL_RANDOMIZER_SIZE - 1) {
|
||||
/* Last octet: 7-bit counter, directionBit restored */
|
||||
uint8_t counter = (uint8_t)((randomizer[i] + 1) & 0x7F);
|
||||
randomizer[i] = (uint8_t)(counter | direction_bit);
|
||||
break;
|
||||
}
|
||||
|
||||
randomizer[i]++;
|
||||
if (randomizer[i] != 0) {
|
||||
break; /* No carry into the next octet */
|
||||
}
|
||||
}
|
||||
}
|
||||
473
components/bt/ble_profiles/nimble/ble_esl/src/esl/ble_esl_int.h
Normal file
473
components/bt/ble_profiles/nimble/ble_esl/src/esl/ble_esl_int.h
Normal file
@@ -0,0 +1,473 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
/**
|
||||
* @file ble_esl_int.h
|
||||
* @brief ESL component internal header — shared interfaces across source files
|
||||
*
|
||||
* Not part of the public API. Groups internal declarations by source file.
|
||||
*/
|
||||
|
||||
#ifndef BLE_ESL_INT_H
|
||||
#define BLE_ESL_INT_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
#include "esp_err.h"
|
||||
#include "ble_esl.h"
|
||||
#include "ble_esl_common.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* ========================== Constants ========================== */
|
||||
|
||||
/** @brief Sync/Unsync state timeout: 60 minutes in microseconds (for esp_timer) */
|
||||
#define ESL_STATE_TIMEOUT_US (60ULL * 60ULL * 1000000ULL)
|
||||
|
||||
/** @brief ECP Procedure Timeout in microseconds */
|
||||
#define ESL_ECP_TIMEOUT_US (30ULL * 1000000ULL)
|
||||
|
||||
/** @brief Maximum number of TLVs in a single ESL Payload */
|
||||
#define ESL_MAX_TLVS_PER_PAYLOAD 24
|
||||
|
||||
/** @brief Sensor transport context: ECP */
|
||||
#define ESL_TRANSPORT_ECP 0
|
||||
|
||||
/** @brief Sensor transport context: PAwR */
|
||||
#define ESL_TRANSPORT_PAWR 1
|
||||
|
||||
/* ---- esl_state.c ---- */
|
||||
|
||||
/**
|
||||
* @brief Return the active ACL connection handle
|
||||
* @return Connection handle, or BLE_HS_CONN_HANDLE_NONE if not connected
|
||||
*/
|
||||
uint16_t esl_state_get_conn_handle(void);
|
||||
|
||||
/**
|
||||
* @brief Check whether the ESL is synchronized to the AP's PAwR train
|
||||
*
|
||||
* Reflects the actual synchronization condition rather than the state machine
|
||||
* state: the sync is established on entry to Synchronized and retained while in
|
||||
* the Updating state (where PAwR data is ignored but sync is not lost). It is
|
||||
* cleared on Unsynchronized, Configuring and Unassociated.
|
||||
*
|
||||
* @return true if synchronized to the AP
|
||||
*/
|
||||
bool esl_is_pawr_synced(void);
|
||||
|
||||
/**
|
||||
* @brief Execute a state transition with associated side-effects
|
||||
*
|
||||
* Validates legality, changes GAP mode, arms/disarms timeouts, deletes data
|
||||
* on unassociate/factory-reset, and fires BLE_ESL_EVT_STATE_CHANGED callback.
|
||||
*
|
||||
* @param[in] new_state Target state
|
||||
* @return ESP_OK on success; ESP_ERR_INVALID_STATE if transition not permitted
|
||||
*/
|
||||
esp_err_t esl_state_transition(ble_esl_state_t new_state);
|
||||
|
||||
/**
|
||||
* @brief Get the current ESL absolute time in milliseconds
|
||||
*
|
||||
* The time counter is set via the ESL Current Absolute Time characteristic
|
||||
* and increments by 1 every millisecond, wrapping at 0xFFFFFFFF.
|
||||
*
|
||||
* @return Current absolute time in milliseconds
|
||||
*/
|
||||
uint32_t esl_get_abs_time(void);
|
||||
|
||||
/**
|
||||
* @brief Fire an ESL event to the application callback
|
||||
*
|
||||
* @param[in] event Event type
|
||||
* @param[in] param Event data (may be NULL)
|
||||
*/
|
||||
void esl_notify_app(ble_esl_event_t event, ble_esl_cb_param_t *param);
|
||||
|
||||
/**
|
||||
* @brief Get the ESL's configured address
|
||||
*
|
||||
* @param[out] esl_id ESL_ID (0x00–0xFE)
|
||||
* @param[out] group_id Group_ID (7-bit)
|
||||
* @return ESP_OK if address is configured; ESP_ERR_INVALID_STATE if not
|
||||
*/
|
||||
esp_err_t esl_get_address(uint8_t *esl_id, uint8_t *group_id);
|
||||
|
||||
/**
|
||||
* @brief Get the ESL's stored configuration
|
||||
*
|
||||
* @return Pointer to the stored ble_esl_config_t (valid while initialized)
|
||||
*/
|
||||
const ble_esl_config_t *esl_get_config(void);
|
||||
|
||||
/**
|
||||
* @brief Get the stored AP Sync Key Material
|
||||
*
|
||||
* @return Pointer to 24-byte key material, or NULL if not configured
|
||||
*/
|
||||
const ble_esl_key_material_t *esl_get_ap_sync_key(void);
|
||||
|
||||
/**
|
||||
* @brief Get the stored ESL Response Key Material
|
||||
*
|
||||
* @return Pointer to 24-byte key material, or NULL if not configured
|
||||
*/
|
||||
const ble_esl_key_material_t *esl_get_resp_key(void);
|
||||
|
||||
/**
|
||||
* @brief Clear all stored configuration data (keys, address, timed commands)
|
||||
*
|
||||
* Called during unassociate, factory reset, and unsync timeout transitions.
|
||||
*
|
||||
* @param[in] clear_images If true, also clear stored image data (factory reset only)
|
||||
*/
|
||||
void esl_clear_stored_data(bool clear_images);
|
||||
|
||||
/**
|
||||
* @brief Notify the state machine that the Update Complete command was received
|
||||
*
|
||||
* In the Updating state, both PAST and Update Complete are required before
|
||||
* transitioning to Synchronized. This function records that Update Complete
|
||||
* has been received and triggers the transition if PAST was already received.
|
||||
*/
|
||||
void esl_notify_update_complete(void);
|
||||
|
||||
/* ---- esl_gatts.c ---- */
|
||||
/**
|
||||
* @brief Send one response TLV as an ECP characteristic notification
|
||||
*
|
||||
* @param[in] response_tlv Pointer to the encoded response TLV buffer
|
||||
* @param[in] response_len Length of the response TLV in bytes
|
||||
* @return ESP_OK on success; ESP_FAIL if notification could not be sent
|
||||
*/
|
||||
esp_err_t esl_send_ecp_response(const uint8_t *response_tlv, uint8_t response_len);
|
||||
|
||||
/**
|
||||
* @brief Handle completion of an ECP notification transmission
|
||||
*
|
||||
* Used by the GAP event handler to run any command cleanup that must happen
|
||||
* after the ECP response notification has actually been transmitted.
|
||||
*/
|
||||
void esl_handle_ecp_notify_tx(uint16_t conn_handle, uint16_t attr_handle,
|
||||
int status, uint8_t indication);
|
||||
|
||||
/* ---- esl_cmd_lifecycle.c ---- */
|
||||
|
||||
/**
|
||||
* @brief Result of command dispatch
|
||||
*/
|
||||
typedef struct {
|
||||
bool has_response; /*!< True if command produces a response TLV */
|
||||
uint8_t resp_opcode; /*!< Response opcode byte */
|
||||
uint8_t resp_params[BLE_ESL_TLV_MAX_SIZE]; /*!< Response parameter bytes */
|
||||
uint8_t resp_params_len; /*!< Length of resp_params in octets */
|
||||
} ble_esl_cmd_result_t;
|
||||
|
||||
/**
|
||||
* @brief Central command dispatch function
|
||||
*
|
||||
* Validates ESL_ID, checks for unrecognized opcodes, and routes to the
|
||||
* appropriate per-opcode handler. Used by both ECP and PAwR paths.
|
||||
*
|
||||
* @param[in] opcode Command opcode byte
|
||||
* @param[in] params Pointer to command parameter bytes (first byte is ESL_ID)
|
||||
* @param[in] params_len Length of params in octets
|
||||
* @param[out] result Output: populated with the response
|
||||
* @param[in] transport Transport context: ESL_TRANSPORT_ECP or ESL_TRANSPORT_PAWR
|
||||
* @return ESP_OK if the command was processed — including rejections that carry
|
||||
* an Error response in @p result; ESP_ERR_INVALID_STATE if the module is
|
||||
* not initialized
|
||||
*/
|
||||
esp_err_t esl_cmd_dispatch(uint8_t opcode, const uint8_t *params,
|
||||
uint8_t params_len, ble_esl_cmd_result_t *result,
|
||||
uint8_t transport);
|
||||
|
||||
/**
|
||||
* @brief Assemble the 16-bit Basic State response bitmap
|
||||
*
|
||||
* Queries Service Needed, Synchronized, Active LED, Pending LED Update,
|
||||
* and Pending Display Update flags from respective modules.
|
||||
*
|
||||
* @param[out] bitmap_out The 16-bit Basic State bitmap
|
||||
* @return ESP_OK on success
|
||||
*/
|
||||
esp_err_t esl_build_basic_state(uint16_t *bitmap_out);
|
||||
|
||||
/**
|
||||
* @brief Check if factory reset is pending
|
||||
* @return true if factory reset guard flag is set
|
||||
*/
|
||||
bool esl_is_factory_reset_pending(void);
|
||||
|
||||
/**
|
||||
* @brief Clear the factory reset pending flag
|
||||
*
|
||||
* Called after factory reset cleanup is complete (e.g., after clearing stored
|
||||
* data and transitioning to Unassociated state in the disconnect handler).
|
||||
*/
|
||||
void esl_clear_factory_reset_pending(void);
|
||||
|
||||
/**
|
||||
* @brief Initialize the lifecycle command module
|
||||
* @return ESP_OK on success
|
||||
*/
|
||||
esp_err_t esl_cmd_lifecycle_init(void);
|
||||
|
||||
/**
|
||||
* @brief Deinitialize the lifecycle command module
|
||||
*/
|
||||
void esl_cmd_lifecycle_deinit(void);
|
||||
|
||||
/* ---- esl_cmd_display.c ---- */
|
||||
|
||||
/**
|
||||
* @brief Handle Read Sensor Data command (opcode 0x10)
|
||||
*
|
||||
* @param[in] params Command parameters (after opcode)
|
||||
* @param[in] params_len Length of params
|
||||
* @param[out] result Command result
|
||||
* @param[in] transport ESL_TRANSPORT_ECP or ESL_TRANSPORT_PAWR
|
||||
* @return ESP_OK on success
|
||||
*/
|
||||
esp_err_t esl_cmd_read_sensor(const uint8_t *params, uint8_t params_len,
|
||||
ble_esl_cmd_result_t *result, uint8_t transport);
|
||||
|
||||
/**
|
||||
* @brief Handle Refresh Display command (opcode 0x11)
|
||||
*
|
||||
* @param[in] params Command parameters
|
||||
* @param[in] params_len Length of params
|
||||
* @param[out] result Command result
|
||||
* @return ESP_OK on success
|
||||
*/
|
||||
esp_err_t esl_cmd_refresh_display(const uint8_t *params, uint8_t params_len,
|
||||
ble_esl_cmd_result_t *result);
|
||||
|
||||
/**
|
||||
* @brief Handle Display Image command (opcode 0x20)
|
||||
*
|
||||
* @param[in] params Command parameters
|
||||
* @param[in] params_len Length of params
|
||||
* @param[out] result Command result
|
||||
* @return ESP_OK on success
|
||||
*/
|
||||
esp_err_t esl_cmd_display_image(const uint8_t *params, uint8_t params_len,
|
||||
ble_esl_cmd_result_t *result);
|
||||
|
||||
/**
|
||||
* @brief Handle Display Timed Image command (opcode 0x60)
|
||||
*
|
||||
* @param[in] params Command parameters
|
||||
* @param[in] params_len Length of params
|
||||
* @param[out] result Command result
|
||||
* @return ESP_OK on success
|
||||
*/
|
||||
esp_err_t esl_cmd_display_timed_image(const uint8_t *params, uint8_t params_len,
|
||||
ble_esl_cmd_result_t *result);
|
||||
|
||||
/**
|
||||
* @brief Get the Pending Display Update flag for Basic State bitmap
|
||||
* @return true if at least one display has a pending timed image command
|
||||
*/
|
||||
bool esl_get_display_pending(void);
|
||||
|
||||
/**
|
||||
* @brief Initialize the display/sensor command module
|
||||
* @return ESP_OK on success
|
||||
*/
|
||||
esp_err_t esl_cmd_display_init(void);
|
||||
|
||||
/**
|
||||
* @brief Deinitialize the display/sensor command module
|
||||
*/
|
||||
void esl_cmd_display_deinit(void);
|
||||
|
||||
/**
|
||||
* @brief Cancel all pending timed display commands
|
||||
*
|
||||
* Called during unassociate / factory reset.
|
||||
*/
|
||||
void esl_cmd_display_cancel_all(void);
|
||||
|
||||
/* ---- esl_cmd_led.c ---- */
|
||||
|
||||
/**
|
||||
* @brief LED status flags for Basic State bitmap
|
||||
*/
|
||||
typedef struct {
|
||||
bool active_led; /*!< True if any LED is currently active */
|
||||
bool pending_led_update; /*!< True if any LED timed command is pending */
|
||||
} esl_led_status_t;
|
||||
|
||||
/**
|
||||
* @brief Handle LED Control command (opcode 0xB0)
|
||||
*
|
||||
* @param[in] params Command parameters
|
||||
* @param[in] params_len Length of params
|
||||
* @param[out] result Command result
|
||||
* @return ESP_OK on success
|
||||
*/
|
||||
esp_err_t esl_cmd_led_control(const uint8_t *params, uint8_t params_len,
|
||||
ble_esl_cmd_result_t *result);
|
||||
|
||||
/**
|
||||
* @brief Handle LED Timed Control command (opcode 0xF0)
|
||||
*
|
||||
* @param[in] params Command parameters
|
||||
* @param[in] params_len Length of params
|
||||
* @param[out] result Command result
|
||||
* @return ESP_OK on success
|
||||
*/
|
||||
esp_err_t esl_cmd_led_timed_control(const uint8_t *params, uint8_t params_len,
|
||||
ble_esl_cmd_result_t *result);
|
||||
|
||||
/**
|
||||
* @brief Handle Vendor-specific command (opcodes 0x_F)
|
||||
*
|
||||
* @param[in] opcode Full opcode byte
|
||||
* @param[in] params Command parameters
|
||||
* @param[in] params_len Length of params
|
||||
* @param[out] result Command result
|
||||
* @return ESP_OK on success
|
||||
*/
|
||||
esp_err_t esl_cmd_vendor_specific(uint8_t opcode, const uint8_t *params,
|
||||
uint8_t params_len, ble_esl_cmd_result_t *result);
|
||||
|
||||
/**
|
||||
* @brief Get Active LED and Pending LED Update flags
|
||||
* @return esl_led_status_t with current flags
|
||||
*/
|
||||
esl_led_status_t esl_get_led_status(void);
|
||||
|
||||
/**
|
||||
* @brief Initialize the LED command module
|
||||
* @return ESP_OK on success
|
||||
*/
|
||||
esp_err_t esl_cmd_led_init(void);
|
||||
|
||||
/**
|
||||
* @brief Deinitialize the LED command module
|
||||
*/
|
||||
void esl_cmd_led_deinit(void);
|
||||
|
||||
/**
|
||||
* @brief Cancel all pending timed LED commands
|
||||
*
|
||||
* Called during unassociate / factory reset.
|
||||
*/
|
||||
void esl_cmd_led_cancel_all(void);
|
||||
|
||||
/* ---- esl_pawr.c ---- */
|
||||
|
||||
/**
|
||||
* @brief Slot info tracked during PAwR packet processing
|
||||
*/
|
||||
typedef struct {
|
||||
uint8_t slot_number; /*!< PAwR response slot */
|
||||
uint8_t tlv_count; /*!< Number of individually-addressed TLVs */
|
||||
uint8_t broadcast_count; /*!< Number of broadcast TLVs */
|
||||
} esl_pawr_slot_info_t;
|
||||
|
||||
/**
|
||||
* @brief Process a received PAwR sync packet
|
||||
*
|
||||
* Strips the inner AD wrapper ([len][0x34]), decodes the ESL Payload,
|
||||
* validates Group_ID, dispatches commands, calculates response slot,
|
||||
* assembles and encrypts response, and transmits.
|
||||
*
|
||||
* @param[in] subevent PAwR subevent number
|
||||
* @param[in] data Pointer to the decrypted plaintext, i.e. the inner AD
|
||||
* structure [inner_len][ESL Tag 0x34][ESL Payload]
|
||||
* @param[in] data_len Length of the decrypted plaintext
|
||||
* @return ESP_OK on success, error code on failure
|
||||
*/
|
||||
esp_err_t esl_pawr_process_sync_packet(uint8_t subevent, const uint8_t *data,
|
||||
uint8_t data_len);
|
||||
|
||||
/**
|
||||
* @brief Calculate the response slot for this ESL
|
||||
*
|
||||
* @param[in] data Pointer to the decrypted ESL Payload buffer
|
||||
* @param[in] tlv_offsets Array of offsets for each command TLV
|
||||
* @param[in] tlv_lens Array of lengths for each command TLV
|
||||
* @param[in] tlv_count Total number of command TLVs
|
||||
* @param[in] esl_id This ESL's ESL_ID
|
||||
* @return Response slot number (>= 0), or -1 if not individually addressed
|
||||
*/
|
||||
int16_t esl_pawr_calc_response_slot(const uint8_t *data,
|
||||
const uint8_t *tlv_offsets,
|
||||
const uint8_t *tlv_lens,
|
||||
uint8_t tlv_count, uint8_t esl_id);
|
||||
|
||||
/**
|
||||
* @brief Assemble response TLVs into a single ESL Payload
|
||||
*
|
||||
* @param[in] resp_tlvs Array of pointers to encoded response TLV buffers
|
||||
* @param[in] resp_tlv_lens Array of response TLV lengths
|
||||
* @param[in] resp_count Number of response TLVs
|
||||
* @param[out] out_buf Output buffer (min 48 octets)
|
||||
* @param[out] out_len Total payload size written
|
||||
* @return ESP_OK on success; ESP_ERR_INVALID_SIZE if payload exceeds 48 octets
|
||||
*/
|
||||
esp_err_t esl_pawr_assemble_response(const uint8_t *resp_tlvs[],
|
||||
const uint8_t resp_tlv_lens[],
|
||||
uint8_t resp_count,
|
||||
uint8_t *out_buf, uint8_t *out_len);
|
||||
|
||||
/**
|
||||
* @brief Encrypt and send response in the assigned PAwR response slot
|
||||
*
|
||||
* @param[in] slot Response slot number
|
||||
* @param[in] payload Assembled response ESL Payload
|
||||
* @param[in] payload_len Length of the payload
|
||||
* @return ESP_OK on success, error code on failure
|
||||
*/
|
||||
esp_err_t esl_pawr_encrypt_and_send(uint8_t slot, const uint8_t *payload,
|
||||
uint8_t payload_len);
|
||||
|
||||
/**
|
||||
* @brief Check if an Unassociate post-response cleanup is pending
|
||||
* @return true if Unassociate cleanup needs to run after ECP response sent
|
||||
*/
|
||||
bool esl_is_unassociate_pending(void);
|
||||
|
||||
/**
|
||||
* @brief Execute deferred Unassociate cleanup after ECP response is sent
|
||||
*
|
||||
* Clears stored data, transitions to Unassociated, fires UNASSOCIATE event.
|
||||
*/
|
||||
void esl_execute_unassociate_cleanup(void);
|
||||
|
||||
/**
|
||||
* @brief Set PAwR sync context before processing a sync packet
|
||||
*
|
||||
* Must be called from the PAwR sync-received event handler before
|
||||
* calling esl_pawr_process_sync_packet().
|
||||
*
|
||||
* @param[in] sync_handle PAwR sync handle
|
||||
* @param[in] event_counter PAwR event counter
|
||||
*/
|
||||
void esl_pawr_set_sync_context(uint16_t sync_handle, uint16_t event_counter);
|
||||
|
||||
/**
|
||||
* @brief Initialize the PAwR module
|
||||
* @return ESP_OK on success
|
||||
*/
|
||||
esp_err_t esl_pawr_init(void);
|
||||
|
||||
/**
|
||||
* @brief Deinitialize the PAwR module
|
||||
*/
|
||||
void esl_pawr_deinit(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* BLE_ESL_INT_H */
|
||||
@@ -0,0 +1,100 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
/**
|
||||
* @file ble_esl_state_int.h
|
||||
* @brief ESL component internal header — shared interfaces across source files
|
||||
*
|
||||
* Not part of the public API. Groups internal declarations by source file.
|
||||
*/
|
||||
|
||||
#ifndef BLE_ESL_STATE_INT_H
|
||||
#define BLE_ESL_STATE_INT_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/semphr.h"
|
||||
#include "esp_err.h"
|
||||
#include "esp_timer.h"
|
||||
#include "nimble/ble.h"
|
||||
#include "ble_esl.h"
|
||||
#include "ble_esl_common.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* ========================== Configuration Bitmask ========================== */
|
||||
|
||||
#define CONFIG_BIT_ADDRESS (1 << 0)
|
||||
#define CONFIG_BIT_AP_SYNC_KEY (1 << 1)
|
||||
#define CONFIG_BIT_RESP_KEY (1 << 2)
|
||||
#define CONFIG_BIT_ABS_TIME (1 << 3)
|
||||
#define CONFIG_COMPLETE_MASK (CONFIG_BIT_ADDRESS | CONFIG_BIT_AP_SYNC_KEY | \
|
||||
CONFIG_BIT_RESP_KEY | CONFIG_BIT_ABS_TIME)
|
||||
|
||||
/* ========================== Internal State Context ========================== */
|
||||
typedef struct {
|
||||
ble_esl_state_t state; /* Current ESL state */
|
||||
ble_esl_config_t config; /* Copy of init config */
|
||||
ble_esl_address_t esl_address; /* Stored ESL address */
|
||||
ble_esl_key_material_t ap_sync_key; /* AP Sync Key Material */
|
||||
ble_esl_key_material_t resp_key; /* ESL Response Key Material */
|
||||
uint32_t abs_time_base; /* Written absolute time value */
|
||||
int64_t abs_time_offset_us; /* esp_timer_get_time() at write */
|
||||
uint8_t config_complete; /* 4-bit bitmask */
|
||||
uint16_t conn_handle; /* Active ACL connection handle */
|
||||
ble_esl_cb_t app_cb; /* Application callback */
|
||||
bool started; /* Whether ble_esl_start() called */
|
||||
bool initialized; /* Whether ble_esl_init() called */
|
||||
esp_timer_handle_t sync_timer; /* 60-min sync timeout */
|
||||
esp_timer_handle_t unsync_timer; /* 60-min unsync timeout */
|
||||
esp_timer_handle_t ecp_timer; /* 30-sec ECP procedure timeout */
|
||||
uint16_t ecp_val_handle; /* ECP characteristic value handle */
|
||||
ble_addr_t bonded_peer_addr; /* Address of bonded AP */
|
||||
bool has_bonded_peer; /* Whether we have a bonded peer */
|
||||
bool ap_sync_key_valid; /* AP sync key written */
|
||||
bool resp_key_valid; /* Response key written */
|
||||
bool address_valid; /* ESL address written */
|
||||
uint16_t pawr_sync_handle; /* Active PAwR periodic sync handle; BLE_HS_CONN_HANDLE_NONE if none */
|
||||
bool past_received; /* PAST completed in Updating state */
|
||||
bool pawr_synced; /* Synchronized to the AP's PAwR train (retained across Updating) */
|
||||
bool past_pending; /* PAST re-arm deferred until SYNC_LOST frees pool slot */
|
||||
bool update_complete_received; /* Update Complete cmd received in Updating state */
|
||||
bool deinit_pending; /* ble_esl_deinit() is waiting for disconnect */
|
||||
SemaphoreHandle_t deinit_sem; /* Signaled when disconnect completes during deinit */
|
||||
} esl_state_ctx_t;
|
||||
|
||||
/* ---- esl_state.c ---- */
|
||||
|
||||
/**
|
||||
* @brief Return the singleton ESL state context
|
||||
* @return Pointer to the global esl_state_ctx_t instance
|
||||
*/
|
||||
esl_state_ctx_t *esl_state_ctx_get(void);
|
||||
|
||||
/* ---- esl_gatts.c ---- */
|
||||
|
||||
/**
|
||||
* @brief Initialize the GATT server and register the ESL Service
|
||||
*
|
||||
* @param[in] config Pointer to the user-supplied ESL configuration
|
||||
* @return ESP_OK on success
|
||||
*/
|
||||
esp_err_t esl_gatts_init(const ble_esl_config_t *config);
|
||||
|
||||
/**
|
||||
* @brief Deregister the ESL Service and free GATT server resources
|
||||
* @return ESP_OK on success
|
||||
*/
|
||||
esp_err_t esl_gatts_deinit(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* BLE_ESL_STATE_INT_H */
|
||||
@@ -0,0 +1,904 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
/**
|
||||
* @file esl_cmd_display.c
|
||||
* @brief ESL Display & Sensor command handlers
|
||||
*
|
||||
* Implements Read Sensor Data, Refresh Display, Display Image, and
|
||||
* Display Timed Image command processing, including timed command
|
||||
* scheduling, sensor read deferral, and Pending Display Update flag
|
||||
* management.
|
||||
*/
|
||||
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
#include "esp_log.h"
|
||||
#include "esp_timer.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/semphr.h"
|
||||
#include "ble_esl_int.h"
|
||||
|
||||
static const char *TAG = "esl_cmd_display";
|
||||
|
||||
/* ========================== Internal Data Structures ========================== */
|
||||
|
||||
/** @brief Per-display internal state */
|
||||
typedef struct {
|
||||
bool has_active_image; /*!< Whether an image is currently displayed */
|
||||
uint8_t current_image_index; /*!< Image index currently displayed (valid if has_active_image) */
|
||||
bool timed_pending; /*!< Whether a timed display command is pending */
|
||||
uint8_t timed_image_index; /*!< Image index for the pending timed command */
|
||||
uint32_t timed_absolute_time; /*!< Absolute time for the pending timed command */
|
||||
esp_timer_handle_t timer; /*!< Timer handle for timed display commands */
|
||||
} display_state_t;
|
||||
|
||||
/** @brief Maximum sensor data length (ESL spec: up to 15 bytes) */
|
||||
#define SENSOR_DATA_CACHE_MAX_LEN 15
|
||||
|
||||
/** @brief Per-sensor internal state */
|
||||
typedef struct {
|
||||
bool read_pending; /*!< Whether a sensor read is in progress */
|
||||
uint8_t transport_context; /*!< ESL_TRANSPORT_ECP or ESL_TRANSPORT_PAWR */
|
||||
bool cache_valid; /*!< Whether cached sensor data is available (PAwR) */
|
||||
uint8_t cache_error_code; /*!< Cached error code (0 = success) */
|
||||
uint8_t cache_data[SENSOR_DATA_CACHE_MAX_LEN]; /*!< Cached sensor data */
|
||||
uint8_t cache_data_len; /*!< Length of cached sensor data */
|
||||
} sensor_state_t;
|
||||
|
||||
/** @brief Aggregated display/sensor module context */
|
||||
typedef struct {
|
||||
display_state_t *displays; /*!< Array of per-display state [num_displays] */
|
||||
sensor_state_t *sensors; /*!< Array of per-sensor state [num_sensors] */
|
||||
uint8_t num_displays; /*!< Number of displays from config */
|
||||
uint8_t num_images; /*!< Number of images from config */
|
||||
uint8_t num_sensors; /*!< Number of sensors from config */
|
||||
} display_ctx_t;
|
||||
|
||||
static display_ctx_t *s_ctx = NULL;
|
||||
|
||||
/** @brief File-scope mutex protecting s_ctx and all shared display/sensor state.
|
||||
* Decoupled from s_ctx lifetime so the lock remains valid even after s_ctx is freed. */
|
||||
static SemaphoreHandle_t s_lock = NULL;
|
||||
|
||||
/* ========================== Forward Declarations ========================== */
|
||||
|
||||
static void timed_display_timer_cb(void *arg);
|
||||
static void build_error_response(ble_esl_cmd_result_t *result, uint8_t error_code);
|
||||
static void build_display_state_response(ble_esl_cmd_result_t *result,
|
||||
uint8_t display_index, uint8_t image_index);
|
||||
static bool is_image_available(uint8_t image_index);
|
||||
static void restore_sensor_pending(uint8_t sensor_index, uint8_t transport);
|
||||
|
||||
/* ========================== Helper Functions ========================== */
|
||||
|
||||
/**
|
||||
* @brief Build an Error response (opcode 0x00) into the result structure
|
||||
*/
|
||||
static void build_error_response(ble_esl_cmd_result_t *result, uint8_t error_code)
|
||||
{
|
||||
result->has_response = true;
|
||||
result->resp_opcode = BLE_ESL_RESP_ERROR;
|
||||
result->resp_params[0] = error_code;
|
||||
result->resp_params_len = 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Build a Display State response (opcode 0x11) into the result structure
|
||||
*/
|
||||
static void build_display_state_response(ble_esl_cmd_result_t *result,
|
||||
uint8_t display_index, uint8_t image_index)
|
||||
{
|
||||
result->has_response = true;
|
||||
result->resp_opcode = BLE_ESL_RESP_DISPLAY_STATE;
|
||||
result->resp_params[0] = display_index;
|
||||
result->resp_params[1] = image_index;
|
||||
result->resp_params_len = 2;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Check if an image slot contains valid data
|
||||
*
|
||||
* An image is considered available if:
|
||||
* - The image_writable_mask is NULL: all images are read-only (static/pre-loaded)
|
||||
* and therefore always available.
|
||||
* - The image_writable_mask entry is false: the image is read-only (protected)
|
||||
* and therefore always available.
|
||||
* - The image_writable_mask entry is true: the image is writable and assumed
|
||||
* to have been written by the AP.
|
||||
*
|
||||
* In other words, any valid image index within range is considered available.
|
||||
*/
|
||||
static bool is_image_available(uint8_t image_index)
|
||||
{
|
||||
const ble_esl_config_t *config = esl_get_config();
|
||||
if (config == NULL) {
|
||||
return false;
|
||||
}
|
||||
if (image_index >= config->num_images) {
|
||||
return false;
|
||||
}
|
||||
/* All valid image indices are considered available:
|
||||
* - Read-only images (no writable mask, or mask[i] == false) are
|
||||
* static/pre-loaded and always available.
|
||||
* - Writable images (mask[i] == true) are assumed written by the AP. */
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Restore sensor pending state after a failed response send
|
||||
*
|
||||
* Re-acquires the lock, validates that the context and sensor index are
|
||||
* still valid, and restores read_pending and transport_context so the
|
||||
* application can retry calling ble_esl_report_sensor_data().
|
||||
*
|
||||
* @param[in] sensor_index Index of the sensor to restore
|
||||
* @param[in] transport Original transport context to restore
|
||||
*/
|
||||
static void restore_sensor_pending(uint8_t sensor_index, uint8_t transport)
|
||||
{
|
||||
xSemaphoreTake(s_lock, portMAX_DELAY);
|
||||
if (s_ctx != NULL && sensor_index < s_ctx->num_sensors) {
|
||||
s_ctx->sensors[sensor_index].read_pending = true;
|
||||
s_ctx->sensors[sensor_index].transport_context = transport;
|
||||
}
|
||||
xSemaphoreGive(s_lock);
|
||||
}
|
||||
|
||||
/* ========================== Timer Callback ========================== */
|
||||
|
||||
/**
|
||||
* @brief Timer callback for Display Timed Image execution
|
||||
*
|
||||
* Runs in timer task context — keeps work minimal: updates state and
|
||||
* fires the application callback.
|
||||
*/
|
||||
static void timed_display_timer_cb(void *arg)
|
||||
{
|
||||
xSemaphoreTake(s_lock, portMAX_DELAY);
|
||||
|
||||
if (s_ctx == NULL) {
|
||||
/* Context was freed while we waited for the lock or before — nothing to do. */
|
||||
xSemaphoreGive(s_lock);
|
||||
return;
|
||||
}
|
||||
|
||||
/* arg encodes the display index */
|
||||
uint8_t display_index = (uint8_t)(uintptr_t)arg;
|
||||
if (display_index >= s_ctx->num_displays) {
|
||||
xSemaphoreGive(s_lock);
|
||||
return;
|
||||
}
|
||||
|
||||
display_state_t *disp = &s_ctx->displays[display_index];
|
||||
if (!disp->timed_pending) {
|
||||
xSemaphoreGive(s_lock);
|
||||
return;
|
||||
}
|
||||
|
||||
/* Update per-display current image tracking */
|
||||
disp->has_active_image = true;
|
||||
disp->current_image_index = disp->timed_image_index;
|
||||
uint8_t image_index = disp->current_image_index;
|
||||
|
||||
/* Clear pending state */
|
||||
disp->timed_pending = false;
|
||||
|
||||
xSemaphoreGive(s_lock);
|
||||
|
||||
/* Fire BLE_ESL_EVT_DISPLAY_IMAGE callback outside the lock to avoid
|
||||
* potential deadlocks in application code that may call back into ESL. */
|
||||
ble_esl_cb_param_t param;
|
||||
memset(¶m, 0, sizeof(param));
|
||||
param.display_image.display_index = display_index;
|
||||
param.display_image.image_index = image_index;
|
||||
esl_notify_app(BLE_ESL_EVT_DISPLAY_IMAGE, ¶m);
|
||||
|
||||
ESP_LOGI(TAG, "Timed display executed: display=%u image=%u",
|
||||
display_index, image_index);
|
||||
}
|
||||
|
||||
/* ========================== Init / Deinit ========================== */
|
||||
|
||||
esp_err_t esl_cmd_display_init(void)
|
||||
{
|
||||
if (s_ctx != NULL) {
|
||||
ESP_LOGW(TAG, "Already initialized");
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
|
||||
const ble_esl_config_t *config = esl_get_config();
|
||||
if (config == NULL) {
|
||||
ESP_LOGE(TAG, "Config not available");
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
|
||||
/* Create the file-scope lock once (survives init/deinit cycles) */
|
||||
if (s_lock == NULL) {
|
||||
s_lock = xSemaphoreCreateMutex();
|
||||
if (s_lock == NULL) {
|
||||
ESP_LOGE(TAG, "Failed to create display module mutex");
|
||||
return ESP_ERR_NO_MEM;
|
||||
}
|
||||
}
|
||||
|
||||
s_ctx = calloc(1, sizeof(display_ctx_t));
|
||||
if (s_ctx == NULL) {
|
||||
ESP_LOGE(TAG, "Failed to allocate display context");
|
||||
return ESP_ERR_NO_MEM;
|
||||
}
|
||||
|
||||
s_ctx->num_displays = config->num_displays;
|
||||
s_ctx->num_images = config->num_images;
|
||||
s_ctx->num_sensors = config->num_sensors;
|
||||
|
||||
/* Allocate per-display state */
|
||||
if (s_ctx->num_displays > 0) {
|
||||
s_ctx->displays = calloc(s_ctx->num_displays, sizeof(display_state_t));
|
||||
if (s_ctx->displays == NULL) {
|
||||
ESP_LOGE(TAG, "Failed to allocate display state array");
|
||||
free(s_ctx);
|
||||
s_ctx = NULL;
|
||||
return ESP_ERR_NO_MEM;
|
||||
}
|
||||
|
||||
/* Create esp_timers for each display slot */
|
||||
for (uint8_t i = 0; i < s_ctx->num_displays; i++) {
|
||||
esp_timer_create_args_t timer_args = {
|
||||
.callback = timed_display_timer_cb,
|
||||
.arg = (void *)(uintptr_t)i,
|
||||
.dispatch_method = ESP_TIMER_TASK,
|
||||
.name = "esl_disp_timed",
|
||||
};
|
||||
esp_err_t ret = esp_timer_create(&timer_args, &s_ctx->displays[i].timer);
|
||||
if (ret != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to create timer for display %u: %s", i, esp_err_to_name(ret));
|
||||
/* Clean up previously created timers */
|
||||
for (uint8_t j = 0; j < i; j++) {
|
||||
esp_timer_delete(s_ctx->displays[j].timer);
|
||||
}
|
||||
free(s_ctx->displays);
|
||||
free(s_ctx);
|
||||
s_ctx = NULL;
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Allocate per-sensor state */
|
||||
if (s_ctx->num_sensors > 0) {
|
||||
s_ctx->sensors = calloc(s_ctx->num_sensors, sizeof(sensor_state_t));
|
||||
if (s_ctx->sensors == NULL) {
|
||||
ESP_LOGE(TAG, "Failed to allocate sensor state array");
|
||||
/* Clean up display timers */
|
||||
for (uint8_t i = 0; i < s_ctx->num_displays; i++) {
|
||||
esp_timer_delete(s_ctx->displays[i].timer);
|
||||
}
|
||||
free(s_ctx->displays);
|
||||
free(s_ctx);
|
||||
s_ctx = NULL;
|
||||
return ESP_ERR_NO_MEM;
|
||||
}
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Display/sensor module initialized: displays=%u images=%u sensors=%u",
|
||||
s_ctx->num_displays, s_ctx->num_images, s_ctx->num_sensors);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
void esl_cmd_display_deinit(void)
|
||||
{
|
||||
if (s_lock == NULL) {
|
||||
return;
|
||||
}
|
||||
|
||||
/* Acquire the file-scope lock before touching any s_ctx field so that the
|
||||
* timer stop/delete operations and the teardown of the state arrays are
|
||||
* serialised against concurrent callers (e.g., esl_get_display_pending,
|
||||
* esl_cmd_display_cancel_all, esl_cmd_display_timed_image) which also
|
||||
* operate on the same timer handles under this lock.
|
||||
* Holding the lock across esp_timer_delete() is safe: for ESP_TIMER_TASK
|
||||
* dispatch the delete is processed by the timer task, so it does not wait
|
||||
* inline for timed_display_timer_cb() (which itself takes s_lock). */
|
||||
xSemaphoreTake(s_lock, portMAX_DELAY);
|
||||
|
||||
if (s_ctx == NULL) {
|
||||
xSemaphoreGive(s_lock);
|
||||
return;
|
||||
}
|
||||
|
||||
/* Stop all display timers first to prevent new callback invocations.
|
||||
* esp_timer_stop only removes the timer from the armed list; an
|
||||
* already-dispatched callback may still be running — it will observe
|
||||
* s_ctx == NULL once it acquires the lock we hold here. */
|
||||
if (s_ctx->displays != NULL) {
|
||||
for (uint8_t i = 0; i < s_ctx->num_displays; i++) {
|
||||
if (s_ctx->displays[i].timer != NULL) {
|
||||
esp_timer_stop(s_ctx->displays[i].timer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Delete all display timers. */
|
||||
if (s_ctx->displays != NULL) {
|
||||
for (uint8_t i = 0; i < s_ctx->num_displays; i++) {
|
||||
if (s_ctx->displays[i].timer != NULL) {
|
||||
esp_timer_delete(s_ctx->displays[i].timer);
|
||||
s_ctx->displays[i].timer = NULL;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Free display state */
|
||||
if (s_ctx->displays != NULL) {
|
||||
free(s_ctx->displays);
|
||||
}
|
||||
|
||||
/* Free sensor state */
|
||||
if (s_ctx->sensors != NULL) {
|
||||
free(s_ctx->sensors);
|
||||
}
|
||||
|
||||
free(s_ctx);
|
||||
s_ctx = NULL;
|
||||
|
||||
xSemaphoreGive(s_lock);
|
||||
|
||||
/* s_lock is intentionally NOT deleted — it is a file-scope static that
|
||||
* survives init/deinit cycles so that late callers safely see s_ctx == NULL
|
||||
* under the lock instead of accessing a deleted semaphore. */
|
||||
|
||||
ESP_LOGI(TAG, "Display/sensor module deinitialized");
|
||||
}
|
||||
|
||||
/* ========================== Command Handlers ========================== */
|
||||
|
||||
/**
|
||||
* @brief Consume cached PAwR sensor data and build the ESL response.
|
||||
*
|
||||
* Caller must hold s_lock. If valid cached data is present it is consumed
|
||||
* (the cache is cleared) and either a Sensor Value or an Error response is
|
||||
* built into @p result.
|
||||
*
|
||||
* @return true if cached data was consumed and a response was built.
|
||||
*/
|
||||
static bool serve_cached_sensor_locked(sensor_state_t *sensor,
|
||||
uint8_t sensor_index,
|
||||
ble_esl_cmd_result_t *result)
|
||||
{
|
||||
if (!sensor->cache_valid) {
|
||||
return false;
|
||||
}
|
||||
|
||||
uint8_t cached_error = sensor->cache_error_code;
|
||||
uint8_t cached_data_len = sensor->cache_data_len;
|
||||
|
||||
/* Clear the cache */
|
||||
sensor->cache_valid = false;
|
||||
|
||||
if (cached_error == 0) {
|
||||
/* Build Sensor Value response */
|
||||
uint8_t resp_opcode = (uint8_t)((cached_data_len << 4) | BLE_ESL_RESP_SENSOR_VALUE_TAG);
|
||||
result->has_response = true;
|
||||
result->resp_opcode = resp_opcode;
|
||||
result->resp_params[0] = sensor_index;
|
||||
memcpy(&result->resp_params[1], sensor->cache_data, cached_data_len);
|
||||
result->resp_params_len = cached_data_len + 1;
|
||||
} else {
|
||||
/* Build Error response with cached error code */
|
||||
build_error_response(result, cached_error);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
esp_err_t esl_cmd_read_sensor(const uint8_t *params, uint8_t params_len,
|
||||
ble_esl_cmd_result_t *result, uint8_t transport)
|
||||
{
|
||||
assert(params != NULL);
|
||||
assert(result != NULL);
|
||||
|
||||
xSemaphoreTake(s_lock, portMAX_DELAY);
|
||||
|
||||
if (s_ctx == NULL) {
|
||||
ESP_LOGE(TAG, "Module not initialized");
|
||||
xSemaphoreGive(s_lock);
|
||||
build_error_response(result, BLE_ESL_ERR_UNSPECIFIED);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
/* params[0] = Sensor_Index */
|
||||
uint8_t sensor_index = params[0];
|
||||
|
||||
/* Validate Sensor_Index */
|
||||
if (sensor_index >= s_ctx->num_sensors) {
|
||||
ESP_LOGW(TAG, "Invalid sensor index: %u (max %u)", sensor_index, s_ctx->num_sensors);
|
||||
xSemaphoreGive(s_lock);
|
||||
build_error_response(result, BLE_ESL_ERR_INVALID_PARAMS);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
sensor_state_t *sensor = &s_ctx->sensors[sensor_index];
|
||||
|
||||
/* For PAwR transport, if cached sensor data from a previous read is
|
||||
* available, return it directly instead of issuing another Retry. */
|
||||
if (transport == ESL_TRANSPORT_PAWR && sensor->cache_valid) {
|
||||
uint8_t cached_error = sensor->cache_error_code;
|
||||
serve_cached_sensor_locked(sensor, sensor_index, result);
|
||||
xSemaphoreGive(s_lock);
|
||||
|
||||
ESP_LOGI(TAG, "Sensor read served from cache (PAwR): index=%u error=0x%02x",
|
||||
sensor_index, cached_error);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
/* Mark sensor read as pending with transport context */
|
||||
sensor->read_pending = true;
|
||||
sensor->transport_context = transport;
|
||||
|
||||
xSemaphoreGive(s_lock);
|
||||
|
||||
/* Fire BLE_ESL_EVT_SENSOR_READ callback outside the lock */
|
||||
ble_esl_cb_param_t cb_param;
|
||||
memset(&cb_param, 0, sizeof(cb_param));
|
||||
cb_param.sensor_read.sensor_index = sensor_index;
|
||||
esl_notify_app(BLE_ESL_EVT_SENSOR_READ, &cb_param);
|
||||
|
||||
if (transport == ESL_TRANSPORT_PAWR) {
|
||||
/* If the app reported the sensor data synchronously from within the
|
||||
* callback above, the cache is now valid — serve the Sensor Value in
|
||||
* this same response and avoid forcing the AP through a Retry +
|
||||
* re-poll round trip. */
|
||||
xSemaphoreTake(s_lock, portMAX_DELAY);
|
||||
/* Re-validate sensor_index: the module may have been deinitialised and
|
||||
* re-initialised with fewer sensors while the lock was released. */
|
||||
bool served = (s_ctx != NULL) && (sensor_index < s_ctx->num_sensors) &&
|
||||
serve_cached_sensor_locked(&s_ctx->sensors[sensor_index],
|
||||
sensor_index, result);
|
||||
xSemaphoreGive(s_lock);
|
||||
|
||||
if (served) {
|
||||
ESP_LOGI(TAG, "Sensor read served synchronously (PAwR): index=%u",
|
||||
sensor_index);
|
||||
} else {
|
||||
/* The read is still pending (app will report it later). Respond
|
||||
* with Retry so the AP re-issues the command in a subsequent
|
||||
* subevent, at which point the cached value is returned. */
|
||||
build_error_response(result, BLE_ESL_ERR_RETRY);
|
||||
ESP_LOGI(TAG, "Sensor read deferred (PAwR): index=%u, responding Retry",
|
||||
sensor_index);
|
||||
}
|
||||
} else {
|
||||
/* ECP transport: response is deferred — will be sent when app
|
||||
* calls ble_esl_report_sensor_data() */
|
||||
result->has_response = false;
|
||||
ESP_LOGI(TAG, "Sensor read requested: index=%u transport=ECP", sensor_index);
|
||||
}
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t esl_cmd_refresh_display(const uint8_t *params, uint8_t params_len,
|
||||
ble_esl_cmd_result_t *result)
|
||||
{
|
||||
assert(params != NULL);
|
||||
assert(result != NULL);
|
||||
|
||||
xSemaphoreTake(s_lock, portMAX_DELAY);
|
||||
|
||||
if (s_ctx == NULL) {
|
||||
ESP_LOGE(TAG, "Module not initialized");
|
||||
xSemaphoreGive(s_lock);
|
||||
build_error_response(result, BLE_ESL_ERR_UNSPECIFIED);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
/* params[0] = Display_Index */
|
||||
uint8_t display_index = params[0];
|
||||
|
||||
/* Validate Display_Index */
|
||||
if (display_index >= s_ctx->num_displays) {
|
||||
ESP_LOGW(TAG, "Invalid display index: %u (max %u)", display_index, s_ctx->num_displays);
|
||||
xSemaphoreGive(s_lock);
|
||||
build_error_response(result, BLE_ESL_ERR_INVALID_PARAMS);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
/* Check that an image is currently being displayed */
|
||||
display_state_t *disp = &s_ctx->displays[display_index];
|
||||
if (!disp->has_active_image) {
|
||||
ESP_LOGW(TAG, "No active image on display %u", display_index);
|
||||
xSemaphoreGive(s_lock);
|
||||
build_error_response(result, BLE_ESL_ERR_IMAGE_NOT_AVAILABLE);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
/* Save image index to local before releasing lock to avoid data race */
|
||||
uint8_t image_index = disp->current_image_index;
|
||||
|
||||
/* Build Display State response */
|
||||
build_display_state_response(result, display_index, image_index);
|
||||
|
||||
xSemaphoreGive(s_lock);
|
||||
|
||||
/* Fire BLE_ESL_EVT_REFRESH_DISPLAY callback outside the lock */
|
||||
ble_esl_cb_param_t cb_param;
|
||||
memset(&cb_param, 0, sizeof(cb_param));
|
||||
cb_param.refresh_display.display_index = display_index;
|
||||
esl_notify_app(BLE_ESL_EVT_REFRESH_DISPLAY, &cb_param);
|
||||
|
||||
ESP_LOGI(TAG, "Refresh display: index=%u image=%u", display_index, image_index);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t esl_cmd_display_image(const uint8_t *params, uint8_t params_len,
|
||||
ble_esl_cmd_result_t *result)
|
||||
{
|
||||
assert(params != NULL);
|
||||
assert(result != NULL);
|
||||
|
||||
xSemaphoreTake(s_lock, portMAX_DELAY);
|
||||
|
||||
if (s_ctx == NULL) {
|
||||
ESP_LOGE(TAG, "Module not initialized");
|
||||
xSemaphoreGive(s_lock);
|
||||
build_error_response(result, BLE_ESL_ERR_UNSPECIFIED);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
/* params[0] = Display_Index, params[1] = Image_Index */
|
||||
uint8_t display_index = params[0];
|
||||
uint8_t image_index = params[1];
|
||||
|
||||
/* Validate Display_Index */
|
||||
if (display_index >= s_ctx->num_displays) {
|
||||
ESP_LOGW(TAG, "Invalid display index: %u (max %u)", display_index, s_ctx->num_displays);
|
||||
xSemaphoreGive(s_lock);
|
||||
build_error_response(result, BLE_ESL_ERR_INVALID_PARAMS);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
/* Validate Image_Index range */
|
||||
if (image_index >= s_ctx->num_images) {
|
||||
ESP_LOGW(TAG, "Invalid image index: %u (max %u)", image_index, s_ctx->num_images);
|
||||
xSemaphoreGive(s_lock);
|
||||
build_error_response(result, BLE_ESL_ERR_INVALID_IMAGE_INDEX);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
/* Check image availability */
|
||||
if (!is_image_available(image_index)) {
|
||||
ESP_LOGW(TAG, "Image not available: index=%u", image_index);
|
||||
xSemaphoreGive(s_lock);
|
||||
build_error_response(result, BLE_ESL_ERR_IMAGE_NOT_AVAILABLE);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
/* Update internal state */
|
||||
display_state_t *disp = &s_ctx->displays[display_index];
|
||||
disp->has_active_image = true;
|
||||
disp->current_image_index = image_index;
|
||||
|
||||
/* Build Display State response */
|
||||
build_display_state_response(result, display_index, image_index);
|
||||
|
||||
xSemaphoreGive(s_lock);
|
||||
|
||||
/* Fire BLE_ESL_EVT_DISPLAY_IMAGE callback outside the lock */
|
||||
ble_esl_cb_param_t cb_param;
|
||||
memset(&cb_param, 0, sizeof(cb_param));
|
||||
cb_param.display_image.display_index = display_index;
|
||||
cb_param.display_image.image_index = image_index;
|
||||
esl_notify_app(BLE_ESL_EVT_DISPLAY_IMAGE, &cb_param);
|
||||
|
||||
ESP_LOGI(TAG, "Display image: display=%u image=%u", display_index, image_index);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t esl_cmd_display_timed_image(const uint8_t *params, uint8_t params_len,
|
||||
ble_esl_cmd_result_t *result)
|
||||
{
|
||||
assert(params != NULL);
|
||||
assert(result != NULL);
|
||||
|
||||
xSemaphoreTake(s_lock, portMAX_DELAY);
|
||||
|
||||
if (s_ctx == NULL) {
|
||||
ESP_LOGE(TAG, "Module not initialized");
|
||||
xSemaphoreGive(s_lock);
|
||||
build_error_response(result, BLE_ESL_ERR_UNSPECIFIED);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
/* params[0] = Display_Index, params[1] = Image_Index, params[2..5] = Absolute_Time (LE) */
|
||||
uint8_t display_index = params[0];
|
||||
uint8_t image_index = params[1];
|
||||
uint32_t absolute_time = (uint32_t)params[2] |
|
||||
((uint32_t)params[3] << 8) |
|
||||
((uint32_t)params[4] << 16) |
|
||||
((uint32_t)params[5] << 24);
|
||||
|
||||
/* Validate Display_Index */
|
||||
if (display_index >= s_ctx->num_displays) {
|
||||
ESP_LOGW(TAG, "Invalid display index: %u (max %u)", display_index, s_ctx->num_displays);
|
||||
xSemaphoreGive(s_lock);
|
||||
build_error_response(result, BLE_ESL_ERR_INVALID_PARAMS);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
/* Validate Image_Index range */
|
||||
if (image_index >= s_ctx->num_images) {
|
||||
ESP_LOGW(TAG, "Invalid image index: %u (max %u)", image_index, s_ctx->num_images);
|
||||
xSemaphoreGive(s_lock);
|
||||
build_error_response(result, BLE_ESL_ERR_INVALID_IMAGE_INDEX);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
/* Check image availability */
|
||||
if (!is_image_available(image_index)) {
|
||||
ESP_LOGW(TAG, "Image not available: index=%u", image_index);
|
||||
xSemaphoreGive(s_lock);
|
||||
build_error_response(result, BLE_ESL_ERR_IMAGE_NOT_AVAILABLE);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
display_state_t *disp = &s_ctx->displays[display_index];
|
||||
|
||||
/* Handle Absolute_Time = 0 (cancellation) */
|
||||
if (absolute_time == 0x00000000) {
|
||||
ESP_LOGI(TAG, "Cancelling timed display for display %u", display_index);
|
||||
if (disp->timed_pending) {
|
||||
esp_timer_stop(disp->timer);
|
||||
disp->timed_pending = false;
|
||||
}
|
||||
/* Send Display State response with command's Display_Index and Image_Index */
|
||||
build_display_state_response(result, display_index, image_index);
|
||||
xSemaphoreGive(s_lock);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
/* Validate Absolute_Time plausibility */
|
||||
uint32_t current_time = esl_get_abs_time();
|
||||
uint32_t delta;
|
||||
if (absolute_time >= current_time) {
|
||||
delta = absolute_time - current_time;
|
||||
} else {
|
||||
/* Wrap-around: time until wrap + time after wrap */
|
||||
delta = (0xFFFFFFFF - current_time) + absolute_time + 1;
|
||||
}
|
||||
|
||||
if (delta > BLE_ESL_IMPLAUSIBLE_TIME_MS) {
|
||||
ESP_LOGW(TAG, "Implausible absolute time: target=%lu current=%lu delta=%lu",
|
||||
(unsigned long)absolute_time, (unsigned long)current_time, (unsigned long)delta);
|
||||
xSemaphoreGive(s_lock);
|
||||
build_error_response(result, BLE_ESL_ERR_IMPLAUSIBLE_ABS_TIME);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
/* Handle pending timed command conflict */
|
||||
if (disp->timed_pending) {
|
||||
if (absolute_time == disp->timed_absolute_time) {
|
||||
/* Same time → replace the pending command (update Image_Index) */
|
||||
ESP_LOGI(TAG, "Replacing timed display for display %u: new image=%u",
|
||||
display_index, image_index);
|
||||
disp->timed_image_index = image_index;
|
||||
/* Timer is already running for the same time, no need to restart */
|
||||
build_display_state_response(result, display_index, image_index);
|
||||
xSemaphoreGive(s_lock);
|
||||
return ESP_OK;
|
||||
} else {
|
||||
/* Different time → Queue Full error */
|
||||
ESP_LOGW(TAG, "Queue full for display %u: pending_time=%lu new_time=%lu",
|
||||
display_index, (unsigned long)disp->timed_absolute_time,
|
||||
(unsigned long)absolute_time);
|
||||
xSemaphoreGive(s_lock);
|
||||
build_error_response(result, BLE_ESL_ERR_QUEUE_FULL);
|
||||
return ESP_OK;
|
||||
}
|
||||
}
|
||||
|
||||
/* Schedule timed execution */
|
||||
disp->timed_pending = true;
|
||||
disp->timed_image_index = image_index;
|
||||
disp->timed_absolute_time = absolute_time;
|
||||
|
||||
/* Convert delta from ms to us for esp_timer */
|
||||
uint64_t timeout_us = (uint64_t)delta * 1000ULL;
|
||||
if (timeout_us == 0) {
|
||||
/* If delta is 0 (target time is now), use a minimal timeout */
|
||||
timeout_us = 1;
|
||||
}
|
||||
|
||||
esp_err_t ret = esp_timer_start_once(disp->timer, timeout_us);
|
||||
if (ret != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to start timed display timer: %s", esp_err_to_name(ret));
|
||||
disp->timed_pending = false;
|
||||
xSemaphoreGive(s_lock);
|
||||
build_error_response(result, BLE_ESL_ERR_UNSPECIFIED);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Timed display scheduled: display=%u image=%u time=%lu delta=%lu ms",
|
||||
display_index, image_index, (unsigned long)absolute_time, (unsigned long)delta);
|
||||
|
||||
/* Send Display State response immediately */
|
||||
build_display_state_response(result, display_index, image_index);
|
||||
xSemaphoreGive(s_lock);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
/* ========================== Pending Display Update Flag ========================== */
|
||||
|
||||
bool esl_get_display_pending(void)
|
||||
{
|
||||
if (s_lock == NULL) {
|
||||
return false;
|
||||
}
|
||||
|
||||
xSemaphoreTake(s_lock, portMAX_DELAY);
|
||||
|
||||
if (s_ctx == NULL || s_ctx->displays == NULL) {
|
||||
xSemaphoreGive(s_lock);
|
||||
return false;
|
||||
}
|
||||
|
||||
bool pending = false;
|
||||
for (uint8_t i = 0; i < s_ctx->num_displays; i++) {
|
||||
if (s_ctx->displays[i].timed_pending) {
|
||||
pending = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
xSemaphoreGive(s_lock);
|
||||
return pending;
|
||||
}
|
||||
|
||||
/* ========================== Cancel All Timed Display Commands ========================== */
|
||||
|
||||
void esl_cmd_display_cancel_all(void)
|
||||
{
|
||||
if (s_lock == NULL) {
|
||||
return;
|
||||
}
|
||||
|
||||
xSemaphoreTake(s_lock, portMAX_DELAY);
|
||||
|
||||
if (s_ctx == NULL || s_ctx->displays == NULL) {
|
||||
xSemaphoreGive(s_lock);
|
||||
return;
|
||||
}
|
||||
|
||||
for (uint8_t i = 0; i < s_ctx->num_displays; i++) {
|
||||
if (s_ctx->displays[i].timed_pending) {
|
||||
esp_timer_stop(s_ctx->displays[i].timer);
|
||||
s_ctx->displays[i].timed_pending = false;
|
||||
ESP_LOGI(TAG, "Cancelled timed display for display %u", i);
|
||||
}
|
||||
}
|
||||
|
||||
xSemaphoreGive(s_lock);
|
||||
}
|
||||
|
||||
/* ========================== Sensor Data Report (Public API) ========================== */
|
||||
|
||||
esp_err_t ble_esl_report_sensor_data(uint8_t sensor_index, uint8_t error_code,
|
||||
const uint8_t *data, uint8_t data_len)
|
||||
{
|
||||
xSemaphoreTake(s_lock, portMAX_DELAY);
|
||||
|
||||
if (s_ctx == NULL) {
|
||||
ESP_LOGE(TAG, "Module not initialized");
|
||||
xSemaphoreGive(s_lock);
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
|
||||
/* Validate sensor_index */
|
||||
if (sensor_index >= s_ctx->num_sensors) {
|
||||
ESP_LOGE(TAG, "Invalid sensor index: %u", sensor_index);
|
||||
xSemaphoreGive(s_lock);
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
|
||||
/* Check that a read is pending for this sensor */
|
||||
sensor_state_t *sensor = &s_ctx->sensors[sensor_index];
|
||||
if (!sensor->read_pending) {
|
||||
ESP_LOGE(TAG, "No sensor read pending for index %u", sensor_index);
|
||||
xSemaphoreGive(s_lock);
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
|
||||
/* Validate data parameters when error_code is 0 (success) */
|
||||
if (error_code == 0) {
|
||||
if (data == NULL) {
|
||||
ESP_LOGE(TAG, "Sensor data pointer is NULL");
|
||||
xSemaphoreGive(s_lock);
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
if (data_len == 0 || data_len > SENSOR_DATA_CACHE_MAX_LEN) {
|
||||
ESP_LOGE(TAG, "Invalid sensor data length: %u (must be 1-%u)",
|
||||
data_len, SENSOR_DATA_CACHE_MAX_LEN);
|
||||
xSemaphoreGive(s_lock);
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t transport = sensor->transport_context;
|
||||
|
||||
/* Clear pending state */
|
||||
sensor->read_pending = false;
|
||||
|
||||
if (transport == ESL_TRANSPORT_PAWR) {
|
||||
/* PAwR transport: cache the sensor data so that when the AP
|
||||
* re-issues the Read Sensor Data command in a subsequent PAwR
|
||||
* subevent, the cached data can be returned directly instead
|
||||
* of another Retry. */
|
||||
sensor->cache_valid = true;
|
||||
sensor->cache_error_code = error_code;
|
||||
if (error_code == 0) {
|
||||
memcpy(sensor->cache_data, data, data_len);
|
||||
sensor->cache_data_len = data_len;
|
||||
} else {
|
||||
sensor->cache_data_len = 0;
|
||||
}
|
||||
|
||||
xSemaphoreGive(s_lock);
|
||||
|
||||
ESP_LOGI(TAG, "PAwR sensor data cached (index=%u error=0x%02x), awaiting AP re-poll",
|
||||
sensor_index, error_code);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
xSemaphoreGive(s_lock);
|
||||
|
||||
/* ECP transport: build and send the response immediately */
|
||||
uint8_t tlv_buf[BLE_ESL_TLV_MAX_SIZE];
|
||||
uint8_t tlv_len = 0;
|
||||
|
||||
if (error_code == 0) {
|
||||
/* Build Sensor Value response:
|
||||
* Opcode = (data_len << 4) | 0x0E
|
||||
* Params = [sensor_index, data[0], data[1], ..., data[data_len-1]]
|
||||
*/
|
||||
uint8_t resp_opcode = (uint8_t)((data_len << 4) | BLE_ESL_RESP_SENSOR_VALUE_TAG);
|
||||
uint8_t resp_params[16]; /* max 1 + 15 = 16 */
|
||||
resp_params[0] = sensor_index;
|
||||
memcpy(&resp_params[1], data, data_len);
|
||||
uint8_t resp_params_len = data_len + 1;
|
||||
|
||||
esp_err_t ret = ble_esl_tlv_encode(resp_opcode, resp_params, resp_params_len,
|
||||
tlv_buf, &tlv_len);
|
||||
if (ret != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to encode sensor value TLV: %s", esp_err_to_name(ret));
|
||||
restore_sensor_pending(sensor_index, transport);
|
||||
return ret;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Sensor value response: index=%u data_len=%u", sensor_index, data_len);
|
||||
} else {
|
||||
/* Build Error response: opcode 0x00, params = [error_code] */
|
||||
uint8_t resp_params[1] = { error_code };
|
||||
esp_err_t ret = ble_esl_tlv_encode(BLE_ESL_RESP_ERROR, resp_params, 1,
|
||||
tlv_buf, &tlv_len);
|
||||
if (ret != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to encode error TLV: %s", esp_err_to_name(ret));
|
||||
restore_sensor_pending(sensor_index, transport);
|
||||
return ret;
|
||||
}
|
||||
|
||||
ESP_LOGW(TAG, "Sensor error response: index=%u error=0x%02x", sensor_index, error_code);
|
||||
}
|
||||
|
||||
/* Send response via ECP */
|
||||
esp_err_t ret = esl_send_ecp_response(tlv_buf, tlv_len);
|
||||
if (ret != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to send ECP sensor response: %s", esp_err_to_name(ret));
|
||||
restore_sensor_pending(sensor_index, transport);
|
||||
return ret;
|
||||
}
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
850
components/bt/ble_profiles/nimble/ble_esl/src/esl/esl_cmd_led.c
Normal file
850
components/bt/ble_profiles/nimble/ble_esl/src/esl/esl_cmd_led.c
Normal file
@@ -0,0 +1,850 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
/**
|
||||
* @file esl_cmd_led.c
|
||||
* @brief ESL LED Control, LED Timed Control, and Vendor-Specific command handlers
|
||||
*
|
||||
* Implements: LED Control (opcode 0xB0), LED Timed Control (opcode 0xF0),
|
||||
* Vendor-specific tag (opcodes 0x_F), LED status query for Basic State bitmap,
|
||||
* pattern expiry via esp_timer, and timed LED scheduling.
|
||||
*/
|
||||
|
||||
#include <string.h>
|
||||
#include <inttypes.h>
|
||||
#include "esp_log.h"
|
||||
#include "esp_timer.h"
|
||||
#include "ble_esl.h"
|
||||
#include "ble_esl_common.h"
|
||||
#include "ble_esl_int.h"
|
||||
|
||||
static const char *TAG = "esl_cmd_led";
|
||||
|
||||
/* ========================================================================== */
|
||||
/* Per-LED Internal State */
|
||||
/* ========================================================================== */
|
||||
|
||||
/** @brief Stored LED control settings for timed commands */
|
||||
typedef struct {
|
||||
ble_esl_led_control_t settings; /*!< Raw LED control settings from command */
|
||||
uint32_t absolute_time; /*!< Target absolute time for execution */
|
||||
} esl_led_timed_params_t;
|
||||
|
||||
/** @brief Per-LED state tracking */
|
||||
typedef struct {
|
||||
bool active; /*!< LED is currently executing a command */
|
||||
bool timed_pending; /*!< A timed LED command is pending */
|
||||
esl_led_timed_params_t timed_params;/*!< Stored timed command parameters */
|
||||
ble_esl_led_control_evt_param_t current_params; /*!< Current active LED parameters */
|
||||
esp_timer_handle_t pattern_timer; /*!< Timer for pattern expiry */
|
||||
esp_timer_handle_t timed_timer; /*!< Timer for timed command execution */
|
||||
} esl_led_state_t;
|
||||
|
||||
/* ========================================================================== */
|
||||
/* Module Context */
|
||||
/* ========================================================================== */
|
||||
|
||||
/** @brief Aggregate internal state for the LED command module */
|
||||
typedef struct {
|
||||
esl_led_state_t leds[CONFIG_BLE_ESL_MAX_LEDS]; /*!< Per-LED state array */
|
||||
bool initialized; /*!< Module initialized flag */
|
||||
} esl_cmd_led_ctx_t;
|
||||
|
||||
static esl_cmd_led_ctx_t *s_ctx = NULL;
|
||||
|
||||
/* ========================================================================== */
|
||||
/* Forward Declarations */
|
||||
/* ========================================================================== */
|
||||
|
||||
static void pattern_expiry_timer_cb(void *arg);
|
||||
static void timed_led_timer_cb(void *arg);
|
||||
static void build_led_state_response(uint8_t led_index, ble_esl_cmd_result_t *result);
|
||||
static void build_error_response(uint8_t error_code, ble_esl_cmd_result_t *result);
|
||||
static void parse_led_control_settings(uint8_t led_index,
|
||||
const ble_esl_led_control_t *settings,
|
||||
ble_esl_led_control_evt_param_t *out_params);
|
||||
static void apply_led_command(uint8_t led_index, const ble_esl_led_control_evt_param_t *params);
|
||||
static uint64_t compute_pattern_duration_us(const ble_esl_led_control_evt_param_t *params);
|
||||
|
||||
/* ========================================================================== */
|
||||
/* Helper: Build LED State Response */
|
||||
/* ========================================================================== */
|
||||
|
||||
/**
|
||||
* @brief Build an LED State response (opcode 0x01) into the result struct
|
||||
*
|
||||
* @param led_index LED index to include in the response
|
||||
* @param result Output result
|
||||
*/
|
||||
static void build_led_state_response(uint8_t led_index, ble_esl_cmd_result_t *result)
|
||||
{
|
||||
result->has_response = true;
|
||||
result->resp_opcode = BLE_ESL_RESP_LED_STATE;
|
||||
result->resp_params[0] = led_index;
|
||||
result->resp_params_len = 1;
|
||||
}
|
||||
|
||||
/* ========================================================================== */
|
||||
/* Helper: Build Error Response */
|
||||
/* ========================================================================== */
|
||||
|
||||
/**
|
||||
* @brief Build an Error response (opcode 0x00) into the result struct
|
||||
*
|
||||
* @param error_code ESL error code
|
||||
* @param result Output result
|
||||
*/
|
||||
static void build_error_response(uint8_t error_code, ble_esl_cmd_result_t *result)
|
||||
{
|
||||
result->has_response = true;
|
||||
result->resp_opcode = BLE_ESL_RESP_ERROR;
|
||||
result->resp_params[0] = error_code;
|
||||
result->resp_params_len = 1;
|
||||
}
|
||||
|
||||
/* ========================================================================== */
|
||||
/* Helper: Parse LED Control Settings */
|
||||
/* ========================================================================== */
|
||||
|
||||
/**
|
||||
* @brief Parse raw LED control settings into the application-facing params struct
|
||||
*
|
||||
* @param led_index LED index
|
||||
* @param settings Raw LED control settings from the command
|
||||
* @param out_params Output parsed parameters
|
||||
*/
|
||||
static void parse_led_control_settings(uint8_t led_index,
|
||||
const ble_esl_led_control_t *settings,
|
||||
ble_esl_led_control_evt_param_t *out_params)
|
||||
{
|
||||
memset(out_params, 0, sizeof(*out_params));
|
||||
|
||||
out_params->led_index = led_index;
|
||||
|
||||
/* Parse color and brightness from color_brightness byte */
|
||||
uint8_t cb = settings->color_brightness;
|
||||
out_params->color_red = BLE_ESL_LED_CTRL_RED(cb);
|
||||
out_params->color_green = BLE_ESL_LED_CTRL_GREEN(cb);
|
||||
out_params->color_blue = BLE_ESL_LED_CTRL_BLUE(cb);
|
||||
out_params->brightness = BLE_ESL_LED_CTRL_BRIGHTNESS(cb);
|
||||
|
||||
/* Parse flashing pattern: 40-bit pattern stored in 5 bytes (little-endian) */
|
||||
uint64_t pattern = 0;
|
||||
for (int i = 0; i < 5; i++) {
|
||||
pattern |= (uint64_t)settings->flashing.pattern[i] << (8 * i);
|
||||
}
|
||||
out_params->pattern = pattern;
|
||||
|
||||
out_params->bit_off_period = settings->flashing.bit_off_period;
|
||||
out_params->bit_on_period = settings->flashing.bit_on_period;
|
||||
|
||||
/* Parse repeat field */
|
||||
uint16_t repeat = settings->repeat;
|
||||
out_params->repeat_type = BLE_ESL_LED_CTRL_REPEAT_TYPE(repeat);
|
||||
out_params->repeats_duration = BLE_ESL_LED_CTRL_REPEATS_DURATION(repeat);
|
||||
|
||||
out_params->is_off = false;
|
||||
}
|
||||
|
||||
/* ========================================================================== */
|
||||
/* Helper: Compute Pattern Duration in Microseconds */
|
||||
/* ========================================================================== */
|
||||
|
||||
/**
|
||||
* @brief Compute the total duration of the LED pattern for timer arming
|
||||
*
|
||||
* For repeat_type=0 (count): estimates total time based on pattern bits and repeat count.
|
||||
* For repeat_type=1 (duration): uses the duration in seconds directly.
|
||||
*
|
||||
* @param params Parsed LED control parameters
|
||||
* @return Duration in microseconds, or 0 if continuous/off
|
||||
*/
|
||||
static uint64_t compute_pattern_duration_us(const ble_esl_led_control_evt_param_t *params)
|
||||
{
|
||||
if (params->repeats_duration == 0) {
|
||||
/* Special cases: continuous on or off — no finite duration */
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (params->repeat_type == 1) {
|
||||
/* Duration mode: repeats_duration is in 1-second increments */
|
||||
return (uint64_t)params->repeats_duration * 1000000ULL;
|
||||
}
|
||||
|
||||
/* Count mode: estimate total time from pattern bits and periods */
|
||||
/* Find the meaningful pattern length (from first '1' bit to last bit) */
|
||||
uint64_t pat = params->pattern;
|
||||
if (pat == 0) {
|
||||
/* No pattern bits set — treat as immediate off after count */
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Count meaningful bits: from MSB first '1' to LSB */
|
||||
int first_one = -1;
|
||||
for (int i = 39; i >= 0; i--) {
|
||||
if (pat & (1ULL << i)) {
|
||||
first_one = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (first_one < 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Count on-bits and off-bits in the meaningful range */
|
||||
int on_bits = 0;
|
||||
int off_bits = 0;
|
||||
for (int i = first_one; i >= 0; i--) {
|
||||
if (pat & (1ULL << i)) {
|
||||
on_bits++;
|
||||
} else {
|
||||
off_bits++;
|
||||
}
|
||||
}
|
||||
|
||||
/* Time per pattern cycle in ms */
|
||||
uint32_t cycle_ms = (uint32_t)on_bits * (uint32_t)params->bit_on_period * 2 +
|
||||
(uint32_t)off_bits * (uint32_t)params->bit_off_period * 2;
|
||||
|
||||
/* Total time = cycle_ms * repeats_duration (count) */
|
||||
uint64_t total_ms = (uint64_t)cycle_ms * (uint64_t)params->repeats_duration;
|
||||
|
||||
return total_ms * 1000ULL; /* Convert to microseconds */
|
||||
}
|
||||
|
||||
/* ========================================================================== */
|
||||
/* Helper: Apply LED Command */
|
||||
/* ========================================================================== */
|
||||
|
||||
/**
|
||||
* @brief Apply an LED command: update state, fire callback, arm pattern timer
|
||||
*
|
||||
* @param led_index LED index
|
||||
* @param params Parsed LED control parameters
|
||||
*/
|
||||
static void apply_led_command(uint8_t led_index, const ble_esl_led_control_evt_param_t *params)
|
||||
{
|
||||
assert(led_index < CONFIG_BLE_ESL_MAX_LEDS);
|
||||
esl_led_state_t *led = &s_ctx->leds[led_index];
|
||||
|
||||
/* Stop any running pattern expiry timer (superseding) */
|
||||
esp_timer_stop(led->pattern_timer);
|
||||
|
||||
/* Store current parameters */
|
||||
memcpy(&led->current_params, params, sizeof(ble_esl_led_control_evt_param_t));
|
||||
|
||||
/* Determine special cases based on repeat_type and repeats_duration */
|
||||
if (params->repeat_type == 0 && params->repeats_duration == 0) {
|
||||
/* Continuous off: LED should be turned off */
|
||||
led->active = false;
|
||||
led->current_params.is_off = true;
|
||||
|
||||
/* Fire callback with is_off = true */
|
||||
ble_esl_cb_param_t cb_param;
|
||||
memset(&cb_param, 0, sizeof(cb_param));
|
||||
memcpy(&cb_param.led_control, &led->current_params, sizeof(ble_esl_led_control_evt_param_t));
|
||||
cb_param.led_control.is_off = true;
|
||||
esl_notify_app(BLE_ESL_EVT_LED_CONTROL, &cb_param);
|
||||
|
||||
ESP_LOGI(TAG, "LED %u: turned off (repeat_type=0, duration=0)", led_index);
|
||||
return;
|
||||
}
|
||||
|
||||
if (params->repeat_type == 1 && params->repeats_duration == 0) {
|
||||
/* Continuous on: LED illuminated steadily, no pattern timer */
|
||||
led->active = true;
|
||||
|
||||
/* Fire callback — pattern is ignored, steady illumination */
|
||||
ble_esl_cb_param_t cb_param;
|
||||
memset(&cb_param, 0, sizeof(cb_param));
|
||||
memcpy(&cb_param.led_control, &led->current_params, sizeof(ble_esl_led_control_evt_param_t));
|
||||
esl_notify_app(BLE_ESL_EVT_LED_CONTROL, &cb_param);
|
||||
|
||||
ESP_LOGI(TAG, "LED %u: continuous on (repeat_type=1, duration=0)", led_index);
|
||||
return;
|
||||
}
|
||||
|
||||
/* Normal case: finite pattern with count or duration */
|
||||
|
||||
/* Compute pattern duration before committing state */
|
||||
uint64_t duration_us = compute_pattern_duration_us(params);
|
||||
|
||||
if (duration_us == 0) {
|
||||
/* Zero duration (e.g. all-zero pattern): treat as immediate off */
|
||||
led->active = false;
|
||||
led->current_params.is_off = true;
|
||||
|
||||
ble_esl_cb_param_t cb_param;
|
||||
memset(&cb_param, 0, sizeof(cb_param));
|
||||
memcpy(&cb_param.led_control, &led->current_params, sizeof(ble_esl_led_control_evt_param_t));
|
||||
cb_param.led_control.is_off = true;
|
||||
esl_notify_app(BLE_ESL_EVT_LED_CONTROL, &cb_param);
|
||||
|
||||
ESP_LOGI(TAG, "LED %u: zero-duration pattern, immediately off", led_index);
|
||||
return;
|
||||
}
|
||||
|
||||
led->active = true;
|
||||
|
||||
/* Fire callback with LED parameters */
|
||||
ble_esl_cb_param_t cb_param;
|
||||
memset(&cb_param, 0, sizeof(cb_param));
|
||||
memcpy(&cb_param.led_control, &led->current_params, sizeof(ble_esl_led_control_evt_param_t));
|
||||
esl_notify_app(BLE_ESL_EVT_LED_CONTROL, &cb_param);
|
||||
|
||||
/* Arm pattern expiry timer */
|
||||
{
|
||||
esp_err_t ret = esp_timer_start_once(led->pattern_timer, duration_us);
|
||||
if (ret != ESP_OK) {
|
||||
ESP_LOGE(TAG, "LED %u: failed to start pattern timer: %s",
|
||||
led_index, esp_err_to_name(ret));
|
||||
|
||||
/* Roll back: mark LED inactive and notify app to turn off */
|
||||
led->active = false;
|
||||
led->current_params.is_off = true;
|
||||
|
||||
ble_esl_cb_param_t off_param;
|
||||
memset(&off_param, 0, sizeof(off_param));
|
||||
memcpy(&off_param.led_control, &led->current_params, sizeof(ble_esl_led_control_evt_param_t));
|
||||
off_param.led_control.is_off = true;
|
||||
esl_notify_app(BLE_ESL_EVT_LED_CONTROL, &off_param);
|
||||
} else {
|
||||
ESP_LOGI(TAG, "LED %u: pattern timer armed for %" PRIu64 " us",
|
||||
led_index, duration_us);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ========================================================================== */
|
||||
/* Pattern Expiry Timer Callback */
|
||||
/* ========================================================================== */
|
||||
|
||||
/**
|
||||
* @brief esp_timer callback for pattern expiry
|
||||
*
|
||||
* Sets the LED inactive, fires BLE_ESL_EVT_LED_CONTROL with is_off=true,
|
||||
* and updates the active flag.
|
||||
*
|
||||
* @param arg LED index (cast from uintptr_t)
|
||||
*/
|
||||
static void pattern_expiry_timer_cb(void *arg)
|
||||
{
|
||||
uint8_t led_index = (uint8_t)(uintptr_t)arg;
|
||||
|
||||
if (s_ctx == NULL) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (led_index >= CONFIG_BLE_ESL_MAX_LEDS) {
|
||||
ESP_LOGE(TAG, "Pattern expiry callback: invalid led_index %u", led_index);
|
||||
return;
|
||||
}
|
||||
|
||||
esl_led_state_t *led = &s_ctx->leds[led_index];
|
||||
|
||||
/* Mark LED as inactive */
|
||||
led->active = false;
|
||||
|
||||
ESP_LOGI(TAG, "LED %u: pattern expired, turning off", led_index);
|
||||
|
||||
/* Fire callback with is_off = true */
|
||||
ble_esl_cb_param_t cb_param;
|
||||
memset(&cb_param, 0, sizeof(cb_param));
|
||||
memcpy(&cb_param.led_control, &led->current_params, sizeof(ble_esl_led_control_evt_param_t));
|
||||
cb_param.led_control.is_off = true;
|
||||
esl_notify_app(BLE_ESL_EVT_LED_CONTROL, &cb_param);
|
||||
}
|
||||
|
||||
/* ========================================================================== */
|
||||
/* Timed LED Timer Callback */
|
||||
/* ========================================================================== */
|
||||
|
||||
/**
|
||||
* @brief esp_timer callback for timed LED command execution
|
||||
*
|
||||
* When the scheduled Absolute_Time arrives, parses the stored parameters,
|
||||
* applies the LED state, fires the callback, and arms the pattern expiry
|
||||
* timer if the duration is finite.
|
||||
*
|
||||
* @param arg LED index (cast from uintptr_t)
|
||||
*/
|
||||
static void timed_led_timer_cb(void *arg)
|
||||
{
|
||||
uint8_t led_index = (uint8_t)(uintptr_t)arg;
|
||||
|
||||
if (s_ctx == NULL) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (led_index >= CONFIG_BLE_ESL_MAX_LEDS) {
|
||||
ESP_LOGE(TAG, "Timed LED callback: invalid led_index %u", led_index);
|
||||
return;
|
||||
}
|
||||
|
||||
esl_led_state_t *led = &s_ctx->leds[led_index];
|
||||
|
||||
if (!led->timed_pending) {
|
||||
ESP_LOGW(TAG, "Timed LED callback: no pending command for LED %u", led_index);
|
||||
return;
|
||||
}
|
||||
|
||||
/* Clear pending state */
|
||||
led->timed_pending = false;
|
||||
|
||||
ESP_LOGI(TAG, "Timed LED executed: LED %u", led_index);
|
||||
|
||||
/* Parse the stored settings */
|
||||
ble_esl_led_control_evt_param_t params;
|
||||
parse_led_control_settings(led_index, &led->timed_params.settings, ¶ms);
|
||||
|
||||
/* Apply the LED command (handles special cases, fires callback, arms pattern timer) */
|
||||
apply_led_command(led_index, ¶ms);
|
||||
}
|
||||
|
||||
/* ========================================================================== */
|
||||
/* Command Handler: LED Control (opcode 0xB0) */
|
||||
/* ========================================================================== */
|
||||
|
||||
esp_err_t esl_cmd_led_control(const uint8_t *params, uint8_t params_len,
|
||||
ble_esl_cmd_result_t *result)
|
||||
{
|
||||
assert(s_ctx != NULL);
|
||||
assert(params != NULL);
|
||||
assert(result != NULL);
|
||||
|
||||
memset(result, 0, sizeof(*result));
|
||||
|
||||
/*
|
||||
* LED Control command parameters (after ESL_ID, which is already stripped
|
||||
* by the dispatch layer):
|
||||
* [LED_Index(1), LED_Control_Settings(10)]
|
||||
* LED_Control_Settings = color_brightness(1) + flashing_pattern(7) + repeat(2)
|
||||
* Total params_len expected: 1 + 10 = 11
|
||||
*/
|
||||
if (params_len < 1 + (uint8_t)sizeof(ble_esl_led_control_t)) {
|
||||
ESP_LOGE(TAG, "LED Control: insufficient params (len=%u, need %u)",
|
||||
params_len, (unsigned)(1 + sizeof(ble_esl_led_control_t)));
|
||||
build_error_response(BLE_ESL_ERR_INVALID_PARAMS, result);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
uint8_t led_index = params[0];
|
||||
|
||||
/* Validate LED_Index */
|
||||
const ble_esl_config_t *config = esl_get_config();
|
||||
if (config == NULL) {
|
||||
ESP_LOGE(TAG, "LED Control: config not available");
|
||||
build_error_response(BLE_ESL_ERR_UNSPECIFIED, result);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
if (led_index >= CONFIG_BLE_ESL_MAX_LEDS || led_index >= config->num_leds) {
|
||||
ESP_LOGW(TAG, "LED Control: invalid led_index %u (max=%d, num_leds=%u)",
|
||||
led_index, CONFIG_BLE_ESL_MAX_LEDS, config->num_leds);
|
||||
build_error_response(BLE_ESL_ERR_INVALID_PARAMS, result);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
/* Parse LED Control Settings */
|
||||
const ble_esl_led_control_t *settings =
|
||||
(const ble_esl_led_control_t *)¶ms[1];
|
||||
|
||||
/* Parse into application-facing params */
|
||||
ble_esl_led_control_evt_param_t led_params;
|
||||
parse_led_control_settings(led_index, settings, &led_params);
|
||||
|
||||
/* Validate Bit_On_Period / Bit_Off_Period when flashing is required */
|
||||
if (led_params.repeats_duration > 0) {
|
||||
if (led_params.bit_on_period == 0 || led_params.bit_off_period == 0) {
|
||||
ESP_LOGW(TAG, "LED Control: invalid bit_on/off_period (on=%u, off=%u) "
|
||||
"with repeats_duration=%u",
|
||||
led_params.bit_on_period, led_params.bit_off_period,
|
||||
led_params.repeats_duration);
|
||||
build_error_response(BLE_ESL_ERR_INVALID_PARAMS, result);
|
||||
return ESP_OK;
|
||||
}
|
||||
}
|
||||
|
||||
/* Apply the LED command */
|
||||
apply_led_command(led_index, &led_params);
|
||||
|
||||
/* Build LED State response */
|
||||
build_led_state_response(led_index, result);
|
||||
|
||||
ESP_LOGI(TAG, "LED Control: led=%u, R=%u G=%u B=%u, bright=%u, "
|
||||
"repeat_type=%u, duration=%u",
|
||||
led_index, led_params.color_red, led_params.color_green,
|
||||
led_params.color_blue, led_params.brightness,
|
||||
led_params.repeat_type, led_params.repeats_duration);
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
/* ========================================================================== */
|
||||
/* Command Handler: LED Timed Control (opcode 0xF0) */
|
||||
/* ========================================================================== */
|
||||
|
||||
esp_err_t esl_cmd_led_timed_control(const uint8_t *params, uint8_t params_len,
|
||||
ble_esl_cmd_result_t *result)
|
||||
{
|
||||
assert(s_ctx != NULL);
|
||||
assert(params != NULL);
|
||||
assert(result != NULL);
|
||||
|
||||
memset(result, 0, sizeof(*result));
|
||||
|
||||
/*
|
||||
* LED Timed Control command parameters (after ESL_ID):
|
||||
* [LED_Index(1), LED_Control_Settings(10), Absolute_Time(4 LE)]
|
||||
* Total params_len expected: 1 + 10 + 4 = 15
|
||||
*/
|
||||
uint8_t expected_len = 1 + (uint8_t)sizeof(ble_esl_led_control_t) + 4;
|
||||
if (params_len < expected_len) {
|
||||
ESP_LOGE(TAG, "LED Timed Control: insufficient params (len=%u, need %u)",
|
||||
params_len, expected_len);
|
||||
build_error_response(BLE_ESL_ERR_INVALID_PARAMS, result);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
uint8_t led_index = params[0];
|
||||
|
||||
/* Validate LED_Index */
|
||||
const ble_esl_config_t *config = esl_get_config();
|
||||
if (config == NULL) {
|
||||
ESP_LOGE(TAG, "LED Timed Control: config not available");
|
||||
build_error_response(BLE_ESL_ERR_UNSPECIFIED, result);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
if (led_index >= CONFIG_BLE_ESL_MAX_LEDS || led_index >= config->num_leds) {
|
||||
ESP_LOGW(TAG, "LED Timed Control: invalid led_index %u (max=%d, num_leds=%u)",
|
||||
led_index, CONFIG_BLE_ESL_MAX_LEDS, config->num_leds);
|
||||
build_error_response(BLE_ESL_ERR_INVALID_PARAMS, result);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
/* Parse LED Control Settings */
|
||||
const ble_esl_led_control_t *settings =
|
||||
(const ble_esl_led_control_t *)¶ms[1];
|
||||
|
||||
/* Validate Bit_On_Period / Bit_Off_Period when flashing is required */
|
||||
{
|
||||
ble_esl_led_control_evt_param_t tmp_params;
|
||||
parse_led_control_settings(led_index, settings, &tmp_params);
|
||||
if (tmp_params.repeats_duration > 0) {
|
||||
if (tmp_params.bit_on_period == 0 || tmp_params.bit_off_period == 0) {
|
||||
ESP_LOGW(TAG, "LED Timed Control: invalid bit_on/off_period (on=%u, off=%u) "
|
||||
"with repeats_duration=%u",
|
||||
tmp_params.bit_on_period, tmp_params.bit_off_period,
|
||||
tmp_params.repeats_duration);
|
||||
build_error_response(BLE_ESL_ERR_INVALID_PARAMS, result);
|
||||
return ESP_OK;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Parse Absolute_Time (little-endian, 4 bytes after LED_Control_Settings) */
|
||||
uint8_t abs_time_offset = 1 + (uint8_t)sizeof(ble_esl_led_control_t);
|
||||
uint32_t absolute_time = (uint32_t)params[abs_time_offset] |
|
||||
((uint32_t)params[abs_time_offset + 1] << 8) |
|
||||
((uint32_t)params[abs_time_offset + 2] << 16) |
|
||||
((uint32_t)params[abs_time_offset + 3] << 24);
|
||||
|
||||
esl_led_state_t *led = &s_ctx->leds[led_index];
|
||||
|
||||
/* Handle Absolute_Time = 0 (cancellation) */
|
||||
if (absolute_time == 0x00000000) {
|
||||
if (led->timed_pending) {
|
||||
/* Stop the timed timer */
|
||||
esp_err_t ret = esp_timer_stop(led->timed_timer);
|
||||
if (ret != ESP_OK && ret != ESP_ERR_INVALID_STATE) {
|
||||
ESP_LOGW(TAG, "Failed to stop timed LED timer: %s", esp_err_to_name(ret));
|
||||
}
|
||||
led->timed_pending = false;
|
||||
ESP_LOGI(TAG, "LED Timed Control: cancelled for LED %u", led_index);
|
||||
}
|
||||
/* Respond with LED State response confirming deletion */
|
||||
build_led_state_response(led_index, result);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
/* Validate Absolute_Time plausibility */
|
||||
uint32_t current_time = esl_get_abs_time();
|
||||
uint32_t delta;
|
||||
if (absolute_time >= current_time) {
|
||||
delta = absolute_time - current_time;
|
||||
} else {
|
||||
/* Time wraps around */
|
||||
delta = (0xFFFFFFFFUL - current_time) + absolute_time + 1;
|
||||
}
|
||||
|
||||
if (delta > BLE_ESL_IMPLAUSIBLE_TIME_MS) {
|
||||
ESP_LOGW(TAG, "LED Timed Control: implausible abs_time=0x%08" PRIx32
|
||||
" (delta=%" PRIu32 " ms)", absolute_time, delta);
|
||||
build_error_response(BLE_ESL_ERR_IMPLAUSIBLE_ABS_TIME, result);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
/* Handle pending timed command conflict */
|
||||
if (led->timed_pending) {
|
||||
if (led->timed_params.absolute_time == absolute_time) {
|
||||
/* Same time: replace the pending command */
|
||||
memcpy(&led->timed_params.settings, settings, sizeof(ble_esl_led_control_t));
|
||||
ESP_LOGI(TAG, "LED Timed Control: replaced pending for LED %u", led_index);
|
||||
} else {
|
||||
/* Different time and non-zero: reject with Queue Full */
|
||||
ESP_LOGW(TAG, "LED Timed Control: queue full for LED %u", led_index);
|
||||
build_error_response(BLE_ESL_ERR_QUEUE_FULL, result);
|
||||
return ESP_OK;
|
||||
}
|
||||
} else {
|
||||
/* No pending command: accept and schedule */
|
||||
led->timed_pending = true;
|
||||
memcpy(&led->timed_params.settings, settings, sizeof(ble_esl_led_control_t));
|
||||
led->timed_params.absolute_time = absolute_time;
|
||||
|
||||
/* Schedule the timed timer */
|
||||
uint64_t timeout_us = (uint64_t)delta * 1000ULL;
|
||||
|
||||
/* Stop timer if it happens to be running (safety) */
|
||||
esp_timer_stop(led->timed_timer);
|
||||
|
||||
esp_err_t ret = esp_timer_start_once(led->timed_timer, timeout_us);
|
||||
if (ret != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to start timed LED timer: %s", esp_err_to_name(ret));
|
||||
led->timed_pending = false;
|
||||
build_error_response(BLE_ESL_ERR_INSUFFICIENT_RESOURCES, result);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "LED Timed Control: scheduled LED=%u, abs_time=0x%08" PRIx32
|
||||
" (delta=%" PRIu32 " ms)", led_index, absolute_time, delta);
|
||||
}
|
||||
|
||||
/* Respond immediately with LED State response */
|
||||
build_led_state_response(led_index, result);
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
/* ========================================================================== */
|
||||
/* Command Handler: Vendor-Specific (opcodes 0x_F) */
|
||||
/* ========================================================================== */
|
||||
|
||||
esp_err_t esl_cmd_vendor_specific(uint8_t opcode, const uint8_t *params,
|
||||
uint8_t params_len, ble_esl_cmd_result_t *result)
|
||||
{
|
||||
assert(result != NULL);
|
||||
|
||||
memset(result, 0, sizeof(*result));
|
||||
|
||||
(void)params;
|
||||
(void)params_len;
|
||||
|
||||
#if CONFIG_BLE_ESL_VENDOR_SPECIFIC
|
||||
/*
|
||||
* Vendor-specific support is enabled, but no vendor handlers are registered
|
||||
* in this base implementation. Return Invalid Opcode.
|
||||
*/
|
||||
ESP_LOGW(TAG, "Vendor-specific opcode 0x%02X: no handler registered", opcode);
|
||||
build_error_response(BLE_ESL_ERR_INVALID_OPCODE, result);
|
||||
#else
|
||||
/* Vendor-specific support disabled: return Invalid Opcode */
|
||||
ESP_LOGW(TAG, "Vendor-specific opcode 0x%02X: vendor support disabled", opcode);
|
||||
build_error_response(BLE_ESL_ERR_INVALID_OPCODE, result);
|
||||
#endif /* CONFIG_BLE_ESL_VENDOR_SPECIFIC */
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
/* ========================================================================== */
|
||||
/* Internal Interface: esl_get_led_status() */
|
||||
/* ========================================================================== */
|
||||
|
||||
esl_led_status_t esl_get_led_status(void)
|
||||
{
|
||||
esl_led_status_t status = {
|
||||
.active_led = false,
|
||||
.pending_led_update = false,
|
||||
};
|
||||
|
||||
if (s_ctx == NULL) {
|
||||
return status;
|
||||
}
|
||||
|
||||
for (int i = 0; i < CONFIG_BLE_ESL_MAX_LEDS; i++) {
|
||||
if (s_ctx->leds[i].active) {
|
||||
status.active_led = true;
|
||||
}
|
||||
if (s_ctx->leds[i].timed_pending) {
|
||||
status.pending_led_update = true;
|
||||
}
|
||||
/* Early exit if both flags are already set */
|
||||
if (status.active_led && status.pending_led_update) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return status;
|
||||
}
|
||||
|
||||
/* ========================================================================== */
|
||||
/* Internal Interface: esl_cmd_led_cancel_all() */
|
||||
/* ========================================================================== */
|
||||
|
||||
void esl_cmd_led_cancel_all(void)
|
||||
{
|
||||
if (s_ctx == NULL) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = 0; i < CONFIG_BLE_ESL_MAX_LEDS; i++) {
|
||||
esl_led_state_t *led = &s_ctx->leds[i];
|
||||
|
||||
/* Stop pattern expiry timer */
|
||||
if (led->pattern_timer != NULL) {
|
||||
esp_timer_stop(led->pattern_timer);
|
||||
}
|
||||
|
||||
/* Stop timed command timer */
|
||||
if (led->timed_timer != NULL) {
|
||||
esp_timer_stop(led->timed_timer);
|
||||
}
|
||||
|
||||
/* Notify application to turn off active LEDs */
|
||||
if (led->active) {
|
||||
ble_esl_cb_param_t cb_param;
|
||||
memset(&cb_param, 0, sizeof(cb_param));
|
||||
memcpy(&cb_param.led_control, &led->current_params, sizeof(ble_esl_led_control_evt_param_t));
|
||||
cb_param.led_control.is_off = true;
|
||||
esl_notify_app(BLE_ESL_EVT_LED_CONTROL, &cb_param);
|
||||
}
|
||||
|
||||
/* Clear all state */
|
||||
led->active = false;
|
||||
led->timed_pending = false;
|
||||
memset(&led->timed_params, 0, sizeof(led->timed_params));
|
||||
memset(&led->current_params, 0, sizeof(led->current_params));
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "All LED commands cancelled");
|
||||
}
|
||||
|
||||
/* ========================================================================== */
|
||||
/* Init / Deinit */
|
||||
/* ========================================================================== */
|
||||
|
||||
esp_err_t esl_cmd_led_init(void)
|
||||
{
|
||||
if (s_ctx != NULL) {
|
||||
ESP_LOGW(TAG, "LED command module already initialized");
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
|
||||
s_ctx = calloc(1, sizeof(esl_cmd_led_ctx_t));
|
||||
if (s_ctx == NULL) {
|
||||
ESP_LOGE(TAG, "Failed to allocate LED command context");
|
||||
return ESP_ERR_NO_MEM;
|
||||
}
|
||||
|
||||
/* Create esp_timers for each LED slot (pattern expiry + timed command) */
|
||||
esp_err_t ret = ESP_OK;
|
||||
for (int i = 0; i < CONFIG_BLE_ESL_MAX_LEDS; i++) {
|
||||
/* Pattern expiry timer */
|
||||
esp_timer_create_args_t pattern_timer_args = {
|
||||
.callback = pattern_expiry_timer_cb,
|
||||
.arg = (void *)(uintptr_t)i,
|
||||
.dispatch_method = ESP_TIMER_TASK,
|
||||
.name = "esl_led_pattern",
|
||||
};
|
||||
|
||||
ret = esp_timer_create(&pattern_timer_args,
|
||||
&s_ctx->leds[i].pattern_timer);
|
||||
if (ret != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to create pattern timer for LED %d: %s",
|
||||
i, esp_err_to_name(ret));
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
/* Timed command timer */
|
||||
esp_timer_create_args_t timed_timer_args = {
|
||||
.callback = timed_led_timer_cb,
|
||||
.arg = (void *)(uintptr_t)i,
|
||||
.dispatch_method = ESP_TIMER_TASK,
|
||||
.name = "esl_led_timed",
|
||||
};
|
||||
|
||||
ret = esp_timer_create(&timed_timer_args, &s_ctx->leds[i].timed_timer);
|
||||
if (ret != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to create timed timer for LED %d: %s",
|
||||
i, esp_err_to_name(ret));
|
||||
/* Delete the pattern timer we just created for this LED */
|
||||
esp_timer_delete(s_ctx->leds[i].pattern_timer);
|
||||
s_ctx->leds[i].pattern_timer = NULL;
|
||||
goto cleanup;
|
||||
}
|
||||
}
|
||||
|
||||
s_ctx->initialized = true;
|
||||
ESP_LOGI(TAG, "LED command module initialized (%d LEDs)", CONFIG_BLE_ESL_MAX_LEDS);
|
||||
|
||||
return ESP_OK;
|
||||
|
||||
cleanup:
|
||||
/* Clean up already created timers */
|
||||
for (int j = 0; j < CONFIG_BLE_ESL_MAX_LEDS; j++) {
|
||||
if (s_ctx->leds[j].pattern_timer != NULL) {
|
||||
esp_timer_delete(s_ctx->leds[j].pattern_timer);
|
||||
s_ctx->leds[j].pattern_timer = NULL;
|
||||
}
|
||||
if (s_ctx->leds[j].timed_timer != NULL) {
|
||||
esp_timer_delete(s_ctx->leds[j].timed_timer);
|
||||
s_ctx->leds[j].timed_timer = NULL;
|
||||
}
|
||||
}
|
||||
free(s_ctx);
|
||||
s_ctx = NULL;
|
||||
return ret;
|
||||
}
|
||||
|
||||
void esl_cmd_led_deinit(void)
|
||||
{
|
||||
if (s_ctx == NULL) {
|
||||
return;
|
||||
}
|
||||
|
||||
/* Notify application to turn off all active LEDs before teardown */
|
||||
esl_cmd_led_cancel_all();
|
||||
|
||||
/* Stop and delete all timers */
|
||||
for (int i = 0; i < CONFIG_BLE_ESL_MAX_LEDS; i++) {
|
||||
if (s_ctx->leds[i].pattern_timer != NULL) {
|
||||
esp_timer_stop(s_ctx->leds[i].pattern_timer);
|
||||
esp_err_t ret = esp_timer_delete(s_ctx->leds[i].pattern_timer);
|
||||
if (ret != ESP_OK) {
|
||||
ESP_LOGW(TAG, "Failed to delete pattern timer for LED %d: %s",
|
||||
i, esp_err_to_name(ret));
|
||||
}
|
||||
s_ctx->leds[i].pattern_timer = NULL;
|
||||
}
|
||||
|
||||
if (s_ctx->leds[i].timed_timer != NULL) {
|
||||
esp_timer_stop(s_ctx->leds[i].timed_timer);
|
||||
esp_err_t ret = esp_timer_delete(s_ctx->leds[i].timed_timer);
|
||||
if (ret != ESP_OK) {
|
||||
ESP_LOGW(TAG, "Failed to delete timed timer for LED %d: %s",
|
||||
i, esp_err_to_name(ret));
|
||||
}
|
||||
s_ctx->leds[i].timed_timer = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
free(s_ctx);
|
||||
s_ctx = NULL;
|
||||
|
||||
ESP_LOGI(TAG, "LED command module deinitialized");
|
||||
}
|
||||
@@ -0,0 +1,536 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
/**
|
||||
* @file esl_cmd_lifecycle.c
|
||||
* @brief ESL ECP command dispatch pipeline, lifecycle commands, and Basic State
|
||||
* bitmap construction.
|
||||
*
|
||||
* Handles command routing (ESL_ID validation, opcode recognition, factory-reset
|
||||
* guard), lifecycle commands (Ping, Unassociate, Service Reset, Factory Reset,
|
||||
* Update Complete), and the Basic State bitmap assembly.
|
||||
*/
|
||||
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
#include "esp_log.h"
|
||||
#include "esp_err.h"
|
||||
#include "host/ble_hs.h"
|
||||
#include "host/ble_gap.h"
|
||||
|
||||
#include "ble_esl.h"
|
||||
#include "ble_esl_common.h"
|
||||
#include "ble_esl_int.h"
|
||||
|
||||
static const char *TAG = "esl_cmd_lifecycle";
|
||||
|
||||
/* ========================== Internal Context ========================== */
|
||||
|
||||
/**
|
||||
* @brief Lifecycle module internal state
|
||||
*/
|
||||
typedef struct {
|
||||
bool service_needed; /*!< Service Needed flag (Basic State bit 0) */
|
||||
bool factory_reset_pending; /*!< Factory Reset guard — reject all subsequent cmds */
|
||||
bool unassociate_pending; /*!< Post-response cleanup flag for Unassociate */
|
||||
} lifecycle_ctx_t;
|
||||
|
||||
static lifecycle_ctx_t *s_ctx = NULL;
|
||||
|
||||
/* ========================== Forward Declarations ========================== */
|
||||
|
||||
static void build_error_response(ble_esl_cmd_result_t *result, uint8_t error_code);
|
||||
static void build_basic_state_response(ble_esl_cmd_result_t *result, uint16_t bitmap);
|
||||
static bool is_valid_opcode(uint8_t opcode);
|
||||
|
||||
static esp_err_t handle_ping(const uint8_t *params, uint8_t params_len,
|
||||
ble_esl_cmd_result_t *result);
|
||||
static esp_err_t handle_unassociate(const uint8_t *params, uint8_t params_len,
|
||||
ble_esl_cmd_result_t *result);
|
||||
static esp_err_t handle_service_reset(const uint8_t *params, uint8_t params_len,
|
||||
ble_esl_cmd_result_t *result);
|
||||
static esp_err_t handle_factory_reset(const uint8_t *params, uint8_t params_len,
|
||||
ble_esl_cmd_result_t *result);
|
||||
static esp_err_t handle_update_complete(const uint8_t *params, uint8_t params_len,
|
||||
ble_esl_cmd_result_t *result);
|
||||
|
||||
/* ========================== Helper Functions ========================== */
|
||||
|
||||
/**
|
||||
* @brief Build an Error response TLV into the result structure
|
||||
*
|
||||
* @param[out] result Command result to populate
|
||||
* @param[in] error_code ESL error code
|
||||
*/
|
||||
static void build_error_response(ble_esl_cmd_result_t *result, uint8_t error_code)
|
||||
{
|
||||
result->has_response = true;
|
||||
/* Error Response opcode: Tag=0x0, Length=0x0 → opcode = 0x00 */
|
||||
result->resp_opcode = BLE_ESL_RESP_ERROR;
|
||||
result->resp_params[0] = error_code;
|
||||
result->resp_params_len = 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Build a Basic State response TLV into the result structure
|
||||
*
|
||||
* @param[out] result Command result to populate
|
||||
* @param[in] bitmap 16-bit Basic State bitmap (little-endian)
|
||||
*/
|
||||
static void build_basic_state_response(ble_esl_cmd_result_t *result, uint16_t bitmap)
|
||||
{
|
||||
result->has_response = true;
|
||||
/* Basic State Response opcode: Tag=0x0, Length=0x1 → opcode = 0x10 */
|
||||
result->resp_opcode = BLE_ESL_RESP_BASIC_STATE;
|
||||
result->resp_params[0] = (uint8_t)(bitmap & 0xFF);
|
||||
result->resp_params[1] = (uint8_t)((bitmap >> 8) & 0xFF);
|
||||
result->resp_params_len = 2;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Check if an opcode is a recognized ESL command opcode
|
||||
*
|
||||
* @param[in] opcode Command opcode byte
|
||||
* @return true if recognized, false otherwise
|
||||
*/
|
||||
static bool is_valid_opcode(uint8_t opcode)
|
||||
{
|
||||
/* Check fixed opcodes */
|
||||
switch (opcode) {
|
||||
case BLE_ESL_CMD_PING:
|
||||
case BLE_ESL_CMD_UNASSOCIATE:
|
||||
case BLE_ESL_CMD_SERVICE_RESET:
|
||||
case BLE_ESL_CMD_FACTORY_RESET:
|
||||
case BLE_ESL_CMD_UPDATE_COMPLETE:
|
||||
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:
|
||||
break;
|
||||
}
|
||||
|
||||
/* Check vendor-specific: Tag nibble (bits [3:0]) == 0x0F */
|
||||
if (BLE_ESL_TLV_TAG(opcode) == BLE_ESL_CMD_VENDOR_TAG) {
|
||||
#if CONFIG_BLE_ESL_VENDOR_SPECIFIC
|
||||
return true;
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/* ========================== Lifecycle Command Handlers ========================== */
|
||||
|
||||
/**
|
||||
* @brief Handle Ping command (opcode 0x00)
|
||||
*
|
||||
* No side effects. Always returns a Basic State response.
|
||||
*/
|
||||
static esp_err_t handle_ping(const uint8_t *params, uint8_t params_len,
|
||||
ble_esl_cmd_result_t *result)
|
||||
{
|
||||
uint16_t bitmap = 0;
|
||||
esp_err_t ret = esl_build_basic_state(&bitmap);
|
||||
if (ret != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to build basic state for Ping: %d", ret);
|
||||
/* Ping shall never produce an Error response, build best-effort bitmap */
|
||||
bitmap = 0;
|
||||
}
|
||||
|
||||
build_basic_state_response(result, bitmap);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Handle Unassociate from AP command (opcode 0x01)
|
||||
*
|
||||
* Builds a Basic State response. The actual cleanup (bonding removal, state
|
||||
* transition to Unassociated, event notification) happens AFTER the response
|
||||
* is sent — signaled via the unassociate_pending flag.
|
||||
*/
|
||||
static esp_err_t handle_unassociate(const uint8_t *params, uint8_t params_len,
|
||||
ble_esl_cmd_result_t *result)
|
||||
{
|
||||
uint16_t bitmap = 0;
|
||||
esp_err_t ret = esl_build_basic_state(&bitmap);
|
||||
if (ret != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to build basic state for Unassociate: %d", ret);
|
||||
build_error_response(result, BLE_ESL_ERR_UNSPECIFIED);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
build_basic_state_response(result, bitmap);
|
||||
|
||||
/* Set post-response flag: esl_state.c checks this after sending ECP response */
|
||||
s_ctx->unassociate_pending = true;
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Handle Service Reset command (opcode 0x02)
|
||||
*
|
||||
* Attempts to clear the Service Needed flag. If no persistent condition remains,
|
||||
* the flag is cleared. Always returns a Basic State response.
|
||||
*/
|
||||
static esp_err_t handle_service_reset(const uint8_t *params, uint8_t params_len,
|
||||
ble_esl_cmd_result_t *result)
|
||||
{
|
||||
/*
|
||||
* Attempt to clear service_needed. Per spec, the flag is only cleared if
|
||||
* the underlying condition no longer persists. Since we don't track the
|
||||
* persistent condition separately, we simply clear the flag here.
|
||||
* The application can re-set it via ble_esl_set_service_needed(true) if
|
||||
* the condition still persists.
|
||||
*/
|
||||
if (s_ctx->service_needed) {
|
||||
s_ctx->service_needed = false;
|
||||
ESP_LOGI(TAG, "Service Needed flag cleared by Service Reset");
|
||||
}
|
||||
|
||||
uint16_t bitmap = 0;
|
||||
esp_err_t ret = esl_build_basic_state(&bitmap);
|
||||
if (ret != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to build basic state for Service Reset: %d", ret);
|
||||
build_error_response(result, BLE_ESL_ERR_UNSPECIFIED);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
build_basic_state_response(result, bitmap);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Handle Factory Reset command (opcode 0x03)
|
||||
*
|
||||
* Sets the factory_reset_pending guard, initiates disconnect, and fires the
|
||||
* FACTORY_RESET event. Produces no response (has_response = false).
|
||||
*/
|
||||
static esp_err_t handle_factory_reset(const uint8_t *params, uint8_t params_len,
|
||||
ble_esl_cmd_result_t *result)
|
||||
{
|
||||
/* Set guard flag — all subsequent commands will be rejected */
|
||||
s_ctx->factory_reset_pending = true;
|
||||
|
||||
/* No response for Factory Reset */
|
||||
result->has_response = false;
|
||||
result->resp_params_len = 0;
|
||||
|
||||
/* Initiate disconnection using the actual active connection handle.
|
||||
* We terminate with Remote User Terminated Connection reason. */
|
||||
uint16_t conn_handle = esl_state_get_conn_handle();
|
||||
if (conn_handle != BLE_HS_CONN_HANDLE_NONE) {
|
||||
int rc = ble_gap_terminate(conn_handle, BLE_ERR_REM_USER_CONN_TERM);
|
||||
if (rc != 0 && rc != BLE_HS_EALREADY) {
|
||||
ESP_LOGW(TAG, "Failed to initiate disconnect for factory reset: %d", rc);
|
||||
}
|
||||
} else {
|
||||
ESP_LOGW(TAG, "No active connection found for factory reset disconnect");
|
||||
}
|
||||
|
||||
/* Fire FACTORY_RESET event to the application */
|
||||
esl_notify_app(BLE_ESL_EVT_FACTORY_RESET, NULL);
|
||||
|
||||
ESP_LOGI(TAG, "Factory Reset initiated");
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Handle Update Complete command (opcode 0x04)
|
||||
*
|
||||
* Records that Update Complete was received and lets the state module decide
|
||||
* when to advance to Synchronized. The transition must NOT happen until PAST
|
||||
* (PAwR sync transfer) has also completed, because entering Synchronized tears
|
||||
* down the ACL link over which PAST is delivered — advancing early would leave
|
||||
* the ESL un-synced to the PAwR train and unable to answer commands. Per ESL
|
||||
* spec Section 3.9.2.5.2, the Update Complete command has no response.
|
||||
*/
|
||||
static esp_err_t handle_update_complete(const uint8_t *params, uint8_t params_len,
|
||||
ble_esl_cmd_result_t *result)
|
||||
{
|
||||
/* No response for Update Complete per spec */
|
||||
result->has_response = false;
|
||||
result->resp_params_len = 0;
|
||||
|
||||
/* Defer the SYNCHRONIZED transition to the state module, which gates it on
|
||||
* PAST reception (and, in Configuring, on the mandatory configuration
|
||||
* writes). */
|
||||
esl_notify_update_complete();
|
||||
|
||||
ESP_LOGI(TAG, "Update Complete processed");
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
/* ========================== Public / Internal API ========================== */
|
||||
|
||||
esp_err_t esl_cmd_lifecycle_init(void)
|
||||
{
|
||||
if (s_ctx != NULL) {
|
||||
ESP_LOGW(TAG, "Lifecycle module already initialized");
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
|
||||
s_ctx = calloc(1, sizeof(lifecycle_ctx_t));
|
||||
if (s_ctx == NULL) {
|
||||
ESP_LOGE(TAG, "Failed to allocate lifecycle context");
|
||||
return ESP_ERR_NO_MEM;
|
||||
}
|
||||
|
||||
s_ctx->service_needed = false;
|
||||
s_ctx->factory_reset_pending = false;
|
||||
s_ctx->unassociate_pending = false;
|
||||
|
||||
ESP_LOGI(TAG, "Lifecycle command module initialized");
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
void esl_cmd_lifecycle_deinit(void)
|
||||
{
|
||||
if (s_ctx != NULL) {
|
||||
free(s_ctx);
|
||||
s_ctx = NULL;
|
||||
}
|
||||
ESP_LOGI(TAG, "Lifecycle command module deinitialized");
|
||||
}
|
||||
|
||||
esp_err_t esl_cmd_dispatch(uint8_t opcode, const uint8_t *params,
|
||||
uint8_t params_len, ble_esl_cmd_result_t *result,
|
||||
uint8_t transport)
|
||||
{
|
||||
assert(result != NULL);
|
||||
|
||||
/* Initialize result to safe defaults */
|
||||
memset(result, 0, sizeof(ble_esl_cmd_result_t));
|
||||
result->has_response = false;
|
||||
|
||||
if (s_ctx == NULL) {
|
||||
ESP_LOGE(TAG, "Lifecycle module not initialized");
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
|
||||
/* Factory Reset Pending Guard: reject all commands with Unspecified Error.
|
||||
* The rejection is a normal, spec-mandated outcome that carries an Error
|
||||
* response, so return ESP_OK — as every other rejection path below does —
|
||||
* so callers actually transmit the response instead of treating it as a
|
||||
* dispatch failure. */
|
||||
if (s_ctx->factory_reset_pending) {
|
||||
ESP_LOGW(TAG, "Factory reset pending, rejecting command opcode=0x%02X", opcode);
|
||||
build_error_response(result, BLE_ESL_ERR_UNSPECIFIED);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
/* Validate ESL_ID (first parameter byte) */
|
||||
if (params == NULL || params_len < 1) {
|
||||
ESP_LOGE(TAG, "Command has no parameters (missing ESL_ID)");
|
||||
build_error_response(result, BLE_ESL_ERR_INVALID_PARAMS);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
uint8_t cmd_esl_id = params[0];
|
||||
uint8_t my_esl_id = 0;
|
||||
uint8_t my_group_id = 0;
|
||||
esp_err_t ret = esl_get_address(&my_esl_id, &my_group_id);
|
||||
|
||||
if (cmd_esl_id == BLE_ESL_BROADCAST_ADDRESS) {
|
||||
if (transport == ESL_TRANSPORT_ECP) {
|
||||
/* Broadcast Address (0xFF) is only valid over PAwR, not ECP. */
|
||||
ESP_LOGW(TAG, "Broadcast address 0xFF rejected on ECP");
|
||||
build_error_response(result, BLE_ESL_ERR_INVALID_PARAMS);
|
||||
return ESP_OK;
|
||||
}
|
||||
/* Over PAwR, broadcast address is valid — skip ESL_ID matching */
|
||||
} else {
|
||||
if (ret != ESP_OK) {
|
||||
/* ESL address not configured — cannot validate ESL_ID */
|
||||
ESP_LOGW(TAG, "ESL address not configured, rejecting command");
|
||||
build_error_response(result, BLE_ESL_ERR_INVALID_PARAMS);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
if (cmd_esl_id != my_esl_id) {
|
||||
/* ESL_ID mismatch */
|
||||
ESP_LOGW(TAG, "ESL_ID mismatch: received=0x%02X, own=0x%02X", cmd_esl_id, my_esl_id);
|
||||
build_error_response(result, BLE_ESL_ERR_INVALID_PARAMS);
|
||||
return ESP_OK;
|
||||
}
|
||||
}
|
||||
|
||||
/* Check for unrecognized opcode */
|
||||
if (!is_valid_opcode(opcode)) {
|
||||
ESP_LOGW(TAG, "Unrecognized opcode: 0x%02X", opcode);
|
||||
build_error_response(result, BLE_ESL_ERR_INVALID_OPCODE);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
/* Strip ESL_ID byte before delegating to sub-handlers */
|
||||
const uint8_t *cmd_params = params + 1;
|
||||
uint8_t cmd_params_len = params_len - 1;
|
||||
|
||||
/* Route to appropriate handler */
|
||||
switch (opcode) {
|
||||
/* Lifecycle commands — handled inline */
|
||||
case BLE_ESL_CMD_PING:
|
||||
return handle_ping(cmd_params, cmd_params_len, result);
|
||||
|
||||
case BLE_ESL_CMD_UNASSOCIATE:
|
||||
if (transport != ESL_TRANSPORT_ECP) {
|
||||
ESP_LOGW(TAG, "Unassociate rejected: only valid over ECP");
|
||||
build_error_response(result, BLE_ESL_ERR_INVALID_STATE);
|
||||
return ESP_OK;
|
||||
}
|
||||
return handle_unassociate(cmd_params, cmd_params_len, result);
|
||||
|
||||
case BLE_ESL_CMD_SERVICE_RESET:
|
||||
return handle_service_reset(cmd_params, cmd_params_len, result);
|
||||
|
||||
case BLE_ESL_CMD_FACTORY_RESET:
|
||||
return handle_factory_reset(cmd_params, cmd_params_len, result);
|
||||
|
||||
case BLE_ESL_CMD_UPDATE_COMPLETE:
|
||||
return handle_update_complete(cmd_params, cmd_params_len, result);
|
||||
|
||||
/* Display/Sensor commands — delegated to esl_cmd_display.c */
|
||||
case BLE_ESL_CMD_READ_SENSOR:
|
||||
return esl_cmd_read_sensor(cmd_params, cmd_params_len, result, transport);
|
||||
|
||||
case BLE_ESL_CMD_REFRESH_DISPLAY:
|
||||
return esl_cmd_refresh_display(cmd_params, cmd_params_len, result);
|
||||
|
||||
case BLE_ESL_CMD_DISPLAY_IMAGE:
|
||||
return esl_cmd_display_image(cmd_params, cmd_params_len, result);
|
||||
|
||||
case BLE_ESL_CMD_DISPLAY_TIMED_IMAGE:
|
||||
return esl_cmd_display_timed_image(cmd_params, cmd_params_len, result);
|
||||
|
||||
/* LED commands — delegated to esl_cmd_led.c */
|
||||
case BLE_ESL_CMD_LED_CONTROL:
|
||||
return esl_cmd_led_control(cmd_params, cmd_params_len, result);
|
||||
|
||||
case BLE_ESL_CMD_LED_TIMED_CONTROL:
|
||||
return esl_cmd_led_timed_control(cmd_params, cmd_params_len, result);
|
||||
|
||||
default:
|
||||
/* Vendor-specific commands (Tag nibble == 0x0F) */
|
||||
if (BLE_ESL_TLV_TAG(opcode) == BLE_ESL_CMD_VENDOR_TAG) {
|
||||
return esl_cmd_vendor_specific(opcode, cmd_params, cmd_params_len, result);
|
||||
}
|
||||
/* Should not reach here due to is_valid_opcode check above */
|
||||
build_error_response(result, BLE_ESL_ERR_INVALID_OPCODE);
|
||||
return ESP_OK;
|
||||
}
|
||||
}
|
||||
|
||||
esp_err_t esl_build_basic_state(uint16_t *bitmap_out)
|
||||
{
|
||||
if (bitmap_out == NULL) {
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
|
||||
if (s_ctx == NULL) {
|
||||
ESP_LOGE(TAG, "Lifecycle module not initialized");
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
|
||||
uint16_t bitmap = 0;
|
||||
|
||||
/* Bit 0: Service Needed */
|
||||
if (s_ctx->service_needed) {
|
||||
bitmap |= BLE_ESL_BASIC_STATE_SERVICE_NEEDED;
|
||||
}
|
||||
|
||||
/* Bit 1: Synchronized — per spec this reports whether the ESL is
|
||||
* synchronized to the AP, not whether it sits in the Synchronized state.
|
||||
* The Updating state keeps the PAwR sync (it only ignores the data), so an
|
||||
* ESL that entered Updating from Synchronized must still report bit 1. */
|
||||
ble_esl_state_t state = ble_esl_get_state();
|
||||
if (state == BLE_ESL_STATE_SYNCHRONIZED ||
|
||||
(state == BLE_ESL_STATE_UPDATING && esl_is_pawr_synced())) {
|
||||
bitmap |= BLE_ESL_BASIC_STATE_SYNCHRONIZED;
|
||||
}
|
||||
|
||||
/* Bits 2-3: Active LED and Pending LED Update */
|
||||
esl_led_status_t led_status = esl_get_led_status();
|
||||
if (led_status.active_led) {
|
||||
bitmap |= BLE_ESL_BASIC_STATE_ACTIVE_LED;
|
||||
}
|
||||
if (led_status.pending_led_update) {
|
||||
bitmap |= BLE_ESL_BASIC_STATE_PENDING_LED_UPDATE;
|
||||
}
|
||||
|
||||
/* Bit 4: Pending Display Update */
|
||||
if (esl_get_display_pending()) {
|
||||
bitmap |= BLE_ESL_BASIC_STATE_PENDING_DISP_UPDATE;
|
||||
}
|
||||
|
||||
/* Bits 5-15: RFU, set to 0 (already 0 from initialization) */
|
||||
|
||||
*bitmap_out = bitmap;
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t ble_esl_set_service_needed(bool flag)
|
||||
{
|
||||
if (s_ctx == NULL) {
|
||||
ESP_LOGE(TAG, "Lifecycle module not initialized");
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
|
||||
s_ctx->service_needed = flag;
|
||||
ESP_LOGI(TAG, "Service Needed flag set to %s", flag ? "true" : "false");
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
bool esl_is_factory_reset_pending(void)
|
||||
{
|
||||
if (s_ctx == NULL) {
|
||||
return false;
|
||||
}
|
||||
return s_ctx->factory_reset_pending;
|
||||
}
|
||||
|
||||
void esl_clear_factory_reset_pending(void)
|
||||
{
|
||||
if (s_ctx != NULL) {
|
||||
s_ctx->factory_reset_pending = false;
|
||||
ESP_LOGI(TAG, "Factory reset pending flag cleared");
|
||||
}
|
||||
}
|
||||
|
||||
bool esl_is_unassociate_pending(void)
|
||||
{
|
||||
if (s_ctx == NULL) {
|
||||
return false;
|
||||
}
|
||||
return s_ctx->unassociate_pending;
|
||||
}
|
||||
|
||||
void esl_execute_unassociate_cleanup(void)
|
||||
{
|
||||
if (s_ctx == NULL || !s_ctx->unassociate_pending) {
|
||||
return;
|
||||
}
|
||||
|
||||
/* Clear the pending flag first */
|
||||
s_ctx->unassociate_pending = false;
|
||||
|
||||
ESP_LOGI(TAG, "Executing post-response Unassociate cleanup");
|
||||
|
||||
/* Transition to Unassociated state (internally clears stored data) */
|
||||
esp_err_t ret = esl_state_transition(BLE_ESL_STATE_UNASSOCIATED);
|
||||
if (ret != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to transition to Unassociated state: %d", ret);
|
||||
}
|
||||
|
||||
/* Fire UNASSOCIATE event to the application */
|
||||
esl_notify_app(BLE_ESL_EVT_UNASSOCIATE, NULL);
|
||||
}
|
||||
861
components/bt/ble_profiles/nimble/ble_esl/src/esl/esl_gatts.c
Normal file
861
components/bt/ble_profiles/nimble/ble_esl/src/esl/esl_gatts.c
Normal file
@@ -0,0 +1,861 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
/**
|
||||
* @file esl_gatts.c
|
||||
* @brief ESL GATT server, OTS setup, DIS PnP ID.
|
||||
*/
|
||||
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
#include <assert.h>
|
||||
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/semphr.h"
|
||||
#include "esp_log.h"
|
||||
#include "esp_timer.h"
|
||||
|
||||
#include "nimble/ble.h"
|
||||
#include "nimble/nimble_port.h"
|
||||
#include "host/ble_hs.h"
|
||||
#include "host/ble_gap.h"
|
||||
#include "host/ble_gatt.h"
|
||||
#include "host/ble_uuid.h"
|
||||
#include "host/ble_store.h"
|
||||
#include "services/dis/ble_svc_dis.h"
|
||||
|
||||
#include "ble_esl.h"
|
||||
#include "ble_esl_int.h"
|
||||
#include "ble_esl_state_int.h"
|
||||
|
||||
#if CONFIG_BLE_ESL_OTS_SUPPORT
|
||||
#include "ble_ots_server.h"
|
||||
#endif
|
||||
|
||||
static const char *TAG = "esl_gatts";
|
||||
|
||||
/* ========================== Forward Declarations ========================== */
|
||||
static int esl_gatt_access_cb(uint16_t conn_handle, uint16_t attr_handle,
|
||||
struct ble_gatt_access_ctxt *ctxt, void *arg);
|
||||
|
||||
/* ========================== GATT Service Definition ========================== */
|
||||
|
||||
/*
|
||||
* We build the GATT service table dynamically at init time because
|
||||
* conditional characteristics depend on config element counts.
|
||||
* Maximum: 9 characteristics + 1 terminator = 10 entries.
|
||||
*/
|
||||
#define ESL_MAX_CHRS 10
|
||||
|
||||
/* Error response TLV: opcode byte (Tag=0, Length=0) + error code byte */
|
||||
#define ESL_ECP_ERROR_RESP_SIZE 2
|
||||
|
||||
static const ble_uuid16_t s_esl_svc_uuid = BLE_UUID16_INIT(BLE_ESL_SVC_UUID);
|
||||
static const ble_uuid16_t s_uuid_address = BLE_UUID16_INIT(BLE_ESL_CHR_UUID_ESL_ADDRESS);
|
||||
static const ble_uuid16_t s_uuid_ap_sync = BLE_UUID16_INIT(BLE_ESL_CHR_UUID_AP_SYNC_KEY);
|
||||
static const ble_uuid16_t s_uuid_resp_key = BLE_UUID16_INIT(BLE_ESL_CHR_UUID_RESP_KEY);
|
||||
static const ble_uuid16_t s_uuid_abs_time = BLE_UUID16_INIT(BLE_ESL_CHR_UUID_CURRENT_ABS_TIME);
|
||||
static const ble_uuid16_t s_uuid_disp_info = BLE_UUID16_INIT(BLE_ESL_CHR_UUID_DISPLAY_INFO);
|
||||
static const ble_uuid16_t s_uuid_img_info = BLE_UUID16_INIT(BLE_ESL_CHR_UUID_IMAGE_INFO);
|
||||
static const ble_uuid16_t s_uuid_sensor_info = BLE_UUID16_INIT(BLE_ESL_CHR_UUID_SENSOR_INFO);
|
||||
static const ble_uuid16_t s_uuid_led_info = BLE_UUID16_INIT(BLE_ESL_CHR_UUID_LED_INFO);
|
||||
static const ble_uuid16_t s_uuid_ecp = BLE_UUID16_INIT(BLE_ESL_CHR_UUID_ECP);
|
||||
|
||||
/* Index mapping for characteristic handles */
|
||||
enum {
|
||||
CHR_IDX_ADDRESS = 0,
|
||||
CHR_IDX_AP_SYNC,
|
||||
CHR_IDX_RESP_KEY,
|
||||
CHR_IDX_ABS_TIME,
|
||||
CHR_IDX_ECP,
|
||||
/* Conditional chars start here; indices assigned dynamically */
|
||||
};
|
||||
|
||||
/*
|
||||
* All of this module's mutable static state is aggregated into a single
|
||||
* heap-allocated context, so it costs no RAM until the ESL GATT server is
|
||||
* initialized. NimBLE retains pointers into esl_chrs / esl_svcs / chr_handles
|
||||
* after ble_gatts_add_svcs(), so this context must live from esl_gatts_init()
|
||||
* until esl_gatts_deinit().
|
||||
*/
|
||||
typedef struct {
|
||||
esl_state_ctx_t *ctx; /* borrowed state context (not owned) */
|
||||
|
||||
struct ble_gatt_chr_def esl_chrs[ESL_MAX_CHRS];
|
||||
uint16_t chr_handles[9]; /* val handles for up to 9 chars */
|
||||
struct ble_gatt_svc_def esl_svcs[2]; /* service + terminator */
|
||||
uint8_t ecp_chr_idx; /* actual index of ECP in array */
|
||||
|
||||
#if CONFIG_BLE_ESL_OTS_SUPPORT
|
||||
ble_ots_obj_id_t ots_obj_ids[CONFIG_BLE_ESL_MAX_IMAGES]; /* reverse map */
|
||||
uint8_t ots_obj_count;
|
||||
bool ots_initialized; /* OTS server init state */
|
||||
#endif
|
||||
} esl_gatts_ctx_t;
|
||||
|
||||
static esl_gatts_ctx_t *s_esl_gatts = NULL;
|
||||
|
||||
/**
|
||||
* @brief Build the ESL GATT service definition based on config
|
||||
*/
|
||||
static void esl_build_gatt_svcs(const ble_esl_config_t *config)
|
||||
{
|
||||
int idx = 0;
|
||||
|
||||
/* Mandatory: ESL Address (Write, Encrypted) */
|
||||
memset(&s_esl_gatts->esl_chrs[idx], 0, sizeof(struct ble_gatt_chr_def));
|
||||
s_esl_gatts->esl_chrs[idx].uuid = &s_uuid_address.u;
|
||||
s_esl_gatts->esl_chrs[idx].access_cb = esl_gatt_access_cb;
|
||||
s_esl_gatts->esl_chrs[idx].val_handle = &s_esl_gatts->chr_handles[idx];
|
||||
s_esl_gatts->esl_chrs[idx].flags = BLE_GATT_CHR_F_WRITE | BLE_GATT_CHR_F_WRITE_ENC;
|
||||
idx++;
|
||||
|
||||
/* Mandatory: AP Sync Key Material (Write, Encrypted) */
|
||||
memset(&s_esl_gatts->esl_chrs[idx], 0, sizeof(struct ble_gatt_chr_def));
|
||||
s_esl_gatts->esl_chrs[idx].uuid = &s_uuid_ap_sync.u;
|
||||
s_esl_gatts->esl_chrs[idx].access_cb = esl_gatt_access_cb;
|
||||
s_esl_gatts->esl_chrs[idx].val_handle = &s_esl_gatts->chr_handles[idx];
|
||||
s_esl_gatts->esl_chrs[idx].flags = BLE_GATT_CHR_F_WRITE | BLE_GATT_CHR_F_WRITE_ENC;
|
||||
idx++;
|
||||
|
||||
/* Mandatory: ESL Response Key Material (Write, Encrypted) */
|
||||
memset(&s_esl_gatts->esl_chrs[idx], 0, sizeof(struct ble_gatt_chr_def));
|
||||
s_esl_gatts->esl_chrs[idx].uuid = &s_uuid_resp_key.u;
|
||||
s_esl_gatts->esl_chrs[idx].access_cb = esl_gatt_access_cb;
|
||||
s_esl_gatts->esl_chrs[idx].val_handle = &s_esl_gatts->chr_handles[idx];
|
||||
s_esl_gatts->esl_chrs[idx].flags = BLE_GATT_CHR_F_WRITE | BLE_GATT_CHR_F_WRITE_ENC;
|
||||
idx++;
|
||||
|
||||
/* Mandatory: ESL Current Absolute Time (Write, Encrypted) */
|
||||
memset(&s_esl_gatts->esl_chrs[idx], 0, sizeof(struct ble_gatt_chr_def));
|
||||
s_esl_gatts->esl_chrs[idx].uuid = &s_uuid_abs_time.u;
|
||||
s_esl_gatts->esl_chrs[idx].access_cb = esl_gatt_access_cb;
|
||||
s_esl_gatts->esl_chrs[idx].val_handle = &s_esl_gatts->chr_handles[idx];
|
||||
s_esl_gatts->esl_chrs[idx].flags = BLE_GATT_CHR_F_WRITE | BLE_GATT_CHR_F_WRITE_ENC;
|
||||
idx++;
|
||||
|
||||
/* Conditional: Display Information (Read, Encrypted) */
|
||||
if (config->num_displays > 0) {
|
||||
memset(&s_esl_gatts->esl_chrs[idx], 0, sizeof(struct ble_gatt_chr_def));
|
||||
s_esl_gatts->esl_chrs[idx].uuid = &s_uuid_disp_info.u;
|
||||
s_esl_gatts->esl_chrs[idx].access_cb = esl_gatt_access_cb;
|
||||
s_esl_gatts->esl_chrs[idx].val_handle = &s_esl_gatts->chr_handles[idx];
|
||||
s_esl_gatts->esl_chrs[idx].flags = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_READ_ENC;
|
||||
idx++;
|
||||
}
|
||||
|
||||
/* Conditional: Image Information (Read, Encrypted) — requires both display and image support */
|
||||
if (config->num_displays > 0 && config->num_images > 0) {
|
||||
memset(&s_esl_gatts->esl_chrs[idx], 0, sizeof(struct ble_gatt_chr_def));
|
||||
s_esl_gatts->esl_chrs[idx].uuid = &s_uuid_img_info.u;
|
||||
s_esl_gatts->esl_chrs[idx].access_cb = esl_gatt_access_cb;
|
||||
s_esl_gatts->esl_chrs[idx].val_handle = &s_esl_gatts->chr_handles[idx];
|
||||
s_esl_gatts->esl_chrs[idx].flags = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_READ_ENC;
|
||||
idx++;
|
||||
}
|
||||
|
||||
/* Conditional: Sensor Information (Read, Encrypted) */
|
||||
if (config->num_sensors > 0) {
|
||||
memset(&s_esl_gatts->esl_chrs[idx], 0, sizeof(struct ble_gatt_chr_def));
|
||||
s_esl_gatts->esl_chrs[idx].uuid = &s_uuid_sensor_info.u;
|
||||
s_esl_gatts->esl_chrs[idx].access_cb = esl_gatt_access_cb;
|
||||
s_esl_gatts->esl_chrs[idx].val_handle = &s_esl_gatts->chr_handles[idx];
|
||||
s_esl_gatts->esl_chrs[idx].flags = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_READ_ENC;
|
||||
idx++;
|
||||
}
|
||||
|
||||
/* Conditional: LED Information (Read, Encrypted) */
|
||||
if (config->num_leds > 0) {
|
||||
memset(&s_esl_gatts->esl_chrs[idx], 0, sizeof(struct ble_gatt_chr_def));
|
||||
s_esl_gatts->esl_chrs[idx].uuid = &s_uuid_led_info.u;
|
||||
s_esl_gatts->esl_chrs[idx].access_cb = esl_gatt_access_cb;
|
||||
s_esl_gatts->esl_chrs[idx].val_handle = &s_esl_gatts->chr_handles[idx];
|
||||
s_esl_gatts->esl_chrs[idx].flags = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_READ_ENC;
|
||||
idx++;
|
||||
}
|
||||
|
||||
/* Mandatory: ECP (Write Without Response, Write, Notify, Encrypted) */
|
||||
s_esl_gatts->ecp_chr_idx = idx;
|
||||
memset(&s_esl_gatts->esl_chrs[idx], 0, sizeof(struct ble_gatt_chr_def));
|
||||
s_esl_gatts->esl_chrs[idx].uuid = &s_uuid_ecp.u;
|
||||
s_esl_gatts->esl_chrs[idx].access_cb = esl_gatt_access_cb;
|
||||
s_esl_gatts->esl_chrs[idx].val_handle = &s_esl_gatts->chr_handles[idx];
|
||||
s_esl_gatts->esl_chrs[idx].flags = BLE_GATT_CHR_F_WRITE_NO_RSP | BLE_GATT_CHR_F_WRITE |
|
||||
BLE_GATT_CHR_F_NOTIFY |
|
||||
BLE_GATT_CHR_F_WRITE_ENC;
|
||||
idx++;
|
||||
|
||||
/* Terminator */
|
||||
memset(&s_esl_gatts->esl_chrs[idx], 0, sizeof(struct ble_gatt_chr_def));
|
||||
|
||||
/* Build service definition */
|
||||
memset(s_esl_gatts->esl_svcs, 0, sizeof(s_esl_gatts->esl_svcs));
|
||||
s_esl_gatts->esl_svcs[0].type = BLE_GATT_SVC_TYPE_PRIMARY;
|
||||
s_esl_gatts->esl_svcs[0].uuid = &s_esl_svc_uuid.u;
|
||||
s_esl_gatts->esl_svcs[0].characteristics = s_esl_gatts->esl_chrs;
|
||||
/* s_esl_gatts->esl_svcs[1] is zero-terminated */
|
||||
}
|
||||
|
||||
/* ========================== GATT Access Callback ========================== */
|
||||
|
||||
/**
|
||||
* @brief Helper to extract flat data from an mbuf
|
||||
*/
|
||||
static int esl_gatt_write_flat(struct os_mbuf *om, void *dst,
|
||||
uint16_t min_len, uint16_t max_len,
|
||||
uint16_t *out_len)
|
||||
{
|
||||
uint16_t om_len = OS_MBUF_PKTLEN(om);
|
||||
if (om_len < min_len || om_len > max_len) {
|
||||
return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN;
|
||||
}
|
||||
int rc = ble_hs_mbuf_to_flat(om, dst, max_len, out_len);
|
||||
if (rc != 0) {
|
||||
return BLE_ATT_ERR_UNLIKELY;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Handle write to ESL Address characteristic
|
||||
*/
|
||||
static int handle_write_esl_address(struct os_mbuf *om)
|
||||
{
|
||||
ble_esl_address_t addr;
|
||||
uint16_t len;
|
||||
int rc = esl_gatt_write_flat(om, &addr, sizeof(addr), sizeof(addr), &len);
|
||||
if (rc != 0) {
|
||||
return rc;
|
||||
}
|
||||
|
||||
if (addr.esl_id == BLE_ESL_BROADCAST_ADDRESS) {
|
||||
ESP_LOGW(TAG, "Rejected broadcast address as ESL address");
|
||||
return BLE_ATT_ERR_VALUE_NOT_ALLOWED;
|
||||
}
|
||||
|
||||
s_esl_gatts->ctx->esl_address = addr;
|
||||
s_esl_gatts->ctx->address_valid = true;
|
||||
s_esl_gatts->ctx->config_complete |= CONFIG_BIT_ADDRESS;
|
||||
ESP_LOGI(TAG, "ESL Address set: id=0x%02x group=0x%02x",
|
||||
addr.esl_id, BLE_ESL_ADDR_GROUP_ID(addr));
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Handle write to AP Sync Key Material characteristic
|
||||
*/
|
||||
static int handle_write_ap_sync_key(struct os_mbuf *om)
|
||||
{
|
||||
uint16_t len;
|
||||
int rc = esl_gatt_write_flat(om, &s_esl_gatts->ctx->ap_sync_key,
|
||||
BLE_ESL_KEY_MATERIAL_SIZE,
|
||||
BLE_ESL_KEY_MATERIAL_SIZE, &len);
|
||||
if (rc != 0) {
|
||||
return rc;
|
||||
}
|
||||
|
||||
s_esl_gatts->ctx->ap_sync_key_valid = true;
|
||||
s_esl_gatts->ctx->config_complete |= CONFIG_BIT_AP_SYNC_KEY;
|
||||
ESP_LOGI(TAG, "AP Sync Key Material written");
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Handle write to ESL Response Key Material characteristic
|
||||
*/
|
||||
static int handle_write_resp_key(struct os_mbuf *om)
|
||||
{
|
||||
uint16_t len;
|
||||
int rc = esl_gatt_write_flat(om, &s_esl_gatts->ctx->resp_key,
|
||||
BLE_ESL_KEY_MATERIAL_SIZE,
|
||||
BLE_ESL_KEY_MATERIAL_SIZE, &len);
|
||||
if (rc != 0) {
|
||||
return rc;
|
||||
}
|
||||
|
||||
s_esl_gatts->ctx->resp_key_valid = true;
|
||||
s_esl_gatts->ctx->config_complete |= CONFIG_BIT_RESP_KEY;
|
||||
ESP_LOGI(TAG, "ESL Response Key Material written");
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Handle write to ESL Current Absolute Time characteristic
|
||||
*/
|
||||
static int handle_write_abs_time(struct os_mbuf *om)
|
||||
{
|
||||
uint32_t time_val;
|
||||
uint16_t len;
|
||||
int rc = esl_gatt_write_flat(om, &time_val, sizeof(time_val),
|
||||
sizeof(time_val), &len);
|
||||
if (rc != 0) {
|
||||
return rc;
|
||||
}
|
||||
|
||||
s_esl_gatts->ctx->abs_time_base = time_val;
|
||||
s_esl_gatts->ctx->abs_time_offset_us = esp_timer_get_time();
|
||||
s_esl_gatts->ctx->config_complete |= CONFIG_BIT_ABS_TIME;
|
||||
ESP_LOGI(TAG, "ESL Absolute Time set: %" PRIu32 " ms", time_val);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Handle write to ECP characteristic — decode TLV, dispatch, respond
|
||||
*/
|
||||
static int handle_write_ecp(uint16_t conn_handle, struct os_mbuf *om)
|
||||
{
|
||||
uint8_t buf[BLE_ESL_TLV_MAX_SIZE];
|
||||
uint16_t om_len = OS_MBUF_PKTLEN(om);
|
||||
|
||||
if (om_len < BLE_ESL_TLV_MIN_SIZE || om_len > BLE_ESL_TLV_MAX_SIZE) {
|
||||
ESP_LOGW(TAG, "ECP write: invalid length %u", om_len);
|
||||
return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN;
|
||||
}
|
||||
|
||||
uint16_t flat_len;
|
||||
int rc = ble_hs_mbuf_to_flat(om, buf, sizeof(buf), &flat_len);
|
||||
if (rc != 0) {
|
||||
return BLE_ATT_ERR_UNLIKELY;
|
||||
}
|
||||
|
||||
/* Decode TLV */
|
||||
uint8_t opcode;
|
||||
const uint8_t *params;
|
||||
uint8_t params_len;
|
||||
esp_err_t err = ble_esl_tlv_decode(buf, flat_len, &opcode, ¶ms, ¶ms_len);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGW(TAG, "ECP: TLV decode failed");
|
||||
return BLE_ATT_ERR_UNLIKELY;
|
||||
}
|
||||
|
||||
/* Start ECP procedure timeout timer */
|
||||
esp_timer_stop(s_esl_gatts->ctx->ecp_timer);
|
||||
esp_timer_start_once(s_esl_gatts->ctx->ecp_timer, ESL_ECP_TIMEOUT_US);
|
||||
|
||||
/* Dispatch command */
|
||||
ble_esl_cmd_result_t result;
|
||||
memset(&result, 0, sizeof(result));
|
||||
err = esl_cmd_dispatch(opcode, params, params_len, &result, ESL_TRANSPORT_ECP);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGW(TAG, "ECP: command dispatch failed: %d", (int)err);
|
||||
}
|
||||
|
||||
/* Send response notification if command produced one */
|
||||
if (result.has_response) {
|
||||
/* Stop ECP timeout — response is being sent now */
|
||||
esp_timer_stop(s_esl_gatts->ctx->ecp_timer);
|
||||
uint8_t resp_buf[BLE_ESL_TLV_MAX_SIZE];
|
||||
uint8_t resp_len;
|
||||
err = ble_esl_tlv_encode(result.resp_opcode, result.resp_params,
|
||||
result.resp_params_len,
|
||||
resp_buf, &resp_len);
|
||||
if (err != ESP_OK) {
|
||||
/* The AP must always get a response (the timeout timer is already
|
||||
* stopped), so fall back to an Unspecified Error TLV — same
|
||||
* defensive handling as the PAwR transport. */
|
||||
ESP_LOGW(TAG, "ECP: response TLV encode failed, sending Unspecified Error");
|
||||
resp_buf[0] = BLE_ESL_RESP_ERROR;
|
||||
resp_buf[1] = BLE_ESL_ERR_UNSPECIFIED;
|
||||
resp_len = ESL_ECP_ERROR_RESP_SIZE;
|
||||
}
|
||||
err = esl_send_ecp_response(resp_buf, resp_len);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGW(TAG, "ECP: response send failed: %d", (int)err);
|
||||
/* No notification was enqueued, so neither the notify callback nor
|
||||
* the notify-tx event will run the deferred Unassociate cleanup. */
|
||||
if (esl_is_unassociate_pending()) {
|
||||
esl_execute_unassociate_cleanup();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
/* No response expected — stop the ECP timeout timer */
|
||||
esp_timer_stop(s_esl_gatts->ctx->ecp_timer);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Handle read of Display Information characteristic
|
||||
*/
|
||||
static int handle_read_display_info(struct ble_gatt_access_ctxt *ctxt)
|
||||
{
|
||||
if (s_esl_gatts->ctx->config.display_info == NULL || s_esl_gatts->ctx->config.num_displays == 0) {
|
||||
return BLE_ATT_ERR_UNLIKELY;
|
||||
}
|
||||
uint16_t len = (uint16_t)(s_esl_gatts->ctx->config.num_displays * sizeof(ble_esl_display_info_t));
|
||||
int rc = os_mbuf_append(ctxt->om, s_esl_gatts->ctx->config.display_info, len);
|
||||
return (rc == 0) ? 0 : BLE_ATT_ERR_INSUFFICIENT_RES;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Handle read of Image Information characteristic
|
||||
*/
|
||||
static int handle_read_image_info(struct ble_gatt_access_ctxt *ctxt)
|
||||
{
|
||||
if (s_esl_gatts->ctx->config.num_images == 0) {
|
||||
return BLE_ATT_ERR_UNLIKELY;
|
||||
}
|
||||
uint8_t max_image_index = s_esl_gatts->ctx->config.num_images - 1;
|
||||
int rc = os_mbuf_append(ctxt->om, &max_image_index, 1);
|
||||
return (rc == 0) ? 0 : BLE_ATT_ERR_INSUFFICIENT_RES;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Handle read of Sensor Information characteristic
|
||||
*/
|
||||
static int handle_read_sensor_info(struct ble_gatt_access_ctxt *ctxt)
|
||||
{
|
||||
if (s_esl_gatts->ctx->config.sensor_info == NULL || s_esl_gatts->ctx->config.sensor_info_len == 0) {
|
||||
return BLE_ATT_ERR_UNLIKELY;
|
||||
}
|
||||
int rc = os_mbuf_append(ctxt->om, s_esl_gatts->ctx->config.sensor_info,
|
||||
s_esl_gatts->ctx->config.sensor_info_len);
|
||||
return (rc == 0) ? 0 : BLE_ATT_ERR_INSUFFICIENT_RES;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Handle read of LED Information characteristic
|
||||
*/
|
||||
static int handle_read_led_info(struct ble_gatt_access_ctxt *ctxt)
|
||||
{
|
||||
if (s_esl_gatts->ctx->config.led_info == NULL || s_esl_gatts->ctx->config.num_leds == 0) {
|
||||
return BLE_ATT_ERR_UNLIKELY;
|
||||
}
|
||||
int rc = os_mbuf_append(ctxt->om, s_esl_gatts->ctx->config.led_info,
|
||||
s_esl_gatts->ctx->config.num_leds);
|
||||
return (rc == 0) ? 0 : BLE_ATT_ERR_INSUFFICIENT_RES;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief GATT access callback for all ESL Service characteristics
|
||||
*/
|
||||
static int esl_gatt_access_cb(uint16_t conn_handle, uint16_t attr_handle,
|
||||
struct ble_gatt_access_ctxt *ctxt, void *arg)
|
||||
{
|
||||
if (s_esl_gatts == NULL || s_esl_gatts->ctx == NULL) {
|
||||
ESP_LOGW(TAG, "GATT access while context is NULL");
|
||||
return BLE_ATT_ERR_UNLIKELY;
|
||||
}
|
||||
|
||||
const ble_uuid_t *uuid;
|
||||
|
||||
switch (ctxt->op) {
|
||||
case BLE_GATT_ACCESS_OP_WRITE_CHR:
|
||||
uuid = ctxt->chr->uuid;
|
||||
if (ble_uuid_cmp(uuid, &s_uuid_address.u) == 0 ||
|
||||
ble_uuid_cmp(uuid, &s_uuid_ap_sync.u) == 0 ||
|
||||
ble_uuid_cmp(uuid, &s_uuid_resp_key.u) == 0 ||
|
||||
ble_uuid_cmp(uuid, &s_uuid_abs_time.u) == 0) {
|
||||
/* ESL spec: configurable characteristics may only be written
|
||||
* in Configuring or Updating states */
|
||||
if (s_esl_gatts->ctx->state != BLE_ESL_STATE_CONFIGURING &&
|
||||
s_esl_gatts->ctx->state != BLE_ESL_STATE_UPDATING) {
|
||||
ESP_LOGW(TAG, "GATT write rejected: invalid state %d", s_esl_gatts->ctx->state);
|
||||
return BLE_ATT_ERR_WRITE_NOT_PERMITTED;
|
||||
}
|
||||
}
|
||||
if (ble_uuid_cmp(uuid, &s_uuid_address.u) == 0) {
|
||||
return handle_write_esl_address(ctxt->om);
|
||||
}
|
||||
if (ble_uuid_cmp(uuid, &s_uuid_ap_sync.u) == 0) {
|
||||
return handle_write_ap_sync_key(ctxt->om);
|
||||
}
|
||||
if (ble_uuid_cmp(uuid, &s_uuid_resp_key.u) == 0) {
|
||||
return handle_write_resp_key(ctxt->om);
|
||||
}
|
||||
if (ble_uuid_cmp(uuid, &s_uuid_abs_time.u) == 0) {
|
||||
return handle_write_abs_time(ctxt->om);
|
||||
}
|
||||
if (ble_uuid_cmp(uuid, &s_uuid_ecp.u) == 0) {
|
||||
return handle_write_ecp(conn_handle, ctxt->om);
|
||||
}
|
||||
break;
|
||||
|
||||
case BLE_GATT_ACCESS_OP_READ_CHR:
|
||||
uuid = ctxt->chr->uuid;
|
||||
if (ble_uuid_cmp(uuid, &s_uuid_disp_info.u) == 0) {
|
||||
return handle_read_display_info(ctxt);
|
||||
}
|
||||
if (ble_uuid_cmp(uuid, &s_uuid_img_info.u) == 0) {
|
||||
return handle_read_image_info(ctxt);
|
||||
}
|
||||
if (ble_uuid_cmp(uuid, &s_uuid_sensor_info.u) == 0) {
|
||||
return handle_read_sensor_info(ctxt);
|
||||
}
|
||||
if (ble_uuid_cmp(uuid, &s_uuid_led_info.u) == 0) {
|
||||
return handle_read_led_info(ctxt);
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return BLE_ATT_ERR_UNLIKELY;
|
||||
}
|
||||
|
||||
/* ========================== OTS Setup ========================== */
|
||||
|
||||
#if CONFIG_BLE_ESL_OTS_SUPPORT
|
||||
|
||||
/* OTS object-ID reverse map, count and init state live in esl_gatts_ctx_t. */
|
||||
|
||||
/**
|
||||
* @brief OTS event callback — handles write-complete events,
|
||||
* maps object ID to image index and notifies app
|
||||
*/
|
||||
static void esl_ots_write_cb(ble_ots_server_event_t event,
|
||||
ble_ots_server_cb_param_t *param)
|
||||
{
|
||||
if (event != BLE_OTS_SERVER_EVT_WRITE_COMPLETE || param == NULL) {
|
||||
return;
|
||||
}
|
||||
|
||||
ble_ots_obj_id_t obj_id = param->write_complete.object_id;
|
||||
uint32_t offset = param->write_complete.offset;
|
||||
uint32_t length = param->write_complete.bytes_received;
|
||||
|
||||
if (s_esl_gatts == NULL || s_esl_gatts->ctx == NULL) {
|
||||
return;
|
||||
}
|
||||
/* Map obj_id back to image_index through the reverse map built by
|
||||
* esl_setup_ots(). The OTS server assigns IDs from its own monotonic
|
||||
* counter, so they are not guaranteed to start at
|
||||
* BLE_ESL_OTS_OBJECT_ID_BASE nor to be contiguous. */
|
||||
uint8_t image_index = 0;
|
||||
bool found = false;
|
||||
for (uint8_t i = 0; i < s_esl_gatts->ots_obj_count; i++) {
|
||||
if (s_esl_gatts->ots_obj_ids[i] == obj_id) {
|
||||
image_index = i;
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found) {
|
||||
ESP_LOGW(TAG, "OTS write: unknown obj_id=0x%06llx", (unsigned long long)obj_id);
|
||||
return;
|
||||
}
|
||||
|
||||
ble_esl_cb_param_t cb_param = {
|
||||
.image_write = {
|
||||
.image_index = image_index,
|
||||
.data = NULL,
|
||||
.length = length,
|
||||
.offset = offset,
|
||||
}
|
||||
};
|
||||
esl_notify_app(BLE_ESL_EVT_IMAGE_WRITE, &cb_param);
|
||||
}
|
||||
|
||||
static esp_err_t esl_setup_ots(const ble_esl_config_t *config)
|
||||
{
|
||||
if (config->num_images == 0) {
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
s_esl_gatts->ots_obj_count = 0;
|
||||
|
||||
ble_ots_server_config_t ots_config = {
|
||||
.oacp_features = BLE_OTS_OACP_FEAT_WRITE | BLE_OTS_OACP_FEAT_TRUNCATE,
|
||||
.olcp_features = BLE_OTS_OLCP_FEAT_GO_TO,
|
||||
.supported_types = NULL,
|
||||
.num_supported_types = 0,
|
||||
.include_obj_first_created = false,
|
||||
.include_obj_last_modified = false,
|
||||
.include_obj_changed = false,
|
||||
.include_obj_list_filter = false,
|
||||
.has_realtime_clock = false,
|
||||
.obj_name_writable = false,
|
||||
.obj_properties_writable = false,
|
||||
};
|
||||
|
||||
int rc = ble_ots_server_init(&ots_config);
|
||||
if (rc != 0) {
|
||||
ESP_LOGE(TAG, "OTS server init failed: rc=%d", rc);
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
s_esl_gatts->ots_initialized = true;
|
||||
|
||||
/* Pre-create objects for each image slot */
|
||||
for (uint8_t i = 0; i < config->num_images; i++) {
|
||||
char name[16];
|
||||
snprintf(name, sizeof(name), "Image_%u", i);
|
||||
|
||||
uint32_t props = BLE_OTS_OBJ_PROP_READ;
|
||||
if (config->image_writable_mask != NULL && config->image_writable_mask[i]) {
|
||||
props |= BLE_OTS_OBJ_PROP_WRITE | BLE_OTS_OBJ_PROP_TRUNCATE;
|
||||
}
|
||||
|
||||
ble_ots_obj_type_entry_t obj_type = { .uuid_len = 2, .uuid = {0} };
|
||||
ble_ots_server_obj_params_t obj_params = {
|
||||
.name = name,
|
||||
.name_len = strlen(name),
|
||||
.type = obj_type,
|
||||
.properties = props,
|
||||
.first_created = {0},
|
||||
.last_modified = {0},
|
||||
.data = NULL,
|
||||
.data_len = 0,
|
||||
.allocated_size = CONFIG_BLE_ESL_MAX_IMAGE_SIZE,
|
||||
};
|
||||
|
||||
ble_ots_obj_id_t obj_id;
|
||||
rc = ble_ots_server_add_object(&obj_params, &obj_id);
|
||||
if (rc != 0) {
|
||||
ESP_LOGE(TAG, "OTS add object %u failed: rc=%d", i, rc);
|
||||
return ESP_FAIL;
|
||||
}
|
||||
if (s_esl_gatts->ots_obj_count < CONFIG_BLE_ESL_MAX_IMAGES) {
|
||||
s_esl_gatts->ots_obj_ids[s_esl_gatts->ots_obj_count++] = obj_id;
|
||||
}
|
||||
ESP_LOGI(TAG, "OTS object created: Image_%u, obj_id=0x%06llx",
|
||||
i, (unsigned long long)obj_id);
|
||||
}
|
||||
|
||||
/* Register write callback for image data reception */
|
||||
rc = ble_ots_server_register_cb(esl_ots_write_cb);
|
||||
if (rc != 0) {
|
||||
ESP_LOGE(TAG, "OTS register write cb failed: rc=%d", rc);
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
static void esl_teardown_ots(void)
|
||||
{
|
||||
if (s_esl_gatts->ots_initialized) {
|
||||
ble_ots_server_deinit();
|
||||
s_esl_gatts->ots_initialized = false;
|
||||
}
|
||||
}
|
||||
#endif /* CONFIG_BLE_ESL_OTS_SUPPORT */
|
||||
|
||||
/* ========================== DIS PnP ID Setup ========================== */
|
||||
|
||||
#if CONFIG_BLE_ESL_VENDOR_SPECIFIC
|
||||
/**
|
||||
* @brief Static buffer for PnP ID to work around NimBLE DIS strlen limitation.
|
||||
*
|
||||
* NimBLE's ble_svc_dis uses strlen() on the PnP ID pointer, which truncates
|
||||
* binary data containing 0x00 bytes. We copy the 7-octet PnP ID into a static
|
||||
* buffer padded with 0xFF to avoid premature truncation by strlen, and cap at
|
||||
* 7 bytes on the read side (NimBLE clamps len to 7).
|
||||
*/
|
||||
static uint8_t s_pnp_id_buf[8]; /* 7 octets + padding byte */
|
||||
|
||||
static esp_err_t esl_setup_dis_pnp(const ble_esl_config_t *config)
|
||||
{
|
||||
if (!config->vendor_specific || config->pnp_id == NULL) {
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
ble_svc_dis_init();
|
||||
|
||||
/* Copy 7-octet binary PnP ID and pad with non-zero byte to prevent
|
||||
* strlen from reading past the buffer. Note: if any of the 7 data
|
||||
* bytes is 0x00, NimBLE's strlen-based read will still truncate the
|
||||
* value. This is a known NimBLE limitation for binary characteristics. */
|
||||
memcpy(s_pnp_id_buf, config->pnp_id, 7);
|
||||
s_pnp_id_buf[7] = 0xFF; /* non-zero padding to bound strlen */
|
||||
|
||||
/* Check for embedded zero bytes and warn */
|
||||
for (int i = 0; i < 7; i++) {
|
||||
if (s_pnp_id_buf[i] == 0x00) {
|
||||
ESP_LOGW(TAG, "PnP ID contains 0x00 at offset %d — NimBLE DIS may "
|
||||
"truncate the value due to strlen usage", i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
int rc = ble_svc_dis_pnp_id_set((const char *)s_pnp_id_buf);
|
||||
if (rc != 0) {
|
||||
ESP_LOGE(TAG, "DIS PnP ID set failed: rc=%d", rc);
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "DIS PnP ID registered");
|
||||
return ESP_OK;
|
||||
}
|
||||
#endif /* CONFIG_BLE_ESL_VENDOR_SPECIFIC */
|
||||
|
||||
/* ========================== ECP Response ========================== */
|
||||
|
||||
/*
|
||||
* The ECP response notification must not be sent synchronously from within a
|
||||
* GATT access callback (the ATT Write Response for the ECP write has not yet
|
||||
* been transmitted). The notification mbuf is built up-front (the caller's
|
||||
* TLV buffer may live on the stack) and the actual ble_gatts_notify_custom()
|
||||
* call is deferred to a NimBLE host-task event.
|
||||
*/
|
||||
struct esl_ecp_notify_ctx {
|
||||
struct ble_npl_event ev;
|
||||
uint16_t conn_handle;
|
||||
uint16_t val_handle;
|
||||
struct os_mbuf *om;
|
||||
};
|
||||
|
||||
static void esl_ecp_notify_event_cb(struct ble_npl_event *ev)
|
||||
{
|
||||
struct esl_ecp_notify_ctx *ctx =
|
||||
(struct esl_ecp_notify_ctx *)ble_npl_event_get_arg(ev);
|
||||
if (ctx == NULL) {
|
||||
return;
|
||||
}
|
||||
|
||||
/* ble_gatts_notify_custom() consumes the mbuf on all return paths. */
|
||||
int rc = ble_gatts_notify_custom(ctx->conn_handle, ctx->val_handle, ctx->om);
|
||||
if (rc != 0) {
|
||||
ESP_LOGW(TAG, "ECP response: notify failed rc=%d", rc);
|
||||
if (esl_is_unassociate_pending()) {
|
||||
esl_execute_unassociate_cleanup();
|
||||
}
|
||||
}
|
||||
ble_npl_event_deinit(ev);
|
||||
free(ctx);
|
||||
}
|
||||
|
||||
void esl_handle_ecp_notify_tx(uint16_t conn_handle, uint16_t attr_handle,
|
||||
int status, uint8_t indication)
|
||||
{
|
||||
if (s_esl_gatts == NULL || s_esl_gatts->ctx == NULL || indication ||
|
||||
s_esl_gatts->ctx->conn_handle != conn_handle ||
|
||||
attr_handle != s_esl_gatts->chr_handles[s_esl_gatts->ecp_chr_idx] ||
|
||||
!esl_is_unassociate_pending()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (status != 0) {
|
||||
ESP_LOGW(TAG, "ECP response notify-tx failed: status=%d", status);
|
||||
}
|
||||
|
||||
esl_execute_unassociate_cleanup();
|
||||
}
|
||||
|
||||
esp_err_t esl_send_ecp_response(const uint8_t *response_tlv, uint8_t response_len)
|
||||
{
|
||||
if (s_esl_gatts == NULL || s_esl_gatts->ctx == NULL ||
|
||||
response_tlv == NULL || response_len == 0) {
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
|
||||
/* Stop ECP procedure timeout — response is being sent */
|
||||
esp_timer_stop(s_esl_gatts->ctx->ecp_timer);
|
||||
|
||||
if (s_esl_gatts->ctx->conn_handle == BLE_HS_CONN_HANDLE_NONE) {
|
||||
ESP_LOGW(TAG, "ECP response: no active connection");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
struct os_mbuf *om = ble_hs_mbuf_from_flat(response_tlv, response_len);
|
||||
if (om == NULL) {
|
||||
ESP_LOGE(TAG, "ECP response: mbuf alloc failed");
|
||||
return ESP_ERR_NO_MEM;
|
||||
}
|
||||
|
||||
/* Defer the actual notification to a NimBLE host-task event so it is not
|
||||
* sent from within a GATT access callback. */
|
||||
struct esl_ecp_notify_ctx *ctx = malloc(sizeof(*ctx));
|
||||
if (ctx == NULL) {
|
||||
os_mbuf_free_chain(om);
|
||||
ESP_LOGE(TAG, "ECP response: event context alloc failed");
|
||||
return ESP_ERR_NO_MEM;
|
||||
}
|
||||
ctx->conn_handle = s_esl_gatts->ctx->conn_handle;
|
||||
ctx->val_handle = s_esl_gatts->chr_handles[s_esl_gatts->ecp_chr_idx];
|
||||
ctx->om = om;
|
||||
ble_npl_event_init(&ctx->ev, esl_ecp_notify_event_cb, ctx);
|
||||
ble_npl_eventq_put(nimble_port_get_dflt_eventq(), &ctx->ev);
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t esl_gatts_init(const ble_esl_config_t *config)
|
||||
{
|
||||
esp_err_t err;
|
||||
|
||||
if (s_esl_gatts != NULL) {
|
||||
ESP_LOGW(TAG, "esl_gatts already initialized");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
/* Allocate the module context once. NimBLE will retain pointers into the
|
||||
* GATT tables held here, so it must persist until esl_gatts_deinit(). */
|
||||
s_esl_gatts = calloc(1, sizeof(*s_esl_gatts));
|
||||
if (s_esl_gatts == NULL) {
|
||||
ESP_LOGE(TAG, "Failed to allocate esl_gatts context");
|
||||
return ESP_ERR_NO_MEM;
|
||||
}
|
||||
s_esl_gatts->ecp_chr_idx = CHR_IDX_ECP;
|
||||
|
||||
s_esl_gatts->ctx = esl_state_ctx_get();
|
||||
if (s_esl_gatts->ctx == NULL) {
|
||||
free(s_esl_gatts);
|
||||
s_esl_gatts = NULL;
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
/* Build and register GATT service */
|
||||
esl_build_gatt_svcs(config);
|
||||
|
||||
int rc = ble_gatts_count_cfg(s_esl_gatts->esl_svcs);
|
||||
if (rc != 0) {
|
||||
free(s_esl_gatts);
|
||||
s_esl_gatts = NULL;
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
rc = ble_gatts_add_svcs(s_esl_gatts->esl_svcs);
|
||||
if (rc != 0) {
|
||||
free(s_esl_gatts);
|
||||
s_esl_gatts = NULL;
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
#if CONFIG_BLE_ESL_OTS_SUPPORT
|
||||
err = esl_setup_ots(config);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "OTS setup failed");
|
||||
/* Leave the module in its pre-init state: drop any partial OTS state
|
||||
* and the already-registered GATT service. */
|
||||
esl_teardown_ots();
|
||||
ble_gatts_delete_svc(&s_esl_svc_uuid.u);
|
||||
free(s_esl_gatts);
|
||||
s_esl_gatts = NULL;
|
||||
return err;
|
||||
}
|
||||
#endif
|
||||
|
||||
#if CONFIG_BLE_ESL_VENDOR_SPECIFIC
|
||||
err = esl_setup_dis_pnp(config);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGW(TAG, "DIS PnP setup failed (non-fatal)");
|
||||
}
|
||||
#endif
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t esl_gatts_deinit(void)
|
||||
{
|
||||
if (s_esl_gatts == NULL) {
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
#if CONFIG_BLE_ESL_OTS_SUPPORT
|
||||
esl_teardown_ots();
|
||||
#endif
|
||||
|
||||
/* Remove GATT service */
|
||||
ble_gatts_delete_svc(&s_esl_svc_uuid.u);
|
||||
|
||||
free(s_esl_gatts);
|
||||
s_esl_gatts = NULL;
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
627
components/bt/ble_profiles/nimble/ble_esl/src/esl/esl_pawr.c
Normal file
627
components/bt/ble_profiles/nimble/ble_esl/src/esl/esl_pawr.c
Normal file
@@ -0,0 +1,627 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
/**
|
||||
* @file esl_pawr.c
|
||||
* @brief ESL PAwR (Periodic Advertising with Responses) module
|
||||
*
|
||||
* Handles PAwR sync packet reception, command dispatch, response slot
|
||||
* calculation, response assembly with capacity-limit handling, AES-CCM
|
||||
* encryption, and response transmission via NimBLE PAwR response API.
|
||||
*/
|
||||
|
||||
#include <string.h>
|
||||
#include <assert.h>
|
||||
|
||||
#include "esp_log.h"
|
||||
#include "esp_err.h"
|
||||
|
||||
#include "nimble/ble.h"
|
||||
#include "host/ble_hs.h"
|
||||
#include "host/ble_gap.h"
|
||||
#include "os/os_mbuf.h"
|
||||
|
||||
#include "host/ble_ead.h"
|
||||
|
||||
#include "ble_esl.h"
|
||||
#include "ble_esl_common.h"
|
||||
#include "ble_esl_int.h"
|
||||
|
||||
static const char *TAG = "esl_pawr";
|
||||
|
||||
/* ========================== Constants ========================== */
|
||||
|
||||
/** Maximum response ESL Payload size in octets */
|
||||
#define ESL_PAWR_MAX_RESPONSE_PAYLOAD BLE_ESL_PAYLOAD_MAX_SIZE /* 48 */
|
||||
|
||||
/** Error response TLV size: opcode (1) + error_code (1) = 2 bytes */
|
||||
#define ESL_ERROR_RESP_SIZE 2
|
||||
|
||||
/** Error response opcode byte */
|
||||
#define ESL_ERROR_RESP_OPCODE BLE_ESL_RESP_ERROR /* 0x00 */
|
||||
|
||||
|
||||
/* ========================== Internal Context ========================== */
|
||||
|
||||
/**
|
||||
* @brief PAwR module internal state
|
||||
*/
|
||||
typedef struct {
|
||||
uint8_t resp_buf[ESL_PAWR_MAX_RESPONSE_PAYLOAD]; /*!< Response assembly buffer */
|
||||
uint8_t response_randomizer[BLE_ESL_RANDOMIZER_SIZE]; /*!< ESL -> AP EAD Randomizer */
|
||||
uint16_t sync_handle; /*!< Sync handle for the current PAwR sync (set externally before processing) */
|
||||
uint16_t event_counter; /*!< Event counter for the current PAwR subevent report */
|
||||
uint8_t subevent; /*!< Subevent number for the current PAwR report */
|
||||
} esl_pawr_ctx_t;
|
||||
|
||||
static esl_pawr_ctx_t *s_esl_pawr = NULL;
|
||||
|
||||
/* ========================== Forward Declarations ========================== */
|
||||
|
||||
static void build_error_response_tlv(uint8_t error_code, uint8_t *out_buf,
|
||||
uint8_t *out_len);
|
||||
static bool is_forbidden_sync_opcode(uint8_t opcode);
|
||||
|
||||
/* ========================== Helper Functions ========================== */
|
||||
|
||||
/**
|
||||
* @brief Build a 2-byte Error response TLV
|
||||
*
|
||||
* @param[in] error_code ESL error code
|
||||
* @param[out] out_buf Output buffer (min 2 bytes)
|
||||
* @param[out] out_len Set to 2 on output
|
||||
*/
|
||||
static void build_error_response_tlv(uint8_t error_code, uint8_t *out_buf,
|
||||
uint8_t *out_len)
|
||||
{
|
||||
/* Error response: opcode = 0x00 (Tag=0, Length=0), param = error_code */
|
||||
out_buf[0] = ESL_ERROR_RESP_OPCODE;
|
||||
out_buf[1] = error_code;
|
||||
*out_len = ESL_ERROR_RESP_SIZE;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Check if an opcode is forbidden in the Synchronized state
|
||||
*
|
||||
* Factory Reset (0x03) and Update Complete (0x04) are not allowed via PAwR.
|
||||
*
|
||||
* @param[in] opcode Command opcode byte
|
||||
* @return true if the opcode is forbidden
|
||||
*/
|
||||
static bool is_forbidden_sync_opcode(uint8_t opcode)
|
||||
{
|
||||
return (opcode == BLE_ESL_CMD_FACTORY_RESET ||
|
||||
opcode == BLE_ESL_CMD_UPDATE_COMPLETE);
|
||||
}
|
||||
|
||||
/* ========================== Public Functions ========================== */
|
||||
|
||||
esp_err_t esl_pawr_init(void)
|
||||
{
|
||||
if (s_esl_pawr != NULL) {
|
||||
ESP_LOGW(TAG, "PAwR module already initialized");
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
|
||||
s_esl_pawr = calloc(1, sizeof(esl_pawr_ctx_t));
|
||||
if (s_esl_pawr == NULL) {
|
||||
ESP_LOGE(TAG, "Failed to allocate PAwR context");
|
||||
return ESP_ERR_NO_MEM;
|
||||
}
|
||||
|
||||
ble_esl_ead_randomizer_init(s_esl_pawr->response_randomizer,
|
||||
BLE_ESL_EAD_DIR_ESL_TO_AP);
|
||||
|
||||
ESP_LOGI(TAG, "PAwR module initialized");
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
void esl_pawr_deinit(void)
|
||||
{
|
||||
if (s_esl_pawr != NULL) {
|
||||
free(s_esl_pawr);
|
||||
s_esl_pawr = NULL;
|
||||
}
|
||||
ESP_LOGI(TAG, "PAwR module deinitialized");
|
||||
}
|
||||
|
||||
void esl_pawr_set_sync_context(uint16_t sync_handle, uint16_t event_counter)
|
||||
{
|
||||
if (s_esl_pawr == NULL) {
|
||||
ESP_LOGE(TAG, "PAwR module not initialized");
|
||||
return;
|
||||
}
|
||||
|
||||
s_esl_pawr->sync_handle = sync_handle;
|
||||
s_esl_pawr->event_counter = event_counter;
|
||||
}
|
||||
|
||||
int16_t esl_pawr_calc_response_slot(const uint8_t *data,
|
||||
const uint8_t *tlv_offsets,
|
||||
const uint8_t *tlv_lens,
|
||||
uint8_t tlv_count, uint8_t esl_id)
|
||||
{
|
||||
/*
|
||||
* Response slot allocation algorithm:
|
||||
* 1. Number TLVs sequentially 1..N (all TLVs, including broadcast).
|
||||
* 2. Skip broadcast TLVs (ESL_ID = 0xFF) — they don't produce responses.
|
||||
* 3. Find the last individually-addressed TLV for this esl_id.
|
||||
* 4. If that TLV is number N (1-based), response slot = N - 1 (0-based).
|
||||
*/
|
||||
if (data == NULL || tlv_offsets == NULL ||
|
||||
tlv_lens == NULL || tlv_count == 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
int16_t last_tlv_number = -1; /* 1-based TLV number of last match */
|
||||
|
||||
for (uint8_t i = 0; i < tlv_count; i++) {
|
||||
/*
|
||||
* TLV layout in the payload: [opcode (1 byte)] [params (Length+1 bytes)]
|
||||
* ESL_ID is the first parameter byte, located at payload[tlv_offsets[i] + 1].
|
||||
*/
|
||||
if (tlv_lens[i] < BLE_ESL_TLV_MIN_SIZE) {
|
||||
/* TLV too short to contain ESL_ID — skip */
|
||||
continue;
|
||||
}
|
||||
|
||||
uint8_t cmd_esl_id = data[tlv_offsets[i] + 1];
|
||||
|
||||
/* Skip broadcast TLVs — they don't count for slot allocation */
|
||||
if (cmd_esl_id == BLE_ESL_BROADCAST_ADDRESS) {
|
||||
continue;
|
||||
}
|
||||
|
||||
/* Check if addressed to our ESL */
|
||||
if (cmd_esl_id == esl_id) {
|
||||
last_tlv_number = (int16_t)(i + 1); /* 1-based */
|
||||
}
|
||||
}
|
||||
|
||||
if (last_tlv_number < 0) {
|
||||
return -1; /* Not individually addressed */
|
||||
}
|
||||
|
||||
return (int16_t)(last_tlv_number - 1); /* 0-based slot */
|
||||
}
|
||||
|
||||
esp_err_t esl_pawr_assemble_response(const uint8_t *resp_tlvs[],
|
||||
const uint8_t resp_tlv_lens[],
|
||||
uint8_t resp_count,
|
||||
uint8_t *out_buf, uint8_t *out_len)
|
||||
{
|
||||
if (resp_tlvs == NULL || resp_tlv_lens == NULL ||
|
||||
out_buf == NULL || out_len == NULL) {
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
|
||||
if (resp_count == 0) {
|
||||
*out_len = 0;
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
/*
|
||||
* First pass: calculate total size to determine if capacity limit
|
||||
* substitution is needed.
|
||||
*/
|
||||
uint16_t total_size = 0;
|
||||
for (uint8_t i = 0; i < resp_count; i++) {
|
||||
total_size += resp_tlv_lens[i];
|
||||
}
|
||||
|
||||
if (total_size <= ESL_PAWR_MAX_RESPONSE_PAYLOAD) {
|
||||
/* All responses fit — concatenate directly */
|
||||
uint8_t offset = 0;
|
||||
for (uint8_t i = 0; i < resp_count; i++) {
|
||||
memcpy(out_buf + offset, resp_tlvs[i], resp_tlv_lens[i]);
|
||||
offset += resp_tlv_lens[i];
|
||||
}
|
||||
*out_len = offset;
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
/*
|
||||
* Responses exceed 48 bytes. Substitute trailing TLVs with
|
||||
* Error: Capacity Limit (2 bytes: opcode 0x00, param 0x07).
|
||||
*
|
||||
* Strategy: keep as many leading TLVs as possible, then replace
|
||||
* the rest with 2-byte Capacity Limit error responses.
|
||||
*/
|
||||
uint8_t offset = 0;
|
||||
uint8_t keep_count = 0;
|
||||
|
||||
/*
|
||||
* Determine how many TLVs from the front we can keep while still
|
||||
* fitting the remaining substituted error TLVs within the limit.
|
||||
*/
|
||||
for (uint8_t i = 0; i < resp_count; i++) {
|
||||
/* How many TLVs remain after this one (to be substituted) */
|
||||
uint8_t remaining_subs = resp_count - i - 1;
|
||||
uint16_t remaining_error_bytes = (uint16_t)remaining_subs * ESL_ERROR_RESP_SIZE;
|
||||
uint16_t projected = (uint16_t)offset + resp_tlv_lens[i] + remaining_error_bytes;
|
||||
|
||||
if (projected <= ESL_PAWR_MAX_RESPONSE_PAYLOAD) {
|
||||
/* This TLV can be kept */
|
||||
memcpy(out_buf + offset, resp_tlvs[i], resp_tlv_lens[i]);
|
||||
offset += resp_tlv_lens[i];
|
||||
keep_count = i + 1;
|
||||
} else {
|
||||
/* This TLV and all subsequent must be replaced */
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/* Fill remaining slots with Capacity Limit error responses */
|
||||
for (uint8_t i = keep_count; i < resp_count; i++) {
|
||||
if ((uint16_t)offset + ESL_ERROR_RESP_SIZE > ESL_PAWR_MAX_RESPONSE_PAYLOAD) {
|
||||
/* Should not happen if algorithm is correct, but guard anyway */
|
||||
break;
|
||||
}
|
||||
out_buf[offset++] = ESL_ERROR_RESP_OPCODE;
|
||||
out_buf[offset++] = BLE_ESL_ERR_CAPACITY_LIMIT;
|
||||
}
|
||||
|
||||
*out_len = offset;
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t esl_pawr_encrypt_and_send(uint8_t slot, const uint8_t *payload,
|
||||
uint8_t payload_len)
|
||||
{
|
||||
if (s_esl_pawr == NULL) {
|
||||
ESP_LOGE(TAG, "PAwR module not initialized");
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
|
||||
if (payload == NULL || payload_len == 0) {
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
|
||||
if (payload_len > ESL_PAWR_MAX_RESPONSE_PAYLOAD) {
|
||||
ESP_LOGE(TAG, "payload_len %u exceeds max %u", payload_len, ESL_PAWR_MAX_RESPONSE_PAYLOAD);
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
|
||||
/* Get the ESL Response Key Material */
|
||||
const ble_esl_key_material_t *resp_key = esl_get_resp_key();
|
||||
if (resp_key == NULL) {
|
||||
ESP_LOGE(TAG, "Response key material not configured");
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
|
||||
/*
|
||||
* Step 1: Build inner AD structure (plaintext to encrypt)
|
||||
* [inner_len (1)] [ESL Tag 0x34 (1)] [ESL Payload (payload_len)]
|
||||
* inner_len = 1 (tag) + payload_len
|
||||
* The AP strips this wrapper after decryption (ble_esl_ap_pawr_parse_response),
|
||||
* mirroring the AP transmit path (ble_esl_ap_pawr_send). Without it the AP
|
||||
* reads the first response TLV bytes as the inner Len/Tag and rejects the
|
||||
* response (tag != 0x34).
|
||||
*/
|
||||
uint8_t inner_len = 1 + payload_len; /* ESL Tag + ESL Payload */
|
||||
uint8_t plaintext_len = 1 + inner_len; /* Len octet + inner_len */
|
||||
uint8_t plaintext[1 + 1 + ESL_PAWR_MAX_RESPONSE_PAYLOAD];
|
||||
|
||||
plaintext[0] = inner_len;
|
||||
plaintext[1] = BLE_ESL_AD_TYPE_ESL; /* 0x34 */
|
||||
memcpy(&plaintext[2], payload, payload_len);
|
||||
|
||||
/*
|
||||
* Step 2: Output packet layout (outer AD structure)
|
||||
* [AD Length (1)] [AD Type 0x31 (1)] [Randomizer (5)] [Ciphertext (plaintext_len)] [MIC (4)]
|
||||
*
|
||||
* AD Length = 1 (type) + 5 (randomizer) + plaintext_len (ciphertext) + 4 (MIC)
|
||||
*/
|
||||
uint16_t ad_length = 1 + BLE_EAD_RANDOMIZER_SIZE + plaintext_len + BLE_EAD_MIC_SIZE;
|
||||
uint16_t pkt_total = 1 + ad_length; /* 1 byte for AD Length field itself */
|
||||
uint8_t pkt_buf[1 + 1 + BLE_EAD_RANDOMIZER_SIZE + (1 + 1 + ESL_PAWR_MAX_RESPONSE_PAYLOAD) + BLE_EAD_MIC_SIZE];
|
||||
uint8_t pkt_len = 0;
|
||||
|
||||
assert(pkt_total <= sizeof(pkt_buf));
|
||||
|
||||
/* Outer AD structure header */
|
||||
pkt_buf[pkt_len++] = ad_length;
|
||||
pkt_buf[pkt_len++] = BLE_ESL_AD_TYPE_ENCRYPTED_DATA; /* 0x31 */
|
||||
|
||||
/* Keep response timing tight by avoiding HCI RAND in this PAwR callback.
|
||||
* Only this host-task callback touches the Randomizer, so no lock needed. */
|
||||
esp_err_t enc_err = ble_esl_ead_encrypt(resp_key->session_key, resp_key->iv,
|
||||
s_esl_pawr->response_randomizer,
|
||||
plaintext, plaintext_len,
|
||||
&pkt_buf[pkt_len]);
|
||||
if (enc_err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "PAwR response EAD encrypt failed: 0x%x", enc_err);
|
||||
return enc_err;
|
||||
}
|
||||
|
||||
pkt_len += BLE_EAD_RANDOMIZER_SIZE + plaintext_len + BLE_EAD_MIC_SIZE;
|
||||
|
||||
/* Transmit via NimBLE PAwR response API */
|
||||
struct ble_gap_periodic_adv_response_params params = {
|
||||
.request_event = s_esl_pawr->event_counter,
|
||||
.request_subevent = s_esl_pawr->subevent,
|
||||
.response_subevent = s_esl_pawr->subevent,
|
||||
.response_slot = slot,
|
||||
};
|
||||
|
||||
struct os_mbuf *om = os_msys_get_pkthdr(pkt_len, 0);
|
||||
if (om == NULL) {
|
||||
ESP_LOGE(TAG, "Failed to allocate mbuf for response");
|
||||
return ESP_ERR_NO_MEM;
|
||||
}
|
||||
|
||||
int rc = os_mbuf_append(om, pkt_buf, pkt_len);
|
||||
if (rc != 0) {
|
||||
ESP_LOGE(TAG, "os_mbuf_append failed: %d", rc);
|
||||
os_mbuf_free_chain(om);
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
rc = ble_gap_periodic_adv_set_response_data(s_esl_pawr->sync_handle, ¶ms, om);
|
||||
os_mbuf_free_chain(om);
|
||||
|
||||
if (rc != 0) {
|
||||
ESP_LOGE(TAG, "ble_gap_periodic_adv_set_response_data failed: 0x%x", rc);
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "PAwR response queued: event=%u subevent=%u slot=%u payload_len=%u pkt_len=%u",
|
||||
s_esl_pawr->event_counter, s_esl_pawr->subevent, slot, payload_len, pkt_len);
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t esl_pawr_process_sync_packet(uint8_t subevent, const uint8_t *data,
|
||||
uint8_t data_len)
|
||||
{
|
||||
if (s_esl_pawr == NULL) {
|
||||
ESP_LOGE(TAG, "PAwR module not initialized");
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
|
||||
if (data == NULL || data_len == 0) {
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
|
||||
/*
|
||||
* --- Step 0: Strip the inner AD structure wrapping the ESL Payload ---
|
||||
*
|
||||
* The decrypted plaintext is an AD structure (CSS Encrypted Data type):
|
||||
* [inner_len(1)] [ESL Tag 0x34(1)] [ESL Payload]
|
||||
* The ESL Payload (Group_ID + TLVs) begins after this 2-byte header.
|
||||
* This mirrors the AP transmit/parse paths (ble_esl_ap_pawr_send /
|
||||
* ble_esl_ap_pawr_parse_response); without it the header bytes get
|
||||
* misread as Group_ID / opcode and payload decode fails.
|
||||
*/
|
||||
if (data_len < 2 || data[1] != BLE_ESL_AD_TYPE_ESL) {
|
||||
ESP_LOGW(TAG, "Invalid inner AD tag: 0x%02x (expected 0x%02x)",
|
||||
data_len < 2 ? 0 : data[1], BLE_ESL_AD_TYPE_ESL);
|
||||
return ESP_ERR_INVALID_SIZE;
|
||||
}
|
||||
|
||||
uint8_t inner_len = data[0]; /* length of ESL Tag + ESL Payload */
|
||||
if (inner_len < 1 || (uint16_t)(1 + inner_len) > data_len) {
|
||||
ESP_LOGW(TAG, "Inner AD length mismatch: inner_len=%u, data_len=%u",
|
||||
inner_len, data_len);
|
||||
return ESP_ERR_INVALID_SIZE;
|
||||
}
|
||||
|
||||
/* Advance past the [inner_len][0x34] header to the ESL Payload itself */
|
||||
data += 2;
|
||||
data_len = inner_len - 1; /* subtract the ESL Tag octet */
|
||||
|
||||
/* --- Step 1: Decode the ESL Payload to extract Group_ID and TLVs --- */
|
||||
uint8_t group_id = 0;
|
||||
uint8_t tlv_count = 0;
|
||||
uint8_t tlv_offsets[ESL_MAX_TLVS_PER_PAYLOAD];
|
||||
uint8_t tlv_lens[ESL_MAX_TLVS_PER_PAYLOAD];
|
||||
|
||||
ESP_LOG_BUFFER_HEX("PA data", data, data_len);
|
||||
|
||||
esp_err_t err = ble_esl_payload_decode(data, data_len, &group_id,
|
||||
&tlv_count, tlv_offsets, tlv_lens,
|
||||
ESL_MAX_TLVS_PER_PAYLOAD);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Payload decode failed: 0x%x", err);
|
||||
return err;
|
||||
}
|
||||
|
||||
/* --- Step 2: Validate Group_ID against ESL's configured address --- */
|
||||
uint8_t my_esl_id = 0;
|
||||
uint8_t my_group_id = 0;
|
||||
err = esl_get_address(&my_esl_id, &my_group_id);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "ESL address not configured");
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
|
||||
if (group_id != my_group_id) {
|
||||
ESP_LOGD(TAG, "Group_ID mismatch: received %u, expected %u",
|
||||
group_id, my_group_id);
|
||||
return ESP_OK; /* Silently discard */
|
||||
}
|
||||
|
||||
/* --- Step 3: Validate ESL is in Synchronized state --- */
|
||||
ble_esl_state_t state = ble_esl_get_state();
|
||||
if (state != BLE_ESL_STATE_SYNCHRONIZED) {
|
||||
ESP_LOGW(TAG, "Not in Synchronized state (current: %d), ignoring",
|
||||
state);
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
|
||||
/* Store subevent for use by encrypt_and_send */
|
||||
s_esl_pawr->subevent = subevent;
|
||||
|
||||
/* --- Step 4: Iterate TLVs, dispatch commands, collect responses --- */
|
||||
|
||||
/* Response collection arrays */
|
||||
uint8_t resp_bufs[ESL_MAX_TLVS_PER_PAYLOAD][BLE_ESL_TLV_MAX_SIZE];
|
||||
uint8_t resp_lens[ESL_MAX_TLVS_PER_PAYLOAD];
|
||||
const uint8_t *resp_ptrs[ESL_MAX_TLVS_PER_PAYLOAD];
|
||||
uint8_t resp_count = 0;
|
||||
bool has_individual = false;
|
||||
uint8_t individual_tlv_count = 0;
|
||||
uint8_t broadcast_tlv_count = 0;
|
||||
|
||||
for (uint8_t i = 0; i < tlv_count; i++) {
|
||||
/* Decode each TLV */
|
||||
uint8_t opcode = 0;
|
||||
const uint8_t *params = NULL;
|
||||
uint8_t params_len = 0;
|
||||
|
||||
err = ble_esl_tlv_decode(data + tlv_offsets[i], tlv_lens[i],
|
||||
&opcode, ¶ms, ¶ms_len);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGW(TAG, "TLV %u decode failed: 0x%x, skipping", i, err);
|
||||
/* Use Length field for forward compatibility — skip this TLV */
|
||||
continue;
|
||||
}
|
||||
|
||||
/* ESL_ID is the first parameter byte */
|
||||
if (params_len < 1) {
|
||||
ESP_LOGW(TAG, "TLV %u has no ESL_ID parameter, skipping", i);
|
||||
continue;
|
||||
}
|
||||
uint8_t cmd_esl_id = params[0];
|
||||
|
||||
if (cmd_esl_id == my_esl_id) {
|
||||
/* ---- Individually addressed to this ESL ---- */
|
||||
has_individual = true;
|
||||
individual_tlv_count++;
|
||||
|
||||
/* Forbidden over PAwR (Factory Reset / Update Complete): do NOT
|
||||
* execute the command, but since it is individually addressed to
|
||||
* this ESL we must still send an Error: Invalid State (0x03)
|
||||
* response in the assigned slot (per esl_pawr.md step 6c and
|
||||
* ESLS/SR/UPD/BV-01-C). Only broadcast forbidden commands are
|
||||
* dropped without a response (see the broadcast branch below). */
|
||||
if (is_forbidden_sync_opcode(opcode)) {
|
||||
ESP_LOGW(TAG, "Forbidden opcode 0x%02x (unicast) in Synchronized state, responding Invalid State",
|
||||
opcode);
|
||||
uint8_t err_len = 0;
|
||||
build_error_response_tlv(BLE_ESL_ERR_INVALID_STATE,
|
||||
resp_bufs[resp_count], &err_len);
|
||||
resp_lens[resp_count] = err_len;
|
||||
resp_ptrs[resp_count] = resp_bufs[resp_count];
|
||||
resp_count++;
|
||||
continue;
|
||||
}
|
||||
|
||||
/* Dispatch the command and collect response */
|
||||
ble_esl_cmd_result_t result;
|
||||
memset(&result, 0, sizeof(result));
|
||||
|
||||
err = esl_cmd_dispatch(opcode, params, params_len, &result, ESL_TRANSPORT_PAWR);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGW(TAG, "Command dispatch failed for TLV %u: 0x%x",
|
||||
i, err);
|
||||
/* Build unspecified error response */
|
||||
uint8_t err_len = 0;
|
||||
build_error_response_tlv(BLE_ESL_ERR_UNSPECIFIED,
|
||||
resp_bufs[resp_count], &err_len);
|
||||
resp_lens[resp_count] = err_len;
|
||||
resp_ptrs[resp_count] = resp_bufs[resp_count];
|
||||
resp_count++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (result.has_response) {
|
||||
/* Encode the response TLV */
|
||||
uint8_t encoded_len = 0;
|
||||
err = ble_esl_tlv_encode(result.resp_opcode,
|
||||
result.resp_params,
|
||||
result.resp_params_len,
|
||||
resp_bufs[resp_count],
|
||||
&encoded_len);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGW(TAG, "Response TLV encode failed for TLV %u", i);
|
||||
uint8_t err_len = 0;
|
||||
build_error_response_tlv(BLE_ESL_ERR_UNSPECIFIED,
|
||||
resp_bufs[resp_count], &err_len);
|
||||
resp_lens[resp_count] = err_len;
|
||||
} else {
|
||||
resp_lens[resp_count] = encoded_len;
|
||||
}
|
||||
resp_ptrs[resp_count] = resp_bufs[resp_count];
|
||||
resp_count++;
|
||||
}
|
||||
|
||||
} else if (cmd_esl_id == BLE_ESL_BROADCAST_ADDRESS) {
|
||||
/* ---- Broadcast message ---- */
|
||||
broadcast_tlv_count++;
|
||||
|
||||
/* Forbidden broadcast commands: just skip, no response */
|
||||
if (is_forbidden_sync_opcode(opcode)) {
|
||||
ESP_LOGW(TAG, "Forbidden broadcast opcode 0x%02x, skipping",
|
||||
opcode);
|
||||
continue;
|
||||
}
|
||||
|
||||
/* Dispatch broadcast command, discard result */
|
||||
ble_esl_cmd_result_t result;
|
||||
memset(&result, 0, sizeof(result));
|
||||
err = esl_cmd_dispatch(opcode, params, params_len, &result, ESL_TRANSPORT_PAWR);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGW(TAG, "Broadcast dispatch failed for TLV %u: 0x%x",
|
||||
i, err);
|
||||
}
|
||||
/* No response for broadcast */
|
||||
|
||||
} else {
|
||||
/* ---- Command for a different ESL — skip ---- */
|
||||
individual_tlv_count++;
|
||||
ESP_LOGD(TAG, "TLV %u for ESL_ID 0x%02x, skipping", i, cmd_esl_id);
|
||||
}
|
||||
}
|
||||
|
||||
/* --- Step 5: If no individually-addressed commands, do not transmit --- */
|
||||
if (!has_individual || resp_count == 0) {
|
||||
ESP_LOGD(TAG, "No individually-addressed commands for this ESL");
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
/* --- Step 6: Calculate response slot --- */
|
||||
int16_t slot = esl_pawr_calc_response_slot(data, tlv_offsets, tlv_lens,
|
||||
tlv_count, my_esl_id);
|
||||
if (slot < 0) {
|
||||
ESP_LOGW(TAG, "Response slot calculation returned -1, not transmitting");
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
/* Track slot info for logging/diagnostics */
|
||||
esl_pawr_slot_info_t slot_info = {
|
||||
.slot_number = (uint8_t)slot,
|
||||
.tlv_count = individual_tlv_count,
|
||||
.broadcast_count = broadcast_tlv_count,
|
||||
};
|
||||
ESP_LOGD(TAG, "Slot info: slot=%u, individual_tlvs=%u, broadcast_tlvs=%u",
|
||||
slot_info.slot_number, slot_info.tlv_count, slot_info.broadcast_count);
|
||||
|
||||
/* --- Step 7: Assemble response payload --- */
|
||||
uint8_t assembled_payload[ESL_PAWR_MAX_RESPONSE_PAYLOAD];
|
||||
uint8_t assembled_len = 0;
|
||||
|
||||
err = esl_pawr_assemble_response(resp_ptrs, resp_lens, resp_count,
|
||||
assembled_payload, &assembled_len);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Response assembly failed: 0x%x", err);
|
||||
return err;
|
||||
}
|
||||
|
||||
if (assembled_len == 0) {
|
||||
ESP_LOGW(TAG, "Assembled response is empty");
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
/* --- Step 8: Encrypt and send --- */
|
||||
err = esl_pawr_encrypt_and_send((uint8_t)slot, assembled_payload,
|
||||
assembled_len);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Encrypt and send failed: 0x%x", err);
|
||||
}
|
||||
|
||||
return err;
|
||||
}
|
||||
1453
components/bt/ble_profiles/nimble/ble_esl/src/esl/esl_state.c
Normal file
1453
components/bt/ble_profiles/nimble/ble_esl/src/esl/esl_state.c
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user