Fusic Tech Blog

Fusion of Society, IT and Culture

M5StickCでiBeaconをスキャンする
2020/02/24

M5StickCでiBeaconをスキャンする

こんにちは、岡嵜です。

年末から「人を検知する」というテーマでいろいろ試行錯誤をしています。
「超音波センサー」「人感センサー」などを試してきましたが、これだと検知した人が誰なのか区別するのが難しいです。

そこで、「ビーコン」のようなものを人に持たせて、誰を検知したか区別することができないか検討してみました。

iBeaconとは

iBeaconとはAppleが2013年に発表した、BLE(Bluetooth Low Energy)を使ったビーコンの一種です。

片方の端末がUUIDをブロードキャストで送信(アドバタイズ)し、もう片方の端末がそれを受信することで誰が接近したか検知するというものです。

例えば、ショッピングモールなどで、3つの地点にiBeaconの発信機を設置し、スマートフォンアプリでこれを受信することで、付近の店舗のセール情報等を通知することができます。

スマートフォンでiBeaconを受信

この場合、スマートフォンは必ずしも受信側である必要は無く、送信・受信が逆にすることで、スマートフォンの動きをトレースする、といったことも可能です。

スマートフォンでiBeaconを送信

検証内容

上記を踏まえ、今回はM5StickCを使ってスマートフォンからのアドバタイズを受信し、UUIDやアドレス、RSSIを取得する検証を行いました。

M5StickCでiBeaconを受信

iBeaconのアドバタイズパケット

iBeaconのパケットは下図のようになっています。

iBeaconのアドバタイズパケット

パケットを受信したら以下を検証します。

受信したパケットの先頭2バイトは「Info Header」と呼ばれるもので、BLEの場合は「0x1A, 0xFF」となります。ここまではBLEのライブラリで判定してくれているようです。

次の2バイトが「Apple Company ID」で、これは「0x4C00」です。さらに次の2バイトが「iBeacon識別子」であり「0x1502」となっているはずです。

ここまでの検証で特に問題が無ければiBeaconのアドバタイズパケットと判断できるので、以降の処理に進みます。

アドバタイズパケットから情報を読み出す

アドレス

アドレスはコールバック関数の引数となっている BLEAdvertisedDevicegetAddress() を呼び出すことで取得できます。

RSSI

RSSIは BLEAdvertisedDevicegetRSSI() を呼び出すことで取得できます。

UUID

UUIDは BLEAdvertisedDevicegetManufacturerData() を呼び出すことで製造者特有情報を取得し、先頭から4バイト目以降に格納されています。

プログラム

参考までに作成したプログラムを載せておきます。おそらくM5Stackでも同じプログラムで動作すると思われます。

#include <BLEDevice.h>
#include <BLEUtils.h>
#include <BLEScan.h>
#include <BLEAdvertisedDevice.h>

class IBeaconAdvertised: public BLEAdvertisedDeviceCallbacks {
  public:
    // BLE検出時のコールバック
    void onResult(BLEAdvertisedDevice device) {
      if (!isIBeacon(device)) {
        return;
      }
      printIBeacon(device);
    }

  private:
    // iBeaconパケット判定
    bool isIBeacon(BLEAdvertisedDevice device) {
      if (device.getManufacturerData().length() < 25) {
        return false;
      }
      if (getCompanyId(device) != 0x004C) {
        return false;
      }
      if (getIBeaconHeader(device) != 0x1502) {
        return false;
      }
      return true;
    }

    // CompanyId取得
    unsigned short getCompanyId(BLEAdvertisedDevice device) {
      const unsigned short* pCompanyId = (const unsigned short*)&device
                                         .getManufacturerData()
                                         .c_str()[0];
      return *pCompanyId;
    }

    // iBeacon Header取得
    unsigned short getIBeaconHeader(BLEAdvertisedDevice device) {
      const unsigned short* pHeader = (const unsigned short*)&device
                                      .getManufacturerData()
                                      .c_str()[2];
      return *pHeader;
    }

    // UUID取得
    String getUuid(BLEAdvertisedDevice device) {
      const char* pUuid = &device.getManufacturerData().c_str()[4];
      char uuid[64] = {0};
      sprintf(
        uuid,
        "%02X%02X%02X%02X-%02X%02X-%02X%02X-%02X%02X-%02X%02X%02X%02X%02X%02X",
        pUuid[0], pUuid[1], pUuid[2], pUuid[3], pUuid[4], pUuid[5], pUuid[6], pUuid[7],
        pUuid[8], pUuid[9], pUuid[10], pUuid[11], pUuid[12], pUuid[13], pUuid[14], pUuid[15]
      );
      return String(uuid);
    }

    // TxPower取得
    signed char getTxPower(BLEAdvertisedDevice device) {
      const signed char* pTxPower = (const signed char*)&device
                                    .getManufacturerData()
                                    .c_str()[24];
      return *pTxPower;
    }

    // iBeaconの情報をシリアル出力
    void printIBeacon(BLEAdvertisedDevice device) {
      Serial.printf("addr:%s rssi:%d uuid:%s power:%d\r\n",
                    device.getAddress().toString().c_str(),
                    device.getRSSI(),
                    getUuid(device).c_str(),
                    *(signed char*)&device.getManufacturerData().c_str()[24]);
    }
};

void setup() {
  Serial.begin(115200);
  BLEDevice::init("");
}

void loop() {
  Serial.println("start.");
  BLEScan* scan = BLEDevice::getScan();
  scan->setAdvertisedDeviceCallbacks(new IBeaconAdvertised(), true);
  scan->setActiveScan(true);
  scan->start(60);
  Serial.println("complete.");
}

動作確認

PCの準備

今回はMacbookを使っての確認だったので、 screen を使ってM5StickCにシリアル接続しました。

screen /dev/tty.usbserial-7D52762E45 115200

スマートフォンの準備

手元にiPhoneが無かったのでBLEに対応しているAndroid端末を使用しました。

以下のアプリをインストールして、iBeaconのアドバタイズを行うよう設定します。

Beacon Simulator

ちなみに、このアプリを入れて試しに1日中アドバタイズを行ってみましたが、体感バッテリ消費への影響はほとんど無さそうでした。さすがBLE。

Beacon Simulator

M5StickCの電源をON

電源をONすると、スマートフォンからアドバタイズされたiBeaconをM5StickCが受信し、シリアル経由でPCのTerminalへ出力します。

start.
addr:XX:XX:XX:XX:XX:XX rssi:-68 uuid:0D9FE9FD-C47C-4100-AD82-FFABEBC6FDB1 power:-63
addr:XX:XX:XX:XX:XX:XX rssi:-65 uuid:0D9FE9FD-C47C-4100-AD82-FFABEBC6FDB1 power:-63
addr:XX:XX:XX:XX:XX:XX rssi:-66 uuid:0D9FE9FD-C47C-4100-AD82-FFABEBC6FDB1 power:-63
addr:XX:XX:XX:XX:XX:XX rssi:-65 uuid:0D9FE9FD-C47C-4100-AD82-FFABEBC6FDB1 power:-63
addr:XX:XX:XX:XX:XX:XX rssi:-65 uuid:0D9FE9FD-C47C-4100-AD82-FFABEBC6FDB1 power:-63
addr:XX:XX:XX:XX:XX:XX rssi:-70 uuid:0D9FE9FD-C47C-4100-AD82-FFABEBC6FDB1 power:-63
addr:XX:XX:XX:XX:XX:XX rssi:-66 uuid:0D9FE9FD-C47C-4100-AD82-FFABEBC6FDB1 power:-63
(省略)
complete.

ちなみに screen を起動したままだとプログラムが書き込めません。 control + aky の順でキー入力して終了してください。

トラブルシューティング

プログラムを書き込むときに「serial.serialutil.SerialException: [Errno 16] could not open port /dev/cu.usbserial-xxxxxxxxxx」が発生する

前述のとおり、 screen を起動したままであるケースが多いです。シリアル通信を切断してから再度書き込んでみてください。

まとめ

以上、M5StickCでiBeaconを受信する方法でした。

これを使うことで、IoTシステムの開発がますます捗りそうです。

参考

yuuu

yuuu

2018年の年明けに組込み畑からやってきた、2児の父 兼 Webエンジニアです。 mockmockの開発・運用を担当しており、組込みエンジニア時代の経験を活かしてデバイスをプログラミングしたり、簡易的なIoTシステムを作ったりしています。主な開発言語はRuby、時々Go。