A Raspberry Pi and a £12 USB GPS dongle make a tracker you can leave in a vehicle, a boat or a backpack. The hardware is the easy half. The half that usually stalls is getting a clean stream of fixes out of Linux and somewhere useful.
gpsd solves the first part and has done for twenty years. This is the second
part.
Plug it in and confirm the kernel sees it
Most modern dongles are u-blox and appear as a USB CDC ACM device:
ls /dev/ttyACM* /dev/ttyUSB*
dmesg | tail -20
/dev/ttyACM0 is typical for u-blox; /dev/ttyUSB0 for the older Prolific and
CP210x serial bridges. If nothing appears, it is a cable or a power problem, not
a software one — try a powered hub before anything else.
Install gpsd
sudo apt update
sudo apt install gpsd gpsd-clients
Point it at the device:
sudo nano /etc/default/gpsd
DEVICES="/dev/ttyACM0"
GPSD_OPTIONS="-n"
USBAUTO="true"
-n matters. Without it gpsd waits for a client before it starts polling the
receiver, so the first fix after boot arrives minutes late — and if your only
client is a script that gives up when there is no data, it never arrives at all.
sudo systemctl enable --now gpsd
cgps -s # live view; ctrl-C to quit
cgps showing a latitude means the hard part is done. A cold receiver with a
clear view of the sky takes 30 seconds to a few minutes.
Get a session
curl -s -X POST https://gpstrack.dev/api/session
Keep the token. Put it somewhere the script can read and other users cannot:
sudo install -m 600 /dev/null /etc/gpstrack.env
echo 'GPSTRACK_TOKEN=tk_9d41b0e6a7c2f5' | sudo tee /etc/gpstrack.env > /dev/null
Mode 600 is not ceremony. Anything that can read that file can push points as
your device.
The bridge script
gpspipe -w emits one JSON object per line. The ones that carry a position have
"class":"TPV":
sudo nano /usr/local/bin/gpstrack-bridge.sh
#!/usr/bin/env bash
set -euo pipefail
: "${GPSTRACK_TOKEN:?GPSTRACK_TOKEN is not set}"
API="https://gpstrack.dev/api/ingest"
LABEL="${GPSTRACK_LABEL:-raspberry-pi}"
MIN_INTERVAL=2 # seconds; the relay accepts 1/sec, this leaves headroom
last=0
gpspipe -w | while read -r line; do
# TPV is the position report. Ignore SKY, VERSION and the rest.
[[ "$line" == *'"class":"TPV"'* ]] || continue
read -r lat lng alt spd trk mode <<<"$(printf '%s' "$line" | jq -r \
'[.lat, .lon, (.alt // 0), (.speed // 0), (.track // 0), (.mode // 0)] | @tsv')"
# mode 2 is a 2D fix, 3 is 3D. Anything less has no usable position.
[[ "$mode" -ge 2 ]] || continue
[[ "$lat" != "null" && "$lng" != "null" ]] || continue
now=$(date +%s)
(( now - last >= MIN_INTERVAL )) || continue
last=$now
# gpsd reports speed in METRES PER SECOND. The relay's `speed` field is km/h.
kph=$(awk "BEGIN{printf \"%.1f\", $spd * 3.6}")
curl -fsS -X POST "$API" \
-H "Content-Type: application/json" \
-H "X-API-Key: $GPSTRACK_TOKEN" \
-d "{\"lat\":$lat,\"lng\":$lng,\"speed\":$kph,\"heading\":$trk,\"altitude\":$alt,\"label\":\"$LABEL\"}" \
> /dev/null || true
done
sudo chmod +x /usr/local/bin/gpstrack-bridge.sh
sudo apt install jq # if it is not already there
The m/s trap. gpsd reports speed in metres per second. The relay's speed
field is km/h. Skip the conversion and every reading is 3.6× too low — 100 km/h
shows as 28, which is wrong in a way that looks plausible enough to survive
testing. Multiply by 3.6.
Run it as a service
A script in a terminal dies with the SSH session. A unit file survives reboots, which is the whole point of putting a Pi in a vehicle:
sudo nano /etc/systemd/system/gpstrack.service
[Unit]
Description=Stream gpsd fixes to gpstrack
After=network-online.target gpsd.service
Wants=network-online.target
Requires=gpsd.service
[Service]
Type=simple
EnvironmentFile=/etc/gpstrack.env
ExecStart=/usr/local/bin/gpstrack-bridge.sh
Restart=always
RestartSec=10
User=nobody
[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl enable --now gpstrack
journalctl -u gpstrack -f
Restart=always with RestartSec=10 is the important pair. A vehicle drives out
of coverage constantly; the script should die and come back rather than try to be
clever about it.
Surviving a tunnel
The script above drops points it cannot send. For live tracking that is correct — a viewer wants where you are now, not where you were eleven minutes ago, and a backlog replayed at 1/sec would take longer to drain than the outage lasted.
If you want the gap filled in afterwards, that is a different job: log every fix to a local file as well, and upload that separately. Do not try to make one loop do both. The Track Me recorder takes the same position for the same reason — the local recording is complete, and the live push is best-effort.
Checking it without a vehicle
# Are fixes arriving at all?
gpspipe -w -n 5
# Is the relay accepting them?
curl -s "https://gpstrack.dev/api/session/s_7f3a91c4/points?since=0" | jq '.points | length'
If the second number climbs, you are done. If it stays at zero while gpspipe
shows positions, the token and the session id belong to different sessions —
easily done when you have created a few while testing.
Where to go next
- Send custom telemetry alongside GPS — the Pi knows its CPU temperature and battery voltage too
- HTTP or MQTT for GPS tracking? — a mains-powered Pi is the case where HTTP is genuinely fine