2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

この記事は
ビジネスエンジニアリング株式会社(B-EN-G)アドベントカレンダー2024の記事です。

はじめに

私はB-EN-GのIoT領域のSIを担当する部署に所属していますが、普段の仕事はIoTというよりIT寄りの仕事をしています。
せっかくのアドベントカレンダーという機会を有効活用し、会社の備品で遊ぼうとIoT技術を学ぼうと筆を取りました。
(素人考えなところが多々あるかと思いますが、何卒ご容赦を)

目的

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

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

機材構成

全体のイメージ

概要図.png

ハードウェア

図中の名前 メーカー 型番など 購入先 参考リンク
カード型Beacon サンワサプライ MM-BLEBC8 Amazon メーカー公式
カード型RFID NXP MFRC522 Amazon マニュアル
RFIDリーダー NXP MFRC522 Amazon マニュアル
Raspberry Pi Raspberry Pi Foundation Raspberry Pi5 Amazon メーカー公式
Windows PC MSI Modern 14 MSI公式 なし
ハードウェア補足 1. RFIDについて
RFIDは、WayinTopというメーカーのラズパイ向けチュートリアルキットに含まれているものを使用しました。
参考リンク先にマニュアルや動かすためのコードが掲載されています。
型番はデータシートに記載のMFRC522としました。こちら単品でも販売されている様です。
某なんとかエクスプレスでは、2桁円/個で売ってました。安すぎて怖い。

2. Windows PCについて
本題とは関係の無いソフトウェアが載るハードウェアなので、正直何でも大丈夫です。

3. ラズパイ5すごい
ラズパイ3から4を飛ばして5触ったんですが、ハイスペックを謳っているだけあって動きがサクサクで感激しました。
ラズパイ上で動いている2本のプログラムは、ラズパイ上のVSCodeで開発しました。
標準のChromiumブラウザで調べつつやっていて、操作感はPCでやっているのとそう変わらなかったです。
(プログラムが超小規模、VNC接続ではなくネイティブ操作、など要因もあるとは思いますが)

ソフトウェア(プログラム)

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

Beacon連携

1. Beaconデバイスの設定

設定の話の前に、Beaconの実物はこちらです。
beacon4-1.jpg
せっかくカードホルダーなので社員証を挿そうとしたのですが、
弊社の社員証、厚くて押し込まないと入りませんでした。
破損が懸念されること、特に入れる必要もないことからやめておきました。
beacon5-2a.png
参考までに、社員証は1.5ミリでした。1ミリ程度のカードならすんなり入りました。

設定の話に戻ります。
サンワサプライ社のマニュアルに沿って設定しました。(Androidアプリ:SSS-825が必要)
Majorに社員番号を入れておきます。
beacon1-2.jpg

2. ラズパイの設定

主要ライブラリの事前準備

今回は@abandonware/nobleというライブラリを使うことにしました。
最初は本家にあたるnobleを使おうとしたんですが、古いせいか動かずでしたので、nobleからフォークしたこちらに流れつきました。

そちらのInstallationに従って必要モジュールのインストールを行いました。(詳細はリンク先参照)

sudo apt-get install bluetooth bluez libbluetooth-dev libudev-dev
Node.jsのインストール

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

Node.jsのバージョンは18.19.0を使用しています。

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

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

3. Beacon連携プログラムの開発

処理は大きく2つで、どちらも定期処理というか、待機処理というか、繰り返し続けます。

  1. 電波受信待機と在席ステータス送信(コードのnoble.onでイベントリスニングする処理)
  2. 不在確認と離席ステータス送信(コードのcheckAbsent)
実装したコード

nobleの部分はExampleから持ってきたコードと、ChatGPTに聞いてコピペし書きました。
検証用の(処理には不要な)データ抽出とその標準出力が大量に有って、長いですね。。。

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

Beaconに触れた感想

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

データの見え方

緑っぽいところが在席、黄色が離席
sc_1-1.png

補足情報として場所も入れました。
ラズパイも複数台あるので、家と会社に設置して、、、
なんて考えましたが手が回らなくて断念しました。
(ので、在宅監視システムになってます。のでので、このデータは他人には公開しません!)
sc_2-1aaa.png
sc_4-1.png
(他にも色々画面はあるものの、このツールは記事の本題ではないのでこの辺りにします)

後編へ続きます

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

  1. mcframe SIGNAL CHAIN 稼働モニタリング

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?