# 11. 蓝牙 蓝牙是一种短距通信系统,其关键特性包括鲁棒性、低功耗、低成本等。蓝牙系统分为两种不同的技术:经典蓝牙 (Classic Bluetooth) 和蓝牙低功耗 (Bluetooth Low Energy)。Edge101WE 主板支持双模蓝牙,即同时支持经典蓝牙和蓝牙低功耗。从整体结构上,蓝⽛可分为控制器器 (Controller) 和主机 (Host) 两⼤部分:控制器包括了 PHY、Baseband、Link Controller、Link Manager、Device Manager、HCI 等模块,⽤于硬件接口管理、链路管理等;主机则包括了 L2CAP、SMP、SDP、ATT、GATT、GAP 以及各种规范,构建了向应用层提供接口的基础,方便便应用层对蓝牙系统的访问。主机可以与控制器运行在同⼀个宿主上,也可以分布在不同的宿主上。Edge101WE 主板可以支持上述两种方式。 ## 11.1 Bluetooth Classic 经典蓝牙(BluetoothSerial 蓝牙串口) BluetoothSerial蓝牙串口,可使用经典蓝牙方式在电脑或者手机上识别到一个串口设备,可通过此串口设备对主板的参数进行配置,或用于其他收发数据的场景。 ### API参考 #### available() - 获取可以从蓝牙串行端口读取的字节数 **语法** ```c++ #include "BluetoothSerial.h" int BluetoothSerial::available(void); ``` **参数** 无 **返回** | 返回值 | 说明 | 值范围 | | :----: | :------------- | ------ | | int | 可读取的字节数 | | #### begin() - 启动蓝牙串行设备 注册蓝牙设备的本地名称,然后启动蓝牙串行设备。 **语法** ```c++ #include "BluetoothSerial.h" bool BluetoothSerial::begin(String localName, bool isMaster); ``` **参数** | 传入值 | 说明 | 值范围 | | :-------: | :----------------------------------: | :----: | | localName | 本地名称。如果省略,则设置String() | | | isMaster | 是Master吗?默认为false。 | | **返回** | 返回值 | 说明 | 值范围 | | :----: | :-------------------------------------- | ------ | | bool | true:启动成功。
false:启动不成功 | | #### read() - 读取从蓝牙串行端口接收的数据 **语法** ```c++ #include "BluetoothSerial.h" int BluetoothSerial::read(void); ``` **参数** 无 **返回** | 返回值 | 说明 | 值范围 | | :----: | :-------------------------------------------------- | ------ | | int | 接收到的数据的第一个字节。如果没有收到数据,则为0。 | | #### write() - 读取从蓝牙串行端口接收的数据 **语法** ```c++ #include "BluetoothSerial.h" size_t BluetoothSerial::write(uint8_t c); size_t BluetoothSerial::write(const uint8_t *buffer, size_t size); ``` **参数** | 传入值 | 说明 | 值范围 | | :----: | :------------------------: | :----: | | c | 要发送的值(1个字节) | | | buffer | 数据以字节字符串的形式发送 | | | size | 数据大小 | | **返回** | 返回值 | 说明 | 值范围 | | :----: | :----------- | ------ | | size_t | 发送的字节数 | | ### 例程:SerialToSerialBT ### 主板蓝牙工作在Bluetooth Classic模式,生成一个蓝牙串口 (参考Arduino IDE例程 Examples -> Examples for Edge101WE ->BluetoothSerial\examples\SerialToSerialBT) ```c++ //This example code is in the Public Domain (or CC0 licensed, at your option.) //By Evandro Copercini - 2018 // //This example creates a bridge between Serial and Classical Bluetooth (SPP) //and also demonstrate that SerialBT have the same functionalities of a normal Serial #include "BluetoothSerial.h" #if !defined(CONFIG_BT_ENABLED) || !defined(CONFIG_BLUEDROID_ENABLED) #error Bluetooth is not enabled! Please run `make menuconfig` to and enable it #endif BluetoothSerial SerialBT; void setup() { Serial.begin(115200); SerialBT.begin("Edge101WE_Device"); //Bluetooth device name Serial.println("The device started, now you can pair it with bluetooth!"); } void loop() { if (Serial.available()) { SerialBT.write(Serial.read()); } if (SerialBT.available()) { Serial.write(SerialBT.read()); } delay(20); } ``` 在安卓系统手机上安装 Serial Bluetooth Terminal 软件。手机打开蓝牙,搜索到 Edge101WE_Device 这个蓝牙设备连接上后打开 Serial Bluetooth Terminal 软件选择 Edge101WE_Device 设备。连接后可在界面上发送数据到主板,主板串口将打印出手机发送的数据。同时主板也可以通过串口发送数据到手机。 ### 例程:带配对功能的蓝牙串口 (参考Arduino IDE例程 Examples -> Examples for Edge101WE ->BluetoothSerial/examples/SerialToSerialBT_SSP_pairing) ```c++ //This example code is in the Public Domain (or CC0 licensed, at your option.) //By Richard Li - 2020 // //This example creates a bridge between Serial and Classical Bluetooth (SPP with authentication) //and also demonstrate that SerialBT have the same functionalities of a normal Serial #include "BluetoothSerial.h" #if !defined(CONFIG_BT_ENABLED) || !defined(CONFIG_BLUEDROID_ENABLED) #error Bluetooth is not enabled! Please run `make menuconfig` to and enable it #endif BluetoothSerial SerialBT; boolean confirmRequestPending = true; void BTConfirmRequestCallback(uint32_t numVal) { confirmRequestPending = true; Serial.println(numVal); } void BTAuthCompleteCallback(boolean success) { confirmRequestPending = false; if (success) { Serial.println("Pairing success!!"); } else { Serial.println("Pairing failed, rejected by user!!"); } } void setup() { Serial.begin(115200); SerialBT.enableSSP(); SerialBT.onConfirmRequest(BTConfirmRequestCallback); SerialBT.onAuthComplete(BTAuthCompleteCallback); SerialBT.begin("ESP32test"); //Bluetooth device name Serial.println("The device started, now you can pair it with bluetooth!"); } void loop() { if (confirmRequestPending) { if (Serial.available()) { int dat = Serial.read(); if (dat == 'Y' || dat == 'y') { SerialBT.confirmReply(true); } else { SerialBT.confirmReply(false); } } } else { if (Serial.available()) { SerialBT.write(Serial.read()); } if (SerialBT.available()) { Serial.write(SerialBT.read()); } delay(20); } } ``` 打开手机蓝牙,在蓝牙设置页面搜索到 “ESP32test” 蓝牙设备,点击配对,此时配对页面将会显示一个随机的配对码(例程中显示的 586767),同时主板USB串口也会打印出一个配对码,如果配对码相同说明手机当前是在和当前的主板进行配对。通过串口发送字符 ‘Y’ 或 ‘y’ 进行确认。显示配对成功。此时可以通过手机端安装的 Serial Bluetooth Terminal 进行通信。 ![image-20220127161658628](pictures/image-20220127161658628.png) 手机发送数据 ![image-20220127162956010](pictures/image-20220127162956010.png) 串口输出 ``` The device started, now you can pair it with bluetooth! 586767 Pairing success!! This is phone speaking ``` ## 11.2 BLE (Bluetooth Low Energy低功耗蓝牙) 低功耗蓝牙(BLE)是蓝牙的一种节能形式。BLE 的主要应用是少量数据的短距离传输(低带宽)。与始终打开的蓝牙不同,BLE 始终保持睡眠模式,除非启动连接时。这使其功耗非常低。BLE 的功耗比蓝牙低约100倍(取决于使用情况)。 由于其特性,BLE 适用于需要纽扣电池上定期运行的少量数据的应用。例如 BLE 在医疗保健、健身、跟踪、信标、安全和家庭自动化行业中具有很大的用途。 使用低功耗蓝牙,设备有两种:服务器 (Server)和客户端(Client)。Edge101WE 主板既可以充当客户端,也可以充当服务器。服务器通告其存在,以便其他设备可以找到它,并包含客户端可以读取的数据。客户端扫描附近的设备,并在找到要查找的服务器时建立连接并监听传入的数据。这称为点对点通信。 如果想要让主板处于别人随时可以搜索连接的情况要配置为服务端。如果想让主板通过扫描连接周围可连接的蓝牙设备,需要把它设置成客户端,正好和WiFi模式的设定相反。 BLE 通用访问规范 (GAP) 接口 API 的实现和使用流程,GAP 协议层定义了 BLE 设备的发现流程,设备管理和设备接的建立。BLE GAP 协议层采用 API 调用和事件 (Event) 返回的设计模式,通过事件返回来获取 API在协议栈的处理结果。当对端设备主动发起请求时,也是通过事件返回获取对端设备的状态。 BLE 设备定义了四类 GAP 角色: - 广播者 (Broadcaster):处于这种角色的设备通过发送广播 (Advertising) 让接收者发现自己。这种角色只能发广播,不能被连接。 - 观察者 (Observer):处于这种角色的设备通过接收广播事件并发送扫描 (Scan) 请求。这种角色只能发送扫描请求,不能被连接。 - 外围设备 (Peripheral):当广播者接受了观察者发来的连接请求后就会进入这种角色。当设备进入了这种角色之后,将会作为从设备 (Slave) 在链路路中进⾏行行通信。 - 中央设备 (Central):当观察者主动进⾏行行初始化,并建⽴立⼀一个物理理链路路时就会进⼊入这种角色。这种角色在链路路中同样被称为主设备 (Master)。 ### ATT 属性协议 BLE 里面的数据以属性 (Attribute) 方式存在,每条属性由四个元素组成: - **属性句柄 (Attribute Handle)**:正如我们可以使用内存地址查找内存中的内容一样,ATT 属性的句柄也可以协助我们找到相应的属性,例如第一个属性的句柄是0x0001,第二个属性的句柄是 0x0002,以此类推,最大可以到 0xFFFF。 - **属性类型 (Attribute UUID)**:每个数据有自己需要代表的意思,例如表示温度、发射功率、电池等各种各样的信息。蓝牙组织 (Bluetooth SIG) 对常用的一些数据类型进行了归类,赋予不同的数据类型不同的标识码 (UUID)。例例如 0x2A09 表示电池信息,0x2A6E 表示温度信息。UUID 可以是 16 比特的 (16-bit UUID),也可以是 128 比特的 (128-bit UUID)。 - **属性值 (Attribute Value)**:属性值是每个属性真正要承载的信息,其他 3 个元素都是为了让对方能够更好地获取属性值。有些属性的长度是固定的,例例如电池属性(Battery Level) 的长度只有 1 个字节,因为需要表示的数据仅有 0~100%,而 1 个字节足以表示 1~100 的范围;而有些属性的长度是可变的,例如基于 BLE 实现的透传模块。 - **属性许可 (Attribute Permissions)**:每个属性对各自的属性值有相应的访问限制,比如有些属性是可读的、有些是可写的、有些是可读又可写的等。拥有数据的一方可以通过属性许可,控制本地数据的可读写属性。 | 属性句柄 (Attribute Handle) | 属性类型 (Attribute UUID) | 属性值 (Attribute Value) | 属性许可 (Attribute Permissions) | | --------------------------- | ------------------------- | ------------------------ | -------------------------------- | | 0x0001 | 0x2A09 表示电池信息 | 100% | 可读 | | 0x0002 | 0x2A6E 表示温度信息 | 28摄氏度 | 可读 | | ……… | | | | | 0xFFFE | | | | | 0xFFFF | | | | 我们把存有数据(即属性)的设备叫做服务器 (Server),而将获取别人设备数据的设备叫做客户端 (Client)。下面是服务器和客户端间的常用操作: - 客户端给服务端发数据,通过对服务器的数据进行写操作 (Write),来完成数据发送工作。写操作分两种,一种是写入请求 (Write Request),一种是写入命令 (WriteCommand),两者的主要区别是前者需要对方回复响应 (Write Response),而后者不需要对方回复响应。 - 服务端给客户端发数据,主要通过服务端指示 (Indication) 或者通知 (Notification) 的形式,实现将服务端更新的数据发给客户端。与写操作类似,指示和通知的主要区别是前者需要对方设备在收到数据指示后,进行回复 (Confirmation)。 - 客户端也可以主动通过读操作读取服务端的数据。 服务器和客户端之间的交互操作都是通过上述的消息 ATT PDU 实现的。每个设备可以指定自己设备支持的最大 ATT 消息长度,我们称之为 MTU。Edge101WE 主板里面规定 MTU 可以设置的范围是 23~517 字节,对属性值的总长度没有做限制。如果用户需要发送的数据包长度大于 (MTU-3),则需要调用准入写入请求 (Prepare WriteRequest) 来完成数据的写操作。同理理,在读取一个数据时候,如果数据的长度超过 (MTU-1),则需要通过大对象读取请求 (Read Blob Request) 来继续读取剩余的值。 ### GATT 规范 GATT是“通用属性配置文件”(Generic Attribute Profile)的缩写。ATT 属性协议规定了在 BLE 中的最小数据存储单位,而 GATT 规范则定义了一个层次化的数据结构,及其如何用特性值和描述符表示一个数据,如何把相似的数据聚合成服务 (Service),以及如何发现对端设备拥有哪些服务和数据。这意味着 GATT 定义了两个 BLE 设备发送和接收标准消息的方式。 GATT 规范引进了特性值的概念。这是由于在某些时候,一个数据可能并不只是单纯的数值,还会带有一些额外的信息: - 比如这个数据的单位是什么?是重量单位千克 kg、温度单位摄⽒氏度 ℃,还是其他单位; - 比如希望具体告知对方这个数值的名称,例如同为温度属性 UUID 下,希望告知对方该数据表示“主卧温度”,另一个数据表示“客厅温度”; - 比如在表示 230000、460000 等大数据时,可以增加指数信息,告知对方该数据的指数是 10^4,这样仅需在空中传递 23、46 即可。 上述内容仅为清楚描述一个数据众多需求中的几个例子,实际应用中还可能出现其他以各种方式表达的数据需求。为了包含这些信息,每个属性中均需要安排一⼤大段数据空间,存储这些额外信息。然而,一个数据很有可能用不到绝大部分的额外信息,因此这种设计并这不符合 BLE “协议尽可能精简”的要求。在此背景下,GATT 规范引进了描述符的概念,每种描述符可以表达一种意思,用户可使用描述符,描述数据的额外信息。必需说明的是,每个数据和描述符并非一 一对应,即一个复杂的数据可以拥有多个描述符,而一个简单的数据可以没有任何描述符。 数据本身的属性值及其可能携带的描述符,构成了特性 (Characteristic)的概念。数据特性包含以下几个部分: - **特性声明 (Characteristic Declaration)**:主要告诉对方此声明后面跟的内容为特性数值。从当前特性声明开始到下一个特性声明之间的所有句柄 (Handle) 将构成一个完整的特性。此外,特性声明还包括紧跟其后的特性数值的可写可读属性信息。 - **特性数值 (Characteristic Value)**:特性的核心部分,一般紧跟在特性声明后⾯面,承载特性的真正内容。 - **描述符 (Descriptor)**:描述符可以对特性进行进一步描述,每个特性可以有多个描述符,也可以没有描述符。 BLE 协议中会把一些常用的功能定义成一个个的服务 (Service),例如把电池相关的特性和行为定义成电池服务 (Battery Service);把心率测试相关的特性和行为定义成心跳服务(Heart Rate Service);把体重测试相关的特性和行为定义成体重服务 (Weight ScaleService)。可以看到,每个服务包含若干个特性,每个特性包含若干个描述符。用户可以根据自己的应用需求选择需要的服务,并组成最终的产品应用。 一个完整服务的特性定义参考如下: | 属性句柄 | 属性类型 | | -------- | ---------- | | 0x0001 | 服务 1 | | 0x0002 | 特性声明 1 | | 0x0003 | 特性数值 1 | | 0x0004 | 描述符 1 | | 0x0005 | 特性声明 2 | | 0x0006 | 特性数值 2 | | 0x0007 | 描述符 2 | | 0x0008 | 描述符 3 | | 0x0009 | 服务 2 | | ……… | ……… | GATT 已经成为 BLE 通信的规定,每一个设备中存在很多的 “service” (服务),service 中还包含有多个 “Characteristic”(特征值)。 在蓝牙实际数据交换中,就是通过读写这些 “Characteristic”(特征值) 来实现的。 ![image-20220127163310146](pictures/image-20220127163310146.png) - 一个鼠标是一个 BLEDevice - 一个 BLEDevice 建立了一个BLE服务器 BLEServer - 一个 BLE 服务器里有多个服务 BLEService - 一个服务里有多个特征值 BLECharacteristic 每个特征值是一种数据.就是通过读写这些 “Characteristic” 实现读写数据 每个 characteristic 的值可以在不加密的状态下读写,但配对的操作是加密的。 还有当 characteristic 的值已改变时,可接收通知(notify)。 ### UUID 服务和 characteristic 是通过 UUID 来进行识别的。 UUID 是32位的,但那些被蓝牙技术联盟的标准中定义的 UUID 是以四个数字来表示的。UUID 既有16位的也有128位的,我们需要了解的是16位的 UUID 是经过蓝牙组织认证的,是需要购买的,当然也有一些通用的16位 UUID。 UUID:由蓝牙设备厂商提供的 UUID,UUID是在硬件编程里已经确定了的,想要操作特定的服务、特征值都需要通过 UUID 来找。 ### notify通知的概念 如果主机的一个特征值 characteristic 发生改变, 可以使用通知notify来告诉客户端. 这是服务器主动给客户端发的信息, 并非是响应客户端的请求. 这样做有很多好处, 比如 Edge101WE 主板采集到了温度的变化, 可以将数据写入对应的特征值 characteristic, 然后 notify 通知客户端. 我们创建特征值时, 把它规定为通知类型, 当这个特征值发生变化时,可以通知客户端,像这样: ```c++ pCharacteristic = pService->createCharacteristic(CHARACTERISTIC_UUID_TX, BLECharacteristic::PROPERTY_NOTIFY); //创建一个(读)特征值, 它是通知下发类型的特征值 ``` 其实客户端可以不接受服务器发送的 notify, 方法是修改自己的 Descriptor. BLE 服务器看到你对自己的描述中标识了不想接收 notify, 也就不会再给你发了。 ### write写入的概念 我们可以把特征值定为写入类型, 这样客户端可以给我们写入, 触发写入回调函数 ```php BLECharacteristic *pCharacteristic = pService->createCharacteristic(CHARACTERISTIC_UUID_RX, BLECharacteristic::PROPERTY_WRITE); //创建一个(写)特征, 它是写入类型的特征值 pCharacteristic->setCallbacks(new MyCallbacks()); //为特征添加一个回调 ``` ### 例程:SimpleBLE 程序将建立一个 BLE 设备,当按下主板的用户按钮时,将修改 BLE 设备的名字,可通过手机查看 BLE 设备的名字变化。 (参考Arduino IDE例程 Examples -> Examples for Edge101WE ->SimpleBLE/examples/SimpleBleDevice) ```c++ // Copyright 2015-2016 Espressif Systems (Shanghai) PTE LTD // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Sketch shows how to use SimpleBLE to advertise the name of the device and change it on the press of a button // Useful if you want to advertise some sort of message // Button is attached between GPIO 0 and GND, and the device name changes each time the button is pressed #include "SimpleBLE.h" //使用库SimpleBLE #if !defined(CONFIG_BT_ENABLED) || !defined(CONFIG_BLUEDROID_ENABLED) #error Bluetooth is not enabled! Please run `make menuconfig` to and enable it #endif SimpleBLE ble; void onButton(){ String out = "BLE32 name: "; out += String(millis() / 1000); Serial.println(out); ble.begin(out); // 修改BLE的名字 } void setup() { Serial.begin(115200); Serial.setDebugOutput(true); pinMode(38, INPUT_PULLUP); //GPIO38 主板的用户按钮,用于改变BLE设备名字 Serial.print("Edge101WE SDK: "); Serial.println(ESP.getSdkVersion()); ble.begin("Edge101WE SimpleBLE");//在设备上启用蓝牙后,连接到FireBeetle MESH SimpleBLE。按下按钮时,更改设备名称。 Serial.println("Press the button to change the device's name"); } void loop() { static uint8_t lastPinState = 1; uint8_t pinState = digitalRead(38); //读取GPIO38 主板的用户按钮电平状态 if(!pinState && lastPinState){ onButton(); } lastPinState = pinState; while(Serial.available()) Serial.write(Serial.read()); } ``` ### 例程: BLE Server ```c++ /* Based on Neil Kolban example for IDF: https://github.com/nkolban/esp32-snippets/blob/master/cpp_utils/tests/BLE%20Tests/SampleServer.cpp Ported to Arduino ESP32 by Evandro Copercini updates by chegewara */ #include #include #include // See the following for generating UUIDs: // https://www.uuidgenerator.net/ #define SERVICE_UUID "4fafc201-1fb5-459e-8fcc-c5c9c331914b" #define CHARACTERISTIC_UUID "beb5483e-36e1-4688-b7f5-ea07361b26a8" void setup() { Serial.begin(115200); Serial.println("Starting BLE work!"); BLEDevice::init("Long name works now"); BLEServer *pServer = BLEDevice::createServer(); BLEService *pService = pServer->createService(SERVICE_UUID); BLECharacteristic *pCharacteristic = pService->createCharacteristic( CHARACTERISTIC_UUID, BLECharacteristic::PROPERTY_READ | BLECharacteristic::PROPERTY_WRITE ); pCharacteristic->setValue("Hello World says Neil"); pService->start(); // BLEAdvertising *pAdvertising = pServer->getAdvertising(); // this still is working for backward compatibility BLEAdvertising *pAdvertising = BLEDevice::getAdvertising(); pAdvertising->addServiceUUID(SERVICE_UUID); pAdvertising->setScanResponse(true); pAdvertising->setMinPreferred(0x06); // functions that help with iPhone connections issue pAdvertising->setMinPreferred(0x12); BLEDevice::startAdvertising(); Serial.println("Characteristic defined! Now you can read it in your phone!"); } void loop() { // put your main code here, to run repeatedly: delay(2000); } ``` 在手机安装 nRF Connect 应用,打开手机蓝牙。在 nRF Connect 应用上连接 "Long name works now" 这个蓝牙设备,即可读取回特征值 "Hello World says Neil"。 ![image-20210417142450634](./pictures/image-20210417142450634.png) ### 例程:BLE Server_multicconnect 创建一个 BL E服务器,一旦收到连接,它将定期发送通知。 **使用步骤** - 创建一个 BLE Server - 创建一个 BLE Service - 在 BLE Service 的基础上创建一个 Characteristic - 在 Characteristic 的基础上创建一个 Descriptor - 启动 Service - 开始广播 ```c++ /* Video: https://www.youtube.com/watch?v=oCMOYS71NIU Based on Neil Kolban example for IDF: https://github.com/nkolban/esp32-snippets/blob/master/cpp_utils/tests/BLE%20Tests/SampleNotify.cpp Ported to Arduino ESP32 by Evandro Copercini updated by chegewara 创建一个BLE服务器,一旦收到连接,它将定期发送通知。 Create a BLE server that, once we receive a connection, will send periodic notifications. The service advertises itself as: 4fafc201-1fb5-459e-8fcc-c5c9c331914b And has a characteristic of: beb5483e-36e1-4688-b7f5-ea07361b26a8 The design of creating the BLE server is: 1. Create a BLE Server 2. Create a BLE Service 3. Create a BLE Characteri stic on the Service 4. Create a BLE Descriptor on the characteristic 5. Start the service. 6. Start advertising. 一个与服务器相关联的连接处理程序将启动一个后台任务,每隔几秒钟发送一次通知。 A connect hander associated with the server starts a background task that performs notification every couple of seconds. */ #include #include #include #include BLEServer* pServer = NULL; BLECharacteristic* pCharacteristic = NULL; bool deviceConnected = false; bool oldDeviceConnected = false; uint32_t value = 0; // 从下列网址生成UUIDs // See the following for generating UUIDs: // https://www.uuidgenerator.net/ #define SERVICE_UUID "4fafc201-1fb5-459e-8fcc-c5c9c331914b" #define CHARACTERISTIC_UUID "beb5483e-36e1-4688-b7f5-ea07361b26a8" class MyServerCallbacks: public BLEServerCallbacks { void onConnect(BLEServer* pServer) { deviceConnected = true; // 如果有设备连接 BLEDevice::startAdvertising(); // 开始广播 }; // 如果无设备 void onDisconnect(BLEServer* pServer) { deviceConnected = false; } }; void setup() { Serial.begin(115200); // Create the BLE Device BLEDevice::init("Edge101WE BLE"); // BLE设备初始化 // Create the BLE Server pServer = BLEDevice::createServer(); // 创建Server pServer->setCallbacks(new MyServerCallbacks()); // 设置匿名回调函数(实例化MyServerCallbacks) // Create the BLE Service BLEService *pService = pServer->createService(SERVICE_UUID); // 创建BLE Service // 创建BLE 特征(Characterristic_UUID,长度) // Create a BLE Characteristic pCharacteristic = pService->createCharacteristic( CHARACTERISTIC_UUID, BLECharacteristic::PROPERTY_READ | BLECharacteristic::PROPERTY_WRITE | BLECharacteristic::PROPERTY_NOTIFY | BLECharacteristic::PROPERTY_INDICATE ); // https://www.bluetooth.com/specifications/gatt/viewer?attributeXmlFile=org.bluetooth.descriptor.gatt.client_characteristic_configuration.xml // Create a BLE Descriptor pCharacteristic->addDescriptor(new BLE2902()); // 创建BLE 描述 // Start the service pService->start(); // 服务启动 // Start advertising // 定义了一个BLEAdvertising类指针pAdvertising,它指向BLEDevice::getAdvertising() BLEAdvertising *pAdvertising = BLEDevice::getAdvertising(); pAdvertising->addServiceUUID(SERVICE_UUID); pAdvertising->setScanResponse(false); // 设置值为0x00以不广播该参数 pAdvertising->setMinPreferred(0x0); // set value to 0x00 to not advertise this parameter BLEDevice::startAdvertising(); // 开始广播 Serial.println("Waiting a client connection to notify..."); } void loop() { // notify changed value // 如果设备已连接 if (deviceConnected) { pCharacteristic->setValue((uint8_t*)&value, 4); // 设置值为value pCharacteristic->notify(); // 发送通知 notify value++; // value自加 // 可修改发送通知的延迟,便于手机端观察值的变化 delay(1000); // bluetooth stack will go into congestion, if too many packets are sent, in 6 hours test i was able to go as low as 3ms } // disconnecting if (!deviceConnected && oldDeviceConnected) { delay(500); // give the bluetooth stack the chance to get things ready pServer->startAdvertising(); //重新广播 restart advertising Serial.println("start advertising"); oldDeviceConnected = deviceConnected; } // connecting if (deviceConnected && !oldDeviceConnected) { // do stuff here on connecting oldDeviceConnected = deviceConnected; } } ``` 打开手机蓝牙,在 nRF Connect 应用上连接 "Edge101WE" 这个蓝牙设备,在 Service 列表,点击三个箭头的图标可读取回特征值,可查看 Value 每秒钟数值加一次。 ![image-20220127164000412](pictures/image-20220127164000412.png) ### 例程:BLE_uart BLE 的异步通信及安卓 app 测试 ```C++ /* Video: https://www.youtube.com/watch?v=oCMOYS71NIU Based on Neil Kolban example for IDF: https://github.com/nkolban/esp32-snippets/blob/master/cpp_utils/tests/BLE%20Tests/SampleNotify.cpp Ported to Arduino ESP32 by Evandro Copercini 创建一个BLE服务器,一旦我们收到连接,将会周期性发送通知 Create a BLE server that, once we receive a connection, will send periodic notifications. The service advertises itself as: 6E400001-B5A3-F393-E0A9-E50E24DCCA9E Has a characteristic of: 6E400002-B5A3-F393-E0A9-E50E24DCCA9E - used for receiving data with "WRITE" Has a characteristic of: 6E400003-B5A3-F393-E0A9-E50E24DCCA9E - used to send data with "NOTIFY" The design of creating the BLE server is: 1. Create a BLE Server 2. Create a BLE Service 3. Create a BLE Characteristic on the Service 4. Create a BLE Descriptor on the characteristic 5. Start the service. 6. Start advertising. 在本例中,rxvalue是接收到的数据(仅在该函数内可访问)。txValue是要发送的数据,在这个例子中每秒递增一个字节。 In this example rxValue is the data received (only accessible inside that function). And txValue is the data to be sent, in this example just a byte incremented every second. */ #include #include #include #include BLEServer *pServer = NULL; // BLEServer指针 pServer BLECharacteristic * pTxCharacteristic; // BLECharacteristic指针 pTxCharacteristic bool deviceConnected = false; // 本次连接状态 bool oldDeviceConnected = false; // 上次连接状态 uint8_t txValue = 0; // See the following for generating UUIDs: // https://www.uuidgenerator.net/ #define SERVICE_UUID "6E400001-B5A3-F393-E0A9-E50E24DCCA9E" // UART service UUID #define CHARACTERISTIC_UUID_RX "6E400002-B5A3-F393-E0A9-E50E24DCCA9E" #define CHARACTERISTIC_UUID_TX "6E400003-B5A3-F393-E0A9-E50E24DCCA9E" // 创建MyServerCallbacks类,其继承自BLEServerCallbacks class MyServerCallbacks: public BLEServerCallbacks { void onConnect(BLEServer* pServer) { deviceConnected = true; }; void onDisconnect(BLEServer* pServer) { deviceConnected = false; } }; // 创建MyCallbacks类,其继承自BLECharacteristicCallbacks class MyCallbacks: public BLECharacteristicCallbacks { void onWrite(BLECharacteristic *pCharacteristic) { std::string rxValue = pCharacteristic->getValue(); // 接收信息 // 向串口输出收到的值 if (rxValue.length() > 0) { Serial.println("*********"); Serial.print("Received Value: "); for (int i = 0; i < rxValue.length(); i++) Serial.print(rxValue[i]); Serial.println(); Serial.println("*********"); } } }; void setup() { Serial.begin(115200); // Create the BLE Device BLEDevice::init("UART Service"); // Create the BLE Server pServer = BLEDevice::createServer(); pServer->setCallbacks(new MyServerCallbacks()); // 设置回调函数 // Create the BLE Service BLEService *pService = pServer->createService(SERVICE_UUID); // Create a BLE Characteristic pTxCharacteristic = pService->createCharacteristic( CHARACTERISTIC_UUID_TX, BLECharacteristic::PROPERTY_NOTIFY ); pTxCharacteristic->addDescriptor(new BLE2902()); BLECharacteristic * pRxCharacteristic = pService->createCharacteristic( CHARACTERISTIC_UUID_RX, BLECharacteristic::PROPERTY_WRITE ); pRxCharacteristic->setCallbacks(new MyCallbacks()); // 设置回调函数 // Start the service pService->start(); // 开始服务 // Start advertising pServer->getAdvertising()->start(); // 开始广播 Serial.println("Waiting a client connection to notify..."); } void loop() { if (deviceConnected) { pTxCharacteristic->setValue(&txValue, 1); // 设置要发送的值为1 pTxCharacteristic->notify(); // 广播 txValue++; // 值自加1 // 如果有太多包要发送,蓝牙会堵塞,所以这里增加一个延迟,可增加延迟方便观察 delay(1000); // bluetooth stack will go into congestion, if too many packets are sent } // 如果断开连接 disconnecting if (!deviceConnected && oldDeviceConnected) { // 留时间给蓝牙缓冲 delay(500); // give the bluetooth stack the chance to get things ready pServer->startAdvertising(); // 重新广播restart advertising Serial.println("start advertising"); oldDeviceConnected = deviceConnected; } // 正在连接 connecting if (deviceConnected && !oldDeviceConnected) { // do stuff here on connecting oldDeviceConnected = deviceConnected; } } ``` 打开手机蓝牙,在 nRF Connect 应用上连接 ”UART Service" 这个蓝牙设备,在Service列表的 TX Characteristic,点击三个箭头的图标可读取回特征值,可查看Value 值变化。 在 RX Characteristic 点击向上的箭头符号,将数据 “hello” 发送到 Edge101WE 主板。 ![image-20220127164023729](pictures/image-20220127164023729.png) 在Arduino IDE的串口打印出接收到的 hello字符串数据。 ``` Waiting a client connection to notify... ********* Received Value: hello ********* ``` ### 例程:BLE_write ```c++ /* Based on Neil Kolban example for IDF: https://github.com/nkolban/esp32-snippets/blob/master/cpp_utils/tests/BLE%20Tests/SampleWrite.cpp Ported to Arduino ESP32 by Evandro Copercini */ #include #include #include // See the following for generating UUIDs: // https://www.uuidgenerator.net/ #define SERVICE_UUID "4fafc201-1fb5-459e-8fcc-c5c9c331914b" #define CHARACTERISTIC_UUID "beb5483e-36e1-4688-b7f5-ea07361b26a8" class MyCallbacks: public BLECharacteristicCallbacks { void onWrite(BLECharacteristic *pCharacteristic) { // 写方法 std::string value = pCharacteristic->getValue(); // 接收值 if (value.length() > 0) { Serial.println("*********"); Serial.print("New value: "); for (int i = 0; i < value.length(); i++) // 遍历输出字符串 Serial.print(value[i]); Serial.println(); Serial.println("*********"); } } }; void setup() { Serial.begin(115200); Serial.println("1- Download and install an BLE scanner app in your phone"); Serial.println("2- Scan for BLE devices in the app"); Serial.println("3- Connect to Edge101WE"); Serial.println("4- Go to CUSTOM CHARACTERISTIC in CUSTOM SERVICE and write something"); Serial.println("5- See the magic =)"); BLEDevice::init("Edge101WE"); //设备初始化,名称Edge101WE BLEServer *pServer = BLEDevice::createServer(); // BLEServer指针,创建Server BLEService *pService = pServer->createService(SERVICE_UUID); // BLEService指针,创建Service // BLECharacteristic指针,创建Characteristic BLECharacteristic *pCharacteristic = pService->createCharacteristic( CHARACTERISTIC_UUID, BLECharacteristic::PROPERTY_READ | BLECharacteristic::PROPERTY_WRITE ); pCharacteristic->setCallbacks(new MyCallbacks()); // 设置回调函数 pCharacteristic->setValue("Hello World"); // 设置值 "Hello World" pService->start(); // 开启服务 BLEAdvertising *pAdvertising = pServer->getAdvertising(); // 初始化广播 pAdvertising->start(); // 开始广播 } void loop() { // put your main code here, to run repeatedly: delay(2000); } ``` 打开手机蓝牙,在 nRF Connect 应用上连接 "Edge101WE“ 这个蓝牙设备,在 Service 列表的 TX Characteristic,点击向下箭头图标可读取回特征值,当前 Value值为 “Hello World”。 ![image-20220127164051056](pictures/image-20220127164051056.png) 在 Characteristic 点击向上箭头,将数据发送给 Edge101WE 主板。数据类型选择 TEXT,输入 “hello edge101we”,点击 SEND 发送数据。 可以看到串口有打印出接收到的数据。 ![image-20220127164111000](pictures/image-20220127164111000.png) 串口打印数据 ``` 1- Download and install an BLE scanner app in your phone 2- Scan for BLE devices in the app 3- Connect to Edge101WE 4- Go to CUSTOM CHARACTERISTIC in CUSTOM SERVICE and write something 5- See the magic =) ********* New value: hello edge101we ********* ``` 此时再次读取 Vaule,发现已经被修改为 “hello edge101we” ![image-20220127164131865](pictures/image-20220127164131865.png) ### 例程:BLE client 配合之前的**BLE Server**例程演示。使用一块主板烧录BLE Server的例程,另一块主板烧录下方的BLE client的例程 ```c++ /** * A BLE client example that is rich in capabilities. * There is a lot new capabilities implemented. * author unknown * updated by chegewara */ #include "BLEDevice.h" //#include "BLEScan.h" // The remote service we wish to connect to. static BLEUUID serviceUUID("4fafc201-1fb5-459e-8fcc-c5c9c331914b"); // The characteristic of the remote service we are interested in. static BLEUUID charUUID("beb5483e-36e1-4688-b7f5-ea07361b26a8"); static boolean doConnect = false; static boolean connected = false; static boolean doScan = false; static BLERemoteCharacteristic* pRemoteCharacteristic; static BLEAdvertisedDevice* myDevice; static void notifyCallback( BLERemoteCharacteristic* pBLERemoteCharacteristic, uint8_t* pData, size_t length, bool isNotify) { Serial.print("Notify callback for characteristic "); Serial.print(pBLERemoteCharacteristic->getUUID().toString().c_str()); Serial.print(" of data length "); Serial.println(length); Serial.print("data: "); Serial.println((char*)pData); } class MyClientCallback : public BLEClientCallbacks { void onConnect(BLEClient* pclient) { } void onDisconnect(BLEClient* pclient) { connected = false; Serial.println("onDisconnect"); } }; bool connectToServer() { Serial.print("Forming a connection to "); Serial.println(myDevice->getAddress().toString().c_str()); BLEClient* pClient = BLEDevice::createClient(); Serial.println(" - Created client"); pClient->setClientCallbacks(new MyClientCallback()); // Connect to the remove BLE Server. pClient->connect(myDevice); // if you pass BLEAdvertisedDevice instead of address, it will be recognized type of peer device address (public or private) Serial.println(" - Connected to server"); // Obtain a reference to the service we are after in the remote BLE server. BLERemoteService* pRemoteService = pClient->getService(serviceUUID); if (pRemoteService == nullptr) { Serial.print("Failed to find our service UUID: "); Serial.println(serviceUUID.toString().c_str()); pClient->disconnect(); return false; } Serial.println(" - Found our service"); // Obtain a reference to the characteristic in the service of the remote BLE server. pRemoteCharacteristic = pRemoteService->getCharacteristic(charUUID); if (pRemoteCharacteristic == nullptr) { Serial.print("Failed to find our characteristic UUID: "); Serial.println(charUUID.toString().c_str()); pClient->disconnect(); return false; } Serial.println(" - Found our characteristic"); // Read the value of the characteristic. if(pRemoteCharacteristic->canRead()) { std::string value = pRemoteCharacteristic->readValue(); Serial.print("The characteristic value was: "); Serial.println(value.c_str()); } if(pRemoteCharacteristic->canNotify()) pRemoteCharacteristic->registerForNotify(notifyCallback); connected = true; return true; } /** * Scan for BLE servers and find the first one that advertises the service we are looking for. */ class MyAdvertisedDeviceCallbacks: public BLEAdvertisedDeviceCallbacks { /** * Called for each advertising BLE server. */ void onResult(BLEAdvertisedDevice advertisedDevice) { Serial.print("BLE Advertised Device found: "); Serial.println(advertisedDevice.toString().c_str()); // We have found a device, let us now see if it contains the service we are looking for. if (advertisedDevice.haveServiceUUID() && advertisedDevice.isAdvertisingService(serviceUUID)) { BLEDevice::getScan()->stop(); myDevice = new BLEAdvertisedDevice(advertisedDevice); doConnect = true; doScan = true; } // Found our server } // onResult }; // MyAdvertisedDeviceCallbacks void setup() { Serial.begin(115200); Serial.println("Starting Arduino BLE Client application..."); BLEDevice::init(""); // Retrieve a Scanner and set the callback we want to use to be informed when we // have detected a new device. Specify that we want active scanning and start the // scan to run for 5 seconds. BLEScan* pBLEScan = BLEDevice::getScan(); pBLEScan->setAdvertisedDeviceCallbacks(new MyAdvertisedDeviceCallbacks()); pBLEScan->setInterval(1349); pBLEScan->setWindow(449); pBLEScan->setActiveScan(true); pBLEScan->start(5, false); } // End of setup. // This is the Arduino main loop function. void loop() { // If the flag "doConnect" is true then we have scanned for and found the desired // BLE Server with which we wish to connect. Now we connect to it. Once we are // connected we set the connected flag to be true. if (doConnect == true) { if (connectToServer()) { Serial.println("We are now connected to the BLE Server."); } else { Serial.println("We have failed to connect to the server; there is nothin more we will do."); } doConnect = false; } // If we are connected to a peer BLE Server, update the characteristic each time we are reached // with the current time since boot. if (connected) { String newValue = "Time since boot: " + String(millis()/1000); Serial.println("Setting new characteristic value to \"" + newValue + "\""); // Set the characteristic's value to be the array of bytes that is actually a string. pRemoteCharacteristic->writeValue(newValue.c_str(), newValue.length()); }else if(doScan){ BLEDevice::getScan()->start(0); // this is just example to start scan after disconnect, most likely there is better way to do it in arduino } delay(1000); // Delay a second between loops. } // End of loop ``` 两块主板复位后,打开client这边的串口监视器,可以看到如下输出 ``` Starting Arduino BLE Client application... BLE Advertised Device found: Name: Long name works now, Address: 08:3a:f2:26:b5:ea, serviceUUID: 4fafc201-1fb5-459e-8fcc-c5c9c331914b, txPower: 3 Forming a connection to 08:3a:f2:26:b5:ea Created client Connected to server Found our service Found our characteristic The characteristic value was: Hello World says Neil We are now connected to the BLE Server. Setting new characteristic value to "Time since boot: 2" Setting new characteristic value to "Time since boot: 3" Setting new characteristic value to "Time since boot: 4" Setting new characteristic value to "Time since boot: 5" Setting new characteristic value to "Time since boot: 6" Setting new characteristic value to "Time since boot: 7" Setting new characteristic value to "Time since boot: 8" Setting new characteristic value to "Time since boot: 9" ``` ### 例程:BLE client 控制外设 Edge101WE 主板作为 BLE 客户端 连接蓝牙手环的服务端。当手环靠近主板,主板上GPIO 15 用户LED灯亮,当离开一定距离 LED灯灭。 此方案可用户办公场所节能,例如人进入房间,房间里面的灯自动打开,当离开房间后灯自动熄灭。或用于工厂机器的安全防护,例如只有佩戴授权ID的蓝牙手环才能去操作此台机器,当操作人员离开后机器自动关闭。 ```c++ /* * Program to operate ESP32 in client mode and use fitness band as proximity switch * Program by: Aswinth Raj B * Dated: 31-10-2018 * Website: www.circuitdigest.com * Reference: https://github.com/nkolban/esp32-snippets * //NOTE: The My_BLE_Address, serviceUUID and charUUID should be changed based on the BLe server you are using */ #include //Header file for BLE static BLEUUID serviceUUID("0000fee7-0000-1000-8000-00805f9b34fb"); //Service UUID of fitnessband obtained through nRF connect application static BLEUUID charUUID("0000fee7-0000-1000-8000-00805f9b34fb"); //Characteristic UUID of fitnessband obtained through nRF connect application String My_BLE_Address = "c7:f0:69:f0:68:81"; //Hardware Bluetooth MAC of my fitnessband, will vary for every band obtained through nRF connect application static BLERemoteCharacteristic* pRemoteCharacteristic; BLEScan* pBLEScan; //Name the scanning device as pBLEScan BLEScanResults foundDevices; static BLEAddress *Server_BLE_Address; String Scaned_BLE_Address; boolean paired = false; //boolean variable to togge light bool connectToserver (BLEAddress pAddress){ BLEClient* pClient = BLEDevice::createClient(); Serial.println(" - Created client"); // Connect to the BLE Server. pClient->connect(pAddress); Serial.println(" - Connected to fitnessband"); // Obtain a reference to the service we are after in the remote BLE server. BLERemoteService* pRemoteService = pClient->getService(serviceUUID); if (pRemoteService != nullptr) { Serial.println(" - Found our service"); return true; } else return false; // Obtain a reference to the characteristic in the service of the remote BLE server. pRemoteCharacteristic = pRemoteService->getCharacteristic(charUUID); if (pRemoteCharacteristic != nullptr) Serial.println(" - Found our characteristic"); return true; } class MyAdvertisedDeviceCallbacks: public BLEAdvertisedDeviceCallbacks { void onResult(BLEAdvertisedDevice advertisedDevice) { Serial.printf("Scan Result: %s \n", advertisedDevice.toString().c_str()); Server_BLE_Address = new BLEAddress(advertisedDevice.getAddress()); Scaned_BLE_Address = Server_BLE_Address->toString().c_str(); } }; void setup() { Serial.begin(115200); //Start serial monitor Serial.println("FireBeetle MESH BLE Server program"); //Intro message BLEDevice::init(""); pBLEScan = BLEDevice::getScan(); //create new scan pBLEScan->setAdvertisedDeviceCallbacks(new MyAdvertisedDeviceCallbacks()); //Call the class that is defined above pBLEScan->setActiveScan(true); //active scan uses more power, but get results faster pinMode (15,OUTPUT); //Declare the in-built LED pin as output } void loop() { foundDevices = pBLEScan->start(3); //Scan for 3 seconds to find the Fitness band while (foundDevices.getCount() >= 1) { if (Scaned_BLE_Address == My_BLE_Address && paired == false) { Serial.println("Found Device :-)... connecting to Server as client"); if (connectToserver(*Server_BLE_Address)) { paired = true; Serial.println("********************LED turned ON************************"); digitalWrite (15,LOW); break; } else { Serial.println("Pairing failed"); break; } } if (Scaned_BLE_Address == My_BLE_Address && paired == true) { Serial.println("Our device went out of range"); paired = false; Serial.println("********************LED OF************************"); digitalWrite (15,HIGH); ESP.restart(); break; } else { Serial.println("We have some other BLe device in range"); break; } } } ```