meshcore-mqtt-live-map/howto.md
2026-04-17 10:57:10 -04:00

9.5 KiB

How-To: MQTT Broker + Live Map

This guide covers two parts: stand up a MeshCore MQTT broker and point the live map at it. Current version: 1.8.4 (see VERSIONS.md).

1) MQTT broker (meshcore-mqtt-broker)

Use the broker repo from Michael Hart.

Install dependencies:

apt install git curl npm

Clone the repo:

git clone https://github.com/michaelhart/meshcore-mqtt-broker
cd meshcore-mqtt-broker

Create the data folder:

mkdir data

Create your .env:

cp .env.example .env

Edit .env and update the database path (use an absolute path that matches your checkout). While you are there, fill out the rest of the .env to your liking, and make sure AUTH_EXPECTED_AUDIENCE is set to the hostname of your MQTT server:

# Abuse Detection - Persistence
ABUSE_PERSISTENCE_PATH=/home/user/meshcore-mqtt-broker/data/abuse-detection.db

For the live map, the easiest broker auth setup is a dedicated read-only subscriber account. Add at least one SUBSCRIBER_N entry to the broker .env:

# Format: SUBSCRIBER_N=username:password:role
# Role 1 = admin (includes /internal + $SYS)
# Role 2 = full_access (recommended for most maps)
# Role 3 = limited (filtered payload fields)
SUBSCRIBER_1=meshmap:change-this-password:2

Recommended role:

  • Use role 2 for most live maps.
  • Use role 1 only if you explicitly want /internal topics or admin-only broker visibility.

Important:

  • The live map does not generate MeshCore signed auth tokens on its own.
  • Do not put a node-style username like v1_<PUBLIC_KEY> in the map unless you are also handling JWT token generation and rotation elsewhere.
  • For normal map deployments against meshcore-mqtt-broker, use a SUBSCRIBER_N username/password pair instead.

Build and run:

npm install
npm run build
npm start

Run it as a systemd service (optional)

Create /etc/systemd/system/meshcore-mqtt.service:

[Unit]
Description=MeshCore MQTT Broker
After=network.target

[Service]
Type=simple
User=user
WorkingDirectory=/home/user/meshcore-mqtt-broker

# Start the broker with node + tsx directly
ExecStart=/home/user/.nvm/versions/node/v22.21.1/bin/node /home/user/meshcore-mqtt-broker/node_modules/.bin/tsx src/server.ts

Restart=always
RestartSec=5
Environment=NODE_ENV=production

# Optional logging to files (you can drop these if you prefer journalctl only)
StandardOutput=append:/var/log/meshcore-mqtt.log
StandardError=append:/var/log/meshcore-mqtt-error.log

[Install]
WantedBy=multi-user.target

Enable and start:

sudo systemctl daemon-reload
sudo systemctl enable --now meshcore-mqtt

Cloudflare Tunnel for MQTT over TLS (optional)

Follow the broker docs for Cloudflare Tunnel setup:

https://github.com/michaelhart/meshcore-mqtt-broker/blob/main/docs/cloudflare-tunnels.md

Note: You can run plain TCP on port 1883, but using a tunnel (or another TLS front) avoids exposing the port on the host. If you do use TCP, make sure you lock it down.

2) Configure the live map

Clone the repo and enter it:

git clone https://github.com/yellowcooln/meshcore-mqtt-live-map
cd meshcore-mqtt-live-map

Copy .env.example to .env and update MQTT settings. The map expects MQTT over WebSockets by default:

MQTT_HOST=your-mqtt-host
MQTT_PORT=443
MQTT_USERNAME=meshmap
MQTT_PASSWORD=change-this-password
MQTT_TRANSPORT=websockets
MQTT_WS_PATH=/
MQTT_TLS=true
# Comma-separated list supported, e.g. meshcore/BOS/#,meshcore/CON/#
MQTT_TOPIC=meshcore/#

# MQTT online presence tuning (v1.5+)
MQTT_ONLINE_SECONDS=300
MQTT_ONLINE_STATUS_TTL_SECONDS=300
MQTT_ONLINE_INTERNAL_TTL_SECONDS=300
MQTT_ACTIVITY_PACKETS_TTL_SECONDS=300
MQTT_STATUS_OFFLINE_VALUES=offline,disconnected

Those MQTT_USERNAME / MQTT_PASSWORD values should match the subscriber account you created in the broker .env, for example:

SUBSCRIBER_1=meshmap:change-this-password:2

Authentication summary:

  • meshcore-mqtt-broker supports signed MeshCore node auth for publishers.
  • This live map is normally a read-only subscriber, so it should use a broker subscriber account instead.
  • If you use subscriber role 3, some metadata fields are filtered out. Role 2 is the recommended default for maps.

Presence behavior:

  • /status + /internal determine whether a node is shown as MQTT online.
  • /packets is tracked as feed activity and does not, by itself, mark a node online.

Optional coordinate overrides (for fixed node placement):

DEVICE_COORDS_FILE=/data/device_coords.json

Optional neighbor override controls (manual + auto):

NEIGHBOR_OVERRIDES_FILE=/data/neighbor_overrides.json
AUTO_NEIGHBOR_OVERRIDES_ENABLED=false
AUTO_NEIGHBOR_OVERRIDES_FILE=/data/neighbor_overrides.auto.json
AUTO_NEIGHBOR_ACTIVE_DAYS=7
AUTO_NEIGHBOR_MIN_EDGE_COUNT=3
AUTO_NEIGHBOR_REFRESH_SECONDS=60

Optional channel secrets file (for decrypting sender names from supported group-text packets):

CHANNEL_SECRETS_FILE=/data/channel_secrets.json

Copy channel_secrets.example.json to your chosen path and keep only the channels you want to ship.

Optional automatic backups:

BACKUP_ENABLED=true
BACKUP_INTERVAL_SECONDS=43200
BACKUP_DIR=/backup
BACKUP_RETENTION_DAYS=7

Notes:

  • backups are written as timestamped .tar.gz archives like meshmap-backup-2026-03-28T21-15-00.tar.gz
  • only files that currently exist are included
  • the default compose setup mounts ./backup:/backup
  • live state stays under /data; backups stay under /backup

Optional packet analyzer link base (used for Route Details hashes):

PACKET_ANALYZER_URL=https://analyzer.letsmesh.net/packets?packet_hash=
QR_CODE_BUTTON_ENABLED=false
PEERS_DEFAULT_LIMIT=8
MAP_BOUNDARY_MODE=radius
MAP_BOUNDARY_FILE=/data/map_boundary.json
MAP_BOUNDARY_SHOW=false
LOS_CURVATURE_ENABLED=true
LOS_CURVATURE_FACTOR=1.333333
ROUTE_ALLOW_AMBIGUOUS_ONE_BYTE_FALLBACK=false

LOS note:

  • The live map LOS tool now includes Earth curvature by default.
  • Leave LOS_CURVATURE_ENABLED unset to keep the default true.
  • Leave LOS_CURVATURE_FACTOR unset to keep the default 1.333333.

Optional polygon boundary mode:

MAP_BOUNDARY_MODE=polygon
MAP_BOUNDARY_FILE=/data/map_boundary.json
MAP_BOUNDARY_SHOW=true

Boundary files:

  • Use map_boundary.example.json as the schema reference.
  • Open tools/map-boundary-builder.html directly in a browser, or use the hosted copy at https://yellowcooln.com/map-boundary-builder/, to click out a polygon and export map_boundary.json.

Optional: enable the coverage layer by setting COVERAGE_API_URL (the Coverage button hides itself when blank):

COVERAGE_API_URL=https://coverage.example.com
# The envs below are MeshMapper-only. The legacy coverage map does not use them.
# Optional for MeshMapper coverage.php requests
COVERAGE_API_KEY=
# Show only the last N days on the map. Default 30. Set 0 to disable age filtering.
COVERAGE_MAX_AGE_DAYS=30
# MeshMapper only: fallback cooldown after HTTP 429 if the API does not report resets_in_hours
COVERAGE_RATE_LIMIT_COOLDOWN_SECONDS=3600
# MeshMapper only: local file used to serve cached coverage to users
COVERAGE_CACHE_FILE=/data/coverage_cache.json
# MeshMapper only: server-side refresh interval; default hourly
COVERAGE_SYNC_INTERVAL_SECONDS=3600
#
# Routing note: on large meshes, ambiguous 1-byte prefixes are handled
# conservatively by default. If your network lost routes after v1.7.0 and you
# need the older behavior back, set:
# ROUTE_ALLOW_AMBIGUOUS_ONE_BYTE_FALLBACK=true
# That restores the legacy closest/time-based fallback for colliding 1-byte
# prefixes.
#
# MeshMapper uses the documented domain:
# COVERAGE_API_URL=https://meshmapper.net
WEATHER_RADAR_COUNTRY_BOUNDS_ENABLED=true
# Optional override. Default is /weather/radar/country-bounds on this app.
# This is an HTTP URL path, not a filesystem directory.
WEATHER_RADAR_COUNTRY_LOOKUP_URL=/weather/radar/country-bounds
WEATHER_WIND_ENABLED=true
WEATHER_WIND_API_URL=https://api.open-meteo.com/v1/forecast
WEATHER_WIND_GRID_SIZE=3
WEATHER_WIND_REFRESH_SECONDS=180

Optional: configure Weather (Radar + Wind) behavior:

# Master radar feature flag.
WEATHER_RADAR_ENABLED=true
# Keep radar clipped to the active country bounds around map center.
WEATHER_RADAR_COUNTRY_BOUNDS_ENABLED=true
# Keep default unless you run your own lookup endpoint.
WEATHER_RADAR_COUNTRY_LOOKUP_URL=/weather/radar/country-bounds

# Enable/disable wind overlay in the Weather panel.
WEATHER_WIND_ENABLED=true
# Open-Meteo compatible wind API endpoint.
WEATHER_WIND_API_URL=https://api.open-meteo.com/v1/forecast
# Wind sampling density (1-5): higher = more arrows + more API load.
WEATHER_WIND_GRID_SIZE=3
# Wind refresh interval in seconds (minimum 30).
WEATHER_WIND_REFRESH_SECONDS=180

If both WEATHER_RADAR_ENABLED=false and WEATHER_WIND_ENABLED=false, the Weather button is hidden.

If you are using plain TCP MQTT, set:

MQTT_TRANSPORT=tcp
MQTT_TLS=false
MQTT_PORT=1883

3) Run the map

Background (detached):

docker compose up -d --build

Foreground (watch logs):

docker compose up --build

Open the UI at http://localhost:8080.

4) Verify data flow

  • /snapshot should show devices once adverts arrive.
  • /stats shows ingest totals.

Example:

curl -s http://localhost:8080/stats | jq

Notes:

  • The map only places nodes once it decodes adverts that include a valid location.
  • If no nodes appear, verify the MQTT topic and that the broker is sending /packets data with raw hex payloads.