RFID Süßigkeitenbox + Twitter

Bald ist Weihnachten. Die Vorweihnachtszeit ist eingeläutet. Man wird von oben bis unten mit Süßigkeiten überschüttet. Ob es die klassische Schokolade, die Marzipankartoffeln, Spritzgebäck oder Lebkuchen ist: Alles liegt in greifbarer Nähe und wartet nur darauf gierig verschlungen zu werden.
Jedes Jahr nehme ich mir vor, nicht so viel davon zu essen und jedes Mal verliere ich gegen meinen inneren Schweinehund. Vollgestopft mit Süßkram liege ich jährlich auf der Couch und gelobe, es nächstes Jahr besser zu machen. Dieses Jahr wird das nicht so. Dieses Jahr sorge ich vor:
Ich baue eine Kiste, die sich nur mittels RFID Token öffnen lässt und das auch nur 3x täglich. Haha! Schweinehund ausgetrickst...

Dieses Jahr spielte mir der Zufall etwas in die Hände, da ich Anfang des Monats aus Spaß und Interesse ein paar RFID-Module bestellte und mit denen etwas rumspielte. Mich interessierte, wie sie funktionieren und wie man sie ansteuert, ausliest und beschreibt. Nur fiel mir kein sonderlich guter Anwendungsfall dafür ein. Der Vermieter wird vermutlich nicht sonderlich erfreut sein, wenn jemand die Schließanlage manipuliert und sein Gelöte dazwischen klemmt. Schade. Spießer! Mittlerweile fahre ich 2x die Woche eine etwas längere Strecke zur Arbeit und habe viel Zeit zum Nachdenken. So kam ich während der Fahrt auf die Idee eine Süßigkeitenbox zu bauen, die sich nur via Token öffnen lässt.
Ein paar Skizzen später stand fest, dass die notwendige Technik möglichst nicht sichtbar sein soll und ein doppelter Boden von Nöten ist. Im Zwischenboden soll ein Arduino, als Herz, seinen Platz finden und die Anfrage zum Öffnen des Deckels via WLAN an einen Kontrollserver schicken. Dieser Server muss entscheiden, ob ich meine Süßigkeiten bekomme, oder eben nicht und die entsprechende Antwort zurückschicken. Der Arduino dreht dann einen Servomotor, nach rechts, wodurch der Deckel sich öffnen lässt. Schließt man die Kiste wieder, erkennt dies der Controller mittels Reedkontakt und verriegelt den Deckel.

Folgende Teile werden benötigt:

Wichtig: Das RFID Modul läuft mit 3.3V und ist NICHT 5V kompatibel.

Die Einzelteile werden nun wie folgt zusammengesteckt:

Die LEDs bekommen jeweils einen 220 ohmigen Widerstand verpasst und der Reedkontakt wird mittels PullUp (4,7kOhm) an D2 geklemmt. Die rote LED hängt auf D4, die Grüne auf D3.
Der Servo bekommt seine extra 5V und steckt auf D1.
Das RFID-Modul hängt wie folgt am Board:

Wemos D1RC522
3.3V3.3V
GGND
D0RST
D5SCK
D6MISO
D7MOSI
D8SDA


Nacht etwas Löten sieht das fertige Board dann wie folgt aus:


Um alle Bauteile an Ort und Stelle zu halten, habe ich ein paar Halterungen gedruckt, diese kann man hier oder auf Thingiverse.com runterladen.

Der Servo wird in die passend gedruckte Form hinein geschoben und mittels zwei Schrauben links & rechts verschraubt.
Das RFID Modul wird zwischen die Wand der Kiste und dem "Deckel" geschoben. Verschraubt werden sie von innen nach außen, sodass die Schrauben den Deckel und das Modul gegen die Innenwand drücken.
Der Reedkontakt kann platziert werden wie man auch immer lustig ist. Die Kontakte müssen sich halt lediglich beim Schließen des Deckels möglichst nahe kommen, bestenfalls berühren.
Ich habe den Servo und den Kontakt beide in die gleiche Ecke gebaut, um alles technische in einer "Gegend" zu konzentrieren:

Auf den obigen Bildern ist das verlängerte Servohorn gut zu erkennen. Der Druck wird mittels kurzer Schraube und etwas Kleber auf dem vorhandenem Horn fixiert und dient als Sperre zum Öffnen, wenn der Servo in seine "Ruheposition" gefahren ist. Der Gegenpart, der zum Versperren genutzt wird, besteht aus drei Teilen: Die Bodenplatte wird mit dem Mittelteil mit etwas Aceton verklebt. Die zweite Verbindung mittels einer Schraube, ein paar Unterlegscheiben und einer Mutter verschraubt. So kann man bei Bedarf den Winkel und die Entfernung etwas justieren. Klappt ganz gut: Danke Gehirn! Einfach in den Deckel schrauben. Möglichst so, dass das Horn bereits gut über den Winkel greift.


Alle Leitungen verlaufen dann in den doppelten Boden der Kiste und werden dort an das gelötete Board angeschlossen:

Um die DC Buchse vernünftig am Gehäuse zu befestigen, ist bei den Modellen eine Adapterplatte dabei. Diese wird auf die Buchse gesteckt und mit der Überwurfmutter fixiert. Die Platte wird dann mit zwei kleinen Schrauben an's Gehäuse gespaxt. Die Kabel sollte man vorher am besten schon an die Buchse gelötet haben, sonst wird das etwas eng und unangenehm. Dann noch den MicroUSBStecker anlöten und fertig ist die Sache mit dem Strom:


Hardwaremäßig war's das dann schon. Fehlt noch die richtige Software.

Arduino Code

/* BunteKiste Source.
Some code to handle a crate full of sweets, to open and close it via RFID tokens
by Christian Figge - info ät flazer punked net (flazer.com)
*/

#include <Arduino.h>
#include <SPI.h>
#include <MFRC522.h>
#include <Servo.h>
#include <ESP8266WiFi.h>
#include <ESP8266WiFiMulti.h>
#include <ESP8266HTTPClient.h>

ESP8266WiFiMulti WiFiMulti;

//RFID SPI
#define RST_PIN  D0 
#define SS_PIN   D8

//SERVO
#define SRV_PIN  D1

//REED SWITCH
#define REED_SW  D2


//STATUS LEDS
#define LED_RED  D4
#define LED_GRN  D3

MFRC522 mfrc522(SS_PIN, RST_PIN);

boolean doorState = false;
boolean isOpen = false;
int loopWaitDoorCheck = 0;
int blameCount = 0;

Servo servo;

/**
 * Initialize.
 */
void setup() {
    Serial.begin(57600);
    while (!Serial); 
    SPI.begin();
    mfrc522.PCD_Init();
    
    WiFiMulti.addAP("YOUR_AP_SSID", "YOUR_AP_PASSWORD");

    servo.attach(SRV_PIN);
    pinMode(REED_SW, INPUT);
    pinMode(LED_RED, OUTPUT);
    pinMode(LED_GRN, OUTPUT);
    
    //Just light both LEDs for 2 secs
    displayWelcome();
    displayBlame();
    delay(2000);
    ledsOut();

    handleServo(false);
    doorState = getCurrentDoorState();
}

/**
 * Main loop.
 */
void loop() {
    delay(100);
    if(waitForDoorStatusChange()) {
        delay(2000);
        if(!doorState) {
            if(isOpen) {
                handleServo(false);
                isOpen = false;
                ledsOut();
            }
        }
    }

    if(blameCount > 0) {
        blameCounter();
    }

    // Look for new cards
    if (!mfrc522.PICC_IsNewCardPresent())
        return;

    // Select one of the cards
    if (!mfrc522.PICC_ReadCardSerial())
        return;

    MFRC522::PICC_Type piccType = mfrc522.PICC_GetType(mfrc522.uid.sak);

    // Check for compatibility
    if (piccType != MFRC522::PICC_TYPE_MIFARE_MINI
        &&  piccType != MFRC522::PICC_TYPE_MIFARE_1K
        &&  piccType != MFRC522::PICC_TYPE_MIFARE_4K) {
        Serial.println(F("No MIFARE Classic card."));
        return;
    }

    String uid = getID();
    Serial.print("UID: "); Serial.print(uid);
    Serial.println("");

    if(uid != "") {
        if(check(uid)) {
            Serial.println("Simsalabim");
            handleServo(true);
            isOpen = true;
            displayWelcome();
        }else{
            blameCount = 1;
            displayBlame();
        }
    }

    mfrc522.PICC_HaltA();
    mfrc522.PCD_StopCrypto1();
}

/**
 * handles time for showing blame led
 */
void blameCounter() {
    blameCount++;
    if(blameCount > 10) {
      ledsOut();
      blameCount = 0;
    }
}

/**
 * fires green led
 */
void displayWelcome() {
    digitalWrite(LED_GRN, 1);
}

/**
 * fires red led
 */
void displayBlame() {
    digitalWrite(LED_RED, 1);
}

/**
 * just blackout both leds
 */
void ledsOut() {
    digitalWrite(LED_RED, 0);
    digitalWrite(LED_GRN, 0);
}

/**
 * returns current lid's state
 */
boolean getCurrentDoorState() {
    boolean status = false;
    if(digitalRead(REED_SW) < 1) {
        status = true;
    }
  return status;
}

/**
 * checks if status of lid changes
 **/
boolean waitForDoorStatusChange() {
    if(doorState != getCurrentDoorState()) {
        doorState = getCurrentDoorState();
        if(!doorState) {
            Serial.println("CLOSED!!!!");
        }else{
            Serial.println("OPENED!!!!");
        }
        return true;
    }
    return false;
}

/**
 *  Sends request to server
 */
boolean check(String uid) {
    boolean result = false;
    if(WiFiMulti.run() != WL_CONNECTED) {
        Serial.println("NOT CONNECTED!");
        return false;
    }
    HTTPClient http;
    Serial.println("[HTTP] begin...");
    http.begin("http:///PATH_TO_YOUR_CONTROLLSERVER/api/crate/" + uid); //HTTP
    Serial.println("[HTTP] GET...");
    int httpCode = http.GET();
    if(httpCode > 0) {
        Serial.printf("[HTTP] GET... code: %d\n", httpCode);
        Serial.println("");
        if(httpCode == HTTP_CODE_OK) {
            String payload = http.getString();
            Serial.println(payload);
            if(payload == "granted") {
                result = true;
            }
        }
    }else{
        Serial.printf("[HTTP] GET... failed, error: %s\n", http.errorToString(httpCode).c_str());
    }
    http.end();
    return result;
}

/**
 * Handles Servo to open/close the crate
 */
void handleServo(boolean direction) {
    int pos;
    if(direction) {
        for(pos = 0; pos <= 120; pos += 1) {
            servo.write(pos);
            delay(1); 
        }
    }else if(!direction) {
        for(pos = 180; pos>=0; pos-=1) {                                
            servo.write(pos);
            delay(1); 
        }
    }
}

/**
 * Get Uid and transform to uppercase
 */
String getID(){
    String code ="";
    for (byte i = 0; i < mfrc522.uid.size; i++) {
        code += String(mfrc522.uid.uidByte[i], HEX) + ":";
    }

    code.remove(code.length()-1);
    code.toUpperCase();
    return code;
}

/**
 * Helper routine to dump a byte array as hex values to Serial.
 */
void dump_byte_array(byte *buffer, byte bufferSize) {
    for (byte i = 0; i < bufferSize; i++) {
        Serial.print(buffer[i] < 0x10 ? " 0" : " ");
        Serial.print(buffer[i], HEX);
    }
}

Ein paar Anmerkungen zum Code:
Der Arduino sendet, nachdem er die Karte eingelesen hat, via WLAN eine Anfrage zu einer API (Webseite). Diese nimmt zwei Parameter entgegen: Die Art des Schlosses (crate) und die UID des Tokens bzw. der Karte. Nun prüft der Webserver, ob die Karte bekannt ist und wie oft die Kiste schon geöffnet wurde. Ist das Limit erreicht, oder die Karte unbekannt, dann antwortet die API mit "denied". Wenn Zugang gewährt wird, dann gibt's ein "granted".

Der Code ist nicht der Sauberste, das liegt an folgenden Punkten:

  • Mein absolutes Unvermögen sauberen Code zu schreiben
  • Mein Versagen C zu verstehen
  • Die benötigte Zeit unterschätzt und dann musste ich mich beeilen

Ein paar Dinge müssen im Code zudem noch angepasst werden:

  • YOUR_AP_SSID - Der Namen des WLANs
  • YOUR_AP_PASSWORD - Das Passwort des WLANs
  • PATH_TO_YOUR_CONTROLLSERVER - URL zur API

Der Code wird nicht direkt lauffähig sein. Zunächst muss die RFID-Library installiert werden. Die findet man auf Github: https://github.com/miguelbalboa/rfid. Dort den "Master" runterladen, entpacken und in den "libraries"-Ordner der Arduino IDE schieben. Jetzt noch das "-master" im Ordnernamen entfernen.
Um das Board überhaupt programmieren zu können, muss nun ersteinmal der Treiber installiert werden, denn das WeMos D1 nutzt nicht den Standard-FTDI-Chip sondern einen Chip mit der Bezeichnung "CH340G". Die Treiber habe ich für Windows hier gefunden: http://www.arduined.eu/tag/ch340g/ bzw. für Mac in Björns Blog: https://blog.sengotta.net/arduino-nano-wird-nicht-erkannt-was-tun/.

Leider reicht das immer noch nicht aus: Jetzt muss noch der Boardtyp mit der Arduino IDE bekannt gemacht werden. Dafür fügt man folgende URL in den Einstellung der IDE in das Feld "Zusätzliche Boardverwalter-URLs" hinzu:
http://arduino.esp8266.com/stable/package_esp8266com_index.json

Falls die URL irgendwann, in ferner Zukunft, mal nicht mehr funktionieren sollte, dann wird ein Blick in das dazugehörige Gitrepo sicherlich hilfreich sein: https://github.com/esp8266/Arduino
Nun muss das Paket noch über den Boardverwalter herunter geladen werden. Dies stößt man an, indem man über "Werkzeuge -> Board -> Boardverwalter" navigiert. Hier sucht man nun nach ESP8266 und installiert den Krempel. Das dauert einen Moment, danach sind ein paar mehr Boards auswählbar:

Juhu! Nun kann man den Code endlich kompilieren und auf's Board laden.

Leider darf ich den Code der API nicht veröffentlichen, da ich Großteile des Backends während meiner Arbeitszeit schrieb (Ja! Unter der Woche habe ich einen normalen Beruf). Wir brauchten dort ein schniekes Adminpanel und so konnte ich Freizeit und Arbeit wunderschön vermischen. Der Programmieraufwand hält sich aber in Grenzen, da ich auf etliche OpenSource-Projekte zurückgegriffen habe. Technische Daten und ein paar Screenshots kann ich aber gerne raushauen.

Der Webserver ist ein Nginx mit PHP-extension auf einem Raspberry Pi. Als Datenbank läuft eine MariaDB. Da ich das Rad nicht (mehr) andauernd neu erfinden möchte, dient Laravel 5.3 als Framework.
Das Theme kommt auch nicht von mir, sondern ist ein freies Admin-Theme namens "Matrix". Dieses habe ich mir mittels Bootstrap und etwas Javascript (JQuery) zurecht gebogen. Nach erfolgreicher Arbeit sieht das Ganze dann so aus:

Nicht sonderlich hübsch, aber es tut was es soll.

Und weil man sich nicht immer nur an OpenSource-Projekten bedienen, sondern auch mal etwas zurückgeben sollte, haben wir eine weitere Twitter-API geschrieben. Die so einfach zu benutzen ist, dass das fast ein bisschen weh tut. Die kann man hier runterladen, oder auch weiter entwickeln: https://github.com/FrozenDonkey/twitter-api-php. Der Code wird auch in der Kiste benutzt und twittert zufällige Texte, je nachdem, ob der Zugriff erlaubt oder verboten wird.

Wie bindet man den Code ein?
Ganz einfach:

$twitter = new TwitterAPI(
    "YOUR_OAUTH_ACCESS_TOKEN",
    "YOUR_OAUTH_ACCESS_TOKEN_SECRET",
    "YOUR_CONSUMER_KEY",
    "YOUR_CONSUMER_SECRET"
);

Nun kann ein Tweet mit folgender Zeile gesendet werden:

$res = $twitter->tweet("THIS IS YOUR TWEET");

Möchte man ein Bild mit anhängen, dann geht das so:

$res = $twitter->tweetImage('THIS IS A TWEET WITH AN IMAGE', 'path/to/image.jpg');

Voll einfach, oder?


Die Box sieht, nachdem Bracka noch ein Logo draufgepinselt hat, so aus:


Twittern tut sie unter dem Namen @DieBunteKiste. Kannste ja mal reingucken, wenn du Bock hast.

Hier gibt's auch noch ein Video von der ganzen Bastelei: