使用Arduino开发ESP32(22):蓝牙作为客户端使用

2023-11-18

目的

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

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

使用Arduino开发ESP32(22):蓝牙作为客户端使用 的相关文章

  • 软件测试—第六章白盒测试基本路径测试法

    一 基本路径法 1 为什么使用基本路径法 一是降低了测试用例设计的难度 只要搞清了各种流程 就可以设计出高质量的测试用例来 而不用太多测试方面的经验 二是在测试时间较紧的情况下 可以有的放矢的选择测试用例 而不用完全根据经验来取舍 2 什么
  • 如何设置当我们点击鼠标右键时,可以有新建Typora的选项

    最近研究了一下注册表 感觉这个东西还是挺有意思的 今天周末放假 打算写一点日记 当然是不发表的那种 然后我果断打开了Typora 不过由于好久没用Typora了 我忘记把他放在哪里了 找了一段时间终于找到了 我突然想到 为什么不把Typor
  • 目标检测算法之YOLOV3

    本博客中YOLO系列均为个人理解笔记 欢迎评论指出理解有误或者要讨论的地方 YOLOV3模型相比于v2来说 实质性的改进并不大 更多的是一些技术的堆叠 其并不像yolov2对于v1一样 由巨大的改变和提升 其相对于yolov2 改变在于 1

随机推荐

  • Java并发(多线程和锁机制)

    part 01 Java线程 1 进程和线程的区别 进程是操作系统进行资源分配的最小单位 线程是操作系统进行任务分配的最小单位 一个进程可以有多个线程 2 Java开启线程的方式 继承Tread类 重写run方法 实现Runnable接口
  • 红日安全靶机实战(一)

    0x01 入侵web服务器 0x 1 1 信息收集 首先使用 nmap 来扫描ip 段存活的主机 nmap sn 192 168 127 0 24 这里网上有的用 netdiscover 来扫描 其实和 nmap sn 扫描原理是一样的 都
  • .secret勒索病毒数据恢复

    导言 在数字时代 随着科技的迅猛发展 我们的生活更加便捷 但也引发了一系列新的威胁 其中之一就是勒索软件 而 secret 勒索病毒 则是这个威胁中的一颗明亮而又毒辣的星 91数据恢复本文将带您深入了解 secre 勒索病毒的特点 并探讨如
  • Image Sensor的FSIN/VSYNC

    本文介绍Image Sensor的FSIN VSYNC 产品开发过程 比如3D成像 中 有时会遇到需要2个及以上的Image Sensor同步采集 因此 Image Sensor厂家对于他们的产品都提供了同步功能 也就是我们经常所见的FSI
  • 学习Vue基础的分享

    Vue是一个前端框架 所以就有自己特定的语法 所以在这里列出学到的基础语法作用 基础模板参考 div h1 testName a baidu a h1 div
  • c语言ofstream未定义标识符,关于c++:c中ifstream及ofstream超详细说明

    前文说过 ifstream是继承于istream ofstream是继承于ostream fstream是继承于iostream类 而他们应用的缓冲区类是filebuf 对于这些类之间的关系 有趣味能够去查看我之前的文章 c 规范输入输出流
  • httpclient错误

    Caused by org apache http ProtocolException Target host is not specified at org apache http impl conn DefaultRoutePlanne
  • 用python实现计算器

    上次我用我学习的python做一个简易的计算器 我对计算器进行了 更改优化 变成了一个真正的计算器 实现流程 1 计算机布局 2 计算机执行 首先导入模块 Tkinter 作为 Python GUI 开发工具之一 它具有 GUI 软件包的必
  • 技术大佬和普通程序员改bug的区别!

    阅读本文大概需要2min 文 强哥 未经授权禁止转载 在我这么多年的工作生涯里 难免遇到那些工作糊弄的开发同事 随意编程的实习生 不够细致的测试 缺乏专业度的产品 产品的体验 取决于多个环节的把控 但很多情况下 由于bug严重影响体验 或者
  • 【原创】遇到 ORACLE 错误 1017

    1 错误描述 expdp sys leixiao orcl1 schemas sys directory DATA PUMP DIR dumpfile expdp test1 dmp logfile expdp test1 log 以sys
  • 做影视剪辑短视频,新手小白一个月赚5000多,用点心你也可以

    大周有一个学员把短视频当自己兴趣爱好 空余时间做二次剪辑 谁想到一个月多赚了5000多 他在一家私企上班 工作还算稳定 每天朝九晚五的工作 他不想就这样日复一日平淡的过下去 找到了大周 开始了自己短视频之旅 他这个人比较腼腆 本人出镜拍视频
  • 解决Android通过chrome://inspect/调试WebView出现 HTTP/1.1 404 Not Found 的问题

    问题描述 无论是调试Web页面还是调试Hybrid混合应用 只要是调试Android的webview 都需要使用Chrome inspect进行调试 但是国内开发者会出现404 Not Found错误 原因解析 国内网络无法访问 https
  • Java中九大内置对象

    1 Request对象 该对象封装了用户提交的信息 通过调用该对象相应的方法可以获取封装的信息 即使用该对象可以获取用户提交的信息 当Request对象获取客户提交的汉字字符时 会出现乱码问题 必须进行特殊处理 首先 将获取的字符串用ISO
  • 全国计算机等级考试题库二级C操作题100套(第91套)

    第91套 函数fun的功能是 计算请在程序的下划线处填入正确的内容并把下划线删除 使程序得出正确的结果 注意 源程序存放在考生文件夹下的BLANK1 C中 不得增行或删行 也不得更改程序的结构 给定源程序 include
  • Spring Cloud 2.2.2 源码之三十九nacos配置动态刷新原理一

    Spring Cloud 2 2 2 源码之三十九nacos配置动态刷新原理一 RefreshScope注解类实例化基本流程 nacos如何通过RefreshScope注解进行属性刷新 RefreshEventListener的handle
  • Selenium安装及环境配置

    目录 一 Selenium 简介 1 组件 2 特点 二 安装Selenium 三 下载对应版本的Chromedriver 1 查看Chrome的版本号 2 下载驱动 chromedriver和配置 3 解压到本地 4 复制文件放入pyth
  • 一次吃透Qt中信号与槽(包含信号与槽的使用,自定义以及重构示例,建议收藏)

    1 Qt中信号和槽 信号与槽 信号与槽 Signal Slot 是 Qt 编程的基础 也是 Qt 的一大创新 因为有了信号与槽的编程机制 在 Qt 中处理界面各个组件的交互操作时变得更加直观和简单 它可以让应用程序编程人员把这些互不了解的对
  • 2023领导力测评启示录

    导读 在现今这个由数据驱动的世界里 人力资源专业人士也越来越注重在进行人才选拔和发展时 运用客观数据来辅助决策 然而 面对市场上种类繁多的测评选择 首要挑战就是要了解不同类型的领导力测评 通常 测评主要分为两类 这两类测评的区别在于收集的数
  • Go语言实现区块链与加密货币-Part1(基本原型、工作量证明、持久化)

    区块链 Blockchain 是21世纪最具革命性的技术之一 它仍然处于不断成长的阶段 而且还有很多潜力尚未显现 作为比特币的底层技术 它本质上只是一个分布式数据库 不过使它独一无二的是 区块链是一个公开的而不是私人的数据库 每个使用它的人
  • 使用Arduino开发ESP32(22):蓝牙作为客户端使用

    文章目录 目的 基础准备 搜索蓝牙设备 搜索设备 信息查询 连接与交互 总结 目的 ESP32的蓝牙除了作为服务器 从设备 使用还可以作为客户端 主机 使用 这篇文章将对相关内容做个简单说明 基础准备 这篇文章中测试需要先准备一个蓝牙服务器