本帖最后由 HonestQiao 于 2021-9-8 19:00 编辑
FireBeetle Board ESP32-E IoT
开发板支持iBeacon,因此,这篇文章中,我们进行了iBeacon的基础应用:通过RSSI进行距离测算。本次的操作,是在Mac环境下的Arduino中进行的。在其他操作系统下面,代码没有差异,Arduino和串口的操作方法,根据系统具体而定。
为了方便同时操作两块ESP32开发版,我同时使用了Arduino和Arduino IDE两个编辑器,两者独立运行,分别对应一块开发版,可以同时进行烧录和串口监控运行状态。
通过本篇文章,你可以了解到:
1. 关于蓝牙测距的基础知识
2. 低功耗iBeacon发射端
3. iBeacon扫描端
4. ESP32中断与定时器
5. ESP32-C3板载三色LED颜色显示
下面进入正题:
1. 关于蓝牙测距的基础知识
我们先了解下,什么是iBeacon:
引用: iBeacon是Apple推出的一项低耗能蓝牙技术,由蓝牙设备发射包含指定信息的信号,再由移动设备接收信号,从而实现近场通信。这里有两个重点:一个是蓝牙技术,一个是低功耗。
通过蓝牙技术,可以进行近距离设备发现和近场通讯。
而低功耗,能够让设备以较小的功耗持续运行
然后,我们再了解一下蓝牙测距的知识:
引用: 任何无线电波,经过发射后,都会随着传播距离的增加,都会发生衰减。
而蓝牙信标发射的信号强度(rssi)与收发设备之间的距离,由于衰减的原因,在某种程度上是正相关的。
因此通过合理的参数设置和运算转化,可以通过探测到的rssi的值,来反推出发送设备与接收设备间的距离。
但是,需要给大家一点点冷静:因为这个衰减,受环境因素影响非常大。在不同的环境中,相关的经验参数,可能存在着较大的差异。
但是好在,距离与信号强度(rssi)是正相关的,在同一个环境中,如果干扰不多变化不大的化,还是可以用于非精准距离测量的,下面是一个基本的公示:
其中,A和n都是经验值,发射端和接收端相隔1米时的信号强度,n是环境衰减因子,需要通过反复测量和计算得到。关于这两个经验值的获取,在后续iBeacon接收端中会进行说明。
为了测量的更精准,还有应用更复杂,考虑因素更多的公示;同时,也会使用多个iBeacon发送设备,来实现三角定位(通过三个发射设备,来确定接收设备的具体位置)来实现更高的精度。
因为这篇文章,是初阶的,所以我们仅使用1块FireBeetle Board ESP32-E IoT 开发板来做iBeacon发射端,一个ESP32-C3来做接收端。
2. 低功耗iBeacon发射端
发射端,我们使用一块FireBeetle Board ESP32-E IoT开发板,这个开发板支持iBeacon和低功耗运行。
这部分的代码,我们直接使用Arduino的例子->FireBeetle Board ESP32-E的例子->ESP32 BLE Arduino->BLE_iBeacon,具体操作如下:
具体代码如下:
在上面的代码中,会发送广告信息,我们不用管它。
关键在于iBeacon帧的设置setBeacon(),以及这个设备的BEACON_UUID,该UUID值,可以进行修改;再就是esp_deep_sleep()实现深度睡眠。
设置后代码后,我们把FireBeetle Board ESP32-E IoT开发板连上电脑的USB口,按照如下步骤进行设置:
1)选择开发版:
2)设置调试信息级别:
3)选择串口:
4)编译代码:
5)下载代码:
6)运行监控:
3. iBeacon扫描端
扫描/接收端,我们使用一块ESP32-C3-Mini1 开发板,这个开发板支持蓝牙,并有一个板载三色LED,便于我们显示不同的状态的。
这部分的代码,我们直接使用Arduino的例子->ESP32 C3 Dev Module的例子->ESP32 BLE Arduino->BLE_Beacon_Scanner,具体操作如下:
这个例子,用于基础的Beacon和iBeacon扫描,为了适应我们的需要,进行了适当的修改,具体代码如下:
- /*
- Based on Neil Kolban example for IDF: https://github.com/nkolban/esp32-snippets/blob/master/cpp_utils/tests/BLE%20Tests/SampleScan.cpp
- Ported to Arduino ESP32 by Evandro Copercini
- Changed to a beacon scanner to report iBeacon, EddystoneURL and EddystoneTLM beacons by beegee-tokyo
- */
- #include
- #include
- #include
- #include
- #include
- #include
- #include
- #include
- #include "esp_system.h"
- #include "led_strip.h"
- #define ENDIAN_CHANGE_U16(x) ((((x)&0xFF00) >> 8) + (((x)&0xFF) << 8))
- // RSSI经验参数
- #define A 66
- #define n 2.0
- // 目标SSID
- #define IBEACON_MY_SSID "8ec76ea3-6668-48da-9866-75be8bc86f4d"
- // ESP32-C3板载LED
- #define CONFIG_BLINK_LED_RMT_CHANNEL 0
- #define BLINK_GPIO 8
- // 延时时间
- #define TIME_DELAY 500
- // 循环处理参数
- static uint8_t s_loop = 0;
- static uint8_t s_led_idx = 0;
- // LED处理
- static uint8_t s_led_state = 0;
- static led_strip_t *pStrip_a;
- static unsigned long ms = 0;
- // Beacon扫描时间
- int scanTime = 5; //In seconds
- // 延时计数,防止发射端睡眠时被认为掉线
- int delaymax = 30;
- // 蓝牙
- BLEScan *pBLEScan;
- static float dis = 0;
- hw_timer_t *timer = NULL;
- volatile SemaphoreHandle_t timerSemaphore;
- portMUX_TYPE timerMux = portMUX_INITIALIZER_UNLOCKED;
- volatile uint32_t isrCounter = 0;
- volatile uint32_t lastIsrAt = 0;
- // 通过RSSI计算距离
- float calcDistByRSSI(int rssi)
- {
- int iRssi = abs(rssi);
- float power = (iRssi-A)/(10*n);
- return pow(10, power);
- }
- // 定时器设置
- void ARDUINO_ISR_ATTR onTimer() {
- // 定时器触发时,将设置信号量,以便loop部分检测使用
- // Increment the counter and set the time of ISR
- portENTER_CRITICAL_ISR(&timerMux);
- isrCounter++;
- lastIsrAt = millis();
- portEXIT_CRITICAL_ISR(&timerMux);
- // Give a semaphore that we can check in the loop
- xSemaphoreGiveFromISR(timerSemaphore, NULL);
- // It is safe to use digitalRead/Write here if you want to toggle an output
- }
- // 点亮LED
- static void blink_led(void) {
- /* If the addressable LED is enabled */
- if (s_led_state) {
- Serial.print("LED ONn");
- /* Set the LED pixel using RGB from 0 (0%) to 255 (100%) for each color */
- if(s_led_idx>0) {
- // 正常状态为绿色
- pStrip_a->set_pixel(pStrip_a, 0, 0, 32, 0);
- } else {
- // 白色
- pStrip_a->set_pixel(pStrip_a, 0, 32, 32, 32);
- }
- /* Refresh the strip to send data */
- pStrip_a->refresh(pStrip_a, 100);
- } else {
- Serial.print("LED OFFn");
- /* Set all LED off to clear all pixels */
- // pStrip_a->clear(pStrip_a, 50);
- // 红色
- pStrip_a->set_pixel(pStrip_a, 0, 32, 0, 0);
- pStrip_a->refresh(pStrip_a, 100);
- }
- }
- // 设置LED
- static void configure_led(void) {
- Serial.print("configured to blink addressable LED!");
- pStrip_a = led_strip_init(CONFIG_BLINK_LED_RMT_CHANNEL, BLINK_GPIO, 1);
- pStrip_a->clear(pStrip_a, 50);
- }
- // 蓝牙发现处理
- class MyAdvertisedDeviceCallbacks : public BLEAdvertisedDeviceCallbacks {
- void onResult(BLEAdvertisedDevice advertisedDevice) {
- if (advertisedDevice.haveName()) {
- Serial.print("Device name: ");
- Serial.println(advertisedDevice.getName().c_str());
- Serial.println("");
- }
- if (advertisedDevice.haveServiceUUID()) {
- return;
- // BLEUUID devUUID = advertisedDevice.getServiceUUID();
- // Serial.print("Found ServiceUUID: ");
- // Serial.println(devUUID.toString().c_str());
- // Serial.println("");
- } else {
- if (advertisedDevice.haveManufacturerData() == true) {
- std::string strManufacturerData = advertisedDevice.getManufacturerData();
- uint8_t cManufacturerData[100];
- strManufacturerData.copy((char *)cManufacturerData, strManufacturerData.length(), 0);
- // iBeacon帧检测
- if (strManufacturerData.length() == 25 && cManufacturerData[0] == 0x4C && cManufacturerData[1] == 0x00) {
- Serial.println("Found an iBeacon!");
- BLEBeacon oBeacon = BLEBeacon();
- oBeacon.setData(strManufacturerData);
- Serial.printf("iBeacon Framen");
- dis = calcDistByRSSI(advertisedDevice.getRSSI());
- Serial.printf("ID: %04X Major: %d Minor: %d UUID: %s Power: %d RSSI: %d Dis:%0.3fn", oBeacon.getManufacturerId(), ENDIAN_CHANGE_U16(oBeacon.getMajor()), ENDIAN_CHANGE_U16(oBeacon.getMinor()), oBeacon.getProximityUUID().toString().c_str(), oBeacon.getSignalPower(), advertisedDevice.getRSSI(), dis);
- // 目标SSID检测
- if (strcmp(oBeacon.getProximityUUID().toString().c_str(), IBEACON_MY_SSID) == 0) {
- Serial.printf("********iBeacon ssid found********n");
- s_led_idx = delaymax;
- }
- } else {
- return;
- // Serial.println("Found another manufacturers beacon!");
- // Serial.printf("strManufacturerData: %d ", strManufacturerData.length());
- // for (int i = 0; i < strManufacturerData.length(); i++) {
- // Serial.printf("[%X]", cManufacturerData[i]);
- // }
- // Serial.printf("n");
- }
- }
- }
- return;
- // uint8_t *payLoad = advertisedDevice.getPayload();
- // BLEUUID checkUrlUUID = (uint16_t)0xfeaa;
- // if (advertisedDevice.getServiceUUID().equals(checkUrlUUID)) {
- // if (payLoad[11] == 0x10) {
- // Serial.println("Found an EddystoneURL beacon!");
- // BLEEddystoneURL foundEddyURL = BLEEddystoneURL();
- // std::string eddyContent((char *)&payLoad[11]); // incomplete EddystoneURL struct!
- // foundEddyURL.setData(eddyContent);
- // std::string bareURL = foundEddyURL.getURL();
- // if (bareURL[0] == 0x00) {
- // size_t payLoadLen = advertisedDevice.getPayloadLength();
- // Serial.println("DATA-->");
- // for (int idx = 0; idx < payLoadLen; idx++) {
- // Serial.printf("0x%08X ", payLoad[idx]);
- // }
- // Serial.println("nInvalid Data");
- // return;
- // }
- // Serial.printf("Found URL: %sn", foundEddyURL.getURL().c_str());
- // Serial.printf("Decoded URL: %sn", foundEddyURL.getDecodedURL().c_str());
- // Serial.printf("TX power %dn", foundEddyURL.getPower());
- // Serial.println("n");
- // } else if (payLoad[11] == 0x20) {
- // Serial.println("Found an EddystoneTLM beacon!");
- // BLEEddystoneTLM foundEddyURL = BLEEddystoneTLM();
- // std::string eddyContent((char *)&payLoad[11]); // incomplete EddystoneURL struct!
- // eddyContent = "01234567890123";
- // for (int idx = 0; idx < 14; idx++) {
- // eddyContent[idx] = payLoad[idx + 11];
- // }
- // foundEddyURL.setData(eddyContent);
- // Serial.printf("Reported battery voltage: %dmVn", foundEddyURL.getVolt());
- // Serial.printf("Reported temperature from TLM class: %.2fCn", (double)foundEddyURL.getTemp());
- // int temp = (int)payLoad[16] + (int)(payLoad[15] << 8);
- // float calcTemp = temp / 256.0f;
- // Serial.printf("Reported temperature from data: %.2fCn", calcTemp);
- // Serial.printf("Reported advertise count: %dn", foundEddyURL.getCount());
- // Serial.printf("Reported time since last reboot: %dsn", foundEddyURL.getTime());
- // Serial.println("n");
- // Serial.print(foundEddyURL.toString().c_str());
- // Serial.println("n");
- // }
- // }
- }
- };
- void setup() {
- Serial.begin(115200);
- Serial.println("Scanning...");
- // 配置LED
- configure_led();
- // 创建信号量
- timerSemaphore = xSemaphoreCreateBinary();
- // 定时器设置
- timer = timerBegin(0, 80, true);
- timerAttachInterrupt(timer, &onTimer, true);
- timerAlARMWrite(timer, 1000 * 1000, true);
- timerAlarmEnable(timer);
- // 蓝牙初始化
- BLEDevice::init("");
- pBLEScan = BLEDevice::getScan(); //create new scan
- pBLEScan->setAdvertisedDeviceCallbacks(new MyAdvertisedDeviceCallbacks());
- pBLEScan->setActiveScan(true); //active scan uses more power, but get results faster
- pBLEScan->setInterval(100);
- pBLEScan->setWindow(99); // less or equal setInterval value
- }
- void loop() {
- ms = millis();
- // put your main code here, to run repeatedly:
- if (s_loop==0) {
- // 蓝牙扫描
- BLEScanResults foundDevices = pBLEScan->start(scanTime, false);
- Serial.print("Devices found: ");
- Serial.println(foundDevices.getCount());
- Serial.println("Scan done!");
- pBLEScan->clearResults(); // delete results fromBLEScan buffer to release memory
- }
- s_loop++;
- if(s_loop==8) {
- // 每循环8次,进行1次扫描
- s_loop = 0;
- }
- // If Timer has fired
- if (xSemaphoreTake(timerSemaphore, 0) == pdTRUE) {
- // 状态和延时次数处理
- if (s_led_idx > 0) {
- s_led_idx--;
- }
- if (s_led_idx > 0) {
- // 检测到的情况下,常亮绿色
- s_led_state = 1;
- } else {
- // 没有检测到的情况下,红白切换
- s_led_state = !s_led_state;
- }
- uint32_t isrCount = 0, isrTime = 0;
- // Read the interrupt count and time
- portENTER_CRITICAL(&timerMux);
- isrCount = isrCounter;
- isrTime = lastIsrAt;
- portEXIT_CRITICAL(&timerMux);
- // Print it
- Serial.print("onTimer no. ");
- Serial.print(isrCount);
- Serial.print(" at ");
- Serial.print(isrTime);
- Serial.print(" ms");
- Serial.print(" loop:");
- Serial.print(s_loop);
- Serial.print(" idx:");
- Serial.print(s_led_idx);
- Serial.print(" state:");
- Serial.print(s_led_state);
- Serial.println(" ");
- blink_led();
- // timerWrite(timer, 0);
- }
- // 延时处理,去掉本轮处理已经用掉 的时间
- ms = millis() - ms;
- if(ms>TIME_DELAY) {
- ms = 0;
- } else {
- ms = TIME_DELAY - ms;
- }
- delay(ms);
- }
以上代码中,需要使用led_strip.h,在附件中一并提供。
在标准示例代码的基础上,我们添加了几个部分:
1)LED的处理,包括设置和点亮处理;当检测到对应的设备后,常亮绿色显示,否则红白交替显示。对应的函数为configure_led()、blink_led(),用于设置和控制板载3色LED
2)定时器的处理:我们需要做LED交替显示,同时又要进行蓝牙Beacon扫描,所以通过定时器配合,来进行信号量的设置,从而在loop中,根据信号量的情况,来进行LED的控制。对应的函数为onTimer()
3)RSSI测距基本算法,对应的函数为calcDistByRSSI()
这个代码中,做了详细的注释,大家可以仔细查看。
在Arduino IDE的操作步骤如下:
选择开发版:
选择串口和设置:
编译下载:
运行监控:
上述运行解控界面中,如下部分,显示了搜索到的iBeacon信息:
- Found an iBeacon!
- iBeacon Frame
- ID: 004C Major: 0 Minor: 91 UUID: 8ec76ea3-6668-48da-9866-75be8bc86f4d Power: 0 RSSI: -30 Dis:0.016
- ********iBeacon ssid found********
其中,Dis就表示位置信息。
在本部分的代码中,包含如下的经验设置和距离计算函数:
- // RSSI经验参数
- #define A 66
- #define n 2.0
这两个经验值,可以通过运行时,上述iBeacon Frame值来设置。1)将设备相距1米放置,获取此时的rssi值,设为A
2)通过在电脑上,利用上述函数,调整n的值,使得计算结果接近1
3)然后将设备分别放置0.5米和2米,获取rssi值,并且调整n,使得结果接近距离值
以上需要多次取样,进行计算,以使得结果尽可能的准确。
4. ESP32中断与定时器
。。。
ESP32 芯片提供两组硬件定时器,每组包含两个通用硬件定时器。
通过下面的代码,可以很容易使用定时器:
- // 定时器设置
- void ARDUINO_ISR_ATTR onTimer() {
- isrCounter++;
- //定时器逻辑处理,可以写自己的代码
- }
- //setup中设置
- // 定时器设置
- timer = timerBegin(0, 80, true);
- timerAttachInterrupt(timer, &onTimer, true);
- timerAlarmWrite(timer, 1000 * 1000, true);
- timerAlarmEnable(timer);
因为Ardunio中,ESP32定时器的处理,设计到中断,因此,在定时器触发后,处理逻辑需要快准狠,不能调用任何会阻塞 CPU 的 API,否则会导致混乱。因此,上面的代码中,我们通过在定时触发处理逻辑中设置信号量,然后在loop逻辑中进行检测和处理,避免了对中断的影响。
5. ESP32-C3板载三色LED颜色显示
ESP32-C3板载LED的显示,需要通过pixel方式进行处理,其基本代码来源于ESP-IDF中的例子。
主要的调用方式如下:
- #include "led_strip.h"
- // RMT通道
- #define CONFIG_BLINK_LED_RMT_CHANNEL 0
- // GPIO针脚
- #define BLINK_GPIO 8
- // 定义处理句柄
- static led_strip_t *pStrip_a;
- // 初始化
- pStrip_a = led_strip_init(CONFIG_BLINK_LED_RMT_CHANNEL, BLINK_GPIO, 1);
- // 清除显示
- pStrip_a->clear(pStrip_a, 50);
- // 设置颜色
- /* Set the LED pixel using RGB from 0 (0%) to 255 (100%) for each color */
- pStrip_a->set_pixel(pStrip_a, 0, 32, 0, 0);
- pStrip_a->set_pixel(pStrip_a, 0, 0, 32, 0); //绿色
- // 刷新显示
- pStrip_a->refresh(pStrip_a, 100);
6. 扩展研究:
在本文中,我们只使用了1块iBeacon发射设备,并且使用了最基础的RSSI距离算法。在实际应用中,会布置多个iBacon设备,从而同时通过多个设备,来提高定位精度。以及使用更为优化的算法,来提高精度减小误差。
7. 实际效果:见视频