1. 简介
相信我们最早接触蓝牙,就是在某宝上买一个小巧的蓝牙模块,接到单片机上,通过AT指令进行简单配置,就可以用手机连接该模块,然后远程发送信息给单片机。这里面用到的就是SPP协议(Serial Port Protocol),全称蓝牙串口协议。
1.1 SPP协议
SPP协议是一个经典蓝牙才有的协议,用于完成蓝牙设备之间创建串口进行数据传输,它的协议和过程充分借鉴了RS232总线的通信。它的简要通信流程如下:
1. 创建虚拟连接。
- 使用SDP协议提交一个请求来查找RFCOMM服务信道号码;
- 请求对远端设备进行认证;
- 向远端的RFCOMM通道发起一个新的L2CAP请求;
- 在L2CAP通道上初始化一个RFCOMM连接;
- 在RFCOMM连接上创建一个新的数据连接。
2. 接受虚拟串口连接。
- 接受发起设备端的认证请求并做处理;
- 在L2CAP层接收一个新的连接;
- 接受RFCOMM连接请求在RFCOMM通道上;
- 在RFCOMM通道上接收数据连接请求。
3. 在本地SDP数据上注册服务。
2. 例程
例程部分有2个,一个是ESP32做发送端,连接服务端并定时发送消息;另一个是ESP32做服务端,手机或电脑做客户端向ESP32发消息。
我这里使用手机进行测试,在应用商店搜“蓝牙调试助手”就会有很多的调试上位机,选一个下载即可,一定要选支持SPP协议的上位机。
代码部分包含Bluedroid和GAP协议的部分在上一篇文章中有详细的介绍,所以下面不再赘述;同样menuconfig配置也只说明与之前不同的地方。
2.1 menuconfig
menuconfig中需要打开SPP、L2CAP和SDP协议的支持。
2.2 客户端例程
2.2.1 代码
#include <stdint.h>
#include <string.h>
#include <inttypes.h>
#include <stdbool.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "nvs.h"
#include "nvs_flash.h"
#include "esp_system.h"
#include "esp_log.h"
#include "esp_bt.h"
#include "esp_bt_main.h"
#include "esp_bt_device.h"
#include "esp_gap_bt_api.h"
#include "esp_spp_api.h"
#define TAG "app"
#define OWN_NAME "ESP32"
#define SERVER_NAME "Jackie的P40 Pro"
#define SPP_DATA "Hello from ESP32"
#define SPP_DATA_SEND_BIT (1 << 0)
#define BDASTR "%02X:%02X:%02X:%02X:%02X:%02X"
#define BDA2STR(x) (x)[0], (x)[1], (x)[2], (x)[3], (x)[4], (x)[5]
#define MIN(x, y) ((x) < (y) ? (x) : (y))
typedef struct {
esp_bd_addr_t bda;
char name[ESP_BT_GAP_MAX_BDNAME_LEN];
uint32_t handle;
} bt_dev_t;
static bt_dev_t server = {0};
static EventGroupHandle_t event_group = NULL;
static bool check_device(esp_bt_gap_cb_param_t *param)
{
esp_bt_gap_dev_prop_t *p = param->disc_res.prop;
for (int i = 0; i < param->disc_res.num_prop; i++) {
if (p[i].type == ESP_BT_GAP_DEV_PROP_BDNAME && p[i].val && p[i].len > 0) {
ESP_LOGI(TAG, "name: %s", (char*) p[i].val);
if (memcmp(p[i].val, SERVER_NAME, MIN(p[i].len, strlen(SERVER_NAME)))) {
uint8_t len = p[i].len > ESP_BT_GAP_MAX_BDNAME_LEN - 1 ? ESP_BT_GAP_MAX_BDNAME_LEN - 1 : p[i].len;
memcpy(server.name, p[i].val, len);
memcpy(server.bda, param->disc_res.bda, sizeof(esp_bd_addr_t));
return true;
}
}
/* 从EIR字段中提取服务端名 */
else if (strlen(server.name) == 0 && p[i].type == ESP_BT_GAP_DEV_PROP_EIR && p[i].val && p[i].len > 0) {
uint8_t* eir = (uint8_t *)(p[i].val);
uint8_t len = 0;
uint8_t* bdname = esp_bt_gap_resolve_eir_data(eir, ESP_BT_EIR_TYPE_CMPL_LOCAL_NAME, &len);
if (!bdname) {
bdname = esp_bt_gap_resolve_eir_data(eir, ESP_BT_EIR_TYPE_SHORT_LOCAL_NAME, &len);
}
if (bdname) {
ESP_LOGI(TAG, "name: %s", bdname);
if (memcmp(p[i].val, SERVER_NAME, MIN(p[i].len, strlen(SERVER_NAME)))) {
len = len > ESP_BT_GAP_MAX_BDNAME_LEN - 1 ? ESP_BT_GAP_MAX_BDNAME_LEN - 1 : len;
memcpy(server.name, bdname, len);
memcpy(server.bda, param->disc_res.bda, sizeof(esp_bd_addr_t));
return true;
}
}
}
}
return false;
}
static void esp_gap_cb(esp_bt_gap_cb_event_t event, esp_bt_gap_cb_param_t *param)
{
switch (event) {
/* 设备搜索结果 */
case ESP_BT_GAP_DISC_RES_EVT:
{
if (check_device(param)) {
esp_bt_gap_cancel_discovery();
esp_spp_start_discovery(server.bda);
}
break;
}
/* 搜索状态改变 */
case ESP_BT_GAP_DISC_STATE_CHANGED_EVT:
{
if (param->disc_st_chg.state == ESP_BT_GAP_DISCOVERY_STOPPED) {
// ESP_LOGI(TAG, "Discovery stopped");
if (strlen(server.name) == 0) {
esp_bt_gap_start_discovery(ESP_BT_INQ_MODE_GENERAL_INQUIRY, 4, 30);
}
} else if (param->disc_st_chg.state == ESP_BT_GAP_DISCOVERY_STARTED) {
ESP_LOGI(TAG, "Discoverying devices...");
}
break;
}
/* 验证完成 */
case ESP_BT_GAP_AUTH_CMPL_EVT:
{
if (param->auth_cmpl.stat == ESP_BT_STATUS_SUCCESS) {
ESP_LOGI(TAG, "authentication success: %s", param->auth_cmpl.device_name);
ESP_LOG_BUFFER_HEX(TAG, param->auth_cmpl.bda, ESP_BD_ADDR_LEN);
} else {
ESP_LOGE(TAG, "authentication failed, status:%d", param->auth_cmpl.stat);
}
break;
}
default:
break;
}
}
static void esp_spp_cb(esp_spp_cb_event_t event, esp_spp_cb_param_t *param)
{
switch (event) {
/* 初始化完成 */
case ESP_SPP_INIT_EVT:
if (param->init.status == ESP_SPP_SUCCESS) {
esp_bt_gap_set_device_name(OWN_NAME);
esp_bt_gap_set_scan_mode(ESP_BT_CONNECTABLE, ESP_BT_GENERAL_DISCOVERABLE);
esp_bt_gap_start_discovery(ESP_BT_INQ_MODE_GENERAL_INQUIRY, 4, 30);
} else {
ESP_LOGE(TAG, "spp init failed, status: %d", param->init.status);
}
break;
/* 服务发现完成 */
case ESP_SPP_DISCOVERY_COMP_EVT:
if (param->disc_comp.status == ESP_SPP_SUCCESS) {
ESP_LOGI(TAG, "spp discovered %d services:", param->disc_comp.scn_num);
for (uint8_t i = 0; i < param->disc_comp.scn_num; i++) {
ESP_LOGI(TAG, "%d. SCN: %d, name: %s", i, param->disc_comp.scn[i], param->disc_comp.service_name[i]);
}
/* 连接第一个服务 */
esp_spp_connect(ESP_SPP_SEC_NONE, ESP_SPP_ROLE_MASTER, param->disc_comp.scn[0], server.bda);
} else {
ESP_LOGE(TAG, "spp discovery failed, status: %d", param->disc_comp.status);
}
break;
/* 连接开启 */
case ESP_SPP_OPEN_EVT:
if (param->open.status == ESP_SPP_SUCCESS) {
server.handle = param->open.handle;
ESP_LOGI(TAG, "spp connection opened, handle: %" PRIu32 " rem_bda: " BDASTR, param->open.handle, BDA2STR(param->open.rem_bda));
/* 通知写数据 */
xEventGroupSetBits(event_group, SPP_DATA_SEND_BIT);
} else {
ESP_LOGE(TAG, "spp connection opened failed, status:%d", param->open.status);
}
break;
/* 连接关闭 */
case ESP_SPP_CLOSE_EVT:
ESP_LOGI(TAG, "spp connection closed, status:%d handle: %" PRIu32 " close_by_remote: %d", param->close.status, param->close.handle, param->close.async);
break;
/* 从机连接初始化 */
case ESP_SPP_CL_INIT_EVT:
if (param->cl_init.status == ESP_SPP_SUCCESS) {
ESP_LOGI(TAG, "spp connected, handle: %" PRIu32 " sec_id: %d", param->cl_init.handle, param->cl_init.sec_id);
} else {
ESP_LOGE(TAG, "spp connection failed, status:%d", param->cl_init.status);
}
break;
/* 写数据 */
case ESP_SPP_WRITE_EVT:
if (param->write.status == ESP_SPP_SUCCESS) {
ESP_LOGI(TAG, "spp write %d bytes of data, cong: %d", param->write.len, param->write.cong);
} else {
ESP_LOGE(TAG, "spp write data failed, status: %d", param->write.status);
}
/* 检查管道阻塞 */
if (!param->write.cong) {
xEventGroupSetBits(event_group, SPP_DATA_SEND_BIT);
}
break;
/* 阻塞状态改变 */
case ESP_SPP_CONG_EVT:
if (!param->cong.cong) {
xEventGroupSetBits(event_group, SPP_DATA_SEND_BIT);
}
break;
default:
break;
}
}
int app_main()
{
/* 初始化NVS */
esp_err_t ret = nvs_flash_init();
if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
ESP_ERROR_CHECK(nvs_flash_erase());
ESP_ERROR_CHECK(nvs_flash_init());
}
/* 释放低功耗蓝牙资源 */
ESP_ERROR_CHECK(esp_bt_controller_mem_release(ESP_BT_MODE_BLE));
/* 初始化蓝牙控制器 */
esp_bt_controller_config_t bt_cfg = BT_CONTROLLER_INIT_CONFIG_DEFAULT();
if ((ret = esp_bt_controller_init(&bt_cfg)) != ESP_OK) {
ESP_LOGE(TAG, "initialize controller failed: %s", esp_err_to_name(ret));
return -1;
}
/* 使能蓝牙控制器 */
if ((ret = esp_bt_controller_enable(ESP_BT_MODE_CLASSIC_BT)) != ESP_OK) {
ESP_LOGE(TAG, "enable controller failed: %s", esp_err_to_name(ret));
return -1;
}
/* 初始化bluedroid */
esp_bluedroid_config_t bluedroid_cfg = BT_BLUEDROID_INIT_CONFIG_DEFAULT();
if ((ret = esp_bluedroid_init_with_cfg(&bluedroid_cfg)) != ESP_OK) {
ESP_LOGE(TAG, "initialize bluedroid failed: %s", esp_err_to_name(ret));
return -1;
}
/* 使能bluedroid */
if ((ret = esp_bluedroid_enable()) != ESP_OK) {
ESP_LOGE(TAG, "enable bluedroid failed: %s", esp_err_to_name(ret));
return -1;
}
/* 注册GAP协议回调 */
if ((ret = esp_bt_gap_register_callback(esp_gap_cb)) != ESP_OK) {
ESP_LOGE(TAG, "gap register failed: %s", esp_err_to_name(ret));
return -1;
}
/* 注册SPP协议回调 */
if ((ret = esp_spp_register_callback(esp_spp_cb)) != ESP_OK) {
ESP_LOGE(TAG, "spp register failed: %s", esp_err_to_name(ret));
return -1;
}
/* 初始化SPP */
const esp_spp_cfg_t spp_cfg = {
.mode = ESP_SPP_MODE_CB,
.enable_l2cap_ertm = true,
.tx_buffer_size = 0,
};
if ((ret = esp_spp_enhanced_init(&spp_cfg)) != ESP_OK) {
ESP_LOGE(TAG, "spp init failed: %s", esp_err_to_name(ret));
return -1;
}
event_group = xEventGroupCreate();
/* 定时发送消息 */
while (1) {
xEventGroupWaitBits(event_group, SPP_DATA_SEND_BIT, pdTRUE, pdTRUE, portMAX_DELAY);
if (esp_spp_write(server.handle, strlen(SPP_DATA), (uint8_t*) SPP_DATA) != ESP_OK) {
ESP_LOGE(TAG, "spp send data failed: %s", esp_err_to_name(ret));
}
vTaskDelay(2000 / portTICK_PERIOD_MS);
}
return 0;
}
1. 初始化NVS。
2. 初始化蓝牙控制器。
3. 初始化Bluedroid。
4. 注册GAP协议回调。
回调函数中,设备发现结果事件中依次查询设备的名字,主要关注BDNAME和EIR两个属性,从这两个属性中提取名字,如果名字匹配就会关闭GAP设备发现并使能SPP服务发现。
设备发现状态改变事件,发现结束后如果仍然找不到服务端,那么会重新开始设备发现。
验证完成事件,连接SPP服务端时会进行验证,验证完成后会发送该事件。
5. 注册SPP协议回调。
初始化完成事件,这时就可以使能GAP的设备发现。
服务发现完成事件,一个蓝牙设备其实是可以同时开启多个SPP服务的,在这里我们连接第一个服务,调用esp_spp_connect函数;第一个参数表示验证方式,取决于上位机的配置,默认就是无安全验证;第二个参数表示对方的角色,主机或从机,这里填主机;第三个参数为对方的SCN码;第四个参数为对方的BDA码。
连接开启事件,当成功开启串口通信时会触发,此时会返回一个handle,之后的读写操作都是要用到的。
连接关闭事件,当串口通信被关闭时触发,会返回handle和关闭方。
从机连接初始化完成事件,当从机成功创建了一个SPP连接时会触发。
写数据事件,当数据成功发送时触发,会返回发送的字节数和当前管道的状态;通过判断管道状态(有无阻塞),来决定是否要继续发送消息。
阻塞状态改变事件,通过这个事件可以关注管道的状态,调整消息的发送。
6. 初始化SPP协议。
调用esp_spp_enhanced_init函数,初始化结构体如下:
typedef struct {
esp_spp_mode_t mode;
bool enable_l2cap_ertm;
uint16_t tx_buffer_size;
} esp_spp_cfg_t;
- mode:模式,有2个ESP_SPP_MODE_CB和ESP_SPP_MODE_VFS,前者是通过回调函数处理消息,后者是通过虚拟文件系统处理消息。
- enable_l2cap_ertm:使能L2CAP协议增强重传模式,推荐开启。
- tx_buffer_size:发送缓冲区大小,仅VFS模式使用。
7. 主函数。
主函数我们通过EventGroup来控制消息发送,它用来等待管道空闲,之后会调用esp_spp_write函数发送消息,发送后等待2秒就会继续尝试发送。
2.2.2 效果
先在手机上创建SPP客户端,等待设备连接。然后启动ESP32,如果配置正确那么将会自动连接。
2.3 服务端例程
2.3.1 代码
#include <stdint.h>
#include <string.h>
#include <inttypes.h>
#include <stdbool.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/queue.h"
#include "nvs.h"
#include "nvs_flash.h"
#include "esp_system.h"
#include "esp_log.h"
#include "esp_bt.h"
#include "esp_bt_main.h"
#include "esp_bt_device.h"
#include "esp_gap_bt_api.h"
#include "esp_spp_api.h"
#define TAG "app"
#define OWN_NAME "ESP32"
#define SPP_DATA "I received your message"
#define BDASTR "%02X:%02X:%02X:%02X:%02X:%02X"
#define BDA2STR(x) (x)[0], (x)[1], (x)[2], (x)[3], (x)[4], (x)[5]
typedef struct {
esp_bd_addr_t bda;
uint32_t handle;
} spp_dev_t;
static spp_dev_t client;
static QueueHandle_t send_queue;
static void update_client(const esp_spp_cb_param_t* param)
{
memcpy(client.bda, param->open.rem_bda, sizeof(esp_bd_addr_t));
client.handle = param->open.handle;
}
static void delete_client()
{
memset(&client, 0, sizeof(spp_dev_t));
}
static void esp_gap_cb(esp_bt_gap_cb_event_t event, esp_bt_gap_cb_param_t *param)
{
switch (event) {
/* 验证完成 */
case ESP_BT_GAP_AUTH_CMPL_EVT:
{
if (param->auth_cmpl.stat == ESP_BT_STATUS_SUCCESS) {
ESP_LOGI(TAG, "authentication success: %s [" BDASTR "]", param->auth_cmpl.device_name, BDA2STR(param->auth_cmpl.bda));
} else {
ESP_LOGE(TAG, "authentication failed, status:%d", param->auth_cmpl.stat);
}
break;
}
default:
break;
}
}
static void esp_spp_cb(esp_spp_cb_event_t event, esp_spp_cb_param_t *param)
{
switch (event) {
/* 初始化完成 */
case ESP_SPP_INIT_EVT:
if (param->init.status == ESP_SPP_SUCCESS) {
esp_spp_start_srv(ESP_SPP_SEC_NONE, ESP_SPP_ROLE_SLAVE, 0, OWN_NAME);
} else {
ESP_LOGE(TAG, "spp init failed, status: %d", param->init.status);
}
break;
/* 服务端启动完成 */
case ESP_SPP_START_EVT:
if (param->start.status == ESP_SPP_SUCCESS) {
ESP_LOGI(TAG, "server started, handle:%" PRIu32 " sec_id: %d scn: %d", param->start.handle, param->start.sec_id, param->start.scn);
esp_bt_gap_set_device_name(OWN_NAME);
esp_bt_gap_set_scan_mode(ESP_BT_CONNECTABLE, ESP_BT_GENERAL_DISCOVERABLE);
} else {
ESP_LOGE(TAG, "server start failed, status: %d", param->start.status);
}
break;
/* 连接开启 */
case ESP_SPP_OPEN_EVT:
if (param->open.status == ESP_SPP_SUCCESS) {
/* 保存客户端信息 */
update_client(param);
ESP_LOGI(TAG, "spp connection opened, handle: %" PRIu32 " rem_bda: " BDASTR, param->open.handle, BDA2STR(param->open.rem_bda));
} else {
ESP_LOGE(TAG, "spp connection opened failed, status:%d", param->open.status);
}
break;
/* 连接关闭 */
case ESP_SPP_CLOSE_EVT:
/* 清除客户端 */
delete_client();
ESP_LOGI(TAG, "spp connection closed, status:%d handle: %" PRIu32 " close_by_remote: %d", param->close.status, param->close.handle, param->close.async);
break;
/* 读数据事件 */
case ESP_SPP_DATA_IND_EVT:
ESP_LOGI(TAG, "[" BDASTR "]: %s", BDA2STR(client.bda), param->data_ind.data);
/* 通知回复 */
xQueueSend(send_queue, ¶m->data_ind.handle, portMAX_DELAY);
break;
/* 写数据事件 */
case ESP_SPP_WRITE_EVT:
if (param->write.status == ESP_SPP_SUCCESS) {
ESP_LOGI(TAG, "server write %d bytes of data, cong: %d", param->write.len, param->write.cong);
} else {
ESP_LOGE(TAG, "server write data failed, status: %d", param->write.status);
}
default:
break;
}
}
int app_main()
{
/* 初始化NVS */
esp_err_t ret = nvs_flash_init();
if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
ESP_ERROR_CHECK(nvs_flash_erase());
ESP_ERROR_CHECK(nvs_flash_init());
}
/* 释放低功耗蓝牙资源 */
ESP_ERROR_CHECK(esp_bt_controller_mem_release(ESP_BT_MODE_BLE));
/* 初始化蓝牙 */
esp_bt_controller_config_t bt_cfg = BT_CONTROLLER_INIT_CONFIG_DEFAULT();
if ((ret = esp_bt_controller_init(&bt_cfg)) != ESP_OK) {
ESP_LOGE(TAG, "initialize controller failed: %s", esp_err_to_name(ret));
return -1;
}
/* 使能蓝牙控制器 */
if ((ret = esp_bt_controller_enable(ESP_BT_MODE_CLASSIC_BT)) != ESP_OK) {
ESP_LOGE(TAG, "enable controller failed: %s", esp_err_to_name(ret));
return -1;
}
/* 初始化blueroid */
esp_bluedroid_config_t bluedroid_cfg = BT_BLUEDROID_INIT_CONFIG_DEFAULT();
if ((ret = esp_bluedroid_init_with_cfg(&bluedroid_cfg)) != ESP_OK) {
ESP_LOGE(TAG, "initialize bluedroid failed: %s", esp_err_to_name(ret));
return -1;
}
/* 使能blueroid */
if ((ret = esp_bluedroid_enable()) != ESP_OK) {
ESP_LOGE(TAG, "enable bluedroid failed: %s", esp_err_to_name(ret));
return -1;
}
/* 注册GAP协议回调 */
if ((ret = esp_bt_gap_register_callback(esp_gap_cb)) != ESP_OK) {
ESP_LOGE(TAG, "gap register failed: %s", esp_err_to_name(ret));
return -1;
}
/* 注册SPP协议回调 */
if ((ret = esp_spp_register_callback(esp_spp_cb)) != ESP_OK) {
ESP_LOGE(TAG, "spp register failed: %s", esp_err_to_name(ret));
return -1;
}
/* 初始化SPP */
const esp_spp_cfg_t spp_cfg = {
.mode = ESP_SPP_MODE_CB,
.enable_l2cap_ertm = true,
.tx_buffer_size = 0,
};
if ((ret = esp_spp_enhanced_init(&spp_cfg)) != ESP_OK) {
ESP_LOGE(TAG, "spp init failed: %s", esp_err_to_name(ret));
return -1;
}
send_queue = xQueueCreate(3, sizeof(uint32_t));
while (1) {
uint32_t handle = 0;
if (pdTRUE == xQueueReceive(send_queue, &handle, portMAX_DELAY)) {
if (esp_spp_write(handle, strlen(SPP_DATA), (uint8_t*) SPP_DATA) != ESP_OK) {
ESP_LOGE(TAG, "server send data failed: %s", esp_err_to_name(ret));
}
}
}
return 0;
}
代码与前面主要是回调函数的内容有差别。
GAP回调函数只保留了验证成功事件。
SPP回调函数,初始化完成事件,这里面负责启动服务端,调用esp_spp_start_srv函数,第一个参数设置验证类型,我这里选择无需验证;第二个参数设置对方的角色(主机/从机),这里设置从机;第三个参数设置通道,默认置0,表示选择任意通道;第四个参数设置服务端的名字。
服务端启动完成事件,这里主要就是设置GAP协议下的名字和设备可检测性。
连接开启事件,当客户端成功开启服务端串口时触发,会返回客户端的handle,这个在后面的读写操作是要用到的,同时这里会保存客户端的信息。
连接关闭事件,当串口关闭时触发,返回handle和中断方,这里的处理是复位客户端信息。
数据接收事件,当接收到客户端数据时触发,这里就是简单地做一个打印。
写数据数据事件,当调用esp_spp_write成功后会触发。
主函数中创建一个消息队列用来接收客户端的句柄,这是在数据接收事件中发送过来的,通过这个句柄服务器发送一条回复消息到客户端。
2.3.2 效果
烧录程序后等待服务创建完成,这时候打开手机上的上位机,找到我们的ESP32。
点击“连接”,配对成功就会进入到一个串口助手类似的界面,这时就可以向ESP32发消息了,ESP32接收到消息后就会回复对应的消息。