Skip to main content

MQTT to Node-RED Integration

Node-RED is a browser-based visual programming tool built on Node.js, ideal for wiring together MQTT data streams from Meshtastic with downstream services such as dashboards, databases, and messaging platforms. This page covers installing Node-RED, subscribing to Meshtastic MQTT topics, parsing protobuf and JSON payloads, building a simple monitoring dashboard, and forwarding mesh alerts to Telegram and Discord.

Installing Node-RED

On Raspberry Pi / Debian / Ubuntu

bash <(curl -sL https://raw.githubusercontent.com/node-red/linux-installers/master/deb/update-nodejs-and-nodered)

# Enable and start Node-RED as a service
sudo systemctl enable nodered.service
sudo systemctl start nodered.service

# Node-RED web UI is available at http://<IP>:1880

On x86 Linux via npm

sudo npm install -g --unsafe-perm node-red
# Start manually:
node-red
# Or install as systemd service manually

Installing Required Palettes

Open the Node-RED web UI at http://<IP>:1880, then click the hamburger menu → Manage Palette → Install. Search for and install:

  • node-red-dashboard - UI widgets (gauges, charts, text displays).
  • node-red-contrib-protobuf - decode Meshtastic protobuf payloads.
  • node-red-contrib-telegrambot - send messages to Telegram.

Alternatively via CLI

cd ~/.node-red
npm install node-red-dashboard node-red-contrib-protobuf node-red-contrib-telegrambot
sudo systemctl restart nodered.service

Subscribing to Meshtastic MQTT Topics

Add an mqtt in node to your flow:

  • Server: your broker address and port.
  • Topic: msh/# to receive all Meshtastic traffic, or msh/US/2/json/LongFast/# for JSON output from the LongFast channel.
  • QoS: 0 (at most once) is sufficient for mesh monitoring.
  • Output: auto-detect (string or buffer).

Parsing Protobuf Payloads

The protobuf format requires a .proto schema file. Obtain the Meshtastic proto files:

mkdir -p ~/.node-red/proto
cd ~/.node-red/proto
wget https://raw.githubusercontent.com/meshtastic/protobufs/master/meshtastic/mesh.proto
wget https://raw.githubusercontent.com/meshtastic/protobufs/master/meshtastic/portnums.proto
wget https://raw.githubusercontent.com/meshtastic/protobufs/master/meshtastic/telemetry.proto

Flow wiring for protobuf decode:

[mqtt in] → [function: base64 to buffer] → [protobuf decode] → [debug / further processing]

Add a function node between mqtt in and protobuf decode with this code:

// Convert base64 MQTT payload to Buffer for protobuf decoder
msg.payload = Buffer.from(msg.payload, 'base64');
msg.protofile = '/home/pi/.node-red/proto/mesh.proto';
msg.protobufType = 'meshtastic.ServiceEnvelope';
return msg;

The protobuf decode node outputs a JavaScript object representing the ServiceEnvelope. The inner packet.decoded field contains the decrypted Data (if Node-RED has the channel key - see note below).

Note on channel key decryption: The node-red-contrib-meshtastic community package (if available) can handle channel key decryption. Without it, you can only decode the outer envelope metadata. For full access to message text and telemetry, enable JSON output mode on the gateway (which the gateway decrypts before publishing).

Parsing JSON Payloads (Recommended for Simplicity)

If JSON output is enabled on your gateway node, subscribe to the JSON topic and use a json node to parse directly - no protobuf library needed:

[mqtt in (topic: msh/US/2/json/LongFast/#)] → [json] → [switch] → [handlers]

A decoded JSON message looks like:

{
 "id": 3456789012,
 "channel": 0,
 "from": 2887456789,
 "sender": "!abcd1234",
 "type": "text",
 "payload": {
 "text": "Hello from City A!"
 },
 "timestamp": 1714953600,
 "rssi": -87,
 "snr": 6.25,
 "hops_away": 2
}

Building a Simple Dashboard

With node-red-dashboard installed, add a UI group for "Mesh Monitor":

  1. Add a ui_text node: label "Last Message", wired from the JSON parse output using a function to extract msg.payload.payload.text.
  2. Add a ui_gauge node: label "RSSI (dBm)", min -130, max -50, wired to msg.payload.rssi.
  3. Add a ui_chart node: "Message Rate / min", use an rbe (report-by-exception) node plus a counter function to track message frequency.
  4. Add a ui_table node: "Recent Messages" - build a rolling array of the last 20 messages using a context store.

Access the dashboard at http://<IP>:1880/ui.

Forwarding Alerts to Telegram

Pre-requisite: create a Telegram Bot via @BotFather and note the bot token and your chat ID.

Wire a flow like this:

[mqtt in] → [json] → [switch: type == "text"] → [function: format alert] → [telegram sender]

Function node code to format the Telegram message:

const p = msg.payload;
if (!p.payload || !p.payload.text) return null; // skip non-text packets

msg.payload = {
 chatId: "YOUR_CHAT_ID_HERE",
 type: "message",
 content: `📡 Mesh Message
` +
 `From: ${p.sender}
` +
 `Text: ${p.payload.text}
` +
 `RSSI: ${p.rssi} dBm | SNR: ${p.snr} | Hops: ${p.hops_away}`
};
return msg;

Configure the Telegram Sender node with your bot token.

Forwarding Alerts to Discord

Use a http request node pointed at your Discord webhook URL:

[mqtt in] → [json] → [switch: type == "text"] → [function: build webhook body] → [http request]

Function node code:

const p = msg.payload;
if (!p.payload || !p.payload.text) return null;

msg.method = "POST";
msg.url = "https://discord.com/api/webhooks/YOUR_WEBHOOK_ID/YOUR_WEBHOOK_TOKEN";
msg.headers = { "Content-Type": "application/json" };
msg.payload = JSON.stringify({
 username: "MeshAmerica Bot",
 content: `**Mesh Message** from \`${p.sender}\`
` +
 `> ${p.payload.text}
` +
 `RSSI: ${p.rssi} dBm | SNR: ${p.snr} | Hops: ${p.hops_away}`
});
return msg;

Example: Full Flow JSON Export

Below is a minimal Node-RED flow (importable via Import → Clipboard) that subscribes to the JSON MQTT topic, parses messages, displays them in a dashboard, and forwards them to a Discord webhook:

[
 {
 "id": "mqtt_in_1",
 "type": "mqtt in",
 "name": "Meshtastic JSON",
 "topic": "msh/US/2/json/LongFast/#",
 "qos": "0",
 "broker": "local_broker",
 "wires": [["json_parse_1"]]
 },
 {
 "id": "json_parse_1",
 "type": "json",
 "name": "Parse JSON",
 "wires": [["switch_1", "dashboard_table_1"]]
 },
 {
 "id": "switch_1",
 "type": "switch",
 "name": "Text only",
 "property": "payload.type",
 "rules": [{"t": "eq", "v": "text"}],
 "wires": [["format_discord_1"]]
 },
 {
 "id": "format_discord_1",
 "type": "function",
 "name": "Format Discord",
 "func": "const p = msg.payload;
if (!p.payload||!p.payload.text) return null;
msg.method='POST';
msg.url='https://discord.com/api/webhooks/XXX/YYY';
msg.headers={'Content-Type':'application/json'};
msg.payload=JSON.stringify({username:'MeshBot',content:`**${p.sender}**: ${p.payload.text}`});
return msg;",
 "wires": [["http_discord_1"]]
 },
 {
 "id": "http_discord_1",
 "type": "http request",
 "name": "Discord Webhook",
 "method": "POST",
 "wires": [[]]
 }
]

Replace the Discord webhook URL placeholder with your real webhook before importing.