ESP-IDF 经典蓝牙 SDP 服务发现 API 实战指南:服务搜索、记录发布与源码实现解析
ESP-IDF 经典蓝牙 SDP 服务发现 API 实战指南服务搜索、记录发布与源码实现解析【免费下载链接】esp-idfEspressif IoT Development Framework. Official development framework for Espressif SoCs.项目地址: https://gitcode.com/GitHub_Trending/es/esp-idf本文以 docs/en/api-reference/bluetooth/esp_sdp.rst中文文档 docs/zh_CN/api-reference/bluetooth/esp_sdp.rst 通过 include 指令引用英文原文为核心骨架结合 ESP-IDF 中 Bluedroid 协议栈的 SDP 源码实现与经典蓝牙示例工程系统讲解 SDPService Discovery Protocol服务发现协议API 的完整使用方式。读完本文你将掌握如何注册 SDP 回调并初始化 SDP 模块、如何通过esp_sdp_search_record()搜索远端设备上的服务并解析搜索结果、如何通过esp_sdp_create_record()创建并发布可被远端设备发现的服务记录含 L2CAP PSM / RFCOMM 通道信息以及这些 API 背后在 Bluedroid 协议栈中的执行链路与参数校验规则。一、SDP API 概述经典蓝牙的服务目录SDPService Discovery Protocol是蓝牙经典Bluetooth ClassicBR/EDR协议栈中的核心协议之一负责在设备之间发现对方提供了哪些服务以及这些服务的具体属性。根据 esp_sdp.rst 的定义ESP-IDF 提供的 SDP API 使设备能够服务搜索Service Search查询远端蓝牙设备提供的服务服务属性检索Service Attribute Retrieval获取服务的属性信息服务连接建立Service Connection Establishment基于检索结果如 L2CAP PSM、RFCOMM 通道号发起或接受上层服务连接。在实际的经典蓝牙应用中SDP 是先发现、后连接流程的第一步例如一台手机客户端想知道附近的嵌入式设备是否支持 Object Push对象推送或 PBAP电话簿就需要先通过 SDP 查询而作为服务端的嵌入式设备则需要把自身支持的协议栈信息服务名称、UUID、协议描述符等组织成 SDP 记录并注册到 SDP 数据库中供远端设备查询。ESP-IDF 中该 API 属于 Bluedroid 协议栈components/bt/host/bluedroid对外头文件为 esp_sdp_api.h实现位于 esp_sdp_api.c 与 btc_sdp.c。二、API 总览七个核心函数SDP API 的全部函数均在头文件 esp_sdp_api.h第 252–328 行中声明完整清单如下函数作用关键约束esp_sdp_register_callback()向 SDP 模块注册回调函数须在 Bluedroid 使能后调用内部检查ESP_BLUEDROID_STATUS_ENABLED回调不能为 NULLesp_sdp_init()初始化 SDP 模块完成后回调收到ESP_SDP_INIT_EVT须在esp_bluedroid_enable()成功之后调用esp_sdp_deinit()去初始化 SDP 模块会先移除所有 SDP 记录每移除一条触发一次ESP_SDP_REMOVE_RECORD_COMP_EVT最后触发ESP_SDP_DEINIT_EVTesp_sdp_search_record()对指定远端设备执行服务发现传入远端 BD 地址与目标服务 UUID完成后回调收到ESP_SDP_SEARCH_COMP_EVTesp_sdp_create_record()创建并注册一条 SDP 记录传入esp_bluetooth_sdp_record_t完成后回调收到ESP_SDP_CREATE_RECORD_COMP_EVTesp_sdp_remove_record()移除一条 SDP 记录传入创建时返回的record_handle完成后回调收到ESP_SDP_REMOVE_RECORD_COMP_EVTesp_sdp_get_protocol_status()查询 SDP 协议状态输出esp_sdp_protocol_status_t含是否已初始化与已创建记录数其中esp_sdp_search_record()、esp_sdp_create_record()、esp_sdp_remove_record()必须在esp_sdp_init()成功之后、esp_sdp_deinit()之前调用头文件中的函数注释明确标注了这一约束。2.1 状态码所有回调参数中的操作结果使用esp_sdp_status_t枚举esp_sdp_api.h表示状态含义ESP_SDP_SUCCESS操作成功ESP_SDP_FAILURE通用失败ESP_SDP_NO_RESOURCE资源不足如记录槽位已满ESP_SDP_NEED_INITSDP 模块尚未初始化ESP_SDP_NEED_DEINITSDP 模块需要先去初始化ESP_SDP_NO_CREATE_RECORD未创建任何记录如在无记录时执行搜索2.2 回调事件回调函数esp_sdp_cb_t的第一个参数是事件类型esp_sdp_cb_event_tesp_sdp_api.h共有五个事件ESP_SDP_INIT_EVTSDP 初始化完成ESP_SDP_DEINIT_EVTSDP 去初始化完成ESP_SDP_SEARCH_COMP_EVT服务搜索完成ESP_SDP_CREATE_RECORD_COMP_EVT创建 SDP 记录完成ESP_SDP_REMOVE_RECORD_COMP_EVT移除 SDP 记录完成。回调参数为联合体esp_sdp_cb_param_tesp_sdp_api.h按事件类型取用对应字段init.status/deinit.status初始化/去初始化状态search.status、search.remote_addr、search.sdp_uuid、search.record_count、search.records搜索状态、远端设备地址、查询的 UUID、命中的记录条数与记录数组指针create_record.status、create_record.record_handle创建状态与记录句柄remove_record.status、remove_record.record_handle移除状态与记录句柄。2.3 协议状态结构esp_sdp_protocol_status_tesp_sdp_api.h包含两个字段sdp_initedboolSDP 是否已初始化records_numuint8_t当前已创建的记录条数。三、记录模型从通用头部到各 Profile 专属结构SDP 记录通过联合体esp_bluetooth_sdp_record_t承载esp_sdp_api.h其第一个成员是通用头部hdr其余成员按记录类型对应不同 Profile 的专属参数。3.1 记录类型esp_bluetooth_sdp_types_tesp_sdp_api.h定义了八种记录类型类型含义ESP_SDP_TYPE_RAW原始记录用于承载未知 UUID 的 SDP 搜索数据ESP_SDP_TYPE_MAP_MASMessage Access Profile – Server消息访问服务端ESP_SDP_TYPE_MAP_MNSMessage Access Profile – Client/Notification Server消息通知服务端ESP_SDP_TYPE_PBAP_PSEPhone Book Profile – Server电话簿服务端ESP_SDP_TYPE_PBAP_PCEPhone Book Profile – Client电话簿客户端ESP_SDP_TYPE_OPP_SERVERObject Push Profile对象推送服务端ESP_SDP_TYPE_SAP_SERVERSIM Access ProfileSIM 卡访问服务端ESP_SDP_TYPE_DIP_SERVERDevice Identification Profile设备识别服务端3.2 通用头部所有记录共享通用头部esp_bluetooth_sdp_hdr_overlay_tesp_sdp_api.h字段类型说明typeesp_bluetooth_sdp_types_t记录类型uuidesp_bt_uuid_tUUID含长度仅创建 RAW 记录时需要设置service_name_lengthuint32_t服务名称长度service_namechar *服务名称字符串rfcomm_channel_numberint32_tRFCOMM 通道号不使用设为 -1l2cap_psmint32_tL2CAP PSM不使用设为 -1profile_versionint32_tProfile 版本号user1_ptr_lenint用户数据 1 长度仅 RAW 记录搜索时使用user1_ptruint8_t *用户数据 1 指针指向 RAW SDP 响应数据仅 RAW 记录搜索时使用3.3 各 Profile 专属参数MAP MASesp_bluetooth_sdp_mas_record_t在通用头部基础上增加mas_instance_idMAS 实例 ID、supported_featuresMAP 支持特性位图、supported_message_types支持的消息类型。源码校验要求mas_instance_id与supported_message_types均为规范定义的 uint8_t 量见 esp_sdp_api.c。MAP MNSesp_bluetooth_sdp_mns_record_t增加supported_features支持特性。PBAP PSEesp_bluetooth_sdp_pse_record_t增加supported_featuresPBAP 支持特性、supported_repositories支持的电话簿仓库。源码校验要求supported_repositories为规范定义的 uint8_t 量esp_sdp_api.c。PBAP PCEesp_bluetooth_sdp_pce_record_t仅有通用头部。OPP Serveresp_bluetooth_sdp_ops_record_t增加supported_formats_list_len与supported_formats_list[]格式列表最大长度由宏SDP_OPP_SUPPORTED_FORMATS_MAX_LENGTH值为 15限定esp_sdp_api.h。SAP Serveresp_bluetooth_sdp_sap_record_t仅有通用头部。DIP Serveresp_bluetooth_sdp_dip_record_t增加vendor厂商 ID、vendor_id_source厂商 ID 来源ESP_SDP_VENDOR_ID_SRC_BT1 表示蓝牙分配ESP_SDP_VENDOR_ID_SRC_USB2 表示 USB 分配其余保留、product产品 ID、version发布版本格式0xJJMNJJ 为主版本号、M 为次版本号、N 为次次版本号、primary_record是否主记录单条设备记录须设为 true。DIP 主记录约束头文件注释明确指出SDP 数据库中只能添加一条主 Device Identification 服务记录若重复创建主 DIP 记录只有最后一条生效。这一行为在 btc_sdp.c 的alloc_sdp_slot()中有对应实现当检测到新的主 DIP 记录时会先查找并覆盖已存在的主 DIP 槽位打印overwrite primary di record!警告而不是新增槽位。3.4 预置 UUID 宏esp_sdp_api.h 提供了一组常用 Profile 的 16 位 UUID 宏便于直接构造 UUID 参数宏值对应服务ESP_SDP_UUID_MAP_MAS0x1132Message Access ServiceESP_SDP_UUID_MAP_MNS0x1133Message Notification ServiceESP_SDP_UUID_PBAP_PSE0x112FPhone Book Server EquipmentESP_SDP_UUID_PBAP_PCE0x112EPhone Book Client EquipmentESP_SDP_UUID_OPP0x1105Object Push ProfileESP_SDP_UUID_SAP0x112DSIM Access ProfileESP_SDP_UUID_DIP0x1200Device Identification Profile辅助宏ESP_SDP_BUILD_BT_UUID16(uuid16_val)esp_sdp_api.h可直接把一个 16 位值构造成esp_bt_uuid_tesp_bt_uuid_t uuid ESP_SDP_BUILD_BT_UUID16(ESP_SDP_UUID_OPP);另外两个长度相关的宏ESP_SDP_SERVER_NAME_MAX32限制服务名称最大长度SDP_OPP_SUPPORTED_FORMATS_MAX_LENGTH15限制 OPP 支持格式列表最大长度。四、典型调用流程客户端搜索与服务端发布SDP API 的使用遵循注册回调 → 初始化 → 搜索 / 创建记录 / 移除记录→ 去初始化的生命周期。4.1 通用初始化序列/* 1. 先完成 Bluedroid 使能此处省略 esp_bluedroid_init / enable 细节 */ /* 2. 注册 SDP 回调 */ esp_err_t ret esp_sdp_register_callback(esp_sdp_cb); if (ret ! ESP_OK) { ESP_LOGE(TAG, Failed to register SDP callback); } /* 3. 初始化 SDP 模块 */ ret esp_sdp_init(); if (ret ! ESP_OK) { ESP_LOGE(TAG, Failed to init SDP); }回调函数按事件分发处理static void esp_sdp_cb(esp_sdp_cb_event_t event, esp_sdp_cb_param_t *param) { switch (event) { case ESP_SDP_INIT_EVT: ESP_LOGI(TAG, SDP init, status: %d, param-init.status); break; case ESP_SDP_DEINIT_EVT: ESP_LOGI(TAG, SDP deinit, status: %d, param-deinit.status); break; case ESP_SDP_SEARCH_COMP_EVT: /* 处理搜索结果遍历 param-search.records */ break; case ESP_SDP_CREATE_RECORD_COMP_EVT: ESP_LOGI(TAG, Create record, status: %d, handle: %d, param-create_record.status, param-create_record.record_handle); break; case ESP_SDP_REMOVE_RECORD_COMP_EVT: ESP_LOGI(TAG, Remove record, status: %d, handle: %d, param-remove_record.status, param-remove_record.record_handle); break; default: break; } }4.2 客户端搜索远端服务文档给出的首个示例是bt_l2cap_client演示了使用 SDP API 搜索远端蓝牙设备上的服务注册 SDP 回调、初始化 SDP、用esp_sdp_search_record()发起服务发现并从搜索结果中取出 L2CAP PSM 值以建立 L2CAP 连接。示例位于 examples/bluetooth/bluedroid/classic_bt/bt_l2cap_client/main/main.c。其关键流程结合 main.c 源码通过 GAP 设备发现esp_bt_gap_start_discovery扫描周边设备在ESP_BT_GAP_DISC_RES_EVT事件中解析设备的 EIRExtended Inquiry Response数据比对设备名找到目标设备后取消发现以 128 位未知 UUID示例中的UUID_UNKNOWN构造esp_bt_uuid_t调用esp_sdp_search_record(param-disc_res.bda, uuid)发起服务搜索uuid.len sizeof(UUID_UNKNOWN); memcpy(uuid.uuid.uuid128, UUID_UNKNOWN, sizeof(UUID_UNKNOWN)); esp_sdp_search_record(param-disc_res.bda, uuid);搜索完成后在ESP_SDP_SEARCH_COMP_EVT中从param-search.records取出记录解析其中的 L2CAP PSM进而调用 L2CAP 连接 API 建立通信链路。4.3 服务端创建并发布 SDP 记录文档给出的第二个示例是bt_l2cap_server演示了如何创建并发布服务记录注册 SDP 回调、初始化 SDP、用esp_sdp_create_record()携带 L2CAP PSM 信息创建 SDP 记录使服务对远端客户端可发现。示例位于 examples/bluetooth/bluedroid/classic_bt/bt_l2cap_server。以 RAW 记录发布一个带 L2CAP PSM 的服务为例记录构造方式如下esp_bluetooth_sdp_record_t record {0}; record.hdr.type ESP_SDP_TYPE_RAW; record.hdr.uuid ESP_SDP_BUILD_BT_UUID16(ESP_SDP_UUID_OPP); /* 示例OPP 服务 */ record.hdr.service_name (char *)ESP32 OPP Service; record.hdr.service_name_length strlen(record.hdr.service_name) 1; /* 含 \0 */ record.hdr.l2cap_psm 0x1001; /* 要发布的 L2CAP PSM */ record.hdr.rfcomm_channel_number -1; /* 不使用 RFCOMM 通道 */ esp_err_t ret esp_sdp_create_record(record); if (ret ! ESP_OK) { ESP_LOGE(TAG, Failed to create SDP record); }服务名称的取值约束源码 esp_sdp_api.c 中的完整性检查要求service_name不能为 NULL、service_name_length不能超过ESP_SDP_SERVER_NAME_MAX32且必须满足strlen(service_name) 1 service_name_length即长度包含结尾的\0。构造记录时若不满足这些条件esp_sdp_create_record()将直接返回ESP_ERR_INVALID_ARG。DIP 记录不受此检查约束服务名称可省略。创建成功后回调的ESP_SDP_CREATE_RECORD_COMP_EVT会返回record_handle该句柄用于后续esp_sdp_remove_record(record_handle)移除记录或在esp_sdp_deinit()时由协议栈统一清理。五、源码实现纵深API 到协议栈的执行链路理解 SDP API 的底层执行机制有助于排查问题并合理设计应用流程。ESP-IDF 的 SDP 实现采用 Bluedroid 经典的API 层 → BTC 层 → BTA 层分层结构。5.1 BTC 上下文切换esp_sdp_init()/esp_sdp_deinit()/esp_sdp_search_record()/esp_sdp_create_record()/esp_sdp_remove_record()五个函数在 esp_sdp_api.c 中都没有直接操作协议栈数据而是构造btc_msg_t消息pid BTC_PID_SDP通过btc_transfer_context()切换到 BTCBluetooth Controller 桥接上下文执行从而实现线程安全esp_sdp_api.cmsg.sig BTC_SIG_API_CALL; msg.pid BTC_PID_SDP; msg.act BTC_SDP_ACT_INIT; /* 对应 BTC_SDP_ACT_DEINIT / SEARCH / CREATE_RECORD / REMOVE_RECORD */ stat btc_transfer_context(msg, NULL, 0, NULL, NULL); return (stat BT_STATUS_SUCCESS) ? ESP_OK : ESP_FAIL;动作码定义在 btc_sdp.hBTC_SDP_ACT_INIT、BTC_SDP_ACT_DEINIT、BTC_SDP_ACT_SEARCH、BTC_SDP_ACT_CREATE_RECORD、BTC_SDP_ACT_REMOVE_RECORD。其中esp_sdp_create_record()额外指定了深拷贝/深释放回调btc_sdp_arg_deep_copy/btc_sdp_arg_deep_free因为记录中携带service_name等指针数据需要跨上下文安全传递。5.2 参数完整性检查在切换到 BTC 上下文之前esp_sdp_create_record()会先调用esp_sdp_record_integrity_check()esp_sdp_api.c做参数校验校验失败的记录直接返回ESP_ERR_INVALID_ARG不会进入协议栈。校验项包括type必须在ESP_SDP_TYPE_RAW到ESP_SDP_TYPE_DIP_SERVER的合法范围内DIP 记录的vendor_id_source必须为ESP_SDP_VENDOR_ID_SRC_BT或ESP_SDP_VENDOR_ID_SRC_USBMAP MAS 记录的mas_instance_id与supported_message_types高位不能越界规范定义为 uint8_tPBAP PSE 记录的supported_repositories高位不能越界规范定义为 uint8_tOPP 记录的supported_formats_list_len必须大于 0 且不超过SDP_OPP_SUPPORTED_FORMATS_MAX_LENGTH除 DIP 外service_name不能为 NULL、长度不能超过 32、且strlen 1 service_name_length。5.3 记录槽位Slot管理BTC 层在 btc_sdp.c 中维护一个本地 SDP 记录池sdp_local_param.sdp_slots[]是一个大小为SDP_MAX_RECORDS的槽位数组每个槽位sdp_slot_t记录状态SDP_RECORD_FREE/SDP_RECORD_ALLOCED、DIP 标志位、sdp_handle、UUID 及记录数据指针并用互斥锁sdp_slot_mutex保护btc_sdp.c。分配alloc_sdp_slot()计算记录所需内存get_sdp_record_size()结构体大小 服务名称长度 结尾\0拷贝记录后查找空闲槽位若槽位已满i SDP_MAX_RECORDS则返回 -1对应状态ESP_SDP_NO_RESOURCE主 DIP 覆盖新主 DIP 记录会覆盖已有主 DIP 槽位这印证了头文件中仅最后一条主 DIP 记录生效的注释释放free_sdp_slot()在解锁后释放记录数据与槽位内存返回sdp_handle供上层使用去初始化清理btc_sdp_cleanup()遍历所有槽位释放记录数据并销毁互斥锁。5.4 记录到底层的属性化以 RAW 与 MAP MAS 为例add_raw_sdp()btc_sdp.c展示了 SDP 记录在底层是如何被翻译成标准 SDP 属性的调用SDP_CreateRecord()创建底层句柄依据 UUID 长度16/32/128 位把 UUID 编码为 SDP 数据类型描述符写入ATTR_ID_SERVICE_CLASS_ID_LIST服务类别列表属性构造协议列表L2CAP 协议元素固定位于首位若rfcomm_channel_number 0则追加 RFCOMM 协议元素UUID_PROTOCOL_L2CAP→UUID_PROTOCOL_RFCOMM添加ATTR_ID_SERVICE_NAME服务名称属性若l2cap_psm ! -1则以ATTR_ID_GOEP_L2CAP_PSM属性写入 PSMUINT16_TO_BE_STREAM编码为大端序——这正是客户端从搜索结果中取回 PSM 以建立 L2CAP 连接的底层来源添加ATTR_ID_BROWSE_GROUP_LIST公共浏览组UUID_SERVCLASS_PUBLIC_BROWSE_GROUP使服务可被搜索最后通过bta_sys_add_uuid()系列函数把服务 UUID 注册到系统 UUID 列表供其他模块如 GAP感知该服务的存在。add_maps_sdp()btc_sdp.c则是 Profile 型记录的代表它依次写入服务类别 ID 列表UUID_SERVCLASS_MESSAGE_ACCESS、L2CAP→RFCOMM→OBEX 三级协议列表、服务名称、Profile 描述符列表UUID_SERVCLASS_MAP_PROFILEprofile_version、MAS 实例 ID、支持的消息类型与 MAP 支持特性位图并在存在 PSM 时同样写入 L2CAP PSM 属性。类似地其余 ProfileMNS/PSE/PCE/OPP/SAP在 btc_sdp.c 中都有对应的add_*_sdp()实现函数。5.5 搜索结果的 RAW 承载对于未知 UUID 的搜索场景客户端可使用ESP_SDP_TYPE_RAW类型esp_bluetooth_sdp_hdr_overlay_t中的user1_ptr/user1_ptr_len字段专门用于承载原始 SDP 响应数据。bt_l2cap_client示例正是以 128 位UUID_UNKNOWN发起搜索把远端返回的原始记录交给上层解析。六、与其它组件的配合及使用建议与 GAP 的配合SDP 搜索需要目标设备的 BD 地址通常来自 GAP 设备发现结果如ESP_BT_GAP_DISC_RES_EVT中的disc_res.bda服务端若希望服务可被发现还应在 GAP 层设置可发现模式。与 L2CAP / SPP 的配合SDP 记录中发布的l2cap_psm与rfcomm_channel_number是上层连接L2CAP 或 RFCOMM/SPP建立的依据。服务端须保证 SDP 记录中声明的 PSM/通道号与后续实际监听的值一致否则会出现发现了但连不上的问题。调用时序所有 API 须在esp_sdp_init()成功之后调用esp_sdp_deinit()会先触发与记录数等量的ESP_SDP_REMOVE_RECORD_COMP_EVT再触发ESP_SDP_DEINIT_EVT应用若依赖这些事件做资源清理需正确计数等待。记录数量上限本地 SDP 记录受SDP_MAX_RECORDS槽位上限约束创建过多记录会返回资源不足状态应通过esp_sdp_get_protocol_status()的records_num字段监控记录数量。服务名称除 DIP 外所有记录必须携带合法的服务名称长度含\0不超过 32否则esp_sdp_create_record()会直接失败。回调注册esp_sdp_register_callback()传入 NULL 会返回ESP_FAIL且回调注册/所有 API 调用都要求 Bluedroid 处于已使能状态。七、进一步阅读协议文档docs/en/api-reference/bluetooth/esp_sdp.rst中文页 docs/zh_CN/api-reference/bluetooth/esp_sdp.rst 通过 include 引用英文原文公开 API 头文件components/bt/host/bluedroid/api/include/api/esp_sdp_api.hAPI 实现含完整性检查与 BTC 上下文切换components/bt/host/bluedroid/api/esp_sdp_api.cBTC 层实现槽位管理、底层属性化components/bt/host/bluedroid/btc/profile/std/sdp/btc_sdp.c、btc_sdp.h客户端搜索示例examples/bluetooth/bluedroid/classic_bt/bt_l2cap_client/main/main.c服务端发布示例examples/bluetooth/bluedroid/classic_bt/bt_l2cap_server同仓库中另一处 SDP API 应用参考examples/bluetooth/esp_hid_device/main/esp_hid_device_main.c【免费下载链接】esp-idfEspressif IoT Development Framework. Official development framework for Espressif SoCs.项目地址: https://gitcode.com/GitHub_Trending/es/esp-idf创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考