Reverse Geocache Troubleshooting

Hi all,

I am relatively new to this amazing Arduino 'game', and have started to build a Reverse Geocache inspired by Mikal Hart. I have it mostly working, but I'm getting some crazy text scrolling on the 16x2 LCD when the gps module is searching for satellites while the project is inside. Once outside and the gps has a lock on the satellites, it performs as normal.
I have attached a link to a short video of the behaviour of the lcd.
Really hoping someone can help me out with this ... cheers.

/* 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;
  
};/code]

As your experiments have proven, GPS doesn't work indoors.

When the GPS module can't find satellites, it is programmed to display "Take me outside", which it does for a short while until the scrambled text starts

Do fix your indentation: ctrl-T in the IDE, to make your code more readable.

jasenjanine:

int gpsWasFixed = HIGH;                                                  // did the GPS have a fix?

Make this a bool, and use true/false rather than HIGH/LOW for bools. Sure they do the same thing but it's clearer to stick to conventions.

  // 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
  }

Don't use return here!
Set a flag or so, if your library doesn't simply keep the old value, and an if statement later that checks this flag. A return makes the loop() restart, and that can be very confusing when debugging your sketch ("why doesn't it get there?!").

   gpsWasFixed = HIGH;

Nowhere in your sketch does this get changed to LOW/false - now what if you lose GPS fix?

  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);
  }

This part looks sound to me... How is that GPS connected?
My best guess is some form of interference.
And now just hope she doesn't take too long to find the place, and that you're there as well :slight_smile:

Sigh... so many example programs have the wrong structure. Then when somebody tries to modify them, they don't work right. :frowning:

Here is a version of your that sketch with the correct structure. It only recalculates the range when a new GPS fix is ready. That happens once per second, so you don't need delay any more. Actually, delay can cause the sketch to lose GPS data.

When the range is recalculated, it is compared to the threshold of 20m. If it's more than that, it just displays the new range. If it's less than that, it pops the lid... and the question. :slight_smile:

I also converted it to use my NeoGPS library. NeoGPS is smaller, faster, more reliable and more accurate than all other GPS libraries. Here ya' go:

/* 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

Modified by /dev here:
  https://forum.arduino.cc/index.php?topic=514292.0

Credit to Mikal Hart of http://arduiniana.org/ for the original idea of Reverse Geocache.

*/

#include <LiquidCrystal.h>
LiquidCrystal lcd(7, 8, 6, 10, 11, 12);  // the interface pins

#include <NMEAGPS.h>
NMEAGPS gps;

#include <NeoSWSerial.h>
NeoSWSerial gpsPort(3,2);

#include <PWMServo.h>
PWMServo servoLatch;

//Declarations
const int LED_FIX_PIN  = 4;
const int SERVO_PIN    = 9;
const int SERVO_LOCK   = 110;    // angle (deg) of "locked" servo
const int SERVO_UNLOCK = 0;      // angle (deg) of "unlocked" servo

// Desired Location. Make sure you use the degrees from google maps * 10,000,000
NeoGPS::Location_t there( 388568167L, -770221333L );

void setup()
{
  servoLatch.attach(SERVO_PIN_A);
  servoLatch.write(SERVO_LOCK);
  delay(50);

  lcd.begin(16, 2);

  Serial.begin(115200);
  Serial.println( F("Debug GPS Test:") ); // F macro saves RAM

  gpsPort.begin(9600);

  // configure RMC (recommended minimum) only
  gps.send_P( &gpsPort, F("PMTK314,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0") ); 
  // configure 1 Hz update rate
  gps.send_P( &gpsPort, F("PMTK220,1000") );
}

void loop()
{

  // Parse GPS characters until a complete fix is ready
  if (gps.available( gpsPort )) {

    gps_fix fix = gps.read(); // get the new GPS info, all in one structure

    digitalWrite( LED_FIX_PIN, fix.valid.location ); // HIGH if valid, LOW if not valid

    if (fix.valid.location) {

      // recalculate RANGE when we have a valid location
      float range = there.DistanceKm( fix.location ) * 1000.0;

      // for GPS debug
      Serial.print  ( F("Here: ") );            
      Serial.print  ( fix.location.latF(), 6 );
      Serial.print  ( F(", ") );
      Serial.println( fix.location.lonF(), 6 );
      Serial.print  ( F("There: ") );
      Serial.println( there.latF(), 6 );
      Serial.print  ( F(", ") );
      Serial.println( there.lonF(), 6 );
      Serial.print  ( F("Range: ") );
      Serial.print  ( range );
      Serial.println( 'm' );

      // Display how close we are
      lcd.clear();
      lcd.setCursor(0,0);

      if (range > 20.0){
        // Too far!
        lcd.print( F("Distance to LOYL") );
        //lcd.setCursor(0,1);
        //lcd.print("                ");
        lcd.setCursor(0,1);
        lcd.print(range);

      } else {
        // She's there!
        servoLatch.write(SERVO_UNLOCK);
        delay(1000);

        lcd.print("Jenny: Will you ");
        lcd.setCursor(0,1);
        lcd.print("Marry Me?!");

        for (;;); // hang here... waiting... for a YES
      }

    } else {
      // No GPS fix - take box outside
      lcd.clear();
      lcd.setCursor(0,0);
      lcd.print( F("Hello Jenny!") );
      lcd.setCursor(0,1);
      lcd.print( F("Take me outside!") );
    }
  }
}

BTW, SoftwareSerial is very inefficient, because it disables interrupts for the entire time that each character is received, about 1ms. That's an eternity for a 16MHz Arduino. While listening to one port, all other activity is disabled. Please read this for better alternatives. I used NeoSWSerial above so it could use the same pins that you have now.

There are a few other things I cleaned up: indentation, as others noted; uppercase constant names for the pins; F macro for printed string constants (saves RAM); eliminate the nasty String class (saves headaches); and it's a much smaller sketch:

The original sketch of 201 lines of code uses 18266 bytes of program space and 1057 bytes RAM.
The NeoGPS version of 127 lines of code uses 12824 bytes of program space and 431 bytes RAM, a significant savings.

If you want to try it, NeoGPS and NeoSWSerial are available from the Arduino IDE Library Manager, under the menu Sketch -> Include Library -> Manage Libraries.

Cheers,
/dev

Thank you /dev,

For the record, I didn't change the code at all. It was copied straight from the Adafruit website. I will try it out when I get home from work and let you know how it goes.

Cheers

For the record, I didn't change the code at all.

Sorry, I was referring to the Adafruit_GPS example that this sketch was based on.

Reply #4 modified to include the link to the Installation instructions, which covers serial port choices.

jasenjanine:
Thank you /dev,

For the record, I didn't change the code at all. It was copied straight from the Adafruit website. I will try it out when I get home from work and let you know how it goes.

Cheers

That seems to be working great /dev ... thank you!. Just have a small issue with the desired co-ordinates. These are the ones I want according to google maps ... -31.730940, 116.360850 (*10,000,000 ???)
How do I get them to work in the sketch where you have ... 388568167L, -770221333L
Sorry about my ignorance ... just a newbie who's keen as mustard :wink:

-31.730940, 116.360850 (*10,000,000 ???)
How do I get them to work in the sketch

Well, -31.730940 * 10000000 = -317309400. Because it's a "big" number (i.e. a [u]l[/u]ong integer), you should put the 'L' after the constant. Simlarly for 116.360850, which would be 1163608500. Then you would code that as:

    NeoGPS::Location_t there( -317309400L, 1163608500L );

And keep us posted! :slight_smile:

Thanks so much /dev ... works great now
Now, if I can ask for one more bit of guidance?
As this project will be an official geocache and not a wedding proposal, I need the box to close itself once it has been taken away from the desired co-ordinates. Can you help me out with that please?

Much easier: have the box lock itself the moment it's closed, regardless of the location. This can be done using e..g a microswitch that get triggered by the lid, or a light sensor (LDR) that detects the total darkness of a closed lid.

Then to re-open, if you're still at the correct location, you just switch off and on the box. Coordinates are checked again, and if correct, the box opens.

You also have to add a self-power-off, after say 30-60 seconds from obtaining the GPS fix, unless you want to do maintenance (replace batteries) every few days. Same for when the box is closed again: the moment the lid is locked, you can switch it off.

wvmarle:
Much easier: have the box lock itself the moment it's closed, regardless of the location. This can be done using e..g a microswitch that get triggered by the lid, or a light sensor (LDR) that detects the total darkness of a closed lid.

Then to re-open, if you're still at the correct location, you just switch off and on the box. Coordinates are checked again, and if correct, the box opens.

You also have to add a self-power-off, after say 30-60 seconds from obtaining the GPS fix, unless you want to do maintenance (replace batteries) every few days. Same for when the box is closed again: the moment the lid is locked, you can switch it off.

Thanks for that wvmarle.

Those ideas sound great, and would certainly work. I was however, wondering if I could just add some code to the sketch to reverse the servo once the box is taken away from the 'zone'. This would save me buying more hardware and having to program that as well. Just thought it would keep it simpler with less parts?
Also, I have a momentary switch I will be using to power on the box so the batteries won't go flat.

You can't be sure that the cachers will always switch it off, nor that the list is closed when taken out of the zone. If the mechanism is engaged while the lid is open it can't lock any more.
Or what if someone switched it off while in the zone? So lid isn't locked and won't be when moved back to listed coordinates.

wvmarle:
You can't be sure that the cachers will always switch it off, nor that the list is closed when taken out of the zone. If the mechanism is engaged while the lid is open it can't lock any more.
Or what if someone switched it off while in the zone? So lid isn't locked and won't be when moved back to listed coordinates.

With a momentary switch installed, the box will only power up while the button is pressed so don't have to worry about anyone leaving it on. If the box ONLY opens when in the zone, and ONLY closes when out of the zone, I see no major issues.
If the box is closed when out of the zone (zone being a 10m radius), it of course won't lock and will have to be taken back to the zone to activate the servo again to allow the box to be locked. It will have a message to hold the power button on with the lid closed while leaving the zone .. hopefully this will avoid any complications?
Can you see this working maybe?

I wouldn't trust the other cachers to even read instructions, let alone to activate it again to have it close. I would just make sure the box takes care of itself as much as possible.