mirror of
https://github.com/espressif/esp-idf.git
synced 2026-09-22 13:01:16 +03:00
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:
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
20
components/bt/ble_profiles/nimble/CMakeLists.txt
Normal file
20
components/bt/ble_profiles/nimble/CMakeLists.txt
Normal 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)
|
||||
9
components/bt/ble_profiles/nimble/Kconfig.in
Normal file
9
components/bt/ble_profiles/nimble/Kconfig.in
Normal 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"
|
||||
43
components/bt/ble_profiles/nimble/ble_ots/CMakeLists.txt
Normal file
43
components/bt/ble_profiles/nimble/ble_ots/CMakeLists.txt
Normal 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)
|
||||
128
components/bt/ble_profiles/nimble/ble_ots/Kconfig.in
Normal file
128
components/bt/ble_profiles/nimble/ble_ots/Kconfig.in
Normal 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
|
||||
@@ -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 (0–120) */
|
||||
} ble_ots_client_object_name_read_t;
|
||||
|
||||
/**
|
||||
* @brief Object Type read event data.
|
||||
*/
|
||||
typedef struct {
|
||||
int status; /*!< 0 on success, ATT error on failure */
|
||||
uint8_t uuid[16]; /*!< UUID value (2 or 16 octets, LE) */
|
||||
uint8_t uuid_len; /*!< 2 for 16-bit, 16 for 128-bit UUID */
|
||||
} ble_ots_client_object_type_read_t;
|
||||
|
||||
/**
|
||||
* @brief Object Size read event data.
|
||||
*/
|
||||
typedef struct {
|
||||
int status; /*!< 0 on success, ATT error on failure */
|
||||
uint32_t current_size; /*!< Actual object size in octets */
|
||||
uint32_t allocated_size; /*!< Allocated size in octets */
|
||||
} ble_ots_client_object_size_read_t;
|
||||
|
||||
/**
|
||||
* @brief Date-time read event data (First-Created / Last-Modified).
|
||||
*/
|
||||
typedef struct {
|
||||
int status; /*!< 0 on success, ATT error on failure */
|
||||
ble_ots_date_time_t datetime; /*!< Parsed date-time value */
|
||||
} ble_ots_client_datetime_read_t;
|
||||
|
||||
/**
|
||||
* @brief Object ID read event data.
|
||||
*/
|
||||
typedef struct {
|
||||
int status; /*!< 0 on success, ATT error on failure */
|
||||
uint64_t object_id; /*!< UINT48 Object ID (lower 48 bits) */
|
||||
} ble_ots_client_object_id_read_t;
|
||||
|
||||
/**
|
||||
* @brief Object Properties read event data.
|
||||
*/
|
||||
typedef struct {
|
||||
int status; /*!< 0 on success, ATT error on failure */
|
||||
uint32_t properties; /*!< ble_ots_obj_property_t bit-field */
|
||||
} ble_ots_client_properties_read_t;
|
||||
|
||||
/**
|
||||
* @brief Metadata identifier for write events.
|
||||
*/
|
||||
typedef enum {
|
||||
BLE_OTS_CLIENT_METADATA_OBJECT_NAME = 0, /*!< Object Name */
|
||||
BLE_OTS_CLIENT_METADATA_FIRST_CREATED, /*!< Object First-Created */
|
||||
BLE_OTS_CLIENT_METADATA_LAST_MODIFIED, /*!< Object Last-Modified */
|
||||
BLE_OTS_CLIENT_METADATA_PROPERTIES, /*!< Object Properties */
|
||||
} ble_ots_client_metadata_type_t;
|
||||
|
||||
/**
|
||||
* @brief Metadata written event data.
|
||||
*/
|
||||
typedef struct {
|
||||
int status; /*!< 0 on success, ATT error on failure */
|
||||
ble_ots_client_metadata_type_t metadata_type; /*!< Which metadata was written */
|
||||
} ble_ots_client_metadata_written_t;
|
||||
|
||||
/**
|
||||
* @brief OLCP response event data.
|
||||
*/
|
||||
typedef struct {
|
||||
uint8_t request_opcode; /*!< Original OLCP Op Code (0x01–0x08) */
|
||||
uint8_t result_code; /*!< OLCP result code */
|
||||
uint32_t num_objects; /*!< Valid only for Request Number of Objects + Success */
|
||||
} ble_ots_client_olcp_response_t;
|
||||
|
||||
/**
|
||||
* @brief Filter set event data.
|
||||
*/
|
||||
typedef struct {
|
||||
uint8_t instance; /*!< Filter instance index (0, 1, 2) */
|
||||
int status; /*!< 0 on success, ATT error on failure */
|
||||
} ble_ots_client_filter_set_t;
|
||||
|
||||
/**
|
||||
* @brief Filter read event data.
|
||||
*/
|
||||
typedef struct {
|
||||
uint8_t instance; /*!< Filter instance index (0, 1, 2) */
|
||||
int status; /*!< 0 on success, ATT error on failure */
|
||||
uint8_t filter_type; /*!< One of ble_ots_list_filter_type_t */
|
||||
const uint8_t *param; /*!< Filter parameter bytes (valid during callback), NULL if none */
|
||||
uint16_t param_len; /*!< Length of param in octets (0 if none) */
|
||||
} ble_ots_client_filter_read_t;
|
||||
|
||||
/**
|
||||
* @brief Object Changed event data.
|
||||
*/
|
||||
typedef struct {
|
||||
uint8_t source_of_change; /*!< 0 = Server, 1 = another Client */
|
||||
bool contents_changed; /*!< true if object contents changed */
|
||||
bool metadata_changed; /*!< true if object metadata changed */
|
||||
bool object_created; /*!< true if a new object was created */
|
||||
bool object_deleted; /*!< true if an object was deleted */
|
||||
uint64_t object_id; /*!< UINT48 Object ID (lower 48 bits) */
|
||||
} ble_ots_client_object_changed_t;
|
||||
|
||||
/**
|
||||
* @brief OACP response event data.
|
||||
*/
|
||||
typedef struct {
|
||||
uint8_t request_opcode; /*!< Original OACP Op Code (0x01–0x07) */
|
||||
uint8_t result_code; /*!< OACP result code */
|
||||
uint32_t checksum; /*!< CRC-32 (valid only for Calculate Checksum + Success) */
|
||||
bool has_checksum; /*!< true if checksum field is valid */
|
||||
} ble_ots_client_oacp_response_t;
|
||||
|
||||
/**
|
||||
* @brief Channel open event data.
|
||||
*/
|
||||
typedef struct {
|
||||
uint16_t conn_id; /*!< Connection identifier */
|
||||
uint16_t channel_id; /*!< L2CAP channel identifier */
|
||||
uint16_t mtu; /*!< Negotiated L2CAP MTU */
|
||||
uint16_t mps; /*!< Negotiated MPS (LE Credit Based only) */
|
||||
} ble_ots_client_channel_info_t;
|
||||
|
||||
/**
|
||||
* @brief Channel closed event data.
|
||||
*/
|
||||
typedef struct {
|
||||
uint16_t conn_id; /*!< Connection identifier */
|
||||
uint8_t reason; /*!< 0 = local close, nonzero = remote/link loss */
|
||||
} ble_ots_client_channel_closed_t;
|
||||
|
||||
/**
|
||||
* @brief Data received event data.
|
||||
*/
|
||||
typedef struct {
|
||||
const uint8_t *data; /*!< Pointer to received data chunk */
|
||||
uint16_t data_len; /*!< Length of received data chunk */
|
||||
uint32_t offset; /*!< Logical byte offset in the object */
|
||||
} ble_ots_client_data_received_t;
|
||||
|
||||
/**
|
||||
* @brief Data sent event data.
|
||||
*/
|
||||
typedef struct {
|
||||
uint16_t data_len; /*!< Octets acknowledged as sent */
|
||||
uint32_t remaining; /*!< Remaining octets to send */
|
||||
} ble_ots_client_data_sent_t;
|
||||
|
||||
/**
|
||||
* @brief Transfer timeout event data.
|
||||
*/
|
||||
typedef struct {
|
||||
uint16_t conn_id; /*!< Connection identifier */
|
||||
uint8_t opcode; /*!< OACP Op Code (0 = data-transfer inactivity) */
|
||||
} ble_ots_client_transfer_timeout_t;
|
||||
|
||||
/*****************************************************************************
|
||||
* Callback Type
|
||||
*****************************************************************************/
|
||||
|
||||
/**
|
||||
* @brief Application event callback type.
|
||||
*
|
||||
* All OTS client events from all modules are delivered through this callback.
|
||||
* The callback is invoked from the BLE host task context; the application must
|
||||
* not block.
|
||||
*
|
||||
* @param conn_id Connection identifier
|
||||
* @param event One of ble_ots_client_event_t values
|
||||
* @param param Event-specific parameter structure (valid during callback only)
|
||||
*/
|
||||
typedef void (*ble_ots_client_event_cb_t)(uint16_t conn_id,
|
||||
ble_ots_client_event_t event,
|
||||
const void *param);
|
||||
|
||||
/*****************************************************************************
|
||||
* Public APIs — Discovery & Metadata (ble_ots_client_discovery.c / ble_ots_client_metadata.c)
|
||||
*****************************************************************************/
|
||||
|
||||
/**
|
||||
* @brief Initialize the OTS client module.
|
||||
*
|
||||
* Allocates internal resources and registers the application event callback.
|
||||
* Must be called before any other OTS client API. Call ble_ots_client_deinit()
|
||||
* first to re-initialize.
|
||||
*
|
||||
* @param callback Application event callback function
|
||||
* @return 0 on success, error code on failure
|
||||
*/
|
||||
int ble_ots_client_init(ble_ots_client_event_cb_t callback);
|
||||
|
||||
/**
|
||||
* @brief Deinitialize the OTS client module.
|
||||
*
|
||||
* Releases all resources (per-connection contexts, timers), unregisters the
|
||||
* callback, and resets internal state.
|
||||
*
|
||||
* @return 0 on success, error code on failure
|
||||
*/
|
||||
int ble_ots_client_deinit(void);
|
||||
|
||||
/**
|
||||
* @brief Discover the OTS service on a connected server.
|
||||
*
|
||||
* Discovers all service characteristics and descriptors, and auto-configures
|
||||
* OACP/OLCP CCCDs for indications. Result reported via
|
||||
* BLE_OTS_CLIENT_EVT_DISCOVER_COMPLETE.
|
||||
*
|
||||
* @param conn_id BLE connection identifier
|
||||
* @return 0 on success (discovery started), error code on failure
|
||||
*/
|
||||
int ble_ots_client_discover_service(uint16_t conn_id);
|
||||
|
||||
/**
|
||||
* @brief Read the OTS Feature characteristic (Feature Discovery).
|
||||
*
|
||||
* The result is cached and reported via BLE_OTS_CLIENT_EVT_FEATURE_READ.
|
||||
*
|
||||
* @param conn_id BLE connection identifier
|
||||
* @return 0 on success (read initiated), error code on failure
|
||||
*/
|
||||
int ble_ots_client_read_feature(uint16_t conn_id);
|
||||
|
||||
/**
|
||||
* @brief Read the Object Name of the Current Object.
|
||||
*
|
||||
* Uses Read Long if needed. Result via BLE_OTS_CLIENT_EVT_OBJECT_NAME_READ.
|
||||
*
|
||||
* @param conn_id BLE connection identifier
|
||||
* @return 0 on success (read initiated), error code on failure
|
||||
*/
|
||||
int ble_ots_client_read_object_name(uint16_t conn_id);
|
||||
|
||||
/**
|
||||
* @brief Read the Object Type UUID of the Current Object.
|
||||
*
|
||||
* Result via BLE_OTS_CLIENT_EVT_OBJECT_TYPE_READ.
|
||||
*
|
||||
* @param conn_id BLE connection identifier
|
||||
* @return 0 on success (read initiated), error code on failure
|
||||
*/
|
||||
int ble_ots_client_read_object_type(uint16_t conn_id);
|
||||
|
||||
/**
|
||||
* @brief Read the Object Size of the Current Object.
|
||||
*
|
||||
* Result via BLE_OTS_CLIENT_EVT_OBJECT_SIZE_READ.
|
||||
*
|
||||
* @param conn_id BLE connection identifier
|
||||
* @return 0 on success (read initiated), error code on failure
|
||||
*/
|
||||
int ble_ots_client_read_object_size(uint16_t conn_id);
|
||||
|
||||
/**
|
||||
* @brief Read the First-Created date-time of the Current Object.
|
||||
*
|
||||
* Result via BLE_OTS_CLIENT_EVT_FIRST_CREATED_READ.
|
||||
*
|
||||
* @param conn_id BLE connection identifier
|
||||
* @return 0 on success (read initiated), error code on failure
|
||||
*/
|
||||
int ble_ots_client_read_object_first_created(uint16_t conn_id);
|
||||
|
||||
/**
|
||||
* @brief Read the Last-Modified date-time of the Current Object.
|
||||
*
|
||||
* Result via BLE_OTS_CLIENT_EVT_LAST_MODIFIED_READ.
|
||||
*
|
||||
* @param conn_id BLE connection identifier
|
||||
* @return 0 on success (read initiated), error code on failure
|
||||
*/
|
||||
int ble_ots_client_read_object_last_modified(uint16_t conn_id);
|
||||
|
||||
/**
|
||||
* @brief Read the Object ID of the Current Object.
|
||||
*
|
||||
* Result via BLE_OTS_CLIENT_EVT_OBJECT_ID_READ.
|
||||
*
|
||||
* @param conn_id BLE connection identifier
|
||||
* @return 0 on success (read initiated), error code on failure
|
||||
*/
|
||||
int ble_ots_client_read_object_id(uint16_t conn_id);
|
||||
|
||||
/**
|
||||
* @brief Read the Object Properties of the Current Object.
|
||||
*
|
||||
* Result via BLE_OTS_CLIENT_EVT_PROPERTIES_READ.
|
||||
*
|
||||
* @param conn_id BLE connection identifier
|
||||
* @return 0 on success (read initiated), error code on failure
|
||||
*/
|
||||
int ble_ots_client_read_object_properties(uint16_t conn_id);
|
||||
|
||||
/**
|
||||
* @brief Write a new name for the Current Object.
|
||||
*
|
||||
* Uses Write Long if name exceeds ATT_MTU - 3. Result via
|
||||
* BLE_OTS_CLIENT_EVT_METADATA_WRITTEN with METADATA_OBJECT_NAME.
|
||||
*
|
||||
* @param conn_id BLE connection identifier
|
||||
* @param name Pointer to UTF-8 encoded name buffer, need not be NUL-terminated
|
||||
* @param name_len Length of name in octets (1–120)
|
||||
* @return 0 on success (write initiated), error code on failure
|
||||
*/
|
||||
int ble_ots_client_write_object_name(uint16_t conn_id,
|
||||
const char *name,
|
||||
uint16_t name_len);
|
||||
|
||||
/**
|
||||
* @brief Write the First-Created date-time of the Current Object.
|
||||
*
|
||||
* Result via BLE_OTS_CLIENT_EVT_METADATA_WRITTEN with METADATA_FIRST_CREATED.
|
||||
*
|
||||
* @param conn_id BLE connection identifier
|
||||
* @param datetime Pointer to date-time value to write
|
||||
* @return 0 on success (write initiated), error code on failure
|
||||
*/
|
||||
int ble_ots_client_write_object_first_created(uint16_t conn_id,
|
||||
const ble_ots_date_time_t *datetime);
|
||||
|
||||
/**
|
||||
* @brief Write the Last-Modified date-time of the Current Object.
|
||||
*
|
||||
* Result via BLE_OTS_CLIENT_EVT_METADATA_WRITTEN with METADATA_LAST_MODIFIED.
|
||||
*
|
||||
* @param conn_id BLE connection identifier
|
||||
* @param datetime Pointer to date-time value to write
|
||||
* @return 0 on success (write initiated), error code on failure
|
||||
*/
|
||||
int ble_ots_client_write_object_last_modified(uint16_t conn_id,
|
||||
const ble_ots_date_time_t *datetime);
|
||||
|
||||
/**
|
||||
* @brief Write the Object Properties of the Current Object.
|
||||
*
|
||||
* Result via BLE_OTS_CLIENT_EVT_METADATA_WRITTEN with METADATA_PROPERTIES.
|
||||
*
|
||||
* @param conn_id BLE connection identifier
|
||||
* @param properties ble_ots_obj_property_t bit-field value
|
||||
* @return 0 on success (write initiated), error code on failure
|
||||
*/
|
||||
int ble_ots_client_write_object_properties(uint16_t conn_id,
|
||||
uint32_t properties);
|
||||
|
||||
/*****************************************************************************
|
||||
* Public APIs — Object Navigation (ble_ots_client_object_nav.c)
|
||||
*****************************************************************************/
|
||||
|
||||
/**
|
||||
* @brief Navigate to the first object (OLCP First, 0x01).
|
||||
*
|
||||
* Result via BLE_OTS_CLIENT_EVT_OLCP_RESPONSE.
|
||||
*
|
||||
* @param conn_id Connection identifier
|
||||
* @return 0 on success, error code on failure
|
||||
*/
|
||||
int ble_ots_client_first_object(uint16_t conn_id);
|
||||
|
||||
/**
|
||||
* @brief Navigate to the last object (OLCP Last, 0x02).
|
||||
*
|
||||
* Result via BLE_OTS_CLIENT_EVT_OLCP_RESPONSE.
|
||||
*
|
||||
* @param conn_id Connection identifier
|
||||
* @return 0 on success, error code on failure
|
||||
*/
|
||||
int ble_ots_client_last_object(uint16_t conn_id);
|
||||
|
||||
/**
|
||||
* @brief Navigate to the previous object (OLCP Previous, 0x03).
|
||||
*
|
||||
* Result via BLE_OTS_CLIENT_EVT_OLCP_RESPONSE.
|
||||
*
|
||||
* @param conn_id Connection identifier
|
||||
* @return 0 on success, error code on failure
|
||||
*/
|
||||
int ble_ots_client_prev_object(uint16_t conn_id);
|
||||
|
||||
/**
|
||||
* @brief Navigate to the next object (OLCP Next, 0x04).
|
||||
*
|
||||
* Result via BLE_OTS_CLIENT_EVT_OLCP_RESPONSE.
|
||||
*
|
||||
* @param conn_id Connection identifier
|
||||
* @return 0 on success, error code on failure
|
||||
*/
|
||||
int ble_ots_client_next_object(uint16_t conn_id);
|
||||
|
||||
/**
|
||||
* @brief Select an object by Object ID (OLCP Go To, 0x05).
|
||||
*
|
||||
* Result via BLE_OTS_CLIENT_EVT_OLCP_RESPONSE.
|
||||
*
|
||||
* @param conn_id Connection identifier
|
||||
* @param object_id UINT48 Object ID (lower 48 bits used)
|
||||
* @return 0 on success, error code on failure
|
||||
*/
|
||||
int ble_ots_client_goto_object(uint16_t conn_id, uint64_t object_id);
|
||||
|
||||
/**
|
||||
* @brief Re-order the object list (OLCP Order, 0x06).
|
||||
*
|
||||
* Result via BLE_OTS_CLIENT_EVT_OLCP_RESPONSE.
|
||||
*
|
||||
* @param conn_id Connection identifier
|
||||
* @param sort_order One of ble_ots_list_sort_order_t values
|
||||
* @return 0 on success, error code on failure
|
||||
*/
|
||||
int ble_ots_client_order_objects(uint16_t conn_id, uint8_t sort_order);
|
||||
|
||||
/**
|
||||
* @brief Request number of objects matching current filters (OLCP 0x07).
|
||||
*
|
||||
* Result via BLE_OTS_CLIENT_EVT_OLCP_RESPONSE with num_objects.
|
||||
*
|
||||
* @param conn_id Connection identifier
|
||||
* @return 0 on success, error code on failure
|
||||
*/
|
||||
int ble_ots_client_request_num_objects(uint16_t conn_id);
|
||||
|
||||
/**
|
||||
* @brief Clear marking on all filtered objects (OLCP 0x08).
|
||||
*
|
||||
* Result via BLE_OTS_CLIENT_EVT_OLCP_RESPONSE.
|
||||
*
|
||||
* @param conn_id Connection identifier
|
||||
* @return 0 on success, error code on failure
|
||||
*/
|
||||
int ble_ots_client_clear_marking(uint16_t conn_id);
|
||||
|
||||
/**
|
||||
* @brief Write an Object List Filter value to a filter instance.
|
||||
*
|
||||
* Result via BLE_OTS_CLIENT_EVT_FILTER_SET.
|
||||
*
|
||||
* @param conn_id Connection identifier
|
||||
* @param instance Filter instance index (0, 1, or 2)
|
||||
* @param filter_type One of ble_ots_list_filter_type_t values
|
||||
* @param param Filter parameter data (NULL for No Filter / Marked Objects)
|
||||
* @param param_len Length of param in octets
|
||||
* @return 0 on success, error code on failure
|
||||
*/
|
||||
int ble_ots_client_set_filter(uint16_t conn_id,
|
||||
uint8_t instance,
|
||||
uint8_t filter_type,
|
||||
const uint8_t *param,
|
||||
uint16_t param_len);
|
||||
|
||||
/**
|
||||
* @brief Read the Object List Filter value of a filter instance.
|
||||
*
|
||||
* Reads back the filter type and parameter currently configured on the given
|
||||
* filter instance. Uses Read Long automatically when the value exceeds
|
||||
* ATT_MTU - 1. Result via BLE_OTS_CLIENT_EVT_FILTER_READ.
|
||||
*
|
||||
* @param conn_id Connection identifier
|
||||
* @param instance Filter instance index (0, 1, or 2)
|
||||
* @return 0 on success (read initiated), error code on failure
|
||||
*/
|
||||
int ble_ots_client_read_filter(uint16_t conn_id, uint8_t instance);
|
||||
|
||||
/**
|
||||
* @brief Enable or disable Object Changed indications.
|
||||
*
|
||||
* When enabled, BLE_OTS_CLIENT_EVT_OBJECT_CHANGED events are received.
|
||||
*
|
||||
* @param conn_id Connection identifier
|
||||
* @param enable true to enable, false to disable
|
||||
* @return 0 on success, error code on failure
|
||||
*/
|
||||
int ble_ots_client_subscribe_object_changed(uint16_t conn_id, bool enable);
|
||||
|
||||
/*****************************************************************************
|
||||
* Public APIs — Transfer (ble_ots_client_transfer.c)
|
||||
*****************************************************************************/
|
||||
|
||||
/**
|
||||
* @brief Create a new object on the server (OACP Create, 0x01).
|
||||
*
|
||||
* Result via BLE_OTS_CLIENT_EVT_OACP_RESPONSE.
|
||||
*
|
||||
* @param conn_id Connection identifier
|
||||
* @param size Allocated size in octets
|
||||
* @param type_uuid Pointer to Object Type UUID (2 or 16 octets)
|
||||
* @param type_uuid_len Length of the type UUID: 2 or 16
|
||||
* @return 0 on success, error code on failure
|
||||
*/
|
||||
int ble_ots_client_create_object(uint16_t conn_id,
|
||||
uint32_t size,
|
||||
const uint8_t *type_uuid,
|
||||
uint8_t type_uuid_len);
|
||||
|
||||
/**
|
||||
* @brief Delete the Current Object (OACP Delete, 0x02).
|
||||
*
|
||||
* Result via BLE_OTS_CLIENT_EVT_OACP_RESPONSE.
|
||||
*
|
||||
* @param conn_id Connection identifier
|
||||
* @return 0 on success, error code on failure
|
||||
*/
|
||||
int ble_ots_client_delete_object(uint16_t conn_id);
|
||||
|
||||
/**
|
||||
* @brief Execute the Current Object (OACP Execute, 0x04).
|
||||
*
|
||||
* Result via BLE_OTS_CLIENT_EVT_OACP_RESPONSE.
|
||||
*
|
||||
* @param conn_id Connection identifier
|
||||
* @return 0 on success, error code on failure
|
||||
*/
|
||||
int ble_ots_client_execute_object(uint16_t conn_id);
|
||||
|
||||
/**
|
||||
* @brief Initiate reading object contents (OACP Read, 0x05).
|
||||
*
|
||||
* Data arrives via BLE_OTS_CLIENT_EVT_DATA_RECEIVED. The Object Transfer
|
||||
* Channel must already be open.
|
||||
*
|
||||
* @param conn_id Connection identifier
|
||||
* @param offset Zero-based byte offset to start reading
|
||||
* @param length Number of octets to read; MUST be non-zero. The transfer
|
||||
* channel has no end-of-data marker, so the client tracks
|
||||
* completion by counting octets against this value.
|
||||
* @return 0 on success, BLE_HS_EINVAL if length is 0, error code on failure
|
||||
*/
|
||||
int ble_ots_client_read_object_content(uint16_t conn_id,
|
||||
uint32_t offset,
|
||||
uint32_t length);
|
||||
|
||||
/**
|
||||
* @brief Initiate writing object contents (OACP Write, 0x06).
|
||||
*
|
||||
* After OACP Success, send data via ble_ots_client_send_data(). The Object
|
||||
* Transfer Channel must already be open.
|
||||
*
|
||||
* @param conn_id Connection identifier
|
||||
* @param offset Zero-based byte offset to start writing
|
||||
* @param length Number of octets to write; MUST be non-zero
|
||||
* @param mode Write mode bit-field (bit 1 = Truncate)
|
||||
* @return 0 on success, BLE_HS_EINVAL if length is 0, error code on failure
|
||||
*/
|
||||
int ble_ots_client_write_object_content(uint16_t conn_id,
|
||||
uint32_t offset,
|
||||
uint32_t length,
|
||||
uint8_t mode);
|
||||
|
||||
/**
|
||||
* @brief Send a chunk of object data over the Object Transfer Channel.
|
||||
*
|
||||
* Called repeatedly during OACP Write until all declared octets are sent.
|
||||
*
|
||||
* @param conn_id Connection identifier
|
||||
* @param data Pointer to the object data to send
|
||||
* @param data_len Length of the data chunk in octets
|
||||
* @return 0 on success, error code on failure
|
||||
*/
|
||||
int ble_ots_client_send_data(uint16_t conn_id,
|
||||
const uint8_t *data,
|
||||
uint16_t data_len);
|
||||
|
||||
/**
|
||||
* @brief Request a CRC-32 checksum (OACP Calculate Checksum, 0x03).
|
||||
*
|
||||
* Result via BLE_OTS_CLIENT_EVT_OACP_RESPONSE with checksum.
|
||||
*
|
||||
* @param conn_id Connection identifier
|
||||
* @param offset Zero-based byte offset
|
||||
* @param length Number of octets to checksum
|
||||
* @return 0 on success, error code on failure
|
||||
*/
|
||||
int ble_ots_client_calculate_checksum(uint16_t conn_id,
|
||||
uint32_t offset,
|
||||
uint32_t length);
|
||||
|
||||
/**
|
||||
* @brief Abort an in-progress OACP Read transfer (OACP Abort, 0x07).
|
||||
*
|
||||
* Result via BLE_OTS_CLIENT_EVT_OACP_RESPONSE. Per OTS v1.0 section 4.4 this
|
||||
* may be issued while the server is still processing another OACP procedure,
|
||||
* so it is not rejected with BLE_HS_EBUSY the way the other OACP commands are.
|
||||
*
|
||||
* @param conn_id Connection identifier
|
||||
* @return 0 on success, error code on failure
|
||||
*/
|
||||
int ble_ots_client_abort_transfer(uint16_t conn_id);
|
||||
|
||||
/**
|
||||
* @brief Open an L2CAP Object Transfer Channel (PSM_OTS 0x0025).
|
||||
*
|
||||
* Result via BLE_OTS_CLIENT_EVT_CHANNEL_OPEN or BLE_OTS_CLIENT_EVT_CHANNEL_CLOSED.
|
||||
*
|
||||
* @param conn_id Connection identifier
|
||||
* @return 0 on success, error code on failure
|
||||
*/
|
||||
int ble_ots_client_open_channel(uint16_t conn_id);
|
||||
|
||||
/**
|
||||
* @brief Close the Object Transfer Channel.
|
||||
*
|
||||
* Result via BLE_OTS_CLIENT_EVT_CHANNEL_CLOSED.
|
||||
*
|
||||
* @param conn_id Connection identifier
|
||||
* @return 0 on success, error code on failure
|
||||
*/
|
||||
int ble_ots_client_close_channel(uint16_t conn_id);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* BLE_OTS_CLIENT_H */
|
||||
@@ -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 (1582–9999; 0 = unknown) */
|
||||
uint8_t month; /**< Month (1–12; 0 = unknown) */
|
||||
uint8_t day; /**< Day (1–31; 0 = unknown) */
|
||||
uint8_t hours; /**< Hours (0–23) */
|
||||
uint8_t minutes; /**< Minutes (0–59) */
|
||||
uint8_t seconds; /**< Seconds (0–59) */
|
||||
} __attribute__((packed)) ble_ots_date_time_t;
|
||||
|
||||
/*****************************************************************************
|
||||
* Object List Filter Types
|
||||
*****************************************************************************/
|
||||
typedef enum {
|
||||
BLE_OTS_FILTER_NO_FILTER = 0x00,
|
||||
BLE_OTS_FILTER_NAME_STARTS_WITH = 0x01,
|
||||
BLE_OTS_FILTER_NAME_ENDS_WITH = 0x02,
|
||||
BLE_OTS_FILTER_NAME_CONTAINS = 0x03,
|
||||
BLE_OTS_FILTER_NAME_IS_EXACTLY = 0x04,
|
||||
BLE_OTS_FILTER_OBJECT_TYPE = 0x05,
|
||||
BLE_OTS_FILTER_CREATED_BETWEEN = 0x06,
|
||||
BLE_OTS_FILTER_MODIFIED_BETWEEN = 0x07,
|
||||
BLE_OTS_FILTER_CURRENT_SIZE_BETWEEN = 0x08,
|
||||
BLE_OTS_FILTER_ALLOCATED_SIZE_BETWEEN = 0x09,
|
||||
BLE_OTS_FILTER_MARKED_OBJECTS = 0x0A,
|
||||
} ble_ots_list_filter_type_t;
|
||||
|
||||
/*****************************************************************************
|
||||
* Object List Filter Characteristic Structure
|
||||
*****************************************************************************/
|
||||
typedef struct {
|
||||
uint8_t filter_type; /**< One of ble_ots_list_filter_type_t */
|
||||
union {
|
||||
/** For Name Starts With / Ends With / Contains / Is Exactly */
|
||||
struct {
|
||||
uint8_t name[BLE_OTS_OBJECT_NAME_MAX_LEN];
|
||||
uint8_t name_len;
|
||||
} name;
|
||||
/** For Object Type filter */
|
||||
struct {
|
||||
uint8_t uuid[16]; /**< 2 or 16 octets */
|
||||
uint8_t uuid_len; /**< 2 or 16 */
|
||||
} obj_type;
|
||||
/** For Created Between / Modified Between */
|
||||
struct {
|
||||
ble_ots_date_time_t timestamp1;
|
||||
ble_ots_date_time_t timestamp2;
|
||||
} timestamp_range;
|
||||
/** For Current Size Between / Allocated Size Between */
|
||||
struct {
|
||||
uint32_t size1;
|
||||
uint32_t size2;
|
||||
} size_range;
|
||||
} param;
|
||||
} ble_ots_list_filter_t;
|
||||
|
||||
/*****************************************************************************
|
||||
* Object Changed Flags Bit-Field (8-bit)
|
||||
*****************************************************************************/
|
||||
#define BLE_OTS_OBJ_CHANGED_FLAG_SOURCE (1 << 0) /**< 0 = Server, 1 = Client */
|
||||
#define BLE_OTS_OBJ_CHANGED_FLAG_CONTENT (1 << 1) /**< Object contents changed */
|
||||
#define BLE_OTS_OBJ_CHANGED_FLAG_METADATA (1 << 2) /**< Object metadata changed */
|
||||
#define BLE_OTS_OBJ_CHANGED_FLAG_CREATION (1 << 3) /**< Object creation */
|
||||
#define BLE_OTS_OBJ_CHANGED_FLAG_DELETION (1 << 4) /**< Object deletion */
|
||||
|
||||
/*****************************************************************************
|
||||
* Object Changed Characteristic Structure (7 octets)
|
||||
*****************************************************************************/
|
||||
typedef struct {
|
||||
uint8_t flags; /**< Object Changed flags bit-field */
|
||||
ble_ots_obj_id_t object_id; /**< Object ID of the changed object */
|
||||
} ble_ots_obj_changed_t;
|
||||
|
||||
/*****************************************************************************
|
||||
* List Sort Order Values
|
||||
*****************************************************************************/
|
||||
typedef enum {
|
||||
BLE_OTS_SORT_ORDER_NAME_ASC = 0x01,
|
||||
BLE_OTS_SORT_ORDER_TYPE_ASC = 0x02,
|
||||
BLE_OTS_SORT_ORDER_CURRENT_SIZE_ASC = 0x03,
|
||||
BLE_OTS_SORT_ORDER_FIRST_CREATED_ASC = 0x04,
|
||||
BLE_OTS_SORT_ORDER_LAST_MODIFIED_ASC = 0x05,
|
||||
BLE_OTS_SORT_ORDER_NAME_DESC = 0x11,
|
||||
BLE_OTS_SORT_ORDER_TYPE_DESC = 0x12,
|
||||
BLE_OTS_SORT_ORDER_CURRENT_SIZE_DESC = 0x13,
|
||||
BLE_OTS_SORT_ORDER_FIRST_CREATED_DESC = 0x14,
|
||||
BLE_OTS_SORT_ORDER_LAST_MODIFIED_DESC = 0x15,
|
||||
} ble_ots_list_sort_order_t;
|
||||
|
||||
/*****************************************************************************
|
||||
* Application Error Codes
|
||||
*****************************************************************************/
|
||||
typedef enum {
|
||||
BLE_OTS_APP_ERR_WRITE_REQUEST_REJECTED = 0x80,
|
||||
BLE_OTS_APP_ERR_OBJECT_NOT_SELECTED = 0x81,
|
||||
BLE_OTS_APP_ERR_CONCURRENCY_LIMIT_EXCEEDED = 0x82,
|
||||
BLE_OTS_APP_ERR_OBJECT_NAME_ALREADY_EXISTS = 0x83,
|
||||
} ble_ots_app_error_code_t;
|
||||
|
||||
/*****************************************************************************
|
||||
* Directory Listing Object Record Flags (8-bit)
|
||||
*****************************************************************************/
|
||||
#define BLE_OTS_DLO_FLAG_TYPE_UUID_128 (1 << 0) /**< 0 = 16-bit, 1 = 128-bit UUID */
|
||||
#define BLE_OTS_DLO_FLAG_CURRENT_SIZE_PRESENT (1 << 1)
|
||||
#define BLE_OTS_DLO_FLAG_ALLOCATED_SIZE_PRESENT (1 << 2)
|
||||
#define BLE_OTS_DLO_FLAG_FIRST_CREATED_PRESENT (1 << 3)
|
||||
#define BLE_OTS_DLO_FLAG_LAST_MODIFIED_PRESENT (1 << 4)
|
||||
#define BLE_OTS_DLO_FLAG_PROPERTIES_PRESENT (1 << 5)
|
||||
#define BLE_OTS_DLO_FLAG_EXTENDED_FLAGS_PRESENT (1 << 7)
|
||||
|
||||
/*****************************************************************************
|
||||
* Object Type Entry (for storing 16-bit or 128-bit UUIDs)
|
||||
*****************************************************************************/
|
||||
typedef struct {
|
||||
uint8_t uuid_len; /**< 2 for 16-bit UUID, 16 for 128-bit UUID */
|
||||
uint8_t uuid[16]; /**< UUID value (little-endian) */
|
||||
} ble_ots_obj_type_entry_t;
|
||||
|
||||
/*****************************************************************************
|
||||
* CRC-32 Checksum Calculation
|
||||
*****************************************************************************/
|
||||
|
||||
/**
|
||||
* @brief Compute ISO/IEC 3309 CRC-32 over a byte range of object data.
|
||||
*
|
||||
* Uses the Ethernet/HDLC CRC-32 algorithm:
|
||||
* - Polynomial: 0x04C11DB7
|
||||
* - Initial value: 0xFFFFFFFF
|
||||
* - Final XOR: 0xFFFFFFFF
|
||||
* - Input/output reflection: enabled
|
||||
*
|
||||
* @param data Pointer to the object data buffer.
|
||||
* @param offset Byte offset from the start of data at which to begin CRC calculation.
|
||||
* @param length Number of octets over which to compute the CRC, starting from offset.
|
||||
* @return The computed 32-bit CRC value.
|
||||
*/
|
||||
uint32_t ble_ots_checksum_calculate(const uint8_t *data, uint32_t offset, uint32_t length);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* BLE_OTS_COMMON_H */
|
||||
@@ -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 (0–120 octets), may be NULL */
|
||||
uint8_t name_len; /*!< Length of name in octets */
|
||||
ble_ots_obj_type_entry_t type; /*!< Object type UUID */
|
||||
uint32_t properties; /*!< Initial object properties bit-field */
|
||||
ble_ots_date_time_t first_created; /*!< First-created timestamp */
|
||||
ble_ots_date_time_t last_modified; /*!< Last-modified timestamp */
|
||||
const uint8_t *data; /*!< Initial content data, may be NULL */
|
||||
uint32_t data_len; /*!< Length of initial data in octets */
|
||||
uint32_t allocated_size; /*!< Allocated size (>= data_len) */
|
||||
} ble_ots_server_obj_params_t;
|
||||
|
||||
/*****************************************************************************
|
||||
* Event Types
|
||||
*****************************************************************************/
|
||||
|
||||
/**
|
||||
* @brief OTS server event types dispatched through the application callback.
|
||||
*/
|
||||
typedef enum {
|
||||
BLE_OTS_SERVER_EVT_OBJECT_CREATED, /*!< Object created via OACP Create */
|
||||
BLE_OTS_SERVER_EVT_OBJECT_DELETED, /*!< Object deleted via OACP Delete */
|
||||
BLE_OTS_SERVER_EVT_EXECUTE, /*!< OACP Execute on current object */
|
||||
BLE_OTS_SERVER_EVT_CHECKSUM_REQUEST, /*!< OACP Calculate Checksum completed */
|
||||
BLE_OTS_SERVER_EVT_READ_COMPLETE, /*!< Read transfer completed */
|
||||
BLE_OTS_SERVER_EVT_WRITE_COMPLETE, /*!< Write transfer completed */
|
||||
BLE_OTS_SERVER_EVT_METADATA_WRITTEN, /*!< Client wrote a metadata characteristic */
|
||||
} ble_ots_server_event_t;
|
||||
|
||||
/*****************************************************************************
|
||||
* Event Data Structures
|
||||
*****************************************************************************/
|
||||
|
||||
/**
|
||||
* @brief Event data for BLE_OTS_SERVER_EVT_OBJECT_CREATED.
|
||||
*/
|
||||
typedef struct {
|
||||
ble_ots_obj_id_t object_id; /*!< Allocated Object ID */
|
||||
ble_ots_obj_type_entry_t type; /*!< Object Type UUID */
|
||||
uint32_t allocated_size; /*!< Allocated size in octets */
|
||||
} ble_ots_server_oacp_create_evt_t;
|
||||
|
||||
/**
|
||||
* @brief Event data for BLE_OTS_SERVER_EVT_OBJECT_DELETED.
|
||||
*/
|
||||
typedef struct {
|
||||
ble_ots_obj_id_t object_id; /*!< Deleted Object ID */
|
||||
} ble_ots_server_oacp_delete_evt_t;
|
||||
|
||||
/**
|
||||
* @brief Event data for BLE_OTS_SERVER_EVT_EXECUTE.
|
||||
*
|
||||
* @note The callback carries out the execute action and reports its outcome in
|
||||
* @p result, which the server then indicates to the client. Work that
|
||||
* cannot complete inside the callback must be acknowledged with
|
||||
* BLE_OTS_OACP_RESULT_SUCCESS once accepted, since the OACP indication is
|
||||
* sent as soon as the callback returns.
|
||||
*/
|
||||
typedef struct {
|
||||
ble_ots_obj_id_t object_id; /*!< Current Object ID */
|
||||
const uint8_t *param; /*!< Optional parameter data (NULL if none), valid
|
||||
only for the duration of the callback */
|
||||
uint16_t param_len; /*!< Parameter data length (0 if none) */
|
||||
uint8_t result; /*!< [out] OACP Result Code sent to the client, one of
|
||||
ble_ots_oacp_result_code_t. Pre-set to
|
||||
BLE_OTS_OACP_RESULT_SUCCESS; overwrite it with e.g.
|
||||
BLE_OTS_OACP_RESULT_INVALID_PARAMETER or
|
||||
BLE_OTS_OACP_RESULT_OPERATION_FAILED to report a
|
||||
failure. Out-of-range values are reported as
|
||||
BLE_OTS_OACP_RESULT_OPERATION_FAILED. */
|
||||
} ble_ots_server_oacp_execute_evt_t;
|
||||
|
||||
/**
|
||||
* @brief Event data for BLE_OTS_SERVER_EVT_CHECKSUM_REQUEST.
|
||||
*/
|
||||
typedef struct {
|
||||
ble_ots_obj_id_t object_id; /*!< Object ID */
|
||||
uint32_t offset; /*!< Byte offset of checksum range */
|
||||
uint32_t length; /*!< Length of checksum range */
|
||||
uint32_t checksum; /*!< Computed CRC-32 value */
|
||||
} ble_ots_server_oacp_checksum_evt_t;
|
||||
|
||||
/**
|
||||
* @brief Event data for BLE_OTS_SERVER_EVT_READ_COMPLETE.
|
||||
*/
|
||||
typedef struct {
|
||||
ble_ots_obj_id_t object_id; /*!< Object ID that was read */
|
||||
uint32_t offset; /*!< Starting offset */
|
||||
uint32_t length; /*!< Requested length in octets */
|
||||
uint32_t bytes_sent; /*!< Actual octets sent */
|
||||
ble_ots_server_transfer_status_t status; /*!< Completion status */
|
||||
} ble_ots_server_evt_read_complete_t;
|
||||
|
||||
/**
|
||||
* @brief Event data for BLE_OTS_SERVER_EVT_WRITE_COMPLETE.
|
||||
*/
|
||||
typedef struct {
|
||||
ble_ots_obj_id_t object_id; /*!< Object ID that was written */
|
||||
uint32_t offset; /*!< Starting offset */
|
||||
uint32_t bytes_received; /*!< Actual octets received */
|
||||
ble_ots_server_transfer_status_t status; /*!< Completion status */
|
||||
} ble_ots_server_evt_write_complete_t;
|
||||
|
||||
/**
|
||||
* @brief Event data for BLE_OTS_SERVER_EVT_METADATA_WRITTEN.
|
||||
*/
|
||||
typedef struct {
|
||||
uint16_t conn_id; /*!< Connection ID of the writing client */
|
||||
ble_ots_obj_id_t object_id; /*!< Object ID of affected object */
|
||||
uint16_t char_uuid; /*!< UUID of written characteristic */
|
||||
const char *name; /*!< New name (valid when char_uuid == 0x2ABE) */
|
||||
uint8_t name_len; /*!< Name length (valid when char_uuid == 0x2ABE) */
|
||||
ble_ots_date_time_t date_time; /*!< New timestamp (valid for 0x2AC1/0x2AC2) */
|
||||
uint32_t properties; /*!< New properties (valid for 0x2AC4) */
|
||||
} ble_ots_server_metadata_evt_t;
|
||||
|
||||
/*****************************************************************************
|
||||
* Callback Parameter Union
|
||||
*****************************************************************************/
|
||||
|
||||
/**
|
||||
* @brief Union of all OTS server event parameter structures.
|
||||
*/
|
||||
typedef union {
|
||||
ble_ots_server_oacp_create_evt_t object_created; /*!< BLE_OTS_SERVER_EVT_OBJECT_CREATED */
|
||||
ble_ots_server_oacp_delete_evt_t object_deleted; /*!< BLE_OTS_SERVER_EVT_OBJECT_DELETED */
|
||||
ble_ots_server_oacp_execute_evt_t execute; /*!< BLE_OTS_SERVER_EVT_EXECUTE */
|
||||
ble_ots_server_oacp_checksum_evt_t checksum; /*!< BLE_OTS_SERVER_EVT_CHECKSUM_REQUEST */
|
||||
ble_ots_server_evt_read_complete_t read_complete; /*!< BLE_OTS_SERVER_EVT_READ_COMPLETE */
|
||||
ble_ots_server_evt_write_complete_t write_complete; /*!< BLE_OTS_SERVER_EVT_WRITE_COMPLETE */
|
||||
ble_ots_server_metadata_evt_t metadata_written; /*!< BLE_OTS_SERVER_EVT_METADATA_WRITTEN */
|
||||
} ble_ots_server_cb_param_t;
|
||||
|
||||
/*****************************************************************************
|
||||
* Callback Type
|
||||
*****************************************************************************/
|
||||
|
||||
/**
|
||||
* @brief Application callback for all OTS server events.
|
||||
*
|
||||
* Runs in the NimBLE host task with the OTS server's internal lock held, so it
|
||||
* must not block: no delays, no waiting on semaphores or queues, no blocking
|
||||
* I/O. Post the work to your own task instead. Calling ble_ots_server_* APIs
|
||||
* from within the callback is safe.
|
||||
*
|
||||
* @param event Event type
|
||||
* @param param Event-specific parameter data
|
||||
*/
|
||||
typedef void (*ble_ots_server_cb_t)(ble_ots_server_event_t event,
|
||||
ble_ots_server_cb_param_t *param);
|
||||
|
||||
/*****************************************************************************
|
||||
* Public APIs
|
||||
*****************************************************************************/
|
||||
|
||||
/**
|
||||
* @brief Initialize the OTS server.
|
||||
*
|
||||
* Validates configuration, allocates object database and concurrency pool,
|
||||
* registers the GATT service with all OTS characteristics and CCCDs,
|
||||
* registers the L2CAP server on PSM_OTS, and creates the Directory Listing
|
||||
* Object if the server supports multiple objects.
|
||||
*
|
||||
* @note The GATT service and the L2CAP PSM listener are registered only on the
|
||||
* first call after boot, because NimBLE offers no way to unregister
|
||||
* them. A later call following ble_ots_server_deinit() re-creates the
|
||||
* object database and the concurrency pool but keeps the characteristic
|
||||
* layout established by the first initialization — @p config fields that
|
||||
* affect that layout (obj_name_writable, include_obj_changed,
|
||||
* include_obj_list_filter, ...) are ignored from then on.
|
||||
*
|
||||
* @param config Pointer to server configuration
|
||||
* @return 0 on success, non-zero error code on failure
|
||||
*/
|
||||
int ble_ots_server_init(const ble_ots_server_config_t *config);
|
||||
|
||||
/**
|
||||
* @brief Deinitialize the OTS server.
|
||||
*
|
||||
* Stops serving OTS, closes open OTC channels, unregisters the GAP event
|
||||
* listener and frees object storage and the concurrency pool. Further requests
|
||||
* from peers are rejected until the server is initialized again.
|
||||
*
|
||||
* @note The GATT service and the L2CAP PSM listener stay registered with the
|
||||
* NimBLE host: it provides no API to remove them. Their callbacks remain
|
||||
* live and reject every request while the server is deinitialized.
|
||||
*
|
||||
* @return 0 on success, non-zero error code on failure
|
||||
*/
|
||||
int ble_ots_server_deinit(void);
|
||||
|
||||
/**
|
||||
* @brief Register the application event callback.
|
||||
*
|
||||
* Only one callback may be registered; calling again replaces the previous one.
|
||||
*
|
||||
* @param callback Application event callback function
|
||||
* @return 0 on success, non-zero error code on failure
|
||||
*/
|
||||
int ble_ots_server_register_cb(ble_ots_server_cb_t callback);
|
||||
|
||||
/**
|
||||
* @brief Add a pre-populated object to the server object database.
|
||||
*
|
||||
* Server-initiated creation (not via OACP). Allocates a unique Object ID,
|
||||
* stores metadata and optional initial content. Triggers Object Changed
|
||||
* indication with Creation flag.
|
||||
*
|
||||
* @param params Pointer to object parameters
|
||||
* @param out_obj_id Output: allocated Object ID on success (may be NULL)
|
||||
* @return 0 on success, negative error code on failure
|
||||
*/
|
||||
int ble_ots_server_add_object(const ble_ots_server_obj_params_t *params,
|
||||
ble_ots_obj_id_t *out_obj_id);
|
||||
|
||||
/**
|
||||
* @brief Remove an object from the server object database.
|
||||
*
|
||||
* Server-initiated deletion. The DLO (Object ID 0) cannot be removed.
|
||||
* Triggers Object Changed indication with Deletion flag.
|
||||
*
|
||||
* @param object_id Object ID to remove
|
||||
* @return 0 on success, negative error code on failure
|
||||
*/
|
||||
int ble_ots_server_remove_object(ble_ots_obj_id_t object_id);
|
||||
|
||||
/**
|
||||
* @brief Set or update object content data from the server side.
|
||||
*
|
||||
* Updates Current Size and may increase Allocated Size. Triggers Object
|
||||
* Changed indication with Content Changed flag.
|
||||
*
|
||||
* @param object_id Object ID of the target object
|
||||
* @param data Pointer to data buffer
|
||||
* @param offset Byte offset within the object
|
||||
* @param length Number of octets to write
|
||||
* @return 0 on success, negative error code on failure
|
||||
*/
|
||||
int ble_ots_server_set_object_data(ble_ots_obj_id_t object_id,
|
||||
const uint8_t *data,
|
||||
uint32_t offset,
|
||||
uint32_t length);
|
||||
|
||||
/**
|
||||
* @brief Trigger Object Changed indication for a server-initiated change.
|
||||
*
|
||||
* Dispatches indication to all subscribed clients. Source of Change (bit 0)
|
||||
* is forced to 0 (Server). The DLO (ID 0) cannot be the target.
|
||||
*
|
||||
* @param object_id Object ID of the changed object
|
||||
* @param flags Change flags (BLE_OTS_OBJ_CHANGED_FLAG_*)
|
||||
* @return 0 on success, non-zero error code on failure
|
||||
*/
|
||||
int ble_ots_server_object_changed(ble_ots_obj_id_t object_id, uint8_t flags);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* BLE_OTS_SERVER_H */
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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 */
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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(¶m, 0, sizeof(param));
|
||||
param.metadata_written.conn_id = conn_handle;
|
||||
param.metadata_written.object_id = object_id;
|
||||
param.metadata_written.char_uuid = char_uuid;
|
||||
if (name) {
|
||||
param.metadata_written.name = name;
|
||||
param.metadata_written.name_len = name_len;
|
||||
}
|
||||
if (dt) {
|
||||
param.metadata_written.date_time = *dt;
|
||||
}
|
||||
param.metadata_written.properties = properties;
|
||||
|
||||
ble_ots_server_dispatch_event(BLE_OTS_SERVER_EVT_METADATA_WRITTEN, ¶m);
|
||||
|
||||
/* Dispatch object changed: Source = Client (bit 0 = 1), Metadata changed (bit 2 = 1) */
|
||||
uint8_t flags = BLE_OTS_OBJ_CHANGED_FLAG_SOURCE | BLE_OTS_OBJ_CHANGED_FLAG_METADATA;
|
||||
ble_ots_server_dispatch_obj_changed(object_id, flags, conn_handle);
|
||||
}
|
||||
|
||||
/*****************************************************************************
|
||||
* OTS Feature Characteristic (UUID 0x2ABD)
|
||||
*****************************************************************************/
|
||||
|
||||
static int
|
||||
ble_ots_server_feature_access(uint16_t conn_handle, uint16_t attr_handle,
|
||||
struct ble_gatt_access_ctxt *ctxt, void *arg)
|
||||
{
|
||||
int rc;
|
||||
|
||||
if (ctxt->op == BLE_GATT_ACCESS_OP_READ_CHR) {
|
||||
/* Return 8-byte OTS Feature: OACP Features (4) + OLCP Features (4) */
|
||||
uint8_t buf[8];
|
||||
uint32_t oacp = ble_ots_server_env.ots_feature.oacp_features;
|
||||
uint32_t olcp = ble_ots_server_env.ots_feature.olcp_features;
|
||||
|
||||
/* Little-endian encoding */
|
||||
buf[0] = (uint8_t)(oacp & 0xFF);
|
||||
buf[1] = (uint8_t)((oacp >> 8) & 0xFF);
|
||||
buf[2] = (uint8_t)((oacp >> 16) & 0xFF);
|
||||
buf[3] = (uint8_t)((oacp >> 24) & 0xFF);
|
||||
buf[4] = (uint8_t)(olcp & 0xFF);
|
||||
buf[5] = (uint8_t)((olcp >> 8) & 0xFF);
|
||||
buf[6] = (uint8_t)((olcp >> 16) & 0xFF);
|
||||
buf[7] = (uint8_t)((olcp >> 24) & 0xFF);
|
||||
|
||||
rc = os_mbuf_append(ctxt->om, buf, sizeof(buf));
|
||||
return rc == 0 ? 0 : BLE_ATT_ERR_INSUFFICIENT_RES;
|
||||
}
|
||||
|
||||
/* Not writable */
|
||||
return BLE_ATT_ERR_WRITE_NOT_PERMITTED;
|
||||
}
|
||||
|
||||
/*****************************************************************************
|
||||
* Object Name Characteristic (UUID 0x2ABE)
|
||||
*****************************************************************************/
|
||||
|
||||
static int
|
||||
ble_ots_server_obj_name_access(uint16_t conn_handle, uint16_t attr_handle,
|
||||
struct ble_gatt_access_ctxt *ctxt, void *arg)
|
||||
{
|
||||
ble_ots_server_obj_t *obj = NULL;
|
||||
ble_ots_obj_id_t obj_id = 0;
|
||||
int rc;
|
||||
|
||||
rc = metadata_precheck(conn_handle, &obj, &obj_id);
|
||||
if (rc != 0) {
|
||||
return rc;
|
||||
}
|
||||
|
||||
if (ctxt->op == BLE_GATT_ACCESS_OP_READ_CHR) {
|
||||
/* Return the object name as UTF-8 string */
|
||||
rc = os_mbuf_append(ctxt->om, obj->name, obj->name_len);
|
||||
return rc == 0 ? 0 : BLE_ATT_ERR_INSUFFICIENT_RES;
|
||||
}
|
||||
|
||||
if (ctxt->op == BLE_GATT_ACCESS_OP_WRITE_CHR) {
|
||||
/* Extract the written value */
|
||||
char name_buf[BLE_OTS_OBJECT_NAME_MAX_LEN];
|
||||
uint16_t name_len = 0;
|
||||
uint16_t om_len = OS_MBUF_PKTLEN(ctxt->om);
|
||||
|
||||
/* Validate name length */
|
||||
if (om_len > BLE_OTS_OBJECT_NAME_MAX_LEN) {
|
||||
return BLE_OTS_APP_ERR_WRITE_REQUEST_REJECTED;
|
||||
}
|
||||
|
||||
rc = ble_hs_mbuf_to_flat(ctxt->om, name_buf, sizeof(name_buf), &name_len);
|
||||
if (rc != 0) {
|
||||
return BLE_ATT_ERR_UNLIKELY;
|
||||
}
|
||||
|
||||
/*
|
||||
* Per OTP spec 4.5.5.1, the name shall not be a zero length string and
|
||||
* shall not include any ASCII control characters.
|
||||
*/
|
||||
if (!ble_ots_obj_name_is_valid(name_buf, name_len)) {
|
||||
return BLE_OTS_APP_ERR_WRITE_REQUEST_REJECTED;
|
||||
}
|
||||
|
||||
/* Check uniqueness: the new name must not belong to another object */
|
||||
if (!ble_ots_server_name_is_unique(name_buf, (uint8_t)name_len, obj_id)) {
|
||||
return BLE_OTS_APP_ERR_OBJECT_NAME_ALREADY_EXISTS;
|
||||
}
|
||||
|
||||
/* Update the object name */
|
||||
memcpy(obj->name, name_buf, name_len);
|
||||
obj->name_len = (uint8_t)name_len;
|
||||
|
||||
/* Dispatch events */
|
||||
metadata_dispatch_write_event(conn_handle, obj_id,
|
||||
BLE_OTS_UUID_OBJECT_NAME,
|
||||
obj->name, obj->name_len,
|
||||
NULL, 0);
|
||||
return 0;
|
||||
}
|
||||
|
||||
return BLE_ATT_ERR_REQ_NOT_SUPPORTED;
|
||||
}
|
||||
|
||||
/*****************************************************************************
|
||||
* Object Type Characteristic (UUID 0x2ABF)
|
||||
*****************************************************************************/
|
||||
|
||||
static int
|
||||
ble_ots_server_obj_type_access(uint16_t conn_handle, uint16_t attr_handle,
|
||||
struct ble_gatt_access_ctxt *ctxt, void *arg)
|
||||
{
|
||||
ble_ots_server_obj_t *obj = NULL;
|
||||
int rc;
|
||||
|
||||
rc = metadata_precheck(conn_handle, &obj, NULL);
|
||||
if (rc != 0) {
|
||||
return rc;
|
||||
}
|
||||
|
||||
if (ctxt->op == BLE_GATT_ACCESS_OP_READ_CHR) {
|
||||
/* Return the type UUID: 2 octets for 16-bit, 16 octets for 128-bit */
|
||||
rc = os_mbuf_append(ctxt->om, obj->type_uuid, obj->type_uuid_len);
|
||||
return rc == 0 ? 0 : BLE_ATT_ERR_INSUFFICIENT_RES;
|
||||
}
|
||||
|
||||
/* Not writable */
|
||||
return BLE_ATT_ERR_WRITE_NOT_PERMITTED;
|
||||
}
|
||||
|
||||
/*****************************************************************************
|
||||
* Object Size Characteristic (UUID 0x2AC0)
|
||||
*****************************************************************************/
|
||||
|
||||
static int
|
||||
ble_ots_server_obj_size_access(uint16_t conn_handle, uint16_t attr_handle,
|
||||
struct ble_gatt_access_ctxt *ctxt, void *arg)
|
||||
{
|
||||
ble_ots_server_obj_t *obj = NULL;
|
||||
int rc;
|
||||
|
||||
rc = metadata_precheck(conn_handle, &obj, NULL);
|
||||
if (rc != 0) {
|
||||
return rc;
|
||||
}
|
||||
|
||||
if (ctxt->op == BLE_GATT_ACCESS_OP_READ_CHR) {
|
||||
/* Return 8 octets: Current Size (UINT32 LE) + Allocated Size (UINT32 LE) */
|
||||
uint8_t buf[8];
|
||||
uint32_t cs = obj->current_size;
|
||||
uint32_t as = obj->allocated_size;
|
||||
|
||||
buf[0] = (uint8_t)(cs & 0xFF);
|
||||
buf[1] = (uint8_t)((cs >> 8) & 0xFF);
|
||||
buf[2] = (uint8_t)((cs >> 16) & 0xFF);
|
||||
buf[3] = (uint8_t)((cs >> 24) & 0xFF);
|
||||
buf[4] = (uint8_t)(as & 0xFF);
|
||||
buf[5] = (uint8_t)((as >> 8) & 0xFF);
|
||||
buf[6] = (uint8_t)((as >> 16) & 0xFF);
|
||||
buf[7] = (uint8_t)((as >> 24) & 0xFF);
|
||||
|
||||
rc = os_mbuf_append(ctxt->om, buf, sizeof(buf));
|
||||
return rc == 0 ? 0 : BLE_ATT_ERR_INSUFFICIENT_RES;
|
||||
}
|
||||
|
||||
/* Not writable */
|
||||
return BLE_ATT_ERR_WRITE_NOT_PERMITTED;
|
||||
}
|
||||
|
||||
/*****************************************************************************
|
||||
* Object First-Created Characteristic (UUID 0x2AC1)
|
||||
*****************************************************************************/
|
||||
|
||||
/**
|
||||
* @brief Encode a ble_ots_date_time_t into a 7-byte little-endian buffer.
|
||||
*/
|
||||
static void
|
||||
encode_date_time(const ble_ots_date_time_t *dt, uint8_t *buf)
|
||||
{
|
||||
buf[0] = (uint8_t)(dt->year & 0xFF);
|
||||
buf[1] = (uint8_t)((dt->year >> 8) & 0xFF);
|
||||
buf[2] = dt->month;
|
||||
buf[3] = dt->day;
|
||||
buf[4] = dt->hours;
|
||||
buf[5] = dt->minutes;
|
||||
buf[6] = dt->seconds;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Decode a 7-byte little-endian buffer into a ble_ots_date_time_t.
|
||||
*/
|
||||
static void
|
||||
decode_date_time(const uint8_t *buf, ble_ots_date_time_t *dt)
|
||||
{
|
||||
dt->year = (uint16_t)buf[0] | ((uint16_t)buf[1] << 8);
|
||||
dt->month = buf[2];
|
||||
dt->day = buf[3];
|
||||
dt->hours = buf[4];
|
||||
dt->minutes = buf[5];
|
||||
dt->seconds = buf[6];
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Validate a decoded ble_ots_date_time_t per the BLE OTS specification.
|
||||
*
|
||||
* Valid ranges: year 1582–9999 or 0, month 1–12 or 0, day 1–31 or 0,
|
||||
* hours 0–23, minutes 0–59, seconds 0–59.
|
||||
*
|
||||
* A year of 0 denotes the special "unknown" timestamp (e.g. a newly created
|
||||
* object); in that case the month, day, hours, minutes and seconds MUST all be
|
||||
* zero. A non-zero month/day/time alongside a zero year is not a valid value.
|
||||
*
|
||||
* @param dt Pointer to the date-time structure to validate
|
||||
* @return true if all fields are within valid ranges, false otherwise
|
||||
*/
|
||||
static bool
|
||||
validate_date_time(const ble_ots_date_time_t *dt)
|
||||
{
|
||||
if (dt->year == 0) {
|
||||
/* Unknown timestamp: every other field must also be zero */
|
||||
return dt->month == 0 && dt->day == 0 && dt->hours == 0 &&
|
||||
dt->minutes == 0 && dt->seconds == 0;
|
||||
}
|
||||
if (dt->year < 1582 || dt->year > 9999) {
|
||||
return false;
|
||||
}
|
||||
if (dt->month > 12) {
|
||||
return false;
|
||||
}
|
||||
if (dt->day > 31) {
|
||||
return false;
|
||||
}
|
||||
if (dt->hours > 23) {
|
||||
return false;
|
||||
}
|
||||
if (dt->minutes > 59) {
|
||||
return false;
|
||||
}
|
||||
if (dt->seconds > 59) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static int
|
||||
ble_ots_server_obj_first_created_access(uint16_t conn_handle, uint16_t attr_handle,
|
||||
struct ble_gatt_access_ctxt *ctxt, void *arg)
|
||||
{
|
||||
ble_ots_server_obj_t *obj = NULL;
|
||||
ble_ots_obj_id_t obj_id = 0;
|
||||
int rc;
|
||||
|
||||
rc = metadata_precheck(conn_handle, &obj, &obj_id);
|
||||
if (rc != 0) {
|
||||
return rc;
|
||||
}
|
||||
|
||||
if (ctxt->op == BLE_GATT_ACCESS_OP_READ_CHR) {
|
||||
uint8_t buf[7];
|
||||
encode_date_time(&obj->first_created, buf);
|
||||
rc = os_mbuf_append(ctxt->om, buf, sizeof(buf));
|
||||
return rc == 0 ? 0 : BLE_ATT_ERR_INSUFFICIENT_RES;
|
||||
}
|
||||
|
||||
if (ctxt->op == BLE_GATT_ACCESS_OP_WRITE_CHR) {
|
||||
uint8_t buf[7];
|
||||
uint16_t len = 0;
|
||||
uint16_t om_len = OS_MBUF_PKTLEN(ctxt->om);
|
||||
|
||||
if (om_len != 7) {
|
||||
return BLE_OTS_APP_ERR_WRITE_REQUEST_REJECTED;
|
||||
}
|
||||
|
||||
rc = ble_hs_mbuf_to_flat(ctxt->om, buf, sizeof(buf), &len);
|
||||
if (rc != 0) {
|
||||
return BLE_ATT_ERR_UNLIKELY;
|
||||
}
|
||||
|
||||
ble_ots_date_time_t new_dt;
|
||||
decode_date_time(buf, &new_dt);
|
||||
|
||||
/* Validate date-time fields per OTS specification */
|
||||
if (!validate_date_time(&new_dt)) {
|
||||
return BLE_OTS_APP_ERR_WRITE_REQUEST_REJECTED;
|
||||
}
|
||||
|
||||
/* Update the object's first-created timestamp */
|
||||
obj->first_created = new_dt;
|
||||
|
||||
/* Dispatch events */
|
||||
metadata_dispatch_write_event(conn_handle, obj_id,
|
||||
BLE_OTS_UUID_OBJECT_FIRST_CREATED,
|
||||
NULL, 0, &new_dt, 0);
|
||||
return 0;
|
||||
}
|
||||
|
||||
return BLE_ATT_ERR_REQ_NOT_SUPPORTED;
|
||||
}
|
||||
|
||||
/*****************************************************************************
|
||||
* Object Last-Modified Characteristic (UUID 0x2AC2)
|
||||
*****************************************************************************/
|
||||
|
||||
static int
|
||||
ble_ots_server_obj_last_modified_access(uint16_t conn_handle, uint16_t attr_handle,
|
||||
struct ble_gatt_access_ctxt *ctxt, void *arg)
|
||||
{
|
||||
ble_ots_server_obj_t *obj = NULL;
|
||||
ble_ots_obj_id_t obj_id = 0;
|
||||
int rc;
|
||||
|
||||
rc = metadata_precheck(conn_handle, &obj, &obj_id);
|
||||
if (rc != 0) {
|
||||
return rc;
|
||||
}
|
||||
|
||||
if (ctxt->op == BLE_GATT_ACCESS_OP_READ_CHR) {
|
||||
uint8_t buf[7];
|
||||
encode_date_time(&obj->last_modified, buf);
|
||||
rc = os_mbuf_append(ctxt->om, buf, sizeof(buf));
|
||||
return rc == 0 ? 0 : BLE_ATT_ERR_INSUFFICIENT_RES;
|
||||
}
|
||||
|
||||
if (ctxt->op == BLE_GATT_ACCESS_OP_WRITE_CHR) {
|
||||
/* Writable only when server does NOT have a real-time clock */
|
||||
if (ble_ots_server_env.config.has_realtime_clock) {
|
||||
return BLE_ATT_ERR_WRITE_NOT_PERMITTED;
|
||||
}
|
||||
|
||||
uint8_t buf[7];
|
||||
uint16_t len = 0;
|
||||
uint16_t om_len = OS_MBUF_PKTLEN(ctxt->om);
|
||||
|
||||
if (om_len != 7) {
|
||||
return BLE_OTS_APP_ERR_WRITE_REQUEST_REJECTED;
|
||||
}
|
||||
|
||||
rc = ble_hs_mbuf_to_flat(ctxt->om, buf, sizeof(buf), &len);
|
||||
if (rc != 0) {
|
||||
return BLE_ATT_ERR_UNLIKELY;
|
||||
}
|
||||
|
||||
ble_ots_date_time_t new_dt;
|
||||
decode_date_time(buf, &new_dt);
|
||||
|
||||
/* Validate date-time fields per OTS specification */
|
||||
if (!validate_date_time(&new_dt)) {
|
||||
return BLE_OTS_APP_ERR_WRITE_REQUEST_REJECTED;
|
||||
}
|
||||
|
||||
/* Update the object's last-modified timestamp */
|
||||
obj->last_modified = new_dt;
|
||||
|
||||
/* Dispatch events */
|
||||
metadata_dispatch_write_event(conn_handle, obj_id,
|
||||
BLE_OTS_UUID_OBJECT_LAST_MODIFIED,
|
||||
NULL, 0, &new_dt, 0);
|
||||
return 0;
|
||||
}
|
||||
|
||||
return BLE_ATT_ERR_REQ_NOT_SUPPORTED;
|
||||
}
|
||||
|
||||
/*****************************************************************************
|
||||
* Object ID Characteristic (UUID 0x2AC3)
|
||||
*****************************************************************************/
|
||||
|
||||
static int
|
||||
ble_ots_server_obj_id_access(uint16_t conn_handle, uint16_t attr_handle,
|
||||
struct ble_gatt_access_ctxt *ctxt, void *arg)
|
||||
{
|
||||
ble_ots_server_obj_t *obj = NULL;
|
||||
int rc;
|
||||
|
||||
rc = metadata_precheck(conn_handle, &obj, NULL);
|
||||
if (rc != 0) {
|
||||
return rc;
|
||||
}
|
||||
|
||||
if (ctxt->op == BLE_GATT_ACCESS_OP_READ_CHR) {
|
||||
/* Return 6-byte UINT48 Object ID in little-endian */
|
||||
uint8_t buf[6];
|
||||
uint64_t id = obj->object_id;
|
||||
|
||||
buf[0] = (uint8_t)(id & 0xFF);
|
||||
buf[1] = (uint8_t)((id >> 8) & 0xFF);
|
||||
buf[2] = (uint8_t)((id >> 16) & 0xFF);
|
||||
buf[3] = (uint8_t)((id >> 24) & 0xFF);
|
||||
buf[4] = (uint8_t)((id >> 32) & 0xFF);
|
||||
buf[5] = (uint8_t)((id >> 40) & 0xFF);
|
||||
|
||||
rc = os_mbuf_append(ctxt->om, buf, sizeof(buf));
|
||||
return rc == 0 ? 0 : BLE_ATT_ERR_INSUFFICIENT_RES;
|
||||
}
|
||||
|
||||
/* Not writable */
|
||||
return BLE_ATT_ERR_WRITE_NOT_PERMITTED;
|
||||
}
|
||||
|
||||
/*****************************************************************************
|
||||
* Object Properties Characteristic (UUID 0x2AC4)
|
||||
*****************************************************************************/
|
||||
|
||||
static int
|
||||
ble_ots_server_obj_properties_access(uint16_t conn_handle, uint16_t attr_handle,
|
||||
struct ble_gatt_access_ctxt *ctxt, void *arg)
|
||||
{
|
||||
ble_ots_server_obj_t *obj = NULL;
|
||||
ble_ots_obj_id_t obj_id = 0;
|
||||
int rc;
|
||||
|
||||
rc = metadata_precheck(conn_handle, &obj, &obj_id);
|
||||
if (rc != 0) {
|
||||
return rc;
|
||||
}
|
||||
|
||||
if (ctxt->op == BLE_GATT_ACCESS_OP_READ_CHR) {
|
||||
/* Return 4-byte properties bit-field in little-endian */
|
||||
uint8_t buf[4];
|
||||
uint32_t props = obj->properties;
|
||||
|
||||
buf[0] = (uint8_t)(props & 0xFF);
|
||||
buf[1] = (uint8_t)((props >> 8) & 0xFF);
|
||||
buf[2] = (uint8_t)((props >> 16) & 0xFF);
|
||||
buf[3] = (uint8_t)((props >> 24) & 0xFF);
|
||||
|
||||
rc = os_mbuf_append(ctxt->om, buf, sizeof(buf));
|
||||
return rc == 0 ? 0 : BLE_ATT_ERR_INSUFFICIENT_RES;
|
||||
}
|
||||
|
||||
if (ctxt->op == BLE_GATT_ACCESS_OP_WRITE_CHR) {
|
||||
uint8_t buf[4];
|
||||
uint16_t len = 0;
|
||||
uint16_t om_len = OS_MBUF_PKTLEN(ctxt->om);
|
||||
|
||||
if (om_len != 4) {
|
||||
return BLE_OTS_APP_ERR_WRITE_REQUEST_REJECTED;
|
||||
}
|
||||
|
||||
rc = ble_hs_mbuf_to_flat(ctxt->om, buf, sizeof(buf), &len);
|
||||
if (rc != 0) {
|
||||
return BLE_ATT_ERR_UNLIKELY;
|
||||
}
|
||||
|
||||
uint32_t new_props = (uint32_t)buf[0] |
|
||||
((uint32_t)buf[1] << 8) |
|
||||
((uint32_t)buf[2] << 16) |
|
||||
((uint32_t)buf[3] << 24);
|
||||
|
||||
/* Reject if any RFU bits (8–31) are set */
|
||||
if (new_props & 0xFFFFFF00) {
|
||||
return BLE_OTS_APP_ERR_WRITE_REQUEST_REJECTED;
|
||||
}
|
||||
|
||||
/* Update the object's properties */
|
||||
obj->properties = new_props;
|
||||
|
||||
/* Dispatch events */
|
||||
metadata_dispatch_write_event(conn_handle, obj_id,
|
||||
BLE_OTS_UUID_OBJECT_PROPERTIES,
|
||||
NULL, 0, NULL, new_props);
|
||||
return 0;
|
||||
}
|
||||
|
||||
return BLE_ATT_ERR_REQ_NOT_SUPPORTED;
|
||||
}
|
||||
|
||||
/*****************************************************************************
|
||||
* Metadata Read/Write Dispatchers — called from ots_server_init.c
|
||||
*****************************************************************************/
|
||||
|
||||
int
|
||||
ble_ots_server_metadata_read(uint16_t conn_handle, uint16_t attr_handle,
|
||||
struct ble_gatt_access_ctxt *ctxt)
|
||||
{
|
||||
const ble_uuid_t *uuid = ctxt->chr->uuid;
|
||||
if (uuid->type != BLE_UUID_TYPE_16) {
|
||||
return BLE_ATT_ERR_UNLIKELY;
|
||||
}
|
||||
uint16_t uuid16 = BLE_UUID16(uuid)->value;
|
||||
|
||||
switch (uuid16) {
|
||||
case BLE_OTS_UUID_OTS_FEATURE:
|
||||
return ble_ots_server_feature_access(conn_handle, attr_handle, ctxt, NULL);
|
||||
case BLE_OTS_UUID_OBJECT_NAME:
|
||||
return ble_ots_server_obj_name_access(conn_handle, attr_handle, ctxt, NULL);
|
||||
case BLE_OTS_UUID_OBJECT_TYPE:
|
||||
return ble_ots_server_obj_type_access(conn_handle, attr_handle, ctxt, NULL);
|
||||
case BLE_OTS_UUID_OBJECT_SIZE:
|
||||
return ble_ots_server_obj_size_access(conn_handle, attr_handle, ctxt, NULL);
|
||||
case BLE_OTS_UUID_OBJECT_FIRST_CREATED:
|
||||
return ble_ots_server_obj_first_created_access(conn_handle, attr_handle, ctxt, NULL);
|
||||
case BLE_OTS_UUID_OBJECT_LAST_MODIFIED:
|
||||
return ble_ots_server_obj_last_modified_access(conn_handle, attr_handle, ctxt, NULL);
|
||||
case BLE_OTS_UUID_OBJECT_ID:
|
||||
return ble_ots_server_obj_id_access(conn_handle, attr_handle, ctxt, NULL);
|
||||
case BLE_OTS_UUID_OBJECT_PROPERTIES:
|
||||
return ble_ots_server_obj_properties_access(conn_handle, attr_handle, ctxt, NULL);
|
||||
default:
|
||||
return BLE_ATT_ERR_UNLIKELY;
|
||||
}
|
||||
}
|
||||
|
||||
int
|
||||
ble_ots_server_metadata_write(uint16_t conn_handle, uint16_t attr_handle,
|
||||
struct ble_gatt_access_ctxt *ctxt)
|
||||
{
|
||||
const ble_uuid_t *uuid = ctxt->chr->uuid;
|
||||
if (uuid->type != BLE_UUID_TYPE_16) {
|
||||
return BLE_ATT_ERR_UNLIKELY;
|
||||
}
|
||||
uint16_t uuid16 = BLE_UUID16(uuid)->value;
|
||||
|
||||
/* The Directory Listing Object is read-only: its metadata is generated by
|
||||
* the server from the object database, so a client must not be able to
|
||||
* overwrite it while the DLO is its Current Object. When no object is
|
||||
* selected the ID is BLE_OTS_OBJ_ID_INVALID, which falls through to the
|
||||
* access handlers so that Object Not Selected keeps being reported. */
|
||||
if (ble_ots_server_current_obj_get(conn_handle) ==
|
||||
BLE_OTS_OBJ_ID_DIRECTORY_LISTING) {
|
||||
return BLE_ATT_ERR_WRITE_NOT_PERMITTED;
|
||||
}
|
||||
|
||||
switch (uuid16) {
|
||||
case BLE_OTS_UUID_OBJECT_NAME:
|
||||
return ble_ots_server_obj_name_access(conn_handle, attr_handle, ctxt, NULL);
|
||||
case BLE_OTS_UUID_OBJECT_FIRST_CREATED:
|
||||
return ble_ots_server_obj_first_created_access(conn_handle, attr_handle, ctxt, NULL);
|
||||
case BLE_OTS_UUID_OBJECT_LAST_MODIFIED:
|
||||
return ble_ots_server_obj_last_modified_access(conn_handle, attr_handle, ctxt, NULL);
|
||||
case BLE_OTS_UUID_OBJECT_PROPERTIES:
|
||||
return ble_ots_server_obj_properties_access(conn_handle, attr_handle, ctxt, NULL);
|
||||
default:
|
||||
return BLE_ATT_ERR_UNLIKELY;
|
||||
}
|
||||
}
|
||||
|
||||
/*****************************************************************************
|
||||
* Public APIs — Server-Initiated Object Management
|
||||
*****************************************************************************/
|
||||
|
||||
/**
|
||||
* @brief Create the object and populate its metadata.
|
||||
*
|
||||
* Caller must hold the OTS server mutex: the uniqueness check, the slot
|
||||
* allocation and the metadata/content writes below form one atomic operation
|
||||
* with respect to the NimBLE host task.
|
||||
*
|
||||
* @param params Object parameters supplied by the application
|
||||
* @param out_obj_id Receives the allocated Object ID on success
|
||||
* @return 0 on success, negative errno on failure
|
||||
*/
|
||||
static int ots_add_object_locked(const ble_ots_server_obj_params_t *params,
|
||||
ble_ots_obj_id_t *out_obj_id)
|
||||
{
|
||||
/* Validate name length */
|
||||
if (params->name_len > BLE_OTS_OBJECT_NAME_MAX_LEN) {
|
||||
return -EINVAL;
|
||||
}
|
||||
|
||||
/* Reject a length without a buffer, as ble_ots_server_set_object_data does,
|
||||
* instead of silently creating an object with no content */
|
||||
if (params->data == NULL && params->data_len > 0) {
|
||||
return -EINVAL;
|
||||
}
|
||||
|
||||
/* Validate allocated_size >= data_len */
|
||||
uint32_t alloc_size = params->allocated_size;
|
||||
if (alloc_size == 0 && params->data_len > 0) {
|
||||
alloc_size = params->data_len;
|
||||
}
|
||||
if (alloc_size < params->data_len) {
|
||||
return -EINVAL;
|
||||
}
|
||||
|
||||
/* Validate type UUID length */
|
||||
if (params->type.uuid_len != 2 && params->type.uuid_len != 16) {
|
||||
return -EINVAL;
|
||||
}
|
||||
|
||||
/* Check name uniqueness */
|
||||
const char *name = params->name;
|
||||
uint8_t name_len = name ? params->name_len : 0;
|
||||
|
||||
/* Only named objects take part in the uniqueness check: an object created
|
||||
* over OACP starts with a zero-length name and the Create procedure has no
|
||||
* result code for a name clash, so several unnamed objects must be able to
|
||||
* coexist until the client assigns each one a name. */
|
||||
if (name_len > 0) {
|
||||
if (!ble_ots_server_name_is_unique(name, name_len, BLE_OTS_OBJ_ID_INVALID)) {
|
||||
return -EEXIST;
|
||||
}
|
||||
}
|
||||
|
||||
/* Create object in database (allocates Object ID) */
|
||||
ble_ots_obj_id_t new_id = ble_ots_server_obj_db_create(¶ms->type, alloc_size);
|
||||
if (new_id == 0) {
|
||||
return -ENOMEM;
|
||||
}
|
||||
|
||||
/* Look up the newly created object to set metadata */
|
||||
ble_ots_server_obj_t *obj = ble_ots_server_obj_db_lookup(new_id);
|
||||
if (obj == NULL) {
|
||||
return -ENOMEM;
|
||||
}
|
||||
|
||||
/* Set name */
|
||||
if (name_len > 0) {
|
||||
memcpy(obj->name, name, name_len);
|
||||
}
|
||||
obj->name_len = name_len;
|
||||
|
||||
/* Set properties */
|
||||
obj->properties = params->properties;
|
||||
|
||||
/* Set timestamps */
|
||||
obj->first_created = params->first_created;
|
||||
obj->last_modified = params->last_modified;
|
||||
|
||||
/* Set initial data if provided */
|
||||
if (params->data != NULL && params->data_len > 0) {
|
||||
int rc = ble_ots_server_obj_data_write(new_id, 0, params->data, params->data_len);
|
||||
if (rc != 0) {
|
||||
ESP_LOGE(TAG, "Failed to write initial object data, rc=%d", rc);
|
||||
ble_ots_server_obj_db_delete(new_id);
|
||||
return (rc == BLE_HS_ENOMEM) ? -ENOMEM :
|
||||
(rc == BLE_HS_ENOENT) ? -ENOENT : -EINVAL;
|
||||
}
|
||||
obj->current_size = params->data_len;
|
||||
} else {
|
||||
obj->current_size = 0;
|
||||
}
|
||||
|
||||
obj->allocated_size = alloc_size;
|
||||
|
||||
/* Output the allocated Object ID */
|
||||
*out_obj_id = new_id;
|
||||
|
||||
ESP_LOGI(TAG, "Object added: ID=0x%012llX name_len=%u",
|
||||
(unsigned long long)new_id, name_len);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int
|
||||
ble_ots_server_add_object(const ble_ots_server_obj_params_t *params,
|
||||
ble_ots_obj_id_t *out_obj_id)
|
||||
{
|
||||
ble_ots_obj_id_t new_id = BLE_OTS_OBJ_ID_INVALID;
|
||||
int rc;
|
||||
|
||||
if (!BLE_OTS_SERVER_ENV_OK()) {
|
||||
return -EPERM;
|
||||
}
|
||||
|
||||
if (params == NULL) {
|
||||
return -EINVAL;
|
||||
}
|
||||
|
||||
ble_ots_server_lock();
|
||||
rc = ots_add_object_locked(params, &new_id);
|
||||
ble_ots_server_unlock();
|
||||
|
||||
if (rc != 0) {
|
||||
return rc;
|
||||
}
|
||||
|
||||
if (out_obj_id != NULL) {
|
||||
*out_obj_id = new_id;
|
||||
}
|
||||
|
||||
/* Dispatch Object Changed: Creation (bit 3), Source = Server (bit 0 = 0).
|
||||
* Done outside the lock — it queues indications and reaches NimBLE. */
|
||||
ble_ots_server_dispatch_obj_changed(new_id,
|
||||
BLE_OTS_OBJ_CHANGED_FLAG_CREATION,
|
||||
BLE_OTS_CONN_ID_NONE);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int
|
||||
ble_ots_server_remove_object(ble_ots_obj_id_t object_id)
|
||||
{
|
||||
if (!BLE_OTS_SERVER_ENV_OK()) {
|
||||
return -EPERM;
|
||||
}
|
||||
|
||||
/* DLO cannot be removed */
|
||||
if (object_id == BLE_OTS_OBJ_ID_DIRECTORY_LISTING) {
|
||||
return -EINVAL;
|
||||
}
|
||||
|
||||
ble_ots_server_lock();
|
||||
|
||||
/* Look up the object */
|
||||
ble_ots_server_obj_t *obj = ble_ots_server_obj_db_lookup(object_id);
|
||||
if (obj == NULL) {
|
||||
ble_ots_server_unlock();
|
||||
return -ENOENT;
|
||||
}
|
||||
|
||||
/* Invalidate current object for any connection that has this object selected */
|
||||
for (int i = 0; i < CONFIG_BLE_OTS_SERVER_MAX_CONCURRENCY; i++) {
|
||||
ble_ots_server_conn_state_t *cs = &ble_ots_server_env.conn_pool[i];
|
||||
if (cs->in_pool && cs->current_obj_valid &&
|
||||
cs->current_object_id == object_id) {
|
||||
cs->current_obj_valid = false;
|
||||
cs->current_object_id = BLE_OTS_OBJ_ID_INVALID;
|
||||
ESP_LOGW(TAG, "Invalidated current object for conn=%u due to removal",
|
||||
cs->conn_id);
|
||||
}
|
||||
}
|
||||
|
||||
ble_ots_server_unlock();
|
||||
|
||||
/* Delete the object from the database. Takes the lock itself and then
|
||||
* aborts any transfer on the object without it, since that reaches the
|
||||
* application callback. */
|
||||
int rc = ble_ots_server_obj_db_delete(object_id);
|
||||
if (rc != 0) {
|
||||
ESP_LOGE(TAG, "Failed to delete object 0x%012llX, rc=%d",
|
||||
(unsigned long long)object_id, rc);
|
||||
return (rc == BLE_HS_ENOENT) ? -ENOENT :
|
||||
(rc == BLE_HS_EINVAL) ? -EINVAL : -EINVAL;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Object removed: ID=0x%012llX", (unsigned long long)object_id);
|
||||
|
||||
/* Dispatch Object Changed: Deletion (bit 4), Source = Server (bit 0 = 0) */
|
||||
ble_ots_server_dispatch_obj_changed(object_id,
|
||||
BLE_OTS_OBJ_CHANGED_FLAG_DELETION,
|
||||
BLE_OTS_CONN_ID_NONE);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int
|
||||
ble_ots_server_set_object_data(ble_ots_obj_id_t object_id,
|
||||
const uint8_t *data,
|
||||
uint32_t offset,
|
||||
uint32_t length)
|
||||
{
|
||||
if (!BLE_OTS_SERVER_ENV_OK()) {
|
||||
return -EPERM;
|
||||
}
|
||||
|
||||
if (data == NULL && length > 0) {
|
||||
return -EINVAL;
|
||||
}
|
||||
|
||||
/* Validate that offset + length does not overflow uint32_t */
|
||||
if (length > 0 && offset > UINT32_MAX - length) {
|
||||
return -EINVAL;
|
||||
}
|
||||
|
||||
ble_ots_server_lock();
|
||||
|
||||
/* Look up the object */
|
||||
ble_ots_server_obj_t *obj = ble_ots_server_obj_db_lookup(object_id);
|
||||
if (obj == NULL) {
|
||||
ble_ots_server_unlock();
|
||||
return -ENOENT;
|
||||
}
|
||||
|
||||
/* Write data at the specified offset */
|
||||
int rc = ble_ots_server_obj_data_write(object_id, offset, data, length);
|
||||
if (rc != 0) {
|
||||
ble_ots_server_unlock();
|
||||
ESP_LOGE(TAG, "Failed to write object data for 0x%012llX, rc=%d",
|
||||
(unsigned long long)object_id, rc);
|
||||
return (rc == BLE_HS_ENOMEM) ? -ENOMEM :
|
||||
(rc == BLE_HS_ENOENT) ? -ENOENT : -EINVAL;
|
||||
}
|
||||
|
||||
/* If server has a real-time clock, update Last-Modified timestamp.
|
||||
* Note: In a real implementation this would use the actual RTC time.
|
||||
* Since we may not have a real RTC API available, we reset to zero
|
||||
* if no real-time clock, or set a placeholder if we do. */
|
||||
if (ble_ots_server_env.config.has_realtime_clock) {
|
||||
/* In a production system, this would call an RTC API to get current UTC.
|
||||
* For now, we leave the timestamp as-is since we don't have a
|
||||
* portable RTC interface. The application can update it via the
|
||||
* metadata write event or directly. */
|
||||
}
|
||||
|
||||
ESP_LOGD(TAG, "Object data set: ID=0x%012llX offset=%lu len=%lu cur_size=%lu",
|
||||
(unsigned long long)object_id,
|
||||
(unsigned long)offset, (unsigned long)length,
|
||||
(unsigned long)obj->current_size);
|
||||
|
||||
ble_ots_server_unlock();
|
||||
|
||||
/* Dispatch Object Changed: Content changed (bit 1), Source = Server (bit 0 = 0) */
|
||||
ble_ots_server_dispatch_obj_changed(object_id,
|
||||
BLE_OTS_OBJ_CHANGED_FLAG_CONTENT,
|
||||
BLE_OTS_CONN_ID_NONE);
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -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, ¶m[4], uuid_len);
|
||||
|
||||
/* Priority 2: Check if type is supported */
|
||||
if (!type_is_supported(&type)) {
|
||||
oacp_send_response(conn_id, opcode,
|
||||
BLE_OTS_OACP_RESULT_UNSUPPORTED_TYPE, NULL, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
/* Priority 3: Create object in database (checks resources internally) */
|
||||
ble_ots_obj_id_t new_id = ble_ots_server_obj_db_create(&type, size);
|
||||
if (new_id == 0) {
|
||||
oacp_send_response(conn_id, opcode,
|
||||
BLE_OTS_OACP_RESULT_INSUFFICIENT_RESOURCES, NULL, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
/* Reset filters for this connection if Object List Filter is supported */
|
||||
if (ble_ots_server_env.config.include_obj_list_filter) {
|
||||
ble_ots_server_filter_reset(conn_id);
|
||||
}
|
||||
|
||||
/* Set newly created object as Current Object */
|
||||
int rc = ble_ots_server_current_obj_set(conn_id, new_id);
|
||||
if (rc != 0) {
|
||||
ESP_LOGE(TAG, "Failed to set current object after create, rc=%d", rc);
|
||||
/* Attempt to clean up */
|
||||
ble_ots_server_obj_db_delete(new_id);
|
||||
oacp_send_response(conn_id, opcode,
|
||||
BLE_OTS_OACP_RESULT_OPERATION_FAILED, NULL, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
/* Track created object for post-disconnect cleanup */
|
||||
ble_ots_server_conn_state_t *cs = ble_ots_server_conn_state_get(conn_id);
|
||||
if (cs != NULL && cs->created_obj_count < CONFIG_BLE_OTS_SERVER_MAX_OBJECTS) {
|
||||
cs->created_obj_ids[cs->created_obj_count++] = new_id;
|
||||
}
|
||||
|
||||
/* Send success indication */
|
||||
oacp_send_response(conn_id, opcode,
|
||||
BLE_OTS_OACP_RESULT_SUCCESS, NULL, 0);
|
||||
|
||||
/* Dispatch EVT_OBJECT_CREATED to application */
|
||||
ble_ots_server_obj_t *obj = ble_ots_server_obj_db_lookup(new_id);
|
||||
ble_ots_server_cb_param_t cb_param;
|
||||
memset(&cb_param, 0, sizeof(cb_param));
|
||||
cb_param.object_created.object_id = new_id;
|
||||
cb_param.object_created.type = type;
|
||||
cb_param.object_created.allocated_size = (obj != NULL) ? obj->allocated_size : size;
|
||||
ble_ots_server_dispatch_event(BLE_OTS_SERVER_EVT_OBJECT_CREATED, &cb_param);
|
||||
|
||||
/* Dispatch Object Changed indication with Creation flag + Source=Client */
|
||||
uint8_t flags = BLE_OTS_OBJ_CHANGED_FLAG_CREATION | BLE_OTS_OBJ_CHANGED_FLAG_SOURCE;
|
||||
ble_ots_server_dispatch_obj_changed(new_id, flags, conn_id);
|
||||
}
|
||||
|
||||
/*****************************************************************************
|
||||
* OACP Delete Procedure
|
||||
*****************************************************************************/
|
||||
static void oacp_delete_proc(uint16_t conn_id)
|
||||
{
|
||||
uint8_t opcode = BLE_OTS_OACP_OPCODE_DELETE;
|
||||
|
||||
/* Priority 1: Check if Delete is supported */
|
||||
if (!(ble_ots_server_env.config.oacp_features & BLE_OTS_OACP_FEAT_DELETE)) {
|
||||
oacp_send_response(conn_id, opcode,
|
||||
BLE_OTS_OACP_RESULT_OP_CODE_NOT_SUPPORTED, NULL, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
/* Priority 2: Check if Current Object is valid */
|
||||
ble_ots_obj_id_t obj_id = ble_ots_server_current_obj_get(conn_id);
|
||||
if (obj_id == BLE_OTS_OBJ_ID_INVALID) {
|
||||
oacp_send_response(conn_id, opcode,
|
||||
BLE_OTS_OACP_RESULT_INVALID_OBJECT, NULL, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
ble_ots_server_obj_t *obj = ble_ots_server_obj_db_lookup(obj_id);
|
||||
if (obj == NULL) {
|
||||
oacp_send_response(conn_id, opcode,
|
||||
BLE_OTS_OACP_RESULT_INVALID_OBJECT, NULL, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
/* Priority 3: Check Delete property bit */
|
||||
if (!(obj->properties & BLE_OTS_OBJ_PROP_DELETE)) {
|
||||
oacp_send_response(conn_id, opcode,
|
||||
BLE_OTS_OACP_RESULT_PROCEDURE_NOT_PERMITTED, NULL, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
/* Priority 4 & 5: Check if locked or transfer in progress */
|
||||
if (obj->locked_by != BLE_OTS_CONN_ID_NONE) {
|
||||
oacp_send_response(conn_id, opcode,
|
||||
BLE_OTS_OACP_RESULT_OBJECT_LOCKED, NULL, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
/* Check if any transfer is active on this object */
|
||||
for (int i = 0; i < CONFIG_BLE_OTS_SERVER_MAX_CONCURRENCY; i++) {
|
||||
ble_ots_server_conn_state_t *cs = &ble_ots_server_env.conn_pool[i];
|
||||
if (cs->in_pool && cs->transfer_active &&
|
||||
cs->transfer_object_id == obj_id) {
|
||||
oacp_send_response(conn_id, opcode,
|
||||
BLE_OTS_OACP_RESULT_OBJECT_LOCKED, NULL, 0);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/* Delete the object from the database */
|
||||
int rc = ble_ots_server_obj_db_delete(obj_id);
|
||||
if (rc != 0) {
|
||||
oacp_send_response(conn_id, opcode,
|
||||
BLE_OTS_OACP_RESULT_OPERATION_FAILED, NULL, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
/* Remove deleted object from all connections' created_obj_ids tracking */
|
||||
for (int i = 0; i < CONFIG_BLE_OTS_SERVER_MAX_CONCURRENCY; i++) {
|
||||
ble_ots_server_conn_state_t *cs = &ble_ots_server_env.conn_pool[i];
|
||||
if (!cs->in_pool) {
|
||||
continue;
|
||||
}
|
||||
for (int j = 0; j < cs->created_obj_count; j++) {
|
||||
if (cs->created_obj_ids[j] == obj_id) {
|
||||
/* Shift remaining entries left */
|
||||
for (int k = j; k < cs->created_obj_count - 1; k++) {
|
||||
cs->created_obj_ids[k] = cs->created_obj_ids[k + 1];
|
||||
}
|
||||
cs->created_obj_count--;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Invalidate Current Object for all connections that had this object selected */
|
||||
for (int i = 0; i < CONFIG_BLE_OTS_SERVER_MAX_CONCURRENCY; i++) {
|
||||
ble_ots_server_conn_state_t *cs = &ble_ots_server_env.conn_pool[i];
|
||||
if (cs->in_pool && cs->current_obj_valid &&
|
||||
cs->current_object_id == obj_id) {
|
||||
cs->current_obj_valid = false;
|
||||
cs->current_object_id = BLE_OTS_OBJ_ID_INVALID;
|
||||
}
|
||||
}
|
||||
|
||||
/* Send success indication */
|
||||
oacp_send_response(conn_id, opcode,
|
||||
BLE_OTS_OACP_RESULT_SUCCESS, NULL, 0);
|
||||
|
||||
/* Dispatch EVT_OBJECT_DELETED to application */
|
||||
ble_ots_server_cb_param_t cb_param;
|
||||
memset(&cb_param, 0, sizeof(cb_param));
|
||||
cb_param.object_deleted.object_id = obj_id;
|
||||
ble_ots_server_dispatch_event(BLE_OTS_SERVER_EVT_OBJECT_DELETED, &cb_param);
|
||||
|
||||
/* Dispatch Object Changed indication with Deletion flag + Source=Client */
|
||||
uint8_t flags = BLE_OTS_OBJ_CHANGED_FLAG_DELETION | BLE_OTS_OBJ_CHANGED_FLAG_SOURCE;
|
||||
ble_ots_server_dispatch_obj_changed(obj_id, flags, conn_id);
|
||||
}
|
||||
|
||||
/*****************************************************************************
|
||||
* OACP Calculate Checksum Procedure
|
||||
*****************************************************************************/
|
||||
static void oacp_calculate_checksum_proc(uint16_t conn_id, const uint8_t *param,
|
||||
uint16_t param_len)
|
||||
{
|
||||
uint8_t opcode = BLE_OTS_OACP_OPCODE_CALCULATE_CHECKSUM;
|
||||
|
||||
/* Priority 1: Check if Calculate Checksum is supported */
|
||||
if (!(ble_ots_server_env.config.oacp_features & BLE_OTS_OACP_FEAT_CALCULATE_CHECKSUM)) {
|
||||
oacp_send_response(conn_id, opcode,
|
||||
BLE_OTS_OACP_RESULT_OP_CODE_NOT_SUPPORTED, NULL, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
/* Priority 2: Check if Current Object is valid */
|
||||
ble_ots_obj_id_t obj_id = ble_ots_server_current_obj_get(conn_id);
|
||||
if (obj_id == BLE_OTS_OBJ_ID_INVALID) {
|
||||
oacp_send_response(conn_id, opcode,
|
||||
BLE_OTS_OACP_RESULT_INVALID_OBJECT, NULL, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
ble_ots_server_obj_t *obj = ble_ots_server_obj_db_lookup(obj_id);
|
||||
if (obj == NULL) {
|
||||
oacp_send_response(conn_id, opcode,
|
||||
BLE_OTS_OACP_RESULT_INVALID_OBJECT, NULL, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
/* Parse parameters: Offset (UINT32) + Length (UINT32) = 8 bytes */
|
||||
if (param_len != 8) {
|
||||
oacp_send_response(conn_id, opcode,
|
||||
BLE_OTS_OACP_RESULT_INVALID_PARAMETER, NULL, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
uint32_t offset = (uint32_t)param[0] |
|
||||
((uint32_t)param[1] << 8) |
|
||||
((uint32_t)param[2] << 16) |
|
||||
((uint32_t)param[3] << 24);
|
||||
|
||||
uint32_t length = (uint32_t)param[4] |
|
||||
((uint32_t)param[5] << 8) |
|
||||
((uint32_t)param[6] << 16) |
|
||||
((uint32_t)param[7] << 24);
|
||||
|
||||
/* Priority 3: The checksum range must be a non-empty range inside the
|
||||
* object contents. A zero Length, or an Offset at or past the end of the
|
||||
* object, selects no octets at all — same rule the Read procedure applies
|
||||
* (see ble_ots_server_oacp_read_proc). */
|
||||
if (length == 0 || offset >= obj->current_size ||
|
||||
(uint64_t)offset + (uint64_t)length > obj->current_size) {
|
||||
oacp_send_response(conn_id, opcode,
|
||||
BLE_OTS_OACP_RESULT_INVALID_PARAMETER, NULL, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
/* Priority 4 & 5: Check if locked by another connection or transfer in progress */
|
||||
if (object_is_locked_or_transferring(obj_id, conn_id)) {
|
||||
oacp_send_response(conn_id, opcode,
|
||||
BLE_OTS_OACP_RESULT_OBJECT_LOCKED, NULL, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
/* Compute CRC-32 directly from the resident object data buffer */
|
||||
if (obj->data == NULL) {
|
||||
oacp_send_response(conn_id, opcode,
|
||||
BLE_OTS_OACP_RESULT_OPERATION_FAILED, NULL, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
/* Object data is already in memory — compute checksum without copying */
|
||||
uint32_t checksum = ble_ots_checksum_calculate(obj->data, offset, length);
|
||||
|
||||
/* Build response parameter: Checksum (UINT32, little-endian) */
|
||||
uint8_t checksum_param[4];
|
||||
checksum_param[0] = (uint8_t)(checksum & 0xFF);
|
||||
checksum_param[1] = (uint8_t)((checksum >> 8) & 0xFF);
|
||||
checksum_param[2] = (uint8_t)((checksum >> 16) & 0xFF);
|
||||
checksum_param[3] = (uint8_t)((checksum >> 24) & 0xFF);
|
||||
|
||||
/* Send success indication with checksum */
|
||||
oacp_send_response(conn_id, opcode,
|
||||
BLE_OTS_OACP_RESULT_SUCCESS, checksum_param, 4);
|
||||
|
||||
/* Dispatch EVT_CHECKSUM_REQUEST to application */
|
||||
ble_ots_server_cb_param_t cb_param;
|
||||
memset(&cb_param, 0, sizeof(cb_param));
|
||||
cb_param.checksum.object_id = obj_id;
|
||||
cb_param.checksum.offset = offset;
|
||||
cb_param.checksum.length = length;
|
||||
cb_param.checksum.checksum = checksum;
|
||||
ble_ots_server_dispatch_event(BLE_OTS_SERVER_EVT_CHECKSUM_REQUEST, &cb_param);
|
||||
}
|
||||
|
||||
/*****************************************************************************
|
||||
* OACP Execute Procedure
|
||||
*****************************************************************************/
|
||||
static void oacp_execute_proc(uint16_t conn_id, const uint8_t *param,
|
||||
uint16_t param_len)
|
||||
{
|
||||
uint8_t opcode = BLE_OTS_OACP_OPCODE_EXECUTE;
|
||||
|
||||
/* Priority 1: Check if Execute is supported */
|
||||
if (!(ble_ots_server_env.config.oacp_features & BLE_OTS_OACP_FEAT_EXECUTE)) {
|
||||
oacp_send_response(conn_id, opcode,
|
||||
BLE_OTS_OACP_RESULT_OP_CODE_NOT_SUPPORTED, NULL, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
/* Priority 2: Check if Current Object is valid */
|
||||
ble_ots_obj_id_t obj_id = ble_ots_server_current_obj_get(conn_id);
|
||||
if (obj_id == BLE_OTS_OBJ_ID_INVALID) {
|
||||
oacp_send_response(conn_id, opcode,
|
||||
BLE_OTS_OACP_RESULT_INVALID_OBJECT, NULL, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
ble_ots_server_obj_t *obj = ble_ots_server_obj_db_lookup(obj_id);
|
||||
if (obj == NULL) {
|
||||
oacp_send_response(conn_id, opcode,
|
||||
BLE_OTS_OACP_RESULT_INVALID_OBJECT, NULL, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
/* Priority 3: Check Execute property bit */
|
||||
if (!(obj->properties & BLE_OTS_OBJ_PROP_EXECUTE)) {
|
||||
oacp_send_response(conn_id, opcode,
|
||||
BLE_OTS_OACP_RESULT_PROCEDURE_NOT_PERMITTED, NULL, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
/* Priority 4: Check if locked by another connection or transfer in progress */
|
||||
if (object_is_locked_or_transferring(obj_id, conn_id)) {
|
||||
oacp_send_response(conn_id, opcode,
|
||||
BLE_OTS_OACP_RESULT_OBJECT_LOCKED, NULL, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
/* The Result Code has to report whether the execution itself succeeded, so
|
||||
* the application runs the action first. The callback is invoked
|
||||
* synchronously in this context and may overwrite cb_param.execute.result. */
|
||||
ble_ots_server_cb_param_t cb_param;
|
||||
memset(&cb_param, 0, sizeof(cb_param));
|
||||
cb_param.execute.object_id = obj_id;
|
||||
cb_param.execute.param = (param_len > 0) ? param : NULL;
|
||||
cb_param.execute.param_len = param_len;
|
||||
cb_param.execute.result = BLE_OTS_OACP_RESULT_SUCCESS;
|
||||
ble_ots_server_dispatch_event(BLE_OTS_SERVER_EVT_EXECUTE, &cb_param);
|
||||
|
||||
/* Guard against an application returning a code outside the OACP range */
|
||||
uint8_t result = cb_param.execute.result;
|
||||
if (result < BLE_OTS_OACP_RESULT_SUCCESS ||
|
||||
result > BLE_OTS_OACP_RESULT_OPERATION_FAILED) {
|
||||
ESP_LOGE(TAG, "Invalid execute result 0x%02x from application", result);
|
||||
result = BLE_OTS_OACP_RESULT_OPERATION_FAILED;
|
||||
}
|
||||
|
||||
/* Send the indication carrying the application's result */
|
||||
oacp_send_response(conn_id, opcode, result, NULL, 0);
|
||||
}
|
||||
|
||||
/*****************************************************************************
|
||||
* OACP Read Procedure (delegate to ots_server_oacp_transfer.c)
|
||||
*****************************************************************************/
|
||||
static int oacp_handle_read(uint16_t conn_id, const uint8_t *param,
|
||||
uint16_t param_len)
|
||||
{
|
||||
uint8_t opcode = BLE_OTS_OACP_OPCODE_READ;
|
||||
|
||||
/* Parameter: Offset (UINT32) + Length (UINT32) = 8 bytes */
|
||||
if (param_len != 8) {
|
||||
oacp_send_response(conn_id, opcode,
|
||||
BLE_OTS_OACP_RESULT_INVALID_PARAMETER, NULL, 0);
|
||||
return 0;
|
||||
}
|
||||
|
||||
uint32_t offset = (uint32_t)param[0] |
|
||||
((uint32_t)param[1] << 8) |
|
||||
((uint32_t)param[2] << 16) |
|
||||
((uint32_t)param[3] << 24);
|
||||
|
||||
uint32_t length = (uint32_t)param[4] |
|
||||
((uint32_t)param[5] << 8) |
|
||||
((uint32_t)param[6] << 16) |
|
||||
((uint32_t)param[7] << 24);
|
||||
|
||||
return ble_ots_server_oacp_read_proc(conn_id, offset, length);
|
||||
}
|
||||
|
||||
/*****************************************************************************
|
||||
* OACP Write Procedure (delegate to ots_server_oacp_transfer.c)
|
||||
*****************************************************************************/
|
||||
static int oacp_handle_write(uint16_t conn_id, const uint8_t *param,
|
||||
uint16_t param_len)
|
||||
{
|
||||
uint8_t opcode = BLE_OTS_OACP_OPCODE_WRITE;
|
||||
|
||||
/* Parameter: Offset (UINT32) + Length (UINT32) + Mode (UINT8) = 9 bytes.
|
||||
* All three fields are mandatory for the Write Op Code. */
|
||||
if (param_len != 9) {
|
||||
oacp_send_response(conn_id, opcode,
|
||||
BLE_OTS_OACP_RESULT_INVALID_PARAMETER, NULL, 0);
|
||||
return 0;
|
||||
}
|
||||
|
||||
uint32_t offset = (uint32_t)param[0] |
|
||||
((uint32_t)param[1] << 8) |
|
||||
((uint32_t)param[2] << 16) |
|
||||
((uint32_t)param[3] << 24);
|
||||
|
||||
uint32_t length = (uint32_t)param[4] |
|
||||
((uint32_t)param[5] << 8) |
|
||||
((uint32_t)param[6] << 16) |
|
||||
((uint32_t)param[7] << 24);
|
||||
|
||||
uint8_t mode = param[8];
|
||||
|
||||
return ble_ots_server_oacp_write_proc(conn_id, offset, length, mode);
|
||||
}
|
||||
|
||||
/*****************************************************************************
|
||||
* OACP Abort Procedure (delegate to ots_server_oacp_transfer.c)
|
||||
*****************************************************************************/
|
||||
static int oacp_handle_abort(uint16_t conn_id)
|
||||
{
|
||||
return ble_ots_server_oacp_abort_proc(conn_id);
|
||||
}
|
||||
|
||||
/*****************************************************************************
|
||||
* OACP Characteristic Write Handler — main dispatch
|
||||
*
|
||||
* This is the GATT access callback for writes to the OACP characteristic.
|
||||
* It is called from the GATT service table registered in ots_server_init.c.
|
||||
*****************************************************************************/
|
||||
static int ble_ots_server_oacp_write_handler(uint16_t conn_id, uint16_t attr_handle,
|
||||
struct ble_gatt_access_ctxt *ctxt, void *arg)
|
||||
{
|
||||
uint8_t stack_buf[64];
|
||||
uint8_t *buf = stack_buf;
|
||||
uint8_t *dyn_buf = NULL;
|
||||
uint16_t data_len = 0;
|
||||
int rc;
|
||||
|
||||
/* Extract write data from mbuf */
|
||||
uint16_t om_len = OS_MBUF_PKTLEN(ctxt->om);
|
||||
if (om_len == 0) {
|
||||
return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN;
|
||||
}
|
||||
|
||||
if (om_len > sizeof(stack_buf)) {
|
||||
/* Only the Execute opcode allows arbitrary-length parameters.
|
||||
* Peek at the first byte (opcode) to decide whether to accept. */
|
||||
uint8_t peek_opcode;
|
||||
rc = ble_hs_mbuf_to_flat(ctxt->om, &peek_opcode, 1, NULL);
|
||||
if (rc != 0 || peek_opcode != BLE_OTS_OACP_OPCODE_EXECUTE) {
|
||||
return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN;
|
||||
}
|
||||
|
||||
dyn_buf = malloc(om_len);
|
||||
if (dyn_buf == NULL) {
|
||||
return BLE_ATT_ERR_INSUFFICIENT_RES;
|
||||
}
|
||||
buf = dyn_buf;
|
||||
}
|
||||
|
||||
rc = ble_hs_mbuf_to_flat(ctxt->om, buf, om_len, &data_len);
|
||||
if (rc != 0) {
|
||||
free(dyn_buf);
|
||||
return BLE_ATT_ERR_UNLIKELY;
|
||||
}
|
||||
|
||||
if (data_len < 1) {
|
||||
free(dyn_buf);
|
||||
return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN;
|
||||
}
|
||||
|
||||
uint8_t opcode = buf[0];
|
||||
const uint8_t *param = (data_len > 1) ? &buf[1] : NULL;
|
||||
uint16_t param_len = data_len - 1;
|
||||
|
||||
/* ATT-level check: concurrency */
|
||||
if (!ble_ots_server_concurrency_check(conn_id)) {
|
||||
free(dyn_buf);
|
||||
return BLE_OTS_APP_ERR_CONCURRENCY_LIMIT_EXCEEDED;
|
||||
}
|
||||
|
||||
/* ATT-level check: OACP CCCD must be configured for indications */
|
||||
if (!oacp_cccd_configured(conn_id)) {
|
||||
free(dyn_buf);
|
||||
return ATT_ERR_CCCD_IMPROPERLY_CONFIGURED;
|
||||
}
|
||||
|
||||
/* ATT-level check: procedure already in progress */
|
||||
ble_ots_server_conn_state_t *cs = ble_ots_server_conn_state_get(conn_id);
|
||||
if (cs == NULL) {
|
||||
free(dyn_buf);
|
||||
return BLE_ATT_ERR_UNLIKELY;
|
||||
}
|
||||
|
||||
/* For Read/Write/Abort, check if a transfer is already active (only for
|
||||
* non-abort opcodes when a transfer is in progress) */
|
||||
if (cs->transfer_active && opcode != BLE_OTS_OACP_OPCODE_ABORT) {
|
||||
free(dyn_buf);
|
||||
return ATT_ERR_PROC_ALREADY_IN_PROGRESS;
|
||||
}
|
||||
|
||||
/* ATT-level validation: parameter size for opcodes that require no parameter */
|
||||
switch (opcode) {
|
||||
case BLE_OTS_OACP_OPCODE_DELETE:
|
||||
case BLE_OTS_OACP_OPCODE_ABORT:
|
||||
if (param_len != 0) {
|
||||
free(dyn_buf);
|
||||
return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN;
|
||||
}
|
||||
break;
|
||||
|
||||
case BLE_OTS_OACP_OPCODE_CREATE:
|
||||
/* Size (4) + Type UUID (2 or 16) = 6 or 20 */
|
||||
if (param_len < 6 || (param_len != 6 && param_len != 20)) {
|
||||
free(dyn_buf);
|
||||
return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN;
|
||||
}
|
||||
break;
|
||||
|
||||
case BLE_OTS_OACP_OPCODE_CALCULATE_CHECKSUM:
|
||||
/* Offset (4) + Length (4) = 8 */
|
||||
if (param_len != 8) {
|
||||
free(dyn_buf);
|
||||
return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN;
|
||||
}
|
||||
break;
|
||||
|
||||
case BLE_OTS_OACP_OPCODE_READ:
|
||||
/* Offset (4) + Length (4) = 8 */
|
||||
if (param_len != 8) {
|
||||
free(dyn_buf);
|
||||
return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN;
|
||||
}
|
||||
break;
|
||||
|
||||
case BLE_OTS_OACP_OPCODE_WRITE:
|
||||
/* Offset (4) + Length (4) + Mode (1) = 9 */
|
||||
if (param_len != 9) {
|
||||
free(dyn_buf);
|
||||
return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN;
|
||||
}
|
||||
break;
|
||||
|
||||
case BLE_OTS_OACP_OPCODE_EXECUTE:
|
||||
/* Optional parameter — any length is acceptable */
|
||||
break;
|
||||
|
||||
default:
|
||||
/* Unknown/reserved opcode — accept the write, then send Op Code Not Supported */
|
||||
break;
|
||||
}
|
||||
|
||||
/* ATT Write Response is sent by NimBLE stack (return 0).
|
||||
* Now dispatch to the appropriate procedure handler.
|
||||
* The procedure handler will send the OACP response indication. */
|
||||
|
||||
switch (opcode) {
|
||||
case BLE_OTS_OACP_OPCODE_CREATE:
|
||||
oacp_create_proc(conn_id, param, param_len);
|
||||
break;
|
||||
|
||||
case BLE_OTS_OACP_OPCODE_DELETE:
|
||||
oacp_delete_proc(conn_id);
|
||||
break;
|
||||
|
||||
case BLE_OTS_OACP_OPCODE_CALCULATE_CHECKSUM:
|
||||
oacp_calculate_checksum_proc(conn_id, param, param_len);
|
||||
break;
|
||||
|
||||
case BLE_OTS_OACP_OPCODE_EXECUTE:
|
||||
oacp_execute_proc(conn_id, param, param_len);
|
||||
break;
|
||||
|
||||
case BLE_OTS_OACP_OPCODE_READ:
|
||||
rc = oacp_handle_read(conn_id, param, param_len);
|
||||
if (rc != 0) {
|
||||
ESP_LOGE(TAG, "OACP Read proc failed, rc=%d", rc);
|
||||
}
|
||||
break;
|
||||
|
||||
case BLE_OTS_OACP_OPCODE_WRITE:
|
||||
rc = oacp_handle_write(conn_id, param, param_len);
|
||||
if (rc != 0) {
|
||||
ESP_LOGE(TAG, "OACP Write proc failed, rc=%d", rc);
|
||||
}
|
||||
break;
|
||||
|
||||
case BLE_OTS_OACP_OPCODE_ABORT:
|
||||
rc = oacp_handle_abort(conn_id);
|
||||
if (rc != 0) {
|
||||
ESP_LOGE(TAG, "OACP Abort proc failed, rc=%d", rc);
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
/* Reserved or unknown opcode — respond with Op Code Not Supported */
|
||||
oacp_send_response(conn_id, opcode,
|
||||
BLE_OTS_OACP_RESULT_OP_CODE_NOT_SUPPORTED, NULL, 0);
|
||||
break;
|
||||
}
|
||||
|
||||
free(dyn_buf);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*****************************************************************************
|
||||
* OACP Characteristic Write — public wrapper
|
||||
*
|
||||
* Called from the central GATT access callback in ots_server_init.c.
|
||||
*****************************************************************************/
|
||||
int ble_ots_server_oacp_write(uint16_t conn_handle,
|
||||
struct ble_gatt_access_ctxt *ctxt)
|
||||
{
|
||||
return ble_ots_server_oacp_write_handler(conn_handle, 0, ctxt, NULL);
|
||||
}
|
||||
@@ -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 1582–9999; anything else is "unknown" */
|
||||
int year = tm_now.tm_year + 1900;
|
||||
if (year < 1582 || year > 9999) {
|
||||
return false;
|
||||
}
|
||||
|
||||
ts->year = (uint16_t)year;
|
||||
ts->month = (uint8_t)(tm_now.tm_mon + 1);
|
||||
ts->day = (uint8_t)tm_now.tm_mday;
|
||||
ts->hours = (uint8_t)tm_now.tm_hour;
|
||||
ts->minutes = (uint8_t)tm_now.tm_min;
|
||||
/* Date-Time has no leap-second representation */
|
||||
ts->seconds = (uint8_t)(tm_now.tm_sec > 59 ? 59 : tm_now.tm_sec);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/*****************************************************************************
|
||||
* Helper: Read-transfer back-off on transient buffer exhaustion
|
||||
*
|
||||
* A bulk read fills the host mbuf pool faster than the controller drains it.
|
||||
* The resulting allocation failures are flow-control backpressure, not transfer
|
||||
* failures, so the transfer pauses and resumes from a short callout instead of
|
||||
* tearing the OTC channel down.
|
||||
*****************************************************************************/
|
||||
static void transfer_retry_stop(ble_ots_server_conn_state_t *cs)
|
||||
{
|
||||
if (cs->transfer_retry_timer_inited) {
|
||||
ble_npl_callout_stop(&cs->transfer_retry_timer);
|
||||
}
|
||||
cs->transfer_retry_count = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Pause the read transfer and schedule a retry of the current chunk.
|
||||
*
|
||||
* @param cs Connection state owning the transfer
|
||||
* @param cause Human-readable reason, logged on retry and on give-up
|
||||
* @return 0 when a retry is pending, BLE_HS_ENOMEM when the budget is exhausted
|
||||
*/
|
||||
static int transfer_backoff(ble_ots_server_conn_state_t *cs, const char *cause)
|
||||
{
|
||||
if (cs->transfer_retry_count >= OTS_TRANSFER_RETRY_MAX) {
|
||||
ESP_LOGE(TAG, "Read transfer aborted: %s persisted over %d retries",
|
||||
cause, OTS_TRANSFER_RETRY_MAX);
|
||||
return BLE_HS_ENOMEM;
|
||||
}
|
||||
|
||||
if (!cs->transfer_retry_timer_inited) {
|
||||
int rc = ble_npl_callout_init(&cs->transfer_retry_timer,
|
||||
nimble_port_get_dflt_eventq(),
|
||||
ots_transfer_retry_cb, cs);
|
||||
if (rc != 0) {
|
||||
ESP_LOGE(TAG, "Failed to init transfer retry callout, rc=%d", rc);
|
||||
return BLE_HS_ENOMEM;
|
||||
}
|
||||
cs->transfer_retry_timer_inited = true;
|
||||
}
|
||||
|
||||
cs->transfer_retry_count++;
|
||||
ESP_LOGD(TAG, "Read transfer backpressure (%s), retry %u in %d ms",
|
||||
cause, cs->transfer_retry_count, OTS_TRANSFER_RETRY_DELAY_MS);
|
||||
|
||||
ble_npl_callout_reset(&cs->transfer_retry_timer,
|
||||
ble_npl_time_ms_to_ticks32(OTS_TRANSFER_RETRY_DELAY_MS));
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*****************************************************************************
|
||||
* Helper: Send object data over L2CAP OTC in chunks
|
||||
*****************************************************************************/
|
||||
static int send_object_data_chunks(ble_ots_server_conn_state_t *cs)
|
||||
{
|
||||
if (!cs || !cs->transfer_active || !cs->transfer_is_read) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
struct ble_l2cap_chan *chan = cs->otc_chan;
|
||||
if (!chan) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* Get channel info to determine MTU for chunking */
|
||||
struct ble_l2cap_chan_info chan_info;
|
||||
int rc = ble_l2cap_get_chan_info(chan, &chan_info);
|
||||
if (rc != 0) {
|
||||
ESP_LOGE(TAG, "Failed to get L2CAP channel info, rc=%d", rc);
|
||||
return rc;
|
||||
}
|
||||
|
||||
uint16_t mtu = chan_info.peer_coc_mtu;
|
||||
if (mtu == 0) {
|
||||
mtu = 256; /* fallback */
|
||||
}
|
||||
|
||||
while (cs->transfer_bytes_done < cs->transfer_length) {
|
||||
uint32_t remaining = cs->transfer_length - cs->transfer_bytes_done;
|
||||
uint32_t chunk_size = (remaining > mtu) ? mtu : remaining;
|
||||
|
||||
/* Allocate mbuf for this chunk. A failure here means the fragments
|
||||
* already queued for the controller still hold the pool, so wait for
|
||||
* them to drain rather than failing the transfer. */
|
||||
struct os_mbuf *sdu = os_msys_get_pkthdr(chunk_size, 0);
|
||||
if (!sdu) {
|
||||
return transfer_backoff(cs, "mbuf pool exhausted");
|
||||
}
|
||||
|
||||
/* Read object data into a temporary buffer */
|
||||
uint8_t *tmp_buf = malloc(chunk_size);
|
||||
if (!tmp_buf) {
|
||||
os_mbuf_free_chain(sdu);
|
||||
return transfer_backoff(cs, "temp buffer allocation failed");
|
||||
}
|
||||
|
||||
rc = ble_ots_server_obj_data_read(cs->transfer_object_id,
|
||||
cs->transfer_offset + cs->transfer_bytes_done,
|
||||
chunk_size, tmp_buf);
|
||||
if (rc != 0) {
|
||||
free(tmp_buf);
|
||||
os_mbuf_free_chain(sdu);
|
||||
ESP_LOGE(TAG, "Failed to read object data, rc=%d", rc);
|
||||
return rc;
|
||||
}
|
||||
|
||||
rc = os_mbuf_append(sdu, tmp_buf, (uint16_t)chunk_size);
|
||||
free(tmp_buf);
|
||||
if (rc != 0) {
|
||||
os_mbuf_free_chain(sdu);
|
||||
ESP_LOGE(TAG, "Failed to append data to mbuf, rc=%d", rc);
|
||||
return rc;
|
||||
}
|
||||
|
||||
rc = ble_l2cap_send(chan, sdu);
|
||||
if (rc == BLE_HS_ESTALLED) {
|
||||
/* L2CAP credits exhausted. The stack keeps the SDU queued and
|
||||
* flushes it once the peer grants more credits, so count the chunk
|
||||
* as sent and resume when TX_UNSTALLED fires. */
|
||||
cs->transfer_bytes_done += chunk_size;
|
||||
cs->transfer_retry_count = 0;
|
||||
ble_ots_server_transfer_timer_reset(cs->conn_id);
|
||||
ESP_LOGD(TAG, "L2CAP send stalled, bytes_done=%lu", (unsigned long)cs->transfer_bytes_done);
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (rc != 0) {
|
||||
/* ble_l2cap_send() consumes the SDU only on success, and on
|
||||
* ESTALLED where the stack retains it. On every other return the
|
||||
* mbuf is still ours — including the internal failure paths, which
|
||||
* detach it from the channel without freeing it. */
|
||||
os_mbuf_free_chain(sdu);
|
||||
|
||||
/* Host or controller buffers ran dry for a moment; this is
|
||||
* backpressure, not a transfer failure. */
|
||||
if (rc == BLE_HS_ENOMEM || rc == BLE_HS_EBUSY) {
|
||||
return transfer_backoff(cs, "L2CAP TX buffers exhausted");
|
||||
}
|
||||
|
||||
ESP_LOGE(TAG, "ble_l2cap_send failed, rc=%d", rc);
|
||||
return rc;
|
||||
}
|
||||
|
||||
/* Chunk accepted: the back-off budget applies per chunk, not per transfer */
|
||||
cs->transfer_bytes_done += chunk_size;
|
||||
cs->transfer_retry_count = 0;
|
||||
ble_ots_server_transfer_timer_reset(cs->conn_id);
|
||||
}
|
||||
|
||||
/* All data sent successfully */
|
||||
if (cs->transfer_bytes_done >= cs->transfer_length) {
|
||||
transfer_complete_read(cs, BLE_OTS_TRANSFER_SUCCESS);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*****************************************************************************
|
||||
* Transfer Complete: Read
|
||||
*****************************************************************************/
|
||||
static void transfer_complete_read(ble_ots_server_conn_state_t *cs,
|
||||
ble_ots_server_transfer_status_t status)
|
||||
{
|
||||
if (!cs) {
|
||||
return;
|
||||
}
|
||||
|
||||
ble_ots_server_transfer_timer_stop(cs->conn_id);
|
||||
transfer_retry_stop(cs);
|
||||
|
||||
ble_ots_server_cb_param_t param;
|
||||
memset(¶m, 0, sizeof(param));
|
||||
param.read_complete.object_id = cs->transfer_object_id;
|
||||
param.read_complete.offset = cs->transfer_offset;
|
||||
param.read_complete.length = cs->transfer_length;
|
||||
param.read_complete.bytes_sent = cs->transfer_bytes_done;
|
||||
param.read_complete.status = status;
|
||||
|
||||
cs->transfer_active = false;
|
||||
cs->transfer_is_read = false;
|
||||
|
||||
ble_ots_server_dispatch_event(BLE_OTS_SERVER_EVT_READ_COMPLETE, ¶m);
|
||||
}
|
||||
|
||||
/*****************************************************************************
|
||||
* Transfer Complete: Write
|
||||
*****************************************************************************/
|
||||
static void transfer_complete_write(ble_ots_server_conn_state_t *cs,
|
||||
ble_ots_server_transfer_status_t status)
|
||||
{
|
||||
if (!cs) {
|
||||
return;
|
||||
}
|
||||
|
||||
ble_ots_server_transfer_timer_stop(cs->conn_id);
|
||||
|
||||
ble_ots_obj_id_t obj_id = cs->transfer_object_id;
|
||||
uint32_t offset = cs->transfer_offset;
|
||||
uint32_t bytes_received = cs->transfer_bytes_done;
|
||||
|
||||
/* The object changed if data landed in it, or if the procedure already
|
||||
* truncated it — a Truncate takes effect when the Write procedure starts,
|
||||
* so it stands even when the transfer is later interrupted. */
|
||||
bool obj_modified = (bytes_received > 0) || cs->transfer_truncated;
|
||||
|
||||
/* Update current size if the write extended the object */
|
||||
ble_ots_server_obj_t *obj = ble_ots_server_obj_db_lookup(obj_id);
|
||||
if (obj) {
|
||||
uint32_t new_end = offset + bytes_received;
|
||||
if (new_end > obj->current_size) {
|
||||
obj->current_size = new_end;
|
||||
}
|
||||
|
||||
/* Refresh Last-Modified only when the object actually changed and a
|
||||
* real time source is available; otherwise keep the value the client
|
||||
* or the application wrote instead of blanking it to "unknown". */
|
||||
ble_ots_date_time_t now;
|
||||
if (obj_modified && get_current_timestamp(&now)) {
|
||||
obj->last_modified = now;
|
||||
}
|
||||
}
|
||||
|
||||
/* Unlock the object */
|
||||
ble_ots_server_obj_unlock(obj_id);
|
||||
|
||||
cs->transfer_active = false;
|
||||
cs->transfer_is_read = false;
|
||||
cs->transfer_truncated = false;
|
||||
|
||||
/* Dispatch Object Changed indication with content flag */
|
||||
if (status == BLE_OTS_TRANSFER_SUCCESS || obj_modified) {
|
||||
uint8_t flags = BLE_OTS_OBJ_CHANGED_FLAG_SOURCE | BLE_OTS_OBJ_CHANGED_FLAG_CONTENT;
|
||||
ble_ots_server_dispatch_obj_changed(obj_id, flags, cs->conn_id);
|
||||
}
|
||||
|
||||
/* Fire write complete event */
|
||||
ble_ots_server_cb_param_t param;
|
||||
memset(¶m, 0, sizeof(param));
|
||||
param.write_complete.object_id = obj_id;
|
||||
param.write_complete.offset = offset;
|
||||
param.write_complete.bytes_received = bytes_received;
|
||||
param.write_complete.status = status;
|
||||
|
||||
ble_ots_server_dispatch_event(BLE_OTS_SERVER_EVT_WRITE_COMPLETE, ¶m);
|
||||
}
|
||||
|
||||
/*****************************************************************************
|
||||
* Read-Transfer Back-off Retry Callback
|
||||
*
|
||||
* Fires in the NimBLE host task after transfer_backoff() paused a read that
|
||||
* ran out of TX buffers.
|
||||
*****************************************************************************/
|
||||
static void ots_transfer_retry_cb(struct ble_npl_event *ev)
|
||||
{
|
||||
if (!BLE_OTS_SERVER_ENV_OK()) {
|
||||
return;
|
||||
}
|
||||
|
||||
ble_ots_server_conn_state_t *cs =
|
||||
(ble_ots_server_conn_state_t *)ble_npl_event_get_arg(ev);
|
||||
if (!cs || !cs->in_pool || !cs->transfer_active || !cs->transfer_is_read) {
|
||||
return;
|
||||
}
|
||||
|
||||
int rc = send_object_data_chunks(cs);
|
||||
if (rc != 0) {
|
||||
ESP_LOGE(TAG, "Failed to resume read transfer after back-off, rc=%d", rc);
|
||||
ble_ots_server_otc_close(cs->conn_id);
|
||||
transfer_complete_read(cs, BLE_OTS_TRANSFER_CHANNEL_CLOSED);
|
||||
}
|
||||
}
|
||||
|
||||
/*****************************************************************************
|
||||
* L2CAP OTC Data Receive Callback (for write transfers)
|
||||
*****************************************************************************/
|
||||
void ble_ots_server_otc_receive_cb(uint16_t conn_handle, struct ble_l2cap_chan *chan,
|
||||
struct os_mbuf *sdu_rx)
|
||||
{
|
||||
ble_ots_server_conn_state_t *cs = ble_ots_server_conn_state_get(conn_handle);
|
||||
if (!cs || !cs->transfer_active || cs->transfer_is_read) {
|
||||
/* Not expecting data; free the mbuf */
|
||||
if (sdu_rx) {
|
||||
os_mbuf_free_chain(sdu_rx);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!sdu_rx) {
|
||||
return;
|
||||
}
|
||||
|
||||
/* Calculate total received data length from mbuf chain */
|
||||
uint16_t data_len = OS_MBUF_PKTLEN(sdu_rx);
|
||||
if (data_len == 0) {
|
||||
os_mbuf_free_chain(sdu_rx);
|
||||
return;
|
||||
}
|
||||
|
||||
/* Check for excess data */
|
||||
uint32_t remaining = cs->transfer_length - cs->transfer_bytes_done;
|
||||
if (data_len > remaining) {
|
||||
ESP_LOGW(TAG, "Excess data received: got %u, expected at most %lu",
|
||||
data_len, (unsigned long)remaining);
|
||||
os_mbuf_free_chain(sdu_rx);
|
||||
|
||||
/* Close OTC channel to prevent further data */
|
||||
ble_ots_server_otc_close(conn_handle);
|
||||
transfer_complete_write(cs, BLE_OTS_TRANSFER_EXCESS_DATA);
|
||||
return;
|
||||
}
|
||||
|
||||
/* Extract data from mbuf chain and write to object */
|
||||
uint32_t write_offset = cs->transfer_offset + cs->transfer_bytes_done;
|
||||
struct os_mbuf *cur = sdu_rx;
|
||||
uint32_t written = 0;
|
||||
|
||||
int write_err = 0;
|
||||
while (cur != NULL && written < data_len) {
|
||||
if (cur->om_len > 0) {
|
||||
int rc = ble_ots_server_obj_data_write(cs->transfer_object_id,
|
||||
write_offset + written,
|
||||
cur->om_data, cur->om_len);
|
||||
if (rc != 0) {
|
||||
ESP_LOGE(TAG, "Failed to write object data, rc=%d", rc);
|
||||
write_err = rc;
|
||||
break;
|
||||
}
|
||||
written += cur->om_len;
|
||||
}
|
||||
cur = SLIST_NEXT(cur, om_next);
|
||||
}
|
||||
|
||||
os_mbuf_free_chain(sdu_rx);
|
||||
|
||||
cs->transfer_bytes_done += written;
|
||||
|
||||
/* If writing to the object database failed, abort the transfer immediately */
|
||||
if (write_err != 0) {
|
||||
ESP_LOGE(TAG, "Aborting write transfer due to obj_data_write failure, rc=%d", write_err);
|
||||
ble_ots_server_otc_close(conn_handle);
|
||||
transfer_complete_write(cs, BLE_OTS_TRANSFER_CHANNEL_CLOSED);
|
||||
return;
|
||||
}
|
||||
|
||||
/* Reset inactivity timer */
|
||||
ble_ots_server_transfer_timer_reset(cs->conn_id);
|
||||
|
||||
ESP_LOGD(TAG, "Write transfer: received %lu/%lu bytes",
|
||||
(unsigned long)cs->transfer_bytes_done,
|
||||
(unsigned long)cs->transfer_length);
|
||||
|
||||
/* Check if transfer is complete */
|
||||
if (cs->transfer_bytes_done >= cs->transfer_length) {
|
||||
transfer_complete_write(cs, BLE_OTS_TRANSFER_SUCCESS);
|
||||
}
|
||||
}
|
||||
|
||||
/*****************************************************************************
|
||||
* L2CAP OTC TX Un-stalled Callback (for read transfers)
|
||||
*****************************************************************************/
|
||||
void ble_ots_server_otc_tx_unstalled_cb(uint16_t conn_handle,
|
||||
struct ble_l2cap_chan *chan)
|
||||
{
|
||||
ble_ots_server_conn_state_t *cs = ble_ots_server_conn_state_get(conn_handle);
|
||||
if (!cs || !cs->transfer_active || !cs->transfer_is_read) {
|
||||
return;
|
||||
}
|
||||
|
||||
/* Resume sending data */
|
||||
int rc = send_object_data_chunks(cs);
|
||||
if (rc != 0) {
|
||||
ESP_LOGE(TAG, "Failed to resume read transfer, rc=%d", rc);
|
||||
ble_ots_server_otc_close(conn_handle);
|
||||
transfer_complete_read(cs, BLE_OTS_TRANSFER_CHANNEL_CLOSED);
|
||||
}
|
||||
}
|
||||
|
||||
/*****************************************************************************
|
||||
* L2CAP OTC Channel Closed Callback
|
||||
*****************************************************************************/
|
||||
void ble_ots_server_otc_disconnected_cb(uint16_t conn_handle,
|
||||
struct ble_l2cap_chan *chan)
|
||||
{
|
||||
ble_ots_server_conn_state_t *cs = ble_ots_server_conn_state_get(conn_handle);
|
||||
if (!cs) {
|
||||
return;
|
||||
}
|
||||
|
||||
/* Clear the OTC channel reference */
|
||||
cs->otc_chan = NULL;
|
||||
|
||||
if (!cs->transfer_active) {
|
||||
return;
|
||||
}
|
||||
|
||||
ESP_LOGW(TAG, "OTC channel closed during transfer on conn_id=%d", conn_handle);
|
||||
|
||||
if (cs->transfer_is_read) {
|
||||
transfer_complete_read(cs, BLE_OTS_TRANSFER_CHANNEL_CLOSED);
|
||||
} else {
|
||||
transfer_complete_write(cs, BLE_OTS_TRANSFER_CHANNEL_CLOSED);
|
||||
}
|
||||
}
|
||||
|
||||
/*****************************************************************************
|
||||
* Deferred read-data send via NimBLE event queue
|
||||
*
|
||||
* The initial burst of object data must be sent AFTER the OACP response
|
||||
* indication. Both are posted to the NimBLE default event queue (FIFO), so
|
||||
* queueing the data-send event after the indication event guarantees ordering.
|
||||
*****************************************************************************/
|
||||
static void ots_transfer_data_event_cb(struct ble_npl_event *ev)
|
||||
{
|
||||
/* cs points into the control block — only valid while it is allocated */
|
||||
if (!BLE_OTS_SERVER_ENV_OK()) {
|
||||
return;
|
||||
}
|
||||
|
||||
ble_ots_server_conn_state_t *cs =
|
||||
(ble_ots_server_conn_state_t *)ble_npl_event_get_arg(ev);
|
||||
if (!cs || !cs->in_pool || !cs->transfer_active || !cs->transfer_is_read) {
|
||||
return;
|
||||
}
|
||||
|
||||
int rc = send_object_data_chunks(cs);
|
||||
if (rc != 0) {
|
||||
ESP_LOGE(TAG, "Failed to send object data (event), rc=%d", rc);
|
||||
ble_ots_server_otc_close(cs->conn_id);
|
||||
transfer_complete_read(cs, BLE_OTS_TRANSFER_CHANNEL_CLOSED);
|
||||
}
|
||||
}
|
||||
|
||||
static void ots_transfer_data_schedule(ble_ots_server_conn_state_t *cs)
|
||||
{
|
||||
if (!cs->transfer_data_ev_inited) {
|
||||
ble_npl_event_init(&cs->transfer_data_ev, ots_transfer_data_event_cb, cs);
|
||||
cs->transfer_data_ev_inited = true;
|
||||
}
|
||||
ble_npl_eventq_put(nimble_port_get_dflt_eventq(), &cs->transfer_data_ev);
|
||||
}
|
||||
|
||||
/*****************************************************************************
|
||||
* OACP Read Procedure
|
||||
*****************************************************************************/
|
||||
int ble_ots_server_oacp_read_proc(uint16_t conn_id, uint32_t offset, uint32_t length)
|
||||
{
|
||||
ble_ots_server_conn_state_t *cs = ble_ots_server_conn_state_get(conn_id);
|
||||
if (!cs) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* Priority 1: Check OACP Read feature support (bit 4) */
|
||||
if (!(ble_ots_server_env.ots_feature.oacp_features & BLE_OTS_OACP_FEAT_READ)) {
|
||||
send_oacp_response(conn_id, BLE_OTS_OACP_OPCODE_READ,
|
||||
BLE_OTS_OACP_RESULT_OP_CODE_NOT_SUPPORTED);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Priority 2: Check Current Object is valid */
|
||||
if (!cs->current_obj_valid) {
|
||||
send_oacp_response(conn_id, BLE_OTS_OACP_OPCODE_READ,
|
||||
BLE_OTS_OACP_RESULT_INVALID_OBJECT);
|
||||
return 0;
|
||||
}
|
||||
|
||||
ble_ots_obj_id_t obj_id = cs->current_object_id;
|
||||
ble_ots_server_obj_t *obj = ble_ots_server_obj_db_lookup(obj_id);
|
||||
if (!obj) {
|
||||
send_oacp_response(conn_id, BLE_OTS_OACP_OPCODE_READ,
|
||||
BLE_OTS_OACP_RESULT_INVALID_OBJECT);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Priority 3: Check object Read property (bit 2) */
|
||||
if (!(obj->properties & BLE_OTS_OBJ_PROP_READ)) {
|
||||
send_oacp_response(conn_id, BLE_OTS_OACP_OPCODE_READ,
|
||||
BLE_OTS_OACP_RESULT_PROCEDURE_NOT_PERMITTED);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Priority 4: Check OTC channel available */
|
||||
struct ble_l2cap_chan *chan = ble_ots_server_otc_get(conn_id);
|
||||
if (!chan) {
|
||||
send_oacp_response(conn_id, BLE_OTS_OACP_OPCODE_READ,
|
||||
BLE_OTS_OACP_RESULT_CHANNEL_UNAVAILABLE);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Priority 5: Offset exceeds Current Size */
|
||||
if (offset > obj->current_size) {
|
||||
send_oacp_response(conn_id, BLE_OTS_OACP_OPCODE_READ,
|
||||
BLE_OTS_OACP_RESULT_INVALID_PARAMETER);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Offset and Length are raw client input: reject a sum that would wrap
|
||||
* before it can slip past the Priority 6 bounds check below. */
|
||||
if (length > 0 && offset > UINT32_MAX - length) {
|
||||
send_oacp_response(conn_id, BLE_OTS_OACP_OPCODE_READ,
|
||||
BLE_OTS_OACP_RESULT_INVALID_PARAMETER);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Priority 6: Offset + Length exceeds Current Size */
|
||||
if ((offset + length) > obj->current_size) {
|
||||
send_oacp_response(conn_id, BLE_OTS_OACP_OPCODE_READ,
|
||||
BLE_OTS_OACP_RESULT_INVALID_PARAMETER);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Priority 7: Length exceeds server capacity to read
|
||||
* (we assume we can always read from our own DB, so skip unless length is 0) */
|
||||
if (length == 0) {
|
||||
send_oacp_response(conn_id, BLE_OTS_OACP_OPCODE_READ,
|
||||
BLE_OTS_OACP_RESULT_INVALID_PARAMETER);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Priority 8: Object locked by another client */
|
||||
if (obj->locked_by != BLE_OTS_CONN_ID_NONE && obj->locked_by != conn_id) {
|
||||
send_oacp_response(conn_id, BLE_OTS_OACP_OPCODE_READ,
|
||||
BLE_OTS_OACP_RESULT_OBJECT_LOCKED);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Priority 9: Transfer already in progress on this object */
|
||||
if (cs->transfer_active) {
|
||||
send_oacp_response(conn_id, BLE_OTS_OACP_OPCODE_READ,
|
||||
BLE_OTS_OACP_RESULT_OBJECT_LOCKED);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* All checks passed — mark transfer active */
|
||||
cs->transfer_active = true;
|
||||
cs->transfer_is_read = true;
|
||||
cs->transfer_object_id = obj_id;
|
||||
cs->transfer_offset = offset;
|
||||
cs->transfer_length = length;
|
||||
cs->transfer_bytes_done = 0;
|
||||
transfer_retry_stop(cs);
|
||||
|
||||
/* Send Success indication */
|
||||
int rc = send_oacp_response(conn_id, BLE_OTS_OACP_OPCODE_READ,
|
||||
BLE_OTS_OACP_RESULT_SUCCESS);
|
||||
if (rc != 0) {
|
||||
ESP_LOGE(TAG, "Failed to send OACP Read success indication, rc=%d", rc);
|
||||
cs->transfer_active = false;
|
||||
cs->transfer_is_read = false;
|
||||
return rc;
|
||||
}
|
||||
|
||||
/* Start transfer inactivity timer */
|
||||
ble_ots_server_transfer_timer_start(conn_id);
|
||||
|
||||
/* Queue the object data send as a NimBLE event. Because the response
|
||||
* indication was also queued (via ble_ots_server_indicate_response) and the
|
||||
* default event queue is FIFO, the data is sent AFTER the indication. */
|
||||
ots_transfer_data_schedule(cs);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*****************************************************************************
|
||||
* OACP Write Procedure
|
||||
*****************************************************************************/
|
||||
int ble_ots_server_oacp_write_proc(uint16_t conn_id, uint32_t offset,
|
||||
uint32_t length, uint8_t mode)
|
||||
{
|
||||
ble_ots_server_conn_state_t *cs = ble_ots_server_conn_state_get(conn_id);
|
||||
if (!cs) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
bool truncate = (mode & BLE_OTS_OACP_WRITE_MODE_TRUNCATE) != 0;
|
||||
|
||||
/* Priority 1: Check OACP Write feature support (bit 5) */
|
||||
if (!(ble_ots_server_env.ots_feature.oacp_features & BLE_OTS_OACP_FEAT_WRITE)) {
|
||||
send_oacp_response(conn_id, BLE_OTS_OACP_OPCODE_WRITE,
|
||||
BLE_OTS_OACP_RESULT_OP_CODE_NOT_SUPPORTED);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Priority 2: Check Current Object is valid */
|
||||
if (!cs->current_obj_valid) {
|
||||
send_oacp_response(conn_id, BLE_OTS_OACP_OPCODE_WRITE,
|
||||
BLE_OTS_OACP_RESULT_INVALID_OBJECT);
|
||||
return 0;
|
||||
}
|
||||
|
||||
ble_ots_obj_id_t obj_id = cs->current_object_id;
|
||||
ble_ots_server_obj_t *obj = ble_ots_server_obj_db_lookup(obj_id);
|
||||
if (!obj) {
|
||||
send_oacp_response(conn_id, BLE_OTS_OACP_OPCODE_WRITE,
|
||||
BLE_OTS_OACP_RESULT_INVALID_OBJECT);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Priority 3: Check object Write property (bit 3) */
|
||||
if (!(obj->properties & BLE_OTS_OBJ_PROP_WRITE)) {
|
||||
send_oacp_response(conn_id, BLE_OTS_OACP_OPCODE_WRITE,
|
||||
BLE_OTS_OACP_RESULT_PROCEDURE_NOT_PERMITTED);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Check for integer overflow in offset + length (before any arithmetic) */
|
||||
if (length > 0 && offset > UINT32_MAX - length) {
|
||||
send_oacp_response(conn_id, BLE_OTS_OACP_OPCODE_WRITE,
|
||||
BLE_OTS_OACP_RESULT_INVALID_PARAMETER);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Priority 4: Patching check — if not truncating and offset + length <= current_size,
|
||||
* this is a patch operation. Check OACP Patch feature (bit 8). */
|
||||
if (!truncate && (offset + length) <= obj->current_size) {
|
||||
if (!(ble_ots_server_env.ots_feature.oacp_features & BLE_OTS_OACP_FEAT_PATCH)) {
|
||||
send_oacp_response(conn_id, BLE_OTS_OACP_OPCODE_WRITE,
|
||||
BLE_OTS_OACP_RESULT_PROCEDURE_NOT_PERMITTED);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Priority 5: Check object Patch property (bit 6) */
|
||||
if (!(obj->properties & BLE_OTS_OBJ_PROP_PATCH)) {
|
||||
send_oacp_response(conn_id, BLE_OTS_OACP_OPCODE_WRITE,
|
||||
BLE_OTS_OACP_RESULT_PROCEDURE_NOT_PERMITTED);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* Priority 6: Truncation check — if truncate bit set, check Truncate property (bit 5) */
|
||||
if (truncate) {
|
||||
if (!(ble_ots_server_env.ots_feature.oacp_features & BLE_OTS_OACP_FEAT_TRUNCATE)) {
|
||||
send_oacp_response(conn_id, BLE_OTS_OACP_OPCODE_WRITE,
|
||||
BLE_OTS_OACP_RESULT_PROCEDURE_NOT_PERMITTED);
|
||||
return 0;
|
||||
}
|
||||
if (!(obj->properties & BLE_OTS_OBJ_PROP_TRUNCATE)) {
|
||||
send_oacp_response(conn_id, BLE_OTS_OACP_OPCODE_WRITE,
|
||||
BLE_OTS_OACP_RESULT_PROCEDURE_NOT_PERMITTED);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* Priority 7: Check OTC channel available */
|
||||
struct ble_l2cap_chan *chan = ble_ots_server_otc_get(conn_id);
|
||||
if (!chan) {
|
||||
send_oacp_response(conn_id, BLE_OTS_OACP_OPCODE_WRITE,
|
||||
BLE_OTS_OACP_RESULT_CHANNEL_UNAVAILABLE);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Priority 8: Check RFU bits in mode (bits 0, 2-7 except bit 1) */
|
||||
uint8_t rfu_mask = (uint8_t)~BLE_OTS_OACP_WRITE_MODE_TRUNCATE;
|
||||
if (mode & rfu_mask) {
|
||||
send_oacp_response(conn_id, BLE_OTS_OACP_OPCODE_WRITE,
|
||||
BLE_OTS_OACP_RESULT_INVALID_PARAMETER);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Priority 9: Offset exceeds Current Size (for non-truncate, offset must be <= current_size) */
|
||||
if (!truncate && offset > obj->current_size) {
|
||||
send_oacp_response(conn_id, BLE_OTS_OACP_OPCODE_WRITE,
|
||||
BLE_OTS_OACP_RESULT_INVALID_PARAMETER);
|
||||
return 0;
|
||||
}
|
||||
/* For truncate mode, offset can be <= current_size (truncation sets current_size = offset) */
|
||||
if (truncate && offset > obj->current_size) {
|
||||
send_oacp_response(conn_id, BLE_OTS_OACP_OPCODE_WRITE,
|
||||
BLE_OTS_OACP_RESULT_INVALID_PARAMETER);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Priority 10: Offset + Length exceeds Allocated Size and no append support */
|
||||
bool needs_append = false;
|
||||
if ((offset + length) > obj->allocated_size) {
|
||||
needs_append = true;
|
||||
}
|
||||
|
||||
if (needs_append && !(ble_ots_server_env.ots_feature.oacp_features & BLE_OTS_OACP_FEAT_APPEND)) {
|
||||
send_oacp_response(conn_id, BLE_OTS_OACP_OPCODE_WRITE,
|
||||
BLE_OTS_OACP_RESULT_INVALID_PARAMETER);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Priority 11: Length exceeds server capacity to write.
|
||||
* A plain write of zero octets selects nothing and is rejected. Combined
|
||||
* with Truncate it is meaningful — Truncate sets the Current Size to Offset
|
||||
* plus the octets written, so Length 0 is how a client shrinks an object
|
||||
* without supplying new content. */
|
||||
if (!truncate && length == 0) {
|
||||
send_oacp_response(conn_id, BLE_OTS_OACP_OPCODE_WRITE,
|
||||
BLE_OTS_OACP_RESULT_INVALID_PARAMETER);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Priority 12: Object locked by another client */
|
||||
if (obj->locked_by != BLE_OTS_CONN_ID_NONE && obj->locked_by != conn_id) {
|
||||
send_oacp_response(conn_id, BLE_OTS_OACP_OPCODE_WRITE,
|
||||
BLE_OTS_OACP_RESULT_OBJECT_LOCKED);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Priority 13: Transfer already in progress */
|
||||
if (cs->transfer_active) {
|
||||
send_oacp_response(conn_id, BLE_OTS_OACP_OPCODE_WRITE,
|
||||
BLE_OTS_OACP_RESULT_OBJECT_LOCKED);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Supplementary Append checks (only when needs_append is true) */
|
||||
if (needs_append) {
|
||||
/* Append Priority 1: Check object Append property (bit 4) */
|
||||
if (!(obj->properties & BLE_OTS_OBJ_PROP_APPEND)) {
|
||||
send_oacp_response(conn_id, BLE_OTS_OACP_OPCODE_WRITE,
|
||||
BLE_OTS_OACP_RESULT_PROCEDURE_NOT_PERMITTED);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Append Priority 2: Check server capacity to increase allocated size.
|
||||
* The actual reallocation is deferred to ble_ots_server_obj_data_write
|
||||
* when data chunks arrive. Do not update obj->allocated_size here. */
|
||||
}
|
||||
|
||||
/* Lock the object under concurrency */
|
||||
if (CONFIG_BLE_OTS_SERVER_MAX_CONCURRENCY > 1) {
|
||||
int lock_rc = ble_ots_server_obj_lock(obj_id, conn_id);
|
||||
if (lock_rc != 0) {
|
||||
send_oacp_response(conn_id, BLE_OTS_OACP_OPCODE_WRITE,
|
||||
BLE_OTS_OACP_RESULT_OBJECT_LOCKED);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* Mark transfer active */
|
||||
cs->transfer_active = true;
|
||||
cs->transfer_is_read = false;
|
||||
cs->transfer_object_id = obj_id;
|
||||
cs->transfer_offset = offset;
|
||||
cs->transfer_length = length;
|
||||
cs->transfer_bytes_done = 0;
|
||||
cs->transfer_truncated = false;
|
||||
|
||||
/* Send Success indication */
|
||||
int rc = send_oacp_response(conn_id, BLE_OTS_OACP_OPCODE_WRITE,
|
||||
BLE_OTS_OACP_RESULT_SUCCESS);
|
||||
if (rc != 0) {
|
||||
ESP_LOGE(TAG, "Failed to send OACP Write success indication, rc=%d", rc);
|
||||
cs->transfer_active = false;
|
||||
ble_ots_server_obj_unlock(obj_id);
|
||||
return rc;
|
||||
}
|
||||
|
||||
/* Perform truncation if requested (deferred until after successful response) */
|
||||
if (truncate) {
|
||||
obj->current_size = offset;
|
||||
cs->transfer_truncated = true;
|
||||
}
|
||||
|
||||
/* A Truncate-only request carries no object data, so the procedure is
|
||||
* already done. Completing it here releases the object lock and emits the
|
||||
* Object Changed indication instead of leaving a transfer that could only
|
||||
* ever end on the inactivity timeout. */
|
||||
if (length == 0) {
|
||||
ESP_LOGI(TAG, "Object truncated: obj_id=0x%llx, current_size=%lu",
|
||||
(unsigned long long)obj_id, (unsigned long)offset);
|
||||
transfer_complete_write(cs, BLE_OTS_TRANSFER_SUCCESS);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Start transfer inactivity timer */
|
||||
ble_ots_server_transfer_timer_start(conn_id);
|
||||
|
||||
ESP_LOGI(TAG, "Write transfer started: obj_id=0x%llx, offset=%lu, length=%lu, mode=0x%02x",
|
||||
(unsigned long long)obj_id, (unsigned long)offset,
|
||||
(unsigned long)length, mode);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*****************************************************************************
|
||||
* OACP Abort Procedure
|
||||
*****************************************************************************/
|
||||
int ble_ots_server_oacp_abort_proc(uint16_t conn_id)
|
||||
{
|
||||
ble_ots_server_conn_state_t *cs = ble_ots_server_conn_state_get(conn_id);
|
||||
if (!cs) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* Priority 1: Check OACP Abort feature support (bit 9) */
|
||||
if (!(ble_ots_server_env.ots_feature.oacp_features & BLE_OTS_OACP_FEAT_ABORT)) {
|
||||
send_oacp_response(conn_id, BLE_OTS_OACP_OPCODE_ABORT,
|
||||
BLE_OTS_OACP_RESULT_OP_CODE_NOT_SUPPORTED);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Priority 2: No Read operation in progress */
|
||||
if (!cs->transfer_active || !cs->transfer_is_read) {
|
||||
send_oacp_response(conn_id, BLE_OTS_OACP_OPCODE_ABORT,
|
||||
BLE_OTS_OACP_RESULT_OPERATION_FAILED);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Priority 3: Abort must come from the same client that initiated the read.
|
||||
* Since cs is per-connection and we already matched conn_id, this is inherently satisfied.
|
||||
* But we verify the transfer is on this connection's state. */
|
||||
|
||||
/* Cancel the in-progress read transfer */
|
||||
ble_ots_server_transfer_timer_stop(conn_id);
|
||||
transfer_retry_stop(cs);
|
||||
|
||||
/* Save transfer info before clearing */
|
||||
ble_ots_obj_id_t obj_id = cs->transfer_object_id;
|
||||
uint32_t transfer_offset = cs->transfer_offset;
|
||||
uint32_t transfer_length = cs->transfer_length;
|
||||
uint32_t bytes_sent = cs->transfer_bytes_done;
|
||||
|
||||
/* Mark transfer as no longer active */
|
||||
cs->transfer_active = false;
|
||||
cs->transfer_is_read = false;
|
||||
|
||||
/* Send Success indication for Abort */
|
||||
int rc = send_oacp_response(conn_id, BLE_OTS_OACP_OPCODE_ABORT,
|
||||
BLE_OTS_OACP_RESULT_SUCCESS);
|
||||
if (rc != 0) {
|
||||
ESP_LOGE(TAG, "Failed to send OACP Abort success indication, rc=%d", rc);
|
||||
}
|
||||
|
||||
/* Fire Read Complete event with ABORTED status */
|
||||
ble_ots_server_cb_param_t param;
|
||||
memset(¶m, 0, sizeof(param));
|
||||
param.read_complete.object_id = obj_id;
|
||||
param.read_complete.offset = transfer_offset;
|
||||
param.read_complete.length = transfer_length;
|
||||
param.read_complete.bytes_sent = bytes_sent;
|
||||
param.read_complete.status = BLE_OTS_TRANSFER_ABORTED;
|
||||
|
||||
ble_ots_server_dispatch_event(BLE_OTS_SERVER_EVT_READ_COMPLETE, ¶m);
|
||||
|
||||
ESP_LOGI(TAG, "Read transfer aborted: obj_id=0x%llx, bytes_sent=%lu/%lu",
|
||||
(unsigned long long)obj_id, (unsigned long)bytes_sent,
|
||||
(unsigned long)transfer_length);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*****************************************************************************
|
||||
* Non-static wrappers (called from ots_server_init.c via extern)
|
||||
*****************************************************************************/
|
||||
|
||||
/**
|
||||
* @brief Transfer timeout handler called from the init module.
|
||||
*
|
||||
* Delegates to the internal timeout callback logic.
|
||||
*
|
||||
* @param conn_id BLE connection identifier
|
||||
*/
|
||||
void ble_ots_server_oacp_transfer_timeout(uint16_t conn_id)
|
||||
{
|
||||
ble_ots_server_conn_state_t *cs = ble_ots_server_conn_state_get(conn_id);
|
||||
if (!cs || !cs->transfer_active) {
|
||||
return;
|
||||
}
|
||||
|
||||
ESP_LOGW(TAG, "Transfer timeout on conn_id=%d", conn_id);
|
||||
|
||||
if (cs->transfer_is_read) {
|
||||
ble_ots_server_otc_close(conn_id);
|
||||
transfer_complete_read(cs, BLE_OTS_TRANSFER_TIMEOUT);
|
||||
} else {
|
||||
ble_ots_server_otc_close(conn_id);
|
||||
transfer_complete_write(cs, BLE_OTS_TRANSFER_TIMEOUT);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief L2CAP COC data receive handler called from the init module.
|
||||
*
|
||||
* Handles incoming L2CAP COC data during write transfers by delegating
|
||||
* to the internal receive callback.
|
||||
*
|
||||
* @param conn_handle BLE connection handle
|
||||
* @param chan L2CAP channel
|
||||
* @param sdu_rx Received SDU mbuf chain
|
||||
* @return 0 on success
|
||||
*/
|
||||
int ble_ots_server_oacp_l2cap_recv(uint16_t conn_handle,
|
||||
struct ble_l2cap_chan *chan,
|
||||
struct os_mbuf *sdu_rx)
|
||||
{
|
||||
ble_ots_server_otc_receive_cb(conn_handle, chan, sdu_rx);
|
||||
return 0;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
Reference in New Issue
Block a user