Building a Geo-Cache-Transposed (GCT)

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

lcd-test

#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:
gps-test

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

servo-test

#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
complete
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:
20171222_202108
The updated code of previous tests and the complete GCT listing is available here: GCT-code.zip.