|
|
||
|---|---|---|
| .github | ||
| backend | ||
| data | ||
| tests | ||
| tools | ||
| .env.example | ||
| .gitignore | ||
| AGENTS.md | ||
| ARCHITECTURE.md | ||
| channel_secrets.example.json | ||
| CONTRIBUTING.md | ||
| docker-compose.yaml | ||
| docs.md | ||
| example.gif | ||
| example2.gif | ||
| howto.md | ||
| LICENSE | ||
| map_boundary.example.json | ||
| pytest.ini | ||
| README.md | ||
| requirements-dev.txt | ||
| VERSION.txt | ||
| VERSIONS.md | ||
Mesh Live Map
Version: 1.8.4 (see VERSIONS.md)
Live MeshCore traffic map that renders nodes, routes, and activity in real time on a Leaflet map. The backend subscribes to MQTT over WebSockets+TLS or TCP, decodes MeshCore packets with the official @michaelhart/meshcore-decoder, and streams updates to the browser via WebSockets.
Live example sites: https://live.bostonme.sh/ - Greater Boston Mesh Map (reference)
Other community maps (versions may differ):
- https://meshcore-map.ctmesh.org/ - CTMesh MeshCore Map
- https://livemap.wcmesh.com/ - West Coast Mesh
- https://map.eastmesh.au/ - Aus Eastern Mesh Live Map
- https://mapa.meshcore.cz/ - Czech Republic
- https://map.meshcorebayreuth.de/ - Bayreuth Live Map
Features
- Live node markers with roles (Repeater, Companion, Room Server, Unknown)
- MQTT online indicator (green outline + popup status) based on MQTT
status/internaltopics, with conservative role detection from explicit MQTT role fields only - Animated route/trace lines
- Hidden arcade Pacman route visualization mode for live route direction
- Dev route inspection: click a route line in dev (
PROD_MODE=false) to log hop-by-hop details in the browser console, including resolvedpoint_id/point_labeldata (PR #14, credit: https://github.com/sefator) - Heat map for the last 10 minutes of message activity (includes adverts)
- Persistent device state and optional trails (disable with
TRAIL_LEN=0) - 24-hour route history tool with volume-based coloring, click-to-view packet details, a heat-band slider, and a link-size slider
- History panel can be dismissed with an X without hiding history lines (re-open via History tool)
- Peers tool showing incoming/outgoing neighbors with on-map lines (blue = incoming, purple = outgoing)
- Coverage layer from the legacy coverage map API or the new MeshMapper Coverage API (button hidden when not configured)
- MeshMapper coverage viewport sync reuses cached rectangles instead of recreating every visible square on each pan/zoom, which keeps the coverage layer responsive on larger meshes
- Weather tool panel with independent Radar and Wind toggles
- Update available banner (git local vs upstream) with dismiss
- UI controls: legend toggle, dark map, topo map, units toggle (km/mi), labels toggle, hide nodes, heat toggle
- Share button that copies a URL with current view + settings
- URL parameters to open the map at a specific view (center, zoom, toggles)
- Node search by name or public key
- Node popups can copy the full public key from the short key shown under the node name, with an optional MeshCore contact QR modal that shows the node name and a clickable truncated key
- Adjustable node size slider (defaults from env, saves locally)
- LOS tool with elevation profile + peak markers, hover sync, realtime draggable endpoints (Shift+click or long‑press nodes), and Earth-curvature-aware blockage checks
- Embeddable metadata (Open Graph/Twitter tags) driven by env vars
- Preview image renders in-bounds device dots for shared links
- Route pruning via neighbor-aware closest-hop selection + max hop distance (configurable)
- Route lines are derived from decoded packet paths only (no MQTT observer/receiver fallback)
- First-hop collision fix prefers the closest repeater/room to the sender (Issue #11)
- Mixed hop-prefix support for path decoding (
AB,ABCD, andABCDEF, including mixed networks during rollout) - Optional legacy route fallback for colliding 1-byte hop prefixes via
ROUTE_ALLOW_AMBIGUOUS_ONE_BYTE_FALLBACK=true - Propagation panel lives on the right and keeps the last render until you generate a new one (click an origin marker to remove it)
- Propagation tool supports adjustable TX antenna gain (dBi), and now defaults Rx AGL to 1m
- Installable PWA (manifest + service worker) for Add to Home Screen
- Click the logo to hide/show the left HUD panel while tools stay open
Project Structure
backend/app.py: FastAPI server wiring, MQTT lifecycle, WS broadcastbackend/config.py: environment configurationbackend/state.py: shared in-memory state + dataclassesbackend/decoder.py: payload parsing + multibyte MeshCore decoder integrationbackend/los.py: LOS math + elevation helpersbackend/history.py: route history persistence + pruningbackend/weather.py: weather radar country-bounds lookup APIbackend/static/index.html: HTML shell + template placeholdersbackend/static/styles.css: UI stylesbackend/static/app.js: map logic + UI controlsbackend/static/sw.js: PWA service workerdocker-compose.yaml: runtime configuration (reads from.env)data/: runtime state (created at first run)
Quick Start
- Clone the repo and enter it:
git clone https://github.com/yellowcooln/meshcore-mqtt-live-map
cd meshcore-mqtt-live-map
- Copy env template:
cp .env.example .env
- Edit
.envwith your MQTT broker and site metadata.- See
howto.mdfor a step-by-step guide to setting up the MQTT server and this live map.
- See
- Build and run:
docker compose up -d --build
- Open:
http://localhost:8080/(or yourWEB_PORT)
Configuration (.env)
Debugging:
DEBUG_PAYLOAD(verbose decode logs)DEBUG_PAYLOAD_MAX/PAYLOAD_PREVIEW_MAX(log truncation limits)DEBUG_LAST_MAX/DEBUG_STATUS_MAX(debug endpoint entry caps)
Storage + server:
STATE_DIR(persisted state path)BACKUP_ENABLED(enable automatic.tar.gzbackups)BACKUP_INTERVAL_SECONDS(seconds between backups; default43200/ 12 hours)BACKUP_DIR(backup archive directory; default/backup)BACKUP_RETENTION_DAYS(delete backup archives older than N days;0disables pruning)STATE_FILE(full state file path override)DEVICE_ROLES_FILE(optional role override JSON file)DEVICE_COORDS_FILE(optional coordinate override JSON file; default/data/device_coords.json)NEIGHBOR_OVERRIDES_FILE(optional JSON mapping for neighbor overrides)CHANNEL_SECRETS_FILE(optional JSON file of MeshCore channel secrets for decrypting sender names from group text packets)STATE_SAVE_INTERVAL(seconds between state saves)WEB_PORT(host port for the web UI)PROD_MODE(true to require a token for API + WS)PROD_TOKEN(required token; send via?token=orAuthorization: Bearer)
Backups:
- when enabled, the server writes timestamped
meshmap-backup-YYYY-MM-DDTHH-MM-SS.tar.gzarchives toBACKUP_DIR - backup archives are separate from
/databy default; the compose file mounts./backup:/backup - with the default backup settings (
12hours,7days retention), expect under30 MBof backup storage on a network around250nodes
Turnstile protection (prod-only):
TURNSTILE_ENABLED(requiresPROD_MODE=true)TURNSTILE_SITE_KEYTURNSTILE_SECRET_KEYTURNSTILE_API_URLTURNSTILE_TOKEN_TTL_SECONDSTURNSTILE_BOT_BYPASS(allowlist embed bots like Discord)TURNSTILE_BOT_ALLOWLIST(comma-separated user-agent tokens; default:discordbot,twitterbot,slackbot,facebookexternalhit,linkedinbot,telegrambot,whatsapp,skypeuripreview,redditbot)
Site metadata (page title + embeds):
SITE_TITLESITE_DESCRIPTIONSITE_OG_IMAGE(optional; leave blank to omit embed image)SITE_URL(public URL)SITE_ICONSITE_FEED_NOTECUSTOM_LINK_URL(optional extra HUD link; hidden when blank)PACKET_ANALYZER_URL(optional analyzer base URL for Route Details hashes; e.g.https://analyzer.letsmesh.net/packets?packet_hash=)QR_CODE_BUTTON_ENABLED(show aGenerate QR Codebutton in node popups that opens a theme-aware MeshCore-compatible contact QR modal; defaultfalse)PEERS_DEFAULT_LIMIT(optional default number of incoming/outgoing peers returned by/peers/{device_id}; default8)MAP_BOUNDARY_MODE(radiusorpolygon; defaultradius)MAP_BOUNDARY_FILE(JSON file used whenMAP_BOUNDARY_MODE=polygon; default/data/map_boundary.json)MAP_BOUNDARY_SHOW(draw the active radius/polygon boundary overlay on the map)DISTANCE_UNITS(kmormi, default display units)NODE_MARKER_RADIUS(default node marker size in pixels)
MQTT:
MQTT_HOSTMQTT_PORTMQTT_USERNAME(formeshcore-mqtt-broker, usually a broker-sideSUBSCRIBER_Nusername, notv1_<PUBLIC_KEY>)MQTT_PASSWORD(matching subscriber password from the broker config)MQTT_TRANSPORT(tcporwebsockets)MQTT_WS_PATH(usually/or/mqtt)MQTT_TLS(true)MQTT_TLS_INSECURE(allow invalid TLS certs)MQTT_CA_CERT(custom CA bundle path)MQTT_CLIENT_ID(optional client id override)MQTT_TOPIC(e.g.meshcore/#ormeshcore/#,other/topic/+for multiple topics)
Coverage layer:
COVERAGE_API_URL(legacy coverage-map base URL, orhttps://meshmapper.net; button hidden when blank)COVERAGE_API_KEY(MeshMapper only; optional key forhttps://meshmapper.net/coverage.php; not used by legacy coverage maps)COVERAGE_MAX_AGE_DAYS(MeshMapper only; default30; only coverage from the last N days is sent to the map, while MeshMapper can still cache the full upstream dataset locally; not used by legacy coverage maps)COVERAGE_RATE_LIMIT_COOLDOWN_SECONDS(MeshMapper only; fallback cooldown after HTTP 429 if the API does not reportresets_in_hours)COVERAGE_CACHE_FILE(MeshMapper only; local JSON file served to users after server-side sync)COVERAGE_SYNC_INTERVAL_SECONDS(MeshMapper only; how often the server refreshes the local coverage cache file, default hourly)
Routing accuracy:
- Ambiguous 1-byte hop prefixes are now handled conservatively on large meshes.
- If multiple nodes share the same first byte, the map no longer guesses from broad closest/time-based fallbacks.
- Colliding 1-byte hops only resolve when there is stronger evidence such as a unique candidate or known neighbor/manual adjacency.
ROUTE_ALLOW_AMBIGUOUS_ONE_BYTE_FALLBACK=truerestores the pre-v1.7.0fallback behavior for networks that need the older route rendering.
Weather overlay:
WEATHER_RADAR_ENABLED: master switch for radar support. If bothWEATHER_RADAR_ENABLED=falseandWEATHER_WIND_ENABLED=false, the Weather button is hidden.WEATHER_RADAR_COUNTRY_BOUNDS_ENABLED: settrueto keep radar inside the country around the map center; setfalsefor unrestricted radar tiles.WEATHER_RADAR_COUNTRY_LOOKUP_URL: keep default (/weather/radar/country-bounds) unless you run your own country-bounds endpoint.WEATHER_WIND_ENABLED: settrueto show Wind in the Weather panel, orfalseto disable wind entirely.WEATHER_WIND_API_URL: endpoint for wind data (default is Open-Meteo; custom APIs must provide current wind speed/direction).WEATHER_WIND_GRID_SIZE: number of wind samples per side (1-5); larger grid gives more arrows but increases API load.WEATHER_WIND_REFRESH_SECONDS: wind refresh interval (seconds, minimum30); increase this to reduce API calls.
Device + route tuning:
DEVICE_TTL_HOURS(advert/device stale window; default96)PATH_TTL_SECONDS(path stale window; default172800)TRAIL_LEN(points per device trail;0disables trails)ROUTE_TTL_SECONDSROUTE_PATH_MAX_LEN(skip oversized path-hash lists)ROUTE_PAYLOAD_TYPES(packet types used for live routes)ROUTE_MAX_HOP_DISTANCE(km; prunes unrealistic hops)ROUTE_INFRA_ONLY(true = only repeaters/rooms in route lines)MESSAGE_ORIGIN_TTL_SECONDS
History overlay:
ROUTE_HISTORY_ENABLEDROUTE_HISTORY_HOURSROUTE_HISTORY_MAX_SEGMENTSROUTE_HISTORY_COMPACT_INTERVALROUTE_HISTORY_FILEROUTE_HISTORY_PAYLOAD_TYPESROUTE_HISTORY_ALLOWED_MODES(comma-separated route modes; defaultpath)HISTORY_LINK_SCALE(default history line weight multiplier)
Heat + online status:
HEAT_TTL_SECONDSMQTT_ONLINE_SECONDS(legacy/global fallback TTL for MQTT presence)MQTT_ONLINE_STATUS_TTL_SECONDS(how long/statuskeeps a node connected)MQTT_ONLINE_INTERNAL_TTL_SECONDS(how long/internalkeeps a node connected)MQTT_ACTIVITY_PACKETS_TTL_SECONDS(how long/packetscounts as feeding activity)MQTT_STATUS_OFFLINE_VALUES(comma-separated status values that force offline, even inside TTL)MQTT_ONLINE_TOPIC_SUFFIXES(legacy compatibility setting; primary online source is status/internal TTLs)MQTT_SEEN_BROADCAST_MIN_SECONDSMQTT_ONLINE_FORCE_NAMES(comma-separated names to force as MQTT online; also excluded from peers)
Update checks:
GIT_CHECK_ENABLED(show update banner if repo is behind)GIT_CHECK_FETCH(fetch before comparing)GIT_CHECK_PATH(path to git repo in the container)GIT_CHECK_INTERVAL_SECONDS(defaults to 43200 = 12h)
Map + LOS:
MAP_START_LAT/MAP_START_LON/MAP_START_ZOOM(default map view)MAP_DEFAULT_LAYER(light,dark, ortopo; localStorage overrides)MAP_RADIUS_KM(0disables radius filtering;.env.exampleuses241.4km ≈ 150mi)MAP_RADIUS_SHOW(truedraws the radius debug circle)LOS_ELEVATION_URL(elevation API for LOS tool)LOS_ELEVATION_PROXY_URL(server proxy for client-side LOS elevation fetches)LOS_SAMPLE_MIN/LOS_SAMPLE_MAX/LOS_SAMPLE_STEP_METERSELEVATION_CACHE_TTL(seconds)LOS_CURVATURE_ENABLED(defaulttrue; include Earth curvature in LOS calculations)LOS_CURVATURE_FACTOR(default1.333333; effective Earth radius multiplier used by the LOS tool)LOS_PEAKS_MAX(max peaks shown on LOS profile)
Decoder helpers:
DECODE_WITH_NODE(toggle Node-based MeshCore decoder usage)NODE_DECODE_TIMEOUT_SECONDSDIRECT_COORDS_MODE(topicorpayload)DIRECT_COORDS_TOPIC_REGEX(topic matcher for direct coords)DIRECT_COORDS_ALLOW_ZERO(allow0,0coords iftrue)
Common Commands
- Rebuild/restart:
docker compose up -d --build - Logs:
docker compose logs -f meshmap-live - Tests:
pip install -r requirements-dev.txt && pytest -q - Snapshot:
curl -s http://localhost:8080/snapshot - Stats:
curl -s http://localhost:8080/stats
Production Token
Enable protection by setting:
PROD_MODE=true
PROD_TOKEN=<random-string>
Turnstile protection is also gated by PROD_MODE=true. If PROD_MODE=false,
Turnstile stays off even when TURNSTILE_ENABLED=true.
When Turnstile is enabled, browser sessions can authenticate the map + WebSocket
with a Turnstile auth token (meshmap_auth cookie or ?auth= on /ws), but
protected API routes (/snapshot, /api/nodes, /peers/{id}) still require
PROD_TOKEN.
Ensure PROD_MODE/PROD_TOKEN are set in .env (docker-compose passes them through).
Generate a token:
openssl rand -hex 32
Use it:
- HTTP:
http://host:8080/snapshot?token=YOUR_TOKEN - WS:
ws://host:8080/ws?token=YOUR_TOKEN - Or send
Authorization: Bearer YOUR_TOKEN
Notes
- The map can only draw routes for hops that appear in your MQTT feed.
- To see full paths, the feed must include Path/Trace packets (payload types 8/9).
- Runtime state is persisted to
data/state.json. - MQTT disconnects are handled; the client will reconnect when the broker returns.
- When using Michael Hart's
meshcore-mqtt-broker, the map should usually log in with a brokerSUBSCRIBER_Naccount such asSUBSCRIBER_1=meshmap:change-this-password:2. - The map does not mint MeshCore JWT auth tokens by itself, so node-style
publisher auth (
v1_<PUBLIC_KEY>) is not the normal setup for this app. - Subscriber role
2is the recommended broker role for most maps; role1is only needed if you explicitly want/internaltopics or other admin-only broker visibility. - MQTT connectivity (
MQTT online) is based on/status+/internal;/packetsis treated as feed activity and does not by itself mark a node online. - If a node is still MQTT-online but has stopped sending fresh location packets, the map keeps its last known position visible until MQTT presence expires.
- Live route IDs are observer-aware (
message_hash:receiver_id) so the same message seen by multiple MQTT observers does not overwrite active lines. - Line-of-sight tool: click LOS tool and add pins to build a chained path, or Shift+click nodes to place LOS pins from existing nodes. Drag endpoints or click a pin and then click the map to move that specific point. Heights are stored per pin.
- On mobile, long‑press a node to select it for LOS.
- LOS elevations are fetched via
/los/elevationsand LOS/relay math runs client-side (with/losfallback). - LOS now includes Earth curvature by default using an effective Earth radius factor of
1.333333, unless you override the LOS curvature envs. - History tool always loads off (use the button or
history=onin the URL). - Peers tool uses dedicated rolling peer-history buckets so 24h counts stay accurate even on high-volume meshes; peer links are still counted from route
point_idseven when a hop could not be drawn on the map, and forced MQTT listeners are excluded from peer lists. - URL params override stored settings:
lat,lon/lng/long,zoom,layer,history,heat,coverage,weather,weather_radar,weather_wind,labels,nodes,legend,menu,units,history_filter. - Dark map also darkens node popups for readability.
- Route styling uses payload type: 2/5 = Message (blue), 8/9 = Trace (orange), 4 = Advert (green).
- Turnstile browser auth (
meshmap_auth/?auth=) is for map + WS session flow; protected API endpoints still requirePROD_TOKEN. - If hop hashes collide, the backend prefers known neighbors (or overrides) before picking the closest hop and pruning beyond
ROUTE_MAX_HOP_DISTANCE. - Route hop prefixes can now be 1-byte, 2-byte, or 3-byte; Show Hops displays
Prefix: AB/Prefix: ABCD/Prefix: ABCDEF. - Device pruning can use both stale windows together (
DEVICE_TTL_HOURSandPATH_TTL_SECONDS). - Coordinates at
0,0(including string values) are filtered from devices, trails, and routes. - With Turnstile enabled, common embed bots (Discord, Slack, etc.) can be
allowlisted via
TURNSTILE_BOT_BYPASSandTURNSTILE_BOT_ALLOWLIST.
API
The backend exposes a nodes API for external tools (e.g. MeshBuddy):
GET /api/nodes?token=YOUR_TOKEN- Default response:
{"data":[...], "nodes":[...]} - Optional:
format=nestedreturns{"data":{"nodes":[...]}} updated_sinceapplies delta filtering automatically- Optional:
mode=full(orall/snapshot) forces full-list response
- Default response:
Example:
https://your-host/api/nodes?token=YOUR_TOKEN
https://your-host/api/nodes?token=YOUR_TOKEN&updated_since=2025-01-01T12:00:00Z
https://your-host/api/nodes?token=YOUR_TOKEN&format=nested
https://your-host/api/nodes?token=YOUR_TOKEN&mode=full
Each node includes:
public_key, name, device_role (1/2/3), last_seen (ISO), timestamp (epoch), and location with latitude/longitude.
Peer summary:
GET /peers/{device_id}?token=YOUR_TOKEN
Boundary mode:
- Default behavior remains radius-based filtering.
- Set
MAP_BOUNDARY_MODE=polygonto filter nodes, routes, and history against a polygon fromMAP_BOUNDARY_FILE. - Use
map_boundary.example.jsonas the file format reference. - The repo includes a standalone builder at
tools/map-boundary-builder.html; a hosted copy is available at https://yellowcooln.com/map-boundary-builder/. - Use either copy to click points on the map and export
map_boundary.json.- Returns incoming/outgoing neighbors with counts/percentages from route history.
License
This project was vibe-coded with Codex—please expect rough edges and the occasional bug.

