Windows10デバイスでiBeaconの全データを取得する方法

IoTデバイス(?)として、ビーコン(iBeacon)という小さなデバイスがあります。
使い方としては顧客が近づくとビーコンから出ている電波をキャッチして、その場所にあった情報をスマートフォンなどに発信するといった利用方法が主なのですが、登場したころは対応機器がiPhoneだけでしたので、Windowsの世界は置いてけぼりとなっていました。
(※Androidはいつの間にか対応済み)

「Windows10デバイスでもiBeaconのデータを使った処理がしたい!」
ということで、調べました。

実際、ビーコンの仕組みとしては「Bluetooth Low Energy」(BLE)が使われていただけなので、Bluetoothに対応されていれば通信は可能だったはずでした。
ですが、調べたところ通信規格でかなり特殊なことを行っていたため、通常の方法ではうまく取得できませんでした。

そもそも当初Bluetoothはペアリング(接続後に相互通信)を前提としたように作られており、「一方通行」の情報発信用には作られていなかったのです。
(ペアリングの通信規格を拡張して無理やり処理した感じです。なので、ビーコンとペアリングできるけど、ひたすらエラーとなる。。。)

それが、やっとWindows10で対応が進んできました。
使うのは「Windows.Devices.Bluetooth.Advertisement」(Windows.Winmdの参照が必要)の名前空間になります。
(※Windows10 PreviewのころからGitHUBに存在はしていたが、まともに動かなかった。。。)

このクラスを利用することで、ビーコン特有の「アドバタイズメント・パケット」を取得、解析することが出来ます。

C#のコード

using System;
using Windows.Devices.Bluetooth.Advertisement;
using Windows.Storage.Streams;

public class winBeacon
{
    private BluetoothLEAdvertisementWatcher watcher;
    static void Main()
    {
        watcher = new BluetoothLEAdvertisementWatcher();
        watcher.Received += OnAdvertisementReceived;//ここのイベント内で取得する
        watcher.Start();
        Console.WriteLine("終了するには何かキーを押してください...");
        Console.ReadKey();
        watcher.Stop();
    }

    private void OnAdvertisementReceived(BluetoothLEAdvertisementWatcher watcher, BluetoothLEAdvertisementReceivedEventArgs eventArgs)
    {
        //イベントの値から取得
        DateTimeOffset timestamp = eventArgs.Timestamp;
        BluetoothLEAdvertisementType advertisementType = eventArgs.AdvertisementType;
        string rssi = eventArgs.RawSignalStrengthInDBm;
        string name = eventArgs.Advertisement.LocalName;

        Console.WriteLine(string.Format("timestamp:{0}", timestamp.ToString("HH\\:mm\\:ss\\.fff")));
        Console.WriteLine(string.Format("rssi:{0}", rssi.ToString("D")));
    }
}

単純にはこのような形で簡単にRSSI(受信信号強度)の値などが取得できます。

ですが、iPhoneやAndroidのAPIではもっとさまざまな下記の値が取得できるようになっています。

  • UUID(Universally Unique Identifier): ビーコンに設定されてい128bitの値
  • メジャー値:ビーコンに設定されている整数
  • マイナー値:ビーコンに設定されている整数
  • measuredPower: 1mでのRSSI強度
  • RSSI: 受信信号強度
  • Accuracy: measuredPowerとRSSIから計算したビーコンとの距離(精度)
  • proximity: ビーコンと距離(unknown(不明), immediate(すぐそこ), near(近い), far(遠い))

iPhoneやAndroidと共通のクロスプラットフォームアプリを作ろうと思ったら全然足りません。
なので、なんとか自力で専用クラスを作成して取得できるようにしました。

C#のコード

using System;
using Windows.Devices.Bluetooth.Advertisement;
using Windows.Storage.Streams;

public class iBeacon
{
    private const int MinimumLengthInBytes = 25;//最小の長さ
    private const int AdjustedLengthInBytes = -2;//CompanyID分の2桁ずれている為読み取り位置補正

    //プロパティ
    public string Name { get; set; }
    public DateTimeOffset Timestamp { get; set; }
    public BluetoothLEAdvertisementType AdvertisementType { get; set; }

    public int ManufacturerId { get; set; }
    public int Major { get; set; }
    public int Minor { get; set; }
    public string UUID { get; set; }
    public short Rssi { get; set; }
    public short MeasuredPower { get; set; }
    public double ManufacturerReserved { get; set; }

    //精度(accuracy)
    public double Accuracy {
        get { return calcAccuracy(MeasuredPower, Rssi); }
    }

    //近接度(Proximity):近接(immidiate)、1m以内(near)、1m以遠(far)、不明(Unknown)
    public string Proximity {
        get {

            string _Proximity = "Unknown";

            //Rssi未取得ならUnknown
            if (Rssi == 0) { return _Proximity; }

            //rssi値からProximityを判別
            if (Rssi > -40)
            {
                _Proximity = "immidiate";//近接
            }
            else if (Rssi > -59)
            {
                _Proximity = "near";//1m以内
            }
            else
            {
                _Proximity = "far";//1m以遠
            }
            return _Proximity;
        }
    }

    //コンストラクタ
    public iBeacon(){
        ManufacturerId = -1;
        Major = -1;
        Minor = -1;
        Rssi = 0;
        UUID = "";
        MeasuredPower = -1;
        ManufacturerReserved = -1.0;
    }

    //コンストラクタ2
    public iBeacon(BluetoothLEAdvertisementReceivedEventArgs eventArgs){

        //出力されているbyteデータから各値を抽出する
        var manufacturerSections = eventArgs.Advertisement.ManufacturerData;
        Timestamp = eventArgs.Timestamp;
        AdvertisementType = eventArgs.AdvertisementType;

        if (manufacturerSections.Count > 0)
        {
            var manufacturerData = manufacturerSections[0];
            var data = new byte[manufacturerData.Data.Length];

            iBeacon bcon = new iBeacon();

            using (var reader = DataReader.FromBuffer(manufacturerData.Data))
            {
                reader.ReadBytes(data);
            }

            //長さをチェック
            if (data == null || data.Length < MinimumLengthInBytes + AdjustedLengthInBytes)
            {
                return;
            }

            //イベントから取得
            Rssi = eventArgs.RawSignalStrengthInDBm;
            Name = eventArgs.Advertisement.LocalName;
            ManufacturerId = manufacturerData.CompanyId;

            //バイトデータから抽出
            //公式での出力値(Windowsでは2byteずれているので補正が必要)
            // Byte(s)  WinByte(s) Name
            // --------------------------
            // 0-1      none       Manufacturer ID (16-bit unsigned integer, big endian)
            // 2-3      0-1        Beacon code (two 8-bit unsigned integers, but can be considered as one 16-bit unsigned integer in little endian)
            // 4-19     2-17       ID1 (UUID)
            // 20-21    18-19      ID2 (16-bit unsigned integer, big endian)
            // 22-23    20-21      ID3 (16-bit unsigned integer, big endian)
            // 24       22         Measured Power (signed 8-bit integer)
            // 25       23         Reserved for use by the manufacturer to implement special features (optional)

            //BigEndianの値を取得
            UUID = BitConverter.ToString(data, 4 + AdjustedLengthInBytes, 16); // Bytes 2-17
            MeasuredPower = Convert.ToSByte(BitConverter.ToString(data, 24 + AdjustedLengthInBytes, 1), 16); // Byte 22

            //もし追加データがあればここで取得
            if (data.Length >= MinimumLengthInBytes + AdjustedLengthInBytes + 1)
            {
                ManufacturerReserved = data[25 + AdjustedLengthInBytes]; // Byte 23
            }

            //.NET FramewarkのEndianはCPUに依存するらしい
            if (BitConverter.IsLittleEndian)
            {
                //LittleEndianの値を取得
                byte[] revData;
                
                revData = new byte[] { data[20 + AdjustedLengthInBytes], data[21 + AdjustedLengthInBytes] };// Bytes 18-19
                Array.Reverse(revData);
                Major = BitConverter.ToUInt16(revData, 0); 

                revData = new byte[] { data[22 + AdjustedLengthInBytes], data[23 + AdjustedLengthInBytes] };// Bytes 20-21
                Array.Reverse(revData);
                Minor = BitConverter.ToUInt16(revData, 0); 
            }
            else
            {
                //BigEndianの値を取得
                Major = BitConverter.ToUInt16(data, 20 + AdjustedLengthInBytes); // Bytes 18-19
                Minor = BitConverter.ToUInt16(data, 22 + AdjustedLengthInBytes); // Bytes 20-21
            }
        } else {
        	new iBeacon();
        }
    }
    
    //精度を計算する
    protected static double calcAccuracy(short measuredPower, short rssi)
    {
        if (rssi == 0)
        {
            return -1.0; //nodata return -1.
        }

        double ratio = rssi * 1.0 / measuredPower;
        if (ratio < 1.0)
        {
            return Math.Pow(ratio, 10);
        }
        else
        {
            double accuracy = (0.89976) * Math.Pow(ratio, 7.7095) + 0.111;
            return accuracy;
        }
    }
}

これをこのようにして使います。

C#のコード

    private void OnAdvertisementReceived(BluetoothLEAdvertisementWatcher watcher, BluetoothLEAdvertisementReceivedEventArgs eventArgs)
    {
        //プロパティ値から取得
        DateTimeOffset timestamp = eventArgs.Timestamp;
        BluetoothLEAdvertisementType advertisementType = eventArgs.AdvertisementType;

        iBeacon bcon = new iBeacon(eventArgs);

        string retBeaconData;
        retBeaconData = "{";
        retBeaconData += string.Format("uuid:'{0}',", bcon.UUID);//"00000000-0000-0000-0000-000000000000"
        retBeaconData += string.Format("major:{0},", bcon.Major.ToString("D"));
        retBeaconData += string.Format("minor:{0},", bcon.Minor.ToString("D"));
        retBeaconData += string.Format("measuredPower:{0},", bcon.MeasuredPower.ToString("D"));
        retBeaconData += string.Format("rssi:{0},", bcon.Rssi.ToString("D"));
        retBeaconData += string.Format("accuracy:{0},", bcon.Accuracy.ToString("F6"));
        retBeaconData += string.Format("proximity:'{0}'", bcon.Proximity);
        retBeaconData += "}";
        
        Console.WriteLine(string.Format("timestamp:{0}", timestamp.ToString("HH\\:mm\\:ss\\.fff")));
        Console.WriteLine(retBeaconData);
    }

2バイト分ずれていたとか、BigEndianとLittieEndianを判定して処理しなければいけないとか、すごい罠だらけでしたが、何とかこれで取得できます。

※環境によって異なる動作になるかもしれません。訂正、指摘などあればコメントをお願いします。

Search index:
[iBeacon] Getting packet major and minor ID in Windows 10 Bluetooth LE Advertisement
[iBeacon] Convert the Bluetooth LE Advertising bytes packet of Windows 10

この記事をシェアする
Tweet about this on TwitterShare on Facebook9

Comments

Comments