GPSTrack

Live-track a Raspberry Pi with a USB GPS dongle

4 min read3 of 5 in Telematics

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

Open the dashboard → Push points from any device over HTTP or MQTT and watch them move on a live map.
More on Telematics dashboard All 5 →
  1. Telematics

    Track anything with one HTTP request

    Put a moving thing on a live map with nothing but curl — no account, no SDK, no app. A complete GPS tracking walkthrough in two requests.

    15 June 2026 · 4 min read

  2. Telematics

    HTTP or MQTT for GPS tracking? Pick by power budget

    Both deliver the same point. The difference is what a connection costs your device. A decision table, the handshake arithmetic, and where each one falls apart.

    25 June 2026 · 4 min read