(二十)零基础开发小安派-Eyes-S1【番外篇】——BLE基础通讯

[复制链接]
查看3213 | 回复21 | 2024-5-14 16:38:41 | 显示全部楼层 |阅读模式

本帖最后由 起个名字好难啊 于 2024-5-14 16:49 编辑

(饭)前(撒)yan

前两个星期,园长就问大家有没有需要学的,然后:

image.png

行行行,那就满足你们,

这次这个BLE帖子的篇幅有点长,因为我是边学边写,相当于一个手记了,顺便也贴一下我在看的教学视频:

https://www.bilibili.com/video/BV1ad4y1d7AM?p=1&vd_source=02a465997504a99b4366d967ab71e479

学习本贴需要体现下载BLE 调试工具,App 推荐下载NRF Connecet:点击下载安装包

也可以下载鹏老师自己写的电脑端BLE调试助手:https://gitee.com/ospanic/BlueCom

什么是BLE?

1.gif

看到这里,你就应该知道,BLE就是低功耗蓝牙的简称,莫工对BLE也不是很了解👀️ 。我只知道,现在的手机不能在系统设置当中直接搜索到BLE设备,只能使用APP或者小程序搜。

一、BLE 广播数据

BLE广播数据是什么呢?由什么作用呢?

广播数据是BLE从机发送一段信息,数据上包含了BLE设备的MAC地址,设备名称等内容,手机作为主机搜索到广播数据之后进行识别显示。

根据教学视频的讲义,BLE广播数据的字节最高只能到37个(字节),而且BLE MAC地址默认占用了6个,只剩下31个字节给我们使用。这31个字节全部由"AD Structure" 结构体组成,但是不会只有一个AD Structure,实际上会有好几个这样的结构体。

Ai-M6x发送广播数据的实践

在使用教程中鹏老师给我们演示了如何使用MicroPython来实现了广播数据的发送,但是Ai-M61不支持MicroPython,所以我们直接使用C语言来进行实践。根据教程,我们的逻辑应该是:

st=>start: 开始
op=>operation: 初始化协议栈
op1=>operation: 启动蓝牙
cond=>condition: 启动成功
op2=>operation: 发送广播
e=>end: 结束
st->op->op1->cond
cond(no)->e
cond(yes)->op2->e

Ai-M61 启用蓝牙组件

在工程的 proj.conf 文件中,添加以下代码启用BLE组件:

set(CONFIG_BLUETOOTH 1)
set(CONFIG_BTBLECONTROLLER_LIB ble1m0s1bredr0)
set(CONFIG_BLE_USE_MAC2 0)
set(CONFIG_BLE_TP_SERVER 1)
set(CONFIG_BTBLECONTROLLER_LIB ble1m10s1bredr0)

需要引用的头文件

#include "bluetooth.h"
#include "hci_driver.h"
#include "hci_core.h"
#include "conn.h"
#include "conn_internal.h"
#include "gatt.h"
#include "btble_lib_api.h"

创建广播数据

最简单的广播数据就是MAC+设备名称,但是因为广播数据需要定义TYPE(第一个AD Structure),所以设备名称在第二个AD Structure里定义:

#define ble_slave_name "Ai-M61-BLE"
/*  定义广播数据 总子节数不得超过31 byte  */
static const struct bt_data salve_adv[] = {
    BT_DATA_BYTES(BT_DATA_FLAGS,(BT_LE_AD_GENERAL| BT_LE_AD_NO_BREDR)),  //数据头占用 2 byte
    BT_DATA(BT_DATA_NAME_COMPLETE,ble_slave_name,sizeof(ble_slave_name)-1),//第二个数据,定义名称
};

启动BLE协议栈和蓝牙

Ai-M61自动蓝牙的函数需要一个回调函数,当蓝牙启动成功之后,会通过触发回调,我们可以在回调函数当中打印蓝牙MAC地址信息和开始广播:

**
 * @brief 蓝牙启动回调
 *
 * @param err
*/
static void bt_enable_cb(int err)
{
    if (!err)
    {
        bt_addr_le_t bt_addr;
        bt_get_local_public_address(&bt_addr);
        LOG_I("BD_ADDR:(MSB)%02x:%02x:%02x:%02x:%02x:%02x(LSB)",
               bt_addr.a.val[5], bt_addr.a.val[4], bt_addr.a.val[3], bt_addr.a.val[2], bt_addr.a.val[1], bt_addr.a.val[0]);
        //蓝牙启动完成之后发送广播包
        int err = -1;
        err = bt_le_adv_start(BT_LE_ADV_CONN, salve_adv, ARRAY_SIZE(salve_adv), NULL, 0);
        if (err)
            LOG_E("[BLE] adv fail(err %d)", err);
    }
}
/**
 * @brief 启动BLE 协议栈
 *
*/
static void ble_stack_start(void)
{
    // Initialize BLE controller
    btble_controller_init(configMAX_PRIORITIES - 1);
    // Initialize BLE Host stack
    hci_driver_init();
    bt_enable(bt_enable_cb);
}

所以我们只需要在main 当中调用 ble_stack_start() 函数就能让Ai-M61的BLE发送广播,使用电脑的BLE调试助手的搜索结果:

image.png

二、扫描响应

什么是扫描响应

看完教程之后,大家可能有个疑虑?什么是扫描响应? 说到,扫描响应分可分为:

  • 可连接非定向(常用)
  • 可连接定向 (快连)
  • 不可连接非定向(信标)
  • 可扫描非定向 (能看不能连)

对于我的理解,扫描响应得数据应该是区别于广播数据,但是也能被广播出去,这就以为着,当我们的广播数据太长的时候,可以使用扫描响应数据。通常情况下就是体现在BLE 蓝牙名字的设置上。

创建扫描响应

扫描响应数据的创建和广播数据一样,用的结构体也一样,在这里,我把BLE的名称放在扫描响应数据里面:

//定义蓝牙名称
#define ble_slave_name "Ai-M61-BLE"
/*  定义广播数据 总子节数不得超过31 byte  */
static const struct bt_data salve_adv[] = {
    BT_DATA_BYTES(BT_DATA_FLAGS,(BT_LE_AD_GENERAL| BT_LE_AD_NO_BREDR)),  //数据头占用 2 byte
    // BT_DATA(BT_DATA_NAME_COMPLETE,ble_slave_name,sizeof(ble_slave_name)-1),//第二个数据,定义名称
};
/*  定义扫描响应数据 总字节不得超过31byte*/
static const struct bt_data salve_rsp[] = {
    BT_DATA(BT_DATA_NAME_COMPLETE,ble_slave_name,sizeof(ble_slave_name)-1),//把设备名称放在扫描响应当中
};

在Ai-M6x的BLE 组件中,扫描响应数据的添加和广播数据用的函数是同一个bt_le_adv_start,它的第四个第五个参数就是扫描响应的数据和数据长度, 所以,只需要给bt_le_adv_start函数的第四和第五个参数传递扫描响应数据和它的数据长度就行了。

bt_le_adv_start(BT_LE_ADV_CONN, salve_adv, ARRAY_SIZE(salve_adv), salve_rsp, ARRAY_SIZE(salve_rsp));

实践结果

image.png

三、状态切换

教程中,BLE 状态一共有五种:

  • 就绪态: BLE启动之后的状态
  • 广播态: BLE从机发送广播包时的状态
  • 连接态: BLE连接成功之后
  • 扫描态(BLE主机模式): BLE主机扫描从机时的状态
  • 发起态(BLE主机模式): BLE主机对从机发起连接时的状态

本教程只关注从机模式的状态,从机模式合理的状态切换的逻辑:

st=>start: 开始
op=>operation: 启动蓝牙(就绪态)
op1=>operation: 发起广播(广播态)
cond=>condition: 是否已经连接
op2=>operation: 连接态
cond1=>condition: 是否断开连接
e=>end: 结束

 st->op->op1->cond
cond(no)->op1
cond(yes)->op2->e

同样的,我们也只对连接和断开连接两个事件进行处理,连接对应的是连接态,而断开连接会变为就绪态,我们应该发起广播,把状态切换成广播态,让手机等BLE主机能够扫描到它。

连接回调和断开回调的创建

连接回调函数

我们可以在连接回调函数当中直接打印“connected:现在是连接态”信息,并且主动关闭广播,这样可以节省功耗,就像这样:

static void _connected(struct bt_conn *conn, u8_t err)
{
    if (err) {
        LOG_E("connected err:%d", err);
        bt_conn_unref(conn);
        return ;
    }
   LOG_I("connected:现在是连接态"); 
   bt_le_adv_stop();
    return ;
}

断开连接回调

断开连接的回调函数里,除了打印之外,还需要主动打开广播:

static void _disconnected(struct bt_conn *conn, u8_t reason)
{
    LOG_W("disconnected, 此时是就绪态reason:%d", reason);
    LOG_I("切换到 广播态");
    int err = bt_le_adv_start(BT_LE_ADV_CONN, salve_adv, ARRAY_SIZE(salve_adv), salve_rsp, ARRAY_SIZE(salve_rsp));
    if (err)
       LOG_E("[BLE] adv fail(err %d)", err);
}

注册回调函数

回调函数不能直接运行,它必须有接口调用它们,在Ai-M6x的BLE协议库中,使用 struct bt_conn_cb声明回调函数接口,并以下面这个函数来注册:

bt_conn_cb_register

用法示例:

/* 定义struct bt_conn_cb 结构体来指定相应的回调函数 */
static struct bt_conn_cb conn_callbacks = {
.connected = _connected,
.disconnected = _disconnected,
};
/* 在初始化函数中注册回调函数*/
/**
 * @brief 启动BLE 协议栈
 *
*/
static void ble_stack_start(void)
{
    // Initialize BLE controller
#ifdef CONFIG_Ai_M6x  
    btble_controller_init(configMAX_PRIORITIES - 1);
#endif
#ifdef CONFIG_Ai_WB2
    ble_controller_init(configMAX_PRIORITIES - 1);
#endif
    // Initialize BLE Host stack
    hci_driver_init();
    bt_enable(bt_enable_cb);
    bt_conn_cb_register(&conn_callbacks);//注册回调
}

运行结果,根据代码去试试吧

image.png

四、服务与特征(特性)

BLE 的服务和特性,是BLE数据互交的基础,前面所讲的内容全部都是让BLE 设备具备可连接功能而且。所以服务和特性直接决定了BLE设备是否可以接收和发送数据的能力。

服务决定了BLE设备的BLE设备类型,比如BLE HID是鼠标设备还是键盘设备。都需要通过定义服务来确定,在这里,不会深究BLE具备的服务类型。一个服务下面有一个或者多个特征,这些特征决定了服务的功能,一个服务到底是用来传数据的还是接收数据的,都需要特征来决定。 在实际编程当中,服务通常以service作为标识,而特征作为service的成员来定义,GATT的库当中,给了相关的宏定义让我们方便地创建特征。

UUID介绍

UUID是每个服务和特征的标识符,最长是128Bit(位),UUID有三种长度:

  • 16Bit UUID
  • 32Bit UUID
  • 128Bit UUID

特征的属性和权限

属性

特征除了拥有UUID之外,还需要属性来声明这个特征的功能,比如这个特征是用来做通知的,还是用来读写的等,GATT一共给了以下几个属性:

  • BT_GATT_CHRC_BROADCAST:广播类型的特征(非标准,不常用)
  • BT_GATT_CHRC_READ:可读属性(常用)
  • BT_GATT_CHRC_WRITE_WITHOUT_RESP:特征写入回复
  • BT_GATT_CHRC_WRITE:可写属性
  • BT_GATT_CHRC_NOTIFY:可通知属性(notify)
  • BT_GATT_CHRC_INDICATE:需要从机对数据确认的属性
  • BT_GATT_CHRC_AUTH:连接认证
  • BT_GATT_CHRC_EXT_PROP:扩展的属性

权限

特征定义好了属性之后,还需要给权限,读或者写?还是其他的权限,GATT库也给我们声明好了:

  • BT_GATT_PERM_NONE:无权限,只有notify 支持
  • BT_GATT_PERM_READ: 可读权限
  • BT_GATT_PERM_WRITE:可写权限
  • BT_GATT_PERM_READ_ENCRYPT:加密连接的可读权限
  • BT_GATT_PERM_WRITE_ENCRYPT:加密连接的可写权限
  • BT_GATT_PERM_READ_AUTHEN:需要验证的可读权限
  • BT_GATT_PERM_WRITE_AUTHEN:需要验证的可写权限
  • BT_GATT_PERM_PREPARE_WRITE:需要准备的可写权限

在这里,我们可以得知服务与特征的一些关系:

image.png

服务创建的实践

开发流程

从图中我们可以看到,UUID 作为标识符,用来识别了服务和特征,在编程当中,一个服务和特征的创建都要先从UUID开始,教程也是一样,先定义了三个UUID:9011(服务的UUID),9012(特征1的UUID),9013(特征2)的UUID,流程就这么出来了:

st=>start: 开始
op=>operation: 创建UUID
op1=>operation: 声明服务的UUID
op2=>operation: 声明特征1的UUID,属性,特征
op3=>operation: 声明特征2的UUID,属性,特征
op4=>operation: 创建服务接口
op5=>operation: 注册服务
e=>end: 结束

st->op->op1->op2->op3->op4->op5->e

创建UUID

创建UUID可以使用宏定义来创建:

#define SERVER_UUID_16BIT  BT_UUID_DECLARE_16(0X9011) //服务器UUID
#define NOTIFY_UUID_16BIT  BT_UUID_DECLARE_16(0X9012) //特征1 UUID
#define WRITER_UUID_16BIT  BT_UUID_DECLARE_16(0X9013) //特征2  UUID

声明服务和特征

声明服务和特征有一个专门的结构体,struct bt_gatt_attr,结构体里只能创建一个服务,但是能过声明多个特征,所以当创建一个服务及其特征的时候,通常以创建 struct bt_gatt_attr 结构数组来实现,例如:

/* 定义一个服务,这个服务有两个特性  */
static struct bt_gatt_attr salve_uuid_server[] = {
    /* 服务 UUID */
    BT_GATT_PRIMARY_SERVICE(SERVER_UUID_16BIT),
    /* 可读可通知的特征 */
    BT_GATT_CHARACTERISTIC(NOTIFY_UUID_16BIT,BT_GATT_CHRC_NOTIFY,BT_GATT_PERM_READ,NULL,NULL,NULL),
    /* 可读可写属性的特征 */
    BT_GATT_CHARACTERISTIC(WRITER_UUID_16BIT,BT_GATT_CHRC_WRITE,BT_GATT_PERM_READ|BT_GATT_PERM_WRITE,NULL,NULL,NULL),
};

注册服务接口

struct bt_gatt_attr结构体不能直接进行服务注册,需要创建 bt_gatt_service ble_uuid_server结构体,并指向 struct bt_gatt_attr之后才能使用注册,例如:

/* 创建 BLE 服务接口*/
static struct bt_gatt_service ble_uuid_server = BT_GATT_SERVICE(salve_uuid_server);

然后使用 bt_gatt_service_register 函数进行一个服务的注册,这个注册函数放在广播之前调用,我们在蓝牙启动之后,没进入广播态之后调用:

static void ble_stack_start(void)
{
    // Initialize BLE controller
    btble_controller_init(configMAX_PRIORITIES - 1);
    // Initialize BLE Host stack
    hci_driver_init();
    bt_enable(bt_enable_cb);
    bt_conn_cb_register(&conn_callbacks);
    conn_callbacks._next = NULL;
    /* 注册BLE 服务*/
    int ret = bt_gatt_service_register(&ble_uuid_server);
}

实践结果

image.png

五、数据收发

接收数据

在视频教程的第6节,鹏老师提到BLE设备想要收到手机的数据,需要写一个“被写”的回调函数,也就是接收回调,在回调函数里打印手机发送来的数据。这个函数只需要在具有可写属性的特征里声明即可,根据上一节的定义,特则2(UUID:9013)就是一个可写特征,这个接收回调函数是放在特征2里:

static ssize_t ble_uuid_write_val(struct bt_conn* conn, const struct bt_gatt_attr* attr, const void* buf, u16_t len, u16_t offset, u8_t flags)
{
    uint8_t* recv_buffer;
    recv_buffer = pvPortMalloc(sizeof(uint8_t) * len);
    memcpy(recv_buffer, buf, len);
    LOG_RD("recv ble data len= %d:%s -----", len, recv_buffer);
    for (size_t i = 0; i < len; i++)
    {
        LOG_RI("0x%x ", recv_buffer[i]);
    }
    printf("\r\n");
    vPortFree(recv_buffer);

    return len;
}

/* 定义一个服务,这个服务有两个特性  */
static struct bt_gatt_attr salve_uuid_server[] = {
    /* 服务 UUID */
    BT_GATT_PRIMARY_SERVICE(SERVER_UUID_16BIT),
    /* 可读可通知的特征 */
    BT_GATT_CHARACTERISTIC(NOTIFY_UUID_16BIT,BT_GATT_CHRC_NOTIFY,BT_GATT_PERM_READ,NULL,NULL,NULL),
    /* 可读可写属性的特征 并声明接收回调函数*/
    BT_GATT_CHARACTERISTIC(WRITER_UUID_16BIT,BT_GATT_CHRC_WRITE_WITHOUT_RESP,BT_GATT_PERM_WRITE,NULL,ble_uuid_write_val,NULL),
};

实践结果

image.png

发送数据

在视频当中,BLE设备给手机发送数据是通过Notify通知手机读取,但是在BLE GATT当中,BLE 设备Notify 需要通过GATT_CCC 来报告Notify的状态,所以需要改一下 struct bt_gatt_attr 结构体,需要在具有“Notify”属性的特征之下增加 BT_GATT_CCC

/* 定义一个服务,这个服务有两个特性  */
static struct bt_gatt_attr salve_uuid_server[] = {
    /* 服务 UUID */
    BT_GATT_PRIMARY_SERVICE(SERVER_UUID_16BIT),
    /* 可读可通知的特征 */
    BT_GATT_CHARACTERISTIC(NOTIFY_UUID_16BIT,BT_GATT_CHRC_NOTIFY,BT_GATT_PERM_READ,NULL,NULL,NULL),
    /*添加GATT_CCC*/
    BT_GATT_CCC(NULL, BT_GATT_PERM_READ | BT_GATT_PERM_WRITE),
    /* 可读可写属性的特征 并声明接收回调函数*/
    BT_GATT_CHARACTERISTIC(WRITER_UUID_16BIT,BT_GATT_CHRC_WRITE,BT_GATT_PERM_WRITE,NULL,ble_uuid_write_val,NULL),
};

这时候,特征1 才能被监听,在NRF Connect App中可以看出区别:

image.png

调用Notify发送数据

在接收回调当中使用 bt_gatt_notify函数给手机发送数据,当手机发送数据下来的时候就回复"Hello Master":

static ssize_t ble_uuid_write_val(struct bt_conn* conn, const struct bt_gatt_attr* attr, const void* buf, u16_t len, u16_t offset, u8_t flags)
{
    uint8_t* recv_buffer;
    recv_buffer = pvPortMalloc(sizeof(uint8_t) * len);
    memcpy(recv_buffer, buf, len);
    LOG_D("recv ble data len= %d:%s", len, recv_buffer);
    for (size_t i = 0; i < len; i++)
    {
        LOG_RI("0x%x ", recv_buffer[i]);
    }
    printf("\r\n");
    vPortFree(recv_buffer);
    /* 给手机发送数据 */
    bt_gatt_notify(conn, &salve_uuid_server[2], "Hello Master", strlen("Hello Master"));
    return len;
}

实践结果

2.gif

image.png

六、声明

我是跟着鹏老师说的原理及流程来开发Ai-M6x的BLE,再次之前对一些概念并不是很清楚,所以难免会有一些理解不到位的情况。 上面的五个小节在例程当中都分别由相应的源码,请运行在AiPi-Open-Kits的AiPi-aiThinkerClud分支,并且在 examples 使用git 指令获取demo 源码:

git clone https://gitee.com/seahi007/Ai-BLE_exmple.git

必看

如果觉得鹏老师讲得好,三连加关注吧

回复

使用道具 举报

1084504793 | 2024-5-14 16:56:14 | 显示全部楼层
回复

使用道具 举报

方源 | 2024-5-14 16:56:27 | 显示全部楼层
莫工辛苦了
回复 支持 反对

使用道具 举报

爱笑 | 2024-5-14 17:01:27 | 显示全部楼层
排版又好看,帖子内容又有深度!莫工出品,必属精品
用心做好保姆工作
回复 支持 反对

使用道具 举报

mowhale | 2024-5-14 17:15:51 | 显示全部楼层
太赞了
回复

使用道具 举报

mowhale | 2024-5-14 17:22:44 | 显示全部楼层
回复

使用道具 举报

lovzx | 2024-5-14 20:05:04 | 显示全部楼层
学习
回复

使用道具 举报

起个名字好难啊 | 2024-5-14 21:31:50 | 显示全部楼层
爱笑 发表于 2024-5-14 17:01
排版又好看,帖子内容又有深度!莫工出品,必属精品

那不给我加精?
回复 支持 反对

使用道具 举报

hrqwe | 2024-5-15 08:00:01 | 显示全部楼层
向莫工学习
日拱一卒,功不唐捐
回复 支持 反对

使用道具 举报

WT_0213 | 2024-5-15 08:49:22 | 显示全部楼层
学习中…
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则