[日本語Alexa] 社員証をかざして出退勤を記録するスキルを作ってみた
1 はじめに
今回は、カードリーダーとAlexaの連携サンプルとして、出退勤を記録するスキルを作ってみました。
社員証を使用することで、誰がAlexaに話しかけたのかを判別しています。
最初に、動作している様子をご覧ください。
社員証をかざすと出退勤が記録できます。社員証をかざさなかったり、社員証以外のカード(Felica)を提示すると怒られます。
2 構成
構成は、図のとおりです。
カードリーダーに社員証をかざすと、IDを読み取ってMQTTでパブリッシュします。AWS IoT Coreでは、ルールに基づいてその内容をDynamoDB(カード情報保存DB)に書き込みます。
Echoから呼び出されたスキルは、最初にカード情報保存DBを確認し、10秒以内に更新された情報を対象に、カードのIDが有効なものかを確認してから動作を初めます。
スキルは、社員証が有効な場合のみ、勤怠記録DBに出勤若しくは、退勤を記録します。
勤怠記録DBが更新されると、Webページ更新用のLambdaが起動され、表示用のWebページを更新してS3に格納します。また、同時にMQTTで、ブラウザにページが更新されたことを伝えます。
ブラウザは、サブスクライブ状態で待機しており、トピックが来ると、ページをリフレッシュしています。
3 カードリーダー
今回使用したカードリーダーは、ソニー SONY 非接触ICカードリーダー/ライター PaSoRi RC-S380です。
PythonでNFCリーダを取り扱うライブラリとしてnfcpyを利用させて頂きました。
1 2 | $ sudo pip install nfcpy $ git clone https://github.com/nfcpy/nfcpy.git |
サンプルコードで、動作の確認を行うことができます。
1 2 3 4 5 | $ sudo python nfcpy/examples/tagtool.py No handlers could be found for logger "nfc.llcp.sec" [nfc.clf] searching for reader on path usb [nfc.clf] using SONY RC-S380/P NFC Port-100 v1.11 at usb:001:004 ** waiting for a tag ** |
試しにKitaca(Suicaの北海道版)を読ませてみると下記のように表示されてました。
1 | Type3Tag 'FeliCa Standard (RC-S???)' ID=010102XXXXXXX0C25 PMM=100B4XXXXXXXD0FF SYS=0003 |
4 AWS IoT Core
(1) Publish
作成した「モノ」は、attendanceです。証明書を発行し、デバイスにコピーします。
パブリッシュが正常に動作しているかどうかは、AWSコンソールのテストで確認できます。
(2) ルール
続いて、ルールを設定して、publishされたデータをDynamoDBに書き込みます。
テーブルは、予め作成しておきます。
そして、AWSコンソールのACTからルールを追加します。
DynamoDBへのアクションの設定は、以下のようになっています。
正常に動作するとDynamoDBのデータが更新されることを確認できます。
5 コード(RaspberryPi上)
デバイス側でカードをIDを読み取って、パブリッシュするコードは、以下のとおりです。
コマンドラインからは、以下のように使用します。(sudoしているのは、カードリーダーデバイスの使用のための権限です)
1 | $sudo node ./card.js Device001 |
card.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 | <br />const deviceModule = require( './node_modules/aws-iot-device-sdk' ).device; const exec = require( 'child_process' ).exec; const topicName = 'attendance_card' ; const host = 'xxxxxxxxx.iot.us-east-1.amazonaws.com' const region = 'us-east-1' ; const clientId = 'attendance' ; const device_id = process.argv[2]; const device = deviceModule({ keyPath: './cert/private.key' , certPath: './cert/cert.pem' , caPath: './cert/root-CA.crt' , clientId: clientId, host: host, region: region, reconnectPeriod:10 }); device.on( 'connect' , async () => { // MQTT接続完了 console.log( 'device connect' ); while ( true ){ try { // カード情報の読み取り const card_id = await card_read(); let data = { date_time: create_datetime_string(), device_id: device_id, card_id : card_id, }; // パブリッシュ device.publish(topicName, JSON.stringify(data)); console.log( "publish " + JSON.stringify(data)); // チャイム await chime(); } catch (error) { console.log(error); } } }); function create_datetime_string() { var now = new Date(); return now.getFullYear() + '/' + ( "0" + ( now.getMonth() + 1 ) ).slice(-2) + '/' + ( "0" + now.getDate() ).slice(-2) + ' ' + ( "0" + now.getHours()).slice(-2) + ':' + ( "0" + now.getMinutes()).slice(-2) + ':' + ( "0" + now.getSeconds()).slice(-2); } async function card_read(){ return new Promise((resolve, reject) => { exec( 'python ../nfc/nfcpy/examples/tagtool.py' , (err, stdout, stderr) => { if (err) { reject(err); } else { const card_id = stdout.match(/ID=(.*?)\s/); resolve(card_id[1]); } }); }) } async function chime(){ return new Promise((resolve, reject) => { const cmd = 'mpg321 chime.mp3' ; exec(cmd, (err, stdout, stderr) => { if (err) { reject(err); } else { console.log(cmd); resolve( "success" ); } }); }) } |
6 スキル作成
(1) インテント
インテントとして定義したのは、以下のようなものです。
- RecordIntent 記録して/タイムカード
- WorkingHoursOfDayIntent 今日の勤務時間は?/今日は何時間だった?
- WorkingHoursOfMonthIntent 先月の勤務時間は?/先月は何時間だった?
勤怠の記録を行っているメインのインテントはRecordIntentです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | const RecordIntentHandler = { canHandle(h) { return isMatch(h, 'RecordIntent' , 'LaunchRequest' ); }, async handle(h) { console.log(JSON.stringify(h.requestEnvelope)); // DynamoDBからカードIDを取得する const card_id = await cardRead( 'Device001' ); // カードIDから社員の名前を取得する const name = getNameFromCardId(card_id); let speak = '' ; if (card_id == undefined){ speak = '社員証をかざしてからご利用ください' ; } else if (name == undefined) { speak = 'IDが無効です。この社員証はご利用になれません。' ; } else { // 日付文字列の生成 const datetime = CreateDateTime(); const time = CreateTime(); // 勤怠記録DBに記録する(出勤を記録した場合1、退勤を記録した場合0が返される) const state = await record(datetime, card_id); if (state == 1) { speak = name + 'さん、おはようございます。' + time + '<break time="300ms"/>出勤を記録しました。今日もいちにち、宜しくお願いいたします。' } else { speak = name + 'さん、お疲れ様でした。 <break time="1s"/>' + time + '<break time="300ms"/>退勤を記録しました。' } } return h.responseBuilder .speak(speak) .getResponse(); } }; |
7 勤怠記録DBの更新とWebページの更新
スキルで社員証が認識され、勤怠記録が走ると、出勤若しくは、退勤の時間がDBに保存されます。
このDBが更新されたタイミングで発火されるLambdaは、下記のとおりです。
予めブラウザ表示用のページの元となるファイル(base.html)をS3に配置しておき、そのファイルを読み込んで、データベースの内容を反映したindex.htmlを更新します。また、同時に、ブラウザに対してMQTTで更新されたことを伝え、リフレッシュさせています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 | const AWS = require( "./AWS" ); const tableName = 'attendance_record_table' ; const bucket = 'attendance-html' ; const srcHtml = 'base.html' ; const dstHtml = 'index.html' ; const endpoint = 'xxxxxxxxxx.iot.us-east-1.amazonaws.com' ; const topic = "attendance_refresh" ; exports.handler = async (event) => { const aws = new AWS(); // S3からbase.htmlをダウンロードする const srcData = await aws.s3_get(bucket, srcHtml); let text = decodeURIComponent(escape(String.fromCharCode.apply( null , srcData.Body))); // DynamoDBの勤怠レコードでbase.htmlからindex.htmlを作成する const scanData = await aws.dynamoDb_scan(tableName); if (scanData) { // 日付でソート scanData.Items.sort( function (a,b){ if ( a.datetime.S < b.datetime.S ) return 1; if ( a.datetime.S > b.datetime.S ) return -1; return 0; }); let n = 0; scanData.Items.forEach ( item => { if (n < 10){ text = text.replace( '${DateTime_0' + n + '}' ,item.datetime.S); text = text.replace( '${Name_0' + n + '}' ,createNameString(item.card_id.S)); text = text.replace( '${State_0' + n + '}' ,createStateString(item.state.N)); n++; } }); } // index.htmlをアップロードする const contentType = "text/html" ; await aws.s3_upload(bucket, dstHtml, text, contentType); // MQTTでブラウザに更新されたことを伝える let result = await aws.iot_publish(topic, endpoint, '{"action":"refresh"}' ); console.log(result); }; |
予め用意されているファイル(base.html)は、以下のようなものです。
そして更新されたファイル(index.html)は、次のようになります。
8 最後に
今回は、カードリーダーとAlexaの連携を試してみました。 AWS Iot を経由することで、AWSのリソースと簡単に連接できました。
なんでも自由につなげそうですが、あくまで、起動がAlexa側になるので、そのUIをどのように設計するかは、非常に大事だと感じました。
カードをかざしたときの各種の効果音は、効果音ラボのものを利用させて頂きました。