9.6 KiB
MeshCore Analytics
A real-time mesh network analytics platform for MeshCore networks. Ingests MQTT packets via mctomqtt, decodes them with @michaelhart/meshcore-decoder, stores them in TimescaleDB, and presents a live RF/signals-intelligence-style dashboard with node mapping, coverage viewshed polygons, packet statistics, and a decoded live feed.
Features
- Live map of repeater nodes with animated packet arcs between observers
- RF coverage viewshed polygons computed per node using SRTM terrain data
- Gap detection overlay showing areas without coverage
- Decoded live packet feed (Adverts, Group messages, DMs, ACKs, Trace routes)
- Statistics page with charts: packet rates, unique radios, hop distribution, top chatters
- 28-day rolling packet retention via TimescaleDB
- Multi-observer support: duplicate packets deduplicated by hash
Roadmap
Phase 1 — Core platform (complete)
- MQTT ingestion via
mctomqttwith multi-observer support - Packet decoding with
@michaelhart/meshcore-decoder - TimescaleDB storage with 28-day rolling retention
- Live WebSocket feed to browser clients
- React dashboard: node map, animated packet arc trails, decoded live feed
- TX/RX deduplication by packet hash
Phase 2 — RF coverage (complete)
- Viewshed worker: SRTM terrain-aware radio horizon computation per repeater
- Coverage polygons served as GeoJSON and rendered on the map
- Gap detection overlay highlighting areas with no coverage
- Dynamic radius calculation based on node elevation
- UK mainland clipping to remove sea coverage artefacts
Phase 3 — Repeater owner portal (planned)
- Ed25519 JWT authentication for repeater owners
- Owner-facing dashboard for their own nodes
- Planned node placement tool: drop a marker, preview estimated coverage before deploying
Phase 4 — Public website (complete)
- Separate public-facing site at a different hostname from the analytics dashboard
- Node documentation, install guides, MQTT connection instructions
- Network statistics page with charts
Quick Start
# 1. Clone and enter the project
git clone https://github.com/youruser/meshcore-analytics.git
cd meshcore-analytics
# 2. Copy and configure environment
cp .env.example .env
# Edit .env — at minimum set POSTGRES_PASSWORD, JWT_SECRET, MQTT_PASSWORD
# 3. Start everything
docker compose up -d
# 4. Check logs
docker compose logs -f app
The app will be available at http://localhost:3000.
To expose it publicly, configure a Cloudflare Tunnel (see below) or reverse proxy of your choice.
Environment Variables
Copy .env.example to .env and fill in your values. All variables used by the app:
| Variable | Default | Description |
|---|---|---|
POSTGRES_DB |
meshcore |
TimescaleDB database name |
POSTGRES_USER |
meshcore |
TimescaleDB user |
POSTGRES_PASSWORD |
(required) | TimescaleDB password |
MQTT_BROKER_URL |
ws://mosquitto:9001 |
Mosquitto WebSocket URL (internal) |
MQTT_USERNAME |
backend |
MQTT client username |
MQTT_PASSWORD |
(required) | MQTT client password |
REDIS_URL |
redis://redis:6379 |
Redis URL for WebSocket pub/sub |
JWT_SECRET |
(required) | Secret for JWT verification |
ALLOWED_ORIGINS |
http://localhost:3000,http://localhost:3001 |
Comma-separated browser origins allowed for CORS and WebSocket |
VITE_APP_HOSTNAME |
(blank — always shows dashboard) | If set, only this hostname serves the analytics dashboard; all others serve the public website layout |
MESHCORE_CHANNEL_SECRETS |
(blank) | Comma-separated channel secrets for decrypting GroupText packets. Format: name:hex or bare hex. The default MeshCore public channel key is always included. |
OPENTOPODATA_API |
https://api.opentopodata.org |
Elevation API endpoint for viewshed computation |
CLOUDFLARE_TUNNEL_TOKEN |
(optional) | Cloudflare Zero Trust tunnel token |
PORT |
3000 |
Internal app port |
Mosquitto Setup
Mosquitto is configured for WebSocket-only access with password authentication. After first starting the stack, add a password for the backend client and any node clients:
# Add the backend client password (must match MQTT_PASSWORD in .env)
docker exec meshcore-analytics-mosquitto-1 \
mosquitto_passwd -b /mosquitto/config/passwd backend your_password
# Add a node client
docker exec meshcore-analytics-mosquitto-1 \
mosquitto_passwd -b /mosquitto/config/passwd node1 another_password
docker compose restart mosquitto
Edit mosquitto/acl to grant the appropriate topic permissions to each user.
Cloudflare Tunnel (optional)
To expose the app and MQTT broker publicly without opening firewall ports:
- Go to Cloudflare Zero Trust → Networks → Tunnels
- Create a tunnel and copy the token
- Add to
.env:CLOUDFLARE_TUNNEL_TOKEN=<token> - Start with the tunnel profile:
docker compose --profile tunnel up -d - Configure public hostnames in the Cloudflare dashboard:
app.yourdomain.com→http://app:3000mqtt.yourdomain.com→http://mosquitto:9001(WebSocket)
MQTT Topic Structure
The backend subscribes to meshcore/#. MeshCore devices publish via mctomqtt to topics of the form:
meshcore/<IATA>/<observer-public-key>/packets # received/transmitted packets
meshcore/<IATA>/<observer-public-key>/status # node status advertisement
Payloads are JSON envelopes containing a raw hex field (the MeshCore packet) plus metadata (RSSI, SNR, direction, hash, etc.).
Architecture
MeshCore Devices
│ LoRa RF
▼
mctomqtt (on node machine)
│ MQTT over WebSocket/TLS
▼
Mosquitto ─────────────────────────────── (optional Cloudflare Tunnel)
│ subscribe meshcore/#
▼
App (Node.js/TypeScript)
│
├─ meshcore-decoder → TimescaleDB (packets · 28d retention)
│ (nodes, planned_nodes, observers · persistent)
│
├─ Redis pub/sub
│
├─ WebSocket → Frontend live updates
└─ REST API /api/*
│
└─ Static Frontend (React + Leaflet + deck.gl)
Viewshed Worker (Python)
├─ Redis job queue
├─ SRTM terrain tiles (auto-downloaded)
└─ node_coverage table → coverage polygons served via /api/coverage
Services
| Service | Image | Purpose |
|---|---|---|
timescaledb |
timescale/timescaledb:latest-pg16 |
Time-series packet storage with retention |
mosquitto |
eclipse-mosquitto:2 |
MQTT broker (WebSocket only) |
redis |
redis:7-alpine |
WebSocket fan-out pub/sub and job queue |
app |
Built from Dockerfile |
Backend API + frontend static files |
viewshed-worker |
Built from viewshed-worker/Dockerfile |
Terrain-aware RF coverage computation |
cloudflared |
cloudflare/cloudflared |
Optional Cloudflare Tunnel (use --profile tunnel) |
Data Retention
- Packets hypertable: automatic 28-day retention via TimescaleDB retention policy applied on first startup
- Nodes, planned_nodes, observers, node_coverage: persist indefinitely
Acknowledgements
This project is built on the following open source libraries and tools:
Frontend
| Package | License |
|---|---|
| React | MIT |
| Vite | MIT |
| TypeScript | Apache 2.0 |
| Leaflet | BSD 2-Clause |
| react-leaflet | Hippocratic 2.1 |
| deck.gl | MIT |
| react-router-dom | MIT |
| Recharts | MIT |
| polygon-clipping | MIT |
Backend
| Package | License |
|---|---|
| Express | MIT |
| MQTT.js | MIT |
| ws | MIT |
| ioredis | MIT |
| node-postgres | MIT |
| cors | MIT |
| express-rate-limit | MIT |
| @michaelhart/meshcore-decoder | MIT |
Viewshed worker (Python)
| Package | License |
|---|---|
| NumPy | BSD 3-Clause |
| SciPy | BSD 3-Clause |
| Shapely | BSD 3-Clause |
| rasterio | BSD 3-Clause |
| psycopg2 | LGPL v3 |
| redis-py | MIT |
| Requests | Apache 2.0 |
Infrastructure
| Tool | License |
|---|---|
| TimescaleDB | Apache 2.0 (Community) |
| Redis | BSD 3-Clause |
| Eclipse Mosquitto | EPL 2.0 / EDL 1.0 |
| Docker | Apache 2.0 |
Data
| Source | License |
|---|---|
| SRTM Elevation Data | Public Domain (NASA) |
| Natural Earth | Public Domain |
License
This project is licensed under MIT — see LICENSE.
Note on dependencies: react-leaflet (Hippocratic License 2.1) and Eclipse Mosquitto (EPL 2.0) are used as dependencies but not modified or redistributed. All other runtime dependencies use MIT, BSD, or Apache 2.0 licenses. The Hippocratic License adds ethical use clauses not present in standard open source licenses.