目的
ESP32的蓝牙除了作为服务器(从设备)使用还可以作为客户端(主机)使用。这篇文章将对相关内容做个简单说明。
基础准备
这篇文章中测试需要先准备一个蓝牙服务器(从设备),我这里直接拿了一个ESP32模块启用蓝牙服务器功能作为测试,代码如下:
#include <BLEDevice.h>
#include <BLE2902.h>
#define SERVICE_UUID "4fafc201-1fb5-459e-8fcc-c5c9c331914b"
#define CHARACTERISTIC_UUID "beb5483e-36e1-4688-b7f5-ea07361b26a8"
class MyServerCallbacks: public BLEServerCallbacks {
void onConnect(BLEServer* pServer) {};
void onDisconnect(BLEServer* pServer) {
pServer->startAdvertising(); // 如果客户端断开连接了就重新开启Advertising广播,使客户端可以再次搜索到自己
}
};
BLECharacteristic* pCharacteristic = NULL;
void setup() {
Serial.begin(115200);
Serial.println();
BLEDevice::init("ESP32-BLE");
BLEServer *pServer = BLEDevice::createServer(); // 创建服务器
pServer->setCallbacks(new MyServerCallbacks());
BLEService *pService = pServer->createService(SERVICE_UUID); // 创建服务
pCharacteristic = pService->createCharacteristic(
CHARACTERISTIC_UUID,
BLECharacteristic::PROPERTY_READ |
BLECharacteristic::PROPERTY_WRITE |
BLECharacteristic::PROPERTY_NOTIFY |
BLECharacteristic::PROPERTY_INDICATE
);
pCharacteristic->setValue("Hello World! ");
pCharacteristic->addDescriptor(new BLE2902());
pService->start();
BLEAdvertising *pAdvertising = BLEDevice::getAdvertising();
pAdvertising->addServiceUUID(SERVICE_UUID); // 广播服务的UUID
BLEDevice::startAdvertising();
}
unsigned long previousMillis = 0;
const long interval = 2000;
void loop() {
unsigned long currentMillis = millis();
if (currentMillis - previousMillis >= interval)
{
previousMillis = currentMillis;
pCharacteristic->notify(); // 每隔interval时间主动推送一次数据
}
}
将上面代码上传到ESP32中,那么你就有一个名称为 ESP32-BLE 、主动暴露服务的UUID、可供读写的、连接后每隔两秒主动推送一次消息的蓝牙设备了。
ESP32蓝牙服务器相关内容可以参考下面文章:
《使用Arduino开发ESP32(21):蓝牙基础说明与作为服务器使用》
搜索蓝牙设备
搜索设备
蓝牙作为客户端使用流程上来说无非就是先搜索设备,然后再连接其中某个设备,连接成功后就可以通讯了。这里先讲一讲在Arduino core for the ESP32中如何搜索蓝牙设备。
首先试试官方例程中搜索蓝牙设备的例子:
#include <BLEDevice.h>
BLEScan* pBLEScan;
class MyAdvertisedDeviceCallbacks: public BLEAdvertisedDeviceCallbacks {
void onResult(BLEAdvertisedDevice advertisedDevice) {
Serial.printf("Advertised Device: %s \n", advertisedDevice.toString().c_str()); // 打印设备信息
}
};
void setup() {
Serial.begin(115200);
Serial.println("Scanning...");
BLEDevice::init("");
pBLEScan = BLEDevice::getScan(); // 获取扫描对象
pBLEScan->setAdvertisedDeviceCallbacks(new MyAdvertisedDeviceCallbacks()); // 添加搜索到设备Advertising广播时的回调函数
pBLEScan->setActiveScan(true); // 启用主动扫描
pBLEScan->setInterval(100); // 相邻两次扫描开始时间间隔(单位:毫秒)
pBLEScan->setWindow(80); // 一次扫描持续时间(单位:毫秒)
// 这个参数必须小于等于setInterval()中的参数
// 如果这个参数等于setInterval()中的参数则表示不间断的连续扫描
}
void loop() {
BLEScanResults foundDevices = pBLEScan->start(2); // 启动扫描2秒,该方式此处会阻塞直到扫描结束
Serial.print("Devices found: ");
Serial.println(foundDevices.getCount()); // 打印扫描到的设备数量
Serial.println("Scan done!");
pBLEScan->clearResults(); // 清除扫描结果释放空间
delay(60000);
}
上面演示中可以看到通过搜索可以获取到附近蓝牙设备的名称、地址等各种信息,通常来说对于同一个设备而言设备地址是唯一的。
目前搜索蓝牙设备的这个功能好像是存在一些问题的(ESP32 Arduino Release 1.0.6 based on ESP-IDF v3.3.5),上面代码中 setAdvertisedDeviceCallbacks() 这个方法第二个参数用来控制是否响应搜索到的地址相同的蓝牙设备,默认情况下是false,去除重复设备。但是实际测试的时候不管使用true还是false,结果都会出现重复设备。另外还有一个问题,上面演示中使用搜索结束后返回的 BLEScanResults 对象打印了搜索到的设备数量,虽然有搜索到设备,但打印出数量却是0。
对于上面的问题其实也可以自行去重统计,关键就是每个设备(BLEAdvertisedDevice)有一个唯一的Address。
上面的例子中用了阻塞的方式搜索,我们还可以使用非阻塞的方式搜索:
#include <BLEDevice.h>
BLEScan* pBLEScan;
class MyAdvertisedDeviceCallbacks: public BLEAdvertisedDeviceCallbacks {
void onResult(BLEAdvertisedDevice advertisedDevice) {
Serial.printf("Advertised Device: %s \n", advertisedDevice.toString().c_str()); // 打印设备信息
}
};
void ScanCompleteCB(BLEScanResults foundDevices) { // 扫描结束回调函数
Serial.print("Devices found: ");
Serial.println(foundDevices.getCount()); // 打印扫描到的设备数量
Serial.println("Scan done!");
pBLEScan->clearResults(); // 清除扫描结果释放空间
}
void setup() {
Serial.begin(115200);
Serial.println("Scanning...");
BLEDevice::init("");
pBLEScan = BLEDevice::getScan(); // 获取扫描对象
pBLEScan->setAdvertisedDeviceCallbacks(new MyAdvertisedDeviceCallbacks()); // 添加搜索到设备Advertising广播时的回调函数
pBLEScan->setActiveScan(true); // 启用主动扫描
pBLEScan->setInterval(100); // 相邻两次扫描开始时间间隔(单位:毫秒)
pBLEScan->setWindow(80); // 一次扫描持续时间(单位:毫秒)
// 这个参数必须小于等于setInterval()中的参数
// 如果这个参数等于setInterval()中的参数则表示不间断的连续扫描
while (!pBLEScan->start(2, ScanCompleteCB)) {} // 启动非阻塞式扫描2秒,该方法启动成功时返回true
}
void loop() {
Serial.println("++++++++++++++++++++");
delay(500);
}
信息查询
上面演示中在每次扫描到设备后会触发回调函数,在其中会有一个 BLEAdvertisedDevice 类型的对象,该对象就是扫描到的设备,我们可以通过此获取到设备的一些信息。
#include <BLEDevice.h>
BLEScan* pBLEScan;
class MyAdvertisedDeviceCallbacks: public BLEAdvertisedDeviceCallbacks {
void onResult(BLEAdvertisedDevice advertisedDevice) {
if (advertisedDevice.haveName() && (advertisedDevice.getName()=="ESP32-BLE")) {
// pBLEScan->stop(); // 停止当前扫描
advertisedDevice.getScan()->stop(); // 停止当前扫描
if (advertisedDevice.isAdvertisingService(BLEUUID("4fafc201-1fb5-459e-8fcc-c5c9c331914b"))) {
Serial.println("设备广播了UUID为 4fafc201-1fb5-459e-8fcc-c5c9c331914b 的服务");
}
Serial.printf("Address: %s\r\n", advertisedDevice.getAddress().toString().c_str());
if (advertisedDevice.haveAppearance()) {
Serial.printf("Appearance: %d\r\n", advertisedDevice.getAppearance());
}
if (advertisedDevice.haveManufacturerData()) {
Serial.printf("ManufacturerData: %s\r\n", advertisedDevice.getManufacturerData().c_str());
}
if (advertisedDevice.haveRSSI()) {
Serial.printf("RSSI: %d\r\n", advertisedDevice.getRSSI());
}
if (advertisedDevice.haveServiceData()) {
Serial.printf("ServiceData: %s\r\n", advertisedDevice.getServiceData().c_str());
Serial.printf("ServiceDataUUID: %s\r\n", advertisedDevice.getServiceDataUUID().toString().c_str());
Serial.printf("ServiceDataCount: %d\r\n", advertisedDevice.getServiceDataCount());
Serial.printf("ServiceDataUUIDCount: %d\r\n", advertisedDevice.getServiceDataUUIDCount());
}
if (advertisedDevice.haveServiceUUID()) {
Serial.printf("ServiceUUID: %s\r\n", advertisedDevice.getServiceUUID().toString().c_str());
Serial.printf("ServiceUUIDCount: %d\r\n", advertisedDevice.getServiceUUIDCount());
}
if (advertisedDevice.haveTXPower()) {
Serial.printf("TXPower: %d\r\n", advertisedDevice.getTXPower());
}
}
}
};
void ScanCompleteCB(BLEScanResults foundDevices) {
Serial.print("Devices found: ");
Serial.println(foundDevices.getCount());
Serial.println("Scan done!");
pBLEScan->clearResults();
}
void setup() {
Serial.begin(115200);
Serial.println("Scanning...");
BLEDevice::init("");
pBLEScan = BLEDevice::getScan();
pBLEScan->setAdvertisedDeviceCallbacks(new MyAdvertisedDeviceCallbacks());
pBLEScan->setActiveScan(true);
pBLEScan->setInterval(100);
pBLEScan->setWindow(80);
while (!pBLEScan->start(2, ScanCompleteCB)) {}
}
void loop() {}
连接与交互
上面的介绍中我们已经可以搜索到设备,并且可以获取到一些设备的信息,根据这些东西我们就可以筛选锁定我们需要的设备,然后连接该设备以进行数据交互。下面是个简单的演示:
#include <BLEDevice.h>
#define SERVICE_UUID "4fafc201-1fb5-459e-8fcc-c5c9c331914b"
#define CHARACTERISTIC_UUID "beb5483e-36e1-4688-b7f5-ea07361b26a8"
boolean doSacn = true;
boolean doConnect = false;
boolean connected = false;
BLEAdvertisedDevice* pServer;
BLERemoteCharacteristic* pRemoteCharacteristic;
// 搜索到设备时回调功能
class MyAdvertisedDeviceCallbacks: public BLEAdvertisedDeviceCallbacks {
void onResult(BLEAdvertisedDevice advertisedDevice) {
// if (advertisedDevice.haveServiceUUID() && advertisedDevice.isAdvertisingService(BLEUUID("4fafc201-1fb5-459e-8fcc-c5c9c331914b"))) {
if (advertisedDevice.haveName() && (advertisedDevice.getName()=="ESP32-BLE")) {
advertisedDevice.getScan()->stop(); // 停止当前扫描
pServer = new BLEAdvertisedDevice(advertisedDevice); // 暂存设备
doSacn = false;
doConnect = true;
Serial.println("发现想要连接的设备");
}
}
};
// 客户端与服务器连接与断开回调功能
class MyClientCallback : public BLEClientCallbacks {
void onConnect(BLEClient* pclient) {}
void onDisconnect(BLEClient* pclient) {
doSacn = true;
connected = false;
Serial.println("失去与设备的连接");
}
};
// 收到服务推送的数据时的回调函数
void NotifyCallback(BLERemoteCharacteristic* pBLERemoteCharacteristic, uint8_t* pData, size_t length, bool isNotify) {
char buf[length + 1];
for (size_t i = 0; i < length; i++) {
buf[i] = pData[i];
}
buf[length] = 0;
Serial.printf("该消息长度为: %d; 内容为: %s\r\n", length, buf);
}
// 用来连接设备获取其中的服务与特征
bool ConnectToServer(void) {
BLEClient* pClient = BLEDevice::createClient(); // 创建客户端
pClient->setClientCallbacks(new MyClientCallback()); // 添加客户端与服务器连接与断开回调功能
if (!pClient->connect(pServer)) { // 尝试连接设备
return false;
}
Serial.println("连接设备成功");
BLERemoteService* pRemoteService = pClient->getService(SERVICE_UUID); // 尝试获取设备中的服务
if (pRemoteService == nullptr) {
Serial.println("获取服务失败");
pClient->disconnect();
return false;
}
Serial.println("获取服务成功");
pRemoteCharacteristic = pRemoteService->getCharacteristic(CHARACTERISTIC_UUID); // 尝试获取服务中的特征
if (pRemoteCharacteristic == nullptr) {
Serial.println("获取特性失败");
pClient->disconnect();
return false;
}
Serial.println("获取特征成功");
if(pRemoteCharacteristic->canRead()) { // 如果特征值可以读取则读取数据
Serial.printf("该特征值可以读取并且当前值为: %s\r\n", pRemoteCharacteristic->readValue().c_str());
}
if(pRemoteCharacteristic->canNotify()) { // 如果特征值启用了推送则添加推送接收处理
pRemoteCharacteristic->registerForNotify(NotifyCallback);
}
}
void setup() {
Serial.begin(115200);
Serial.println();
BLEDevice::init("");
BLEScan* pBLEScan = BLEDevice::getScan();
pBLEScan->setAdvertisedDeviceCallbacks(new MyAdvertisedDeviceCallbacks());
pBLEScan->setActiveScan(true);
pBLEScan->setInterval(100);
pBLEScan->setWindow(80);
}
void loop() {
// 如果需要扫描则进行扫描
if (doSacn) {
Serial.println("开始搜索设备");
BLEDevice::getScan()->clearResults();
BLEDevice::getScan()->start(0); // 持续搜索设备
}
// 如果找到设备就尝试连接设备
if (doConnect) {
if (ConnectToServer()) {
connected = true;
}
else {
doSacn = true;
}
doConnect = false;
}
// 如果已经连接就可以向设备发送数据
if (connected) {
if(pRemoteCharacteristic->canWrite()) { // 如果可以向特征值写数据
delay(3500);
String newValue = "mytime: " + String(millis()/1000);
Serial.printf("像特征写入消息: %s\r\n", newValue.c_str());
pRemoteCharacteristic->writeValue(newValue.c_str(), newValue.length());
}
}
}
上面演示已经是蓝牙作为客户端使用的一个整体的流程了,包含了搜索设备、连接设备并获取其中的服务和特征、读写数据、接收推送等功能。另外我们也可以通过BLERemoteCharacteristic来获取对应的BLERemoteDescriptor对象,并读写其内容。
总结
Arduino core for the ESP32中ESP32作为蓝牙客户端使用内容主要就是上面这些了,更多内容可以参考下面链接:
https://github.com/espressif/arduino-esp32/tree/master/libraries/BLE