BeaconやRFIDを使って自分で自分を監視する ①Beacon編

この記事は
[ビジネスエンジニアリング株式会社（B-EN-G）アドベントカレンダー2024](https://qiita.com/advent-calendar/2024/b-en-g "B-EN-G アドベントカレンダー2024")の記事です。
 # はじめに
私はB-EN-GのIoT領域のSIを担当する部署に所属していますが、普段の仕事はIoTというよりIT寄りの仕事をしています。
せっかくのアドベントカレンダーという機会を有効活用し、~~会社の備品で遊ぼうと~~IoT技術を学ぼうと筆を取りました。
(素人考えなところが多々あるかと思いますが、何卒ご容赦を)

# 目的
BeaconとRFIDを使って、自分の在席状況と出退勤データを収集します。
集めたデータを可視化したり集計したりして、改善ポイントを探っていきたいと思います。

「自分の監視」について、今回の[B-EN-Gアドベントカレンダー2024](https://qiita.com/advent-calendar/2024/b-en-g "B-EN-G アドベントカレンダー2024")にて、Beacon編とRFID編の計2本の記事を投稿する予す。
前書きや機材構成はBeacon編に記載し、まとめはRFID編に記載する形で分割しております。
本題部分は独立しておりますが、共通部分含めてご興味がありましたら合わせてお読みいただけますと幸いです。
ちなみに両編通してNode.jsとPythonのコードが出てきますが、どちらも本業ではあまり使っていないので、粗が多いことご了承ください。


# 機材構成
### 全体のイメージ
![概要図.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3935919/400d0a34-87ad-67f9-3ce3-86b502c4fa3a.png)

### ハードウェア
| 図中の名前 | メーカー | 型番など | 購入先 | 参考リンク |
|:-----------|:------------|:------------|:------------|:------------|
| カード型Beacon|サンワサプライ|MM-BLEBC8|Amazon|[メーカー公式](https://direct.sanwa.co.jp/ItemPage/400-MMBLEBC8-1 "https://direct.sanwa.co.jp/ItemPage/400-MMBLEBC8-1")|
| カード型RFID|NXP|MFRC522| Amazon  |[マニュアル](https://github.com/WayinTop/C-Python-Java-JP "https://github.com/WayinTop/C-Python-Java-JP")|
| RFIDリーダー| NXP|MFRC522| Amazon  |[マニュアル](https://github.com/WayinTop/C-Python-Java-JP "https://github.com/WayinTop/C-Python-Java-JP")|
| Raspberry Pi|Raspberry Pi Foundation|Raspberry Pi5| Amazon | [メーカー公式](https://www.raspberrypi.com/products/raspberry-pi-5/ "https://www.raspberrypi.com/products/raspberry-pi-5/")|
| Windows PC|MSI|Modern 14|MSI公式| なし |
<table>
<details><summary>ハードウェア補足</summary>
1. RFIDについて<br>
RFIDは、WayinTopというメーカーのラズパイ向けチュートリアルキットに含まれているものを使用しました。<br>
参考リンク先にマニュアルや動かすためのコードが掲載されています。<br>
型番はデータシートに記載のMFRC522としました。こちら単品でも販売されている様です。<br>
某なんとかエクスプレスでは、2桁円/個で売ってました。安すぎて怖い。<br><br>
2. Windows PCについて<br>
本題とは関係の無いソフトウェアが載るハードウェアなので、正直何でも大丈夫です。<br><br>
3. ラズパイ5すごい<br>
ラズパイ3から4を飛ばして5触ったんですが、ハイスペックを謳っているだけあって動きがサクサクで感激しました。<br> 
ラズパイ上で動いている2本のプログラムは、ラズパイ上のVSCodeで開発しました。<br> 
標準のChromiumブラウザで調べつつやっていて、操作感はPCでやっているのとそう変わらなかったです。<br> 
(プログラムが超小規模、VNC接続ではなくネイティブ操作、など要因もあるとは思いますが)


</details>


### ソフトウェア(プログラム)
| 図中の名前 | 説明 |
|:-----------|:------------|
|Beacon連携プログラム|Node.js製。Beaconの電波を受信して、パケットに含まれる社員番号を抽出・送信します。|
|RFID連携プログラム|Python製。RFIDに含まれるデータを読み込んで送信します。|
|出退勤登録画面|Java＋画面のWebアプリ。<br>RFIDのデータ表示と出退勤登録をします。<br>(本題ではないので詳細は割愛します)|
|在席情報可視化ツール(既製品)|私の部署で使っている現場設備の稼働可視化ツール[^tool]です。今回は自分を設備扱いして勤務を可視化しました。<br>(こちらも本題ではないので詳細は割愛します)|

# Beacon連携
### 1. Beaconデバイスの設定
設定の話の前に、Beaconの実物はこちらです。
![beacon4-1.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3935919/827c5bdc-934f-4c2a-8a67-6de7baf82434.jpeg)
せっかくカードホルダーなので社員証を挿そうとしたのですが、
弊社の社員証、厚くて押し込まないと入りませんでした。
破損が懸念されること、特に入れる必要もないことからやめておきました。
![beacon5-2a.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3935919/3fcadcc1-0329-ad14-3fbb-0055fba5127c.png)
参考までに、社員証は1.5ミリでした。1ミリ程度のカードならすんなり入りました。


設定の話に戻ります。
サンワサプライ社のマニュアルに沿って設定しました。(Androidアプリ：SSS-825が必要)
Majorに社員番号を入れておきます。
![beacon1-2.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3935919/5cbb292a-0711-c3b0-fd9a-1c1fa40ac378.jpeg)

### 2. ラズパイの設定
###### 主要ライブラリの事前準備
今回は[@abandonware/noble](https://github.com/abandonware/noble "https://github.com/abandonware/noble")というライブラリを使うことにしました。
最初は本家にあたる[noble](https://github.com/noble/noble "https://github.com/noble/noble")を使おうとしたんですが、古いせいか動かずでしたので、nobleからフォークしたこちらに流れつきました。

そちらのInstallationに従って必要モジュールのインストールを行いました。(詳細はリンク先参照)
```
sudo apt-get install bluetooth bluez libbluetooth-dev libudev-dev
```

###### Node.jsのインストール
Node.jsもインストールします。
一応[nvm](https://github.com/nvm-sh/nvm "https://github.com/nvm-sh/nvm")を入れて、バージョンを切り替えられる様にしました。
インストールで特に困った記憶は無く、リンク先通りのコマンドにてすんなり出来ました。

Node.jsのバージョンは18.19.0を使用しています。
<details><summary>Node.jsバージョンに関する余談</summary>
最初は22でやってたんですが、開発中にエラーが出て、バージョンのせいかと疑って、<br>
サポートギリギリのバージョンまで下げてみました。<br>
結局、(詳細を忘れるくらいの凡ミスで)バージョンは関係なかったんですが、動いたのでそのままにしてます。<br>
(お仕事だったら開発期間にサポートが切れちゃいそうなバージョンは使えないですねぇ)

</details>

[補足]
その他、bluetoothをONにするなどは今回やった記憶は無いですが、
新品のラズパイではないので以前やっているかもしれません。

### 3. Beacon連携プログラムの開発
処理は大きく2つで、どちらも定期処理というか、待機処理というか、繰り返し続けます。
1. 電波受信待機と在席ステータス送信(コードのnoble.onでイベントリスニングする処理)
2. 不在確認と離席ステータス送信(コードのcheckAbsent)

###### 実装したコード
nobleの部分はExampleから持ってきたコードと、ChatGPTに聞いて~~コピペし~~書きました。
検証用の(処理には不要な)データ抽出とその標準出力が大量に有って、長いですね。。。
```javascript
const noble = require('@abandonware/noble');
const { requestCurrentStatus, createEventAbsent, createEventPresent } = require('./zaiseki-tool-util');
const { PERSON_LIST, STATUS_PRESENT, STATUS_ABSENT } = require('./constants');
// (PERSON_LISTには、Beaconを持ってるユーザコードがリスト化されている。現実にはユーザマスタから取るみたいな話だけど、今回は自分しかいないので固定で持っとく。)

class BeaconStatus {
  id;
  status;
  lastFoundDate;
}
const userStatusMap = {};

// iBeaconのUUIDを指定しない場合、すべてのBLEデバイスをスキャンします。
noble.on('stateChange', (state) => {
  console.log('stateChange');
  if (state === 'poweredOn') {
    console.log('poweredOn');
    // すべてのデバイスをスキャン
    noble.startScanning([], true);
  } else {
    console.log('Bluetooth is not powered on');
  }
});

// iBeaconアドバタイズパケットを検出
noble.on('discover', (peripheral) => {
  // iBeaconの情報をアドバタイズパケットから取得
  const advertisement = peripheral.advertisement;

  // iBeaconのUUID, Major, Minorを探す
  if (!advertisement || !advertisement.manufacturerData) {
    // アドバタイズパケットなし
    console.log(`アドバタイズパケットなし`);
    return;
  }
  const manufacturerData = advertisement.manufacturerData;
  // 今回使っているBeaconデバイスのサイズは25
  if (manufacturerData.length !== 25) {
    // 自分のBeaconじゃない
    // console.log(`自分のBeaconじゃない: ${manufacturerData.length}`);
    return;
  }
  const all = manufacturerData.toString('hex');  
  const dataLen = manufacturerData.readUInt8(0); 
  const dataType = manufacturerData.readUInt8(1); 
  const flagData = manufacturerData.readUInt8(2); 
  const dataLength = manufacturerData.readUInt8(3); 
  const uuid1 = manufacturerData.readUInt16BE(5); 
  const mainDataLength = manufacturerData.readUInt8(7);
  // iBeaconのデータ構造に基づいて、MajorとMinorを取得
  const uuid = manufacturerData.slice(4, 20).toString('hex');  // 16バイトのUUID
  const major = manufacturerData.readUInt16BE(20);  // Major（2バイト）
  const minor = manufacturerData.readUInt16BE(22);  // Minor（2バイト）
  const power = manufacturerData.readUInt8(24);  // BatteryLevel or Power（1バイト） ←端末による
  if (PERSON_LIST.indexOf(major) === -1) {
    // majorはユーザコード。対象のユーザじゃない。
    return;
  }
  // とりあえず確認用にありったけ出しとく
  console.log(`iBeacon found!`);
  console.log(`advertisement.localName: ${advertisement.localName}`);
  console.log(`txPowerLevel: ${advertisement.txPowerLevel}`);
  console.log(`serviceUuids: ${advertisement.serviceUuids}`);
  console.log(`serviceSolicitationUuid: ${advertisement.serviceSolicitationUuid}`);
  console.log(`id: ${peripheral.id}`);
  console.log(`address: ${peripheral.address}`);
  console.log(`addressType: ${peripheral.addressType}`);
  console.log(`rssi: ${peripheral.rssi}`);
  console.log(`mtu: ${peripheral.mtu}`);
  console.log(`UUID: ${uuid}`);
  console.log(`Major: ${major}`);
  console.log(`Minor: ${minor}`);
  console.log(`power: ${power}`);
  console.log(`uuid1: ${uuid1}`);
  console.log(`mainDataLength: ${mainDataLength}`);
  console.log(`dataLen: ${dataLen}`);
  console.log(`dataType: ${dataType}`);
  console.log(`flagData: ${flagData}`);
  console.log(`dataLength: ${dataLength}`);
  console.log(`manufacturerData length: ${manufacturerData.length}`);
  console.log(`all: ${all}`);
  console.log('');
  const currentDate = new Date();
  console.log(currentDate);
  const previous = userStatusMap[major];
  if (previous && previous.status === STATUS_PRESENT) {
    // 本当は、サーバ側から現在のステータス取らないといけない。(受信アプリが1台とは限らないので。でも今回は無視してメモリ上で前回判定する。)
    // 前回のステータスも在席だったので、登録はしない
    // マップの時刻だけ更新
    console.log('マップの時刻だけ更新');
    previous.lastFoundDate = currentDate;
    return;
  }

  // 在席ステータスを送信する
  const beaconStatus = new BeaconStatus();
  beaconStatus.id = major;
  beaconStatus.status = STATUS_PRESENT;
  beaconStatus.lastFoundDate = currentDate;
  userStatusMap[major] = beaconStatus;
  // HTTPで送るだけ。送信先は既製品なので諸々割愛。
  requestCurrentStatus(beaconStatus);

});
function checkAbsent() {
  console.log('checkAbsent');
  Object.keys(userStatusMap).forEach(id => {
    const beaconStatus = userStatusMap[id];
    if (beaconStatus.status === STATUS_ABSENT) {
      // 不在送信済みなので次へ
      return;
    }
    // 既定時間経ってるか？
    let previousFoundDate = beaconStatus.lastFoundDate;
    const currentDate = new Date();
    const timeDelta = currentDate.getTime() - previousFoundDate.getTime();
    console.log(`${beaconStatus.id} : ${currentDate.toISOString()} - ${previousFoundDate.toISOString()} = ${timeDelta}`);
    // 最後に検知して5分反応がなかったら、その時間に離席したとみなす → 実際は5分くらいかなとおもったけど、ちゃっちゃと検証できるように10秒にしとく
    // if ((currentDate.getDate() - previousFoundDate.getDate()) <= 1000 * 300) {
      if (timeDelta <= 1000 * 10) {
      // 既定時間以内なので次のユーザをチェック
      return;
    }
    // 最後の検知から既定時間経ってるので、不在を送信する
    beaconStatus.status = STATUS_ABSENT;
    requestCurrentStatus(beaconStatus);
  });
  // 大体1分おきに不在チェック(少しずつずれるけど、細かい精度は要らない) → ちゃっちゃと(略)
  // setTimeout(checkAbsent, 60000);
  setTimeout(checkAbsent, 5000);
}
// 大体1分おきに不在チェック(少しずつずれるけど、細かい精度は要らない) → ちゃっちゃと(略)
// setTimeout(checkAbsent, 60000);
setTimeout(checkAbsent, 5000);
```
ところどころに言い訳が書いてあるのはご容赦を。

###### 動作風景
Majorしか使わないんですが、こんな感じでログが流れてます。
たぶん、いや絶対に間違ってる項目あります。「uuid1」で5桁はないだろうよ。
製品の取扱説明書にデータフォーマットの記載がありますので、正しくはそちらを。
![beacon3-2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3935919/fd2e13c0-6db8-f568-8246-214f93e02d6b.png)

###### Beaconに触れた感想
機器を設置しているのは戸建住宅なんですが、電波自体は結構届くという印象です。
障害物の有無がかなり影響すると思っていて、断熱性の低い寒い我が家では電波が阻まれてないかもしれません。
2階にラズパイがあって、1階に居ても電波拾ってたり、同じ2階でも拾わなかったり、ドアの開閉状況とかで変動してるかもしれません。(ほんとによくわからん)
Beaconの測位は難しい、と経験された方は皆さんおっしゃってますが、確かにこんな不安定な物を責任持ってお客様に提供するのは工夫と努力が必要だなと実感しました。
この記事では何の工夫もせずデータを取っているので上記の様な状態ですが、
実際の事例では、設置場所の現地調整、電波強度(RSSI)のしきい値調整など、現場に合わせた対応を入念に行って精度を高めているとのことでした。



###### データの見え方
緑っぽいところが在席、黄色が離席
![sc_1-1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3935919/b86f0dbb-29f2-08e9-b273-a129cdb8b046.png)

補足情報として場所も入れました。
ラズパイも複数台あるので、家と会社に設置して、、、
なんて考えましたが手が回らなくて断念しました。
(ので、在宅監視システムになってます。のでので、このデータは他人には公開しません！)
![sc_2-1aaa.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3935919/e04352d4-cf0e-6929-d4d8-417e4e1155bf.png)
![sc_4-1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3935919/992edb22-eb85-1134-f9c5-aad5edbbe88b.png)
(他にも色々画面はあるものの、このツールは記事の本題ではないのでこの辺りにします)


# 後編へ続きます
今回はBeaconで在席状況の取得を行ってみました。
次回はRFIDを使って出退勤登録を行ってみたいと思います。


[^tool]: [mcframe SIGNAL CHAIN 稼働モニタリング](https://www.mcframe.com/ja/product/signalchain)