reTerminalでカードリーダーを使ったタイムレコーダーを実装する

# enebularで簡単にタイムレコーダーを実装
会社での勤怠管理や社内イベントの入退場管理で人の流れをデータで見える化することは、労働時間順守や効率的なイベント運営など役立つことが多いでしょう。
そこで会社のIDカードをカードリーダーで読み取ってデータを記録するタイムレコーダーを使ってみましょう！と言っても、IDカードを使うタイムレコーダーはなかなか高額なので、このために複数台用意するとなると効率が悪い気がしますよね。

なので、今回はenebularのエージェント実行環境とreTerminal(Rasberry Pi 4)を使って、カードリーダーにIDカードをタッチすることで、カードの情報とタイムスタンプをGoogle スプレッドシートに記録するタイムレコーダーを実装します。

reTerminalであれば比較的安価ですし、別の用途でも利用できます！加えて、enebularのエージェント実行環境を使えば、無料アカウントでも遠隔でデバイスへのフローのデプロイやログの確認、メンテナンスが可能です！
enebularのエージェント実行環境の利用方法については、[enebularのドキュメント](https://docs.enebular.com/ja/GetStarted/AgentExecutionEnvironmentTutorial.html)を参照ください。
![arch.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3081906/a9f8e56e-8231-b3a4-aea8-a3a5ce91cf3e.png)


# 事前準備
では本題の実装にはいります。今回はreTerminal（Raspberry Pi）にインストールしたenebularのエージェント実行環境でフローを動作させるので、まずは以下のものを用意してください。
* reTerminal
    * Raspberry Pi Compute Module 4
    * OS：Raspbian GNU/Linux 11 (bullseye)
* ACアダプター
    * DC5V 3A以上推奨（USB Type-Cコネクタから給電）
* カードリーダー
    * RC-S380
* IDカード
    * MIFARE
* enebularアカウント
* Googleアカウント

# Google スプレッドシートの準備
今回はNode-REDのG SheetノードでGoogle スプレッドシートの読み書きをするため、[enebular blog](https://blog.enebular.com/nodes/letsuse-google-spreadsheet-node/)を参照して、Google スプレッドシート APIを利用するための設定をしてください。
さらに、利用するスプレッドシートに以下の2つのシートを作成してください。
* カードから読み取った情報の書き込みを行うシート
    * 名称：TimeRecorder
* カードのIDと社員名の対応関係のシート
    * 名称：IdList

カードのIDと社員名の対応関係のシートは画像のように2行目以降のA列に社員名、B列にカードのIDを入力します。
![list.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3081906/3e93f246-db9b-e885-c955-9154c5302f03.png)


# enebular側の設定
今回は以下のフローをエージェント実行環境で動作させます。
![flow2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3081906/18e14465-d4c1-b79c-3cab-d60aba6d5746.png)
```
[{"id":"90b8741ee0f2ce5e","type":"tab","label":"フロー1","disabled":false,"info":"","env":[]},{"id":"70873b8c7c4b2365","type":"ui_tab","name":"タイムレコーダー","icon":"dashboard","disabled":false,"hidden":false},{"id":"743d52f95ad5cccd","type":"ui_base","theme":{"name":"theme-light","lightTheme":{"default":"#0094CE","baseColor":"#0094CE","baseFont":"-apple-system,BlinkMacSystemFont,SegoeUI,Roboto,Oxygen-Sans,Ubuntu,Cantarell,HelveticaNeue,sans-serif","edited":true,"reset":false},"darkTheme":{"default":"#097479","baseColor":"#097479","baseFont":"-apple-system,BlinkMacSystemFont,SegoeUI,Roboto,Oxygen-Sans,Ubuntu,Cantarell,HelveticaNeue,sans-serif","edited":false},"customTheme":{"name":"UntitledTheme1","default":"#4B7930","baseColor":"#4B7930","baseFont":"-apple-system,BlinkMacSystemFont,SegoeUI,Roboto,Oxygen-Sans,Ubuntu,Cantarell,HelveticaNeue,sans-serif"},"themeState":{"base-color":{"default":"#0094CE","value":"#0094CE","edited":false},"page-titlebar-backgroundColor":{"value":"#0094CE","edited":false},"page-backgroundColor":{"value":"#fafafa","edited":false},"page-sidebar-backgroundColor":{"value":"#ffffff","edited":false},"group-textColor":{"value":"#1bbfff","edited":false},"group-borderColor":{"value":"#ffffff","edited":false},"group-backgroundColor":{"value":"#ffffff","edited":false},"widget-textColor":{"value":"#111111","edited":false},"widget-backgroundColor":{"value":"#0094ce","edited":false},"widget-borderColor":{"value":"#ffffff","edited":false},"base-font":{"value":"-apple-system,BlinkMacSystemFont,SegoeUI,Roboto,Oxygen-Sans,Ubuntu,Cantarell,HelveticaNeue,sans-serif"}},"angularTheme":{"primary":"indigo","accents":"blue","warn":"red","background":"grey","palette":"light"}},"site":{"name":"Node-REDダッシュボード","hideToolbar":"false","allowSwipe":"false","lockMenu":"false","allowTempTheme":"true","dateFormat":"YYYY/MM/DD","sizes":{"sx":48,"sy":48,"gx":6,"gy":6,"cx":6,"cy":6,"px":0,"py":0}}},{"id":"6cf70f7cda391dcd","type":"ui_group","name":"カードリーダーサンプル","tab":"70873b8c7c4b2365","order":1,"disp":false,"width":"25","collapse":false,"className":""},{"id":"a9d46f53.6fb3b","type":"ui_group","name":"メーター","tab":"1999e4bf.78d11b","order":1,"disp":false,"width":"10","collapse":false},{"id":"5af2ba41.c53b94","type":"ui_group","name":"グラフ","tab":"1999e4bf.78d11b","order":2,"disp":false,"width":"10","collapse":false},{"id":"1999e4bf.78d11b","type":"ui_tab","name":"環境センサ","icon":"dashboard","disabled":false,"hidden":false},{"id":"0337c4901d050b8e","type":"gauth","name":"card-readers@cardreadertest-368208.iam.gserviceaccount.com"},{"id":"af116394dd7cc880","type":"debug","z":"90b8741ee0f2ce5e","name":"","active":true,"tosidebar":true,"console":true,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":310,"y":760,"wires":[]},{"id":"ff04755c0e936d75","type":"function","z":"90b8741ee0f2ce5e","name":"toUpperCase","func":"msg.payload=msg.payload.toUpperCase();\nreturnmsg;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":330,"y":820,"wires":[["90dcef19598be322"]]},{"id":"552714e27ff7fb8e","type":"catch","z":"90b8741ee0f2ce5e","name":"","scope":["3cbfd01c45339fcb","c089b203df0d1b1d","2cbc12203771b275"],"uncaught":false,"x":1310,"y":1400,"wires":[["76cf48f90a28102e"]]},{"id":"76cf48f90a28102e","type":"debug","z":"90b8741ee0f2ce5e","name":"","active":true,"tosidebar":true,"console":true,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":1550,"y":1400,"wires":[]},{"id":"cfd1c9275fab2f9c","type":"comment","z":"90b8741ee0f2ce5e","name":"スプレットシートへの書き込み","info":"","x":1010,"y":820,"wires":[]},{"id":"3d68f51ee67a85aa","type":"inject","z":"90b8741ee0f2ce5e","name":"初期化","props":[{"p":"sheet","v":"SPREAD_SHEET_ID","vt":"env"},{"p":"cells","v":"CELLS_RANGE","vt":"env"}],"repeat":"","crontab":"","once":true,"onceDelay":0.1,"topic":"","x":160,"y":100,"wires":[["50c8a2ce370cdaf9","345bbbc1f047f772","98602f51f78d5750","54a3f61407048f7a","f141eb1c2f79caea"]]},{"id":"50c8a2ce370cdaf9","type":"change","z":"90b8741ee0f2ce5e","name":"","rules":[{"t":"set","p":"sheet","pt":"global","to":"sheet","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":380,"y":180,"wires":[[]]},{"id":"4e74c9e4124db382","type":"comment","z":"90b8741ee0f2ce5e","name":"フロー開始時に処理","info":"","x":170,"y":60,"wires":[]},{"id":"90dcef19598be322","type":"change","z":"90b8741ee0f2ce5e","name":"","rules":[{"t":"change","p":"payload","pt":"msg","from":"TYPE2TAGID=","fromt":"str","to":"","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":560,"y":820,"wires":[["186c8ec2dc357456"]]},{"id":"37638457fd71790b","type":"ui_text","z":"90b8741ee0f2ce5e","group":"6cf70f7cda391dcd","order":1,"width":25,"height":11,"name":"","label":"<fontsize=7color={{msg.color}}>{{msg.status}}</font>","format":"<fontsize=6>{{msg.employeeId}}</font><br><fontsize=6>{{msg.cardId}}</font>","layout":"col-center","className":"","x":1110,"y":600,"wires":[]},{"id":"345bbbc1f047f772","type":"exec","z":"90b8741ee0f2ce5e","command":"DISPLAY=:0chromium-browser--noerrdialogs--kiosk--incognitohttp://localhost:1880/ui","addpay":false,"append":"","useSpawn":"false","timer":"","winHide":false,"oldrc":false,"name":"","x":610,"y":260,"wires":[[],[],[]]},{"id":"98602f51f78d5750","type":"exec","z":"90b8741ee0f2ce5e","command":"DISPLAY=:0xset-dpms;DISPLAY=:0xsetsoff;DISPLAY=:0xsetsnoblank","addpay":true,"append":"","useSpawn":"false","timer":"","oldrc":false,"name":"","x":570,"y":360,"wires":[[],[],[]]},{"id":"6e659a08a1b258b6","type":"inject","z":"90b8741ee0f2ce5e","name":"画面表示内容の初期化","props":[{"p":"status","v":"PleasescanyourIDcard...","vt":"str"},{"p":"color","v":"#000000","vt":"str"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":true,"onceDelay":0.1,"topic":"","x":200,"y":580,"wires":[["fe56274b5700dd96"]]},{"id":"fe56274b5700dd96","type":"change","z":"90b8741ee0f2ce5e","name":"","rules":[{"t":"set","p":"payload","pt":"msg","to":"","tot":"str"},{"t":"set","p":"status","pt":"msg","to":"PleasescanyourIDcard...","tot":"str"},{"t":"set","p":"color","pt":"msg","to":"#000000","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":560,"y":600,"wires":[["37638457fd71790b"]]},{"id":"a15590cb38122652","type":"delay","z":"90b8741ee0f2ce5e","name":"","pauseType":"delay","timeout":"1500","timeoutUnits":"milliseconds","rate":"1","nbRateUnits":"1","rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":false,"allowrate":false,"outputs":1,"x":340,"y":660,"wires":[["fe56274b5700dd96"]]},{"id":"55287707b88de4d9","type":"change","z":"90b8741ee0f2ce5e","name":"","rules":[{"t":"set","p":"id_list","pt":"global","to":"payload","tot":"msg","dc":true}],"action":"","property":"","from":"","to":"","reg":false,"x":640,"y":460,"wires":[[]]},{"id":"54a3f61407048f7a","type":"change","z":"90b8741ee0f2ce5e","name":"","rules":[{"t":"set","p":"cell_count","pt":"global","to":"2","tot":"num"},{"t":"set","p":"before_id","pt":"global","to":"","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":380,"y":100,"wires":[[]]},{"id":"64c4af4c1b22c1a8","type":"comment","z":"90b8741ee0f2ce5e","name":"コンテキストの初期化","info":"","x":400,"y":60,"wires":[]},{"id":"dbcea7f5d98c6611","type":"comment","z":"90b8741ee0f2ce5e","name":"スプレッドシートのIDをセット","info":"","x":430,"y":140,"wires":[]},{"id":"50f799c21904be10","type":"comment","z":"90b8741ee0f2ce5e","name":"スリープしないようにキャンセル","info":"","x":430,"y":320,"wires":[]},{"id":"afc449ae58621aff","type":"comment","z":"90b8741ee0f2ce5e","name":"ブラウザ表示のディスプレイ指定","info":"","x":430,"y":220,"wires":[]},{"id":"8462b9c5929d5849","type":"comment","z":"90b8741ee0f2ce5e","name":"IDと社員名との対応の読み込み","info":"","x":430,"y":420,"wires":[]},{"id":"b51eedc1ce83b9d4","type":"comment","z":"90b8741ee0f2ce5e","name":"画面表示を待機状態に変更","info":"","x":190,"y":620,"wires":[]},{"id":"c3eb194a9c3ced67","type":"comment","z":"90b8741ee0f2ce5e","name":"画面表示","info":"","x":1120,"y":560,"wires":[]},{"id":"41fb79bb40de4d1a","type":"comment","z":"90b8741ee0f2ce5e","name":"エラー時の出力","info":"","x":1340,"y":1360,"wires":[]},{"id":"186c8ec2dc357456","type":"function","z":"90b8741ee0f2ce5e","name":"","func":"varinfo=[];\nmsg.sheet=global.get(\"sheet\");\nmsg.id=msg.payload;\ninfo.push(msg.id);\nconstid_list=global.get(\"id_list\");\nvarid=msg.id;\nid=id.replace(/\\r?\\n/g,\"\");\nmsg.payload=\"未登録\";\nid_list.map((item)=>{\nif(item[1]==id){\nmsg.payload=item[0];\n}\n})\ninfo.push(msg.payload);\nvard=newDate();\nvarlocalTime=d.getTime();\nvarlocalOffset=d.getTimezoneOffset()*60000;\nvarutc=localTime+localOffset;\nvaroffset=-9.0;\nvarresult=utc-(3600000*offset);\nvardate=parseInt(newDate(result)/1000);\nvardt=newDate(result);\n\nvaryear=dt.getFullYear();\nvarmonth=dt.getMonth()+1;\nvarday=dt.getDate();\nvarhour=dt.getHours();\nvarminute=dt.getMinutes();\nvarsecond=dt.getSeconds();\nmsg.payload=year+\"/\"+month+\"/\"+day+\"\"+hour+\":\"+minute+\":\"+second;\ninfo.push(msg.payload);\nmsg.payload=info;\nreturnmsg;","outputs":1,"noerr":2,"initialize":"","finalize":"","libs":[],"x":780,"y":820,"wires":[["3f8ff640880eeebd","3c4cbf91af2da274"]]},{"id":"433ca2b6c7dc6b06","type":"complete","z":"90b8741ee0f2ce5e","name":"","scope":["8ea142f5fc8a7904"],"uncaught":false,"x":150,"y":660,"wires":[["a15590cb38122652"]]},{"id":"8ea142f5fc8a7904","type":"debug","z":"90b8741ee0f2ce5e","name":"output","active":true,"tosidebar":true,"console":true,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":1180,"y":860,"wires":[]},{"id":"3c4cbf91af2da274","type":"function","z":"90b8741ee0f2ce5e","name":"","func":"msg.status='OK!';\nmsg.color='#00ff7f';\nmsg.cardId='ID:'+msg.payload[0];\nmsg.employeeId='社員名:'+msg.payload[1];\nreturnmsg;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":940,"y":780,"wires":[["37638457fd71790b"]]},{"id":"aa76b074e6c0f053","type":"catch","z":"90b8741ee0f2ce5e","name":"","scope":["3f8ff640880eeebd"],"uncaught":false,"x":690,"y":560,"wires":[["d7b20fcfb30d4f23"]]},{"id":"d7b20fcfb30d4f23","type":"change","z":"90b8741ee0f2ce5e","name":"","rules":[{"t":"set","p":"status","pt":"msg","to":"ERROR","tot":"str"},{"t":"set","p":"cardId","pt":"msg","to":"情報を正しく取得できませんでした","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":880,"y":560,"wires":[["37638457fd71790b"]]},{"id":"ea96790dc56d1ec5","type":"comment","z":"90b8741ee0f2ce5e","name":"スプレットシートへの書き込み時のエラー出力","info":"","x":820,"y":520,"wires":[]},{"id":"f54006c86b48f355","type":"nfc-id-reader","z":"90b8741ee0f2ce5e","name":"","x":150,"y":780,"wires":[["af116394dd7cc880","ff04755c0e936d75"]]},{"id":"f141eb1c2f79caea","type":"GSheet","z":"90b8741ee0f2ce5e","creds":"0337c4901d050b8e","method":"get","action":"","sheet":"","cells":"","flatten":false,"name":"IDとの対応の読み込み","x":400,"y":460,"wires":[["55287707b88de4d9"]]},{"id":"3f8ff640880eeebd","type":"GSheet","z":"90b8741ee0f2ce5e","creds":"0337c4901d050b8e","method":"append","action":"","sheet":"","cells":"TimeRecorder!A2","flatten":false,"name":"","x":970,"y":860,"wires":[["8ea142f5fc8a7904"]]}]
```
本フローの動作には、nfc-id-readerノードのインポート、G Sheetノードのインストールと設定、環境変数の設定が必要です。
G Sheetノードの設定は、[enebular blog](https://blog.enebular.com/nodes/letsuse-google-spreadsheet-node/)を参照ください。

## 環境変数の設定
エージェント実行環境をreTerminalにインストールした後、エージェント実行環境の設定で以下の2つの環境変数を追加してください。
* カードのIDと名前の対応関係のスプレッドシートの範囲の設定
    * キー：CELLS_RANGE
    * 値：対応関係のスプレッドシートの範囲（例：`IdList!A2:B50`）
* 利用するスプレッドシートIDの設定
    * キー：SPREAD_SHEET_ID
    * 値：スプレッドシートID
    `https://docs.google.com/spreadsheets/d/*****スプレッドシートID*****/edit#gid=0`

## nfc-id-readerノードのインポート
本フローでカードリーダーを利用するための[nfc-id-readerノード](https://enebular.com/discover/privatenode/cf95897e-3c58-4366-a7e6-884d86ce4aa2)をインポートします。
nfc-id-readerノードを利用する際に、Python3の環境と必要なPythonモジュールのインストール、設定が必要になります。Raspberry Piで以下のコマンドを実行して、「Sony Corp.」という表示左の4文字のデバイス番号を確認します。
```
$ lsusb
```
次に以下のコマンドを実行します。
```
$ pip3 install nfcpy
$ cat << EOF | sudo tee /etc/udev/rules.d/nfcdev.rules
SUBSYSTEM=="usb", ACTION=="add", ATTRS{idVendor}=="054c", ATTRS{idProduct}=="確認したデバイス番号", GROUP="plugdev"
EOF
$ reboot
```
最後に、フローをreTerminalにインストールしたエージェント実行環境にデプロイすれば準備完了です。

# 動作確認
reTerminalを起動すると、以下のような待機画面表示になります。
![wait_page.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3081906/7dbfd9b4-0590-43b9-e419-0406fc47810d.png)

IDカードをカードリーダーにタッチすると、「OK!」とカードの情報が表示されます。少しすると、待機画面に戻ります。
![ok.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3081906/1dbe51b7-6c21-9624-5224-66817cfb4ceb.png)

スプレッドシートを確認すると、以下のようにIDとそれに対応する社員名、タイムスタンプが記録されました。
![tr.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3081906/9a659b5e-2999-1b26-5018-ba3908924d1f.png)

# まとめ
enebularとreTerminalを使って、タイムレコーダーを実装できました。
今回はreTerminalを使うことを前提にフローを作成しましたが、少し編集すれば通常のRaspberry Piでも利用できます。
記録する項目を追加したり、タッチすると音が鳴るようにするなど自由に作り変えてみてはください。