Internet of Things:Device側のPythonを記述する
行動をタイムスタンプするシステム(行動と時間の紐づけ)
仕様
- 言語はPython 2.7を使用する
- ハードウェアはRaspberryPiを使用する
処 理 |
---|
正確な時を刻むこと(NTP利用) |
FeliCaのIDmを読み取ること |
確認と管理用に本人と上司にメールを送信すること |
クラウド上のデータベースアクセスが可能であること |
マルチスレッド(FeliCa読み取り、メール送信、LCD表示)環境で動作すること |
セキュリティ(データの揮発性)が担保されること |
使用基板の外観
コンテキストダイヤグラム
-
USBインターフェースに接続されたFeliCaカードリーダー、GPIO(汎用入出力)に接続されたブザー、i2cインターフェースに接続された8桁2行の小さな液晶ディスプレイ、有線または無線のネットワークで構成されています。
-
タイムカードをFeliCaカードリーダにタッチすると登録されたIDであればブザーを鳴らし、クラウド上のデータベースに時刻と社員コードを送信します。データベースに記録されると再度ブザーを鳴らし、タッチされた時刻を電子メールで本人と上司に送信します。
-
正確な時間を担保するためにNTPを利用しています
デバイスの外観
0.TimeCardSystemのスレッド
メインスレッドは2と3の2本のスレッドを立ち上げタイムカードがタッチされた時にメッセージによってそれらのスレッドを駆動する。
メインスレッドを含む全てのスレッドは常時ブロックされているが、2のLCD表示スレッドは時刻を表示するためにメッセージQUEに書き込みがない場合は200ms毎に例外が発生し時刻の更新(システム秒の歩進)を確認している。
1のタイムカード読取りルーチンはメインスレッドから起動され以下の一連の動作を行います。
- カードから読み取ったIDm(製造者ID)から社員リストを検索し社員コードを2のLCD表示スレッドにメッセージを送信する。
- 同時に社員コード以外に社員名、メールアドレス、上司のメールアドレスを取り出し、読取時刻と共に3のMail送信スレッドに
メッセージを送信する。 - 社員コードと読取時刻をクラウド上のデータベースに送信する。
1.FeliCaカードを読取る
- 起動時にクラウド上のデータベースから社員リストを読み取る
- ローカルデータベース(sqlite3)に社員リストを登録する
- カードがタッチされるとIDmを読取りconnectedが起動する
- 表示とメールのメッセージを送信し、クラウド上のデータベースに書き込みを行う
2.LCDに時刻を表示する
通常は上段に年月日、下段に時分秒を表示している(1秒毎に表示を更新する)。
表示メッセージを受信した時に社員コードを1秒間表示する。
- システム時刻はNTPで補正され正しい時刻を刻みます。
- 200ms毎に例外が発生し秒が更新しているかを確認します。秒が歩進したときに表示を更新します。
- また、一時間毎にデータベース接続確認を行います。(コネクションが切断されることを防止するため)
イベント | 動作 |
---|---|
メッセージ受信 | 社員コードを1秒間表示 |
タイムアウト(200ms) | 時刻を更新 |
タイムアウト(1hour) | データベース確認 |
3.Mailを送信する
メッセージを受信しメールを送信する
送信するメールのヘッダと本文
標題に社員名、本文に時刻を記述している
MIME-Version: 1.0
Content-Type: text/plain; charset="iso-2022-jp"
Content-Transfer-Encoding: 7bit
Subject: 小林万壽夫
From: mex.timecard.system@gmail.com
To: kobayashi@xxxxxxx.co.jp
Cc: boss@xxxxxxx.co.jp
Date: Tue, 02 May 2017 08:50:27 -0000
2017-05-02 17:50:26
4.インポートライブラリ
import Queue
import threading
import time
import datetime
import smbus
import nfc
import binascii
import RPi.GPIO as GPIO
import sqlite3
import sys
import MySQLdb
import smtplib
from email.MIMEText import MIMEText
from email.Header import Header
from email.Utils import formatdate
import logging
import logging.config
from logging import FileHandler, Formatter
import socket
5.スレッドの使い方
# LCD表示用のスレッドを作成
queue = Queue.Queue()
t = ThreadLcd(queue)
t.setDaemon(True)
t.start()
# 電子メール送信用のスレッドを作成
queue1 = Queue.Queue()
m = ThreadMail(queue1)
m.setDaemon(True)
m.start()
5.1 LCD表示
class ThreadLcd(threading.Thread):
"""Threaded Lcd Grab"""
def __init__(self, queue):
threading.Thread.__init__(self,name='Lcd')
self.queue = queue
now = datetime.datetime.now()
self.sec = now.second
self.hour = now.hour
lcd.setaddress(0, 0) # 上段(現在の年月日を表示)
lcd.puts(now.strftime("%y-%m-%d"))
lcd.setaddress(1, 0) # 下段(現在の時分秒を表示)
lcd.puts(now.strftime("%H:%M:%S"))
def run(self):
while True:
try:
#grabs host from queue
host = self.queue.get(True,0.2) # 0.2秒間メッセージを受信できなかった場合、割り込みが発生する
lcd.clear()
lcd.setaddress(0, 0) # 16文字のメッセージを8文字に分解し上下段に表示する
lcd.puts(host[:8])
lcd.setaddress(1, 0)
lcd.puts(host[8:])
time.sleep(1)
#signals to queue job is done
self.queue.task_done()
except:
now = datetime.datetime.now()
if self.sec != now.second: # 秒更新
self.sec = now.second
lcd.setaddress(0, 0) # 上段
lcd.puts(now.strftime("%y-%m-%d"))
lcd.setaddress(1, 0) # 下段
lcd.puts(now.strftime("%H:%M:%S"))
if self.hour != now.hour: # 時更新
self.hour = now.hour
ping_connection() # SQL keep alive
def ping_connection():
# MySQL WDT
cur1 = db.cursor()
try:
cur1.execute("SELECT 1")
except Exception as ex:
logger.error(ex)
cur1.close()
5.2 電子メールを送信
class ThreadMail(threading.Thread):
"""Threaded Mail Grab"""
def __init__(self, queue):
threading.Thread.__init__(self,name='Mail')
self.queue = queue
def run(self):
while True:
try:
#grabs host from queue
r = self.queue.get(True)
from_addr = 'mex.timecard.system@gmail.com'
to_addr = r[2]
cc_addr = r[3]
bcc_addr = 'mex.timecard.system@gmail.com'
msg = create_message2(from_addr, to_addr, cc_addr, r[1], r[5], 'ISO-2022-JP')
send_via_gmail(from_addr, to_addr, cc_addr, bcc_addr, msg)
#signals to queue job is done
self.queue.task_done()
except Exception as ex:
logger.error(ex)
def create_message2(from_addr, to_addr, cc_addr, subject, body, encoding):
# 'text/plain; charset="encoding"'というMIME文書を作ります
msg = MIMEText(body, 'plain', encoding)
msg['Subject'] = Header(subject, encoding)
msg['From'] = from_addr
msg['To'] = to_addr
msg['CC'] = cc_addr
msg['Date'] = formatdate()
return msg
def send_via_gmail(from_addr, to_addr, cc_addr, bcc_addr, msg):
s = smtplib.SMTP('smtp.gmail.com',587)
s.ehlo()
s.starttls()
s.ehlo()
s.login('mex.timecard.system', 'xxxxxxxxxx')
s.sendmail(from_addr, [to_addr,cc_addr,bcc_addr], msg.as_string())
s.quit()
6. データベース
6.1 オンメモリーで動作する軽量データベース sqlite3にスキーマ(構造)を設定する
try:
lcd.setaddress(0, 0)
lcd.puts('Starting')
lcd.setaddress(1, 0)
lcd.puts('Sqlite3 ')
con = sqlite3.connect(':memory:', isolation_level=None)
with open('names.sql', 'rt') as f:
schema = f.read()
con.executescript(schema)
#print schema
time.sleep(1)
except:
show_reboot()
quit()
スキーマの内容[names.sql]
PRAGMA encoding="UTF-8";
DROP TABLE IF EXISTS `names`;
CREATE TABLE `names` (
`社員コード` integer,
`社員名` text,
`社員メールアドレス` text,
`上司メールアドレス` text,
`社員カードID` text PRIMARY KEY
);
6.2 Google Cloud Platformで動作する CloudSQL (MySQL)
try:
lcd.setaddress(0, 0)
lcd.puts('Starting')
lcd.setaddress(1, 0)
lcd.puts('MySQLdb ')
db = MySQLdb.connect(host="xxx.xxx.xxx.xxx", # your host, usually localhost
user="timecard", # your username
passwd="xxxxxx", # your password
db="timecard",
charset="utf8") # name of the data base
cur = db.cursor()
time.sleep(1)
except:
show_reboot()
quit()
データベース(timecard)のテーブル定義ファイル(names, timestamp)
CREATE DATABASE IF NOT EXISTS `timecard` /*!40100 DEFAULT CHARACTER SET utf8 */;
USE `timecard`;
-- MySQL dump 10.13 Distrib 5.6.17, for Win32 (x86)
--
-- Host: Server-5 Database: timecard
-- ------------------------------------------------------
-- Server version 5.6.20
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
/*!40101 SET NAMES utf8 */;
/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */;
/*!40103 SET TIME_ZONE='+00:00' */;
/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
--
-- Table structure for table `names`
--
DROP TABLE IF EXISTS `names`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `names` (
`社員コード` int(6) unsigned zerofill NOT NULL COMMENT '社員は000xxx、パートさんは900xxx',
`社員名` varchar(45) NOT NULL,
`社員メールアドレス` varchar(45) DEFAULT NULL,
`上司メールアドレス` varchar(45) DEFAULT NULL,
`社員カードID` varchar(16) NOT NULL,
PRIMARY KEY (`社員コード`),
UNIQUE KEY `社員コード_UNIQUE` (`社員コード`),
UNIQUE KEY `社員名_UNIQUE` (`社員名`),
UNIQUE KEY `IDm_UNIQUE` (`社員カードID`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Dumping data for table `names`
--
LOCK TABLES `names` WRITE;
/*!40000 ALTER TABLE `names` DISABLE KEYS */;
INSERT INTO `names` VALUES (000149,'小林万寿夫','kobayashi@xxxxxxx.co.jp','boss@xxxxxxxx.co.jp','012e34e73001xxxx');
INSERT INTO `names` VALUES (999999,'予備1(伊丹)','','','0115e500f30axxxx');
/*!40000 ALTER TABLE `names` ENABLE KEYS */;
UNLOCK TABLES;
/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;
/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
-- Dump completed on 2014-09-20 17:33:41
CREATE DATABASE IF NOT EXISTS `timecard` /*!40100 DEFAULT CHARACTER SET utf8 */;
USE `timecard`;
-- MySQL dump 10.13 Distrib 5.6.17, for Win32 (x86)
--
-- Host: Server-5 Database: timecard
-- ------------------------------------------------------
-- Server version 5.6.20
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
/*!40101 SET NAMES utf8 */;
/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */;
/*!40103 SET TIME_ZONE='+00:00' */;
/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
--
-- Table structure for table `timestamp`
--
DROP TABLE IF EXISTS `timestamp`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `timestamp` (
`idtimestamp` int(11) NOT NULL AUTO_INCREMENT,
`社員コード` int(6) unsigned zerofill NOT NULL,
`出退勤時刻` varchar(45) NOT NULL,
PRIMARY KEY (`idtimestamp`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='出退勤の時刻を記録';
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Dumping data for table `timestamp`
--
LOCK TABLES `timestamp` WRITE;
/*!40000 ALTER TABLE `timestamp` DISABLE KEYS */;
/*!40000 ALTER TABLE `timestamp` ENABLE KEYS */;
UNLOCK TABLES;
/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;
/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
-- Dump completed on 2014-09-20 17:33:41
6.3 起動時にCloud SQLからsqlite3にデータをコピーする
# 0 :社員コード
# 1 :社員名
# 2 :社員メールアドレス
# 3 :上司メールアドレス
# 4 :社員カードID
try:
cur.execute("SELECT * FROM names")
except Exception as ex:
logger.error(ex)
for row in cur.fetchall() :
sql = u"insert into `names` values(" + str(row[0]) + ",'" + row[1] + "','" + row[2] + "','" + row[3] + "','" + row[4] + "')"
try:
con.execute(sql)
except sqlite3.Error as e:
logger.error(e)
7. FeliCaカードリーダ
# タッチ時のハンドラを設定して待機する
clf = nfc.ContactlessFrontend('usb')
while True:
try:
clf.connect(rdwr={'on-connect': connected,'targets': [nfc.clf.TTF(br=212,idm=None,pmm=None,sys=None)]})
except:
logger.info('FeliCa connect')
def connected(tag):
# タグのIDを確認する
if isinstance(tag, nfc.tag.tt3.Type3Tag):
try:
# 登録されたIDmであることを確認
sql = u"SELECT * FROM names WHERE `社員カードID`='" + binascii.hexlify(tag.idm) + "'"
c = con.execute(sql)
r = c.fetchone()
if r: # 登録済
GPIO.output(IO_NO, True) # ブザーを鳴動(ピィ)
time.sleep(0.1)
GPIO.output(IO_NO, False)
r = r + (datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"),)
sql = u"insert into `timestamp` (`社員コード`,`出退勤時刻`) values(" + str(r[0]) + ",'" + r[5] + "')"
queue.put(str(r[0]).zfill(6)) # LCD表示スレッドに社員コードを送信
cur.execute(sql) # クラウド上のデータベースに書き込む
db.commit() # クラウド上のデータベースをコミットする
GPIO.output(IO_NO, True) # ブザーを鳴動(ピィ)
time.sleep(0.1)
GPIO.output(IO_NO, False)
queue1.put(r) # メール送信スレッドにデータを送信する
else: # 未登録
logger.info('Touch: %s' % binascii.hexlify(tag.idm)) # 未登録カードをロギング
for i in range(3): # ブザーを3回鳴動(ピィ・ピィ・ピィ)
GPIO.output(IO_NO, True)
time.sleep(0.1)
GPIO.output(IO_NO, False)
time.sleep(0.1)
except Exception as e:
logger.error(e)
else: # Type3タグではない場合
logger.info('Touch: Not a Type3Tag')
GPIO.output(IO_NO, True) # ブザーを1秒間鳴動(ピィー)
time.sleep(1)
GPIO.output(IO_NO, False)
while tag.is_present: # カードが離れるのを待つ
time.sleep(0.5)
8. その他(起動時の引数、初期化、IPアドレスの表示)
if __name__ == "__main__":
argvs = sys.argv # 起動時の引数設定(ここでは事業所)
argc = len(argvs)
if (argc < 2):
print 'Usage: # python %s [IJ AJ OJ TE]' % argvs[0]
quit()
if (argc == 2 and (argvs[1] == 'IJ' or argvs[1] == 'AJ' or argvs[1] == 'OJ' or argvs[1] == 'TE')):
pass
else:
print 'Usage: # python %s [IJ AJ OJ TE]' % argvs[0]
quit()
logging.config.fileConfig('config.ini') # ロギングの初期化
logger = logging.getLogger(__name__)
logger.info('System Start (%s)' % argvs[1])
start = time.time() # 起動時刻の取得
GPIO.setmode(GPIO.BCM) # 汎用入出力(GPIO)の初期化
GPIO.setup(IO_NO, GPIO.OUT)
lcd = i2clcd() # LCDの初期化
lcd.clear() # LCDのクリア
lcd.setaddress(0, 0) # インターネットへの接続とローカルIPアドレスの表示
lcd.puts("WAITING")
lcd.setaddress(1, 0)
lcd.puts(". ")
i = 0
while True:
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
try:
s.connect(("google.com",80))
except:
pass
localIP = (s.getsockname()[0])
s.close()
if localIP != "0.0.0.0":
break
time.sleep(1)
i = i + 1
if i == 8:
i = 0
lcd.setaddress(1, 0)
lcd.puts(". ")
else:
lcd.setaddress(1, i)
lcd.puts(".")
lcd.clear() # LCDのクリア
lcd.setaddress(0, 0) # ローカルIPアドレスの表示(例:192.168.0.130)
lcd.puts(localIP[:8])
lcd.setaddress(1, 0)
lcd.puts(localIP[8:])
time.sleep(5)