I liked the idea of my collegue Stefan Michaelis to give a transposed geo-cache as a gift to a special person. As he detailed the building instructions and shared his sourcecode on github, I decided to give it a try.
The updated code is available here: GCT-code.zip.
Step 0: Piecelist
To get started I ordered the following pieces:
Arduino Uno Rev.3 | Direct link to Amazon |
Navilock GPS EM-406A | Direct link to Amazon |
GPS Connector Cable (6pin) | Direct link to Amazon |
Servo HS-311 | Direct link to Amazon |
SainSmart IIC/I2C/TWI 1602 Serial LCD Modul Display | Direct link to Amazon |
battery pack | Direct link to Amazon |
Button | Direct link to Amazon |
2pin DC Connector | Direct link to Amazon |
Pololu Button | Direct link to Amazon |
Before start, each component is connected to the arduino board individually, and simple tests are run to check its functionality.
Step 1: Connecting Serial Pololu switch and LCD Module
#include <Wire.h> #include <LiquidCrystal_I2C.h> LiquidCrystal_I2C lcd(0x27,2,1,0,4,5,6,7); int i; int PololuPin=12; void setup() { pinMode(PololuPin, OUTPUT); lcd.setBacklightPin(3,POSITIVE); lcd.begin(16,2); lcd.setCursor(0,0); lcd.print("Time in s"); } void loop() { i++; lcd.setCursor(0,1); lcd.print(i); if (i==10) { digitalWrite(PololuPin, HIGH); }; delay(1000); }
Step 2: Connecting Navilock EM406 GPS Module to Arduino board
The GPS is connected as follows:
Next program parses NMEA messages and reports the position at serial connection.
#include <TinyGPS.h>; #include <SoftwareSerial.h>; const byte GPS_IN_PIN = 10; const byte GPS_OUT_PIN = 3; TinyGPS gps; SoftwareSerial gpsSerial(GPS_IN_PIN, GPS_OUT_PIN); void setup() { Serial.begin(9600); gpsSerial.begin(4800); } void loop() { if (gpsSerial.available()>0) { byte c=gpsSerial.read(); if (gps.encode(c)) { unsigned long fix_age; float flat,flon; gps.f_get_position(&flat, &flon, &fix_age); Serial.print ("Latitude : "); Serial.println (flat,10); Serial.print ("Longitude: "); Serial.println (flon,10); } } }
Step 3: Connecting Servo HS-311
#include <Servo.h> Servo myservo; // create servo object to control a servo int pos = 0; // variable to store the servo position void setup() { myservo.attach(8); // attaches the servo on pin 8 to the servo object myservo.write(270); } void loop() { }
Step 4: Putting things together
All connections need to be combined as follows
Next listing combines previous tests and runs the GCT. This is a slightly modified Version from Stefan Michaelis.
/* CC BY-NC-SA 3.0 Copyright (c) 2017 [Thomas Liebig](http://www.thomas-liebig.eu) based upon the version of stefan [Stefan Michaelis] 2011 (http://www.stefan-michaelis.name) changes made: * changed some PINs * removed max tries * altered the servo direction for closing and opening the box * compiles with current libraries This software is licensed under the terms of the Creative Commons "Attribution Non-Commercial Share Alike" license, version 3.0, which grants the limited right to use or modify it NON- COMMERCIALLY, so long as appropriate credit is given and derivative works are licensed under the IDENTICAL TERMS. For license details see http://creativecommons.org/licenses/by-nc-sa/3.0/ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ #include <Streaming.h> #include <EEPROM.h> #include <TinyGPS.h> #include <SoftwareSerial.h> #include <Servo.h> #include <math.h> #include <LiquidCrystal_I2C.h> LiquidCrystal_I2C lcd(0x27, 2, 1, 0, 4, 5, 6, 7); /** Testdefinition. If this is uncommented, use testing data without actual GPS measurements */ /*#ifndef __GPS_TESTING__ #define __GPS_TESTING__ #endif */ #ifdef __GPS_TESTING__ const float test_latlons[] = {51.12345f, 7.12345f, 51.234523f, 6.345345f, 51.234565f, 7.123456f, 51.453456f, 7.123541f }; #endif /* *** Quest Config *** */ // TODO: Target coordinates. const float target_latlons[] = {51.234545f, 7.234546f, 51.605220f, 7.234556f, 51.345654f, 7.234456f, 51.456785f, 7.345567f, 51.345567f, 6.345678f, 51.345578f, 7.345678f }; // TODO: Max deviation around each target in m. One for each target. const int target_dev[] = {10, 10, 15, 20, 10000, 30000, 1000, 30000, 1000}; // Maximum number of tries before box is locked const int MAX_TRIES = 50; /* *** System Config *** */ const byte POLOLU_PIN = 12; //const byte LCD_PIN = 9; const byte GPS_IN_PIN = 10; const byte GPS_OUT_PIN = 3; const byte SERVO_PIN = 8; const int ADDR_TRIES = 0; const int ADDR_STAGE = 1; const int ADDR_STARTCOORDS = 2; const float EARTH_RADIUS = 6378.388f; const float MY_DEG_TO_RAD = 0.0174532925f; // TODO: If around these coordinates, the box will reset to the beginning of the quest. const float HOME_LAT = 51.234859f; const float HOME_LON = 7.234563f; // Switch off, if no signal after this time const unsigned long GPS_DELAY_MS = 120000; enum Phase {INIT, SEARCH_GPS, EVAL_POS, DISPLAY_TRIES, UNLOCK, SEAL, SHUTDOWN, SEND_COORDS, NO_GPS}; Phase currentPhase = INIT; float current_latlon[] = {0.0f, 0.0f}; // Input, Output //SoftwareSerial lcdSerial(0, LCD_PIN); TinyGPS gps; SoftwareSerial gpsSerial(GPS_IN_PIN, GPS_OUT_PIN); Servo servo; unsigned long starttime = 0; /***************** Methods ************************/ void lcdclear() { //lcdSerial.write(0xFE); //lcdSerial.write(0x01); lcd.clear(); } void lcdpos(byte x, byte y) { //lcdSerial.write(254); //lcdSerial.write(128+x+64*y); lcd.setCursor(x, y); } void blink_on() { // Blinking cursor on //lcdSerial.write(0xFE); //lcdSerial.write(0x0D); //lcd.blink(); } void shutdown() { //lcdclear(); lcd.clear(); //lcdSerial.print( " Schalte ab..."); lcd.print(" Schalte ab..."); delay(3000); digitalWrite( POLOLU_PIN, HIGH ); currentPhase = SEND_COORDS; } /* Distance calculation. */ float gcd(float lat_a, float lon_a, float lat_b, float lon_b) { // Haversine float a = sin((lat_a - lat_b) / 2); float b = sin((lon_a - lon_b) / 2); float d = 2 * asin(sqrt(a * a + cos(lat_a) * cos(lat_b) * b * b)); return fabs(EARTH_RADIUS * d); } void lock() { servo.attach(SERVO_PIN); servo.write(180); delay(2000); servo.detach(); } boolean check_seal() { return false; } void unlock() { servo.attach(SERVO_PIN); servo.write(0); delay(3000); servo.detach(); } void reset_box() { lcd.clear(); lcd.write("Setze Box zur"); lcd.write(0xF5); lcd.write("ck"); EEPROM.write(ADDR_TRIES, 0); EEPROM.write(ADDR_STAGE, 0); delay(3000); unlock(); delay(20000); lock(); shutdown(); } // Read from Serial, reset box in case reset command retrieved void wait_for_reset() { const int MAX_MESSAGE_LEN = 128; const int LINE_FEED = 13; char message_text[MAX_MESSAGE_LEN]; int index = 0; Serial << "Waiting for reset-Command.\n"; while (true) { if (Serial.available() > 0) { int current_char = Serial.read(); if (current_char == LINE_FEED || index == MAX_MESSAGE_LEN - 1) { message_text[index] = 0; index = 0; // Reset Command? Serial.println(message_text); if (strcmp(message_text, "reset") == 0) { reset_box(); } } else { message_text[index++] = current_char; } } } } void welcome() { // Check, that Latlon-Arraylength = dev-Arraylength. If not, check array definitions above if (sizeof(target_latlons) / 8 != sizeof(target_dev) / 2) { lcd.write("Fehlkonfiguration der Ziele"); delay(5000); } lcd.write(" Willkommen! "); lcdpos(0, 1); lcd.write(" === Ralph === "); byte stage = EEPROM.read(ADDR_STAGE); //lcdpos(0,1); //lcd.write(stage); delay(2000); if (stage >= sizeof(target_latlons) / 8) { currentPhase = UNLOCK; return; } if (check_seal()) { return; } byte tries = EEPROM.read(ADDR_TRIES); lcdclear(); lcd.write("Wir sind bei"); lcdpos(0, 1); lcd.write("Station "); char charVal2[1]; dtostrf(stage+1,1,0,charVal2); lcd.write(charVal2); lcd.write(" von "); char charVal1[1]; dtostrf(sizeof(target_latlons)/8,1,0,charVal1); lcd.write(charVal1); delay(5000); lcdclear(); lcd.write("und Versuch "); //lcdpos(0, 1); char charVal3[2]; dtostrf(tries+1,2,0,charVal3); lcd.write(charVal3); //lcd.write(" von "); //lcd.write(MAX_TRIES); lcd.write("."); // TODO // Increase used tries EEPROM.write(ADDR_TRIES, ++tries); delay(5000); currentPhase = SEARCH_GPS; } template <class T> int EEPROM_writeAnything(int ee, const T& value) { const byte* p = (const byte*)(const void*)&value; int i; for (i = 0; i < sizeof(value); i++) EEPROM.write(ee++, *p++); return i; } template <class T> int EEPROM_readAnything(int ee, T& value) { byte* p = (byte*)(void*)&value; int i; for (i = 0; i < sizeof(value); i++) *p++ = EEPROM.read(ee++); return i; } void save_coords() { byte tries = EEPROM.read(ADDR_TRIES); EEPROM_writeAnything(ADDR_STARTCOORDS + (tries - 1)*sizeof(current_latlon), current_latlon); } void seal() { lcd.clear(); lcd.print(" Box versiegelt"); delay(5000); lcd.clear(); lcd.write(" Bitte zur"); lcd.write(0xF5); lcd.print("ck"); lcd.print(" zum Absender"); delay(5000); } void displaytries() { lcd.clear(); byte tries = EEPROM.read(ADDR_TRIES); String versuche = " Versuche"; if (MAX_TRIES - tries == 1) { versuche = " Versuch"; } lcd.write("Noch "); lcd.write(MAX_TRIES-tries); lcd.print(versuche); lcdpos(0, 1); lcd.write(0xF5); lcd.print("brig"); delay(5000); } void no_gps() { currentPhase = DISPLAY_TRIES; lcd.clear(); lcd.print("konnte kein"); lcdpos(0, 1); lcd.print("Signal finden"); delay(5000); } void evalpos() { // Compare to home coordinates float dist = 1000.0f * gcd( current_latlon[0] * MY_DEG_TO_RAD, current_latlon[1] * MY_DEG_TO_RAD, HOME_LAT * MY_DEG_TO_RAD, HOME_LON * MY_DEG_TO_RAD ); if (dist < 200) { // Near home? => Reset box reset_box(); } byte stage = EEPROM.read(ADDR_STAGE); dist = 1000.0f * gcd( current_latlon[0] * MY_DEG_TO_RAD, current_latlon[1] * MY_DEG_TO_RAD, target_latlons[stage * 2] * MY_DEG_TO_RAD, target_latlons[stage * 2 + 1] * MY_DEG_TO_RAD ); // Debug Serial << "Lat current: "; Serial.print(current_latlon[0], 6); Serial << " Lon current: "; Serial.println(current_latlon[1], 6); Serial << "Lat target: "; Serial.print(target_latlons[stage * 2], 6); Serial << " Lon target: "; Serial.println(target_latlons[stage * 2 + 1], 6); Serial << "Current distance: " << dist << " m\n"; lcd.clear(); if (dist < target_dev[stage]) { lcd.print("Station "); lcdpos(0, 1); lcd.print("erreicht."); delay(5000); EEPROM.write(ADDR_STAGE, ++stage); if (stage >= sizeof(target_latlons) / 8) { // Last target reached currentPhase = UNLOCK; } else { // Current target reached, but more to go lcd.clear(); String stationen = " Stationen"; char charVal5[1]; dtostrf((sizeof(target_latlons) / 8) - stage,1,0,charVal5); if ((sizeof(target_latlons) / 8) - stage == 1) { stationen = " Station"; } lcd.write("Noch "); lcd.write(charVal5); lcd.print(stationen); lcdpos(0, 1); lcd.write("bis zum Ziel."); delay(5000); currentPhase = EVAL_POS; } } else { // Target missed lcd.write("Entfernung zur "); lcdpos(0, 1); lcd.write("n"); lcd.write(0xE1); lcd.write("chsten Station"); delay(5000); lcdclear(); char charVal4[1]; if (dist > 5000) { dtostrf(dist / 1000.0f,4,0,charVal4); lcd.write(charVal4); lcd.write(" km."); } else { dtostrf(dist,4,0,charVal4); lcd.write(charVal4); lcd.write(" m."); } delay(5000); lcdclear(); lcd.write(" Ziel noch nicht"); lcdpos(0, 1); lcd.write(" erreicht."); delay(3000); currentPhase = DISPLAY_TRIES; } } void searchgps() { lcdclear(); blink_on(); lcd.write("Suche GPS-Signal"); delay(3000); // Brightness 30% lcd.write(0x7C); lcd.write(137); int numFixes = 0; unsigned long waitUntil = millis() + GPS_DELAY_MS; boolean signalfound = false; while ( (millis() < waitUntil) && (!signalfound) ) { #ifndef __GPS_TESTING__ if (gpsSerial.available()) { // Debug //char c = gpsSerial.read(); //Serial.print(c); if (gps.encode(gpsSerial.read())) { unsigned long fix_age; float flat, flon; gps.f_get_position(&flat, &flon, &fix_age); if ( (fix_age > 5000) || (fix_age == TinyGPS::GPS_INVALID_AGE) ) continue; if ( numFixes < 5 ) { numFixes++; // lcdSerial << "."; // Debug Serial << "Fix" << flat << ", " << flon << ", " << fix_age << "\n"; continue; } current_latlon[0] = fabs(flat); current_latlon[1] = fabs(flon); signalfound = true; } } #else // GPS Testing delay(10000); randomSeed(analogRead(0)); long result = random(0, sizeof(test_latlons) / 8); Serial.println(result); current_latlon[0] = test_latlons[result * 2]; current_latlon[1] = test_latlons[result * 2 + 1]; signalfound = true; #endif } // Debug: GPS-Stats printing unsigned long chars; unsigned short sentences; unsigned short failed_cs; gps.stats(&chars, &sentences, &failed_cs); Serial.println("GPS-Stats:"); Serial << "Number of chars: " << chars << "\n"; Serial << "Number of sentences: " << sentences << "\n"; Serial << "Checksum errors: " << failed_cs << "\n"; // Blinking cursor on lcd.write(0xFE); lcd.write(0x0C); // Brightness 100% lcd.write(0x7C); lcd.write(157); if (signalfound) { currentPhase = EVAL_POS; } else { currentPhase = NO_GPS; } save_coords(); } // Send history of activation positions via serial void send_coords() { Serial << "Coordinates: \n"; byte tries = EEPROM.read(ADDR_TRIES); for (byte i = 0; i < tries; i++) { EEPROM_readAnything(ADDR_STARTCOORDS + i * sizeof(current_latlon), current_latlon); Serial.print(current_latlon[0], 6); Serial << ","; Serial.println(current_latlon[1], 6); } } void setup() { //pinMode(LCD_PIN, OUTPUT); pinMode(GPS_OUT_PIN, OUTPUT); pinMode(POLOLU_PIN, OUTPUT); pinMode(SERVO_PIN, OUTPUT); Serial.begin(9600); gpsSerial.begin(4800); //lcdSerial.begin(9600); //lcdclear(); lcd.setBacklightPin(3, POSITIVE); // Bei NEGATIVE ist die Beleuchtung aus. lcd.begin(16, 2); // In dieser Zeile unterscheidet sich der Code. Er ist auf 16 Zeichen und 2 Zeilen gesetzt lcd.setCursor(0, 0); starttime = millis(); } void loop() { switch (currentPhase) { case INIT: welcome(); break; case SEARCH_GPS: searchgps(); break; case EVAL_POS: evalpos(); break; case SHUTDOWN: shutdown(); break; case DISPLAY_TRIES: //displaytries(); currentPhase = SHUTDOWN; check_seal(); break; case UNLOCK: unlock(); currentPhase = SHUTDOWN; break; case SEAL: seal(); currentPhase = SHUTDOWN; break; case NO_GPS: no_gps(); break; case SEND_COORDS: send_coords(); delay(1000); wait_for_reset(); break; default: shutdown(); } // Emergy switch off after 10 min if something fails if (millis() > starttime + 600000) { lcdclear(); lcd.write("Notabschaltung"); delay(5000); digitalWrite( POLOLU_PIN, HIGH ); } } // end loop
The updated code of the complete GCT listing is available here: GCT-code.zip.
Step5:
Next, the hardware should be placed within the box. The box needs to be chosen carefully. Questions to consider are: What lock will be available? Which thinngs do you want to place inside? How suitable is the box for travelling the desired route? Should batteries be exchangable? Is there a way to open the box for maintanance? I decided for a simple watch box. In the lower drawer is the secret, in the upper drawer the arduino. You get an impression of the box here:
The updated code of previous tests and the complete GCT listing is available here: GCT-code.zip.