motoh's blog

主に趣味の電子工作やプログラミングについて書いていきます

IoT乾電池MaBeeeをESP32でコントロールしてみた

MaBeeeは、スマホアプリから電圧をコントロールすることができる乾電池で、これを使えば電車などのおもちゃをラジコンのようにコントロールすることができます。今回はこのMaBeeeを、スマホではなくESP32マイコンからコントロールできるようにしてみました。これにより、例えばMaBeeeとM5Stack(ESP32を搭載した小型デバイス)をおもちゃやロボットキットに組み込めば、はんだ付け等の手間をかけずに自動制御の改造ができそうです。

MaBeeeをBLEで制御する、ESP32のプログラム

MaBeeeはBluetooth LE (BLE)で制御できるように作られています。ESP32はBLEモジュールを内蔵しており、ライブラリを利用してプログラミングすることができます。以下がプログラムの全容です。ソースコード(Arduino用)も貼ります。

  • MaBeeeがBLEサーバ(ペリフェラル)なので、ESP32がBLEクライアントになります。
  • BLEデバイスをスキャンし、MaBeeeを見つけたら接続します。接続後はUUIDによりPWM制御のサービス、キャラクタリスティックを取得します。
  • 以降は、キャラクタリスティックを使い、1秒毎にPWM値を0~100%の間で少しずつ変化させながらWriteします(100までいった後は少しずつ小さくしていきたいので、sin関数を使っています)。

動いている様子を動画で紹介します。 MaBeeeにつないだテスターの電圧測定値が少しずつ変化しています。 ESP32デバイスとしてM5Stack(Timer Camera X)を使っています。

youtu.be

■BLEClient.ino

#include "BLEDevice.h"
#include <string.h>
#include <math.h>

#define SERVICE_UUID    "b9f5ff00-d813-46c6-8b61-b453ee2c74d9"
#define CHARACTERISTIC_UUID   "b9f53006-d813-46c6-8b61-b453ee2c74d9"    //pwm-duty
#define SERVER_NAME           "MaBeee018904"
#define M_PI  (3.141592)

static BLEUUID serviceUUID(SERVICE_UUID);
static BLEUUID charUUID(CHARACTERISTIC_UUID);

static BLEAddress *pServerAddress;
static BLEAdvertisedDevice* myDevice;
static boolean doConnect = false;
static boolean connected = false;
static BLERemoteCharacteristic* pRemoteCharacteristic;

bool connectToServer(BLEAddress pAddress) {
    Serial.print("Forming a connection to ");
    Serial.println(pAddress.toString().c_str());
    BLEClient* pClient = BLEDevice::createClient();
    pClient->connect(myDevice);
    Serial.print(" - Connected to server :");
    BLERemoteService* pRemoteService = pClient->getService(serviceUUID);
    Serial.print(" - Get service :");
    Serial.println(serviceUUID.toString().c_str());
    if(pRemoteService == nullptr){
      Serial.print("Failed to find our service UUID: ");
      Serial.println(serviceUUID.toString().c_str());
      return false;
    }
    pRemoteCharacteristic = pRemoteService->getCharacteristic(charUUID);
    Serial.print(" - Found our characteristic UUID: ");
    Serial.println(charUUID.toString().c_str());
    if (pRemoteCharacteristic == nullptr) {
      Serial.print("Failed to find our characteristic UUID: ");
      return false;
    }
    Serial.println(" - Found our characteristic");
}

//
// スキャンでデバイスを見つけたときのコールバック
//
class MyAdvertisedDeviceCallbacks: public BLEAdvertisedDeviceCallbacks {
  void onResult(BLEAdvertisedDevice advertisedDevice) {
    Serial.print("BLE Advertised Device found: ");
    Serial.println(advertisedDevice.toString().c_str());
    String serverName = advertisedDevice.getName().c_str();
    Serial.println(serverName);

    //目的のデバイスだったら接続し、スキャンを終了
    if(serverName.equals(SERVER_NAME)){ 
      Serial.println(advertisedDevice.getAddress().toString().c_str());
      advertisedDevice.getScan()->stop();
      pServerAddress = new BLEAddress(advertisedDevice.getAddress());
      myDevice = new BLEAdvertisedDevice(advertisedDevice);
      doConnect = true;
    }
  }
};

//
// 起動後一度だけ呼ばれる処理
//
void setup() {
  pinMode(5, OUTPUT);
  Serial.begin(115200);
  BLEDevice::init("");
  BLEScan* pBLEScan = BLEDevice::getScan();
  Serial.println("getScan");
  pBLEScan->setAdvertisedDeviceCallbacks(new MyAdvertisedDeviceCallbacks());
  Serial.println("setAdvertisedDeviceCallbacks");
  pBLEScan->setActiveScan(true);
  pBLEScan->start(10);
  Serial.println("");
  Serial.println("End of setup");
}

//
// メインループ処理
//
void loop() {
  static float w = M_PI * 3 / 2;
  uint8_t duty = 0;
  uint8_t command[5];
  
  if (doConnect == true) {
    if (connectToServer(*pServerAddress)) {
      Serial.println("We are now connected to the BLE Server.");
      connected = true;
    } else {
      Serial.println("We have failed to connect to the server; there is nothin more we will do.");
      connected = false;
    }
    doConnect = false;
  }

  // PWM値を少しずつ増減させる
  if (connected) {
    w += M_PI / 10;
    if(w > M_PI * 2){
      w = 0;
    }
    duty = (sin(w) + 1.0) * 100.0 / 2.0;
    command[0] = 0x01;
    command[1] = duty;
    command[2] = 0x00;
    command[3] = 0x00;
    command[4] = 0x00;

    pRemoteCharacteristic->writeValue(command, 5);
    Serial.println(duty);
  } else{
    Serial.println("Not connected");
    doConnect = true;
  }
  delay(1000);
}

余談

MaBeeeのBLEの仕様は公開されていなかったため、自力で解析する必要がありました。 具体的には、上記プログラムで使用している以下の情報を調べる必要がありました。

  • サーバ名 (SERVER_NAME)
  • サービスUUID (SERVICE_UUID)
  • キャラクタリスティックUUID (CHARACTERISTIC_UUID)
  • PWM値をWriteする際のコマンドフォーマット "0x01 XX 00 00 00" (XX : 0x00~0x64のPWM値)

まずはこちらの記事を参考に、サーバ名とUUIDを調べました。

BLE(Bluetooth Low Energy)を人力で解析する - Qiita

しかし、コマンドフォーマットは勘を頼りにいろいろ試しましたが、まったくわからず...

そこで次に、こちらの記事を参考に、ラズパイ+micro:bitでスニファを構成し、Mabeeeとスマホアプリ「MaBeeeコントロール」の間の通信をのぞき見することで、ようやくコマンドフォーマットを推測することができました。

snifferでBLEをキャプチャする->Micro:Bitでなんとかなるかと - chakokuのブログ(rev4)

その他参考サイト

ESP32によるBLEクライアントの作成方法については、以下の記事を参考にさせていただきました。

ESP32のArduino IDEでBLEを試してみました – 計測・解析 ラボ

ESP32・BLEクライアントの開発

ESP32によるBLEスキャンとアルプスのセンサネットワークモジュール/SensorTagからのセンサーデータ取得 | TomoSoft