GPS Puzzle
An Arduino GPS puzzle utilizing NFC for a curious friend

Origins

Some years back a reverse geocache project caught my attention and I really wanted to create something similar but also expand upon it. A friend of mine has a deep penchant the mysterious (as well as a love for metal detecting) and so I ordered some components and broke out the breadboard to see what I could throw together for him.

puzzle showing distance to target

Requirements

  • It had to arrive anonymously with an air of mystery
  • It had to take him to more than one location, preferably four or more
  • The navigation needed to be a puzzle in itself
  • Communication would be constrained to a 32 char LCD display and beeper
  • It needed to retain progress even through power loss
  • The resulting prize at the end needed to be roughly the size of a liquor bottle

The Plan

I decided that rather than have the device simply beep and flash a message of “you made it!” upon arrival at a waypoint that it would be way more fun to require some kind of physical interaction, hence the addition of the NFC reader and RFID stickers: Arrive at the waypoint, hear some beeping, hold the device up to the RFID sticker, flash a congrats message, and then have the holder be notified to move on to the next waypoint. That way it would be possible to have my friend visit various interesting places and make the puzzle take a bit of time. Just like the reverse geocache box, I’d only provide a distance to the destination which would mean some mapping skills were required. Concerns over losing progress to power loss were handled by poking at the EEPROM that Arduinos have to store a simple progress tracker, although during the testing resetting this int over and over again became really tedious as I had not wired up any kind of switch because I didn’t feel there was a need to require input from the user. I also really enjoyed the box having only a power switch and it giving the user no control.

Wiring it Up

The biggest challenge was having enough pins to handle all the features of the device which was a bit of a trick. I got lazy and found a piezo buzzer that required no amplification circuit and didn’t need a modulated signal which kept it incredibly simple in terms of wires and code. Also, thank goodness for I2C. The breadboard was a bit of a nightmare as was soldering it all up which I’m still not terribly good at – I promised myself I’d start printing boards if I ever started doing more projects like this in the future.

breadboard layout

soldered board

The Deployment

This was a bit of a rush job due to scheduling so a lot ended up happening in parallel. Since I didn’t have a 3D printer at the time, I ended up shipping the soldered-up mess to a friend in Michigan who quickly threw together a basic case for me and then sent it back. I ran around town placing the RFID stickers and then a large arrow sticker over top (there was no time to custom order stickers which I desperately wanted). To seed the intrigue, a friend of mine across the country began to send out purposefully mangled letters I’d created (opened and then taped shut again, missing pages, grammar errors, etc) simply indicating that something was coming as I figured that there might be some hesitation at opening an unexpected package and then powering on the student project-looking box inside.

letter1

letter3

letter2

receipt

As the letters arrived, the text messages poured in as my friend boggled over what was going on. Of course he suspected me, but the letters being from across the country threw him a little, as did the printed receipts. The real fun, however, began when The Box arrived on his doorstep.

text1

text2

I hadn’t anticipated the suspicion a random package showing up would create – seeing it being handled with rubber gloves was unexpected. In retrospect it might have had something to do with the joke warning message on the printed receipt about this product not being a good thing to use where there were landmines present. It took a lot of convincing to get him to even consider powering it on and he only agreed to do so when I was around which pretty much confirmed for me that he knew I was the one behind this. Yet, when I arrived at his house to pick him and the device up so we could talk about it at the bar, he retrieved it carefully from the outside trash can and placed it into the trunk of my vehicle with considerable care as if it were a thing to be feared.

The Device sitting at the bar

We arrived at the bar and – after a significant amount of convincing – he flipped the switch and the display came alive.

“TAKE ME OUTSIDE TO SEE THE SKY”

My friend’s eyes grew huge – something else I had not anticipated was the glowing power lights of the NFC reader, the Arduino, and the blinking status light on the GPS being visible through the plastic case at night which gave the device qualities of it being way more complex than it actually was. All manner of theories erupted now, including the box connecting to cellphone towers and possibly listening in on us. He turned it off. It took more encouragement (as well as a few drinks) to get my friend to power it up once more as we took to an outdoor table at the bar.

“CHECKING… 0/5”

The first number slowly incremented until the box reached the minimum of five GPS signals to get a fix before it beeped and the display changed.

“Dist to target: 11.8mi”

The Hunt

I figured that I would go with him – and even drive him – to the first waypoint to make sure all went okay. It took a while but eventually he found a suitable tool via Google that would plot circles on maps and show intersecting lines where the waypoint was. The first one was in a business park with the sticker stuck to the bottom of a metal pole protecting a mailbox along a lonely road.

the mailbox along a road with a sticker on one of the poles

Again, there was a considerable issue with what actually needed to happen once there. Once close enough, the display’s measurements switched to feet and once we were under 45 feet the box beeped and notified the user what to do.

“Hold me up to sticker.”

He looked all over the place and once he found the sticker (which I conveniently had missed in my helpful searching) he practically jumped up and down with excitement. Now I had included a printed photograph of the sticker in the instructions that came with the device and even circles the tip of the arrow but the problem occurred when the NFC reader proved to be a lot more finnicky about its positioning over the RFID sticker than in my tests. Thankfully I was there and held it up for him after he tried unsuccessfully several times. beep

“Waypoint found. Onward!

“Dist to target: 2.5mi”

“Oh, let’s keep going let’s keep going!” He was really excited about this now and I couldn’t just ditch him at this point. So we drove to the next one which had been placed on the back of a sign at the end of a dirt road at the entrance to a park trail.

the mailbox along a road with a sticker on one of the poles

This place was far more sinister at night – I hadn’t planned on him undertaking this adventure with the sun being down. This time, though, he confidently held the box up, waited for the beep, and then ran back to the car expecting us to go to the next waypoint. I explained, though, that I was tired and needed to get to sleep as I wanted him to do this part on his own since he was getting close to the goal. Apparently too excited to sleep, he went off on his own after I dropped him off and successfully located the last waypoint before the end goal which was - in an astounding coincidence - on the side of the bar we’d just been drinking at an hour earlier.

Now at this point I had assumed it all would have clicked that this was all my doing, but I’d done a better job than expected and he hadn’t figured it out just yet. He messaged me with photographs of the sticker but, sadly, the sticker wouldn’t scan for some reason. He was devastated. I told him I’d take a look at it in the morning.

He was standing in his driveway when I pulled in, holding The Device, and was clearly saddened that he couldn’t get it to work. At this point I broke the news that I was the one who created the puzzle for him and that I’d fix it by forcing the EEPROM counter to increment by one. I got a big hug for this and that was a great reward for the work I’d put into the game – it was absolutely worth seeing him so driven and enthusiastic about solving the puzzles once he had gotten over his fear of the mysterious box. I took the box back, quickly wrote the change to the EEPROM, then called him over so he could snag the box from me and continue with his hunt.

“Dist to target: 1.2mi”

The Treasure

The box beeped on the last waypoint in the woods at a park near my house before displaying a final message: “You made it! Congratulations! Now detect and dig around.” Later that day he returned with his metal detector and easily discovered the vertically buried, sealed PVC tube with stamped metal plate on top that contained a bottle of gin.

the mailbox along a road with a sticker on one of the poles

Lessons Learned

  • Not everyone will approach an unknown the way you expect
  • especially when you’re the one that designed the unknown and already know it!
  • Test and test and test through the play many times, even if it involves a lot of driving
  • Test even more. Test with someone else before releasing your project to the wild!

The Code

#include <LiquidCrystal.h>
#include <Adafruit_GPS.h>
#include <SPI.h>
#include <MFRC522.h>
#include <EEPROM.h>

/*
 * 
 * Metro Mini Pins
 * -----
 * 0 ->
 * 1 ->
 * 2 -> Beeper+
 * 3 -> RFID SDA
 * 4 -> RFID RST
 * 5 -> LCD13
 * 6 -> LCD14
 * 7 -> LCD4
 * 8 -> LCD6
 * 9 -> LCD11
 * 10 -> LCD12
 * 11 -> RFID MOSI
 * 12 -> RFID MISO
 * 13 -> RFID SCK
 * A0 -> 
 * A1 -> 
 * A2 -> 
 * A3 -> 
 * A4 -> GPS BLUE
 * A5 -> GPS YELLOW
 * USB -> 
 * RST -> 
 * 3V -> GPS RED
 * 5 -> LCD2, LCD15, POT1
 * GND -> LCD1, LCD5, POT3, GPS BLK, RFID GND 
 * 
 * POT
 * 1 -> 5V
 * 2 -> LCD3
 * 3 -> GND
 * 
 */



int MinimumSats = 5;
float successfulRadiusInFeetToWaypoints = 45.0;
float successfulRadiusInFeetToGoal = 30.0;

// Target listing max array
int maxStepNumber = 3;

// Array of Latitudes
float Latitudes[4] = {28.15963,228.15963,28.15963,28.15963}; // Replace these with puzzle values

// Array of Longitudes
float Longitudes[4] = {82.28096,82.28096,82.28096,82.28096}; // Replace these with puzzle values

// Target Tag IDs
char *UIDS[] = {"20A4F34C", "F024ED4C", "E05BF04C"}; // Replace these with RFID values

bool gotFix = false;
bool veryClose = false;


// Setup the LCD pins
LiquidCrystal lcd(7, 8, 9, 10, 5, 6);

// Connect to the GPS on the hardware I2C port
Adafruit_GPS GPS(&Wire);

// Create the card reader instance
MFRC522 mfrc522(3,4);


// Define EEPROM place to store a bit
#define EEPROMlocation 1

// Set GPSECHO to 'false' to turn off echoing the GPS data to the Serial console
// Set to 'true' if you want to debug and listen to the raw GPS sentences
#define GPSECHO false

uint32_t timer = millis();


// Get EEPROM value
int currentStepNumber = EEPROM.read(EEPROMlocation);


/***************************************************
 * Functions
 ***************************************************/
  
 void beepTwice() {
    digitalWrite(2,1);
    delay(5);
    digitalWrite(2,0);
    delay(100);
    digitalWrite(2,1);
    delay(5);
    digitalWrite(2,0);
  }

  void beepLong() {
    digitalWrite(2,1);
    delay(500);
    digitalWrite(2,0);
  }

   void beepLongTwice() {
    digitalWrite(2,1);
    delay(300);
    digitalWrite(2,0);
    delay(300);
    digitalWrite(2,1);
    delay(300);
    digitalWrite(2,0);
  }

  void successBeep() {
    digitalWrite(2,1);
    delay(20);
    digitalWrite(2,0);
    delay(20);
    digitalWrite(2,1);
    delay(20);
    digitalWrite(2,0);
    digitalWrite(2,1);
    delay(20);
    digitalWrite(2,0);
    delay(20);
    digitalWrite(2,1);
    delay(20);
    digitalWrite(2,0);
  }

 void printGoalMessage() {
    lcd.clear();
    lcd.setCursor(0,0);
    lcd.print("You made it!");
    lcd.setCursor(0,1);
    lcd.print("Congratulations!");
    delay(5000);
    lcd.clear();
    lcd.setCursor(0,0);
    lcd.print("Now detect and");
    lcd.setCursor(0,1);
    lcd.print("dig around.");
    delay(5000);
 }

  void printStatusMessage(int StepNumber, int MaxStep) {
    lcd.clear();
    lcd.setCursor(0,0);
    lcd.print("You are on step");
    lcd.setCursor(0,1);
    lcd.print("number ");
    lcd.print(StepNumber+1);
    lcd.print(" of ");
    lcd.print(MaxStep+1);
    delay(5000);
  }


  int advanceStepNumberInEEPROM(int EEPROMloc) {
    int currentValue = EEPROM.read(EEPROMloc);
    currentValue++;
    EEPROM.write(EEPROMlocation, currentValue);
    currentValue = EEPROM.read(EEPROMloc);
    return currentValue;
  }

  void printCheckingMessage(int NumberOfSatellites) {
      lcd.clear();
      lcd.setCursor(0,0);

      if ((millis() / 6000) % 2 == 0) {
        lcd.clear();
        lcd.print("take me outside");
        lcd.setCursor(0,1);
        lcd.print("to see the sky");
      }
      else {
        lcd.clear();
        lcd.setCursor(0,0);
        lcd.print("checking...");
        lcd.setCursor(0,1);
        lcd.print(NumberOfSatellites);
        lcd.print("/");
        lcd.print(MinimumSats);
      }
  }



  float convertToDegrees(float weirdFormat) {
    float degWhole = int(weirdFormat/100);
    float degDec = (weirdFormat - degWhole*100)/60;
    float deg = degWhole + degDec;
    return deg;
  }

  float calculateDistance(float flat1, float flon1, float flat2, float flon2)
  {
    float dist_calc = 0;
    float dist_calc2 = 0;
    float diflat = 0;
    float diflon = 0;

    diflat = radians(flat2-flat1);
    flat1 = radians(flat1);
    flat2 = radians(flat2);
    diflon = radians((flon2)-(flon1));
    dist_calc = (sin(diflat/2.0)*sin(diflat/2.0));
    dist_calc2 = cos(flat1);
    dist_calc2 *= cos(flat2);
    dist_calc2 *= sin(diflon/2.0);
    dist_calc2 *= sin(diflon/2.0);
    dist_calc += dist_calc2;
    dist_calc = (2*atan2(sqrt(dist_calc),sqrt(1.0-dist_calc)));
  
    dist_calc *= 3958.755866; // convert to non-science units

    return dist_calc;
  }

  float convertMilesToFeet(float miles) {
    return miles*5280;
  }


  float printDistanceToTarget(float distanceToTargetInMiles) {
    
    lcd.clear();
    lcd.setCursor(0, 0);
    lcd.print("Dist to target: ");
    lcd.setCursor(0,1);
    
    if (distanceToTargetInMiles < 0.759) {
      float distanceToTargetInFeet = convertMilesToFeet(distanceToTargetInMiles);
      lcd.print(distanceToTargetInFeet);
      lcd.print(" ft");
    }
    else {
      lcd.print(distanceToTargetInMiles,2);
      lcd.print(" mi");
    } 
  }




  String getRFIDUID() {
    String content= "";
    byte letter;
    for (byte i = 0; i < mfrc522.uid.size; i++) 
    {
       content.concat(String(mfrc522.uid.uidByte[i] < 0x10 ? "0" : ""));
       content.concat(String(mfrc522.uid.uidByte[i], HEX));
    }
    content.toUpperCase();
    return(content);
  }

  
  
  void askToScanSticker() {
    //beep
    lcd.clear();
    lcd.setCursor(0, 0);
    lcd.print("Hold me");
    lcd.setCursor(0,1);
    lcd.print("up to sticker.");
  }

  void printWaypointFindMessage() {
    lcd.clear();
    lcd.setCursor(0, 0);
    lcd.print("Waypoint found.");
    lcd.setCursor(0,1);
    lcd.print("Onward!");
  }

/***************************************************
 * Setup
 ***************************************************/


// the setup function runs once when you press reset or power the board
void setup() {

 
  //set up the beeper
  pinMode(2, OUTPUT);
  
  //Serial.println("###########################");
  //EEPROM.write(EEPROMlocation, 0); // Force set REMOVE THIS -- THIS IS NEEDED TO BE SET TO N TO START GAME

  // Init the LCD
  lcd.begin(16, 2);
  lcd.clear();

  // Init the card reader
  SPI.begin();
  mfrc522.PCD_Init();


  
  // Get the step number from the EEPROM and print it
  int currentStepNumber = EEPROM.read(EEPROMlocation);
  printStatusMessage(currentStepNumber,maxStepNumber);
  
  // Set up the GPS
  GPS.begin(0x10);  // The I2C address to use is 0x10
  GPS.sendCommand(PMTK_SET_NMEA_OUTPUT_RMCGGA);
  GPS.sendCommand(PMTK_SET_NMEA_UPDATE_1HZ); // 1 Hz update rate
  GPS.sendCommand(PGCMD_ANTENNA);

  delay(1000);

  // Ask for firmware version
  GPS.println(PMTK_Q_RELEASE);

}



/***************************************************
 * Loop
 ***************************************************/
void loop() {
  // read data from the GPS in the 'main loop'
  char c = GPS.read();
  if (GPS.newNMEAreceived()) {
    if (!GPS.parse(GPS.lastNMEA())) // this also sets the newNMEAreceived() flag to false
      return; // we can fail to parse a sentence in which case we should just wait for another
  }
  // approximately every 2 seconds or so, print out the current stats
  if (millis() - timer > 2000) {
    timer = millis(); // reset the timer
    if (GPS.fix && GPS.satellites >= MinimumSats) {
      if (!gotFix) {
        beepTwice();
      }
      gotFix = true;   
      float currentLongitude = convertToDegrees(GPS.longitude);
      float currentLatitude = convertToDegrees(GPS.latitude);
      float distanceInMilesToTarget = calculateDistance(currentLatitude, currentLongitude, Latitudes[currentStepNumber], Longitudes[currentStepNumber]);
      float distanceInFeetToTarget = convertMilesToFeet(distanceInMilesToTarget);
      printDistanceToTarget(distanceInMilesToTarget);
      float successfulRadiusInFeetToNext = 35;
      if (currentStepNumber == maxStepNumber) {successfulRadiusInFeetToNext = successfulRadiusInFeetToGoal;}
      else {successfulRadiusInFeetToNext = successfulRadiusInFeetToWaypoints;}
      if (distanceInFeetToTarget < successfulRadiusInFeetToNext) {
        if (!veryClose) {
          beepLongTwice();
        }
        veryClose = true; 
        askToScanSticker();
        mfrc522.PICC_IsNewCardPresent();
        if (mfrc522.PICC_ReadCardSerial()) 
        {
          String UID = getRFIDUID();
          beepLong();        
          if (UID == UIDS[currentStepNumber]) {
            currentStepNumber = advanceStepNumberInEEPROM(EEPROMlocation);
            successBeep();
            printWaypointFindMessage();
          }
          delay(500);
        }
        if (currentStepNumber == maxStepNumber) {
          printGoalMessage();
        }
      }
      else {
        veryClose = false; 
      }
    }
    else {
      printCheckingMessage(GPS.satellites);
    }
  }

Last modified on 2021-10-30