5. BLE控制

我们使用BLE (Bluetooth Low Energy,蓝牙低功耗) 对轮足机器人控制。

具体的BLE技术细节,请参考蓝牙技术联盟编写的蓝牙协议栈手册:

https://www.bluetooth.com/learn-about-bluetooth/tech-overview/

1763709573439-15a1db60-a475-4718-9d27-06a575c22874.png

建立一次BLE连接需要如下步骤:

  1. 服务器端初始化蓝牙连接,初始化蓝牙设备
  2. 创建服务器和对应的BLE服务
  3. 配置TX/RX特征值
  4. 启动服务,开始广播
  5. 手机扫描蓝牙并连接
  6. 通信
  7. 如断开,尝试重连

接下来,我们将解释代码中的核心函数功能。

5.1 核心程序

首先我们需要在宏定义中配置蓝牙服务的UUID,注意需要与APP端保持一致。我们需要配置三个UUID:首先是服务的UUID,其次是TX和RX的UUID。然后配置EEPROM的参数,以及BLE_Magic(魔数)。

bool BleAppHandler::begin(const char* deviceName) {
    Serial.println("[BLE App] Initializing BLE service...");

    // 初始化EEPROM
    EEPROM.begin(EEPROM_SIZE);

    // 尝试从EEPROM读取保存的蓝牙名称
    char savedName[32] = {0};
    char fullDeviceName[32] = {0};

    if (loadBLENameFromEEPROM(savedName, sizeof(savedName))) {
        // 使用保存的名称,添加前缀
        snprintf(fullDeviceName, sizeof(fullDeviceName), "XGORider_%s", savedName);
        Serial.printf("[BLE App] Loaded name from EEPROM: %s\n", fullDeviceName);
    } else {
        // 使用默认名称
        strncpy(fullDeviceName, deviceName, sizeof(fullDeviceName) - 1);
        Serial.printf("[BLE App] Using default name: %s\n", fullDeviceName);
    }

    // 初始化BLE设备
    BLEDevice::init(fullDeviceName);

    // 创建BLE服务器
    pServer = BLEDevice::createServer();
    pServer->setCallbacks(new ServerCallbacks(this));

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

    // 创建TX特征(ESP32发送数据给App)
    pTxCharacteristic = pService->createCharacteristic(
        CHARACTERISTIC_UUID_TX,
        BLECharacteristic::PROPERTY_READ |
        BLECharacteristic::PROPERTY_NOTIFY |
        BLECharacteristic::PROPERTY_INDICATE
        );
    pTxCharacteristic->addDescriptor(new BLE2902());

    // 创建RX特征(ESP32接收App的数据)
    pRxCharacteristic = pService->createCharacteristic(
        CHARACTERISTIC_UUID_RX,
        BLECharacteristic::PROPERTY_WRITE
        );
    pRxCharacteristic->setCallbacks(new CharacteristicCallbacks(this));

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

    // 开始广播
    BLEAdvertising* pAdvertising = BLEDevice::getAdvertising();
    pAdvertising->addServiceUUID(SERVICE_UUID);
    pAdvertising->setScanResponse(true);
    pAdvertising->setMinPreferred(0x06);
    pAdvertising->setMinPreferred(0x12);
    BLEDevice::startAdvertising();

    Serial.printf("[BLE App] Service started, device name: %s\n", deviceName);
    Serial.println("[BLE App] Waiting for client connection...");

    return true;
}
  1. 初始化EEPROM:首先启动EEPROM,用于读取之前保存的蓝牙名称。
  2. 读取保存的蓝牙名称:尝试从EEPROM中读取之前保存的蓝牙名称。如果读取成功(包括魔数验证、长度和字符验证),则使用保存的名称,并加上前缀"XGORider_"组成完整设备名称;否则,使用传入的默认设备名称。
  3. 初始化BLE设备:使用完整的设备名称初始化BLE设备。
  4. 创建BLE服务器:创建BLE服务器,并设置连接回调(用于处理连接和断开事件)。
  5. 创建BLE服务:使用预定义的SERVICE_UUID创建一个BLE服务。
  6. 创建特征(Characteristic)
    • 创建TX特征(UUID为CHARACTERISTIC_UUID_TX),属性包括读、通知和指示,并添加一个BLE2902描述符(为了支持通知和指示)。
    • 创建RX特征(UUID为CHARACTERISTIC_UUID_RX),属性为写,并设置回调函数,当App发送数据时会触发回调。
  7. 启动服务:启动BLE服务。
  8. 开始广播
    • 获取广告对象,设置服务UUID,设置扫描响应为true,并设置推荐参数(0x06和0x12)。
    • 开始广播,等待客户端连接。
  9. 打印日志:输出服务启动信息和设备名称,并等待客户端连接。
  10. 返回true:表示初始化成功。

5.2 发送数据到手机APP

void BleAppHandler::sendData(const uint8_t* data, size_t length) {
    if (!deviceConnected || pTxCharacteristic == nullptr) {
        return;
    }

    // 分包发送(如果数据超过MTU-3)
    const size_t maxChunkSize = 500;  // MTU 503 - 3
    size_t offset = 0;

    while (offset < length) {
        size_t chunkSize = min(maxChunkSize, length - offset);
        pTxCharacteristic->setValue((uint8_t*)(data + offset), chunkSize);
        pTxCharacteristic->notify();
        offset += chunkSize;

        // 短暂延迟,确保数据包能够正确发送
        if (offset < length) {
            delay(20);
        }
    }
}
  1. 检查是否已连接和特征是否存在,如果没有连接或特征未初始化,则直接返回。
  2. 考虑到BLE的MTU(最大传输单元)限制,将数据分包发送。这里设置每个包最大500字节(因为MTU为503,减去3个字节的开销)。
  3. 循环发送数据,每次发送一个数据块,然后调用notify()方法将数据发送给手机。
  4. 在发送每个数据包之间,如果还有剩余数据,则延迟20毫秒,保证数据包能够正确发送,避免连续发送导致的数据丢失。

5.3 特征写入回调

void BleAppHandler::CharacteristicCallbacks::onWrite(BLECharacteristic* pCharacteristic) {
    std::string value = pCharacteristic->getValue();

    if (value.length() > 0) {
        // 解析是否为“读取版本号(0x02, addr=0x07)”请求,若是则不打印
        bool suppressLog = false;
        if (value.length() >= 8) {
            const uint8_t* buf = (const uint8_t*)value.data();
            if (buf[0] == 0x55 && buf[1] == 0x00) {
                uint8_t cmdType = buf[3];
                uint8_t address = buf[4];
                if (cmdType == 0x02 && address == 0x07) {
                    suppressLog = true;
                }
            }
        }
        if (!suppressLog) {
            Serial.printf("[BLE App] Received %d bytes\n", value.length());
        }

        // 调用用户回调
        if (bleHandler->dataCallback != nullptr) {
            bleHandler->dataCallback((const uint8_t*)value.data(), value.length());
        }
    }
}
  1. 获取写入的数据。
  2. 检查数据长度,如果大于0则处理。
  3. 解析数据,判断是否是读取版本号的请求(特定指令),如果是则抑制日志输出(避免频繁打印版本号请求)。
  4. 如果不是读取版本号的请求,则打印接收到的字节数。
  5. 调用用户设置的回调函数,将数据传递给上层应用程序处理。