Industrial IoT

MTConnect to MQTT: Streaming CNC Machine Data to Dashboards

MTConnect agents expose CNC data over HTTP and XML. Build a bridge that follows the agent's sequence numbers, picks the data items that matter and publishes machine state over MQTT for dashboards and analytics.

CNC machines feeding an MTConnect agent whose XML stream is converted by an MTConnect to MQTT bridge into live dashboard topics
Run this on your own MQTT brokerGet a private, managed MQTT broker on the Free plan and start from ready-made code samples.
Try it free Code samples No card required

MTConnect to MQTT is the shortest path from a CNC machine's standardised data to live dashboards, alerts and utilisation reports. In this tutorial you will learn how an MTConnect agent exposes data, then build a Python bridge that follows the agent's sequence numbers without missing observations, maps data items such as execution state, spindle speed and part count to readable topics, and publishes them over MQTT. The same bridge works for one machine or a whole cell.

How MTConnect Exposes Machine Data

MTConnect is an open standard for manufacturing equipment, especially machine tools. It defines a vocabulary (what "Execution", "SpindleSpeed" or "PartCount" mean) and a read-only HTTP interface served by an agent. Machines talk to the agent through an adapter, often supplied by the control vendor or built into the control, and clients read from the agent.

The agent offers three main requests:

Request Returns Use it for
/probe The device model: components and data items with IDs, types, units Building a data item catalogue once at startup
/current The latest value of every data item, plus header sequence info Initial state, and resynchronising after errors
/sample?from=N&count=M Every observation from sequence N onward, up to M items Following changes without missing any

Every observation carries a sequence number assigned by the agent, a timestamp and a dataItemId. The response header contains firstSequence, lastSequence, nextSequence and an instanceId. Those four values are the key to a reliable bridge: you start from /current, remember nextSequence, and ask for samples from there.

MTConnect observations come in three categories, which map neatly to how you should treat them on MQTT:

  • Samples are numeric measurements such as spindle speed, load or position.
  • Events are discrete states or values such as execution state, controller mode, program name or part count.
  • Conditions describe health: Normal, Warning, Fault or Unavailable, often with a native alarm code.

A data item whose value is not known reports UNAVAILABLE. Your bridge should pass that through, not hide it, because "the machine is off or disconnected" is important information.

Why Bridge MTConnect to MQTT

MTConnect is pull-based and XML-heavy. That is fine for one dashboard, but each new consumer polls the agent separately and has to parse XML. Bridging to MQTT gives you push delivery, one consumer-friendly JSON format, retained current state for late joiners, and the ability to combine machine data with PLC, energy and quality data in a single topic hierarchy. If you are building a Unified Namespace, MTConnect machines become one more source feeding it; our guide on ISA-95 MQTT topics shows how to place them.

Recent releases of the MTConnect reference agent can also publish to an MQTT broker directly through a built-in MQTT sink. If your agent version supports it, that is worth evaluating; the configuration keys vary between versions, so follow the agent's own documentation. The bridge in this tutorial works with any agent version and gives you full control over topic names and payload shape.

Prerequisites

  • Python 3.10 or newer with requests and paho-mqtt 2.x.
  • An MTConnect agent URL, such as http://192.168.10.50:5000. If you do not have a machine connected, the reference agent project includes sample configurations and a simulator you can run locally.
  • A broker. For testing, use the free public MQTT broker with a unique topic prefix.
pip install requests "paho-mqtt>=2.0"
curl -s http://localhost:5000/probe | head -40     # check the agent answers and list data items

Step 1: Choose the Data Items

A modern CNC can expose hundreds of data items. Publish what consumers need, not everything. A practical starting set for utilisation and status dashboards:

MTConnect type Category Example values Why it matters
AVAILABILITY Event AVAILABLE, UNAVAILABLE Is the machine connected at all
EXECUTION Event ACTIVE, READY, INTERRUPTED, FEED_HOLD, STOPPED Core of utilisation tracking
CONTROLLER_MODE Event AUTOMATIC, MANUAL, MANUAL_DATA_INPUT Separates production from setup
PROGRAM Event O1234 Which job is running
PART_COUNT Event 1843 Output
ROTARY_VELOCITY (spindle) Sample 12000 Cutting activity
PATH_FEEDRATE_OVERRIDE Event or Sample 100 Operator intervention
System and logic conditions Condition Normal, Warning, Fault Alarms

Run /probe against your agent and note the id of each data item you want. IDs are chosen by whoever configured the agent, so they differ between installations.

Step 2: The MTConnect to MQTT Bridge

The bridge resolves data item IDs to topic names, reads /current for the initial state, then loops on /sample. It detects agent restarts (a new instanceId) and buffer overruns (a from older than firstSequence) and resynchronises in both cases.

# mtconnect_mqtt_bridge.py - follow an MTConnect agent and publish observations to MQTT
import json, logging, time, uuid
import xml.etree.ElementTree as ET
import paho.mqtt.client as mqtt
import requests

logging.basicConfig(level=logging.INFO)
log = logging.getLogger("mtc")

AGENT = "http://localhost:5000"
TOPIC_ROOT = "connectmq/demo/f40b/shop1/cnc"        # your own random id
INTERVAL = 1.0                                       # seconds between /sample requests
MQTT_HOST, MQTT_PORT = "broker.connectmq.com", 1883  # production: 8883 with TLS
MQTT_USER, MQTT_PASS = None, None                    # production: "YOUR_USERNAME", "YOUR_PASSWORD"
CLIENT_ID = f"mtc-bridge-{uuid.uuid4().hex[:8]}"     # production: "YOUR_CLIENT_ID"

# dataItemId -> topic leaf. Take the IDs from your agent's /probe output.
WATCH = {
    "avail": "availability",
    "exec": "execution",
    "mode": "controller_mode",
    "program": "program",
    "pc": "part_count",
    "Sspeed": "spindle_speed",
    "logic_cond": "logic_condition",
}

CONDITION_TAGS = {"Normal", "Warning", "Fault", "Unavailable"}


def local(tag):
    """Strip the versioned MTConnect XML namespace from a tag name."""
    return tag.rsplit("}", 1)[-1]


def fetch(path):
    r = requests.get(f"{AGENT}{path}", timeout=10)
    r.raise_for_status()
    return ET.fromstring(r.content)


def parse(root):
    header = next(e for e in root.iter() if local(e.tag) == "Header")
    observations = []
    for stream in root.iter():
        if local(stream.tag) != "DeviceStream":
            continue
        device = stream.get("name")
        for el in stream.iter():
            item_id = el.get("dataItemId")
            if item_id is None:
                continue
            tag = local(el.tag)
            if tag in CONDITION_TAGS:
                value = {"level": tag, "native_code": el.get("nativeCode"), "text": (el.text or "").strip()}
            else:
                value = (el.text or "").strip()
            observations.append({
                "device": device, "id": item_id, "value": value,
                "ts": el.get("timestamp"), "seq": int(el.get("sequence", 0)),
            })
    observations.sort(key=lambda o: o["seq"])
    return header, observations


def coerce(value):
    if isinstance(value, dict) or value == "UNAVAILABLE":
        return value
    try:
        return float(value) if "." in value else int(value)
    except ValueError:
        return value


def publish(mq, obs):
    leaf = WATCH.get(obs["id"])
    if not leaf:
        return
    topic = f"{TOPIC_ROOT}/{obs['device']}/{leaf}"
    payload = {"value": coerce(obs["value"]), "ts": obs["ts"], "seq": obs["seq"]}
    mq.publish(topic, json.dumps(payload), qos=1, retain=True)
    log.info("%s = %s", topic, payload["value"])


def main():
    mq = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, client_id=CLIENT_ID)
    if MQTT_USER:
        mq.username_pw_set(MQTT_USER, MQTT_PASS)
        mq.tls_set()
    mq.will_set(f"{TOPIC_ROOT}/bridge", "offline", qos=1, retain=True)
    mq.on_connect = lambda c, u, f, rc, p: c.publish(f"{TOPIC_ROOT}/bridge", "online", qos=1, retain=True)
    mq.connect(MQTT_HOST, MQTT_PORT, keepalive=30)
    mq.loop_start()

    instance, next_seq = None, None
    while True:
        try:
            if next_seq is None:                       # (re)synchronise from /current
                header, obs = parse(fetch("/current"))
                instance, next_seq = header.get("instanceId"), int(header.get("nextSequence"))
                for o in obs:
                    publish(mq, o)
                log.info("synchronised: instance %s, next %s", instance, next_seq)
            else:
                header, obs = parse(fetch(f"/sample?from={next_seq}&count=1000"))
                if header.get("instanceId") != instance:
                    log.warning("agent restarted, resynchronising")
                    next_seq = None
                    continue
                for o in obs:
                    publish(mq, o)
                next_seq = int(header.get("nextSequence"))
        except (ET.ParseError, StopIteration, TypeError) as exc:
            # Bad XML, or an MTConnectError document without a normal Header.
            log.error("unexpected agent response: %s", exc)
            next_seq = None
        except requests.RequestException as exc:
            # Network errors, or an error status such as an out-of-range "from".
            log.error("agent request failed: %s", exc)
            next_seq = None
            time.sleep(5)
        time.sleep(INTERVAL)


if __name__ == "__main__":
    main()

A few points about the design:

  • Sorting by sequence guarantees that observations are published in the order the agent recorded them, even when the XML groups them by component.
  • Retained messages mean a dashboard opened mid-shift instantly shows the current execution state, program and part count.
  • Conditions become objects with a level and native alarm code, so consumers can distinguish a warning from a fault.
  • Resynchronising on error is safe: /current republishes the latest state, so consumers end up correct even if a few intermediate samples were lost.

Step 3: Run and Watch

python mtconnect_mqtt_bridge.py
mosquitto_sub -h broker.connectmq.com -p 1883 -i mtc-viewer-f40b -t "connectmq/demo/f40b/#" -v
connectmq/demo/f40b/shop1/cnc/VMC-3Axis/execution {"value": "ACTIVE", "ts": "2026-10-16T11:02:44.501Z", "seq": 90213}
connectmq/demo/f40b/shop1/cnc/VMC-3Axis/spindle_speed {"value": 11980.0, "ts": "2026-10-16T11:02:44.612Z", "seq": 90215}

The public broker allows ten messages per second per client, which is plenty for state events but can be exceeded by fast-changing samples such as axis positions. Keep WATCH focused, or add a deadband for samples, as explained in our article on report by exception.

Building a Machine Status Dashboard

With execution, mode and availability on MQTT, a basic utilisation view is a matter of subscribing and timing state changes. A common simplified mapping:

  1. Running: availability AVAILABLE, mode AUTOMATIC, execution ACTIVE.
  2. Idle: available, execution READY or STOPPED.
  3. Interrupted: execution INTERRUPTED, FEED_HOLD or OPTIONAL_STOP, often an operator wait.
  4. Alarm: any condition at Fault level.
  5. Offline: availability UNAVAILABLE or the bridge status topic reads offline.

Any MQTT-capable dashboard tool can compute these states, and a historian subscribed to the same topics gives you shift and job reports. Our MQTT historian guide covers storing the stream.

Production Notes

  • Network placement. Run the bridge on the shop network next to the agent and let it connect outbound to the broker on port 8883. Do not expose the agent's HTTP port to the internet.
  • Several machines. One agent often serves several devices; the bridge already handles multiple DeviceStream elements and puts each device under its own branch.
  • Clock quality. MTConnect timestamps come from the agent or adapter. Make sure those hosts use NTP, or duration calculations will drift.
  • Private broker. The public broker is visible to everyone, so real machine data belongs on a private managed MQTT broker. On ConnectMQ, the devices view groups your machines by ISA-95 level and the 3D digital twin demo shows the kind of live CNC visualisation you can build on secure WebSockets.

For the full picture of how machine data, PLCs and gateways fit together, read the industrial IoT MQTT guide.

Get Started with ConnectMQ

Put your CNC data on a private broker in minutes. Sign up at app.connectmq.com, pick the free Serverless plan (no credit card), create a broker and paste the generated username, password and client ID into the bridge. Then set a threshold alert rule in the ConnectMQ portal, for example on spindle speed, and watch the alerts log as the machine runs.

Frequently Asked Questions

Does MTConnect support MQTT natively?

The MTConnect standard is built around an HTTP interface, but recent releases of the reference agent include an MQTT sink that publishes observations to a broker. A small bridge like the one above works with any agent version.

Why use /sample instead of polling /current?

Polling /current only shows the latest value and can miss short states between polls. Following /sample from nextSequence returns every observation in order, so brief alarms or stops are not lost.

What does UNAVAILABLE mean in MTConnect?

It means the agent does not currently know the value, usually because the machine or adapter is disconnected. Publish it as is so dashboards can show the machine as offline.

Can I write to a CNC through MTConnect?

No. MTConnect is a read-only standard for monitoring. Commands to the machine require the control vendor's own interfaces.

Related articles

IIoT Edge Gateway: What It Does and How to Choose One

An IIoT edge gateway connects machines that speak Modbus, OPC UA or proprietary protocols to MQTT and the cloud. Learn its core roles, common architectures and how to choose the right one.

7 min read

All articles →

Your private MQTT broker is a few clicks away

Create an account, get your broker credentials and connect your first device — no servers to install, patch or scale.