sighook.im

· All notes

Cartochron: reading recorded locations without inventing movement

Imagine watching an anthill for a month. They have their routines: same paths, same corners, occasional detours into unknown territory.

We’re ants too, in that sense — just equipped with Android phones and an unhealthy interest in location telemetry.

Watch those routes long enough and the questions get interesting. Do we keep taking the exact same circuits? When two trails cross, were we actually there at the same time, or did one of us pass through five minutes later while nobody was watching?

So I wrote Cartochron: a small Python receiver, SQLite database, and Leaflet map for collecting location observations and poking around their history.

Not another GPS dashboard. I wanted to look back through recordings, compare different runs, find recurring places, and examine where two sets of observations come close in space and time.

Preferably without the map inventing adventures between two GPS fixes.

First, some imaginary ants

Before involving real phones, try the included demonstration:

git clone https://github.com/cartochron/cartochron.git
cd cartochron
make demo

Open http://127.0.0.1:8877/.

The demo has several synthetic recordings: different routes, stale positions, missing intervals, and a couple of suspiciously convenient crossings.

Try demo-oneplus-morning in History. There’s a gap in the recording. Inspect its endpoints; the map doesn’t know what happened in between.

Then compare demo-oneplus-evening with demo-lg-evening. Look at the reported coordinates and their receipt times. For a different kind of coincidence, compare routes that cross geographically but were recorded at different times.

There’s more under Compare routes, What was here?, and the history overlap searches. The in-app Guide explains the controls.

All synthetic. Nobody’s actual movements, no private locations, and no unfortunate neighbour accidentally becoming a research specimen.

The receiver

For longer recordings, I use Docker Compose with a persistent SQLite database.

From the Cartochron checkout:

install -d -m 700 data

( umask 077; printf 'operator:%s\n' \
    "$(openssl rand -hex 32)" > data/auth )

( umask 077; printf 'CARTOCHRON_UID=%s\nCARTOCHRON_GID=%s\n' \
    "$(id -u)" "$(id -g)" > .env )

docker compose up --build -d

Open http://127.0.0.1:8765/ and use the credentials from data/auth.

That’s the receiver. It stores observations in data/observations.sqlite3, serves the map, and survives container restarts without losing the recording history.

It listens on host loopback. If you want it on a VPS, put HTTPS in front of it rather than exposing the backend and its Basic-auth credentials over the Internet.

Feed the beast

Cartochron doesn’t care where an observation came from, provided an adapter speaks its HTTP API.

For ordinary GPS recording, there’s cartochron-gpsd. It reads GPSD reports and forwards valid position fixes to the receiver.

The Android side is familiar from my Kismet wardriving setup: install GPSd Forwarder or gpsdRelay from F-Droid on a spare phone. gpsdRelay has a configurable NMEA generation interval (in milliseconds), worth trying for longer recordings. It controls requested GPS location updates, but raw NMEA sentences can still be relayed as they arrive; check the actual sampling and battery use on your phone.

One phone works. Two are more interesting.

Android phone A --- UDP/29998 ---+
                                 |
Android phone B --- UDP/29999 ---+-- GPSD
                                      |
                                cartochron-gpsd
                                      |
                                      v
                               Cartochron
                                SQLite + map

Clone the adapter alongside the receiver:

cd ..
git clone https://github.com/cartochron/cartochron-gpsd.git
cd cartochron-gpsd

cp sources.toml.example sources.toml
cp .env.example .env

The sample sources.toml already contains two input channels. Change their names if you like, and assign each phone its own UDP port.

In .env, set GPSD_BIND_IP to the laptop’s address reachable by the phones, and set COLLECTOR_UID and COLLECTOR_GID to your id -u and id -g values. The collector uses the receiver’s private authentication file from the sibling checkout.

Then:

make check-config
docker compose up --build -d
docker compose exec gpsd gpspipe -w -n 30

Configure each phone’s forwarder with the corresponding host address and UDP port. Check that GPSD is actually receiving valid fixes, then watch Cartochron’s Monitor.

A small gotcha: my Kismet wardriving stack uses GPSD ports 2947 and 29998 too. Stop that stack before starting this one with the defaults.

Two daemons, one socket. Nobody wins.

The full GPSD Compose guide covers enrollment, additional phones, and troubleshooting. Keep the UDP inputs on a trusted network: a configured input channel isn’t authentication of the phone behind it.

Give it some history

Take the same little circuit a few times, preferably with different stops and at different hours. Use your own devices, or devices whose owners have explicitly agreed to the recording.

A few days of observations become much more interesting than watching a marker move in real time.

In Monitor, you can follow the latest run for a channel. In History, older runs remain separate. Restarting a collector starts a new recording rather than stitching yesterday’s last coordinate to today’s first one.

You can compare routes, inspect individual observations, search a place, or look for nearby reports from two different recording runs.

Here’s where timestamps become useful. Two paths crossing on a map might be interesting. Two reported positions close together within a few minutes are more interesting. Neither alone tells us that two people met, but now there’s something concrete to examine.

Cartochron also keeps the server’s receipt time separate from a GPSD-reported measurement time. An old position arriving today shouldn’t quietly become a fresh GPS measurement.

The distinction matters when a feed disconnects, a phone wakes up, or an adapter reconnects. Missing observations stay missing. The map doesn’t get to fill in the blanks just because a straight line looks nicer.

Other ways to collect

There’s also cartochron-metasploit, an adapter for authorized Android Meterpreter sessions.

It uses the session’s reported network-provider location rather than GPSD. Those positions may be cached, and their original measurement time is unknown. Useful for certain laboratory experiments, but not something to confuse with a fresh GPS fix.

The receiver and both adapters are independent repositories. The Cartochron handbook has the detailed investigation workflows; the operations guide covers backups, authentication, and deployment.

Keep the recordings and database private. A month’s location history is rather more personal than the cute little dots suggest.