For the last while I’ve been building something I’ve wanted for ages: a small, self-contained GPS tracker that logs where it is, what it can see in the sky, and how healthy its signal is — and streams all of that to a live dashboard on this website. No subscription, no cloud middleman, no app. Just a microcontroller, a GPS receiver, and a bit of code on both ends.
Here’s the whole thing — what it is, how it works, and what I learned dragging it into existence.
What it actually is
The tracker is a two-part system:
- A device — an ESP32-S3 microcontroller paired with a u-blox NEO-M9N GNSS receiver. It works out its position from satellites overhead and, every ten seconds, packages up a snapshot of everything it knows into a small JSON message and sends it over Wi-Fi.
- A website half — a WordPress plugin I wrote that receives those messages, stores them, and renders a rich live dashboard: an interactive map with the track, a sky plot of every satellite in view, signal-strength charts, trip statistics, and a “receiver health” panel.
The two halves are joined by nothing more than a simple JSON contract, which means either side can be worked on independently. The whole project lives in a public GitHub repository, and the plugin even updates itself from GitHub releases.
The hardware
The brains and senses are deliberately cheap and off-the-shelf:
- ESP32-S3 — a dual-core microcontroller with built-in Wi-Fi. It runs the firmware, drives the screen, reads the GPS, and does the uploading.
- u-blox NEO-M9N — the GNSS receiver. This is the star of the show. It’s a modern single-band (L1) receiver that tracks four satellite systems at once — GPS (USA), GLONASS (Russia), Galileo (EU) and BeiDou (China) — plus augmentation from SBAS/EGNOS. In good conditions it’s accurate to around two metres. Datasheet
- A 2.8″ ILI9341 colour TFT — an on-device screen showing live position, satellite count, speed and fix quality, so the tracker is useful even with no network.
- A 3×4 keypad and the BOOT button — to page through screens (position, sky plot, trip stats, diagnostics).
- Power — a USB-C power bank, so it can run untethered.
A quick word on how GPS actually works
It still feels a bit magical. Each navigation satellite, orbiting about 20,000 km up, continuously broadcasts the time and its own position. The receiver listens to several at once and measures how long each signal took to arrive. Because radio travels at a known speed, each delay becomes a distance — and with enough distances from enough satellites, the receiver can solve for exactly where it is, plus its altitude, speed and heading.
Using four constellations at once matters more than it sounds: it means the receiver can see far more satellites, get a faster and more reliable fix, and keep working among buildings and trees where a GPS-only receiver would struggle.
The firmware: squeezing everything out of the receiver
Most hobby GPS projects read the receiver’s plain-text NMEA output and stop at latitude and longitude. I wanted more, so the firmware talks to the NEO-M9N in its native UBX binary protocol. That unlocks a much richer stream of data, five times a second:
- Position, altitude, speed and heading — the basics, but with the receiver’s own accuracy estimates in metres, not just a vague quality flag.
- Full dilution-of-precision (HDOP/VDOP/PDOP) — a measure of how favourable the satellite geometry is.
- A per-satellite sky map — every satellite in view with its position, signal strength and whether it’s being used in the fix.
- A position error ellipse — derived from the receiver’s covariance data and drawn right on the map, so you can see not just where it thinks it is, but how confident it is and in which direction.
- Receiver health and integrity — the M9N monitors its own radio front end for interference and jamming, watches its automatic gain control, and runs basic spoofing detection. All of that is surfaced.
- An on-chip odometer and time-to-first-fix — hardware-filtered trip distance, and a stopwatch on how long the last cold start took.
To keep it all inside the receiver’s serial bandwidth I run navigation at 5 Hz and pace the heavier messages down to roughly 1 Hz. The device also degrades gracefully: if it can’t reach Wi-Fi, it just keeps tracking and displaying locally.
The pipeline: from silicon to browser
Every ten seconds the firmware builds a JSON telemetry frame and POSTs it over HTTPS to a REST endpoint provided by the WordPress plugin, authenticated with a secret API key. The plugin sanitises it, stores it in its own database table, and that’s it — the data is now available to the dashboard. It’s a genuinely tiny, dependency-free pipeline, and it means my location history lives on my own server, not someone else’s.
The dashboard
This is where it comes to life. The plugin adds a shortcode you can drop on any page, and it renders:
- An interactive Leaflet map with the track drawn as a line coloured by speed, a heading marker, and the live accuracy ellipse.
- A sky plot — a radar-style view of the whole sky with every satellite plotted by bearing and elevation, coloured by signal strength. It’s my favourite part; you can watch satellites drift across the sky in real time.
- Signal-strength and constellation charts — a bar chart of carrier-to-noise for each satellite, and a breakdown of how many satellites each of the four systems is contributing.
- History charts — speed, altitude, vertical speed and accuracy over your chosen time range.
- Trip statistics — distance, moving time, max and average speed, and the on-chip odometer.
- A “receiver health & integrity” panel — interference, RF gain, noise floor, antenna status, spoofing and time-to-first-fix, each with plain-English explanations and honest “not measured” states.
- CSV / GPX export — download the whole track to open in Google Earth or any mapping tool.
- Privacy options — a switch to hide the exact location and show a message instead, for when you want to share the dashboard publicly without giving away where you are.
There are also built-in “how this works” explainers throughout, because half the fun of a project like this is understanding what you’re looking at.
The hard bits (there are always hard bits)
No project this size goes smoothly, and the failures taught me the most.
- Power brownouts. Early on the screen would flash white and the device would silently reboot. The culprit was the combined current draw of the TFT backlight, the SD card and the Wi-Fi radio sagging the power rail at exactly the wrong moment. Disabling the SD card and trimming the load fixed it.
- A frozen main loop. After a while the whole thing would lock up — screen stuck, buttons dead. Two causes: a blocking “wait for the key to be released” loop in the keypad code that could spin forever, and a Wi-Fi reconnect path that could wedge the driver. I rewrote the keypad to be fully non-blocking and added a software watchdog that reboots the board if the main loop ever stalls for more than 20 seconds. Now a hang becomes a brief auto-recovery instead of a dead device.
- The Wi-Fi that refused to connect. The most stubborn of all — the device suddenly wouldn’t join the network despite a correct password and a router a foot away, rejecting every attempt with a cryptic
reason=2. After eliminating firmware, MAC, security modes and a full flash erase, the fix turned out to be one line: lowering the Wi-Fi transmit power. At full power the little radio was over-driving itself and the router was refusing the messy handshake. I wrote that whole saga up separately — it’s worth a read if you ever hitreason=2yourself. (link to the troubleshooting article)
What’s next
The NEO-M9N still has capability I haven’t tapped. On the shortlist: a live RF spectrum view to make interference visible at a glance, a higher navigation rate once I bump the serial baud, on-chip geofencing so it can flag when it leaves a defined area, and AssistNow to slash the cold-start time. The dashboard, meanwhile, keeps getting small refinements — clearer health tiles, better exports, and honest labelling everywhere.
Wrapping up
What I love about this project is that it’s complete end to end and entirely mine: a receiver pulling signals out of the sky, firmware wringing every last field out of it, and a dashboard on my own website turning raw telemetry into something you can actually read. It cost very little, it taught me an enormous amount about GNSS and embedded reliability, and every part of it — firmware and plugin — is open source.
If you’d like to build one, the code’s on GitHub. And if your ESP32 ever starts throwing reason=2 at you… turn the power down.
Position & track
Sky plot
Signal strength (C/N₀)
Constellations in view
Receiver health & integrity
Receiver
Live RF spectrum
Spectrum waterfall
Speed over time
Altitude over time
Vertical speed over time
Accuracy over time
Trip statistics
How this works
What am I looking at?
A GNSS receiver (this one is a u-blox NEO-M9N) listens to radio signals from navigation satellites orbiting ~20,000 km up. By measuring how long each signal takes to arrive, it works out its own position, altitude, speed and heading, then streams that to this site over WiFi.
The four constellations
GPS (USA), GLONASS (Russia), Galileo (EU) and BeiDou (China) are independent satellite systems. A multi-constellation receiver uses all of them at once, so it sees more satellites and gets a faster, more reliable fix — especially among buildings or trees.
Sky plot & signal strength
The sky plot shows where each satellite is in the sky: the centre is straight overhead (zenith), the edge is the horizon, and the angle around the circle is the compass bearing. Colour shows signal strength (C/N₀ in dB-Hz) — green is strong, red is weak. Satellites spread evenly across the sky give the most accurate fix.
What is HDOP / accuracy?
HDOP (Horizontal Dilution of Precision) describes how favourable the satellite geometry is. Lower is better: under 1 is ideal, 1–2 excellent, 2–5 good. A low HDOP with many satellites means the position is trustworthy to within a few metres.
Recent Comments