既に COCOA も廃止になっていて、実測することはできないのですが、EN API のアドバタイズ受信機も Windows + C# で作成できます。当時は Android を使って受信させていたのですが、iBeacon の受信機と同じように Windows で作ることができます。
電文フォーマットがわかっているので、受信データを解析すればよいだけです。iBeacon とは違って、16 bit のサービス UUID(0xFD6F)を持っているので、これを選別してデータを受信します。
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("COCOA Check");
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 uuids = args.Advertisement.ServiceUuids;
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 (uuids.Count == 0) return;
// 0xFD6F は Exposure Notification のサービスUUID
if (uuids.FirstOrDefault(t => t.ToString() == "0000fd6f-0000-1000-8000-00805f9b34fb") == Guid.Empty) return;
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)
{
byte[] rpi = data[2..18];
Console.WriteLine($"{time} [{tohex(rpi)}] {rssi} dBm {mac}");
}
}
}
string tohex( byte[] data )
{
return BitConverter.ToString(data).Replace("-", "").ToLower();
}
}
1. args.Advertisement.ServiceUuids で Service UUID 0xFD6F を確認します。 2. args.Advertisement.DataSections で AD Type 0x16(Service Data – 16-bit UUID)を探します。 3. DataSections(Service Data) から Service Data UUID 0xFD6F を探し出します。 4. 見つかった Service Data から RPI 情報を取り出します
BluetoothLEAdvertisementWatcher で受信するときに、args.Advertisement.ServiceUuids とか args.Advertisement.DataSections のように複数の Service UUID が取れるような感じになっていますが、BLE アドバタイズは 32 bytes までしかないので、実質 1 つしか送れません。何故こうなっているのかわからないのですが、拡張のためでしょうか? 確か、GATT の場合は Service UUID をペリフェラルに問い合わせるモードがあるのでそのためかもしれません。
この 16 bit Service UUID を使ったフォーマットは、Bluetooth SIG で 16 bit Company ID を持った会社であれば自由に作れるので、iBeacon のように色々な用途で使えます。温度センサーをアドバタイズで発信するとか、位置情報を発信するとか、そういう用途です。GATT コネクションの Notify よりも手軽ので、受信側を問わなければ結構使い道があると思うのですが、いまのところあまり使われていないようです。と云うか、受信機をアプリ等で作らないといけないので、何か専門用途に使っていてあまり外部流出しないのでしょう。EN API の場合のように一般的なアプリが受信するというパターンは珍しいのだと思います。
受信データを全部見る場合
Android の場合は ScanCallback を使って受信した生データを取得することができるのですが、BluetoothLEAdvertisementWatcher クラスでは生データという形でみることはできません。ただし、Advertisement.DataSections を使って中身をみることができます…というのは既に書いてあるのですが、すべての AD セクションを表示するコードは以下のようになります。
// できるだけ raw data を全部表示する場合
foreach (var section in args.Advertisement.DataSections)
{
byte[] buf = new byte[section.Data.Length];
DataReader.FromBuffer(section.Data).ReadBytes(buf);
Console.WriteLine($"AD 0x{section.DataType:X2} len={buf.Length} data={BitConverter.ToString(buf)}");
}
AD 0x01 len=1 data=1A
AD 0x03 len=2 data=6F-FD
AD 0x16 len=22 data=6F-FD-01-01-01-01-02-02-02-02-03-03-03-03-04-04-04-04-05-05-05-05
FROM openjdk:11
# Set the location of the verticles
ENV VERTICLE_HOME /usr/verticles
# Set the name of the verticle to deploy
ENV VERTICLE_AWARE_JAR micro-1.0.0-SNAPSHOT-fat.jar
ENV VERTICLE_AWARE_CONFIG aware-config.json
EXPOSE 8080
# Set vertx option
ENV VERTX_OPTIONS ""
# Copy your verticle and configuration to the container
COPY $VERTICLE_AWARE_JAR $VERTICLE_HOME/
COPY $VERTICLE_AWARE_CONFIG $VERTICLE_HOME/
WORKDIR $VERTICLE_HOME
ENTRYPOINT ["sh", "-c"]
CMD ["exec java -jar $VERTICLE_AWARE_JAR"]
CREATE TABLE IF NOT EXISTS `$table`
(
`_id` INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`timestamp` DOUBLE NOT NULL,
`device_id` VARCHAR(128) NOT NULL,
`data` JSON NOT NULL,
INDEX `timestamp_device` (`timestamp`, `device_id`)
)
技術系の原稿を書くときに、何を使っているかと言うと最近はもっぱら vscode + markdown 形式です。まあ、markdown 形式のみで済むかと言うとそうではなくて、原稿の提出前に Word に貼り付けてチェックをしています。もともと Word の文書チェックの機能を使っていたのと、画面キャプチャのチェックに見た感じのものが必須だからですね。以前はテキストのみで書いていたことも多いのですが、提出した後に画像ファイル番号のずれが多くて、なかなか大変だったので事前に見た目でチェックしようという発想があります。つまりは、私が原稿を書くときに画像ファイル番号を間違えているわけですが。 最近では、markdown 形式のプレビュー機能を使ってチェックする場合も多いのですが、図の矢印とか吹き出しとかもあるので、やはり Word に貼り付けることが多いです。
なので、BLE アドバタイズ自身は Data types specification の書式に従っていれば自由に BLE アドバタイズのデータ送信が可能です。実際、Apple の iBeacon のフォーマットや、Google の Eddystone のフォーマットもこの書式に従っています。おそらく BLE 端末の機器メーカーも流しているのかはこれかな、と思うのですが、定かではありません。
BLE アドバタイズのフォーマットは自由に決められるということは解ったのですが、接触確認アプリのように既存の OS や BLE ライブラリを使う場合にはそれほど自由ではありません。と言いまうsか、Apple の場合は iBeacon フォーマットしか使えません。Android の場合は、もう少し自由に作れますが、iPhone との共同運用を考えると iBeacon フォーマットしか使えないというのが現実的な選択肢です。
もちろん、m5stack とか ESP32 を使て独自の BLE アドバタイズのフォーマットを使えば自由に作れます。Android の場合も多少の制限がありますが、Android 同士であれば結構自由に作れます。これは先行き解説していきます。
BLE の規格としては、Manufacturer Specific Data を示す 0xFF のフラグと、その後の Company ID までで、その後は各社自由に作れます。自由に作れるといっても、アドバタイズのデータ全体が 31 バイトまでの制限なので、さらに小さくなります。
さらに、Company ID 自体は Bluetooth SIG が管理しているので、お金を払っている Apple 社は 16 ビット(2バイト)の Company ID を持っているのですが、実験的には 0xFFFF などを使わないといけません。 このあたりの細かいところは、後で ESP32 などで実装するときに詳しく解説する予定です。すくなくとも m5stack などの BLE ライブラリを使うと、かなり自由に BLE アドバタイズのフォーマットが作れるので iBeacon フォーマットにこだわる必要はありません。
iBeacon フォーマットの区切りがややこしいのですが、最初の Lnetgh + Flag の組みあわせは BLE 規格のほうで、 Manufacturer ID 以降の、Sub Type + Sub Length のほうの組み合わせは iBeacon 規格を出している Apple 独自の仕様です。ここで、Lenth と Type が逆になっているのはそのせいです。これは当悩んだのですが、つまりは内部規格ということです。
他にも Google の定義する AltBeacon フォーマットとか Eddystone フォーマットとかもありますが、これはまた別に解説します。同じ Beacon フォーマットとはいえ、ちょっとずつ違っているのは各社設定してしまっているからです。
とはいえ、Beacon タグからのデータは Apple だげが受信するものでもなく、Beacon タグの作成自体も Apple だけが作るわけでもありません。
iBeacon UUID: 90FA7ABEFAB6485EB7001A17804CAA13 が取得できればひとまず ok です。 このデータ自体、Major や Minor、Tx Power も取得できるので、iBeacon フォーマットとしては正しいことがわかります。 実は、実測すると分かるのですが 3 バイト目のFlag データの 0x1A の部分は、iBeacon 発信機によって異なります。ここは BLE アドバタイズに仕様なので、先の iBeacon 仕様の例のように同じ値になるとは限りません。つまりは、内部的に Maniufacturer Specific Data の部分は同じ Apple の iBeacon の規格を使っているとしても前後の BLE のフラグは発信する機種/メーカーによって異なります。
2020年当時に日本の保健所が逼迫していたのが、感染者の聞き取り調査の部分です。聞き取り自体がアナログでしかできないし、その追跡自体も人海戦術のようでした。基本的な感染症数理モデルの SIR 型モデルを知ったのもこの頃ですが、統計学のそれも興味の範囲ではあったけど、直接 IT 技術者として手が付けられそうなのは、この接触確認アプリの近接データの扱いです。
結論から先に言うと、Goolge/Apple の Exposure Notification API を使った場合には二世代前の接触者などを追跡することはできません。サーバーに集められるのは、Temporary Exposure Keys (TEK) と呼ばれる感染者の端末が生成した一時的な鍵情報なだけで、接触自体はアプリ内で確認することになるからです。 これは個人情報保護としてはいいのですが、先にあるように保健所のトレーシング業務を支援することは不可能なわけです。ある意味 EN API の設計の失敗(実用度の失敗でもありますが)はここにあると思っています。
1. A で感染者が発覚する 2. A にヒアリングして、過去の B の集団が感染が疑われる 3. A にヒアリングして、最近の C の集団が感染が疑われる 4. B の感染疑いから、遡って C の感染疑いが発覚する 5. 過去の D の集団は、2 週間(感染期間)より前なので除外する 6. 未来の E の集団は、2 週間以内なので感染疑いとする 7. 未来の F の集団は、2 週間後なので除外する
基本的に感染者 A が発生したときは、その周辺の人達(家族や会社の同僚など)に注意喚起をするわけで、ちょっと前に合っていた B の集団に注意を喚起するわけです。 同時に、感染者 A がうろうろ歩き回ることを考えると、将来的に接触しそうな E に集団にも中期喚起が必要なわけです。まあ、新型コロナウィルスの場合は監禁ということになっていたので、うろうろ歩き回ってはいけないのですが。このあたりは、接触確認アプリの目的としてはどうなのでしょう、というところがあります。
で、実際のところ機能していたのは B の集団に対する注意喚起であって、感染者 A が感染登録をすると、B の集団の持っているスマホに「感染者に接触した可能性あります」という通知が送られるというのが EN API の基本的な仕組みです。 プライバシーの観点から、誰が感染したかどうかは分からないようになっています。が、「感染者に接触した可能性があります」という文言自体があまりにも曖昧なので、結果的にどう扱っていよいか分からなかったのが当時の実情です。結果的に「接触確認アプリが使えない」という批判を浴びたのもこういうところでしょう。
実際のところ、突き合わせは集団 B の人達が持っているスマホ内で照合が行われるので、集団 B の人達が感染者 A がその人であることを知ることはできません。この制限は、あったほうが良いかったのか、ないほうが良かったのかは不明ですが、EN API の設計としてはこうなっているわけです。
で、保健所でトレースをとっているのは、
1. 感染者 A が集団 B に感染させている可能性 2. 感染者 A が集団 B の誰かに感染させられた可能性
の二つがあります。新型コロナウィルスの場合、無症状状態があってその間に感染させている期間があるという前提となっているので(実はこれはインフルエンザでもあることが最近知られています)、感染者 A は自分が感染していることを知らずに、集団 B の誰かに感染させている可能性がある、という想定です。と、同時に、集団 B が既に感染をしていてその誰かからか感染させられたという可能性です。 これは、感染したという事実は、必ずしも保健所に報告されないことがあるためです。あるいは、無症状のまま感染している状態もあるので、集団 B には潜在的な感染者がいるという可能性を考えるわけです。
新型コロナウィルス自体は、人から人に伝播するわけですから、感染者 A が集団 B に感染させられたときには、その以前いた集団 C にも感染者がいる可能性があります。 こんな風に、感染の経路を保健所の職員がヒアリングして突き止めていたわけですが…果たして、接触確認アプリはどのくらい貢献できたでしょうか?
というか、EN API の設計上で、このトレースは取ることができたでしょうか? という疑問があります。
先に書いた通り、EN API の設計上、個人のスマホ内でしか照合をしないので、感染通知が出せるのは集団 B の人達に対してだけです。しかも、どの時間に誰に接触したかを知らせることはしないので(サーバーとクライアントのデータを照合すれば特定可能ではありますが…機能上できません)、感染者 A がうろついた場所でしか注意喚起ができません。いや、むしろ、注意喚起ばかりが大きくて、迷惑だったという事実があります。まあ、端的に言えば、役に立たなかったのです。
これは、EN API の設計に引きずられてしまったという要因もあります。
先に書いた通り、EN API の設計上、感染者 A がすれ違った集団 B の人達というあいまいな特定の仕方しかできないので、アプリ自体がどう工夫してもこの制限を超えることができないのです。
では、もし、EN API の設計を変えたとして、保健所のようなトレース(集団 B や集団 C までの範囲の特定、そして 集団 D には通知しないなど)ができる程度まで、データの照合ができるとしたら、どのような仕組みを考えればよいでしょうか? というのが FolkBears の課題であると私は思っています。
当然、EN API のようにプライバシーを守る必要があります。コンタクトトレースの場合を取る場合は、一番乱暴な方法としては GPS の位置情報をサーバーに送ってしまう、という方法があります。ですが、これはプライバシーの観点からは最悪です。感染者 A の行動履歴だけでなく、集団 B や C までが丸裸になってしまうからです。実際、GPS の位置情報を利用したアプリで、犯罪者の行動履歴を取ることもあります。
では、GPS の位置情報ではなく、単純に BLE を使って近接した(いわゆる iBeacon のように)だけを使って、どのくらいまで保健所のトレースと同じことができるでしょうか?
広告間隔(Advertising Interval) – 最小 20 ms、最大 10,240 ms – デフォルトは 100 ms 程度
アドバタイズデータのデータ量 – 最大 47 バイト程度 – 実質的に 0.4 ms 程度で送信可能
アドバタイズの揺らぎ advDelay – 0 〜 10 ms のランダムな遅延が追加される
送信している時間は 0.4 ms と短いのですが、それが常に送信されているわけではありません。広告間隔(Advertising Interval)によって、一定の間隔で送信されています。この他に誤差ではありますが、アドバタイズの揺らぎ(advDelay)として、0 〜 10 ms のランダムな遅延が追加されています。これはきっちりとした間隔で送信してしまうと、受信側で同期していしまってアドバタイズが受信できなくなってしまうからです。このあたりは BLE アドバタイズの仕様ですね。
ただし、さきほど書いたように送信時間自体は 0.4 ms と非常に短い状態です。 これに対してスマホの Scan Window は最小の設定でも 2 ms 程度はあるし、もっと長くすることも可能です。 バッテリーの問題として、データの発信には電力が必要ですが、データの受信にはあまり電力が必要ではありません。よって、発信は短く、受信は長くしておくのがセオリーです。実際 BLE の送受信はそういう仕組みになっています。
しかし、図のように、必ずしも BLE のアドバタイズのデータがスマホ側の Scan Window にあてはまるとは限りません。真ん中のデータはうまく受信していますが、左のデータは Scan Window の外側で発信されているために受信できていません。
BLE スキャンを開始すると、周辺の BLE デバイスをスキャンします。「スキャンします」という云い方をしますが、実際は、BLE ペリフェラルが発信しているアドバタイデータを傍受している、という感じです。ちょうどレーダー受信機みたいなものですね。なので、周辺にある BLE 端末がごっそり表示されます。
これ、周辺には結構な量の BLE デバイスがあることが分かります。大抵の端末は名前がついていないので「Name Unknown」となっています。ハッキリ言って、どれがどの発信をしているのか区別が付きません。接続しようとすると接続できないものも混じっているので(ひとつの端末が複数のアドバタイズを使っているものもあり、中華な製品だとデータ内容がめちゃくちゃなものもあります、これは別途ツールを使うと調査ができます)、なかなか目的ものを見つけるのが大変です。
ひとまず、スマホの Android 端末とか、ワイヤレスイヤフォンとか BLE 接続のスピーカーみたいなのも見つかります。 デバイス名がついていないと、どの端末かわからないというのと、同じデバイス名がついていると判別がつかないというのが難点ですが、サービス UUID さえわかっていれば見つけやすいですね。
電波強度が強い順、つまり近い順から表示されるので目的の BLE デバイスを見つけやすいです。ひとまず、手元の近くにあるものが上に来ています。下のほうにあるのは、遠くのものなのでひょっとすると外で歩いている人の BLE ワイヤレスイヤフォンとか、iPhone の端末を拾っている可能性が高いです。
デバイスの RAW DATA の部分をタップすると、アドバタイズされているデータが表示されます。
一見すると何がよいのか分かりませんが(私も BLE を最初に扱ったときにはわかりませんでした)この表示が意外と重宝します。BLE データの 32 byte(実際には、先頭のフラグないときがありますが)が 16 進数で表示されているので、データそのものを解読することができます。
ただ、iOS の BLE ライブラリは受信データの制限がきついので BLE Scanner はどうやっているんだろう?という不思議なところがあります。タイプが 0xFF なので manufacturer data を受信しているのだと思いますが… ただし、無理に iOS で動かさずとも、Android や Windows あるいは m5stack などの端末を使えば、これらのデータは自由に解析できるようになります。
いわゆる、BLE セントラルを作っているときに、ペリフェラルの端末のほうがないとか、複数のペリフェラルを作りたいときに便利です。まあ、これも BLE デバイスのほうがないというパターンはあまりないので使いどころが難しいのですが、BLE をサーチするサーバー側を作って、温度やバッテリーなどのセンサーの類を探す、ってところに便利です。
そんな訳で、公開されているアプリを使うと便利に BLE デバイスのテストができるんですが、目的の BLE デバイスを見つけるのが結構大変で手間がかかります。SimpleLink Connect とかで見るとわかりますが、周辺には大量に、身元不明の BLE デバイスがあるわけです。 なので、テスト用や負荷試験のために自前で BLE デバイスや検出器を作っていきました。ってのが次の話題です。
開発経緯自体は、別のことろでまとめるとして、ここでは FolkBears の本質なところの BLE 相互通信の部分の解説をします。
COCOA の BLE 通信構造
COCOA の BLE 通信構造は、Google/Apple の Exposure Notification API (ENA) https://developer.apple.com/documentation/exposurenotification で提供される API を利用しています。ENA 自体は、国が開発することが前提となっているライブラリなので、現在一般的な開発を試みることはできません。しかし、ENA の BLE 通信構造自体は公開されているので、その構造を真似ることは可能です。
端的に言えば、ENA は Bluetooth Low Energy (BLE) を使って、コネクション無しの広告型(アドバタイジング)で ID を配布します。受信する端末は ID を保管しておき、サーバーで ID 同士を比較するという形になっています。問題は、このアドバタイジングの部分です。
ENA のアドバタイズはコネクションレスである
BLE Service UUID は 16 bit(0xFD6F)である
という仕様となっています。
FolkBears も同様の仕様にしたいところですが、これを真似ることはできません。BLE Service UUID は一般に使う場合には 128 bit を使うことが推奨されており、16 bit UUID は Bluetooth SIG に登録しなければいけません。お金がかかります。0xFD6F は Apple の UUID であり、ENA で使うようにしてありますが、これが使えません(OS レベルでガードが掛かっています)。また、ENA で使っているコネクションレスの接続方法は、iOS では使えません。ENA が使えるのは OS レベルで特別な仕様として使っているもので、iOS の API としては用意されていないわけです。
そのあたりの諸々の事情もあり、元ネタである「まもりあい Japan」バージョンは BLE によるコネクション版が使われています。FolkBears も同様にコネクション版を使うことができます。
そうなると、ENA のようのコネクションしない状態で、つまり、アドバイズする BLE データ自体に ID を乗せて配信するのがよさそうです。実際、Google/Apple の ENA もそうなっています。 ですが、先に書いた通り iOS ではコネクションレスのアドバイズができないので、ENA と同じ動作にはできない、という問題が残ります。実験機としては Android 限定でもよいのですが、実運用を考えるとそうもいきません。
そこで、iBeacon 形式を利用したコネクションレスのアドバタイズを使うことにしました。iBeacon 形式は Apple が提唱している BLE のアドバタイズ形式で、iOS でもサポートされています。iBeacon 形式では、Service UUID の代わりに Proximity UUID (128 bit)、Major (16 bit)、Minor (16 bit) を使ってデータを送信します。これを ID としてうことにしています。
これだと、コネクション版と違い台数が多くなっても大丈夫そうだし、通信形式が ENA に似ているので検証としてもよさそうです。
ただし、これも難点があって、ENA の ID/RPI は 16 byte ですが、iBeacon 形式では Major (16 bit) + Minor (16 bit) で合計 4 byte となってしまいかなり範囲が狭いです。衝突の問題がもありますが、ひとまず研究用として、4 byte の ID を使うことにしています。
REM ***** BASIC *****
sub Main
rem ----------------------------------------------------------------------
rem define variables
dim document as object
dim dispatcher as object
rem ----------------------------------------------------------------------
rem get access to the document
document = ThisComponent.CurrentController.Frame
dispatcher = createUnoService("com.sun.star.frame.DispatchHelper")
rem ----------------------------------------------------------------------
dim args1(0) as new com.sun.star.beans.PropertyValue
args1(0).Name = "ToPoint"
args1(0).Value = "$A$1"
dispatcher.executeDispatch(document, ".uno:GoToCell", "", 0, args1())
rem ----------------------------------------------------------------------
dim args2(0) as new com.sun.star.beans.PropertyValue
args2(0).Name = "StringName"
args2(0).Value = "masuda"
dispatcher.executeDispatch(document, ".uno:EnterString", "", 0, args2())
rem ----------------------------------------------------------------------
dispatcher.executeDispatch(document, ".uno:JumpToNextCell", "", 0, Array())
end sub
sub Main
dim oSheet as object
oSheet = ThisComponent.Sheets.getByIndex(0)
dim oCell as object
oCell = oSheet.getCellByPosition(0, 0) ' A1 セル
oCell.String = "masuda"
end sub
を、ScriptForge ライブラリを使うと次のように書けます。
sub main
GlobalScope.BasicLibraries.loadLibrary("ScriptForge")
Dim oDoc As Object
Set oDoc = CreateScriptService("Document", ThisComponent)
oDoc.SetValue("A1","masuda")
end sub