Diesmal habe ich versucht Hobby und Beruf zu kombinieren. Herausgekommen ist etwas, was ich „Ambient SEO Hardware“ nennen möchte.
Was ist es, was macht es?
Bunte Blubber-Wassersäule, die sich die aktuellen Rankings zu einer Webseite von mir aus der Manhattan Tool API besorgt. Das Farbspiel wenn es nicht blubbert zeigt den Datenbereich an und die Farbe beim Blubbern zeigt die Anzahl an hinzugewonnenen oder verlorenen Keywords in dem jeweiligen Bereich an.
Die Säule ist per heimischem WLAN mit dem Internet verbunden und versorgt sich beim Einschalten mit neuen Daten. In dem folgenden Codebeispiel ist es etwas abgewandelt, dort holen wir die Daten in einem Interval, damit neue Daten ohne Neustart des Geräts verfügbar sind. Auch nutzen wir im Codebeispiel nicht das WLAN Modul, sondern gehen von einem Arduino Ethernet oder Arduino Uno mit Ethernet-Shield aus.
Zutaten
- 50€ China-Ware Wasssersäule Amazon-Link
- Arduino Uno
- Roving WiFly RN-XV (bei EXP-Tech)
- Manhattan Tool API
Nice to know
Der Arduino Uno hat sehr wenig Speicher. Weil dadurch u.A. https-Verbindungen nicht möglich ist und das Handling komplexerer APIs nicht ganz trivial ist, empfehle ich für solche Projekte kleine Proxy-Scripte. Die legt ihr irgendwo im Netz, erreichbar per http ab und stellt nur genau die Daten bereit, die ihr wirklich braucht. Ich meinem Fall brauchte ich die Top 10, Top 30 und Top 100 Rankingentwicklung, mehr nicht. Drei Zahlen.
Den elektronischen Part kann man mit fast jedem Microcontroller/Kleinstcomputer Ökosystem umsetzen. Wer also auf eigene Proxies verzichten möchte, sollte etwas Richtung Arduino Zero/Yún oder Raspberry/Beaglebone/alike mit deutlich mehr Power ausprobieren.
Es gibt für den Arduino selbst durchaus Libaries, die z.B. in der Lage sind zumindest etwas verschachteltes JSON parsen zu können. Der Nachteil, sie brauchen ggf. wertvollen Speicher, mit dem Arduinos nicht gerade gesegnet sind. Aber das macht auch ein wenig den Reiz aus – mit dem was da ist etwas umgesetzt zu bekommen.
Ein weiterer Vorteil der Proxy-Lösung: Gerade wenn der Arduino irgendwo fest verbaut ist, nervt es ihn ausbauen zu müssen (oder nen Anschluss einzuplanen/einzubauen) um den Code an z.B. ein Update der Anbieter-API anzupassen. Das kann man als Einsteiger lieber PHP überlassen, damit man sich beim Arduino auf die Steuerung konzentrieren kann.
Mini Proxy Script
Was macht es: Ein einfaches Script, dass per cUrl eine API als GET aufruft, dabei SSL nicht verifiziert. Dann das ankommende JSON parsed und drei Werte aus der Datei als einfachen String weiterreicht. Das kann man besser lösen, aber so sollte das eigentlich out of the Box bei jedem funktionieren. Die Werte zu der von euch genutzten API müsst ihr natürlich auch noch anpassen (bei mir z.B. das $api_reply->top10).
<?php // GET ORIGINAL API REPLY $ch = curl_init("###YOUR_API_URL###"); curl_setopt($ch, CURLOPT_USERAGENT, "Mein Agent"); curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0); curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0); $api_reply = json_decode(curl_exec($ch)); // EXTRACT REQUIRED VALUES $reply = “{“ . $api_reply->top10 . “,“ . $api_reply->top30 . “,“ . $api_reply->top100 . “}“; // SEND ADJUSTED REPLY header("HTTP/1.1 200 OK"); echo $reply; ?>
Proxy Antwort
Die Antwort des Proxies sieht dann wie nachfolgend aus (Beispiel). Wir werden später mit dem Arduino Ethernet einen GET-Request gegen dieses Script durchführen. Nehmen wir für unser Beispiel an, es liegt hier: http://example.com/proxy-script.php. Ein paar HTTP Header und ein wenig Text.
HTTP/1.1 200 OK Date: Sun, 18 Jan 2015 00:49:48 GMT Server: Apache X-Powered-By: PHP/5.5.15 Content-Length: 56 Connection: close Content-Type: text/html {25,54,-21}
Netzwerkverbindung herstellen
Mit dem Arduino müssen wir jetzt eine Verbindung zum Netzwerk und dem Internet herstellen. In diesem Fall geben wir dem Arduino eine konkrete Netzwerk-Konfiguration mit. Verzichtet man auf DHCP werden diese Teile der Ethernet-Library nicht mit auf dem Arduino gespeichert wodurch mehr Platz im Speicher verbleibt. Mehr Infos zum Initialisieren der Netzwerkverbindung (Ethernet.begin()) in Englisch auf der Arduino Seite.
#include <SPI.h> #include <Ethernet.h> byte mac[] = { 0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0xED}; IPAddress ip(192,168,2,199); IPAddress myDns(192,168,2,1); void setup() { Serial.begin(9600); // give the ethernet module time to boot up: delay(1000); Ethernet.begin(mac, ip, myDns); Serial.println(Ethernet.localIP()); }
Als nächstes brauchen wir eine Funktion, die wir dann z.B. alle 60 Sekunden aufrufen, um die Daten zu aktualisieren.
void httpRequest() { if (client.connect(server, 80)) { client.println("GET /proxy-script.php HTTP/1.1"); client.print("Host: "); client.println(host); client.println("User-Agent: arduino-ethernet"); client.println("Connection: close"); client.println(); } else { // if you couldn't make a connection: Serial.println("connection failed"); client.stop(); } // note the time that the connection was tried: lastConnectionTime = millis(); }
Wir verwenden Client.Connect() zum Aufbau der Verbindung zum Proxy (Client.connect()) und senden dann mit Client.Print() und Client.PrintLn() Daten an den Server
Mit dem if(client.connect(…)) prüfen wir, ob wir mit dem genannten Server verbinden können (auf dem unser Proxy-Script liegt). Ist dies erfolgreich, senden wir dem Proxy-Script unsere Anfrage. Dazu senden wir dem Server die Zeichenfolge: GET /proxy-script.php HTTP/1.1 sowie weitere gewünschte Werte (z.B. User-Agent oder wie im Beispiel zur Verbindung). Danach senden wir noch eine leere Zeile um dem Webserver zu signalisieren, dass unsere Anfrage beendet ist. Wir speichern noch einen Timestamp der Anfrage, um den Interval zu realisieren. Die Ethernet-Verbindung verhält sich vom Handling wie man es von der normalen seriellen Schnittstelle gewohnt ist.
Die Proxy Antwort annehmen
Die Ethernet Schnittstelle stellt uns nun Zeichen für Zeichen die Antwort des Proxy-Scripts zur Verfügung. Da unsere drei Werte in geschweiften Klammern stehen, können wir die Sache sehr einfach lösen. Wir gehen einfach durch alle Zeichen der Antwort der Reihe nach durch. Ist es ein „{„, also der Anfang unserer Datensequenz, stellen wir unseren Aufnahme-Modus an. Ist das Zeichen ein „}“ hören wir mit der Aufnahme auf. Dazwischen befinden sich unsere 3 Zahlen als Komma-separierter als String in einer Zeile. Also 25,54,-21.
Der Code ist etwas vereinfacht, also Copy/Paste funktioniert nicht. Der Gesamtcode folgt am Ende, hier möchte ich nur das Vorgehen erläutern. Wir haben ein Character-Array mit dem Namen message und einen boolschen Speicherplatz für unseren Aufnahme-Modus recording.
char *message; bool recording = FALSE; if (client.available()) { char ch = client.read(); if (ch == '{') { // identified string start recording = TRUE; } else if (ch == '}') { // identified string end recording = FALSE; } else if (recording == TRUE) { message += ch; } }
Damit haben wir die drei Zahlen aus dem Netz schonmal in den Arduino geholt. Sie liegen nun als eine Zeichenfolge in der globalen Variable mit dem Namen message.
Am besten bündeln wir alle Abläufe zum Handling der Connection in einer Funktion. Diese können wir dann in jedem Durchlauf des Main-Loops aufrufen. So bekommen wir alles mit und haben es etwas aufgeräumter. Die Gesamte Funktion zum Handling des Updates:
void handle_refresh() { // if there's incoming data from the net connection. // read the incomming data character per character // use starting curly braces to indication interesting // json part. // If found, start recording until end indicator is found. // store message in type String if (client.available()) { char ch = client.read(); if (ch == '{') { // identified string start recording = true; message += ch; } else if (ch == '}') { // identified string end recording = false; recordingDone = true; // we don't need more, flush the client client.flush(); message += ch; } else if (recording == true) { message += ch; } } // if there's no net connection, but there was one last time // through the loop, then stop the client: if (!client.connected() && lastConnected) { Serial.println(); Serial.println("disconnecting."); client.stop(); } // if you're not connected, and ten seconds have passed since // your last connection, then connect again and send data: if(!client.connected() && (millis() - lastConnectionTime > interval)) { httpRequest(); } // store the state of the connection for next time through // the loop: lastConnected = client.connected(); }
Wir arbeiten hier mit client.connected() und client.available(). Client.available() liefert die Anzahl noch lesbarer Bytes der Ethernet-Verbindung zurück und eignet sich somit zum Prüfen ob noch Inhalt kommt. Client.connected liefert Infos zur Verbindung. Hinweis: client.connected gilt auch bei abgebauter Verbindung als wahr wenn noch ungelesene Bytes vorliegen.
Integer Werte aus dem String herauslösen
Um später mit den Werten aus dem String message z.B. die Wassersäule an und aus zu schalten, ist es praktisch, wenn wir mit Zahlen rechnen können. Wenn wir z.B. nur eine Zahl hätten und aus der Antwort „123“ herausgelöst hätten, könnten wir diesen Wert nicht in einem zahlenbasierten Vergleich nutzen, da es sich um unterschiedliche Datentypen handelt. if( message > 100 ) ginge also nicht. Da Message ein String ist und kein Type-Casting wie bei z.B. PHP verfügbar ist.
In unserem Beispiel gehen wir davon aus, drei Zahlen vorzufinden. Das int rankStats[3]; definiert ein globales Array mit drei Integerwerten. Hier speichern wir unsere gefundenen Werte.
Danach definieren wir eine Funktion, der wir unsere Nachricht übergeben können und dieses Array mit Integer-Werten befüllt. Bzw. wir übergeben Ihr die Nachricht nicht, diese liegt ja als globale Variable vor. Die Funktion wertet die aktuell bekannte globale message aus und befüllt aus dieser das globale Ergebnis-Array rankStats[].
int rankStats[3]; void decode_message() { int idx = message.indexOf(","); int beginIdx = 0; int arrayPoint = 0; String arg; char buffer[10]; while (idx != -1) { arg = message.substring(beginIdx,idx); arg.toCharArray(buffer,10); rankStats[arrayPoint] = atoi(buffer); beginIdx = idx + 1; arrayPoint++; idx = message.indexOf(",", beginIdx); } }
Es wird geprüft, an welchen Stellen das Komma sitzt, der Text zwischen den Kommas herausgelöst und in Zahlen umgewandelt und in das Array gespeichert. An dieser Stelle hab‘ ich es etwas umständlich gelöst. Das geht sicherlich noch ohne den Umweg der Umwandlung von message (als String) in ein Buchstaben-Array (buffer). Ich müsste bereits beim Einlesen der Werte ein Array benutzen. Als C-Neuling hat es ein wenig gedauert bis ich das unterschiedliche Handling der verschiedenen Datentypen verstanden hab. Als Umsetzungsimpuls sollte das aber reichen.
Der Main-Loop
Mit diesen Bausteinen können wir uns nun einen übersichtlichen Main-Loop aufbauen.
void loop() { handle_refresh(); // if there is an ended recording and message is > empty // Parse the Data from the message // Reset recording status and message // Do the required action if (recordingDone == true && message != "") { Serial.println(message); decode_message(); recordingDone = false; message= ""; action(); } }
Der Arduino schaut nun mit handle_refresh() kontinuierlich ob es Zeit ist die Anfrage zu wiederholen. Wenn die Antwort vorliegt (recordingDone == true) wird sie in ihre Werte zerlegt (decode_message()). Danach werden message und recordingDone zurückgesetzt. action() verwertet dann die im Ergebnis-Array gespeicherten Werte.
Die action() Funktion
Wir haben jetzt drei saubere Integer Werte in einem Array stehen und können damit nach Belieben schalten. In meinem Beispiel der Wassersäule nutze ich vier digitale Ausgänge. Über drei Transistoren schalte ich ich jeweils eine Gruppe von Leuchtdioden (je 4x Rot/Grün/Blau) und über einen vierten Transitor ein Relais, mit dem ich die Stromzufuhr zur Blubberbläschen-Pumpe steuere. Als Beispiel hier: Ist die erste Zahl (z.B. die Entwicklung Anzahl Keywords mit Top 10 Rankings) größer Null, die grünen LED an, wenn kleiner die Roten und wenn unverändert die Blauen. Danach die Pumpe anschalten und 5 Sekunden warten. Bzw. hier nehme ich noch den absoluten Wert (falls die Zahl Negativ ist) und multipliziere ihn mit 100. Pro 10 neue Keywords in den Top 10 dann eine Sekunde länger blubbern und leuchten. Danach alle Ausgänge wieder ausschalten. Ob, wie, usw etc pp liegt dann an euch. Was soll wie angezeigt werden? Your turn.
void action() { if (rankStats[0] > 0) { digitalWrite(green,HIGH); } else if (rankStats[0] < 0) { digitalWrite(red,HIGH); } else{ digitalWrite(blue,HIGH); } digitalWrite(pump,HIGH); delay(5000 + abs(rankStats[0])*10); digitalWrite(green,LOW); digitalWrite(red,LOW); digitalWrite(blue,LOW); digitalWrite(pump,LOW); }
Die Elektronik und Bastelbilder
Hier ein Überblick über das von mir verwendete Shield. Was an Arduino tierisch nervt ist der verringerte Abstand auf der Reihe der digitalen I/O. Deswegen ist das mit Standard-Lochrasterplatinen immer ein Krampf. Wenn’s ein halbwegs ernstes Projekt ist, nehm ich dann gerne die Proto-Shields. Ich habe den Aufbau bei mir mit W-LAN, deswegen sieht es bei mir etwas anders als, als das Shield für einen normalen Arduino Ethernet aussehen würde, aber es soll euch ein grobes Gefühl geben.
- Relais. An die zwei hochragenden Pins wird die Pumpe angeschlossen. Das Relais trennt den Stromkreis und kann im Grunde alle Zimmerpumpen steuern. An die Freilaufdiode denken (sieht man hier nicht, auf der Unterseite). Das Relais wird über den Transistor (4) gesteuert.
- Pin-Leiste zu den LED. Vom Arduino wird Vin direkt an den Pluspol für die LEDs gelegt (nicht die 5V vom Arduino Spannnungswandler, sondern die Spannung die ihr anlegt). Danach folgend die Massen der Kanäle Rot/Grün/Blau, die mit jeweils einem der Transitoren unter (3) geschaltet wird
- 3 Transistoren die vom Arduino direkt geschaltet werden (Pins 6, 7 und 8). Sie schalten die Masse der LEDs einer gemeinsamen Farbe.
- Dieser Transistor schaltet das Relais.
- Hier wird das WLAN Modul angeschlossen
- Software Serial Debugging Schnittstelle
- Das Neztteil der Pumpe liefert 12V Wechselstrom, wollte mich daran klemmen. Habe den Plan aber wieder verworfen, weil die Spannung beim Anlaufen der Pumpe doch schwankte und ich nicht wusste, wie der Arduino darauf reagieren würde. Das sind die Reste des Gleichrichters.
Als Halterung für die Beleuchtung habe ich die Vorhandene Platine weiterverwendet (da muss man dann keine Neue basteln). Ich habe die vorhandene Elektronik entfernt, nutze aber noch die Leiterbahnen zu den LEDs. Da ich die Masse der jeweiligen Farbe schalte, musste ich die Leuchtdioden alle einmal herauslöten, um 180° drehen und wieder einlöten. Zwei klare rote habe ich gegen dunklere LED ersetz, da das Rot sehr grell wahr. Kabel dran, Tropfen heisskleber als Zugentlastung, dazu noch ’nen Stecker und fertig.
Anmerkungen zur Elektronik
Eine Besonderheit im Arduino Kosmos ist ein wenig nervig. Das lässt sich nicht vermeiden aber es schadet nicht es auf dem Schirm zu haben. Pin-Belegung ist so ein Beispiel. Beim Arduino Ethernet oder Ethernet Shield sind die Digitalen Ports 10-13 vom Ethernet Shield belegt und auch Pin 4 ist belegt. I.d.R. durch den SD Card Slot auf den Ethernet-Komponenten. Er fällt also als Ein/Ausgabe Port im Grunde flach. Die Elektronik ist nicht sonderlich kompliziert.
Der gesamte Sketch
/* SEO Water-Pillar by Nils Haack Using Arduino Ethernet or Arduino Uno with Ethernet Shield (based on Wiznet) Based on work by Tom Igoe (created 19 Apr 2012) http://arduino.cc/en/Tutorial/WebClientRepeating This code is in the public domain. */ #include <SPI.h> #include <Ethernet.h> #include <ArduinoJson.h> ////////////////////////////// // HTTP Configuration ////////////////////////////// char server[] = "example.com"; char host[] = "example.com"; char get[] = "/proxy-script.php"; const unsigned long interval = 5000; // delay between updates, in milliseconds (e.g. 5000 = 5 seconds) ////////////////////////////// // Ethernet Configuration ////////////////////////////// // assign a MAC address for the ethernet controller. // fill in your address here: byte mac[] = { 0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0xED}; // fill in an available IP address on your network here, // for manual configuration: IPAddress ip(192,168,2,199); // fill in your Domain Name Server address here: IPAddress myDns(192,168,2,1); // client library EthernetClient client; // Some variables we'll need later on for the periodic connection handling unsigned long lastConnectionTime = 0; // last time you connected to the server, in milliseconds boolean lastConnected = false; // state of the connection last time through the main loop ////////////////////////////// // String Handling Variables ////////////////////////////// boolean recording = false; boolean recordingDone = false; String message; int rankStats[3]; ////////////////////////////// // PIN Configuration ////////////////////////////// // 0 is free, occupied as Tx when using hardware serial für debugging // 1 is free, occupied as Rx when using hardware serial für debugging // 2 is free // 3 is free // 4 is usually occupied by SD Card int red = 5; // Pin to drive transistor switching mass of red led channel int green = 6; // Pin to drive transistor switching mass of green led channel int blue = 7; // Pin to drive transistor switching mass of blue led channel int pump = 8; // Pin to drive transistor switching mass of relais switching pump // 9 is free // 10 occupied by Ethernet // 11 occupied by Ethernet // 12 occupied by Ethernet // 13 occupied by Ethernet ////////////////////////////// // SETUP, run once per boot ////////////////////////////// void setup() { // Initial hardware serial connection for debugging Serial.begin(9600); // give the ethernet module time to boot up: delay(1000); // Start Ethernet with fixed Networkconfiguration // use Ethernet.begin(mac); to enable DHCP; will increase sketch size Ethernet.begin(mac, ip, myDns); // print IP of device to serial connection Serial.println(Ethernet.localIP()); } ////////////////////////////// // Main Loop, runs continously ////////////////////////////// void loop() { handle_refresh(); // if there is an ended recording and message is > empty // Parse the Data from the message if (recordingDone == true && message != "") { Serial.println(message); decode_message(); // Reset recording status and message recordingDone = false; message= ""; // Switch stuff with the values extracted action(); } } ////////////////////////////// // handling connection and interval ////////////////////////////// void handle_refresh() { // if there's incoming data from the net connection: // read the incomming data character per character // use starting curly braces to indicate data. // If found, start recording until end indicator is found. // store message if (client.available()) { char ch = client.read(); if (ch == '{') { // identified string start recording = true; } else if (ch == '}') { // identified string end recording = false; recordingDone = true; client.flush(); } else if (recording == true) { message += ch; } } // if there's no net connection, but there was one last time // through the loop, then stop the client: if (!client.connected() && lastConnected) { Serial.println(); Serial.println("disconnecting."); client.stop(); } // if you're not connected, and ten seconds have passed since // your last connection, then connect again and send data: if(!client.connected() && (millis() - lastConnectionTime > interval)) { httpRequest(); } // store the state of the connection for next time through // the loop: lastConnected = client.connected(); } ////////////////////////////// // Making the request to Proxy ////////////////////////////// void httpRequest() { // Connect to Server (IP or Name) if (client.connect(server, 80)) { // call proxy-script client.println("GET /mht_proxy.php HTTP/1.1"); // for compatibility we also name the host (usually the same as server) client.print("Host: "); client.println(host); // be polite and send your user agent client.println("User-Agent: arduino-ethernet"); client.println("Connection: close"); client.println(); } else { // if you couldn't make a connection: Serial.println("connection failed"); client.stop(); } // note the time that the connection was tried: lastConnectionTime = millis(); } ////////////////////////////// // Extract Integers from Message ////////////////////////////// void decode_message() { // find first occurance of "," in String int idx = message.indexOf(","); // default values for local variables and declaration int beginIdx = 0; int arrayPoint = 0; String arg; char buffer[10]; // repeat as long "," is found in string while (idx != -1) { // Characters between "," or at beginning/end arg = message.substring(beginIdx,idx); // Convert String to Character-Array arg.toCharArray(buffer,10); // Convert Character-Array to Integer and store in result array rankStats[arrayPoint] = atoi(buffer); // Set new start position beginIdx = idx + 1; arrayPoint++; // find next "," in string idx = message.indexOf(",", beginIdx); } } ////////////////////////////// // Switch Stuff with the values ////////////////////////////// void action() { if (rankStats[0] > 0) { digitalWrite(green,HIGH); digitalWrite(pump,HIGH); } else if (rankStats[0] < 0) { digitalWrite(red,HIGH); digitalWrite(pump,HIGH); } else{ digitalWrite(blue,HIGH); digitalWrite(pump,HIGH); } delay(5000 + abs(rankStats[0])*10); digitalWrite(green,LOW); digitalWrite(red,LOW); digitalWrite(blue,LOW); digitalWrite(pump,LOW); }
Nils,
Found your „Nerf Stampede Arduino“ post on YouTube. I’m in the market for a „Nerf Arduino Turret“ along those very lines.
Two blaster rig 125PSI Turret w/ full Arduino control unit. Kind of know what I’m looking for. Let me know if interested in exploring the project. I’ll send you some pics.
Best
LJ
Hi LJ,
I haven’t been tinkering around with Nerf for a while now. But if I can be of any help, sure.
Nils