diff --git a/components/bt/ble_profiles/CMakeLists.txt b/components/bt/ble_profiles/CMakeLists.txt index 0b9405fc074..205b92c087f 100644 --- a/components/bt/ble_profiles/CMakeLists.txt +++ b/components/bt/ble_profiles/CMakeLists.txt @@ -18,5 +18,12 @@ if(CONFIG_BT_PRF_TASK_ENABLED) list(APPEND _srcs "${CMAKE_CURRENT_LIST_DIR}/common/src/bt_prf_task.c") endif() +# NimBLE profile cores, mirroring the "if BT_NIMBLE_ENABLED" guard in Kconfig.in. +if(CONFIG_BT_NIMBLE_ENABLED) + add_subdirectory(nimble) + list(APPEND _srcs ${nimble_profiles_srcs}) + list(APPEND _include_dirs ${nimble_profiles_include_dirs}) +endif() + set(ble_profiles_srcs "${_srcs}" PARENT_SCOPE) set(ble_profiles_include_dirs "${_include_dirs}" PARENT_SCOPE) diff --git a/components/bt/ble_profiles/Kconfig.in b/components/bt/ble_profiles/Kconfig.in index a8be997a9b1..b6f6a762baa 100644 --- a/components/bt/ble_profiles/Kconfig.in +++ b/components/bt/ble_profiles/Kconfig.in @@ -5,3 +5,7 @@ # source "$IDF_PATH/components/bt/ble_profiles/common/Kconfig.in" + +if BT_NIMBLE_ENABLED + source "$IDF_PATH/components/bt/ble_profiles/nimble/Kconfig.in" +endif diff --git a/components/bt/ble_profiles/nimble/CMakeLists.txt b/components/bt/ble_profiles/nimble/CMakeLists.txt new file mode 100644 index 00000000000..9d1a93b9955 --- /dev/null +++ b/components/bt/ble_profiles/nimble/CMakeLists.txt @@ -0,0 +1,20 @@ +# NimBLE profile cores: the list of profiles built on top of the NimBLE host. +# +# Only reached when CONFIG_BT_NIMBLE_ENABLED is set (see ../CMakeLists.txt). +# Each profile owns its own file list and its own Kconfig switch, and returns +# early when that switch is off, so plugging a new one in is three lines here. +# + +set(nimble_profiles_srcs "" PARENT_SCOPE) +set(nimble_profiles_include_dirs "" PARENT_SCOPE) + +set(_srcs "") +set(_include_dirs "") + +# Object Transfer Service +add_subdirectory(ble_ots) +list(APPEND _srcs ${ble_ots_srcs}) +list(APPEND _include_dirs ${ble_ots_include_dirs}) + +set(nimble_profiles_srcs "${_srcs}" PARENT_SCOPE) +set(nimble_profiles_include_dirs "${_include_dirs}" PARENT_SCOPE) diff --git a/components/bt/ble_profiles/nimble/Kconfig.in b/components/bt/ble_profiles/nimble/Kconfig.in new file mode 100644 index 00000000000..6e1c16e21f6 --- /dev/null +++ b/components/bt/ble_profiles/nimble/Kconfig.in @@ -0,0 +1,9 @@ +# NimBLE profile cores: the list of profiles built on top of the NimBLE host. +# +# Only sourced when BT_NIMBLE_ENABLED is set (see ../Kconfig.in). Each profile +# owns its own enable switch, so it contributes menu entries only when the user +# turns it on, and plugging a new one in is a single source line here. +# + +# Object Transfer Service +source "$IDF_PATH/components/bt/ble_profiles/nimble/ble_ots/Kconfig.in" diff --git a/components/bt/ble_profiles/nimble/ble_ots/CMakeLists.txt b/components/bt/ble_profiles/nimble/ble_ots/CMakeLists.txt new file mode 100644 index 00000000000..184a3e1f710 --- /dev/null +++ b/components/bt/ble_profiles/nimble/ble_ots/CMakeLists.txt @@ -0,0 +1,43 @@ +# Object Transfer Service (OTS) profile core for the NimBLE host. +# +# This directory is not a standalone IDF component: it is pulled in by +# components/bt/ble_profiles/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_ots_srcs "" PARENT_SCOPE) +set(ble_ots_include_dirs "" PARENT_SCOPE) + +if(NOT CONFIG_BLE_OTS_ENABLED) + return() +endif() + +set(_srcs "${CMAKE_CURRENT_LIST_DIR}/src/ble_ots_common.c") + +# The role-private headers (src//*_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_OTS_CLIENT_ENABLED) + list(APPEND _srcs + "${CMAKE_CURRENT_LIST_DIR}/src/client/ble_ots_client_discovery.c" + "${CMAKE_CURRENT_LIST_DIR}/src/client/ble_ots_client_metadata.c" + "${CMAKE_CURRENT_LIST_DIR}/src/client/ble_ots_client_object_nav.c" + "${CMAKE_CURRENT_LIST_DIR}/src/client/ble_ots_client_transfer.c" + ) +endif() + +if(CONFIG_BLE_OTS_SERVER_ENABLED) + list(APPEND _srcs + "${CMAKE_CURRENT_LIST_DIR}/src/server/ots_server_init.c" + "${CMAKE_CURRENT_LIST_DIR}/src/server/ots_server_metadata.c" + "${CMAKE_CURRENT_LIST_DIR}/src/server/ots_server_oacp_ops.c" + "${CMAKE_CURRENT_LIST_DIR}/src/server/ots_server_oacp_transfer.c" + "${CMAKE_CURRENT_LIST_DIR}/src/server/ots_server_olcp.c" + "${CMAKE_CURRENT_LIST_DIR}/src/server/ots_server_filter_changed.c" + ) +endif() + +set(ble_ots_srcs "${_srcs}" PARENT_SCOPE) +set(ble_ots_include_dirs "${_include_dirs}" PARENT_SCOPE) diff --git a/components/bt/ble_profiles/nimble/ble_ots/Kconfig.in b/components/bt/ble_profiles/nimble/ble_ots/Kconfig.in new file mode 100644 index 00000000000..ce86c51fd6c --- /dev/null +++ b/components/bt/ble_profiles/nimble/ble_ots/Kconfig.in @@ -0,0 +1,128 @@ +menu "Object Transfer Service (OTS) (EXPERIMENTAL)" + + config BLE_OTS_ENABLED + bool "Enable Object Transfer Service (OTS) (EXPERIMENTAL)" + depends on BT_NIMBLE_ENABLED && (BT_NIMBLE_L2CAP_COC_MAX_NUM >= 1) + depends on IDF_EXPERIMENTAL_FEATURES + default n + help + Build the Object Transfer Service profile core on top of the NimBLE + host. The client and server roles are selected separately below, so a + device only pays for the role it actually implements. + Object contents are transferred over an L2CAP connection oriented + channel, so BT_NIMBLE_L2CAP_COC_MAX_NUM must be set to at least 1. + + 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. + + menu "OTS Client" + + config BLE_OTS_CLIENT_ENABLED + bool "Enable OTS Client" + default n + depends on BLE_OTS_ENABLED + help + Enable the Object Transfer Service Client component. + When disabled, no OTS client code is compiled in, saving + flash and RAM on devices that act only as OTS servers. + + config BLE_OTS_CLIENT_OACP_TIMEOUT_MS + int "OACP response timeout (ms)" + default 30000 + depends on BLE_OTS_CLIENT_ENABLED + range 1000 120000 + help + Maximum time in milliseconds to wait for an OACP + control-point response indication (T_OACP) before + reporting a CP_TIMEOUT event. Increase this value on + links with high latency or when the server performs + slow storage operations. + + config BLE_OTS_CLIENT_OLCP_TIMEOUT_MS + int "OLCP response timeout (ms)" + default 30000 + depends on BLE_OTS_CLIENT_ENABLED + range 1000 120000 + help + Maximum time in milliseconds to wait for an OLCP + control-point response indication (T_OLCP) before + reporting a CP_TIMEOUT event. Increase this value if + the server's object list is very large and sorting or + filtering takes significant time. + + config BLE_OTS_CLIENT_TRANSFER_TIMEOUT_MS + int "Transfer inactivity timeout (ms)" + default 30000 + depends on BLE_OTS_CLIENT_ENABLED + range 1000 120000 + help + Inactivity timeout in milliseconds for the Object + Transfer Channel. If no data is sent or received within + this period a TRANSFER_TIMEOUT event is raised. Raise + this value for very large objects over slow links. + + config BLE_OTS_CLIENT_MAX_NAME_LEN + int "Maximum Object Name length (octets)" + default 120 + depends on BLE_OTS_CLIENT_ENABLED + range 1 512 + help + Maximum length of an Object Name string in octets. + This controls the size of internal buffers used when + reading or writing the Object Name characteristic. + The Bluetooth specification allows up to 120 octets; + increase only if the server uses a vendor extension. + + endmenu + + menu "OTS Server" + + config BLE_OTS_SERVER_ENABLED + bool "Enable OTS Server" + default n + depends on BLE_OTS_ENABLED + help + Enable the Object Transfer Service Server component. + When disabled, no OTS server code is compiled in, saving + flash and RAM on devices that act only as OTS clients. + + config BLE_OTS_SERVER_MAX_OBJECTS + int "Maximum number of objects" + default 10 + depends on BLE_OTS_SERVER_ENABLED + range 1 255 + help + Maximum number of objects the server can store in its + internal database at the same time. Each object slot + consumes RAM for metadata and content storage. Lower + this value on memory-constrained devices. + + config BLE_OTS_SERVER_MAX_CONCURRENCY + int "Maximum concurrent client connections" + default 1 + depends on BLE_OTS_SERVER_ENABLED + range 1 16 + help + Maximum number of client connections that can perform + object operations concurrently. Each concurrent slot + allocates per-connection state (current object, filters, + transfer context). Set this to the expected peak number + of simultaneous BLE connections. + + config BLE_OTS_SERVER_TRANSFER_TIMEOUT_SEC + int "Transfer inactivity timeout (seconds)" + default 30 + depends on BLE_OTS_SERVER_ENABLED + range 1 300 + help + Inactivity timeout in seconds for object read and write + transfers over the Object Transfer Channel. If no data + is exchanged within this period the transfer is aborted + and the object lock is released. Increase for very large + objects or slow links. + + endmenu + +endmenu diff --git a/components/bt/ble_profiles/nimble/ble_ots/include/ble_ots_client.h b/components/bt/ble_profiles/nimble/ble_ots/include/ble_ots_client.h new file mode 100644 index 00000000000..73445345a15 --- /dev/null +++ b/components/bt/ble_profiles/nimble/ble_ots/include/ble_ots_client.h @@ -0,0 +1,734 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef BLE_OTS_CLIENT_H +#define BLE_OTS_CLIENT_H + +#include +#include +#include "ble_ots_common.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/***************************************************************************** + * Kconfig Defaults + *****************************************************************************/ +#ifndef CONFIG_BLE_OTS_CLIENT_OACP_TIMEOUT_MS +#define CONFIG_BLE_OTS_CLIENT_OACP_TIMEOUT_MS 30000 +#endif + +#ifndef CONFIG_BLE_OTS_CLIENT_OLCP_TIMEOUT_MS +#define CONFIG_BLE_OTS_CLIENT_OLCP_TIMEOUT_MS 30000 +#endif + +#ifndef CONFIG_BLE_OTS_CLIENT_TRANSFER_TIMEOUT_MS +#define CONFIG_BLE_OTS_CLIENT_TRANSFER_TIMEOUT_MS 30000 +#endif + +#ifndef CONFIG_BLE_OTS_CLIENT_MAX_NAME_LEN +#define CONFIG_BLE_OTS_CLIENT_MAX_NAME_LEN 120 +#endif + +/***************************************************************************** + * Event Codes + *****************************************************************************/ + +/** + * @brief OTS client event codes delivered through the application callback. + */ +typedef enum { + /* Discovery module events */ + BLE_OTS_CLIENT_EVT_DISCOVER_COMPLETE = 0, /*!< Service/char/desc discovery finished */ + BLE_OTS_CLIENT_EVT_FEATURE_READ, /*!< OTS Feature read completed */ + BLE_OTS_CLIENT_EVT_OBJECT_NAME_READ, /*!< Object Name read completed */ + BLE_OTS_CLIENT_EVT_OBJECT_TYPE_READ, /*!< Object Type UUID read completed */ + BLE_OTS_CLIENT_EVT_OBJECT_SIZE_READ, /*!< Object Size read completed */ + BLE_OTS_CLIENT_EVT_FIRST_CREATED_READ, /*!< First-Created date-time read completed */ + BLE_OTS_CLIENT_EVT_LAST_MODIFIED_READ, /*!< Last-Modified date-time read completed */ + BLE_OTS_CLIENT_EVT_OBJECT_ID_READ, /*!< Object ID read completed */ + BLE_OTS_CLIENT_EVT_PROPERTIES_READ, /*!< Object Properties read completed */ + BLE_OTS_CLIENT_EVT_METADATA_WRITTEN, /*!< Metadata write completed */ + + /* Object navigation events */ + BLE_OTS_CLIENT_EVT_OLCP_RESPONSE, /*!< OLCP indication received */ + BLE_OTS_CLIENT_EVT_FILTER_SET, /*!< Object List Filter write completed */ + BLE_OTS_CLIENT_EVT_FILTER_READ, /*!< Object List Filter read completed */ + BLE_OTS_CLIENT_EVT_OBJECT_CHANGED, /*!< Object Changed indication received */ + + /* Transfer events */ + BLE_OTS_CLIENT_EVT_OACP_RESPONSE, /*!< OACP indication received */ + BLE_OTS_CLIENT_EVT_CHANNEL_OPEN, /*!< Object Transfer Channel opened */ + BLE_OTS_CLIENT_EVT_CHANNEL_CLOSED, /*!< Object Transfer Channel closed */ + BLE_OTS_CLIENT_EVT_DATA_RECEIVED, /*!< Object data chunk received */ + BLE_OTS_CLIENT_EVT_DATA_SENT, /*!< Object data chunk sent */ + BLE_OTS_CLIENT_EVT_CP_TIMEOUT, /*!< Control-point response timer expired */ + BLE_OTS_CLIENT_EVT_TRANSFER_TIMEOUT, /*!< Data transfer inactivity timeout */ +} ble_ots_client_event_t; + +/***************************************************************************** + * Event Data Structures + *****************************************************************************/ + +/** + * @brief Discovered characteristic handle set for a single OTS instance. + * + * A handle value of 0x0000 means the characteristic was not found. + */ +typedef struct { + uint16_t ots_feature_handle; /*!< OTS Feature (0x2ABD) */ + uint16_t object_name_handle; /*!< Object Name (0x2ABE) */ + uint16_t object_type_handle; /*!< Object Type (0x2ABF) */ + uint16_t object_size_handle; /*!< Object Size (0x2AC0) */ + uint16_t first_created_handle; /*!< First-Created (0x2AC1), 0 if absent */ + uint16_t last_modified_handle; /*!< Last-Modified (0x2AC2), 0 if absent */ + uint16_t object_id_handle; /*!< Object ID (0x2AC3), 0 if absent */ + uint16_t object_properties_handle; /*!< Object Properties (0x2AC4) */ + uint16_t oacp_handle; /*!< OACP (0x2AC5) */ + uint16_t oacp_cccd_handle; /*!< OACP CCCD */ + uint16_t olcp_handle; /*!< OLCP (0x2AC6), 0 if absent */ + uint16_t olcp_cccd_handle; /*!< OLCP CCCD, 0 if absent */ + uint16_t object_list_filter_handle[3]; /*!< Object List Filter (0x2AC7); per spec there are + either three instances or none — all 0 if absent */ + uint16_t object_changed_handle; /*!< Object Changed (0x2AC8), 0 if absent */ + uint16_t object_changed_cccd_handle; /*!< Object Changed CCCD, 0 if absent */ +} ble_ots_client_char_handles_t; + +/** + * @brief Discovery complete event data. + */ +typedef struct { + int status; /*!< 0 on success, ATT/GATT error on failure */ + ble_ots_client_char_handles_t handles; /*!< Copy of discovered handles */ + bool multi_object_server; /*!< true if OLCP is exposed */ +} ble_ots_client_discover_complete_t; + +/** + * @brief Feature read event data. + */ +typedef struct { + int status; /*!< 0 on success, ATT error on failure */ + ble_ots_feature_t feature; /*!< OACP + OLCP feature bit-fields */ +} ble_ots_client_feature_read_t; + +/** + * @brief Object Name read event data. + */ +typedef struct { + int status; /*!< 0 on success, ATT error on failure */ + const char *name; /*!< UTF-8 name buffer, not NUL-terminated */ + uint16_t name_len; /*!< Name length in octets (0–120) */ +} ble_ots_client_object_name_read_t; + +/** + * @brief Object Type read event data. + */ +typedef struct { + int status; /*!< 0 on success, ATT error on failure */ + uint8_t uuid[16]; /*!< UUID value (2 or 16 octets, LE) */ + uint8_t uuid_len; /*!< 2 for 16-bit, 16 for 128-bit UUID */ +} ble_ots_client_object_type_read_t; + +/** + * @brief Object Size read event data. + */ +typedef struct { + int status; /*!< 0 on success, ATT error on failure */ + uint32_t current_size; /*!< Actual object size in octets */ + uint32_t allocated_size; /*!< Allocated size in octets */ +} ble_ots_client_object_size_read_t; + +/** + * @brief Date-time read event data (First-Created / Last-Modified). + */ +typedef struct { + int status; /*!< 0 on success, ATT error on failure */ + ble_ots_date_time_t datetime; /*!< Parsed date-time value */ +} ble_ots_client_datetime_read_t; + +/** + * @brief Object ID read event data. + */ +typedef struct { + int status; /*!< 0 on success, ATT error on failure */ + uint64_t object_id; /*!< UINT48 Object ID (lower 48 bits) */ +} ble_ots_client_object_id_read_t; + +/** + * @brief Object Properties read event data. + */ +typedef struct { + int status; /*!< 0 on success, ATT error on failure */ + uint32_t properties; /*!< ble_ots_obj_property_t bit-field */ +} ble_ots_client_properties_read_t; + +/** + * @brief Metadata identifier for write events. + */ +typedef enum { + BLE_OTS_CLIENT_METADATA_OBJECT_NAME = 0, /*!< Object Name */ + BLE_OTS_CLIENT_METADATA_FIRST_CREATED, /*!< Object First-Created */ + BLE_OTS_CLIENT_METADATA_LAST_MODIFIED, /*!< Object Last-Modified */ + BLE_OTS_CLIENT_METADATA_PROPERTIES, /*!< Object Properties */ +} ble_ots_client_metadata_type_t; + +/** + * @brief Metadata written event data. + */ +typedef struct { + int status; /*!< 0 on success, ATT error on failure */ + ble_ots_client_metadata_type_t metadata_type; /*!< Which metadata was written */ +} ble_ots_client_metadata_written_t; + +/** + * @brief OLCP response event data. + */ +typedef struct { + uint8_t request_opcode; /*!< Original OLCP Op Code (0x01–0x08) */ + uint8_t result_code; /*!< OLCP result code */ + uint32_t num_objects; /*!< Valid only for Request Number of Objects + Success */ +} ble_ots_client_olcp_response_t; + +/** + * @brief Filter set event data. + */ +typedef struct { + uint8_t instance; /*!< Filter instance index (0, 1, 2) */ + int status; /*!< 0 on success, ATT error on failure */ +} ble_ots_client_filter_set_t; + +/** + * @brief Filter read event data. + */ +typedef struct { + uint8_t instance; /*!< Filter instance index (0, 1, 2) */ + int status; /*!< 0 on success, ATT error on failure */ + uint8_t filter_type; /*!< One of ble_ots_list_filter_type_t */ + const uint8_t *param; /*!< Filter parameter bytes (valid during callback), NULL if none */ + uint16_t param_len; /*!< Length of param in octets (0 if none) */ +} ble_ots_client_filter_read_t; + +/** + * @brief Object Changed event data. + */ +typedef struct { + uint8_t source_of_change; /*!< 0 = Server, 1 = another Client */ + bool contents_changed; /*!< true if object contents changed */ + bool metadata_changed; /*!< true if object metadata changed */ + bool object_created; /*!< true if a new object was created */ + bool object_deleted; /*!< true if an object was deleted */ + uint64_t object_id; /*!< UINT48 Object ID (lower 48 bits) */ +} ble_ots_client_object_changed_t; + +/** + * @brief OACP response event data. + */ +typedef struct { + uint8_t request_opcode; /*!< Original OACP Op Code (0x01–0x07) */ + uint8_t result_code; /*!< OACP result code */ + uint32_t checksum; /*!< CRC-32 (valid only for Calculate Checksum + Success) */ + bool has_checksum; /*!< true if checksum field is valid */ +} ble_ots_client_oacp_response_t; + +/** + * @brief Channel open event data. + */ +typedef struct { + uint16_t conn_id; /*!< Connection identifier */ + uint16_t channel_id; /*!< L2CAP channel identifier */ + uint16_t mtu; /*!< Negotiated L2CAP MTU */ + uint16_t mps; /*!< Negotiated MPS (LE Credit Based only) */ +} ble_ots_client_channel_info_t; + +/** + * @brief Channel closed event data. + */ +typedef struct { + uint16_t conn_id; /*!< Connection identifier */ + uint8_t reason; /*!< 0 = local close, nonzero = remote/link loss */ +} ble_ots_client_channel_closed_t; + +/** + * @brief Data received event data. + */ +typedef struct { + const uint8_t *data; /*!< Pointer to received data chunk */ + uint16_t data_len; /*!< Length of received data chunk */ + uint32_t offset; /*!< Logical byte offset in the object */ +} ble_ots_client_data_received_t; + +/** + * @brief Data sent event data. + */ +typedef struct { + uint16_t data_len; /*!< Octets acknowledged as sent */ + uint32_t remaining; /*!< Remaining octets to send */ +} ble_ots_client_data_sent_t; + +/** + * @brief Transfer timeout event data. + */ +typedef struct { + uint16_t conn_id; /*!< Connection identifier */ + uint8_t opcode; /*!< OACP Op Code (0 = data-transfer inactivity) */ +} ble_ots_client_transfer_timeout_t; + +/***************************************************************************** + * Callback Type + *****************************************************************************/ + +/** + * @brief Application event callback type. + * + * All OTS client events from all modules are delivered through this callback. + * The callback is invoked from the BLE host task context; the application must + * not block. + * + * @param conn_id Connection identifier + * @param event One of ble_ots_client_event_t values + * @param param Event-specific parameter structure (valid during callback only) + */ +typedef void (*ble_ots_client_event_cb_t)(uint16_t conn_id, + ble_ots_client_event_t event, + const void *param); + +/***************************************************************************** + * Public APIs — Discovery & Metadata (ble_ots_client_discovery.c / ble_ots_client_metadata.c) + *****************************************************************************/ + +/** + * @brief Initialize the OTS client module. + * + * Allocates internal resources and registers the application event callback. + * Must be called before any other OTS client API. Call ble_ots_client_deinit() + * first to re-initialize. + * + * @param callback Application event callback function + * @return 0 on success, error code on failure + */ +int ble_ots_client_init(ble_ots_client_event_cb_t callback); + +/** + * @brief Deinitialize the OTS client module. + * + * Releases all resources (per-connection contexts, timers), unregisters the + * callback, and resets internal state. + * + * @return 0 on success, error code on failure + */ +int ble_ots_client_deinit(void); + +/** + * @brief Discover the OTS service on a connected server. + * + * Discovers all service characteristics and descriptors, and auto-configures + * OACP/OLCP CCCDs for indications. Result reported via + * BLE_OTS_CLIENT_EVT_DISCOVER_COMPLETE. + * + * @param conn_id BLE connection identifier + * @return 0 on success (discovery started), error code on failure + */ +int ble_ots_client_discover_service(uint16_t conn_id); + +/** + * @brief Read the OTS Feature characteristic (Feature Discovery). + * + * The result is cached and reported via BLE_OTS_CLIENT_EVT_FEATURE_READ. + * + * @param conn_id BLE connection identifier + * @return 0 on success (read initiated), error code on failure + */ +int ble_ots_client_read_feature(uint16_t conn_id); + +/** + * @brief Read the Object Name of the Current Object. + * + * Uses Read Long if needed. Result via BLE_OTS_CLIENT_EVT_OBJECT_NAME_READ. + * + * @param conn_id BLE connection identifier + * @return 0 on success (read initiated), error code on failure + */ +int ble_ots_client_read_object_name(uint16_t conn_id); + +/** + * @brief Read the Object Type UUID of the Current Object. + * + * Result via BLE_OTS_CLIENT_EVT_OBJECT_TYPE_READ. + * + * @param conn_id BLE connection identifier + * @return 0 on success (read initiated), error code on failure + */ +int ble_ots_client_read_object_type(uint16_t conn_id); + +/** + * @brief Read the Object Size of the Current Object. + * + * Result via BLE_OTS_CLIENT_EVT_OBJECT_SIZE_READ. + * + * @param conn_id BLE connection identifier + * @return 0 on success (read initiated), error code on failure + */ +int ble_ots_client_read_object_size(uint16_t conn_id); + +/** + * @brief Read the First-Created date-time of the Current Object. + * + * Result via BLE_OTS_CLIENT_EVT_FIRST_CREATED_READ. + * + * @param conn_id BLE connection identifier + * @return 0 on success (read initiated), error code on failure + */ +int ble_ots_client_read_object_first_created(uint16_t conn_id); + +/** + * @brief Read the Last-Modified date-time of the Current Object. + * + * Result via BLE_OTS_CLIENT_EVT_LAST_MODIFIED_READ. + * + * @param conn_id BLE connection identifier + * @return 0 on success (read initiated), error code on failure + */ +int ble_ots_client_read_object_last_modified(uint16_t conn_id); + +/** + * @brief Read the Object ID of the Current Object. + * + * Result via BLE_OTS_CLIENT_EVT_OBJECT_ID_READ. + * + * @param conn_id BLE connection identifier + * @return 0 on success (read initiated), error code on failure + */ +int ble_ots_client_read_object_id(uint16_t conn_id); + +/** + * @brief Read the Object Properties of the Current Object. + * + * Result via BLE_OTS_CLIENT_EVT_PROPERTIES_READ. + * + * @param conn_id BLE connection identifier + * @return 0 on success (read initiated), error code on failure + */ +int ble_ots_client_read_object_properties(uint16_t conn_id); + +/** + * @brief Write a new name for the Current Object. + * + * Uses Write Long if name exceeds ATT_MTU - 3. Result via + * BLE_OTS_CLIENT_EVT_METADATA_WRITTEN with METADATA_OBJECT_NAME. + * + * @param conn_id BLE connection identifier + * @param name Pointer to UTF-8 encoded name buffer, need not be NUL-terminated + * @param name_len Length of name in octets (1–120) + * @return 0 on success (write initiated), error code on failure + */ +int ble_ots_client_write_object_name(uint16_t conn_id, + const char *name, + uint16_t name_len); + +/** + * @brief Write the First-Created date-time of the Current Object. + * + * Result via BLE_OTS_CLIENT_EVT_METADATA_WRITTEN with METADATA_FIRST_CREATED. + * + * @param conn_id BLE connection identifier + * @param datetime Pointer to date-time value to write + * @return 0 on success (write initiated), error code on failure + */ +int ble_ots_client_write_object_first_created(uint16_t conn_id, + const ble_ots_date_time_t *datetime); + +/** + * @brief Write the Last-Modified date-time of the Current Object. + * + * Result via BLE_OTS_CLIENT_EVT_METADATA_WRITTEN with METADATA_LAST_MODIFIED. + * + * @param conn_id BLE connection identifier + * @param datetime Pointer to date-time value to write + * @return 0 on success (write initiated), error code on failure + */ +int ble_ots_client_write_object_last_modified(uint16_t conn_id, + const ble_ots_date_time_t *datetime); + +/** + * @brief Write the Object Properties of the Current Object. + * + * Result via BLE_OTS_CLIENT_EVT_METADATA_WRITTEN with METADATA_PROPERTIES. + * + * @param conn_id BLE connection identifier + * @param properties ble_ots_obj_property_t bit-field value + * @return 0 on success (write initiated), error code on failure + */ +int ble_ots_client_write_object_properties(uint16_t conn_id, + uint32_t properties); + +/***************************************************************************** + * Public APIs — Object Navigation (ble_ots_client_object_nav.c) + *****************************************************************************/ + +/** + * @brief Navigate to the first object (OLCP First, 0x01). + * + * Result via BLE_OTS_CLIENT_EVT_OLCP_RESPONSE. + * + * @param conn_id Connection identifier + * @return 0 on success, error code on failure + */ +int ble_ots_client_first_object(uint16_t conn_id); + +/** + * @brief Navigate to the last object (OLCP Last, 0x02). + * + * Result via BLE_OTS_CLIENT_EVT_OLCP_RESPONSE. + * + * @param conn_id Connection identifier + * @return 0 on success, error code on failure + */ +int ble_ots_client_last_object(uint16_t conn_id); + +/** + * @brief Navigate to the previous object (OLCP Previous, 0x03). + * + * Result via BLE_OTS_CLIENT_EVT_OLCP_RESPONSE. + * + * @param conn_id Connection identifier + * @return 0 on success, error code on failure + */ +int ble_ots_client_prev_object(uint16_t conn_id); + +/** + * @brief Navigate to the next object (OLCP Next, 0x04). + * + * Result via BLE_OTS_CLIENT_EVT_OLCP_RESPONSE. + * + * @param conn_id Connection identifier + * @return 0 on success, error code on failure + */ +int ble_ots_client_next_object(uint16_t conn_id); + +/** + * @brief Select an object by Object ID (OLCP Go To, 0x05). + * + * Result via BLE_OTS_CLIENT_EVT_OLCP_RESPONSE. + * + * @param conn_id Connection identifier + * @param object_id UINT48 Object ID (lower 48 bits used) + * @return 0 on success, error code on failure + */ +int ble_ots_client_goto_object(uint16_t conn_id, uint64_t object_id); + +/** + * @brief Re-order the object list (OLCP Order, 0x06). + * + * Result via BLE_OTS_CLIENT_EVT_OLCP_RESPONSE. + * + * @param conn_id Connection identifier + * @param sort_order One of ble_ots_list_sort_order_t values + * @return 0 on success, error code on failure + */ +int ble_ots_client_order_objects(uint16_t conn_id, uint8_t sort_order); + +/** + * @brief Request number of objects matching current filters (OLCP 0x07). + * + * Result via BLE_OTS_CLIENT_EVT_OLCP_RESPONSE with num_objects. + * + * @param conn_id Connection identifier + * @return 0 on success, error code on failure + */ +int ble_ots_client_request_num_objects(uint16_t conn_id); + +/** + * @brief Clear marking on all filtered objects (OLCP 0x08). + * + * Result via BLE_OTS_CLIENT_EVT_OLCP_RESPONSE. + * + * @param conn_id Connection identifier + * @return 0 on success, error code on failure + */ +int ble_ots_client_clear_marking(uint16_t conn_id); + +/** + * @brief Write an Object List Filter value to a filter instance. + * + * Result via BLE_OTS_CLIENT_EVT_FILTER_SET. + * + * @param conn_id Connection identifier + * @param instance Filter instance index (0, 1, or 2) + * @param filter_type One of ble_ots_list_filter_type_t values + * @param param Filter parameter data (NULL for No Filter / Marked Objects) + * @param param_len Length of param in octets + * @return 0 on success, error code on failure + */ +int ble_ots_client_set_filter(uint16_t conn_id, + uint8_t instance, + uint8_t filter_type, + const uint8_t *param, + uint16_t param_len); + +/** + * @brief Read the Object List Filter value of a filter instance. + * + * Reads back the filter type and parameter currently configured on the given + * filter instance. Uses Read Long automatically when the value exceeds + * ATT_MTU - 1. Result via BLE_OTS_CLIENT_EVT_FILTER_READ. + * + * @param conn_id Connection identifier + * @param instance Filter instance index (0, 1, or 2) + * @return 0 on success (read initiated), error code on failure + */ +int ble_ots_client_read_filter(uint16_t conn_id, uint8_t instance); + +/** + * @brief Enable or disable Object Changed indications. + * + * When enabled, BLE_OTS_CLIENT_EVT_OBJECT_CHANGED events are received. + * + * @param conn_id Connection identifier + * @param enable true to enable, false to disable + * @return 0 on success, error code on failure + */ +int ble_ots_client_subscribe_object_changed(uint16_t conn_id, bool enable); + +/***************************************************************************** + * Public APIs — Transfer (ble_ots_client_transfer.c) + *****************************************************************************/ + +/** + * @brief Create a new object on the server (OACP Create, 0x01). + * + * Result via BLE_OTS_CLIENT_EVT_OACP_RESPONSE. + * + * @param conn_id Connection identifier + * @param size Allocated size in octets + * @param type_uuid Pointer to Object Type UUID (2 or 16 octets) + * @param type_uuid_len Length of the type UUID: 2 or 16 + * @return 0 on success, error code on failure + */ +int ble_ots_client_create_object(uint16_t conn_id, + uint32_t size, + const uint8_t *type_uuid, + uint8_t type_uuid_len); + +/** + * @brief Delete the Current Object (OACP Delete, 0x02). + * + * Result via BLE_OTS_CLIENT_EVT_OACP_RESPONSE. + * + * @param conn_id Connection identifier + * @return 0 on success, error code on failure + */ +int ble_ots_client_delete_object(uint16_t conn_id); + +/** + * @brief Execute the Current Object (OACP Execute, 0x04). + * + * Result via BLE_OTS_CLIENT_EVT_OACP_RESPONSE. + * + * @param conn_id Connection identifier + * @return 0 on success, error code on failure + */ +int ble_ots_client_execute_object(uint16_t conn_id); + +/** + * @brief Initiate reading object contents (OACP Read, 0x05). + * + * Data arrives via BLE_OTS_CLIENT_EVT_DATA_RECEIVED. The Object Transfer + * Channel must already be open. + * + * @param conn_id Connection identifier + * @param offset Zero-based byte offset to start reading + * @param length Number of octets to read; MUST be non-zero. The transfer + * channel has no end-of-data marker, so the client tracks + * completion by counting octets against this value. + * @return 0 on success, BLE_HS_EINVAL if length is 0, error code on failure + */ +int ble_ots_client_read_object_content(uint16_t conn_id, + uint32_t offset, + uint32_t length); + +/** + * @brief Initiate writing object contents (OACP Write, 0x06). + * + * After OACP Success, send data via ble_ots_client_send_data(). The Object + * Transfer Channel must already be open. + * + * @param conn_id Connection identifier + * @param offset Zero-based byte offset to start writing + * @param length Number of octets to write; MUST be non-zero + * @param mode Write mode bit-field (bit 1 = Truncate) + * @return 0 on success, BLE_HS_EINVAL if length is 0, error code on failure + */ +int ble_ots_client_write_object_content(uint16_t conn_id, + uint32_t offset, + uint32_t length, + uint8_t mode); + +/** + * @brief Send a chunk of object data over the Object Transfer Channel. + * + * Called repeatedly during OACP Write until all declared octets are sent. + * + * @param conn_id Connection identifier + * @param data Pointer to the object data to send + * @param data_len Length of the data chunk in octets + * @return 0 on success, error code on failure + */ +int ble_ots_client_send_data(uint16_t conn_id, + const uint8_t *data, + uint16_t data_len); + +/** + * @brief Request a CRC-32 checksum (OACP Calculate Checksum, 0x03). + * + * Result via BLE_OTS_CLIENT_EVT_OACP_RESPONSE with checksum. + * + * @param conn_id Connection identifier + * @param offset Zero-based byte offset + * @param length Number of octets to checksum + * @return 0 on success, error code on failure + */ +int ble_ots_client_calculate_checksum(uint16_t conn_id, + uint32_t offset, + uint32_t length); + +/** + * @brief Abort an in-progress OACP Read transfer (OACP Abort, 0x07). + * + * Result via BLE_OTS_CLIENT_EVT_OACP_RESPONSE. Per OTS v1.0 section 4.4 this + * may be issued while the server is still processing another OACP procedure, + * so it is not rejected with BLE_HS_EBUSY the way the other OACP commands are. + * + * @param conn_id Connection identifier + * @return 0 on success, error code on failure + */ +int ble_ots_client_abort_transfer(uint16_t conn_id); + +/** + * @brief Open an L2CAP Object Transfer Channel (PSM_OTS 0x0025). + * + * Result via BLE_OTS_CLIENT_EVT_CHANNEL_OPEN or BLE_OTS_CLIENT_EVT_CHANNEL_CLOSED. + * + * @param conn_id Connection identifier + * @return 0 on success, error code on failure + */ +int ble_ots_client_open_channel(uint16_t conn_id); + +/** + * @brief Close the Object Transfer Channel. + * + * Result via BLE_OTS_CLIENT_EVT_CHANNEL_CLOSED. + * + * @param conn_id Connection identifier + * @return 0 on success, error code on failure + */ +int ble_ots_client_close_channel(uint16_t conn_id); + +#ifdef __cplusplus +} +#endif + +#endif /* BLE_OTS_CLIENT_H */ diff --git a/components/bt/ble_profiles/nimble/ble_ots/include/ble_ots_common.h b/components/bt/ble_profiles/nimble/ble_ots/include/ble_ots_common.h new file mode 100644 index 00000000000..04f9d84dd35 --- /dev/null +++ b/components/bt/ble_profiles/nimble/ble_ots/include/ble_ots_common.h @@ -0,0 +1,330 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef BLE_OTS_COMMON_H +#define BLE_OTS_COMMON_H + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/***************************************************************************** + * OTS Service and Characteristic UUIDs (16-bit) + *****************************************************************************/ +#define BLE_OTS_UUID_OTS_SERVICE 0x1825 +#define BLE_OTS_UUID_OTS_FEATURE 0x2ABD +#define BLE_OTS_UUID_OBJECT_NAME 0x2ABE +#define BLE_OTS_UUID_OBJECT_TYPE 0x2ABF +#define BLE_OTS_UUID_OBJECT_SIZE 0x2AC0 +#define BLE_OTS_UUID_OBJECT_FIRST_CREATED 0x2AC1 +#define BLE_OTS_UUID_OBJECT_LAST_MODIFIED 0x2AC2 +#define BLE_OTS_UUID_OBJECT_ID 0x2AC3 +#define BLE_OTS_UUID_OBJECT_PROPERTIES 0x2AC4 +#define BLE_OTS_UUID_OACP 0x2AC5 +#define BLE_OTS_UUID_OLCP 0x2AC6 +#define BLE_OTS_UUID_OBJECT_LIST_FILTER 0x2AC7 +#define BLE_OTS_UUID_OBJECT_CHANGED 0x2AC8 +#define BLE_OTS_UUID_DIRECTORY_LISTING 0x2ACB + +/***************************************************************************** + * PSM_OTS — L2CAP PSM / SPSM for Object Transfer Channel + *****************************************************************************/ +#define BLE_OTS_PSM_OTS 0x0025 + +/***************************************************************************** + * Object Name Constraints + *****************************************************************************/ +#define BLE_OTS_OBJECT_NAME_MAX_LEN 120 + +/***************************************************************************** + * Object ID Constraints (UINT48 stored as uint64_t) + *****************************************************************************/ +typedef uint64_t ble_ots_obj_id_t; + +#define BLE_OTS_OBJ_ID_DIRECTORY_LISTING 0x000000000000ULL +#define BLE_OTS_OBJ_ID_MIN_USER 0x000000000100ULL +#define BLE_OTS_OBJ_ID_MAX_USER 0xFFFFFFFFFFFFULL + +/***************************************************************************** + * OACP Op Codes + *****************************************************************************/ +typedef enum { + BLE_OTS_OACP_OPCODE_CREATE = 0x01, + BLE_OTS_OACP_OPCODE_DELETE = 0x02, + BLE_OTS_OACP_OPCODE_CALCULATE_CHECKSUM = 0x03, + BLE_OTS_OACP_OPCODE_EXECUTE = 0x04, + BLE_OTS_OACP_OPCODE_READ = 0x05, + BLE_OTS_OACP_OPCODE_WRITE = 0x06, + BLE_OTS_OACP_OPCODE_ABORT = 0x07, + BLE_OTS_OACP_OPCODE_RESPONSE = 0x60, +} ble_ots_oacp_opcode_t; + +/***************************************************************************** + * OACP Write Mode Bit-Field (8-bit) + *****************************************************************************/ +#define BLE_OTS_OACP_WRITE_MODE_TRUNCATE (1 << 1) + +/***************************************************************************** + * OACP Result Codes + *****************************************************************************/ +typedef enum { + BLE_OTS_OACP_RESULT_SUCCESS = 0x01, + BLE_OTS_OACP_RESULT_OP_CODE_NOT_SUPPORTED = 0x02, + BLE_OTS_OACP_RESULT_INVALID_PARAMETER = 0x03, + BLE_OTS_OACP_RESULT_INSUFFICIENT_RESOURCES = 0x04, + BLE_OTS_OACP_RESULT_INVALID_OBJECT = 0x05, + BLE_OTS_OACP_RESULT_CHANNEL_UNAVAILABLE = 0x06, + BLE_OTS_OACP_RESULT_UNSUPPORTED_TYPE = 0x07, + BLE_OTS_OACP_RESULT_PROCEDURE_NOT_PERMITTED = 0x08, + BLE_OTS_OACP_RESULT_OBJECT_LOCKED = 0x09, + BLE_OTS_OACP_RESULT_OPERATION_FAILED = 0x0A, +} ble_ots_oacp_result_code_t; + +/***************************************************************************** + * OACP Response Value + *****************************************************************************/ +typedef struct { + uint8_t request_op_code; + uint8_t result_code; + uint8_t response_parameter[4]; /**< Variable length; up to 4 bytes (e.g., checksum UINT32) */ +} ble_ots_oacp_response_value_t; + +/***************************************************************************** + * OLCP Op Codes + *****************************************************************************/ +typedef enum { + BLE_OTS_OLCP_OPCODE_FIRST = 0x01, + BLE_OTS_OLCP_OPCODE_LAST = 0x02, + BLE_OTS_OLCP_OPCODE_PREVIOUS = 0x03, + BLE_OTS_OLCP_OPCODE_NEXT = 0x04, + BLE_OTS_OLCP_OPCODE_GO_TO = 0x05, + BLE_OTS_OLCP_OPCODE_ORDER = 0x06, + BLE_OTS_OLCP_OPCODE_REQUEST_NUM_OF_OBJECTS = 0x07, + BLE_OTS_OLCP_OPCODE_CLEAR_MARKING = 0x08, + BLE_OTS_OLCP_OPCODE_RESPONSE = 0x70, +} ble_ots_olcp_opcode_t; + +/***************************************************************************** + * OLCP Result Codes + *****************************************************************************/ +typedef enum { + BLE_OTS_OLCP_RESULT_SUCCESS = 0x01, + BLE_OTS_OLCP_RESULT_OP_CODE_NOT_SUPPORTED = 0x02, + BLE_OTS_OLCP_RESULT_INVALID_PARAMETER = 0x03, + BLE_OTS_OLCP_RESULT_OPERATION_FAILED = 0x04, + BLE_OTS_OLCP_RESULT_OUT_OF_BOUNDS = 0x05, + BLE_OTS_OLCP_RESULT_TOO_MANY_OBJECTS = 0x06, + BLE_OTS_OLCP_RESULT_NO_OBJECT = 0x07, + BLE_OTS_OLCP_RESULT_OBJECT_ID_NOT_FOUND = 0x08, +} ble_ots_olcp_result_code_t; + +/***************************************************************************** + * OLCP Response Value + *****************************************************************************/ +typedef struct { + uint8_t request_op_code; + uint8_t result_code; + uint32_t total_num_of_objects; /**< Present only for Request Number of Objects on Success */ +} ble_ots_olcp_response_value_t; + +/***************************************************************************** + * OACP Features Bit-Field (32-bit) + *****************************************************************************/ +#define BLE_OTS_OACP_FEAT_CREATE (1UL << 0) +#define BLE_OTS_OACP_FEAT_DELETE (1UL << 1) +#define BLE_OTS_OACP_FEAT_CALCULATE_CHECKSUM (1UL << 2) +#define BLE_OTS_OACP_FEAT_EXECUTE (1UL << 3) +#define BLE_OTS_OACP_FEAT_READ (1UL << 4) +#define BLE_OTS_OACP_FEAT_WRITE (1UL << 5) +#define BLE_OTS_OACP_FEAT_APPEND (1UL << 6) +#define BLE_OTS_OACP_FEAT_TRUNCATE (1UL << 7) +#define BLE_OTS_OACP_FEAT_PATCH (1UL << 8) +#define BLE_OTS_OACP_FEAT_ABORT (1UL << 9) + +/***************************************************************************** + * OLCP Features Bit-Field (32-bit) + *****************************************************************************/ +#define BLE_OTS_OLCP_FEAT_GO_TO (1UL << 0) +#define BLE_OTS_OLCP_FEAT_ORDER (1UL << 1) +#define BLE_OTS_OLCP_FEAT_REQUEST_NUM_OF_OBJECTS (1UL << 2) +#define BLE_OTS_OLCP_FEAT_CLEAR_MARKING (1UL << 3) + +/***************************************************************************** + * OTS Feature Characteristic Structure (8 octets) + *****************************************************************************/ +typedef struct { + uint32_t oacp_features; /**< OACP Features bit-field */ + uint32_t olcp_features; /**< OLCP Features bit-field */ +} ble_ots_feature_t; + +/***************************************************************************** + * Object Properties Bit-Field (32-bit) + *****************************************************************************/ +#define BLE_OTS_OBJ_PROP_DELETE (1UL << 0) +#define BLE_OTS_OBJ_PROP_EXECUTE (1UL << 1) +#define BLE_OTS_OBJ_PROP_READ (1UL << 2) +#define BLE_OTS_OBJ_PROP_WRITE (1UL << 3) +#define BLE_OTS_OBJ_PROP_APPEND (1UL << 4) +#define BLE_OTS_OBJ_PROP_TRUNCATE (1UL << 5) +#define BLE_OTS_OBJ_PROP_PATCH (1UL << 6) +#define BLE_OTS_OBJ_PROP_MARK (1UL << 7) + +/***************************************************************************** + * Object Size Characteristic Structure (8 octets) + *****************************************************************************/ +typedef struct { + uint32_t current_size; /**< Actual size of the object in octets */ + uint32_t allocated_size; /**< Allocated size in octets (>= current_size) */ +} ble_ots_obj_size_t; + +/***************************************************************************** + * Date-Time Structure (7 octets) + *****************************************************************************/ +typedef struct { + uint16_t year; /**< Gregorian year (1582–9999; 0 = unknown) */ + uint8_t month; /**< Month (1–12; 0 = unknown) */ + uint8_t day; /**< Day (1–31; 0 = unknown) */ + uint8_t hours; /**< Hours (0–23) */ + uint8_t minutes; /**< Minutes (0–59) */ + uint8_t seconds; /**< Seconds (0–59) */ +} __attribute__((packed)) ble_ots_date_time_t; + +/***************************************************************************** + * Object List Filter Types + *****************************************************************************/ +typedef enum { + BLE_OTS_FILTER_NO_FILTER = 0x00, + BLE_OTS_FILTER_NAME_STARTS_WITH = 0x01, + BLE_OTS_FILTER_NAME_ENDS_WITH = 0x02, + BLE_OTS_FILTER_NAME_CONTAINS = 0x03, + BLE_OTS_FILTER_NAME_IS_EXACTLY = 0x04, + BLE_OTS_FILTER_OBJECT_TYPE = 0x05, + BLE_OTS_FILTER_CREATED_BETWEEN = 0x06, + BLE_OTS_FILTER_MODIFIED_BETWEEN = 0x07, + BLE_OTS_FILTER_CURRENT_SIZE_BETWEEN = 0x08, + BLE_OTS_FILTER_ALLOCATED_SIZE_BETWEEN = 0x09, + BLE_OTS_FILTER_MARKED_OBJECTS = 0x0A, +} ble_ots_list_filter_type_t; + +/***************************************************************************** + * Object List Filter Characteristic Structure + *****************************************************************************/ +typedef struct { + uint8_t filter_type; /**< One of ble_ots_list_filter_type_t */ + union { + /** For Name Starts With / Ends With / Contains / Is Exactly */ + struct { + uint8_t name[BLE_OTS_OBJECT_NAME_MAX_LEN]; + uint8_t name_len; + } name; + /** For Object Type filter */ + struct { + uint8_t uuid[16]; /**< 2 or 16 octets */ + uint8_t uuid_len; /**< 2 or 16 */ + } obj_type; + /** For Created Between / Modified Between */ + struct { + ble_ots_date_time_t timestamp1; + ble_ots_date_time_t timestamp2; + } timestamp_range; + /** For Current Size Between / Allocated Size Between */ + struct { + uint32_t size1; + uint32_t size2; + } size_range; + } param; +} ble_ots_list_filter_t; + +/***************************************************************************** + * Object Changed Flags Bit-Field (8-bit) + *****************************************************************************/ +#define BLE_OTS_OBJ_CHANGED_FLAG_SOURCE (1 << 0) /**< 0 = Server, 1 = Client */ +#define BLE_OTS_OBJ_CHANGED_FLAG_CONTENT (1 << 1) /**< Object contents changed */ +#define BLE_OTS_OBJ_CHANGED_FLAG_METADATA (1 << 2) /**< Object metadata changed */ +#define BLE_OTS_OBJ_CHANGED_FLAG_CREATION (1 << 3) /**< Object creation */ +#define BLE_OTS_OBJ_CHANGED_FLAG_DELETION (1 << 4) /**< Object deletion */ + +/***************************************************************************** + * Object Changed Characteristic Structure (7 octets) + *****************************************************************************/ +typedef struct { + uint8_t flags; /**< Object Changed flags bit-field */ + ble_ots_obj_id_t object_id; /**< Object ID of the changed object */ +} ble_ots_obj_changed_t; + +/***************************************************************************** + * List Sort Order Values + *****************************************************************************/ +typedef enum { + BLE_OTS_SORT_ORDER_NAME_ASC = 0x01, + BLE_OTS_SORT_ORDER_TYPE_ASC = 0x02, + BLE_OTS_SORT_ORDER_CURRENT_SIZE_ASC = 0x03, + BLE_OTS_SORT_ORDER_FIRST_CREATED_ASC = 0x04, + BLE_OTS_SORT_ORDER_LAST_MODIFIED_ASC = 0x05, + BLE_OTS_SORT_ORDER_NAME_DESC = 0x11, + BLE_OTS_SORT_ORDER_TYPE_DESC = 0x12, + BLE_OTS_SORT_ORDER_CURRENT_SIZE_DESC = 0x13, + BLE_OTS_SORT_ORDER_FIRST_CREATED_DESC = 0x14, + BLE_OTS_SORT_ORDER_LAST_MODIFIED_DESC = 0x15, +} ble_ots_list_sort_order_t; + +/***************************************************************************** + * Application Error Codes + *****************************************************************************/ +typedef enum { + BLE_OTS_APP_ERR_WRITE_REQUEST_REJECTED = 0x80, + BLE_OTS_APP_ERR_OBJECT_NOT_SELECTED = 0x81, + BLE_OTS_APP_ERR_CONCURRENCY_LIMIT_EXCEEDED = 0x82, + BLE_OTS_APP_ERR_OBJECT_NAME_ALREADY_EXISTS = 0x83, +} ble_ots_app_error_code_t; + +/***************************************************************************** + * Directory Listing Object Record Flags (8-bit) + *****************************************************************************/ +#define BLE_OTS_DLO_FLAG_TYPE_UUID_128 (1 << 0) /**< 0 = 16-bit, 1 = 128-bit UUID */ +#define BLE_OTS_DLO_FLAG_CURRENT_SIZE_PRESENT (1 << 1) +#define BLE_OTS_DLO_FLAG_ALLOCATED_SIZE_PRESENT (1 << 2) +#define BLE_OTS_DLO_FLAG_FIRST_CREATED_PRESENT (1 << 3) +#define BLE_OTS_DLO_FLAG_LAST_MODIFIED_PRESENT (1 << 4) +#define BLE_OTS_DLO_FLAG_PROPERTIES_PRESENT (1 << 5) +#define BLE_OTS_DLO_FLAG_EXTENDED_FLAGS_PRESENT (1 << 7) + +/***************************************************************************** + * Object Type Entry (for storing 16-bit or 128-bit UUIDs) + *****************************************************************************/ +typedef struct { + uint8_t uuid_len; /**< 2 for 16-bit UUID, 16 for 128-bit UUID */ + uint8_t uuid[16]; /**< UUID value (little-endian) */ +} ble_ots_obj_type_entry_t; + +/***************************************************************************** + * CRC-32 Checksum Calculation + *****************************************************************************/ + +/** + * @brief Compute ISO/IEC 3309 CRC-32 over a byte range of object data. + * + * Uses the Ethernet/HDLC CRC-32 algorithm: + * - Polynomial: 0x04C11DB7 + * - Initial value: 0xFFFFFFFF + * - Final XOR: 0xFFFFFFFF + * - Input/output reflection: enabled + * + * @param data Pointer to the object data buffer. + * @param offset Byte offset from the start of data at which to begin CRC calculation. + * @param length Number of octets over which to compute the CRC, starting from offset. + * @return The computed 32-bit CRC value. + */ +uint32_t ble_ots_checksum_calculate(const uint8_t *data, uint32_t offset, uint32_t length); + +#ifdef __cplusplus +} +#endif + +#endif /* BLE_OTS_COMMON_H */ diff --git a/components/bt/ble_profiles/nimble/ble_ots/include/ble_ots_server.h b/components/bt/ble_profiles/nimble/ble_ots/include/ble_ots_server.h new file mode 100644 index 00000000000..af0c5a76388 --- /dev/null +++ b/components/bt/ble_profiles/nimble/ble_ots/include/ble_ots_server.h @@ -0,0 +1,342 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef BLE_OTS_SERVER_H +#define BLE_OTS_SERVER_H + +#include +#include +#include "ble_ots_common.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/***************************************************************************** + * Kconfig Defaults + *****************************************************************************/ +#ifndef CONFIG_BLE_OTS_SERVER_MAX_OBJECTS +#define CONFIG_BLE_OTS_SERVER_MAX_OBJECTS 10 +#endif + +#ifndef CONFIG_BLE_OTS_SERVER_MAX_CONCURRENCY +#define CONFIG_BLE_OTS_SERVER_MAX_CONCURRENCY 1 +#endif + +#ifndef CONFIG_BLE_OTS_SERVER_TRANSFER_TIMEOUT_SEC +#define CONFIG_BLE_OTS_SERVER_TRANSFER_TIMEOUT_SEC 30 +#endif + +/***************************************************************************** + * Constants + *****************************************************************************/ + +/** @brief Invalid Object ID sentinel (outside the valid 48-bit range) */ +#define BLE_OTS_OBJ_ID_INVALID 0xFFFFFFFFFFFFFFFFULL + +/***************************************************************************** + * Transfer Completion Status + *****************************************************************************/ + +/** + * @brief Status codes for object transfer completion. + */ +typedef enum { + BLE_OTS_TRANSFER_SUCCESS = 0x00, /*!< All expected octets transferred */ + BLE_OTS_TRANSFER_ABORTED = 0x01, /*!< Aborted via OACP Abort */ + BLE_OTS_TRANSFER_TIMEOUT = 0x02, /*!< Inactivity timeout */ + BLE_OTS_TRANSFER_CHANNEL_CLOSED = 0x03, /*!< OTC closed unexpectedly */ + BLE_OTS_TRANSFER_EXCESS_DATA = 0x04, /*!< More data received than expected */ +} ble_ots_server_transfer_status_t; + +/***************************************************************************** + * Server Configuration + *****************************************************************************/ + +/** + * @brief Configuration for OTS server initialization. + */ +typedef struct { + uint32_t oacp_features; /*!< OACP feature bit-field */ + uint32_t olcp_features; /*!< OLCP feature bit-field */ + const ble_ots_obj_type_entry_t *supported_types; /*!< Supported type UUIDs for OACP Create */ + uint8_t num_supported_types; /*!< Number of entries in supported_types */ + bool include_obj_first_created; /*!< Include Object First-Created char */ + bool include_obj_last_modified; /*!< Include Object Last-Modified char */ + bool include_obj_changed; /*!< Include Object Changed char */ + bool include_obj_list_filter; /*!< Include 3x Object List Filter chars */ + bool has_realtime_clock; /*!< Server has a real-time clock */ + bool obj_name_writable; /*!< Object Name supports Write */ + bool obj_properties_writable; /*!< Object Properties supports Write */ +} ble_ots_server_config_t; + +/***************************************************************************** + * Object Parameters (for ble_ots_server_add_object) + *****************************************************************************/ + +/** + * @brief Parameters for adding a pre-populated object to the server. + */ +typedef struct { + const char *name; /*!< UTF-8 object name (0–120 octets), may be NULL */ + uint8_t name_len; /*!< Length of name in octets */ + ble_ots_obj_type_entry_t type; /*!< Object type UUID */ + uint32_t properties; /*!< Initial object properties bit-field */ + ble_ots_date_time_t first_created; /*!< First-created timestamp */ + ble_ots_date_time_t last_modified; /*!< Last-modified timestamp */ + const uint8_t *data; /*!< Initial content data, may be NULL */ + uint32_t data_len; /*!< Length of initial data in octets */ + uint32_t allocated_size; /*!< Allocated size (>= data_len) */ +} ble_ots_server_obj_params_t; + +/***************************************************************************** + * Event Types + *****************************************************************************/ + +/** + * @brief OTS server event types dispatched through the application callback. + */ +typedef enum { + BLE_OTS_SERVER_EVT_OBJECT_CREATED, /*!< Object created via OACP Create */ + BLE_OTS_SERVER_EVT_OBJECT_DELETED, /*!< Object deleted via OACP Delete */ + BLE_OTS_SERVER_EVT_EXECUTE, /*!< OACP Execute on current object */ + BLE_OTS_SERVER_EVT_CHECKSUM_REQUEST, /*!< OACP Calculate Checksum completed */ + BLE_OTS_SERVER_EVT_READ_COMPLETE, /*!< Read transfer completed */ + BLE_OTS_SERVER_EVT_WRITE_COMPLETE, /*!< Write transfer completed */ + BLE_OTS_SERVER_EVT_METADATA_WRITTEN, /*!< Client wrote a metadata characteristic */ +} ble_ots_server_event_t; + +/***************************************************************************** + * Event Data Structures + *****************************************************************************/ + +/** + * @brief Event data for BLE_OTS_SERVER_EVT_OBJECT_CREATED. + */ +typedef struct { + ble_ots_obj_id_t object_id; /*!< Allocated Object ID */ + ble_ots_obj_type_entry_t type; /*!< Object Type UUID */ + uint32_t allocated_size; /*!< Allocated size in octets */ +} ble_ots_server_oacp_create_evt_t; + +/** + * @brief Event data for BLE_OTS_SERVER_EVT_OBJECT_DELETED. + */ +typedef struct { + ble_ots_obj_id_t object_id; /*!< Deleted Object ID */ +} ble_ots_server_oacp_delete_evt_t; + +/** + * @brief Event data for BLE_OTS_SERVER_EVT_EXECUTE. + * + * @note The callback carries out the execute action and reports its outcome in + * @p result, which the server then indicates to the client. Work that + * cannot complete inside the callback must be acknowledged with + * BLE_OTS_OACP_RESULT_SUCCESS once accepted, since the OACP indication is + * sent as soon as the callback returns. + */ +typedef struct { + ble_ots_obj_id_t object_id; /*!< Current Object ID */ + const uint8_t *param; /*!< Optional parameter data (NULL if none), valid + only for the duration of the callback */ + uint16_t param_len; /*!< Parameter data length (0 if none) */ + uint8_t result; /*!< [out] OACP Result Code sent to the client, one of + ble_ots_oacp_result_code_t. Pre-set to + BLE_OTS_OACP_RESULT_SUCCESS; overwrite it with e.g. + BLE_OTS_OACP_RESULT_INVALID_PARAMETER or + BLE_OTS_OACP_RESULT_OPERATION_FAILED to report a + failure. Out-of-range values are reported as + BLE_OTS_OACP_RESULT_OPERATION_FAILED. */ +} ble_ots_server_oacp_execute_evt_t; + +/** + * @brief Event data for BLE_OTS_SERVER_EVT_CHECKSUM_REQUEST. + */ +typedef struct { + ble_ots_obj_id_t object_id; /*!< Object ID */ + uint32_t offset; /*!< Byte offset of checksum range */ + uint32_t length; /*!< Length of checksum range */ + uint32_t checksum; /*!< Computed CRC-32 value */ +} ble_ots_server_oacp_checksum_evt_t; + +/** + * @brief Event data for BLE_OTS_SERVER_EVT_READ_COMPLETE. + */ +typedef struct { + ble_ots_obj_id_t object_id; /*!< Object ID that was read */ + uint32_t offset; /*!< Starting offset */ + uint32_t length; /*!< Requested length in octets */ + uint32_t bytes_sent; /*!< Actual octets sent */ + ble_ots_server_transfer_status_t status; /*!< Completion status */ +} ble_ots_server_evt_read_complete_t; + +/** + * @brief Event data for BLE_OTS_SERVER_EVT_WRITE_COMPLETE. + */ +typedef struct { + ble_ots_obj_id_t object_id; /*!< Object ID that was written */ + uint32_t offset; /*!< Starting offset */ + uint32_t bytes_received; /*!< Actual octets received */ + ble_ots_server_transfer_status_t status; /*!< Completion status */ +} ble_ots_server_evt_write_complete_t; + +/** + * @brief Event data for BLE_OTS_SERVER_EVT_METADATA_WRITTEN. + */ +typedef struct { + uint16_t conn_id; /*!< Connection ID of the writing client */ + ble_ots_obj_id_t object_id; /*!< Object ID of affected object */ + uint16_t char_uuid; /*!< UUID of written characteristic */ + const char *name; /*!< New name (valid when char_uuid == 0x2ABE) */ + uint8_t name_len; /*!< Name length (valid when char_uuid == 0x2ABE) */ + ble_ots_date_time_t date_time; /*!< New timestamp (valid for 0x2AC1/0x2AC2) */ + uint32_t properties; /*!< New properties (valid for 0x2AC4) */ +} ble_ots_server_metadata_evt_t; + +/***************************************************************************** + * Callback Parameter Union + *****************************************************************************/ + +/** + * @brief Union of all OTS server event parameter structures. + */ +typedef union { + ble_ots_server_oacp_create_evt_t object_created; /*!< BLE_OTS_SERVER_EVT_OBJECT_CREATED */ + ble_ots_server_oacp_delete_evt_t object_deleted; /*!< BLE_OTS_SERVER_EVT_OBJECT_DELETED */ + ble_ots_server_oacp_execute_evt_t execute; /*!< BLE_OTS_SERVER_EVT_EXECUTE */ + ble_ots_server_oacp_checksum_evt_t checksum; /*!< BLE_OTS_SERVER_EVT_CHECKSUM_REQUEST */ + ble_ots_server_evt_read_complete_t read_complete; /*!< BLE_OTS_SERVER_EVT_READ_COMPLETE */ + ble_ots_server_evt_write_complete_t write_complete; /*!< BLE_OTS_SERVER_EVT_WRITE_COMPLETE */ + ble_ots_server_metadata_evt_t metadata_written; /*!< BLE_OTS_SERVER_EVT_METADATA_WRITTEN */ +} ble_ots_server_cb_param_t; + +/***************************************************************************** + * Callback Type + *****************************************************************************/ + +/** + * @brief Application callback for all OTS server events. + * + * Runs in the NimBLE host task with the OTS server's internal lock held, so it + * must not block: no delays, no waiting on semaphores or queues, no blocking + * I/O. Post the work to your own task instead. Calling ble_ots_server_* APIs + * from within the callback is safe. + * + * @param event Event type + * @param param Event-specific parameter data + */ +typedef void (*ble_ots_server_cb_t)(ble_ots_server_event_t event, + ble_ots_server_cb_param_t *param); + +/***************************************************************************** + * Public APIs + *****************************************************************************/ + +/** + * @brief Initialize the OTS server. + * + * Validates configuration, allocates object database and concurrency pool, + * registers the GATT service with all OTS characteristics and CCCDs, + * registers the L2CAP server on PSM_OTS, and creates the Directory Listing + * Object if the server supports multiple objects. + * + * @note The GATT service and the L2CAP PSM listener are registered only on the + * first call after boot, because NimBLE offers no way to unregister + * them. A later call following ble_ots_server_deinit() re-creates the + * object database and the concurrency pool but keeps the characteristic + * layout established by the first initialization — @p config fields that + * affect that layout (obj_name_writable, include_obj_changed, + * include_obj_list_filter, ...) are ignored from then on. + * + * @param config Pointer to server configuration + * @return 0 on success, non-zero error code on failure + */ +int ble_ots_server_init(const ble_ots_server_config_t *config); + +/** + * @brief Deinitialize the OTS server. + * + * Stops serving OTS, closes open OTC channels, unregisters the GAP event + * listener and frees object storage and the concurrency pool. Further requests + * from peers are rejected until the server is initialized again. + * + * @note The GATT service and the L2CAP PSM listener stay registered with the + * NimBLE host: it provides no API to remove them. Their callbacks remain + * live and reject every request while the server is deinitialized. + * + * @return 0 on success, non-zero error code on failure + */ +int ble_ots_server_deinit(void); + +/** + * @brief Register the application event callback. + * + * Only one callback may be registered; calling again replaces the previous one. + * + * @param callback Application event callback function + * @return 0 on success, non-zero error code on failure + */ +int ble_ots_server_register_cb(ble_ots_server_cb_t callback); + +/** + * @brief Add a pre-populated object to the server object database. + * + * Server-initiated creation (not via OACP). Allocates a unique Object ID, + * stores metadata and optional initial content. Triggers Object Changed + * indication with Creation flag. + * + * @param params Pointer to object parameters + * @param out_obj_id Output: allocated Object ID on success (may be NULL) + * @return 0 on success, negative error code on failure + */ +int ble_ots_server_add_object(const ble_ots_server_obj_params_t *params, + ble_ots_obj_id_t *out_obj_id); + +/** + * @brief Remove an object from the server object database. + * + * Server-initiated deletion. The DLO (Object ID 0) cannot be removed. + * Triggers Object Changed indication with Deletion flag. + * + * @param object_id Object ID to remove + * @return 0 on success, negative error code on failure + */ +int ble_ots_server_remove_object(ble_ots_obj_id_t object_id); + +/** + * @brief Set or update object content data from the server side. + * + * Updates Current Size and may increase Allocated Size. Triggers Object + * Changed indication with Content Changed flag. + * + * @param object_id Object ID of the target object + * @param data Pointer to data buffer + * @param offset Byte offset within the object + * @param length Number of octets to write + * @return 0 on success, negative error code on failure + */ +int ble_ots_server_set_object_data(ble_ots_obj_id_t object_id, + const uint8_t *data, + uint32_t offset, + uint32_t length); + +/** + * @brief Trigger Object Changed indication for a server-initiated change. + * + * Dispatches indication to all subscribed clients. Source of Change (bit 0) + * is forced to 0 (Server). The DLO (ID 0) cannot be the target. + * + * @param object_id Object ID of the changed object + * @param flags Change flags (BLE_OTS_OBJ_CHANGED_FLAG_*) + * @return 0 on success, non-zero error code on failure + */ +int ble_ots_server_object_changed(ble_ots_obj_id_t object_id, uint8_t flags); + +#ifdef __cplusplus +} +#endif + +#endif /* BLE_OTS_SERVER_H */ diff --git a/components/bt/ble_profiles/nimble/ble_ots/src/ble_ots_common.c b/components/bt/ble_profiles/nimble/ble_ots/src/ble_ots_common.c new file mode 100644 index 00000000000..5cac2dbf2cb --- /dev/null +++ b/components/bt/ble_profiles/nimble/ble_ots/src/ble_ots_common.c @@ -0,0 +1,17 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "esp_rom_crc.h" +#include "ble_ots_common.h" + +uint32_t ble_ots_checksum_calculate(const uint8_t *data, uint32_t offset, uint32_t length) +{ + /* esp_rom_crc32_le() inverts the CRC register on entry and on exit, so the + * CRC-32 flavour OTS requires (init 0xFFFFFFFF, refin/refout enabled, + * xorout 0xFFFFFFFF) is obtained by seeding with ~0xFFFFFFFF == 0 and + * returning the value unchanged. A zero-length range yields 0, as before. */ + return esp_rom_crc32_le(0, data + offset, length); +} diff --git a/components/bt/ble_profiles/nimble/ble_ots/src/client/ble_ots_client_discovery.c b/components/bt/ble_profiles/nimble/ble_ots/src/client/ble_ots_client_discovery.c new file mode 100644 index 00000000000..8bf5f047d22 --- /dev/null +++ b/components/bt/ble_profiles/nimble/ble_ots/src/client/ble_ots_client_discovery.c @@ -0,0 +1,878 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include + +#include "esp_log.h" + +#include "nimble/ble.h" +#include "nimble/nimble_port.h" +#include "nimble/nimble_npl.h" +#include "host/ble_hs.h" +#include "host/ble_gap.h" +#include "host/ble_gatt.h" +#include "host/ble_uuid.h" + +#include "ble_ots_client.h" +#include "ble_ots_client_int.h" + +static const char *TAG = "ots_client_disc"; + +/***************************************************************************** + * Discovery State Machine Steps + *****************************************************************************/ +enum { + DISC_STATE_IDLE = 0, + DISC_STATE_SVC, + DISC_STATE_CHRS, + DISC_STATE_DSCS, + DISC_STATE_OACP_CCCD, + DISC_STATE_OLCP_CCCD, + DISC_STATE_DONE, +}; + +/***************************************************************************** + * Global Module State + *****************************************************************************/ +ble_ots_client_state_t *g_ots_client = NULL; + +/***************************************************************************** + * Forward Declarations + *****************************************************************************/ +static int ots_client_gap_event_handler(struct ble_gap_event *event, void *arg); +static int disc_svc_cb(uint16_t conn_handle, const struct ble_gatt_error *error, + const struct ble_gatt_svc *service, void *arg); +static int disc_chr_cb(uint16_t conn_handle, const struct ble_gatt_error *error, + const struct ble_gatt_chr *chr, void *arg); +static int disc_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 int write_cccd_cb(uint16_t conn_handle, const struct ble_gatt_error *error, + struct ble_gatt_attr *attr, void *arg); +static int feature_read_cb(uint16_t conn_handle, const struct ble_gatt_error *error, + struct ble_gatt_attr *attr, void *arg); +static void disc_complete(uint16_t conn_handle, int status); +static int disc_next_step(uint16_t conn_handle); + +/***************************************************************************** + * Context Management + *****************************************************************************/ + +static ble_ots_client_conn_ctx_t *alloc_conn_ctx(uint16_t conn_id) +{ + if (!g_ots_client) { + return NULL; + } + + /* Check if context already exists for this conn_id — fully clean up and + * release it so the allocation loop below can assign a fresh slot. */ + for (int i = 0; i < BLE_OTS_CLIENT_MAX_CONNECTIONS; i++) { + if (g_ots_client->conns[i].in_use && + g_ots_client->conns[i].conn_id == conn_id) { + ble_ots_client_remove_conn_ctx(conn_id); + break; + } + } + + /* Allocate a free slot */ + for (int i = 0; i < BLE_OTS_CLIENT_MAX_CONNECTIONS; i++) { + if (!g_ots_client->conns[i].in_use) { + ble_ots_client_conn_ctx_t *ctx = &g_ots_client->conns[i]; + memset(ctx, 0, sizeof(*ctx)); + ctx->conn_id = conn_id; + ctx->in_use = true; + ctx->server_supports_read_long = true; + ctx->server_supports_write_long = true; + + /* Timers (CP / transfer / OTC retry) are NimBLE callouts created + * lazily on first use; nothing to do here after the memset. */ + + return ctx; + } + } + + ESP_LOGE(TAG, "No free connection context slots"); + return NULL; +} + +ble_ots_client_conn_ctx_t *ble_ots_client_get_conn_ctx(uint16_t conn_id) +{ + if (!g_ots_client) { + return NULL; + } + + for (int i = 0; i < BLE_OTS_CLIENT_MAX_CONNECTIONS; i++) { + if (g_ots_client->conns[i].in_use && + g_ots_client->conns[i].conn_id == conn_id) { + return &g_ots_client->conns[i]; + } + } + return NULL; +} + +void ble_ots_client_remove_conn_ctx(uint16_t conn_id) +{ + if (!g_ots_client) { + return; + } + + for (int i = 0; i < BLE_OTS_CLIENT_MAX_CONNECTIONS; i++) { + ble_ots_client_conn_ctx_t *ctx = &g_ots_client->conns[i]; + if (ctx->in_use && ctx->conn_id == conn_id) { + /* Disconnect L2CAP channel if open */ + if (ctx->otc_open && ctx->otc_chan != NULL) { + ble_l2cap_disconnect(ctx->otc_chan); + ctx->otc_chan = NULL; + ctx->otc_open = false; + } + /* Stop and release the CP timer callout */ + if (ctx->cp_timer_inited) { + ble_npl_callout_stop(&ctx->cp_timer); + ble_npl_callout_deinit(&ctx->cp_timer); + ctx->cp_timer_inited = false; + ctx->cp_timer_active = false; + } + /* Stop and release the transfer timer callout */ + if (ctx->transfer_timer_inited) { + ble_npl_callout_stop(&ctx->transfer_timer); + ble_npl_callout_deinit(&ctx->transfer_timer); + ctx->transfer_timer_inited = false; + ctx->transfer_timer_active = false; + } + /* Stop and release the OTC connect retry callout */ + if (ctx->otc_retry_timer_inited) { + ble_npl_callout_stop(&ctx->otc_retry_timer); + ble_npl_callout_deinit(&ctx->otc_retry_timer); + ctx->otc_retry_timer_inited = false; + } + /* Remove any pending deferred OACP dispatch. ble_npl_event_deinit() + * is required as well: the ESP-IDF NPL port allocates the internal + * event object on first ble_npl_event_init(), and alloc_conn_ctx() + * memsets the whole context when the slot is reused — which would + * drop the pointer and leak the block. */ + if (ctx->oacp_dispatch_ev_ready) { + ble_npl_eventq_remove(nimble_port_get_dflt_eventq(), + &ctx->oacp_dispatch_ev); + ble_npl_event_deinit(&ctx->oacp_dispatch_ev); + ctx->oacp_dispatch_ev_ready = false; + } + ctx->in_use = false; + ESP_LOGI(TAG, "Removed conn ctx for conn_handle=%d", conn_id); + return; + } + } +} + +/***************************************************************************** + * Event Dispatch + *****************************************************************************/ + +void ble_ots_client_dispatch_event(uint16_t conn_id, + ble_ots_client_event_t event, + const void *param) +{ + if (g_ots_client && g_ots_client->app_cb) { + g_ots_client->app_cb(conn_id, event, param); + } +} + +/***************************************************************************** + * Control-Point Timer + *****************************************************************************/ + +/* CP-timeout handler. The CP timer is a NimBLE callout bound to the host + * default event queue, so this runs in the NimBLE host task — serialised with + * the response handlers that stop the timer, and honouring the documented + * "callback runs in the BLE host task" contract for the app callback. */ +static void cp_timer_cb(struct ble_npl_event *ev) +{ + uint16_t conn_id = (uint16_t)(uintptr_t)ble_npl_event_get_arg(ev); + + ble_ots_client_conn_ctx_t *ctx = ble_ots_client_get_conn_ctx(conn_id); + if (!ctx) { + return; + } + + /* Guard against a callout that was already stopped but whose event had + * been posted to the queue before the stop took effect. */ + if (!ctx->cp_timer_active) { + ESP_LOGD(TAG, "CP timeout ignored (timer already stopped), conn_handle=%d", conn_id); + return; + } + + ESP_LOGW(TAG, "CP timer expired for conn_handle=%d", conn_id); + + ctx->cp_timer_active = false; + ctx->cp_timed_out = true; + + ble_ots_client_transfer_timeout_t evt = { + .conn_id = conn_id, + .opcode = ctx->cp_pending_opcode, + }; + + /* The procedure is over (unsuccessfully): release the synchronous busy + * flag so it does not block later control-point operations. */ + ctx->cp_pending_opcode = 0; + + ble_ots_client_dispatch_event(conn_id, BLE_OTS_CLIENT_EVT_CP_TIMEOUT, &evt); +} + +int ble_ots_client_start_cp_timer(uint16_t conn_id, uint32_t timeout_ms) +{ + ble_ots_client_conn_ctx_t *ctx = ble_ots_client_get_conn_ctx(conn_id); + if (!ctx) { + return BLE_HS_ENOTCONN; + } + + if (ctx->cp_timed_out) { + ESP_LOGE(TAG, "CP timed out previously, cannot start new timer"); + return BLE_HS_EREJECT; + } + + /* Initialise the callout lazily on first use */ + if (!ctx->cp_timer_inited) { + int rc = ble_npl_callout_init(&ctx->cp_timer, nimble_port_get_dflt_eventq(), + cp_timer_cb, (void *)(uintptr_t)conn_id); + if (rc != 0) { + ESP_LOGE(TAG, "Failed to init CP timer callout: %d", rc); + return BLE_HS_ENOMEM; + } + ctx->cp_timer_inited = true; + } + + /* ble_npl_callout_reset() re-arms an already-running callout, so there is + * no need to stop it first. */ + ble_npl_error_t err = ble_npl_callout_reset(&ctx->cp_timer, + ble_npl_time_ms_to_ticks32(timeout_ms)); + if (err != BLE_NPL_OK) { + ESP_LOGE(TAG, "Failed to start CP timer: %d", err); + return BLE_HS_EOS; + } + + ctx->cp_timer_active = true; + ESP_LOGD(TAG, "CP timer started: %lu ms, conn_handle=%d", (unsigned long)timeout_ms, conn_id); + return 0; +} + +void ble_ots_client_stop_cp_timer(uint16_t conn_id) +{ + ble_ots_client_conn_ctx_t *ctx = ble_ots_client_get_conn_ctx(conn_id); + if (!ctx) { + return; + } + + /* Stop unconditionally (not gated on cp_timer_active) so a callout can + * never be left armed because the flag was out of step with the callout. */ + if (ctx->cp_timer_inited) { + ble_npl_callout_stop(&ctx->cp_timer); + ctx->cp_timer_active = false; + ESP_LOGD(TAG, "CP timer stopped for conn_handle=%d", conn_id); + } +} + +/***************************************************************************** + * Init / Deinit + *****************************************************************************/ + +int ble_ots_client_init(ble_ots_client_event_cb_t callback) +{ + if (!callback) { + ESP_LOGE(TAG, "Callback must not be NULL"); + return BLE_HS_EINVAL; + } + + if (g_ots_client && g_ots_client->initialized) { + ESP_LOGE(TAG, "Already initialized, call deinit first"); + return BLE_HS_EALREADY; + } + + g_ots_client = calloc(1, sizeof(ble_ots_client_state_t)); + if (!g_ots_client) { + ESP_LOGE(TAG, "Failed to allocate module state"); + return BLE_HS_ENOMEM; + } + + g_ots_client->app_cb = callback; + g_ots_client->initialized = true; + + /* Register GAP event listener. */ + int rc = ble_gap_event_listener_register(&g_ots_client->gap_listener, + ots_client_gap_event_handler, + NULL); + if (rc != 0) { + ESP_LOGE(TAG, "Failed to register GAP listener: %d", rc); + free(g_ots_client); + g_ots_client = NULL; + return rc; + } + + ESP_LOGI(TAG, "OTS client initialized"); + return 0; +} + +int ble_ots_client_deinit(void) +{ + if (!g_ots_client) { + return 0; + } + + /* Remove all connection contexts */ + for (int i = 0; i < BLE_OTS_CLIENT_MAX_CONNECTIONS; i++) { + if (g_ots_client->conns[i].in_use) { + ble_ots_client_remove_conn_ctx(g_ots_client->conns[i].conn_id); + } + } + + /* Unregister the GAP event listener */ + ble_gap_event_listener_unregister(&g_ots_client->gap_listener); + + free(g_ots_client); + g_ots_client = NULL; + + ESP_LOGI(TAG, "OTS client deinitialized"); + return 0; +} + +/***************************************************************************** + * Service Discovery State Machine + *****************************************************************************/ + +int ble_ots_client_discover_service(uint16_t conn_id) +{ + if (!g_ots_client || !g_ots_client->initialized) { + ESP_LOGE(TAG, "OTS client not initialized"); + return BLE_HS_EINVAL; + } + + ble_ots_client_conn_ctx_t *ctx = alloc_conn_ctx(conn_id); + if (!ctx) { + return BLE_HS_ENOMEM; + } + + ctx->disc_state = DISC_STATE_SVC; + + ble_uuid16_t svc_uuid = BLE_UUID16_INIT(BLE_OTS_UUID_OTS_SERVICE); + int rc = ble_gattc_disc_svc_by_uuid(conn_id, &svc_uuid.u, disc_svc_cb, NULL); + if (rc != 0) { + ESP_LOGE(TAG, "Failed to start service discovery: %d", rc); + ble_ots_client_remove_conn_ctx(conn_id); + return rc; + } + + ESP_LOGI(TAG, "OTS service discovery started, conn_handle=%d", conn_id); + return 0; +} + +static int disc_svc_cb(uint16_t conn_handle, const struct ble_gatt_error *error, + const struct ble_gatt_svc *service, void *arg) +{ + ble_ots_client_conn_ctx_t *ctx = ble_ots_client_get_conn_ctx(conn_handle); + if (!ctx) { + return BLE_HS_ENOTCONN; + } + + if (error->status == 0 && service != NULL) { + /* Service found — store handles */ + ctx->disc_svc_start_handle = service->start_handle; + ctx->disc_svc_end_handle = service->end_handle; + ESP_LOGI(TAG, "OTS service found: start=0x%04x end=0x%04x", + service->start_handle, service->end_handle); + return 0; + } + + if (error->status == BLE_HS_EDONE) { + /* Discovery procedure complete */ + if (ctx->disc_svc_start_handle == 0) { + ESP_LOGE(TAG, "OTS service not found"); + disc_complete(conn_handle, BLE_HS_ENOENT); + return 0; + } + /* Move to characteristic discovery */ + ctx->disc_state = DISC_STATE_CHRS; + int rc = disc_next_step(conn_handle); + if (rc != 0) { + disc_complete(conn_handle, rc); + } + return 0; + } + + /* Error */ + ESP_LOGE(TAG, "Service discovery error: status=%d", error->status); + disc_complete(conn_handle, error->status); + return 0; +} + +static void map_chr_uuid_to_handle(ble_ots_client_conn_ctx_t *ctx, + const struct ble_gatt_chr *chr) +{ + uint16_t uuid16 = 0; + + if (chr->uuid.u.type == BLE_UUID_TYPE_16) { + uuid16 = BLE_UUID16(&chr->uuid.u)->value; + } else { + return; /* OTS uses only 16-bit UUIDs */ + } + + switch (uuid16) { + case BLE_OTS_UUID_OTS_FEATURE: + ctx->handles.ots_feature_handle = chr->val_handle; + break; + case BLE_OTS_UUID_OBJECT_NAME: + ctx->handles.object_name_handle = chr->val_handle; + break; + case BLE_OTS_UUID_OBJECT_TYPE: + ctx->handles.object_type_handle = chr->val_handle; + break; + case BLE_OTS_UUID_OBJECT_SIZE: + ctx->handles.object_size_handle = chr->val_handle; + break; + case BLE_OTS_UUID_OBJECT_FIRST_CREATED: + ctx->handles.first_created_handle = chr->val_handle; + break; + case BLE_OTS_UUID_OBJECT_LAST_MODIFIED: + ctx->handles.last_modified_handle = chr->val_handle; + break; + case BLE_OTS_UUID_OBJECT_ID: + ctx->handles.object_id_handle = chr->val_handle; + break; + case BLE_OTS_UUID_OBJECT_PROPERTIES: + ctx->handles.object_properties_handle = chr->val_handle; + break; + case BLE_OTS_UUID_OACP: + ctx->handles.oacp_handle = chr->val_handle; + break; + case BLE_OTS_UUID_OLCP: + ctx->handles.olcp_handle = chr->val_handle; + break; + case BLE_OTS_UUID_OBJECT_LIST_FILTER: + /* Spec mandates either three instances or none; store them in discovery order */ + for (int i = 0; i < 3; i++) { + if (ctx->handles.object_list_filter_handle[i] == 0) { + ctx->handles.object_list_filter_handle[i] = chr->val_handle; + break; + } + } + break; + case BLE_OTS_UUID_OBJECT_CHANGED: + ctx->handles.object_changed_handle = chr->val_handle; + break; + default: + /* Unknown characteristic — be tolerant */ + break; + } +} + +static int disc_chr_cb(uint16_t conn_handle, const struct ble_gatt_error *error, + const struct ble_gatt_chr *chr, void *arg) +{ + ble_ots_client_conn_ctx_t *ctx = ble_ots_client_get_conn_ctx(conn_handle); + if (!ctx) { + return BLE_HS_ENOTCONN; + } + + if (error->status == 0 && chr != NULL) { + map_chr_uuid_to_handle(ctx, chr); + return 0; + } + + if (error->status == BLE_HS_EDONE) { + /* Characteristic discovery complete — move to descriptor discovery */ + ESP_LOGI(TAG, "Characteristic discovery complete"); + + /* Determine multi-object server */ + ctx->multi_object_server = (ctx->handles.olcp_handle != 0); + + /* Move to descriptor discovery */ + ctx->disc_state = DISC_STATE_DSCS; + int rc = disc_next_step(conn_handle); + if (rc != 0) { + disc_complete(conn_handle, rc); + } + return 0; + } + + ESP_LOGE(TAG, "Characteristic discovery error: status=%d", error->status); + disc_complete(conn_handle, error->status); + return 0; +} + +/** + * @brief Find the smallest characteristic value handle that is strictly greater + * than the given handle, within the discovered OTS characteristics. + * + * Used to compute the upper bound for CCCD-to-characteristic mapping. + * + * @param handles Pointer to the discovered handle set + * @param chr_handle The characteristic value handle to find the upper bound for + * @param svc_end The service end handle (used as fallback upper bound) + * @return The upper bound handle (exclusive); may be 0x10000 when svc_end is + * 0xFFFF, hence the wider return type to avoid uint16_t overflow. + */ +static uint32_t find_next_chr_handle(const ble_ots_client_char_handles_t *handles, + uint16_t chr_handle, uint16_t svc_end) +{ + /* Use a 32-bit bound so that svc_end + 1 does not overflow to 0 when + * svc_end is 0xFFFF (a legal BLE service end handle). */ + uint32_t upper = (uint32_t)svc_end + 1; /* default: end of service range (exclusive) */ + + /* Collect all known characteristic value handles */ + const uint16_t all_handles[] = { + handles->ots_feature_handle, + handles->object_name_handle, + handles->object_type_handle, + handles->object_size_handle, + handles->first_created_handle, + handles->last_modified_handle, + handles->object_id_handle, + handles->object_properties_handle, + handles->oacp_handle, + handles->olcp_handle, + handles->object_list_filter_handle[0], + handles->object_list_filter_handle[1], + handles->object_list_filter_handle[2], + handles->object_changed_handle, + }; + + for (size_t i = 0; i < sizeof(all_handles) / sizeof(all_handles[0]); i++) { + if (all_handles[i] > chr_handle && all_handles[i] < upper) { + upper = all_handles[i]; + } + } + + return upper; +} + +static int disc_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_ots_client_conn_ctx_t *ctx = ble_ots_client_get_conn_ctx(conn_handle); + if (!ctx) { + return BLE_HS_ENOTCONN; + } + + if (error->status == 0 && dsc != NULL) { + /* Check if this is a CCCD */ + if (dsc->uuid.u.type == BLE_UUID_TYPE_16 && + BLE_UUID16(&dsc->uuid.u)->value == BLE_GATT_DSC_CLT_CFG_UUID16) { + /* Map CCCD to the correct characteristic using upper-bound checks */ + if (ctx->handles.oacp_handle != 0 && + dsc->handle > ctx->handles.oacp_handle && + dsc->handle < find_next_chr_handle(&ctx->handles, ctx->handles.oacp_handle, ctx->disc_svc_end_handle) && + ctx->handles.oacp_cccd_handle == 0) { + ctx->handles.oacp_cccd_handle = dsc->handle; + ESP_LOGD(TAG, "OACP CCCD found: handle=0x%04x", dsc->handle); + } else if (ctx->handles.olcp_handle != 0 && + dsc->handle > ctx->handles.olcp_handle && + dsc->handle < find_next_chr_handle(&ctx->handles, ctx->handles.olcp_handle, ctx->disc_svc_end_handle) && + ctx->handles.olcp_cccd_handle == 0) { + ctx->handles.olcp_cccd_handle = dsc->handle; + ESP_LOGD(TAG, "OLCP CCCD found: handle=0x%04x", dsc->handle); + } else if (ctx->handles.object_changed_handle != 0 && + dsc->handle > ctx->handles.object_changed_handle && + dsc->handle < find_next_chr_handle(&ctx->handles, ctx->handles.object_changed_handle, ctx->disc_svc_end_handle) && + ctx->handles.object_changed_cccd_handle == 0) { + ctx->handles.object_changed_cccd_handle = dsc->handle; + ESP_LOGD(TAG, "Object Changed CCCD found: handle=0x%04x", dsc->handle); + } + } + return 0; + } + + if (error->status == BLE_HS_EDONE) { + /* Descriptor discovery complete — move to CCCD configuration */ + ESP_LOGI(TAG, "Descriptor discovery complete"); + ctx->disc_state = DISC_STATE_OACP_CCCD; + int rc = disc_next_step(conn_handle); + if (rc != 0) { + disc_complete(conn_handle, rc); + } + return 0; + } + + ESP_LOGE(TAG, "Descriptor discovery error: status=%d", error->status); + disc_complete(conn_handle, error->status); + return 0; +} + +static int write_cccd_cb(uint16_t conn_handle, const struct ble_gatt_error *error, + struct ble_gatt_attr *attr, void *arg) +{ + ble_ots_client_conn_ctx_t *ctx = ble_ots_client_get_conn_ctx(conn_handle); + if (!ctx) { + return 0; + } + + if (error->status != 0) { + ESP_LOGE(TAG, "CCCD write failed: status=%d handle=0x%04x", + error->status, attr ? attr->handle : 0); + disc_complete(conn_handle, error->status); + return 0; + } + + ESP_LOGD(TAG, "CCCD write success, disc_state=%d", ctx->disc_state); + + /* Advance to next step */ + if (ctx->disc_state == DISC_STATE_OACP_CCCD) { + ctx->disc_state = DISC_STATE_OLCP_CCCD; + } else if (ctx->disc_state == DISC_STATE_OLCP_CCCD) { + ctx->disc_state = DISC_STATE_DONE; + } + + int rc = disc_next_step(conn_handle); + if (rc != 0) { + disc_complete(conn_handle, rc); + } + return 0; +} + +static int disc_next_step(uint16_t conn_handle) +{ + ble_ots_client_conn_ctx_t *ctx = ble_ots_client_get_conn_ctx(conn_handle); + if (!ctx) { + return BLE_HS_ENOTCONN; + } + + int rc; + + switch (ctx->disc_state) { + case DISC_STATE_CHRS: + rc = ble_gattc_disc_all_chrs(conn_handle, + ctx->disc_svc_start_handle, + ctx->disc_svc_end_handle, + disc_chr_cb, NULL); + if (rc != 0) { + ESP_LOGE(TAG, "Failed to start chr discovery: %d", rc); + } + return rc; + + case DISC_STATE_DSCS: + /* Discover all descriptors within the service range */ + rc = ble_gattc_disc_all_dscs(conn_handle, + ctx->disc_svc_start_handle, + ctx->disc_svc_end_handle, + disc_dsc_cb, NULL); + if (rc != 0) { + ESP_LOGE(TAG, "Failed to start dsc discovery: %d", rc); + } + return rc; + + case DISC_STATE_OACP_CCCD: { + if (ctx->handles.oacp_cccd_handle == 0) { + ESP_LOGE(TAG, "OACP CCCD not found, discovery failed"); + disc_complete(conn_handle, BLE_HS_ENOENT); + return 0; + } + /* Write CCCD to enable indications (0x0002) */ + uint8_t val[2] = { 0x02, 0x00 }; + rc = ble_gattc_write_flat(conn_handle, + ctx->handles.oacp_cccd_handle, + val, sizeof(val), + write_cccd_cb, NULL); + if (rc != 0) { + ESP_LOGE(TAG, "Failed to write OACP CCCD: %d", rc); + } + return rc; + } + + case DISC_STATE_OLCP_CCCD: { + if (ctx->handles.olcp_cccd_handle == 0) { + if (ctx->handles.olcp_handle != 0) { + /* OLCP characteristic is present but its CCCD is missing — protocol error */ + ESP_LOGE(TAG, "OLCP CCCD not found but OLCP characteristic is present"); + disc_complete(conn_handle, BLE_HS_ENOENT); + return 0; + } + /* OLCP not present — skip to done */ + ctx->disc_state = DISC_STATE_DONE; + disc_complete(conn_handle, 0); + return 0; + } + /* Write CCCD to enable indications (0x0002) */ + uint8_t val[2] = { 0x02, 0x00 }; + rc = ble_gattc_write_flat(conn_handle, + ctx->handles.olcp_cccd_handle, + val, sizeof(val), + write_cccd_cb, NULL); + if (rc != 0) { + ESP_LOGE(TAG, "Failed to write OLCP CCCD: %d", rc); + } + return rc; + } + + case DISC_STATE_DONE: + disc_complete(conn_handle, 0); + return 0; + + default: + ESP_LOGE(TAG, "Unexpected disc_state=%d", ctx->disc_state); + return BLE_HS_EUNKNOWN; + } +} + +static void disc_complete(uint16_t conn_handle, int status) +{ + ble_ots_client_conn_ctx_t *ctx = ble_ots_client_get_conn_ctx(conn_handle); + if (!ctx) { + return; + } + + ctx->disc_state = DISC_STATE_IDLE; + + ble_ots_client_discover_complete_t evt = { + .status = status, + .multi_object_server = ctx->multi_object_server, + }; + memcpy(&evt.handles, &ctx->handles, sizeof(evt.handles)); + + if (status != 0) { + ESP_LOGE(TAG, "Discovery failed: status=%d, conn_handle=%d", status, conn_handle); + /* Release the context BEFORE dispatching the failure event. The event + * callback runs synchronously and may re-enter the OTS client API to + * retry discovery (ble_ots_client_discover_service), which allocates a + * fresh context for this conn_handle. Cleaning up first ensures the + * deferred remove below (if any) cannot destroy that re-allocated + * context. evt already holds a full local copy of the data the + * callback needs, so dispatching after cleanup is safe. */ + ble_ots_client_remove_conn_ctx(conn_handle); + ble_ots_client_dispatch_event(conn_handle, BLE_OTS_CLIENT_EVT_DISCOVER_COMPLETE, &evt); + } else { + ESP_LOGI(TAG, "Discovery complete: conn_handle=%d multi_obj=%d", + conn_handle, ctx->multi_object_server); + ble_ots_client_dispatch_event(conn_handle, BLE_OTS_CLIENT_EVT_DISCOVER_COMPLETE, &evt); + } +} + +/***************************************************************************** + * Feature Read + *****************************************************************************/ + +static int feature_read_cb(uint16_t conn_handle, const struct ble_gatt_error *error, + struct ble_gatt_attr *attr, void *arg) +{ + ble_ots_client_conn_ctx_t *ctx = ble_ots_client_get_conn_ctx(conn_handle); + ble_ots_client_feature_read_t evt; + memset(&evt, 0, sizeof(evt)); + + if (error->status != 0) { + ESP_LOGE(TAG, "Feature read failed: status=%d", error->status); + evt.status = error->status; + ble_ots_client_dispatch_event(conn_handle, BLE_OTS_CLIENT_EVT_FEATURE_READ, &evt); + return 0; + } + + /* Parse 8 bytes: OACP Features (4) + OLCP Features (4), little-endian */ + uint16_t data_len = OS_MBUF_PKTLEN(attr->om); + if (data_len < 8) { + ESP_LOGE(TAG, "Feature value too short: %d bytes", data_len); + evt.status = BLE_HS_EBADDATA; + ble_ots_client_dispatch_event(conn_handle, BLE_OTS_CLIENT_EVT_FEATURE_READ, &evt); + return 0; + } + + uint8_t buf[8]; + int rc = os_mbuf_copydata(attr->om, 0, 8, buf); + if (rc != 0) { + evt.status = BLE_HS_EUNKNOWN; + ble_ots_client_dispatch_event(conn_handle, BLE_OTS_CLIENT_EVT_FEATURE_READ, &evt); + return 0; + } + + evt.feature.oacp_features = (uint32_t)buf[0] | + ((uint32_t)buf[1] << 8) | + ((uint32_t)buf[2] << 16) | + ((uint32_t)buf[3] << 24); + evt.feature.olcp_features = (uint32_t)buf[4] | + ((uint32_t)buf[5] << 8) | + ((uint32_t)buf[6] << 16) | + ((uint32_t)buf[7] << 24); + evt.status = 0; + + /* Cache in context */ + if (ctx) { + ctx->feature = evt.feature; + ctx->feature_valid = true; + } + + ESP_LOGI(TAG, "Feature read: OACP=0x%08lx OLCP=0x%08lx", + (unsigned long)evt.feature.oacp_features, + (unsigned long)evt.feature.olcp_features); + + ble_ots_client_dispatch_event(conn_handle, BLE_OTS_CLIENT_EVT_FEATURE_READ, &evt); + return 0; +} + +int ble_ots_client_read_feature(uint16_t conn_id) +{ + if (!g_ots_client || !g_ots_client->initialized) { + return BLE_HS_EINVAL; + } + + ble_ots_client_conn_ctx_t *ctx = ble_ots_client_get_conn_ctx(conn_id); + if (!ctx) { + ESP_LOGE(TAG, "No context for conn_handle=%d", conn_id); + return BLE_HS_ENOTCONN; + } + + if (ctx->handles.ots_feature_handle == 0) { + ESP_LOGE(TAG, "OTS Feature handle not discovered"); + return BLE_HS_ENOENT; + } + + int rc = ble_gattc_read(conn_id, ctx->handles.ots_feature_handle, + feature_read_cb, NULL); + if (rc != 0) { + ESP_LOGE(TAG, "Failed to read OTS Feature: %d", rc); + } + return rc; +} + +/***************************************************************************** + * GAP Event Handler + *****************************************************************************/ + +static int ots_client_gap_event_handler(struct ble_gap_event *event, void *arg) +{ + switch (event->type) { + case BLE_GAP_EVENT_DISCONNECT: { + uint16_t conn_handle = event->disconnect.conn.conn_handle; + ESP_LOGI(TAG, "Disconnect event: conn_handle=%d reason=%d", + conn_handle, event->disconnect.reason); + ble_ots_client_remove_conn_ctx(conn_handle); + return 0; + } + + case BLE_GAP_EVENT_NOTIFY_RX: { + uint16_t conn_handle = event->notify_rx.conn_handle; + uint16_t attr_handle = event->notify_rx.attr_handle; + + ble_ots_client_conn_ctx_t *ctx = ble_ots_client_get_conn_ctx(conn_handle); + if (!ctx) { + return 0; + } + + /* Route OACP indications to transfer module */ + if (attr_handle == ctx->handles.oacp_handle) { + ble_ots_client_handle_oacp_indication(conn_handle, attr_handle, + event->notify_rx.om); + return 0; + } + + /* Route OLCP and Object Changed indications to object_nav module */ + if ((ctx->handles.olcp_handle != 0 && attr_handle == ctx->handles.olcp_handle) || + (ctx->handles.object_changed_handle != 0 && + attr_handle == ctx->handles.object_changed_handle)) { + return ble_ots_client_object_nav_gap_event(event, arg); + } + + return 0; + } + + default: + return 0; + } +} diff --git a/components/bt/ble_profiles/nimble/ble_ots/src/client/ble_ots_client_int.h b/components/bt/ble_profiles/nimble/ble_ots/src/client/ble_ots_client_int.h new file mode 100644 index 00000000000..eb01ff731fd --- /dev/null +++ b/components/bt/ble_profiles/nimble/ble_ots/src/client/ble_ots_client_int.h @@ -0,0 +1,193 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef BLE_OTS_CLIENT_INT_H +#define BLE_OTS_CLIENT_INT_H + +#include +#include +#include "host/ble_gap.h" +#include "host/ble_l2cap.h" +#include "os/os_mbuf.h" +#include "nimble/nimble_npl.h" +#include "ble_ots_client.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/***************************************************************************** + * Constants + *****************************************************************************/ +#define BLE_OTS_CLIENT_MAX_CONNECTIONS 3 /**< Max simultaneous OTS client connections */ + +/***************************************************************************** + * Per-Connection Client Context + *****************************************************************************/ + +/** + * @brief Per-connection OTS client context. + * + * One instance per active connection. Created on successful discovery, + * destroyed on disconnect or deinit. + */ +typedef struct { + uint16_t conn_id; /*!< BLE connection identifier */ + bool in_use; /*!< true if this context slot is allocated */ + ble_ots_client_char_handles_t handles; /*!< Discovered characteristic handle set */ + ble_ots_feature_t feature; /*!< Cached OTS Feature value */ + bool feature_valid; /*!< true after Feature has been read */ + bool server_supports_read_long; /*!< Initially true; false if server returns Not Supported */ + bool server_supports_write_long; /*!< Initially true; false if server returns Not Supported */ + bool multi_object_server; /*!< true if OLCP was discovered */ + + /* Control-point timeout management */ + struct ble_npl_callout cp_timer; /*!< Control-point response timer (host-task callout) */ + bool cp_timer_inited; /*!< true once cp_timer callout is initialised */ + bool cp_timer_active; /*!< true while CP timer is running */ + bool cp_timed_out; /*!< true if last CP operation timed out */ + uint8_t cp_pending_opcode; /*!< Opcode of pending CP operation; also a synchronous + busy flag that serialises control-point procedures */ + + /* Discovery state */ + uint16_t disc_svc_start_handle; /*!< OTS service start handle */ + uint16_t disc_svc_end_handle; /*!< OTS service end handle */ + uint8_t disc_state; /*!< Internal discovery state machine step */ + + /* L2CAP Object Transfer Channel state (used by transfer module) */ + struct ble_l2cap_chan *otc_chan; /*!< L2CAP channel pointer, NULL if not open */ + bool otc_open; /*!< true if OTC is established */ + uint8_t otc_retry_count; /*!< OTC connect retries attempted (ENOTSUP backoff) */ + struct ble_npl_callout otc_retry_timer; /*!< One-shot backoff callout for OTC connect retry */ + bool otc_retry_timer_inited; /*!< true once otc_retry_timer callout is initialised */ + + /* Transfer tracking (used by transfer module) */ + struct ble_npl_callout transfer_timer; /*!< Data transfer inactivity callout (host-task) */ + bool transfer_timer_inited; /*!< true once transfer_timer callout is initialised */ + bool transfer_timer_active; /*!< true while transfer timer is running */ + uint32_t transfer_offset; /*!< Read/write current byte offset */ + uint32_t transfer_length; /*!< Total read/write length */ + uint32_t transfer_received; /*!< Bytes received so far (read) */ + uint32_t transfer_sent; /*!< Bytes sent so far (write) */ + bool transfer_in_progress; /*!< true during active data transfer */ + uint8_t transfer_opcode; /*!< OACP opcode for active transfer */ + + /* Deferred OACP-response dispatch for Write success */ + struct ble_npl_event oacp_dispatch_ev; /*!< Host-task event to defer OACP_RESPONSE dispatch */ + bool oacp_dispatch_ev_ready; /*!< true once oacp_dispatch_ev has been initialised */ + ble_ots_client_oacp_response_t pending_oacp_resp; /*!< Response payload for the deferred dispatch */ + + /* Name read buffer (used by metadata module for Read Long) */ + char name_buf[CONFIG_BLE_OTS_CLIENT_MAX_NAME_LEN]; /*!< Object name (UTF-8, not NUL-terminated) */ + uint16_t name_buf_len; /*!< Current bytes accumulated in name_buf */ +} ble_ots_client_conn_ctx_t; + +/***************************************************************************** + * Module State + *****************************************************************************/ + +/** + * @brief Global OTS client module state. + * + * Allocated dynamically by ble_ots_client_init(), freed by ble_ots_client_deinit(). + */ +typedef struct { + ble_ots_client_event_cb_t app_cb; /*!< Registered application callback */ + struct ble_gap_event_listener gap_listener; /*!< GAP listener for connect/disconnect events */ + ble_ots_client_conn_ctx_t conns[BLE_OTS_CLIENT_MAX_CONNECTIONS]; /*!< Per-connection contexts */ + bool initialized; /*!< true after successful init */ +} ble_ots_client_state_t; + +/** + * @brief Global module state pointer (owned by ble_ots_client_discovery.c). + */ +extern ble_ots_client_state_t *g_ots_client; + +/* ---- ble_ots_client_discovery.c ---- */ + +/** + * @brief Retrieve the per-connection client context. + * + * @param conn_id BLE connection identifier + * @return Pointer to context, or NULL if not found + */ +ble_ots_client_conn_ctx_t *ble_ots_client_get_conn_ctx(uint16_t conn_id); + +/** + * @brief Dispatch an event to the application callback. + * + * @param conn_id Connection identifier (for logging) + * @param event Event code + * @param param Event-specific parameter structure + */ +void ble_ots_client_dispatch_event(uint16_t conn_id, + ble_ots_client_event_t event, + const void *param); + +/** + * @brief Start a control-point response timer. + * + * If the timer expires, dispatches BLE_OTS_CLIENT_EVT_CP_TIMEOUT or + * BLE_OTS_CLIENT_EVT_TRANSFER_TIMEOUT and sets cp_timed_out = true. + * + * @param conn_id BLE connection identifier + * @param timeout_ms Timeout period in milliseconds + * @return 0 on success, error code on failure + */ +int ble_ots_client_start_cp_timer(uint16_t conn_id, uint32_t timeout_ms); + +/** + * @brief Stop a running control-point response timer. + * + * Safe to call when no timer is running (no-op). + * + * @param conn_id BLE connection identifier + */ +void ble_ots_client_stop_cp_timer(uint16_t conn_id); + +/** + * @brief Remove and free a per-connection context (called on disconnect). + * + * @param conn_id BLE connection identifier + */ +void ble_ots_client_remove_conn_ctx(uint16_t conn_id); + +/* ---- ble_ots_client_object_nav.c ---- */ + +/** + * @brief GAP event handler for OLCP and Object Changed indications. + * + * Routes BLE_GAP_EVENT_NOTIFY_RX events for OLCP and Object Changed + * characteristic handles to the appropriate indication parsers. + * + * @param event GAP event + * @param arg Unused + * @return 0 + */ +int ble_ots_client_object_nav_gap_event(struct ble_gap_event *event, void *arg); + +/* ---- ble_ots_client_transfer.c ---- */ + +/** + * @brief Handle an OACP indication received from the server. + * + * Parses the OACP Response Code indication, stops the CP timer, and + * dispatches BLE_OTS_CLIENT_EVT_OACP_RESPONSE. For Read/Write Success, + * sets up transfer tracking state. + * + * @param conn_handle Connection handle + * @param attr_handle Attribute handle of the OACP characteristic + * @param om Indication payload + */ +void ble_ots_client_handle_oacp_indication(uint16_t conn_handle, + uint16_t attr_handle, + struct os_mbuf *om); + +#ifdef __cplusplus +} +#endif + +#endif /* BLE_OTS_CLIENT_INT_H */ diff --git a/components/bt/ble_profiles/nimble/ble_ots/src/client/ble_ots_client_metadata.c b/components/bt/ble_profiles/nimble/ble_ots/src/client/ble_ots_client_metadata.c new file mode 100644 index 00000000000..735254a60b5 --- /dev/null +++ b/components/bt/ble_profiles/nimble/ble_ots/src/client/ble_ots_client_metadata.c @@ -0,0 +1,911 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include "esp_log.h" +#include "host/ble_hs.h" +#include "host/ble_gatt.h" +#include "host/ble_att.h" +#include "host/ble_hs_mbuf.h" +#include "os/os_mbuf.h" +#include "os/endian.h" +#include "ble_ots_client_int.h" + +static const char *TAG = "ble_ots_meta"; + +/***************************************************************************** + * Helper: encode ble_ots_date_time_t into 7-octet LE buffer + *****************************************************************************/ +static void encode_datetime(const ble_ots_date_time_t *dt, uint8_t buf[7]) +{ + put_le16(buf, dt->year); + buf[2] = dt->month; + buf[3] = dt->day; + buf[4] = dt->hours; + buf[5] = dt->minutes; + buf[6] = dt->seconds; +} + +/***************************************************************************** + * Helper: decode 7-octet LE buffer into ble_ots_date_time_t + *****************************************************************************/ +static void decode_datetime(const uint8_t buf[7], ble_ots_date_time_t *dt) +{ + dt->year = get_le16(buf); + dt->month = buf[2]; + dt->day = buf[3]; + dt->hours = buf[4]; + dt->minutes = buf[5]; + dt->seconds = buf[6]; +} + +/***************************************************************************** + * Helper: get LE uint48 from 6-byte buffer (stored in uint64_t) + *****************************************************************************/ +static uint64_t ots_get_le48(const uint8_t *buf) +{ + uint64_t val = 0; + for (int i = 5; i >= 0; i--) { + val = (val << 8) | buf[i]; + } + return val; +} + +/***************************************************************************** + * GATT Read Callback: Object Name (Read Long fallback) + *****************************************************************************/ + +/* Context passed to name read long callback */ +typedef struct { + uint16_t conn_id; +} name_read_ctx_t; + +static int +name_read_long_cb(uint16_t conn_handle, + const struct ble_gatt_error *error, + struct ble_gatt_attr *attr, + void *arg) +{ + name_read_ctx_t *ctx = (name_read_ctx_t *)arg; + ble_ots_client_conn_ctx_t *conn_ctx = ble_ots_client_get_conn_ctx(ctx->conn_id); + + if (conn_ctx == NULL) { + /* Connection was disconnected during Read Long */ + ble_ots_client_object_name_read_t evt = {0}; + evt.status = (error->status != 0) ? error->status : BLE_HS_ENOTCONN; + evt.name = NULL; + evt.name_len = 0; + ble_ots_client_dispatch_event(ctx->conn_id, + BLE_OTS_CLIENT_EVT_OBJECT_NAME_READ, + &evt); + free(ctx); + return 0; + } + + if (error->status == 0 && attr != NULL && attr->om != NULL) { + /* Accumulate data fragment into name_buf */ + uint16_t data_len = OS_MBUF_PKTLEN(attr->om); + uint16_t space = CONFIG_BLE_OTS_CLIENT_MAX_NAME_LEN - conn_ctx->name_buf_len; + uint16_t copy_len = data_len < space ? data_len : space; + + if (copy_len > 0) { + os_mbuf_copydata(attr->om, 0, copy_len, + conn_ctx->name_buf + conn_ctx->name_buf_len); + conn_ctx->name_buf_len += copy_len; + } + /* More fragments may follow; return 0 to continue */ + return 0; + } + + /* Completion or error */ + ble_ots_client_object_name_read_t evt = {0}; + + if (error->status == BLE_HS_EDONE || error->status == 0) { + /* Read Long completed successfully */ + evt.status = 0; + evt.name = conn_ctx->name_buf; + evt.name_len = conn_ctx->name_buf_len; + } else if (error->status == BLE_HS_ATT_ERR(BLE_ATT_ERR_ATTR_NOT_LONG)) { + /* No more data beyond what we already have */ + evt.status = 0; + evt.name = conn_ctx->name_buf; + evt.name_len = conn_ctx->name_buf_len; + } else if (error->status == BLE_HS_ATT_ERR(BLE_ATT_ERR_REQ_NOT_SUPPORTED)) { + /* Server doesn't support Read Long */ + if (conn_ctx) { + conn_ctx->server_supports_read_long = false; + } + evt.status = 0; + evt.name = conn_ctx->name_buf; + evt.name_len = conn_ctx->name_buf_len; + } else { + evt.status = error->status; + evt.name = conn_ctx->name_buf; + evt.name_len = conn_ctx->name_buf_len; + } + + ble_ots_client_dispatch_event(ctx->conn_id, + BLE_OTS_CLIENT_EVT_OBJECT_NAME_READ, + &evt); + free(ctx); + return 0; +} + +static int +name_read_cb(uint16_t conn_handle, + const struct ble_gatt_error *error, + struct ble_gatt_attr *attr, + void *arg) +{ + uint16_t conn_id = conn_handle; + ble_ots_client_conn_ctx_t *conn_ctx = ble_ots_client_get_conn_ctx(conn_id); + + if (error->status != 0 || attr == NULL || attr->om == NULL) { + /* Error or no data */ + ble_ots_client_object_name_read_t evt = {0}; + evt.status = error->status; + ble_ots_client_dispatch_event(conn_id, + BLE_OTS_CLIENT_EVT_OBJECT_NAME_READ, + &evt); + return 0; + } + + if (conn_ctx == NULL) { + ble_ots_client_object_name_read_t evt = {0}; + evt.status = BLE_HS_ENOTCONN; + ble_ots_client_dispatch_event(conn_id, + BLE_OTS_CLIENT_EVT_OBJECT_NAME_READ, + &evt); + return 0; + } + + /* Copy initial read data into name_buf */ + uint16_t data_len = OS_MBUF_PKTLEN(attr->om); + uint16_t copy_len = data_len; + if (copy_len > CONFIG_BLE_OTS_CLIENT_MAX_NAME_LEN) { + copy_len = CONFIG_BLE_OTS_CLIENT_MAX_NAME_LEN; + } + + conn_ctx->name_buf_len = 0; + os_mbuf_copydata(attr->om, 0, copy_len, conn_ctx->name_buf); + conn_ctx->name_buf_len = copy_len; + + /* Check if we need Read Long: if data_len == ATT_MTU - 1, there may be more */ + uint16_t att_mtu = ble_att_mtu(conn_handle); + if (att_mtu > 0 && data_len == (att_mtu - 1) && + conn_ctx->server_supports_read_long && + conn_ctx->name_buf_len < CONFIG_BLE_OTS_CLIENT_MAX_NAME_LEN) { + /* Issue Read Long starting from offset = data_len */ + name_read_ctx_t *ctx = malloc(sizeof(name_read_ctx_t)); + if (ctx == NULL) { + ble_ots_client_object_name_read_t evt = {0}; + evt.status = BLE_HS_ENOMEM; + evt.name = conn_ctx->name_buf; + evt.name_len = conn_ctx->name_buf_len; + ble_ots_client_dispatch_event(conn_id, + BLE_OTS_CLIENT_EVT_OBJECT_NAME_READ, + &evt); + return 0; + } + ctx->conn_id = conn_id; + + int rc = ble_gattc_read_long(conn_handle, + conn_ctx->handles.object_name_handle, + data_len, + name_read_long_cb, + ctx); + if (rc != 0) { + ESP_LOGE(TAG, "Read Long failed to initiate; rc=%d", rc); + free(ctx); + /* Fall through and report what we have */ + ble_ots_client_object_name_read_t evt = {0}; + evt.status = rc; + evt.name = conn_ctx->name_buf; + evt.name_len = conn_ctx->name_buf_len; + ble_ots_client_dispatch_event(conn_id, + BLE_OTS_CLIENT_EVT_OBJECT_NAME_READ, + &evt); + } + return 0; + } + + /* No Read Long needed — report the result directly */ + ble_ots_client_object_name_read_t evt = {0}; + evt.status = 0; + evt.name = conn_ctx->name_buf; + evt.name_len = conn_ctx->name_buf_len; + ble_ots_client_dispatch_event(conn_id, + BLE_OTS_CLIENT_EVT_OBJECT_NAME_READ, + &evt); + return 0; +} + +/***************************************************************************** + * GATT Read Callback: Object Type + *****************************************************************************/ +static int +type_read_cb(uint16_t conn_handle, + const struct ble_gatt_error *error, + struct ble_gatt_attr *attr, + void *arg) +{ + ble_ots_client_object_type_read_t evt = {0}; + + if (error->status != 0 || attr == NULL || attr->om == NULL) { + evt.status = error->status; + ble_ots_client_dispatch_event(conn_handle, + BLE_OTS_CLIENT_EVT_OBJECT_TYPE_READ, + &evt); + return 0; + } + + uint16_t data_len = OS_MBUF_PKTLEN(attr->om); + + if (data_len == 2 || data_len == 16) { + evt.status = 0; + evt.uuid_len = (uint8_t)data_len; + os_mbuf_copydata(attr->om, 0, data_len, evt.uuid); + } else { + ESP_LOGE(TAG, "Unexpected Object Type length: %u", data_len); + evt.status = BLE_HS_EINVAL; + } + + ble_ots_client_dispatch_event(conn_handle, + BLE_OTS_CLIENT_EVT_OBJECT_TYPE_READ, + &evt); + return 0; +} + +/***************************************************************************** + * GATT Read Callback: Object Size + *****************************************************************************/ +static int +size_read_cb(uint16_t conn_handle, + const struct ble_gatt_error *error, + struct ble_gatt_attr *attr, + void *arg) +{ + ble_ots_client_object_size_read_t evt = {0}; + + if (error->status != 0 || attr == NULL || attr->om == NULL) { + evt.status = error->status; + ble_ots_client_dispatch_event(conn_handle, + BLE_OTS_CLIENT_EVT_OBJECT_SIZE_READ, + &evt); + return 0; + } + + uint16_t data_len = OS_MBUF_PKTLEN(attr->om); + if (data_len < 8) { + ESP_LOGE(TAG, "Object Size too short: %u", data_len); + evt.status = BLE_HS_EINVAL; + ble_ots_client_dispatch_event(conn_handle, + BLE_OTS_CLIENT_EVT_OBJECT_SIZE_READ, + &evt); + return 0; + } + + uint8_t buf[8]; + os_mbuf_copydata(attr->om, 0, 8, buf); + + evt.status = 0; + evt.current_size = get_le32(buf); + evt.allocated_size = get_le32(buf + 4); + + ble_ots_client_dispatch_event(conn_handle, + BLE_OTS_CLIENT_EVT_OBJECT_SIZE_READ, + &evt); + return 0; +} + +/***************************************************************************** + * GATT Read Callback: First-Created + *****************************************************************************/ +static int +first_created_read_cb(uint16_t conn_handle, + const struct ble_gatt_error *error, + struct ble_gatt_attr *attr, + void *arg) +{ + ble_ots_client_datetime_read_t evt = {0}; + + if (error->status != 0 || attr == NULL || attr->om == NULL) { + evt.status = error->status; + ble_ots_client_dispatch_event(conn_handle, + BLE_OTS_CLIENT_EVT_FIRST_CREATED_READ, + &evt); + return 0; + } + + uint16_t data_len = OS_MBUF_PKTLEN(attr->om); + if (data_len < 7) { + ESP_LOGE(TAG, "First-Created too short: %u", data_len); + evt.status = BLE_HS_EINVAL; + ble_ots_client_dispatch_event(conn_handle, + BLE_OTS_CLIENT_EVT_FIRST_CREATED_READ, + &evt); + return 0; + } + + uint8_t buf[7]; + os_mbuf_copydata(attr->om, 0, 7, buf); + decode_datetime(buf, &evt.datetime); + evt.status = 0; + + ble_ots_client_dispatch_event(conn_handle, + BLE_OTS_CLIENT_EVT_FIRST_CREATED_READ, + &evt); + return 0; +} + +/***************************************************************************** + * GATT Read Callback: Last-Modified + *****************************************************************************/ +static int +last_modified_read_cb(uint16_t conn_handle, + const struct ble_gatt_error *error, + struct ble_gatt_attr *attr, + void *arg) +{ + ble_ots_client_datetime_read_t evt = {0}; + + if (error->status != 0 || attr == NULL || attr->om == NULL) { + evt.status = error->status; + ble_ots_client_dispatch_event(conn_handle, + BLE_OTS_CLIENT_EVT_LAST_MODIFIED_READ, + &evt); + return 0; + } + + uint16_t data_len = OS_MBUF_PKTLEN(attr->om); + if (data_len < 7) { + ESP_LOGE(TAG, "Last-Modified too short: %u", data_len); + evt.status = BLE_HS_EINVAL; + ble_ots_client_dispatch_event(conn_handle, + BLE_OTS_CLIENT_EVT_LAST_MODIFIED_READ, + &evt); + return 0; + } + + uint8_t buf[7]; + os_mbuf_copydata(attr->om, 0, 7, buf); + decode_datetime(buf, &evt.datetime); + evt.status = 0; + + ble_ots_client_dispatch_event(conn_handle, + BLE_OTS_CLIENT_EVT_LAST_MODIFIED_READ, + &evt); + return 0; +} + +/***************************************************************************** + * GATT Read Callback: Object ID + *****************************************************************************/ +static int +object_id_read_cb(uint16_t conn_handle, + const struct ble_gatt_error *error, + struct ble_gatt_attr *attr, + void *arg) +{ + ble_ots_client_object_id_read_t evt = {0}; + + if (error->status != 0 || attr == NULL || attr->om == NULL) { + evt.status = error->status; + ble_ots_client_dispatch_event(conn_handle, + BLE_OTS_CLIENT_EVT_OBJECT_ID_READ, + &evt); + return 0; + } + + uint16_t data_len = OS_MBUF_PKTLEN(attr->om); + if (data_len < 6) { + ESP_LOGE(TAG, "Object ID too short: %u", data_len); + evt.status = BLE_HS_EINVAL; + ble_ots_client_dispatch_event(conn_handle, + BLE_OTS_CLIENT_EVT_OBJECT_ID_READ, + &evt); + return 0; + } + + uint8_t buf[6]; + os_mbuf_copydata(attr->om, 0, 6, buf); + evt.status = 0; + evt.object_id = ots_get_le48(buf); + + ble_ots_client_dispatch_event(conn_handle, + BLE_OTS_CLIENT_EVT_OBJECT_ID_READ, + &evt); + return 0; +} + +/***************************************************************************** + * GATT Read Callback: Object Properties + *****************************************************************************/ +static int +properties_read_cb(uint16_t conn_handle, + const struct ble_gatt_error *error, + struct ble_gatt_attr *attr, + void *arg) +{ + ble_ots_client_properties_read_t evt = {0}; + + if (error->status != 0 || attr == NULL || attr->om == NULL) { + evt.status = error->status; + ble_ots_client_dispatch_event(conn_handle, + BLE_OTS_CLIENT_EVT_PROPERTIES_READ, + &evt); + return 0; + } + + uint16_t data_len = OS_MBUF_PKTLEN(attr->om); + if (data_len < 4) { + ESP_LOGE(TAG, "Object Properties too short: %u", data_len); + evt.status = BLE_HS_EINVAL; + ble_ots_client_dispatch_event(conn_handle, + BLE_OTS_CLIENT_EVT_PROPERTIES_READ, + &evt); + return 0; + } + + uint8_t buf[4]; + os_mbuf_copydata(attr->om, 0, 4, buf); + evt.status = 0; + evt.properties = get_le32(buf); + + ble_ots_client_dispatch_event(conn_handle, + BLE_OTS_CLIENT_EVT_PROPERTIES_READ, + &evt); + return 0; +} + +/***************************************************************************** + * GATT Write Callback: Metadata Written (generic for all metadata writes) + *****************************************************************************/ + +typedef struct { + uint16_t conn_id; + ble_ots_client_metadata_type_t metadata_type; +} metadata_write_ctx_t; + +static int +metadata_write_cb(uint16_t conn_handle, + const struct ble_gatt_error *error, + struct ble_gatt_attr *attr, + void *arg) +{ + metadata_write_ctx_t *ctx = (metadata_write_ctx_t *)arg; + + ble_ots_client_metadata_written_t evt = {0}; + evt.status = error->status; + evt.metadata_type = ctx->metadata_type; + + /* Check if Write Long was rejected with "Request Not Supported" */ + if (error->status == BLE_HS_ATT_ERR(BLE_ATT_ERR_REQ_NOT_SUPPORTED)) { + ble_ots_client_conn_ctx_t *conn_ctx = ble_ots_client_get_conn_ctx(ctx->conn_id); + if (conn_ctx) { + conn_ctx->server_supports_write_long = false; + } + } + + ble_ots_client_dispatch_event(ctx->conn_id, + BLE_OTS_CLIENT_EVT_METADATA_WRITTEN, + &evt); + free(ctx); + return 0; +} + +/***************************************************************************** + * Public API: Read Object Name + *****************************************************************************/ +int ble_ots_client_read_object_name(uint16_t conn_id) +{ + if (g_ots_client == NULL || !g_ots_client->initialized) { + return BLE_HS_EINVAL; + } + + ble_ots_client_conn_ctx_t *ctx = ble_ots_client_get_conn_ctx(conn_id); + if (ctx == NULL) { + return BLE_HS_ENOTCONN; + } + + if (ctx->handles.object_name_handle == 0) { + return BLE_HS_EINVAL; + } + + /* Reset name buffer */ + ctx->name_buf_len = 0; + memset(ctx->name_buf, 0, sizeof(ctx->name_buf)); + + int rc = ble_gattc_read(conn_id, ctx->handles.object_name_handle, + name_read_cb, NULL); + if (rc != 0) { + ESP_LOGE(TAG, "Failed to initiate Object Name read; rc=%d", rc); + } + + return rc; +} + +/***************************************************************************** + * Public API: Read Object Type + *****************************************************************************/ +int ble_ots_client_read_object_type(uint16_t conn_id) +{ + if (g_ots_client == NULL || !g_ots_client->initialized) { + return BLE_HS_EINVAL; + } + + ble_ots_client_conn_ctx_t *ctx = ble_ots_client_get_conn_ctx(conn_id); + if (ctx == NULL) { + return BLE_HS_ENOTCONN; + } + + if (ctx->handles.object_type_handle == 0) { + return BLE_HS_EINVAL; + } + + int rc = ble_gattc_read(conn_id, ctx->handles.object_type_handle, + type_read_cb, NULL); + if (rc != 0) { + ESP_LOGE(TAG, "Failed to initiate Object Type read; rc=%d", rc); + } + + return rc; +} + +/***************************************************************************** + * Public API: Read Object Size + *****************************************************************************/ +int ble_ots_client_read_object_size(uint16_t conn_id) +{ + if (g_ots_client == NULL || !g_ots_client->initialized) { + return BLE_HS_EINVAL; + } + + ble_ots_client_conn_ctx_t *ctx = ble_ots_client_get_conn_ctx(conn_id); + if (ctx == NULL) { + return BLE_HS_ENOTCONN; + } + + if (ctx->handles.object_size_handle == 0) { + return BLE_HS_EINVAL; + } + + int rc = ble_gattc_read(conn_id, ctx->handles.object_size_handle, + size_read_cb, NULL); + if (rc != 0) { + ESP_LOGE(TAG, "Failed to initiate Object Size read; rc=%d", rc); + } + + return rc; +} + +/***************************************************************************** + * Public API: Read Object First-Created + *****************************************************************************/ +int ble_ots_client_read_object_first_created(uint16_t conn_id) +{ + if (g_ots_client == NULL || !g_ots_client->initialized) { + return BLE_HS_EINVAL; + } + + ble_ots_client_conn_ctx_t *ctx = ble_ots_client_get_conn_ctx(conn_id); + if (ctx == NULL) { + return BLE_HS_ENOTCONN; + } + + if (ctx->handles.first_created_handle == 0) { + ESP_LOGE(TAG, "First-Created characteristic not discovered"); + return BLE_HS_EINVAL; + } + + int rc = ble_gattc_read(conn_id, ctx->handles.first_created_handle, + first_created_read_cb, NULL); + if (rc != 0) { + ESP_LOGE(TAG, "Failed to initiate First-Created read; rc=%d", rc); + } + + return rc; +} + +/***************************************************************************** + * Public API: Read Object Last-Modified + *****************************************************************************/ +int ble_ots_client_read_object_last_modified(uint16_t conn_id) +{ + if (g_ots_client == NULL || !g_ots_client->initialized) { + return BLE_HS_EINVAL; + } + + ble_ots_client_conn_ctx_t *ctx = ble_ots_client_get_conn_ctx(conn_id); + if (ctx == NULL) { + return BLE_HS_ENOTCONN; + } + + if (ctx->handles.last_modified_handle == 0) { + ESP_LOGE(TAG, "Last-Modified characteristic not discovered"); + return BLE_HS_EINVAL; + } + + int rc = ble_gattc_read(conn_id, ctx->handles.last_modified_handle, + last_modified_read_cb, NULL); + if (rc != 0) { + ESP_LOGE(TAG, "Failed to initiate Last-Modified read; rc=%d", rc); + } + + return rc; +} + +/***************************************************************************** + * Public API: Read Object ID + *****************************************************************************/ +int ble_ots_client_read_object_id(uint16_t conn_id) +{ + if (g_ots_client == NULL || !g_ots_client->initialized) { + return BLE_HS_EINVAL; + } + + ble_ots_client_conn_ctx_t *ctx = ble_ots_client_get_conn_ctx(conn_id); + if (ctx == NULL) { + return BLE_HS_ENOTCONN; + } + + if (ctx->handles.object_id_handle == 0) { + ESP_LOGE(TAG, "Object ID characteristic not discovered (single-object server)"); + return BLE_HS_EINVAL; + } + + int rc = ble_gattc_read(conn_id, ctx->handles.object_id_handle, + object_id_read_cb, NULL); + if (rc != 0) { + ESP_LOGE(TAG, "Failed to initiate Object ID read; rc=%d", rc); + } + + return rc; +} + +/***************************************************************************** + * Public API: Read Object Properties + *****************************************************************************/ +int ble_ots_client_read_object_properties(uint16_t conn_id) +{ + if (g_ots_client == NULL || !g_ots_client->initialized) { + return BLE_HS_EINVAL; + } + + ble_ots_client_conn_ctx_t *ctx = ble_ots_client_get_conn_ctx(conn_id); + if (ctx == NULL) { + return BLE_HS_ENOTCONN; + } + + if (ctx->handles.object_properties_handle == 0) { + return BLE_HS_EINVAL; + } + + int rc = ble_gattc_read(conn_id, ctx->handles.object_properties_handle, + properties_read_cb, NULL); + if (rc != 0) { + ESP_LOGE(TAG, "Failed to initiate Object Properties read; rc=%d", rc); + } + + return rc; +} + +/***************************************************************************** + * Public API: Write Object Name + *****************************************************************************/ +int ble_ots_client_write_object_name(uint16_t conn_id, + const char *name, + uint16_t name_len) +{ + if (g_ots_client == NULL || !g_ots_client->initialized) { + return BLE_HS_EINVAL; + } + + if (name == NULL || name_len == 0 || name_len > BLE_OTS_OBJECT_NAME_MAX_LEN) { + return BLE_HS_EINVAL; + } + + ble_ots_client_conn_ctx_t *ctx = ble_ots_client_get_conn_ctx(conn_id); + if (ctx == NULL) { + return BLE_HS_ENOTCONN; + } + + if (ctx->handles.object_name_handle == 0) { + return BLE_HS_EINVAL; + } + + /* Allocate write context */ + metadata_write_ctx_t *wctx = malloc(sizeof(metadata_write_ctx_t)); + if (wctx == NULL) { + return BLE_HS_ENOMEM; + } + wctx->conn_id = conn_id; + wctx->metadata_type = BLE_OTS_CLIENT_METADATA_OBJECT_NAME; + + uint16_t att_mtu = ble_att_mtu(conn_id); + uint16_t max_write_len = (att_mtu > 3) ? (att_mtu - 3) : 0; + + if (name_len <= max_write_len || max_write_len == 0) { + /* Simple write fits within ATT_MTU - 3 */ + int rc = ble_gattc_write_flat(conn_id, + ctx->handles.object_name_handle, + name, name_len, + metadata_write_cb, wctx); + if (rc != 0) { + ESP_LOGE(TAG, "Failed to initiate Object Name write; rc=%d", rc); + free(wctx); + } + return rc; + } + + /* Name exceeds ATT_MTU - 3 */ + if (ctx->server_supports_write_long) { + /* Attempt Write Long */ + struct os_mbuf *om = ble_hs_mbuf_from_flat(name, name_len); + if (om == NULL) { + free(wctx); + return BLE_HS_ENOMEM; + } + + int rc = ble_gattc_write_long(conn_id, + ctx->handles.object_name_handle, + 0, om, + metadata_write_cb, wctx); + if (rc != 0) { + ESP_LOGE(TAG, "Failed to initiate Object Name Write Long; rc=%d", rc); + free(wctx); + /* Note: ble_gattc_write_long consumes om regardless of rc */ + } + return rc; + } + + /* Server doesn't support Write Long — truncate to ATT_MTU - 3 */ + ESP_LOGW(TAG, "Server doesn't support Write Long, truncating name to %u bytes", + max_write_len); + int rc = ble_gattc_write_flat(conn_id, + ctx->handles.object_name_handle, + name, max_write_len, + metadata_write_cb, wctx); + if (rc != 0) { + ESP_LOGE(TAG, "Failed to initiate truncated Object Name write; rc=%d", rc); + free(wctx); + } + return rc; +} + +/***************************************************************************** + * Public API: Write Object First-Created + *****************************************************************************/ +int ble_ots_client_write_object_first_created(uint16_t conn_id, + const ble_ots_date_time_t *datetime) +{ + if (g_ots_client == NULL || !g_ots_client->initialized) { + return BLE_HS_EINVAL; + } + + if (datetime == NULL) { + return BLE_HS_EINVAL; + } + + ble_ots_client_conn_ctx_t *ctx = ble_ots_client_get_conn_ctx(conn_id); + if (ctx == NULL) { + return BLE_HS_ENOTCONN; + } + + if (ctx->handles.first_created_handle == 0) { + ESP_LOGE(TAG, "First-Created characteristic not discovered"); + return BLE_HS_EINVAL; + } + + metadata_write_ctx_t *wctx = malloc(sizeof(metadata_write_ctx_t)); + if (wctx == NULL) { + return BLE_HS_ENOMEM; + } + wctx->conn_id = conn_id; + wctx->metadata_type = BLE_OTS_CLIENT_METADATA_FIRST_CREATED; + + uint8_t buf[7]; + encode_datetime(datetime, buf); + + int rc = ble_gattc_write_flat(conn_id, + ctx->handles.first_created_handle, + buf, sizeof(buf), + metadata_write_cb, wctx); + if (rc != 0) { + ESP_LOGE(TAG, "Failed to initiate First-Created write; rc=%d", rc); + free(wctx); + } + + return rc; +} + +/***************************************************************************** + * Public API: Write Object Last-Modified + *****************************************************************************/ +int ble_ots_client_write_object_last_modified(uint16_t conn_id, + const ble_ots_date_time_t *datetime) +{ + if (g_ots_client == NULL || !g_ots_client->initialized) { + return BLE_HS_EINVAL; + } + + if (datetime == NULL) { + return BLE_HS_EINVAL; + } + + ble_ots_client_conn_ctx_t *ctx = ble_ots_client_get_conn_ctx(conn_id); + if (ctx == NULL) { + return BLE_HS_ENOTCONN; + } + + if (ctx->handles.last_modified_handle == 0) { + ESP_LOGE(TAG, "Last-Modified characteristic not discovered"); + return BLE_HS_EINVAL; + } + + metadata_write_ctx_t *wctx = malloc(sizeof(metadata_write_ctx_t)); + if (wctx == NULL) { + return BLE_HS_ENOMEM; + } + wctx->conn_id = conn_id; + wctx->metadata_type = BLE_OTS_CLIENT_METADATA_LAST_MODIFIED; + + uint8_t buf[7]; + encode_datetime(datetime, buf); + + int rc = ble_gattc_write_flat(conn_id, + ctx->handles.last_modified_handle, + buf, sizeof(buf), + metadata_write_cb, wctx); + if (rc != 0) { + ESP_LOGE(TAG, "Failed to initiate Last-Modified write; rc=%d", rc); + free(wctx); + } + + return rc; +} + +/***************************************************************************** + * Public API: Write Object Properties + *****************************************************************************/ +int ble_ots_client_write_object_properties(uint16_t conn_id, + uint32_t properties) +{ + if (g_ots_client == NULL || !g_ots_client->initialized) { + return BLE_HS_EINVAL; + } + + ble_ots_client_conn_ctx_t *ctx = ble_ots_client_get_conn_ctx(conn_id); + if (ctx == NULL) { + return BLE_HS_ENOTCONN; + } + + if (ctx->handles.object_properties_handle == 0) { + return BLE_HS_EINVAL; + } + + metadata_write_ctx_t *wctx = malloc(sizeof(metadata_write_ctx_t)); + if (wctx == NULL) { + return BLE_HS_ENOMEM; + } + wctx->conn_id = conn_id; + wctx->metadata_type = BLE_OTS_CLIENT_METADATA_PROPERTIES; + + uint8_t buf[4]; + put_le32(buf, properties); + + int rc = ble_gattc_write_flat(conn_id, + ctx->handles.object_properties_handle, + buf, sizeof(buf), + metadata_write_cb, wctx); + if (rc != 0) { + ESP_LOGE(TAG, "Failed to initiate Object Properties write; rc=%d", rc); + free(wctx); + } + + return rc; +} diff --git a/components/bt/ble_profiles/nimble/ble_ots/src/client/ble_ots_client_object_nav.c b/components/bt/ble_profiles/nimble/ble_ots/src/client/ble_ots_client_object_nav.c new file mode 100644 index 00000000000..b44b7cc33eb --- /dev/null +++ b/components/bt/ble_profiles/nimble/ble_ots/src/client/ble_ots_client_object_nav.c @@ -0,0 +1,860 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include "esp_log.h" +#include "nimble/ble.h" +#include "host/ble_hs.h" +#include "host/ble_gap.h" +#include "host/ble_gatt.h" +#include "host/ble_att.h" +#include "host/ble_hs_mbuf.h" +#include "os/os_mbuf.h" + +#include "ble_ots_client_int.h" + +static const char *TAG = "ble_ots_client_nav"; + +/***************************************************************************** + * Forward Declarations + *****************************************************************************/ +static int olcp_write_cb(uint16_t conn_handle, + const struct ble_gatt_error *error, + struct ble_gatt_attr *attr, + void *arg); +static int filter_write_cb(uint16_t conn_handle, + const struct ble_gatt_error *error, + struct ble_gatt_attr *attr, + void *arg); +static int filter_write_long_cb(uint16_t conn_handle, + const struct ble_gatt_error *error, + struct ble_gatt_attr *attr, + void *arg); +static int filter_read_cb(uint16_t conn_handle, + const struct ble_gatt_error *error, + struct ble_gatt_attr *attr, + void *arg); +static int filter_read_long_cb(uint16_t conn_handle, + const struct ble_gatt_error *error, + struct ble_gatt_attr *attr, + void *arg); +static int obj_changed_cccd_write_cb(uint16_t conn_handle, + const struct ble_gatt_error *error, + struct ble_gatt_attr *attr, + void *arg); + +/***************************************************************************** + * Helper: Common OLCP prerequisite checks + *****************************************************************************/ + +/** + * @brief Validate that OLCP operations can be performed on this connection. + * + * Checks: module initialized, context exists, multi-object server (OLCP handle + * present), OLCP CCCD configured, and no prior CP timeout. + * + * @param conn_id Connection identifier + * @param[out] ctx Pointer to receive the connection context + * @return 0 on success, BLE_HS error code on failure + */ +static int olcp_validate(uint16_t conn_id, ble_ots_client_conn_ctx_t **ctx) +{ + if (!g_ots_client || !g_ots_client->initialized) { + ESP_LOGE(TAG, "OTS client not initialized"); + return BLE_HS_ENOTCONN; + } + + ble_ots_client_conn_ctx_t *c = ble_ots_client_get_conn_ctx(conn_id); + if (!c) { + ESP_LOGE(TAG, "No context for conn_id=%d", conn_id); + return BLE_HS_ENOTCONN; + } + + if (!c->multi_object_server || c->handles.olcp_handle == 0) { + ESP_LOGE(TAG, "OLCP not available (not a multi-object server)"); + return BLE_HS_ENOTSUP; + } + + if (c->handles.olcp_cccd_handle == 0) { + ESP_LOGE(TAG, "OLCP CCCD not configured"); + return BLE_HS_ENOTSUP; + } + + if (c->cp_timed_out) { + ESP_LOGE(TAG, "CP timed out; reject new CP operations"); + return BLE_HS_ETIMEOUT; + } + + if (c->cp_timer_active || c->cp_pending_opcode != 0) { + ESP_LOGE(TAG, "CP operation already in progress"); + return BLE_HS_EBUSY; + } + + *ctx = c; + return 0; +} + +/***************************************************************************** + * Helper: Write a simple OLCP command (opcode only, no parameters) + *****************************************************************************/ +static int olcp_write_simple(uint16_t conn_id, uint8_t opcode) +{ + ble_ots_client_conn_ctx_t *ctx = NULL; + int rc; + + rc = olcp_validate(conn_id, &ctx); + if (rc != 0) { + return rc; + } + + uint8_t buf[1]; + buf[0] = opcode; + + ctx->cp_pending_opcode = opcode; + + rc = ble_gattc_write_flat(conn_id, ctx->handles.olcp_handle, + buf, sizeof(buf), olcp_write_cb, (void *)(uintptr_t)conn_id); + if (rc != 0) { + ESP_LOGE(TAG, "OLCP write failed; opcode=0x%02x rc=%d", opcode, rc); + ctx->cp_pending_opcode = 0; + return rc; + } + + return 0; +} + +/***************************************************************************** + * OLCP Write Callback + *****************************************************************************/ + +/** + * @brief Callback for OLCP GATT write completion. + * + * On success (ATT Write Response), starts the CP timer. + * On failure (ATT Error Response), the operation is not started. + */ +static int olcp_write_cb(uint16_t conn_handle, + const struct ble_gatt_error *error, + struct ble_gatt_attr *attr, + void *arg) +{ + uint16_t conn_id = (uint16_t)(uintptr_t)arg; + + if (error->status == 0) { + /* Write accepted — start CP timer */ + ESP_LOGD(TAG, "OLCP write accepted, starting CP timer; conn_id=%d", conn_id); + int rc = ble_ots_client_start_cp_timer(conn_id, CONFIG_BLE_OTS_CLIENT_OLCP_TIMEOUT_MS); + if (rc != 0) { + /* The server has already accepted the command and will eventually + * complete it by sending an OLCP indication. We only failed to arm + * the local timeout timer, so we must NOT abandon the operation: + * clearing cp_pending_opcode or dispatching a synthetic failure + * here would desynchronize the client state machine from the + * server and could let the app start a concurrent CP command. + * Keep the pending state intact (just without timeout protection) + * and let handle_olcp_indication complete it normally. */ + ESP_LOGE(TAG, "Failed to start CP timer; rc=%d (operation continues without timeout protection)", rc); + } + } else { + /* ATT error — operation not started, no timer needed */ + ESP_LOGE(TAG, "OLCP write error; status=0x%04x conn_id=%d", error->status, conn_id); + ble_ots_client_conn_ctx_t *ctx = ble_ots_client_get_conn_ctx(conn_id); + if (ctx) { + uint8_t pending_opcode = ctx->cp_pending_opcode; + ctx->cp_pending_opcode = 0; + + /* Notify the application of the failure so it does not hang */ + ble_ots_client_olcp_response_t resp = { + .request_opcode = pending_opcode, + .result_code = BLE_OTS_OLCP_RESULT_OPERATION_FAILED, + .num_objects = 0, + }; + ble_ots_client_dispatch_event(conn_id, BLE_OTS_CLIENT_EVT_OLCP_RESPONSE, &resp); + } + } + + return 0; +} + +/***************************************************************************** + * Public APIs — OLCP Navigation + *****************************************************************************/ + +int ble_ots_client_first_object(uint16_t conn_id) +{ + return olcp_write_simple(conn_id, BLE_OTS_OLCP_OPCODE_FIRST); +} + +int ble_ots_client_last_object(uint16_t conn_id) +{ + return olcp_write_simple(conn_id, BLE_OTS_OLCP_OPCODE_LAST); +} + +int ble_ots_client_prev_object(uint16_t conn_id) +{ + return olcp_write_simple(conn_id, BLE_OTS_OLCP_OPCODE_PREVIOUS); +} + +int ble_ots_client_next_object(uint16_t conn_id) +{ + return olcp_write_simple(conn_id, BLE_OTS_OLCP_OPCODE_NEXT); +} + +int ble_ots_client_goto_object(uint16_t conn_id, uint64_t object_id) +{ + ble_ots_client_conn_ctx_t *ctx = NULL; + int rc; + + rc = olcp_validate(conn_id, &ctx); + if (rc != 0) { + return rc; + } + + /* OLCP Go To: 1-byte opcode + 6-byte Object ID (UINT48, little-endian) */ + uint8_t buf[7]; + buf[0] = BLE_OTS_OLCP_OPCODE_GO_TO; + buf[1] = (uint8_t)(object_id & 0xFF); + buf[2] = (uint8_t)((object_id >> 8) & 0xFF); + buf[3] = (uint8_t)((object_id >> 16) & 0xFF); + buf[4] = (uint8_t)((object_id >> 24) & 0xFF); + buf[5] = (uint8_t)((object_id >> 32) & 0xFF); + buf[6] = (uint8_t)((object_id >> 40) & 0xFF); + + ctx->cp_pending_opcode = BLE_OTS_OLCP_OPCODE_GO_TO; + + rc = ble_gattc_write_flat(conn_id, ctx->handles.olcp_handle, + buf, sizeof(buf), olcp_write_cb, (void *)(uintptr_t)conn_id); + if (rc != 0) { + ESP_LOGE(TAG, "OLCP Go To write failed; rc=%d", rc); + ctx->cp_pending_opcode = 0; + return rc; + } + + return 0; +} + +int ble_ots_client_order_objects(uint16_t conn_id, uint8_t sort_order) +{ + ble_ots_client_conn_ctx_t *ctx = NULL; + int rc; + + rc = olcp_validate(conn_id, &ctx); + if (rc != 0) { + return rc; + } + + /* OLCP Order: 1-byte opcode + 1-byte sort order */ + uint8_t buf[2]; + buf[0] = BLE_OTS_OLCP_OPCODE_ORDER; + buf[1] = sort_order; + + ctx->cp_pending_opcode = BLE_OTS_OLCP_OPCODE_ORDER; + + rc = ble_gattc_write_flat(conn_id, ctx->handles.olcp_handle, + buf, sizeof(buf), olcp_write_cb, (void *)(uintptr_t)conn_id); + if (rc != 0) { + ESP_LOGE(TAG, "OLCP Order write failed; rc=%d", rc); + ctx->cp_pending_opcode = 0; + return rc; + } + + return 0; +} + +int ble_ots_client_request_num_objects(uint16_t conn_id) +{ + return olcp_write_simple(conn_id, BLE_OTS_OLCP_OPCODE_REQUEST_NUM_OF_OBJECTS); +} + +int ble_ots_client_clear_marking(uint16_t conn_id) +{ + return olcp_write_simple(conn_id, BLE_OTS_OLCP_OPCODE_CLEAR_MARKING); +} + +/***************************************************************************** + * Object List Filter Write + *****************************************************************************/ + +/** Context passed through filter write callbacks */ +typedef struct { + uint16_t conn_id; + uint8_t instance; +} filter_write_ctx_t; + +static int filter_write_cb(uint16_t conn_handle, + const struct ble_gatt_error *error, + struct ble_gatt_attr *attr, + void *arg) +{ + filter_write_ctx_t *fctx = (filter_write_ctx_t *)arg; + if (!fctx) { + return 0; + } + + ble_ots_client_filter_set_t evt = { + .instance = fctx->instance, + .status = error->status, + }; + + ESP_LOGD(TAG, "Filter write complete; instance=%d status=0x%04x", fctx->instance, error->status); + + ble_ots_client_dispatch_event(fctx->conn_id, BLE_OTS_CLIENT_EVT_FILTER_SET, &evt); + + free(fctx); + return 0; +} + +static int filter_write_long_cb(uint16_t conn_handle, + const struct ble_gatt_error *error, + struct ble_gatt_attr *attr, + void *arg) +{ + filter_write_ctx_t *fctx = (filter_write_ctx_t *)arg; + if (!fctx) { + return 0; + } + + /* Check if server returned "Request Not Supported" for Write Long */ + if (error->status == BLE_HS_ATT_ERR(BLE_ATT_ERR_REQ_NOT_SUPPORTED)) { + ble_ots_client_conn_ctx_t *ctx = ble_ots_client_get_conn_ctx(fctx->conn_id); + if (ctx) { + ctx->server_supports_write_long = false; + ESP_LOGW(TAG, "Server does not support Write Long; disabling for this connection"); + } + } + + ble_ots_client_filter_set_t evt = { + .instance = fctx->instance, + .status = error->status, + }; + + ESP_LOGD(TAG, "Filter write long complete; instance=%d status=0x%04x", fctx->instance, error->status); + + ble_ots_client_dispatch_event(fctx->conn_id, BLE_OTS_CLIENT_EVT_FILTER_SET, &evt); + + free(fctx); + return 0; +} + +int ble_ots_client_set_filter(uint16_t conn_id, + uint8_t instance, + uint8_t filter_type, + const uint8_t *param, + uint16_t param_len) +{ + if (!g_ots_client || !g_ots_client->initialized) { + ESP_LOGE(TAG, "OTS client not initialized"); + return BLE_HS_ENOTCONN; + } + + if (instance > 2) { + ESP_LOGE(TAG, "Invalid filter instance: %d", instance); + return BLE_HS_EINVAL; + } + + if (param_len > 0 && param == NULL) { + ESP_LOGE(TAG, "param is NULL but param_len=%d", param_len); + return BLE_HS_EINVAL; + } + + ble_ots_client_conn_ctx_t *ctx = ble_ots_client_get_conn_ctx(conn_id); + if (!ctx) { + ESP_LOGE(TAG, "No context for conn_id=%d", conn_id); + return BLE_HS_ENOTCONN; + } + + uint16_t filter_handle = ctx->handles.object_list_filter_handle[instance]; + if (filter_handle == 0) { + ESP_LOGE(TAG, "Filter instance %d handle not discovered", instance); + return BLE_HS_ENOTSUP; + } + + /* Build the characteristic value: 1-byte filter_type + param data */ + uint16_t total_len = 1 + param_len; + uint8_t *buf = malloc(total_len); + if (!buf) { + ESP_LOGE(TAG, "Failed to allocate filter buffer"); + return BLE_HS_ENOMEM; + } + + buf[0] = filter_type; + if (param && param_len > 0) { + memcpy(&buf[1], param, param_len); + } + + /* Allocate callback context */ + filter_write_ctx_t *fctx = malloc(sizeof(filter_write_ctx_t)); + if (!fctx) { + free(buf); + ESP_LOGE(TAG, "Failed to allocate filter write context"); + return BLE_HS_ENOMEM; + } + fctx->conn_id = conn_id; + fctx->instance = instance; + + int rc; + + /* Determine if we need Write Long. + * ATT_MTU - 3 is the max payload for a single Write Request. + * We use ble_att_mtu() to get the current MTU for the connection. */ + uint16_t mtu = ble_att_mtu(conn_id); + uint16_t max_write_len = (mtu > 3) ? (mtu - 3) : 0; + + if (total_len > max_write_len && ctx->server_supports_write_long) { + /* Use Write Long */ + struct os_mbuf *om = ble_hs_mbuf_from_flat(buf, total_len); + free(buf); + if (!om) { + free(fctx); + ESP_LOGE(TAG, "Failed to allocate mbuf for Write Long"); + return BLE_HS_ENOMEM; + } + + rc = ble_gattc_write_long(conn_id, filter_handle, 0, om, + filter_write_long_cb, fctx); + if (rc != 0) { + ESP_LOGE(TAG, "Filter Write Long failed; rc=%d", rc); + /* om is consumed by ble_gattc_write_long regardless of outcome */ + free(fctx); + return rc; + } + } else if (total_len > max_write_len && !ctx->server_supports_write_long) { + /* Server doesn't support Write Long and data is too long */ + free(buf); + free(fctx); + ESP_LOGE(TAG, "Filter data too long and Write Long not supported"); + return BLE_HS_EMSGSIZE; + } else { + /* Use regular Write */ + rc = ble_gattc_write_flat(conn_id, filter_handle, + buf, total_len, filter_write_cb, fctx); + free(buf); + if (rc != 0) { + ESP_LOGE(TAG, "Filter write failed; rc=%d", rc); + free(fctx); + return rc; + } + } + + return 0; +} + +/***************************************************************************** + * Object List Filter Read + *****************************************************************************/ + +/** Context passed through filter read callbacks (accumulates Read Long data). */ +typedef struct { + uint16_t conn_id; + uint8_t instance; + uint16_t buf_len; + uint8_t buf[1 + BLE_OTS_OBJECT_NAME_MAX_LEN]; +} filter_read_ctx_t; + +static void filter_read_dispatch(filter_read_ctx_t *fctx, int status) +{ + ble_ots_client_filter_read_t evt = { + .instance = fctx->instance, + .status = status, + .filter_type = 0, + .param = NULL, + .param_len = 0, + }; + + if (status == 0 && fctx->buf_len >= 1) { + evt.filter_type = fctx->buf[0]; + if (fctx->buf_len > 1) { + evt.param = &fctx->buf[1]; + evt.param_len = fctx->buf_len - 1; + } + } else if (status == 0) { + /* Success but empty value is not a valid filter encoding */ + evt.status = BLE_HS_EINVAL; + } + + ble_ots_client_dispatch_event(fctx->conn_id, BLE_OTS_CLIENT_EVT_FILTER_READ, &evt); +} + +static int filter_read_long_cb(uint16_t conn_handle, + const struct ble_gatt_error *error, + struct ble_gatt_attr *attr, + void *arg) +{ + filter_read_ctx_t *fctx = (filter_read_ctx_t *)arg; + if (!fctx) { + return 0; + } + + ble_ots_client_conn_ctx_t *conn_ctx = ble_ots_client_get_conn_ctx(fctx->conn_id); + if (conn_ctx == NULL) { + /* Connection dropped during Read Long */ + filter_read_dispatch(fctx, (error->status != 0) ? error->status : BLE_HS_ENOTCONN); + free(fctx); + /* If this was a fragment reception (status == 0), we must return a + * non-zero value so NimBLE aborts the Read Long procedure and does not + * re-invoke this callback with the now-freed context. */ + return (error->status == 0) ? BLE_HS_ENOTCONN : 0; + } + + if (error->status == 0 && attr != NULL && attr->om != NULL) { + /* Accumulate a fragment; more may follow */ + uint16_t data_len = OS_MBUF_PKTLEN(attr->om); + uint16_t space = sizeof(fctx->buf) - fctx->buf_len; + uint16_t copy_len = data_len < space ? data_len : space; + if (copy_len > 0) { + os_mbuf_copydata(attr->om, 0, copy_len, fctx->buf + fctx->buf_len); + fctx->buf_len += copy_len; + } + return 0; + } + + /* Completion or error */ + if (error->status == BLE_HS_EDONE || error->status == BLE_HS_ATT_ERR(BLE_ATT_ERR_ATTR_NOT_LONG)) { + filter_read_dispatch(fctx, 0); + } else if (error->status == BLE_HS_ATT_ERR(BLE_ATT_ERR_REQ_NOT_SUPPORTED)) { + conn_ctx->server_supports_read_long = false; + filter_read_dispatch(fctx, 0); + } else { + filter_read_dispatch(fctx, error->status); + } + + free(fctx); + + /* Note: if we reach here with error->status == 0 (e.g. a defensive check + * such as attr == NULL failed on a fragment reception), we must return a + * non-zero value to abort the procedure. Returning 0 would cause NimBLE to + * issue another Read Blob request and re-invoke this callback with the + * freed context, resulting in a use-after-free / double-free. */ + return (error->status == 0) ? BLE_HS_EAPP : 0; +} + +static int filter_read_cb(uint16_t conn_handle, + const struct ble_gatt_error *error, + struct ble_gatt_attr *attr, + void *arg) +{ + filter_read_ctx_t *fctx = (filter_read_ctx_t *)arg; + if (!fctx) { + return 0; + } + + if (error->status != 0 || attr == NULL || attr->om == NULL) { + filter_read_dispatch(fctx, (error->status != 0) ? error->status : BLE_HS_EINVAL); + free(fctx); + return 0; + } + + ble_ots_client_conn_ctx_t *conn_ctx = ble_ots_client_get_conn_ctx(fctx->conn_id); + if (conn_ctx == NULL) { + filter_read_dispatch(fctx, BLE_HS_ENOTCONN); + free(fctx); + return 0; + } + + /* Copy the initial fragment */ + uint16_t data_len = OS_MBUF_PKTLEN(attr->om); + uint16_t copy_len = data_len < sizeof(fctx->buf) ? data_len : sizeof(fctx->buf); + os_mbuf_copydata(attr->om, 0, copy_len, fctx->buf); + fctx->buf_len = copy_len; + + /* A full (ATT_MTU - 1) payload suggests the value was truncated; continue + * with Read Long if the server supports it and we have buffer space. */ + uint16_t att_mtu = ble_att_mtu(conn_handle); + if (att_mtu > 0 && data_len == (att_mtu - 1) && + conn_ctx->server_supports_read_long && + fctx->buf_len < sizeof(fctx->buf)) { + int rc = ble_gattc_read_long(conn_handle, + conn_ctx->handles.object_list_filter_handle[fctx->instance], + data_len, + filter_read_long_cb, fctx); + if (rc != 0) { + ESP_LOGE(TAG, "Filter Read Long failed to initiate; rc=%d", rc); + filter_read_dispatch(fctx, 0); /* report what we already have */ + free(fctx); + } + return 0; + } + + filter_read_dispatch(fctx, 0); + free(fctx); + return 0; +} + +int ble_ots_client_read_filter(uint16_t conn_id, uint8_t instance) +{ + if (!g_ots_client || !g_ots_client->initialized) { + ESP_LOGE(TAG, "OTS client not initialized"); + return BLE_HS_ENOTCONN; + } + + if (instance > 2) { + ESP_LOGE(TAG, "Invalid filter instance: %d", instance); + return BLE_HS_EINVAL; + } + + ble_ots_client_conn_ctx_t *ctx = ble_ots_client_get_conn_ctx(conn_id); + if (!ctx) { + ESP_LOGE(TAG, "No context for conn_id=%d", conn_id); + return BLE_HS_ENOTCONN; + } + + uint16_t filter_handle = ctx->handles.object_list_filter_handle[instance]; + if (filter_handle == 0) { + ESP_LOGE(TAG, "Filter instance %d handle not discovered", instance); + return BLE_HS_ENOTSUP; + } + + filter_read_ctx_t *fctx = malloc(sizeof(filter_read_ctx_t)); + if (!fctx) { + ESP_LOGE(TAG, "Failed to allocate filter read context"); + return BLE_HS_ENOMEM; + } + fctx->conn_id = conn_id; + fctx->instance = instance; + fctx->buf_len = 0; + + int rc = ble_gattc_read(conn_id, filter_handle, filter_read_cb, fctx); + if (rc != 0) { + ESP_LOGE(TAG, "Filter read failed; instance=%d rc=%d", instance, rc); + free(fctx); + return rc; + } + + return 0; +} + +/***************************************************************************** + * Object Changed CCCD Subscription + *****************************************************************************/ + +static int obj_changed_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, "Object Changed CCCD write error; status=0x%04x", error->status); + } else { + ESP_LOGD(TAG, "Object Changed CCCD write success; conn_handle=%d", conn_handle); + } + + return 0; +} + +int ble_ots_client_subscribe_object_changed(uint16_t conn_id, bool enable) +{ + if (!g_ots_client || !g_ots_client->initialized) { + ESP_LOGE(TAG, "OTS client not initialized"); + return BLE_HS_ENOTCONN; + } + + ble_ots_client_conn_ctx_t *ctx = ble_ots_client_get_conn_ctx(conn_id); + if (!ctx) { + ESP_LOGE(TAG, "No context for conn_id=%d", conn_id); + return BLE_HS_ENOTCONN; + } + + if (ctx->handles.object_changed_cccd_handle == 0) { + ESP_LOGE(TAG, "Object Changed CCCD handle not discovered"); + return BLE_HS_ENOTSUP; + } + + /* CCCD value: 0x0002 for indications, 0x0000 to disable */ + uint8_t value[2]; + if (enable) { + value[0] = 0x02; + value[1] = 0x00; + } else { + value[0] = 0x00; + value[1] = 0x00; + } + + int rc = ble_gattc_write_flat(conn_id, ctx->handles.object_changed_cccd_handle, + value, sizeof(value), + obj_changed_cccd_write_cb, NULL); + if (rc != 0) { + ESP_LOGE(TAG, "Object Changed CCCD write failed; rc=%d", rc); + return rc; + } + + return 0; +} + +/***************************************************************************** + * OLCP Indication Handler + *****************************************************************************/ + +/** + * @brief Handle an OLCP indication (Response Code 0x70). + * + * Parses the indication payload: + * byte[0] = Response opcode (0x70) + * byte[1] = Request opcode + * byte[2] = Result code + * byte[3..6] = Optional num_objects (UINT32 LE, only for Request Num Objects + Success) + * + * Stops the CP timer and dispatches BLE_OTS_CLIENT_EVT_OLCP_RESPONSE. + */ +static void handle_olcp_indication(uint16_t conn_id, const uint8_t *data, uint16_t data_len) +{ + if (data_len < 3) { + ESP_LOGE(TAG, "OLCP indication too short; len=%d", data_len); + return; + } + + /* byte[0] should be 0x70 (Response Code) — already verified by caller */ + uint8_t request_opcode = data[1]; + uint8_t result_code = data[2]; + + ble_ots_client_olcp_response_t resp = { + .request_opcode = request_opcode, + .result_code = result_code, + .num_objects = 0, + }; + + /* If Request Number of Objects and Success, parse the UINT32 response parameter */ + if (request_opcode == BLE_OTS_OLCP_OPCODE_REQUEST_NUM_OF_OBJECTS && + result_code == BLE_OTS_OLCP_RESULT_SUCCESS && + data_len >= 7) { + resp.num_objects = (uint32_t)data[3] | + ((uint32_t)data[4] << 8) | + ((uint32_t)data[5] << 16) | + ((uint32_t)data[6] << 24); + } + + /* Stop the CP timer */ + ble_ots_client_stop_cp_timer(conn_id); + + /* Clear pending opcode */ + ble_ots_client_conn_ctx_t *ctx = ble_ots_client_get_conn_ctx(conn_id); + if (ctx) { + ctx->cp_pending_opcode = 0; + } + + ESP_LOGD(TAG, "OLCP response: req_op=0x%02x result=0x%02x num_objects=%lu", + request_opcode, result_code, (unsigned long)resp.num_objects); + + ble_ots_client_dispatch_event(conn_id, BLE_OTS_CLIENT_EVT_OLCP_RESPONSE, &resp); +} + +/***************************************************************************** + * Object Changed Indication Handler + *****************************************************************************/ + +/** + * @brief Handle an Object Changed indication. + * + * Parses the 7-byte payload: + * byte[0] = Flags + * byte[1..6] = Object ID (UINT48, little-endian) + * + * Dispatches BLE_OTS_CLIENT_EVT_OBJECT_CHANGED. + */ +static void handle_object_changed_indication(uint16_t conn_id, const uint8_t *data, uint16_t data_len) +{ + if (data_len < 7) { + ESP_LOGE(TAG, "Object Changed indication too short; len=%d", data_len); + return; + } + + uint8_t flags = data[0]; + + /* Decode Object ID (UINT48, little-endian) */ + uint64_t object_id = (uint64_t)data[1] | + ((uint64_t)data[2] << 8) | + ((uint64_t)data[3] << 16) | + ((uint64_t)data[4] << 24) | + ((uint64_t)data[5] << 32) | + ((uint64_t)data[6] << 40); + + ble_ots_client_object_changed_t evt = { + .source_of_change = (flags & BLE_OTS_OBJ_CHANGED_FLAG_SOURCE) ? 1 : 0, + .contents_changed = (flags & BLE_OTS_OBJ_CHANGED_FLAG_CONTENT) ? true : false, + .metadata_changed = (flags & BLE_OTS_OBJ_CHANGED_FLAG_METADATA) ? true : false, + .object_created = (flags & BLE_OTS_OBJ_CHANGED_FLAG_CREATION) ? true : false, + .object_deleted = (flags & BLE_OTS_OBJ_CHANGED_FLAG_DELETION) ? true : false, + .object_id = object_id, + }; + + ESP_LOGD(TAG, "Object Changed: flags=0x%02x obj_id=0x%012llx", + flags, (unsigned long long)object_id); + + ble_ots_client_dispatch_event(conn_id, BLE_OTS_CLIENT_EVT_OBJECT_CHANGED, &evt); +} + +/***************************************************************************** + * GAP Event Handler — Notification/Indication Reception + *****************************************************************************/ + +/** + * @brief GAP event handler for OLCP and Object Changed indications. + * + * This function should be registered as a GAP event listener or called from + * the main GAP event handler. It processes BLE_GAP_EVENT_NOTIFY_RX events + * for the OLCP and Object Changed characteristic handles. + * + * @param event GAP event + * @param arg Unused + * @return 0 on success + */ +int ble_ots_client_object_nav_gap_event(struct ble_gap_event *event, void *arg) +{ + if (!g_ots_client || !g_ots_client->initialized) { + return 0; + } + + if (event->type != BLE_GAP_EVENT_NOTIFY_RX) { + return 0; + } + + uint16_t conn_handle = event->notify_rx.conn_handle; + uint16_t attr_handle = event->notify_rx.attr_handle; + + /* Only process indications */ + if (!event->notify_rx.indication) { + return 0; + } + + ble_ots_client_conn_ctx_t *ctx = ble_ots_client_get_conn_ctx(conn_handle); + if (!ctx) { + return 0; + } + + /* Copy data from mbuf */ + uint16_t data_len = OS_MBUF_PKTLEN(event->notify_rx.om); + if (data_len == 0) { + return 0; + } + + uint8_t data_buf[32]; /* Max expected indication payload size */ + uint16_t copy_len = (data_len > sizeof(data_buf)) ? sizeof(data_buf) : data_len; + int rc = os_mbuf_copydata(event->notify_rx.om, 0, copy_len, data_buf); + if (rc != 0) { + ESP_LOGE(TAG, "Failed to copy indication data; rc=%d", rc); + return 0; + } + + /* Check if this is an OLCP indication */ + if (ctx->handles.olcp_handle != 0 && attr_handle == ctx->handles.olcp_handle) { + /* Verify Response Code opcode (0x70) */ + if (copy_len >= 1 && data_buf[0] == BLE_OTS_OLCP_OPCODE_RESPONSE) { + handle_olcp_indication(conn_handle, data_buf, copy_len); + } else { + ESP_LOGW(TAG, "Unexpected OLCP indication opcode: 0x%02x", + copy_len > 0 ? data_buf[0] : 0); + } + return 0; + } + + /* Check if this is an Object Changed indication */ + if (ctx->handles.object_changed_handle != 0 && attr_handle == ctx->handles.object_changed_handle) { + handle_object_changed_indication(conn_handle, data_buf, copy_len); + return 0; + } + + return 0; +} diff --git a/components/bt/ble_profiles/nimble/ble_ots/src/client/ble_ots_client_transfer.c b/components/bt/ble_profiles/nimble/ble_ots/src/client/ble_ots_client_transfer.c new file mode 100644 index 00000000000..fb020fff4ca --- /dev/null +++ b/components/bt/ble_profiles/nimble/ble_ots/src/client/ble_ots_client_transfer.c @@ -0,0 +1,1332 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include +#include "esp_log.h" + +#include "nimble/nimble_port.h" +#include "nimble/nimble_npl.h" +#include "host/ble_hs.h" +#include "host/ble_gap.h" +#include "host/ble_gatt.h" +#include "host/ble_l2cap.h" +#include "os/os_mbuf.h" + +#include "ble_ots_client_int.h" + +static const char *TAG = "ots_client_transfer"; + +/***************************************************************************** + * Constants + *****************************************************************************/ +#define PSM_OTS 0x0025 +#define OTC_MTU 256 + +#define OACP_RESPONSE_OPCODE 0x60 + +/***************************************************************************** + * Forward Declarations + *****************************************************************************/ +static int ble_ots_client_l2cap_event_cb(struct ble_l2cap_event *event, void *arg); +static void ble_ots_client_transfer_timer_cb(struct ble_npl_event *ev); + +/***************************************************************************** + * Transfer Inactivity Timer + * + * All OTS client timers are NimBLE callouts bound to the host default event + * queue, so their callbacks run in the NimBLE host task. That serialises them + * with the GATT/L2CAP handlers that mutate the same connection context (no + * data races, no lock needed), keeps NimBLE API calls on the host task as the + * stack requires, and honours the ble_ots_client_event_cb_t contract that + * application callbacks are invoked from the BLE host task. + *****************************************************************************/ + +/** + * @brief Create the transfer inactivity timer for a connection context. + */ +static int ble_ots_client_create_transfer_timer(ble_ots_client_conn_ctx_t *ctx) +{ + if (ctx->transfer_timer_inited) { + return 0; + } + + int rc = ble_npl_callout_init(&ctx->transfer_timer, nimble_port_get_dflt_eventq(), + ble_ots_client_transfer_timer_cb, + (void *)(uintptr_t)ctx->conn_id); + if (rc != 0) { + ESP_LOGE(TAG, "Failed to init transfer timer callout: %d", rc); + return BLE_HS_ENOMEM; + } + + ctx->transfer_timer_inited = true; + return 0; +} + +/** + * @brief Start or restart the transfer inactivity timer. + */ +static int ble_ots_client_start_transfer_timer(ble_ots_client_conn_ctx_t *ctx) +{ + int rc = ble_ots_client_create_transfer_timer(ctx); + if (rc != 0) { + return rc; + } + + /* ble_npl_callout_reset() re-arms an already-running callout. */ + ble_npl_error_t err = + ble_npl_callout_reset(&ctx->transfer_timer, + ble_npl_time_ms_to_ticks32(CONFIG_BLE_OTS_CLIENT_TRANSFER_TIMEOUT_MS)); + if (err != BLE_NPL_OK) { + ESP_LOGE(TAG, "Failed to start transfer timer: %d", err); + return BLE_HS_EOS; + } + + ctx->transfer_timer_active = true; + return 0; +} + +/** + * @brief Stop the transfer inactivity timer. + */ +static void ble_ots_client_stop_transfer_timer(ble_ots_client_conn_ctx_t *ctx) +{ + /* Stop unconditionally (not gated on transfer_timer_active) so the callout + * can never be left armed because the flag was out of step with it. */ + if (ctx->transfer_timer_inited) { + ble_npl_callout_stop(&ctx->transfer_timer); + } + ctx->transfer_timer_active = false; +} + +/** + * @brief Transfer inactivity timer callback (runs in the NimBLE host task). + */ +static void ble_ots_client_transfer_timer_cb(struct ble_npl_event *ev) +{ + uint16_t conn_id = (uint16_t)(uintptr_t)ble_npl_event_get_arg(ev); + + ble_ots_client_conn_ctx_t *ctx = ble_ots_client_get_conn_ctx(conn_id); + if (ctx == NULL) { + return; + } + + /* The callout may have been stopped after its event was already queued. */ + if (!ctx->transfer_timer_active) { + ESP_LOGD(TAG, "Transfer timeout ignored (timer already stopped), conn_id=%d", conn_id); + return; + } + + ESP_LOGW(TAG, "Transfer inactivity timeout, conn_id=%d", conn_id); + + ctx->transfer_timer_active = false; + ctx->transfer_in_progress = false; + ctx->transfer_opcode = 0; + + ble_ots_client_transfer_timeout_t evt = { + .conn_id = conn_id, + .opcode = 0x00, /* Data-transfer inactivity timeout */ + }; + + ble_ots_client_dispatch_event(conn_id, BLE_OTS_CLIENT_EVT_TRANSFER_TIMEOUT, &evt); +} + +/***************************************************************************** + * OACP Write Callback + *****************************************************************************/ + +/** + * @brief GATT write callback for OACP control-point writes. + */ +static int ble_ots_client_oacp_write_cb(uint16_t conn_handle, + const struct ble_gatt_error *error, + struct ble_gatt_attr *attr, + void *arg) +{ + uint8_t opcode = (uint8_t)(uintptr_t)arg; + + if (error->status != 0) { + ESP_LOGE(TAG, "OACP write failed: status=%d, opcode=0x%02x", error->status, opcode); + + ble_ots_client_conn_ctx_t *err_ctx = ble_ots_client_get_conn_ctx(conn_handle); + if (err_ctx != NULL) { + /* The procedure never started — release the synchronous busy flag + * so the next control-point operation is not rejected forever. */ + err_ctx->cp_pending_opcode = 0; + + /* Clear transfer_in_progress if it was set early for Read/Write */ + if (opcode == BLE_OTS_OACP_OPCODE_READ || opcode == BLE_OTS_OACP_OPCODE_WRITE) { + err_ctx->transfer_in_progress = false; + err_ctx->transfer_opcode = 0; + } + } + + /* Dispatch a failure event so the application is not left waiting */ + ble_ots_client_oacp_response_t resp = { + .request_opcode = opcode, + .result_code = BLE_OTS_OACP_RESULT_OPERATION_FAILED, + .checksum = 0, + .has_checksum = false, + }; + ble_ots_client_dispatch_event(conn_handle, BLE_OTS_CLIENT_EVT_OACP_RESPONSE, &resp); + return 0; + } + + ESP_LOGD(TAG, "OACP write success, opcode=0x%02x, starting CP timer", opcode); + + /* Start the control-point response timer. cp_pending_opcode was already + * set synchronously by the command API, so it is not touched here. */ + ble_ots_client_conn_ctx_t *ctx = ble_ots_client_get_conn_ctx(conn_handle); + if (ctx != NULL) { + int rc = ble_ots_client_start_cp_timer(conn_handle, CONFIG_BLE_OTS_CLIENT_OACP_TIMEOUT_MS); + if (rc != 0) { + /* The server has accepted the command and will answer with an + * indication; only the local timeout guard is missing. Keep the + * pending state so the state machine stays in sync with the peer. */ + ESP_LOGE(TAG, "Failed to start CP timer: rc=%d (procedure continues unguarded)", rc); + } + } + + return 0; +} + +/***************************************************************************** + * OACP Indication Handler + *****************************************************************************/ + +/* Deferred dispatch of a Write-success OACP response. Posted to the NimBLE host + * event queue so it runs in a later host-task iteration — after this + * indication's processing (and its ATT confirmation) has completed — so the + * object-data send the app issues from its OACP_RESPONSE handler goes out after + * the confirmation. A synchronous dispatch would race it and the peer would + * miss the data. */ +static void ble_ots_client_oacp_dispatch_ev_cb(struct ble_npl_event *ev) +{ + uint16_t conn_id = (uint16_t)(uintptr_t)ble_npl_event_get_arg(ev); + + ble_ots_client_conn_ctx_t *ctx = ble_ots_client_get_conn_ctx(conn_id); + if (ctx == NULL) { + return; + } + + ble_ots_client_dispatch_event(conn_id, BLE_OTS_CLIENT_EVT_OACP_RESPONSE, + &ctx->pending_oacp_resp); +} + +/** + * @brief Handle an OACP indication (called from GAP event handler in discovery module). + * + * Parses the OACP Response Code indication: + * [0] = Response Op Code (0x60) + * [1] = Request Op Code + * [2] = Result Code + * [3..6] = Optional checksum (for Calculate Checksum + Success) + */ +void ble_ots_client_handle_oacp_indication(uint16_t conn_handle, + uint16_t attr_handle, + struct os_mbuf *om) +{ + ble_ots_client_conn_ctx_t *ctx = ble_ots_client_get_conn_ctx(conn_handle); + if (ctx == NULL) { + ESP_LOGE(TAG, "OACP indication: no context for conn_handle=%d", conn_handle); + return; + } + + uint16_t data_len = OS_MBUF_PKTLEN(om); + if (data_len < 3) { + ESP_LOGE(TAG, "OACP indication too short: len=%d", data_len); + return; + } + + uint8_t buf[7]; /* Max: opcode(1) + req_opcode(1) + result(1) + checksum(4) */ + uint16_t copy_len = data_len > sizeof(buf) ? sizeof(buf) : data_len; + int rc = os_mbuf_copydata(om, 0, copy_len, buf); + if (rc != 0) { + ESP_LOGE(TAG, "Failed to copy OACP indication data"); + return; + } + + uint8_t response_opcode = buf[0]; + if (response_opcode != OACP_RESPONSE_OPCODE) { + ESP_LOGW(TAG, "Unexpected OACP opcode: 0x%02x", response_opcode); + return; + } + + uint8_t request_opcode = buf[1]; + uint8_t result_code = buf[2]; + + ESP_LOGI(TAG, "OACP response: req_opcode=0x%02x, result=0x%02x", request_opcode, result_code); + + /* The procedure is complete: stop the control-point timer and release the + * synchronous busy flag. Without clearing cp_pending_opcode here, every + * later OACP/OLCP operation would be rejected with BLE_HS_EBUSY for the + * rest of the connection. */ + ble_ots_client_stop_cp_timer(conn_handle); + ctx->cp_pending_opcode = 0; + + /* Build the OACP response event */ + ble_ots_client_oacp_response_t resp = { + .request_opcode = request_opcode, + .result_code = result_code, + .checksum = 0, + .has_checksum = false, + }; + + /* Check for checksum in Calculate Checksum + Success */ + if (request_opcode == BLE_OTS_OACP_OPCODE_CALCULATE_CHECKSUM && + result_code == BLE_OTS_OACP_RESULT_SUCCESS && + data_len >= 7) { + resp.checksum = (uint32_t)buf[3] | + ((uint32_t)buf[4] << 8) | + ((uint32_t)buf[5] << 16) | + ((uint32_t)buf[6] << 24); + resp.has_checksum = true; + ESP_LOGI(TAG, "OACP checksum: 0x%08" PRIx32, resp.checksum); + } + + /* Manage transfer tracking BEFORE dispatching the event, so the transfer + * state is already set up when the app issues a data send from its + * OACP_RESPONSE handler. */ + bool defer_dispatch = false; + if (request_opcode == BLE_OTS_OACP_OPCODE_READ || + request_opcode == BLE_OTS_OACP_OPCODE_WRITE) { + if (result_code == BLE_OTS_OACP_RESULT_SUCCESS) { + if (!ctx->otc_open) { + /* The OTC was open when the command was issued but has since + * closed, so no object data can flow. Report the procedure to + * the application as failed rather than passing the server's + * Success up: otherwise the app would wait for data that can + * never arrive, or push data that cannot be sent. */ + ESP_LOGE(TAG, "OTC not open, cannot start transfer for opcode=0x%02x", + request_opcode); + ctx->transfer_in_progress = false; + ctx->transfer_opcode = 0; + resp.result_code = BLE_OTS_OACP_RESULT_CHANNEL_UNAVAILABLE; + } else if (request_opcode == BLE_OTS_OACP_OPCODE_READ) { + ctx->transfer_opcode = BLE_OTS_OACP_OPCODE_READ; + ctx->transfer_received = 0; + /* transfer_offset and transfer_length were set when the command was issued */ + ble_ots_client_start_transfer_timer(ctx); + ESP_LOGI(TAG, "Read transfer started: offset=%" PRIu32 ", length=%" PRIu32, + ctx->transfer_offset, ctx->transfer_length); + } else { + ctx->transfer_opcode = BLE_OTS_OACP_OPCODE_WRITE; + ctx->transfer_sent = 0; + /* transfer_offset and transfer_length were set when the command was issued */ + ble_ots_client_start_transfer_timer(ctx); + ESP_LOGI(TAG, "Write transfer started: offset=%" PRIu32 ", length=%" PRIu32, + ctx->transfer_offset, ctx->transfer_length); + /* On Write success the app pushes object data over the OTC from + * its OACP_RESPONSE handler; defer that dispatch so the send + * goes out after this indication's ATT confirmation. */ + defer_dispatch = true; + } + } else { + /* OACP response indicates failure — clear the early transfer_in_progress flag */ + ctx->transfer_in_progress = false; + ctx->transfer_opcode = 0; + } + } + + if (defer_dispatch) { + /* Initialise the deferral event once per connection */ + if (!ctx->oacp_dispatch_ev_ready) { + ble_npl_event_init(&ctx->oacp_dispatch_ev, + ble_ots_client_oacp_dispatch_ev_cb, + (void *)(uintptr_t)conn_handle); + ctx->oacp_dispatch_ev_ready = true; + } + ctx->pending_oacp_resp = resp; + /* Runs in a later host-task iteration, i.e. after this indication's ATT + * confirmation has been emitted, so the app's data send follows it. */ + ble_npl_eventq_put(nimble_port_get_dflt_eventq(), &ctx->oacp_dispatch_ev); + } else { + ble_ots_client_dispatch_event(conn_handle, + BLE_OTS_CLIENT_EVT_OACP_RESPONSE, &resp); + } +} + +/***************************************************************************** + * OACP Command APIs + *****************************************************************************/ + +/** + * @brief Report whether a control-point procedure is already outstanding. + * + * cp_timer_active alone is not sufficient: it is only set from the ATT write + * response callback, so between a command API returning and that callback + * running it is still false. cp_pending_opcode is set synchronously by the + * command APIs and therefore closes that window. + */ +static bool ble_ots_client_cp_busy(const ble_ots_client_conn_ctx_t *ctx) +{ + return ctx->cp_timer_active || ctx->cp_pending_opcode != 0; +} + +int ble_ots_client_create_object(uint16_t conn_id, + uint32_t size, + const uint8_t *type_uuid, + uint8_t type_uuid_len) +{ + if (g_ots_client == NULL || !g_ots_client->initialized) { + return BLE_HS_EINVAL; + } + + if (type_uuid == NULL || (type_uuid_len != 2 && type_uuid_len != 16)) { + return BLE_HS_EINVAL; + } + + ble_ots_client_conn_ctx_t *ctx = ble_ots_client_get_conn_ctx(conn_id); + if (ctx == NULL) { + ESP_LOGE(TAG, "No context for conn_id=%d", conn_id); + return BLE_HS_ENOTCONN; + } + + if (ctx->cp_timed_out) { + ESP_LOGE(TAG, "CP timed out, reconnect required"); + return BLE_HS_ETIMEOUT; + } + + if (ble_ots_client_cp_busy(ctx)) { + ESP_LOGE(TAG, "CP operation already in progress"); + return BLE_HS_EBUSY; + } + + if (ctx->handles.oacp_handle == 0) { + ESP_LOGE(TAG, "OACP handle not discovered"); + return BLE_HS_ENOENT; + } + + /* Build OACP Create command: + * [0] = Op Code (0x01) + * [1..4] = Size (UINT32, little-endian) + * [5..N] = Type UUID (2 or 16 octets) + */ + uint8_t buf[1 + 4 + 16]; /* Max size */ + uint16_t buf_len = 0; + + buf[buf_len++] = BLE_OTS_OACP_OPCODE_CREATE; + buf[buf_len++] = (uint8_t)(size & 0xFF); + buf[buf_len++] = (uint8_t)((size >> 8) & 0xFF); + buf[buf_len++] = (uint8_t)((size >> 16) & 0xFF); + buf[buf_len++] = (uint8_t)((size >> 24) & 0xFF); + memcpy(&buf[buf_len], type_uuid, type_uuid_len); + buf_len += type_uuid_len; + + /* Mark the control point busy before the write so a second command issued + * before the write-response callback runs is rejected. */ + ctx->cp_pending_opcode = BLE_OTS_OACP_OPCODE_CREATE; + + int rc = ble_gattc_write_flat(conn_id, ctx->handles.oacp_handle, + buf, buf_len, + ble_ots_client_oacp_write_cb, + (void *)(uintptr_t)BLE_OTS_OACP_OPCODE_CREATE); + if (rc != 0) { + ESP_LOGE(TAG, "Failed to write OACP Create: rc=%d", rc); + ctx->cp_pending_opcode = 0; + } + + return rc; +} + +int ble_ots_client_delete_object(uint16_t conn_id) +{ + if (g_ots_client == NULL || !g_ots_client->initialized) { + return BLE_HS_EINVAL; + } + + ble_ots_client_conn_ctx_t *ctx = ble_ots_client_get_conn_ctx(conn_id); + if (ctx == NULL) { + ESP_LOGE(TAG, "No context for conn_id=%d", conn_id); + return BLE_HS_ENOTCONN; + } + + if (ctx->cp_timed_out) { + ESP_LOGE(TAG, "CP timed out, reconnect required"); + return BLE_HS_ETIMEOUT; + } + + if (ble_ots_client_cp_busy(ctx)) { + ESP_LOGE(TAG, "CP operation already in progress"); + return BLE_HS_EBUSY; + } + + if (ctx->handles.oacp_handle == 0) { + ESP_LOGE(TAG, "OACP handle not discovered"); + return BLE_HS_ENOENT; + } + + uint8_t buf[1]; + buf[0] = BLE_OTS_OACP_OPCODE_DELETE; + + ctx->cp_pending_opcode = BLE_OTS_OACP_OPCODE_DELETE; + + int rc = ble_gattc_write_flat(conn_id, ctx->handles.oacp_handle, + buf, sizeof(buf), + ble_ots_client_oacp_write_cb, + (void *)(uintptr_t)BLE_OTS_OACP_OPCODE_DELETE); + if (rc != 0) { + ESP_LOGE(TAG, "Failed to write OACP Delete: rc=%d", rc); + ctx->cp_pending_opcode = 0; + } + + return rc; +} + +int ble_ots_client_calculate_checksum(uint16_t conn_id, + uint32_t offset, + uint32_t length) +{ + if (g_ots_client == NULL || !g_ots_client->initialized) { + return BLE_HS_EINVAL; + } + + ble_ots_client_conn_ctx_t *ctx = ble_ots_client_get_conn_ctx(conn_id); + if (ctx == NULL) { + ESP_LOGE(TAG, "No context for conn_id=%d", conn_id); + return BLE_HS_ENOTCONN; + } + + if (ctx->cp_timed_out) { + ESP_LOGE(TAG, "CP timed out, reconnect required"); + return BLE_HS_ETIMEOUT; + } + + if (ble_ots_client_cp_busy(ctx)) { + ESP_LOGE(TAG, "CP operation already in progress"); + return BLE_HS_EBUSY; + } + + if (ctx->handles.oacp_handle == 0) { + ESP_LOGE(TAG, "OACP handle not discovered"); + return BLE_HS_ENOENT; + } + + /* Build OACP Calculate Checksum command: + * [0] = Op Code (0x03) + * [1..4] = Offset (UINT32, little-endian) + * [5..8] = Length (UINT32, little-endian) + */ + uint8_t buf[9]; + buf[0] = BLE_OTS_OACP_OPCODE_CALCULATE_CHECKSUM; + buf[1] = (uint8_t)(offset & 0xFF); + buf[2] = (uint8_t)((offset >> 8) & 0xFF); + buf[3] = (uint8_t)((offset >> 16) & 0xFF); + buf[4] = (uint8_t)((offset >> 24) & 0xFF); + buf[5] = (uint8_t)(length & 0xFF); + buf[6] = (uint8_t)((length >> 8) & 0xFF); + buf[7] = (uint8_t)((length >> 16) & 0xFF); + buf[8] = (uint8_t)((length >> 24) & 0xFF); + + ctx->cp_pending_opcode = BLE_OTS_OACP_OPCODE_CALCULATE_CHECKSUM; + + int rc = ble_gattc_write_flat(conn_id, ctx->handles.oacp_handle, + buf, sizeof(buf), + ble_ots_client_oacp_write_cb, + (void *)(uintptr_t)BLE_OTS_OACP_OPCODE_CALCULATE_CHECKSUM); + if (rc != 0) { + ESP_LOGE(TAG, "Failed to write OACP Calculate Checksum: rc=%d", rc); + ctx->cp_pending_opcode = 0; + } + + return rc; +} + +int ble_ots_client_execute_object(uint16_t conn_id) +{ + if (g_ots_client == NULL || !g_ots_client->initialized) { + return BLE_HS_EINVAL; + } + + ble_ots_client_conn_ctx_t *ctx = ble_ots_client_get_conn_ctx(conn_id); + if (ctx == NULL) { + ESP_LOGE(TAG, "No context for conn_id=%d", conn_id); + return BLE_HS_ENOTCONN; + } + + if (ctx->cp_timed_out) { + ESP_LOGE(TAG, "CP timed out, reconnect required"); + return BLE_HS_ETIMEOUT; + } + + if (ble_ots_client_cp_busy(ctx)) { + ESP_LOGE(TAG, "CP operation already in progress"); + return BLE_HS_EBUSY; + } + + if (ctx->handles.oacp_handle == 0) { + ESP_LOGE(TAG, "OACP handle not discovered"); + return BLE_HS_ENOENT; + } + + uint8_t buf[1]; + buf[0] = BLE_OTS_OACP_OPCODE_EXECUTE; + + ctx->cp_pending_opcode = BLE_OTS_OACP_OPCODE_EXECUTE; + + int rc = ble_gattc_write_flat(conn_id, ctx->handles.oacp_handle, + buf, sizeof(buf), + ble_ots_client_oacp_write_cb, + (void *)(uintptr_t)BLE_OTS_OACP_OPCODE_EXECUTE); + if (rc != 0) { + ESP_LOGE(TAG, "Failed to write OACP Execute: rc=%d", rc); + ctx->cp_pending_opcode = 0; + } + + return rc; +} + +int ble_ots_client_read_object_content(uint16_t conn_id, + uint32_t offset, + uint32_t length) +{ + if (g_ots_client == NULL || !g_ots_client->initialized) { + return BLE_HS_EINVAL; + } + + if (length == 0) { + ESP_LOGE(TAG, "OACP Read length must be non-zero"); + return BLE_HS_EINVAL; + } + + ble_ots_client_conn_ctx_t *ctx = ble_ots_client_get_conn_ctx(conn_id); + if (ctx == NULL) { + ESP_LOGE(TAG, "No context for conn_id=%d", conn_id); + return BLE_HS_ENOTCONN; + } + + if (ctx->cp_timed_out) { + ESP_LOGE(TAG, "CP timed out, reconnect required"); + return BLE_HS_ETIMEOUT; + } + + if (ble_ots_client_cp_busy(ctx)) { + ESP_LOGE(TAG, "CP operation already in progress"); + return BLE_HS_EBUSY; + } + + if (!ctx->otc_open) { + ESP_LOGE(TAG, "Object Transfer Channel not open"); + return BLE_HS_ENOENT; + } + + if (ctx->transfer_in_progress) { + ESP_LOGE(TAG, "Transfer already in progress"); + return BLE_HS_EBUSY; + } + + if (ctx->handles.oacp_handle == 0) { + ESP_LOGE(TAG, "OACP handle not discovered"); + return BLE_HS_ENOENT; + } + + /* Store transfer parameters for tracking after OACP Success */ + ctx->transfer_offset = offset; + ctx->transfer_length = length; + ctx->transfer_received = 0; + ctx->transfer_opcode = 0; /* Set only once the server answers Success */ + + /* Build OACP Read command: + * [0] = Op Code (0x05) + * [1..4] = Offset (UINT32, little-endian) + * [5..8] = Length (UINT32, little-endian) + */ + uint8_t buf[9]; + buf[0] = BLE_OTS_OACP_OPCODE_READ; + buf[1] = (uint8_t)(offset & 0xFF); + buf[2] = (uint8_t)((offset >> 8) & 0xFF); + buf[3] = (uint8_t)((offset >> 16) & 0xFF); + buf[4] = (uint8_t)((offset >> 24) & 0xFF); + buf[5] = (uint8_t)(length & 0xFF); + buf[6] = (uint8_t)((length >> 8) & 0xFF); + buf[7] = (uint8_t)((length >> 16) & 0xFF); + buf[8] = (uint8_t)((length >> 24) & 0xFF); + + ctx->cp_pending_opcode = BLE_OTS_OACP_OPCODE_READ; + + int rc = ble_gattc_write_flat(conn_id, ctx->handles.oacp_handle, + buf, sizeof(buf), + ble_ots_client_oacp_write_cb, + (void *)(uintptr_t)BLE_OTS_OACP_OPCODE_READ); + if (rc != 0) { + ESP_LOGE(TAG, "Failed to write OACP Read: rc=%d", rc); + ctx->transfer_offset = 0; + ctx->transfer_length = 0; + ctx->cp_pending_opcode = 0; + } else { + /* Mark transfer pending immediately to prevent overlapping requests */ + ctx->transfer_in_progress = true; + } + + return rc; +} + +int ble_ots_client_write_object_content(uint16_t conn_id, + uint32_t offset, + uint32_t length, + uint8_t mode) +{ + if (g_ots_client == NULL || !g_ots_client->initialized) { + return BLE_HS_EINVAL; + } + + /* Same reasoning as ble_ots_client_read_object_content(): a zero-length + * transfer cannot be tracked over the OTC and would complete on the first + * chunk, so reject it up front. */ + if (length == 0) { + ESP_LOGE(TAG, "OACP Write length must be non-zero"); + return BLE_HS_EINVAL; + } + + ble_ots_client_conn_ctx_t *ctx = ble_ots_client_get_conn_ctx(conn_id); + if (ctx == NULL) { + ESP_LOGE(TAG, "No context for conn_id=%d", conn_id); + return BLE_HS_ENOTCONN; + } + + if (ctx->cp_timed_out) { + ESP_LOGE(TAG, "CP timed out, reconnect required"); + return BLE_HS_ETIMEOUT; + } + + if (ble_ots_client_cp_busy(ctx)) { + ESP_LOGE(TAG, "CP operation already in progress"); + return BLE_HS_EBUSY; + } + + if (!ctx->otc_open) { + ESP_LOGE(TAG, "Object Transfer Channel not open"); + return BLE_HS_ENOENT; + } + + if (ctx->transfer_in_progress) { + ESP_LOGE(TAG, "Transfer already in progress"); + return BLE_HS_EBUSY; + } + + if (ctx->handles.oacp_handle == 0) { + ESP_LOGE(TAG, "OACP handle not discovered"); + return BLE_HS_ENOENT; + } + + /* Store transfer parameters for tracking after OACP Success */ + ctx->transfer_offset = offset; + ctx->transfer_length = length; + ctx->transfer_sent = 0; + ctx->transfer_opcode = 0; /* Set only once the server answers Success */ + + /* Build OACP Write command: + * [0] = Op Code (0x06) + * [1..4] = Offset (UINT32, little-endian) + * [5..8] = Length (UINT32, little-endian) + * [9] = Mode (UINT8) + */ + uint8_t buf[10]; + buf[0] = BLE_OTS_OACP_OPCODE_WRITE; + buf[1] = (uint8_t)(offset & 0xFF); + buf[2] = (uint8_t)((offset >> 8) & 0xFF); + buf[3] = (uint8_t)((offset >> 16) & 0xFF); + buf[4] = (uint8_t)((offset >> 24) & 0xFF); + buf[5] = (uint8_t)(length & 0xFF); + buf[6] = (uint8_t)((length >> 8) & 0xFF); + buf[7] = (uint8_t)((length >> 16) & 0xFF); + buf[8] = (uint8_t)((length >> 24) & 0xFF); + buf[9] = mode; + + ctx->cp_pending_opcode = BLE_OTS_OACP_OPCODE_WRITE; + + int rc = ble_gattc_write_flat(conn_id, ctx->handles.oacp_handle, + buf, sizeof(buf), + ble_ots_client_oacp_write_cb, + (void *)(uintptr_t)BLE_OTS_OACP_OPCODE_WRITE); + if (rc != 0) { + ESP_LOGE(TAG, "Failed to write OACP Write: rc=%d", rc); + ctx->transfer_offset = 0; + ctx->transfer_length = 0; + ctx->cp_pending_opcode = 0; + } else { + /* Mark transfer pending immediately to prevent overlapping requests. + * This only reserves the transfer slot — it does NOT authorise data on + * the OTC; ble_ots_client_send_data() additionally requires + * transfer_opcode, which is set only when the server answers Success. */ + ctx->transfer_in_progress = true; + } + + return rc; +} + +int ble_ots_client_abort_transfer(uint16_t conn_id) +{ + if (g_ots_client == NULL || !g_ots_client->initialized) { + return BLE_HS_EINVAL; + } + + ble_ots_client_conn_ctx_t *ctx = ble_ots_client_get_conn_ctx(conn_id); + if (ctx == NULL) { + ESP_LOGE(TAG, "No context for conn_id=%d", conn_id); + return BLE_HS_ENOTCONN; + } + + if (ctx->cp_timed_out) { + ESP_LOGE(TAG, "CP timed out, reconnect required"); + return BLE_HS_ETIMEOUT; + } + + /* OTS v1.0 section 4.4 explicitly allows the client to write the Abort Op + * Code while the server is processing another OACP procedure, so Abort is + * deliberately exempt from the ble_ots_client_cp_busy() guard — that is + * the whole point of the opcode. Only a second concurrent Abort is + * refused. Note that this overwrites cp_pending_opcode, so whichever + * indication arrives first releases the busy flag; that fails open (an + * extra command may be accepted) rather than deadlocking the client. */ + if (ctx->cp_pending_opcode == BLE_OTS_OACP_OPCODE_ABORT) { + ESP_LOGE(TAG, "Abort already in progress"); + return BLE_HS_EBUSY; + } + + if (ctx->handles.oacp_handle == 0) { + ESP_LOGE(TAG, "OACP handle not discovered"); + return BLE_HS_ENOENT; + } + + uint8_t buf[1]; + buf[0] = BLE_OTS_OACP_OPCODE_ABORT; + + uint8_t prev_opcode = ctx->cp_pending_opcode; + ctx->cp_pending_opcode = BLE_OTS_OACP_OPCODE_ABORT; + + int rc = ble_gattc_write_flat(conn_id, ctx->handles.oacp_handle, + buf, sizeof(buf), + ble_ots_client_oacp_write_cb, + (void *)(uintptr_t)BLE_OTS_OACP_OPCODE_ABORT); + if (rc != 0) { + /* The Abort never reached the stack. Leave the transfer state alone: + * clearing transfer_in_progress and stopping the inactivity timer here + * would lock out ble_ots_client_send_data() for a Write transfer and + * remove the only mechanism that could still recover the stalled OTC. */ + ESP_LOGE(TAG, "Failed to write OACP Abort: rc=%d", rc); + ctx->cp_pending_opcode = prev_opcode; + return rc; + } + + /* The Abort is queued — the client stops feeding/expecting object data. */ + ble_ots_client_stop_transfer_timer(ctx); + ctx->transfer_in_progress = false; + ctx->transfer_opcode = 0; + + return 0; +} + +/***************************************************************************** + * L2CAP Channel Management + *****************************************************************************/ + +/* + * Some OTS servers transiently reject the LE Credit Based Connection Request + * for the Object Transfer Channel with "LE_PSM not supported" + * (BLE_HS_ENOTSUP) — most notably PTS, which unregisters and immediately + * re-registers PSM 0x0025 right before it expects the OTC to open. A single + * connect attempt races that re-registration and gets rejected, so we back + * off briefly and retry a bounded number of times before giving up. + */ +#define OTC_CONNECT_MAX_RETRIES 3 +#define OTC_CONNECT_RETRY_DELAY_MS 200 + +/* Issue a single LE Credit Based Connection Request for the OTC. + * Caller must have verified the channel is not already open. */ +static int ble_ots_client_otc_connect(ble_ots_client_conn_ctx_t *ctx) +{ + struct os_mbuf *sdu_rx = os_msys_get_pkthdr(OTC_MTU, 0); + if (sdu_rx == NULL) { + ESP_LOGE(TAG, "Failed to allocate SDU rx buffer"); + return BLE_HS_ENOMEM; + } + + /* ble_l2cap_connect() takes ownership of sdu_rx on BOTH the success and + * the failure path: ble_l2cap_sig_connect_nolock() frees it directly on + * its early error returns, and once ble_l2cap_coc_chan_alloc() has + * attached it to the channel, every later error frees the channel and with + * it the SDU. Freeing it here as well would be a double free. */ + int rc = ble_l2cap_connect(ctx->conn_id, PSM_OTS, OTC_MTU, sdu_rx, + ble_ots_client_l2cap_event_cb, + (void *)(uintptr_t)ctx->conn_id); + if (rc != 0) { + ESP_LOGE(TAG, "Failed to initiate L2CAP connect: rc=%d", rc); + return rc; + } + + ESP_LOGI(TAG, "L2CAP connect initiated, conn_id=%d, PSM=0x%04x", + ctx->conn_id, PSM_OTS); + return 0; +} + +/* Backoff timer callback: re-attempt the OTC connect after an ENOTSUP reject. + * Runs in the NimBLE host task (callout on the host default event queue), which + * is required because it calls ble_l2cap_connect() and dispatches app events. */ +static void ble_ots_client_otc_retry_timer_cb(struct ble_npl_event *ev) +{ + uint16_t conn_id = (uint16_t)(uintptr_t)ble_npl_event_get_arg(ev); + + ble_ots_client_conn_ctx_t *ctx = ble_ots_client_get_conn_ctx(conn_id); + if (ctx == NULL || ctx->otc_open) { + return; /* Gone, or already connected in the meantime */ + } + + ESP_LOGW(TAG, "Retrying OTC connect (attempt %d/%d)", + ctx->otc_retry_count, OTC_CONNECT_MAX_RETRIES); + + if (ble_ots_client_otc_connect(ctx) != 0) { + /* Could not even queue the request — report the channel as closed */ + ble_ots_client_channel_closed_t closed_evt = { + .conn_id = conn_id, + .reason = (uint8_t)BLE_HS_ENOTSUP, + }; + ble_ots_client_dispatch_event(conn_id, + BLE_OTS_CLIENT_EVT_CHANNEL_CLOSED, &closed_evt); + } +} + +int ble_ots_client_open_channel(uint16_t conn_id) +{ + if (g_ots_client == NULL || !g_ots_client->initialized) { + return BLE_HS_EINVAL; + } + + ble_ots_client_conn_ctx_t *ctx = ble_ots_client_get_conn_ctx(conn_id); + if (ctx == NULL) { + ESP_LOGE(TAG, "No context for conn_id=%d", conn_id); + return BLE_HS_ENOTCONN; + } + + if (ctx->otc_open) { + ESP_LOGW(TAG, "Object Transfer Channel already open"); + return BLE_HS_EALREADY; + } + + /* Create the transfer timer if not yet created */ + int rc = ble_ots_client_create_transfer_timer(ctx); + if (rc != 0) { + return rc; + } + + /* Create the OTC connect retry timer if not yet created */ + if (!ctx->otc_retry_timer_inited) { + rc = ble_npl_callout_init(&ctx->otc_retry_timer, nimble_port_get_dflt_eventq(), + ble_ots_client_otc_retry_timer_cb, + (void *)(uintptr_t)ctx->conn_id); + if (rc != 0) { + ESP_LOGE(TAG, "Failed to init OTC retry callout: %d", rc); + return BLE_HS_ENOMEM; + } + ctx->otc_retry_timer_inited = true; + } + + ctx->otc_retry_count = 0; + return ble_ots_client_otc_connect(ctx); +} + +int ble_ots_client_close_channel(uint16_t conn_id) +{ + if (g_ots_client == NULL || !g_ots_client->initialized) { + return BLE_HS_EINVAL; + } + + ble_ots_client_conn_ctx_t *ctx = ble_ots_client_get_conn_ctx(conn_id); + if (ctx == NULL) { + ESP_LOGE(TAG, "No context for conn_id=%d", conn_id); + return BLE_HS_ENOTCONN; + } + + if (!ctx->otc_open || ctx->otc_chan == NULL) { + ESP_LOGW(TAG, "Object Transfer Channel not open"); + return BLE_HS_ENOENT; + } + + int rc = ble_l2cap_disconnect(ctx->otc_chan); + if (rc != 0) { + ESP_LOGE(TAG, "Failed to disconnect L2CAP channel: rc=%d", rc); + return rc; + } + + /* Stop any active transfer only after disconnect succeeded */ + ble_ots_client_stop_transfer_timer(ctx); + ctx->transfer_in_progress = false; + ctx->transfer_opcode = 0; + + return 0; +} + +/***************************************************************************** + * L2CAP Send Data + *****************************************************************************/ + +int ble_ots_client_send_data(uint16_t conn_id, + const uint8_t *data, + uint16_t data_len) +{ + if (g_ots_client == NULL || !g_ots_client->initialized) { + return BLE_HS_EINVAL; + } + + if (data == NULL || data_len == 0) { + return BLE_HS_EINVAL; + } + + ble_ots_client_conn_ctx_t *ctx = ble_ots_client_get_conn_ctx(conn_id); + if (ctx == NULL) { + ESP_LOGE(TAG, "No context for conn_id=%d", conn_id); + return BLE_HS_ENOTCONN; + } + + if (!ctx->otc_open || ctx->otc_chan == NULL) { + ESP_LOGE(TAG, "Object Transfer Channel not open"); + return BLE_HS_ENOENT; + } + + if (!ctx->transfer_in_progress) { + ESP_LOGE(TAG, "No transfer in progress"); + return BLE_HS_EINVAL; + } + + /* transfer_in_progress is set as soon as the OACP Write command is queued, + * to reserve the transfer slot. It is therefore NOT proof that the server + * authorised the transfer. transfer_opcode is set only by the OACP Write + * Success indication, so it is what actually gates data on the OTC — + * without this check the app could push object data before the server has + * accepted (or while it is rejecting) the Write procedure. */ + if (ctx->transfer_opcode != BLE_OTS_OACP_OPCODE_WRITE) { + ESP_LOGE(TAG, "OACP Write not acknowledged yet, cannot send object data"); + return BLE_HS_EINVAL; + } + + /* Allocate an mbuf for the data */ + struct os_mbuf *sdu_tx = os_msys_get_pkthdr(data_len, 0); + if (sdu_tx == NULL) { + ESP_LOGE(TAG, "Failed to allocate SDU tx buffer"); + return BLE_HS_ENOMEM; + } + + int rc = os_mbuf_append(sdu_tx, data, data_len); + if (rc != 0) { + ESP_LOGE(TAG, "Failed to append data to mbuf"); + os_mbuf_free_chain(sdu_tx); + return BLE_HS_ENOMEM; + } + + /* mbuf ownership rules of ble_l2cap_send()/ble_l2cap_coc_send(): + * 0 -> the whole SDU went out and the stack freed it. + * BLE_HS_ESTALLED -> the stack HAS taken the SDU (it is parked in + * chan->coc_tx.sdus[0]) and will finish sending it + * when the peer grants credits; freeing it here + * would be a double free / use-after-free. + * anything else -> the stack either rejected the SDU before taking it + * (EBADDATA/EBUSY) or dropped its reference without + * freeing, because ble_l2cap_coc_send() calls + * ble_l2cap_coc_continue_tx() with free_on_err=false. + * In both cases the caller still owns the mbuf and + * must free it, or it leaks. */ + rc = ble_l2cap_send(ctx->otc_chan, sdu_tx); + if (rc == BLE_HS_ESTALLED) { + /* Credits exhausted mid-SDU. The stack owns and will deliver the rest, + * so count the octets as sent, but do NOT declare the transfer + * complete yet — completion is detected on TX_UNSTALLED once the SDU + * has actually drained. Keep the inactivity timer armed so a peer that + * never returns credits still trips the timeout. */ + ESP_LOGD(TAG, "L2CAP send stalled, waiting for unstall"); + ctx->transfer_sent += data_len; + ble_ots_client_start_transfer_timer(ctx); + return 0; + } else if (rc != 0) { + ESP_LOGE(TAG, "Failed to send L2CAP data: rc=%d", rc); + os_mbuf_free_chain(sdu_tx); + return rc; + } + + /* Data sent successfully — update tracking */ + ctx->transfer_sent += data_len; + + /* Reset the transfer inactivity timer */ + ble_ots_client_start_transfer_timer(ctx); + + /* Dispatch DATA_SENT event */ + uint32_t remaining = 0; + if (ctx->transfer_length > ctx->transfer_sent) { + remaining = ctx->transfer_length - ctx->transfer_sent; + } + + ble_ots_client_data_sent_t evt = { + .data_len = data_len, + .remaining = remaining, + }; + + ble_ots_client_dispatch_event(conn_id, BLE_OTS_CLIENT_EVT_DATA_SENT, &evt); + + /* Check if transfer is complete */ + if (ctx->transfer_sent >= ctx->transfer_length) { + ESP_LOGI(TAG, "Write transfer complete: sent=%" PRIu32, ctx->transfer_sent); + ble_ots_client_stop_transfer_timer(ctx); + ctx->transfer_in_progress = false; + ctx->transfer_opcode = 0; + } + + return 0; +} + +/***************************************************************************** + * L2CAP Event Handler + *****************************************************************************/ + +static int ble_ots_client_l2cap_event_cb(struct ble_l2cap_event *event, void *arg) +{ + uint16_t conn_id = (uint16_t)(uintptr_t)arg; + + switch (event->type) { + case BLE_L2CAP_EVENT_COC_CONNECTED: { + ESP_LOGI(TAG, "L2CAP COC connected: status=%d, conn_handle=%d", + event->connect.status, event->connect.conn_handle); + + conn_id = event->connect.conn_handle; + ble_ots_client_conn_ctx_t *ctx = ble_ots_client_get_conn_ctx(conn_id); + if (ctx == NULL) { + ESP_LOGE(TAG, "No context for conn_id=%d on L2CAP connect", conn_id); + return 0; + } + + if (event->connect.status != 0) { + /* Connection failed */ + ESP_LOGE(TAG, "L2CAP COC connect failed: status=%d", event->connect.status); + ctx->otc_chan = NULL; + ctx->otc_open = false; + + /* A peer that is momentarily not accepting the PSM (e.g. PTS while + * it re-registers PSM 0x0025) rejects with ENOTSUP. Back off and + * retry a bounded number of times before giving up. */ + if (event->connect.status == BLE_HS_ENOTSUP && + ctx->otc_retry_timer_inited && + ctx->otc_retry_count < OTC_CONNECT_MAX_RETRIES) { + ctx->otc_retry_count++; + ble_npl_error_t err = + ble_npl_callout_reset(&ctx->otc_retry_timer, + ble_npl_time_ms_to_ticks32(OTC_CONNECT_RETRY_DELAY_MS)); + if (err == BLE_NPL_OK) { + ESP_LOGW(TAG, "OTC connect rejected (ENOTSUP); retry %d/%d in %d ms", + ctx->otc_retry_count, OTC_CONNECT_MAX_RETRIES, + OTC_CONNECT_RETRY_DELAY_MS); + return 0; + } + ESP_LOGE(TAG, "Failed to arm OTC retry timer: %d", err); + } + + ble_ots_client_channel_closed_t closed_evt = { + .conn_id = conn_id, + .reason = (uint8_t)event->connect.status, + }; + ble_ots_client_dispatch_event(conn_id, BLE_OTS_CLIENT_EVT_CHANNEL_CLOSED, &closed_evt); + return 0; + } + + ctx->otc_chan = event->connect.chan; + ctx->otc_open = true; + ctx->otc_retry_count = 0; + if (ctx->otc_retry_timer_inited) { + ble_npl_callout_stop(&ctx->otc_retry_timer); + } + + /* Get channel info for MTU/MPS */ + struct ble_l2cap_chan_info chan_info; + memset(&chan_info, 0, sizeof(chan_info)); + ble_l2cap_get_chan_info(event->connect.chan, &chan_info); + + ble_ots_client_channel_info_t open_evt = { + .conn_id = conn_id, + .channel_id = chan_info.scid, + .mtu = chan_info.our_coc_mtu, + .mps = chan_info.our_l2cap_mtu, + }; + + ESP_LOGI(TAG, "OTC opened: channel_id=%d, mtu=%d, mps=%d", + open_evt.channel_id, open_evt.mtu, open_evt.mps); + + ble_ots_client_dispatch_event(conn_id, BLE_OTS_CLIENT_EVT_CHANNEL_OPEN, &open_evt); + return 0; + } + + case BLE_L2CAP_EVENT_COC_DISCONNECTED: { + ESP_LOGI(TAG, "L2CAP COC disconnected: conn_handle=%d", + event->disconnect.conn_handle); + + conn_id = event->disconnect.conn_handle; + ble_ots_client_conn_ctx_t *ctx = ble_ots_client_get_conn_ctx(conn_id); + if (ctx == NULL) { + ESP_LOGW(TAG, "No context for conn_id=%d on L2CAP disconnect", conn_id); + return 0; + } + + /* Stop any active transfer */ + ble_ots_client_stop_transfer_timer(ctx); + ctx->transfer_in_progress = false; + ctx->transfer_opcode = 0; + ctx->otc_chan = NULL; + ctx->otc_open = false; + + ble_ots_client_channel_closed_t closed_evt = { + .conn_id = conn_id, + .reason = 0, /* Local or remote close */ + }; + + ble_ots_client_dispatch_event(conn_id, BLE_OTS_CLIENT_EVT_CHANNEL_CLOSED, &closed_evt); + return 0; + } + + case BLE_L2CAP_EVENT_COC_DATA_RECEIVED: { + conn_id = event->receive.conn_handle; + ble_ots_client_conn_ctx_t *ctx = ble_ots_client_get_conn_ctx(conn_id); + if (ctx == NULL) { + ESP_LOGW(TAG, "No context for conn_id=%d on L2CAP data received", conn_id); + os_mbuf_free_chain(event->receive.sdu_rx); + return 0; + } + + struct os_mbuf *sdu_rx = event->receive.sdu_rx; + uint16_t data_len = OS_MBUF_PKTLEN(sdu_rx); + + ESP_LOGD(TAG, "L2CAP data received: conn_id=%d, len=%d", conn_id, data_len); + + /* Drop unsolicited or trailing data. The OTC stays open after a + * transfer is aborted, times out or completes, so in-flight SDUs can + * still arrive. Feeding them into the tracking counters would corrupt + * the offsets of the next transfer, and — worse — re-arm the + * inactivity timer for a transfer that will never make progress, + * producing a spurious TRANSFER_TIMEOUT event later. Just replenish + * the channel credits and discard. */ + if (!ctx->transfer_in_progress) { + ESP_LOGW(TAG, "Discarding %d bytes on OTC with no transfer in progress", data_len); + os_mbuf_free_chain(sdu_rx); + struct os_mbuf *stale_sdu = os_msys_get_pkthdr(OTC_MTU, 0); + if (stale_sdu != NULL) { + ble_l2cap_recv_ready(event->receive.chan, stale_sdu); + } + return 0; + } + + /* Copy data from mbuf. A failure here means this chunk is lost; since + * the OTC carries no retransmission and transfer_received would stop + * tracking the peer's byte stream, every later chunk would be handed + * to the application at the wrong offset. Tear the channel down + * instead of silently corrupting the object — the resulting + * COC_DISCONNECTED event tells the application the transfer failed. */ + uint8_t *data_buf = malloc(data_len); + if (data_buf == NULL) { + ESP_LOGE(TAG, "Failed to allocate buffer for received data, aborting transfer"); + os_mbuf_free_chain(sdu_rx); + ble_ots_client_stop_transfer_timer(ctx); + ctx->transfer_in_progress = false; + ctx->transfer_opcode = 0; + if (ctx->otc_chan != NULL) { + ble_l2cap_disconnect(ctx->otc_chan); + } + return 0; + } + + int rc = os_mbuf_copydata(sdu_rx, 0, data_len, data_buf); + if (rc != 0) { + ESP_LOGE(TAG, "Failed to copy received data from mbuf, aborting transfer"); + free(data_buf); + os_mbuf_free_chain(sdu_rx); + ble_ots_client_stop_transfer_timer(ctx); + ctx->transfer_in_progress = false; + ctx->transfer_opcode = 0; + if (ctx->otc_chan != NULL) { + ble_l2cap_disconnect(ctx->otc_chan); + } + return 0; + } + + /* Free the received SDU mbuf chain — ownership was transferred to us */ + os_mbuf_free_chain(sdu_rx); + + /* Calculate the current offset for this chunk */ + uint32_t chunk_offset = ctx->transfer_offset + ctx->transfer_received; + + /* Update tracking */ + ctx->transfer_received += data_len; + + /* Reset the transfer inactivity timer */ + ble_ots_client_start_transfer_timer(ctx); + + /* Dispatch DATA_RECEIVED event */ + ble_ots_client_data_received_t recv_evt = { + .data = data_buf, + .data_len = data_len, + .offset = chunk_offset, + }; + + ble_ots_client_dispatch_event(conn_id, BLE_OTS_CLIENT_EVT_DATA_RECEIVED, &recv_evt); + + free(data_buf); + + /* Check if transfer is complete */ + if (ctx->transfer_received >= ctx->transfer_length) { + ESP_LOGI(TAG, "Read transfer complete: received=%" PRIu32, ctx->transfer_received); + ble_ots_client_stop_transfer_timer(ctx); + ctx->transfer_in_progress = false; + ctx->transfer_opcode = 0; + } + + /* Provide a new SDU rx buffer for the next receive */ + struct os_mbuf *next_sdu = os_msys_get_pkthdr(OTC_MTU, 0); + if (next_sdu != NULL) { + ble_l2cap_recv_ready(event->receive.chan, next_sdu); + } + + return 0; + } + + case BLE_L2CAP_EVENT_COC_TX_UNSTALLED: { + conn_id = event->tx_unstalled.conn_handle; + ESP_LOGD(TAG, "L2CAP TX un-stalled: conn_id=%d, status=%d", + conn_id, event->tx_unstalled.status); + + ble_ots_client_conn_ctx_t *ctx = ble_ots_client_get_conn_ctx(conn_id); + if (ctx == NULL) { + return 0; + } + + /* The previously stalled SDU has now drained. If it was the last one, + * complete the transfer here — ble_ots_client_send_data() deliberately + * leaves completion to this point when it returns on ESTALLED. */ + if (ctx->transfer_in_progress && ctx->transfer_sent >= ctx->transfer_length) { + ESP_LOGI(TAG, "Write transfer complete (un-stalled): sent=%" PRIu32, + ctx->transfer_sent); + ble_ots_client_stop_transfer_timer(ctx); + ctx->transfer_in_progress = false; + ctx->transfer_opcode = 0; + } else if (ctx->transfer_in_progress) { + /* Reset the transfer inactivity timer */ + ble_ots_client_start_transfer_timer(ctx); + } + + /* Dispatch DATA_SENT event to let the application know it can send more */ + uint32_t remaining = 0; + if (ctx->transfer_length > ctx->transfer_sent) { + remaining = ctx->transfer_length - ctx->transfer_sent; + } + + ble_ots_client_data_sent_t sent_evt = { + .data_len = 0, /* No new data was sent in this event */ + .remaining = remaining, + }; + + ble_ots_client_dispatch_event(conn_id, BLE_OTS_CLIENT_EVT_DATA_SENT, &sent_evt); + return 0; + } + + default: + ESP_LOGD(TAG, "Unhandled L2CAP event: type=%d", event->type); + return 0; + } +} diff --git a/components/bt/ble_profiles/nimble/ble_ots/src/server/ble_ots_server_int.h b/components/bt/ble_profiles/nimble/ble_ots/src/server/ble_ots_server_int.h new file mode 100644 index 00000000000..7e3fbeaa76b --- /dev/null +++ b/components/bt/ble_profiles/nimble/ble_ots/src/server/ble_ots_server_int.h @@ -0,0 +1,528 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef BLE_OTS_SERVER_INT_H +#define BLE_OTS_SERVER_INT_H + +#include +#include +#include "freertos/FreeRTOS.h" +#include "freertos/semphr.h" +#include "host/ble_hs.h" +#include "nimble/nimble_npl.h" +#include "ble_ots_common.h" +#include "ble_ots_server.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/***************************************************************************** + * Internal Constants + *****************************************************************************/ + +/** @brief Invalid connection handle sentinel */ +#define BLE_OTS_CONN_ID_NONE 0xFFFF + +/** @brief Maximum number of filter instances per connection */ +#define BLE_OTS_FILTER_INSTANCE_COUNT 3 + +/** @brief Maximum DLO content buffer size (estimated worst-case) */ +#define BLE_OTS_DLO_MAX_BUF_SIZE 4096 + +/** @brief Maximum serialized length of an Object List Filter characteristic value. */ +#define BLE_OTS_FILTER_VALUE_MAX_LEN (1 + BLE_OTS_OBJECT_NAME_MAX_LEN) + +/* ATT error codes defined by the Supplement to the Bluetooth Core + * Specification (CSS), Part B "Common Profile and Service Error Codes". + * NimBLE's ble_att.h only covers the core ATT codes (up to 0x13). */ + +/** @brief Client Characteristic Configuration Descriptor Improperly Configured */ +#define ATT_ERR_CCCD_IMPROPERLY_CONFIGURED 0xFD + +/** @brief Procedure Already in Progress */ +#define ATT_ERR_PROC_ALREADY_IN_PROGRESS 0xFE + +/***************************************************************************** + * Internal Data Structures — ots_server_init.c + *****************************************************************************/ + +/** + * @brief Object database entry — a single object in the object store. + */ +typedef struct { + bool in_use; /*!< Whether this slot is occupied */ + ble_ots_obj_id_t object_id; /*!< 48-bit Object ID */ + char name[BLE_OTS_OBJECT_NAME_MAX_LEN]; /*!< Object name (UTF-8, not NUL-terminated) */ + uint8_t name_len; /*!< Length of name in octets */ + uint8_t type_uuid_len; /*!< 2 or 16 */ + uint8_t type_uuid[16]; /*!< Object type UUID (little-endian) */ + uint32_t current_size; /*!< Actual content size in octets */ + uint32_t allocated_size; /*!< Allocated storage size */ + ble_ots_date_time_t first_created; /*!< First-Created timestamp */ + ble_ots_date_time_t last_modified; /*!< Last-Modified timestamp */ + uint32_t properties; /*!< Object properties bit-field */ + uint8_t *data; /*!< Object content data buffer */ + uint16_t locked_by; /*!< Connection holding lock, or BLE_OTS_CONN_ID_NONE */ + uint32_t marked; /*!< Per-bond marking bitmask */ +} ble_ots_server_obj_t; + +/** @brief Deferred indication request (opaque, defined in ots_server_init.c) */ +struct ots_indicate_ctx; + +/** + * @brief Per-connection state managed by the concurrency pool. + */ +typedef struct { + bool in_pool; /*!< Whether connection is in concurrency pool */ + uint16_t conn_id; /*!< BLE connection identifier */ + ble_ots_obj_id_t current_object_id; /*!< Current Object ID for this connection */ + bool current_obj_valid; /*!< false = Invalid Object state */ + ble_ots_list_filter_t filter[BLE_OTS_FILTER_INSTANCE_COUNT]; /*!< Per-connection filters */ + struct ble_l2cap_chan *otc_chan; /*!< L2CAP OTC channel, or NULL */ + bool transfer_active; /*!< Transfer (read or write) in progress */ + bool transfer_is_read; /*!< true = read transfer, false = write */ + ble_ots_obj_id_t transfer_object_id; /*!< Object ID being transferred */ + uint32_t transfer_offset; /*!< Transfer start offset */ + uint32_t transfer_length; /*!< Total transfer length requested */ + uint32_t transfer_bytes_done; /*!< Bytes transferred so far */ + bool transfer_truncated; /*!< Write transfer applied an OACP truncation */ + struct ble_npl_callout transfer_timer; /*!< Inactivity timeout callout (NimBLE task context) */ + bool transfer_timer_inited; /*!< Whether transfer_timer has been initialized */ + struct ble_npl_callout transfer_retry_timer; /*!< Read-transfer back-off callout (NimBLE task context) */ + bool transfer_retry_timer_inited; /*!< Whether transfer_retry_timer has been initialized */ + uint16_t transfer_retry_count; /*!< Consecutive back-off retries of the current chunk */ + struct ble_npl_event transfer_data_ev; /*!< Deferred read-data send event (NimBLE task context) */ + bool transfer_data_ev_inited; /*!< Whether transfer_data_ev has been initialized */ + ble_ots_obj_id_t created_obj_ids[CONFIG_BLE_OTS_SERVER_MAX_OBJECTS]; /*!< Objects created this session */ + uint8_t created_obj_count; /*!< Number of objects in created_obj_ids */ + ble_ots_list_sort_order_t sort_order; /*!< Current sort order for this connection */ + + bool olcp_indicate_subscribed; /*!< OLCP CCCD indication subscription state */ + bool oacp_subscribed; /*!< OACP CCCD indication subscription state */ + bool obj_changed_subscribed; /*!< Object Changed CCCD indication subscription state */ + + /* ATT allows only one outstanding indication per connection, so queued + * indications are drained one at a time as confirmations arrive. */ + struct ots_indicate_ctx *indicate_head; /*!< Oldest queued indication, or NULL */ + struct ots_indicate_ctx *indicate_tail; /*!< Newest queued indication, or NULL */ + bool indicate_in_flight; /*!< An indication is awaiting confirmation */ +} ble_ots_server_conn_state_t; + +/** + * @brief OTS server global control block — aggregated internal state. + */ +typedef struct { + bool initialized; /*!< Server initialized */ + bool started; /*!< Server started */ + ble_ots_server_config_t config; /*!< Copy of configuration */ + ble_ots_server_cb_t app_cb; /*!< Application callback */ + ble_ots_feature_t ots_feature; /*!< OTS Feature value */ + ble_ots_server_obj_t obj_db[CONFIG_BLE_OTS_SERVER_MAX_OBJECTS]; /*!< Object database */ + ble_ots_obj_id_t next_obj_id; /*!< Next Object ID to allocate */ + ble_ots_server_conn_state_t conn_pool[CONFIG_BLE_OTS_SERVER_MAX_CONCURRENCY]; /*!< Concurrency pool */ + uint8_t *dlo_content; /*!< Directory Listing Object content buffer */ + uint32_t dlo_content_size; /*!< Current DLO content size */ +} ble_ots_server_cb_env_t; + +/** + * @brief Global OTS server control block (defined in ots_server_init.c). + */ +extern ble_ots_server_cb_env_t *p_ble_ots_server_env; + +/** @brief Control block accessor — only valid when BLE_OTS_SERVER_ENV_OK() */ +#define ble_ots_server_env (*p_ble_ots_server_env) + +/** @brief Whether the control block is currently allocated */ +#define BLE_OTS_SERVER_ENV_OK() (p_ble_ots_server_env != NULL) + +/** + * @brief Acquire the OTS server mutex. + * + * Guards the object database, the object content buffers and the connection + * pool, all of which are reached from both the NimBLE host task and the + * application task. The mutex is recursive, so nesting is safe. Composite + * read-modify-write sequences over that state must hold it for their whole + * duration, not just around the individual accessors they call. + */ +void ble_ots_server_lock(void); + +/** + * @brief Release the OTS server mutex acquired with ble_ots_server_lock(). + */ +void ble_ots_server_unlock(void); + +/***************************************************************************** + * Internal Interfaces — ots_server_init.c + *****************************************************************************/ + +/** + * @brief Create a new empty object in the database. + * + * @param type Object type UUID entry + * @param size Initial allocated size + * @return Newly allocated Object ID, or 0 on failure (DB full) + */ +ble_ots_obj_id_t ble_ots_server_obj_db_create(const ble_ots_obj_type_entry_t *type, + uint32_t size); + +/** + * @brief Delete an object from the database by Object ID. + * + * @param object_id Object ID to delete (DLO cannot be deleted) + * @return 0 on success, non-zero on failure + */ +int ble_ots_server_obj_db_delete(ble_ots_obj_id_t object_id); + +/** + * @brief Look up an object by Object ID. + * + * @param object_id Object ID to look up + * @return Pointer to the object structure, or NULL if not found + */ +ble_ots_server_obj_t *ble_ots_server_obj_db_lookup(ble_ots_obj_id_t object_id); + +/** + * @brief Read object content data into a buffer. + * + * @param object_id Target object ID + * @param offset Byte offset within the object + * @param length Number of octets to read + * @param buf Destination buffer (must be >= length octets) + * @return 0 on success, non-zero on failure + */ +int ble_ots_server_obj_data_read(ble_ots_obj_id_t object_id, uint32_t offset, + uint32_t length, uint8_t *buf); + +/** + * @brief Write object content data into the database. + * + * @param object_id Target object ID + * @param offset Byte offset within the object + * @param data Source data buffer + * @param length Number of octets to write + * @return 0 on success, non-zero on failure + */ +int ble_ots_server_obj_data_write(ble_ots_obj_id_t object_id, uint32_t offset, + const uint8_t *data, uint32_t length); + +/** + * @brief Get the Current Object ID for a connection. + * + * @param conn_id BLE connection identifier + * @return Current Object ID, or BLE_OTS_OBJ_ID_INVALID if invalid + */ +ble_ots_obj_id_t ble_ots_server_current_obj_get(uint16_t conn_id); + +/** + * @brief Set the Current Object for a connection. + * + * @param conn_id BLE connection identifier + * @param object_id Object ID to set (BLE_OTS_OBJ_ID_INVALID to invalidate) + * @return 0 on success, non-zero on failure + */ +int ble_ots_server_current_obj_set(uint16_t conn_id, ble_ots_obj_id_t object_id); + +/** + * @brief Check if a connection is within the concurrency pool. + * + * @param conn_id BLE connection identifier + * @return true if within pool, false if exceeded + */ +bool ble_ots_server_concurrency_check(uint16_t conn_id); + +/** + * @brief Lock an object for exclusive write transfer. + * + * @param object_id Object ID to lock + * @param conn_id Connection requesting the lock + * @return 0 on success, non-zero if already locked by another connection + */ +int ble_ots_server_obj_lock(ble_ots_obj_id_t object_id, uint16_t conn_id); + +/** + * @brief Unlock an object after transfer completion. + * + * @param object_id Object ID to unlock + * @return 0 on success, non-zero if not locked + */ +int ble_ots_server_obj_unlock(ble_ots_obj_id_t object_id); + +/** + * @brief Get the L2CAP OTC channel handle for a connection. + * + * @param conn_id BLE connection identifier + * @return L2CAP channel pointer, or NULL if none open + */ +struct ble_l2cap_chan *ble_ots_server_otc_get(uint16_t conn_id); + +/** + * @brief Close the L2CAP OTC channel for a connection. + * + * @param conn_id BLE connection identifier + * @return 0 on success, non-zero on failure + */ +int ble_ots_server_otc_close(uint16_t conn_id); + +/** + * @brief Send a GATT indication on the specified characteristic. + * + * The indication mbuf is built synchronously (so @p data may be on the + * caller's stack), but the actual transmission is deferred to a NimBLE + * host-task event. This keeps indications from being sent inside a GATT + * access callback. A return of 0 therefore means "successfully queued". + * + * @param conn_id BLE connection identifier + * @param char_uuid Characteristic UUID (OACP/OLCP/Object Changed) + * @param data Indication payload + * @param len Length of data in octets + * @return 0 if queued successfully, non-zero on failure + */ +int ble_ots_server_indicate_response(uint16_t conn_id, uint16_t char_uuid, + const uint8_t *data, uint16_t len); + +/** + * @brief Get the per-connection state for a given connection ID. + * + * @param conn_id BLE connection identifier + * @return Pointer to connection state, or NULL if not found + */ +ble_ots_server_conn_state_t *ble_ots_server_conn_state_get(uint16_t conn_id); + +/** + * @brief Get the OACP characteristic value handle. + * + * @return OACP value handle, or 0 if the service has never been registered + */ +uint16_t ble_ots_server_oacp_handle_get(void); + +/** + * @brief Get the OLCP characteristic value handle. + * + * @return OLCP value handle, or 0 if the service has never been registered + */ +uint16_t ble_ots_server_olcp_handle_get(void); + +/** + * @brief Dispatch an event to the application callback. + * + * @param event Event type + * @param param Event parameter union + */ +void ble_ots_server_dispatch_event(ble_ots_server_event_t event, + ble_ots_server_cb_param_t *param); + +/** + * @brief Start the transfer inactivity timer for a connection. + * + * @param conn_id BLE connection identifier + */ +void ble_ots_server_transfer_timer_start(uint16_t conn_id); + +/** + * @brief Stop the transfer inactivity timer for a connection. + * + * @param conn_id BLE connection identifier + */ +void ble_ots_server_transfer_timer_stop(uint16_t conn_id); + +/** + * @brief Reset the transfer inactivity timer for a connection. + * + * @param conn_id BLE connection identifier + */ +void ble_ots_server_transfer_timer_reset(uint16_t conn_id); + +/** + * @brief Check whether the given object name is unique across the object DB. + * + * @param name Name to check + * @param name_len Length of name + * @param exclude_id Object ID to exclude from the check (or BLE_OTS_OBJ_ID_INVALID) + * @return true if name is unique, false if duplicate exists + */ +bool ble_ots_server_name_is_unique(const char *name, uint8_t name_len, + ble_ots_obj_id_t exclude_id); + +/***************************************************************************** + * Internal Interfaces — ots_server_filter_changed.c + *****************************************************************************/ + +/** + * @brief Filtered list result for OLCP navigation. + */ +typedef struct { + uint32_t count; /*!< Number of matching objects */ + ble_ots_obj_id_t object_ids[CONFIG_BLE_OTS_SERVER_MAX_OBJECTS]; /*!< Object IDs passing filters */ +} ble_ots_server_filtered_list_t; + +/** + * @brief Allocate the filter/changed sub-module context. + * + * Must be called once on the OTS server init path before any other + * filter/changed API is used. Idempotent. + * + * @return 0 on success, BLE_HS_ENOMEM on allocation failure + */ +int ble_ots_server_filter_changed_init(void); + +/** + * @brief Free the filter/changed sub-module context. + * + * Called on the OTS server deinit path. Safe to call when never allocated. + */ +void ble_ots_server_filter_changed_deinit(void); + +/** + * @brief Get the filtered and sorted object list for a connection. + * + * Evaluates all three filter instances with AND logic against the object DB. + * + * @param conn_id Connection identifier + * @return Pointer to the reusable filtered list, or NULL on error + */ +ble_ots_server_filtered_list_t *ble_ots_server_filter_get_list(uint16_t conn_id); + +/** + * @brief Dispatch Object Changed indication to subscribed clients. + * + * @param object_id Object ID that changed (must not be DLO) + * @param flags Change flags (BLE_OTS_OBJ_CHANGED_FLAG_*) + * @param source_conn_id Connection that caused the change, or BLE_OTS_CONN_ID_NONE for server + */ +void ble_ots_server_dispatch_obj_changed(ble_ots_obj_id_t object_id, uint8_t flags, + uint16_t source_conn_id); + +/** + * @brief Reset all three filter instances for a connection to No Filter. + * + * @param conn_id Connection identifier + */ +void ble_ots_server_filter_reset(uint16_t conn_id); + +/** + * @brief Handle a client write to an Object List Filter instance. + * + * @param conn_id Connection identifier + * @param instance_idx Filter instance index (0, 1, or 2) + * @param data Raw write data (filter type + parameters) + * @param length Length of write data + * @return 0 on success, ATT error code on failure + */ +int ble_ots_server_filter_write(uint16_t conn_id, uint8_t instance_idx, + const uint8_t *data, uint16_t length); + +/** + * @brief Handle a client read of an Object List Filter instance. + * + * @param conn_id Connection identifier + * @param instance_idx Filter instance index (0, 1, or 2) + * @param buf Output buffer + * @param buf_len In: buffer capacity; Out: actual length written + * @return 0 on success, non-zero on failure + */ +int ble_ots_server_filter_read(uint16_t conn_id, uint8_t instance_idx, + uint8_t *buf, uint16_t *buf_len); + +/** + * @brief Rebuild the Directory Listing Object content. + * + * Iterates over all objects in the database and serializes them into DLO records. + */ +void ble_ots_server_dlo_rebuild(void); + +/** + * @brief Set or clear the Mark property for an object on a per-bond basis. + * + * @param conn_id Connection identifier (for bond determination) + * @param object_id Object ID to mark/unmark + * @param mark true to mark, false to unmark + * @return 0 on success, non-zero on failure + */ +int ble_ots_server_mark_object(uint16_t conn_id, ble_ots_obj_id_t object_id, bool mark); + +/** + * @brief Clean up marked-object state on disconnection. + * + * For non-bonded peers, clears the marked bits from all objects and frees the + * bond-index slot. For bonded peers, the slot and marked bits are preserved + * so that marked state persists across reconnections. + * + * Takes the peer identity rather than a connection handle: NimBLE deletes the + * connection object before delivering BLE_GAP_EVENT_DISCONNECT, so + * ble_gap_conn_find() can no longer resolve the peer at this point. Pass the + * fields of the connection descriptor carried by the disconnect event + * (event->disconnect.conn). + * + * @param peer_id_addr Peer identity address of the disconnecting peer + * @param bonded Whether the peer was bonded at disconnection time + */ +void ble_ots_server_mark_cleanup_on_disconnect(const ble_addr_t *peer_id_addr, bool bonded); + +/***************************************************************************** + * Internal Interfaces — ots_server_oacp_transfer.c + *****************************************************************************/ + +/** + * @brief Handle OACP Read procedure. + * + * @param conn_id Connection identifier + * @param offset Byte offset within the Current Object + * @param length Number of octets to read and send + * @return 0 if accepted, negative error code otherwise + */ +int ble_ots_server_oacp_read_proc(uint16_t conn_id, uint32_t offset, uint32_t length); + +/** + * @brief Handle OACP Write procedure. + * + * @param conn_id Connection identifier + * @param offset Byte offset within the Current Object + * @param length Total octets the client intends to send + * @param mode Write mode bit-field + * @return 0 if accepted, negative error code otherwise + */ +int ble_ots_server_oacp_write_proc(uint16_t conn_id, uint32_t offset, + uint32_t length, uint8_t mode); + +/** + * @brief Handle OACP Abort procedure. + * + * @param conn_id Connection identifier + * @return 0 if accepted, negative error code otherwise + */ +int ble_ots_server_oacp_abort_proc(uint16_t conn_id); + +/***************************************************************************** + * Internal Interfaces — ots_server_olcp.c + *****************************************************************************/ + +/** + * @brief Handle a BLE_GAP_EVENT_SUBSCRIBE for OLCP CCCD tracking. + * + * Should be called from the GAP event callback when a subscribe event is + * received. Updates the per-connection OLCP indication subscription state. + * + * @param conn_handle BLE connection handle + * @param attr_handle Attribute handle from the subscribe event + * @param cur_indicate Current indication subscription state (1 = subscribed) + */ +void ble_ots_server_olcp_handle_subscribe(uint16_t conn_handle, + uint16_t attr_handle, + bool cur_indicate); + +/** + * @brief Check if the object database contains any objects. + * + * @return true if at least one object slot is in use, false otherwise + */ +bool ble_ots_server_obj_db_has_objects(void); + +#ifdef __cplusplus +} +#endif + +#endif /* BLE_OTS_SERVER_INT_H */ diff --git a/components/bt/ble_profiles/nimble/ble_ots/src/server/ots_server_filter_changed.c b/components/bt/ble_profiles/nimble/ble_ots/src/server/ots_server_filter_changed.c new file mode 100644 index 00000000000..8a483feb087 --- /dev/null +++ b/components/bt/ble_profiles/nimble/ble_ots/src/server/ots_server_filter_changed.c @@ -0,0 +1,1101 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include "esp_log.h" +#include "host/ble_hs.h" +#include "host/ble_gatt.h" +#include "host/ble_gap.h" +#include "os/os_mbuf.h" +#include "ble_ots_server_int.h" + +static const char *TAG = "ots_filter_changed"; + +/***************************************************************************** + * Bond-index mapping: maps peer identity addresses to stable bit indices + * (0–31) for the per-object `marked` bitmask. + *****************************************************************************/ + +/** @brief Maximum number of bond index slots (must be <= 32 for uint32_t bitmask) */ +#define OTS_MAX_BOND_SLOTS 32 + +/** @brief A single bond-index slot */ +typedef struct { + bool in_use; /*!< Whether this slot is occupied */ + ble_addr_t peer_addr; /*!< Peer identity address */ +} ots_bond_slot_t; + +/** + * @brief Aggregated module context for the filter/changed sub-module. + * + * Holds all of this module's mutable global state in a single heap-allocated + * block: the bond-index slot table, the reusable filtered list, and the + * transient sort order used to bridge ble_ots_server_filter_get_list() and the + * qsort() comparison callback. + */ +typedef struct { + ots_bond_slot_t bond_slots[OTS_MAX_BOND_SLOTS]; /*!< Bond-index slot table */ + ble_ots_server_filtered_list_t filtered_list; /*!< Reusable filtered list */ + ble_ots_list_sort_order_t sort_order; /*!< Active sort order for sort_compare */ +} ots_server_filter_changed_ctx_t; + +static ots_server_filter_changed_ctx_t *s_ots_server_filter_changed; + +/** + * @brief Allocate the filter/changed module context. + * + * Called once on the OTS server init path, before any filter/changed API is + * used. Idempotent: a no-op if the context is already allocated. + * + * @return 0 on success, BLE_HS_ENOMEM on allocation failure + */ +int +ble_ots_server_filter_changed_init(void) +{ + if (s_ots_server_filter_changed) { + return 0; + } + + s_ots_server_filter_changed = calloc(1, sizeof(*s_ots_server_filter_changed)); + if (!s_ots_server_filter_changed) { + ESP_LOGE(TAG, "filter_changed_init: failed to allocate context"); + return BLE_HS_ENOMEM; + } + + return 0; +} + +/** + * @brief Free the filter/changed module context. + * + * Called on the OTS server deinit path. Safe to call when the context was + * never allocated. + */ +void +ble_ots_server_filter_changed_deinit(void) +{ + if (s_ots_server_filter_changed) { + free(s_ots_server_filter_changed); + s_ots_server_filter_changed = NULL; + } +} + +/** + * @brief Resolve a transient conn_id to a stable bond index (0–31). + * + * Looks up the peer identity address via ble_gap_conn_find(), then searches + * the bond-slot table for an existing entry. If none is found, a free slot + * is allocated. + * + * @param conn_id BLE connection handle + * @param[out] out_index Receives the bond index on success + * @return 0 on success, non-zero on failure (connection not found or table full) + */ +static int +resolve_bond_index(uint16_t conn_id, uint8_t *out_index) +{ + if (!s_ots_server_filter_changed) { + return BLE_HS_ENOMEM; + } + + struct ble_gap_conn_desc desc; + int rc = ble_gap_conn_find(conn_id, &desc); + if (rc != 0) { + ESP_LOGW(TAG, "resolve_bond_index: conn_find failed conn_id=%d rc=%d", + conn_id, rc); + return rc; + } + + const ble_addr_t *peer = &desc.peer_id_addr; + ots_bond_slot_t *bond_slots = s_ots_server_filter_changed->bond_slots; + + /* Search for an existing slot with the same peer identity address */ + int free_slot = -1; + for (int i = 0; i < OTS_MAX_BOND_SLOTS; i++) { + if (bond_slots[i].in_use) { + if (bond_slots[i].peer_addr.type == peer->type && + memcmp(bond_slots[i].peer_addr.val, peer->val, 6) == 0) { + *out_index = (uint8_t)i; + return 0; + } + } else if (free_slot < 0) { + free_slot = i; + } + } + + /* Allocate a new slot */ + if (free_slot < 0) { + ESP_LOGW(TAG, "resolve_bond_index: bond slot table full"); + return BLE_HS_ENOMEM; + } + + bond_slots[free_slot].in_use = true; + bond_slots[free_slot].peer_addr = *peer; + *out_index = (uint8_t)free_slot; + return 0; +} + +/***************************************************************************** + * Helper: Date-Time comparison + * Returns <0 if a < b, 0 if a == b, >0 if a > b + *****************************************************************************/ +static int +datetime_cmp(const ble_ots_date_time_t *a, const ble_ots_date_time_t *b) +{ + if (a->year != b->year) { + return (int)a->year - (int)b->year; + } + if (a->month != b->month) { + return (int)a->month - (int)b->month; + } + if (a->day != b->day) { + return (int)a->day - (int)b->day; + } + if (a->hours != b->hours) { + return (int)a->hours - (int)b->hours; + } + if (a->minutes != b->minutes) { + return (int)a->minutes - (int)b->minutes; + } + if (a->seconds != b->seconds) { + return (int)a->seconds - (int)b->seconds; + } + return 0; +} + +/***************************************************************************** + * Helper: Check if a date-time is all zeros (not valid) + *****************************************************************************/ +static bool +datetime_is_zero(const ble_ots_date_time_t *dt) +{ + return (dt->year == 0 && dt->month == 0 && dt->day == 0 && + dt->hours == 0 && dt->minutes == 0 && dt->seconds == 0); +} + +/***************************************************************************** + * Helper: Parse a date-time from raw bytes (7 octets, little-endian) + *****************************************************************************/ +static void +parse_date_time(const uint8_t *data, ble_ots_date_time_t *dt) +{ + dt->year = (uint16_t)data[0] | ((uint16_t)data[1] << 8); + dt->month = data[2]; + dt->day = data[3]; + dt->hours = data[4]; + dt->minutes = data[5]; + dt->seconds = data[6]; +} + +/***************************************************************************** + * Helper: Serialize a date-time to raw bytes (7 octets, little-endian) + *****************************************************************************/ +static void +serialize_date_time(const ble_ots_date_time_t *dt, uint8_t *buf) +{ + buf[0] = (uint8_t)(dt->year & 0xFF); + buf[1] = (uint8_t)((dt->year >> 8) & 0xFF); + buf[2] = dt->month; + buf[3] = dt->day; + buf[4] = dt->hours; + buf[5] = dt->minutes; + buf[6] = dt->seconds; +} + +/***************************************************************************** + * Helper: Check if a single object passes a single filter + *****************************************************************************/ +static bool +object_passes_filter(const ble_ots_server_obj_t *obj, + const ble_ots_list_filter_t *filter, + uint16_t conn_id) +{ + switch (filter->filter_type) { + case BLE_OTS_FILTER_NO_FILTER: + return true; + + case BLE_OTS_FILTER_NAME_STARTS_WITH: { + uint8_t plen = filter->param.name.name_len; + if (obj->name_len < plen) { + return false; + } + return (memcmp(obj->name, filter->param.name.name, plen) == 0); + } + + case BLE_OTS_FILTER_NAME_ENDS_WITH: { + uint8_t plen = filter->param.name.name_len; + if (obj->name_len < plen) { + return false; + } + return (memcmp(obj->name + obj->name_len - plen, + filter->param.name.name, plen) == 0); + } + + case BLE_OTS_FILTER_NAME_CONTAINS: { + uint8_t plen = filter->param.name.name_len; + if (plen == 0) { + return true; + } + if (obj->name_len < plen) { + return false; + } + /* Byte-level substring search */ + for (uint8_t i = 0; i <= obj->name_len - plen; i++) { + if (memcmp(obj->name + i, filter->param.name.name, plen) == 0) { + return true; + } + } + return false; + } + + case BLE_OTS_FILTER_NAME_IS_EXACTLY: { + uint8_t plen = filter->param.name.name_len; + if (obj->name_len != plen) { + return false; + } + return (memcmp(obj->name, filter->param.name.name, plen) == 0); + } + + case BLE_OTS_FILTER_OBJECT_TYPE: { + if (obj->type_uuid_len != filter->param.obj_type.uuid_len) { + return false; + } + return (memcmp(obj->type_uuid, filter->param.obj_type.uuid, + obj->type_uuid_len) == 0); + } + + case BLE_OTS_FILTER_CREATED_BETWEEN: { + /* Objects with zero/invalid first_created always pass */ + if (datetime_is_zero(&obj->first_created)) { + return true; + } + if (datetime_cmp(&obj->first_created, &filter->param.timestamp_range.timestamp1) < 0) { + return false; + } + if (datetime_cmp(&obj->first_created, &filter->param.timestamp_range.timestamp2) > 0) { + return false; + } + return true; + } + + case BLE_OTS_FILTER_MODIFIED_BETWEEN: { + /* Objects with zero/invalid last_modified always pass */ + if (datetime_is_zero(&obj->last_modified)) { + return true; + } + if (datetime_cmp(&obj->last_modified, &filter->param.timestamp_range.timestamp1) < 0) { + return false; + } + if (datetime_cmp(&obj->last_modified, &filter->param.timestamp_range.timestamp2) > 0) { + return false; + } + return true; + } + + case BLE_OTS_FILTER_CURRENT_SIZE_BETWEEN: + return (obj->current_size >= filter->param.size_range.size1 && + obj->current_size <= filter->param.size_range.size2); + + case BLE_OTS_FILTER_ALLOCATED_SIZE_BETWEEN: + return (obj->allocated_size >= filter->param.size_range.size1 && + obj->allocated_size <= filter->param.size_range.size2); + + case BLE_OTS_FILTER_MARKED_OBJECTS: { + /* Resolve conn_id to a stable bond index for the per-object marked bitmask */ + uint8_t bond_idx; + if (resolve_bond_index(conn_id, &bond_idx) != 0) { + return false; + } + return (obj->marked & (1UL << bond_idx)) != 0; + } + + default: + return true; + } +} + +/***************************************************************************** + * Helper: Compare two object names byte-wise, ascending + * Returns <0 if a < b, 0 if a == b, >0 if a > b + *****************************************************************************/ +static int +obj_name_cmp(const ble_ots_server_obj_t *a, const ble_ots_server_obj_t *b) +{ + int min_len = (a->name_len < b->name_len) ? a->name_len : b->name_len; + int result = memcmp(a->name, b->name, min_len); + if (result == 0) { + result = (int)a->name_len - (int)b->name_len; + } + return result; +} + +/***************************************************************************** + * Helper: Compare two Object IDs, ascending + * Returns <0 if a < b, 0 if a == b, >0 if a > b + *****************************************************************************/ +static int +obj_id_cmp(ble_ots_obj_id_t a, ble_ots_obj_id_t b) +{ + if (a < b) { + return -1; + } + return (a > b) ? 1 : 0; +} + +/***************************************************************************** + * Sort comparison context and callback for qsort + *****************************************************************************/ +static int +sort_compare(const void *a, const void *b) +{ + ble_ots_obj_id_t id_a = *(const ble_ots_obj_id_t *)a; + ble_ots_obj_id_t id_b = *(const ble_ots_obj_id_t *)b; + + ble_ots_server_obj_t *obj_a = ble_ots_server_obj_db_lookup(id_a); + ble_ots_server_obj_t *obj_b = ble_ots_server_obj_db_lookup(id_b); + + if (!obj_a || !obj_b) { + /* Should not happen: the list is built from live objects. Fall back to + * Object ID order so the result stays a valid total ordering. */ + return obj_id_cmp(id_a, id_b); + } + + int result = 0; + + switch (s_ots_server_filter_changed->sort_order) { + case BLE_OTS_SORT_ORDER_NAME_ASC: + case BLE_OTS_SORT_ORDER_NAME_DESC: + result = obj_name_cmp(obj_a, obj_b); + break; + + case BLE_OTS_SORT_ORDER_TYPE_ASC: + case BLE_OTS_SORT_ORDER_TYPE_DESC: { + /* Compare type UUIDs */ + if (obj_a->type_uuid_len != obj_b->type_uuid_len) { + result = (int)obj_a->type_uuid_len - (int)obj_b->type_uuid_len; + } else { + result = memcmp(obj_a->type_uuid, obj_b->type_uuid, obj_a->type_uuid_len); + } + break; + } + + case BLE_OTS_SORT_ORDER_CURRENT_SIZE_ASC: + case BLE_OTS_SORT_ORDER_CURRENT_SIZE_DESC: + if (obj_a->current_size < obj_b->current_size) { + result = -1; + } else if (obj_a->current_size > obj_b->current_size) { + result = 1; + } + break; + + case BLE_OTS_SORT_ORDER_FIRST_CREATED_ASC: + case BLE_OTS_SORT_ORDER_FIRST_CREATED_DESC: + result = datetime_cmp(&obj_a->first_created, &obj_b->first_created); + break; + + case BLE_OTS_SORT_ORDER_LAST_MODIFIED_ASC: + case BLE_OTS_SORT_ORDER_LAST_MODIFIED_DESC: + result = datetime_cmp(&obj_a->last_modified, &obj_b->last_modified); + break; + + default: + /* Default: ascending by name */ + result = obj_name_cmp(obj_a, obj_b); + break; + } + + /* Reverse for descending orders. The ascending sort orders are 0x01-0x05 + * and the descending ones 0x11-0x15, so a single >= test against the first + * descending value selects exactly the descending half. */ + if (s_ots_server_filter_changed->sort_order >= BLE_OTS_SORT_ORDER_NAME_DESC) { + result = -result; + } + + /* Objects that compare equal on the sort criterion must still be ordered + * deterministically: OLCP First/Last/Next/Previous rebuild and re-sort this + * list on every procedure and locate the current object by its position, so + * an unstable order (qsort is not a stable sort) would make navigation skip + * or repeat objects. Tie-break on name and then Object ID, always + * ascending — applied after the inversion above so that descending orders + * do not flip the tie-breaker as well. */ + if (result == 0) { + result = obj_name_cmp(obj_a, obj_b); + if (result == 0) { + result = obj_id_cmp(id_a, id_b); + } + } + + return result; +} + +/***************************************************************************** + * ble_ots_server_filter_reset + *****************************************************************************/ +void +ble_ots_server_filter_reset(uint16_t conn_id) +{ + ble_ots_server_conn_state_t *cs = ble_ots_server_conn_state_get(conn_id); + if (!cs) { + ESP_LOGW(TAG, "filter_reset: conn_id=%d not found", conn_id); + return; + } + + for (int i = 0; i < BLE_OTS_FILTER_INSTANCE_COUNT; i++) { + memset(&cs->filter[i], 0, sizeof(ble_ots_list_filter_t)); + cs->filter[i].filter_type = BLE_OTS_FILTER_NO_FILTER; + } +} + +/***************************************************************************** + * ble_ots_server_filter_write + *****************************************************************************/ +int +ble_ots_server_filter_write(uint16_t conn_id, uint8_t instance_idx, + const uint8_t *data, uint16_t length) +{ + if (instance_idx >= BLE_OTS_FILTER_INSTANCE_COUNT) { + return BLE_ATT_ERR_UNLIKELY; + } + + if (!ble_ots_server_concurrency_check(conn_id)) { + return BLE_OTS_APP_ERR_CONCURRENCY_LIMIT_EXCEEDED; + } + + ble_ots_server_conn_state_t *cs = ble_ots_server_conn_state_get(conn_id); + if (!cs) { + return BLE_ATT_ERR_UNLIKELY; + } + + if (length < 1) { + return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN; + } + + uint8_t filter_type = data[0]; + uint16_t param_len = length - 1; + const uint8_t *param_data = data + 1; + + /* Validate filter type range */ + if (filter_type > BLE_OTS_FILTER_MARKED_OBJECTS) { + return BLE_OTS_APP_ERR_WRITE_REQUEST_REJECTED; + } + + /* Validate parameter length per filter type */ + ble_ots_list_filter_t new_filter; + memset(&new_filter, 0, sizeof(new_filter)); + new_filter.filter_type = filter_type; + + switch (filter_type) { + case BLE_OTS_FILTER_NO_FILTER: + if (param_len != 0) { + return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN; + } + break; + + case BLE_OTS_FILTER_NAME_STARTS_WITH: + case BLE_OTS_FILTER_NAME_ENDS_WITH: + case BLE_OTS_FILTER_NAME_CONTAINS: + /* At least 1 octet of parameter required for 0x01-0x03 */ + if (param_len < 1 || param_len > BLE_OTS_OBJECT_NAME_MAX_LEN) { + return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN; + } + memcpy(new_filter.param.name.name, param_data, param_len); + new_filter.param.name.name_len = (uint8_t)param_len; + break; + + case BLE_OTS_FILTER_NAME_IS_EXACTLY: + /* 0 or more octets for Name Is Exactly */ + if (param_len > BLE_OTS_OBJECT_NAME_MAX_LEN) { + return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN; + } + if (param_len > 0) { + memcpy(new_filter.param.name.name, param_data, param_len); + } + new_filter.param.name.name_len = (uint8_t)param_len; + break; + + case BLE_OTS_FILTER_OBJECT_TYPE: + if (param_len != 2 && param_len != 16) { + return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN; + } + memcpy(new_filter.param.obj_type.uuid, param_data, param_len); + new_filter.param.obj_type.uuid_len = (uint8_t)param_len; + break; + + case BLE_OTS_FILTER_CREATED_BETWEEN: + case BLE_OTS_FILTER_MODIFIED_BETWEEN: + /* 14 bytes: two date_time (7 each) */ + if (param_len != 14) { + return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN; + } + parse_date_time(param_data, &new_filter.param.timestamp_range.timestamp1); + parse_date_time(param_data + 7, &new_filter.param.timestamp_range.timestamp2); + /* Validate timestamp1 <= timestamp2 */ + if (datetime_cmp(&new_filter.param.timestamp_range.timestamp1, + &new_filter.param.timestamp_range.timestamp2) > 0) { + return BLE_OTS_APP_ERR_WRITE_REQUEST_REJECTED; + } + break; + + case BLE_OTS_FILTER_CURRENT_SIZE_BETWEEN: + case BLE_OTS_FILTER_ALLOCATED_SIZE_BETWEEN: + /* 8 bytes: two uint32 */ + if (param_len != 8) { + return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN; + } + new_filter.param.size_range.size1 = (uint32_t)param_data[0] | + ((uint32_t)param_data[1] << 8) | + ((uint32_t)param_data[2] << 16) | + ((uint32_t)param_data[3] << 24); + new_filter.param.size_range.size2 = (uint32_t)param_data[4] | + ((uint32_t)param_data[5] << 8) | + ((uint32_t)param_data[6] << 16) | + ((uint32_t)param_data[7] << 24); + /* Validate size1 <= size2 */ + if (new_filter.param.size_range.size1 > new_filter.param.size_range.size2) { + return BLE_OTS_APP_ERR_WRITE_REQUEST_REJECTED; + } + break; + + case BLE_OTS_FILTER_MARKED_OBJECTS: + if (param_len != 0) { + return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN; + } + break; + + default: + return BLE_OTS_APP_ERR_WRITE_REQUEST_REJECTED; + } + + /* Store the new filter */ + memcpy(&cs->filter[instance_idx], &new_filter, sizeof(ble_ots_list_filter_t)); + + /* Check if current object still passes all filters; invalidate if not */ + if (cs->current_obj_valid) { + ble_ots_server_obj_t *cur_obj = ble_ots_server_obj_db_lookup(cs->current_object_id); + if (cur_obj) { + bool passes = true; + for (int i = 0; i < BLE_OTS_FILTER_INSTANCE_COUNT; i++) { + if (!object_passes_filter(cur_obj, &cs->filter[i], conn_id)) { + passes = false; + break; + } + } + if (!passes) { + ble_ots_server_current_obj_set(conn_id, BLE_OTS_OBJ_ID_INVALID); + } + } + } + + return 0; +} + +/***************************************************************************** + * ble_ots_server_filter_read + *****************************************************************************/ +int +ble_ots_server_filter_read(uint16_t conn_id, uint8_t instance_idx, + uint8_t *buf, uint16_t *buf_len) +{ + if (instance_idx >= BLE_OTS_FILTER_INSTANCE_COUNT) { + return BLE_ATT_ERR_UNLIKELY; + } + + if (!ble_ots_server_concurrency_check(conn_id)) { + return BLE_OTS_APP_ERR_CONCURRENCY_LIMIT_EXCEEDED; + } + + ble_ots_server_conn_state_t *cs = ble_ots_server_conn_state_get(conn_id); + if (!cs) { + return BLE_ATT_ERR_UNLIKELY; + } + + const ble_ots_list_filter_t *filter = &cs->filter[instance_idx]; + uint16_t offset = 0; + uint16_t capacity = *buf_len; + + /* First byte: filter type */ + if (offset < capacity) { + buf[offset] = filter->filter_type; + } + offset++; + + /* Parameter bytes depend on filter type */ + switch (filter->filter_type) { + case BLE_OTS_FILTER_NO_FILTER: + case BLE_OTS_FILTER_MARKED_OBJECTS: + /* No parameter */ + break; + + case BLE_OTS_FILTER_NAME_STARTS_WITH: + case BLE_OTS_FILTER_NAME_ENDS_WITH: + case BLE_OTS_FILTER_NAME_CONTAINS: + case BLE_OTS_FILTER_NAME_IS_EXACTLY: { + uint8_t nlen = filter->param.name.name_len; + if (nlen > 0 && (offset + nlen) <= capacity) { + memcpy(buf + offset, filter->param.name.name, nlen); + } + offset += nlen; + break; + } + + case BLE_OTS_FILTER_OBJECT_TYPE: { + uint8_t ulen = filter->param.obj_type.uuid_len; + if (ulen > 0 && (offset + ulen) <= capacity) { + memcpy(buf + offset, filter->param.obj_type.uuid, ulen); + } + offset += ulen; + break; + } + + case BLE_OTS_FILTER_CREATED_BETWEEN: + case BLE_OTS_FILTER_MODIFIED_BETWEEN: + if ((offset + 14) <= capacity) { + serialize_date_time(&filter->param.timestamp_range.timestamp1, buf + offset); + serialize_date_time(&filter->param.timestamp_range.timestamp2, buf + offset + 7); + } + offset += 14; + break; + + case BLE_OTS_FILTER_CURRENT_SIZE_BETWEEN: + case BLE_OTS_FILTER_ALLOCATED_SIZE_BETWEEN: + if ((offset + 8) <= capacity) { + uint32_t s1 = filter->param.size_range.size1; + uint32_t s2 = filter->param.size_range.size2; + buf[offset + 0] = (uint8_t)(s1 & 0xFF); + buf[offset + 1] = (uint8_t)((s1 >> 8) & 0xFF); + buf[offset + 2] = (uint8_t)((s1 >> 16) & 0xFF); + buf[offset + 3] = (uint8_t)((s1 >> 24) & 0xFF); + buf[offset + 4] = (uint8_t)(s2 & 0xFF); + buf[offset + 5] = (uint8_t)((s2 >> 8) & 0xFF); + buf[offset + 6] = (uint8_t)((s2 >> 16) & 0xFF); + buf[offset + 7] = (uint8_t)((s2 >> 24) & 0xFF); + } + offset += 8; + break; + + default: + break; + } + + *buf_len = offset; + return 0; +} + +/***************************************************************************** + * ble_ots_server_filter_get_list + *****************************************************************************/ +ble_ots_server_filtered_list_t * +ble_ots_server_filter_get_list(uint16_t conn_id) +{ + if (!s_ots_server_filter_changed) { + return NULL; + } + + if (!ble_ots_server_concurrency_check(conn_id)) { + return NULL; + } + + ble_ots_server_conn_state_t *cs = ble_ots_server_conn_state_get(conn_id); + if (!cs) { + return NULL; + } + + ble_ots_server_filtered_list_t *filtered_list = + &s_ots_server_filter_changed->filtered_list; + + memset(filtered_list, 0, sizeof(*filtered_list)); + + /* Iterate all objects in the database */ + for (int i = 0; i < CONFIG_BLE_OTS_SERVER_MAX_OBJECTS; i++) { + ble_ots_server_obj_t *obj = &ble_ots_server_env.obj_db[i]; + if (!obj->in_use) { + continue; + } + + /* Check all three filters (AND logic) */ + bool passes = true; + for (int f = 0; f < BLE_OTS_FILTER_INSTANCE_COUNT; f++) { + if (!object_passes_filter(obj, &cs->filter[f], conn_id)) { + passes = false; + break; + } + } + + if (passes && filtered_list->count < CONFIG_BLE_OTS_SERVER_MAX_OBJECTS) { + filtered_list->object_ids[filtered_list->count] = obj->object_id; + filtered_list->count++; + } + } + + /* Sort the result list according to the connection's sort order */ + s_ots_server_filter_changed->sort_order = cs->sort_order; + if (filtered_list->count > 1) { + qsort(filtered_list->object_ids, filtered_list->count, + sizeof(ble_ots_obj_id_t), sort_compare); + } + + return filtered_list; +} + +/***************************************************************************** + * ble_ots_server_dispatch_obj_changed + *****************************************************************************/ +void +ble_ots_server_dispatch_obj_changed(ble_ots_obj_id_t object_id, uint8_t flags, + uint16_t source_conn_id) +{ + /* DLO is excluded from Object Changed indications */ + if (object_id == BLE_OTS_OBJ_ID_DIRECTORY_LISTING) { + return; + } + + /* Build 7-byte indication payload: Flags (1) + Object ID (6, LE) */ + uint8_t payload[7]; + + /* Determine source flag */ + if (source_conn_id != BLE_OTS_CONN_ID_NONE) { + /* Client-originated: set bit 0 = 1 */ + flags |= BLE_OTS_OBJ_CHANGED_FLAG_SOURCE; + } else { + /* Server-originated: clear bit 0 */ + flags &= ~BLE_OTS_OBJ_CHANGED_FLAG_SOURCE; + } + + payload[0] = flags; + payload[1] = (uint8_t)(object_id & 0xFF); + payload[2] = (uint8_t)((object_id >> 8) & 0xFF); + payload[3] = (uint8_t)((object_id >> 16) & 0xFF); + payload[4] = (uint8_t)((object_id >> 24) & 0xFF); + payload[5] = (uint8_t)((object_id >> 32) & 0xFF); + payload[6] = (uint8_t)((object_id >> 40) & 0xFF); + + /* Iterate all connections in the concurrency pool */ + for (int i = 0; i < CONFIG_BLE_OTS_SERVER_MAX_CONCURRENCY; i++) { + ble_ots_server_conn_state_t *cs = &ble_ots_server_env.conn_pool[i]; + if (!cs->in_pool) { + continue; + } + + /* Skip the source client for client-originated changes */ + if (source_conn_id != BLE_OTS_CONN_ID_NONE && + cs->conn_id == source_conn_id) { + continue; + } + + /* Send indication via the helper (which checks CCCD internally) */ + ble_ots_server_indicate_response(cs->conn_id, + BLE_OTS_UUID_OBJECT_CHANGED, + payload, sizeof(payload)); + } + + /* Trigger DLO rebuild for changes that affect the object database */ + ble_ots_server_dlo_rebuild(); +} + +/***************************************************************************** + * ble_ots_server_dlo_rebuild + *****************************************************************************/ + +/** + * @brief Serialize the whole object database into the DLO content buffer. + * + * Caller must hold the OTS server mutex: the object database is read and the + * shared dlo_content buffer is rewritten from scratch, so the rebuild has to + * be atomic with respect to the other task. + */ +static void +dlo_rebuild_locked(void) +{ + if (!ble_ots_server_env.dlo_content) { + ESP_LOGW(TAG, "dlo_rebuild: no DLO content buffer"); + return; + } + + /* Collect all in-use object indices for sorting */ + int obj_indices[CONFIG_BLE_OTS_SERVER_MAX_OBJECTS]; + int obj_count = 0; + + for (int i = 0; i < CONFIG_BLE_OTS_SERVER_MAX_OBJECTS; i++) { + if (ble_ots_server_env.obj_db[i].in_use) { + obj_indices[obj_count++] = i; + } + } + + /* Sort by name ascending (default DLO order) */ + /* Simple insertion sort for small arrays */ + for (int i = 1; i < obj_count; i++) { + int key = obj_indices[i]; + ble_ots_server_obj_t *key_obj = &ble_ots_server_env.obj_db[key]; + int j = i - 1; + while (j >= 0) { + ble_ots_server_obj_t *cmp_obj = &ble_ots_server_env.obj_db[obj_indices[j]]; + int cmp = obj_name_cmp(key_obj, cmp_obj); + if (cmp == 0) { + /* Tie-break on ascending Object ID. Falling back to the obj_db + * slot index would not be deterministic, because slots are + * reused once an object is deleted. */ + cmp = obj_id_cmp(key_obj->object_id, cmp_obj->object_id); + } + if (cmp >= 0) { + break; + } + obj_indices[j + 1] = obj_indices[j]; + j--; + } + obj_indices[j + 1] = key; + } + + /* Serialize all object records into the DLO content buffer */ + uint32_t buf_offset = 0; + uint8_t *buf = ble_ots_server_env.dlo_content; + uint32_t buf_max = BLE_OTS_DLO_MAX_BUF_SIZE; + + for (int idx = 0; idx < obj_count; idx++) { + ble_ots_server_obj_t *obj = &ble_ots_server_env.obj_db[obj_indices[idx]]; + + /* Calculate record flags */ + uint8_t flags = 0; + if (obj->type_uuid_len == 16) { + flags |= BLE_OTS_DLO_FLAG_TYPE_UUID_128; + } + /* Always include current_size and allocated_size */ + flags |= BLE_OTS_DLO_FLAG_CURRENT_SIZE_PRESENT; + flags |= BLE_OTS_DLO_FLAG_ALLOCATED_SIZE_PRESENT; + + /* Include first_created if not zero */ + if (!datetime_is_zero(&obj->first_created)) { + flags |= BLE_OTS_DLO_FLAG_FIRST_CREATED_PRESENT; + } + /* Include last_modified if not zero */ + if (!datetime_is_zero(&obj->last_modified)) { + flags |= BLE_OTS_DLO_FLAG_LAST_MODIFIED_PRESENT; + } + /* Include properties if non-zero */ + if (obj->properties != 0) { + flags |= BLE_OTS_DLO_FLAG_PROPERTIES_PRESENT; + } + + /* Calculate record length (including the 2-byte length field itself) */ + uint16_t record_len = 2; /* Length field */ + record_len += 6; /* Object ID (UINT48) */ + record_len += 1; /* Name length */ + record_len += obj->name_len; /* Name */ + record_len += 1; /* Flags */ + record_len += (obj->type_uuid_len == 16) ? 16 : 2; /* Type UUID */ + + if (flags & BLE_OTS_DLO_FLAG_CURRENT_SIZE_PRESENT) { + record_len += 4; + } + if (flags & BLE_OTS_DLO_FLAG_ALLOCATED_SIZE_PRESENT) { + record_len += 4; + } + if (flags & BLE_OTS_DLO_FLAG_FIRST_CREATED_PRESENT) { + record_len += 7; + } + if (flags & BLE_OTS_DLO_FLAG_LAST_MODIFIED_PRESENT) { + record_len += 7; + } + if (flags & BLE_OTS_DLO_FLAG_PROPERTIES_PRESENT) { + record_len += 4; + } + + /* Check buffer space */ + if (buf_offset + record_len > buf_max) { + ESP_LOGW(TAG, "dlo_rebuild: buffer overflow, truncating"); + break; + } + + /* Write record length (UINT16, LE) */ + buf[buf_offset++] = (uint8_t)(record_len & 0xFF); + buf[buf_offset++] = (uint8_t)((record_len >> 8) & 0xFF); + + /* Write Object ID (UINT48, LE) */ + uint64_t oid = obj->object_id; + buf[buf_offset++] = (uint8_t)(oid & 0xFF); + buf[buf_offset++] = (uint8_t)((oid >> 8) & 0xFF); + buf[buf_offset++] = (uint8_t)((oid >> 16) & 0xFF); + buf[buf_offset++] = (uint8_t)((oid >> 24) & 0xFF); + buf[buf_offset++] = (uint8_t)((oid >> 32) & 0xFF); + buf[buf_offset++] = (uint8_t)((oid >> 40) & 0xFF); + + /* Write Name Length (UINT8) */ + buf[buf_offset++] = obj->name_len; + + /* Write Name (UTF-8) */ + if (obj->name_len > 0) { + memcpy(buf + buf_offset, obj->name, obj->name_len); + buf_offset += obj->name_len; + } + + /* Write Flags */ + buf[buf_offset++] = flags; + + /* Write Object Type UUID */ + if (obj->type_uuid_len == 16) { + memcpy(buf + buf_offset, obj->type_uuid, 16); + buf_offset += 16; + } else { + memcpy(buf + buf_offset, obj->type_uuid, 2); + buf_offset += 2; + } + + /* Write optional fields */ + if (flags & BLE_OTS_DLO_FLAG_CURRENT_SIZE_PRESENT) { + uint32_t val = obj->current_size; + buf[buf_offset++] = (uint8_t)(val & 0xFF); + buf[buf_offset++] = (uint8_t)((val >> 8) & 0xFF); + buf[buf_offset++] = (uint8_t)((val >> 16) & 0xFF); + buf[buf_offset++] = (uint8_t)((val >> 24) & 0xFF); + } + + if (flags & BLE_OTS_DLO_FLAG_ALLOCATED_SIZE_PRESENT) { + uint32_t val = obj->allocated_size; + buf[buf_offset++] = (uint8_t)(val & 0xFF); + buf[buf_offset++] = (uint8_t)((val >> 8) & 0xFF); + buf[buf_offset++] = (uint8_t)((val >> 16) & 0xFF); + buf[buf_offset++] = (uint8_t)((val >> 24) & 0xFF); + } + + if (flags & BLE_OTS_DLO_FLAG_FIRST_CREATED_PRESENT) { + serialize_date_time(&obj->first_created, buf + buf_offset); + buf_offset += 7; + } + + if (flags & BLE_OTS_DLO_FLAG_LAST_MODIFIED_PRESENT) { + serialize_date_time(&obj->last_modified, buf + buf_offset); + buf_offset += 7; + } + + if (flags & BLE_OTS_DLO_FLAG_PROPERTIES_PRESENT) { + uint32_t val = obj->properties; + buf[buf_offset++] = (uint8_t)(val & 0xFF); + buf[buf_offset++] = (uint8_t)((val >> 8) & 0xFF); + buf[buf_offset++] = (uint8_t)((val >> 16) & 0xFF); + buf[buf_offset++] = (uint8_t)((val >> 24) & 0xFF); + } + } + + /* Update DLO content size */ + ble_ots_server_env.dlo_content_size = buf_offset; + + /* Update the DLO object's current_size and allocated_size */ + ble_ots_server_obj_t *dlo = ble_ots_server_obj_db_lookup(BLE_OTS_OBJ_ID_DIRECTORY_LISTING); + if (dlo) { + dlo->current_size = buf_offset; + if (dlo->allocated_size < buf_offset) { + dlo->allocated_size = buf_offset; + } + /* Point DLO data to the content buffer */ + dlo->data = ble_ots_server_env.dlo_content; + } + + ESP_LOGD(TAG, "dlo_rebuild: %lu bytes, %d objects", (unsigned long)buf_offset, obj_count); +} + +void +ble_ots_server_dlo_rebuild(void) +{ + /* A rebuild is triggered from both the application task (add/remove object, + * set object data) and the NimBLE host task (metadata writes, OACP/OLCP), + * and both rewrite the same dlo_content buffer. The mutex is recursive, so + * nesting inside an already-locked GATT access is safe. */ + ble_ots_server_lock(); + dlo_rebuild_locked(); + ble_ots_server_unlock(); +} + +/***************************************************************************** + * ble_ots_server_object_changed (Public API) + *****************************************************************************/ +int +ble_ots_server_object_changed(ble_ots_obj_id_t object_id, uint8_t flags) +{ + if (!BLE_OTS_SERVER_ENV_OK()) { + return BLE_HS_EINVAL; + } + + /* DLO is excluded */ + if (object_id == BLE_OTS_OBJ_ID_DIRECTORY_LISTING) { + return BLE_HS_EINVAL; + } + + /* Force Source of Change bit 0 = 0 (Server) */ + flags &= ~BLE_OTS_OBJ_CHANGED_FLAG_SOURCE; + + /* Dispatch with CONN_ID_NONE to indicate server-originated change */ + ble_ots_server_dispatch_obj_changed(object_id, flags, BLE_OTS_CONN_ID_NONE); + + return 0; +} + +/***************************************************************************** + * ble_ots_server_mark_object + *****************************************************************************/ +int +ble_ots_server_mark_object(uint16_t conn_id, ble_ots_obj_id_t object_id, bool mark) +{ + ble_ots_server_obj_t *obj = ble_ots_server_obj_db_lookup(object_id); + if (!obj) { + return BLE_HS_ENOENT; + } + + /* Resolve conn_id to a stable bond index for the per-object marked bitmask */ + uint8_t bond_idx; + int rc = resolve_bond_index(conn_id, &bond_idx); + if (rc != 0) { + ESP_LOGW(TAG, "mark_object: failed to resolve bond index for conn_id=%d", + conn_id); + return rc; + } + + uint32_t bit = (1UL << bond_idx); + if (mark) { + obj->marked |= bit; + } else { + obj->marked &= ~bit; + } + + return 0; +} + +/***************************************************************************** + * ble_ots_server_mark_cleanup_on_disconnect + *****************************************************************************/ +void +ble_ots_server_mark_cleanup_on_disconnect(const ble_addr_t *peer_id_addr, bool bonded) +{ + if (!s_ots_server_filter_changed || !peer_id_addr) { + return; + } + + /* Bonded peers keep their slot and marked bits so that the marked state + * persists across reconnections; nothing to do here. */ + if (bonded) { + return; + } + + ots_bond_slot_t *bond_slots = s_ots_server_filter_changed->bond_slots; + + /* Find the bond slot for this peer and release it */ + for (int s = 0; s < OTS_MAX_BOND_SLOTS; s++) { + if (!bond_slots[s].in_use) { + continue; + } + if (bond_slots[s].peer_addr.type != peer_id_addr->type || + memcmp(bond_slots[s].peer_addr.val, peer_id_addr->val, 6) != 0) { + continue; + } + + uint32_t bit = (1UL << s); + for (int i = 0; i < CONFIG_BLE_OTS_SERVER_MAX_OBJECTS; i++) { + ble_ots_server_env.obj_db[i].marked &= ~bit; + } + bond_slots[s].in_use = false; + ESP_LOGD(TAG, "Cleared marked state for non-bonded peer slot=%d", s); + break; + } +} diff --git a/components/bt/ble_profiles/nimble/ble_ots/src/server/ots_server_init.c b/components/bt/ble_profiles/nimble/ble_ots/src/server/ots_server_init.c new file mode 100644 index 00000000000..d5ab4993dc1 --- /dev/null +++ b/components/bt/ble_profiles/nimble/ble_ots/src/server/ots_server_init.c @@ -0,0 +1,2034 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include + +#include "esp_log.h" + +#include "nimble/nimble_port.h" +#include "nimble/ble.h" +#include "host/ble_hs.h" +#include "host/ble_gap.h" +#include "host/ble_gatt.h" +#include "host/ble_l2cap.h" +#include "host/ble_uuid.h" +#include "os/os_mbuf.h" + +#include "ble_ots_common.h" +#include "ble_ots_server.h" +#include "ble_ots_server_int.h" + +static const char *TAG = "ots_server_init"; + +/***************************************************************************** + * Global Control Block + * + * Heap-allocated in ble_ots_server_init(), released in ble_ots_server_deinit(). + * NULL while the server is not initialized — see BLE_OTS_SERVER_ENV_OK(). + *****************************************************************************/ +ble_ots_server_cb_env_t *p_ble_ots_server_env; + +/***************************************************************************** + * Mutex helpers — protect object DB and connection pool from concurrent access + * + * The object database, the object content buffers and the connection pool are + * reached from two contexts: the NimBLE host task (GATT/GAP/L2CAP callbacks and + * callouts) and the application task (the public ble_ots_server_* APIs). Every + * accessor of that state must hold this mutex. + * + * The mutex is recursive because the public APIs and the host-task callbacks + * take it at their entry point and then call the internal helpers, which lock + * again. Lock ordering is always OTS mutex -> NimBLE host lock; NimBLE never + * invokes our callbacks with ble_hs_lock() held, so no inversion is possible. + * + * The handle deliberately lives outside the control block and is created only + * once for the lifetime of the module: it has to stay valid while the block + * itself is being torn down in ble_ots_server_deinit(), and destroying it there + * could free a mutex that a host-task callback is still blocked on. + *****************************************************************************/ +static SemaphoreHandle_t s_ots_server_mutex; + +#define OTS_LOCK() do { if (s_ots_server_mutex) xSemaphoreTakeRecursive(s_ots_server_mutex, portMAX_DELAY); } while (0) +#define OTS_UNLOCK() do { if (s_ots_server_mutex) xSemaphoreGiveRecursive(s_ots_server_mutex); } while (0) + +void ble_ots_server_lock(void) +{ + OTS_LOCK(); +} + +void ble_ots_server_unlock(void) +{ + OTS_UNLOCK(); +} + +/***************************************************************************** + * L2CAP OTC MTU + *****************************************************************************/ +#define OTS_L2CAP_COC_MTU 256 + +/***************************************************************************** + * Directory Listing Object name + *****************************************************************************/ +#define BLE_OTS_DLO_NAME "Directory Listing" + +/***************************************************************************** + * Forward Declarations — GATT access callbacks + *****************************************************************************/ +static int ots_gatt_access_cb(uint16_t conn_handle, uint16_t attr_handle, + struct ble_gatt_access_ctxt *ctxt, void *arg); + +/***************************************************************************** + * Forward Declarations — L2CAP event callback + *****************************************************************************/ +static int ots_l2cap_event_cb(struct ble_l2cap_event *event, void *arg); + +/***************************************************************************** + * Forward Declarations — GAP event listener + *****************************************************************************/ +static int ots_gap_event_cb(struct ble_gap_event *event, void *arg); + +/***************************************************************************** + * Forward Declarations — Transfer timer callback + *****************************************************************************/ +static void ots_transfer_timer_cb(struct ble_npl_event *ev); + +/***************************************************************************** + * Forward Declarations — Deferred indication queue + *****************************************************************************/ +static void ots_indicate_queue_flush(ble_ots_server_conn_state_t *cs); + +/***************************************************************************** + * Forward Declarations — External handlers (from other .c files) + *****************************************************************************/ + +/* Metadata handlers — ots_server_metadata.c */ +extern int ble_ots_server_metadata_read(uint16_t conn_handle, uint16_t attr_handle, + struct ble_gatt_access_ctxt *ctxt); +extern int ble_ots_server_metadata_write(uint16_t conn_handle, uint16_t attr_handle, + struct ble_gatt_access_ctxt *ctxt); + +/* OACP handler — ots_server_oacp_ops.c */ +extern int ble_ots_server_oacp_write(uint16_t conn_handle, + struct ble_gatt_access_ctxt *ctxt); + +/* OLCP handler — ots_server_olcp.c */ +extern int ble_ots_server_olcp_write(uint16_t conn_handle, + struct ble_gatt_access_ctxt *ctxt); + +/* Transfer L2CAP data handler — ots_server_oacp_transfer.c */ +extern int ble_ots_server_oacp_l2cap_recv(uint16_t conn_handle, + struct ble_l2cap_chan *chan, + struct os_mbuf *sdu_rx); +extern void ble_ots_server_oacp_transfer_timeout(uint16_t conn_id); +extern void ble_ots_server_otc_disconnected_cb(uint16_t conn_handle, + struct ble_l2cap_chan *chan); +extern void ble_ots_server_otc_tx_unstalled_cb(uint16_t conn_handle, + struct ble_l2cap_chan *chan); + +/***************************************************************************** + * GATT Service Definition — UUIDs + *****************************************************************************/ +static const ble_uuid16_t ots_svc_uuid = BLE_UUID16_INIT(BLE_OTS_UUID_OTS_SERVICE); +static const ble_uuid16_t ots_feature_uuid = BLE_UUID16_INIT(BLE_OTS_UUID_OTS_FEATURE); +static const ble_uuid16_t obj_name_uuid = BLE_UUID16_INIT(BLE_OTS_UUID_OBJECT_NAME); +static const ble_uuid16_t obj_type_uuid = BLE_UUID16_INIT(BLE_OTS_UUID_OBJECT_TYPE); +static const ble_uuid16_t obj_size_uuid = BLE_UUID16_INIT(BLE_OTS_UUID_OBJECT_SIZE); +static const ble_uuid16_t obj_first_created_uuid = BLE_UUID16_INIT(BLE_OTS_UUID_OBJECT_FIRST_CREATED); +static const ble_uuid16_t obj_last_modified_uuid = BLE_UUID16_INIT(BLE_OTS_UUID_OBJECT_LAST_MODIFIED); +static const ble_uuid16_t obj_id_uuid = BLE_UUID16_INIT(BLE_OTS_UUID_OBJECT_ID); +static const ble_uuid16_t obj_properties_uuid = BLE_UUID16_INIT(BLE_OTS_UUID_OBJECT_PROPERTIES); +static const ble_uuid16_t oacp_uuid = BLE_UUID16_INIT(BLE_OTS_UUID_OACP); +static const ble_uuid16_t olcp_uuid = BLE_UUID16_INIT(BLE_OTS_UUID_OLCP); +static const ble_uuid16_t obj_list_filter_uuid = BLE_UUID16_INIT(BLE_OTS_UUID_OBJECT_LIST_FILTER); +static const ble_uuid16_t obj_changed_uuid = BLE_UUID16_INIT(BLE_OTS_UUID_OBJECT_CHANGED); + +/***************************************************************************** + * Dynamic GATT service table + * + * We build the characteristic array dynamically based on config, but use + * a statically-sized array large enough for the worst case: + * 12 characteristics + 1 terminator = 13 entries max + * (OTS Feature, Object Name, Object Type, Object Size, + * Object First-Created, Object Last-Modified, Object ID, + * Object Properties, OACP, OLCP, Object List Filter x3, + * Object Changed) + * Actually max = 15 (3 filter instances counted separately) + 1 terminator = 16 + *****************************************************************************/ +#define OTS_MAX_CHARS 16 + +/***************************************************************************** + * Module-lifetime init context (heap-allocated) + * + * Aggregates the GATT service/characteristic definition tables, the value + * handles we track, and the GAP event listener into a single heap block held + * by s_ots_server_init. + * + * Lifetime: allocated once and then kept for the lifetime of the module. It is + * NOT released by ble_ots_server_deinit() and must not be released once + * ble_gatts_add_svcs() has accepted it: NimBLE stores the caller's svc_defs + * pointer without copying, and every registered attribute keeps a pointer into + * chr_defs[] as its access callback argument. NimBLE exposes no API to + * unregister a service, so freeing this block would leave the host with + * dangling pointers. Only the init error paths that run *before* + * ble_gatts_add_svcs() succeeds may free it — see s_ots_gatt_registered. + *****************************************************************************/ +typedef struct { + /* GAP event listener registered for connect/disconnect/subscribe */ + struct ble_gap_event_listener gap_listener; + + /* Dynamic GATT characteristic table (worst case + terminator) */ + struct ble_gatt_chr_def chr_defs[OTS_MAX_CHARS]; + + /* Service definition table (primary service + terminator) */ + struct ble_gatt_svc_def svc_defs[2]; + + /* Value handles for characteristics we need to track */ + uint16_t ots_feature_val_handle; + uint16_t obj_name_val_handle; + uint16_t obj_type_val_handle; + uint16_t obj_size_val_handle; + uint16_t obj_first_created_val_handle; + uint16_t obj_last_modified_val_handle; + uint16_t obj_id_val_handle; + uint16_t obj_properties_val_handle; + uint16_t oacp_val_handle; + uint16_t olcp_val_handle; + uint16_t obj_changed_val_handle; + uint16_t filter_val_handles[BLE_OTS_FILTER_INSTANCE_COUNT]; +} ots_server_init_ctx_t; + +static ots_server_init_ctx_t *s_ots_server_init; + +/* Set once ble_gatts_add_svcs() has taken ownership of s_ots_server_init-> + * svc_defs. From that point the block is owned by the NimBLE host: it must + * never be freed nor rebuilt, and the service must never be added a second + * time. */ +static bool s_ots_gatt_registered; + +uint16_t ble_ots_server_oacp_handle_get(void) +{ + return s_ots_server_init ? s_ots_server_init->oacp_val_handle : 0; +} + +uint16_t ble_ots_server_olcp_handle_get(void) +{ + return s_ots_server_init ? s_ots_server_init->olcp_val_handle : 0; +} + +/***************************************************************************** + * Helper: conn_state_get — scan concurrency pool by conn_id + *****************************************************************************/ +ble_ots_server_conn_state_t *ble_ots_server_conn_state_get(uint16_t conn_id) +{ + ble_ots_server_conn_state_t *found = NULL; + + OTS_LOCK(); + for (int i = 0; i < CONFIG_BLE_OTS_SERVER_MAX_CONCURRENCY; i++) { + if (ble_ots_server_env.conn_pool[i].in_pool && + ble_ots_server_env.conn_pool[i].conn_id == conn_id) { + found = &ble_ots_server_env.conn_pool[i]; + break; + } + } + OTS_UNLOCK(); + return found; +} + +/***************************************************************************** + * Helper: find a free slot in the concurrency pool + *****************************************************************************/ +static ble_ots_server_conn_state_t *conn_pool_alloc(uint16_t conn_id) +{ + OTS_LOCK(); + for (int i = 0; i < CONFIG_BLE_OTS_SERVER_MAX_CONCURRENCY; i++) { + if (!ble_ots_server_env.conn_pool[i].in_pool) { + ble_ots_server_conn_state_t *cs = &ble_ots_server_env.conn_pool[i]; + memset(cs, 0, sizeof(*cs)); + cs->in_pool = true; + cs->conn_id = conn_id; + cs->current_object_id = BLE_OTS_OBJ_ID_INVALID; + cs->current_obj_valid = false; + cs->otc_chan = NULL; + cs->transfer_active = false; + cs->created_obj_count = 0; + cs->sort_order = BLE_OTS_SORT_ORDER_NAME_ASC; + /* Reset filters to No Filter */ + for (int f = 0; f < BLE_OTS_FILTER_INSTANCE_COUNT; f++) { + cs->filter[f].filter_type = BLE_OTS_FILTER_NO_FILTER; + } + + /* If single-object server, auto-select the only object */ + if (CONFIG_BLE_OTS_SERVER_MAX_OBJECTS == 1) { + for (int j = 0; j < CONFIG_BLE_OTS_SERVER_MAX_OBJECTS; j++) { + if (ble_ots_server_env.obj_db[j].in_use) { + cs->current_object_id = ble_ots_server_env.obj_db[j].object_id; + cs->current_obj_valid = true; + break; + } + } + } + + OTS_UNLOCK(); + return cs; + } + } + OTS_UNLOCK(); + return NULL; +} + +/***************************************************************************** + * Helper: release a concurrency pool slot + *****************************************************************************/ +static void conn_pool_free(ble_ots_server_conn_state_t *cs) +{ + if (cs) { + OTS_LOCK(); + /* Stop and deinit transfer timer if initialized */ + if (cs->transfer_timer_inited) { + ble_npl_callout_stop(&cs->transfer_timer); + ble_npl_callout_deinit(&cs->transfer_timer); + cs->transfer_timer_inited = false; + } + /* Same for the read-transfer back-off callout */ + if (cs->transfer_retry_timer_inited) { + ble_npl_callout_stop(&cs->transfer_retry_timer); + ble_npl_callout_deinit(&cs->transfer_retry_timer); + cs->transfer_retry_timer_inited = false; + } + /* Remove any pending deferred read-data send event. ble_npl_event_init() + * allocates the port-private event object, so it has to be released + * here: the slot is memset() on the next conn_pool_alloc(), which would + * otherwise drop the only pointer to it. */ + if (cs->transfer_data_ev_inited) { + ble_npl_eventq_remove(nimble_port_get_dflt_eventq(), &cs->transfer_data_ev); + ble_npl_event_deinit(&cs->transfer_data_ev); + cs->transfer_data_ev_inited = false; + } + /* Drop indications that will never be sent on this connection */ + ots_indicate_queue_flush(cs); + cs->in_pool = false; + cs->conn_id = BLE_OTS_CONN_ID_NONE; + OTS_UNLOCK(); + } +} + +/***************************************************************************** + * Concurrency Check + * + * A connection normally takes its pool slot on BLE_GAP_EVENT_CONNECT. If the + * pool was full at that moment, admit it lazily here instead of excluding it + * for the rest of the connection: by the time the peer actually uses OTS a slot + * may well have been released by another client. + *****************************************************************************/ +bool ble_ots_server_concurrency_check(uint16_t conn_id) +{ + bool admitted; + + OTS_LOCK(); + admitted = (ble_ots_server_conn_state_get(conn_id) != NULL) || + (conn_pool_alloc(conn_id) != NULL); + OTS_UNLOCK(); + + return admitted; +} + +/***************************************************************************** + * Object Database — Create + * + * Caller must hold the OTS mutex: the free-slot scan and the Object ID search + * below must not be interleaved with another creation, or both would claim the + * same slot and the loser's data buffer would be leaked. + *****************************************************************************/ +static ble_ots_obj_id_t obj_db_create_locked(const ble_ots_obj_type_entry_t *type, + uint32_t size) +{ + /* Find a free slot */ + int slot = -1; + for (int i = 0; i < CONFIG_BLE_OTS_SERVER_MAX_OBJECTS; i++) { + if (!ble_ots_server_env.obj_db[i].in_use) { + slot = i; + break; + } + } + if (slot < 0) { + ESP_LOGE(TAG, "Object database full"); + return 0; + } + + /* Allocate Object ID — always validate uniqueness to prevent collisions */ + ble_ots_obj_id_t new_id = ble_ots_server_env.next_obj_id; + if (new_id > BLE_OTS_OBJ_ID_MAX_USER) { + new_id = BLE_OTS_OBJ_ID_MIN_USER; + } + + { + /* Search for a free (non-colliding) Object ID starting from new_id */ + bool found = false; + ble_ots_obj_id_t candidate = new_id; + ble_ots_obj_id_t start = candidate; + bool wrapped = false; + + while (!wrapped || candidate < start) { + bool in_use = false; + for (int i = 0; i < CONFIG_BLE_OTS_SERVER_MAX_OBJECTS; i++) { + if (ble_ots_server_env.obj_db[i].in_use && + ble_ots_server_env.obj_db[i].object_id == candidate) { + in_use = true; + break; + } + } + if (!in_use) { + new_id = candidate; + found = true; + break; + } + candidate++; + if (candidate > BLE_OTS_OBJ_ID_MAX_USER) { + candidate = BLE_OTS_OBJ_ID_MIN_USER; + wrapped = true; + } + } + if (!found) { + ESP_LOGE(TAG, "No free Object IDs"); + return 0; + } + } + + ble_ots_server_obj_t *obj = &ble_ots_server_env.obj_db[slot]; + memset(obj, 0, sizeof(*obj)); + obj->in_use = true; + obj->object_id = new_id; + obj->name_len = 0; + obj->type_uuid_len = type->uuid_len; + memcpy(obj->type_uuid, type->uuid, type->uuid_len); + obj->current_size = 0; + obj->allocated_size = size; + obj->properties = BLE_OTS_OBJ_PROP_READ | BLE_OTS_OBJ_PROP_WRITE; /* Default: read-write */ + obj->locked_by = BLE_OTS_CONN_ID_NONE; + obj->marked = 0; + memset(&obj->first_created, 0, sizeof(ble_ots_date_time_t)); + memset(&obj->last_modified, 0, sizeof(ble_ots_date_time_t)); + + /* Allocate data buffer if size > 0 */ + if (size > 0) { + obj->data = calloc(1, size); + if (!obj->data) { + ESP_LOGE(TAG, "Failed to allocate object data buffer (%lu bytes)", (unsigned long)size); + obj->in_use = false; + return 0; + } + } else { + obj->data = NULL; + } + + /* Advance next_obj_id */ + ble_ots_server_env.next_obj_id = new_id + 1; + if (ble_ots_server_env.next_obj_id < BLE_OTS_OBJ_ID_MIN_USER) { + ble_ots_server_env.next_obj_id = BLE_OTS_OBJ_ID_MIN_USER; + } + + ESP_LOGI(TAG, "Object created: ID=0x%012llX slot=%d alloc_size=%lu", + (unsigned long long)new_id, slot, (unsigned long)size); + return new_id; +} + +ble_ots_obj_id_t ble_ots_server_obj_db_create(const ble_ots_obj_type_entry_t *type, + uint32_t size) +{ + ble_ots_obj_id_t new_id; + + if (!type) { + return 0; + } + + /* Only 16-bit and 128-bit UUIDs exist. Enforcing it here keeps the copy + * into the fixed 16-octet type_uuid field in bounds, and lets every reader + * of type_uuid_len (Object Type reads, DLO records) trust the field. */ + if (type->uuid_len != 2 && type->uuid_len != 16) { + ESP_LOGE(TAG, "Invalid object type UUID length: %u", type->uuid_len); + return 0; + } + + OTS_LOCK(); + new_id = obj_db_create_locked(type, size); + OTS_UNLOCK(); + + return new_id; +} + +/***************************************************************************** + * Object Database — Delete + *****************************************************************************/ +int ble_ots_server_obj_db_delete(ble_ots_obj_id_t object_id) +{ + uint16_t abort_conn_ids[CONFIG_BLE_OTS_SERVER_MAX_CONCURRENCY]; + int abort_count = 0; + bool found = false; + + /* DLO cannot be deleted */ + if (object_id == BLE_OTS_OBJ_ID_DIRECTORY_LISTING) { + ESP_LOGE(TAG, "Cannot delete Directory Listing Object"); + return BLE_HS_EINVAL; + } + + OTS_LOCK(); + for (int i = 0; i < CONFIG_BLE_OTS_SERVER_MAX_OBJECTS; i++) { + ble_ots_server_obj_t *obj = &ble_ots_server_env.obj_db[i]; + if (!obj->in_use || obj->object_id != object_id) { + continue; + } + + found = true; + free(obj->data); + obj->data = NULL; + obj->in_use = false; + + /* Invalidate the Current Object of every connection pointing at this + * object, and note the connections whose active transfer has to be + * aborted. The abort itself is deferred until the mutex is released: + * it calls into NimBLE and into the application callback, neither of + * which may run under the mutex. */ + for (int c = 0; c < CONFIG_BLE_OTS_SERVER_MAX_CONCURRENCY; c++) { + ble_ots_server_conn_state_t *cs = &ble_ots_server_env.conn_pool[c]; + if (!cs->in_pool) { + continue; + } + if (cs->transfer_active && cs->transfer_object_id == object_id) { + abort_conn_ids[abort_count++] = cs->conn_id; + } + if (cs->current_object_id == object_id) { + cs->current_object_id = BLE_OTS_OBJ_ID_INVALID; + cs->current_obj_valid = false; + } + } + break; + } + OTS_UNLOCK(); + + if (!found) { + ESP_LOGE(TAG, "Object not found for delete: ID=0x%012llX", + (unsigned long long)object_id); + return BLE_HS_ENOENT; + } + + for (int i = 0; i < abort_count; i++) { + ESP_LOGW(TAG, "Aborting active transfer on deleted object " + "ID=0x%012llX conn=%d", + (unsigned long long)object_id, abort_conn_ids[i]); + ble_ots_server_transfer_timer_stop(abort_conn_ids[i]); + ble_ots_server_otc_close(abort_conn_ids[i]); + ble_ots_server_oacp_transfer_timeout(abort_conn_ids[i]); + } + + ESP_LOGI(TAG, "Object deleted: ID=0x%012llX", (unsigned long long)object_id); + return 0; +} + +/***************************************************************************** + * Object Database — Lookup + *****************************************************************************/ +ble_ots_server_obj_t *ble_ots_server_obj_db_lookup(ble_ots_obj_id_t object_id) +{ + for (int i = 0; i < CONFIG_BLE_OTS_SERVER_MAX_OBJECTS; i++) { + if (ble_ots_server_env.obj_db[i].in_use && + ble_ots_server_env.obj_db[i].object_id == object_id) { + return &ble_ots_server_env.obj_db[i]; + } + } + return NULL; +} + +/***************************************************************************** + * Object Data — Read + *****************************************************************************/ +int ble_ots_server_obj_data_read(ble_ots_obj_id_t object_id, uint32_t offset, + uint32_t length, uint8_t *buf) +{ + int rc = 0; + + if (!buf) { + return BLE_HS_EINVAL; + } + + /* The lock must be held across the memcpy, not just around the lookup: + * obj->data can be realloc'd by obj_data_write or freed by obj_db_delete + * from the other task while the copy is in progress. */ + OTS_LOCK(); + + ble_ots_server_obj_t *obj = ble_ots_server_obj_db_lookup(object_id); + if (!obj) { + rc = BLE_HS_ENOENT; + } else if (offset + length > obj->current_size) { + rc = BLE_HS_EINVAL; + } else if (obj->data) { + memcpy(buf, obj->data + offset, length); + } + + OTS_UNLOCK(); + return rc; +} + +/***************************************************************************** + * Object Data — Write + *****************************************************************************/ +int ble_ots_server_obj_data_write(ble_ots_obj_id_t object_id, uint32_t offset, + const uint8_t *data, uint32_t length) +{ + /* DLO is read-only and managed internally; reject external writes */ + if (object_id == BLE_OTS_OBJ_ID_DIRECTORY_LISTING) { + ESP_LOGE(TAG, "Cannot write to Directory Listing Object"); + return BLE_HS_EINVAL; + } + + OTS_LOCK(); + + ble_ots_server_obj_t *obj = ble_ots_server_obj_db_lookup(object_id); + if (!obj) { + OTS_UNLOCK(); + return BLE_HS_ENOENT; + } + if (!data || length == 0) { + OTS_UNLOCK(); + return BLE_HS_EINVAL; + } + + uint32_t end = offset + length; + + /* Expand allocated size if needed */ + if (end > obj->allocated_size) { + uint8_t *new_data = realloc(obj->data, end); + if (!new_data) { + ESP_LOGE(TAG, "Failed to expand object data buffer"); + OTS_UNLOCK(); + return BLE_HS_ENOMEM; + } + /* Zero-fill the gap between old allocated_size and offset if any */ + if (obj->allocated_size < offset) { + memset(new_data + obj->allocated_size, 0, offset - obj->allocated_size); + } + obj->data = new_data; + obj->allocated_size = end; + } + + memcpy(obj->data + offset, data, length); + + /* Update current size */ + if (end > obj->current_size) { + obj->current_size = end; + } + + OTS_UNLOCK(); + return 0; +} + +/***************************************************************************** + * Current Object — Get / Set + *****************************************************************************/ +ble_ots_obj_id_t ble_ots_server_current_obj_get(uint16_t conn_id) +{ + ble_ots_server_conn_state_t *cs = ble_ots_server_conn_state_get(conn_id); + if (!cs) { + return BLE_OTS_OBJ_ID_INVALID; + } + + /* In single-object mode, dynamically auto-select the only object if it + * exists, even if it was created after the connection was established + * (OLCP is not available in single-object mode). */ + if (CONFIG_BLE_OTS_SERVER_MAX_OBJECTS == 1) { + for (int i = 0; i < CONFIG_BLE_OTS_SERVER_MAX_OBJECTS; i++) { + if (ble_ots_server_env.obj_db[i].in_use) { + cs->current_object_id = ble_ots_server_env.obj_db[i].object_id; + cs->current_obj_valid = true; + return cs->current_object_id; + } + } + /* No object exists */ + cs->current_obj_valid = false; + cs->current_object_id = BLE_OTS_OBJ_ID_INVALID; + return BLE_OTS_OBJ_ID_INVALID; + } + + if (!cs->current_obj_valid) { + return BLE_OTS_OBJ_ID_INVALID; + } + return cs->current_object_id; +} + +int ble_ots_server_current_obj_set(uint16_t conn_id, ble_ots_obj_id_t object_id) +{ + ble_ots_server_conn_state_t *cs = ble_ots_server_conn_state_get(conn_id); + if (!cs) { + return BLE_HS_ENOENT; + } + + if (object_id == BLE_OTS_OBJ_ID_INVALID) { + cs->current_object_id = BLE_OTS_OBJ_ID_INVALID; + cs->current_obj_valid = false; + } else { + cs->current_object_id = object_id; + cs->current_obj_valid = true; + } + return 0; +} + +/***************************************************************************** + * Object Locking + *****************************************************************************/ +int ble_ots_server_obj_lock(ble_ots_obj_id_t object_id, uint16_t conn_id) +{ + ble_ots_server_obj_t *obj = ble_ots_server_obj_db_lookup(object_id); + if (!obj) { + return BLE_HS_ENOENT; + } + if (obj->locked_by != BLE_OTS_CONN_ID_NONE && obj->locked_by != conn_id) { + return BLE_HS_EBUSY; + } + obj->locked_by = conn_id; + return 0; +} + +int ble_ots_server_obj_unlock(ble_ots_obj_id_t object_id) +{ + ble_ots_server_obj_t *obj = ble_ots_server_obj_db_lookup(object_id); + if (!obj) { + return BLE_HS_ENOENT; + } + if (obj->locked_by == BLE_OTS_CONN_ID_NONE) { + return BLE_HS_EALREADY; + } + obj->locked_by = BLE_OTS_CONN_ID_NONE; + return 0; +} + +/***************************************************************************** + * OTC Channel — Get / Close + *****************************************************************************/ +struct ble_l2cap_chan *ble_ots_server_otc_get(uint16_t conn_id) +{ + ble_ots_server_conn_state_t *cs = ble_ots_server_conn_state_get(conn_id); + if (!cs) { + return NULL; + } + return cs->otc_chan; +} + +int ble_ots_server_otc_close(uint16_t conn_id) +{ + ble_ots_server_conn_state_t *cs = ble_ots_server_conn_state_get(conn_id); + if (!cs || !cs->otc_chan) { + return BLE_HS_ENOENT; + } + int rc = ble_l2cap_disconnect(cs->otc_chan); + if (rc == 0) { + cs->otc_chan = NULL; + } + return rc; +} + +/***************************************************************************** + * Deferred Indication via NimBLE Event Queue + * + * Indications must not be sent synchronously from within a GATT access + * callback (the ATT Write Response has not yet been transmitted). The + * indication mbuf is built up-front (the caller's data buffer may live on the + * stack) and the actual ble_gatts_indicate_custom() call is deferred to a + * NimBLE host-task event. + * + * ATT permits only one outstanding indication per connection, so the requests + * are additionally queued per connection and released one at a time: the next + * one is posted only once BLE_GAP_EVENT_NOTIFY_TX reports the previous + * indication as completed (confirmed or timed out). Without this, an OACP/OLCP + * response and an Object Changed indication raised close together would both be + * handed to NimBLE inside the same confirmation window. + *****************************************************************************/ +struct ots_indicate_ctx { + struct ble_npl_event ev; + struct ots_indicate_ctx *next; + uint16_t conn_id; + uint16_t val_handle; + struct os_mbuf *om; + bool posted; /* Handed to the host task: it owns the block */ + bool cancelled; /* Connection went away — drop instead of send */ +}; + +static void ots_indicate_event_cb(struct ble_npl_event *ev); + +/** + * @brief Release an indication context and its payload. + */ +static void ots_indicate_ctx_free(struct ots_indicate_ctx *ctx) +{ + ble_npl_event_deinit(&ctx->ev); + os_mbuf_free_chain(ctx->om); + free(ctx); +} + +/** + * @brief Post the head of the connection's indication queue to the host task. + * + * Caller must hold the OTS mutex. Does nothing when the queue is empty or an + * indication is still awaiting its confirmation. + */ +static void ots_indicate_queue_kick(ble_ots_server_conn_state_t *cs) +{ + if (cs->indicate_in_flight || !cs->indicate_head) { + return; + } + + cs->indicate_in_flight = true; + cs->indicate_head->posted = true; + ble_npl_eventq_put(nimble_port_get_dflt_eventq(), &cs->indicate_head->ev); +} + +/** + * @brief Drop every queued indication for a connection. + * + * Caller must hold the OTS mutex. Used when the connection goes away, so the + * queued contexts are never delivered. + */ +static void ots_indicate_queue_flush(ble_ots_server_conn_state_t *cs) +{ + struct ots_indicate_ctx *ctx = cs->indicate_head; + + cs->indicate_head = NULL; + cs->indicate_tail = NULL; + cs->indicate_in_flight = false; + + while (ctx) { + struct ots_indicate_ctx *next = ctx->next; + ctx->next = NULL; + ctx->cancelled = true; + if (!ctx->posted) { + ots_indicate_ctx_free(ctx); + } + ctx = next; + } +} + +/** + * @brief Complete the in-flight indication and start the next queued one. + * + * Called from the GAP NOTIFY_TX handler once the outstanding indication has + * been confirmed by the peer, has timed out, or failed to go out at all. + */ +static void ots_indicate_completed(uint16_t conn_id) +{ + ble_ots_server_conn_state_t *cs; + + OTS_LOCK(); + cs = ble_ots_server_conn_state_get(conn_id); + if (cs) { + cs->indicate_in_flight = false; + ots_indicate_queue_kick(cs); + } + OTS_UNLOCK(); +} + +static void ots_indicate_event_cb(struct ble_npl_event *ev) +{ + struct ots_indicate_ctx *ctx = (struct ots_indicate_ctx *)ble_npl_event_get_arg(ev); + struct os_mbuf *om; + uint16_t conn_id; + uint16_t val_handle; + + if (!ctx) { + return; + } + + OTS_LOCK(); + + if (ctx->cancelled) { + /* The connection was released while this event was in the host queue; + * ots_indicate_queue_flush() left the block for us to reclaim. */ + OTS_UNLOCK(); + ots_indicate_ctx_free(ctx); + return; + } + + /* Unlink before sending: ble_gatts_indicate_custom() reports the outcome + * synchronously through BLE_GAP_EVENT_NOTIFY_TX, and that handler must not + * find this context still queued. */ + ble_ots_server_conn_state_t *cs = BLE_OTS_SERVER_ENV_OK() ? + ble_ots_server_conn_state_get(ctx->conn_id) : NULL; + if (cs && cs->indicate_head == ctx) { + cs->indicate_head = ctx->next; + if (!cs->indicate_head) { + cs->indicate_tail = NULL; + } + } + conn_id = ctx->conn_id; + val_handle = ctx->val_handle; + om = ctx->om; + /* Ownership of the mbuf passes to ble_gatts_indicate_custom() below. */ + ctx->om = NULL; + ots_indicate_ctx_free(ctx); + OTS_UNLOCK(); + + /* ble_gatts_indicate_custom() consumes the mbuf on all return paths, and + * always raises BLE_GAP_EVENT_NOTIFY_TX — the queue is advanced from there, + * both for a failed send and for the eventual confirmation. */ + int rc = ble_gatts_indicate_custom(conn_id, val_handle, om); + if (rc != 0) { + ESP_LOGE(TAG, "indicate_event: ble_gatts_indicate_custom failed rc=%d", rc); + } +} + +/***************************************************************************** + * Indicate Response + *****************************************************************************/ + +/** + * @brief Resolve the value handle to indicate on, checking CCCD subscription. + * + * @param conn_id BLE connection identifier + * @param char_uuid Characteristic UUID (OACP/OLCP/Object Changed) + * @param out_val_handle Receives the characteristic value handle on success + * @return 0 on success, non-zero (BLE_HS_* or ATT error) otherwise + */ +static int ots_indicate_val_handle_get(uint16_t conn_id, uint16_t char_uuid, + uint16_t *out_val_handle) +{ + uint16_t val_handle; + bool subscribed; + + OTS_LOCK(); + + /* Verify CCCD subscription before sending indication */ + ble_ots_server_conn_state_t *cs = ble_ots_server_conn_state_get(conn_id); + if (!cs) { + OTS_UNLOCK(); + return BLE_HS_ENOENT; + } + + switch (char_uuid) { + case BLE_OTS_UUID_OACP: + val_handle = s_ots_server_init->oacp_val_handle; + subscribed = cs->oacp_subscribed; + break; + case BLE_OTS_UUID_OLCP: + val_handle = s_ots_server_init->olcp_val_handle; + subscribed = cs->olcp_indicate_subscribed; + break; + case BLE_OTS_UUID_OBJECT_CHANGED: + val_handle = s_ots_server_init->obj_changed_val_handle; + subscribed = cs->obj_changed_subscribed; + break; + default: + OTS_UNLOCK(); + ESP_LOGE(TAG, "indicate_response: unknown char UUID 0x%04X", char_uuid); + return BLE_HS_EINVAL; + } + + OTS_UNLOCK(); + + if (!subscribed) { + ESP_LOGW(TAG, "indicate_response: CCCD 0x%04X not subscribed for conn=%d", + char_uuid, conn_id); + return ATT_ERR_CCCD_IMPROPERLY_CONFIGURED; + } + if (val_handle == 0) { + ESP_LOGE(TAG, "indicate_response: characteristic not registered"); + return BLE_HS_ENOENT; + } + + *out_val_handle = val_handle; + return 0; +} + +int ble_ots_server_indicate_response(uint16_t conn_id, uint16_t char_uuid, + const uint8_t *data, uint16_t len) +{ + ble_ots_server_conn_state_t *cs; + uint16_t val_handle = 0; + + int rc = ots_indicate_val_handle_get(conn_id, char_uuid, &val_handle); + if (rc != 0) { + return rc; + } + + struct os_mbuf *om = ble_hs_mbuf_from_flat(data, len); + if (!om) { + ESP_LOGE(TAG, "indicate_response: failed to allocate mbuf"); + return BLE_HS_ENOMEM; + } + + /* Defer the actual indication to a NimBLE host-task event so it is not + * sent from within a GATT access callback. */ + struct ots_indicate_ctx *ctx = calloc(1, sizeof(*ctx)); + if (!ctx) { + os_mbuf_free_chain(om); + ESP_LOGE(TAG, "indicate_response: failed to allocate event context"); + return BLE_HS_ENOMEM; + } + ctx->conn_id = conn_id; + ctx->val_handle = val_handle; + ctx->om = om; + ctx->next = NULL; + ble_npl_event_init(&ctx->ev, ots_indicate_event_cb, ctx); + + /* Append to the per-connection queue. It is only posted to the host task + * once the previously sent indication has been acknowledged. */ + OTS_LOCK(); + cs = ble_ots_server_conn_state_get(conn_id); + if (!cs) { + ots_indicate_ctx_free(ctx); + OTS_UNLOCK(); + return BLE_HS_ENOENT; + } + if (cs->indicate_tail) { + cs->indicate_tail->next = ctx; + } else { + cs->indicate_head = ctx; + } + cs->indicate_tail = ctx; + ots_indicate_queue_kick(cs); + OTS_UNLOCK(); + + return 0; +} + +/***************************************************************************** + * Dispatch Event + *****************************************************************************/ +void ble_ots_server_dispatch_event(ble_ots_server_event_t event, + ble_ots_server_cb_param_t *param) +{ + if (ble_ots_server_env.app_cb) { + ble_ots_server_env.app_cb(event, param); + } +} + +/***************************************************************************** + * Transfer Timer Management + *****************************************************************************/ +static void ots_transfer_timer_cb(struct ble_npl_event *ev) +{ + uint16_t conn_id; + + /* The callout fires in the NimBLE host task context, so the pool slot it + * points at can be freed concurrently by the application task. Resolve the + * connection under the mutex, then run the cleanup without it — that path + * reaches the application callback. */ + OTS_LOCK(); + ble_ots_server_conn_state_t *cs = BLE_OTS_SERVER_ENV_OK() ? + (ble_ots_server_conn_state_t *)ble_npl_event_get_arg(ev) : NULL; + if (!cs || !cs->in_pool || !cs->transfer_active) { + OTS_UNLOCK(); + return; + } + conn_id = cs->conn_id; + OTS_UNLOCK(); + + ESP_LOGW(TAG, "Transfer timeout for conn_id=%d", conn_id); + + /* Delegate to transfer module for proper cleanup */ + ble_ots_server_oacp_transfer_timeout(conn_id); +} + +void ble_ots_server_transfer_timer_start(uint16_t conn_id) +{ + ble_ots_server_conn_state_t *cs = ble_ots_server_conn_state_get(conn_id); + if (!cs) { + return; + } + + if (!cs->transfer_timer_inited) { + int rc = ble_npl_callout_init(&cs->transfer_timer, + nimble_port_get_dflt_eventq(), + ots_transfer_timer_cb, cs); + if (rc != 0) { + ESP_LOGE(TAG, "Failed to init transfer callout: %d", rc); + return; + } + cs->transfer_timer_inited = true; + } + + uint32_t timeout_ms = (uint32_t)CONFIG_BLE_OTS_SERVER_TRANSFER_TIMEOUT_SEC * 1000U; + ble_npl_callout_reset(&cs->transfer_timer, + ble_npl_time_ms_to_ticks32(timeout_ms)); +} + +void ble_ots_server_transfer_timer_stop(uint16_t conn_id) +{ + ble_ots_server_conn_state_t *cs = ble_ots_server_conn_state_get(conn_id); + if (!cs || !cs->transfer_timer_inited) { + return; + } + ble_npl_callout_stop(&cs->transfer_timer); +} + +void ble_ots_server_transfer_timer_reset(uint16_t conn_id) +{ + ble_ots_server_transfer_timer_stop(conn_id); + ble_ots_server_transfer_timer_start(conn_id); +} + +/***************************************************************************** + * Name Uniqueness Check + *****************************************************************************/ +bool ble_ots_server_name_is_unique(const char *name, uint8_t name_len, + ble_ots_obj_id_t exclude_id) +{ + bool unique = true; + + if (!name || name_len == 0) { + /* Empty names are not checked for uniqueness */ + return true; + } + + OTS_LOCK(); + for (int i = 0; i < CONFIG_BLE_OTS_SERVER_MAX_OBJECTS; i++) { + ble_ots_server_obj_t *obj = &ble_ots_server_env.obj_db[i]; + if (!obj->in_use) { + continue; + } + if (obj->object_id == exclude_id) { + continue; + } + if (obj->name_len == name_len && + memcmp(obj->name, name, name_len) == 0) { + unique = false; + break; + } + } + OTS_UNLOCK(); + + return unique; +} + +/***************************************************************************** + * GATT Access Callback — routes to appropriate handlers + *****************************************************************************/ +static int ots_gatt_access_handle(uint16_t conn_handle, uint16_t attr_handle, + struct ble_gatt_access_ctxt *ctxt) +{ + const ble_uuid_t *uuid; + uint16_t uuid16; + + /* Check concurrency */ + if (!ble_ots_server_concurrency_check(conn_handle)) { + return BLE_OTS_APP_ERR_CONCURRENCY_LIMIT_EXCEEDED; + } + + switch (ctxt->op) { + case BLE_GATT_ACCESS_OP_READ_CHR: + uuid = ctxt->chr->uuid; + if (uuid->type != BLE_UUID_TYPE_16) { + return BLE_ATT_ERR_UNLIKELY; + } + uuid16 = BLE_UUID16(uuid)->value; + + switch (uuid16) { + case BLE_OTS_UUID_OTS_FEATURE: { + /* OTS Feature: 8 octets (OACP features + OLCP features) */ + uint8_t buf[8]; + buf[0] = (uint8_t)(ble_ots_server_env.ots_feature.oacp_features); + buf[1] = (uint8_t)(ble_ots_server_env.ots_feature.oacp_features >> 8); + buf[2] = (uint8_t)(ble_ots_server_env.ots_feature.oacp_features >> 16); + buf[3] = (uint8_t)(ble_ots_server_env.ots_feature.oacp_features >> 24); + buf[4] = (uint8_t)(ble_ots_server_env.ots_feature.olcp_features); + buf[5] = (uint8_t)(ble_ots_server_env.ots_feature.olcp_features >> 8); + buf[6] = (uint8_t)(ble_ots_server_env.ots_feature.olcp_features >> 16); + buf[7] = (uint8_t)(ble_ots_server_env.ots_feature.olcp_features >> 24); + int rc = os_mbuf_append(ctxt->om, buf, sizeof(buf)); + return rc == 0 ? 0 : BLE_ATT_ERR_INSUFFICIENT_RES; + } + + case BLE_OTS_UUID_OBJECT_NAME: + case BLE_OTS_UUID_OBJECT_TYPE: + case BLE_OTS_UUID_OBJECT_SIZE: + case BLE_OTS_UUID_OBJECT_FIRST_CREATED: + case BLE_OTS_UUID_OBJECT_LAST_MODIFIED: + case BLE_OTS_UUID_OBJECT_ID: + case BLE_OTS_UUID_OBJECT_PROPERTIES: + return ble_ots_server_metadata_read(conn_handle, attr_handle, ctxt); + + case BLE_OTS_UUID_OBJECT_LIST_FILTER: { + /* Determine which filter instance by matching val_handle */ + int instance_idx = -1; + for (int i = 0; i < BLE_OTS_FILTER_INSTANCE_COUNT; i++) { + if (attr_handle == s_ots_server_init->filter_val_handles[i]) { + instance_idx = i; + break; + } + } + if (instance_idx < 0) { + return BLE_ATT_ERR_UNLIKELY; + } + uint8_t buf[BLE_OTS_FILTER_VALUE_MAX_LEN]; + uint16_t buf_len = sizeof(buf); + int rc = ble_ots_server_filter_read(conn_handle, (uint8_t)instance_idx, + buf, &buf_len); + if (rc != 0) { + return rc; + } + rc = os_mbuf_append(ctxt->om, buf, buf_len); + return rc == 0 ? 0 : BLE_ATT_ERR_INSUFFICIENT_RES; + } + + default: + return BLE_ATT_ERR_UNLIKELY; + } + break; + + case BLE_GATT_ACCESS_OP_WRITE_CHR: + uuid = ctxt->chr->uuid; + if (uuid->type != BLE_UUID_TYPE_16) { + return BLE_ATT_ERR_UNLIKELY; + } + uuid16 = BLE_UUID16(uuid)->value; + + switch (uuid16) { + case BLE_OTS_UUID_OBJECT_NAME: + case BLE_OTS_UUID_OBJECT_FIRST_CREATED: + case BLE_OTS_UUID_OBJECT_LAST_MODIFIED: + case BLE_OTS_UUID_OBJECT_PROPERTIES: + return ble_ots_server_metadata_write(conn_handle, attr_handle, ctxt); + + case BLE_OTS_UUID_OACP: + return ble_ots_server_oacp_write(conn_handle, ctxt); + + case BLE_OTS_UUID_OLCP: + return ble_ots_server_olcp_write(conn_handle, ctxt); + + case BLE_OTS_UUID_OBJECT_LIST_FILTER: { + int instance_idx = -1; + for (int i = 0; i < BLE_OTS_FILTER_INSTANCE_COUNT; i++) { + if (attr_handle == s_ots_server_init->filter_val_handles[i]) { + instance_idx = i; + break; + } + } + if (instance_idx < 0) { + return BLE_ATT_ERR_UNLIKELY; + } + /* Reject an oversized write up front */ + uint8_t write_buf[BLE_OTS_FILTER_VALUE_MAX_LEN]; + uint16_t write_len = 0; + if (OS_MBUF_PKTLEN(ctxt->om) > sizeof(write_buf)) { + return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN; + } + int rc = ble_hs_mbuf_to_flat(ctxt->om, write_buf, sizeof(write_buf), &write_len); + if (rc != 0) { + return BLE_ATT_ERR_UNLIKELY; + } + return ble_ots_server_filter_write(conn_handle, (uint8_t)instance_idx, + write_buf, write_len); + } + + default: + return BLE_ATT_ERR_UNLIKELY; + } + break; + + default: + return BLE_ATT_ERR_UNLIKELY; + } + + return BLE_ATT_ERR_UNLIKELY; +} + +static int ots_gatt_access_cb(uint16_t conn_handle, uint16_t attr_handle, + struct ble_gatt_access_ctxt *ctxt, void *arg) +{ + int rc; + + /* NimBLE offers no way to unregister a GATT service, so this callback stays + * dispatchable after ble_ots_server_deinit(). Re-validate the control block + * under the mutex that deinit holds while releasing it — checking it before + * taking the lock would only be a TOCTOU window. */ + OTS_LOCK(); + if (!BLE_OTS_SERVER_ENV_OK() || !ble_ots_server_env.started) { + OTS_UNLOCK(); + return BLE_ATT_ERR_UNLIKELY; + } + rc = ots_gatt_access_handle(conn_handle, attr_handle, ctxt); + OTS_UNLOCK(); + + return rc; +} + +/***************************************************************************** + * Build GATT Service Table + *****************************************************************************/ +static int ots_build_gatt_service(const ble_ots_server_config_t *config) +{ + int idx = 0; + bool multi_object = (CONFIG_BLE_OTS_SERVER_MAX_OBJECTS > 1); + + /* 1. OTS Feature — Mandatory, Read, Encryption Required */ + s_ots_server_init->chr_defs[idx].uuid = &ots_feature_uuid.u; + s_ots_server_init->chr_defs[idx].access_cb = ots_gatt_access_cb; + s_ots_server_init->chr_defs[idx].arg = NULL; + s_ots_server_init->chr_defs[idx].descriptors = NULL; + s_ots_server_init->chr_defs[idx].flags = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_READ_ENC; + s_ots_server_init->chr_defs[idx].min_key_size = 0; + s_ots_server_init->chr_defs[idx].val_handle = &s_ots_server_init->ots_feature_val_handle; + s_ots_server_init->chr_defs[idx].cpfd = NULL; + idx++; + + /* 2. Object Name — Mandatory, Read + optional Write, Encryption Required */ + s_ots_server_init->chr_defs[idx].uuid = &obj_name_uuid.u; + s_ots_server_init->chr_defs[idx].access_cb = ots_gatt_access_cb; + s_ots_server_init->chr_defs[idx].arg = NULL; + s_ots_server_init->chr_defs[idx].descriptors = NULL; + s_ots_server_init->chr_defs[idx].flags = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_READ_ENC; + if (config->obj_name_writable) { + s_ots_server_init->chr_defs[idx].flags |= BLE_GATT_CHR_F_WRITE | BLE_GATT_CHR_F_WRITE_ENC; + } + s_ots_server_init->chr_defs[idx].min_key_size = 0; + s_ots_server_init->chr_defs[idx].val_handle = &s_ots_server_init->obj_name_val_handle; + s_ots_server_init->chr_defs[idx].cpfd = NULL; + idx++; + + /* 3. Object Type — Mandatory, Read, Encryption Required */ + s_ots_server_init->chr_defs[idx].uuid = &obj_type_uuid.u; + s_ots_server_init->chr_defs[idx].access_cb = ots_gatt_access_cb; + s_ots_server_init->chr_defs[idx].arg = NULL; + s_ots_server_init->chr_defs[idx].descriptors = NULL; + s_ots_server_init->chr_defs[idx].flags = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_READ_ENC; + s_ots_server_init->chr_defs[idx].min_key_size = 0; + s_ots_server_init->chr_defs[idx].val_handle = &s_ots_server_init->obj_type_val_handle; + s_ots_server_init->chr_defs[idx].cpfd = NULL; + idx++; + + /* 4. Object Size — Mandatory, Read, Encryption Required */ + s_ots_server_init->chr_defs[idx].uuid = &obj_size_uuid.u; + s_ots_server_init->chr_defs[idx].access_cb = ots_gatt_access_cb; + s_ots_server_init->chr_defs[idx].arg = NULL; + s_ots_server_init->chr_defs[idx].descriptors = NULL; + s_ots_server_init->chr_defs[idx].flags = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_READ_ENC; + s_ots_server_init->chr_defs[idx].min_key_size = 0; + s_ots_server_init->chr_defs[idx].val_handle = &s_ots_server_init->obj_size_val_handle; + s_ots_server_init->chr_defs[idx].cpfd = NULL; + idx++; + + /* 5. Object First-Created — Optional */ + if (config->include_obj_first_created) { + s_ots_server_init->chr_defs[idx].uuid = &obj_first_created_uuid.u; + s_ots_server_init->chr_defs[idx].access_cb = ots_gatt_access_cb; + s_ots_server_init->chr_defs[idx].arg = NULL; + s_ots_server_init->chr_defs[idx].descriptors = NULL; + s_ots_server_init->chr_defs[idx].flags = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_READ_ENC; + /* Write is optional (C.3: mandatory if OACP Create supported) */ + if (config->oacp_features & BLE_OTS_OACP_FEAT_CREATE) { + s_ots_server_init->chr_defs[idx].flags |= BLE_GATT_CHR_F_WRITE | BLE_GATT_CHR_F_WRITE_ENC; + } + s_ots_server_init->chr_defs[idx].min_key_size = 0; + s_ots_server_init->chr_defs[idx].val_handle = &s_ots_server_init->obj_first_created_val_handle; + s_ots_server_init->chr_defs[idx].cpfd = NULL; + idx++; + } + + /* 6. Object Last-Modified — Optional */ + if (config->include_obj_last_modified) { + s_ots_server_init->chr_defs[idx].uuid = &obj_last_modified_uuid.u; + s_ots_server_init->chr_defs[idx].access_cb = ots_gatt_access_cb; + s_ots_server_init->chr_defs[idx].arg = NULL; + s_ots_server_init->chr_defs[idx].descriptors = NULL; + s_ots_server_init->chr_defs[idx].flags = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_READ_ENC; + /* C.4: Write mandatory if no real-time clock */ + if (!config->has_realtime_clock) { + s_ots_server_init->chr_defs[idx].flags |= BLE_GATT_CHR_F_WRITE | BLE_GATT_CHR_F_WRITE_ENC; + } + s_ots_server_init->chr_defs[idx].min_key_size = 0; + s_ots_server_init->chr_defs[idx].val_handle = &s_ots_server_init->obj_last_modified_val_handle; + s_ots_server_init->chr_defs[idx].cpfd = NULL; + idx++; + } + + /* 7. Object ID — C.1: Mandatory if multi-object */ + if (multi_object) { + s_ots_server_init->chr_defs[idx].uuid = &obj_id_uuid.u; + s_ots_server_init->chr_defs[idx].access_cb = ots_gatt_access_cb; + s_ots_server_init->chr_defs[idx].arg = NULL; + s_ots_server_init->chr_defs[idx].descriptors = NULL; + s_ots_server_init->chr_defs[idx].flags = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_READ_ENC; + s_ots_server_init->chr_defs[idx].min_key_size = 0; + s_ots_server_init->chr_defs[idx].val_handle = &s_ots_server_init->obj_id_val_handle; + s_ots_server_init->chr_defs[idx].cpfd = NULL; + idx++; + } + + /* 8. Object Properties — Mandatory, Read + optional Write, Encryption Required */ + s_ots_server_init->chr_defs[idx].uuid = &obj_properties_uuid.u; + s_ots_server_init->chr_defs[idx].access_cb = ots_gatt_access_cb; + s_ots_server_init->chr_defs[idx].arg = NULL; + s_ots_server_init->chr_defs[idx].descriptors = NULL; + s_ots_server_init->chr_defs[idx].flags = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_READ_ENC; + if (config->obj_properties_writable) { + s_ots_server_init->chr_defs[idx].flags |= BLE_GATT_CHR_F_WRITE | BLE_GATT_CHR_F_WRITE_ENC; + } + s_ots_server_init->chr_defs[idx].min_key_size = 0; + s_ots_server_init->chr_defs[idx].val_handle = &s_ots_server_init->obj_properties_val_handle; + s_ots_server_init->chr_defs[idx].cpfd = NULL; + idx++; + + /* 9. OACP — Mandatory, Write + Indicate, Encryption Required, CCCD auto-added */ + s_ots_server_init->chr_defs[idx].uuid = &oacp_uuid.u; + s_ots_server_init->chr_defs[idx].access_cb = ots_gatt_access_cb; + s_ots_server_init->chr_defs[idx].arg = NULL; + s_ots_server_init->chr_defs[idx].descriptors = NULL; + s_ots_server_init->chr_defs[idx].flags = BLE_GATT_CHR_F_WRITE | BLE_GATT_CHR_F_WRITE_ENC | + BLE_GATT_CHR_F_INDICATE; + s_ots_server_init->chr_defs[idx].min_key_size = 0; + s_ots_server_init->chr_defs[idx].val_handle = &s_ots_server_init->oacp_val_handle; + s_ots_server_init->chr_defs[idx].cpfd = NULL; + idx++; + + /* 10. OLCP — C.1: Mandatory if multi-object, Write + Indicate, Encryption Required */ + if (multi_object) { + s_ots_server_init->chr_defs[idx].uuid = &olcp_uuid.u; + s_ots_server_init->chr_defs[idx].access_cb = ots_gatt_access_cb; + s_ots_server_init->chr_defs[idx].arg = NULL; + s_ots_server_init->chr_defs[idx].descriptors = NULL; + s_ots_server_init->chr_defs[idx].flags = BLE_GATT_CHR_F_WRITE | BLE_GATT_CHR_F_WRITE_ENC | + BLE_GATT_CHR_F_INDICATE; + s_ots_server_init->chr_defs[idx].min_key_size = 0; + s_ots_server_init->chr_defs[idx].val_handle = &s_ots_server_init->olcp_val_handle; + s_ots_server_init->chr_defs[idx].cpfd = NULL; + idx++; + } + + /* 11. Object List Filter (×3) — C.2: Optional if multi-object */ + if (multi_object && config->include_obj_list_filter) { + for (int f = 0; f < BLE_OTS_FILTER_INSTANCE_COUNT; f++) { + s_ots_server_init->chr_defs[idx].uuid = &obj_list_filter_uuid.u; + s_ots_server_init->chr_defs[idx].access_cb = ots_gatt_access_cb; + s_ots_server_init->chr_defs[idx].arg = NULL; + s_ots_server_init->chr_defs[idx].descriptors = NULL; + s_ots_server_init->chr_defs[idx].flags = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_READ_ENC | + BLE_GATT_CHR_F_WRITE | BLE_GATT_CHR_F_WRITE_ENC; + s_ots_server_init->chr_defs[idx].min_key_size = 0; + s_ots_server_init->chr_defs[idx].val_handle = &s_ots_server_init->filter_val_handles[f]; + s_ots_server_init->chr_defs[idx].cpfd = NULL; + idx++; + } + } + + /* 12. Object Changed — Optional, Indicate, Encryption Required */ + if (config->include_obj_changed) { + s_ots_server_init->chr_defs[idx].uuid = &obj_changed_uuid.u; + s_ots_server_init->chr_defs[idx].access_cb = ots_gatt_access_cb; + s_ots_server_init->chr_defs[idx].arg = NULL; + s_ots_server_init->chr_defs[idx].descriptors = NULL; + s_ots_server_init->chr_defs[idx].flags = BLE_GATT_CHR_F_INDICATE; + s_ots_server_init->chr_defs[idx].min_key_size = 0; + s_ots_server_init->chr_defs[idx].val_handle = &s_ots_server_init->obj_changed_val_handle; + s_ots_server_init->chr_defs[idx].cpfd = NULL; + idx++; + } + + /* Terminator */ + memset(&s_ots_server_init->chr_defs[idx], 0, sizeof(struct ble_gatt_chr_def)); + + /* Build service definition */ + s_ots_server_init->svc_defs[0].type = BLE_GATT_SVC_TYPE_PRIMARY; + s_ots_server_init->svc_defs[0].uuid = &ots_svc_uuid.u; + s_ots_server_init->svc_defs[0].includes = NULL; + s_ots_server_init->svc_defs[0].characteristics = s_ots_server_init->chr_defs; + + /* Terminator */ + memset(&s_ots_server_init->svc_defs[1], 0, sizeof(struct ble_gatt_svc_def)); + + return 0; +} + +/***************************************************************************** + * Create Directory Listing Object + *****************************************************************************/ +static int ots_create_dlo(void) +{ + /* Find a free slot for the DLO */ + int slot = -1; + for (int i = 0; i < CONFIG_BLE_OTS_SERVER_MAX_OBJECTS; i++) { + if (!ble_ots_server_env.obj_db[i].in_use) { + slot = i; + break; + } + } + if (slot < 0) { + ESP_LOGE(TAG, "No slot for DLO"); + return BLE_HS_ENOMEM; + } + + ble_ots_server_obj_t *dlo = &ble_ots_server_env.obj_db[slot]; + memset(dlo, 0, sizeof(*dlo)); + dlo->in_use = true; + dlo->object_id = BLE_OTS_OBJ_ID_DIRECTORY_LISTING; + dlo->name_len = (uint8_t)(sizeof(BLE_OTS_DLO_NAME) - 1); + memcpy(dlo->name, BLE_OTS_DLO_NAME, dlo->name_len); + dlo->type_uuid_len = 2; + dlo->type_uuid[0] = (uint8_t)(BLE_OTS_UUID_DIRECTORY_LISTING & 0xFF); + dlo->type_uuid[1] = (uint8_t)((BLE_OTS_UUID_DIRECTORY_LISTING >> 8) & 0xFF); + dlo->properties = BLE_OTS_OBJ_PROP_READ; /* Read-only */ + dlo->locked_by = BLE_OTS_CONN_ID_NONE; + dlo->marked = 0; + + /* Allocate DLO content buffer */ + ble_ots_server_env.dlo_content = calloc(1, BLE_OTS_DLO_MAX_BUF_SIZE); + if (!ble_ots_server_env.dlo_content) { + ESP_LOGE(TAG, "Failed to allocate DLO content buffer"); + dlo->in_use = false; + return BLE_HS_ENOMEM; + } + ble_ots_server_env.dlo_content_size = 0; + + /* Point DLO data to the shared buffer */ + dlo->data = ble_ots_server_env.dlo_content; + dlo->current_size = 0; + dlo->allocated_size = BLE_OTS_DLO_MAX_BUF_SIZE; + + ESP_LOGI(TAG, "Directory Listing Object created (ID=0x000000000000)"); + return 0; +} + +/***************************************************************************** + * L2CAP OTC Event Callback + *****************************************************************************/ +static int ots_l2cap_event_handle(struct ble_l2cap_event *event) +{ + switch (event->type) { + case BLE_L2CAP_EVENT_COC_ACCEPT: { + uint16_t conn_handle = event->accept.conn_handle; + ESP_LOGI(TAG, "L2CAP COC accept: conn_handle=%d", conn_handle); + + /* Reject new L2CAP connections if server is not started */ + if (!ble_ots_server_env.started) { + ESP_LOGW(TAG, "L2CAP accept: server not started, rejecting"); + return BLE_HS_ENOTSUP; + } + + ble_ots_server_conn_state_t *cs = ble_ots_server_conn_state_get(conn_handle); + if (!cs) { + ESP_LOGW(TAG, "L2CAP accept: connection not in pool, rejecting"); + return BLE_HS_ENOMEM; + } + + /* Provide an SDU receive buffer */ + struct os_mbuf *sdu_rx = ble_hs_mbuf_from_flat(NULL, 0); + if (!sdu_rx) { + ESP_LOGE(TAG, "L2CAP accept: failed to allocate SDU rx buffer"); + return BLE_HS_ENOMEM; + } + ble_l2cap_recv_ready(event->accept.chan, sdu_rx); + return 0; + } + + case BLE_L2CAP_EVENT_COC_CONNECTED: { + uint16_t conn_handle = event->connect.conn_handle; + if (event->connect.status != 0) { + ESP_LOGW(TAG, "L2CAP COC connect failed: conn_handle=%d status=%d", + conn_handle, event->connect.status); + return 0; + } + ESP_LOGI(TAG, "L2CAP COC connected: conn_handle=%d", conn_handle); + + ble_ots_server_conn_state_t *cs = ble_ots_server_conn_state_get(conn_handle); + if (cs) { + cs->otc_chan = event->connect.chan; + } + return 0; + } + + case BLE_L2CAP_EVENT_COC_DISCONNECTED: { + uint16_t conn_handle = event->disconnect.conn_handle; + ESP_LOGI(TAG, "L2CAP COC disconnected: conn_handle=%d", conn_handle); + + ble_ots_server_otc_disconnected_cb(conn_handle, event->disconnect.chan); + return 0; + } + + case BLE_L2CAP_EVENT_COC_DATA_RECEIVED: { + uint16_t conn_handle = event->receive.conn_handle; + ESP_LOGD(TAG, "L2CAP COC data received: conn_handle=%d", conn_handle); + + ble_ots_server_conn_state_t *cs = ble_ots_server_conn_state_get(conn_handle); + if (!cs) { + /* Free the received SDU to avoid memory leak */ + os_mbuf_free_chain(event->receive.sdu_rx); + return 0; + } + + int rc = ble_ots_server_oacp_l2cap_recv(conn_handle, event->receive.chan, + event->receive.sdu_rx); + + /* Provide a new SDU rx buffer for next receive */ + struct os_mbuf *sdu_rx = ble_hs_mbuf_from_flat(NULL, 0); + if (sdu_rx) { + ble_l2cap_recv_ready(event->receive.chan, sdu_rx); + } + return rc; + } + + case BLE_L2CAP_EVENT_COC_TX_UNSTALLED: { + uint16_t conn_handle = event->tx_unstalled.conn_handle; + ESP_LOGD(TAG, "L2CAP COC TX un-stalled: conn_handle=%d status=%d", + conn_handle, event->tx_unstalled.status); + + ble_ots_server_otc_tx_unstalled_cb(conn_handle, event->tx_unstalled.chan); + return 0; + } + + default: + return 0; + } +} + +static int ots_l2cap_event_cb(struct ble_l2cap_event *event, void *arg) +{ + int rc; + + /* NimBLE offers no way to unregister an L2CAP server, so this callback + * stays dispatchable after ble_ots_server_deinit(); re-validate the control + * block under the mutex deinit holds while releasing it. */ + OTS_LOCK(); + if (!BLE_OTS_SERVER_ENV_OK()) { + OTS_UNLOCK(); + return BLE_HS_ENOTSUP; + } + rc = ots_l2cap_event_handle(event); + OTS_UNLOCK(); + + return rc; +} + +/***************************************************************************** + * Post-Disconnection Cleanup + *****************************************************************************/ +static void ots_disconnect_cleanup(uint16_t conn_handle) +{ + ble_ots_server_conn_state_t *cs = ble_ots_server_conn_state_get(conn_handle); + if (!cs) { + return; + } + + ESP_LOGI(TAG, "Disconnect cleanup for conn_handle=%d", conn_handle); + + /* Step 1: Delete unnamed objects created during this session */ + for (int i = 0; i < cs->created_obj_count; i++) { + ble_ots_obj_id_t obj_id = cs->created_obj_ids[i]; + ble_ots_server_obj_t *obj = ble_ots_server_obj_db_lookup(obj_id); + if (obj && obj->name_len == 0) { + ESP_LOGI(TAG, "Deleting unnamed object ID=0x%012llX", + (unsigned long long)obj_id); + ble_ots_server_obj_db_delete(obj_id); + } + } + + /* Step 2: Unlock objects locked by this connection */ + for (int i = 0; i < CONFIG_BLE_OTS_SERVER_MAX_OBJECTS; i++) { + ble_ots_server_obj_t *obj = &ble_ots_server_env.obj_db[i]; + if (obj->in_use && obj->locked_by == conn_handle) { + obj->locked_by = BLE_OTS_CONN_ID_NONE; + ESP_LOGI(TAG, "Unlocked object ID=0x%012llX on disconnect", + (unsigned long long)obj->object_id); + } + } + + /* Step 3: Filter and current object cache discard (handled by pool free) */ + ble_ots_server_filter_reset(conn_handle); + + /* Step 4: Abort active transfer and notify the application */ + if (cs->transfer_active) { + ble_ots_server_transfer_timer_stop(conn_handle); + /* Notify the application of the aborted transfer before clearing state */ + ble_ots_server_oacp_transfer_timeout(conn_handle); + } + + /* Step 5: Close OTC if still open */ + if (cs->otc_chan) { + cs->otc_chan = NULL; /* Channel is implicitly released on disconnect */ + } + + /* Release the pool slot */ + conn_pool_free(cs); + + /* Rebuild DLO if multi-object (objects may have been deleted) */ + if (CONFIG_BLE_OTS_SERVER_MAX_OBJECTS > 1) { + ble_ots_server_dlo_rebuild(); + } +} + +/***************************************************************************** + * GAP Event Callback + *****************************************************************************/ +static int ots_gap_event_handle(struct ble_gap_event *event) +{ + switch (event->type) { + case BLE_GAP_EVENT_CONNECT: + if (event->connect.status == 0) { + uint16_t conn_handle = event->connect.conn_handle; + ESP_LOGI(TAG, "GAP connect: conn_handle=%d", conn_handle); + + if (!ble_ots_server_env.started) { + ESP_LOGW(TAG, "GAP connect: server not started, ignoring conn_handle=%d", + conn_handle); + return 0; + } + + /* Try to admit to concurrency pool */ + ble_ots_server_conn_state_t *cs = conn_pool_alloc(conn_handle); + if (!cs) { + ESP_LOGW(TAG, "Concurrency pool full, conn_handle=%d excluded", + conn_handle); + } + } + return 0; + + case BLE_GAP_EVENT_DISCONNECT: { + uint16_t conn_handle = event->disconnect.conn.conn_handle; + ESP_LOGI(TAG, "GAP disconnect: conn_handle=%d reason=%d", + conn_handle, event->disconnect.reason); + /* Release the peer's bond slot and marked bits. Done from here rather + * than ots_disconnect_cleanup() because a peer may hold a bond slot + * (via ble_ots_server_mark_object) without ever entering the + * concurrency pool. The peer identity comes from the descriptor + * carried by the event: the connection object is already gone. */ + ble_ots_server_mark_cleanup_on_disconnect(&event->disconnect.conn.peer_id_addr, + event->disconnect.conn.sec_state.bonded); + ots_disconnect_cleanup(conn_handle); + return 0; + } + + case BLE_GAP_EVENT_SUBSCRIBE: { + uint16_t conn_handle = event->subscribe.conn_handle; + uint16_t attr_handle = event->subscribe.attr_handle; + bool indicate = event->subscribe.cur_indicate; + + ble_ots_server_conn_state_t *cs = ble_ots_server_conn_state_get(conn_handle); + if (!cs) { + return 0; + } + + if (attr_handle == s_ots_server_init->oacp_val_handle) { + cs->oacp_subscribed = indicate; + ESP_LOGI(TAG, "OACP CCCD indicate %s for conn=%d", + indicate ? "enabled" : "disabled", conn_handle); + } else if (attr_handle == s_ots_server_init->olcp_val_handle) { + cs->olcp_indicate_subscribed = indicate; + ESP_LOGI(TAG, "OLCP CCCD indicate %s for conn=%d", + indicate ? "enabled" : "disabled", conn_handle); + } else if (attr_handle == s_ots_server_init->obj_changed_val_handle) { + cs->obj_changed_subscribed = indicate; + ESP_LOGI(TAG, "Object Changed CCCD indicate %s for conn=%d", + indicate ? "enabled" : "disabled", conn_handle); + } + return 0; + } + + case BLE_GAP_EVENT_NOTIFY_TX: + /* Only the terminal outcome of one of our own indications releases the + * per-connection slot. status == 0 merely reports that the indication + * was handed to the controller; the confirmation is still pending. */ + if (event->notify_tx.indication && event->notify_tx.status != 0 && + (event->notify_tx.attr_handle == s_ots_server_init->oacp_val_handle || + event->notify_tx.attr_handle == s_ots_server_init->olcp_val_handle || + event->notify_tx.attr_handle == s_ots_server_init->obj_changed_val_handle)) { + ots_indicate_completed(event->notify_tx.conn_handle); + } + return 0; + + default: + return 0; + } +} + +static int ots_gap_event_cb(struct ble_gap_event *event, void *arg) +{ + int rc; + + /* Serialize against the application task and re-validate under the lock: + * ble_ots_server_deinit() releases the control block while holding it. */ + OTS_LOCK(); + if (!BLE_OTS_SERVER_ENV_OK() || !ble_ots_server_env.initialized) { + OTS_UNLOCK(); + return 0; + } + rc = ots_gap_event_handle(event); + OTS_UNLOCK(); + + return rc; +} + +/***************************************************************************** + * Public API: ble_ots_server_init + *****************************************************************************/ +int ble_ots_server_init(const ble_ots_server_config_t *config) +{ + int rc; + + if (!config) { + ESP_LOGE(TAG, "init: config is NULL"); + return BLE_HS_EINVAL; + } + + if (BLE_OTS_SERVER_ENV_OK()) { + ESP_LOGE(TAG, "init: already initialized"); + return BLE_HS_EALREADY; + } + + /* Validate config */ + if ((config->oacp_features & BLE_OTS_OACP_FEAT_CREATE) && !config->obj_name_writable) { + ESP_LOGE(TAG, "init: OACP Create requires obj_name_writable"); + return BLE_HS_EINVAL; + } + + /* Create the mutex before anything else so that every path below — and any + * NimBLE callback still registered from an earlier init/deinit cycle — is + * serialized. It is created once and deliberately never destroyed. */ + if (!s_ots_server_mutex) { + s_ots_server_mutex = xSemaphoreCreateRecursiveMutex(); + if (!s_ots_server_mutex) { + ESP_LOGE(TAG, "init: failed to create mutex"); + return BLE_HS_ENOMEM; + } + } + + /* Allocate the module-lifetime init context (GATT service/characteristic + * tables, tracked value handles, and the GAP event listener) on the heap. + * calloc zero-initializes it. Reused as-is across init/deinit cycles: once + * the GATT service has been registered, NimBLE holds pointers into it and + * it can neither be freed nor rebuilt. */ + if (!s_ots_server_init) { + s_ots_server_init = calloc(1, sizeof(ots_server_init_ctx_t)); + if (!s_ots_server_init) { + ESP_LOGE(TAG, "init: failed to allocate server init context"); + return BLE_HS_ENOMEM; + } + } + + /* Allocate the control block. calloc zero-initializes it, matching the + * previous .bss semantics, so no extra memset is needed below. */ + p_ble_ots_server_env = calloc(1, sizeof(ble_ots_server_cb_env_t)); + if (!p_ble_ots_server_env) { + ESP_LOGE(TAG, "init: failed to allocate control block"); + rc = BLE_HS_ENOMEM; + goto err_cleanup_init_ctx; + } + + /* Copy configuration */ + memcpy(&ble_ots_server_env.config, config, sizeof(ble_ots_server_config_t)); + + /* Set OTS Feature */ + ble_ots_server_env.ots_feature.oacp_features = config->oacp_features; + ble_ots_server_env.ots_feature.olcp_features = config->olcp_features; + + /* Initialize object database */ + for (int i = 0; i < CONFIG_BLE_OTS_SERVER_MAX_OBJECTS; i++) { + ble_ots_server_env.obj_db[i].in_use = false; + ble_ots_server_env.obj_db[i].locked_by = BLE_OTS_CONN_ID_NONE; + } + ble_ots_server_env.next_obj_id = BLE_OTS_OBJ_ID_MIN_USER; + + /* Initialize concurrency pool */ + for (int i = 0; i < CONFIG_BLE_OTS_SERVER_MAX_CONCURRENCY; i++) { + ble_ots_server_env.conn_pool[i].in_pool = false; + ble_ots_server_env.conn_pool[i].conn_id = BLE_OTS_CONN_ID_NONE; + ble_ots_server_env.conn_pool[i].transfer_timer_inited = false; + ble_ots_server_env.conn_pool[i].transfer_retry_timer_inited = false; + } + + /* Allocate the filter/changed sub-module context */ + rc = ble_ots_server_filter_changed_init(); + if (rc != 0) { + ESP_LOGE(TAG, "init: failed to init filter/changed context rc=%d", rc); + goto err_cleanup_env; + } + + /* Create DLO if multi-object */ + if (CONFIG_BLE_OTS_SERVER_MAX_OBJECTS > 1) { + rc = ots_create_dlo(); + if (rc != 0) { + ESP_LOGE(TAG, "init: failed to create DLO rc=%d", rc); + goto err_cleanup_filter; + } + } + + /* Build and register the GATT service — exactly once per boot. + * ble_gatts_add_svcs() stores the svc_defs pointer without copying and the + * registered attributes keep pointers into chr_defs[], so on a re-init the + * tables must be left untouched and the service must not be added again: + * a second registration would expose a duplicate OTS service whose + * attribute handles no longer match the ones tracked here. */ + if (!s_ots_gatt_registered) { + rc = ots_build_gatt_service(config); + if (rc != 0) { + ESP_LOGE(TAG, "init: failed to build GATT service rc=%d", rc); + goto err_cleanup_dlo; + } + + rc = ble_gatts_count_cfg(s_ots_server_init->svc_defs); + if (rc != 0) { + ESP_LOGE(TAG, "init: ble_gatts_count_cfg failed rc=%d", rc); + goto err_cleanup_dlo; + } + + rc = ble_gatts_add_svcs(s_ots_server_init->svc_defs); + if (rc != 0) { + ESP_LOGE(TAG, "init: ble_gatts_add_svcs failed rc=%d", rc); + goto err_cleanup_dlo; + } + + /* From here on the host owns s_ots_server_init. */ + s_ots_gatt_registered = true; + } else { + ESP_LOGW(TAG, "init: reusing the GATT service registered by a previous " + "init; characteristic layout follows that configuration"); + } + + /* Register L2CAP server on PSM_OTS. + * BLE_HS_EALREADY is acceptable — the server may already be registered + * from a previous init cycle (NimBLE does not provide an unregister API). */ + rc = ble_l2cap_create_server(BLE_OTS_PSM_OTS, OTS_L2CAP_COC_MTU, + ots_l2cap_event_cb, NULL); + if (rc != 0 && rc != BLE_HS_EALREADY) { + ESP_LOGE(TAG, "init: ble_l2cap_create_server failed rc=%d", rc); + goto err_cleanup_dlo; + } + + /* Register GAP event listener for connect/disconnect */ + rc = ble_gap_event_listener_register(&s_ots_server_init->gap_listener, ots_gap_event_cb, NULL); + if (rc != 0) { + ESP_LOGE(TAG, "init: ble_gap_event_listener_register failed rc=%d", rc); + goto err_cleanup_dlo; + } + + ble_ots_server_env.initialized = true; + ble_ots_server_env.started = true; + ESP_LOGI(TAG, "OTS server initialized (max_objects=%d, max_concurrency=%d)", + CONFIG_BLE_OTS_SERVER_MAX_OBJECTS, CONFIG_BLE_OTS_SERVER_MAX_CONCURRENCY); + return 0; + +err_cleanup_dlo: + /* Free DLO content buffer and mark DLO slot unused on init failure */ + if (ble_ots_server_env.dlo_content) { + for (int i = 0; i < CONFIG_BLE_OTS_SERVER_MAX_OBJECTS; i++) { + ble_ots_server_obj_t *obj = &ble_ots_server_env.obj_db[i]; + if (obj->in_use && obj->object_id == BLE_OTS_OBJ_ID_DIRECTORY_LISTING) { + obj->data = NULL; + obj->in_use = false; + break; + } + } + free(ble_ots_server_env.dlo_content); + ble_ots_server_env.dlo_content = NULL; + ble_ots_server_env.dlo_content_size = 0; + } +err_cleanup_filter: + ble_ots_server_filter_changed_deinit(); +err_cleanup_env: + /* Callbacks that are already registered guard on BLE_OTS_SERVER_ENV_OK() + * under this mutex, so take it before dropping the control block. */ + OTS_LOCK(); + free(p_ble_ots_server_env); + p_ble_ots_server_env = NULL; + OTS_UNLOCK(); +err_cleanup_init_ctx: + /* Only safe while the host has not taken ownership of the GATT tables. + * Once it has, the block is kept for the lifetime of the module. */ + if (!s_ots_gatt_registered) { + free(s_ots_server_init); + s_ots_server_init = NULL; + } + /* s_ots_server_mutex is intentionally kept — see its definition. */ + return rc; +} + +/***************************************************************************** + * Public API: ble_ots_server_deinit + *****************************************************************************/ +int ble_ots_server_deinit(void) +{ + if (!BLE_OTS_SERVER_ENV_OK() || !ble_ots_server_env.initialized) { + return BLE_HS_EALREADY; + } + + /* Unregister the GAP event listener before anything is torn down: a + * disconnect delivered on the host task would otherwise run + * ots_disconnect_cleanup() against the state being freed here. The GATT + * and L2CAP callbacks cannot be unregistered at all (NimBLE has no such + * API), so they re-validate the control block under the mutex taken below. + */ + ble_gap_event_listener_unregister(&s_ots_server_init->gap_listener); + + OTS_LOCK(); + + /* Stop serving first: NimBLE callbacks that are already running observe + * this and bail out before we start tearing the control block down. */ + ble_ots_server_env.started = false; + + /* Close all OTC channels and free pool slots */ + for (int i = 0; i < CONFIG_BLE_OTS_SERVER_MAX_CONCURRENCY; i++) { + ble_ots_server_conn_state_t *cs = &ble_ots_server_env.conn_pool[i]; + if (cs->in_pool) { + if (cs->otc_chan) { + ble_l2cap_disconnect(cs->otc_chan); + cs->otc_chan = NULL; + } + conn_pool_free(cs); + } + } + + /* Free object data buffers */ + for (int i = 0; i < CONFIG_BLE_OTS_SERVER_MAX_OBJECTS; i++) { + ble_ots_server_obj_t *obj = &ble_ots_server_env.obj_db[i]; + if (obj->in_use) { + /* Don't free DLO data — it's the shared dlo_content buffer */ + if (obj->object_id != BLE_OTS_OBJ_ID_DIRECTORY_LISTING && obj->data) { + free(obj->data); + } + obj->data = NULL; + obj->in_use = false; + } + } + + /* Free DLO content buffer */ + if (ble_ots_server_env.dlo_content) { + free(ble_ots_server_env.dlo_content); + ble_ots_server_env.dlo_content = NULL; + } + + /* Free the filter/changed sub-module context */ + ble_ots_server_filter_changed_deinit(); + + /* Release the control block. The still-registered GATT/L2CAP callbacks + * take this same mutex and re-check BLE_OTS_SERVER_ENV_OK() before + * dereferencing the pointer, so none of them can be mid-access here. */ + ble_ots_server_env.app_cb = NULL; + ble_ots_server_env.initialized = false; + free(p_ble_ots_server_env); + p_ble_ots_server_env = NULL; + + OTS_UNLOCK(); + + /* s_ots_server_mutex and s_ots_server_init are deliberately kept: NimBLE + * still holds the GATT/L2CAP registrations, so their callbacks — and the + * mutex they synchronize on — must stay valid. Both are reused verbatim by + * a subsequent ble_ots_server_init(). */ + + ESP_LOGI(TAG, "OTS server deinitialized"); + return 0; +} + +/***************************************************************************** + * Public API: ble_ots_server_register_cb + *****************************************************************************/ +int ble_ots_server_register_cb(ble_ots_server_cb_t callback) +{ + if (!callback) { + return BLE_HS_EINVAL; + } + + if (!BLE_OTS_SERVER_ENV_OK()) { + ESP_LOGE(TAG, "register_cb: server not initialized"); + return BLE_HS_EINVAL; + } + + ble_ots_server_env.app_cb = callback; + return 0; +} diff --git a/components/bt/ble_profiles/nimble/ble_ots/src/server/ots_server_metadata.c b/components/bt/ble_profiles/nimble/ble_ots/src/server/ots_server_metadata.c new file mode 100644 index 00000000000..1041f8c0833 --- /dev/null +++ b/components/bt/ble_profiles/nimble/ble_ots/src/server/ots_server_metadata.c @@ -0,0 +1,912 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include +#include + +#include "esp_log.h" +#include "host/ble_hs.h" +#include "host/ble_gatt.h" +#include "host/ble_gap.h" +#include "os/os_mbuf.h" + +#include "ble_ots_common.h" +#include "ble_ots_server.h" +#include "ble_ots_server_int.h" + +static const char *TAG = "ots_metadata"; + +/***************************************************************************** + * Helper: common pre-check for metadata read/write + *****************************************************************************/ + +/** + * @brief Common pre-check for all metadata characteristic access. + * + * Checks concurrency, retrieves current object, and looks it up in the DB. + * + * @param conn_handle BLE connection handle + * @param[out] out_obj Pointer to receive the object pointer + * @param[out] out_id Pointer to receive the current object ID + * @return 0 on success, ATT error code on failure + */ +static int +metadata_precheck(uint16_t conn_handle, ble_ots_server_obj_t **out_obj, + ble_ots_obj_id_t *out_id) +{ + /* Check concurrency pool */ + if (!ble_ots_server_concurrency_check(conn_handle)) { + return BLE_OTS_APP_ERR_CONCURRENCY_LIMIT_EXCEEDED; + } + + /* Get current object for this connection */ + ble_ots_obj_id_t obj_id = ble_ots_server_current_obj_get(conn_handle); + if (obj_id == BLE_OTS_OBJ_ID_INVALID) { + return BLE_OTS_APP_ERR_OBJECT_NOT_SELECTED; + } + + /* Look up object in database */ + ble_ots_server_obj_t *obj = ble_ots_server_obj_db_lookup(obj_id); + if (obj == NULL) { + return BLE_OTS_APP_ERR_OBJECT_NOT_SELECTED; + } + + if (out_obj) { + *out_obj = obj; + } + if (out_id) { + *out_id = obj_id; + } + + return 0; +} + +static bool +ble_ots_obj_name_is_valid(const char *name, uint16_t name_len) +{ + if (name_len == 0) { + return false; + } + + for (uint16_t i = 0; i < name_len; i++) { + unsigned char c = (unsigned char)name[i]; + if (c < 0x20 || c == 0x7F) { + return false; + } + } + + return true; +} + +/** + * @brief Dispatch metadata written event and object changed indication. + */ +static void +metadata_dispatch_write_event(uint16_t conn_handle, ble_ots_obj_id_t object_id, + uint16_t char_uuid, const char *name, + uint8_t name_len, ble_ots_date_time_t *dt, + uint32_t properties) +{ + /* Dispatch application event */ + ble_ots_server_cb_param_t param; + memset(¶m, 0, sizeof(param)); + param.metadata_written.conn_id = conn_handle; + param.metadata_written.object_id = object_id; + param.metadata_written.char_uuid = char_uuid; + if (name) { + param.metadata_written.name = name; + param.metadata_written.name_len = name_len; + } + if (dt) { + param.metadata_written.date_time = *dt; + } + param.metadata_written.properties = properties; + + ble_ots_server_dispatch_event(BLE_OTS_SERVER_EVT_METADATA_WRITTEN, ¶m); + + /* Dispatch object changed: Source = Client (bit 0 = 1), Metadata changed (bit 2 = 1) */ + uint8_t flags = BLE_OTS_OBJ_CHANGED_FLAG_SOURCE | BLE_OTS_OBJ_CHANGED_FLAG_METADATA; + ble_ots_server_dispatch_obj_changed(object_id, flags, conn_handle); +} + +/***************************************************************************** + * OTS Feature Characteristic (UUID 0x2ABD) + *****************************************************************************/ + +static int +ble_ots_server_feature_access(uint16_t conn_handle, uint16_t attr_handle, + struct ble_gatt_access_ctxt *ctxt, void *arg) +{ + int rc; + + if (ctxt->op == BLE_GATT_ACCESS_OP_READ_CHR) { + /* Return 8-byte OTS Feature: OACP Features (4) + OLCP Features (4) */ + uint8_t buf[8]; + uint32_t oacp = ble_ots_server_env.ots_feature.oacp_features; + uint32_t olcp = ble_ots_server_env.ots_feature.olcp_features; + + /* Little-endian encoding */ + buf[0] = (uint8_t)(oacp & 0xFF); + buf[1] = (uint8_t)((oacp >> 8) & 0xFF); + buf[2] = (uint8_t)((oacp >> 16) & 0xFF); + buf[3] = (uint8_t)((oacp >> 24) & 0xFF); + buf[4] = (uint8_t)(olcp & 0xFF); + buf[5] = (uint8_t)((olcp >> 8) & 0xFF); + buf[6] = (uint8_t)((olcp >> 16) & 0xFF); + buf[7] = (uint8_t)((olcp >> 24) & 0xFF); + + rc = os_mbuf_append(ctxt->om, buf, sizeof(buf)); + return rc == 0 ? 0 : BLE_ATT_ERR_INSUFFICIENT_RES; + } + + /* Not writable */ + return BLE_ATT_ERR_WRITE_NOT_PERMITTED; +} + +/***************************************************************************** + * Object Name Characteristic (UUID 0x2ABE) + *****************************************************************************/ + +static int +ble_ots_server_obj_name_access(uint16_t conn_handle, uint16_t attr_handle, + struct ble_gatt_access_ctxt *ctxt, void *arg) +{ + ble_ots_server_obj_t *obj = NULL; + ble_ots_obj_id_t obj_id = 0; + int rc; + + rc = metadata_precheck(conn_handle, &obj, &obj_id); + if (rc != 0) { + return rc; + } + + if (ctxt->op == BLE_GATT_ACCESS_OP_READ_CHR) { + /* Return the object name as UTF-8 string */ + rc = os_mbuf_append(ctxt->om, obj->name, obj->name_len); + return rc == 0 ? 0 : BLE_ATT_ERR_INSUFFICIENT_RES; + } + + if (ctxt->op == BLE_GATT_ACCESS_OP_WRITE_CHR) { + /* Extract the written value */ + char name_buf[BLE_OTS_OBJECT_NAME_MAX_LEN]; + uint16_t name_len = 0; + uint16_t om_len = OS_MBUF_PKTLEN(ctxt->om); + + /* Validate name length */ + if (om_len > BLE_OTS_OBJECT_NAME_MAX_LEN) { + return BLE_OTS_APP_ERR_WRITE_REQUEST_REJECTED; + } + + rc = ble_hs_mbuf_to_flat(ctxt->om, name_buf, sizeof(name_buf), &name_len); + if (rc != 0) { + return BLE_ATT_ERR_UNLIKELY; + } + + /* + * Per OTP spec 4.5.5.1, the name shall not be a zero length string and + * shall not include any ASCII control characters. + */ + if (!ble_ots_obj_name_is_valid(name_buf, name_len)) { + return BLE_OTS_APP_ERR_WRITE_REQUEST_REJECTED; + } + + /* Check uniqueness: the new name must not belong to another object */ + if (!ble_ots_server_name_is_unique(name_buf, (uint8_t)name_len, obj_id)) { + return BLE_OTS_APP_ERR_OBJECT_NAME_ALREADY_EXISTS; + } + + /* Update the object name */ + memcpy(obj->name, name_buf, name_len); + obj->name_len = (uint8_t)name_len; + + /* Dispatch events */ + metadata_dispatch_write_event(conn_handle, obj_id, + BLE_OTS_UUID_OBJECT_NAME, + obj->name, obj->name_len, + NULL, 0); + return 0; + } + + return BLE_ATT_ERR_REQ_NOT_SUPPORTED; +} + +/***************************************************************************** + * Object Type Characteristic (UUID 0x2ABF) + *****************************************************************************/ + +static int +ble_ots_server_obj_type_access(uint16_t conn_handle, uint16_t attr_handle, + struct ble_gatt_access_ctxt *ctxt, void *arg) +{ + ble_ots_server_obj_t *obj = NULL; + int rc; + + rc = metadata_precheck(conn_handle, &obj, NULL); + if (rc != 0) { + return rc; + } + + if (ctxt->op == BLE_GATT_ACCESS_OP_READ_CHR) { + /* Return the type UUID: 2 octets for 16-bit, 16 octets for 128-bit */ + rc = os_mbuf_append(ctxt->om, obj->type_uuid, obj->type_uuid_len); + return rc == 0 ? 0 : BLE_ATT_ERR_INSUFFICIENT_RES; + } + + /* Not writable */ + return BLE_ATT_ERR_WRITE_NOT_PERMITTED; +} + +/***************************************************************************** + * Object Size Characteristic (UUID 0x2AC0) + *****************************************************************************/ + +static int +ble_ots_server_obj_size_access(uint16_t conn_handle, uint16_t attr_handle, + struct ble_gatt_access_ctxt *ctxt, void *arg) +{ + ble_ots_server_obj_t *obj = NULL; + int rc; + + rc = metadata_precheck(conn_handle, &obj, NULL); + if (rc != 0) { + return rc; + } + + if (ctxt->op == BLE_GATT_ACCESS_OP_READ_CHR) { + /* Return 8 octets: Current Size (UINT32 LE) + Allocated Size (UINT32 LE) */ + uint8_t buf[8]; + uint32_t cs = obj->current_size; + uint32_t as = obj->allocated_size; + + buf[0] = (uint8_t)(cs & 0xFF); + buf[1] = (uint8_t)((cs >> 8) & 0xFF); + buf[2] = (uint8_t)((cs >> 16) & 0xFF); + buf[3] = (uint8_t)((cs >> 24) & 0xFF); + buf[4] = (uint8_t)(as & 0xFF); + buf[5] = (uint8_t)((as >> 8) & 0xFF); + buf[6] = (uint8_t)((as >> 16) & 0xFF); + buf[7] = (uint8_t)((as >> 24) & 0xFF); + + rc = os_mbuf_append(ctxt->om, buf, sizeof(buf)); + return rc == 0 ? 0 : BLE_ATT_ERR_INSUFFICIENT_RES; + } + + /* Not writable */ + return BLE_ATT_ERR_WRITE_NOT_PERMITTED; +} + +/***************************************************************************** + * Object First-Created Characteristic (UUID 0x2AC1) + *****************************************************************************/ + +/** + * @brief Encode a ble_ots_date_time_t into a 7-byte little-endian buffer. + */ +static void +encode_date_time(const ble_ots_date_time_t *dt, uint8_t *buf) +{ + buf[0] = (uint8_t)(dt->year & 0xFF); + buf[1] = (uint8_t)((dt->year >> 8) & 0xFF); + buf[2] = dt->month; + buf[3] = dt->day; + buf[4] = dt->hours; + buf[5] = dt->minutes; + buf[6] = dt->seconds; +} + +/** + * @brief Decode a 7-byte little-endian buffer into a ble_ots_date_time_t. + */ +static void +decode_date_time(const uint8_t *buf, ble_ots_date_time_t *dt) +{ + dt->year = (uint16_t)buf[0] | ((uint16_t)buf[1] << 8); + dt->month = buf[2]; + dt->day = buf[3]; + dt->hours = buf[4]; + dt->minutes = buf[5]; + dt->seconds = buf[6]; +} + +/** + * @brief Validate a decoded ble_ots_date_time_t per the BLE OTS specification. + * + * Valid ranges: year 1582–9999 or 0, month 1–12 or 0, day 1–31 or 0, + * hours 0–23, minutes 0–59, seconds 0–59. + * + * A year of 0 denotes the special "unknown" timestamp (e.g. a newly created + * object); in that case the month, day, hours, minutes and seconds MUST all be + * zero. A non-zero month/day/time alongside a zero year is not a valid value. + * + * @param dt Pointer to the date-time structure to validate + * @return true if all fields are within valid ranges, false otherwise + */ +static bool +validate_date_time(const ble_ots_date_time_t *dt) +{ + if (dt->year == 0) { + /* Unknown timestamp: every other field must also be zero */ + return dt->month == 0 && dt->day == 0 && dt->hours == 0 && + dt->minutes == 0 && dt->seconds == 0; + } + if (dt->year < 1582 || dt->year > 9999) { + return false; + } + if (dt->month > 12) { + return false; + } + if (dt->day > 31) { + return false; + } + if (dt->hours > 23) { + return false; + } + if (dt->minutes > 59) { + return false; + } + if (dt->seconds > 59) { + return false; + } + return true; +} + +static int +ble_ots_server_obj_first_created_access(uint16_t conn_handle, uint16_t attr_handle, + struct ble_gatt_access_ctxt *ctxt, void *arg) +{ + ble_ots_server_obj_t *obj = NULL; + ble_ots_obj_id_t obj_id = 0; + int rc; + + rc = metadata_precheck(conn_handle, &obj, &obj_id); + if (rc != 0) { + return rc; + } + + if (ctxt->op == BLE_GATT_ACCESS_OP_READ_CHR) { + uint8_t buf[7]; + encode_date_time(&obj->first_created, buf); + rc = os_mbuf_append(ctxt->om, buf, sizeof(buf)); + return rc == 0 ? 0 : BLE_ATT_ERR_INSUFFICIENT_RES; + } + + if (ctxt->op == BLE_GATT_ACCESS_OP_WRITE_CHR) { + uint8_t buf[7]; + uint16_t len = 0; + uint16_t om_len = OS_MBUF_PKTLEN(ctxt->om); + + if (om_len != 7) { + return BLE_OTS_APP_ERR_WRITE_REQUEST_REJECTED; + } + + rc = ble_hs_mbuf_to_flat(ctxt->om, buf, sizeof(buf), &len); + if (rc != 0) { + return BLE_ATT_ERR_UNLIKELY; + } + + ble_ots_date_time_t new_dt; + decode_date_time(buf, &new_dt); + + /* Validate date-time fields per OTS specification */ + if (!validate_date_time(&new_dt)) { + return BLE_OTS_APP_ERR_WRITE_REQUEST_REJECTED; + } + + /* Update the object's first-created timestamp */ + obj->first_created = new_dt; + + /* Dispatch events */ + metadata_dispatch_write_event(conn_handle, obj_id, + BLE_OTS_UUID_OBJECT_FIRST_CREATED, + NULL, 0, &new_dt, 0); + return 0; + } + + return BLE_ATT_ERR_REQ_NOT_SUPPORTED; +} + +/***************************************************************************** + * Object Last-Modified Characteristic (UUID 0x2AC2) + *****************************************************************************/ + +static int +ble_ots_server_obj_last_modified_access(uint16_t conn_handle, uint16_t attr_handle, + struct ble_gatt_access_ctxt *ctxt, void *arg) +{ + ble_ots_server_obj_t *obj = NULL; + ble_ots_obj_id_t obj_id = 0; + int rc; + + rc = metadata_precheck(conn_handle, &obj, &obj_id); + if (rc != 0) { + return rc; + } + + if (ctxt->op == BLE_GATT_ACCESS_OP_READ_CHR) { + uint8_t buf[7]; + encode_date_time(&obj->last_modified, buf); + rc = os_mbuf_append(ctxt->om, buf, sizeof(buf)); + return rc == 0 ? 0 : BLE_ATT_ERR_INSUFFICIENT_RES; + } + + if (ctxt->op == BLE_GATT_ACCESS_OP_WRITE_CHR) { + /* Writable only when server does NOT have a real-time clock */ + if (ble_ots_server_env.config.has_realtime_clock) { + return BLE_ATT_ERR_WRITE_NOT_PERMITTED; + } + + uint8_t buf[7]; + uint16_t len = 0; + uint16_t om_len = OS_MBUF_PKTLEN(ctxt->om); + + if (om_len != 7) { + return BLE_OTS_APP_ERR_WRITE_REQUEST_REJECTED; + } + + rc = ble_hs_mbuf_to_flat(ctxt->om, buf, sizeof(buf), &len); + if (rc != 0) { + return BLE_ATT_ERR_UNLIKELY; + } + + ble_ots_date_time_t new_dt; + decode_date_time(buf, &new_dt); + + /* Validate date-time fields per OTS specification */ + if (!validate_date_time(&new_dt)) { + return BLE_OTS_APP_ERR_WRITE_REQUEST_REJECTED; + } + + /* Update the object's last-modified timestamp */ + obj->last_modified = new_dt; + + /* Dispatch events */ + metadata_dispatch_write_event(conn_handle, obj_id, + BLE_OTS_UUID_OBJECT_LAST_MODIFIED, + NULL, 0, &new_dt, 0); + return 0; + } + + return BLE_ATT_ERR_REQ_NOT_SUPPORTED; +} + +/***************************************************************************** + * Object ID Characteristic (UUID 0x2AC3) + *****************************************************************************/ + +static int +ble_ots_server_obj_id_access(uint16_t conn_handle, uint16_t attr_handle, + struct ble_gatt_access_ctxt *ctxt, void *arg) +{ + ble_ots_server_obj_t *obj = NULL; + int rc; + + rc = metadata_precheck(conn_handle, &obj, NULL); + if (rc != 0) { + return rc; + } + + if (ctxt->op == BLE_GATT_ACCESS_OP_READ_CHR) { + /* Return 6-byte UINT48 Object ID in little-endian */ + uint8_t buf[6]; + uint64_t id = obj->object_id; + + buf[0] = (uint8_t)(id & 0xFF); + buf[1] = (uint8_t)((id >> 8) & 0xFF); + buf[2] = (uint8_t)((id >> 16) & 0xFF); + buf[3] = (uint8_t)((id >> 24) & 0xFF); + buf[4] = (uint8_t)((id >> 32) & 0xFF); + buf[5] = (uint8_t)((id >> 40) & 0xFF); + + rc = os_mbuf_append(ctxt->om, buf, sizeof(buf)); + return rc == 0 ? 0 : BLE_ATT_ERR_INSUFFICIENT_RES; + } + + /* Not writable */ + return BLE_ATT_ERR_WRITE_NOT_PERMITTED; +} + +/***************************************************************************** + * Object Properties Characteristic (UUID 0x2AC4) + *****************************************************************************/ + +static int +ble_ots_server_obj_properties_access(uint16_t conn_handle, uint16_t attr_handle, + struct ble_gatt_access_ctxt *ctxt, void *arg) +{ + ble_ots_server_obj_t *obj = NULL; + ble_ots_obj_id_t obj_id = 0; + int rc; + + rc = metadata_precheck(conn_handle, &obj, &obj_id); + if (rc != 0) { + return rc; + } + + if (ctxt->op == BLE_GATT_ACCESS_OP_READ_CHR) { + /* Return 4-byte properties bit-field in little-endian */ + uint8_t buf[4]; + uint32_t props = obj->properties; + + buf[0] = (uint8_t)(props & 0xFF); + buf[1] = (uint8_t)((props >> 8) & 0xFF); + buf[2] = (uint8_t)((props >> 16) & 0xFF); + buf[3] = (uint8_t)((props >> 24) & 0xFF); + + rc = os_mbuf_append(ctxt->om, buf, sizeof(buf)); + return rc == 0 ? 0 : BLE_ATT_ERR_INSUFFICIENT_RES; + } + + if (ctxt->op == BLE_GATT_ACCESS_OP_WRITE_CHR) { + uint8_t buf[4]; + uint16_t len = 0; + uint16_t om_len = OS_MBUF_PKTLEN(ctxt->om); + + if (om_len != 4) { + return BLE_OTS_APP_ERR_WRITE_REQUEST_REJECTED; + } + + rc = ble_hs_mbuf_to_flat(ctxt->om, buf, sizeof(buf), &len); + if (rc != 0) { + return BLE_ATT_ERR_UNLIKELY; + } + + uint32_t new_props = (uint32_t)buf[0] | + ((uint32_t)buf[1] << 8) | + ((uint32_t)buf[2] << 16) | + ((uint32_t)buf[3] << 24); + + /* Reject if any RFU bits (8–31) are set */ + if (new_props & 0xFFFFFF00) { + return BLE_OTS_APP_ERR_WRITE_REQUEST_REJECTED; + } + + /* Update the object's properties */ + obj->properties = new_props; + + /* Dispatch events */ + metadata_dispatch_write_event(conn_handle, obj_id, + BLE_OTS_UUID_OBJECT_PROPERTIES, + NULL, 0, NULL, new_props); + return 0; + } + + return BLE_ATT_ERR_REQ_NOT_SUPPORTED; +} + +/***************************************************************************** + * Metadata Read/Write Dispatchers — called from ots_server_init.c + *****************************************************************************/ + +int +ble_ots_server_metadata_read(uint16_t conn_handle, uint16_t attr_handle, + struct ble_gatt_access_ctxt *ctxt) +{ + const ble_uuid_t *uuid = ctxt->chr->uuid; + if (uuid->type != BLE_UUID_TYPE_16) { + return BLE_ATT_ERR_UNLIKELY; + } + uint16_t uuid16 = BLE_UUID16(uuid)->value; + + switch (uuid16) { + case BLE_OTS_UUID_OTS_FEATURE: + return ble_ots_server_feature_access(conn_handle, attr_handle, ctxt, NULL); + case BLE_OTS_UUID_OBJECT_NAME: + return ble_ots_server_obj_name_access(conn_handle, attr_handle, ctxt, NULL); + case BLE_OTS_UUID_OBJECT_TYPE: + return ble_ots_server_obj_type_access(conn_handle, attr_handle, ctxt, NULL); + case BLE_OTS_UUID_OBJECT_SIZE: + return ble_ots_server_obj_size_access(conn_handle, attr_handle, ctxt, NULL); + case BLE_OTS_UUID_OBJECT_FIRST_CREATED: + return ble_ots_server_obj_first_created_access(conn_handle, attr_handle, ctxt, NULL); + case BLE_OTS_UUID_OBJECT_LAST_MODIFIED: + return ble_ots_server_obj_last_modified_access(conn_handle, attr_handle, ctxt, NULL); + case BLE_OTS_UUID_OBJECT_ID: + return ble_ots_server_obj_id_access(conn_handle, attr_handle, ctxt, NULL); + case BLE_OTS_UUID_OBJECT_PROPERTIES: + return ble_ots_server_obj_properties_access(conn_handle, attr_handle, ctxt, NULL); + default: + return BLE_ATT_ERR_UNLIKELY; + } +} + +int +ble_ots_server_metadata_write(uint16_t conn_handle, uint16_t attr_handle, + struct ble_gatt_access_ctxt *ctxt) +{ + const ble_uuid_t *uuid = ctxt->chr->uuid; + if (uuid->type != BLE_UUID_TYPE_16) { + return BLE_ATT_ERR_UNLIKELY; + } + uint16_t uuid16 = BLE_UUID16(uuid)->value; + + /* The Directory Listing Object is read-only: its metadata is generated by + * the server from the object database, so a client must not be able to + * overwrite it while the DLO is its Current Object. When no object is + * selected the ID is BLE_OTS_OBJ_ID_INVALID, which falls through to the + * access handlers so that Object Not Selected keeps being reported. */ + if (ble_ots_server_current_obj_get(conn_handle) == + BLE_OTS_OBJ_ID_DIRECTORY_LISTING) { + return BLE_ATT_ERR_WRITE_NOT_PERMITTED; + } + + switch (uuid16) { + case BLE_OTS_UUID_OBJECT_NAME: + return ble_ots_server_obj_name_access(conn_handle, attr_handle, ctxt, NULL); + case BLE_OTS_UUID_OBJECT_FIRST_CREATED: + return ble_ots_server_obj_first_created_access(conn_handle, attr_handle, ctxt, NULL); + case BLE_OTS_UUID_OBJECT_LAST_MODIFIED: + return ble_ots_server_obj_last_modified_access(conn_handle, attr_handle, ctxt, NULL); + case BLE_OTS_UUID_OBJECT_PROPERTIES: + return ble_ots_server_obj_properties_access(conn_handle, attr_handle, ctxt, NULL); + default: + return BLE_ATT_ERR_UNLIKELY; + } +} + +/***************************************************************************** + * Public APIs — Server-Initiated Object Management + *****************************************************************************/ + +/** + * @brief Create the object and populate its metadata. + * + * Caller must hold the OTS server mutex: the uniqueness check, the slot + * allocation and the metadata/content writes below form one atomic operation + * with respect to the NimBLE host task. + * + * @param params Object parameters supplied by the application + * @param out_obj_id Receives the allocated Object ID on success + * @return 0 on success, negative errno on failure + */ +static int ots_add_object_locked(const ble_ots_server_obj_params_t *params, + ble_ots_obj_id_t *out_obj_id) +{ + /* Validate name length */ + if (params->name_len > BLE_OTS_OBJECT_NAME_MAX_LEN) { + return -EINVAL; + } + + /* Reject a length without a buffer, as ble_ots_server_set_object_data does, + * instead of silently creating an object with no content */ + if (params->data == NULL && params->data_len > 0) { + return -EINVAL; + } + + /* Validate allocated_size >= data_len */ + uint32_t alloc_size = params->allocated_size; + if (alloc_size == 0 && params->data_len > 0) { + alloc_size = params->data_len; + } + if (alloc_size < params->data_len) { + return -EINVAL; + } + + /* Validate type UUID length */ + if (params->type.uuid_len != 2 && params->type.uuid_len != 16) { + return -EINVAL; + } + + /* Check name uniqueness */ + const char *name = params->name; + uint8_t name_len = name ? params->name_len : 0; + + /* Only named objects take part in the uniqueness check: an object created + * over OACP starts with a zero-length name and the Create procedure has no + * result code for a name clash, so several unnamed objects must be able to + * coexist until the client assigns each one a name. */ + if (name_len > 0) { + if (!ble_ots_server_name_is_unique(name, name_len, BLE_OTS_OBJ_ID_INVALID)) { + return -EEXIST; + } + } + + /* Create object in database (allocates Object ID) */ + ble_ots_obj_id_t new_id = ble_ots_server_obj_db_create(¶ms->type, alloc_size); + if (new_id == 0) { + return -ENOMEM; + } + + /* Look up the newly created object to set metadata */ + ble_ots_server_obj_t *obj = ble_ots_server_obj_db_lookup(new_id); + if (obj == NULL) { + return -ENOMEM; + } + + /* Set name */ + if (name_len > 0) { + memcpy(obj->name, name, name_len); + } + obj->name_len = name_len; + + /* Set properties */ + obj->properties = params->properties; + + /* Set timestamps */ + obj->first_created = params->first_created; + obj->last_modified = params->last_modified; + + /* Set initial data if provided */ + if (params->data != NULL && params->data_len > 0) { + int rc = ble_ots_server_obj_data_write(new_id, 0, params->data, params->data_len); + if (rc != 0) { + ESP_LOGE(TAG, "Failed to write initial object data, rc=%d", rc); + ble_ots_server_obj_db_delete(new_id); + return (rc == BLE_HS_ENOMEM) ? -ENOMEM : + (rc == BLE_HS_ENOENT) ? -ENOENT : -EINVAL; + } + obj->current_size = params->data_len; + } else { + obj->current_size = 0; + } + + obj->allocated_size = alloc_size; + + /* Output the allocated Object ID */ + *out_obj_id = new_id; + + ESP_LOGI(TAG, "Object added: ID=0x%012llX name_len=%u", + (unsigned long long)new_id, name_len); + + return 0; +} + +int +ble_ots_server_add_object(const ble_ots_server_obj_params_t *params, + ble_ots_obj_id_t *out_obj_id) +{ + ble_ots_obj_id_t new_id = BLE_OTS_OBJ_ID_INVALID; + int rc; + + if (!BLE_OTS_SERVER_ENV_OK()) { + return -EPERM; + } + + if (params == NULL) { + return -EINVAL; + } + + ble_ots_server_lock(); + rc = ots_add_object_locked(params, &new_id); + ble_ots_server_unlock(); + + if (rc != 0) { + return rc; + } + + if (out_obj_id != NULL) { + *out_obj_id = new_id; + } + + /* Dispatch Object Changed: Creation (bit 3), Source = Server (bit 0 = 0). + * Done outside the lock — it queues indications and reaches NimBLE. */ + ble_ots_server_dispatch_obj_changed(new_id, + BLE_OTS_OBJ_CHANGED_FLAG_CREATION, + BLE_OTS_CONN_ID_NONE); + + return 0; +} + +int +ble_ots_server_remove_object(ble_ots_obj_id_t object_id) +{ + if (!BLE_OTS_SERVER_ENV_OK()) { + return -EPERM; + } + + /* DLO cannot be removed */ + if (object_id == BLE_OTS_OBJ_ID_DIRECTORY_LISTING) { + return -EINVAL; + } + + ble_ots_server_lock(); + + /* Look up the object */ + ble_ots_server_obj_t *obj = ble_ots_server_obj_db_lookup(object_id); + if (obj == NULL) { + ble_ots_server_unlock(); + return -ENOENT; + } + + /* Invalidate current object for any connection that has this object selected */ + for (int i = 0; i < CONFIG_BLE_OTS_SERVER_MAX_CONCURRENCY; i++) { + ble_ots_server_conn_state_t *cs = &ble_ots_server_env.conn_pool[i]; + if (cs->in_pool && cs->current_obj_valid && + cs->current_object_id == object_id) { + cs->current_obj_valid = false; + cs->current_object_id = BLE_OTS_OBJ_ID_INVALID; + ESP_LOGW(TAG, "Invalidated current object for conn=%u due to removal", + cs->conn_id); + } + } + + ble_ots_server_unlock(); + + /* Delete the object from the database. Takes the lock itself and then + * aborts any transfer on the object without it, since that reaches the + * application callback. */ + int rc = ble_ots_server_obj_db_delete(object_id); + if (rc != 0) { + ESP_LOGE(TAG, "Failed to delete object 0x%012llX, rc=%d", + (unsigned long long)object_id, rc); + return (rc == BLE_HS_ENOENT) ? -ENOENT : + (rc == BLE_HS_EINVAL) ? -EINVAL : -EINVAL; + } + + ESP_LOGI(TAG, "Object removed: ID=0x%012llX", (unsigned long long)object_id); + + /* Dispatch Object Changed: Deletion (bit 4), Source = Server (bit 0 = 0) */ + ble_ots_server_dispatch_obj_changed(object_id, + BLE_OTS_OBJ_CHANGED_FLAG_DELETION, + BLE_OTS_CONN_ID_NONE); + + return 0; +} + +int +ble_ots_server_set_object_data(ble_ots_obj_id_t object_id, + const uint8_t *data, + uint32_t offset, + uint32_t length) +{ + if (!BLE_OTS_SERVER_ENV_OK()) { + return -EPERM; + } + + if (data == NULL && length > 0) { + return -EINVAL; + } + + /* Validate that offset + length does not overflow uint32_t */ + if (length > 0 && offset > UINT32_MAX - length) { + return -EINVAL; + } + + ble_ots_server_lock(); + + /* Look up the object */ + ble_ots_server_obj_t *obj = ble_ots_server_obj_db_lookup(object_id); + if (obj == NULL) { + ble_ots_server_unlock(); + return -ENOENT; + } + + /* Write data at the specified offset */ + int rc = ble_ots_server_obj_data_write(object_id, offset, data, length); + if (rc != 0) { + ble_ots_server_unlock(); + ESP_LOGE(TAG, "Failed to write object data for 0x%012llX, rc=%d", + (unsigned long long)object_id, rc); + return (rc == BLE_HS_ENOMEM) ? -ENOMEM : + (rc == BLE_HS_ENOENT) ? -ENOENT : -EINVAL; + } + + /* If server has a real-time clock, update Last-Modified timestamp. + * Note: In a real implementation this would use the actual RTC time. + * Since we may not have a real RTC API available, we reset to zero + * if no real-time clock, or set a placeholder if we do. */ + if (ble_ots_server_env.config.has_realtime_clock) { + /* In a production system, this would call an RTC API to get current UTC. + * For now, we leave the timestamp as-is since we don't have a + * portable RTC interface. The application can update it via the + * metadata write event or directly. */ + } + + ESP_LOGD(TAG, "Object data set: ID=0x%012llX offset=%lu len=%lu cur_size=%lu", + (unsigned long long)object_id, + (unsigned long)offset, (unsigned long)length, + (unsigned long)obj->current_size); + + ble_ots_server_unlock(); + + /* Dispatch Object Changed: Content changed (bit 1), Source = Server (bit 0 = 0) */ + ble_ots_server_dispatch_obj_changed(object_id, + BLE_OTS_OBJ_CHANGED_FLAG_CONTENT, + BLE_OTS_CONN_ID_NONE); + + return 0; +} diff --git a/components/bt/ble_profiles/nimble/ble_ots/src/server/ots_server_oacp_ops.c b/components/bt/ble_profiles/nimble/ble_ots/src/server/ots_server_oacp_ops.c new file mode 100644 index 00000000000..bcdacf0cfc7 --- /dev/null +++ b/components/bt/ble_profiles/nimble/ble_ots/src/server/ots_server_oacp_ops.c @@ -0,0 +1,736 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include "esp_log.h" +#include "host/ble_hs.h" +#include "host/ble_gatt.h" +#include "host/ble_gap.h" +#include "host/ble_l2cap.h" +#include "os/os_mbuf.h" +#include "ble_ots_common.h" +#include "ble_ots_server.h" +#include "ble_ots_server_int.h" + +static const char *TAG = "ots_oacp_ops"; + +/***************************************************************************** + * Helper: check if OACP CCCD is configured for indications + *****************************************************************************/ +static bool oacp_cccd_configured(uint16_t conn_id) +{ + uint16_t chr_val_handle = ble_ots_server_oacp_handle_get(); + if (chr_val_handle == 0) { + return false; + } + + /* Check the tracked OACP CCCD subscription state for this connection. + * The oacp_subscribed flag is updated via BLE_GAP_EVENT_SUBSCRIBE in the + * GAP event callback (ots_server_init.c). */ + ble_ots_server_conn_state_t *cs = ble_ots_server_conn_state_get(conn_id); + if (cs == NULL) { + return false; + } + + return cs->oacp_subscribed; +} + +/***************************************************************************** + * Helper: send OACP response indication + *****************************************************************************/ +static int oacp_send_response(uint16_t conn_id, uint8_t request_opcode, + uint8_t result_code, + const uint8_t *resp_param, uint8_t resp_param_len) +{ + uint8_t buf[3 + 4]; /* response opcode + request opcode + result code + up to 4 bytes param */ + uint16_t len = 0; + + buf[len++] = BLE_OTS_OACP_OPCODE_RESPONSE; /* 0x60 */ + buf[len++] = request_opcode; + buf[len++] = result_code; + + if (resp_param != NULL && resp_param_len > 0) { + memcpy(&buf[len], resp_param, resp_param_len); + len += resp_param_len; + } + + return ble_ots_server_indicate_response(conn_id, BLE_OTS_UUID_OACP, buf, len); +} + +/***************************************************************************** + * Helper: check if object type is in supported_types list + *****************************************************************************/ +static bool type_is_supported(const ble_ots_obj_type_entry_t *type) +{ + const ble_ots_server_config_t *cfg = &ble_ots_server_env.config; + + if (cfg->supported_types == NULL || cfg->num_supported_types == 0) { + /* No type restriction — all types accepted */ + return true; + } + + for (uint8_t i = 0; i < cfg->num_supported_types; i++) { + const ble_ots_obj_type_entry_t *entry = &cfg->supported_types[i]; + if (entry->uuid_len == type->uuid_len && + memcmp(entry->uuid, type->uuid, type->uuid_len) == 0) { + return true; + } + } + + return false; +} + +/***************************************************************************** + * Helper: check if an object is locked or has an active transfer + *****************************************************************************/ +static bool object_is_locked_or_transferring(ble_ots_obj_id_t object_id, + uint16_t conn_id) +{ + ble_ots_server_obj_t *obj = ble_ots_server_obj_db_lookup(object_id); + if (obj == NULL) { + return false; + } + + /* Check if locked by another connection */ + if (obj->locked_by != BLE_OTS_CONN_ID_NONE && obj->locked_by != conn_id) { + return true; + } + + /* Check if any connection has an active transfer on this object */ + for (int i = 0; i < CONFIG_BLE_OTS_SERVER_MAX_CONCURRENCY; i++) { + ble_ots_server_conn_state_t *cs = &ble_ots_server_env.conn_pool[i]; + if (cs->in_pool && cs->transfer_active && + cs->transfer_object_id == object_id) { + return true; + } + } + + return false; +} + +/***************************************************************************** + * OACP Create Procedure + *****************************************************************************/ +static void oacp_create_proc(uint16_t conn_id, const uint8_t *param, + uint16_t param_len) +{ + uint8_t opcode = BLE_OTS_OACP_OPCODE_CREATE; + + /* Priority 1: Check if Create is supported */ + if (!(ble_ots_server_env.config.oacp_features & BLE_OTS_OACP_FEAT_CREATE)) { + oacp_send_response(conn_id, opcode, + BLE_OTS_OACP_RESULT_OP_CODE_NOT_SUPPORTED, NULL, 0); + return; + } + + /* Parse parameters: Size (UINT32) + Type (2 or 16 octets UUID) */ + if (param_len < 6) { /* 4 bytes size + at least 2 bytes UUID */ + oacp_send_response(conn_id, opcode, + BLE_OTS_OACP_RESULT_INVALID_PARAMETER, NULL, 0); + return; + } + + uint32_t size = (uint32_t)param[0] | + ((uint32_t)param[1] << 8) | + ((uint32_t)param[2] << 16) | + ((uint32_t)param[3] << 24); + + uint16_t uuid_len = param_len - 4; + if (uuid_len != 2 && uuid_len != 16) { + /* Priority 4: Invalid Parameter — UUID must be 2 or 16 octets */ + oacp_send_response(conn_id, opcode, + BLE_OTS_OACP_RESULT_INVALID_PARAMETER, NULL, 0); + return; + } + + ble_ots_obj_type_entry_t type; + memset(&type, 0, sizeof(type)); + type.uuid_len = (uint8_t)uuid_len; + memcpy(type.uuid, ¶m[4], uuid_len); + + /* Priority 2: Check if type is supported */ + if (!type_is_supported(&type)) { + oacp_send_response(conn_id, opcode, + BLE_OTS_OACP_RESULT_UNSUPPORTED_TYPE, NULL, 0); + return; + } + + /* Priority 3: Create object in database (checks resources internally) */ + ble_ots_obj_id_t new_id = ble_ots_server_obj_db_create(&type, size); + if (new_id == 0) { + oacp_send_response(conn_id, opcode, + BLE_OTS_OACP_RESULT_INSUFFICIENT_RESOURCES, NULL, 0); + return; + } + + /* Reset filters for this connection if Object List Filter is supported */ + if (ble_ots_server_env.config.include_obj_list_filter) { + ble_ots_server_filter_reset(conn_id); + } + + /* Set newly created object as Current Object */ + int rc = ble_ots_server_current_obj_set(conn_id, new_id); + if (rc != 0) { + ESP_LOGE(TAG, "Failed to set current object after create, rc=%d", rc); + /* Attempt to clean up */ + ble_ots_server_obj_db_delete(new_id); + oacp_send_response(conn_id, opcode, + BLE_OTS_OACP_RESULT_OPERATION_FAILED, NULL, 0); + return; + } + + /* Track created object for post-disconnect cleanup */ + ble_ots_server_conn_state_t *cs = ble_ots_server_conn_state_get(conn_id); + if (cs != NULL && cs->created_obj_count < CONFIG_BLE_OTS_SERVER_MAX_OBJECTS) { + cs->created_obj_ids[cs->created_obj_count++] = new_id; + } + + /* Send success indication */ + oacp_send_response(conn_id, opcode, + BLE_OTS_OACP_RESULT_SUCCESS, NULL, 0); + + /* Dispatch EVT_OBJECT_CREATED to application */ + ble_ots_server_obj_t *obj = ble_ots_server_obj_db_lookup(new_id); + ble_ots_server_cb_param_t cb_param; + memset(&cb_param, 0, sizeof(cb_param)); + cb_param.object_created.object_id = new_id; + cb_param.object_created.type = type; + cb_param.object_created.allocated_size = (obj != NULL) ? obj->allocated_size : size; + ble_ots_server_dispatch_event(BLE_OTS_SERVER_EVT_OBJECT_CREATED, &cb_param); + + /* Dispatch Object Changed indication with Creation flag + Source=Client */ + uint8_t flags = BLE_OTS_OBJ_CHANGED_FLAG_CREATION | BLE_OTS_OBJ_CHANGED_FLAG_SOURCE; + ble_ots_server_dispatch_obj_changed(new_id, flags, conn_id); +} + +/***************************************************************************** + * OACP Delete Procedure + *****************************************************************************/ +static void oacp_delete_proc(uint16_t conn_id) +{ + uint8_t opcode = BLE_OTS_OACP_OPCODE_DELETE; + + /* Priority 1: Check if Delete is supported */ + if (!(ble_ots_server_env.config.oacp_features & BLE_OTS_OACP_FEAT_DELETE)) { + oacp_send_response(conn_id, opcode, + BLE_OTS_OACP_RESULT_OP_CODE_NOT_SUPPORTED, NULL, 0); + return; + } + + /* Priority 2: Check if Current Object is valid */ + ble_ots_obj_id_t obj_id = ble_ots_server_current_obj_get(conn_id); + if (obj_id == BLE_OTS_OBJ_ID_INVALID) { + oacp_send_response(conn_id, opcode, + BLE_OTS_OACP_RESULT_INVALID_OBJECT, NULL, 0); + return; + } + + ble_ots_server_obj_t *obj = ble_ots_server_obj_db_lookup(obj_id); + if (obj == NULL) { + oacp_send_response(conn_id, opcode, + BLE_OTS_OACP_RESULT_INVALID_OBJECT, NULL, 0); + return; + } + + /* Priority 3: Check Delete property bit */ + if (!(obj->properties & BLE_OTS_OBJ_PROP_DELETE)) { + oacp_send_response(conn_id, opcode, + BLE_OTS_OACP_RESULT_PROCEDURE_NOT_PERMITTED, NULL, 0); + return; + } + + /* Priority 4 & 5: Check if locked or transfer in progress */ + if (obj->locked_by != BLE_OTS_CONN_ID_NONE) { + oacp_send_response(conn_id, opcode, + BLE_OTS_OACP_RESULT_OBJECT_LOCKED, NULL, 0); + return; + } + + /* Check if any transfer is active on this object */ + for (int i = 0; i < CONFIG_BLE_OTS_SERVER_MAX_CONCURRENCY; i++) { + ble_ots_server_conn_state_t *cs = &ble_ots_server_env.conn_pool[i]; + if (cs->in_pool && cs->transfer_active && + cs->transfer_object_id == obj_id) { + oacp_send_response(conn_id, opcode, + BLE_OTS_OACP_RESULT_OBJECT_LOCKED, NULL, 0); + return; + } + } + + /* Delete the object from the database */ + int rc = ble_ots_server_obj_db_delete(obj_id); + if (rc != 0) { + oacp_send_response(conn_id, opcode, + BLE_OTS_OACP_RESULT_OPERATION_FAILED, NULL, 0); + return; + } + + /* Remove deleted object from all connections' created_obj_ids tracking */ + for (int i = 0; i < CONFIG_BLE_OTS_SERVER_MAX_CONCURRENCY; i++) { + ble_ots_server_conn_state_t *cs = &ble_ots_server_env.conn_pool[i]; + if (!cs->in_pool) { + continue; + } + for (int j = 0; j < cs->created_obj_count; j++) { + if (cs->created_obj_ids[j] == obj_id) { + /* Shift remaining entries left */ + for (int k = j; k < cs->created_obj_count - 1; k++) { + cs->created_obj_ids[k] = cs->created_obj_ids[k + 1]; + } + cs->created_obj_count--; + break; + } + } + } + + /* Invalidate Current Object for all connections that had this object selected */ + for (int i = 0; i < CONFIG_BLE_OTS_SERVER_MAX_CONCURRENCY; i++) { + ble_ots_server_conn_state_t *cs = &ble_ots_server_env.conn_pool[i]; + if (cs->in_pool && cs->current_obj_valid && + cs->current_object_id == obj_id) { + cs->current_obj_valid = false; + cs->current_object_id = BLE_OTS_OBJ_ID_INVALID; + } + } + + /* Send success indication */ + oacp_send_response(conn_id, opcode, + BLE_OTS_OACP_RESULT_SUCCESS, NULL, 0); + + /* Dispatch EVT_OBJECT_DELETED to application */ + ble_ots_server_cb_param_t cb_param; + memset(&cb_param, 0, sizeof(cb_param)); + cb_param.object_deleted.object_id = obj_id; + ble_ots_server_dispatch_event(BLE_OTS_SERVER_EVT_OBJECT_DELETED, &cb_param); + + /* Dispatch Object Changed indication with Deletion flag + Source=Client */ + uint8_t flags = BLE_OTS_OBJ_CHANGED_FLAG_DELETION | BLE_OTS_OBJ_CHANGED_FLAG_SOURCE; + ble_ots_server_dispatch_obj_changed(obj_id, flags, conn_id); +} + +/***************************************************************************** + * OACP Calculate Checksum Procedure + *****************************************************************************/ +static void oacp_calculate_checksum_proc(uint16_t conn_id, const uint8_t *param, + uint16_t param_len) +{ + uint8_t opcode = BLE_OTS_OACP_OPCODE_CALCULATE_CHECKSUM; + + /* Priority 1: Check if Calculate Checksum is supported */ + if (!(ble_ots_server_env.config.oacp_features & BLE_OTS_OACP_FEAT_CALCULATE_CHECKSUM)) { + oacp_send_response(conn_id, opcode, + BLE_OTS_OACP_RESULT_OP_CODE_NOT_SUPPORTED, NULL, 0); + return; + } + + /* Priority 2: Check if Current Object is valid */ + ble_ots_obj_id_t obj_id = ble_ots_server_current_obj_get(conn_id); + if (obj_id == BLE_OTS_OBJ_ID_INVALID) { + oacp_send_response(conn_id, opcode, + BLE_OTS_OACP_RESULT_INVALID_OBJECT, NULL, 0); + return; + } + + ble_ots_server_obj_t *obj = ble_ots_server_obj_db_lookup(obj_id); + if (obj == NULL) { + oacp_send_response(conn_id, opcode, + BLE_OTS_OACP_RESULT_INVALID_OBJECT, NULL, 0); + return; + } + + /* Parse parameters: Offset (UINT32) + Length (UINT32) = 8 bytes */ + if (param_len != 8) { + oacp_send_response(conn_id, opcode, + BLE_OTS_OACP_RESULT_INVALID_PARAMETER, NULL, 0); + return; + } + + uint32_t offset = (uint32_t)param[0] | + ((uint32_t)param[1] << 8) | + ((uint32_t)param[2] << 16) | + ((uint32_t)param[3] << 24); + + uint32_t length = (uint32_t)param[4] | + ((uint32_t)param[5] << 8) | + ((uint32_t)param[6] << 16) | + ((uint32_t)param[7] << 24); + + /* Priority 3: The checksum range must be a non-empty range inside the + * object contents. A zero Length, or an Offset at or past the end of the + * object, selects no octets at all — same rule the Read procedure applies + * (see ble_ots_server_oacp_read_proc). */ + if (length == 0 || offset >= obj->current_size || + (uint64_t)offset + (uint64_t)length > obj->current_size) { + oacp_send_response(conn_id, opcode, + BLE_OTS_OACP_RESULT_INVALID_PARAMETER, NULL, 0); + return; + } + + /* Priority 4 & 5: Check if locked by another connection or transfer in progress */ + if (object_is_locked_or_transferring(obj_id, conn_id)) { + oacp_send_response(conn_id, opcode, + BLE_OTS_OACP_RESULT_OBJECT_LOCKED, NULL, 0); + return; + } + + /* Compute CRC-32 directly from the resident object data buffer */ + if (obj->data == NULL) { + oacp_send_response(conn_id, opcode, + BLE_OTS_OACP_RESULT_OPERATION_FAILED, NULL, 0); + return; + } + + /* Object data is already in memory — compute checksum without copying */ + uint32_t checksum = ble_ots_checksum_calculate(obj->data, offset, length); + + /* Build response parameter: Checksum (UINT32, little-endian) */ + uint8_t checksum_param[4]; + checksum_param[0] = (uint8_t)(checksum & 0xFF); + checksum_param[1] = (uint8_t)((checksum >> 8) & 0xFF); + checksum_param[2] = (uint8_t)((checksum >> 16) & 0xFF); + checksum_param[3] = (uint8_t)((checksum >> 24) & 0xFF); + + /* Send success indication with checksum */ + oacp_send_response(conn_id, opcode, + BLE_OTS_OACP_RESULT_SUCCESS, checksum_param, 4); + + /* Dispatch EVT_CHECKSUM_REQUEST to application */ + ble_ots_server_cb_param_t cb_param; + memset(&cb_param, 0, sizeof(cb_param)); + cb_param.checksum.object_id = obj_id; + cb_param.checksum.offset = offset; + cb_param.checksum.length = length; + cb_param.checksum.checksum = checksum; + ble_ots_server_dispatch_event(BLE_OTS_SERVER_EVT_CHECKSUM_REQUEST, &cb_param); +} + +/***************************************************************************** + * OACP Execute Procedure + *****************************************************************************/ +static void oacp_execute_proc(uint16_t conn_id, const uint8_t *param, + uint16_t param_len) +{ + uint8_t opcode = BLE_OTS_OACP_OPCODE_EXECUTE; + + /* Priority 1: Check if Execute is supported */ + if (!(ble_ots_server_env.config.oacp_features & BLE_OTS_OACP_FEAT_EXECUTE)) { + oacp_send_response(conn_id, opcode, + BLE_OTS_OACP_RESULT_OP_CODE_NOT_SUPPORTED, NULL, 0); + return; + } + + /* Priority 2: Check if Current Object is valid */ + ble_ots_obj_id_t obj_id = ble_ots_server_current_obj_get(conn_id); + if (obj_id == BLE_OTS_OBJ_ID_INVALID) { + oacp_send_response(conn_id, opcode, + BLE_OTS_OACP_RESULT_INVALID_OBJECT, NULL, 0); + return; + } + + ble_ots_server_obj_t *obj = ble_ots_server_obj_db_lookup(obj_id); + if (obj == NULL) { + oacp_send_response(conn_id, opcode, + BLE_OTS_OACP_RESULT_INVALID_OBJECT, NULL, 0); + return; + } + + /* Priority 3: Check Execute property bit */ + if (!(obj->properties & BLE_OTS_OBJ_PROP_EXECUTE)) { + oacp_send_response(conn_id, opcode, + BLE_OTS_OACP_RESULT_PROCEDURE_NOT_PERMITTED, NULL, 0); + return; + } + + /* Priority 4: Check if locked by another connection or transfer in progress */ + if (object_is_locked_or_transferring(obj_id, conn_id)) { + oacp_send_response(conn_id, opcode, + BLE_OTS_OACP_RESULT_OBJECT_LOCKED, NULL, 0); + return; + } + + /* The Result Code has to report whether the execution itself succeeded, so + * the application runs the action first. The callback is invoked + * synchronously in this context and may overwrite cb_param.execute.result. */ + ble_ots_server_cb_param_t cb_param; + memset(&cb_param, 0, sizeof(cb_param)); + cb_param.execute.object_id = obj_id; + cb_param.execute.param = (param_len > 0) ? param : NULL; + cb_param.execute.param_len = param_len; + cb_param.execute.result = BLE_OTS_OACP_RESULT_SUCCESS; + ble_ots_server_dispatch_event(BLE_OTS_SERVER_EVT_EXECUTE, &cb_param); + + /* Guard against an application returning a code outside the OACP range */ + uint8_t result = cb_param.execute.result; + if (result < BLE_OTS_OACP_RESULT_SUCCESS || + result > BLE_OTS_OACP_RESULT_OPERATION_FAILED) { + ESP_LOGE(TAG, "Invalid execute result 0x%02x from application", result); + result = BLE_OTS_OACP_RESULT_OPERATION_FAILED; + } + + /* Send the indication carrying the application's result */ + oacp_send_response(conn_id, opcode, result, NULL, 0); +} + +/***************************************************************************** + * OACP Read Procedure (delegate to ots_server_oacp_transfer.c) + *****************************************************************************/ +static int oacp_handle_read(uint16_t conn_id, const uint8_t *param, + uint16_t param_len) +{ + uint8_t opcode = BLE_OTS_OACP_OPCODE_READ; + + /* Parameter: Offset (UINT32) + Length (UINT32) = 8 bytes */ + if (param_len != 8) { + oacp_send_response(conn_id, opcode, + BLE_OTS_OACP_RESULT_INVALID_PARAMETER, NULL, 0); + return 0; + } + + uint32_t offset = (uint32_t)param[0] | + ((uint32_t)param[1] << 8) | + ((uint32_t)param[2] << 16) | + ((uint32_t)param[3] << 24); + + uint32_t length = (uint32_t)param[4] | + ((uint32_t)param[5] << 8) | + ((uint32_t)param[6] << 16) | + ((uint32_t)param[7] << 24); + + return ble_ots_server_oacp_read_proc(conn_id, offset, length); +} + +/***************************************************************************** + * OACP Write Procedure (delegate to ots_server_oacp_transfer.c) + *****************************************************************************/ +static int oacp_handle_write(uint16_t conn_id, const uint8_t *param, + uint16_t param_len) +{ + uint8_t opcode = BLE_OTS_OACP_OPCODE_WRITE; + + /* Parameter: Offset (UINT32) + Length (UINT32) + Mode (UINT8) = 9 bytes. + * All three fields are mandatory for the Write Op Code. */ + if (param_len != 9) { + oacp_send_response(conn_id, opcode, + BLE_OTS_OACP_RESULT_INVALID_PARAMETER, NULL, 0); + return 0; + } + + uint32_t offset = (uint32_t)param[0] | + ((uint32_t)param[1] << 8) | + ((uint32_t)param[2] << 16) | + ((uint32_t)param[3] << 24); + + uint32_t length = (uint32_t)param[4] | + ((uint32_t)param[5] << 8) | + ((uint32_t)param[6] << 16) | + ((uint32_t)param[7] << 24); + + uint8_t mode = param[8]; + + return ble_ots_server_oacp_write_proc(conn_id, offset, length, mode); +} + +/***************************************************************************** + * OACP Abort Procedure (delegate to ots_server_oacp_transfer.c) + *****************************************************************************/ +static int oacp_handle_abort(uint16_t conn_id) +{ + return ble_ots_server_oacp_abort_proc(conn_id); +} + +/***************************************************************************** + * OACP Characteristic Write Handler — main dispatch + * + * This is the GATT access callback for writes to the OACP characteristic. + * It is called from the GATT service table registered in ots_server_init.c. + *****************************************************************************/ +static int ble_ots_server_oacp_write_handler(uint16_t conn_id, uint16_t attr_handle, + struct ble_gatt_access_ctxt *ctxt, void *arg) +{ + uint8_t stack_buf[64]; + uint8_t *buf = stack_buf; + uint8_t *dyn_buf = NULL; + uint16_t data_len = 0; + int rc; + + /* Extract write data from mbuf */ + uint16_t om_len = OS_MBUF_PKTLEN(ctxt->om); + if (om_len == 0) { + return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN; + } + + if (om_len > sizeof(stack_buf)) { + /* Only the Execute opcode allows arbitrary-length parameters. + * Peek at the first byte (opcode) to decide whether to accept. */ + uint8_t peek_opcode; + rc = ble_hs_mbuf_to_flat(ctxt->om, &peek_opcode, 1, NULL); + if (rc != 0 || peek_opcode != BLE_OTS_OACP_OPCODE_EXECUTE) { + return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN; + } + + dyn_buf = malloc(om_len); + if (dyn_buf == NULL) { + return BLE_ATT_ERR_INSUFFICIENT_RES; + } + buf = dyn_buf; + } + + rc = ble_hs_mbuf_to_flat(ctxt->om, buf, om_len, &data_len); + if (rc != 0) { + free(dyn_buf); + return BLE_ATT_ERR_UNLIKELY; + } + + if (data_len < 1) { + free(dyn_buf); + return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN; + } + + uint8_t opcode = buf[0]; + const uint8_t *param = (data_len > 1) ? &buf[1] : NULL; + uint16_t param_len = data_len - 1; + + /* ATT-level check: concurrency */ + if (!ble_ots_server_concurrency_check(conn_id)) { + free(dyn_buf); + return BLE_OTS_APP_ERR_CONCURRENCY_LIMIT_EXCEEDED; + } + + /* ATT-level check: OACP CCCD must be configured for indications */ + if (!oacp_cccd_configured(conn_id)) { + free(dyn_buf); + return ATT_ERR_CCCD_IMPROPERLY_CONFIGURED; + } + + /* ATT-level check: procedure already in progress */ + ble_ots_server_conn_state_t *cs = ble_ots_server_conn_state_get(conn_id); + if (cs == NULL) { + free(dyn_buf); + return BLE_ATT_ERR_UNLIKELY; + } + + /* For Read/Write/Abort, check if a transfer is already active (only for + * non-abort opcodes when a transfer is in progress) */ + if (cs->transfer_active && opcode != BLE_OTS_OACP_OPCODE_ABORT) { + free(dyn_buf); + return ATT_ERR_PROC_ALREADY_IN_PROGRESS; + } + + /* ATT-level validation: parameter size for opcodes that require no parameter */ + switch (opcode) { + case BLE_OTS_OACP_OPCODE_DELETE: + case BLE_OTS_OACP_OPCODE_ABORT: + if (param_len != 0) { + free(dyn_buf); + return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN; + } + break; + + case BLE_OTS_OACP_OPCODE_CREATE: + /* Size (4) + Type UUID (2 or 16) = 6 or 20 */ + if (param_len < 6 || (param_len != 6 && param_len != 20)) { + free(dyn_buf); + return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN; + } + break; + + case BLE_OTS_OACP_OPCODE_CALCULATE_CHECKSUM: + /* Offset (4) + Length (4) = 8 */ + if (param_len != 8) { + free(dyn_buf); + return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN; + } + break; + + case BLE_OTS_OACP_OPCODE_READ: + /* Offset (4) + Length (4) = 8 */ + if (param_len != 8) { + free(dyn_buf); + return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN; + } + break; + + case BLE_OTS_OACP_OPCODE_WRITE: + /* Offset (4) + Length (4) + Mode (1) = 9 */ + if (param_len != 9) { + free(dyn_buf); + return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN; + } + break; + + case BLE_OTS_OACP_OPCODE_EXECUTE: + /* Optional parameter — any length is acceptable */ + break; + + default: + /* Unknown/reserved opcode — accept the write, then send Op Code Not Supported */ + break; + } + + /* ATT Write Response is sent by NimBLE stack (return 0). + * Now dispatch to the appropriate procedure handler. + * The procedure handler will send the OACP response indication. */ + + switch (opcode) { + case BLE_OTS_OACP_OPCODE_CREATE: + oacp_create_proc(conn_id, param, param_len); + break; + + case BLE_OTS_OACP_OPCODE_DELETE: + oacp_delete_proc(conn_id); + break; + + case BLE_OTS_OACP_OPCODE_CALCULATE_CHECKSUM: + oacp_calculate_checksum_proc(conn_id, param, param_len); + break; + + case BLE_OTS_OACP_OPCODE_EXECUTE: + oacp_execute_proc(conn_id, param, param_len); + break; + + case BLE_OTS_OACP_OPCODE_READ: + rc = oacp_handle_read(conn_id, param, param_len); + if (rc != 0) { + ESP_LOGE(TAG, "OACP Read proc failed, rc=%d", rc); + } + break; + + case BLE_OTS_OACP_OPCODE_WRITE: + rc = oacp_handle_write(conn_id, param, param_len); + if (rc != 0) { + ESP_LOGE(TAG, "OACP Write proc failed, rc=%d", rc); + } + break; + + case BLE_OTS_OACP_OPCODE_ABORT: + rc = oacp_handle_abort(conn_id); + if (rc != 0) { + ESP_LOGE(TAG, "OACP Abort proc failed, rc=%d", rc); + } + break; + + default: + /* Reserved or unknown opcode — respond with Op Code Not Supported */ + oacp_send_response(conn_id, opcode, + BLE_OTS_OACP_RESULT_OP_CODE_NOT_SUPPORTED, NULL, 0); + break; + } + + free(dyn_buf); + return 0; +} + +/***************************************************************************** + * OACP Characteristic Write — public wrapper + * + * Called from the central GATT access callback in ots_server_init.c. + *****************************************************************************/ +int ble_ots_server_oacp_write(uint16_t conn_handle, + struct ble_gatt_access_ctxt *ctxt) +{ + return ble_ots_server_oacp_write_handler(conn_handle, 0, ctxt, NULL); +} diff --git a/components/bt/ble_profiles/nimble/ble_ots/src/server/ots_server_oacp_transfer.c b/components/bt/ble_profiles/nimble/ble_ots/src/server/ots_server_oacp_transfer.c new file mode 100644 index 00000000000..a4bc4f2bfce --- /dev/null +++ b/components/bt/ble_profiles/nimble/ble_ots/src/server/ots_server_oacp_transfer.c @@ -0,0 +1,982 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include "esp_log.h" +#include "os/os_mbuf.h" +#include "host/ble_hs.h" +#include "host/ble_gap.h" +#include "host/ble_l2cap.h" +#include "nimble/nimble_port.h" +#include "ble_ots_common.h" +#include "ble_ots_server.h" +#include "ble_ots_server_int.h" + +static const char *TAG = "ots_oacp_transfer"; + +/** @brief Back-off before retrying a read chunk that hit a transient buffer shortage */ +#define OTS_TRANSFER_RETRY_DELAY_MS 20 + +/** @brief Consecutive back-off retries tolerated before a read transfer is failed */ +#define OTS_TRANSFER_RETRY_MAX 50 + +/***************************************************************************** + * Forward Declarations + *****************************************************************************/ +static void transfer_complete_read(ble_ots_server_conn_state_t *cs, + ble_ots_server_transfer_status_t status); +static void transfer_complete_write(ble_ots_server_conn_state_t *cs, + ble_ots_server_transfer_status_t status); +static int send_oacp_response(uint16_t conn_id, uint8_t req_opcode, + uint8_t result_code); +static int send_object_data_chunks(ble_ots_server_conn_state_t *cs); +static void ots_transfer_retry_cb(struct ble_npl_event *ev); + +/***************************************************************************** + * Helper: Build and send OACP response indication + *****************************************************************************/ +static int send_oacp_response(uint16_t conn_id, uint8_t req_opcode, + uint8_t result_code) +{ + uint8_t buf[3]; + buf[0] = BLE_OTS_OACP_OPCODE_RESPONSE; + buf[1] = req_opcode; + buf[2] = result_code; + return ble_ots_server_indicate_response(conn_id, BLE_OTS_UUID_OACP, buf, sizeof(buf)); +} + +/***************************************************************************** + * Helper: Get current timestamp + * + * Only meaningful when the application declared a real-time clock at init: the + * system clock is otherwise unset and would yield an "unknown" (zeroed) + * Date-Time that must not replace a timestamp the client already wrote. + *****************************************************************************/ +static bool get_current_timestamp(ble_ots_date_time_t *ts) +{ + memset(ts, 0, sizeof(*ts)); + + if (!ble_ots_server_env.config.has_realtime_clock) { + return false; + } + + time_t now = time(NULL); + struct tm tm_now; + if (now <= 0 || gmtime_r(&now, &tm_now) == NULL) { + return false; + } + + /* Date-Time only represents years 1582–9999; anything else is "unknown" */ + int year = tm_now.tm_year + 1900; + if (year < 1582 || year > 9999) { + return false; + } + + ts->year = (uint16_t)year; + ts->month = (uint8_t)(tm_now.tm_mon + 1); + ts->day = (uint8_t)tm_now.tm_mday; + ts->hours = (uint8_t)tm_now.tm_hour; + ts->minutes = (uint8_t)tm_now.tm_min; + /* Date-Time has no leap-second representation */ + ts->seconds = (uint8_t)(tm_now.tm_sec > 59 ? 59 : tm_now.tm_sec); + + return true; +} + +/***************************************************************************** + * Helper: Read-transfer back-off on transient buffer exhaustion + * + * A bulk read fills the host mbuf pool faster than the controller drains it. + * The resulting allocation failures are flow-control backpressure, not transfer + * failures, so the transfer pauses and resumes from a short callout instead of + * tearing the OTC channel down. + *****************************************************************************/ +static void transfer_retry_stop(ble_ots_server_conn_state_t *cs) +{ + if (cs->transfer_retry_timer_inited) { + ble_npl_callout_stop(&cs->transfer_retry_timer); + } + cs->transfer_retry_count = 0; +} + +/** + * @brief Pause the read transfer and schedule a retry of the current chunk. + * + * @param cs Connection state owning the transfer + * @param cause Human-readable reason, logged on retry and on give-up + * @return 0 when a retry is pending, BLE_HS_ENOMEM when the budget is exhausted + */ +static int transfer_backoff(ble_ots_server_conn_state_t *cs, const char *cause) +{ + if (cs->transfer_retry_count >= OTS_TRANSFER_RETRY_MAX) { + ESP_LOGE(TAG, "Read transfer aborted: %s persisted over %d retries", + cause, OTS_TRANSFER_RETRY_MAX); + return BLE_HS_ENOMEM; + } + + if (!cs->transfer_retry_timer_inited) { + int rc = ble_npl_callout_init(&cs->transfer_retry_timer, + nimble_port_get_dflt_eventq(), + ots_transfer_retry_cb, cs); + if (rc != 0) { + ESP_LOGE(TAG, "Failed to init transfer retry callout, rc=%d", rc); + return BLE_HS_ENOMEM; + } + cs->transfer_retry_timer_inited = true; + } + + cs->transfer_retry_count++; + ESP_LOGD(TAG, "Read transfer backpressure (%s), retry %u in %d ms", + cause, cs->transfer_retry_count, OTS_TRANSFER_RETRY_DELAY_MS); + + ble_npl_callout_reset(&cs->transfer_retry_timer, + ble_npl_time_ms_to_ticks32(OTS_TRANSFER_RETRY_DELAY_MS)); + return 0; +} + +/***************************************************************************** + * Helper: Send object data over L2CAP OTC in chunks + *****************************************************************************/ +static int send_object_data_chunks(ble_ots_server_conn_state_t *cs) +{ + if (!cs || !cs->transfer_active || !cs->transfer_is_read) { + return -1; + } + + struct ble_l2cap_chan *chan = cs->otc_chan; + if (!chan) { + return -1; + } + + /* Get channel info to determine MTU for chunking */ + struct ble_l2cap_chan_info chan_info; + int rc = ble_l2cap_get_chan_info(chan, &chan_info); + if (rc != 0) { + ESP_LOGE(TAG, "Failed to get L2CAP channel info, rc=%d", rc); + return rc; + } + + uint16_t mtu = chan_info.peer_coc_mtu; + if (mtu == 0) { + mtu = 256; /* fallback */ + } + + while (cs->transfer_bytes_done < cs->transfer_length) { + uint32_t remaining = cs->transfer_length - cs->transfer_bytes_done; + uint32_t chunk_size = (remaining > mtu) ? mtu : remaining; + + /* Allocate mbuf for this chunk. A failure here means the fragments + * already queued for the controller still hold the pool, so wait for + * them to drain rather than failing the transfer. */ + struct os_mbuf *sdu = os_msys_get_pkthdr(chunk_size, 0); + if (!sdu) { + return transfer_backoff(cs, "mbuf pool exhausted"); + } + + /* Read object data into a temporary buffer */ + uint8_t *tmp_buf = malloc(chunk_size); + if (!tmp_buf) { + os_mbuf_free_chain(sdu); + return transfer_backoff(cs, "temp buffer allocation failed"); + } + + rc = ble_ots_server_obj_data_read(cs->transfer_object_id, + cs->transfer_offset + cs->transfer_bytes_done, + chunk_size, tmp_buf); + if (rc != 0) { + free(tmp_buf); + os_mbuf_free_chain(sdu); + ESP_LOGE(TAG, "Failed to read object data, rc=%d", rc); + return rc; + } + + rc = os_mbuf_append(sdu, tmp_buf, (uint16_t)chunk_size); + free(tmp_buf); + if (rc != 0) { + os_mbuf_free_chain(sdu); + ESP_LOGE(TAG, "Failed to append data to mbuf, rc=%d", rc); + return rc; + } + + rc = ble_l2cap_send(chan, sdu); + if (rc == BLE_HS_ESTALLED) { + /* L2CAP credits exhausted. The stack keeps the SDU queued and + * flushes it once the peer grants more credits, so count the chunk + * as sent and resume when TX_UNSTALLED fires. */ + cs->transfer_bytes_done += chunk_size; + cs->transfer_retry_count = 0; + ble_ots_server_transfer_timer_reset(cs->conn_id); + ESP_LOGD(TAG, "L2CAP send stalled, bytes_done=%lu", (unsigned long)cs->transfer_bytes_done); + return 0; + } + + if (rc != 0) { + /* ble_l2cap_send() consumes the SDU only on success, and on + * ESTALLED where the stack retains it. On every other return the + * mbuf is still ours — including the internal failure paths, which + * detach it from the channel without freeing it. */ + os_mbuf_free_chain(sdu); + + /* Host or controller buffers ran dry for a moment; this is + * backpressure, not a transfer failure. */ + if (rc == BLE_HS_ENOMEM || rc == BLE_HS_EBUSY) { + return transfer_backoff(cs, "L2CAP TX buffers exhausted"); + } + + ESP_LOGE(TAG, "ble_l2cap_send failed, rc=%d", rc); + return rc; + } + + /* Chunk accepted: the back-off budget applies per chunk, not per transfer */ + cs->transfer_bytes_done += chunk_size; + cs->transfer_retry_count = 0; + ble_ots_server_transfer_timer_reset(cs->conn_id); + } + + /* All data sent successfully */ + if (cs->transfer_bytes_done >= cs->transfer_length) { + transfer_complete_read(cs, BLE_OTS_TRANSFER_SUCCESS); + } + + return 0; +} + +/***************************************************************************** + * Transfer Complete: Read + *****************************************************************************/ +static void transfer_complete_read(ble_ots_server_conn_state_t *cs, + ble_ots_server_transfer_status_t status) +{ + if (!cs) { + return; + } + + ble_ots_server_transfer_timer_stop(cs->conn_id); + transfer_retry_stop(cs); + + ble_ots_server_cb_param_t param; + memset(¶m, 0, sizeof(param)); + param.read_complete.object_id = cs->transfer_object_id; + param.read_complete.offset = cs->transfer_offset; + param.read_complete.length = cs->transfer_length; + param.read_complete.bytes_sent = cs->transfer_bytes_done; + param.read_complete.status = status; + + cs->transfer_active = false; + cs->transfer_is_read = false; + + ble_ots_server_dispatch_event(BLE_OTS_SERVER_EVT_READ_COMPLETE, ¶m); +} + +/***************************************************************************** + * Transfer Complete: Write + *****************************************************************************/ +static void transfer_complete_write(ble_ots_server_conn_state_t *cs, + ble_ots_server_transfer_status_t status) +{ + if (!cs) { + return; + } + + ble_ots_server_transfer_timer_stop(cs->conn_id); + + ble_ots_obj_id_t obj_id = cs->transfer_object_id; + uint32_t offset = cs->transfer_offset; + uint32_t bytes_received = cs->transfer_bytes_done; + + /* The object changed if data landed in it, or if the procedure already + * truncated it — a Truncate takes effect when the Write procedure starts, + * so it stands even when the transfer is later interrupted. */ + bool obj_modified = (bytes_received > 0) || cs->transfer_truncated; + + /* Update current size if the write extended the object */ + ble_ots_server_obj_t *obj = ble_ots_server_obj_db_lookup(obj_id); + if (obj) { + uint32_t new_end = offset + bytes_received; + if (new_end > obj->current_size) { + obj->current_size = new_end; + } + + /* Refresh Last-Modified only when the object actually changed and a + * real time source is available; otherwise keep the value the client + * or the application wrote instead of blanking it to "unknown". */ + ble_ots_date_time_t now; + if (obj_modified && get_current_timestamp(&now)) { + obj->last_modified = now; + } + } + + /* Unlock the object */ + ble_ots_server_obj_unlock(obj_id); + + cs->transfer_active = false; + cs->transfer_is_read = false; + cs->transfer_truncated = false; + + /* Dispatch Object Changed indication with content flag */ + if (status == BLE_OTS_TRANSFER_SUCCESS || obj_modified) { + uint8_t flags = BLE_OTS_OBJ_CHANGED_FLAG_SOURCE | BLE_OTS_OBJ_CHANGED_FLAG_CONTENT; + ble_ots_server_dispatch_obj_changed(obj_id, flags, cs->conn_id); + } + + /* Fire write complete event */ + ble_ots_server_cb_param_t param; + memset(¶m, 0, sizeof(param)); + param.write_complete.object_id = obj_id; + param.write_complete.offset = offset; + param.write_complete.bytes_received = bytes_received; + param.write_complete.status = status; + + ble_ots_server_dispatch_event(BLE_OTS_SERVER_EVT_WRITE_COMPLETE, ¶m); +} + +/***************************************************************************** + * Read-Transfer Back-off Retry Callback + * + * Fires in the NimBLE host task after transfer_backoff() paused a read that + * ran out of TX buffers. + *****************************************************************************/ +static void ots_transfer_retry_cb(struct ble_npl_event *ev) +{ + if (!BLE_OTS_SERVER_ENV_OK()) { + return; + } + + ble_ots_server_conn_state_t *cs = + (ble_ots_server_conn_state_t *)ble_npl_event_get_arg(ev); + if (!cs || !cs->in_pool || !cs->transfer_active || !cs->transfer_is_read) { + return; + } + + int rc = send_object_data_chunks(cs); + if (rc != 0) { + ESP_LOGE(TAG, "Failed to resume read transfer after back-off, rc=%d", rc); + ble_ots_server_otc_close(cs->conn_id); + transfer_complete_read(cs, BLE_OTS_TRANSFER_CHANNEL_CLOSED); + } +} + +/***************************************************************************** + * L2CAP OTC Data Receive Callback (for write transfers) + *****************************************************************************/ +void ble_ots_server_otc_receive_cb(uint16_t conn_handle, struct ble_l2cap_chan *chan, + struct os_mbuf *sdu_rx) +{ + ble_ots_server_conn_state_t *cs = ble_ots_server_conn_state_get(conn_handle); + if (!cs || !cs->transfer_active || cs->transfer_is_read) { + /* Not expecting data; free the mbuf */ + if (sdu_rx) { + os_mbuf_free_chain(sdu_rx); + } + return; + } + + if (!sdu_rx) { + return; + } + + /* Calculate total received data length from mbuf chain */ + uint16_t data_len = OS_MBUF_PKTLEN(sdu_rx); + if (data_len == 0) { + os_mbuf_free_chain(sdu_rx); + return; + } + + /* Check for excess data */ + uint32_t remaining = cs->transfer_length - cs->transfer_bytes_done; + if (data_len > remaining) { + ESP_LOGW(TAG, "Excess data received: got %u, expected at most %lu", + data_len, (unsigned long)remaining); + os_mbuf_free_chain(sdu_rx); + + /* Close OTC channel to prevent further data */ + ble_ots_server_otc_close(conn_handle); + transfer_complete_write(cs, BLE_OTS_TRANSFER_EXCESS_DATA); + return; + } + + /* Extract data from mbuf chain and write to object */ + uint32_t write_offset = cs->transfer_offset + cs->transfer_bytes_done; + struct os_mbuf *cur = sdu_rx; + uint32_t written = 0; + + int write_err = 0; + while (cur != NULL && written < data_len) { + if (cur->om_len > 0) { + int rc = ble_ots_server_obj_data_write(cs->transfer_object_id, + write_offset + written, + cur->om_data, cur->om_len); + if (rc != 0) { + ESP_LOGE(TAG, "Failed to write object data, rc=%d", rc); + write_err = rc; + break; + } + written += cur->om_len; + } + cur = SLIST_NEXT(cur, om_next); + } + + os_mbuf_free_chain(sdu_rx); + + cs->transfer_bytes_done += written; + + /* If writing to the object database failed, abort the transfer immediately */ + if (write_err != 0) { + ESP_LOGE(TAG, "Aborting write transfer due to obj_data_write failure, rc=%d", write_err); + ble_ots_server_otc_close(conn_handle); + transfer_complete_write(cs, BLE_OTS_TRANSFER_CHANNEL_CLOSED); + return; + } + + /* Reset inactivity timer */ + ble_ots_server_transfer_timer_reset(cs->conn_id); + + ESP_LOGD(TAG, "Write transfer: received %lu/%lu bytes", + (unsigned long)cs->transfer_bytes_done, + (unsigned long)cs->transfer_length); + + /* Check if transfer is complete */ + if (cs->transfer_bytes_done >= cs->transfer_length) { + transfer_complete_write(cs, BLE_OTS_TRANSFER_SUCCESS); + } +} + +/***************************************************************************** + * L2CAP OTC TX Un-stalled Callback (for read transfers) + *****************************************************************************/ +void ble_ots_server_otc_tx_unstalled_cb(uint16_t conn_handle, + struct ble_l2cap_chan *chan) +{ + ble_ots_server_conn_state_t *cs = ble_ots_server_conn_state_get(conn_handle); + if (!cs || !cs->transfer_active || !cs->transfer_is_read) { + return; + } + + /* Resume sending data */ + int rc = send_object_data_chunks(cs); + if (rc != 0) { + ESP_LOGE(TAG, "Failed to resume read transfer, rc=%d", rc); + ble_ots_server_otc_close(conn_handle); + transfer_complete_read(cs, BLE_OTS_TRANSFER_CHANNEL_CLOSED); + } +} + +/***************************************************************************** + * L2CAP OTC Channel Closed Callback + *****************************************************************************/ +void ble_ots_server_otc_disconnected_cb(uint16_t conn_handle, + struct ble_l2cap_chan *chan) +{ + ble_ots_server_conn_state_t *cs = ble_ots_server_conn_state_get(conn_handle); + if (!cs) { + return; + } + + /* Clear the OTC channel reference */ + cs->otc_chan = NULL; + + if (!cs->transfer_active) { + return; + } + + ESP_LOGW(TAG, "OTC channel closed during transfer on conn_id=%d", conn_handle); + + if (cs->transfer_is_read) { + transfer_complete_read(cs, BLE_OTS_TRANSFER_CHANNEL_CLOSED); + } else { + transfer_complete_write(cs, BLE_OTS_TRANSFER_CHANNEL_CLOSED); + } +} + +/***************************************************************************** + * Deferred read-data send via NimBLE event queue + * + * The initial burst of object data must be sent AFTER the OACP response + * indication. Both are posted to the NimBLE default event queue (FIFO), so + * queueing the data-send event after the indication event guarantees ordering. + *****************************************************************************/ +static void ots_transfer_data_event_cb(struct ble_npl_event *ev) +{ + /* cs points into the control block — only valid while it is allocated */ + if (!BLE_OTS_SERVER_ENV_OK()) { + return; + } + + ble_ots_server_conn_state_t *cs = + (ble_ots_server_conn_state_t *)ble_npl_event_get_arg(ev); + if (!cs || !cs->in_pool || !cs->transfer_active || !cs->transfer_is_read) { + return; + } + + int rc = send_object_data_chunks(cs); + if (rc != 0) { + ESP_LOGE(TAG, "Failed to send object data (event), rc=%d", rc); + ble_ots_server_otc_close(cs->conn_id); + transfer_complete_read(cs, BLE_OTS_TRANSFER_CHANNEL_CLOSED); + } +} + +static void ots_transfer_data_schedule(ble_ots_server_conn_state_t *cs) +{ + if (!cs->transfer_data_ev_inited) { + ble_npl_event_init(&cs->transfer_data_ev, ots_transfer_data_event_cb, cs); + cs->transfer_data_ev_inited = true; + } + ble_npl_eventq_put(nimble_port_get_dflt_eventq(), &cs->transfer_data_ev); +} + +/***************************************************************************** + * OACP Read Procedure + *****************************************************************************/ +int ble_ots_server_oacp_read_proc(uint16_t conn_id, uint32_t offset, uint32_t length) +{ + ble_ots_server_conn_state_t *cs = ble_ots_server_conn_state_get(conn_id); + if (!cs) { + return -1; + } + + /* Priority 1: Check OACP Read feature support (bit 4) */ + if (!(ble_ots_server_env.ots_feature.oacp_features & BLE_OTS_OACP_FEAT_READ)) { + send_oacp_response(conn_id, BLE_OTS_OACP_OPCODE_READ, + BLE_OTS_OACP_RESULT_OP_CODE_NOT_SUPPORTED); + return 0; + } + + /* Priority 2: Check Current Object is valid */ + if (!cs->current_obj_valid) { + send_oacp_response(conn_id, BLE_OTS_OACP_OPCODE_READ, + BLE_OTS_OACP_RESULT_INVALID_OBJECT); + return 0; + } + + ble_ots_obj_id_t obj_id = cs->current_object_id; + ble_ots_server_obj_t *obj = ble_ots_server_obj_db_lookup(obj_id); + if (!obj) { + send_oacp_response(conn_id, BLE_OTS_OACP_OPCODE_READ, + BLE_OTS_OACP_RESULT_INVALID_OBJECT); + return 0; + } + + /* Priority 3: Check object Read property (bit 2) */ + if (!(obj->properties & BLE_OTS_OBJ_PROP_READ)) { + send_oacp_response(conn_id, BLE_OTS_OACP_OPCODE_READ, + BLE_OTS_OACP_RESULT_PROCEDURE_NOT_PERMITTED); + return 0; + } + + /* Priority 4: Check OTC channel available */ + struct ble_l2cap_chan *chan = ble_ots_server_otc_get(conn_id); + if (!chan) { + send_oacp_response(conn_id, BLE_OTS_OACP_OPCODE_READ, + BLE_OTS_OACP_RESULT_CHANNEL_UNAVAILABLE); + return 0; + } + + /* Priority 5: Offset exceeds Current Size */ + if (offset > obj->current_size) { + send_oacp_response(conn_id, BLE_OTS_OACP_OPCODE_READ, + BLE_OTS_OACP_RESULT_INVALID_PARAMETER); + return 0; + } + + /* Offset and Length are raw client input: reject a sum that would wrap + * before it can slip past the Priority 6 bounds check below. */ + if (length > 0 && offset > UINT32_MAX - length) { + send_oacp_response(conn_id, BLE_OTS_OACP_OPCODE_READ, + BLE_OTS_OACP_RESULT_INVALID_PARAMETER); + return 0; + } + + /* Priority 6: Offset + Length exceeds Current Size */ + if ((offset + length) > obj->current_size) { + send_oacp_response(conn_id, BLE_OTS_OACP_OPCODE_READ, + BLE_OTS_OACP_RESULT_INVALID_PARAMETER); + return 0; + } + + /* Priority 7: Length exceeds server capacity to read + * (we assume we can always read from our own DB, so skip unless length is 0) */ + if (length == 0) { + send_oacp_response(conn_id, BLE_OTS_OACP_OPCODE_READ, + BLE_OTS_OACP_RESULT_INVALID_PARAMETER); + return 0; + } + + /* Priority 8: Object locked by another client */ + if (obj->locked_by != BLE_OTS_CONN_ID_NONE && obj->locked_by != conn_id) { + send_oacp_response(conn_id, BLE_OTS_OACP_OPCODE_READ, + BLE_OTS_OACP_RESULT_OBJECT_LOCKED); + return 0; + } + + /* Priority 9: Transfer already in progress on this object */ + if (cs->transfer_active) { + send_oacp_response(conn_id, BLE_OTS_OACP_OPCODE_READ, + BLE_OTS_OACP_RESULT_OBJECT_LOCKED); + return 0; + } + + /* All checks passed — mark transfer active */ + cs->transfer_active = true; + cs->transfer_is_read = true; + cs->transfer_object_id = obj_id; + cs->transfer_offset = offset; + cs->transfer_length = length; + cs->transfer_bytes_done = 0; + transfer_retry_stop(cs); + + /* Send Success indication */ + int rc = send_oacp_response(conn_id, BLE_OTS_OACP_OPCODE_READ, + BLE_OTS_OACP_RESULT_SUCCESS); + if (rc != 0) { + ESP_LOGE(TAG, "Failed to send OACP Read success indication, rc=%d", rc); + cs->transfer_active = false; + cs->transfer_is_read = false; + return rc; + } + + /* Start transfer inactivity timer */ + ble_ots_server_transfer_timer_start(conn_id); + + /* Queue the object data send as a NimBLE event. Because the response + * indication was also queued (via ble_ots_server_indicate_response) and the + * default event queue is FIFO, the data is sent AFTER the indication. */ + ots_transfer_data_schedule(cs); + + return 0; +} + +/***************************************************************************** + * OACP Write Procedure + *****************************************************************************/ +int ble_ots_server_oacp_write_proc(uint16_t conn_id, uint32_t offset, + uint32_t length, uint8_t mode) +{ + ble_ots_server_conn_state_t *cs = ble_ots_server_conn_state_get(conn_id); + if (!cs) { + return -1; + } + + bool truncate = (mode & BLE_OTS_OACP_WRITE_MODE_TRUNCATE) != 0; + + /* Priority 1: Check OACP Write feature support (bit 5) */ + if (!(ble_ots_server_env.ots_feature.oacp_features & BLE_OTS_OACP_FEAT_WRITE)) { + send_oacp_response(conn_id, BLE_OTS_OACP_OPCODE_WRITE, + BLE_OTS_OACP_RESULT_OP_CODE_NOT_SUPPORTED); + return 0; + } + + /* Priority 2: Check Current Object is valid */ + if (!cs->current_obj_valid) { + send_oacp_response(conn_id, BLE_OTS_OACP_OPCODE_WRITE, + BLE_OTS_OACP_RESULT_INVALID_OBJECT); + return 0; + } + + ble_ots_obj_id_t obj_id = cs->current_object_id; + ble_ots_server_obj_t *obj = ble_ots_server_obj_db_lookup(obj_id); + if (!obj) { + send_oacp_response(conn_id, BLE_OTS_OACP_OPCODE_WRITE, + BLE_OTS_OACP_RESULT_INVALID_OBJECT); + return 0; + } + + /* Priority 3: Check object Write property (bit 3) */ + if (!(obj->properties & BLE_OTS_OBJ_PROP_WRITE)) { + send_oacp_response(conn_id, BLE_OTS_OACP_OPCODE_WRITE, + BLE_OTS_OACP_RESULT_PROCEDURE_NOT_PERMITTED); + return 0; + } + + /* Check for integer overflow in offset + length (before any arithmetic) */ + if (length > 0 && offset > UINT32_MAX - length) { + send_oacp_response(conn_id, BLE_OTS_OACP_OPCODE_WRITE, + BLE_OTS_OACP_RESULT_INVALID_PARAMETER); + return 0; + } + + /* Priority 4: Patching check — if not truncating and offset + length <= current_size, + * this is a patch operation. Check OACP Patch feature (bit 8). */ + if (!truncate && (offset + length) <= obj->current_size) { + if (!(ble_ots_server_env.ots_feature.oacp_features & BLE_OTS_OACP_FEAT_PATCH)) { + send_oacp_response(conn_id, BLE_OTS_OACP_OPCODE_WRITE, + BLE_OTS_OACP_RESULT_PROCEDURE_NOT_PERMITTED); + return 0; + } + + /* Priority 5: Check object Patch property (bit 6) */ + if (!(obj->properties & BLE_OTS_OBJ_PROP_PATCH)) { + send_oacp_response(conn_id, BLE_OTS_OACP_OPCODE_WRITE, + BLE_OTS_OACP_RESULT_PROCEDURE_NOT_PERMITTED); + return 0; + } + } + + /* Priority 6: Truncation check — if truncate bit set, check Truncate property (bit 5) */ + if (truncate) { + if (!(ble_ots_server_env.ots_feature.oacp_features & BLE_OTS_OACP_FEAT_TRUNCATE)) { + send_oacp_response(conn_id, BLE_OTS_OACP_OPCODE_WRITE, + BLE_OTS_OACP_RESULT_PROCEDURE_NOT_PERMITTED); + return 0; + } + if (!(obj->properties & BLE_OTS_OBJ_PROP_TRUNCATE)) { + send_oacp_response(conn_id, BLE_OTS_OACP_OPCODE_WRITE, + BLE_OTS_OACP_RESULT_PROCEDURE_NOT_PERMITTED); + return 0; + } + } + + /* Priority 7: Check OTC channel available */ + struct ble_l2cap_chan *chan = ble_ots_server_otc_get(conn_id); + if (!chan) { + send_oacp_response(conn_id, BLE_OTS_OACP_OPCODE_WRITE, + BLE_OTS_OACP_RESULT_CHANNEL_UNAVAILABLE); + return 0; + } + + /* Priority 8: Check RFU bits in mode (bits 0, 2-7 except bit 1) */ + uint8_t rfu_mask = (uint8_t)~BLE_OTS_OACP_WRITE_MODE_TRUNCATE; + if (mode & rfu_mask) { + send_oacp_response(conn_id, BLE_OTS_OACP_OPCODE_WRITE, + BLE_OTS_OACP_RESULT_INVALID_PARAMETER); + return 0; + } + + /* Priority 9: Offset exceeds Current Size (for non-truncate, offset must be <= current_size) */ + if (!truncate && offset > obj->current_size) { + send_oacp_response(conn_id, BLE_OTS_OACP_OPCODE_WRITE, + BLE_OTS_OACP_RESULT_INVALID_PARAMETER); + return 0; + } + /* For truncate mode, offset can be <= current_size (truncation sets current_size = offset) */ + if (truncate && offset > obj->current_size) { + send_oacp_response(conn_id, BLE_OTS_OACP_OPCODE_WRITE, + BLE_OTS_OACP_RESULT_INVALID_PARAMETER); + return 0; + } + + /* Priority 10: Offset + Length exceeds Allocated Size and no append support */ + bool needs_append = false; + if ((offset + length) > obj->allocated_size) { + needs_append = true; + } + + if (needs_append && !(ble_ots_server_env.ots_feature.oacp_features & BLE_OTS_OACP_FEAT_APPEND)) { + send_oacp_response(conn_id, BLE_OTS_OACP_OPCODE_WRITE, + BLE_OTS_OACP_RESULT_INVALID_PARAMETER); + return 0; + } + + /* Priority 11: Length exceeds server capacity to write. + * A plain write of zero octets selects nothing and is rejected. Combined + * with Truncate it is meaningful — Truncate sets the Current Size to Offset + * plus the octets written, so Length 0 is how a client shrinks an object + * without supplying new content. */ + if (!truncate && length == 0) { + send_oacp_response(conn_id, BLE_OTS_OACP_OPCODE_WRITE, + BLE_OTS_OACP_RESULT_INVALID_PARAMETER); + return 0; + } + + /* Priority 12: Object locked by another client */ + if (obj->locked_by != BLE_OTS_CONN_ID_NONE && obj->locked_by != conn_id) { + send_oacp_response(conn_id, BLE_OTS_OACP_OPCODE_WRITE, + BLE_OTS_OACP_RESULT_OBJECT_LOCKED); + return 0; + } + + /* Priority 13: Transfer already in progress */ + if (cs->transfer_active) { + send_oacp_response(conn_id, BLE_OTS_OACP_OPCODE_WRITE, + BLE_OTS_OACP_RESULT_OBJECT_LOCKED); + return 0; + } + + /* Supplementary Append checks (only when needs_append is true) */ + if (needs_append) { + /* Append Priority 1: Check object Append property (bit 4) */ + if (!(obj->properties & BLE_OTS_OBJ_PROP_APPEND)) { + send_oacp_response(conn_id, BLE_OTS_OACP_OPCODE_WRITE, + BLE_OTS_OACP_RESULT_PROCEDURE_NOT_PERMITTED); + return 0; + } + + /* Append Priority 2: Check server capacity to increase allocated size. + * The actual reallocation is deferred to ble_ots_server_obj_data_write + * when data chunks arrive. Do not update obj->allocated_size here. */ + } + + /* Lock the object under concurrency */ + if (CONFIG_BLE_OTS_SERVER_MAX_CONCURRENCY > 1) { + int lock_rc = ble_ots_server_obj_lock(obj_id, conn_id); + if (lock_rc != 0) { + send_oacp_response(conn_id, BLE_OTS_OACP_OPCODE_WRITE, + BLE_OTS_OACP_RESULT_OBJECT_LOCKED); + return 0; + } + } + + /* Mark transfer active */ + cs->transfer_active = true; + cs->transfer_is_read = false; + cs->transfer_object_id = obj_id; + cs->transfer_offset = offset; + cs->transfer_length = length; + cs->transfer_bytes_done = 0; + cs->transfer_truncated = false; + + /* Send Success indication */ + int rc = send_oacp_response(conn_id, BLE_OTS_OACP_OPCODE_WRITE, + BLE_OTS_OACP_RESULT_SUCCESS); + if (rc != 0) { + ESP_LOGE(TAG, "Failed to send OACP Write success indication, rc=%d", rc); + cs->transfer_active = false; + ble_ots_server_obj_unlock(obj_id); + return rc; + } + + /* Perform truncation if requested (deferred until after successful response) */ + if (truncate) { + obj->current_size = offset; + cs->transfer_truncated = true; + } + + /* A Truncate-only request carries no object data, so the procedure is + * already done. Completing it here releases the object lock and emits the + * Object Changed indication instead of leaving a transfer that could only + * ever end on the inactivity timeout. */ + if (length == 0) { + ESP_LOGI(TAG, "Object truncated: obj_id=0x%llx, current_size=%lu", + (unsigned long long)obj_id, (unsigned long)offset); + transfer_complete_write(cs, BLE_OTS_TRANSFER_SUCCESS); + return 0; + } + + /* Start transfer inactivity timer */ + ble_ots_server_transfer_timer_start(conn_id); + + ESP_LOGI(TAG, "Write transfer started: obj_id=0x%llx, offset=%lu, length=%lu, mode=0x%02x", + (unsigned long long)obj_id, (unsigned long)offset, + (unsigned long)length, mode); + + return 0; +} + +/***************************************************************************** + * OACP Abort Procedure + *****************************************************************************/ +int ble_ots_server_oacp_abort_proc(uint16_t conn_id) +{ + ble_ots_server_conn_state_t *cs = ble_ots_server_conn_state_get(conn_id); + if (!cs) { + return -1; + } + + /* Priority 1: Check OACP Abort feature support (bit 9) */ + if (!(ble_ots_server_env.ots_feature.oacp_features & BLE_OTS_OACP_FEAT_ABORT)) { + send_oacp_response(conn_id, BLE_OTS_OACP_OPCODE_ABORT, + BLE_OTS_OACP_RESULT_OP_CODE_NOT_SUPPORTED); + return 0; + } + + /* Priority 2: No Read operation in progress */ + if (!cs->transfer_active || !cs->transfer_is_read) { + send_oacp_response(conn_id, BLE_OTS_OACP_OPCODE_ABORT, + BLE_OTS_OACP_RESULT_OPERATION_FAILED); + return 0; + } + + /* Priority 3: Abort must come from the same client that initiated the read. + * Since cs is per-connection and we already matched conn_id, this is inherently satisfied. + * But we verify the transfer is on this connection's state. */ + + /* Cancel the in-progress read transfer */ + ble_ots_server_transfer_timer_stop(conn_id); + transfer_retry_stop(cs); + + /* Save transfer info before clearing */ + ble_ots_obj_id_t obj_id = cs->transfer_object_id; + uint32_t transfer_offset = cs->transfer_offset; + uint32_t transfer_length = cs->transfer_length; + uint32_t bytes_sent = cs->transfer_bytes_done; + + /* Mark transfer as no longer active */ + cs->transfer_active = false; + cs->transfer_is_read = false; + + /* Send Success indication for Abort */ + int rc = send_oacp_response(conn_id, BLE_OTS_OACP_OPCODE_ABORT, + BLE_OTS_OACP_RESULT_SUCCESS); + if (rc != 0) { + ESP_LOGE(TAG, "Failed to send OACP Abort success indication, rc=%d", rc); + } + + /* Fire Read Complete event with ABORTED status */ + ble_ots_server_cb_param_t param; + memset(¶m, 0, sizeof(param)); + param.read_complete.object_id = obj_id; + param.read_complete.offset = transfer_offset; + param.read_complete.length = transfer_length; + param.read_complete.bytes_sent = bytes_sent; + param.read_complete.status = BLE_OTS_TRANSFER_ABORTED; + + ble_ots_server_dispatch_event(BLE_OTS_SERVER_EVT_READ_COMPLETE, ¶m); + + ESP_LOGI(TAG, "Read transfer aborted: obj_id=0x%llx, bytes_sent=%lu/%lu", + (unsigned long long)obj_id, (unsigned long)bytes_sent, + (unsigned long)transfer_length); + + return 0; +} + +/***************************************************************************** + * Non-static wrappers (called from ots_server_init.c via extern) + *****************************************************************************/ + +/** + * @brief Transfer timeout handler called from the init module. + * + * Delegates to the internal timeout callback logic. + * + * @param conn_id BLE connection identifier + */ +void ble_ots_server_oacp_transfer_timeout(uint16_t conn_id) +{ + ble_ots_server_conn_state_t *cs = ble_ots_server_conn_state_get(conn_id); + if (!cs || !cs->transfer_active) { + return; + } + + ESP_LOGW(TAG, "Transfer timeout on conn_id=%d", conn_id); + + if (cs->transfer_is_read) { + ble_ots_server_otc_close(conn_id); + transfer_complete_read(cs, BLE_OTS_TRANSFER_TIMEOUT); + } else { + ble_ots_server_otc_close(conn_id); + transfer_complete_write(cs, BLE_OTS_TRANSFER_TIMEOUT); + } +} + +/** + * @brief L2CAP COC data receive handler called from the init module. + * + * Handles incoming L2CAP COC data during write transfers by delegating + * to the internal receive callback. + * + * @param conn_handle BLE connection handle + * @param chan L2CAP channel + * @param sdu_rx Received SDU mbuf chain + * @return 0 on success + */ +int ble_ots_server_oacp_l2cap_recv(uint16_t conn_handle, + struct ble_l2cap_chan *chan, + struct os_mbuf *sdu_rx) +{ + ble_ots_server_otc_receive_cb(conn_handle, chan, sdu_rx); + return 0; +} diff --git a/components/bt/ble_profiles/nimble/ble_ots/src/server/ots_server_olcp.c b/components/bt/ble_profiles/nimble/ble_ots/src/server/ots_server_olcp.c new file mode 100644 index 00000000000..26dbcd157ed --- /dev/null +++ b/components/bt/ble_profiles/nimble/ble_ots/src/server/ots_server_olcp.c @@ -0,0 +1,586 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include "esp_log.h" +#include "host/ble_hs.h" +#include "host/ble_gatt.h" +#include "host/ble_gap.h" +#include "os/os_mbuf.h" +#include "ble_ots_server_int.h" + +static const char *TAG = "ots_olcp"; + +/***************************************************************************** + * Helper: Check if OLCP CCCD is configured for indications + * + * Per the OTS specification, if the OLCP CCCD has not been configured for + * indications, the server must reject any write with the ATT error + * "Client Characteristic Configuration Descriptor Improperly Configured" + * (0xFD). Subscription state is tracked via BLE_GAP_EVENT_SUBSCRIBE events. + *****************************************************************************/ +static bool olcp_cccd_configured(uint16_t conn_id) +{ + /* Verify connection exists */ + struct ble_gap_conn_desc desc; + if (ble_gap_conn_find(conn_id, &desc) != 0) { + return false; + } + + /* Verify OLCP value handle is registered */ + if (ble_ots_server_olcp_handle_get() == 0) { + return false; + } + + /* Check the per-connection OLCP indication subscription state */ + ble_ots_server_conn_state_t *state = ble_ots_server_conn_state_get(conn_id); + if (state == NULL) { + return false; + } + + return state->olcp_indicate_subscribed; +} + +/***************************************************************************** + * Handle BLE_GAP_EVENT_SUBSCRIBE for OLCP CCCD tracking + *****************************************************************************/ +void ble_ots_server_olcp_handle_subscribe(uint16_t conn_handle, + uint16_t attr_handle, + bool cur_indicate) +{ + if (attr_handle != ble_ots_server_olcp_handle_get()) { + return; + } + + ble_ots_server_conn_state_t *state = ble_ots_server_conn_state_get(conn_handle); + if (state == NULL) { + return; + } + + state->olcp_indicate_subscribed = cur_indicate; + ESP_LOGI(TAG, "OLCP CCCD subscribe: conn=%d indicate=%d", + conn_handle, cur_indicate); +} + +/***************************************************************************** + * Helper: Check if the object database contains any user objects + * + * The Directory Listing Object is always present by default, so it is skipped + * here to reflect whether any user object exists. + *****************************************************************************/ +bool ble_ots_server_obj_db_has_objects(void) +{ + for (int i = 0; i < CONFIG_BLE_OTS_SERVER_MAX_OBJECTS; i++) { + ble_ots_server_obj_t *obj = &ble_ots_server_env.obj_db[i]; + if (obj->in_use && obj->object_id != BLE_OTS_OBJ_ID_DIRECTORY_LISTING) { + return true; + } + } + return false; +} + +/***************************************************************************** + * Helper: Check if a sort order value is valid + *****************************************************************************/ +static bool is_valid_sort_order(uint8_t order) +{ + switch (order) { + case BLE_OTS_SORT_ORDER_NAME_ASC: + case BLE_OTS_SORT_ORDER_TYPE_ASC: + case BLE_OTS_SORT_ORDER_CURRENT_SIZE_ASC: + case BLE_OTS_SORT_ORDER_FIRST_CREATED_ASC: + case BLE_OTS_SORT_ORDER_LAST_MODIFIED_ASC: + case BLE_OTS_SORT_ORDER_NAME_DESC: + case BLE_OTS_SORT_ORDER_TYPE_DESC: + case BLE_OTS_SORT_ORDER_CURRENT_SIZE_DESC: + case BLE_OTS_SORT_ORDER_FIRST_CREATED_DESC: + case BLE_OTS_SORT_ORDER_LAST_MODIFIED_DESC: + return true; + default: + return false; + } +} + +/***************************************************************************** + * Helper: Check if an opcode is supported based on OLCP features + *****************************************************************************/ +static bool olcp_opcode_supported(uint8_t opcode) +{ + uint32_t olcp_feat = ble_ots_server_env.ots_feature.olcp_features; + + switch (opcode) { + /* Mandatory opcodes - always supported */ + case BLE_OTS_OLCP_OPCODE_FIRST: + case BLE_OTS_OLCP_OPCODE_LAST: + case BLE_OTS_OLCP_OPCODE_PREVIOUS: + case BLE_OTS_OLCP_OPCODE_NEXT: + return true; + + /* Optional opcodes - check feature bits */ + case BLE_OTS_OLCP_OPCODE_GO_TO: + return (olcp_feat & BLE_OTS_OLCP_FEAT_GO_TO) != 0; + case BLE_OTS_OLCP_OPCODE_ORDER: + return (olcp_feat & BLE_OTS_OLCP_FEAT_ORDER) != 0; + case BLE_OTS_OLCP_OPCODE_REQUEST_NUM_OF_OBJECTS: + return (olcp_feat & BLE_OTS_OLCP_FEAT_REQUEST_NUM_OF_OBJECTS) != 0; + case BLE_OTS_OLCP_OPCODE_CLEAR_MARKING: + return (olcp_feat & BLE_OTS_OLCP_FEAT_CLEAR_MARKING) != 0; + + default: + return false; + } +} + +/***************************************************************************** + * Helper: Get expected parameter length for an opcode + *****************************************************************************/ +static int olcp_get_param_len(uint8_t opcode) +{ + switch (opcode) { + case BLE_OTS_OLCP_OPCODE_FIRST: + case BLE_OTS_OLCP_OPCODE_LAST: + case BLE_OTS_OLCP_OPCODE_PREVIOUS: + case BLE_OTS_OLCP_OPCODE_NEXT: + case BLE_OTS_OLCP_OPCODE_REQUEST_NUM_OF_OBJECTS: + case BLE_OTS_OLCP_OPCODE_CLEAR_MARKING: + return 0; + case BLE_OTS_OLCP_OPCODE_GO_TO: + return 6; /* UINT48 Object ID */ + case BLE_OTS_OLCP_OPCODE_ORDER: + return 1; /* UINT8 sort order */ + default: + return -1; /* Unknown opcode */ + } +} + +/***************************************************************************** + * Helper: Send OLCP response indication + *****************************************************************************/ +static void olcp_send_response(uint16_t conn_id, uint8_t request_opcode, + uint8_t result_code, const uint8_t *param, + uint16_t param_len) +{ + /* Response format: [0x70, request_opcode, result_code, optional_param] */ + uint8_t resp_buf[3 + 4]; /* Max: 3 header + 4 bytes param (UINT32) */ + uint16_t resp_len = 3; + + resp_buf[0] = BLE_OTS_OLCP_OPCODE_RESPONSE; /* 0x70 */ + resp_buf[1] = request_opcode; + resp_buf[2] = result_code; + + if (param != NULL && param_len > 0 && param_len <= 4) { + memcpy(&resp_buf[3], param, param_len); + resp_len += param_len; + } + + int rc = ble_ots_server_indicate_response(conn_id, BLE_OTS_UUID_OLCP, + resp_buf, resp_len); + if (rc != 0) { + ESP_LOGE(TAG, "Failed to send OLCP response indication; rc=%d", rc); + } +} + +/***************************************************************************** + * Procedure: First (Op Code 0x01) + * Note: The caller evaluates the filtered list and the No Object check, so + * @p list is guaranteed non-NULL and non-empty here. + *****************************************************************************/ +static void olcp_proc_first(uint16_t conn_id, uint8_t opcode, + const ble_ots_server_filtered_list_t *list) +{ + int rc = ble_ots_server_current_obj_set(conn_id, list->object_ids[0]); + if (rc != 0) { + olcp_send_response(conn_id, opcode, BLE_OTS_OLCP_RESULT_OPERATION_FAILED, + NULL, 0); + return; + } + + olcp_send_response(conn_id, opcode, BLE_OTS_OLCP_RESULT_SUCCESS, NULL, 0); +} + +/***************************************************************************** + * Procedure: Last (Op Code 0x02) + * Note: The caller evaluates the filtered list and the No Object check, so + * @p list is guaranteed non-NULL and non-empty here. + *****************************************************************************/ +static void olcp_proc_last(uint16_t conn_id, uint8_t opcode, + const ble_ots_server_filtered_list_t *list) +{ + int rc = ble_ots_server_current_obj_set(conn_id, + list->object_ids[list->count - 1]); + if (rc != 0) { + olcp_send_response(conn_id, opcode, BLE_OTS_OLCP_RESULT_OPERATION_FAILED, + NULL, 0); + return; + } + + olcp_send_response(conn_id, opcode, BLE_OTS_OLCP_RESULT_SUCCESS, NULL, 0); +} + +/***************************************************************************** + * Procedure: Previous (Op Code 0x03) + * Note: The caller evaluates the filtered list and the No Object check, so + * @p list is guaranteed non-NULL and non-empty here. + *****************************************************************************/ +static void olcp_proc_previous(uint16_t conn_id, uint8_t opcode, + const ble_ots_server_filtered_list_t *list) +{ + ble_ots_obj_id_t current_id = ble_ots_server_current_obj_get(conn_id); + if (current_id == BLE_OTS_OBJ_ID_INVALID) { + olcp_send_response(conn_id, opcode, BLE_OTS_OLCP_RESULT_OPERATION_FAILED, + NULL, 0); + return; + } + + /* Find current object position in the filtered list */ + int pos = -1; + for (uint32_t i = 0; i < list->count; i++) { + if (list->object_ids[i] == current_id) { + pos = (int)i; + break; + } + } + + if (pos < 0) { + /* Current object not found in filtered list */ + olcp_send_response(conn_id, opcode, BLE_OTS_OLCP_RESULT_OPERATION_FAILED, + NULL, 0); + return; + } + + if (pos == 0) { + /* Already at the first object - Out of Bounds */ + olcp_send_response(conn_id, opcode, BLE_OTS_OLCP_RESULT_OUT_OF_BOUNDS, + NULL, 0); + return; + } + + int rc = ble_ots_server_current_obj_set(conn_id, + list->object_ids[pos - 1]); + if (rc != 0) { + olcp_send_response(conn_id, opcode, BLE_OTS_OLCP_RESULT_OPERATION_FAILED, + NULL, 0); + return; + } + + olcp_send_response(conn_id, opcode, BLE_OTS_OLCP_RESULT_SUCCESS, NULL, 0); +} + +/***************************************************************************** + * Procedure: Next (Op Code 0x04) + * Note: The caller evaluates the filtered list and the No Object check, so + * @p list is guaranteed non-NULL and non-empty here. + *****************************************************************************/ +static void olcp_proc_next(uint16_t conn_id, uint8_t opcode, + const ble_ots_server_filtered_list_t *list) +{ + ble_ots_obj_id_t current_id = ble_ots_server_current_obj_get(conn_id); + if (current_id == BLE_OTS_OBJ_ID_INVALID) { + olcp_send_response(conn_id, opcode, BLE_OTS_OLCP_RESULT_OPERATION_FAILED, + NULL, 0); + return; + } + + /* Find current object position in the filtered list */ + int pos = -1; + for (uint32_t i = 0; i < list->count; i++) { + if (list->object_ids[i] == current_id) { + pos = (int)i; + break; + } + } + + if (pos < 0) { + /* Current object not found in filtered list */ + olcp_send_response(conn_id, opcode, BLE_OTS_OLCP_RESULT_OPERATION_FAILED, + NULL, 0); + return; + } + + if (pos == (int)(list->count - 1)) { + /* Already at the last object - Out of Bounds */ + olcp_send_response(conn_id, opcode, BLE_OTS_OLCP_RESULT_OUT_OF_BOUNDS, + NULL, 0); + return; + } + + int rc = ble_ots_server_current_obj_set(conn_id, + list->object_ids[pos + 1]); + if (rc != 0) { + olcp_send_response(conn_id, opcode, BLE_OTS_OLCP_RESULT_OPERATION_FAILED, + NULL, 0); + return; + } + + olcp_send_response(conn_id, opcode, BLE_OTS_OLCP_RESULT_SUCCESS, NULL, 0); +} + +/***************************************************************************** + * Procedure: Go To (Op Code 0x05) + *****************************************************************************/ +static void olcp_proc_go_to(uint16_t conn_id, uint8_t opcode, + const uint8_t *param) +{ + /* Parse 6-byte Object ID (UINT48, little-endian) */ + uint64_t object_id = 0; + for (int i = 0; i < 6; i++) { + object_id |= ((uint64_t)param[i]) << (8 * i); + } + + /* Look up the object directly in the database (filter bypass) */ + ble_ots_server_obj_t *obj = ble_ots_server_obj_db_lookup(object_id); + if (obj == NULL) { + olcp_send_response(conn_id, opcode, + BLE_OTS_OLCP_RESULT_OBJECT_ID_NOT_FOUND, NULL, 0); + return; + } + + /* Reset all three filter instances to No Filter for this connection */ + ble_ots_server_filter_reset(conn_id); + + /* Set the specified object as the Current Object */ + int rc = ble_ots_server_current_obj_set(conn_id, object_id); + if (rc != 0) { + olcp_send_response(conn_id, opcode, + BLE_OTS_OLCP_RESULT_OPERATION_FAILED, NULL, 0); + return; + } + + olcp_send_response(conn_id, opcode, BLE_OTS_OLCP_RESULT_SUCCESS, NULL, 0); +} + +/***************************************************************************** + * Procedure: Order (Op Code 0x06) + *****************************************************************************/ +static void olcp_proc_order(uint16_t conn_id, uint8_t opcode, + const uint8_t *param) +{ + uint8_t sort_order = param[0]; + + /* Validate the sort order value */ + if (!is_valid_sort_order(sort_order)) { + olcp_send_response(conn_id, opcode, + BLE_OTS_OLCP_RESULT_INVALID_PARAMETER, NULL, 0); + return; + } + + /* Store the sort order in the connection state */ + ble_ots_server_conn_state_t *state = ble_ots_server_conn_state_get(conn_id); + if (state == NULL) { + olcp_send_response(conn_id, opcode, + BLE_OTS_OLCP_RESULT_OPERATION_FAILED, NULL, 0); + return; + } + + state->sort_order = (ble_ots_list_sort_order_t)sort_order; + + olcp_send_response(conn_id, opcode, BLE_OTS_OLCP_RESULT_SUCCESS, NULL, 0); +} + +/***************************************************************************** + * Procedure: Request Number of Objects (Op Code 0x07) + * Note: The caller evaluates the filtered list and the No Object check, so + * @p list is guaranteed non-NULL and non-empty here. + *****************************************************************************/ +static void olcp_proc_request_num_objects(uint16_t conn_id, uint8_t opcode, + const ble_ots_server_filtered_list_t *list) +{ + /* Build the 4-byte UINT32 count parameter (little-endian) */ + uint8_t count_param[4]; + uint32_t count = list->count; + count_param[0] = (uint8_t)(count & 0xFF); + count_param[1] = (uint8_t)((count >> 8) & 0xFF); + count_param[2] = (uint8_t)((count >> 16) & 0xFF); + count_param[3] = (uint8_t)((count >> 24) & 0xFF); + + olcp_send_response(conn_id, opcode, BLE_OTS_OLCP_RESULT_SUCCESS, + count_param, sizeof(count_param)); +} + +/***************************************************************************** + * Procedure: Clear Marking (Op Code 0x08) + * Note: The caller evaluates the filtered list and the No Object check, so + * @p list is guaranteed non-NULL and non-empty here. + *****************************************************************************/ +static void olcp_proc_clear_marking(uint16_t conn_id, uint8_t opcode, + const ble_ots_server_filtered_list_t *list) +{ + /* Clear the marked bit on all objects in the filtered list for this + * connection's bond */ + for (uint32_t i = 0; i < list->count; i++) { + int rc = ble_ots_server_mark_object(conn_id, list->object_ids[i], false); + if (rc != 0) { + ESP_LOGW(TAG, "Failed to clear mark for object 0x%06llx; rc=%d", + (unsigned long long)list->object_ids[i], rc); + } + } + + olcp_send_response(conn_id, opcode, BLE_OTS_OLCP_RESULT_SUCCESS, NULL, 0); +} + +/***************************************************************************** + * OLCP Characteristic Access Callback (NimBLE ble_gatt_access_fn) + *****************************************************************************/ +static int ble_ots_server_olcp_access(uint16_t conn_handle, uint16_t attr_handle, + struct ble_gatt_access_ctxt *ctxt, void *arg) +{ + if (ctxt->op != BLE_GATT_ACCESS_OP_WRITE_CHR) { + /* OLCP is write-only from the client's perspective (+ indicate) */ + return BLE_ATT_ERR_REQ_NOT_SUPPORTED; + } + + /* Extract the write data from the mbuf */ + uint8_t buf[1 + 6]; /* Max: 1 opcode + 6 bytes param (UINT48) */ + uint16_t data_len = 0; + uint16_t om_len = OS_MBUF_PKTLEN(ctxt->om); + + if (om_len < 1) { + return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN; + } + + if (om_len > sizeof(buf)) { + return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN; + } + + int rc = ble_hs_mbuf_to_flat(ctxt->om, buf, sizeof(buf), &data_len); + if (rc != 0) { + return BLE_ATT_ERR_UNLIKELY; + } + + uint8_t opcode = buf[0]; + uint8_t *param = (data_len > 1) ? &buf[1] : NULL; + uint16_t param_len = (data_len > 1) ? (data_len - 1) : 0; + + /* --- ATT-level validation (Priority 1) --- */ + + /* Check CCCD configured (ATT error 0xFD) */ + if (!olcp_cccd_configured(conn_handle)) { + return ATT_ERR_CCCD_IMPROPERLY_CONFIGURED; + } + + /* Check concurrency pool (ATT error 0x82) */ + if (!ble_ots_server_concurrency_check(conn_handle)) { + return BLE_OTS_APP_ERR_CONCURRENCY_LIMIT_EXCEEDED; + } + + /* Check no transfer in progress */ + ble_ots_server_conn_state_t *state = ble_ots_server_conn_state_get(conn_handle); + if (state == NULL) { + return BLE_OTS_APP_ERR_CONCURRENCY_LIMIT_EXCEEDED; + } + + if (state->transfer_active) { + return ATT_ERR_PROC_ALREADY_IN_PROGRESS; + } + + /* Validate attribute value length for known opcodes */ + if (opcode >= BLE_OTS_OLCP_OPCODE_FIRST && opcode <= BLE_OTS_OLCP_OPCODE_CLEAR_MARKING) { + int expected_param_len = olcp_get_param_len(opcode); + if (expected_param_len >= 0 && param_len != (uint16_t)expected_param_len) { + return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN; + } + } + + /* --- ATT Write Response is sent (procedure starts) --- */ + /* From here on, errors are indicated via OLCP response, not ATT errors. + * Error priority order (2-8) is enforced below. */ + + /* Priority 2: Op Code Not Supported + * Covers reserved opcodes and the Response Code (0x70), which a client + * must never write, as well as unsupported optional opcodes. */ + if (opcode == 0x00 || + (opcode >= 0x09 && opcode <= 0x6F) || + (opcode >= BLE_OTS_OLCP_OPCODE_RESPONSE)) { + olcp_send_response(conn_handle, opcode, + BLE_OTS_OLCP_RESULT_OP_CODE_NOT_SUPPORTED, NULL, 0); + return 0; + } + + if (!olcp_opcode_supported(opcode)) { + olcp_send_response(conn_handle, opcode, + BLE_OTS_OLCP_RESULT_OP_CODE_NOT_SUPPORTED, NULL, 0); + return 0; + } + + /* Priority 3: No Object - filtered list contains zero objects. + * For Go To (0x05), the procedure bypasses all active filters and + * performs a direct database lookup. Only return No Object for Go To + * if the entire object database is empty. For all other opcodes, + * check the filtered list as usual. */ + ble_ots_server_filtered_list_t *list = ble_ots_server_filter_get_list(conn_handle); + if (list == NULL) { + olcp_send_response(conn_handle, opcode, + BLE_OTS_OLCP_RESULT_OPERATION_FAILED, NULL, 0); + return 0; + } + + if (opcode == BLE_OTS_OLCP_OPCODE_GO_TO) { + /* Go To bypasses filters; only reject if the database is empty */ + if (!ble_ots_server_obj_db_has_objects()) { + olcp_send_response(conn_handle, opcode, + BLE_OTS_OLCP_RESULT_NO_OBJECT, NULL, 0); + return 0; + } + } else if (list->count == 0) { + olcp_send_response(conn_handle, opcode, + BLE_OTS_OLCP_RESULT_NO_OBJECT, NULL, 0); + return 0; + } + + /* Priorities 4-8 are procedure-specific and handled within each proc */ + + /* Execute the procedure based on opcode */ + switch (opcode) { + case BLE_OTS_OLCP_OPCODE_FIRST: + olcp_proc_first(conn_handle, opcode, list); + break; + + case BLE_OTS_OLCP_OPCODE_LAST: + olcp_proc_last(conn_handle, opcode, list); + break; + + case BLE_OTS_OLCP_OPCODE_PREVIOUS: + olcp_proc_previous(conn_handle, opcode, list); + break; + + case BLE_OTS_OLCP_OPCODE_NEXT: + olcp_proc_next(conn_handle, opcode, list); + break; + + case BLE_OTS_OLCP_OPCODE_GO_TO: + olcp_proc_go_to(conn_handle, opcode, param); + break; + + case BLE_OTS_OLCP_OPCODE_ORDER: + olcp_proc_order(conn_handle, opcode, param); + break; + + case BLE_OTS_OLCP_OPCODE_REQUEST_NUM_OF_OBJECTS: + olcp_proc_request_num_objects(conn_handle, opcode, list); + break; + + case BLE_OTS_OLCP_OPCODE_CLEAR_MARKING: + olcp_proc_clear_marking(conn_handle, opcode, list); + break; + + default: + /* Should not reach here due to earlier checks */ + olcp_send_response(conn_handle, opcode, + BLE_OTS_OLCP_RESULT_OP_CODE_NOT_SUPPORTED, NULL, 0); + break; + } + + return 0; +} + +/***************************************************************************** + * OLCP Write Wrapper (called from central GATT callback) + *****************************************************************************/ +int ble_ots_server_olcp_write(uint16_t conn_handle, + struct ble_gatt_access_ctxt *ctxt) +{ + return ble_ots_server_olcp_access(conn_handle, 0, ctxt, NULL); +}