"Where did my pet go?" If you have a pet, you've probably asked this question at least once. One moment they're playing happily in front of you, and the next... they're gone! Maybe they saw a butterfly, started chasing a bird, or simply decided to go on a little adventure without telling you.
Now imagine knowing the moment your pet walked outside a safe area. Instead of worrying and searching from place to place, you could be informed immediately and know exactly when something needs your attention.
In this article, we'll walkthrough that how to build a simple smart pet tracking system that helps monitor a pet's movement and understand how the complete hardware and software work together, step by step. We have also done many interesting projects related to the field of IoT Projects. Spare some time and look at those also to get more ideas. If you are new to Circuit Digest, I suggest you look through the Geolinker wikipage to know the full details about the Geolinker.
How the Pet Tracking System Using IoT Works
The Pet Tracking System continuously monitors the pet's location and performs different actions based on its position. The complete operation of the system can be divided into the following three stages.
System Initialisation and Location Tracking
When the device is powered on, the GeoLinker board initialises the GPS, GSM, GPRS, e-paper display, and cloud services. It also restores the previously saved geofence status from memory to avoid generating false alerts after a restart. Once the initialization is complete, the GPS module starts acquiring the pet's location by obtaining the latitude, longitude, speed, and other GPS information. The current coordinates are then continuously compared with all the predefined geofence zones, such as Home, Garden, Park, and Restricted Area, to check whether the pet has entered or exited a particular zone.
Alert Generation and Safety Features
Whenever the pet enters or leaves a normal geofence, the system immediately sends an SMS to the registered owner containing the current location, speed, battery percentage, and a Google Maps link. If the pet enters a restricted area, the system not only sends the SMS alert but also places a voice call to ensure the alert is noticed immediately. In addition, if the GPS signal is unavailable for multiple consecutive attempts, a GPS-loss alert is automatically sent.
The owner can also request the pet's latest location at any time by sending the "LOCATION" SMS command, to which the system automatically replies with the latest available coordinates and a Google Maps link. Meanwhile, the e-paper display normally shows the pet's name and changes to "My Pet is Lost" along with the owner's phone number whenever the pet enters a restricted area. Since the display retains its contents even without power, the contact information remains visible even if the battery is completely drained. Also, the E-paper only requires a small amount of power to operate, which becomes one of the advantages of the project.
Cloud Monitoring and Continuous Operation
After processing alerts, the system continues monitoring the pet and updates the cloud automatically. This includes the following
Key operations:
- Periodically uploads the pet's GPS location, battery percentage, and signal strength to the CircuitDigest Cloud.
- Allows the owner to monitor the pet remotely through the cloud dashboard using the live map
- Continuously checks for GPS updates, geofence events, incoming SMS commands, and display status.
- Refreshes the e-paper display only when the status changes, improving performance and extending display life.
- Repeats all these operations continuously inside the main program loop for real-time monitoring and reliable system performance.
If you'd like to see another practical implementation, you can explore our IoT Parcel Tracking System tutorial.
Live Demonstration
Components Required for an IoT-Based Pet Tracking System
The components below are essential ones that are used to make the IoT-based pet tracking system.
![]()
| S.No | Components | Purpose | Alternatives |
| 1. | Geolinker Board | GeoLinker GL868_ESP32 is a production-ready, open-source development board that combines an ESP32-S3 and SIM868 GSM modem, used for tracking purposes. | - |
| 2. | IoT / Normal Nano SIM Card | Airtel M2M IoT SIM recommended (3 months free data with board purchase). Any 2G-compatible SIM works. | Regular 2G SIM |
| 3. | 3.7V Li-Ion Battery | Used for powering the board | LiPo pouch battery |
| 4. | USB-C Cable | Programming, testing & charging | - |
| 5. | E-Paper Display | For displaying different messages and retain even when the power is out | - |
Circuit Diagram of Pet Tracker
The diagram on the screen shows all the connections between the GeoLinker board and the e-paper display. One of the biggest advantages of using the GL868 GeoLinker is that it already integrates the ESP32 microcontroller along with the GPS, GSM, and GPRS modules on a single board. This eliminates the need to interface multiple separate modules, making the hardware much more compact. To power the entire system, a 3.7V lithium-ion battery is connected directly to the GeoLinker board, providing portable operation for the smart pet collar.
Hardware Connection for the Real-time Pet Tracking System
The image below shows how the E-paper display and battery are connected to the GeoLinker board and arranged inside the enclosure. This compact layout allows all the components to fit comfortably within the 3D-printed collar enclosure.
![]()
3D Design Files
The enclosure was designed in Autodesk Fusion 360 to create a compact and durable housing for the Pet tracking system.

The design consists of two main parts: a base, which accommodates the GeoLinker board, 3.7V lithium battery, and internal wiring, and a lid with a precisely sized cutout for the E-paper display, ensuring the screen remains visible while protecting the electronics. Collar slots are integrated into both sides of the enclosure, allowing it to be securely mounted onto a standard pet collar.

After completing the CAD model in Fusion 360, the enclosure was 3D printed, resulting in a lightweight, well-fitted case that keeps all the components securely in place while allowing easy assembly.

Code Explanation
This program continuously acquires GPS data, monitors multiple geofence zones, and manages communication through GSM and GPRS services. It processes location updates, detects zone entry and exit, generates alerts, handles incoming SMS commands, updates the e-paper display based on the current status, and periodically uploads data to the cloud.
#define DEVICE_ID "vedha"
#define API_KEY "xxxxxxxx"
#define ALERT_NUMBER "+91XXXXXXXXXX"
#define PET_NAME “Toby"
void setup() {
Serial.begin(115200);
loadZoneStates();
GeoLinker.begin(DEVICE_ID, API_KEY);
GeoLinker.gpsOn();
epdInit();
}This section initializes the entire Pet tracker system. It configures the device credentials, owner number, and pet name before starting the GeoLinker module, GPS, and e-paper display. Previously stored geofence states are also restored from flash memory to ensure the system continues from its last known state instead of starting from scratch.
bool hasLoc = getValidLocation(&gps);
if (hasLoc) {
checkGeofences(&gps, hasLoc);
}
double dist = haversineDistance(
gps->latitude,
gps->longitude,
zones[i].latitude,
zones[i].longitude
);The program continuously acquires the pet's GPS coordinates and verifies whether the location is valid. It then calculates the distance between the current position and every predefined geofence. Based on this distance, the system determines whether the pet has entered or exited a zone and updates the geofence status accordingly
static void sendGeofenceAlert(const char *zoneName, bool entered,
bool restricted, GPSData *gps, bool hasLoc) {
char msg[256];
buildAlertMessage(msg, sizeof(msg), entered ? "ENTERED" : "LEFT",
zoneName, gps, hasLoc);
sendReliableSMS(ALERT_NUMBER, msg);
if (restricted && !callInProgress && !pendingCall) {
pendingCall = true;
pendingCallTimer = millis();
}
}This function is responsible for notifying the owner whenever the pet enters or exits a geofence. It first creates an alert message containing the pet's location, speed, battery percentage, and Google Maps link, then sends it by SMS. If the zone is marked as restricted, the system also schedules a voice call after a short delay, ensuring that critical alerts are not missed.
static void epdUpdateIfNeeded() {
if (petIsLost == displayShowsLost)
return;
if (petIsLost)
epdShowLost();
else
epdShowHealthy();
displayShowsLost = petIsLost;
}This function updates the e-paper display only when the pet's status changes, preventing unnecessary screen refreshes. Under normal conditions, the display shows the pet's name and a healthy status message. If the pet enters any restricted zone, the display immediately switches to a "My Pet is Lost" screen with the owner's phone number, allowing anyone who finds the pet to contact the owner easily.
void loop() {
GeoLinker.update();
checkIncomingSMS();
bool hasLoc = getValidLocation(&gps);
checkGeofences(&gps, hasLoc);
if (!pendingCall && !callInProgress &&
millis() - lastCloudPush >= CLOUD_SEND_INTERVAL * 1000UL) {
lastCloudPush = millis();
cloudPush(&gps, hasLoc);
}
}The loop() function continuously executes to monitor the pet in real time. It updates the GeoLinker module, processes incoming "LOCATION" SMS requests, acquires the latest GPS coordinates, and checks all geofence zones for entry or exit events. Finally, at regular intervals, it uploads the pet's GPS location, battery percentage, and signal strength to the CircuitDigest Cloud, enabling live tracking and remote monitoring through the cloud dashboard.
Output Demonstration of the Pet Tracking System
The image below shows the live tracking interface of the Pet tracking system. It continuously monitors the pet's real-time location on the map and detects whenever the pet enters or exits the predefined geofenced area, enabling instant alerts to the owner.

The image below shows the dog, Toby, used for the project demonstration. It also illustrates the information displayed on the E-paper screen under different conditions. During normal operation, the display shows the owner's contact details, while upon crossing the predefined geofenced area, the display updates with an alert message indicating that the pet has entered a restricted zone, helping anyone who finds the pet understand the situation and contact the owner if necessary.
![]()
The image below shows the SMS and call alerts generated by the Pet tracking system. An SMS notification is sent to the owner whenever Toby exits the predefined safe zone, providing details of the event along with his current location. If Toby enters a restricted area, the system not only sends an SMS but also automatically places a phone call to the owner's mobile number. The call alert is specifically reserved for critical events to ensure that important notifications are not overlooked, even if the SMS is missed or the phone is not actively being monitored.

Troubleshooting
| Problems | Cause | Solutions |
| GSM module fails to connect to the network | SIM card is not inserted correctly, network coverage is weak, or SIM services are inactive. | Check SIM card placement, verify network availability, and ensure the SIM card has an active plan with sufficient balance. |
| Voice call alerts are not working | Network issues, insufficient SIM balance, or incorrect call configuration. | Confirm GSM connectivity, ensure calling services are enabled, and verify emergency contact settings. |
| E-paper display remains blank | Display initialisation failure, loose wiring, or power supply issues. | Verify all SPI connections, check display power requirements, and ensure the display initialisation code executes correctly. |
| Battery drains faster than expected | Continuous GPS tracking, frequent GSM communication, or lack of power-saving features. | Increase update intervals, optimise code efficiency, and enable low-power modes when possible. |
| The system freezes during operation | Memory overflow, software bugs, or communication conflicts between modules. | Debug the code, monitor memory usage, and test each module independently to identify the issue. |
| Whitelisted number not receiving alerts | The phone number is not added correctly, is stored in an incorrect format, or the whitelist configuration has not been updated in the system. | Verify that the number is entered correctly with the appropriate country code, check the whitelist settings in the firmware, and restart the device after updating the configuration. |
Conclusion
The Pet tracking system demonstrates how readily available IoT technologies can be combined to develop a practical smart pet safety solution. While the prototype showcases features such as geofencing, GPS tracking, GSM-based notifications, and an E-paper display, it also has certain limitations. The system relies on GPS and GSM network availability, so its performance may be affected in areas with poor signal coverage or limited satellite visibility. Additionally, battery life depends on the tracking interval and communication frequency, requiring periodic recharging for continuous operation.
Several enhancements can further improve the system. Future versions could integrate a low-power GNSS module for improved positioning accuracy, Wi-Fi or Bluetooth for indoor tracking, an IMU for activity and collar-removal detection, and health-monitoring sensors such as body temperature or heart-rate sensors. You can also explore other GeoLinker-based projects, such as the ESP32 Interactive Voice Response System using GeoLinker and the ESP32 GSM Calling Device using GeoLinker, to understand how voice communication and GSM functionalities can be further integrated into IoT applications and extended to this project.
DIY IoT Pet Tracking GitHub
The complete source code and project resources for this DIY IoT Pet Tracking System are available on GitHub. You can clone the repository, customise the code, and build your own GPS-enabled pet tracker using the step-by-step instructions provided.
Frequently Asked Questions
⇥ Can multiple safe zones be configured?
Yes. The system can be programmed to monitor multiple geofenced areas depending on the application requirements.
⇥ What are the possible future improvements?
Future enhancements may include mobile app integration, cloud connectivity, AI-based analytics, health monitoring sensors, longer battery life, and real-time dashboard visualization.
⇥ What happens when the device moves outside the safe zone?
The system detects the boundary violation and automatically sends an alert message and location details to the registered contact number.
⇥ Why is an e-paper display used instead of a regular display?
E-paper displays consume very little power and remain visible even in bright sunlight, making them ideal for portable and battery-powered applications.
⇥ What is geofencing?
Geofencing is a virtual boundary created around a specific location. When the tracked object enters or exits this area, the system automatically generates alerts and notifications.
⇥ How are alerts sent to users?
Alerts are sent through a GSM module using SMS messages and voice calls, ensuring notifications can be received even without internet access.
Related GPS and IoT Tracking Projects
Explore our collection of GPS tracking projects built using popular development platforms such as Arduino, ESP32, and Raspberry Pi Pico. These tutorials demonstrate how to design real-time GPS trackers with GSM connectivity, making them ideal for learning location tracking, IoT communication, and embedded system development.
Arduino Location Tracker using SIM800L GSM Module and NEO-6M GPS Module
This comprehensive project shows you how to create a fully functional GPS tracking system using Arduino UNO R3, SIM800L GSM module, and NEO-6M GPS module, a perfect low-cost DIY combination for vehicle monitoring, asset protection, or personal safety applications.
Build a Smart ESP32 GPS Tracker with SIM800L and NEO6M Module
This ESP32 GSM GPS system will work anywhere there is cellular coverage, because it does not rely on Wi-Fi. The NEO 6M GPS module with ESP32, in conjunction with the ESP32, will provide reliable and accurate position data via satellites within view of the receiver, while the SIM800L will transmit information in real-time over the mobile data network via GPRS.
Build a Real-Time GPS Tracker with Raspberry Pi Pico & SIM800L GSM Module
This comprehensive Raspberry Pi Pico GPS tracker tutorial demonstrates how to create a professional-grade IoT tracking device using the SIM800L GSM module, Neo 6M GPS module with Raspberry Pi, and the versatile Raspberry Pi Pico microcontroller.
Complete Project Code
#include <GL868_ESP32.h>
#include <math.h>
#include <Preferences.h>
#include <SPI.h>
#include <GxEPD2_BW.h>
#include <Fonts/FreeMonoBold9pt7b.h>
#include <Fonts/FreeMonoBold12pt7b.h>
#include <Fonts/FreeMonoBold18pt7b.h>
// ============================================================================
// Configuration
// ============================================================================
#define DEVICE_ID "Device_name"
#define API_KEY "caretakers_api_key"
#define ALERT_NUMBER "caretaker_number" // primary caretaker
#define PET_NAME "Toby" // shown on display
#define GPS_TIMEOUT 30000UL
#define GPS_MAX_RETRIES 3
#define GPS_POLL_INTERVAL 5000UL
#define CLOUD_SEND_INTERVAL 30 // seconds
#define GPS_LOSS_ALERT_POLLS 3
#define CALL_RING_DURATION 15000UL
#define CALL_PENDING_DELAY 3000UL
#define TIME_OFFSET_HOURS 5
#define TIME_OFFSET_MINS 30
#define HYSTERESIS_METERS 20.0
#define CIPSHUT_SETTLE_MS 2000UL
#ifndef DEG_TO_RAD
static constexpr double DEG_TO_RAD = M_PI / 180.0;
#endif
// ============================================================================
// E-Paper display — pins (TODO: confirm these don't collide with anything
// else you're wiring onto the same board for this build)
// ============================================================================
#define EPD_SCK 12
#define EPD_MOSI 11
#define EPD_CS 10
#define EPD_DC 13
#define EPD_RST 21
#define EPD_BUSY 48
// ASSUMPTION: Waveshare 1.54" 200x200 (GDEH0154D67 controller). Change this
// one line if your panel is different.
GxEPD2_BW<GxEPD2_154_D67, GxEPD2_154_D67::HEIGHT> display(
GxEPD2_154_D67(EPD_CS, EPD_DC, EPD_RST, EPD_BUSY));
// Small 16x16 monochrome paw-print icon (best-effort stand-in for the pet
// emoji — standard GFX fonts can't render emoji/Unicode glyphs, so this is
// a hand-drawn bitmap instead). Purely cosmetic — delete the drawBitmap()
// call in epdShowHealthy() if you'd rather skip it.
static const unsigned char PAW_BITMAP[32] PROGMEM = {
0x00,0x00, 0x06,0x60, 0x0F,0xF0, 0x0F,0xF0,
0x06,0x60, 0x00,0x00, 0x19,0x98, 0x3F,0xFC,
0x3F,0xFC, 0x3F,0xFC, 0x1F,0xF8, 0x0F,0xF0,
0x07,0xE0, 0x00,0x00, 0x00,0x00, 0x00,0x00
};
// ============================================================================
// Geofence zones
// restricted = false → SMS only on enter/exit
// restricted = true → SMS + voice call on enter/exit
//
// TODO: "Road" from the original spec still isn't in here — no coordinates
// given for it yet. Add it as one more line below whenever you have the
// lat/lon, following the same pattern.
// ============================================================================
struct GeofenceZone {
const char *name;
double latitude;
double longitude;
double radiusMeters;
bool restricted;
bool insideNow; // runtime — persisted to NVS on every change
};
GeofenceZone zones[] = {
{ "Home", 11.024934042701, 77.010585083681, 100.0, false, false },
{ "park", 11.019119891223, 77.006775393646, 150.0, false, false },
{ "Garden", 11.010967, 77.012959, 80.0, false, false },
{ "Restricted Area", 11.011156458988, 77.017023144466, 50.0, true, false },
};
constexpr int ZONE_COUNT = sizeof(zones) / sizeof(zones[0]);
static_assert(ZONE_COUNT > 0, "zones[] must not be empty");
// ============================================================================
// Runtime state
// ============================================================================
static Preferences prefs;
static uint32_t lastPollTime = 0;
static uint32_t lastCloudPush = 0;
static int gpsFailStreak = 0;
static bool gpsLossAlertSent = false;
static bool gprsAttached = false;
static bool callInProgress = false;
static uint32_t callStartTime = 0;
static bool pendingCall = false;
static uint32_t pendingCallTimer = 0;
// [F1] After cold boot, first fix silently syncs insideNow without alerting.
static bool firstFixDone = false;
// Last known fix — used to answer "LOCATION" SMS instantly without forcing
// a fresh GPS acquisition (which can take up to GPS_TIMEOUT*GPS_MAX_RETRIES).
static GPSData lastGoodGPS = {};
static bool haveAnyFix = false;
// Display state — only redraw the e-paper when this actually changes,
// since e-paper refreshes are slow (~1-2s) and have a finite refresh-cycle
// lifetime; no point re-flashing the same screen every poll.
static bool petIsLost = false;
static bool displayShowsLost = false; // what's currently ON SCREEN
// ============================================================================
// [F1] Zone state persistence — RESTORE from flash, never clear on boot
// ============================================================================
static void saveZoneStates() {
prefs.begin("gfence", false);
for (int i = 0; i < ZONE_COUNT; i++) {
char key[8];
snprintf(key, sizeof(key), "z%d", i);
prefs.putBool(key, zones[i].insideNow);
}
prefs.end();
}
static void loadZoneStates() {
prefs.begin("gfence", true); // read-only open
for (int i = 0; i < ZONE_COUNT; i++) {
char key[8];
snprintf(key, sizeof(key), "z%d", i);
zones[i].insideNow = prefs.getBool(key, false);
}
prefs.end();
Serial.println("[INIT] Zone states loaded from NVS:");
for (int i = 0; i < ZONE_COUNT; i++) {
Serial.printf(" %s -> %s\n",
zones[i].name,
zones[i].insideNow ? "INSIDE (remembered)" : "outside");
}
}
// ============================================================================
// GPRS helpers
// ============================================================================
static void gprsDetach() {
if (!gprsAttached) return;
Serial.println("[GPRS] Detaching...");
GeoLinker.sendATCommand("+CIPSHUT", 5000);
gprsAttached = false;
delay(CIPSHUT_SETTLE_MS);
Serial.println("[GPRS] Detached.");
}
static void gprsReattach() {
if (gprsAttached) return;
Serial.println("[GPRS] Reattaching...");
if (GeoLinker.gsm.attachGPRS()) {
gprsAttached = true;
Serial.println("[GPRS] Attached.");
} else {
Serial.println("[GPRS] Failed — will retry next push.");
}
}
// ============================================================================
// Raw SMS (direct AT commands, CRLF line endings)
// ============================================================================
static bool sendRawSMS(const char *number, const char *message) {
HardwareSerial &mdm = GeoLinker.getModemSerial();
GeoLinker.sendATCommand("+CMGF=1", 2000);
GeoLinker.sendATCommand("+CSCS=\"GSM\"", 2000);
delay(200);
mdm.printf("AT+CMGS=\"%s\"\r", number);
Serial.printf("[SMS] AT+CMGS -> %s\n", number);
uint32_t t = millis();
bool gotPrompt = false;
while (millis() - t < 8000) {
if (mdm.available()) {
if (mdm.read() == '>') { gotPrompt = true; break; }
}
delay(10);
}
if (!gotPrompt) {
Serial.println("[SMS] No '>' prompt — aborting.");
mdm.write(0x1B); // ESC
return false;
}
for (const char *p = message; *p; p++) {
if (*p == '\n' && (p == message || *(p - 1) != '\r')) mdm.write('\r');
mdm.write(*p);
}
mdm.write(0x1A); // Ctrl+Z to send
t = millis();
while (millis() - t < 30000) {
if (mdm.available()) {
String line = mdm.readStringUntil('\n');
line.trim();
if (line.startsWith("+CMGS:")) {
Serial.println("[SMS] Delivered.");
return true;
}
if (line.indexOf("ERROR") >= 0) {
Serial.printf("[SMS] Error: %s\n", line.c_str());
return false;
}
}
delay(10);
}
Serial.println("[SMS] Timeout waiting for +CMGS.");
return false;
}
static bool sendReliableSMS(const char *number, const char *message) {
gprsDetach();
bool ok = sendRawSMS(number, message);
gprsReattach();
return ok;
}
// ============================================================================
// Raw voice call
// ============================================================================
static bool makeRawCall(const char *number) {
HardwareSerial &mdm = GeoLinker.getModemSerial();
while (mdm.available()) mdm.read(); // flush
mdm.printf("ATD%s;\r", number);
Serial.printf("[CALL] ATD -> %s\n", number);
uint32_t t = millis();
while (millis() - t < 5000) {
if (mdm.available()) {
String line = mdm.readStringUntil('\n');
line.trim();
if (line == "OK") { Serial.println("[CALL] Ringing."); return true; }
if (line.indexOf("ERROR") >= 0 || line.indexOf("NO CARRIER") >= 0) {
Serial.printf("[CALL] Rejected: %s\n", line.c_str());
return false;
}
}
delay(20);
}
Serial.println("[CALL] No explicit OK — assuming ringing.");
return true;
}
// ============================================================================
// Non-blocking call ticker
// ============================================================================
static void tickCall() {
if (!callInProgress) return;
GeoLinker.update();
if (millis() - callStartTime >= CALL_RING_DURATION) {
GeoLinker.sendATCommand("H", 2000);
Serial.println("[CALL] Ring timeout — hung up.");
callInProgress = false;
delay(500);
gprsReattach();
}
}
// ============================================================================
// Haversine distance (metres)
// ============================================================================
static double haversineDistance(double lat1, double lon1,
double lat2, double lon2) {
const double R = 6371000.0;
double dLat = (lat2 - lat1) * DEG_TO_RAD;
double dLon = (lon2 - lon1) * DEG_TO_RAD;
double a = sin(dLat / 2) * sin(dLat / 2) +
cos(lat1 * DEG_TO_RAD) * cos(lat2 * DEG_TO_RAD) *
sin(dLon / 2) * sin(dLon / 2);
return R * 2.0 * atan2(sqrt(a), sqrt(1.0 - a));
}
static void sanitiseForSMS(char *dst, int dstLen, const char *src) {
int j = 0;
for (int i = 0; src[i] && j < dstLen - 1; i++) {
char c = src[i];
if (c == '\r') continue;
if (c == '"') { dst[j++] = '\''; continue; }
dst[j++] = c;
}
dst[j] = '\0';
}
// ============================================================================
// SMS body builders
// ============================================================================
static void buildAlertMessage(char *buf, int bufLen,
const char *verb, const char *zoneName,
GPSData *gps, bool hasLoc) {
char safe[64];
sanitiseForSMS(safe, sizeof(safe), zoneName);
int bat = GeoLinker.getBatteryPercent();
if (hasLoc) {
snprintf(buf, bufLen,
"%s %s %s\n%.6f,%.6f\nSpd:%.1f Bat:%d%%\nmaps.google.com/?q=%.6f,%.6f",
PET_NAME, verb, safe,
gps->latitude, gps->longitude,
gps->speed, bat,
gps->latitude, gps->longitude);
} else {
snprintf(buf, bufLen,
"%s %s %s\nNo GPS fix\nBat:%d%%",
PET_NAME, verb, safe, bat);
}
}
static void buildLocationReplyMessage(char *buf, int bufLen) {
int bat = GeoLinker.getBatteryPercent();
if (haveAnyFix) {
snprintf(buf, bufLen,
"%s's location:\n%.6f,%.6f\nBat:%d%%\nmaps.google.com/?q=%.6f,%.6f",
PET_NAME,
lastGoodGPS.latitude, lastGoodGPS.longitude, bat,
lastGoodGPS.latitude, lastGoodGPS.longitude);
} else {
snprintf(buf, bufLen,
"%s: no GPS fix yet.\nBat:%d%%", PET_NAME, bat);
}
}
// ============================================================================
// Geofence alert dispatcher
// ============================================================================
static void sendGeofenceAlert(const char *zoneName, bool entered,
bool restricted, GPSData *gps, bool hasLoc) {
const char *verb = entered ? "ENTERED" : "LEFT";
Serial.printf("[ALERT] %s %s restricted:%s\n",
verb, zoneName, restricted ? "YES" : "NO");
char msg[256];
buildAlertMessage(msg, sizeof(msg), verb, zoneName, gps, hasLoc);
sendReliableSMS(ALERT_NUMBER, msg);
if (restricted && !callInProgress && !pendingCall) {
Serial.println("[ALERT] Scheduling voice call in 3 s...");
pendingCall = true;
pendingCallTimer = millis();
}
}
// ============================================================================
// GPS-loss alert
// ============================================================================
static void sendGPSLossAlert() {
char msg[160];
snprintf(msg, sizeof(msg),
"%s: GPS lost!\nFailed:%d polls\nBat:%d%%",
PET_NAME, gpsFailStreak, GeoLinker.getBatteryPercent());
Serial.println("[GPS] Sending GPS-loss SMS...");
sendReliableSMS(ALERT_NUMBER, msg);
}
// ============================================================================
// GPS acquisition
// ============================================================================
static bool getValidLocation(GPSData *gps) {
Serial.println("[GPS] Acquiring fix...");
for (int retry = 0; retry < GPS_MAX_RETRIES; retry++) {
Serial.printf("[GPS] Attempt %d/%d\n", retry + 1, GPS_MAX_RETRIES);
uint32_t start = millis();
while (millis() - start < GPS_TIMEOUT) {
GeoLinker.update();
if (GeoLinker.getLocationNow(gps) && gps->valid) {
Serial.printf("[GPS] Fix: %.6f, %.6f Sats:%d\n",
gps->latitude, gps->longitude, gps->satellites);
return true;
}
uint32_t sub = millis();
while (millis() - sub < 1000) { GeoLinker.update(); delay(25); }
}
Serial.println("[GPS] Timed out, retrying...");
}
Serial.println("[GPS] All retries failed.");
return false;
}
// ============================================================================
// E-Paper display
// ============================================================================
static void epdInit() {
SPI.begin(EPD_SCK, -1 /* MISO unused */, EPD_MOSI, EPD_CS);
display.init(115200);
display.setRotation(1);
}
// Centers a line of text horizontally for the current font.
static void epdPrintCentered(const char *text, int16_t y) {
int16_t x1, y1; uint16_t w, h;
display.getTextBounds(text, 0, y, &x1, &y1, &w, &h);
int16_t x = (display.width() - w) / 2;
display.setCursor(x, y);
display.print(text);
}
static void epdShowHealthy() {
display.setFullWindow();
display.firstPage();
do {
display.fillScreen(GxEPD_WHITE);
display.setTextColor(GxEPD_BLACK);
display.setFont(&FreeMonoBold18pt7b);
epdPrintCentered(PET_NAME, 70);
display.setFont(&FreeMonoBold12pt7b);
epdPrintCentered("Hello", 110);
// Small paw icon under the text — cosmetic, see PAW_BITMAP comment above.
int16_t bx = (display.width() - 16) / 2;
display.drawBitmap(bx, 125, PAW_BITMAP, 16, 16, GxEPD_BLACK);
} while (display.nextPage());
}
static void epdShowLost() {
display.setFullWindow();
display.firstPage();
do {
display.fillScreen(GxEPD_WHITE);
display.setTextColor(GxEPD_BLACK);
display.setFont(&FreeMonoBold12pt7b);
epdPrintCentered("My pet is lost!", 60);
epdPrintCentered("Please call:", 95);
display.setFont(&FreeMonoBold9pt7b);
epdPrintCentered(ALERT_NUMBER, 125);
} while (display.nextPage());
}
// Only actually redraws if the state changed since last time on screen —
// e-paper refreshes are slow and have a finite cycle lifetime, no reason
// to repaint the same screen on every single geofence poll.
static void epdUpdateIfNeeded() {
if (petIsLost == displayShowsLost) return;
Serial.printf("[EPD] Updating display -> %s\n", petIsLost ? "LOST" : "HEALTHY");
if (petIsLost) epdShowLost();
else epdShowHealthy();
displayShowsLost = petIsLost;
}
// ============================================================================
// [F1] Geofence checker
// ============================================================================
static void checkGeofences(GPSData *gps, bool hasLoc) {
if (!hasLoc) { Serial.println("[FENCE] No fix — skipping."); return; }
bool stateChanged = false;
bool anyRestrictedIn = false;
for (int i = 0; i < ZONE_COUNT; i++) {
double dist = haversineDistance(gps->latitude, gps->longitude,
zones[i].latitude, zones[i].longitude);
double enterThresh = zones[i].radiusMeters;
double exitThresh = zones[i].radiusMeters + HYSTERESIS_METERS;
bool nowInside = zones[i].insideNow ? (dist <= exitThresh)
: (dist <= enterThresh);
Serial.printf("[FENCE] %-10s | %7.1f m | enter:%.0f exit:%.0f | %-7s | %s\n",
zones[i].name, dist, enterThresh, exitThresh,
nowInside ? "INSIDE" : "outside",
zones[i].restricted ? "RESTRICTED" : "normal");
if (nowInside != zones[i].insideNow) {
zones[i].insideNow = nowInside;
stateChanged = true;
if (!firstFixDone) {
Serial.printf("[FENCE] (silent sync) %s -> %s\n",
zones[i].name, nowInside ? "inside" : "outside");
} else {
sendGeofenceAlert(zones[i].name, nowInside,
zones[i].restricted, gps, hasLoc);
}
}
if (zones[i].restricted && zones[i].insideNow) anyRestrictedIn = true;
}
if (stateChanged) saveZoneStates();
// "Lost" display state = currently inside ANY restricted zone.
petIsLost = anyRestrictedIn;
epdUpdateIfNeeded();
if (!firstFixDone) {
firstFixDone = true;
Serial.println("[FENCE] First-fix sync complete — alerts now armed.");
}
}
// ============================================================================
// Cloud push
// ============================================================================
static void cloudPush(GPSData *gps, bool hasLoc) {
if (!gprsAttached) { gprsReattach(); if (!gprsAttached) return; }
GeoLinker.json.clear();
const GPSData &src = hasLoc ? *gps : GeoLinker.getLastGPS();
GeoLinker.json.addDataPoint(src,
GeoLinker.getBatteryPercent(),
GeoLinker.getSignalStrength());
char payload[1024];
if (!GeoLinker.json.build(payload, sizeof(payload))) {
Serial.println("[CLOUD] JSON build failed."); return;
}
Serial.printf("[CLOUD] Push: %.6f, %.6f Bat:%d%%\n",
src.latitude, src.longitude, GeoLinker.getBatteryPercent());
int code = GeoLinker.gsm.httpPOST(
"http://www.circuitdigest.cloud/api/v1/geolinker",
API_KEY, "application/json", payload);
Serial.printf("[CLOUD] HTTP %d\n", code);
if (code == -1) {
Serial.println("[CLOUD] Failed — will reattach next push.");
gprsAttached = false;
}
}
// ============================================================================
// Caretaker "LOCATION" SMS command
//
// NEW vs the reference code, which never read incoming SMS. We enable
// AT+CNMI so new messages get pushed straight to the UART as a "+CMT:"
// line (sender number + metadata) followed by the message body on the
// next line. We watch for that, and if the body is the word "LOCATION"
// (any case), we reply to WHOEVER sent it with the pet's last known fix.
//
// NOTE: confirm this URC format against your actual SIM868 firmware —
// some firmware revisions wrap it slightly differently. If testing shows
// a mismatch, the fix is just adjusting parseSmsSender()/the body-read
// step below, not the overall approach.
// ============================================================================
static String parseSmsSender(const String &cmtLine) {
int firstQuote = cmtLine.indexOf('"');
if (firstQuote < 0) return "";
int secondQuote = cmtLine.indexOf('"', firstQuote + 1);
if (secondQuote < 0) return "";
return cmtLine.substring(firstQuote + 1, secondQuote);
}
static void checkIncomingSMS() {
HardwareSerial &mdm = GeoLinker.getModemSerial();
if (!mdm.available()) return;
String line = mdm.readStringUntil('\n');
line.trim();
if (line.indexOf("+CMT:") < 0) return;
String sender = parseSmsSender(line);
// Body arrives on the next line.
uint32_t t = millis();
String body;
while (millis() - t < 1000) {
if (mdm.available()) {
body = mdm.readStringUntil('\n');
body.trim();
if (body.length() > 0) break;
}
}
if (sender.length() == 0 || body.length() == 0) return;
Serial.printf("[SMS-IN] From %s: \"%s\"\n", sender.c_str(), body.c_str());
if (body.equalsIgnoreCase("LOCATION")) {
Serial.println("[SMS-IN] LOCATION command recognised — replying.");
char reply[200];
buildLocationReplyMessage(reply, sizeof(reply));
sendReliableSMS(sender.c_str(), reply);
}
}
// ============================================================================
// setup()
// ============================================================================
void setup() {
Serial.begin(115200);
delay(1000);
Serial.println("==============================================");
Serial.println(" PetGuard - Smart Pet Safety System");
Serial.println("==============================================");
loadZoneStates();
GeoLinker.setOperatingMode(MODE_SMS_CALL);
GeoLinker.setTimeOffset(TIME_OFFSET_HOURS, TIME_OFFSET_MINS);
GeoLinker.enableFullPowerOff(false);
GeoLinker.begin(DEVICE_ID, API_KEY);
Serial.println("[INIT] Waiting for modem IDLE...");
{
uint32_t t = millis();
while (GeoLinker.getState() != STATE_IDLE && millis() - t < 50000UL) {
GeoLinker.update(); delay(100);
}
Serial.printf("[INIT] Modem ready (%lu ms)\n", millis() - t);
}
// Enable SMS text mode + push new messages straight to UART as URCs,
// so checkIncomingSMS() in loop() can react to "LOCATION" requests.
GeoLinker.sendATCommand("+CMGF=1", 2000);
GeoLinker.sendATCommand("+CNMI=2,2,0,0,0", 2000);
Serial.println("[INIT] Attaching GPRS...");
if (GeoLinker.gsm.attachGPRS()) {
gprsAttached = true;
Serial.println("[INIT] GPRS attached.");
} else {
Serial.println("[INIT] GPRS failed — retrying on first push.");
}
GeoLinker.gpsOn();
Serial.println("[GPS] GPS powered on.");
epdInit();
epdShowHealthy(); // sensible default until the first fix arrives
displayShowsLost = false;
Serial.println("\nConfigured Zones:");
Serial.println(" # Name Lat Lon Radius Alert Inside?");
Serial.println(" -- ---------- ----------- ---------- -------- --------- -------");
for (int i = 0; i < ZONE_COUNT; i++) {
Serial.printf(" %d %-10s %.6f %.6f %.0f m %-9s %s\n",
i + 1, zones[i].name,
zones[i].latitude, zones[i].longitude,
zones[i].radiusMeters,
zones[i].restricted ? "SMS+CALL" : "SMS only",
zones[i].insideNow ? "YES (remembered)" : "no");
}
Serial.printf("\nPet name : %s\n", PET_NAME);
Serial.printf("Caretaker no. : %s\n", ALERT_NUMBER);
Serial.printf("Poll interval : %lu ms\n", GPS_POLL_INTERVAL);
Serial.printf("Cloud interval: %d s\n\n", CLOUD_SEND_INTERVAL);
Serial.println("Monitoring started.\n");
lastPollTime = millis();
lastCloudPush = millis();
}
// ============================================================================
// loop()
// ============================================================================
void loop() {
GeoLinker.update();
// Watch for an incoming "LOCATION" SMS on every tick — this is cheap to
// check and shouldn't wait for the next GPS poll interval.
checkIncomingSMS();
// Pending call state machine
if (pendingCall && (millis() - pendingCallTimer >= CALL_PENDING_DELAY)) {
if (!callInProgress) {
gprsDetach();
Serial.println("[CALL] Initiating voice call...");
if (makeRawCall(ALERT_NUMBER)) {
callInProgress = true;
callStartTime = millis();
} else {
Serial.println("[CALL] Dial failed — reattaching GPRS.");
gprsReattach();
}
pendingCall = false;
}
}
tickCall();
// GPS poll gate
if (millis() - lastPollTime < GPS_POLL_INTERVAL) return;
lastPollTime = millis();
if (callInProgress) {
Serial.println("[LOOP] Call in progress — skipping poll.");
return;
}
GPSData gps = {};
bool hasLoc = getValidLocation(&gps);
if (hasLoc) {
if (gpsFailStreak > 0)
Serial.printf("[GPS] Restored after %d failure(s).\n", gpsFailStreak);
gpsFailStreak = 0;
gpsLossAlertSent = false;
lastGoodGPS = gps;
haveAnyFix = true;
} else {
gpsFailStreak++;
Serial.printf("[GPS] Failure %d/%d\n", gpsFailStreak, GPS_LOSS_ALERT_POLLS);
if (gpsFailStreak >= GPS_LOSS_ALERT_POLLS && !gpsLossAlertSent) {
sendGPSLossAlert();
gpsLossAlertSent = true;
}
}
// STEP 1: geofence check (alerts + display update)
checkGeofences(&gps, hasLoc);
// STEP 2: cloud push (only if no alert/call activity)
if (!pendingCall && !callInProgress &&
millis() - lastCloudPush >= (uint32_t)CLOUD_SEND_INTERVAL * 1000UL) {
lastCloudPush = millis();
cloudPush(&gps, hasLoc);
}
Serial.printf("[LOOP] Done. Next in %lu ms\n\n", GPS_POLL_INTERVAL);
}