Build a DIY GPS Tracker with an ESP32 and a NEO-6M Module
A DIY GPS tracker is handy for keeping tabs on a bike, a backpack on a trip, or just for learning how a GPS module actually works underneath the blue dot on your phone’s map. With an ESP32 and a cheap NEO-6M module, you have everything needed: read coordinates, log them over time, and even serve a small web page showing the latest position.
How it works
- The NEO-6M module continuously streams NMEA sentences (a standardized text format) over UART, carrying latitude, longitude, time, number of satellites locked, and more.
- The ESP32 has multiple hardware UARTs, so we use a secondary UART port (
HardwareSerial) dedicated to the GPS, completely separate from the defaultSerialused for debugging over USB. - The
TinyGPS++library consumes raw bytes off the UART, automatically parses the NMEA sentences, and exposes ready-to-use values likegps.location.lat()andgps.location.lng(). - The coordinates can be published periodically over MQTT to a broker, or more simply stored in a variable and served from a small web server running right on the ESP32, along with a link that opens a map at that exact position.
Parts list
- An ESP32 DevKit (any ESP32 board with at least 2 usable UARTs)
- A NEO-6M GPS module (with a ceramic or external antenna)
- Jumper wires
- A portable power source (power bank, or a LiPo battery plus charging circuit) for mobile outdoor use
- (Optional) An external GPS antenna for better reception in areas with obstructions
Sample code
Read and parse GPS data, printing coordinates to Serial every 30 seconds:
#include <TinyGPS++.h>
#include <HardwareSerial.h>
HardwareSerial gpsSerial(1); // use ESP32's UART1, separate from debug Serial (UART0)
TinyGPSPlus gps;
const int RXD2 = 16; // wired to the GPS module's TX pin
const int TXD2 = 17; // wired to the GPS module's RX pin
unsigned long lastPublish = 0;
const unsigned long PUBLISH_INTERVAL = 30000; // 30 seconds
void setup() {
Serial.begin(115200);
gpsSerial.begin(9600, SERIAL_8N1, RXD2, TXD2);
Serial.println("Waiting for GPS signal, needs a clear view of the sky...");
}
void loop() {
while (gpsSerial.available() > 0) {
gps.encode(gpsSerial.read());
}
if (gps.location.isUpdated() && gps.location.isValid()) {
double lat = gps.location.lat();
double lon = gps.location.lng();
if (millis() - lastPublish > PUBLISH_INTERVAL) {
Serial.printf("Location: %.6f, %.6f | Satellites: %d | HDOP: %.1f\n",
lat, lon, gps.satellites.value(), gps.hdop.hdop());
publishLocation(lat, lon);
lastPublish = millis();
}
}
// If no NMEA data has arrived after 10 seconds, TX/RX are probably swapped
if (millis() > 10000 && gps.charsProcessed() < 10) {
Serial.println("No GPS data seen - check the TX/RX wiring or allow more time for a cold start");
}
}
void publishLocation(double lat, double lon) {
// Example: publish over MQTT (requires setting up a WiFiClient + PubSubClient beforehand)
// String payload = String(lat, 6) + "," + String(lon, 6);
// mqttClient.publish("tracker/location", payload.c_str());
}
If you’d rather check the latest position from a browser instead of MQTT, add a small web server that returns an HTML page with a link that opens a map at the coordinates (using any generic public map service, no paid API required):
#include <WebServer.h>
WebServer server(80);
double lastLat = 0, lastLon = 0;
void handleRoot() {
String html = "<html><body><h1>Current location</h1>";
html += "<p>" + String(lastLat, 6) + ", " + String(lastLon, 6) + "</p>";
html += "<a href='https://www.openstreetmap.org/?mlat=" + String(lastLat, 6) +
"&mlon=" + String(lastLon, 6) + "#map=16/" + String(lastLat, 6) +
"/" + String(lastLon, 6) + "' target='_blank'>View on map</a></body></html>";
server.send(200, "text/html", html);
}
Common pitfalls
- The first fix (cold start) can take 1-5 minutes — the GPS module needs to download almanac data from the satellites, which only happens reliably with a clear view of the sky. Near a window indoors might work; deep inside a concrete building almost never will.
- Crossed TX/RX wiring: the module’s TX pin must connect to the ESP32’s RX (receive) pin and vice versa — wiring them straight-through is the most common reason nothing gets read.
- The NEO-6M’s default baud rate is 9600 — if you reconfigure the module to a different baud rate (some versions support this), remember to update
gpsSerial.begin()to match. - An unstable power supply can cause the module to hang or reset when the ESP32’s WiFi radio draws a current spike — use a 5V supply rated for at least 500mA, and avoid underpowered computer USB ports.
Where to go from here
Log the full track to an SD card or SPIFFS so the route can be reviewed after a trip, add geofencing alerts for when the device leaves a defined area, put the ESP32 into deep sleep between readings to stretch battery life, or pair it with a LoRa module to transmit location over long range without WiFi.
Comments