FlutterでiBeaconを発信/検出してみた

FlutterアプリでiBeaconを検出したり、発信したりしてみたので、ざっくりまとめておく。

### iBeaconとは
iBeaconそのものについては、下記の記事を参照。主にiOS等で使われるbeacon技術。

[iBeacon - ウィキペディア](https://ja.wikipedia.org/wiki/IBeacon)
[iBeaconとは - ケータイ Watch Watch](https://k-tai.watch.impress.co.jp/docs/column/keyword/648014.html)

### 発信
発信側は[pszklarska/beacon_broadcast](https://github.com/pszklarska/beacon_broadcast)を使って発信する。

```dart

BeaconBroadcast beaconBroadcast = BeaconBroadcast();
bool isAdvertising = await beaconBroadcast.isAdvertising();
BeaconStatus transmissionSupportStatus = await beaconBroadcast.checkTransmissionSupported();

if (!isAdvertising && transmissionSupportStatus == BeaconStatus.SUPPORTED) {
  final String iBeaconFormat = 'm:2-3=0215,i:4-19,i:20-21,i:22-23,p:24-24';
  await beaconBroadcast
      .setUUID(Uuid().v5(Uuid.NAMESPACE_URL, 'sample.beacon.com'))
      .setMajorId(1)
      .setMinorId(100)
      .setIdentifier('com.beacon.sample')
      .setLayout(iBeaconFormat) // Android Only
      .start();
  Timer(
      Duration(seconds: 10),
          () {
        beaconBroadcast.stop();
      }
  );
}
```
BeaconBroadcastの方は`isAdvertising`で発信中かどうか確認して、`checkTransmissionSupported`で動作する端末かどうか確認して、発信している。
ポイントは`setLayout(iBeaconFormat)`の部分で、iBeaconのフォーマットを指定している点。
iOSだと基本的にiBeaconのフォーマットにしか対応していないみたいで、setLayoutに何も指定せずにAndroid端末からbeaconを発信しても、iOS端末でbeaconを検知できない。
ちなみにBeaconBroadcastはデフォルトではAltBeaconのフォーマットになる。

### 検出
beaconを検出する側は[alann-maulana/flutter_beacon](https://github.com/alann-maulana/flutter_beacon)を使う。

```dart
await flutterBeacon.initializeScanning;

AuthorizationStatus status = await flutterBeacon.authorizationStatus;
if (! (status == AuthorizationStatus.allowed
    || status == AuthorizationStatus.always
    || status == AuthorizationStatus.whenInUse)) {
  Scaffold.of(context).showSnackBar(
    SnackBar(content: Text("位置情報サービスをオンにする必要があります"))
  );
  return;
}

final regions = <Region>[
  Region(
    identifier: 'com.beacon.sample',
    proximityUUID: Uuid().v5(Uuid.NAMESPACE_URL, 'sample.beacon.com'),
  ),
];

StreamSubscription streamRanging = flutterBeacon.ranging(regions).listen((RangingResult result) {
  if (result != null && result.beacons.isNotEmpty) {
    result.beacons.forEach((beacon) {
      if (beacon.proximityUUID.toLowerCase() == uuid.toLowerCase()) {
        print('beaconを検出したよ！');
      }
    });
  }
});

StreamSubscription streamMonitoring = flutterBeacon.monitoring(regions).listen((MonitoringResult result) {
  if (result != null && result.region != null) {
    if (result.region.proximityUUID.toLowerCase() == BEACON_UUID.toLowerCase() && result.monitoringState == MonitoringState.inside) {
      print('beacon圏内に入ったよ！');
    }
  }
});
```

flutterBeaconはrangingやmonitoringのstreamをlistenした時点で各プラットフォームのbeacon検出をスタートさせるようになっていて、止める場合は`streamRanging.cancel()`とすると止めることができる。`streamRanging.close`しちゃうとそれ以降リスタートできないので注意。

rangingはアプリが起動中、常に監視して`RangingResult.beacons`に現在圏内にいるbeaconを配列で受け取ることができる。

monitoringはbeaconの圏内に入った/出たを検出できて、バックグラウンドでも動作する。ただ、iOSの場合は位置情報が常に許可になっていないとバックグラウンドで動作しない。
バックグラウンドでの動作を期待する場合は、`AuthorizationStatus.always`が`false`だったらダイアログを出して設定に誘導する等した方が良いかもしれない。



### 参考記事
- [iOS13 & iOS12で位置情報を「常に許可」してもらうための導線設計に関するあれこれ](https://qiita.com/gaussbeam/items/96612baed85edb8f7c6d)
- [iBeaconで出勤する](https://qiita.com/WorldDownTown/items/f7d37064c393f93ed17d)
- [AltBeaconの実装方法＃領域監視](https://qiita.com/a_nishimura/items/2974fd0956c9e00b6bec)
- [iOSでBeaconの振る舞いを確認する](https://qiita.com/nosaka/items/5348dd5f8889d445d533)