COCOA 受信(EN API 仕様)の実際 Windows + C# 版

既に 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 をペリフェラルに問い合わせるモードがあるのでそのためかもしれません。

ひとまず、Service UUID である 0xFD6F を 2 回探していますが、そういう電文フォーマットになっているからです。Service Data UUID のほうの 0xFD6F は自由に決められるので、この値じゃなくても良いはずなのですが、COCOA ではこうなっています。

この 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
  • AD Type: 1byte
  • Length: 1byte
  • Data: n bytes

という形ででているのが良く分かります。

実測状態

ちょっと、m5stack で COCOA 発信機を作らないといけないので、その後で。
受信した状態はこんな感じになります。

RPI 情報は適当に埋めてあるので、そのデータが流れています。COCOA が動作しているとすると、こんな感じで RPI 情報が取得できるわけです。

カテゴリー: 開発, FolkBears | コメントする

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 | コメントする

FolkBears で利用している aware-micro サーバーをビルドして Docker コンテナで動かせるようになるまで

接触確認アプリ FolkBears では、接触データを Aware というサーバーに送信しています。Aware framework https://awareframework.com/ は、モバイルから加速度データとか WiFi 取得のデータとかをうまく収集できる研究用アプリ+サーバーで、結構便利…なんですが、FolkBears の BLE アドバタイズと利用した接触データの収集とはちょっと相性が悪くてですね(これは数年後に気付いたわけですが)、FolkBears 内部では切り離しを進行中です。

が、データ収集部分は Aware を使わないとしても、データ収集をするためのサーバーのほうは aware サーバーでもよいか、ということでそのままになっています。このサーバーコードが aware-micro https://github.com/denzilferreira/aware-micro な訳ですが。さて、これ以前は PHP で書かれていたような気がするのですが、現在は Kotlin で書かれています。Kotlin はまあいいんですが、内部的に Eclipse Vert.x https://vertx.io/ を使っていてですね、ちょっと再構築にコツが入りそうなのです。
というか、1年程前に自前でサーバーを構築しようして失敗したものだから、面倒くさくなって Azure Functions で仮実装して実験的に動かしていました。ごく一部分だけ使うわけだし、FolkBears からのデータアップロードだけ部分なので、C# で書いても大して手間ではなかったのですが。が、内部で SQL Server を使って、暫くほっといていたら月2万円の請求になってしまい(無料枠が1.2万円ほどだたので、実際の支払は8000円ほどです)、これは放置するとアカンなと思った次第です。

本格的にデータ収集するとなるとギガバイト単位の DB になってしまうので考え直さないといけないのですが、実験データ程度ならば aws の EC2 + t3.micro でもいいはずです。まあ、安く済ませるならば、さくらの VPS を借りて PHP で実装したほうがいいのですが、それば別の機会にでも。

以下、試行錯誤も含めて作業ログ的に書き進めていきます。

docker-compose.yml の作成

aware-micro https://github.com/denzilferreira/aware-micro に Dockerfile の例があるので Ubuntu + Docker で構築することを考えます。

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"]

git から aware-micro のコードを持ってきてビルドするのではなくて、あらかじめ micro-1.0.0-SNAPSHOT-fat.jar を作成しておく方式です。設定ファイルとして aware-config.json も必要そうです。以前は、これをどうやってつくるのか?が分からなかったのですが、aware-micro https://github.com/denzilferreira/aware-micro の readme に書いてありました。

こっちのほうは、実際にサーバーを動かす方

cd aware-micro
./gradlew clean build run

こっちのほうが、*.jar ファイルを作る方

cd aware-micro
./gradlew clean build shadowJar

build/lib/micro-1.0.0.SNAPSHOT-fat.jar というファイルができあがります。build.gradle の kotlinOptions.jvmTarget を最新の 17 にしていますが通ります。dependencies にあるライブラリのバージョンが古いかもしれませんが、ひとまずこのままにしておきます。

Copilot を使って docker-compose.yml を作って貰います。

version: "3.9"

services:
  mysql:
    image: mysql:8.0
    command: --default-authentication-plugin=mysql_native_password
    restart: unless-stopped
    environment:
      MYSQL_ROOT_PASSWORD: root
      MYSQL_DATABASE: aware
      MYSQL_USER: aware
      MYSQL_PASSWORD: awarepass
    ports:
      - "3306:3306"
    volumes:
      - mysql-data:/var/lib/mysql
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1", "-proot"]
      interval: 10s
      timeout: 5s
      retries: 5

  aware-micro:
    build:
      context: dev-local
      dockerfile: Dockerfile
    image: aware-micro:dev
    depends_on:
      mysql:
        condition: service_healthy
    environment:
      VERTX_OPTIONS: ""
    ports:
      - "8080:8080"
    volumes:
      - ./dev-local/aware-config.json:/usr/verticles/aware-config.json:ro
    restart: unless-stopped

volumes:
  mysql-data:

volumes 部分を変更して、aware-config.json と micro-1.0.0-SNAPSHOT-fat.jar をコピーできるようにします。このファイルは aware-micro プロジェクトの方からコピーします。

    volumes:
      - ./aware-config.json:/usr/verticles/aware-config.json:ro
      - ./micro-1.0.0-SNAPSHOT-fat.jar:/usr/verticles/micro-1.0.0-SNAPSHOT-fat.jar:ro

aware-config.json の server の設定を変えておきます。データベース名は docker-compose.yml のほうに合わせました。database_host は docker-composer にある “mysql” を使う筈。

{
  "server" : {
    "database_engine" : "mysql",
    "database_host" : "mysql",
    "database_name" : "aware",
    "database_user" : "aware",
    "database_pwd" : "awarepass",
    "database_port" : 3306,
    "server_host" : "http://localhost",
    "server_port" : 8080,
    "websocket_port" : 8081,
    "path_fullchain_pem" : "",
    "path_key_pem" : ""
  },
  "study" : {
    "study_key" : "4lph4num3ric",
    "study_number" : 1,
    "study_name" : "AWARE Micro demo study",
    "study_active" : true,
    "study_start" : 1770433225185,
    "study_description" : "This is a demo study to test AWARE Micro",
    "researcher_first" : "First Name",
    "researcher_last" : "Last Name",
    "researcher_contact" : "your@email.com"
  },
  "sensors" : [ {
    "sensor" : "accelerometer",
    "title" : "Accelerometer",

micro-1.0.0-SNAPSHOT-fat.jar の作成

./gradlew clean build run のほうはサーバー立ち上げなので 8080 番でポート待ちです。

*.jar ファイルを作るのは ./gradlew clean build shadowJar のほう

aware-config.json ファイルは ./gradlew clean build run あるいは ./gradlew run で自動生成されます。

Docker コンテナを作成&実行

ひとまず Windows 上で docker compose up -d を試します。

Javaのランタイムバージョンでこけるので、Dockerfile の先頭を変えておきます。

FROM eclipse-temurin:17-jre

micro-1.0.0-SNAPSHOT-fat.jar をビルドしたときのバージョンとあわせておきます。

無事 Docker コンテナが立ち上がれば ok です。私の環境の場合、ホストPCに MySQL が入っているので、”3307:3306″ にしておきます。

MySQL Workbench で接続できるところまで確認します。

ブラウザから http://localhost:8080/ のように接続すると、こんな形に出れば aware サーバーが動いています。

テーブルを作成する

データベースにテーブルを作成しておきます。もともと aware-micro にはテーブルを作成する機能があるのですが、これがコメントアウトされています。まあ、URL を通してばかすかテーブルを作られてしまっても困るので、ガードしてあります。

その中で createTable メソッドがあるので、次のようにテーブル作ります。

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`)
)

`$table` のところは実際に作成するテーブル名をいれます。クライアントから aware-micro に送られたデータは、data カラムに入ります。data カラムは JSON 形式なのでとにかく JSON にしておくと何でも突っ込めます。逆に言えば JSON 形式しか突っ込めません。

クライアント(FolkBears)からデータを送信する

aware-micro サーバーが出来上がったところで、データを送信してみます。aware-micro に送信するときにフォーマットがちょっとややこしくて、

  • POST メソッドを使う
  • content-type を x-www-form-urlencoded 形式で送る
  • device_id パラメータが必須
  • JSON 形式は必ず配列にして data パラメータで送る(複数データが前提になっている!)

という形式になっています。
curl コマンドで送信するときは以下の形式です。

curl -X POST "http://localhost:8080/index.php/1/4lph4num3ric/sample/insert" `
  -H "Content-Type: application/x-www-form-urlencoded" `
  -d "device_id=xxx" `
  -d 'data=[{"timestamp": 1, "name": "masdua"}]' 

URL がまたややこしくて、aware-config.json に記述した、study_number と study_key を使います。index.php は昔 PHP で作成したときの名残りっぽいです。たぶん、もともとは加速度センサーなどの情報を一気に入れるようになったものを、micro にして汎用化したんじゃないかなぁと。

呼び出す URL の形式はこんな感じ

/index.php/:studyNumber/:studyKey/:table/:operation

  • index.php: 固定
  • :studyNumber: aware-config.json の study_number
  • :studyKey: aware-config.json の study_key
  • :table: 操作するテーブル名
  • :operation: 操作方法(insert, update など)

で、無事データが insert されると、こんな風になります。

ひとまず、aware-micro の Docker コンテナに送信できたので、一旦終了。

FolkBears Android 版からの送信

この方式だと微妙に面倒なので、retrofit2 ライブラリを使っています。

package jp.mamori_i.app.webapi

import retrofit2.Call
import retrofit2.http.Body
import retrofit2.http.Field
import retrofit2.http.FormUrlEncoded
import retrofit2.http.POST

interface AwareService {
    @FormUrlEncoded
    @POST("trace_data/insert")
    fun uploadData(
        @Field("device_id") device_id: String,
        @Field("data") data: String
    ): Call<Void>

    @FormUrlEncoded
    @POST("trace_data/query")
    fun queryData(
        @Field("device_id") device_id: String,
        @Field("start") start: Long,
        @Field("end") end: Long
    ): Call<String>


    @FormUrlEncoded
    @POST("temp_user_id/insert")
    fun uploadTempUserId(
        @Field("device_id") device_id: String,
        @Field("data") data: String
    ): Call<Void>

    @FormUrlEncoded
    @POST("temp_user_id/query")
    fun queryTempUserId(
        @Field("device_id") device_id: String,
        @Field("start") start: Long,
        @Field("end") end: Long
    ): Call<String>

    @FormUrlEncoded
    @POST("deep_contact_user/insert")
    fun uploadDeepContactUser(
        @Field("device_id") device_id: String,
        @Field("data") data: String
    ): Call<Void>

    @FormUrlEncoded
    @POST("deep_contact_user/query")
    fun queryDeepContactUser(
        @Field("device_id") device_id: String,
        @Field("start") start: Long,
        @Field("end") end: Long
    ): Call<String>

}

object RetrofitClient {
    private const val BASE_URL = "https://aware....jp/index.php/1/folkbears/"

    val awareService: AwareService by lazy {
        retrofit2.Retrofit.Builder()
            .baseUrl(BASE_URL)
            .addConverterFactory(retrofit2.converter.scalars.ScalarsConverterFactory.create())
            .build()
            .create(AwareService::class.java)
    }
}

実際の呼び出しは TraceDataUpload.upload で。

package jp.mamori_i.app.webapi

import com.google.gson.Gson
import jp.mamori_i.app.data.TraceDataEntity
import jp.mamori_i.app.webapi.RetrofitClient


class TraceDataUpload
{
    /**
     * Aware に POST 送信する body を作成する
     */
    fun fromTraceDataList(items: List<TraceDataEntity> ) : String {
        if ( items.isEmpty() ) {
            // データがない場合は空配列を返す
            return "[]"
        }

        /** 
        * 送信用の TraceData のデータクラス
        */
        data class TraceData(
            val temp_id: String,
            val timezone: Int,
            val timestamp: Long,
            val tx_power: Int,
            val jsonVersion: Int,
            val os: String,
            val rssi: Int,
            val label: String
        ){}

        var lst = mutableListOf<TraceData>()
        items.map {
            lst.add( TraceData(
                temp_id = it.tempId,
                timezone = 9,
                timestamp = it.timestamp,
                tx_power = 0,
                jsonVersion = 0,
                os = "android",
                rssi =  it.rssi,
                label = ""
            ))
        }
        return Gson().toJson(lst)
    }

    fun upload( deviceId: String, data: String) : Boolean {
        val response = RetrofitClient.awareService
            .uploadData(device_id = deviceId, data = data )
            .execute()
        return response.isSuccessful
    }

カテゴリー: 開発, FolkBears | FolkBears で利用している aware-micro サーバーをビルドして Docker コンテナで動かせるようになるまで はコメントを受け付けていません

markdown で書いて LibreOffice Writer でチェックして wordpress に貼り付けるまで、の計画

技術系の原稿を書くときに、何を使っているかと言うと最近はもっぱら vscode + markdown 形式です。まあ、markdown 形式のみで済むかと言うとそうではなくて、原稿の提出前に Word に貼り付けてチェックをしています。もともと Word の文書チェックの機能を使っていたのと、画面キャプチャのチェックに見た感じのものが必須だからですね。以前はテキストのみで書いていたことも多いのですが、提出した後に画像ファイル番号のずれが多くて、なかなか大変だったので事前に見た目でチェックしようという発想があります。つまりは、私が原稿を書くときに画像ファイル番号を間違えているわけですが。
最近では、markdown 形式のプレビュー機能を使ってチェックする場合も多いのですが、図の矢印とか吹き出しとかもあるので、やはり Word に貼り付けることが多いです。

で、同じパターンで markdown 形式で書いたものを wordpress に貼り付けたいわけですが、これが意外と面倒くさいです。
たいていは、vscode の markdown の原稿からコピペで貼り付けます。

  • 余分な空行がはいる(パラグラフの違い)
  • 箇条書きが1行ずつパラグラフになる
  • 画像が張り付かない
  • 見出しは太文字になる(なぜか、これは対応している)
  • 表は貼り付かない
  • コードブロックは貼り付かない

という問題があります。まあ、workdpress 自身、付属のビジュアルエディタで書くのベターなのでそれでもいいのですが、オフラインで vscode で書いたほうが編集がやりやすいのです。参照するコードとかが色々あるので。

markdown から LibreOffice Writer へ変換する

実験的に ExcelLikeUno ライブラリに connect_writer 関数を生やして、LibreOffice Writer に接続する機能を追加したのが次のコードです。
実は Writer 用に WriterDocument クラスを定義しただけで、paragraph まわりは実装していません。そのまま UNO API を使っています。ExcelLikeUno のほうで完全に網羅しなくても、網羅しきれないところは UNO API を直接使えばいいか、というわけでこの程度ならが書けなくもないか(ほとんど Copilot が書いていますが)というところです。

import uno
from excellikeuno import connect_writer

(desktop, doc) = connect_writer()
# パラグラフを取得して表示する

text = doc.getText()
print("Document Text Paragraphs:" + str(text))

paragraphs = text.createEnumeration()

# LibreOffice text implements enumeration instead of random access.

codeblock = False

while paragraphs.hasMoreElements():
    para = paragraphs.nextElement()
    # print(para.getString())

    # 先頭で ``` 始まるとコードブロックの開始
    if codeblock == True:
        # コードブロックスタイルを適用
        para.setPropertyValue("ParaStyleName", "コード")
        # コードブロック終了判定
        if para.getString().startswith("```"):
            codeblock = not codeblock
        continue
    elif para.getString().startswith("```"): 
        codeblock = not codeblock
         # コードブロックスタイルを適用
        para.setPropertyValue("ParaStyleName", "コード")
        continue

    # 先頭が # の場合、見出しとみなす
    if para.getString().startswith("#"):
        title = para.getString().lstrip("#").strip()
        print(f"Found title: {title}")
        
        # タイトルスタイルを適用
        style_families = doc.raw.getStyleFamilies()
        paragraph_styles = style_families.getByName("ParagraphStyles")
        title_style = paragraph_styles.getByName("Title")
        
        # 先頭が # のみの場合は "見出し1"
        # 先頭が ## の場合は "見出し2" とする
        # 先頭が ### の場合は "見出し3" とする
        level = len(para.getString()) - len(para.getString().lstrip("#"))
        if level == 1:
            para.setPropertyValue("ParaStyleName", "Heading 1")
        elif level == 2:
            para.setPropertyValue("ParaStyleName", "Heading 2")
        elif level == 3:
            para.setPropertyValue("ParaStyleName", "Heading 3")

    # 先頭が - の場合、箇条書きとみなす
    elif para.getString().startswith("-"):
        # 先頭の - を削除
        para.setString(para.getString().lstrip("-").strip())
        # 箇条書きスタイルを適用
        para.setPropertyValue("ParaStyleName", "List 1")

    # 先頭が ![ の場合は画像とみなす
    elif para.getString().startswith("!["):
        # 画像のパスを取得
        start_idx = para.getString().find("](") + 2
        end_idx = para.getString().find(")", start_idx)
        image_path = para.getString()[start_idx:end_idx]

        base_dir = "D:/work/blog/doc/"
        image_path = base_dir + image_path
        # 画像を挿入
        graphic = doc.raw.createInstance("com.sun.star.text.GraphicObject")
        graphic.GraphicURL = f"file:///{image_path}"
        
        print(f"Inserting image: {image_path}")
        # graphic.AnchorType = uno.getConstantByName("com.sun.star.text.TextContentAnchorType.AS_CHARACTER")

        text.insertTextContent(para.End, graphic, False)

        # 画像挿入後、元の段落を削除
        # text.removeTextContent(para)

変更前

変更後

見出し程度ならば手作業とかわらないだろう、ってのもありますが、表とコード、画像の貼りこみ位ができれば確認するにはよいかな、と。現状、図形の貼りこみは小さくなってしまっていますが、図を広げると貼りこまれていることがわかります。 これは、もうちょっと続きを作るのと、Writer 対応の ExcelLikeUno ライブラリを整備に使います。

カテゴリー: 開発, LibreOffice | markdown で書いて LibreOffice Writer でチェックして wordpress に貼り付けるまで、の計画 はコメントを受け付けていません

 BLE アドバタイズのパケット/iBeacon の詳細

なかなか、実際のプログラムコードに至りませんが、その前の前提を知っておいたほうがいいので少しずつ書いていきます。これ、Android や iOS で BLE ライブラリと使って Flutter とか React Native で作るときはさっくりと通していいのですが、m5Stack とか ESP32 とか使って自分で BLE 周りを制御しようとすると途端に必要な知識になっているので(知らないと落とし穴にはまる)、前提として知っておいたほうがいいです。

BLE パケットの Protorol Data Unit (PDU)

BLE パケット全体の大きさは、たかだか 47 バイト程度です。データ通信の場合はもっと長くなるのですが、ここで扱うのは BLE アドバタイズなので、非常に短いデータ量で済みます。データ量が少ないので、0.4 ms 以下という短い単位で送信/受信が可能です。

だから、受信しようと思えば Scan Window を広く取っておけば、いつでも BLE アドバタイズを受信できる確率は高くなります。ただし、それは概念的なので、実際的にはハードウェア的に Scan Window で待ち受けている時間とあ Scan Interval で受信できない時間が交互にやってきます。これ、バッテリーの問題なのか、ハードウェアの制限なのかはよくわからないのですが(つまり、無限に Scan Window を広げられるか私にはわかりません)、現実n BLE のハードウェアはそういう構造になっているということです。

さて、BLE パケットの内、データを乗せることができる Protocol Data Unit (PDU) の詳細をみていきます。

BLUETOOTH CORE SPECIFICATION Version 6.0 の Vol 6 Low Energy Controller の Part B LINK LAYER SPECIFICATION の 2 AIR INTERFACE PACKETS あたりから書いてあります。

全部で 4000 ページ位あるのですが、Core Specification 6.0 | Bluetooth® Technology Website https://www.bluetooth.com/specifications/specs/core60-html/ からダウンロードが可能です。通読するのは大変なので、適度にピックアップして読みます…が、この部分は PDU の部分だけじゃなくて、BLE パケット全体の仕様が書いているので、肝心のデータ部分がわかりません。多分、2.3.1.1 ADV_IND あたりの AdvData の部分のフォーマットだと思うのですが、これは後で調べ直し。

要は、BLE アドバタイズでも色々なヘッダーがあって種類があるのですが、iBeacon のようなフォーマットを送っている場合は ADV_IND の種類を流しておいて、その中のデータ = AdvData を送受信側でプロトコルとして決めるわけです。物理層/リンク層のもうひとつ上のレイヤーになります。

この AdvData の中身を決めているのが、Supplement to the Bluetooth Core Specification https://www.bluetooth.com/specifications/specs/core-specification-supplement-10/ です。これも PDF 形式でダウンロードができます。

この中の Part A DATA TYPES SPECIFICATION にプロトコル仕様が書いてあります。

これはサービス UUID の書き方ですね。

まあ、表だけ並べられても解り辛いので、いくつかのサンプルデータも載っています。

これはデバイス名をアドバタイズする場合です。

BLE のフォーマットはちょっと変わった形式になっていて、

  • データ長
  • データタイプなど
  • データ自身

という順番で並んでいます。データ長が1バイトと制限になるので(最大 255 バイト)とは思います。
上のデータを書き直すとこんな感じになります。

最初の 0x02 のように、フラグだけ並んでいる場合と、次の 0x0A のようにデバイス名のような可変長のデータが続く場合があります。デバイス名は特殊でデータ長が入っているのですが、大抵は Service UUID のように 16 バイトとか 4 バイトとか決まった長さのデータが続く場合が多いです。

なので、BLE アドバタイズ自身は Data types specification の書式に従っていれば自由に BLE アドバタイズのデータ送信が可能です。実際、Apple の iBeacon のフォーマットや、Google の Eddystone のフォーマットもこの書式に従っています。おそらく BLE 端末の機器メーカーも流しているのかはこれかな、と思うのですが、定かではありません。

BLE アドバタイズのフォーマットは自由に決められるということは解ったのですが、接触確認アプリのように既存の OS や BLE ライブラリを使う場合にはそれほど自由ではありません。と言いまうsか、Apple の場合は iBeacon フォーマットしか使えません。Android の場合は、もう少し自由に作れますが、iPhone との共同運用を考えると iBeacon フォーマットしか使えないというのが現実的な選択肢です。

もちろん、m5stack とか ESP32 を使て独自の BLE アドバタイズのフォーマットを使えば自由に作れます。Android の場合も多少の制限がありますが、Android 同士であれば結構自由に作れます。これは先行き解説していきます。

話を元に戻すと、FolkBears のコレクションレスモードの場合は、iBeacon のフォーマットを使っています。この iBeacon フォーマットは何処に記述されているのでしょうか?

https://developer.apple.com/ibeacon/ から Artwork and specifications をダウンロードして、Proximity Beacon Specification を開くと正式なものがあります。

これ、なかなか探してもわからなかったのですが、こんなところにあったんですね。

これだと解り辛いので、書籍や Wikipedia の iBeacon の記事を参考にするとわかりやすいです。

iBeacon – Wikipedia https://en.wikipedia.org/wiki/IBeacon

これを図に書き直すとこんな感じになります。

これが慣れないと難しいのですが、

  • 最初の3バイトは、BLE アドバタイズのヘッダー情報
  • 次の4バイトは、Maniufacturer Specific Data のデータタイプを示すための Length + 0xFF + Company ID (Apple の場合は 0x004C) が続く
  • 次の 2 バイトは、Apple 決めた iBeacon を示すための固定値 0x02 と 長さ(0x15)
  • 次の 16 バイトは、iBeacon の UUID
  • 次の 2 バイトは、Major
  • 次の 2 バイトは、Minor
  • 最後の 1 バイトは、Tx Power

という訳で2段階に分かれています。

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 だけが作るわけでもありません。

  • 非 Apple 製の Beacon タグもある
  • 非 Apple 製の iBeacon 受信アプリもある

という事情があります。互換性というわけです。

iBeacon データを実測する

iBeacon フォーマット詳細がわかったところで実測をしてみましょう。

実測とはいえ、きちんとした iBeacon 受信機を作るところからスタートしないといけないのですが、ここでは私が以前したデータを使います。と、言いますか、ここからが問題があるのです。

iOS から発信された iBeacon データ

02011A1AFF4C00021590FA7ABEFAB6485EB7001A17804CAA13A6D0CFC1C5

これを先の表に従って分解していきます。

iBeacon UUID: 90FA7ABEFAB6485EB7001A17804CAA13 が取得できればひとまず ok です。
このデータ自体、Major や Minor、Tx Power も取得できるので、iBeacon フォーマットとしては正しいことがわかります。
実は、実測すると分かるのですが 3 バイト目のFlag データの 0x1A の部分は、iBeacon 発信機によって異なります。ここは BLE アドバタイズに仕様なので、先の iBeacon 仕様の例のように同じ値になるとは限りません。つまりは、内部的に Maniufacturer Specific Data の部分は同じ Apple の iBeacon の規格を使っているとしても前後の BLE のフラグは発信する機種/メーカーによって異なります。

これを Android が iBeacon データを発信したときのデータを見てみましょう。

1AFF4C00021590FA7ABEFAB6485EB7001A17804CAA13F15FA5E2C5

実は、先頭の 3バイト(02011A)がありません。

この現象は Android の org.altbeacon.beacon ライブラリと使ったときに発生します。

private fun startBeaconTransmission() {
    val beacon = Beacon.Builder()
        .setId1(SERVICE_UUID.toString()) // UUID
        .setId2(major.toString()) // Major (10進数文字列)
        .setId3(minor.toString()) // Minor (10進数文字列)
        .setManufacturer(0x004C) // Apple iBeacon のメーカーコード
        .setTxPower(-59) // 信号強度 (dBm)は仮設定
        .build()

    // val beaconParser = BeaconParser().setBeaconLayout(BeaconParser.ALTBEACON_LAYOUT)
    val beaconParser = BeaconParser().setBeaconLayout("m:2-3=0215,i:4-19,i:20-21,i:22-23,p:24-24")
    beaconTransmitter = org.altbeacon.beacon.BeaconTransmitter(context, beaconParser)
    beaconTransmitter?.startAdvertising(beacon, object : AdvertiseCallback() {
        override fun onStartSuccess(settingsInEffect: AdvertiseSettings) {
            Log.d("BeaconService", "iBeacon 発信開始")
        }
        override fun onStartFailure(errorCode: Int) {
            Log.e("BeaconService", "iBeacon 発信に失敗: $errorCode")
        }
    })
}

この iBeacon 形式のデータを iOS 側で受信させると、うまく受信できるので iBeacon 発信機としてうまく動いています。先頭部分の 02011A がなくても iBeacon フォーマットとしては問題ない、ということなのです。
ただし、この iBeacon データを、android.bluetooth.le.ScanFilter でフィルタリングしようとすると、うまくフィルタリングできない、という問題があります。なんだかよくよくわからないので、FolkBears では Maniufacturer Data の中身をシークしているのですが、ここは現在のところ回避策をとっているところです。

ひとまず、この BLE 規格の部分と iBeacon フォーマットを把握しておけば、BLE アドバタイズで受信したデータを解析できることがわかります。このあたり、m5stack や ESP32 で BLE アドバタイズを受信するときにも使える知識です。

カテゴリー: 開発, FolkBears |  BLE アドバタイズのパケット/iBeacon の詳細 はコメントを受け付けていません

BLE 近接通信データを使った二世代前のコンタクトトレースを探る

もともと、BLE による接触確認アプリに興味を持ったのは、アプリ自体が保持している近接データを駆使すれば、二世代前の接触者まで追跡できろうだろうと思ったわけですし、それをやると思ったんですよ。

コンタクト・トレーシング – Wikipedia https://ja.wikipedia.org/wiki/%E3%82%B3%E3%83%B3%E3%82%BF%E3%82%AF%E3%83%88%E3%83%BB%E3%83%88%E3%83%AC%E3%83%BC%E3%82%B7%E3%83%B3%E3%82%B0

2020年当時に日本の保健所が逼迫していたのが、感染者の聞き取り調査の部分です。聞き取り自体がアナログでしかできないし、その追跡自体も人海戦術のようでした。基本的な感染症数理モデルの SIR 型モデルを知ったのもこの頃ですが、統計学のそれも興味の範囲ではあったけど、直接 IT 技術者として手が付けられそうなのは、この接触確認アプリの近接データの扱いです。

結論から先に言うと、Goolge/Apple の Exposure Notification API を使った場合には二世代前の接触者などを追跡することはできません。サーバーに集められるのは、Temporary Exposure Keys (TEK) と呼ばれる感染者の端末が生成した一時的な鍵情報なだけで、接触自体はアプリ内で確認することになるからです。
これは個人情報保護としてはいいのですが、先にあるように保健所のトレーシング業務を支援することは不可能なわけです。ある意味 EN API の設計の失敗(実用度の失敗でもありますが)はここにあると思っています。

俺の考える最強のコンタクトレース…というか、日本の保健所のヒアリングがこんな感じになっていたと思うのです。いわゆる、新型コロナウィルスの場合もインフルエンザの場合も、感染者が居て集団で感染するクラスターのパターンが顕著に感染が広がります。接触感染じゃななくて空気感染(詳しい事は省略したうえで)なのがミソです。学校や老後施設での集団感染のことです。結局のところ、集団でいたときにマスクで防護するのが一番効率がよいのですが、じゃあ、まだマスクが主要でなかったときに、クラスター感染をどうやって見つけるのか、というのが当初の問題にありました。

そこで、保健所でヒアリングをして、感染源であるクラスターを探して、そのクラスターに居た人に注意喚起をしよう、というのが目的した。

ただし、そのヒアリングが人力であったし、保健所の職員自体も足りなかったので非常に負担であったという事実があります。いまだと、感染自体の報告義務がなくなったので、ヒアリング自体がどうなっているか不明ではありますが。
ただし、将来的に同じパターンで感染が広がったときに「ヒアリング」という手段でよいのか? という問題があります。

IT 技術者としては、ここをなんとか自動化できるのではないか、と考えるわけです。

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 のように)だけを使って、どのくらいまで保健所のトレースと同じことができるでしょうか?

カテゴリー: 開発, FolkBears | BLE 近接通信データを使った二世代前のコンタクトトレースを探る はコメントを受け付けていません

BLE 物理層の詳細とスキャン頻度

BLE(Bluetooth Low Energy) を使う上で、アドバタイズ方式(iBeacon方式)と GATT(Generic Attribute Profile)を使ったコネクション方式があるわけですが、まずは近接しているデバイスを見つけないといけません。iBeacon の場合はスマホアプリから iBeacon の端末を見つけないといけないし、GATT の場合はペリフェラル側のワイヤレスイヤフォン等をスマホアプリから見つけないといけません。

店内にある iBeacon デバイスとか、目の前に接続しようとしているワイヤレスイヤフォンの場合は、アプリを立ち上げた後に “すぐそこにある端末” に接続するまでじっと待つというスタイルになります。これ、当たり前のように思えますが、最初のデバイスを見つける手順はどうなっているのでしょう? ってのが問題です。

この図では、「スマホ」から「iBeacon」に向かって矢印を向けていますが、実際は iBeacon 側から何等かのデータを発信していて、スマホ側のアプリが受け取るという形になります。

こんな風に、iBeacon 側で何度も発信しているものを、スマホ側でなんらかのタイミングで受け取れることができる、ってのが正しいです。

BLE 物理層(PHY) の仕様

BLE の物理層は、2.4 GHz 帯の ISM バンドを使っています。一般的に、広告チャンネル(アドバタイズ)とデータチャンネルの2種類に分かれています。

  • 広告チャンネル(アドバタイズ)が 37,38,39 の 3 チャンネル
  • データチャンネルが 0-36 の 37 チャンネル

合計 40 チャンネルを使っているので、BLE 通信をしているときに、たくさんの端末があってもデータ通信は大丈夫、という仕組みです。
ただし、この3つしかない広告チャンネルってのが問題で、BLE ペリフェラル(発信側)がアドバタイズを行うときには、この3つのチャンネルを順番に使って発信しています。これは、

  • iBeacon や ENA/FolkBears のように、コネクションレスでアドバタイズを行うとき
  • GATT のように、コネクションを確立する前のデバイスを見つけるとき

に重要になります。いや、先の例のように設置してある iBeacon デバイスを見つけるときとか、目の前にあるワイヤレスイヤフォンを見つけるときには問題ありません。スマホアプリを使って接続できるかなぁと暫く待てば、iBeacon もワイヤレスイヤフォンも見つかります。

普通の BLE デバイスの場合は、接続までの時間が 30 秒位あっても問題はありません。確かに店内設置のような iBeacon デバイスを発見するのに 30 秒位掛かると遅くて問題になるかもしれませんが、この発見までの時間は確率的に決まるものなので、早く見つかることもあれば遅く見つかることもあります。これは後述します。

また、接触データの交換をするにしても、最初に相手のデバイスを見つけないといけません。これは BLE のチャンネル自体は 40 チャンネルあってデータ送受信で競合しないようになっていますが、実は相手のデバイスを発見するための広告チャンネルを使うために、3 チャンネルしかないということです。
要は意外と少ないという話です。

さらに、接触確認アプリのようにできるだけ短時間で接触を確認したい場合には、発見までの時間が重要になります。いわゆる発見までの遅延があるわけです。

そういうわけで、接触確認アプリ(m5stack等で作れば接触確認デバイス)を作る場合には、発見までの遅延が重要になります。このあたり、なぜかというと、数秒ですれ違ったりする場合には確率的に相手のデバイスを発見できない可能性があるということです。

  • 広告チャンネルの 3 チャンネルで競合する
  • 発見までの遅延が確率的にある
  • 複数の BLE デバイスが集中する場合、発見しない可能性もある

ということを抑えておく必要があります。

パケットの構造

BLE のパケットの話を少し書いておきます。

最初、BLE データの衝突で、データ落ちがあると思っていたのですが、それはほとんどあり得ません、ということです。

BLE パケットは以下のような構造になっています。

  • Preamble: 1 byte PHY チャンネル識別用
  • Access Address: 4 byte 固定値 0x8E89BED6
  • Header: 2 byte パケット種別、長さ情報
  • Protocol Data Unit (PDU): 0-37 byte ペイロードデータ
  • CRC: 3 byte 誤り検出用

一般的に 32 バイトの BLE データと言っているのが、Protocol Data Unit の部分です。だいたい、データとして利用できる範囲は 31 byte 程度です。これが無線でデータとして飛んできます。周波数なので 0/1 のビット列で飛んでいき、これを受信してチェックするわけです。

データ長自体は、たかだか 47 バイト程度です。データ通信の場合はもう少し多くなるのですが、ここでは広告アドバタイズ(iBeacon)や GATT の最初のデバイス検出だけを考えてみましょう。

BLE 1M PHY では、1 Mbps となるので、47 bytes のデータを送ると概ね 400 μs 程度で送信できます。0.4 ms 以下ですね。実に短く一瞬のうちに終わります。

実に短時間で 47 byte のデータが飛び交っているので、3 チャンネルしかない広告チャンネルであっても、iBeacon デバイスが 100 台ぐらいあっても衝突はほとんど起こりません。これは、データ自体が 0.4 ms 程度の短時間で送受信されるためです。

つまり、接触確認アプリを持ったひとたちが 100 人位ぎゅうぎゅうに集まったとしても(コンサートとか満員電車とか)、BLE の広告チャンネルでの衝突はほとんど起こらない、ということです。

これ、現象として、たくさんの人たちが集まると携帯電話が繋がりにくいという現象とは異なることがわかります。携帯電話の場合は、基地局と常に通信することになるので、BLE の広告アドバタイズとは発見タイミングが異なるということです。

アドバタイズ間隔とデータ量

さて、iBeacon の発信や、GATT のデバイス発見のためのアドバタイズが、常に発信できる(常にセントラルで発見できる)状態になっているとは限りません。と言いますか、広告アドバタイズは、どのように発信されているのでしょうか?

実は、47 bytes のデータが常に発信されているわけではなく、一定間隔で発信されています。この間隔をアドバタイズ間隔(Advertising Interval)と言います。

  • 広告間隔(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 アドバタイズの仕様ですね。

つまりは、こんな風に iBeacon デバイスが発信しているときにうまくスマホアプリが受信していないといけないわけです。

このあたり有線通信ではなくて無線通信ってところがミソです。無線通信の場合は、発信側と受信側が常に同期しているわけではないというのと、発信側のバッテリーの問題があります。特に iBeacon 発信デバイスはバッテリーが少ないので、できるだけ発信回数を減らすことが求められます。
つまりは、電力消費の観点から、できるだけ広告間隔(Advertising Interval)を長くしたい、ということです。

スキャン間隔とスキャンウィンドウ

では、iBeacon や GATT のデバイス発見のために、スマホアプリ側はどのように受信しているのでしょうか?
これも常に受信状態にあるわけではなく、スキャン間隔とスキャンウィンドウという仕組みで受信しています。

  • スキャン間隔(Scan Interval)
    – 最小 2.5 ms、最大 10,240 m
    – デフォルトは 100 ms 程度
  • スキャンウィンドウ(Scan Window)
    – 最小 2.5 ms、最大 10,240 ms
    – スキャン間隔以下の値に設定する必要がある
    – デフォルトは 100 ms 程度

緑の部分がスキャンウィンドウ(Scan Window)で、ここで受信を行っている状態です。その Scan 状態も常に続いているわけではなくて、スキャン間隔(Scan Interval)が空いています。
つまり、iBeacon がうまく送信時間になっているときに、スマホアプリのほうがうまく Scan Window の時間に入っていないと受信できない、ということです。
実に絶妙なタイミングが必要なわけです。

ただし、さきほど書いたように送信時間自体は 0.4 ms と非常に短い状態です。
これに対してスマホの Scan Window は最小の設定でも 2 ms 程度はあるし、もっと長くすることも可能です。
バッテリーの問題として、データの発信には電力が必要ですが、データの受信にはあまり電力が必要ではありません。よって、発信は短く、受信は長くしておくのがセオリーです。実際 BLE の送受信はそういう仕組みになっています。

しかし、図のように、必ずしも BLE のアドバタイズのデータがスマホ側の Scan Window にあてはまるとは限りません。真ん中のデータはうまく受信していますが、左のデータは Scan Window の外側で発信されているために受信できていません。

BLE アドバタイズの取りこぼし or 遅延の原因

この Scan Window, Scan Interval と Advertising Interval の関係で、BLE アドバタイズの取りこぼしや発見遅延が発生します。つまりは、確率的に、最初の一発目で受信できることもあれば、なかなか受信できないという現象が発生するわけです。

概算する式的にはこんな感じになります。

発見遅延 = Advertising Interval / Scan Duty Cycle
Scan Duty Cycle = Scan Window / Scan Interval

例えば、Advertising Interval が 100 ms、Scan Interval が 100 ms、Scan Window が 50 ms の場合、Scan Duty Cycle は 0.5 となり、発見遅延は 200 ms となる。つまり、平均して 200 ms 後に発見されることになります。200 ms 後に発見されるならばまだ誤差として良い方です。

例えば、相互のバッテリー消費を少なくするために、次のような設定をしたとします。

  • Advertising Interval: 1000 ms
  • Scan Interval: 2000 ms
  • Scan Window: 100 ms

この場合、Scan Duty Cycle は 0.05 となり、発見遅延は 20,000 ms となる。つまり、平均して 20 秒後に発見されることになります。

これの意味は、20 秒後に必ず発見されるという意味ではありません。
iBeacon デバイスや BLE ワイヤレスイヤフォンのように、その場に居続ける端末の場合にはいいのですが、近接接触確認アプリのように相互に歩いていたり、席から離れたりするときには 20 秒後には居なくなっている可能性もあります。特にすれ違いの場合はいなくなっています。すれ違いなんって距離を含めれば 5 秒もないでしょう。
なので、発見遅延を考えると、20 秒の発見遅延を許す場合は、すれ違いの発見は実質できないということなります。正確に言えば、すれ違いを発見したり、発見しなかったり、という現象が発生します。

これも、必ずしも発見できないわけではなく、確率的に発見できない可能性があるということです。逆に言えば、接触データを調べたときに、接触データがないなかといって接触がなかったとは限らない、という状態です。いわゆる、信頼区間というものがあるのです。
また、この例でいえば、平均 20 秒程度の遅延が発生するわけですから、接触データの時刻に接触したという訳ではないということです。それよりも平均 20 秒前には既に接触している、という意味になります。

Advertising Interval と Scan Duty Cycle のバランス

というわけで、Advertising Interval と Scan Duty Cycle のバランスが重要になります。
店舗などの入店確認では 1 分程度の遅延が許されるが、接触確認アプリではどのくらいの遅延が許されるのかを考えないといけません。

  • 発見遅延が大きい場合(1,2分とか)、すれ違いの場合は検出できない?
  • 発見遅延が小さい場合(数百 ms)は、電力の消費がどのくらいなのか?
  • 発見遅延が小さい場合、数十台の端末が集まったときはどうなるのか?

ということをシミュレーションする必要がでてきます。

余談

ただし、このあたりは物理層としての電波の発信/受信の問題だけでなく、OS 側の BLE スタックの実装(特に セントラル側のスキャン動作)にも依存すると思われます。

よって

  • 電波の到達だけでなく、実働として OS 側の BLE スタックの動作も調査する必要がある
  • 電波は届いているが、発見遅延により検出されない
  • 電波は届いているが、ペリフェラルの端末が多いため、検出されない(プログラムの問題?)

という違いが出てくる。これを実機で確認する必要がでてきます。

このあたり、スマホ端末での BLE が不意に止まってしまう現象を次の話題にします。特に Android の機種によっては、連続した BLE 運用は考えていないらしく、途中で BLE が止まってしまいます。

カテゴリー: 開発, FolkBears | BLE 物理層の詳細とスキャン頻度 はコメントを受け付けていません

BLE 通信チェックツールのあれこれ

しばらく BLE の話を続けます。

COCOA や FolkBears での BLE 通信状態を調べるのには、それなりのツールが必要なのですが、まずは既存の Android/iOS アプリを探していきました…ってのを記録しておきます。
結論から言うと、どれも汎用的過ぎて、結果的に自前でツールを作ることころに至るのですが、初手として BLE 通信を調べたい場合には以下のようなアプリがあります。

リンク先は Android アプリのものですが、iOS 版もあります。

SimpleLink Connect

Simplelink Connect – Apps on Google Play https://play.google.com/store/apps/details?id=com.ti.connectivity.simplelinkconnect&hl=en-US&pli=1

Texas Instruments のテストツールなのですが、これが仕事上一番使いやすかったです。

BLE スキャンを開始すると、周辺の BLE デバイスをスキャンします。「スキャンします」という云い方をしますが、実際は、BLE ペリフェラルが発信しているアドバタイデータを傍受している、という感じです。ちょうどレーダー受信機みたいなものですね。なので、周辺にある BLE 端末がごっそり表示されます。

これ、周辺には結構な量の BLE デバイスがあることが分かります。大抵の端末は名前がついていないので「Name Unknown」となっています。ハッキリ言って、どれがどの発信をしているのか区別が付きません。接続しようとすると接続できないものも混じっているので(ひとつの端末が複数のアドバタイズを使っているものもあり、中華な製品だとデータ内容がめちゃくちゃなものもあります、これは別途ツールを使うと調査ができます)、なかなか目的ものを見つけるのが大変です。

ひとまず、スマホの Android 端末とか、ワイヤレスイヤフォンとか BLE 接続のスピーカーみたいなのも見つかります。
デバイス名がついていないと、どの端末かわからないというのと、同じデバイス名がついていると判別がつかないというのが難点ですが、サービス UUID さえわかっていれば見つけやすいですね。

ただし、アドバタイズがスキャンできなくなったものもリストに残り続けるので、ずっと開いていると動かなくなります。

BLE Scanner

LightBlue® — Bluetooth LE – Apps on Google Play https://play.google.com/store/apps/details?id=com.macdom.ble.blescanner&hl=en-US

最初の頃によく使っていたツールです。

電波強度が強い順、つまり近い順から表示されるので目的の BLE デバイスを見つけやすいです。ひとまず、手元の近くにあるものが上に来ています。下のほうにあるのは、遠くのものなのでひょっとすると外で歩いている人の BLE ワイヤレスイヤフォンとか、iPhone の端末を拾っている可能性が高いです。

デバイスの RAW DATA の部分をタップすると、アドバタイズされているデータが表示されます。

一見すると何がよいのか分かりませんが(私も BLE を最初に扱ったときにはわかりませんでした)この表示が意外と重宝します。BLE データの 32 byte(実際には、先頭のフラグないときがありますが)が 16 進数で表示されているので、データそのものを解読することができます。

ただ、iOS の BLE ライブラリは受信データの制限がきついので BLE Scanner はどうやっているんだろう?という不思議なところがあります。タイプが 0xFF なので manufacturer data を受信しているのだと思いますが…
ただし、無理に iOS で動かさずとも、Android や Windows あるいは m5stack などの端末を使えば、これらのデータは自由に解析できるようになります。

LightBlue

LightBlue® — Bluetooth LE – Apps on Google Play https://play.google.com/store/apps/details?id=com.punchthrough.lightblueexplorer&hl=en-US

LightBlue も BLE デバイスをスキャンしてくれますが、先の2つのアプリとは違い、接続リストが控え目に表示されます。実は、これコネクションできるデバイスだけしか表示しないので、iBeacon のようにアドバタイズのみの場合は発見できません。

ただし、「virtual Devices」のところが便利で、BLE ペリフェラルをシミュレートしてくれます。

いわゆる、BLE セントラルを作っているときに、ペリフェラルの端末のほうがないとか、複数のペリフェラルを作りたいときに便利です。まあ、これも BLE デバイスのほうがないというパターンはあまりないので使いどころが難しいのですが、BLE をサーチするサーバー側を作って、温度やバッテリーなどのセンサーの類を探す、ってところに便利です。

ただし、いまだと、BLE ペリフェラル側を m5stack で作ってしまったほうが楽です。

BeaconSET Plus

BeaconSET Plus – Apps on Google Play https://play.google.com/store/apps/details?id=com.minew.beaconplus&hl=en-US

確か、iBeacon を検出するためのツールなのですが、いわゆる iBeacon 以外の Beacon(Eddystone など) も検出できるはずです。

もともと BeaconPlus | 株式会社テクサー https://techsor.co.jp/product/02113 という製品の検出アプリです。iBeacon のほうはおまけなんだと思うんですが、これを使って iBeacon を検出することができます。

ただし、iOS の場合は、iBeacon の UUID をあらかじめ入れておかないといけないので使い勝手は悪いです。これは iOS の仕様なので仕方がないですね。使うときは Android 版を使うと良いです。

Beacon Scan

Beacon Scan App – App Store https://is1-ssl.mzstatic.com/image/thumb/Purple7/v4/76/bc/c7/76bcc76c-aaa3-6a1a-1e52-8bcbb2dba1f7/pr_source.png/230x511bb.webp

これは iOS アプリのみですが、iBeacon をスキャンするアプリです。これも、iOS の仕様で UUID を設定しておかないといけないのですが、ほどよく検出が可能です。
たしか、同じ UUID を登録しておいて、複数の iBeacon を検出できます。つまり、同じ UUID を使っていてバージョンを変えることによって、複数の iBeacon を検出する、というテストができます。

Android の場合は、iBeacon の UUID は指定しなくても受信は可能なのですが、iOS の場合は 10 個ぐらいだったか数個しか登録できないという制限があります。

そんな訳で、公開されているアプリを使うと便利に BLE デバイスのテストができるんですが、目的の BLE デバイスを見つけるのが結構大変で手間がかかります。SimpleLink Connect とかで見るとわかりますが、周辺には大量に、身元不明の BLE デバイスがあるわけです。
なので、テスト用や負荷試験のために自前で BLE デバイスや検出器を作っていきました。ってのが次の話題です。

カテゴリー: 開発, FolkBears | BLE 通信チェックツールのあれこれ はコメントを受け付けていません

近接接触確認アプリ FolkBears の BLE 通信構造を探る

前置きが長くなりそうですが、ざっとまとめておきます。もともとの発端は新型コロナウィルスの感染対策用アプリのである「COCOA」の前身の頃がスタートになります。いわゆる、近接接触確認アプリというジャンルです。既に COCOA だけでなく近接接触確認アプリ自体は各国で終了となっていますが、次期のため、といいますか、同様の技術を継承するために FolkBears というアプリを開発し続けて今に至っています。

元ネタは まもりあいJapan https://github.com/mamori-i-japan の mamori-i-japan-android と mamori-i-japan-ios をベースにして、北見工業大学 https://www.kitami-it.ac.jp/ が独自開発をして、FolkBears https://github.com/FolkBearsGroup/folkbears-android を開発するに至っています。現在のところ android 版のみコード公開してますが、ios 版もあり、公開のためにドキュメント等を整え中です。

開発経緯自体は、別のことろでまとめるとして、ここでは 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 も同様にコネクション版を使うことができます。

ただし、、これも諸々の実験の事情もあり、コネクション版も不安定なところがあるため、ちょっとトリッキーな形ですが、

  • コネクションレス版の通信
  • iBeacon 形式を利用したコネクションレスの通信

の2種類を用意することができています。

COCOA / ENS の通信状況が検証されていない

このあたり、有耶無耶になっていると思うのですが、COCOA あるいは ENA の実機における通信状況はほとんど検証されていません。研究段階として、ドイツのアプリで距離を測ったデータなどがあるのですが、実際に 10 人集まった時のデータや、連続運用したときのデータや、人がすれ違ったときのデータなどの詳細なデータは公開されていません。すくなくとも私は目にしたことがありません。

というのも、ENA の仕様自体で、開発が国単位に制限されているのと状況が緊迫していた状態から、開発ログなどの研究データを取得する手段が完全にふさがれているためです。いちおう、デバッグモードの情報を取れるようになったおり、試験サーバーを用意して突き合わせをすることは可能なのですが、それをやっているところは見たことがありません。

そのあたり、緊急事態であったので仕方がないところではありますが、せっかくの技術がブラックボックス化してしまっているのは残念なところです。

できるだけ ENA に似せた状態で、研究データが取れる

FolkBears は、ENA に似せた状態で、かつ研究データが取れるように設計し直しています。本格的な運用(ENA が目指した個人の動向の隠蔽など)という点では現状では達していませんが、まずは、スマホ実機を使ったときの動作や通信状態を確認できるようにしています。

相互した結果は別途 aware サーバーに送ることで、どのような通信が行われたかを確認できるようになっています。これをもとに学部生や大学院生が研究ができることを目指しています。

通信構造とシーケンス

COCOA の場合

COCOA の場合、ENA の仕様に従って、以下のような通信構造とシーケンスになっています。

Exposure_Notification_-_Bluetooth_Specification_v1.2.2 より

BLE のデータ自体は 32 byte しか取れないので、Service UUID などの情報を除くと、ID を送信するため(Rolling Proximity Identifier: RPI)に 16 byte、付加情報(Associated Encrypted Metadata: AEM)に 4 byte しか使えません。
これは、先に書いた通り、お金を払って 16 bit の Service UUID を取得すれば使える問題なのですが、FolkBears ではそういうわけにはいきません。さらに言えば、お金を払って 16 bit UUID を取得したとしても、iOS ではコネクションレスのアドバイズができないので、ENA と同じ動作にはできない、という問題が残ります。

余談ですが、iOS を外してしまって、Android だけで実装するとか、M5Stack のようなマイコンで実装する場合には、16 bit UUID を取得して ENA と同じ構造にすることは可能です。これは実際、実験でできています。

FolkBears の場合(コネクション版)

もともと、まもりあい Japan ではコネクション版の通信を使ってあります。現コードでは、データの送受信状態がうまくいかなかったのと、コードの再構成のためにかなり書き直してはありますが、基本的な通信構造は同じです。

BLE のコネクション版では、Service UUID は 128 bit を使うことができます。これはフリーで使えます。
コネクション版のシーケンスは、次のような形です。

この操作を相互に行うことになります。一見すると、このコネクション版ではうまく動作するような気もしますが、実機で連続運用してみるといくつか問題が発生します。(実は、まもりあい Japan モードの場合は、データ送信(ID) の部分を Write/Read の両方でやっているので、相互にデータ送信が行うようになっていました。が、この部分が不安定だったので、いったん片方だけに変更しています)

  • コネクションを頻繁に行うため、BLE 接続で不安定になることが多い
  • Android の機種によって、BLE デバイスが止まることがある
  • Android のバージョンによってバックグランド動作の違いがある
  • Android の機種によって、BLE のスキャン頻度が異なる
  • Android と iOS の接続頻度が異なる
  • iOS からの接続頻度が多くて、Android 側がパンクすることが多い
  • 5,6 台集めたときに、接続されない端末た出ることが多い

接続頻度に関しては、再接続までの感覚を1分間隔にするなどの工夫をしていますが、まだ根本的な解決に至っていません。特に Android の機種(特に中華製)では、BLE が不安定になることが多く、BLE ドライバの ON/OFF を繰り返さないと復帰でない現象がでています。

2020 年頃の秋ごろに COCOA の Android 版で受信がされない現象が頻発していましたが、これが原因かもしれません。そのあたりも検証したいところです。

FolkBears のコードとしては、folkbears-android の以下に実装されています。

  • app/service/GattAdvertise.kt
  • app/service/GattServer.kt
  • app/service/GattClient.kt
  • app/service/GattTraceService.kt

FolkBears の場合(コネクションレス版)

コネクションの場合、コネクション確立と ID のデータ送信の2手順となるため、多くの端末が集結したときに問題がおきそうです。実際、5,6 台の Android を集めると、通信量が多くなって(単純に接続頻度が5倍になってしまいます)しまうので、満員電車やコンサート会場のように 100 台位の接近した状態になるとパンクしそうです。

ただし、実際のところ COCOA のときに試してみたのですが、受信自体は 20 台程度が限界のようです。これは当時、実際にツールを使って AEON で確認してみました。

そうなると、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 を使うことにしています。

これで良さそうなコネクションレスの iBeacon 版ですが、これも実装上いくつかの問題があります。

  • iOS 側のスキャンでは、移動状態しか判別できない
  • iOS ではバックグラウンドで iBeacon の発信ができない
  • iOS と Android で iBeacon 発信の頻度が大きく異なる

特に iOS 側のスキャンでは、相手が移動した(距離が変わった)ときにしかイベントが発生しないらしく、いまのところ常に取得できているように見えますが、実際にどうなるかは不明です。大抵の iBeacon 受信アプリでは、店内に入ったとときや、美術館や博物館で iBeacon 発信機の近くに寄ったときにイベントが発生するので、常に近接している状態ではイベントが発生しないという仕様になっています。

まあ、それであっても、ENA のようにコネクションレスで ID を配信する形にはなるので、比較対象の研究用としてはよいかと思います。

コード的には

  • app/service/BeaconScan.kt
  • app/service/BeaconTraceService.kt
  • app/service/BeaconTransmitter.kt

に実装されています。

長くなったので

長くなったので続きは、別途書きます。
今度はもうちょっと、BLE 通信部分をコードレベルで、実際の動きの詳細を解説します。

余談ですが

余談ですが、FolkBears のリファクタリングには Copilot を使っています。元コードが絡み合ってしまっていて不具合解消が上手くいかない状態が続いていたのですが、ここ1年位の AI コーディングの進歩がすさまじく、結構綺麗にリファクタリングができています。このあたりのノウハウも公開していく予定です。

参考資料

カテゴリー: 開発, FolkBears | 近接接触確認アプリ FolkBears の BLE 通信構造を探る はコメントを受け付けていません

LibreOffice Calc で マクロを使う標準的な方法をおさらい

年末年始でいきおいで作ってしまった ExcelLikeUno というライブラリは「LibreOffice Calc のマクロを Python で書こうとすると UNO API を直接扱わないと大変なので、Excel の VBA マクロのように簡単に書けるようにしよう」というのが趣旨です。
が、そもそも、 LibreOffice Calc というか LibreOffice のマクロを何で書けばいいのか? ってのを自分のためのおさらいしておきます。

つまりは、勢いで作ったものだから、LibreOffice の標準機能とダブって作っても仕方ない、ということに今更ながら気づいた…というところです。

LibreOffice のマクロドキュメント

[The LibreOffice Help Window](https://help.libreoffice.org/latest/en-US/text/shared/05/new_help.html) の下の「Macros and Scripting」にあります。

なぜかうちの環境ではマクロエディタでコード補完が効かないので(何かバグっている?)、じゃあ Python で作るかと思い立ったのですが、実際はコード補完が効くようです。このあたりはよくわかりません。

LibreOffice Basic

いわゆる文法が Basic なマクロです。
「ツール」→「マクロ」→「マクロの記録」で保存されるのは、この LibreOffice Basic です。
UI から操作できて記録できるので、これが一番参考になります。Excel VBA と似た感じで作れます。

コード補完は [Basic IDE](https://help.libreoffice.org/25.8/ja/text/shared/optionen/BasicIDE.html) によって提供されている…筈なのですが、うちの Windows 環境では動きません。ちなみに Ubuntu 版では「Basic IDE」のメニューがありません。

ちなみに、A1 セルを選択して、”masuda” と入力すると次のようなコードが出力されます。

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

これ、今だから分かるのですが、初心者にとっては相当きついです。というか、無理です。
ただし、Excel VBA と同じように、とりあえず記録をしておいて中身は何も見ないで動かしている、人にはいいかもしれません。完全にピンポイントの動かし方になります。

ちなみに Excel でやるとこんなコードになります。

Sub Macro1()
'
' Macro1 Macro
'

'
    Range("A1").Select
    ActiveCell.FormulaR1C1 = "masuda"
    Range("A2").Select
End Sub

これだと参考にして、何かに拡張できそうですよね。Excel VBA が広まったのは、この手軽さと入門のしやすさがあったので、ここぐらいまでは敷居を下げて欲しいところです。

VBA 互換モード

LibreOffice Basic の中に「VBA 互換モード」というのがあります。
[Support for VBA Macros](https://help.libreoffice.org/latest/en-US/text/sbasic/shared/vbasupport.html?DbPAR=BASIC)

どこまで互換性があるかというと、先の Excel VBA のコードをそのまま貼り付けて動かせる、というレベルです。

option vbasupport 1

Sub Macro1()
'
' Macro1 Macro
'
    Range("A1").Select
    ActiveCell.FormulaR1C1 = "masuda"
    Range("A2").Select
End Sub

もう、Excel VBA から移植する場合にはこれで十分ですね、ってレベルです。
先頭に `option vbasupport 1` を入れてしまえば大抵のものが動きます。

ただし、最大の難点は BASIC で書かないといけないことと、Excel VBA の文法を引きずってしまうところです。何か修正するときに BASIC は、まあいいとして、LibreOffice Calc なのに、何故 Excel VBA で書かなくてはいけないのか? という違和感が将来的に残ります。

ScriptForge ライブラリを使う

実は ScriptForge って OSS のライブラリかと思っていたのですが、LibreOffice に標準で付属しているライブラリなんですね。
[ScriptForge Libraries](https://help.libreoffice.org/latest/en-US/text/sbasic/shared/03/lib_ScriptForge.html?DbPAR=BASIC)

先の LibreOffice Basic で書いていたややこしいコードも ScriptForge ライブラリを使うと、かなりシンプルに書けます。

実際のところは

  • LibreOffice Basic で標準的なセルを扱う操作
  • セルの移動や、コピー&ペーストなどは、ScriptForge ライブラリを使う

という使い分けになります。

セル A1 に “masuda” と入力するだけならば、次のコード

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

ScriptForge ライブラリの場合、どちらかというと UI 操作を抽象化しているので、セルの値を直接操作する場合は、LibreOffice Basic の方がシンプルに書けます。

これも慣れるといいんでしょうが、新たに LibreOffice Basic と ScriptForge ライブラリの両方を覚えないといけないのが難点です。
ただし、LibreOffice Calc だけじゃなくて Writer などの機能の網羅しているので、LibreOffice 全体のマクロを作る場合には便利かもしれません。

Python でも ScriptForge ライブラリは使える

ExcelLikeUno ライブラリを作ってから、最近気づいたのですが、Python からでも ScriptForge ライブラリは使えます。なので、基本的に Python でマクロでも UI 操作はScriptForge ライブラリを使うのが標準なのでしょう。

from scriptforge import CreateScriptService
doc = CreateScriptService("Calc")

def macro1():
    doc.SetValue("A1", "masuda")

Calc の内部から動かすマクロでも XSCRIPTCONTEXT 経由と ScriptForge ライブラリを組み合わせて使うとかなりのところまでいけます。

で、ExcelLikeUno ライブラリはどうするか?

LibreOffice Calc のマクロを作ろうとしたときに難点が

  • LibreOffice Basic でコード補完が使えない
  • UNO API を直接使うのが大変
  • Excel VBA 互換モードは、将来的にも Excel VBA の文法を引きずることになる
  • ScriptForge ライブラリは便利だが、コード補完が使えない
  • UNO API の構造と ScriptForge ライブラリの構造を覚えないといけない

というところだったので、私としては “コード補完が使えない” ところが一番のストレスです。まあ、LibreOffice Basic の IDE はうちの環境だけかもしれないのですが、少なくとも Ubuntu 版では Basic IDE のメニューがないので、コード補完は使えません。

あと、今更 Basic でやるのもちょっと不便です。Web API に連携しようとか、外部データベースにアクセスしようとかした場合には、Python のほうが便利です。というか LibreOffice Basic で外部 Web API を呼び出せるのかわかりません。

そうなると、

  • Python で書く
  • LibreOffice Calc を操作するときにコード補完が有効になる
  • Excel VBA っぽく、直感的に簡単な操作がよい(マニュアルを熟読とかしたくない)

というパターンが自分には残るわけで、ScriptForge ライブラリでちょっと使いづらい部分を ExcelLikeUno ライブラリに移植する、ってのがよさそうです。完全移植をやろうとすると大変なので、ダイアログの表示とかは ScriptForge ライブラリに任せた方がよさそう。

参考先

カテゴリー: 開発, LibreOffice | LibreOffice Calc で マクロを使う標準的な方法をおさらい はコメントを受け付けていません