Meshtastic can be used as a simple mesh between devices, but it also can be used for tracking devices through a Grafana dashboard. Grafana doesn’t work with MQTT very well. MQTT is not designed for long-term storage. It is great for small messages on defined topics. Visualizing and keeping those messages over time is the TIG stack: Telegraf, InfluxDB, and Grafana.

I am not going into detail on Docker or MQTT. Follow this to get a basic stack on Ubuntu. I keep containers under /containers/tigstack.

YAMLdocker-compose.yaml
version: "3.8"
services:
  influxdb:
    image: influxdb:latest
    container_name: influxdb
    ports:
      - "8086:8086"
    volumes:
      - influxdb_data:/var/lib/influxdb2
    environment:
      - DOCKER_INFLUXDB_INIT_MODE=setup
      - DOCKER_INFLUXDB_INIT_USERNAME=admin
      - DOCKER_INFLUXDB_INIT_PASSWORD=SecureLogonPassword
      - DOCKER_INFLUXDB_INIT_ORG=YourOrganization
      - DOCKER_INFLUXDB_INIT_BUCKET=meshtastic_data
      - DOCKER_INFLUXDB_INIT_ADMIN_TOKEN=SecureInfluxPassword

  telegraf:
    image: telegraf:latest
    container_name: telegraf
    depends_on:
      - influxdb
    volumes:
      - ./:/etc/telegraf:ro
    environment:
      - MQTT_BROKER_URL=tcp://10.10.10.10:1883
      - MQTT_USER=meshtastic
      - MQTT_PASS=SecureMQTTPassword
      - INFLUX_TOKEN=InfluxDBAdminToken

  grafana:
    image: grafana/grafana:latest
    container_name: grafana
    ports:
      - "3000:3000"
    depends_on:
      - influxdb
    volumes:
      - grafana_data:/var/lib/grafana

volumes:
  influxdb_data:
  grafana_data:

Create telegraf.conf next to the compose file. Topics must be JSON. NRF nodes need the decode bridge first.

systemdtelegraf.conf
[agent]
  interval = "10s"

[[outputs.influxdb_v2]]
  urls = ["http://influxdb:8086"]
  token = "${INFLUX_TOKEN}"
  organization = "Organization"
  bucket = "meshtastic_data"

[[inputs.mqtt_consumer]]
  servers = ["${MQTT_BROKER_URL}"]
  topics = [
    "msh/Meshtastic/2/json/Main/!9e75dbcc",
    "msh/Meshtastic/2/json/Private/!9e75dbcc"
  ]
  username = "${MQTT_USER}"
  password = "${MQTT_PASS}"
  client_id = "telegraf_meshtastic"
  data_format = "json"
  json_string_fields = ["payload_text"]

Bring it up with docker compose up -d. Log into Influx at :8086, generate an all-access API token, then in Grafana (:3000, admin/admin the first time) add an InfluxDB data source using Flux, the compose org, bucket, and that token.

Message table

This Flux query pulls messages, maps node IDs to names, and keeps channel plus text.

systemdmessages.flux
import "regexp"

from(bucket: "meshtastic_data")
  |> range(start: v.timeRangeStart, stop: v.timeRangeStop)
  |> filter(fn: (r) => r._measurement == "mqtt_consumer")
  |> filter(fn: (r) => r._field == "payload_text" or r._field == "from" or r._field == "payload_from")
  |> pivot(rowKey:["_time"], columnKey: ["_field"], valueColumn: "_value")
  |> map(fn: (r) => ({
      r with
      node_id: if exists r.from then string(v: r.from)
               else if exists r.payload_from then string(v: r.payload_from)
               else "unknown"
    }))
  |> map(fn: (r) => ({
      r with
      node_name: if r.node_id == "2658588132" then "Base"
                 else if r.node_id == "1685377223" then "Node1"
                 else r.node_id
    }))
  |> filter(fn: (r) => exists r.payload_text and r.payload_text != "")
  |> keep(columns: ["_time", "node_name", "payload_text"])
  |> rename(columns: {payload_text: "message", node_name: "node_id"})

Last-known GPS for 30 days is the same pattern: pivot lat/lon, map names, top(n: 1) per node, divide latitude_i by 10,000,000. A second query without top gives the track over the dashboard time range.

ESP32 nodes can publish JSON and encrypted MQTT. NRF nodes do not have Wi-Fi and do not speak JSON. In my setup an ESP32 client uplinks over Wi-Fi into Home Assistant’s MQTT broker, and NRF SenseCaps stay on the mesh as trackers.