この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。
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を利用させて頂きました。
$ sudo pip install nfcpy
$ git clone https://github.com/nfcpy/nfcpy.git
サンプルコードで、動作の確認を行うことができます。
$ 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の北海道版)を読ませてみると下記のように表示されてました。
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しているのは、カードリーダーデバイスの使用のための権限です)
$sudo node ./card.js Device001
card.js
<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です。
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で更新されたことを伝え、リフレッシュさせています。
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をどのように設計するかは、非常に大事だと感じました。
カードをかざしたときの各種の効果音は、効果音ラボのものを利用させて頂きました。