前面章节:html
目录:linux
前言:android
咱们整个基于蓝牙beacon的办公室定位系统主要有两部分组成:git
上一节咱们讲解了如何将数据经过ESP32上传到云端,本节主要讲如何用ESP32扫描周边蓝牙设备。github
蓝牙就在咱们身边:电子信标引导消防员穿过建筑物; 可穿戴医疗设备将患者的生物数据发送给医生的平板电脑; 40万平方英尺仓库的设备监控等。蓝牙技术正在蓬勃发展,预计到2021年将有超过480亿的安装基数(per ABI Internet of Everything Market Tracker)。web
那么蓝牙是如何工做的呢?BLE(蓝牙低功耗) 在2.4GHz的ISM频段中有40个物理信道,每一个信道之间相隔2MHz。蓝牙定义了两种传输类型:数据传输和广播传输。所以,这40个频道中有3个专门用于广播,37个专门用于数据。windows
广播主要会涉及下面几个参数:api
Advertising Parameter | Description | Range |
---|---|---|
Advertising Interval | Time between the start of two consecutive advertising events | 20ms to 10.24s |
Advertising Types | Different PDUs are sent for different types of advertising | See following |
Advertising Channels | Legacy advertising packets are sent on three channels | Different combinations of channels 37, 38 and 39. |
通常状况下,广播信道有channel 37 (2402 MHz), channel 38 (2426 MHz), and channel 39 (2480 MHz)。设备能够在其中一个、两个或三个上进行广播,下图展现了在全部三个频道上进行广播的事件:bash
注意,上列中是在全部通道上都发送了相同的数据(ADV_IND)。因为数据包很是小(广播数据不超过31字节),发送它所需的时间不到10毫秒。设备能够修改成仅在选定的频道上进行广播。在较少的频道上进行广播将节省电力,可是使用更多的频道将增长对等设备接收数据包的可能性。用户能够根据应用程序用例配置广播间隔。例如,若是门锁以较慢的间隔进行广播,则对等设备链接到门锁将须要更长的时间,这将对用户体验产生不利影响。微信
不管是beacon(传输位置、天气或其余数据)仍是与主机(平板电脑或手机)创建长期链接的健身手表,全部外围设备,至少在最初都是以广播模式开始的。
Advertising容许设备去广播有意图的信息。
那么,蓝牙的广播是怎样的呢?
为了便于使用,蓝牙为广播和数据传输定义了一种单一的数据包格式。这个包由四个部分组成:前导码(1字节)、访问地址(4字节)、协议数据单元(2-257字节)和循环冗余校验(3字节);见下图:
PDU部分比较重要,由于它定义了该数据包是广播包仍是数据包。在咱们解析来的讨论中,将重点讨论广播PUD包。
广播PUD包包含16 bits 的头和不定长度的payload:
广播的头部包含6部分,咱们主要关注Length和PUD Type两部分。Length长6bits,定义了payload的长度。Length的取值范围是6-27字节(取决于PUD Type)。
OK,如今咱们知道了广播的时候会有几字节的16进制数据在payload中,可是为何广播呢?这就要提到PUD Type了。在蓝牙低功耗中,有两个缘由须要广播:
所以,不管是智能手表仍是木乃伊都在争夺关注,咱们开发人员则须要关注4种PDU类型:
因此,当须要维持长期链接时,PDU的类型应设置为ADV_IND或ADV_DIRECT_IND;当只是广播一些信息,不须要维持长期链接时,ADV_NONCONN_IND和ADV_SCAN_IND将会被用上,beacon经常使用ADV_NONCONN_IND,当须要广播更多信息的时候,能够把信息放在scan回复中,选用ADV_SCAN_IND。
不管是请求长期链接仍是做为beacon,这一切都始于广播。
当BLE设备未被链接时,能够经过发送广播包来宣传它们的存在,或者扫描附近正在广播的设备。扫描设备的过程被成为设备发现。扫描有两种类型:主动扫描和被动扫描。区别在于:主动扫描器能够主动发送一个扫描请求,请求广播设备进行广播回复;而被动扫描器只能被动扫描广播信息。下图显示了扫描器在广播事件期间向广广播客户发送扫描请求的时序:
当涉及到扫描时间时,您须要熟悉一些参数。每一个参数都有一个由蓝牙核心规范指定的范围。帧间时隙(T_IFS)是同一信道上两个连续数据包之间的时间间隔,由BLE规范设置为150us。
Scan Parameter | Description | Range |
---|---|---|
Scan Interval | The interval between the start of two consecutive scan windows | 10ms to 10.24s |
Scan Window | The duration in which the Link Layer scans on one channel | 10ms to 10.24s |
Scan Duration | The duration in which the device stays in the scanning state | 10ms to infinity |
下图展现了这些参数的关系:
请注意,扫描通道的顺序是固定的。设备将分别在通道37(2402MHz)、通道38(2426MHz)和通道39(2480MHz)上进行扫描,并按照扫描窗口定义的时间长度在每一个扫描间隔上进行扫描。
二级广播信道上的可扫描广播包也能够引起扫描请求和扫描响应。这些被称为AUX_SCAN_REQ and AUX_SCAN_RSP。下表总结了全部与扫描相关的数据包:
Scanning PDU | Transmitting device | Payload |
---|---|---|
SCAN_REQ | Scanner | Scanner's address + advertiser's address |
SCAN_RSP | Advertiser | Advertiser's address + 0-31 bytes scan response data |
AUX_SCAN_REQ | Scanner | Scanner's address + advertiser's address |
AUX_SCAN_RSP | Advertiser | Header + 0-254 bytes data |
蓝牙广播常见的应用有:beacon、室内定位、靠近开门、广播小数据信息等,维基百科[3]上的总结有以下场景:
注:蓝牙5的定位、广播将更具诱人特性。
ESP32是一款2.4 GHz Wi-Fi和蓝牙组合芯片,采用TSMC超低功耗40纳米技术设计。它的设计是为了得到最佳的功率和射频性能,在各类应用和电源方案中显示出鲁棒性、通用性和可靠性。
ESP32 系列芯片包括:ESP32-D0WDQ6, ESP32-D0WD, ESP32-D2WD, and ESP32-S0WD。其架构图以下:
咱们实验用了乐鑫官方的一个开发板:ESP32-WROOM-32。该开发板是一款功能强大的通用Wi-Fi+BT+BLE MCU模块,面向各类应用,从低功耗传感器网络到最苛刻的任务,如语音编码、音乐流和MP3解码。
该模块采用EP32-D0WDQ6芯片,该芯片是双核芯片、可独立控制,时钟频率能够从80MHz ~ 240MHz。用户还能够关闭CPU电源,并利用低功耗协处理器持续监控外围设备的变化或是否超过阈值。ESP32集成了一套丰富的外围设备,包括电容式触摸传感器、霍尔传感器、SD卡接口、以太网、高速SPI、UART、I2s和I2c。
集成了蓝牙、BLE和Wi-Fi,表明着将来:使用WIFI可经过路由器链接到互联网,而使用蓝牙则方便用户链接到手机和低功耗。ESP32芯片的休眠电流小于5uA,所以适用于电池供电和可穿戴电子设备应用。ESP32支持高达150 Mbps的数据速率和20.5 dBm的输出功率,以确保最宽的物理范围。所以,该芯片确实为电子集成、范围、功耗和链接性提供了行业领先的规格和最佳性能。
ESP32可选的操做系统是freeRTOS with LwIP + TLS 1.2 + 硬件内部加速 + 加密的OTA技术。下表是ESP32-WROOM-32的资源总览:
经过下面两个资料,你们能够自行搭建环境:
SDK介绍:对于ESP32乐鑫官方提供了一个IDF :
环境搭建:若是你想本身搭建开发环境,参见乐鑫官方资料:
不过!做为系统洁癖和拒绝重复造轮子的博主,已经写了一个全自动构建环境的脚本、并把该工具在github上开源了:esp32_linux_tool [13]
注:nbtool是博主专门放本身造的或收集到的牛逼轮子的github组
博主造的这个轮子比较好用,基于all-in-one思想(全部相关文件在一个文件夹下;全部相关环境变量不须要额外配置):
#!/bin/bash set -e PROJECT_ROOT=.. TOOLS_PATH=$PROJECT_ROOT/tool SDK_PATH=$PROJECT_ROOT/sdk APP_PATH=$PROJECT_ROOT/app XTENSA_ESP32_ELF_PATH=$TOOLS_PATH/xtensa-esp32-elf ESP_IDF_PATH=$SDK_PATH/esp-idf XTENSA_ESP32_ELF_LINK=https://dl.espressif.com/dl/xtensa-esp32-elf-linux64-1.22.0-80-g6c4433a-5.2.0.tar.gz ESP_IDF_LINK=https://github.com/espressif/esp-idf.git #-------------------------------------------------------------------------- function install_tool_chain(){ echo "> install tool chain ..." echo "> web page: https://docs.espressif.com/projects/esp-idf/zh_CN/v3.1.1/get-started/linux-setup.html" if [ ! -d $XTENSA_ESP32_ELF_PATH ]; then wget $XTENSA_ESP32_ELF_LINK tar -xzf xtensa-esp32-elf*.tar.gz rm xtensa-esp32-elf*.tar.gz fi } function install_esp_idf(){ echo "> install esp idf ..." echo "> web page: https://github.com/espressif/esp-idf" if [ ! -d $ESP_IDF_PATH ]; then git clone $ESP_IDF_LINK mv esp-idf $SDK_PATH/ fi } function create_project(){ if [ "$1" == "" ] || [ "$2" == "" ]; then echo "input error" elif [ -d $1 ] && [ ! -d "$APP_PATH/$2" ]; then cp -r $1 $APP_PATH/$2 file=$APP_PATH/$2/run.sh the_sdk_path=`cd $ESP_IDF_PATH; pwd` the_tool_chain_path=`cd $XTENSA_ESP32_ELF_PATH/bin; pwd` cat > $file <<EOF #!/bin/bash #I don't like to set environment variables in the system, #so I put the environment variables in run.sh. #Every time I use run.sh, the enviroment variables will be set, after use that will be unsetted. PROJECT_ROOT=../.. TOOLS_PATH=\$PROJECT_ROOT/tool SDK_PATH=\$PROJECT_ROOT/sdk APP_PATH=\$PROJECT_ROOT/app XTENSA_ESP32_ELF_PATH=\$TOOLS_PATH/xtensa-esp32-elf ESP_IDF_PATH=\$SDK_PATH/esp-idf the_sdk_path=\`cd \$ESP_IDF_PATH; pwd\` the_tool_chain_path=\`cd \$XTENSA_ESP32_ELF_PATH/bin; pwd\` export PATH="\$PATH:\$the_tool_chain_path" export IDF_PATH="\$the_sdk_path" if [ "\$1" == "config" ]; then make menuconfig elif [ "\$1" == "build" ]; then make all elif [ "\$1" == "flash" ]; then make flash elif [ "\$1" == "build-app" ]; then make app elif [ "\$1" == "flash-app" ]; then make app-flash elif [ "\$1" == "monitor" ]; then make monitor elif [ "\$1" == "clean" ]; then make clean elif [ "\$1" == "help" ]; then echo "bash run.sh config" echo " |- basic configuration by GUI, if we use -j4 to build and flash, we must first config then build or flash!!!" echo "bash run.sh build" echo " |- build all" echo "bash run.sh flash" echo " |- build all and flash the program" echo "bash run.sh build-app" echo " |- just build app, not build bootloader and partition table" echo "bash run.sh flash-app" echo " |- just flash app, when bootloader and partition table have not changed, no need to flash" echo " |- more infomation:https://docs.espressif.com/projects/esp-idf/zh_CN/v3.1.1/get-started/make-project.html" echo "bash run.sh monitor" echo " |- monitor the program, 'Ctrl+]' to stop" echo " |- IDF Monitor:https://docs.espressif.com/projects/esp-idf/zh_CN/v3.1.1/get-started/idf-monitor.html" else echo "error, try bash run.sh help" fi EOF chmod +x $file ls -all $APP_PATH/$2 fi } #-------------------------------------------------------------------------- function tool(){ if [ ! -d $SDK_PATH ]; then mkdir $SDK_PATH fi if [ ! -d $APP_PATH ]; then mkdir $APP_PATH fi install_tool_chain install_esp_idf } function clean(){ echo "cleaning ...." rm -rf $XTENSA_ESP32_ELF_PATH rm -rf $ESP_IDF_PATH rm -rf $SDK_PATH } if [ "$1" == "clean" ]; then clean elif [ "$1" == "tool" ]; then tool elif [ "$1" == "create" ]; then create_project $2 $3 elif [ "$1" == "help" ]; then echo "bash run.sh tool" echo " |- create the build enviroment, including sdk and tool chain" echo "bash run.sh clean" echo " |- clean all the sdk and tools, thats download form web-page when 'bash run.sh tool'" echo "bash run.sh create path_of_example_in_sdk new_name_project" echo " |- copy the example in the sdk to app directory, and rename it new_name_project" else echo "error, try bash run.sh help" fi
上面的run.sh脚本就是完成开发环境构建、工程建立、编译、烧写、跟踪LOG等复杂功能,你们能够慢慢理解。下面先谈谈如何用该开源项目:
#克隆项目到本地 > git clone git@github.com:nbtool/esp32_linux_tool.git #构建esp32开发环境 > cd ./esp32_linux_tool/tool > ./run.sh help > ./run.sh tool #从SDK的example中复制一个DEMO到APP层(例如:hello_world) > bash run.sh create ../sdk/esp-idf/examples/get-started/hello_world hello_world > cd ../app/hello_world > ./run.sh help #烧写固件 > ./run.sh flash #查看LOG > ./run.sh monitor #清空工程 > ./run.sh clean
因为ESP32的IDF中已经有蓝牙扫描的DEMO,所以咱们用下面命令直接从DEMO建立工程:
bash run.sh create ../sdk/esp-idf/examples/bluetooth/bt_discovery bt_discovery
以后将 ./app/bt_discovery/main/bt_discovery.c 修改成:
#include <stdint.h> #include <string.h> #include "freertos/FreeRTOS.h" #include "freertos/task.h" #include "nvs.h" #include "nvs_flash.h" #include "esp_system.h" #include "esp_log.h" #include "esp_bt.h" #include "esp_bt_main.h" #include "esp_bt_device.h" #include "esp_gap_bt_api.h" #define GAP_TAG "GAP" typedef enum { APP_GAP_STATE_IDLE = 0, APP_GAP_STATE_DEVICE_DISCOVERING, APP_GAP_STATE_DEVICE_DISCOVER_COMPLETE, } app_gap_state_t; typedef struct { bool dev_found; uint8_t bdname_len; uint8_t eir_len; uint8_t rssi; uint32_t cod; uint8_t eir[ESP_BT_GAP_EIR_DATA_LEN]; uint8_t bdname[ESP_BT_GAP_MAX_BDNAME_LEN + 1]; esp_bd_addr_t bda; app_gap_state_t state; } app_gap_cb_t; static app_gap_cb_t m_dev_info; static char *bda2str(esp_bd_addr_t bda, char *str, size_t size) { if (bda == NULL || str == NULL || size < 18) { return NULL; } uint8_t *p = bda; sprintf(str, "%02x:%02x:%02x:%02x:%02x:%02x", p[0], p[1], p[2], p[3], p[4], p[5]); return str; } static void update_device_info(esp_bt_gap_cb_param_t *param) { char bda_str[18]; uint32_t cod = 0; int32_t rssi = -129; /* invalid value */ esp_bt_gap_dev_prop_t *p; ESP_LOGI(GAP_TAG, "Device found: %s", bda2str(param->disc_res.bda, bda_str, 18)); for (int i = 0; i < param->disc_res.num_prop; i++) { p = param->disc_res.prop + i; switch (p->type) { case ESP_BT_GAP_DEV_PROP_COD: cod = *(uint32_t *)(p->val); ESP_LOGI(GAP_TAG, "--Class of Device: 0x%x", cod); break; case ESP_BT_GAP_DEV_PROP_RSSI: rssi = *(int8_t *)(p->val); ESP_LOGI(GAP_TAG, "--RSSI: %d", rssi); break; case ESP_BT_GAP_DEV_PROP_BDNAME: default: break; } } /* search for device with MAJOR service class as "rendering" in COD */ app_gap_cb_t *p_dev = &m_dev_info; if (p_dev->dev_found && 0 != memcmp(param->disc_res.bda, p_dev->bda, ESP_BD_ADDR_LEN)) { return; } if (!esp_bt_gap_is_valid_cod(cod) || !(esp_bt_gap_get_cod_major_dev(cod) == ESP_BT_COD_MAJOR_DEV_PHONE)) { return; } memcpy(p_dev->bda, param->disc_res.bda, ESP_BD_ADDR_LEN); p_dev->dev_found = true; for (int i = 0; i < param->disc_res.num_prop; i++) { p = param->disc_res.prop + i; switch (p->type) { case ESP_BT_GAP_DEV_PROP_COD: p_dev->cod = *(uint32_t *)(p->val); break; case ESP_BT_GAP_DEV_PROP_RSSI: p_dev->rssi = *(int8_t *)(p->val); break; case ESP_BT_GAP_DEV_PROP_BDNAME: { uint8_t len = (p->len > ESP_BT_GAP_MAX_BDNAME_LEN) ? ESP_BT_GAP_MAX_BDNAME_LEN : (uint8_t)p->len; memcpy(p_dev->bdname, (uint8_t *)(p->val), len); p_dev->bdname[len] = '\0'; p_dev->bdname_len = len; break; } case ESP_BT_GAP_DEV_PROP_EIR: { memcpy(p_dev->eir, (uint8_t *)(p->val), p->len); p_dev->eir_len = p->len; break; } default: break; } } } void bt_app_gap_init(void) { app_gap_cb_t *p_dev = &m_dev_info; memset(p_dev, 0, sizeof(app_gap_cb_t)); } void bt_app_gap_cb(esp_bt_gap_cb_event_t event, esp_bt_gap_cb_param_t *param) { app_gap_cb_t *p_dev = &m_dev_info; switch (event) { case ESP_BT_GAP_DISC_RES_EVT: { update_device_info(param); break; } case ESP_BT_GAP_DISC_STATE_CHANGED_EVT: { ESP_LOGE(GAP_TAG, "%d", p_dev->state); if(p_dev->state == APP_GAP_STATE_IDLE){ ESP_LOGE(GAP_TAG, "discovery start ..."); p_dev->state = APP_GAP_STATE_DEVICE_DISCOVERING; }else if(p_dev->state == APP_GAP_STATE_DEVICE_DISCOVERING){ ESP_LOGE(GAP_TAG, "discovery timeout ..."); p_dev->state = APP_GAP_STATE_DEVICE_DISCOVER_COMPLETE; esp_bt_gap_start_discovery(ESP_BT_INQ_MODE_GENERAL_INQUIRY, 10, 0); }else{ ESP_LOGE(GAP_TAG, "discovery again ..."); p_dev->state = APP_GAP_STATE_IDLE; } break; } case ESP_BT_GAP_RMT_SRVCS_EVT: { break; } case ESP_BT_GAP_RMT_SRVC_REC_EVT: default: { break; } } return; } void bt_app_gap_start_up(void) { char *dev_name = "ESP_GAP_INQRUIY"; esp_bt_dev_set_device_name(dev_name); /* set discoverable and connectable mode, wait to be connected */ esp_bt_gap_set_scan_mode(ESP_BT_SCAN_MODE_CONNECTABLE_DISCOVERABLE); /* register GAP callback function */ esp_bt_gap_register_callback(bt_app_gap_cb); /* inititialize device information and status */ app_gap_cb_t *p_dev = &m_dev_info; memset(p_dev, 0, sizeof(app_gap_cb_t)); /* start to discover nearby Bluetooth devices */ p_dev->state = APP_GAP_STATE_IDLE; esp_bt_gap_start_discovery(ESP_BT_INQ_MODE_GENERAL_INQUIRY, 10, 0); } void app_main() { /* Initialize NVS — it is used to store PHY calibration data */ esp_err_t ret = nvs_flash_init(); if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) { ESP_ERROR_CHECK(nvs_flash_erase()); ret = nvs_flash_init(); } ESP_ERROR_CHECK( ret ); ESP_ERROR_CHECK(esp_bt_controller_mem_release(ESP_BT_MODE_BLE)); esp_bt_controller_config_t bt_cfg = BT_CONTROLLER_INIT_CONFIG_DEFAULT(); if ((ret = esp_bt_controller_init(&bt_cfg)) != ESP_OK) { ESP_LOGE(GAP_TAG, "%s initialize controller failed: %s\n", __func__, esp_err_to_name(ret)); return; } if ((ret = esp_bt_controller_enable(ESP_BT_MODE_CLASSIC_BT)) != ESP_OK) { ESP_LOGE(GAP_TAG, "%s enable controller failed: %s\n", __func__, esp_err_to_name(ret)); return; } if ((ret = esp_bluedroid_init()) != ESP_OK) { ESP_LOGE(GAP_TAG, "%s initialize bluedroid failed: %s\n", __func__, esp_err_to_name(ret)); return; } if ((ret = esp_bluedroid_enable()) != ESP_OK) { ESP_LOGE(GAP_TAG, "%s enable bluedroid failed: %s\n", __func__, esp_err_to_name(ret)); return; } bt_app_gap_start_up(); }
逐层调用关系:
函数 | 符号 | 执行任务 |
---|---|---|
app_main | -> | 各类初始化,最后调用 bt_app_gap_start_up |
bt_app_gap_start_up | -o | 初始化蓝牙并启动搜索,超时10S,回调事件会被 bt_app_gap_cb 捕捉 |
bt_app_gap_cb | o-> | 开始搜索/搜索超时/再次搜索+搜索到设备事件,超时会再次启动10S搜索,搜到设备会调用update_device_info 打印 |
update_device_info | -o | 将搜索结果打印下来 |
注:-> 会继续调用其余函数;-o 中止调用其余函数;o-> 回调函数;
注:周期性扫描,10S超时后继续扫描,扫到以后打印MAC和RSSI
: 完~
: 你们以为不错,能够点推荐给更多人~
: 最近一段时间准备将这个系列写完,作一套可演示的系统(笑)~
[1]. BLOG - 自制蓝牙工牌办公室定位系统 (一)
[2]. SIG - Bluetooth core specification
[3]. WiKi - Bluetooth advertising
[4]. SIG - Bluetooth Low Energy - It starts with Advertising
[5]. TI - Bluetooth Low Energy Scanning and Advertising
[6]. TI - Bluetooth Low Energy Scanning and Advertising
[7]. Android - Bluetooth Low Energy Advertising
[8]. BLOG - Advertising(解説)
[9]. PDF - ESP32 datasheet
[10]. PDF - ESP32-WROOM-32 datasheet
[11]. ESP32-IDF GITHUB地址
[12]. ESP-IDF Program Guide
[13]. esp32_linux_tool GITHUB地址
@beautifulzzzz 以蓝牙技术为基础的的末梢无线网络系统架构及创新型应用探索! 领域:智能硬件、物联网、自动化、前沿软硬件 博客:https://www.cnblogs.com/zjutlitao/ 园友交流群|微信交流群:414948975|园友交流群