feat(ble/nimble): support ble esl profile

This commit is contained in:
chenjianhua
2026-07-21 16:55:05 +08:00
committed by BOT
parent f66095d7be
commit 17d368eecc
22 changed files with 13495 additions and 0 deletions

View File

@@ -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)

View File

@@ -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"

View 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)

View 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

View 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 (03) */
uint8_t color_green; /*!< 2-bit green component (03) */
uint8_t color_blue; /*!< 2-bit blue component (03) */
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 (115, 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 */

View 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 (1128) */
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 (1255) */
} 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 (0x000xFE) */
uint8_t group_id; /*!< Group_ID to assign (0x000x7F) */
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 (015 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 (116 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 (0x000xFE) */
uint8_t group_id; /*!< Responding Group_ID (0x000x7F) */
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 (03) */
uint8_t color_green; /*!< 2-bit green component (03) */
uint8_t color_blue; /*!< 2-bit blue component (03) */
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 1128;
* 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 (0x000xFE, or 0xFF for broadcast)
* @param group_id Group_ID (0x000x7F)
* @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 (0x000xFE, or 0xFF for broadcast)
* @param group_id Group_ID (0x000x7F)
* @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 (0x000xFE, or 0xFF for broadcast)
* @param group_id Group_ID (0x000x7F)
* @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 (0x000xFE, or 0xFF for broadcast)
* @param group_id Group_ID (0x000x7F)
* @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 (0x000xFE, or 0xFF for broadcast)
* @param group_id Group_ID (0x000x7F)
* @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 (0x000xFE, or 0xFF for broadcast)
* @param group_id Group_ID (0x000x7F)
* @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 (0x000xFE, or 0xFF for broadcast)
* @param group_id Group_ID (0x000x7F)
* @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 (0x000xFE, or 0xFF for broadcast)
* @param group_id Group_ID (0x000x7F)
* @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 (0x000xFE, or 0xFF for broadcast)
* @param group_id Group_ID (0x000x7F)
* @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 (0x000xFE)
* @param group_id Group_ID (0x000x7F)
* @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 (0x000xFE, or 0xFF for broadcast)
* @param group_id Group_ID (0x000x7F)
* @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 (0x000x0F)
* @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 */

View File

@@ -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 (0x000x7F)
* @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 */

File diff suppressed because it is too large Load Diff

View 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);
}
}
}

View 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();
}

File diff suppressed because it is too large Load Diff

View 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, &params, &params_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);
}

View File

@@ -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 (0x000x7F)
* @param payload Pre-assembled ESL Payload (from ble_esl_payload_encode())
* @param payload_len ESL Payload length (148)
* @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 */

View File

@@ -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 */
}
}
}

View 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 (0x000xFE)
* @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 */

View File

@@ -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 */

View File

@@ -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(&param, 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, &param);
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;
}

View 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, &params);
/* Apply the LED command (handles special cases, fires callback, arms pattern timer) */
apply_led_command(led_index, &params);
}
/* ========================================================================== */
/* 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 *)&params[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 *)&params[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");
}

View File

@@ -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);
}

View 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, &params, &params_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;
}

View 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, &params, 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, &params, &params_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;
}

File diff suppressed because it is too large Load Diff