Raspberry PiとNFCタグリーダを使い勤怠管理。出退勤打刻装置を作成（第1回：まずは完成させる）

# はじめに
今回は会社で使用している出退勤管理システム(Akashi)への打刻をNFCタグを使って「ピッ」と記録させようというものです。
現在の打刻方法はPCブラウザやスマホアプリの画面からの操作が必要であり、実現することでよりスマートなものになると思っています。
有料オプションをでNFCタグリーダを導入することも可能なのですが、打刻APIが公開されているということなのでそれを実行する装置を作成してみようと思いましたので備忘録として投稿させていただきます。

# 作成にあたっての方針
時間とお金はかけない。
凝ったプログラミングはしない。
本人認証データ管理はきちんとやる
インターネット上はSSL通信とする
NFCタグは既存のものを代用する

# ということで仕様は
## ハードウェア（大体6,000円程度）
- Raspberry Pi Zero W（打刻装置の制御用）
- sony PaSoRi RC-380（NFCタグリーダ）
- NFCカード(現在使用している各自の入室用のカードキーを使用)
- 自作の「出勤」「退勤」「休憩入り」「休憩戻り」状態切り替えボード
- 打刻完了時に鳴動する圧電ブザー


## ソフトウェア（AWS費用が多少掛かるが気にならない程度）
- タクトスイッチとLEDを制御するプログラムはc言語
- NFCリーダからの読み込みは[nfcpy](https://pypi.python.org/pypi/nfcpy)のpythonサンプルプログラムを改造して利用する。
- ユーザデータを格納するためのデータベースはAWSのDynamoDBを使用する。
- [AkashiのAPI](https://akashi.zendesk.com/hc/ja/articles/115000475854-AKASHI-%E5%85%AC%E9%96%8BAPI-%E4%BB%95%E6%A7%98)を実行するプログラムは、Pythonで書きAWSのLambdaで実行する。
- AWSのLambda functionを実行するために、AWSのAPI Gatewayを使用する。

# 概要

## 全体イメージ
![Untitled (2).png](https://qiita-image-store.s3.amazonaws.com/0/146154/3ff32365-7e42-0207-0b8c-2aa745d2e565.png)


## 打刻装置画像
![20180323_234606.jpg](https://qiita-image-store.s3.amazonaws.com/0/146154/0aa2ddc5-cca3-2197-9c52-c7b48f2cc949.jpeg)

## 打刻装置構成
- Raspberry Pi Zero W 1台
- Pasori 1台
- LED 4個
- タクトスイッチ 1個
- プルダウン抵抗 1個
- 圧電ブザー 1個

## 処理構成

![akashi.PNG](https://qiita-image-store.s3.amazonaws.com/0/146154/012b8f8b-50d2-860f-def3-e7d1943efaef.png)


- Raspberry Pi Zero W にNFCタグリーダ(sony PaSoRi RC-380)を接続
- 「出勤」「退勤」「休憩入り」「休憩戻り」の4状態をボタンで切り替えるためのボードを作成してRaspberry Pi Zero Wに接続
- 打刻が完了したかどうかの判断をするためのブザーをRaspberry Pi Zero Wに接続
- NFCタグは各自に配布している入室用カードが利用できるのでそのIDを使用
- NFCタグからのデータ読み出し確認はスマホアプリ[NFC TOOL](https://play.google.com/store/apps/details?id=com.wakdev.wdnfc)とかを使用
- AKASHIのAPIを実行するために、利用者単位でのトークンが必要。そのデータはAWS DynamoDBに格納する
- AKASHIの打刻APIは、AWS Lambda functionから実行される
- AWS API Gatewayを使いRaspberry Pi Zero Wからのリスエストを受け付けてAWS Lambda functionを実行する
- AWS API Gatewayのリクエストが成功した場合は、Raspberry Pi Zero Wに接続されているブザーが鳴動する
- AKASHIのトークン利用期限が一か月なのでAWS CloudWatch eventをトリガにしてAWS Lambda functionからAKASHIのトークン再発行APIを実行し、トークンを更新する




# 詳細情報

## 打刻装置回路図

![Untitled Sketch_回路図.png](https://qiita-image-store.s3.amazonaws.com/0/146154/bc6d2b3f-c34c-57bf-1ddc-cfb48c059253.png)


## プログラムコード

## raspberry Pi 制御

```c:tactsw-led.c
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <time.h>
#include <sys/time.h>
#include <netdb.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <wiringPi.h>

//rasp GPIO
#define GPIO_17		0	//Pin - wiringPi pin 0 is BCM_GPIO 17
#define GPIO_18		1	//Pin - wiringPi pin 1 is BCM_GPIO 18 for PWM
#define GPIO_22		3	//Pin - wiringPi pin 3 is BCM_GPIO 22
#define GPIO_23		4	//Pin - wiringPi pin 4 is BCM_GPIO 23
#define GPIO_24		5	//Pin - wiringPi pin 5 is BCM_GPIO 24
#define GPIO_25		6	//Pin - wiringPi pin 6 is BCM_GPIO 25


#define FILE_LED_STAUS      "/tmp/tactsw-led.tmp"
#define FILE_BEEP_STAUS     "/tmp/beep.tmp"

//プロトタイプ宣言
int FileWiteStatus(char *fname,int nNo);
int FileReadStatus(char *fname);
/***********************************************************************************
*	タクトスイッチによる４状態を作る(LED表示あり)
***********************************************************************************/
int main(int argc, char *argv[])
{
    int nTmp,nTactSwitch=0,nLedNo=0,nBeep;
    int nGPIO_Led[4] ={GPIO_17,GPIO_18,GPIO_22,GPIO_23};

    if(FileWiteStatus(FILE_LED_STAUS,nLedNo) == -1)return -1;       //LED状態をファイルにファイルに書き込み　初期値
    if(FileWiteStatus(FILE_BEEP_STAUS,0) == -1)return -1;      //BEEP状態をファイルにファイルに書き込み　初期値

	wiringPiSetup();
	pinMode(GPIO_24, INPUT);
	
	pinMode(nGPIO_Led[0], OUTPUT);
	digitalWrite(nGPIO_Led[nLedNo], HIGH);

	pinMode(nGPIO_Led[1], OUTPUT);
	digitalWrite(nGPIO_Led[1], LOW);

	pinMode(nGPIO_Led[2], OUTPUT);
	digitalWrite(nGPIO_Led[2], LOW);

	pinMode(nGPIO_Led[3], OUTPUT);
	digitalWrite(nGPIO_Led[3], LOW);

	pinMode(GPIO_25, OUTPUT);
	digitalWrite(GPIO_25, HIGH);    //beep

	for (;;) {
            usleep(100000);    //100ms

            nTmp = digitalRead(GPIO_24);
            if(nTmp != nTactSwitch){                    //押された？
                nTactSwitch = nTmp;
                printf("%d\n",nTactSwitch);
                if(nTactSwitch == 0){                   //離された？
               	    digitalWrite(nGPIO_Led[nLedNo], LOW);
                    nLedNo++;
                    if(nLedNo >= 4){
                        nLedNo=0;
                    }
                    if(FileWiteStatus(FILE_LED_STAUS,nLedNo) == -1)break;          //LED状態をファイルに書き込み
              	    digitalWrite(nGPIO_Led[nLedNo], HIGH);
                    printf("ledNo:%d\n",nLedNo);
                }
            }

            nBeep = FileReadStatus(FILE_BEEP_STAUS);          //BEE状態をファイルから読み込み
            if(nBeep == -1)break;
            if(nBeep == 1){
            	digitalWrite(GPIO_25, LOW);    //beep ON
                printf("beep[%d]\n",nBeep);
                usleep(150000);    //150ms
                if(FileWiteStatus(FILE_BEEP_STAUS,0) == -1)break;          //BEEP状態をファイルに書き込み beep停止
            	digitalWrite(GPIO_25, HIGH);    //beep OFF
            }
	}

	digitalWrite(nGPIO_Led[0], LOW);
	digitalWrite(nGPIO_Led[1], LOW);
	digitalWrite(nGPIO_Led[2], LOW);
	digitalWrite(nGPIO_Led[3], LOW);
	digitalWrite(GPIO_25, HIGH);

	return 0;
}

/****************************************************************
*       状態をファイルに書き込み
*   No:0..9
*   return -1:NG 0:OK
****************************************************************/
int FileWiteStatus(char *fname,int nNo)
{
    FILE *fp;
    int val=0x30;
    
    if((fp = fopen(fname, "w")) == 0 ) {
		printf("fopen err\n");
		return -1;
    }

    val += nNo;
    fwrite(&val, 1,1, fp);
    fclose(fp);
	
    return 0;
}
/****************************************************************
*       状態をファイルから読み込み
*   return -1:NG 0..9:OK
****************************************************************/
int FileReadStatus(char *fname)
{
    FILE *fp;
    int val;
    
    if((fp = fopen(fname, "rb")) == 0 ) {
        printf("fopen err\n");
        return -1;
    }

    fread(&val, 1,1, fp);
    fclose(fp);
	
    return val-0x30;
}


```


```c:Makefile

all:
	gcc -g tactsw-led.c -o tactsw-led -L/usr/local/lib -I/usr/local/include -lwiringPi
clean:
	rm -f  tactsw-led
```

### nfcタグ読み込み

nfcpyは 0.13.4を使用、cli.pyとtagtool.py
サンプルプログラム tagtool.pyをnfc-tagtool.py として　def show_tag(self, tag):にコードを追加する

```python:nfc-tagtool.py

   def show_tag(self, tag):
        #-------------ここから追加------------------------
        print("--- start")
        print(tag)
        print(vars(tag))
        tagId = "".join(format(x, '02x') for x in tag._nfcid)
        print(tagId)
        #タクトスイッチ状態LED状態read
        f = open("/tmp/tactsw-led.tmp","rb")
        ledPos =  f.read(2)     
        f.close()
        try:
            print("ledPos:"+ledPos)
            data = {
                'userId': tagId,
                'statusCode': ledPos
            }
            headers = {
                'Content-type': 'application/json',
                'x-api-key': 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
            }
            response = requests.post('https://xxxxxxx.execute-api.ap-northeast-1.amazonaws.com/prod/nfc-id',data=json.dumps(data),headers=headers)
            #print(json.dumps(dict(response.headers)))
            print(response.status_code)
            if response.status_code == 200:
                #タッチ完了
                f = open("/tmp/beep.tmp","w")
                val = f.write("1")     
                f.close()
        except:
             print("*** AWS requests failed.")

        print("--- end")
        #------------ここまで----------------------------------
        if tag.ndef:

```

### 打刻処理制御

```python:lambda_function.py

import urllib
import json
import requests
from encodings.utf_8 import decode

def lambda_handler(event, context):
    # TODO implement
    import boto3
    import json    

    if event['userId'] == "":
        raise Exception("Parameter error. Missing field 'userId'.")

    if event['statusCode'] == "":
        raise Exception("Parameter error. Missing field 'statusCode'.")

    try:
        dynamodb = boto3.resource('dynamodb', region_name='ap-northeast-1')
        table    = dynamodb.Table('xxxxxx')
        res1 = table.get_item(
            Key={
                'id': event['userId']
            }
        )
    except:
        raise Exception("Internal error. 'dynamodb'.")

     
    if json.dumps(res1).find('token') == -1:
        raise Exception("Parameter error. token Not found 'userId'.")


    if event['statusCode'] == '0':
        statusCode='11'
    elif event['statusCode'] == '1':
        statusCode='12'
    elif event['statusCode'] == '2':
        statusCode='31'
    elif event['statusCode'] == '3':
        statusCode='32'
    else:
        raise Exception("Parameter error. Missing value 'statusCode 0..3'.")        
    
        
    params = (
        ('token', res1['Item']['token']),
        ('type', statusCode)
    )
    response = requests.post('https://atnd.ak4.jp/api/cooperation/xxxxxxx/stamps', data=params)
    
    if response.status_code != 200:
        raise Exception("Parameter error. Akashi API(1).")

    j = response.json()
    if j['success'] == 'false':
        raise Exception("Parameter error. Akashi API(2).")
                    
    return response                

```

### トークン再発行処理制御

```python:lambda_function.py

import urllib
import json
import requests
import boto3
import datetime
from encodings.utf_8 import decode

def lambda_handler(event, context):
   
    try:
        dynamodb = boto3.resource('dynamodb', region_name='ap-northeast-1')
        table    = dynamodb.Table('XXXXXX')
        response = table.scan()
        
    except:
        raise Exception("Internal error. 'dynamodb'.")

    print('----------------------------')
    today = datetime.datetime.today()
    for num in range(response['ScannedCount']):
        limitDay = datetime.datetime.strptime(response['Items'][num]['token-limit'], "%Y/%m/%d %H:%M:%S")
        if (limitDay - today).days <= 5: # 5日前になったら更新
            print('5日前')
           
            params = {
                'token': response['Items'][num]['token']
            }
            akashiResponse = requests.post('https://atnd.ak4.jp/api/cooperation/token/reissue/xxxxxxx', data=params)
            if akashiResponse.status_code == 200:
                j = akashiResponse.json()
                DynamoWrite(response['Items'][num]['id'],response['Items'][num]['name'],j['response']['token'],j['response']['expired_at'])
    print('----------------------------')
    return response                

def DynamoWrite(id,name,token,tokenLimit):
    print('--- token update ' + id +' ' +name +' ' +token+' ' +tokenLimit)

    try:
        dynamodb = boto3.resource('dynamodb', region_name='ap-northeast-1')
        table    = dynamodb.Table('xxxxxx')
        res1 = table.put_item(
            Item={
                'id':id,
                'name':name,
                'token':token,
                'token-limit':tokenLimit
            }
        )
    except:
        raise Exception("Internal error. 'DynamoWrite() '.")
    
    

```

### Raspberry Pi Zero W 起動時に動作させるため、/etc/rc.localへ設定

```sh:/etc/rc.local

/root/tactsw-led/tactsw-led &
/root/tactsw-led/nfc-tagtool.py -l &

```

# コード(GitHub)
https://github.com/shigeru-yokochi/Attendance-Management

# 次にやること
- 圧電ブザー音が空しいのでmp3などの音源にする。
- 基板むき出しなのきちんと筐体に入れる

# 今回の感想

AWS API GatewayとLambdaを使ったが、パラメータ渡しと応答コードを判定するプログラム作成に時間がかかってしまった。
ネット上にこのあたりの情報が少ないと感じましたので、同じように困っている方はコードを参考にしていただければ幸いです。


# 続きはここ

 [第2回：NG処理とwav音源再生処理の追加](https://qiita.com/shigeru-yokochi/items/791b6836746668b6689a)