Real-time GPS tracking and visualization for Burning Man using Meshtastic mesh networks and a WaveShare ePaper display.
- Overview
- Files & Architecture
- Quick Start (laptop test)
- Configuration
- Map Calibration
- Friend Filtering System
- Raspberry Pi Deployment
- Development
- Data Sources
Connects to a Meshtastic LoRa radio (serial or TCP), polls for node GPS positions, converts them to Burning Man clock+street addresses, and renders them on a WaveShare 7.5" black-and-red ePaper display (800Γ480).
- GPS β BRC address conversion (e.g.
"07:30 + Esplanade") - Anchor-point map projection β calibrate once with 2+ GPSβpixel pairs
- Web calibration tool (
calibrate.py) β click to set anchor positions - Friend filtering β only show whitelisted node IDs on the display
- Web management UI (
friend_server.py) β add/remove/edit friends - Exponential backoff retry on mesh connection drops
- Debug mode with test coordinates and calibration overlays
BRC-Meshtastic-ePaper-Map/
βββ display_map.py # Main loop: poll β filter β render β sleep
βββ config.yaml # User configuration (anchors, screen size, etc.)
βββ config.py # Loads config.yaml + builds MapProjection
β
βββ projection.py # GPSβpixel similarity transform (anchor-based)
βββ coordinates.py # GPSβBRC address, GPSβpixel wrappers
β
βββ mesh.py # Meshtastic connection, polling, node extraction
βββ renderer.py # PIL drawing: dots, pentagon, labels, test coords
βββ friend_store.py # Thread-safe JSON friend database
βββ friend_server.py # REST API + web UI for friend management (port 8051)
β
βββ calibrate.py # Web calibration tool (port 8050)
βββ Makefile # npm-style: make install, make test, make calibrate
βββ pyproject.toml # Package metadata, deps, tool config (package.json equivalent)
β
βββ tests/
β βββ test_projection.py # 9 unit tests for MapProjection
β
βββ media/
β βββ Map_1bit.png # BRC city map (1-bit, 465Γ371)
β βββ Font.ttc # Font for labels
β
βββ requirements.txt # Pi dependencies (tight pins)
βββ requirements-dev.txt # Laptop dependencies (relaxed pins)
β
βββ PLAN.md # Improvement plan & status
βββ SPEC-FRIENDS.md # Friend filtering spec
βββ README.md # This file
Meshtastic Radio
β
βΌ
mesh.py: connect_serial() β get_mesh_info() β add_bm_coordinates()
β β
β coordinates.py: gps_to_burning_man()
β coordinates.py: gps_to_image_coordinates()
β β
βΌ βΌ
display_map.py: filter friends by node_id (friend_store.py)
β
βΌ
renderer.py: draw_node_labels() β ePaper display
# projection.py β similarity transform from 2+ anchor points
proj = MapProjection([
(man_lat, man_lon, man_px_x, man_px_y),
(temple_lat, temple_lon, temple_px_x, temple_px_y),
])
x, y = proj.gps_to_pixel(lat, lon)The transform automatically computes scale, rotation, and translation from the anchor pairs. No hardcoded bounding boxes, angles, or radii.
No ePaper or Meshtastic radio needed. Works like npm install:
cd BRC-Meshtastic-ePaper-Map
make install # creates .venv, installs package in editable mode
make test # runs display in --debug --screen modeA window opens showing the BRC map with test point labels. See all targets:
make helpAll user settings are in config.yaml:
display:
width: 480
height: 800
image_position: [6, 400] # where Map_1bit.png is pasted on screen
sleep_seconds: 60 # mesh poll intervalanchors:
- [40.783247, -119.207884, 240, 516] # The Man: lat, lon, screen_x, screen_y
- [40.788099, -119.201500, 311, 444] # The Temple
feet_per_degree: 364000Only 2 anchors are needed to define the projection. Use make calibrate to
set pixel positions by clicking on the map.
brc:
man_lat: 40.783247
man_long: -119.207884
distance_man_esplanade: 2500
distance_streets: [400, 250, 250, 250, 250, 250, 450, 250, 250, 250, 150, 150]
brc_noon: 1.5friends_file: "friends.json"
friend_server_port: 8051The web calibration tool simulates the e-ink screen:
make calibrate
# Open http://localhost:8050- Select an anchor in the sidebar (e.g. "The Man")
- Click on the map image where that landmark appears
- Repeat for "The Temple" or another known point
- Check the yellow test point dots β they should land on correct map features
- Click Download config.yaml and paste the anchors into
config.yaml
The projection updates in real-time as you click. The pentagon is drawn from GIS trash fence vertices projected through your calibration.
By default, the ePaper shows only whitelisted friends (not all mesh nodes). An empty friends list shows nothing β explicit opt-in.
# Starts automatically with display_map.py
# Open http://<pi-ip>:8051Two panels:
- Left β My Friends: List with inline name/short-name/notes editing, add form, delete buttons
- Right β Mesh Nodes: Live mesh nodes with "+ Add" buttons to quickly whitelist a node
| Method | Path | Description |
|---|---|---|
GET |
/api/friends |
List all friends |
POST |
/api/friends |
Add a friend {"node_id":"!abcd","name":"Alice"} |
PUT |
/api/friends/<id> |
Edit fields {"name":"New name"} |
DELETE |
/api/friends/<id> |
Remove a friend |
GET |
/api/nodes |
Live mesh nodes with is_friend flag |
friends.json β thread-safe JSON with atomic writes. Each friend record:
{
"node_id": "!abcd1234",
"name": "Alice",
"short_name": "AL",
"notes": "Camp Quark @ 7:30 & C",
"added_at": "2026-08-20T12:00:00Z",
"last_seen": "2026-08-20T14:30:00Z"
}python3 display_map.py --no-friends # shows all mesh nodes- Raspberry Pi (Zero 2W, 3, or 4)
- WaveShare 7.5" ePaper HAT (800Γ480, black/red)
- Meshtastic radio (serial or TCP)
git clone <repo>
cd BRC-Meshtastic-ePaper-Map
python3 -m venv .venv
.venv/bin/pip install -r requirements.txtsudo raspi-config
# Interface Options β SPI β Enable.venv/bin/python3 display_map.pyThe friend management server starts automatically at port 8051.
Access it from your phone at http://<pi-ip>:8051.
| Flag | Description |
|---|---|
-d, --debug |
Use test coordinates instead of Meshtastic |
-s, --screen |
Show on desktop window instead of ePaper |
-c, --calibrate |
Print GPSβpixel conversion details |
--no-friends |
Show all mesh nodes (disable friend filtering) |
make install # create venv + pip install -e .[dev] (like npm install)
make install-pi # same but with RPi.GPIO + spidev for Raspberry Pi
make test # run display in --debug --screen mode
make calibrate # launch calibration tool β http://localhost:8050
make pytest # run unit tests
make clean # remove venv, caches, build artifacts
make help # show all targetspyproject.toml is the Python equivalent of package.json. It defines:
- Project name, version, description
- Dependencies with relaxed version pins
[dev]extras: pytest[pi]extras: RPi.GPIO, spidev- Ruff formatter and pytest config
The -e flag in pip install -e . installs in editable mode β changes
to .py files take effect immediately, no reinstall needed.
make pytest # or: .venv/bin/pytest tests/ -v9 tests covering MapProjection: identity, scale, rotation (north-up, east-right), round-trip accuracy, anchor reproduction, input validation, and diagnostic output.
Auto-formatted with ruff. Pre-existing warnings about geopy imports (not installed in dev env) and ast-grep "unchecked-throwing-call" rules are expected in this environment.
- JSON over SQLite for friends β simpler, human-readable, zero-dependency. Migration to SQLite is straightforward if needed (sqlite3 is in stdlib).
- Background thread for web server β avoids separate deployment. Uses
http.serverfrom stdlib (no Flask dependency). - Anchor-point projection β replaced bounding-box math after discovering the map image is geographic north-up, not BRC-grid-up.
- Similarity transform β scale, rotation, and translation from 2+ anchors. Works for any screen size.
All GPS coordinates sourced from the official 2026 Burning Man GIS data:
innovate-GIS-data/2026/GeoJSON/cpns.geojsonβ The Man, Temple, Center Camp, and 40+ named pointsinnovate-GIS-data/2026/GeoJSON/plazas.geojsonβ G and B street plaza centroidsinnovate-GIS-data/2026/GeoJSON/trash_fence.geojsonβ pentagon verticesinnovate-GIS-data/2026/GeoJSON/street_lines.geojsonβ street centerlines
19 landmarks are embedded in calibrate.py and renderer.py as test points.
