Skip to content

蓝牙GAP

  • 广播者: 通过发送数据让接受者发现自己, 只可以发送信息不可以被连接
  • 观察者: 接收发送的请求, 不可以被连接
  • 外围设备: 广播者接收观察者的信息以后会进入这一种, 作为从设备通信
  • 中央设备: 当观察者主动进⾏初始化,并建⽴⼀个物理链路时就会进⼊这 种⻆⾊。这种⻆⾊在链路中同样被称为主设备

image-20240624101046145

beacon(灯塔)信标广播(GAP)

GAP API - ESP32 - — ESP-IDF 编程指南 v5.1.4 文档 (espressif.com)

蓝牙中的GAP指的是“Generic Access Profile”,是蓝牙协议栈中的一个重要部分。它定义了蓝牙设备的广播格式、连接建立和断开、设备发现、连接参数等方面的规范。通过GAP,不同类型的蓝牙设备能够互相通信和交互。

Beacon 设备作为蓝牙低功耗协议中的外围设备,持续向周围广播包含设备标识的特定数据包,但不能和中心设备建立连接。小程序运行的设备作为中心设备,可以收到 Beacon 设备的广播包,实现数据交互。常用于室内定位、消息推送等场景

这是一个蓝牙广播的标准, 不是蓝牙技术联盟提出来的标准, 可以从云端获取咨询吗然后进行广播, 周围的用户可以接收

网络侧定位: 在室内有多个探针, 发送出去的数据有一个距离参数, 通过计算衰减可以获取设备的位置

Eddystone谷歌公司制定的标准。Eddystone相对于iBeacon功能更为广泛,可以广播自定义唯一的信标ID(UID)、广播网址(URL)、广播自身数据(TLM/ETLM)、加密的临时标识符(EID)。

这一个是beacon信标的其中一种, 还有苹果的iBeacon等

ESP32S3蓝牙学习系列---Beacon信标之Eddystone-CSDN博客

可以广播四种帧格式

  • UID: 最高有128位, 实际使用的时候16-32位就可以了, 广播自定义的唯一信标ID
  • URL: 网络地址
  • TLM/ETLM 自身数据(E代表加密)
  • EID: 加密的临时标识符, 用于保密等

UUID: 通用唯一标识符

最高可以有128bit, 一般使用16或者32位就可以了

这一个ID里面有公司, 对象特征值类型, 属性类型声明, 属性特征描述符, 属性服务, 计量单位, 协助标识符, 标准开发组织, 服务规范ID

广播数据包

image-20240610102609584

  • 4个字节固定值

0x8e89bed6

接入地址有两种类型:广播接入地址和数据接入地址。

广播接入地址:固定为0x8E89BED6,在广播、扫描、发起连接时使用

数据接入地址:随机值,不同的连接有不同的值,在连接建立后两个设备间使用。

  • Header头

有两种, 第一种是一些类型, 使用bit6, bit7记录发送的信息是公开的(0)还是随机的(1)

第二种是一个长度, 这一个长度记录的是数据不包括CRC校验的长度

  • 数据

最少是6字节, 记录自己的地址, 之后是实际的数据, 这里的数据是很多AD结构构成的

image-20240610102638669

这个Flag是数据的第一种类型的数据实际格式

Eddystone数据包

实际就是上面的一组数据包

image-20240610102810263

第一个数据包格式

image-20240610215030490

第一个数据对应的是这一个, 只有第三位置1

第二个数据包image-20240610215202175

这一个ID是谷歌公司的ID

image-20240610215311534

数据有四种

这时候发送的功率会在对方接收以后计算距离使用

UID 这是由三个字段组成的主要Eddystone框架,即命名空间标识符(10byte),实例标识符(6byte)和功率校准(1byte)。他将有助于将字段直接配置到BLE信标中,实例标识符旨在唯一地表示一个信标,因为他们具有不同的实例ID,功率校准字段用于根据RSSI帮助计算移动设备和Eddystone信标之间的距离。 URL 该框架的主要目的是提供一种传输URL的方法,以便蓝牙低功耗扫描仪设备检测和发现它,然后BLE设备将连接收到并为用户显示正确的网页。BLE信标发送Eddystone-URL资源,智能手机等移动设备在检测到信标数据后立即在浏览器中自动打开网址。同时该框架提供类似于Eddystone-UID的功率校准功能。 TLM Eddystone-TML 框架的主要是提供关于Eddystone的健康状况的完整报告。当前温度, 当前电池电量, 正常运行时间 (信标已通电的秒数), 和 PduCount (信标在最后一次供电后部署的广告数据包的数量). EID 这是负责小工具安全和隐私的框架。

示例代码

接收数据

使用的时候eddystone的数据格式需要自己准备, eddystone的数据解码是这一个示例里面的用户空间里面实现的image-20240610223305298

c
esp_ble_gap_set_scan_params(&ble_scan_params); //使用这一个函数设置扫描
esp_ble_gap_start_scanning(duration);//开启扫描,在回调函数的配置成功的时候开启
c
#ifndef	_EDDYSTONE_PACKET_H
#define	_EDDYSTONE_PACKET_H

#include <stddef.h>

#define EDDYSTONE_UID	0x00
#define EDDYSTONE_URL	0x10
#define EDDYSTONE_TLM	0x20
#define CONV32(v,a)		(v = ((a)&0xff) << 24 | ((a)&0xff00) << 8 | ((a)&0xff0000) >> 8 | ((a)&0xff000000)>>24)
#define CONV16(v,a)		(v = ((a)&0xff) << 8 | ((a)&0xff00) >> 8)

/**********URL*************/
#define	URL_LEN		20
#define URL_DATA_LEN(dns)	(2+2+1+dns+1)

typedef struct {
	uint16_t uuid;
	uint8_t frame_type;		//类型
	uint8_t tx_power;		//发射功率
	uint8_t name_space[10];	//命名空间
	uint8_t instance[6];	//实例
	uint8_t rfu[2];			//保留位
}__attribute__((packed)) eddystone_uid_t;				//UID

typedef struct {
	uint16_t uuid;
	uint8_t frame_type;	//类型
	uint8_t tx_power;	//发射功率
	uint8_t scheme;		//前缀
	uint8_t encoded[20];//网址
}__attribute__((packed)) eddystone_url_t;			//URL

typedef struct {
	uint16_t 	uuid;
	uint8_t 	frame_type;			//类型
	uint8_t   	version;			//版本
	uint16_t  	battery_voltage; 	//1 mV/bit
	uint16_t  	temperature;      	//温度
	uint32_t	count;				//计数
	uint32_t  	time; 				//时间
	
}__attribute__((packed)) eddystone_tlm_t;

int Eddystone_Set_UID(eddystone_uid_t *uid,char power,uint8_t * name_space,uint8_t *instance);
int Eddystone_Set_URL(eddystone_url_t *url,char power,uint8_t scheme,
							const char *dns_addr ,uint8_t len, uint8_t postfix);
int Eddystone_Set_TLM(eddystone_tlm_t *tlm,uint16_t voltage,float temp,uint32_t time);

#endif
c
#include <stdio.h>
#include <string.h>
#include "eddystone_packet.h"
#include "esp_eddystone_protocol.h"
/********************************************************************
函数名:		Eddystone_Set_UID
描述:			设置eddystone UID 帧格式参数
参数:			uid 	- uid指针
				power 	- 功率
				name_space 	- 命名空间
				instance	- 实例
返回值:		错误	-1
				正确	服务数据长度	
********************************************************************/
int Eddystone_Set_UID(eddystone_uid_t *uid,char power,uint8_t * name_space,uint8_t *instance)
{
	if(uid == NULL || name_space == NULL || instance == NULL)
	{
		return -1;
	}
	memset(uid,0,sizeof(eddystone_uid_t));
	uid->uuid = EDDYSTONE_SERVICE_UUID;
	uid->frame_type = EDDYSTONE_UID;
	uid->tx_power = (uint8_t)power;
	for(int i = 0;i <10;i++)
		uid->name_space[i] = *name_space++;

	for(int i = 0;i <6;i++)
		uid->instance[i] = *instance++;

	return sizeof(eddystone_uid_t);
}
/********************************************************************
函数名:		Eddystone_Set_URL
描述:			设置eddystone URL 帧格式参数
参数:			url 	- uid指针
				power 	- 功率
				scheme 	- 前缀
				dns_addr - 域名
				len		- 域名长度
				postfix	- 后缀
返回值:		错误	-1
				正确	服务数据长度(根据网址长度而定)	
********************************************************************/
int Eddystone_Set_URL(eddystone_url_t *url,char power,uint8_t scheme,
							const char *dns_addr ,uint8_t len, uint8_t postfix)
{
	if(url == NULL || dns_addr == NULL || len > 17)
		return -1;
	
	if(url->encoded == NULL)
		return -1;
		
	memset(url->encoded,0,len);

	url->uuid = EDDYSTONE_SERVICE_UUID;
	url->frame_type = EDDYSTONE_URL;
	url->tx_power = (uint8_t)power;
	url->scheme = scheme;
	memcpy(url->encoded,dns_addr,len);
	url->encoded[len] = postfix;
    
	return URL_DATA_LEN(len);
}
/********************************************************************
函数名:		Eddystone_Set_TLM
描述:			设置eddystone TLM 帧格式参数
参数:			tlm 	- uid指针
				voltage - 电压 单位 mV
				temp 	- 温度	(大于0)
				time 	- 开机到现在时间 以0.1秒为单位计数
返回值:		错误	-1
				正确	服务数据长度(根据网址长度而定)	
********************************************************************/
int Eddystone_Set_TLM(eddystone_tlm_t *tlm,uint16_t voltage,float temp,uint32_t time)
{
	static uint32_t cnt = 0;
	if(tlm == NULL)
		return -1;
	cnt++;
	int8_t temp_integral = (int8_t)temp;
	float temp_decimal = temp - temp_integral;
	uint8_t decimal = (uint8_t)(temp_decimal * 256.0);
	
	printf("temp_integral = %d,temp_decimal = %f,decimal = %d,\r\n",temp_integral,temp_decimal,decimal);
	
	tlm->uuid = EDDYSTONE_SERVICE_UUID;
	tlm->frame_type = EDDYSTONE_TLM;
	tlm->version = 0;
	CONV16(tlm->battery_voltage,voltage);	//电压值
	
	tlm->temperature = (uint16_t)(decimal << 8 | temp_integral);	//温度
	printf("tlm->temperature = %x\r\n",tlm->temperature);
	
	CONV32(tlm->count,cnt);		//发送计数
	CONV32(tlm->time,time);		


	return sizeof(eddystone_tlm_t);
}

image-20240610102944431

ble_eddystone为信标扫描

c
/*
 * SPDX-FileCopyrightText: 2021-2022 Espressif Systems (Shanghai) CO LTD
 *
 * SPDX-License-Identifier: Unlicense OR CC0-1.0
 */


/****************************************************************************
*
* This file is used for eddystone receiver.
*
****************************************************************************/

#include <stdio.h>
#include <stdint.h>
#include <string.h>
#include <inttypes.h>

#include "esp_bt.h"
#include "nvs_flash.h"
#include "esp_log.h"
#include "esp_bt_defs.h"
#include "esp_bt_main.h"
#include "esp_gatt_defs.h"
#include "esp_gattc_api.h"
#include "esp_gap_ble_api.h"
#include "freertos/FreeRTOS.h"

#include "esp_eddystone_protocol.h"
#include "esp_eddystone_api.h"

static const char* DEMO_TAG = "EDDYSTONE_DEMO";

/* declare static functions */
static void esp_gap_cb(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t* param);
static void esp_eddystone_show_inform(const esp_eddystone_result_t* res);
//扫描的参数设置
static esp_ble_scan_params_t ble_scan_params = {
    .scan_type              = BLE_SCAN_TYPE_ACTIVE,  //扫描类型
    .own_addr_type          = BLE_ADDR_TYPE_PUBLIC,  //所有者的地址类型
    .scan_filter_policy     = BLE_SCAN_FILTER_ALLOW_ALL,
    .scan_interval          = 0x50,   //扫描间隔
    .scan_window            = 0x30,   //扫描的持续时间     
    .scan_duplicate         = BLE_SCAN_DUPLICATE_DISABLE  //
};

static void esp_eddystone_show_inform(const esp_eddystone_result_t* res)
{
    //打印信息
    switch(res->common.frame_type)
    {
        //三种类型
        case EDDYSTONE_FRAME_TYPE_UID: {
            ESP_LOGI(DEMO_TAG, "Eddystone UID inform:");
            ESP_LOGI(DEMO_TAG, "Measured power(RSSI at 0m distance):%d dbm", res->inform.uid.ranging_data);
            ESP_LOGI(DEMO_TAG, "EDDYSTONE_DEMO: Namespace ID:0x");
            esp_log_buffer_hex(DEMO_TAG, res->inform.uid.namespace_id, 10);
            ESP_LOGI(DEMO_TAG, "EDDYSTONE_DEMO: Instance ID:0x");
            esp_log_buffer_hex(DEMO_TAG, res->inform.uid.instance_id, 6);
            break;
        }
        case EDDYSTONE_FRAME_TYPE_URL: {
            ESP_LOGI(DEMO_TAG, "Eddystone URL inform:");
            ESP_LOGI(DEMO_TAG, "Measured power(RSSI at 0m distance):%d dbm", res->inform.url.tx_power);
            ESP_LOGI(DEMO_TAG, "URL: %s", res->inform.url.url);
            break;
        }
        case EDDYSTONE_FRAME_TYPE_TLM: {
            ESP_LOGI(DEMO_TAG, "Eddystone TLM inform:");
            ESP_LOGI(DEMO_TAG, "version: %d", res->inform.tlm.version);
            ESP_LOGI(DEMO_TAG, "battery voltage: %d mV", res->inform.tlm.battery_voltage);
            ESP_LOGI(DEMO_TAG, "beacon temperature in degrees Celsius: %6.1f", res->inform.tlm.temperature);
            ESP_LOGI(DEMO_TAG, "adv pdu count since power-up: %" PRIu32, res->inform.tlm.adv_count);
            ESP_LOGI(DEMO_TAG, "time since power-up: %" PRIu32 " s", (res->inform.tlm.time)/10);
            break;
        }
        default:
            break;
    }
}
//回调函数
static void esp_gap_cb(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t* param)
{
    esp_err_t err;

    switch(event)
    {
        //参数设置完成
        case ESP_GAP_BLE_SCAN_PARAM_SET_COMPLETE_EVT: {
            uint32_t duration = 0;
            esp_ble_gap_start_scanning(duration);
            break;
        }
        //扫描启动
        case ESP_GAP_BLE_SCAN_START_COMPLETE_EVT: {
            if((err = param->scan_start_cmpl.status) != ESP_BT_STATUS_SUCCESS) {
                ESP_LOGE(DEMO_TAG,"Scan start failed: %s", esp_err_to_name(err));
            }
            else {
                ESP_LOGI(DEMO_TAG,"Start scanning...");
            }
            break;
        }
        //扫描结束
        case ESP_GAP_BLE_SCAN_RESULT_EVT: {
            //获取数据
            esp_ble_gap_cb_param_t* scan_result = (esp_ble_gap_cb_param_t*)param;
            switch(scan_result->scan_rst.search_evt)
            {
                case ESP_GAP_SEARCH_INQ_RES_EVT: {
                    //获取查询数据
                    esp_eddystone_result_t eddystone_res;
                    memset(&eddystone_res, 0, sizeof(eddystone_res));
                    //进行解码, 查看是不是eddystone的广播包
                    esp_err_t ret = esp_eddystone_decode(scan_result->scan_rst.ble_adv, scan_result->scan_rst.adv_data_len, &eddystone_res);
                    if (ret) {
                        // error:The received data is not an eddystone 
                        // frame packet or a correct eddystone frame packet.
                        // just return
                        return;
                    } else {
                        // The received adv data is a correct eddystone frame packet.
                        // Here, we get the eddystone infomation in eddystone_res, 
                        // we can use the data in res to do other things.
                        // For example, just print them:
                       	//获取信息成功
                        ESP_LOGI(DEMO_TAG, "--------Eddystone Found----------");
                        esp_log_buffer_hex("EDDYSTONE_DEMO: Device address:", scan_result->scan_rst.bda, ESP_BD_ADDR_LEN);
                        ESP_LOGI(DEMO_TAG, "RSSI of packet:%d dbm", scan_result->scan_rst.rssi);
                        esp_eddystone_show_inform(&eddystone_res);
                    }
                    break;
                }
                default:
                    break;
            }
            break;
        }
        //停止的回调函数
        case ESP_GAP_BLE_SCAN_STOP_COMPLETE_EVT:{
            if((err = param->scan_stop_cmpl.status) != ESP_BT_STATUS_SUCCESS) {
                ESP_LOGE(DEMO_TAG,"Scan stop failed: %s", esp_err_to_name(err));
            }
            else {
                ESP_LOGI(DEMO_TAG,"Stop scan successfully");
            }
            break;
        }
        default:
            break;
    }
}

void esp_eddystone_appRegister(void)
{
    esp_err_t status;

    ESP_LOGI(DEMO_TAG,"Register callback");
    //注册一个应用层回调函数
    /*<! register the scan callback function to the gap module */
    if((status = esp_ble_gap_register_callback(esp_gap_cb)) != ESP_OK) {
        ESP_LOGE(DEMO_TAG,"gap register error: %s", esp_err_to_name(status));
        return;
    }
}

void esp_eddystone_init(void)
{
    esp_bluedroid_init(); //初始化信标
    esp_bluedroid_enable(); //使能信标
    esp_eddystone_appRegister();
}

void app_main(void)
{
    ESP_ERROR_CHECK(nvs_flash_init());
    //释放经典蓝牙
    ESP_ERROR_CHECK(esp_bt_controller_mem_release(ESP_BT_MODE_CLASSIC_BT));
    //初始化开启低功耗蓝牙定时器
    esp_bt_controller_config_t bt_cfg = BT_CONTROLLER_INIT_CONFIG_DEFAULT();
    esp_bt_controller_init(&bt_cfg);
    esp_bt_controller_enable(ESP_BT_MODE_BLE);
    //初始化
    esp_eddystone_init();

    /*<! set scan parameters 设置扫描参数*/
    esp_ble_gap_set_scan_params(&ble_scan_params);
}

发送数据

c
// The length of adv data must be less than 31 bytes
//static uint8_t test_manufacturer[TEST_MANUFACTURER_DATA_LEN] =  {0x12, 0x23, 0x45, 0x56};
//adv data 广播的数据包结构体
static esp_ble_adv_data_t adv_data = {
    .set_scan_rsp = false,  //是否响应
    .include_name = true,   //是不是有设备名
    .include_txpower = true,//是不是包含频率
    //最小以及最大间隔
    .min_interval = 0x0006, //slave connection min interval, Time = min_interval * 1.25 msec
    .max_interval = 0x000C, //slave connection max interval, Time = max_interval * 1.25 msec
    .appearance = 0x00,     //设备外观
    .manufacturer_len = 0, //TEST_MANUFACTURER_DATA_LEN, 制造商的名字长度
    .p_manufacturer_data =  NULL, //&test_manufacturer[0], 制造商数据的长度
    .service_data_len = 0,  //服务数据的长度
    .p_service_data = NULL,
    .service_uuid_len = 32, //uuid的长度
    .p_service_uuid = adv_service_uuid128, //uuid指针
    .flag = (ESP_BLE_ADV_FLAG_GEN_DISC | ESP_BLE_ADV_FLAG_BREDR_NOT_SPT), //flag的那一个参数
};
//广播数据设置
static esp_ble_adv_params_t adv_params = {
    .adv_int_min        = 0x20,		   //广告间隔
    .adv_int_max        = 0x40,
    .adv_type           = ADV_TYPE_IND, //可连接通用广播
    .own_addr_type      = BLE_ADDR_TYPE_PUBLIC, //设备地址类型
    //.peer_addr            =				//对端蓝牙设备地址
    //.peer_addr_type       =
    .channel_map        = ADV_CHNL_ALL,		//通道
    .adv_filter_policy = ADV_FILTER_ALLOW_SCAN_ANY_CON_ANY, //过滤
};
c
esp_err_t esp_ble_gap_config_adv_data(esp_ble_adv_data_t *adv_data); //设置发送的数据
esp_err_t esp_ble_gap_start_advertising(esp_ble_adv_params_t *adv_params); //开启广播
c
eddystone_uid_t uid;

int len = Eddystone_Set_UID(&uid, 10, "hello", "12345");
if(len == -1){
    ESP_LOGE("main", "get eddystone error");
    return;
}
adv_data.service_data_len = len;
adv_data.p_service_data = &uid;

esp_ble_gap_config_adv_data(&adv_data);
esp_ble_gap_start_advertising(&adv_params); //这一个函数最好放在回调函数里面

实际的实现

API

开启扫描

c
esp_err_t esp_ble_gap_start_scanning(uint32_t duration);

开启扫描, duration持续的时间

获取特定类型数据

c
uint8_t *esp_ble_resolve_adv_data(uint8_t *adv_data, uint8_t type, uint8_t *length)

type: 是esp_ble_adv_data_type里面的, 解析GATT名字的时候使用的是ESP_BLE_AD_TYPE_NAME_CMPL