我们使用BLE (Bluetooth Low Energy,蓝牙低功耗) 对轮足机器人控制。
具体的BLE技术细节,请参考蓝牙技术联盟编写的蓝牙协议栈手册:
https://www.bluetooth.com/learn-about-bluetooth/tech-overview/。

建立一次BLE连接需要如下步骤:
- 服务器端初始化蓝牙连接,初始化蓝牙设备
- 创建服务器和对应的BLE服务
- 配置TX/RX特征值
- 启动服务,开始广播
- 手机扫描蓝牙并连接
- 通信
- 如断开,尝试重连
接下来,我们将解释代码中的核心函数功能。
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;
}
- 初始化EEPROM:首先启动EEPROM,用于读取之前保存的蓝牙名称。
- 读取保存的蓝牙名称:尝试从EEPROM中读取之前保存的蓝牙名称。如果读取成功(包括魔数验证、长度和字符验证),则使用保存的名称,并加上前缀"XGORider_"组成完整设备名称;否则,使用传入的默认设备名称。
- 初始化BLE设备:使用完整的设备名称初始化BLE设备。
- 创建BLE服务器:创建BLE服务器,并设置连接回调(用于处理连接和断开事件)。
- 创建BLE服务:使用预定义的SERVICE_UUID创建一个BLE服务。
- 创建特征(Characteristic):
- 创建TX特征(UUID为CHARACTERISTIC_UUID_TX),属性包括读、通知和指示,并添加一个BLE2902描述符(为了支持通知和指示)。
- 创建RX特征(UUID为CHARACTERISTIC_UUID_RX),属性为写,并设置回调函数,当App发送数据时会触发回调。
- 启动服务:启动BLE服务。
- 开始广播:
- 获取广告对象,设置服务UUID,设置扫描响应为true,并设置推荐参数(0x06和0x12)。
- 开始广播,等待客户端连接。
- 打印日志:输出服务启动信息和设备名称,并等待客户端连接。
- 返回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);
}
}
}
- 检查是否已连接和特征是否存在,如果没有连接或特征未初始化,则直接返回。
- 考虑到BLE的MTU(最大传输单元)限制,将数据分包发送。这里设置每个包最大500字节(因为MTU为503,减去3个字节的开销)。
- 循环发送数据,每次发送一个数据块,然后调用notify()方法将数据发送给手机。
- 在发送每个数据包之间,如果还有剩余数据,则延迟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());
}
}
}
- 获取写入的数据。
- 检查数据长度,如果大于0则处理。
- 解析数据,判断是否是读取版本号的请求(特定指令),如果是则抑制日志输出(避免频繁打印版本号请求)。
- 如果不是读取版本号的请求,则打印接收到的字节数。
- 调用用户设置的回调函数,将数据传递给上层应用程序处理。
