Google Maps knows where you are. CarChomp knows where you've been. It's a Raspberry Pi car computer that records every drive and tells you, at a glance, when you're on a road you've never driven.
- "Have I driven this road?" The badge reads Been here or New road, with an optional
chirp as you leave known road. Matching follows your heading, so crossing an old track at
an intersection doesn't count (a crossing within
road_heading, 35° by default, does), and it needs no road-network data. - Keeps the shape, not every point. The APRS SmartBeaconing algorithm decides what gets stored: little on a straight road, every bend on a switchback. The screen still sees every fix.
- Hears what's around you. Receive-only APRS through an RTL-SDR dongle brings in nearby stations, weather, road closures, fires and bulletins. No radio license needed.
- Works without a network. Download map regions for offline use. When no known Wi-Fi is in range, it hosts its own hotspot, and it takes its clock from the GPS.
- Touchscreen in the car, browser everywhere else. One app handles both, including GPX and GeoJSON import, GPX export, Wi-Fi, system health and software updates. It can also run headless.
Real UI on simulated sensors: tools/sim.py replaying a drive past an imported one.
It's one Rust daemon, one PostgreSQL/PostGIS database and one Svelte app: a from-scratch take on the carputer capstone, built from the stakeholder's original directive (stakeholder.md). Design: DESIGN.md. Roadmap: ROADMAP.md.
crates/carchomp-core pure logic: gpsd, SmartBeaconing, KISS, APRS (no I/O)
crates/carchompd the daemon: sources -> bus -> PostGIS, /ws, /api
ui one Svelte + MapLibre app for the touchscreen and the browser
deploy install script, systemd unit, production config
tools/sim.py stand-in for the sensors: pretend gpsd + APRS TNC
tools/fetch_map_assets.sh fonts + sprites for the offline map
APRS (Automatic Packet Reporting System) is a ham radio network for sharing local information. Stations broadcast short digital packets on one shared frequency, 144.39 MHz in North America: where they are, the weather where they are, or a message for everyone nearby. Any station in range can decode them. Digipeaters relay packets further, so a mountaintop repeater can carry a car's position across a region.
Transmitting takes an amateur radio license, but receiving doesn't, and CarChomp only receives. It has no way to transmit: Direwolf is configured without transmit audio, push-to-talk or beacons, and an RTL-SDR dongle is receive-only hardware anyway.
The stakeholder's brief asks for surrounding stations to be logged and shown on the map. The other reason to listen is that APRS needs no cell signal or internet, which suits a car on back roads. CarChomp sorts what it hears like this:
| Packet | What you see |
|---|---|
| Positions | Stations on the optional map layer, dropped after an hour unheard |
| Weather reports | Weather stations on the map, with their readings |
| Objects and items | Named things at a place, such as a brush fire or a road closure, listed as incidents until the sender withdraws them or they go unheard for an hour |
| Bulletins and weather-service messages | The Alerts list. Private messages between stations are left out |
What you hear depends on who is transmitting nearby and whether their signal reaches you. Treat it as extra awareness, not as a complete or official alert service.
The radio work is done by existing tools. CarChomp starts where the radio signal has already become bytes:
antenna + RTL-SDR dongle
│ radio signal on 144.39 MHz
rtl_fm FM demodulator: turns the signal into audio
│ 24 kHz audio, over a pipe
Direwolf software modem: decodes 1200-baud AX.25 packets from the audio
│ KISS frames, over TCP on port 8001
carchompd source task unframes KISS, parses APRS (carchomp-core, no I/O)
│ Observation on the bus
├─► recorder aprs_packet in PostGIS, keeping the original text
└─► WebSocket map overlay and APRS panel, live
- Same pipeline as GPS. An APRS packet is one more
Observationfrom asource, as a GPS fix is. The design aims to let future sources join without changing how existing ones are stored. - A TNC is just a port. carchompd only sees a TCP port speaking KISS. Direwolf, a hardware TNC
behind a bridge and
tools/sim.pyall look alike, which is how the radio path is tested without a radio. - Failures stay contained. The radio runs as its own service,
carchomp-radio.service. If it dies or was never plugged in, carchompd keeps retrying the connection and everything else carries on. - Nothing is thrown away. Every packet keeps its full text, so a better parser can re-read old data later.
- Groundwork for finding position by radio. A packet no digipeater relayed was heard directly
from its sender, so that sender is within radio range.
aprs_packet.directrecords this. It could one day give a coarse position, within kilometres, from stations with known locations. That's a stretch goal and isn't built (DESIGN.md).
The frequency is set in deploy/carchomp-radio.service. Outside North America, change it to your
region's APRS frequency (144.80 MHz in Europe).
SmartBeaconing was created by Tony Arnerich (KD7TA) and Steve Bragg (KA9MVA) for the HamHUD tracker. A moving APRS station shouldn't crowd the shared channel by transmitting on a fixed timer, but a slow timer draws a bad map: on a fixed 30-second timer, a parked car repeats itself while a car on a winding road skips its bends. SmartBeaconing reports according to how you're moving: rarely when stopped, more often as you speed up, and right away when you turn.
CarChomp never transmits its position, so it borrows the idea for storage. gpsd delivers a fix every second, and the screen shows all of them. SmartBeaconing decides which ones are written to the database:
- Stopped (under 2 m/s, about 4.5 mph): keep the point where you came to rest, since the power may be cut at any moment, then one every 2 minutes. Turns are ignored, because GPS course is just noise at walking pace.
- Moving: wait
fast_rate × high_speed ÷ speedseconds between points, limited to between 15 and 120 seconds. Between about 8 and 60 mph, that works out to one point every 405 m whatever your speed. - Turning: if your heading has changed from the last stored point by more than
turn_min + turn_slope ÷ speeddegrees, keep a point now, at most once every 3 seconds. Slower vehicles have to turn further, because their course wobbles more.
With the defaults:
| Speed | Stored while going straight | Turn that counts as a corner |
|---|---|---|
| Under 4.5 mph | Every 120 s | Turns ignored |
| 10 mph | Every 91 s (405 m) | 35° |
| 30 mph | Every 30 s (405 m) | 18° |
| 60 mph | Every 15 s (405 m) | 14° |
| 75 mph | Every 15 s (503 m) | 13° |
CarChomp adds one thing for storage. A turn only shows up once you're already past it, so a corner stores two points: the current fix and the one just before it. Otherwise the stored line would cut the corner.
The New road check doesn't depend on any of this. It runs on its own schedule whether or not the
current fix gets stored. Imported drives with timestamps go through the same filter. Ones without
timestamps, such as a planned route, have no speed to filter by and are kept whole. To tune it, use the [beacon] settings in
carchompd.example.toml (metres per second, seconds, degrees).
docker compose -f compose.dev.yml up -d --wait # PostGIS on :5433
(cd ui && npm install && npm run build) # UI -> ui/dist
tools/fetch_map_assets.sh maps # map fonts + sprites (needs git)
python3 tools/sim.py --gpsd 12947 --kiss 18001 & # pretend GPS + APRS radio
cat > dev.toml <<'END'
database_url = "postgres://carchomp:carchomp@localhost:5433/carchomp"
gpsd = "127.0.0.1:12947"
aprs_kiss = "127.0.0.1:18001"
ui_dir = "ui/dist"
maps_dir = "maps"
END
cargo run -p carchompd -- dev.toml # then open http://localhost:8000
The simulator drives a city block, parks, and drives it again with a detour,
forever: the badge goes New road, then Been here / New road on the
second lap, then Been here from then on. The APRS panel fills with a
red-flag warning, a bulletin, a brush fire and a road closure that is later
lifted. --speedup 20 hurries it along, --gpx drive.gpx replays a real
drive out and back, --once exits after one pass. For UI work, npm run dev
in ui/ proxies to the daemon with hot reload. The API by hand:
curl localhost:8000/api/tracks
curl localhost:8000/api/tracks/1 # GeoJSON
curl 'localhost:8000/api/tracks?near=-122.6587,45.5125,50'
cargo test runs the tests that need nothing but a Rust toolchain (1.88+). A few need PostGIS,
so they're opt-in, and CI always runs them. Some create their own carchomp_test_* databases:
DATABASE_URL=postgres://carchomp:carchomp@localhost:5433/carchomp \
cargo test -p carchompd -- --ignored
CarChomp is a rewrite of carputer, which ran on a Raspberry Pi with a GPS module and an RTL-SDR, the
hardware stakeholder.md describes. The rewrite is built and tested end to end on
simulated sensors: tools/sim.py stands in for the GPS and the radio, and CI runs every test, including
the ones against PostGIS. 11 of the 17 stories in ROADMAP.md are done. On its own
hardware so far, a web update ran on a reTerminal: a bundle uploaded there installed, and the daemon
came back.
The rewrite's own hardware runs come next, on the reTerminal:
- GPS fixes from a real receiver, and the clock set from them through chrony.
- APRS from an RTL-SDR dongle through Direwolf.
- Wi-Fi, and the hotspot coming up when no known network is in range.
- Building the SD-card image and booting it into the kiosk.
- Offline maps with the network actually off. So far a region has been downloaded and drawn with the browser blocked from other hosts, not with the network down.
On Raspberry Pi OS or Debian (bookworm or newer), from a checkout or a release bundle:
sudo deploy/install.sh --kiosk "$USER" --hotspot carchomp --gps /dev/ttyACM0 --radio
All options are optional. It installs PostgreSQL/PostGIS, gpsd, chrony, the
pmtiles tool, builds carchompd and the UI if the bundle has no prebuilt
ones, creates the database, and starts carchompd.service on port 80. Run it
again to upgrade; data and /etc/carchomp/carchompd.toml are kept. It only
goes online for packages that are missing, so a re-run from a bundle works on
the car's own hotspot.
--kiosk USERopens the UI full-screen when USER's desktop session starts. Use the desktop user ("$USER"above); current Raspberry Pi OS has nopi.--hotspot SSIDhosts a Wi-Fi access point whenever no known network is in range (the UI is then at http://10.42.0.1/). The installer asks for the passphrase; in a script, pipe it in instead:sudo deploy/install.sh --hotspot carchomp < passphrase.txt. It is never taken from the command line, wherepsand shell history would keep it.--gps DEVICEhas gpsd read a GPS receiver from boot: a USB one is usually/dev/ttyACM0, one on the GPIO header/dev/serial0(enable the serial port and disable the serial console inraspi-configfirst).--radioreceives APRS on 144.39 MHz with an RTL-SDR dongle:carchomp-radio.servicerunsrtl_fminto Direwolf (/etc/carchomp/direwolf.conf, receive only: no PTT, no beacons), andaprs_kissis pointed at Direwolf's KISS port 8001 unless already set. Reboot once so the kernel's DVB-T driver leaves the dongle alone.--demoruns a simulator in place of the GPS and radio (below); it cannot be combined with--gpsor--radio.--admin-net CIDRalso serves the web UI and the.localname to an IPv4 network, /16 or narrower, e.g.--admin-net 192.168.77.0/24for home; repeat it for more (see Who can use it).--reuseruns again with the options of the last install, recorded in/etc/carchomp/install.env(never the hotspot passphrase), and leaves the hotspot as it is.
The clock comes from the GPS: chrony reads gpsd's time and steps the clock whenever it is off, since a Pi has no battery-backed clock. Network time servers are still used when there is a network.
--gps and --radio (and the chrony setup) have not been tested on real
hardware yet. They are kept simple and are safe to re-run; leaving --radio
out of a later run removes the radio service again.
The web UI and the API are unauthenticated and whoever reaches them can manage Wi-Fi and install software, so by default only this device and clients on its hotspot (10.42.0.0/24) can. The car joins networks nobody controls (eduroam, a cafe), so this is enforced three times over:
- carchompd listens on
127.0.0.1:80and10.42.0.1:80only (listenin/etc/carchomp/carchompd.toml); the hotspot address is taken whenever the hotspot is up. - A firewall (
carchomp-firewall.service, tableinet carchompin/etc/carchomp/firewall.nft) drops all inbound traffic except: the hotspot's clients (web UI, SSH, DHCP, DNS, mDNS), SSH from anywhere, replies to the car's own connections, and the ICMP that networks need. The rules match addresses, not interfaces, since the one Wi-Fi radio is sometimes the hotspot and sometimes a client; a strict reverse-path check drops packets whose source address is not routed back out the interface they came in on, so a cafe neighbour cannot pose as a hotspot client. NetworkManager's own hotspot rules are left alone. - carchompd answers anyone outside
trusted_networkswith 403.
--admin-net CIDR opens all three to another network: it is added to the
firewall (web UI and mDNS), to trusted_networks, and listen becomes
["0.0.0.0:80"], since the car's address there comes from DHCP. It is
recorded in /etc/carchomp/install.env, so --reuse and updates keep it; a
run without it closes those networks again. An install from before the
firewall had no --admin-net: its first update takes the networks from
trusted_networks instead, so nobody who used the UI loses it, except from
a network wider than /16 (or 0.0.0.0/0), which the firewall no longer
opens to (the update says so).
The car trusts the address range, not the network: every network it joins
that uses the same range is an admin network too. 192.168.1.0/24,
192.168.0.0/24 and 10.0.0.0/24 are what most routers ship with,
including many cafe and hotel ones, and every client of such a network the
car has saved would get the web UI. Give your home router an unusual subnet
(e.g. 192.168.77.0/24) and pass that. Networks wider than /16 are refused.
The reverse-path check needs the kernel's nftables fib support; the Raspberry
Pi and Debian kernels have it. Where it is missing, the firewall loads without
that one rule and logs a warning (journalctl -u carchomp-firewall) rather
than not at all.
SSH (port 22) stays reachable everywhere, so the installer turns passwords
off (/etc/ssh/sshd_config.d/50-carchomp.conf: keys only, no root login),
but only once a user has a key in ~/.ssh/authorized_keys. Until then it
prints a warning and leaves passwords on rather than lock the owner out:
ssh-copy-id a key, then run the installer again or update.
The .local name (avahi) is announced only while the car is on its hotspot
or an admin network: a NetworkManager hook
(/etc/NetworkManager/dispatcher.d/50-carchomp-avahi) starts and stops
avahi as addresses come and go.
Network access is opportunistic: nothing needs it, but maps, updates and the
clock use it when it is there. Every Wi-Fi network the car knows is retried
forever (NetworkManager gives up after four failures by default), and
carchomp-wifi.timer checks every 30 seconds for a known network in range
before falling back to the hotspot. Once up, the hotspot stays up for at
least two minutes, and as long as a client is on it, before the car looks
for known networks again.
eduroam, or any WPA-Enterprise network, is joined from the Wi-Fi panel, and never without checking the network's certificate, which is what keeps a fake access point from collecting the password:
- Best: get your institution's
.eap-configfile from https://cat.eduroam.org (or the geteduroam app/site) and give it to the Wi-Fi panel. It names the institution's CA and server, and geteduroam can issue a device certificate (EAP-TLS), so no password is stored at all. - Otherwise, enter the identity, the password and the RADIUS server's domain (your IT department publishes it); the certificate is checked against the system's CAs for that domain.
- Prefer a device or app password, where the institution offers one, to your main account password: it is stored on the car.
Enterprise networks saved by an older version without any certificate check no longer join by themselves: the installer (and so an update) turns their autoconnect off and names them. Forget each one in the Wi-Fi panel and join it again as above.
The Pi has one Wi-Fi radio, so the car is either on a network or hosting its hotspot, never both. A second USB Wi-Fi adapter (one for the hotspot, one for the road) or an LTE modem would give both, and a connection between Wi-Fi networks; neither is set up by the installer yet.
A release bundle (carchomp-<version>-<arch>.tar.gz) can be installed from
the web UI: carchompd saves the upload and starts carchomp-update.service,
which checks the bundle, unpacks it and runs its deploy/install.sh --reuse.
Progress and the installer's last lines are in
/var/lib/carchomp/update/status.json and in the UI; the full output is in
/var/lib/carchomp/update/update.log. By hand, unpack the bundle and run
sudo carchomp/deploy/install.sh --reuse.
Bundles come from tools/bundle.sh (arm64 by default; see its header) or
from a release: every v* tag builds one, plus a ready-to-flash Raspberry Pi
OS image (deploy/pi-gen/README.md).
- An install from before web updates has no updater yet: run the bundle's
installer by hand once, as above.
--reuseworks out its options. - An update that adds a package (chrony came after the first installs) must reach the internet for apt. On the car's hotspot alone it stops before changing anything, and the old version keeps running.
sudo deploy/install.sh --kiosk "$USER" --hotspot carchomp --demo
--demo runs tools/sim.py as a service in place of the sensors; everything
else is the real thing, on the real device: PostgreSQL on the CM4, the
systemd units, the kiosk, the touch UI, Wi-Fi and the hotspot. The front
buttons work too: F1 APRS, F2 Tracks, F3 Maps, O back to the map and
following. Run the installer again without --demo (with --gps and
--radio) when the hardware is in; that deletes what the simulator recorded.
The reTerminal's screen and touch panel need Seeed's drivers on a stock
Raspberry Pi OS (seeed-linux-dtoverlays, see Seeed's reTerminal wiki); the
installer does not attempt that.
After the Try it steps, which set up maps_dir and its assets, have
pmtiles (brew install pmtiles) on the PATH. Then use the Maps panel: move the map,
name the region, pick a detail level, download.
install.sh --radio sets this up. By hand: point aprs_kiss at Direwolf's
KISS port, with Direwolf fed by the SDR:
rtl_fm -f 144.39M -s 24000 - | direwolf -c direwolf.conf -r 24000 -D 1 -
(see deploy/direwolf.conf).

