基于 ESP32-WROOM-32 实践 BLE 同时作为服务器和客户端 – PlatformIO

最近更新于 2026-08-18 21:18

测试环境

  • ESP32-WROOM-32
  • PlatformIO IDE 3.3.4
  • PlatformIO Core 6.1.19
  • pioarduino platform-espressif32 55.03.311(Arduino Release v3.3.11 based on ESP-IDF v5.5.5)

platformio.ini

[env:esp32dev]
platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.311/platform-espressif32.zip
board = esp32dev
framework = arduino
monitor_speed = 115200

实践

源码参考来源:https://github.com/espressif/arduino-esp32/blob/3.3.11/libraries/BLE/examples/Client_Server/Client_Server.ino
手机 BLE 调试参考:https://blog.iyatt.com/?p=25142

/*
 * BLE 客户端与服务端共存示例(ESP32)
 *
 * 功能说明:
 * 演示如何在同一台 ESP32 设备上同时运行 BLE 服务端 (Server) 与客户端 (Client) 功能。
 * - 作为服务端:广播自定义服务,监听并响应外部设备的读写请求,定期发送通知 (Notify)。
 * - 作为客户端:扫描周围设备,发现匹配的服务 UUID 后自动发起连接,并读写远程特征值。
 */

#include <Arduino.h>
#include <BLEDevice.h>
#include <BLEUtils.h>
#include <BLEServer.h>
#include <BLEClient.h>
#include <BLEScan.h>

// ==================== 1. 全局配置与 UUID 定义 ====================

// 服务端使用的自定义服务与特征值 UUID
#define SERVER_SERVICE_UUID "4fafc201-1fb5-459e-8fcc-c5c9c331914b"
#define SERVER_CHARACTERISTIC_UUID "beb5483e-36e1-4688-b7f5-ea07361b26a8"

// 客户端搜寻的目标 UUID(此处设置为与服务端相同的 UUID,方便两台 ESP32 互联)
static BLEUUID clientServiceUUID(SERVER_SERVICE_UUID);
static BLEUUID clientCharUUID(SERVER_CHARACTERISTIC_UUID);

// 服务端对象指针
BLEServer *pServer = nullptr;                       // BLE 服务端实例
BLECharacteristic *pServerCharacteristic = nullptr; // BLE 服务端特征值实例

// 客户端对象指针及状态标志
static boolean doConnect = false;                      // 是否准备开始连接标志位
static boolean clientConnected = false;                // 客户端连接状态标志位
static BLERemoteCharacteristic *pRemoteCharacteristic; // 远程服务器特征值对象指针
static BLEAdvertisedDevice *targetDevice;              // 扫描到的目标设备指针
static BLEClient *pClient = nullptr;                   // BLE 客户端实例
BLEScan *pBLEScan = nullptr;                           // BLE 扫描器实例

// ==================== 2. 回调函数类定义 ====================

/**
 * @brief 服务端连接/断开事件回调类
 */
class ServerCallbacks : public BLEServerCallbacks
{
    // 当有客户端连接到本 ESP32 服务端时触发
    void onConnect(BLEServer *pServer)
    {
        Serial.println("服务端:有客户端成功连接");
    }

    // 当连接到本服务端的客户端断开时触发
    void onDisconnect(BLEServer *pServer)
    {
        Serial.println("服务端:客户端已断开连接");
        // 客户端断开后重新开启广播,以便其他设备再次连接
        BLEDevice::startAdvertising();
    }
};

/**
 * @brief 服务端特征值读写事件回调类
 */
class CharacteristicCallbacks : public BLECharacteristicCallbacks
{
    // 当外部客户端向本特征值写入数据时触发
    void onWrite(BLECharacteristic *pCharacteristic)
    {
        String value = pCharacteristic->getValue();
        Serial.print("服务端:收到写入数据,内容为: ");
        Serial.println(value.c_str());
    }

    // 当外部客户端读取本特征值时触发
    void onRead(BLECharacteristic *pCharacteristic)
    {
        Serial.println("服务端:特征值被外部设备读取");
    }
};

/**
 * @brief 客户端连接/断开事件回调类
 */
class ClientCallbacks : public BLEClientCallbacks
{
    // 当本客户端成功连接到远程服务器时触发
    void onConnect(BLEClient *pClient)
    {
        Serial.println("客户端:成功连接到远程服务器");
        clientConnected = true;
    }

    // 当与远程服务器断开连接时触发
    void onDisconnect(BLEClient *pClient)
    {
        Serial.println("客户端:与远程服务器断开连接");
        clientConnected = false;
    }
};

/**
 * @brief 客户端接收 Notify(通知)数据的回调函数
 * @param pBLERemoteCharacteristic 触发通知的特征值指针
 * @param pData 数据内容指针
 * @param length 数据字节长度
 * @param isNotify 是否为 Notify(通知)模式
 */
static void notifyCallback(BLERemoteCharacteristic *pBLERemoteCharacteristic, uint8_t *pData, size_t length, bool isNotify)
{
    Serial.print("客户端:收到特征值 ");
    Serial.print(pBLERemoteCharacteristic->getUUID().toString().c_str());
    Serial.print(" 的通知,数据长度: ");
    Serial.println(length);
    Serial.print("客户端:收到数据: ");
    Serial.write(pData, length);
    Serial.println();
}

/**
 * @brief 客户端扫描发现新设备时的回调类
 */
class AdvertisedDeviceCallbacks : public BLEAdvertisedDeviceCallbacks
{
    // 每扫描到一个广播设备时都会调用此方法
    void onResult(BLEAdvertisedDevice advertisedDevice)
    {
        Serial.print("客户端:扫描到设备: ");
        Serial.println(advertisedDevice.toString().c_str());

        // 判断扫描到的设备是否包含我们正在寻找的目标服务 UUID,且当前未处于连接状态
        if (advertisedDevice.haveServiceUUID() && advertisedDevice.isAdvertisingService(clientServiceUUID) && !clientConnected)
        {

            Serial.println("客户端:找到包含目标服务的设备,停止扫描并准备建立连接...");
            BLEDevice::getScan()->stop();                             // 停止扫描以释放 BLE 资源
            targetDevice = new BLEAdvertisedDevice(advertisedDevice); // 暂存设备信息
            doConnect = true;                                         // 设置标志位,告知 loop() 发起连接
        }
    }
};

// ==================== 3. 核心功能函数 ====================

/**
 * @brief 客户端发起连接与服务搜寻的函数
 * @return true 连接并成功获取特征值;false 连接或搜寻失败
 */
bool connectToServer()
{
    Serial.print("客户端:正在尝试连接到设备 ");
    Serial.println(targetDevice->getAddress().toString().c_str());

    // 如果客户端未创建则实例化,已创建则复用
    if (pClient == nullptr)
    {
        pClient = BLEDevice::createClient();
        pClient->setClientCallbacks(new ClientCallbacks());
        Serial.println("客户端:已创建新的 BLEClient 实例");
    }
    else
    {
        Serial.println("客户端:复用现有的 BLEClient 实例");
    }

    // 发起对目标的物理连接
    if (!pClient->connect(targetDevice))
    {
        Serial.println("客户端:建立连接失败");
        return false;
    }

    Serial.println("客户端:物理连接建立成功");
    pClient->setMTU(517); // 请求最大 MTU (传输单元) 提升数据传输效率

    // 在远程服务器上搜寻目标服务
    BLERemoteService *pRemoteService = pClient->getService(clientServiceUUID);
    if (pRemoteService == nullptr)
    {
        Serial.print("客户端:未能找到指定服务 UUID: ");
        Serial.println(clientServiceUUID.toString().c_str());
        pClient->disconnect();
        return false;
    }
    Serial.println("客户端:已找到目标服务");

    // 在目标服务下获取对应的特征值
    pRemoteCharacteristic = pRemoteService->getCharacteristic(clientCharUUID);
    if (pRemoteCharacteristic == nullptr)
    {
        Serial.print("客户端:未能找到指定特征值 UUID: ");
        Serial.println(clientCharUUID.toString().c_str());
        pClient->disconnect();
        return false;
    }
    Serial.println("客户端:已找到目标特征值");

    // 如果特征值支持“读”属性,尝试读取一次初始值
    if (pRemoteCharacteristic->canRead())
    {
        String value = pRemoteCharacteristic->readValue();
        Serial.print("客户端:读取到的初始特征值: ");
        Serial.println(value.c_str());
    }

    // 如果特征值支持“通知”属性,注册 Notify 回调函数
    if (pRemoteCharacteristic->canNotify())
    {
        pRemoteCharacteristic->registerForNotify(notifyCallback);
        Serial.println("客户端:已成功订阅 Notify 通知");
    }

    return true;
}

/**
 * @brief 初始化配置 BLE 服务端
 */
void setupServer()
{
    Serial.println("正在配置 BLE 服务端...");

    // 创建服务端实例并设置回调
    pServer = BLEDevice::createServer();
    pServer->setCallbacks(new ServerCallbacks());

    // 创建服务
    BLEService *pService = pServer->createService(SERVER_SERVICE_UUID);

    // 创建特征值(授予 读、写、通知 属性)
    pServerCharacteristic = pService->createCharacteristic(
        SERVER_CHARACTERISTIC_UUID,
        BLECharacteristic::PROPERTY_READ |
            BLECharacteristic::PROPERTY_WRITE |
            BLECharacteristic::PROPERTY_NOTIFY);

    // 设置特征值回调与初始值
    pServerCharacteristic->setCallbacks(new CharacteristicCallbacks());
    pServerCharacteristic->setValue("Hello from Coexistence Server");

    // 启动服务
    pService->start();

    // 配置并启动 BLE 广播
    BLEAdvertising *pAdvertising = BLEDevice::getAdvertising();
    pAdvertising->addServiceUUID(SERVER_SERVICE_UUID); // 将服务端 UUID 添加到广播数据中
    pAdvertising->setScanResponse(true);               // 开启扫描响应
    pAdvertising->setMinPreferred(0x06);               // 设置连接间隔偏好值(优化 iOS 连接兼容性)
    pAdvertising->setMinPreferred(0x12);

    BLEDevice::startAdvertising();

    Serial.println("服务端:广播已启动");
}

/**
 * @brief 初始化配置 BLE 客户端扫描器
 */
void setupClient()
{
    Serial.println("正在配置 BLE 客户端...");

    // 配置扫描器属性
    pBLEScan = BLEDevice::getScan();
    pBLEScan->setAdvertisedDeviceCallbacks(new AdvertisedDeviceCallbacks());
    pBLEScan->setActiveScan(true); // 主动扫描模式(消耗更多功率,但可获得扫描响应)
    pBLEScan->setInterval(100);    // 扫描间隔 (ms)
    pBLEScan->setWindow(99);       // 扫描窗口 (ms)

    Serial.println("客户端:扫描器配置完成");
}

// ==================== 4. Arduino 主入口 ====================

void setup()
{
    Serial.begin(115200);
    Serial.println("启动 BLE 客户端-服务端共存示例...");

    // 全局初始化 BLE 设备并命名
    BLEDevice::init("ESP32-Coexistence");

    // 分别初始化服务端和客户端
    setupServer();
    setupClient();

    // 启动首次扫描(扫描 10 秒,非持续重复模式)
    pBLEScan->start(10, false);

    Serial.println("初始化完成!设备正在作为服务端广播,同时作为客户端扫描...");
}

void loop()
{
    static unsigned long lastServerUpdate = 0;
    static unsigned long lastClientWrite = 0;
    static unsigned long lastScanStart = 0;
    unsigned long currentTime = millis();

    // --- 逻辑 1:处理客户端连接请求 ---
    if (doConnect && !clientConnected)
    {
        if (connectToServer())
        {
            Serial.println("客户端:已成功连接至远程服务端");
        }
        else
        {
            Serial.println("客户端:连接失败,准备重新启动扫描");
            pBLEScan->start(10, false); // 连接失败时重新开启扫描
        }
        doConnect = false; // 重置触发标志
    }

    // --- 逻辑 2:服务端定期更新并推送数据(每 5 秒)---
    if (currentTime - lastServerUpdate > 5000)
    {
        String value = "Server time: " + String(millis() / 1000);
        pServerCharacteristic->setValue(value.c_str());
        pServerCharacteristic->notify(); // 向所有已连接本服务端的设备发送数据通知
        Serial.print("服务端:更新特征值并发送 Notify: ");
        Serial.println(value);

        lastServerUpdate = currentTime;
    }

    // --- 逻辑 3:客户端定期向远程服务器写入数据(每 3 秒)---
    if (clientConnected && pRemoteCharacteristic && currentTime - lastClientWrite > 3000)
    {
        if (pRemoteCharacteristic->canWrite())
        {
            String clientValue = "Client msg: " + String(millis() / 1000);
            pRemoteCharacteristic->writeValue(clientValue.c_str(), clientValue.length());
            Serial.print("客户端:向远程特征值写入数据: ");
            Serial.println(clientValue);
            lastClientWrite = currentTime;
        }
    }

    // --- 逻辑 4:未连接时定期重新触发扫描(每 15 秒)---
    if (!clientConnected && currentTime - lastScanStart > 15000)
    {
        Serial.println("客户端:重新开始扫描周围设备...");
        pBLEScan->start(10, false);
        lastScanStart = currentTime;
    }

    delay(100); // 适当主循环延迟,避免主线程过载
}

在作为服务器的同时也可以作为客户端连接别的设备
file

基于 ESP32-WROOM-32 实践 BLE 同时作为服务器和客户端 – PlatformIO
Scroll to top
打开目录