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.
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.
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.
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"
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.
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();
}
}
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:
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);
}
});
}
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.