iBeacon 受信の実際 Windows + C# 版

iBeacon のような BLE アドバタイズや、 GATT サービスが色々飛んでいます、というのをチェックするためのツールは BLE 通信チェックツールのあれこれ https://www.moonmile.net/blog/archives/11961 に書いたわけですが、これだとスマホにアプリを入れてあちこち探すという程度のことしかできません。逆に言えば、実験的に iBeacon の UUID は決めてあるので、それだけ受信できれば良いのです。

これは iBeacon や EN API(COCOA のアドバタイズ)の発信チェックに使えます。何かスマホや M5Stack で作成 iBeacon の発信ツールを作ったときに、本当に発信できているのか、あるいは、発信していないのか(電力消費を抑えるために定期的に停止する場合もあります)を確認するのに使います。

iBeacon の電文フォーマットはわかっているので、これに沿って実装します。

Windows の場合は、BLE を使うのに UWP アプリケーションのライブラリを使います。以前は色々面倒だったのですが、TargetFramework に ‘net10.0-windows10.0.22621.0’ のように指定すれば大丈夫です。これで BLE を受信するための BluetoothLEAdvertisementWatcher クラスが使えるようになります。

using System;
using System.Collections.Generic;
using System.Linq;
using Windows.Devices.Bluetooth.Advertisement;
using Windows.Storage.Streams;

// BLEのスキャナ
BluetoothLEAdvertisementWatcher watcher;

Main(args);

void Main(string[] args)
{
    Console.WriteLine("Folkbears iBeaconCheck");

    watcher = new BluetoothLEAdvertisementWatcher()
    {
        ScanningMode = BluetoothLEScanningMode.Passive
    };
    // スキャンしたときのコールバックを設定
    watcher.Received += Watcher_Received;
    // スキャン開始
    watcher.Start();
    // キーが押されるまで待つ
    Console.WriteLine("Press any key to continue");
    Console.ReadLine();
}

void Watcher_Received(
    BluetoothLEAdvertisementWatcher sender,
    BluetoothLEAdvertisementReceivedEventArgs args)
{
    var mac = string.Join(":",
                BitConverter.GetBytes(args.BluetoothAddress).Reverse()
                .Select(b => b.ToString("X2"))).Substring(6);
    var rssi = args.RawSignalStrengthInDBm;
    var time = args.Timestamp.ToString("yyyy/MM/dd HH:mm:ss.fff");
    

    if ( args.Advertisement.ManufacturerData.Count > 0)
    {
        var data = args.Advertisement.ManufacturerData[0];
        if ( data.CompanyId == 0x004c && data.Data.Length >= 23)
        {
            byte[] ibeacon = new byte[data.Data.Length];
            DataReader.FromBuffer(data.Data).ReadBytes(ibeacon);
            if (ibeacon[0] == 0x02 && ibeacon[1] == 0x15)
            {
                byte[] uuid = ibeacon[2..18];
                byte[] major = ibeacon[18..20];
                byte[] minor = ibeacon[20..22];
                byte txpower = ibeacon[22];

                int majorvalue = major[0] * 256 + major[1];
                int minorvalue = minor[0] * 256 + minor[1];
                Console.WriteLine($"{time} [{tohex(uuid)}] {rssi} dBm {mac} "
                    + string.Format("{0:x04}", majorvalue) + " "
                    + string.Format("{0:x04}", minorvalue) + " "
                    );
            }
        }
    }
    string tohex( byte[] data )
    {
        return BitConverter.ToString(data).Replace("-", "").ToLower();
    }
}

コンソール出力のためにあれこれやっていますが、受信だけなれば数十行で済みます。

1. BluetoothLEAdvertisementWatcher クラスのインスタンスを作成する
2. ScanningMode プロパティを Passive に設定する
3. Received イベントにコールバック関数を設定する
4. Start メソッドでスキャンを開始する

コールバック内は

1. args.Advertisement.ManufacturerData プロパティで、メーカー固有データを取得する
2. CompanyId プロパティで Apple の 0x004c か確認する
3. Data プロパティでデータ本体を取得する
4. iBeacon フォーマットに沿ってデータを解析する

iBeacon を判別するのは ManufacturerData の先頭にある

  • Apple Company ID (0x004c)
  • iBeacon Type (0x02)
  • iBeacon Length (0x15)

の部分でチェックします。

iBeacon 自体の UUID を取得して、どこで発信されたものかを記録していきます。ここでは、iBeacon ならば何でも受信していますが、大抵は目的の UUID つまり、既にアプリが知っている UUID だけを受信する設計のはずです。
FolkBers の場合は、ひとつの UUID を使って、Major と Minor で場所を固有ID(Temp ID)にして使っています。

dotnet run で実行すると、コンソールに出力します。

BluetoothLEScanningMode.Passive と Active の違いは、Active の場合はデバイス名要求などの追加のパケットを送信することです。iBeacon の場合は受信だけで良いので Passive で十分です。GATT でコネクションをするときは Active にします。

Windows では Scan Window を変えられるのか?

BluetoothLEAdvertisementWatcher クラスには Scan Window を変えるプロパティはありません。なので、受信精度を変更することができないので、実測して受信状態を確認することになります。

ちなみに Android の場合では SCAN_MODE_LOW_POWER や SCAN_MODE_BALANCED のように代替ですがスキャン頻度を変えることができます。以前、バッテリー消費を抑えるために SCAN_MODE_LOW_POWER を使ったことがありますが、遅延が多かった覚えがります。これは後で再試験。

m5stack のような ESP32 ベースのマイコンを使えば、細かく指定ができます。

void setup() {
  BLEDevice::init("");
  BLEScan *scan = BLEDevice::getScan();
  scan->setInterval(0x00A0); // 100ms / 0.625ms
  scan->setWindow(0x0050);   // 50ms / 0.625ms
  scan->setActiveScan(false); // 受信のみなら false, スキャン応答も欲しければ true
  scan->start(0, nullptr, false); // 0=無期限
}

Scan Window の実験をする場合は、こっちで確認したほうが良さそうです。これは後で作成してみます。確か、iBeacon の発信機は作ってみたけど、受信機は作ったことがないので。

カテゴリー: 開発, FolkBears パーマリンク

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

*