
前回はNanoPi NEO2用タイムレコーダーの超雛形を作ったが、今回はもう少しだけ実用的な雛形にする。
この記事でやること
- カードの記録をデータベースに格納する
- ユーザーをデータベースで管理する
- カード情報をデータベースで管理する
- カードを読むとユーザー名を読み上げる(音声合成)
- 出勤と退勤を読み上げる
- 本日退勤済みの後に再出勤しようとしたら警告する(弾かない)
- 出勤日時が前日の場合は警告する(弾かない)
- 前回のカード読み込みから30秒以内に同じカード読み込みで前回と今回を取り消し
この記事でやらないこと
- 管理ツール作成
- Felica以外のカードへの対応
当然巨大化するのでブログの1ページでやるようなことじゃない。
MySQLのデータベースとユーザーの作成
$ mysql -u root -pパスワード mysql mysql> create database timerecorder; mysql> GRANT ALL PRIVILEGES ON timerecorder.* TO timemgr@localhost identified by 'パスワード'; mysql> FLUSH PRIVILEGES; mysql> quit;
1行目: MySQLの管理者アカウントでmysqlデータベースに接続
3行目: timerecorderデータベース作成
4行目: localhostでのみ活動可能なtimemgrというユーザーにtimerecorderデータベースの全テーブル対して全権を与える(兼ユーザー作成)
5行目: 2行目で登録した権限を反映
6行目: mysqlデータベースを抜ける
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 | CREATE TABLE timelog (
`id` bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT,
`nid` varchar(16) NOT NULL,
`dtime` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`finout` bit(1) NOT NULL DEFAULT 0,
`flag` bit(1) NOT NULL DEFAULT 1,
PRIMARY KEY (id)
) ENGINE=InnoDB;
CREATE TABLE card (
`id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT,
`nid` varchar(16) NOT NULL,
`uid` int(10) NOT NULL,
`stdate` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`endate` datetime NOT NULL DEFAULT '2199-12-31 23:59:59',
`flag` bit(1) NOT NULL DEFAULT 1,
PRIMARY KEY (id)
) ENGINE=InnoDB;
CREATE TABLE user (
`id` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
`uname` varchar(32) NOT NULL,
`uruby` varchar(32) NOT NULL,
`stdate` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`endate` datetime NOT NULL DEFAULT '2199-12-31 23:59:59',
`flag` bit(1) NOT NULL DEFAULT 1,
PRIMARY KEY (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE junk (
`id` bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT,
`nid` varchar(16) NOT NULL,
`dtime` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id)
) ENGINE=InnoDB;
|
timelogテーブルは利用者がカードリーダーにカードをかざしたときに記録するためのテーブル。junkテーブルも同様だが、timelogテーブルは登録されたカードがかざされた場合に記録され、未登録のカードがかざされた場合はjunkテーブルに入る。
4つのテーブル共にidカラムはオートインクリメントなので基本触らない。
timelog.nid は登録済みのカードのNFCID2が記録される。
timelog.dtime は登録済みのカードがかざされた時間が記録される。
timelog.finout は出勤(1)or退勤(0)が記録される。
timelog.flag は記録として有効(1)か無効(0)かのフラグ。
card.nid はカードのNFCID2が記録される。
card.uid はカード所有者のID(user.id)が記録される。
card.stdate はカードの有効期間の開始日時が記録される。管理用(このページでは未使用)
card.endate はカードの有効期間の終了日時が記録される。管理用(このページでは未使用)
card.flag はカードが有効(1)か無効(0)かのフラグ。管理用
user.uname はカードの利用者(所有者)名が漢字等で記録される。
user.uruby はカードの利用者(所有者)名のふりがなが記録される。
user.stdate は利用者(所有者)の有効期間の開始日時が記録される。管理用(このページでは未使用)
user.endate は利用者(所有者)の有効期間の終了日時が記録される。管理用(このページでは未使用)
user.flag は利用者(所有者)が有効(1)か無効(0)かのフラグ。管理用
junk.nid は未登録のカードのNFCID2が記録される。
junk.dtime は未登録のカードがかざされた時間が記録される。
$ mysql -u timemgr -pパスワード timerecorder < table.sql ←テーブル作成 $ mysql -u timemgr -pパスワード timerecorder ←timerecorderデータベースに入る mysql> insert into user (uname,uruby) values('山田太郎','やまだたろう'); ←利用者登録 mysql> insert into card (nid) values('0000000000000000'); ←カード登録 (NFCID2を登録) mysql> quit;
ユーザーを登録してからカード登録。
cardテーブルのuid(ユーザーID)をNOT NULLにしているのでユーザー無しのカード登録はできない(という前提)、基本的にはユーザーを登録してからカードを登録するという流れになる。もちろんダミーのユーザーIDを登録するなら順序は関係ない。
管理ツールを作成して会社で購入したカードを登録しておくなら最初に全てのカードを管理者ユーザーの所有としてフラグで殺しておいて別ユーザーに割り当て変えをしてフラグを有効化するということになるかと。
ウェブサーバー側(PHPとMySQLが入っていること)
db_recorder.php1 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 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 | <?php
header( 'Expires: Fri, 1 Jab 2010 00:00:00 GMT' );
header( 'Last-Modified: ' . gmdate( 'D, d M Y H:i:s' ) . ' GMT' );
header( 'Cache-Control: no-store, no-cache, must-revalidate' );
header( 'Cache-Control: post-check=0, pre-check=0', false );
header( 'Pragma: no-cache' );
$json_string = file_get_contents('php://input');
$data = json_decode($json_string);
if (json_last_error() != JSON_ERROR_NONE){
echo "不正です";
exit;
}
$nid = $data->nid;
$dsn = 'mysql:dbname=timerecorder;host=localhost;charset=utf8mb4';
$user = 'timemgr'; //MySQL用timemgrアカウント
$password = 'パスワード'; //MySQL用timemgrアカウントのパスワード
$conn = new PDO($dsn, $user, $password);
//$conn->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
$conn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$sql = "select * from card
where nid = '" . $nid . "'
and stdate < NOW()
and endate > NOW()
and flag = 1;";
$stmt = $conn->query($sql);
$results = $stmt->fetchall();
if (empty($results)){
$sql = "insert into junk
(nid)
value
('" . $nid . "');";
$conn->exec($sql);
$msg = "みとうろくカードです";
} else {
foreach ($results as $row) {
$uid = $row['uid'];
}
$sql = "SELECT * FROM user
where id = '" . $uid . "'
and stdate < NOW()
and endate > NOW()
and flag = 1;";
$stmta = $conn->query($sql);
$resultsa = $stmta->fetchall();
if (empty($resultsa)){
$msg = "ユーザーみとうろく";
} else {
//以下有効カードの場合の処理
foreach ($resultsa as $rowa) {
$uname = $rowa['uname'];
$uruby = $rowa['uruby'];
}
//タイムログ処理
//nidの最終記録を確認
$sql = "select * from timelog
where nid = '" . $nid . "'
and dtime < NOW()
and flag = 1
order by dtime desc limit 1;";
$stmta = $conn->query($sql);
$resultsa = $stmta->fetchall();
if (empty($resultsa)){
$finout = 1; //記録が無ければ出勤=1から
$dtime = '2000/01/01 00:00:00';
$msg = $uruby . "さん、しゅっきん。";
} else {
foreach ($resultsa as $rowa) {
$lid = $rowa['id'];
$dtime = $rowa['dtime'];
$finout = $rowa['finout']; //bitカラムの値の扱い要注意
}
if($finout == 1){ //直前の有効レコードが出勤
$finout = 0;
//前回の記録が出勤 日付をまたいでいたら警告
if (mktime(0,0,0) > strtotime($dtime)){
$msg = $uruby . "さん、たいきん。しゅっきんがきのうです。かくにんしてください。";
} else {
$msg = $uruby . "さん、たいきん";
}
} else { //直前の有効レコードが退勤
$finout = 1;
//前回の記録が退勤 それが本日なら警告
if (strtotime($dtime) > strtotime(date("Y/m/d 00:00:00"))){
$msg = $uruby . "さん、しゅっきん。ぜんかいのたいきんがほんじつです。かくにんして下さい。";
} else {
$msg = $uruby . "さん、しゅっきん。";
}
}
}
//最終記録が30秒以内の場合は最終を取り消し
// さらに今回分も無効レコードとして登録(とにかく記録はする)
if ((time() - strtotime($dtime)) < 30 ){
$sql = "update timelog
set flag = 0
where id = $lid;";
$conn->exec($sql);
$sql = "insert into timelog
(nid, finout, flag)
value
('" . $nid . "', " . $finout . ", 0);";
$conn->exec($sql);
if ($finout = 0){
$sfinout = "しゅっきん";
} else {
$sfinout = "たいきん";
}
$msg = $uruby. "さん、30秒いないの" . $sfinout . "、とりけし";
} else {
//記録を付ける
$sql = "insert into timelog
(nid, finout)
value
('" . $nid . "', " . $finout . ");";
$conn->exec($sql);
}
}
}
unset($conn);
echo $msg;
?>
|
PHPでMySQLの操作はPDOを使用。
仕組みだけなのでエラー制御は無し。スマートさより何したいか解るように書いたのでトリッキーなコードも無し。凄い単純なのでネットワークの不通やデータベースとの接続失敗以外は殆どエラーらしいエラーが発生しないと思う。
NanoPiのOpen JTalkに読み上げさせるのでNanoPi側に返すメッセージ($msg)は基本ひらがなとする。ひらがな綴りを上手く読めない(イントネーションがおかしくなる)場合は逆にそこだけ漢字にする。またはイントネーション的に適切になるよう違う漢字を当てる(例: 漢字→幹事)
NanoPi NEO2側
speaking_card_recorder.sh1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | #!/bin/sh
HOSTPATH=example.com/timerecorder/db_recorder.php
LOGFILE=/var/log/card.log
TMP=/tmp/tmp.wav
echo none > /sys/class/leds/nanopi:blue:status/trigger
while :
do
nid=$(nfc-poll | grep NFCID2 | cut -d: -f 2 | sed 's/ //g')
if ! [ x"$nid" = x ];then
echo `date '+%Y/%m/%d %H:%M:%S'` $nid >>$LOGFILE
RESP=`curl http://$HOSTPATH -s -X POST -H "Content-Type: application/json" -d '{"nid": "'$nid'"}' 2&>/dev/null`
echo heartbeat > /sys/class/leds/nanopi:blue:status/trigger
echo $RESP
echo $RESP | open_jtalk -m /usr/share/hts-voice/mei/mei_normal.htsvoice -x /var/lib/mecab/dic/open-jtalk/naist-jdic -ow $TMP && aplay --quiet $TMP
rm -f $TMP
echo none > /sys/class/leds/nanopi:blue:status/trigger
sleep 1
fi
done
|
NanoPi NEO2側は基本的には前回から変わらない。サーバーから返された文字列をOpen JTalkで読み上げる処理が増えただけ。
OpenJTalkでの読み上げについてはNanoPi NEOがTwitterを声でツブヤクンデスを参照。このページにあるMei(女性声)まで入れる。
前回はカード読み取り後2秒は次のカードを読まないようにしていたが、今回はOpen JTalkが喋る間は次のカードを読み取らないようにしている。
読み上げバッチを裏で動かす方法も考えたが、読み上げ中に次の読み上げが発生するのが嫌だった。その分、次の利用者がカード読み込みを待たされることにはなるけど。
サンプル
「山田太郎さん、出勤」
普通に朝に出勤してきてカードをカードリーダーにかざすとこのようになる筈。
「田中一郎さん、退勤」
普通に(朝の)出勤の次にカードをカードリーダーにかざすとこのようになる筈。
「田中一郎さん、出勤。前回の退勤が本日です。確認して下さい。」
本日既に退勤しているのに再び出勤。ありえなくはないシチュエーションだが、一応確認を促す。再出勤が間違いなら30秒以内にもう一度カードをかざすと取り消される。
「山田太郎さん、30秒以内の "退勤" 取り消し。」
これは退勤するつもりが無いのに間違ってカードをかざして退勤になってしまったので30秒以内に再度カードをかざしたときのメッセージ。直前の "退勤" が取り消された(無効フラグ)。もちろん取り消し用の2回めのカードかざしも記録はするが無効フラグ。
「山田太郎さん、退勤。出勤が昨日です。確認して下さい。」
これは残業で日付が変わったなどでありえなくはないシチュエーションだが、昨日の退勤時にカードをかざし忘れた可能性を警告している。
利用者が警告メッセージを聞いて、本当に間違いであれば早急に管理者にデータを修正して貰うという運用を想定している。管理者がデータを修正するとか登録するとか記録されたデータを活用するためのアプリは自分で作ってねと。プログラムできなくてもAccessとかあれば簡単よ。
関連記事:- アッチッチなNanoPi NEO3を冷やしたい パッド交換
- NanoPi NEO3冷却力強化後のUnixBench
- アッチッチなNanoPi NEO3を冷やしたい
- NTPサーバの時刻ソースに対するズレの調整
- NanoPi NEO3をv6プラスのルーターにする systemd-networkd + nftables
- NanoPi NEO3のUSB3.0ポートのネットワーク速度
- NanoPi NEO3でArmbian よきところでUnixBench
- NanoPi NEO3が届いた
- NanoPi NEOにRTCモジュールを付ける
- 新しい中華GPSモジュールとChronyで作るNTPサーバ (中編)
- 新しい中華GPSモジュールとChronyで作るNTPサーバ (前編)
- Prometheus2とGrafana6によるシステム監視 シングルボードコンピュータの温度表示
- NanoPi NEOでNTPサーバ再構築 (全まとめ)
- NanoPi NEO2をv6プラスのルーターにする 後編
- NanoPi NEO2をv6プラスのルーターにする 前編
- ELK Stackでシステム監視 FilebeatでNTP統計ログ取得 Logstashで加工
- NanoPi NEO2(arm64)用にFilebeatをビルド
- NanoPi NEO2を超コンパクトなアルミケースに入れる
- NanoPi NEO2用armbian 5.41 Debian 9 Stretch next 4.14.18
- NanoPi NEO2を100均の灰皿に入れてみた
- NanoPi NEO2のシステム監視 RPi-Monitorとnetdata
- NanoPi NEOとGPSモジュール用アルミケースを作る
- NanoPi NEO2 + DACで音楽プレーヤーVolumioを使う
- NanoPi NEO2にDACを接続
- NanoPi NEO2の最大クロック引き下げ後のUnixBench 再び
- NanoPi NEO2用armbian 5.32 Debian 9 Stretch 4.13.0-RC6
- NanoPi NEO2用armbian 5.32 Debian jessie 4.13.0-RC6
- NanoPi NEOをSIP電話機にする 後編 (その2)
- NanoPi NEO2とICカードリーダーでタイムレコーダーを作る(実用化編)
- NanoPi NEO2とICカードリーダーでタイムレコーダーを作る
お世話になります。
いつも拝見させていただいています。
PHPのどのバージョンを用いてもエラーが発生します。
PHP Parse error: syntax error, unexpected ';' in /var/www/html/nfc/db_recorder.php on line 99
php -lで確認してもエラーが発生します。
文字コードを確認しても同じなのです。
db_recorder.phpをどこかにアップロードをしていただけませんか。
エラーとなること、考えられることなどご教授いただけないでしょうか。
よろしくお願いします。
スミマセン。今ちょっと見たところでは99行目の「<」になっている部分、これがブログで表示するためのHTMLコード装飾でおかしくなった部分のようで、正しくは「 < 」です。HTMLでは「 < 」は「<」と書かないと不完全なHTMLタグと認識されて正しく表示されない場合があるので変換しているのですが、その変換でバグったようです。後で直したいと思います。
ご教授ありがとうございました。
おかげさまで、稼働できました。
素晴らしいソースコードですね。
公開いただき感謝しています。
2点、連絡させていただきます。
1)db_recorder.phpの18行目に全角スペースが入っています。
2)スペルミス
mysql -u timemrg -pパスワード timerecorder < table.sql
正)timemgr
mysql -u timemrg -pパスワード timerecorder
正)timemgr
私の動作環境は以下の通りです。
CentOS 7.7
Apache 2.4.6
MariaDB 10.4.12
PHP 7.4.5
ありがとうございました。
今後ともよろしくお願いいたします。
ご指摘ありがとうございました。直しました。
ソースはわかり易さ優先で低レベル化した結果、無駄に長くなり逆にわかりにくいかもしれません、スイマセン。