- Inverse Geocaching Schatztruhe
- Prall gefüllt mit vielen Gold-Dublonen…
- … aber nur für den, der den geheimen Ort findet!
Dieses Jahr hat mein Freund geheiratet, und ich wollte dem Brautpaar ein ganz besonderes Geschenk machen. In der amerikanischen DIY und How-To Zeitschrift Makezine Volume 37 bin ich auf eine Box gestoßen, die sich mit Hilfe eines verbauten GPS-Sensors, Mikrokontroller und Servo nur an einem ganz bestimmten, im Programm definierten Ort, öffnen lässt. Diese Idee fand ich so gut, dass ich mich selbst daran gemacht habe, eine solche zu bauen. Dafür bedarf es folgender Bauteile:
– Schatztruhe
– Arduino Nano V3 (ich habe einen Klon verwendet)
– LCD 16×2
– GPS-Modul
– Miniatur-Servo
– Lithium-Ionen Ladeschaltung mit 5V Boost Converter
– Lithium-Ionen Akku z.B. aus altem Notebook
– Diverse weitere elektronische Bauteile, Drähte und Platine
In die Decke des Deckels der Schatztruhe wird eine Aussparung für das LC-Display, den Schalter, sowie die GPS-Antenne geschnitten. Letztere kann auch unter einer dünnen Schicht, nicht ferritischen oder abschirmenden Materials platziert sein, und trotzdem Verbindung zu den Satteliten herstellen. Des Weiteren sollte in die Seitenwand des Deckels ein Loch gebohrt werden, durch das der Akku mittels eines Standard Micro-USB-Kabels aufgeladen werden kann. Die Anordnung des LC-Displays, des Schalters, der Lithium-Ionen-Ladeschaltung, des 5V-Boos-Converters, des Arduino Nano Clones, der GPS-Antenne des dazugehörigen Moduls lässt sich auf folgendem Bild grob erkennen. Deine Schatztruhe wird vermutlich etwas anders aussehen.
- Anordnung der angeschlossenen Bauteile
- Aussparung zum Aufladen mittels Micro-USB
Wie die genannten Bauteile an den Arduino Nano angeschlossen werden, kannst du in diversen Foren und weitere Webseiten herausfinden. Dies ist nicht so schwierig und braucht nur etwas Durchhaltevermögen.
Der Miniatur-Servomotor ist, wie im obigen Bild zu sehen, mit zwei ausgedruckten Halterungen an die Innenwand des Deckels geschraubt worden. Das Servohorn ist etwas gekürzt und passt in einen Schlitz in die Innenwand des unteren Teils der Schatztruhe. Wird dem Servo eine Position von 90° vorgegeben, so ist die Box von innen verschlossen. Bei 0° wiederum kann man sie öffnen.
- Locked from the inside als Hinweis gegen gewaltsames Öffnen
- Der passende Schlitz für das Servohorn
- Äußerer und innerer Verschlussmechanismus
Da ich weiß, dass mein Freund programmiertechnisch nicht so viel Ahnung hat und um mir Änderungen am Programmcode zu ermöglichen, habe ich den Mini-USB-Anschluss des Arduino Nano Clones zugänglich gelassen, ihn allerdings verschlossen. Baut man solch eine Schatztruhe für einen Maker mit Programmierkenntnissen und lässt den Zugang offen, so kann die Schatztruhe rein programmatisch gehackt und geöffnet werden. Daher ist also Vorsicht geboten. Jedoch kommt man wohl gewaltsam immer an den Inhalt der Box – je nachdem, wie stabil sie ausgelegt ist.
- Programmier-Anschluss Mini-USB-Buchse
Nun kommen wir zur eigentlichen Programmierung: Dazu habe ich mich der Library TinyGPS von Mikal Hart bedient: http://arduiniana.org/libraries/tinygpsplus/. Mit ihr können die von den Satteliten empfangenen Zeichenketten überprüft und die benötigten Parameter wie Längen- und Breitengrade, Geschwindigkeit, Höhe uvm. ausgegeben werden. Ebenfalls enthalten ist eine Funktion, welche die Distanz zwischen zwei Positionen (jeweils zwei Längen- und Breitengraden) angibt. Somit muss nur noch ein Programm geschrieben werden, welches die verschiedenen Bauteile einbindet und den Servo das Kommando gibt, die Box zu öffnen, sobald die Distanz zwischen Soll- und Istposition auf z.B. 20m gesunken ist. Damit die Box auch nach dem Erreichen der Sollposition nicht als Staubfänger in der Ecke liegt, habe ich zusätzliche Funktionen wie Uhrzeit, Angabe der Längen- und Breitengrade, KMH-Anzeige und einen Höhenmesser verwendet, die erst dann freigeschaltet und auf dem LC-Display ausgegeben werden.
Der folgende Code hilft dir hoffentlich weiter (Meine Referenzen finden sich in der Beschreibung):
/* * Inverse Geocache Box * This code is mostly written by Moritz Emanuel Böker. * However without the TinyGPS-library by Mikal Hart and its example code "simple_test" this would not be possible for me. * Furthermore, credits go to Adafruit Industries and their project "Engagement Box by Kenton Harris 11/12/2012", * which can be found at: * https://learn.adafruit.com/reverse-geocache-engagement-box/software * * Adafruit Industries credits: * Tutorials for these products found on learn.adafruit.com helped with much of this. * Copyright (c) 2012, Adafruit Industries * All rights reserved. * Thanks to bnordlund9 for much of the code. This is simplified verison of his Geobox found here: * http://www.youtube.com/watch?v=g0060tcuofg * Credit to Mikal Hart of http://arduiniana.org/ for the original idea of Reverse Geocache * * The device uses the following products from Adafruit Industries: * Arduino Nano Clone: https://www.amazon.de/gp/product/B01EH4JEYW/ref=oh_aui_detailpage_o00_s00?ie=UTF8&psc=1 * NEO-6M GPS Module: https://www.amazon.de/gp/product/B01N38EMBF/ref=oh_aui_detailpage_o00_s01?ie=UTF8&psc=1 * 16x2 Character LCD: https://www.amazon.de/dp/B01N0FK6I6/ref=sr_1_2?s=computers&ie=UTF8&qid=1503436272&sr=1-2&keywords=16x2+lcd * TPro Micro Servo SG90: https://www.amazon.de/gp/product/B01EH4JEYW/ref=oh_aui_detailpage_o00_s00?ie=UTF8&psc=1 * * This code requires the use of SoftwareSerial for diagnostic purposes, and assumes that you have a * 9600-baud serial GPS device hooked up on pins 3(rx) and 2(tx). */ // Included libraries #include <EEPROM.h> #include <math.h> #include <LiquidCrystal.h> #include <PWMServo.h> // newer <Servo.h> leads to problems due interfering with <SoftwareSerial.h> #include <SoftwareSerial.h> #include <TinyGPS.h> // Variable declarations int eeprom_address = 0; // EEPROM adress for permanentely storing locking status of box const int d4 = 4, d5 = 5, d6 = 6, d7 = 7, en = 11, rs = 12; // Pins lcd int servoPin = 9; // pin for servo must be a PWM pin 9 or 10 due to old servo library <PWMServo.h> int servoPosLock = 90; // angle (deg) of "locked" servo int servoPosUnlock = 0; // angle (deg) of "unlocked" servo const int rx = 3, tx = 2; // Pins gps module const int led_pin = 13; // Pin onboard led float flat_dest = 51.421369, flon_dest = 7.338544; // Destination latitudinal and longitudinal values // Object declarations LiquidCrystal myLcd(rs, en, d4, d5, d6, d7); PWMServo myServo; TinyGPS myGps; SoftwareSerial mySerial(rx, tx); void setup() { /* The following line is to reset the internal status of the box, telling, whether it is locked (== 0) * or unlocked (== 1). Before handing the box over to the receiver, this EEPROM address needs to have * the value for locked status (==0). Once the correct location is found, it will automatically be * changed to 1, in order to keep the box open when it is out of range. */ //EEPROM.write(eeprom_address,0); myLcd.begin(16,2); // row length 16 characters, 2 rows mySerial.begin(9600); // Baudrate for "blox Neo-6M-0-001" pinMode(led_pin, OUTPUT); //onboard led to indicate a gps fix digitalWrite(led_pin, LOW); // Diagnostic data being sent to Serial Monitor Serial.begin(115200); Serial.print("Simple TinyGPS library v. "); Serial.println(TinyGPS::library_version()); Serial.println("by Mikal Hart"); Serial.println(); if(EEPROM.read(eeprom_address) == 1){ // If value in EEPROM == 1, secret location has already been found myServo.attach(servoPin); myServo.write(servoPosUnlock); autoprint2lcd1602("Der Schatz ist " "nun gehoben! " "Neue Funktionen " "freigeschaltet: " "Datum & Uhrzeit," "Koordinaten- " "angabe, Km/h- " "Anzeige und " "Hoehenmeter! " ,1000); } else{ myServo.attach(servoPin); myServo.write(servoPosLock); autoprint2lcd1602("Lieber **** und " "liebe ******, " "herzlichste " "Glueckwuensche " "zu Eurer Ehe! " "Um Euer Geschenk" "zu erhalten, " "muesst ihr erst " "einmal den " "richtigen Ort " "finden, um die " "Schatztruhe " "oeffnen zu koen-" "nen (am besten 1" "Sa frei nehmen " "bis 4. Nov. oder" "ab 25. Mar.):-)!" "Also los geht's!" ,1000); } } void loop(){ bool newData = false; bool gpsfix = false; char buffer_txt[100]={}; // For printing purposes with sprintf unsigned long chars; unsigned long fix_age; unsigned short sentences, csum_err; float flat_src, flon_src; long double range; digitalWrite(led_pin, LOW); // Resetting onboard led(indicating a gps fix) each loop turn // For one second we parse GPS data and report some key values for (unsigned long start = millis(); millis() - start < 1000;) { while (mySerial.available()) { char c = mySerial.read(); // Serial.write(c); // uncomment this line if you want to see the GPS data flowing if (myGps.encode(c)) // Did a new valid sentence come in? newData = true; } } myGps.f_get_position(&flat_src, &flon_src, &fix_age); myGps.stats(&chars, &sentences, &csum_err); Serial.println(); Serial.print(" CHARS="); Serial.print(chars); Serial.print(" SENTENCES="); Serial.print(sentences); Serial.print(" CSUM ERR="); Serial.print(csum_err); Serial.print(" LAT="); Serial.print(flat_src == TinyGPS::GPS_INVALID_F_ANGLE ? 0.0 : flat_src, 6); Serial.print(" LON="); Serial.print(flon_src == TinyGPS::GPS_INVALID_F_ANGLE ? 0.0 : flon_src, 6); Serial.print(" SAT="); Serial.print(myGps.satellites() == TinyGPS::GPS_INVALID_SATELLITES ? 0 : myGps.satellites()); Serial.print(" PREC="); Serial.print(myGps.hdop() == TinyGPS::GPS_INVALID_HDOP ? 0 : myGps.hdop()); Serial.print(" Fix AGE="); Serial.println(fix_age); if(chars == 0){ Serial.print("** No characters received! "); autoprint2lcd1602("Techn. Fehler! " "Kabel defekt! " ,0); } else if(!newData){ Serial.print("** Invalid sentence(s) received! "); autoprint2lcd1602("Kein GPS-Signal!" "Bring mich raus!" ,0); } /* * The following 'else if' condition is deactivated, because it has interrupted * the gps distance calculation too often, although, the gps location has been quite * decent. else if(csum_err != 0){ Serial.print("** Checksum error! "); autoprint2lcd1602("Fehler GPSdaten!Geduld o. Reset!",0); } */ else if(fix_age == TinyGPS::GPS_INVALID_AGE){ Serial.print("** No gps fix! "); autoprint2lcd1602("Kein GPS-Signal!" "Bring mich raus!" ,0); } else if(fix_age > 1500){ Serial.print("** Warning: stale gps data! "); autoprint2lcd1602("Achtung! " "Alte GPS-Pos.! " ,0); } else{ Serial.print("** Yes, gps fixed! "); digitalWrite(led_pin, HIGH); gpsfix = true; } if(gpsfix == true){ if(EEPROM.read(eeprom_address) == 1){ // Secret location has already been found int year; char N_S, E_W; byte month, day, hour, minute, second, hundredths; float fkmph, falt; myGps.crack_datetime(&year, &month, &day, &hour, &minute, &second, &hundredths, &fix_age); fkmph = myGps.f_speed_kmph(); // Speed in kilometers/hour falt = myGps.f_altitude(); // +/- altitude in meters // Determine between North our South if(flat_src < 0){ N_S = 'S'; flat_src = -flat_src; } else{ N_S = 'N'; } // Determine between East or West if(flon_src < 0){ E_W = 'W'; flon_src = -flon_src; } else{ E_W = 'E'; } autoprint2lcd1602("GPS-Signal OK", 1000); myLcd.clear(); myLcd.setCursor(0,0); myLcd.print(flat_src,6); myLcd.setCursor(15,0); myLcd.print(N_S); myLcd.setCursor(0,1); myLcd.print(flon_src,6); myLcd.setCursor(15,1); myLcd.print(E_W); delay(1000); myLcd.clear(); sprintf(buffer_txt,"Time: %02d:%02d:%02d " "Date: %02d:%02d:%4d" , (hour+2)%24, minute, second, day, month, year); autoprint2lcd1602(buffer_txt,1000); Serial.print(buffer_txt); myLcd.clear(); myLcd.setCursor(0,0); myLcd.print(fkmph,0); myLcd.setCursor(12,0); myLcd.println("km/h"); myLcd.setCursor(0,1); myLcd.print(falt,0); myLcd.setCursor(9,1); myLcd.println("m Hoehe"); delay(1000); myLcd.clear(); } else{ // Secret location has not been found yet unsigned long int irange; range = myGps.distance_between(flat_dest, flon_dest, flat_src, flon_src); irange = ceil(range); // Rounding up the distance [m] Serial.print("** Distance: "); Serial.print(irange); Serial.print("m "); sprintf(buffer_txt,"GPS-Signal OK! " "Distanz: %ldm", irange); autoprint2lcd1602(buffer_txt,0); if(range < 20.0){ autoprint2lcd1602("Yippieh!!! " "Ihr habt es ge- " "schafft! Erin- " "nert Ihr Euch an" "diesen Ort? " "Los, macht mich " "endlich auf! " ,1000); myServo.write(servoPosUnlock); EEPROM.write(eeprom_address,1); // EEPROM.write(adress, 1) to signal that secreat location has been found } } } } void autoprint2lcd1602(const char * txt, unsigned int delayms){ /* * This function automatically splits the text that has been given to it into rows of 16 characters. * Then it displays the rows on a 16x2 lcd with a scrolling down effect. */ //Declarations and initialization of variables unsigned int num_char = strlen(txt); // count # of characters of txt unsigned short int num_row = num_char/16; // determine # of rows to display on lcd if(num_char%16 != 0) // increase # of rows by one if there is a rest of characters num_row++; for(int i=0; i<num_row; i++){ // for every row if(i%2 == 0) myLcd.clear(); for(int k=i*16; k<i*16+16; k++){ // for every character (for 1. row: 0..15, 2. row: 16..31, ...) myLcd.setCursor(k-i*16,i%2); // set the Cursor of lcd: character 0..15, row 0..1 if(k<num_char){ //if still characters of txt available copy this character myLcd.print(txt[k]); } else{ //else copy space myLcd.print(' '); } } if(i%2 == 1 || num_row == 1){ delay(delayms); } } }
Wie funktioniert das Ganze? Der Empfänger der Box schaltet die selbige ein und erfährt neben einer Begrüßungsnachricht nur die Distanz zum geheimen Ort an, an der sich die Box öffnet. Die Richtung und die genaue Position muss er durch Ausprobieren oder Triangulation herausbekommen. Erst wenn er innerhalb eines bestimmten Umkreis ist, fährt der Servo aus seiner Position und der Schatz kann gehoben werden.
Nun musst du dir natürlich noch ein passendes Geschenk überlegen, das in der Box Platz finden soll. Ich habe etwas Geld hineingelegt, mit dem Hinweis auf einen nahegelegenen Kletterpark. Bis jetzt habe ich noch keine Rückmeldung von den beiden erhalten – aber ich bin gespannt!