かねてから、接触確認API(Exposure Notifications API)は自作しないとあかんな、と思っていたのでおもむろに自作してみることにします。要は、接触確認アプリのテストがしにくい(EN API が有効な保健省アカウントしかできない)ので、一般サイドから見ると「きちんと動いているかどうかわからない」のが問題ですね。これは、COCOA 自体から EN API を触るときも同様で、去年の6月当初から検証しにくい環境であることができになっています。
で、EN API については内部的な細かい動作はさておき、仕様は Apple/Google の共同文書ということで公開されています。
Apple/Google の EN API インターフェースはさておき、もっと物理層に近いところの BLE 通信の部分と電文の暗号化は公開されているので、これを使うことでビーコンの発信と受信が可能です。
Windows 10 で BLE を受け取る
ビーコンというか、Bluetooth Low Energy の受信は、Apple の iBeacon が発表された 5年前ぐらいに非常に流行りました。なので、サンプルコード回りを探すと 4,5 年間前のものがたくさん出てきて、最近1,2年のものが引っかかりませんが大丈夫です。OS 等の環境は変わっているのですが、おおむね動きます。この「おおむね動く」というのが落とし穴で、OS が新しくなったり開発環境が変わったり(Android が Java から Kotlin へ iOS が Swift へとか)して、元のサンプルコードがそのままでうまく動かなかったりします。
Windows 10 の場合、実は Windows Runtime(UWP)のほうで BLE を自由に扱えるのですが、Windows 10 側で扱える方法はありません。なので、なので、Windows 10(いまから Windows 11 になるけど)の場合、ちょっとだけ工夫が必要です。
以前の .NET Framework の場合、UWP での Runtime を Windows 10 側で動かすときにちょっと工夫が必要だったのですが、.NET 5 以降は以下のように *.csproj に書けば ok です。
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net5.0-windows10.0.19041.0</TargetFramework>
</PropertyGroup>
</Project>
.NET 5 でプロジェクトを作成(.NET 6 も同じです)すると、*.csproj の中身が Microsoft.NET.Sdk を使ったものに切り替わります。ここの TargetFramework のところは通常は「net5.0」になっているのですが「net5.0-windows10.0.19041.0」のように、UWP の Win RT を含めたものにします。後ろのバージョンは、Windows 10 が動作しているバージョンにあわせます。
デスクトップ アプリで Windows ランタイム API を呼び出す – Windows apps | Microsoft Docs
これで、BLE を扱うための、BluetoothLEAdvertisementWatcher クラスが使えるようになります。
受信する BLE の種類
参考先:【BLE】GAP・GATTについて調べてみた – Qiita
あらためて、作ってみて自分の中でごちゃごちゃしていていたので、メモ的に整理しておきます。
- セントラル:BLE を受信する側
- ペリフェラル:BLE を送信する側
- GAP(Generic Access Profile):ペリフェラルから定期的に送信される電文、ブロードキャスト
- GATT、キャラクタリスティックス(characteristics):セントラル側からコネクトして、改めてペリフェラルから電文を得る(相互通信も)
以前、GATT がよくわからなかったのですが、コネクトするやつだったのですね。
なので、巷にあるサンプルはたいていは、GATT を使ってセントラルからコネクトして相互通信するサンプルがほとんどです。GAP で送る最初のデータは、アドバタイジングデータというのですが、これが 31 バイトで制限されています。最初のデータは検知用のブロードキャストなので、セントラルの ID か、iBeacon のようになんらかの UUID をのせて発信させておくという手法です。
で、EN API は、BLE で GAP なブロードキャストを定期的(2秒から5秒間隔ぐらい)で流し続けています。BLE が低電力なのは、通信が 200 msec 間隔のような短い間隔ではなくて、間欠的にデータを発信するからです。単純に考えれば1/10から1/50ぐらいの電力量で済むので、スマホの電源をあまり消費しない…はずなのですが。たぶん、発信のための電力よりも、Bluetooth の受信のほうに電池を使っているような気がします。
とりあえず、Windows 10 で COCOA なビーコンを受信する場合は、
- Windows 10 がセントラルになる
- ブロードキャスト(GAP)で発信されているデータ(アドバタイジングデータ)を受信する
という2つがあれば十分です。
BLE を受信する
ひたすら受信するだけなので、BluetoothLEAdvertisementWatcher クラスを使います。
using Windows.Devices.Bluetooth.Advertisement;
static BluetoothLEAdvertisementWatcher watcher;
static void Main(string[] args)
{
Console.WriteLine("CacaoBeacon Reciever");
// スキャンモードを設定
watcher = new BluetoothLEAdvertisementWatcher()
{
ScanningMode = BluetoothLEScanningMode.Passive
};
// スキャンしたときのコールバックを設定
watcher.Received += Watcher_Received;
// スキャン開始
watcher.Start();
// キーが押されるまで待つ
Console.WriteLine("Press any key to continue");
Console.ReadLine();
}
これは、動作確認のためコンソールアプリケーションで作っていますが、.NET 5 で作れば、WPF でも同じように作れます。多分、Windows フォームも同じです。
BLE のデータを検出するたびに Received イベントが呼び出されます。ひとつにつき1回呼び出されるので、まわりにビーコン(接触確認アプリ等)がたくさんあると、イベント先が溢れます。本格的に作るときは、受信データの処理などは別スレッドにする必要があります。
BLE のデータを処理する(アドバタイジングデータ)
受信したデータは、args.Advertisement の中に入っています。この Advertisement な中にいろいろなプロパティが設定してあって、ペリフェラル(接触確認アプリ)が送信してくるビーコンの中身を分解してくれます。
private static void Watcher_Received(
BluetoothLEAdvertisementWatcher sender,
BluetoothLEAdvertisementReceivedEventArgs args)
{
var uuids = args.Advertisement.ServiceUuids;
var mac = string.Join(":",
BitConverter.GetBytes(args.BluetoothAddress).Reverse()
.Select(b => b.ToString("X2"))).Substring(6);
var name = args.Advertisement.LocalName;
if (uuids.Count == 0) return;
if (uuids.FirstOrDefault(t => t.ToString() == "0000fd6f-0000-1000-8000-00805f9b34fb") == Guid.Empty) return;
// RPI を取得
byte[] rpi = null;
foreach (var it in args.Advertisement.DataSections)
{
if ( it.DataType == 0x16 && it.Data.Length >= 2 + 16)
{
byte[] data = new byte[it.Data.Length];
DataReader.FromBuffer(it.Data).ReadBytes(data);
if ( data[0] == 0x6f && data[1] == 0xfd)
{
rpi = data[2..18];
cbreceiver.Recv(rpi, DateTime.Now, args.RawSignalStrengthInDBm, args.BluetoothAddress);
}
}
}
実は、Advertisement(BluetoothLEAdvertisementクラス)は、さまざまなプロパティやコレクションを持っていますが、データの中身としては 31 バイトしかないので、たいしたことができるわけではありません。ちょうど C言語の Union のように構造体が相乗りになっている(データ構造は排他的になる)のを想像してください。
なので、以下のように DataSections と ManufacturerData を同時に取得して表示させていますが、これが同時に取れることはありません。データ量が 31 バイトしかないので、どちらかしか取れません。たいていのサンプルは ManufacturerData だけを扱います。ここで作成する COCOA からの受信は DataSections だけで十分です。
var dataSections = args.Advertisement.DataSections;
var manufactures = args.Advertisement.ManufacturerData;
Console.WriteLine($"dataSections count:{dataSections.Count}");
foreach (var it in dataSections)
{
Console.WriteLine($" type: {it.DataType.ToString("X2")} size: {it.Data.Length} data: {toHEX(it.Data)}");
byte[] data = new byte[it.Data.Length];
DataReader.FromBuffer(it.Data).ReadBytes(data);
}
Console.WriteLine($"manufactures count:{manufactures.Count}");
foreach (var it in manufactures)
{
Console.WriteLine($" size: {it.Data.Length} data: {toHEX(it.Data)}");
}
データは byte 配列になるので、DataReader とか BitConverter を活用します。
電文の中身を見るのに AD Type をみるのですが、この一覧は Advertising Data ・ BLEDocs を見てください。
EN API の Bluetooth 版を見ると、
- 0x01: Flag
- 0x03: Complete 16-bit Service UUID
- 0x16: Service Data
の3つだけ理解できれば ok です。0x16 は DataSections コレクションになっています。コレクションとはいえ、31 バイトしかないので、基本はひとつしかないです。この中に
- Exposure Notification Service の 16 bit UUID
- RPI(Rooling Proximity Identifier)が 16バイト
- メタデータが4バイト
が入っています。
一応先頭の 2バイト(16ビット)の UUID をチェックして、RPI の 16バイトを取得します。
後日解説しますが、0xFD6F の UUID は、Windows 10, Android, M5Stack では受信できるのですが、iPhone では受信できません。iOS の場合 OS 内部で、EN API の 0xFD6F だけ塞がれています。ちなみにここの UUID を変えれば、iOS でも受信ができます(通常のビーコンは通るということ)。
実際に受信する
動作するコードは https://github.com/openCACAO/CacaoBeacon/blob/main/src/consolerecv/Program.cs にあります。内部で、受信用の CacaoBeacon クラスを使っているので、ひとつ上の *.sln ごと git clone するとよいです。
実際に Windows 10 で動かすとこんな感じになります。
ビーコン発信する機器の MAC アドレスは 10 分程度で切り替わるランダム値になるので、継続監視はあまり意味はありません。RPI の値も 10 分間隔で切り替わります。
10分の間で、接触確認アプリを Windows 10 から遠ざけたり近づけたりすると、RSSI(電波強度)が変わります。consolerecv では連続受信した RPI を集約させて、開始時刻と終了時刻を保存するようにしてあります。
このツールをノートPCに入れて、駅前のスタバに行けば、COCOA のチェックができますね。まあ、できたところで何ができるかというとこれだけでは何もできないのですが。
それに、いちいちノートPCを見てコンソールで確認するのも大変です。WPF アプリに移植するのは後日やるとして、これを Android で動かせるようにします。