BLEでESP32をセントラルとして使用した時にハマったこと備忘録

セントラルの役割を持ったESP32が接続しにいかない

BLEでESP32同士で通信を行いたいと思い、ペリフェラルとセントラルをそれぞれ用意したのですが、なぜかセントラルの方からの接続がうまくいかない事象が発生しました。

うまく行った方

class MyAdvertisedDeviceCallbacks : public BLEAdvertisedDeviceCallbacks
{
  void onResult(BLEAdvertisedDevice advertisedDevice)
  {
    Serial.printf("Advertised Device: %s \n", advertisedDevice.toString().c_str());
    if (advertisedDevice.haveServiceUUID() && advertisedDevice.isAdvertisingService(BLEUUID(SERVICE_UUID)))
    {
      Serial.println("Found our device! Connecting...");
      BLEDevice::getScan()->stop();
      targetDevice = new BLEAdvertisedDevice(advertisedDevice);
    }
  }
};
void loop()
{
if (pClient->isConnected() == false)
  {
    pClient->connect(targetDevice);
    BLERemoteService *pRemoteService = pClient->getService(BLEUUID(SERVICE_UUID));
    delay(1000);
    if (pRemoteService != nullptr)
    {
      Serial.println("Found our service");
      pRemoteCharacteristic = pRemoteService->getCharacteristic(BLEUUID(CHARACTERISTIC_UUID));
    }
  }
  String value = "Hello World!";
  if (pClient->isConnected() && pRemoteCharacteristic != nullptr)
  {
    pRemoteCharacteristic->writeValue(value.c_str(), value.length());
  }
  delay(1000);
}

ダメだった方

class MyAdvertisedDeviceCallbacks : public BLEAdvertisedDeviceCallbacks
{
  void onResult(BLEAdvertisedDevice advertisedDevice)
  {
    Serial.printf("Advertised Device: %s \n", advertisedDevice.toString().c_str());
    if (advertisedDevice.haveServiceUUID() && advertisedDevice.isAdvertisingService(BLEUUID(SERVICE_UUID)))
    {
      Serial.println("Found our device! Connecting...");
      BLEDevice::getScan()->stop();
      pClient->connect(&advertisedDevice);
      delay(1000);
      BLERemoteService *pRemoteService = pClient->getService(BLEUUID(SERVICE_UUID));
      if (pRemoteService != nullptr)
      {
        Serial.println("Found our service");
        pRemoteCharacteristic = pRemoteService->getCharacteristic(BLEUUID(CHARACTERISTIC_UUID));
      }
    }
  }
};
void loop()
{
  String value = "Hello World!";
  if (pClient->isConnected() && pRemoteCharacteristic != nullptr)
  {
    pRemoteCharacteristic->writeValue(value.c_str(), value.length());
  }
  delay(1000);
}

何がダメだったか

どちらもAdvertiseを探すscanが走った後に呼ばれるonResultで得られたサービスをpClientにメモリアドレスとして渡す形になっていて、大きく違いはなさそうですが、一つ違うのがtargetDeviceをヒープで持たせる形になっていることです。

ダメな方でも、接続処理自体が走るのはコールバック内なので、ヒープでなくとも問題なさそうですが、接続自体が非同期で行われる関係で、接続時には参照してるメモリアドレスが破棄されていていて、無効な接続を試みてしまう・・ということになってしまっていた様です。

onResultの寿命とonConnectの非同期な実行タイミングを考えると、サービスの情報をメンバとして持たせてloop側で接続するのが無難みたいです。

おまけ:シリアルを読み取ってBLEでWriteしたい時のTips

Serialを受け取って、上記で接続したBLEペリフェラルにセントラルとして書き込みたい(SerialをBLEで中継したい)時、普通に考えるとloop内で以下の方法を取ります。

void loop(){
if (pClient->isConnected() && pRemoteCharacteristic != nullptr)
  {
    if (Serial.available())
    {
      String value = Serial.readString();
      Serial.println("Sending: " + value);
      pRemoteCharacteristic->writeValue(value.c_str(), value.length());
    }
  }
}

ただしこの方法だと以下の様なエラーが出て書き込みに失敗する場合があります。

[BLERemoteCharacteristic.cpp:289] retrieveDescriptors(): esp_ble_gattc_get_all_descr: Unknown

ネタバラシしてしまうと、これはwriteValueが完了する前にwriteValueが呼ばれてしまっているのが原因でした。BLEの初回の書き込み時にディスクリプタの取得処理(retrieveDescriptors())が呼ばれるのですが、この取得処理中にwriteValueが呼ばれてしまうと、このエラーが発生してしまいます。

Serial.available()はSerialにバッファが溜まっていると毎回呼ばれてしまうのと、Serialが高速で送られてしまっていたのが今回の根本原因で、読み出しが完全に完了した上で、適切なタイミングでwriteValueしてあげるのが良さそうです。

簡単な解決策としては、(そもそも)Serialを大量に高速に送らない、Serial.readStringUntil(‘\n’);などを使ってあげるなどがありそうです。

ちゃんとやるなら、BLEの書き込み完了のコールバックを待ってから次のを書き込む、などでしょうか。

当たり前の話ですが、BLEは速く低レイテンシーとはいえ、Serialの逐次書き込み周期では到底送れないので、この辺りはケアしてあげる必要を強く感じました。

コメントする