Software

I got a lot of help on the software from Bobby Nordlund who built his own Geobox that has a lot more features than mine. Check out his box here. His stuff is based off of Mikal Hart of http://arduiniana.org who had the original idea for the Reverse Geocache box.  Below is the final version of the code I used:
Download: file
/* Engagement Box   by Kenton Harris 11/12/2012
Reverse Geocache Engagement Ring Box
This program unlocks a box that has reached a certain location.

The device uses the following products from Adafruit Industries:
Arduino Uno: https://www.adafruit.com/products/50
Ultimate GPS (version1): http://www.adafruit.com/products/746
16x2 Character LCD: https://www.adafruit.com/products/181
TPro Micro Servo SG90: https://www.adafruit.com/products/169
Half Sized Perma proto: https://www.adafruit.com/products/571

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.

*/

#include <math.h>
#include <LiquidCrystal.h>
#include <Adafruit_GPS.h>
#include <SoftwareSerial.h>
SoftwareSerial mySerial(3,2);
Adafruit_GPS GPS(&mySerial);
#define GPSECHO false												     //make true to debug GPS
boolean usingInterrupt = false;
void useInterrupt(boolean);

//Servo
#include <PWMServo.h>
PWMServo servoLatch;

//Declarations
const float deg2rad = 0.01745329251994;
const float rEarth = 6371000.0;                                          //can replace with 3958.75 mi, 6370.0 km, or 3440.06 NM
float range = 3000;                                                      // distance from HERE to THERE
String here;                                                             // read from GPS

// initialize the library with the numbers of the interface pins
LiquidCrystal lcd(7, 8, 6, 10, 11, 12);

int gpsWasFixed = HIGH;                                                  // did the GPS have a fix?
int ledFix = 4;                                                          // pin for fix LED
int servoPin = 9;                                                        // pin for servo
int servoLock = 110;                                                     // angle (deg) of "locked" servo
int servoUnlock = 0;                                                     // angle (deg) of "unlocked" servo

String there = "N38 51.409, W077 01.328";                                //Desired Location goes here. Make sure you use the same syntax and number of characters

void setup()
{
  servoLatch.attach(SERVO_PIN_A);
  servoLatch.write(servoLock);
  delay(50);
  lcd.begin(16, 2);
  Serial.begin(115200);
  Serial.println("Debug GPS Test:");

  GPS.begin(9600);
  GPS.sendCommand(PMTK_SET_NMEA_OUTPUT_RMCGGA);                          // RMC (recommended minimum) and GGA (fix data) including altitude
  GPS.sendCommand(PMTK_SET_NMEA_UPDATE_1HZ);                             // 1 Hz update rate
  useInterrupt(true);                                                    // reads the steaming data in a background
  delay(1000);
  

}

void loop(){
  // Parse GPS and recalculate RANGE
  if (GPS.newNMEAreceived()) {
    if (!GPS.parse(GPS.lastNMEA()))                                      // also sets the newNMEAreceived() flag to false
      return;                                                            // We can fail to parse a sentence in which case we should just wait for another
  }
    if (GPS.fix) {
    gpsWasFixed = HIGH;
    digitalWrite(ledFix, HIGH);

  
    here = gps2string ((String) GPS.lat, GPS.latitude, (String) GPS.lon, GPS.longitude);
    range = haversine(string2lat(here), string2lon(here), string2lat(there), string2lon(there));
    Serial.print("Here: ");                                        //for GPS debug
    Serial.print(here);
    Serial.print("There: ");
    Serial.println(there);
    Serial.print("Range: ");
    Serial.print(range);
    Serial.println("m");
    
    lcd.clear();
    lcd.setCursor(0,0);
    lcd.print("Distance to LOYL");
    //lcd.setCursor(0,1);
    //lcd.print("                ");
    lcd.setCursor(0,1);
    lcd.print(range);

    delay(500);
  }
  else {                                                              //No GPS fix- take box outside
    lcd.clear();
    lcd.setCursor(0,0);
    lcd.print("Hello Jenny!");
    lcd.setCursor(0,1);
    lcd.print("Take me outside!");
    delay(200);
  }
  
  if (range < 20.0){
    servoLatch.write(servoUnlock);
    delay(1000);
    lcd.clear();
    lcd.setCursor(0,0);
    lcd.print("Jenny: Will you ");
    lcd.setCursor(0,1);
    lcd.print("Marry Me?!");
    delay(5000);
  }
}


SIGNAL(TIMER0_COMPA_vect) {
  // Interrupt is called once a millisecond, looks for any new GPS data, and stores it
  char c = GPS.read();
  if (GPSECHO)
    if (c) UDR0 = c;  
}

void useInterrupt(boolean v) {
  if (v) {
    OCR0A = 0xAF;
    TIMSK0 |= _BV(OCIE0A);
    usingInterrupt = true;
  } else {
    TIMSK0 &= ~_BV(OCIE0A);
    usingInterrupt = false;
  }
}

String int2fw (int x, int n) {
  // returns a string of length n (fixed-width)
  String s = (String) x;
  while (s.length() < n) {
    s = "0" + s;
  }
  return s;
}

String gps2string (String lat, float latitude, String lon, float longitude) {
  // returns "Ndd mm.mmm, Wddd mm.mmm";
  int dd = (int) latitude/100;
  int mm = (int) latitude % 100;
  int mmm = (int) round(1000 * (latitude - floor(latitude)));
  String gps2lat = lat + int2fw(dd, 2) + " " + int2fw(mm, 2) + "." + int2fw(mmm, 3);
  dd = (int) longitude/100;
  mm = (int) longitude % 100;
  mmm = (int) round(1000 * (longitude - floor(longitude)));
  String gps2lon = lon + int2fw(dd, 3) + " " + int2fw(mm, 2) + "." + int2fw(mmm, 3);
  String myString = gps2lat + ", " + gps2lon;
  return myString;
};
/*
float string2radius (String myString) {
  // returns a floating-point number: e.g. String myString = "Radius: 005.1 NM";
  float r = ((myString.charAt(8) - '0') * 100.0) + ((myString.charAt(9) - '0') * 10.0) + ((myString.charAt(10) - '0') * 1.0) + ((myString.charAt(12) - '0') * 0.10);
  return r;
};*/

float string2lat (String myString) {
  // returns radians: e.g. String myString = "N38 58.892, W076 29.177";
  float lat = ((myString.charAt(1) - '0') * 10.0) + (myString.charAt(2) - '0') * 1.0 + ((myString.charAt(4) - '0') / 6.0) + ((myString.charAt(5) - '0') / 60.0) + ((myString.charAt(7) - '0') / 600.0) + ((myString.charAt(8) - '0') / 6000.0) + ((myString.charAt(9) - '0') / 60000.0);
  //Serial.print("float lat: ");
  //Serial.println(lat);
  lat *= deg2rad;
  if (myString.charAt(0) == 'S')
    lat *= -1;                                                           // Correct for hemisphere
  return lat;
};

float string2lon (String myString) {
  // returns radians: e.g. String myString = "N38 58.892, W076 29.177";
  float lon = ((myString.charAt(13) - '0') * 100.0) + ((myString.charAt(14) - '0') * 10.0) + (myString.charAt(15) - '0') * 1.0 + ((myString.charAt(17) - '0') / 6.0) + ((myString.charAt(18) - '0') / 60.0) + ((myString.charAt(20) - '0') / 600.0) + ((myString.charAt(21) - '0') / 6000.0) + ((myString.charAt(22) - '0') / 60000.0);
  //Serial.print("float lon: ");
  //Serial.println(lon);
  lon *= deg2rad;
  if (myString.charAt(12) == 'W')
    lon *= -1;                                                           // Correct for hemisphere
  return lon;
};


float haversine (float lat1, float lon1, float lat2, float lon2) {
  // returns the great-circle distance between two points (radians) on a sphere
  float h = sq((sin((lat1 - lat2) / 2.0))) + (cos(lat1) * cos(lat2) * sq((sin((lon1 - lon2) / 2.0))));
  float d = 2.0 * rEarth * asin (sqrt(h)); 
  //Serial.println(d);
  return d;
  
};
When you turn on the box, it instructs the user to take it outside (to obtain the GPS satellites). Once it has a GPS fix, it displays the distance (in meters) to a programmed latitude/longitude location.
When it gets within 20 meters of the location, the servo rotates the latch, unlocking the box. The display then asks, “Will You Marry Me?”.
This guide was first published on Dec 06, 2012. It was last updated on Dec 06, 2012. This page (Software) was last updated on Nov 11, 2019.