Merge branch 'feat/support_ble_ots_nimble' into 'master'

Feat/support ble ots nimble

See merge request espressif/esp-idf!51019
This commit is contained in:
Island
2026-08-25 11:19:50 +08:00
22 changed files with 12687 additions and 0 deletions

View File

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

View File

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

View File

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

View File

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

View File

@@ -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/<role>/*_int.h) sit next to the sources that
# include them, so only the public include/ has to be exported.
set(_include_dirs "${CMAKE_CURRENT_LIST_DIR}/include")
if(CONFIG_BLE_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)

View File

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

View File

@@ -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 <stdint.h>
#include <stdbool.h>
#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 (0120) */
} 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 (0x010x08) */
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 (0x010x07) */
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 (1120)
* @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 */

View File

@@ -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 <stdint.h>
#include <stdbool.h>
#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 (15829999; 0 = unknown) */
uint8_t month; /**< Month (112; 0 = unknown) */
uint8_t day; /**< Day (131; 0 = unknown) */
uint8_t hours; /**< Hours (023) */
uint8_t minutes; /**< Minutes (059) */
uint8_t seconds; /**< Seconds (059) */
} __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 */

View File

@@ -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 <stdint.h>
#include <stdbool.h>
#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 (0120 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 */

View File

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

View File

@@ -0,0 +1,878 @@
/*
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <string.h>
#include <stdlib.h>
#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;
}
}

View File

@@ -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 <stdint.h>
#include <stdbool.h>
#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 */

View File

@@ -0,0 +1,911 @@
/*
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <string.h>
#include <stdlib.h>
#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;
}

View File

@@ -0,0 +1,860 @@
/*
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <string.h>
#include <stdlib.h>
#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;
}

File diff suppressed because it is too large Load Diff

View File

@@ -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 <stdint.h>
#include <stdbool.h>
#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 */

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,912 @@
/*
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <string.h>
#include <errno.h>
#include <stdint.h>
#include <stdbool.h>
#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(&param, 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, &param);
/* 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 15829999 or 0, month 112 or 0, day 131 or 0,
* hours 023, minutes 059, seconds 059.
*
* 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 (831) 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(&params->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;
}

View File

@@ -0,0 +1,736 @@
/*
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <stdlib.h>
#include <string.h>
#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, &param[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);
}

View File

@@ -0,0 +1,982 @@
/*
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <string.h>
#include <time.h>
#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 15829999; 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(&param, 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, &param);
}
/*****************************************************************************
* 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(&param, 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, &param);
}
/*****************************************************************************
* 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(&param, 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, &param);
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;
}

View File

@@ -0,0 +1,586 @@
/*
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <string.h>
#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);
}