Final Project: Geo-locked Puzzle Box
I’m building a box that only opens at a specific location on Earth. When the button is pressed, it displays how far you are from the correct spot.
Idea
The goal is to create a location-aware box that can detect your current position via GPS. It remains locked until you're physically standing at a pre-defined location. The box will contain a button: pressing it will show the distance to the target spot. Once you're close enough (within a few meters), it will unlock and open.

Implementation
First of all I started testing individual components one by one. I wrote many tiny sketches just to make sure every part would light up, beep or print something. From the very beginning I suspected power distribution would be a headache, because I refused to put any built-in battery inside the box.
My first idea was to use an electromagnetic lock (GM202-UP) powered from the single 9 V battery that the person who finds the box has to bring along.
Because the lock needs 24 V I added a step-up converter and a little MOSFET switch.
However, the coil was never powerful enough to actuate and it dragged the rest of the circuit down with it. I tried sprinkling in some electrolytic capacitors which gave the coil a small kick at the start, but still not enough. The lock would happily stay open once I helped it with a finger, yet it could not pull in by itself.
Here is a video showing the problem:
Here is the test wiring again:

After fighting the electromagnet for far too long I switched to a servo. I redesigned the locking mechanism and did a couple of quick tests:

Unfortunately every time the servo moved it still upset the other modules, so I added a second 9 V battery just for the servo (a common trick with SG-90s). Here is the final wiring diagram:

And here is the same circuit in real life:

I measured every component and designed the smallest possible enclosure in Fusion 360:
Next I laser-cut the parts from 3 mm plywood, glued them together, and fixed the battery holder wires by cutting another jumper in half and soldering the ends:

I tested locking and unlocking with this quick sketch to find the two servo angles:
#include <Servo.h>
const int servoPin = 9; // Servo signal connected to D9
const int angle1 = -10; // Locked
const int angle2 = 20; // Unlocked
const int delayTime = 1000; // Delay between movements in milliseconds
Servo myServo;
void setup() { myServo.attach(servoPin); }
void loop() {
myServo.write(angle1);
delay(delayTime);
myServo.write(angle2);
delay(delayTime);
}
I slowly started putting all components into the box and uploaded the code to the Arduino. Here are two pictures of the build in progress:


The final full code is saved in final_project.ino
and looks like this:
#include <Wire.h>
#include <LiquidCrystal_I2C.h>
#include <SoftwareSerial.h>
#include <TinyGPS++.h>
#include <Servo.h>
// --- LCD + I/O
LiquidCrystal_I2C lcd(0x27, 16, 2);
const int lcdWidth = 16;
const int buttonPin = 2;
const int buzzerPin = 4;
Servo lockServo;
const int servoPin = 6;
// --- GPS Setup
SoftwareSerial gpsSerial(5, 3); // RX, TX, connect RX of GPS to pin 3, TX to pin 5
TinyGPSPlus gps;
// --- Unlock Target
const double targetLat = 40.7110633673; // 40.7110633673
const double targetLon = 13.8544947609; // 13.8544947609
const unsigned long unlockRadius = 50; // meters
const int lockAngle = -10;
const int unlockAngle = 20;
// --- Flags + Timing
bool ignoreButton = false;
bool signalFound = false;
unsigned long signalStartTime = 0;
unsigned long lastAnimTime = 0;
int animFrame = 0;
// ---------- Centering Helper ----------
String centerText(String text, int lineLength) {
if (text.length() > lineLength) return text.substring(0, lineLength);
int spaces = (lineLength - text.length()) / 2;
String padding = "";
for (int i = 0; i < spaces; i++) padding += " ";
return padding + text;
}
// ---------- Distance Formatter ----------
String formatDistance(unsigned long dist) {
if (dist < 10) return " " + String(dist) + " m";
else if (dist < 100) return " " + String(dist) + " m";
else if (dist < 1000) return " " + String(dist) + " m";
else if (dist < 10000) return String(dist) + " m";
else if (dist < 100000) {
String s = String(dist);
return s.substring(0, 2) + "." + s.substring(2, 3) + "km";
} else if (dist < 1000000) {
return String(dist / 1000) + " km";
} else if (dist < 10000000) {
return String(dist / 1000) + "km";
} else {
return "9999km";
}
}
// ---------- Display Functions ----------
void displayWelcome() {
lcd.clear();
lcd.setCursor(0, 0);
lcd.print(centerText("Welcome!", lcdWidth));
lcd.setCursor(0, 1);
lcd.print(centerText("Click To Locate.", lcdWidth));
}
void displayAccessGranted() {
lcd.clear();
lcd.setCursor(0, 0);
lcd.print(centerText("Access Granted!", lcdWidth));
String frames[] = {
"3", "3.", "3..", "3...",
"3... 2", "3... 2.", "3... 2..", "3... 2...",
"3... 2... 1", "3... 2... 1.", "3... 2... 1..", "3... 2... 1..."
};
for (int i = 0; i < 12; i++) {
lcd.setCursor(0, 1);
lcd.print(frames[i]);
delay(250);
}
delay(500);
}
void displayOpening() {
lcd.clear();
lcd.setCursor(0, 0);
lcd.print(centerText("Box Unlocked.", lcdWidth));
lcd.setCursor(0, 1);
lcd.print(centerText("Quick!", lcdWidth));
}
void displayDenied(unsigned long dist) {
lcd.clear();
lcd.setCursor(0, 0);
lcd.print(centerText("Access Denied!", lcdWidth));
lcd.setCursor(0, 1);
lcd.print(centerText("Distance: " + formatDistance(dist), lcdWidth));
}
void displayCongrats() {
lcd.clear();
lcd.setCursor(0, 0);
lcd.print(centerText("Congratulations!", lcdWidth));
lcd.setCursor(0, 1);
lcd.print(centerText("Have a Nice Day.", lcdWidth));
}
// ---------- Buzzing ----------
void buzz(int seconds) {
unsigned long endT = millis() + seconds * 1000UL;
while (millis() < endT) {
tone(buzzerPin, 880); delay(100);
tone(buzzerPin, 988); delay(100);
}
noTone(buzzerPin);
}
void openLock() {
displayOpening();
lockServo.attach(servoPin); // Enable servo
lockServo.write(unlockAngle); // Unlock
delay(500);
lockServo.detach(); // Stop signal
buzz(3); // Play unlocking sound
lockServo.attach(servoPin); // Enable servo
lockServo.write(lockAngle); // Lock
delay(500);
lockServo.detach(); // Stop signal
}
// ---------- Haversine Distance ----------
double haversine(double lat1, double lon1, double lat2, double lon2) {
const double R = 6371000;
double dLat = radians(lat2 - lat1);
double dLon = radians(lon2 - lon1);
double a = sin(dLat / 2) * sin(dLat / 2) +
cos(radians(lat1)) * cos(radians(lat2)) *
sin(dLon / 2) * sin(dLon / 2);
double c = 2 * atan2(sqrt(a), sqrt(1 - a));
return R * c;
}
// ---------- Unlock Logic ----------
bool checkUnlockCondition(unsigned long &distanceOut) {
signalFound = false;
signalStartTime = millis();
lastAnimTime = millis();
animFrame = 0;
lcd.clear();
lcd.setCursor(0, 0);
lcd.print(centerText("No Signal Found.", lcdWidth));
lcd.setCursor(0, 1);
lcd.print(centerText("Searching...", lcdWidth));
String frames[] = { "Searching. ", "Searching.. ", "Searching..." };
// Wait for valid GPS fix
const int sampleCount = 5;
double latSum = 0;
double lonSum = 0;
int collected = 0;
while (collected < sampleCount) {
// Animate
unsigned long now = millis();
if (now - lastAnimTime >= 250) {
lcd.setCursor(0, 1);
lcd.print(centerText(frames[animFrame % 3], lcdWidth));
animFrame++;
lastAnimTime = now;
}
// Feed GPS from serial
while (gpsSerial.available() > 0) {
gps.encode(gpsSerial.read());
}
// Try to read a valid location
if (gps.location.isUpdated() && gps.location.isValid()) {
latSum += gps.location.lat();
lonSum += gps.location.lng();
collected++;
}
}
// Average
double avgLat = latSum / sampleCount;
double avgLon = lonSum / sampleCount;
gpsSerial.end(); // we're done using GPS for now
distanceOut = haversine(avgLat, avgLon, targetLat, targetLon);
return (distanceOut <= unlockRadius);
}
// ---------- Setup ----------
void setup() {
Serial.begin(9600);
lcd.init();
lcd.noBacklight(); // Leave on -> lcd.backlight(); Leave off -> lcd.noBacklight(); // save power
pinMode(buttonPin, INPUT_PULLUP);
pinMode(buzzerPin, OUTPUT);
digitalWrite(buzzerPin, LOW);
displayWelcome();
lockServo.attach(servoPin); // Enable servo
lockServo.write(lockAngle); // Lock
delay(500);
lockServo.detach(); // Stop signal
gpsSerial.begin(9600);
}
// ---------- Loop ----------
void loop() {
// Always feed GPS
while (gpsSerial.available()) {
gps.encode(gpsSerial.read());
}
if (!ignoreButton && digitalRead(buttonPin) == LOW) {
ignoreButton = true;
unsigned long distance = 0;
bool unlocked = checkUnlockCondition(distance);
if (unlocked) {
displayAccessGranted();
openLock();
displayCongrats();
} else {
displayDenied(distance);
}
delay(5000);
displayWelcome();
ignoreButton = false;
gpsSerial.begin(9600);
}
}
The result turned out to be pretty good!
I placed the destination far away on purpose, so reaching the box takes time, patience, and care. When you arrive at the lighthouse, there’s a quiet moment waiting, and something inside that might change everything.
List of components
These are all the parts I used to build the final box, including electronics, connectors, and modules.
Component | Note / Link |
---|---|
9V Battery Holder | BH-9VA |
Dupont Wires | all genders, all positions |
LCD Display 16×2 (I²C) | yellow-green backlight |
GPS Module | NEO-6M |
Servo Motor | SG-90 (9 g) |
Push Button | PBS-33B-G |
Passive Buzzer | simple melody output |
Breadboard | ZY-170-W |
Arduino Nano | clone with ATmega328P |
Step-Down Converter | XL4015 CC/CV |
And here is one more picture of the finished box:
