A full-screen GPS digital instrument cluster for the Raspberry Pi 5, built to run headless in a car — it boots straight to the gauge display with no desktop with the option to switch to terminal or play Minetest(Minecraft) when you are parked!
- Full-screen digital speedometer (factory-orange VFD aesthetic)
- analog gauge layout — speed and 0–60 timer
- Real-time GPS speed, heading, altitude, and fix quality
- 0–60 mph timer with a persistent top-5 leaderboard
- Offline speed limit display (OpenStreetMap
maxspeeddata) - City and street / highway name from GPS coordinates
- 12-hour clock (US Central, DST-aware via zoneinfo)
- GPS signal-strength bars
- Boots directly to the cluster on power-up (systemd)
- Simulation mode when no GPS is connected (no code changes needed)
- Optional Ctrl+M (Minetest) and Ctrl+T (terminal) launch hotkeys
| Part | Notes |
|---|---|
| Raspberry Pi 5 (8 GB) | Pi 4 also works — see compatibility note below |
| GlobalSat BU-353N5 USB GPS | Enumerates as /dev/ttyUSB0 (Prolific serial) |
| 7" 1024×600 HDMI IPS display | Driver-free over HDMI |
| (2) DC-DC 12V/24V to 5V 5A 25W Buck Converter to Type-C Output Power Adapter | 5V to the Pi; the display has its own 5V feed |
| USB-C Extender | Helps with cable management |
This build uses a USB GPS receiver read through gpsd. There is no UART wiring and no serial-console setup required.
git clone https://github.com/FandIguy/PiGpsSpeedometer.git
cd PiGpsSpeedometer
chmod +x install.sh
./install.shThe script installs packages, the font, the systemd service, all config files (with your username substituted automatically), and the app. When it finishes:
- Enable console autologin (required — see below):
sudo raspi-config→ System Options → Boot / Auto Login → Console Autologin - Reboot:
sudo reboot
After reboot the cluster appears on the display automatically.
These are the non-obvious pieces that make a headless KMSDRM app work on a Pi 5. The installer handles all of them; this section explains them for the curious and for troubleshooting.
The Pi 5 exposes two DRM devices:
card0= the V3D GPU — render only, no display connectorscard1= the RP1 display controller — this is where HDMI lives
SDL's KMSDRM backend defaults to device index 0, opens card0, finds no
displays, and silently falls back to the dummy driver (black screen, no
error). The service sets SDL_KMSDRM_DEVICE_INDEX=1 to target the right card.
On a Pi 4 there is only one DRM device — remove that line (see compatibility).
KMSDRM has to become DRM master, which requires a real logind session on
the console. The service uses PAMName=login + TTYPath=/dev/tty1, and the
installed autologin.conf auto-logs-in your user on tty1 at boot so that session
exists. Without it: a silent black screen on cold boot (no crash, no error).
KMSDRM also needs XDG_RUNTIME_DIR (/run/user/<uid>). The installer enables
lingering (loginctl enable-linger) so that directory exists at boot before
the service starts, and the service sets the variable explicitly.
The GPS is USB. The installer writes /etc/default/gpsd pointing at
/dev/ttyUSB0 with -n (poll without a client connected) so a fix is ready as
soon as the cluster starts. The app reads gpsd's JSON socket directly — no
python-gps or python-serial library involved.
Verify a fix after reboot (needs a clear view of the sky; a cold start can take a few minutes):
gpspipe -w -n 5 # raw JSON — look for "mode":3 and a lat/lon
cgps # live view (terminal must be >= 80 columns wide)| Key | Action |
|---|---|
ESC |
Quit |
UP / DOWN |
Adjust speed in simulation mode |
R |
Reset the 0–60 leaderboard (double-press to confirm) |
Ctrl + M |
Launch Minetest (requires optional setup — see below) |
Ctrl + T |
Drop to a terminal on tty2 (type exit to return) |
The speedometer can display the posted speed limit for the road you are on and the city and street name — all offline with no internet connection needed while driving. This uses OpenStreetMap data downloaded once to the Pi.
Geofabrik provides free daily extracts of OpenStreetMap data, pre-cut by region. No account is required.
Navigate to:
https://download.geofabrik.de/north-america/us/
Find your state and download the .osm.pbf file. Example for Minnesota:
mkdir -p ~/osm && cd ~/osm
wget https://download.geofabrik.de/north-america/us/minnesota-latest.osm.pbfFor multiple states (for example a border commute):
wget https://download.geofabrik.de/north-america/us/minnesota-latest.osm.pbf
wget https://download.geofabrik.de/north-america/us/wisconsin-latest.osm.pbfFor the entire US (large — several GB):
wget https://download.geofabrik.de/north-america/us-latest.osm.pbfInstall the required tools:
sudo apt install -y osmium-tool
pip install shapely rtree --break-system-packages
sudo apt install -y libspatialindex-dev # install this first if rtree failsFilter to roads with speed limits and export to GeoJSON:
cd ~/osm
# Single state
osmium tags-filter minnesota-latest.osm.pbf w/highway w/maxspeed -o mn-roads.osm.pbf
osmium export mn-roads.osm.pbf -o maxspeed-roads.geojson
# Multiple states — filter each then merge
osmium tags-filter minnesota-latest.osm.pbf w/highway w/maxspeed -o mn-roads.osm.pbf
osmium tags-filter wisconsin-latest.osm.pbf w/highway w/maxspeed -o wi-roads.osm.pbf
osmium merge mn-roads.osm.pbf wi-roads.osm.pbf -o roads.osm.pbf
osmium export roads.osm.pbf -o maxspeed-roads.geojsonBuild the fast lookup index (one-time, takes a few minutes):
cd ~/osm
python3 speed_limit.py --buildThis creates cached index files (speedlimit_idx.*, speedlimit_segs.pkl) so
the cluster loads the data in under a second on every boot instead of rebuilding
from scratch each time.
Test it with a known coordinate:
python3 speed_limit.py <lat> <lon>A match distance under ~10 m means a confident result. You should see the speed limit, road name, and match distance for that point.
| Coverage | Raw extract | Processed index |
|---|---|---|
| One state (e.g. Minnesota) | ~300 MB | ~130 MB |
| Two states | ~600 MB | ~200 MB |
| Full US | ~8 GB | ~2–3 GB |
A 32 GB SD card has plenty of room for one or two states alongside the OS and the application.
Before processing a large download, confirm your roads are tagged with speed
limits in OSM. Go to openstreetmap.org, click
on a road you drive regularly, and look for a maxspeed tag in the details
panel. Highways and main roads are almost always tagged. Residential streets vary
by area.
If a road you drive is not tagged, you can add the data yourself — OSM is community-edited and contributions are welcome at openstreetmap.org.
Pi 5: uses card1 + KMSDRM with SDL_KMSDRM_DEVICE_INDEX=1 (default in the
service file).
Pi 4: exposes a single DRM device, so SDL selects it automatically. Remove
this line from /etc/systemd/system/speedometer.service:
Environment=SDL_KMSDRM_DEVICE_INDEX=1
Then reload and restart:
sudo systemctl daemon-reload
sudo systemctl restart speedometerIf no GPS receiver is connected (or gpsd has no fix), the app runs in simulation
mode automatically — useful for bench testing the UI. Use UP / DOWN to drive
the simulated speed. No code changes are required.
sudo systemctl status speedometer # is it running?
sudo systemctl restart speedometer # restart after editing speedometer.py
sudo journalctl -u speedometer -f # live logs and errorsBlack screen, no error. Almost always the display-ownership chain. Check in order:
whoshows<your-user> tty1— autologin is working- User is in the
videoandrendergroups — rungroups /run/user/<uid>exists — lingering is enabled- Service has
SDL_KMSDRM_DEVICE_INDEX=1on a Pi 5
The logs will often show the driver falling back to dummy — that is this
problem.
journalctl -u speedometer -n 30 --no-pager | grep -i "driver\|dummy\|error"drmModePageFlip failed (-13) (EACCES). The process is not DRM master. This
happens when you run the script directly over SSH — an SSH session has no console
seat. Always test via the service, or use openvt on tty1, not SSH.
GPS never locks. Confirm a clear view of the sky, that /dev/ttyUSB0 exists,
and that gpsd is running:
ls -l /dev/ttyUSB*
sudo systemctl status gpsd
gpspipe -w -n 5A cold start on the BU-353N5 can take several minutes outdoors.
Permission denied on DRM or input devices. Make sure your user is in the required groups, then reboot:
sudo usermod -aG video,render,input,dialout "$USER"
sudo rebootSpeed stuck at zero. Confirm gpsd is receiving data with gpspipe -w -n 10
and check that the TPV sentences contain a non-zero speed field. The app
falls back to simulation mode automatically if gpsd is unreachable — arrow keys
will move the simulated speed if that is happening.
Issues and pull requests are welcome.
MIT
