feat(bt/bluedroid): Added BLE eddystone sender example

(cherry picked from commit 06366353d8)

Co-authored-by: zhangbowen <zhangbowen@espressif.com>
This commit is contained in:
Chen Jian Hua
2025-04-01 09:30:45 +08:00
parent 47befdb5ea
commit 73c9541920
22 changed files with 540 additions and 1 deletions

View File

@ -59,7 +59,7 @@ typedef struct {
uint16_t batt; /*<! battery voltage, 1mV/bit */
uint16_t temp; /*<! beacon temperature */
uint32_t adv_count; /*<! adv pdu count since power-on or reboot */
uint32_t time; /*<! time sence power-on or reboot, a 0.1 second resolution counter */
uint32_t time; /*<! time since power-on or reboot, a 0.1 second resolution counter */
} __attribute__((packed)) esp_eddystone_tlm_t;
/* AD Structure of flags */

View File

@ -0,0 +1,6 @@
# The following lines of boilerplate have to be in your project's CMakeLists
# in this exact order for cmake to work correctly
cmake_minimum_required(VERSION 3.16)
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
project(ble_eddystone_demo)

View File

@ -0,0 +1,50 @@
| Supported Targets | ESP32 | ESP32-C2 | ESP32-C3 | ESP32-C5 | ESP32-C6 | ESP32-C61 | ESP32-H2 | ESP32-S3 |
| ----------------- | ----- | -------- | -------- | -------- | -------- | --------- | -------- | -------- |
# ESP-IDF Eddystone Example
This example demonstrates Eddystone-compatible BLE sending of eddystone frame, including UID and URL and TLM.
Eddystone is an open beacon protocol specification from Google aimed at improving “proximity-based experiences”
with support for both Android and iOS smart device platforms.
Learn more on [Beacons](https://developers.google.com/nearby/notifications/get-started) and [Eddystone](https://github.com/google/eddystone).
## How to Use Example
Before project configuration and build, be sure to set the correct chip target using:
```bash
idf.py set-target <chip_name>
```
Switch sending mode UID or URL or TLM:
Go to: `idf.py menuconfig --> Component config --> Example 'EDDYSTONE SEND' -->` then select the `EXMAPLE_EDDYSTONE_SEND` option.
### Hardware Required
* A development board with ESP32/ESP32-C3/ESP32-H2/ESP32-C2/ESP32-S3 SoC (e.g., ESP32-DevKitC, ESP-WROVER-KIT, etc.)
* A USB cable for Power supply and programming
See [Development Boards](https://www.espressif.com/en/products/devkits) for more information about it.
### Build and Flash
Run `idf.py -p PORT flash monitor` to build, flash and monitor the project.
(To exit the serial monitor, type ``Ctrl-]``.)
See the [Getting Started Guide](https://idf.espressif.com/) for full steps to configure and use ESP-IDF to build projects.
## Example Output
```
I (532) EDDYSTONE_DEMO: Register callback
I (532) EDDYSTONE_DEMO: Raw advertising data set complete
I (532) EDDYSTONE_DEMO: Advertising started successfully
I (542) main_task: Returned from app_main()
```
## Troubleshooting
For any technical queries, please open an [issue](https://github.com/espressif/esp-idf/issues) on GitHub. We will get back to you soon.

View File

@ -0,0 +1,2 @@
idf_component_register(SRCS "esp_eddystone_demo.c"
INCLUDE_DIRS "")

View File

@ -0,0 +1,22 @@
menu "Example 'EDDYSTONE SENDER' Config"
choice EXMAPLE_EDDYSTONE_SEND
prompt "Eddystone frame type"
default EXMAPLE_EDDYSTONE_SEND_UID
help
The type of Eddystone frame to be sent
config EXMAPLE_EDDYSTONE_SEND_UID
bool "Frame type UID"
config EXMAPLE_EDDYSTONE_SEND_URL
bool "Frame type URL"
config EXMAPLE_EDDYSTONE_SEND_TLM
bool "Frame type TLM"
endchoice
config EXMAPLE_EDDYSTONE_SEND
int
default 0 if EXMAPLE_EDDYSTONE_SEND_UID
default 1 if EXMAPLE_EDDYSTONE_SEND_URL
default 2 if EXMAPLE_EDDYSTONE_SEND_TLM
endmenu

View File

@ -0,0 +1,59 @@
/*
* SPDX-FileCopyrightText: 2021-2024 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Unlicense OR CC0-1.0
*/
#ifndef __ESP_EDDYSTONE_API_H__
#define __ESP_EDDYSTONE_API_H__
typedef struct {
struct {
uint8_t flags; /*<! AD flags data */
uint16_t srv_uuid; /*<! complete list of 16-bit service uuid*/
uint16_t srv_data_type; /*<! service data type */
uint8_t frame_type; /*<! Eddystone UID, URL or TLM */
} common;
union {
struct {
/*<! Eddystone-UID */
int8_t ranging_data; /*<! calibrated Tx power at 0m */
uint8_t namespace_id[10];
uint8_t instance_id[6];
} uid;
struct {
/*<! Eddystone-URL */
int8_t tx_power; /*<! calibrated Tx power at 0m */
uint8_t url_scheme; /*<! the scheme of URL */
char encoded_url[EDDYSTONE_URL_MAX_LEN]; /*>! the encoded URL */
char url[EDDYSTONE_URL_MAX_LEN]; /*<! the decoded URL */
} url;
struct {
/*<! Eddystone-TLM */
uint8_t version; /*<! TLM version,0x00 for now */
uint16_t battery_voltage; /*<! battery voltage in mV */
uint16_t temperature; /*<! beacon temperature in degrees Celsius */
uint32_t adv_count; /*<! adv pdu count since power-up */
uint32_t time; /*<! time since power-up, a 0.1 second resolution counter */
} tlm;
} inform;
} esp_eddystone_result_t;
/* utils */
static inline uint16_t little_endian_read_16(const uint8_t *buffer, uint8_t pos)
{
return ((uint16_t)buffer[pos]) | (((uint16_t)buffer[(pos)+1]) << 8);
}
static inline uint16_t big_endian_read_16(const uint8_t *buffer, uint8_t pos)
{
return (((uint16_t)buffer[pos]) << 8) | ((uint16_t)buffer[(pos)+1]);
}
static inline uint32_t big_endian_read_32(const uint8_t *buffer, uint8_t pos)
{
return (((uint32_t)buffer[pos]) << 24) | (((uint32_t)buffer[(pos)+1]) << 16) | (((uint32_t)buffer[(pos)+2]) << 8) | ((uint32_t)buffer[(pos)+3]);
}
#endif /* __ESP_EDDYSTONE_API_H__ */

View File

@ -0,0 +1,209 @@
/*
* SPDX-FileCopyrightText: 2021-2024 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 eddystone_send_raw(const esp_eddystone_result_t *res);
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,
.channel_map = ADV_CHNL_ALL,
.adv_filter_policy = ADV_FILTER_ALLOW_SCAN_ANY_CON_ANY,
};
static void eddystone_send_raw(const esp_eddystone_result_t *res)
{
uint8_t raw_adv_data[31] = {0};
uint8_t index =0;
raw_adv_data[index++] = 2; // length
raw_adv_data[index++] = ESP_BLE_AD_TYPE_FLAG;
raw_adv_data[index++] = (ESP_BLE_ADV_FLAG_GEN_DISC | ESP_BLE_ADV_FLAG_BREDR_NOT_SPT);
raw_adv_data[index++] = 3; //length
raw_adv_data[index++] = ESP_BLE_AD_TYPE_16SRV_CMPL;
raw_adv_data[index++] = 0xAA; //uuid for eddystone
raw_adv_data[index++] = 0xFE;
switch(res->common.frame_type)
{
case EDDYSTONE_FRAME_TYPE_UID: {
raw_adv_data[index++] = 23; //length
raw_adv_data[index++] = ESP_BLE_AD_TYPE_SERVICE_DATA;
raw_adv_data[index++] = 0xAA;
raw_adv_data[index++] = 0xFE;
uint8_t service_data[EDDYSTONE_UID_DATA_LEN+3] = {0};
service_data[0] = EDDYSTONE_FRAME_TYPE_UID;
service_data[1] = res->inform.uid.ranging_data;
memcpy(&service_data[2], res->inform.uid.namespace_id, EDDYSTONE_UID_NAMESPACE_LEN);
memcpy(&service_data[12], res->inform.uid.instance_id, EDDYSTONE_UID_INSTANCE_LEN);
service_data[18] = 0x00;
service_data[19] = 0x00;
memcpy(&raw_adv_data[index], service_data, EDDYSTONE_UID_DATA_LEN+3);
index += EDDYSTONE_UID_DATA_LEN + 3;
break;
}
case EDDYSTONE_FRAME_TYPE_URL: {
size_t url_len = strlen((char*)res->inform.url.encoded_url); //encoded url length
if(url_len > EDDYSTONE_URL_MAX_LEN){
url_len = EDDYSTONE_URL_MAX_LEN;
}
raw_adv_data[index++] = url_len+6; //length
raw_adv_data[index++] = ESP_BLE_AD_TYPE_SERVICE_DATA;
raw_adv_data[index++] = 0xAA;
raw_adv_data[index++] = 0xFE;
uint8_t service_data[EDDYSTONE_URL_MAX_LEN+2] = {0};
service_data[0] = EDDYSTONE_FRAME_TYPE_URL;
service_data[1] = res->inform.url.tx_power;
service_data[2] = res->inform.url.url_scheme;
memcpy(&service_data[3], res->inform.url.encoded_url, url_len);
memcpy(&raw_adv_data[index], service_data, url_len+3);
index += url_len+3;
break;
}
case EDDYSTONE_FRAME_TYPE_TLM: {
raw_adv_data[index++] = 17; //length
raw_adv_data[index++] = ESP_BLE_AD_TYPE_SERVICE_DATA;
raw_adv_data[index++] = 0xAA;
raw_adv_data[index++] = 0xFE;
uint8_t service_data[EDDYSTONE_TLM_DATA_LEN+1] = {0};
service_data[0] = EDDYSTONE_FRAME_TYPE_TLM;
service_data[1] = res->inform.tlm.version;
service_data[2] = (res->inform.tlm.battery_voltage >> 8) & 0xFF;
service_data[3] = res->inform.tlm.battery_voltage & 0xFF;
service_data[4] = (res->inform.tlm.temperature >> 8) & 0xFF;
service_data[4] = res->inform.tlm.temperature & 0xFF;
service_data[6] = (res->inform.tlm.adv_count >> 24) & 0xFF;
service_data[7] = (res->inform.tlm.adv_count >> 16) & 0xFF;
service_data[8] = (res->inform.tlm.adv_count >> 8) & 0xFF;
service_data[9] = res->inform.tlm.adv_count & 0xFF;
service_data[10] = (res->inform.tlm.time >> 24) & 0xFF;
service_data[11] = (res->inform.tlm.time >> 16) & 0xFF;
service_data[12] = (res->inform.tlm.time >> 8) & 0xFF;
service_data[13] = res->inform.tlm.time & 0xFF;
memcpy(&raw_adv_data[index], service_data, EDDYSTONE_TLM_DATA_LEN+1);
index += EDDYSTONE_TLM_DATA_LEN+1;
break;
}
default:
break;
}
esp_ble_gap_config_adv_data_raw(raw_adv_data, index);
}
static void esp_gap_cb(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t* param)
{
switch(event)
{
case ESP_GAP_BLE_ADV_DATA_RAW_SET_COMPLETE_EVT: {
ESP_LOGI(DEMO_TAG,"Raw advertising data set complete");
esp_ble_gap_start_advertising(&adv_params);
break;
}
case ESP_GAP_BLE_ADV_START_COMPLETE_EVT: {
if(param->adv_start_cmpl.status == ESP_BT_STATUS_SUCCESS){
ESP_LOGI(DEMO_TAG,"Advertising started successfully");
}else{
ESP_LOGE(DEMO_TAG,"Failed to start advertising, error code = %x",param->adv_start_cmpl.status);
}
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();
esp_eddystone_result_t eddystone_result;
#if (CONFIG_EXMAPLE_EDDYSTONE_SEND_UID)
eddystone_result.common.frame_type= EDDYSTONE_FRAME_TYPE_UID;
eddystone_result.inform.uid.ranging_data = 0;
memcpy(eddystone_result.inform.uid.namespace_id, (uint8_t[]){0x01,0x02,0x03,0x04,0x05,0x06,0x07,0x08,0x09,0x0A}, 10);
memcpy(eddystone_result.inform.uid.instance_id, (uint8_t[]){0xB1,0xB2,0xB3,0xB4,0xB5,0xB6}, 6);
#endif /* #if (CONFIG_EXMAPLE_EDDYSTONE_SEND_UID) */
#if (CONFIG_EXMAPLE_EDDYSTONE_SEND_URL)
eddystone_result.common.frame_type= EDDYSTONE_FRAME_TYPE_URL;
eddystone_result.inform.url.tx_power = 0;
eddystone_result.inform.url.url_scheme = 0x00;
strcpy(eddystone_result.inform.url.encoded_url,"example.com");
#endif /* #if (CONFIG_EXMAPLE_EDDYSTONE_SEND_URL) */
#if (CONFIG_EXMAPLE_EDDYSTONE_SEND_TLM)
eddystone_result.common.frame_type = EDDYSTONE_FRAME_TYPE_TLM;
eddystone_result.inform.tlm.version = 0x00;
eddystone_result.inform.tlm.battery_voltage = 3000;
eddystone_result.inform.tlm.temperature = 220;
eddystone_result.inform.tlm.adv_count = 1000;
eddystone_result.inform.tlm.time = 50000;
#endif /* #if (CONFIG_EXMAPLE_EDDYSTONE_SEND_TLM) */
eddystone_send_raw(&eddystone_result);
}

View File

@ -0,0 +1,156 @@
/*
* SPDX-FileCopyrightText: 2021-2024 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Unlicense OR CC0-1.0
*/
#ifndef __ESP_EDDYSTONE_PROTOCOL_H__
#define __ESP_EDDYSTONE_PROTOCOL_H__
#include "stdbool.h"
#include "stdint.h"
/* Eddystone definitions */
#define EDDYSTONE_SERVICE_UUID 0xFEAA
#define EDDYSTONE_FRAME_TYPE_UID 0x00
#define EDDYSTONE_FRAME_TYPE_URL 0x10
#define EDDYSTONE_FRAME_TYPE_TLM 0x20
#define EDDYSTONE_FRAME_TYPE_EID 0x30
//UID
#define EDDYSTONE_UID_RANG_DATA_LEN 1
#define EDDYSTONE_UID_NAMESPACE_LEN 10
#define EDDYSTONE_UID_INSTANCE_LEN 6
#define EDDYSTONE_UID_RFU_LEN 2
#define EDDYSTONE_UID_DATA_LEN (EDDYSTONE_UID_RANG_DATA_LEN + EDDYSTONE_UID_NAMESPACE_LEN + EDDYSTONE_UID_INSTANCE_LEN)
//TLM
#define EDDYSTONE_TLM_VERSION_LEN 1
#define EDDYSTONE_TLM_BATTERY_VOLTAGE_LEN 2
#define EDDYSTONE_TLM_TEMPERATURE_LEN 2
#define EDDYSTONE_TLM_ADV_COUNT_LEN 4
#define EDDYSTONE_TLM_TIME_LEN 4
#define EDDYSTONE_TLM_DATA_LEN (EDDYSTONE_TLM_VERSION_LEN + EDDYSTONE_TLM_BATTERY_VOLTAGE_LEN + \
EDDYSTONE_TLM_TEMPERATURE_LEN + EDDYSTONE_TLM_ADV_COUNT_LEN + EDDYSTONE_TLM_TIME_LEN)
//URL
#define EDDYSTONE_URL_SCHEME_LEN 1
#define EDDYSTONE_URL_ENCODED_MAX_LEN 17
#define EDDYSTONE_URL_MAX_LEN (EDDYSTONE_URL_SCHEME_LEN + EDDYSTONE_URL_ENCODED_MAX_LEN)
#define EDDYSTONE_URL_TX_POWER_LEN 1
/****************** Eddystone UID Frame **************
Byte offset Field Description
0 Frame Type Value = 0x00
1 Ranging Data Calibrated Tx power at 0 m
2 NID[0] 10-byte Namespace
3 NID[1]
4 NID[2]
5 NID[3]
6 NID[4]
7 NID[5]
8 NID[6]
9 NID[7]
10 NID[8]
11 NID[9]
12 BID[0] 6-byte Instance
13 BID[1]
14 BID[2]
15 BID[3]
16 BID[4]
17 BID[5]
18 RFU Reserved for future use, must be0x00
19 RFU Reserved for future use, must be0x00
*********************************************/
typedef struct {
int8_t ranging_data; /*<! calibrated Tx power at 0m */
uint8_t namespace_id[10];
uint8_t instance_id[6];
uint8_t reserved[2];
} __attribute__((packed))esp_eddystone_uid_t;
/****************** Eddystone URL Frame *************
Frame Specification
Byte offset Field Description
0 Frame Type Value = 0x10
1 TX Power Calibrated Tx power at 0 m
2 URL Scheme Encoded Scheme Prefix
3+ Encoded URL Length 1-17
*******************************************************/
typedef struct {
int8_t tx_power; /*<! calibrated Tx power at 0m */
uint8_t url_scheme; /*<! encoded scheme prefix */
uint8_t encoded_url[0]; /*<! length 1-17 */
} __attribute__((packed))esp_eddystone_url_t;
/****************** Eddystone TLM Frame ***************
Unencrypted TLM Frame Specification
Byte offset Field Description
0 Frame Type Value = 0x20
1 Version TLM version, value = 0x00
2 VBATT[0] Battery voltage, 1 mV/bit
3 VBATT[1]
4 TEMP[0] Beacon temperature
5 TEMP[1]
6 ADV_CNT[0] Advertising PDU count
7 ADV_CNT[1]
8 ADV_CNT[2]
9 ADV_CNT[3]
10 SEC_CNT[0] Time since power-on or reboot
11 SEC_CNT[1]
12 SEC_CNT[2]
13 SEC_CNT[3]
************************************************/
typedef struct {
uint8_t version; /*<! TLM version,0x00 for now */
uint16_t batt; /*<! battery voltage, 1mV/bit */
uint16_t temp; /*<! beacon temperature */
uint32_t adv_count; /*<! adv pdu count since power-on or reboot */
uint32_t time; /*<! time since power-on or reboot, a 0.1 second resolution counter */
} __attribute__((packed)) esp_eddystone_tlm_t;
/* AD Structure of flags */
typedef struct {
uint8_t len;
uint8_t type;
uint8_t flags;
} __attribute__((packed)) esp_eddystone_flags_t;
/* AD Structure of complete 16-bit service uuid */
typedef struct {
uint8_t len;
uint8_t type;
uint16_t uuid; /*<! complete list of 16-bit service UUIDs data type value */
} __attribute__((packed)) esp_eddystone_uuid_t;
/* AD Structure of eddystone frame*/
typedef struct {
uint8_t len; /*<! length of eddystone data */
uint8_t type; /*<! service data type,must be 0x16 */
uint16_t uuid; /*<! 16-bit eddystone uuid */
uint8_t frame_type;
union {
esp_eddystone_uid_t uid;
esp_eddystone_url_t url;
esp_eddystone_tlm_t tlm;
} u[0];
} __attribute__((packed)) esp_eddystone_frame_t;
/* eddystone packet type */
typedef struct {
esp_eddystone_flags_t flags;
esp_eddystone_uuid_t uuid;
esp_eddystone_frame_t frame;
} __attribute__((packed)) esp_eddystone_packet_t;
/*
* URLs are written only with the graphic printable characters of the US-ASCII coded character set.
* The octets 00-20 and 7F-FF hexadecimal are not used.
* See “Excluded US-ASCII Characters” in RFC 2936.
*
*/
static inline bool esp_eddystone_is_char_invalid(int ch)
{
return (ch >= 0x00 && ch <= 0x20) || (ch >= 0x7f && ch <= 0xff);
}
#endif /* __ESP_EDDYSTONE_PROTOCOL_H__ */

View File

@ -0,0 +1,11 @@
# This file was generated using idf.py save-defconfig. It can be edited manually.
# Espressif IoT Development Framework (ESP-IDF) Project Minimal Configuration
#
CONFIG_BT_ENABLED=y
# CONFIG_BT_BLE_50_FEATURES_SUPPORTED is not set
CONFIG_BT_BLE_42_FEATURES_SUPPORTED=y
# CONFIG_BT_LE_50_FEATURE_SUPPORT is not used on ESP32, ESP32-C3 and ESP32-S3.
# CONFIG_BT_LE_50_FEATURE_SUPPORT is not set
CONFIG_EXMAPLE_EDDYSTONE_SEND_UID=y
CONFIG_EXMAPLE_EDDYSTONE_SEND_URL=n
CONFIG_EXMAPLE_EDDYSTONE_SEND_TLM=n

View File

@ -0,0 +1,10 @@
# This file was generated using idf.py save-defconfig. It can be edited manually.
# Espressif IoT Development Framework (ESP-IDF) Project Minimal Configuration
#
CONFIG_IDF_TARGET="esp32c2"
CONFIG_BT_ENABLED=y
# CONFIG_BT_BLE_50_FEATURES_SUPPORTED is not set
CONFIG_BT_BLE_42_FEATURES_SUPPORTED=y
# CONFIG_BT_LE_50_FEATURE_SUPPORT is not set
CONFIG_BT_LE_HCI_EVT_BUF_SIZE=257
CONFIG_XTAL_FREQ_26=y

View File

@ -0,0 +1,7 @@
# This file was generated using idf.py save-defconfig. It can be edited manually.
# Espressif IoT Development Framework (ESP-IDF) Project Minimal Configuration
#
CONFIG_IDF_TARGET="esp32c3"
CONFIG_BT_ENABLED=y
# CONFIG_BT_BLE_50_FEATURES_SUPPORTED is not set
CONFIG_BT_BLE_42_FEATURES_SUPPORTED=y

View File

@ -0,0 +1,7 @@
# This file was generated using idf.py save-defconfig. It can be edited manually.
# Espressif IoT Development Framework (ESP-IDF) Project Minimal Configuration
#
CONFIG_IDF_TARGET="esp32s3"
CONFIG_BT_ENABLED=y
# CONFIG_BT_BLE_50_FEATURES_SUPPORTED is not set
CONFIG_BT_BLE_42_FEATURES_SUPPORTED=y