Build a factory MQTT gateway on a Raspberry Pi

Updated Oct 31, 2026 · InduQT engineering team · Guide, not a customer case study

The short answer

A Raspberry Pi running the free Mosquitto MQTT broker is enough to collect data from dozens of off-the-shelf sensors (Shelly meters, ESP32 boards, Bluetooth tags through a gateway, PLCs through Node-RED). The sensors publish to the Pi, you check the readings with Claude or ChatGPT, and a 120-line bridge forwards them to InduQT over HTTPS, so nothing on the plant network is exposed to the internet. Hardware costs about US$100 and setup takes under an hour.

  • One place on the plant network where every sensor sends its readings
  • A way to look at raw data before you trust it
  • Readings in InduQT without opening a port in the firewall
Costabout US$100
Time45–60 minutes
SkillSome wiring
  1. What to buy
  2. How the pieces fit
  3. Set up the MQTT broker (once per plant)
  4. Send a first test message
  5. Check the readings with Claude before you connect anything
  6. Wire it to InduQT
  7. The same bridge in Node-RED
  8. Using a cloud broker instead
  9. Keep it running

What to buy

Raspberry Pi 5 (4 GB) with power supply, case and 32 GB cardabout US$100 for the kit

Runs the MQTT broker and the InduQT bridge. Any always-on PC or small industrial PC works too. Maker: Raspberry Pi. Maker’s page

Eclipse Mosquitto MQTT brokerfree

Receives readings from the sensors. Free and open source. Maker: Eclipse Foundation. Maker’s page

Node-RED (optional)free

Drag-and-drop flows for PLCs (Modbus, S7) and anything that is not MQTT yet. Maker: OpenJS Foundation. Maker’s page

Prices are typical street prices in US dollars when we wrote this, not quotes; buy from the maker or a local distributor. InduQT does not sell sensors and is not paid by any maker named here.

Safety. The gateway sits on the plant network. Put it on the same network segment as the sensors, change the default passwords, and talk to whoever runs IT/OT before connecting anything to a control network.

How the pieces fit

From the sensor to InduQT
PieceWhat it doesWhere it runs
SensorMeasures current, temperature, vibration, cycles or pressure and publishes JSON over MQTTOn or near the machine, on the plant Wi-Fi or Ethernet
Broker (Mosquitto)Receives every message; lets you watch themRaspberry Pi or PC on the plant network
Claude or ChatGPTChecks two minutes of raw readings: plausible values, gaps, which fields matterYour laptop; no data is stored anywhere
BridgeForwards numbers to InduQT over HTTPS every 10 s; outbound onlySame Raspberry Pi
InduQTLearns each machine’s normal, warns before a limit, explains events with the manual and historyCloud (or on-premises with Enterprise)

Topic names follow one rule so every guide on this site works together: factory/<machine>/<what>. The machine name (for example compressor-1) becomes the machine’s externalId in InduQT.

Set up the MQTT broker (once per plant)

The sensors publish their readings to a broker: a small program that receives MQTT messages and hands them to whoever subscribes. Mosquitto is free and runs on a Raspberry Pi, an old PC or a small industrial box on the plant network. You need one per plant, not one per machine. If you already have a broker (Node-RED, Home Assistant, an IoT gateway), use it and skip this step.

Install Mosquitto
# Raspberry Pi OS, Debian or Ubuntu
sudo apt update
sudo apt install -y mosquitto mosquitto-clients

# One user for the sensors and one read-only user for testing
sudo mosquitto_passwd -c /etc/mosquitto/passwd sensors
sudo mosquitto_passwd /etc/mosquitto/passwd viewer
Require a password
# /etc/mosquitto/conf.d/factory.conf
listener 1883
allow_anonymous false
password_file /etc/mosquitto/passwd
acl_file /etc/mosquitto/acl
Who can publish and who can only read
# /etc/mosquitto/acl
user sensors
topic readwrite factory/#

user viewer
topic read factory/#
Start it and watch messages arrive
# Mosquitto 2 wants these files readable only by its own user
sudo chown mosquitto: /etc/mosquitto/passwd /etc/mosquitto/acl
sudo chmod 0700 /etc/mosquitto/passwd /etc/mosquitto/acl

sudo systemctl restart mosquitto
sudo systemctl enable mosquitto

# Watch everything that arrives (Ctrl+C to stop)
mosquitto_sub -h localhost -u viewer -P 'viewer-password' -t 'factory/#' -v
Give the Raspberry Pi a fixed IP address (a DHCP reservation on the router) so the sensors can always find it. Every example below uses 192.168.1.20. Keep the broker on the plant network; it does not need to be reachable from the internet.

Send a first test message

From any computer with mosquitto-clients
mosquitto_pub -h 192.168.1.20 -u sensors -P 'sensors-password' \
  -t factory/test-1/telemetry -m '{"temperature_c": 21.5, "running": true}'

The window running mosquitto_sub shows the message straight away. If it does not, check the IP address, the user name and password, and that port 1883 is not blocked between the two machines.

Check the readings with Claude before you connect anything

Before the data goes anywhere, make sure the sensor is telling the truth. Record two minutes of messages to a file and ask Claude (or ChatGPT) to check them. This catches the usual first-day problems (a clamp on the wrong cable, a probe reading the room instead of the pipe, a sensor that only reports when something changes) while you are still standing at the test machine.

Record two minutes of messages (Raspberry Pi, Mac or Linux)
# Record 2 minutes of messages to a file you can give Claude or ChatGPT
mosquitto_sub -h 192.168.1.20 -u viewer -P 'viewer-password' \
  -t 'factory/#' -v -W 120 | while read -r line; do
  echo "$(date -u +%Y-%m-%dT%H:%M:%SZ) $line"
done > sensor-sample.txt
The same on Windows
# Windows PowerShell, with Mosquitto installed from mosquitto.org
& "C:\Program Files\mosquitto\mosquitto_sub.exe" -h 192.168.1.20 -u viewer -P viewer-password `
  -t "factory/#" -v -W 120 | ForEach-Object { "$(Get-Date -Format o) $_" } | Out-File sensor-sample.txt
Prompt
Paste into Claude or ChatGPT with sensor-sample.txt attached
I've attached sensor-sample.txt: two minutes of MQTT messages from a sensor I just fitted to the test machine.
Each line is: time, topic, payload.

Please check it for me:
1. List each topic and every numeric field in it, with the min, max and typical value.
2. Tell me whether the values are physically plausible for the test machine. For the test message, temperature should be about 21.5 and running should be true.
3. Point out gaps longer than 30 seconds, repeated identical values, or fields that are always 0.
4. Say which fields are worth sending to InduQT as signals and which I can ignore.
Answer in a short table, then a list of anything I should fix before I wire it up.

Claude Code or Claude desktop with a terminal can run the mosquitto_sub command for you: ask it to "subscribe to factory/# on 192.168.1.20 as viewer for two minutes and check the readings". Either way the readings stay on your laptop: nothing is stored in InduQT until you wire it up in the next step.

Using a cloud broker (HiveMQ Cloud, EMQX Serverless or your own server with TLS) instead of one on the plant network? Add the free InduQT connector to Claude (https://claude.ai/directory/connectors/induqt) and Claude can listen to the broker for a few seconds itself:

Prompt
Claude with the InduQT connector
Use InduQT to preview my MQTT broker before I connect it:
url mqtts://<your-cluster>.hivemq.cloud:8883, topic factory/#,
username viewer, password <viewer password>. Listen for 10 seconds.
Then tell me which topic level names the machine and which fields are numeric.

Wire it to InduQT

Once the readings look right, send them to InduQT. The simplest way is a small bridge on the same Raspberry Pi: it listens to your broker and forwards every number to InduQT over HTTPS. It works on every InduQT plan, including the free one (up to 3 machines), and nothing on the plant network has to be opened to the internet.

  1. In Claude with the InduQT connector, paste the prompt below. Claude creates the site, the machine and a key that can only send readings to that site. (No Claude? Do the same in the InduQT app: Add machine with the same externalId, then Settings → API keys → a key with the ingest scope, limited to the site.)
  2. Copy the key Claude shows you. It is shown once.
  3. Start the bridge on the Raspberry Pi with that key.
  4. Ask Claude for the latest values, and set the normal bands.
Prompt
1 · Claude creates the machine and the key
Set up InduQT for this sensor:
1. Create a site called "Plant 1" (skip if it already exists).
2. Create a machine named "test-1" with externalId "test-1" and family "other".
3. Create an ingest key for the site called "Raspberry Pi bridge" and show it to me once.
After I start the bridge, I'll ask you to check the latest values. The signals will be temperature_c and running.
2 · Start the bridge
python3 -m venv ~/induqt && source ~/induqt/bin/activate
pip install "paho-mqtt>=2.0" requests

export MQTT_HOST=localhost MQTT_USER=viewer MQTT_PASSWORD='viewer-password'
export MQTT_TOPICS='factory/#'
export INDUQT_API_KEY='iq_live_…'   # the site key Claude or the app created for you
python3 induqt_bridge.py
induqt_bridge.py (the whole bridge, about 120 lines)
induqt_bridge.py (the whole bridge, about 120 lines)
#!/usr/bin/env python3
"""
induqt_bridge.py — forward MQTT sensor messages to InduQT.

Subscribes to your local broker, turns each message into numbers, and posts them
to the InduQT ingest API in batches. Works on every InduQT plan, including the free one,
and your broker never has to be reachable from the internet.

  factory/<machine>/<anything>        ->  machine = <machine> (matched to the machine's externalId)
  factory/pump-3/vibration {"vib_rms_x_g": 0.12}  ->  signal vib_rms_x_g
  factory/comp-1/status/em:0 {"a_current": 12.4}  ->  signal em_0.a_current
      (two or more levels after the machine: the last level names the group, so
       two probes or two channels never overwrite each other)
  {"temperature": {"tC": 41.2}}       ->  signal temperature.tC
  factory/furnace-1/temp  21.7        ->  signal temp (a bare number takes the last level)

pip install "paho-mqtt>=2.0" requests
"""
import json
import os
import re
import threading
import time

import paho.mqtt.client as mqtt
import requests

MQTT_HOST = os.environ.get("MQTT_HOST", "localhost")
MQTT_PORT = int(os.environ.get("MQTT_PORT", "1883"))
MQTT_USER = os.environ.get("MQTT_USER")
MQTT_PASSWORD = os.environ.get("MQTT_PASSWORD")
TOPICS = [t.strip() for t in os.environ.get("MQTT_TOPICS", "factory/#").split(",") if t.strip()]
MACHINE_LEVEL = int(os.environ.get("MACHINE_LEVEL", "1"))  # factory/<machine>/... is level 1
SEND_EVERY = float(os.environ.get("SEND_EVERY", "10"))     # seconds between posts
IGNORE = set(os.environ.get("IGNORE_FIELDS", "id,ts,time,timestamp,unixtime,Time").split(","))
INGEST_URL = os.environ.get("INDUQT_URL", "https://api.induqt.ai/v1/ingest")
API_KEY = os.environ["INDUQT_API_KEY"]  # a site key from InduQT (Settings -> API keys)

BAD = re.compile(r"[^A-Za-z0-9_.\-]")
buffer, lock = [], threading.Lock()


def key(name):
    return BAD.sub("_", name)[:64]


def numbers(prefix, value, out):
    """Collect every number in a JSON value; true/false become 1/0."""
    if isinstance(value, bool):
        out[key(prefix)] = 1 if value else 0
    elif isinstance(value, (int, float)):
        out[key(prefix)] = value
    elif isinstance(value, str):
        text = value.strip().lower()
        if text in ("on", "true", "online"):
            out[key(prefix)] = 1
        elif text in ("off", "false", "offline"):
            out[key(prefix)] = 0
        else:
            try:
                out[key(prefix)] = float(text)
            except ValueError:
                pass
    elif isinstance(value, dict):
        for k, v in value.items():
            if k not in IGNORE:
                numbers(f"{prefix}.{k}" if prefix else k, v, out)


def on_connect(client, userdata, flags, reason_code, properties):
    print(f"MQTT connected ({reason_code}); subscribing to {TOPICS}")
    for t in TOPICS:
        client.subscribe(t, qos=1)


def on_message(client, userdata, msg):
    levels = msg.topic.split("/")
    if len(levels) <= MACHINE_LEVEL:
        return
    machine = levels[MACHINE_LEVEL]
    try:
        payload = json.loads(msg.payload.decode("utf-8", "replace"))
    except ValueError:
        payload = msg.payload.decode("utf-8", "replace")
    values = {}
    group = levels[-1] if len(levels) - MACHINE_LEVEL > 2 else ""
    if isinstance(payload, dict):
        numbers(group, payload, values)
    else:
        numbers(levels[-1], payload, values)
    if values:
        with lock:
            buffer.append({"machine": machine, "ts": int(time.time() * 1000), "values": values})


def sender():
    while True:
        time.sleep(SEND_EVERY)
        with lock:
            batch = buffer[:500]
        if not batch:
            continue
        try:
            r = requests.post(
                INGEST_URL,
                headers={"Authorization": f"Bearer {API_KEY}"},
                json={"readings": batch, "autoCreate": True},
                timeout=15,
            )
            if r.status_code < 300:
                with lock:
                    del buffer[: len(batch)]
                print(f"sent {len(batch)} readings: {r.json()}")
            elif r.status_code in (400, 401, 403):
                print(f"InduQT refused the batch ({r.status_code}): {r.text}")
                with lock:
                    del buffer[: len(batch)]  # fix the key or mapping; retrying will not help
            else:
                print(f"InduQT returned {r.status_code}; will retry")
        except requests.RequestException as e:
            print(f"network error, will retry: {e}")
        with lock:
            del buffer[:-5000]  # keep at most ~5,000 readings while offline


client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, client_id="induqt-bridge")
if MQTT_USER:
    client.username_pw_set(MQTT_USER, MQTT_PASSWORD)
client.on_connect = on_connect
client.on_message = on_message
threading.Thread(target=sender, daemon=True).start()
client.connect(MQTT_HOST, MQTT_PORT, keepalive=60)
client.loop_forever()
Keep it running after a reboot (systemd)
Keep it running after a reboot (systemd)
# /etc/systemd/system/induqt-bridge.service  — keeps the bridge running after a reboot
[Unit]
Description=MQTT to InduQT bridge
After=network-online.target mosquitto.service

[Service]
User=pi
EnvironmentFile=/home/pi/induqt-bridge.env
ExecStart=/home/pi/induqt/bin/python3 /home/pi/induqt_bridge.py
Restart=always
RestartSec=10

[Install]
WantedBy=multi-user.target

# then:
# sudo systemctl daemon-reload && sudo systemctl enable --now induqt-bridge
# journalctl -u induqt-bridge -f

The bridge treats the topic level after factory/ as the machine (it matches the machine's externalId and creates it if needed), turns every number in the message into a signal (nested fields become names like temperature.tC; when the topic has a group after the machine, such as Shelly's status/em:0, the group goes in front, as em_0.a_current, so two channels never overwrite each other; true/false and on/off become 1/0) and sends a batch every 10 seconds. It was run end to end on 2026-10-31 against Mosquitto 2 with Shelly Gen2, Theengs and plain-number messages. Prefer Node-RED? The same bridge as a Node-RED flow is on the gateway guide.

Prompt
3 · Check it arrived and set what normal looks like
Show me the latest values InduQT has for test-1.
Then set these normal bands so InduQT knows what "normal" looks like: temperature_c 15 to 30.

On the Plant Monitor plan InduQT can also subscribe to a broker directly. Point your sensors (or a Mosquitto bridge) at a cloud broker with TLS, then ask Claude to "create an MQTT connector for this broker, machine from topic level 1", or add it in the app under the site → Connect data → MQTT broker.

The same bridge in Node-RED

If you already use Node-RED, import this flow instead of running the Python bridge (Menu → Import → paste). Set INDUQT_API_KEY in the Node-RED environment (settings.js or the service’s environment) and the viewer user in the Factory broker node. Run end to end on 2026-10-31 with Node-RED 4.0.

Node-RED flow: MQTT → InduQT
Node-RED flow: MQTT → InduQT
[
  {
    "id": "iqb_tab",
    "type": "tab",
    "label": "MQTT → InduQT",
    "disabled": false,
    "info": "Forwards factory/<machine>/... messages to the InduQT ingest API every 10 s. Set INDUQT_API_KEY in the Node-RED environment and the broker user in the \"Factory broker\" node."
  },
  {
    "id": "iqb_broker",
    "type": "mqtt-broker",
    "name": "Factory broker",
    "broker": "localhost",
    "port": "1883",
    "clientid": "induqt-node-red",
    "autoConnect": true,
    "usetls": false,
    "protocolVersion": "4",
    "keepalive": "60",
    "cleansession": true
  },
  {
    "id": "iqb_in",
    "type": "mqtt in",
    "z": "iqb_tab",
    "name": "factory/#",
    "topic": "factory/#",
    "qos": "1",
    "datatype": "auto-detect",
    "broker": "iqb_broker",
    "nl": false,
    "rap": true,
    "rh": 0,
    "inputs": 0,
    "x": 120,
    "y": 80,
    "wires": [
      [
        "iqb_collect"
      ]
    ]
  },
  {
    "id": "iqb_collect",
    "type": "function",
    "z": "iqb_tab",
    "name": "Collect readings",
    "func": "// Turn each MQTT message into an InduQT reading and keep it until the next send.\nconst levels = msg.topic.split('/');\nconst machine = levels[1];                       // factory/<machine>/...\nif (!machine) return null;\nconst IGNORE = ['id', 'ts', 'time', 'timestamp', 'unixtime', 'Time'];\n// Two or more levels after the machine (factory/comp-1/status/em:0): the last level names the group.\nconst values = {};\nconst key = (k) => k.replace(/[^A-Za-z0-9_.\\-]/g, '_').slice(0, 64);\n(function walk(prefix, v) {\n  if (typeof v === 'boolean') values[key(prefix)] = v ? 1 : 0;\n  else if (typeof v === 'number' && isFinite(v)) values[key(prefix)] = v;\n  else if (typeof v === 'string') {\n    const s = v.trim().toLowerCase();\n    if (s === 'on' || s === 'true') values[key(prefix)] = 1;\n    else if (s === 'off' || s === 'false') values[key(prefix)] = 0;\n    else if (s !== '' && !isNaN(Number(s))) values[key(prefix)] = Number(s);\n  } else if (v && typeof v === 'object' && !Array.isArray(v)) {\n    for (const [k, x] of Object.entries(v)) if (!IGNORE.includes(k)) walk(prefix ? prefix + '.' + k : k, x);\n  }\n})(typeof msg.payload === 'object' ? (levels.length > 3 ? levels[levels.length - 1] : '') : levels[levels.length - 1], msg.payload);\nif (!Object.keys(values).length) return null;\nconst buf = flow.get('induqt') || [];\nbuf.push({ machine, ts: Date.now(), values });\nflow.set('induqt', buf.slice(-5000));\nreturn null;",
    "outputs": 1,
    "timeout": 0,
    "noerr": 0,
    "initialize": "",
    "finalize": "",
    "libs": [],
    "x": 320,
    "y": 80,
    "wires": [
      []
    ]
  },
  {
    "id": "iqb_tick",
    "type": "inject",
    "z": "iqb_tab",
    "name": "Every 10 s",
    "props": [
      {
        "p": "payload"
      }
    ],
    "repeat": "10",
    "crontab": "",
    "once": false,
    "onceDelay": "1",
    "topic": "",
    "payload": "",
    "payloadType": "date",
    "x": 130,
    "y": 160,
    "wires": [
      [
        "iqb_flush"
      ]
    ]
  },
  {
    "id": "iqb_flush",
    "type": "function",
    "z": "iqb_tab",
    "name": "Build batch",
    "func": "// Every 10 s, send what has arrived as one batch.\nconst buf = flow.get('induqt') || [];\nif (!buf.length) return null;\nconst batch = buf.slice(0, 500);\nflow.set('induqt', buf.slice(batch.length));\nmsg.headers = {\n  'Authorization': 'Bearer ' + env.get('INDUQT_API_KEY'),\n  'Content-Type': 'application/json'\n};\nmsg.payload = { readings: batch, autoCreate: true };\nreturn msg;",
    "outputs": 1,
    "timeout": 0,
    "noerr": 0,
    "initialize": "",
    "finalize": "",
    "libs": [],
    "x": 320,
    "y": 160,
    "wires": [
      [
        "iqb_post"
      ]
    ]
  },
  {
    "id": "iqb_post",
    "type": "http request",
    "z": "iqb_tab",
    "name": "POST /v1/ingest",
    "method": "POST",
    "ret": "obj",
    "paytoqs": "ignore",
    "url": "https://api.induqt.ai/v1/ingest",
    "tls": "",
    "persist": false,
    "proxy": "",
    "insecureHTTPParser": false,
    "authType": "",
    "senderr": false,
    "headers": [],
    "x": 520,
    "y": 160,
    "wires": [
      [
        "iqb_debug"
      ]
    ]
  },
  {
    "id": "iqb_debug",
    "type": "debug",
    "z": "iqb_tab",
    "name": "InduQT response",
    "active": true,
    "tosidebar": true,
    "console": false,
    "tostatus": true,
    "complete": "payload",
    "targetType": "msg",
    "statusVal": "statusCode",
    "statusType": "msg",
    "x": 730,
    "y": 160,
    "wires": []
  }
]

Using a cloud broker instead

On the Plant Monitor plan InduQT can subscribe to a broker directly, which removes the bridge. The broker must be reachable from the internet with TLS: a managed one (HiveMQ Cloud, EMQX Serverless) or your own. Shelly devices can publish to it straight away:

Shelly → cloud broker over TLS
Shelly app or the device web page (http://<device-ip>)
→ Settings → MQTT
   Enable:                     on
   Server:                     <your-cluster>.hivemq.cloud:8883   (or your EMQX / Mosquitto host)
   Client ID / Topic prefix:   factory/compressor-1
   User / Password:            the broker user for this device
   Use SSL:                    TLS with default CA
   Generic status update over MQTT:   on
→ Save, then reboot the device
Never use a public test broker (test.mosquitto.org, broker.hivemq.com) for plant data. Anyone can subscribe to everything on them: when we listened to one for eight seconds we saw 250 messages from strangers’ devices, with their Wi-Fi names and IP addresses.

Keep it running

systemd service for the bridge
# /etc/systemd/system/induqt-bridge.service  — keeps the bridge running after a reboot
[Unit]
Description=MQTT to InduQT bridge
After=network-online.target mosquitto.service

[Service]
User=pi
EnvironmentFile=/home/pi/induqt-bridge.env
ExecStart=/home/pi/induqt/bin/python3 /home/pi/induqt_bridge.py
Restart=always
RestartSec=10

[Install]
WantedBy=multi-user.target

# then:
# sudo systemctl daemon-reload && sudo systemctl enable --now induqt-bridge
# journalctl -u induqt-bridge -f
  • Back up /etc/mosquitto and the bridge’s environment file.
  • A UPS or at least a good power supply avoids SD-card corruption; an SSD on a Raspberry Pi 5 is more robust.
  • The bridge keeps up to about 5,000 readings while the internet is down and sends them when it is back.

Frequently asked questions

Do I need a Raspberry Pi?

No. Any computer that stays on works: an old PC, a mini PC or an industrial edge box. Mosquitto and the bridge run on Windows, Linux and macOS.

Does the broker have to be on the internet?

No. With the bridge, the Pi only makes outbound HTTPS calls to api.induqt.ai. Only the Plant Monitor direct MQTT connector needs an internet-reachable broker.

How many sensors can one Raspberry Pi handle?

Mosquitto handles thousands of small messages a second on a Raspberry Pi. For a plant of 10 to 100 sensors reporting every few seconds, the Pi is nowhere near its limit.

Can ChatGPT read my sensors directly?

Not directly. Record a sample with mosquitto_sub and upload the file to ChatGPT or Claude. With the InduQT connector in Claude, Claude can also preview a cloud broker and query the data InduQT has stored.

Related