Meshtastic solves the hard part. Nodes form a LoRa mesh, relay for each other, and reach places with no Wi-Fi and no cellular — a valley, a ridge, a field several kilometres from anything.
What it does not solve is what happens to the readings afterwards. The telemetry module carries battery, voltage, channel utilisation, temperature, humidity and pressure, and you can see all of it on a device screen or in the app. Keeping it, charting it over a season, or being told when a value crosses a line is left to you, and the usual answer is to assemble an MQTT broker, a time-series database and Grafana.
There is a shorter path.
The bridge
Meshtastic’s MQTT module forwards mesh packets to an external broker, and it has a JSON option that exists specifically to make integration easy. That gives you a clean seam:
nodes → mesh → gateway node → MQTT (JSON) → bridge → your deployment
Only the gateway node needs internet. Everything else reaches it over LoRa, which is the entire reason to run a mesh in the first place.
Two caveats worth knowing before you build on it:
- JSON packets are not encrypted. The documentation says so plainly. Keep the broker on your own network rather than a public one, and treat anything you publish as visible to whoever can read that broker.
- JSON is not supported on nRF52. If your gateway is an nRF52 node, use an ESP32-based node as the gateway instead.
Configuring the gateway
On the node with connectivity, enable MQTT and turn on JSON output. Point it at your broker — a Mosquitto instance on a Pi is fine — and set a root topic you will recognise.
The telemetry module should be enabled on the nodes doing the sensing, with an update interval that suits the airtime you have. LoRa is slow, so readings every fifteen minutes are ordinary and every thirty seconds is usually antisocial on a shared mesh.
Region settings are not optional
Your
lora.regionmust be set or the device will not transmit at all — it shows a message on screen and stays silent. The band differs by region: roughly 902–928 MHz in the US, 869.4–869.65 MHz on EU_868, and 920.5–923.5 MHz in Japan. EU_433 and EU_868 are additionally limited to a 10% hourly duty cycle, calculated every minute, where most other regions permit 100%. If you are in the EU, that ceiling is the thing that decides your reporting interval; elsewhere it is courtesy to the mesh rather than law.
Forwarding to your deployment
The bridge is small because both ends speak JSON. It subscribes to the topic, pulls telemetry
packets out, and posts them to /v1/telemetry with the node name as a prefix so several nodes can
share one project:
import json
import paho.mqtt.client as mqtt
import requests
BROKER = "192.168.1.50"
TOPIC = "msh/+/2/json/#" # root topic from the MQTT module config
NODRIX = "https://nodrix.you.workers.dev/v1/telemetry"
TOKEN = "tok_your_project_token"
def on_message(_client, _userdata, msg):
try:
packet = json.loads(msg.payload)
except ValueError:
return
if packet.get("type") != "telemetry":
return
node = packet.get("sender") or str(packet.get("from", "unknown"))
payload = packet.get("payload", {})
# Flatten to variables, prefixed per node: cabin_temperature, cabin_battery_level…
metrics = {
f"{node}_{key}": value
for key, value in payload.items()
if isinstance(value, (int, float, bool))
}
if not metrics:
return
requests.post(
NODRIX,
headers={"Authorization": f"Bearer {TOKEN}"},
json={"metrics": metrics},
timeout=10,
)
client = mqtt.Client()
client.on_message = on_message
client.connect(BROKER, 1883, 60)
client.subscribe(TOPIC)
client.loop_forever()
The filter on int, float, bool matters. Telemetry payloads carry strings and nested objects
alongside the numbers, and passing those through creates variables you will spend an evening
deleting.
Run it wherever the broker runs — the same Pi is the obvious place — under systemd so it restarts with the machine.
What you get
Variables appear the first time they arrive, so there is no schema to define. On the deployment:
- Charts per node.
cabin_temperatureandridge_temperatureon one graph, over months rather than the last few points held in device memory. - Battery monitoring that matters. A remote node’s
battery_leveltrending down over weeks is the single most useful thing a mesh can tell you, because it predicts the node going silent before it happens. An automation on a threshold turns that into a message. - Alerts on silence. Because each node reports on an interval, absence is detectable. A node that stops relaying is a node worth visiting, and you would rather learn that from a notification than from noticing a gap three weeks later.
- A read API behind one token, if you want the data in Grafana or your own app anyway.
- Shared dashboards by read-only link, which is how you show a mesh community what the network is doing without giving anyone access to it.
Why not the usual stack
MQTT plus InfluxDB plus Grafana works and plenty of people run it. It is also three services to install, secure, back up and upgrade, on a Pi that is often also the gateway — and InfluxDB’s version transitions have been genuinely disruptive for small self-hosted deployments.
The bridge above keeps the broker, which you need anyway, and replaces the rest with a deployment in your own Cloudflare account: no database to run, no dashboards to provision, no retention policy to tune, and no machine whose SD card failing takes a season of readings with it.
If you already run the full stack and like it, there is no reason to change. If you have been putting it off because it is three days of work before you see a chart, this is an afternoon.