からめもぶろぐ。

俺たちは雰囲気で OAuth をやっている

C# で IBS-TH1 のバッテリー残量を取得する

blog.karamem0.dev

この記事の続き。ひとつ買ってなんとなくうまく稼働していたので、別の部屋のためにもうひとつ買ったりしていましたが、最初の個体が電池切れでお亡くなりになっているのに気付きました。だいたい 4 か月くらいですかね。ネットの情報では IBS-TH1 MINI のほうは 1 か月くらいしか持たないという情報もあるのでまあまあそんなものかなと。いずれにしても電池が切れるのに気付かないのもよくないのでバッテリー残量を取りたいよねという話。

スマートフォンのアプリではバッテリー残量を表示できているのでできなくはないことはわかっています。調べてみると GATT ではどうやってもバッテリー残量は取れなくて、アドバタイズで取るみたいです。あまり詳しくないので間違ってたら申し訳ないですが、アドバタイズと GATT の違いは UDP と TCP の違いみたいな感じでしょうか。アドバタイズはペリフェラル (今回でいうと IBS-TH1) が定期的にブロードキャストするパケットのことです。この Manufacture Specific というデータ領域にバッテリー残量が入っています。ただ、面倒なのは、アドバタイズにはパッシブ スキャンとアクティブ スキャンがあって、アクティブ スキャンでないと Manufacture Specific が入ってこないのと、Manufacture Specific の先頭 2 バイトの企業識別子は含まれないので、その点を考慮する必要があります。

言葉だけだとわからないのでコードを載せておきます。

public static class Program
{

    private const string MacAddress = "{{macaddress}}";

    private static async Task Main()
    {
        var watcher = new BluetoothLEAdvertisementWatcher();
        watcher.ScanningMode = BluetoothLEScanningMode.Active;
        watcher.Received += (sender, e) =>
        {
            var macAddress = string.Join(":",
                BitConverter.GetBytes(e.BluetoothAddress)
                    .Reverse()
                    .Select(x => x.ToString("X2")))
                .Substring(6);
            if (string.Equals(macAddress, MacAddress, StringComparison.OrdinalIgnoreCase) != true)
            {
                return;
            }
            var manifactureData = e.Advertisement
                .GetSectionsByType(BluetoothLEAdvertisementDataTypes.ManufacturerSpecificData)
                .Select(x => x.Data.ToArray()).FirstOrDefault();
            if (manifactureData == null)
            {
                return;
            }
            var t = (double)BitConverter.ToInt16(manifactureData, 0) / 100;
            var h = (double)BitConverter.ToInt16(manifactureData, 2) / 100;
            var b = manifactureData[7];
            Console.WriteLine("{0:s} 温度: {1}, 湿度: {2}, バッテリー: {3}", DateTime.Now, t, h, b);
        };
        watcher.Start();
        await Task.Delay(Timeout.Infinite);
    }

}

アクティブ スキャンの場合はアドバタイズ フレームの受信後にスキャン要求を送って追加の情報を取得する方法で、Manufacture Specific はこの追加の情報として入ってくるデータなので、BluetoothLEAdvertisementWatcher.Received イベントのすべてのイベント データに Manufacture Specific が入ってくるとは限りません。また、BluetoothLEAdvertisement.ManufacturerData プロパティは先頭 2 バイトを取った形でデータを返しますので、GetSectionsByType メソッドで生のデータを取ってくる必要があります。

実行するとこんな感じでデータが取れるはずです。

2021-09-05T16:17:26 温度: 27.36, 湿度: 62.99, バッテリー: 100
2021-09-05T16:21:32 温度: 27.35, 湿度: 62.99, バッテリー: 100
2021-09-05T16:21:34 温度: 27.35, 湿度: 62.99, バッテリー: 100
2021-09-05T16:21:36 温度: 27.35, 湿度: 62.99, バッテリー: 100
2021-09-05T16:21:40 温度: 27.35, 湿度: 62.99, バッテリー: 100
2021-09-05T16:22:33 温度: 27.35, 湿度: 62.99, バッテリー: 100

まあ確かに取れるには取れるのですが、取れるタイミングが非常に安定しない (上記の例だと 1 件目のあとに 3 分間空きがあってそのあと連続してデータが返ってくる) ため、扱いが非常に面倒くさい感じはありますね。