Skip to content
Castor-o7Public

About

Raspberry Pi car computer: drive tracking with SmartBeaconing, APRS alerts over RTL-SDR, offline maps, a touchscreen and web UI. Rust + PostGIS + Svelte. Developed on simulators; not yet tested on hardware.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

CarChomp: take a chomp out of your drive

CarChomp

CI

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.

The CarChomp map with a green New road badge at 26 mph, as today's orange track turns away from the blue track of an earlier drive

The APRS panel beside the map, listing a red flag warning, a net bulletin, a brush fire and a highway closure

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.

Layout

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

How it works

What APRS is, and why a car computer listens to it

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.

How RF gets into the software

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 Observation from a source, 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.py all 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.direct records 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: deciding which points to keep

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:

  1. 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.
  2. Moving: wait fast_rate × high_speed ÷ speed seconds 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.
  3. Turning: if your heading has changed from the last stored point by more than turn_min + turn_slope ÷ speed degrees, 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).

Try it

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

Status

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.

Install on a Pi

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 USER opens the UI full-screen when USER's desktop session starts. Use the desktop user ("$USER" above); current Raspberry Pi OS has no pi.
  • --hotspot SSID hosts 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, where ps and shell history would keep it.
  • --gps DEVICE has 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 in raspi-config first).
  • --radio receives APRS on 144.39 MHz with an RTL-SDR dongle: carchomp-radio.service runs rtl_fm into Direwolf (/etc/carchomp/direwolf.conf, receive only: no PTT, no beacons), and aprs_kiss is pointed at Direwolf's KISS port 8001 unless already set. Reboot once so the kernel's DVB-T driver leaves the dongle alone.
  • --demo runs a simulator in place of the GPS and radio (below); it cannot be combined with --gps or --radio.
  • --admin-net CIDR also serves the web UI and the .local name to an IPv4 network, /16 or narrower, e.g. --admin-net 192.168.77.0/24 for home; repeat it for more (see Who can use it).
  • --reuse runs 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.

Who can use it

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:80 and 10.42.0.1:80 only (listen in /etc/carchomp/carchompd.toml); the hotspot address is taken whenever the hotspot is up.
  • A firewall (carchomp-firewall.service, table inet carchomp in /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_networks with 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.

Staying connected, eduroam

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-config file 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.

Updates

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. --reuse works 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.

On a reTerminal, before the GPS and radio arrive

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.

Offline maps (development)

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.

APRS on the Pi

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).

About

Raspberry Pi car computer: drive tracking with SmartBeaconing, APRS alerts over RTL-SDR, offline maps, a touchscreen and web UI. Rust + PostGIS + Svelte. Developed on simulators; not yet tested on hardware.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages