A small computer on the dash that spots vehicles as you drive, posts a plate-illegible crop with the GPS position it had at that moment, and lets the map's reviewers decide whether it was a patrol car. No phone in a mount, no screen to keep awake, nothing to tap.
--plates what leaves it is a crop at
most 200 px wide, which cannot carry a readable plate. With
--plates it reads the plate on the Pi and keeps it
there; the text is handed over only after the map has published that
vehicle as government. A private vehicle's plate never leaves the Pi. See
reading plates.| Part | Pick | Why |
|---|---|---|
| Computer | Raspberry Pi 5 (4 GB) | Runs the vehicle detector at a usable rate. A Pi 4 works at about half the frame rate; a Zero 2 W is too slow. |
| Camera, to spot patrol cars | Any USB webcam, 720p or better, with a suction or clip mount. Logitech C920-class is plenty. | One cable, a mount you can aim. Enough to tell a patrol car from a car; not enough to read a plate on a moving vehicle. |
| Camera, to read plates | Reolink RLC-810A (4K, 12 V DC) or RLC-811A (adds optical zoom). Plus a 12 V barrel lead from the car and one Ethernet cable to the Pi. | Four times the pixels on the plate, a real lens, and a manual shutter so a plate at 40 mph is not a smear. It runs off the car's 12 V directly; no PoE injector. The relay already speaks its RTSP. |
| GPS | A USB GPS dongle (u-blox 7 or 8 based, "VK-172" style, under $15) | Plugs in and shows up as /dev/ttyACM0. Put it on the
dash where it can see the sky. |
| Internet | Your phone's hotspot, or a USB LTE stick | A crop every few seconds is a few kilobytes; a day of driving is well under 50 MB. |
| Power | A 12 V-to-USB-C adapter rated 5 V / 5 A for the Pi 5 (3 A for a Pi 4) | A cheap 2 A phone charger browns the Pi out under load and corrupts the card. Buy the rated one. |
| Storage | A 32 GB A2 microSD | Nothing big is stored. |
Use Raspberry Pi Imager. In its settings set a hostname, a user, your phone hotspot's Wi-Fi name and password, and turn SSH on. Boot the Pi once at home on the same hotspot and SSH in.
sudo apt update sudo apt install -y python3-opencv python3-numpy python3-pip gpsd gpsd-clients pip3 install --break-system-packages onnxruntime curl -fsSLo ~/relay.py https://map.sparrowmap.com/relay.py
That is the whole dependency list. The relay is one file that imports nothing else from this project; it fetches the 38 MB vehicle detector from the map the first time it runs and checks its hash every run.
sudo sed -i 's#^DEVICES=.*#DEVICES="/dev/ttyACM0"#; s#^GPSD_OPTIONS=.*#GPSD_OPTIONS="-n"#' /etc/default/gpsd sudo systemctl enable --now gpsd gpsmon # wait for a 2D/3D fix; the first one outdoors can take a few minutes
If the dongle is a UART HAT on the GPIO header instead of USB, the device
is /dev/serial0 and it needs sudo raspi-config
→ Interface → Serial (login shell off, port on) first.
ls /dev/video* # usually /dev/video0 python3 ~/relay.py --source /dev/video0 --gps gpsd://127.0.0.1:2947 --dry-run
Wave a toy car in front of it, or point it at the street. You should see
lines like would send car (seen 7x) at LAT,LON.
If it says no GPS fix, it will not post: a crop with no fix is
dropped rather than pinned to the last place the fix was, because a
patrol car on the wrong street is worse than a missing one.
python3 ~/relay.py --source /dev/video0 --gps gpsd://127.0.0.1:2947 --enroll "Dashcam"
The first run waits for a fix, registers the camera with the map as a
mobile node, and remembers its id and token in
~/.sparrowmap/node.json. Every later run needs only
--source and --gps. The token is the camera's
identity: keep the file, and if the card is ever lost, enrol again with a
new name.
sudo tee /etc/systemd/system/sparrow-dashcam.service >/dev/null <<'EOF' [Unit] Description=SparrowMap dashcam relay After=network-online.target gpsd.service Wants=network-online.target [Service] User=%i ExecStart=/usr/bin/python3 /home/USER/relay.py --source /dev/video0 --gps gpsd://127.0.0.1:2947 Restart=always RestartSec=5 [Install] WantedBy=multi-user.target EOF sudo sed -i "s#%i#$USER#; s#/home/USER#$HOME#" /etc/systemd/system/sparrow-dashcam.service sudo systemctl enable --now sparrow-dashcam journalctl -u sparrow-dashcam -f # watch it post
Now it boots when the car does, reconnects to the hotspot on its own, and keeps going when the camera or GPS drops out for a moment.
sudo raspi-config # Performance Options -> Overlay File System -> enable, and make /boot read-only
With the overlay on, nothing writes to the card while driving, so pulling power is harmless. Turn it off again (same menu) when you want to change anything, then back on.
This is what the system was designed to do: read every plate at the camera, keep the government ones, and never store a private one. The Pi does it the safe way round: it reads at capture, holds the read for an hour, and sends it only when the map asks for the good picture of a vehicle it has already published as government.
Power the camera from the car's 12 V (a barrel lead to a fused socket). Run Ethernet from the camera to the Pi and give the Pi's port a fixed address so the two can talk without a router:
sudo nmcli con add type ethernet ifname eth0 con-name cam ip4 192.168.2.1/24 sudo nmcli con up cam
In the Reolink app (over the camera's Wi-Fi setup, or plugged into your
home router first) give the camera the static address
192.168.2.10, set a password, and turn RTSP on. Then in
Display → Exposure choose Manual and a shutter of
1/1000 s or faster: motion blur, not resolution, is what makes a
plate on a moving car unreadable.
--platespip3 install --break-system-packages fast-plate-ocr open-image-models python3 ~/relay.py --source "rtsp://admin:YOURPASS@192.168.2.10:554/h264Preview_01_main" --gps gpsd://127.0.0.1:2947 --plates --dry-run
Both models are ONNX and run on the Pi's CPU; a plate read takes about a
second, once per vehicle. When it works, drop --dry-run,
add --plates to the ExecStart line in the
service from step 6, and sudo systemctl restart sparrow-dashcam.
A read only counts when it is plate-shaped (not the word POLICE off a door), at least three characters, and the recogniser is at least 50 % sure of its least certain character. Every vehicle still goes out as the ordinary 200 px crop; the read waits on the Pi.
--plates, stated
plainly. For every vehicle: the same 200 px crop as before, the class it
saw, and the GPS position. Nothing else, and no text. The plate read and a
crop of the vehicle up to 900 px wide stay on the Pi for one hour. If, in
that hour, the map's classifier and a reviewer publish that vehicle as a
government one, the map asks this camera for the good picture and the Pi
answers with the picture and the read; the map records the plate on that
already-public row. For every other vehicle the read is simply forgotten.
The route that accepts it
refuses
any row that is not already public, so a private plate cannot be stored
even by a modified Pi.Nothing, at first. A dashcam crop goes into the review queue like a phone's does; a reviewer marks it as a patrol vehicle or not, and only confirmed government vehicles are published, at the position your Pi reported. Your route is never stored and your camera has no marker. Reviewing is open to anyone, so the fastest way to see your own catches land is to review.
could not open the stream |
Another program has the camera, or it is /dev/video1.
v4l2-ctl --list-devices shows which. |
no GPS fix for more than a few minutes |
The dongle cannot see the sky, or gpsd is not running:
systemctl status gpsd, then gpsmon. |
HTTP 429 |
The map is rate-limiting enrolments. Wait an hour; a saved
node.json never needs to enrol again. |
| It posts but nothing ever appears | Expected until a reviewer confirms a catch. Cars that are not government vehicles are never published. |
The relay is one readable file. If you want to check what leaves the Pi, read it; the crop cap, the no-fix rule and the plate contract are all in there in plain sight.