ykts.net

NFC attendance system for a small office

AI-Drafted, Human-Verified
NFC Attendance Architecture

はじめに

あるマンション管理事業者の勤怠管理を、NFCカードとRaspberry Piで自動化しました。この記事では、セットアップ手順ではなく「なぜこの構成にしたのか」「どんな問題にぶつかったのか」という部分を中心に書いていきます。

技術的な仕様やインストール手順については GitHub リポジトリの README を参照してください。

背景 ─ 手書きの勤怠管理と給与計算

導入先は、代表者が80歳を超えている小規模なマンション管理事業です。従業員の勤怠は自己申告制で、給与計算も毎回手書きで行われていました。管理がかなりざっくりしていて、実態としては誰が何時間働いたのかを正確に把握できていない状態でした。

確定申告のタイミングで給与の会計まわりを見直す機会があり、そこで私のほうから「勤怠と給与計算を仕組みで回せるようにしましょう」と提案しました。

提案の軸は、毎月の勤務時間と時給を突き合わせて従業員のパフォーマンスを動的に数字で確認できるようにすることです。手書きのノートではなく、スプレッドシート上で常に最新の状態が見える仕組みにすることで、経営判断の材料にもなります。

ハードウェア選定 ─ なぜ Raspberry Pi 2 なのか

普段から Pi を使った多ノード運用をしていて、Pi 2 から Pi 5 までのスペック感は大体把握していました。今回のシステムがやることは「NFCリーダーでカードのUIDを読み取って、GASのエンドポイントにHTTPSで投げる」だけです。Pythonで少し制御を書けば十分で、Pi 2 の性能でもまったく問題ありません。

事務所自体が大きくないので、デスクの横にちょっと置けるサイズというのも Pi が適している理由でした。大げさな機材を持ち込む必要がなく、電源を入れたらあとは放置で動き続けます。

NFCカードの選択

カードは無地の1枚100円程度のNFCカードを選びました。財布に入れて持ち歩けるカード形式なので、従業員にとっては社員証のような感覚で日常的に携帯してもらえます。特別な操作を覚える必要もなく、出勤時にリーダーにかざして、帰るときにもう一度かざすだけです。

社員を追加する際も .env ファイルに名前と時給を書き足すだけで完了します。管理側の手間もほぼゼロに近い設計にしています。

「タッチのみ」というUXの割り切り

ITに詳しくない方が使う前提だったので、UIは徹底的に削ぎ落としました。画面上のボタンを押す操作もなければ、ログイン画面もありません。物理カードをリーダーにタッチする。それだけです。

この割り切りは正解だったと感じています。現地で使い方をレクチャーしたときも10分ほどで済みました。「入るときにかざす、帰るときにかざす」。これ以上シンプルにはできないと思います。

キオスク端末 ─ 古いノートPCの再利用

事務所には Celeron 1005M / 4GB メモリの古いノートPCがありました。Windows のサポートは切れていたので、Linux Mint を入れてキオスク端末として再利用しています。

このキオスク端末には Discord をブラウザでフルスクリーン表示しています。専用アプリのインストールも不要で、Webhook 経由で打刻情報がリアルタイムに流れてきます。NFCをかざすと隣のキオスク画面に名前と入室時刻が表示され、退室時にはトータルの勤務時間が表示されるという仕組みです。

Windows だとノートPCの蓋を閉じるとスリープに入ってしまい、再度パスワードを求められるのでキオスク用途には向いていません。Linux Mint であれば蓋を閉じてもセッションが維持されるように制御できるので、常時表示のダッシュボードとして安定して運用できています。

エッジからクラウドまでのデータフロー

システム全体のデータの流れは以下のようになっています。

NFC Attendance Architecture
  1. エッジ(Pi 2): Sony RC-S300 を PCSC 経由で制御し、NFCカードの UID を読み取る
  2. ロジック(Python): 打刻の状態管理、異常検知、労働時間の丸め処理を行う
  3. バックエンド(GAS): HTTPS API を介して Google スプレッドシートへデータを同期する
  4. 通知・表示(Discord Webhook): キオスク端末上にリアルタイムで打刻結果を表示する

Pi 上の Python サービスは systemd でデーモン化してあるので、電源を入れれば自動的に起動します。人が介在する余地を極力なくすという設計方針です。

泥臭いエッジケースとの戦い

仕様としてはシンプルですが、現実の勤怠管理では色々なイレギュラーが発生します。ここが一番苦労した部分でもあり、一番面白かった部分でもあります。

5分以内の連続打刻(デバウンス)

カードをかざしたあとにリーダーの前に手を置いたままにしたり、ちょっと不安になってもう一度かざしたりするケースは十分に起こり得ます。5分以内の連続打刻は無視するデバウンス処理を入れることで、意図しない二重打刻を防いでいます。

実装としてはシンプルで、前回の打刻時刻との差分を見ているだけです。

_DEBOUNCE = timedelta(minutes=5)

def apply_rules(st, ts, uid, emp):
    if uid in st.uid_last_seen:
        if ts - st.uid_last_seen[uid] < _DEBOUNCE:
            return []  # 5分以内 → 無視
    st.uid_last_seen[uid] = ts

15時間経過後の自動タイムアウト

退勤の打刻を忘れて帰ってしまった場合、入室状態がいつまでも続いてしまいます。15時間を超えた時点で自動的にタイムアウトとしてエラーを記録し、状態をリセットするようにしました。この閾値は、現実的にあり得る最長勤務時間を想定して決めています。

日またぎの処理

日付が変わった後に前日の入室状態が残っていた場合も異常として検知します。バックグラウンドで定期的に状態をスイープするスレッドが走っていて、日付のロールオーバーやタイムアウトを自動的にフラグします。

これらのエラーイベントは Discord にも通知されるので、管理者がスプレッドシート上で該当日を確認して必要に応じて補正できるようになっています。

ネットワーク障害へのリトライ

GAS への API 通信や Discord Webhook への送信は、ネットワーク障害で失敗する可能性があります。すべての HTTP 通信にリトライロジックを組み込んでいて、一時的な接続エラーであれば自動的に再送します。小さな事務所のネットワーク環境は必ずしも安定していないので、この処理がないと日常的にデータ欠損が起きてしまいます。

def _post_discord_retry(webhook_url, text):
    for i in range(3):
        try:
            _http_post_json(webhook_url, {"content": text})
            return
        except Exception as e:
            last_err = e
            if i < 2:
                time.sleep(1.0)
    raise last_err

最大3回、1秒間隔でリトライするだけの素朴な実装ですが、一時的なタイムアウトや接続断であればこれで十分拾えます。

GAS とスプレッドシートの構成

バックエンドは Google Apps Script(GAS)で、データはすべて Google スプレッドシートに格納されます。スプレッドシートには3種類のタブがあります。

「概要」タブでは全社員の給与を一覧で確認できます。前月と当月のタブには社員ごとの日別勤務記録が自動整列で並んでいて、エラーがあった日は個別に日付を確認して対処できるようになっています。

社員を追加した場合もスプレッドシート側の編集は不要です。エッジ側の .env に追加するだけで、GAS 側が自動的に新しい社員のデータを整列して表示します。

技術スタックの振り返り

良かった点

ハードウェア制御に PCSC を使い、サービス管理に systemd を使った構成はシンプルで安定しています。GAS をバックエンドに選んだことで、サーバーの運用コストがゼロになりました。Google スプレッドシートが UI を兼ねているので、管理者にとっても馴染みのあるインターフェースでデータを確認できます。

Discord Webhook を通知と表示の両方に使ったのも良い判断でした。専用のフロントエンドを開発する必要がなく、ブラウザで Discord を開くだけでリアルタイムのダッシュボードになります。

課題

GAS には実行時間の制限やリクエスト数の上限があるので、大規模な運用には向いていません。今回は小規模な事務所だったので問題になりませんでしたが、社員数が増えた場合には別のバックエンドへの移行を検討する必要があります。

また、スプレッドシートの構造がそのまま「データベーススキーマ」になっている点は、柔軟性と引き換えにデータの整合性を完全に担保しづらいという側面もあります。

DX 化のきっかけとして

導入先はかなりアナログな職場でしたが、代表者さま自身は DX 化に関心を持っていました。個人事業として長年やってこられたものの、高齢ということもあり属人的な業務や体制からの脱却を考えていて、法人形態への移行を進めているところです。

今回の勤怠システムは、その第一歩としての位置づけでもあります。小さな仕組みですが「数字で見える」ということが経営にもたらす変化は大きいと感じています。引き続きサポートしていく予定です。

おわりに

Raspberry Pi 2、100円の NFC カード、GAS、Discord Webhook。どれも特別な技術ではありませんが、組み合わせ方と「現場に合わせた割り切り」次第で実用的なシステムになります。

セットアップの詳細や設定ファイルの書き方は README にまとめてあるので、興味のある方はそちらもご覧ください。