前面章節: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|園友交流羣