讲讲 CH58x 做主机时候扫描到广播包的处理 ...... 矜辰所致
前言
在刚开始学习蓝牙 Ble 的时候,博主写过一篇《CH585 蓝牙 示例工程 Central 全解析》 ,介绍了一下 CH585 示例作为主机工作的整体框架流程。
最近应用需要识别扫描到的广播包中的内容,根据广播包的内容进行下一步操作,所以本文我们主要就是介绍一下在 CH58x 的工程结构里面如何解析广播包。
相关文章:
CH585 蓝牙 示例工程 Central 全解析
BLE 蓝牙空中报文格式与解析(广播包)沁恒微 RISC-V 芯片学习系列博文:
【导航】沁恒微 RISC-V 蓝牙 入门教程目录 【快速跳转】.
我是矜辰所致,全网同名,尽量用心写好每一系列文章,不浮夸,不将就,认真对待学知识的我们,矜辰所致,金石为开!
目录
- 前言
- [一、 在哪里收到广播包](#一、 在哪里收到广播包)
- [二、 数据解析](#二、 数据解析)
-
- [2.1 消息结构体说明](#2.1 消息结构体说明)
- [2.2 广播包 MAC 地址和 RSSI 值](#2.2 广播包 MAC 地址和 RSSI 值)
- [2.3 蓝牙设备名字](#2.3 蓝牙设备名字)
- [三、 应用示例](#三、 应用示例)
- 结语
一、 在哪里收到广播包
在博主以前的文章 《CH585 蓝牙 示例工程 Central 全解析》 中,我们已经介绍过主机的正常工作的基本流程都是在 centralEventCB 回调函数中实现的,在这个回调函数中有一个 GAP_DEVICE_INFO_EVENT 事件,是在主机扫描过程中收到广播包就会触发一次。
在以前的表格中,也有说明了,如下图:

这个事件收到广播包会触发一次,收到扫描响应包也会触发一次!
所以我们的数据处理可以在 GAP_DEVICE_INFO_EVENT 中进行。
在示例中,官方使用了一个函数centralAddDeviceInfo 进行数据处理。

除了通常的 GAP_DEVICE_INFO_EVENT ,官方主机示例还支持 BLE5.0 的扩展广播,和定向广播接收,他们是以独立的事件存在的,但是使用后的是同一个处理函数centralAddDeviceInfo ,如下图:

函数 centralAddDeviceInfo 只是官方应用层一个示例函数,我们知道了消息在哪里接收,我们完全可以写自己的解析函数。
在示例中,只是做了简单的 MAC 地址打印处理,在每次扫描到广播包以后,只要不到设定的最大扫描值DEFAULT_MAX_SCAN_RES(示例中为 10),都会把扫描到的设备 MAC 地址打印出来 ,如下图:

二、 数据解析
我们可以在 centralAddDeviceInfo 里处理接收到的数据,当然如果我们只处理非定向一般广播报文,我们也可以在case GAP_DEVICE_INFO_EVENT: 分支中处理。
2.1 消息结构体说明
事件回调函数centralEventCB 的参数是一个gapRoleEvent_t 类型的联合体,联合体的成员就是不同事件的结构体:

每一个结构体都有一个opcode 的成员变量,来表示本次是什么事件,我们收到广播消息的传递的结构体为 gapDeviceInfoEvent_t 结构体,这些都是可以通过代码查看得到的 :
c
//!< Sent during the Device Discovery Process when a device is discovered.
//This event is sent as an tmos message defined as gapDeviceInfoEvent_t.
#define GAP_DEVICE_INFO_EVENT 0x0D

那我们既然知道了消息传递的结构体,我们就可以进行解析了,这里我们直接在GAP_DEVICE_INFO_EVENT 分支中解析。
GAP_EXT_ADV_DEVICE_INFO_EVENT 和 GAP_DIRECT_DEVICE_INFO_EVENT 类型的广播报文解析方式类似,只需要根据他们的对应结构体进行就好:

所以我也一直强调,真的想好好应用,官方提供的源码该好好看还是得好好看,不管什么芯片应用都是如此。
2.2 广播包 MAC 地址和 RSSI 值
根据结构体,我们可以知道,广播包 MAC 地址 和 RSSI 值,直接可以获取到,我们添加如下代码:
c
case GAP_DEVICE_INFO_EVENT:
{
// 收到报文这里解析:
// 打印广播报文整体长度
uint8_t i;
PRINT("Adv:");
for(i=0;i< pEvent->deviceInfo.dataLen;i++)
PRINT("%02X ",pEvent->deviceInfo.pEvtData[i]);
PRINT("\r\n");
PRINT("MAC addr:");
for(i=0;i< B_ADDR_LEN;i++) // B_ADDR_LEN 6
PRINT("%02X ",pEvent->deviceInfo.addr[i]);
PRINT("\r\n");
PRINT("RSSI:%d\r\n",pEvent->deviceInfo.rssi);
centralAddDeviceInfo(pEvent->deviceInfo.addr, pEvent->deviceInfo.addrType);
}
break;
测试效果如下:

我们把蓝牙数据全部都打印出来了,我们可以根据广播报文格式进行解析,蓝牙广播包格式直接看博主以前的文章 BLE 蓝牙空中报文格式与解析(广播包) 就好,这里我直接使用蓝牙分析仪直接抓包对比。
= =!我测试环境中广播报文太多了,太难找到对应了,于是我直接屏蔽了一些信号差的广播报文:
c
case GAP_DEVICE_INFO_EVENT:
{
// 收到报文这里解析:
// 打印广播报文整体长度
uint8_t i;
if(pEvent->deviceInfo.rssi > -50){
PRINT("Adv:");
for(i=0;i< pEvent->deviceInfo.dataLen;i++)
PRINT("%02X ",pEvent->deviceInfo.pEvtData[i]);
PRINT("\r\n");
PRINT("MAC addr:");
for(i=0;i< B_ADDR_LEN;i++) // B_ADDR_LEN 6
PRINT("%02X ",pEvent->deviceInfo.addr[i]);
PRINT("\r\n");
PRINT("RSSI:%d\r\n",pEvent->deviceInfo.rssi);
centralAddDeviceInfo(pEvent->deviceInfo.addr, pEvent->deviceInfo.addrType);
}
}
break;
示例效果如下:

2.3 蓝牙设备名字
在上面广播包或者扫描响应包中是会存在蓝牙名字的,我们可以去分析数据中的「AD Structure」,找到 AD Type 为 0x08 或者 0x09 的去获取名字:

我再修改一下测试代码,增加蓝牙名字获取的部分,测试代码如下:
c
static const char *ad_name_type(uint8_t type)
{
switch (type) {
case 0x08: return "Short Name";
case 0x09: return "Complete Name";
default: return NULL;
}
}
case GAP_DEVICE_INFO_EVENT:
{
// 收到报文这里解析:
// 打印广播报文整体长度
uint8_t i;
const uint8_t *p = pEvent->deviceInfo.pEvtData;
uint16_t len = pEvent->deviceInfo.dataLen;
if(pEvent->deviceInfo.rssi > -50){
PRINT("Adv:");
for(i=0;i< pEvent->deviceInfo.dataLen;i++)
PRINT("%02X ",pEvent->deviceInfo.pEvtData[i]);
PRINT("\r\n");
PRINT("MAC addr:");
for(i=0;i< B_ADDR_LEN;i++) // B_ADDR_LEN 6
PRINT("%02X ",pEvent->deviceInfo.addr[i]);
PRINT("\r\n");
PRINT("RSSI:%d\r\n",pEvent->deviceInfo.rssi);
i = 0;
while (i+1 < len) {
uint8_t fieldLen = p[i];
uint8_t fieldType = p[i + 1];
if (fieldLen == 0) break;
const char *typeStr = ad_name_type(fieldType);
if (typeStr) { // 遇到 0x08 或 0x09
uint8_t nameLen = fieldLen - 1; // 去掉 type 字节
PRINT("%s: ", typeStr);
/* 直接逐字符打印,不用补 0 */
for (uint8_t j = 0; j < nameLen; j++)
PRINT("%c", p[i + 2 + j]);
PRINT("\r\n");
}
i += fieldLen + 1; // 下一个 AD 结构
}
centralAddDeviceInfo(pEvent->deviceInfo.addr, pEvent->deviceInfo.addrType);
}
}
break;
测试效果如下:

上面是打印出广播包里面的蓝牙名字,不管是广播包还是扫描响应包,只要有名字就能够打印出来。
我们实际应用的时候,不一定需要打印,可能只需要找到特定名字的广播包,我们可以用strstr 函数来实现,比如:
c
//找到名字为 "my need name" 的广播报文
if((Name = strstr(pEvent->deviceInfo.pEvtData,"my need name")) != 0)
{
PRINT("Name = %s\r\n",Name);
}
上面的示例简单测试是可以的,但是存在一下潜在的问题,如果名字前面广播报文里面有 0 ,那么就会提前返回找不到,而且通过%s打印出的名字,结尾可能会有问题,因为不一定有字符串结束符 '\0' ,名字后面可能会乱码。
我们更加规范的做法还是先找到 0x08 或者 0x09 的位置,然后再比较,这里我直接上一下测试代码:
c
static const char TARGET_NAME[] = "Simple Peripheral";
...
/*------------------------------------------------------------
* 在广播数据里 **只** 扫描 AD type 0x08/0x09 的字段,然后做 strstr
* 返回:匹配到 -> true ,否则 false
*----------------------------------------------------------*/
static uint8_t scan_for_name(const uint8_t *pData, uint16_t dataLen)
{
uint16_t pos = 0;
while (pos < dataLen - 1) {
uint8_t fieldLen = pData[pos]; // Length
uint8_t fieldType = pData[pos + 1]; // AD Type
if (fieldLen == 0) break; // 安全终止
/* 只处理 Short/Complete Local Name */
if (fieldType == 0x08 || fieldType == 0x09) {
uint8_t nameLen = fieldLen - 1; // 去掉 type 字节
char tmp[32]; // 最长 31 足够
if (nameLen >= sizeof(tmp)) nameLen = sizeof(tmp) - 1;
memcpy(tmp, &pData[pos + 2], nameLen); // 拷出名字
tmp[nameLen] = '\0'; // 手工 '\0' 结尾
if (strstr(tmp, TARGET_NAME)) // 子串匹配
return TRUE;
}
pos += fieldLen + 1; // 跳到下一个 AD 结构
}
return FALSE;
}
//...
case GAP_DEVICE_INFO_EVENT:
{
// 收到报文这里解析:
// 打印广播报文整体长度
// 。。。前面和上面一样
if (scan_for_name(pEvent->deviceInfo.pEvtData,pEvent->deviceInfo.dataLen)) {
PRINT("Name matched: %s\r\n", TARGET_NAME);
//找到名字做自己想做的事情,根据名字连接
}
centralAddDeviceInfo(pEvent->deviceInfo.addr, pEvent->deviceInfo.addrType);
}
}
测试效果如下:

三、 应用示例
在上文,我们已经知道怎么获取广播中的 设备名字,MAC地址,和 RSSI 。
在官方示例中,在扫描完成事件case GAP_DEVICE_DISCOVERY_EVENT: (就在我们上面收到广播包GAP_DEVICE_INFO_EVENT的下面一个分支)设备会查找是否扫描到自己想连接的 MAC 地址设备,进入连接或者重新扫描。
连接函数为(该函数只能通过 MAC 地址建立连接):
c
bStatus_t GAPRole_CentralEstablishLink( uint8_t highDutyCycle, uint8_t whiteList, uint8_t addrTypePeer, uint8_t *peerAddr );
官方的框架已经搭建好了,我们其实只需要处理自己的应用逻辑,我们简单实现一个满足如下条件
信号强度 > -50dbm
设备名字为 :Simple Peripheral
设备的自动连接。
这里直接通过上下实现代码就行了,不过多赘述:
c
static const char TARGET_NAME[] = "Simple Peripheral";
static uint8_t scan_for_name(const uint8_t *pData, uint16_t dataLen)
{
函数内容上文中给了,不重复占用篇幅
}
case GAP_DEVICE_INFO_EVENT:
{
// 收到报文这里解析:
// 打印广播报文整体长度
if (pEvent->deviceInfo.rssi <= -50) break;
if (scan_for_name(pEvent->deviceInfo.pEvtData,pEvent->deviceInfo.dataLen)) {
PRINT("Name matched: %s\r\n", TARGET_NAME);
tmos_memcpy(PeerAddrDef,pEvent->deviceInfo.addr,6);
}
centralAddDeviceInfo(pEvent->deviceInfo.addr, pEvent->deviceInfo.addrType);
}
测试效果如下:

我们可以看到,这里我们找到设备名字,然后把 MAC 地址保存,能够正常进行连接。 但是通过打印我们可以判断上面的做法 tmos_memcpy 会执行多次,虽然没有使用问题,但是还是可以优化一下。
原来的示例中消息处理centralAddDeviceInfo 会把扫描到的不同 MAC 地址的设备放入 centralDevList 数组中,所以干脆再修改一下,只有满足条件的设备,我才会进 centralAddDeviceInfo 处理,使用centralDevList 来修改 MAC 地址,我们可以在 centralAddDeviceInfo 里面修改连接的 MAC(在这里修改要注意其他两种广播不会影响,或者其他广播里面也加入名字查找的过滤):

或者直接加在GAP_DEVICE_INFO_EVENT 后面:

这个细节处理问题大家根据自己喜欢来就好了。
结语
本文我们介绍了使用 CH58x 如何解析处理收到的蓝牙广播数据,这个过程是在未产生连接,扫描过程中发生的。在以后的应用中,我们就可以根据自己的需求过滤掉不需要的广播数据以便于我们后续的操作。
好了,本文就到这里。谢谢大家!