2
10

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

IoT Device : RaspberryPiを使ったFeliCa利用のタイムカードシステム

Last updated at Posted at 2018-08-06

Internet of Things:Device側のPythonを記述する

行動をタイムスタンプするシステム(行動と時間の紐づけ)

仕様

  • 言語はPython 2.7を使用する
  • ハードウェアはRaspberryPiを使用する
処 理
正確な時を刻むこと(NTP利用)
FeliCaのIDmを読み取ること
確認と管理用に本人と上司にメールを送信すること
クラウド上のデータベースアクセスが可能であること
マルチスレッド(FeliCa読み取り、メール送信、LCD表示)環境で動作すること
セキュリティ(データの揮発性)が担保されること

使用基板の外観

timecard5.JPG

コンテキストダイヤグラム

  • USBインターフェースに接続されたFeliCaカードリーダー、GPIO(汎用入出力)に接続されたブザー、i2cインターフェースに接続された8桁2行の小さな液晶ディスプレイ、有線または無線のネットワークで構成されています。

  • タイムカードをFeliCaカードリーダにタッチすると登録されたIDであればブザーを鳴らし、クラウド上のデータベースに時刻と社員コードを送信します。データベースに記録されると再度ブザーを鳴らし、タッチされた時刻を電子メールで本人と上司に送信します。

  • 正確な時間を担保するためにNTPを利用しています

timecard0.png

デバイスの外観

timecard6.JPG


0.TimeCardSystemのスレッド

メインスレッドは2と3の2本のスレッドを立ち上げタイムカードがタッチされた時にメッセージによってそれらのスレッドを駆動する。
メインスレッドを含む全てのスレッドは常時ブロックされているが、2のLCD表示スレッドは時刻を表示するためにメッセージQUEに書き込みがない場合は200ms毎に例外が発生し時刻の更新(システム秒の歩進)を確認している。

1のタイムカード読取りルーチンはメインスレッドから起動され以下の一連の動作を行います。

  • カードから読み取ったIDm(製造者ID)から社員リストを検索し社員コードを2のLCD表示スレッドにメッセージを送信する。
  • 同時に社員コード以外に社員名、メールアドレス、上司のメールアドレスを取り出し、読取時刻と共に3のMail送信スレッドに
    メッセージを送信する。
  • 社員コードと読取時刻をクラウド上のデータベースに送信する。

timecard1.png

1.FeliCaカードを読取る

  • 起動時にクラウド上のデータベースから社員リストを読み取る
  • ローカルデータベース(sqlite3)に社員リストを登録する
  • カードがタッチされるとIDmを読取りconnectedが起動する
  • 表示とメールのメッセージを送信し、クラウド上のデータベースに書き込みを行う

timecard4.png

2.LCDに時刻を表示する

通常は上段に年月日、下段に時分秒を表示している(1秒毎に表示を更新する)。
表示メッセージを受信した時に社員コードを1秒間表示する。

  • システム時刻はNTPで補正され正しい時刻を刻みます。
  • 200ms毎に例外が発生し秒が更新しているかを確認します。秒が歩進したときに表示を更新します。
  • また、一時間毎にデータベース接続確認を行います。(コネクションが切断されることを防止するため)
イベント 動作
メッセージ受信 社員コードを1秒間表示
タイムアウト(200ms) 時刻を更新
タイムアウト(1hour) データベース確認

timecard2.png

3.Mailを送信する

メッセージを受信しメールを送信する

timecard3.png

送信するメールのヘッダと本文

標題に社員名、本文に時刻を記述している

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)
2
10
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
10

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?