Kontakt

DIY Raspberry Pi Stechuhr mit NFC - Teil 1

by Manuel on 15.11.2017

Unser gestriges Neos/Flow Meetup war besonders techi: im Dresdner Büro können wir uns bald mit einem RFID-Schlüsselanhänger einchecken und der persönliche Arbeitszeit-Timer wird automatisch gestartet.

Bisherige Erfassung der Arbeitszeit

Unsere Arbeitszeit erfassen wir mithilfe einer selbstentwickelten Webanwendung auf Basis des Flow Frameworks. Zum einen als Selbstkontrolle und zum anderen für das gute Gewissen zum Feierabend. Überstunden können so fair abgebummelt oder für ein Nickerchen auf der Couch im Büro genutzt werden.

Die bisherige Art und Weise der Erfassung erfordert jedoch, dass man seinen Rechner startet (dank SSD ist dies in Sekunden erledigt), einloggt, authentifiziert und den Timer startet. Eine Lösung, die grundsätzlich funktioniert, uns als eingeschworenen Anwendungsentwicklern aber noch zu aufwändig ist. Wird man bspw. direkt beim Betreten des Büros in ein Gespräch verwickelt, so wird der Timer erst später gestartet und die Zeit muss nachgetragen werden.

Arbeitszeiterfassung Sandstorm

Übersicht der aktuellen Überstunden und heutiger Arbeitszeit

Als technische Grundlage verwendeten wir den beliebten Einplatinencomputer Raspberry Pi, das NFC-Modul RFID RC522, einen 7" Touchscreen sowie einige beschreibbare NFC-Schlüsselanhänger.

Sobald man das Büro betritt, hält man einfach seinen NFC–Schlüsselanhänger an das Raspberry Pi Lesegerät und startet bzw. stoppt seinen Timer.

Starting from Scratch

Eine fertige Lösung ist natürlich einfacher und komfortabler aber in gewisser Weise auch langweilig. Da wir Spaß am Entdecken und Lernen haben, starteten wir also am Anfang: die Pins des NFC-Moduls RFID RC522 wurden an den Raspberry Pi angeschlossen. Wie sich später herausstellen sollte, war dies die einfachste Übung des gesamten Projekts.

Korrekte Verbindung zwischen Raspberry Pi und NFC-Modul

Anschließend setzen wir den Raspberry Pi auf und nannten ihn Frank-Walter Steinmeier, in Anlehnung an den gestrigen Politikerbesuch im BioInnovationszentrum, den wir mit einem Auge vom Fenster aus verfolgten. Mithilfe der Programmiersprache Python steuerten wir das NFC-Modul an und waren nach kurzer Einarbeitungsphase in der Lage die Chips auszulesen.

Installation des Raspberry Pi

Zum auslesen der NFC Tags nutzten wir zunächst eine kleine open-source Bibliothek als Interface zum Reader Modul. Mithilfe des enthaltenen Beispiel Codes wurden die Tags erkannt und die UID ausgelesen. Letztere kann beispielhaft zur Authentifikation verwendet werden.

#!/usr/bin/env python
# -*- coding: utf8 -*-
 
import RPi.GPIO as GPIO
import MFRC522
import signal
 
continue_reading = True
 
# Capture SIGINT for cleanup when the script is aborted
def end_read(signal,frame):
    global continue_reading
    print "Ctrl+C captured, ending read."
    continue_reading = False
    GPIO.cleanup()
 
# Hook the SIGINT
signal.signal(signal.SIGINT, end_read)
 
# Create an object of the class MFRC522
MIFAREReader = MFRC522.MFRC522()
 
# This loop keeps checking for chips. If one is near it will get the UID and authenticate
while continue_reading:
 
    # Scan for cards    
    (status,TagType) = MIFAREReader.MFRC522_Request(MIFAREReader.PICC_REQIDL)
 
    # If a card is found
    if status == MIFAREReader.MI_OK:
        print "Card detected"
 
    # Get the UID of the card
    (status,uid) = MIFAREReader.MFRC522_Anticoll()
 
    # If we have the UID, continue
    if status == MIFAREReader.MI_OK:
 
        # Print UID
        print "Card read UID: "+str(uid[0])+","+str(uid[1])+","+str(uid[2])+","+str(uid[3])
 
        # This is the default key for authentication
        key = [0xFF,0xFF,0xFF,0xFF,0xFF,0xFF]
 
        # Select the scanned tag
        MIFAREReader.MFRC522_SelectTag(uid)
 
        # Authenticate
        status = MIFAREReader.MFRC522_Auth(MIFAREReader.PICC_AUTHENT1A, 8, key, uid)
 
        # Check if authenticated
        if status == MIFAREReader.MI_OK:
            MIFAREReader.MFRC522_Read(8)
            MIFAREReader.MFRC522_StopCrypto1()
        else:
            print "Authentication error"
NFC-Moduls RFID RC522 am Raspberry Pi

Hindernisse voraus

Nachdem wir das technische Fundament vollendet hatten, tauchten die ersten Hindernisse auf. Um die RFID-Chips eineindeutig und sicher dem jeweiligen Sandstormer zuzuordnen, wollten wir diese mit einem zufällig generiertem Token bespielen und diesen dann auslesen. Dafür nutzen wir die Android App NFC Tools. Diese liest und beschreibt entsprechende Chips im NDEF Format. Wir haben für den Payload den MimeType "application/sandstorm-token" benutzt. Der Inhalt besteht zunächst aus einer einfachen ID, z.B. 123456.

Nachdem wir einen Beispiel-Token auf den Chip schrieben, wurde er nicht mehr korrekt vom Python-Skript ausgelesen: "Authentication Error".

Die Tags werden zwar noch erkannt, offenbar versteht das Interface aber keine NDEF Tags und kann die UID nicht mehr auslesen. Die API ist leider nicht dokumentiert und ein Blick in den Source Code lässt vermuten, dass man das Format auch nicht konfigurieren kann. Die mangelnde Maintenance des Repositories der Library weisen auch darauf hin, dass wir uns nach einer anderen Bibliothek umschauen sollten.

Überprüfung der Verbindung zwischen NFC-Moduls RFID RC522 und Raspberry Pi

Flow Controller

Das Gegenstück zur Hardware Seite des Projektes bildet die Schnittstelle zur eigentlichen Arbeitszeit Tracking App, welche eine Flow Anwendung ist. Benötigt wird also ein Flow Controller, der Requests vom Terminal entgegennimmt und anhand des jeweiligen Tokens den Timer des zugehörigen Sandstormers startet und stoppt.

Dazu gehört auch eine UI, die wir auf dem Touchscreen anzeigen, um den Anwender einerseits seine Aktion zu bestätigen und andererseits die aktuelle Zeit auszugeben.

<?php
 
namespace Sandstorm\WorkingTime\Controller;
 
use Neos\Flow\Annotations as Flow;
 
use Neos\Flow\Mvc\Controller\ActionController;
 
use Sandstorm\WorkingTime\Domain\Model\Date as Date;
use Sandstorm\WorkingTime\Domain\Model\Week as Week;
use Sandstorm\WorkingTime\Domain\Service\RfidTokenService;
 
 
/**
 * @Flow\Scope("singleton")
 */
class DongleController extends ActionController
{
 
	/**
	 * @Flow\Inject
	 * @var \Sandstorm\WorkingTime\Domain\Repository\TimeRepository
	 */
	protected $timeRepository;
 
	/**
	 * @Flow\Inject
	 * @var \Sandstorm\WorkingTime\Domain\Service\WorkingTimeCalculationService
	 */
	protected $workingTimeCalculationService;
 
	/**
	 * @Flow\Inject
	 * @var RfidTokenService
	 */
	protected $rfidTokenService;
 
	protected $settings;
 
	/**
	 * @param array $settings
	 */
	public function injectSettings(array $settings)
	{
		$this->settings = $settings;
	}
 
	/**
	 * starts or stops the timer and shows the current overview
	 *
	 * @param string $rfidToken (is a FORM parameter)
	 * @return void
	 */
	public function toggleAction($rfidToken)
	{
		$accountIdentifier = $this->rfidTokenService->accountIdentifierByToken($rfidToken);
		$this->view->assign('rfidToken', $rfidToken);
		if ($accountIdentifier !== null) {
			$todaysTimeSlot = $this->timeRepository->getTimeSlotForTodayByAccountIdentifier($accountIdentifier);
			$todaysTimeSlot->toggle();
			$this->timeRepository->update($todaysTimeSlot);
 
			$workingTime = $this->workingTimeCalculationService->getByAccountIdentifier(new Week(), (new Date())->getWeekday(), $accountIdentifier);
			$this->view->assign('name', $accountIdentifier);
			$this->view->assign('portrait', "dongle/$accountIdentifier.jpg");
			$this->view->assign('overminutes', floor($workingTime->getOvertime()->getMinutes()));
			$this->view->assign('todaysTimeSlot', $todaysTimeSlot);
		} else {
			$this->view->assign('overminutes', 0.0);
		}
 
		$this->persistenceManager->persistAll();
	}
 
 
}
Chris hat seinen Timer per NFC Chip gestartet.

Personalisierter Terminal Screen für Chris

Android App als eigenes Terminal

Parallel dazu arbeiteten wir an einem zweiten Terminal, bestehend aus einem älteren (NFC-fähigen) Android Tablet. Die Idee war eine App auf Basis von Kotlin zu entwickeln, die ebenfalls durch Anhalten des persönlichen Tags den jeweiligen Timer startet und stoppt.

Die NFC Technologie ist seit Android 2.3 fester Bestandteil des Betriebssystems. Es ist daher relativ einfach und ohne zusätzliche Bibliotheken möglich, eigene NFC Anwendungen zu entwickeln. Dafür stellt Android verschiedene Intents zur Verfügung, auf welche unsere App reagiert. In unserem Fall sieht der entsprechende Filter für NDEF formatierte Tags mit dem MimeType "application/sandstorm-token" so aus:

<intent-filter>
  <action android:name="android.nfc.action.NDEF_DISCOVERED" />
  <category android:name="android.intent.category.DEFAULT" />
  <data android:mimeType="application/sandstorm-token" />
</intent-filter>
 

In unserer MainActivity behandeln wir den Intent wie folgt. Wir überschreiben die onNewIntent Methode der Activity und können dann die Intent Action validieren. Wenn uns der Intent interessiert, es also eine NDEF_DISCOVERED action ist, dann lesen wir den Inhalt als NdefMessage[] aus. Anschließend parsen wir den Payload des ersten Records als String und erhalten unseren Token.

override fun onNewIntent(intent: Intent?) {
    super.onNewIntent(intent)
    if (intent != null && NfcAdapter.ACTION_NDEF_DISCOVERED == intent.action) {
        val rawMessages = intent.getParcelableArrayExtra(NfcAdapter.EXTRA_NDEF_MESSAGES)
        if (rawMessages != null) {
            val messages = arrayOfNulls<NdefMessage>(rawMessages.size)
            for (i in rawMessages.indices) {
                messages[i] = rawMessages[i] as NdefMessage
            }
 
            // we only have 1 NdefMessage which has only 1 Record
            val record = messages[0]!!.getRecords()[0]
 
            val token = String(record.payload)
 
            //TODO: POST request with token to Timer API
        }
    }
}
 
function * snackbarLifeCycle(state, notification) {
    switch (state) {
        case Snackbar_LifeCycleStates.Opening:
            yield call(state_opening, notification);
            break;
        case Snackbar_LifeCycleStates.Visible:
            yield call(state_visible);
            break;
        case Snackbar_LifeCycleStates.Closing:
            yield call(state_closing);
            break;
        default: throw new Error('unknown life cycle state:', state);
    }
}
 
export default function * SnackbarSaga() {
    yield takeLatest(actionTypes.UI.Snackbar.SHOW, function * (action) {
        const notification = $get('payload.notification', action);
        if (notification) {
            yield call(snackbarLifeCycle, Snackbar_LifeCycleStates.Opening, notification);
        }
    });
}
Testing der Android App auf einem Tablet
Android App liest die RFID-Chips korrekt

Fortsetzung folgt

Die Bastelzeit verging so schnell, dass wir leicht erschrocken feststellten, bereits 4h an diesem vermeintlich kleinen Projekt gesessen zu haben. Leider konnten wir die Leseprobleme der beschriebenen Chips bis zuletzt nicht lösen und müssen die Projektvollendung sowie Präsentation verschieben.

Das Ergebnis und die Lösung des Lese-Rätsels erfahrt ihr in Teil 2 der Raspberry Pi Stechuhr.

 

P.S. Unser Büro in Darmstadt kommt auch bald in den Genuss dieser komfortablen Zeiterfassung ;-)