background-shape
Bridging OPC UA to MQTT
August 19, 2022 · 4 min read · by Muhammad Amal programming

TL;DR — OPC UA is the standard PLC protocol in 2022 industry; MQTT is the standard for IT/cloud ingest. A bridge subscribes to OPC UA nodes, publishes changes to MQTT. Use commercial gateways (HighByte, Siemens) for fewer projects; open-source / custom for more control.

After Sparkplug, the question of how data actually leaves the PLC. OPC UA is the protocol PLCs (Siemens S7, Allen-Bradley, Beckhoff) speak. MQTT is what your cloud expects. A bridge translates.

What OPC UA is

OPC Unified Architecture. IEC 62541. Open standard. Successor to OPC Classic (Windows-only DCOM). Designed to be cross-platform, secure, and replace the patchwork of industrial protocols.

Key features:

  • Address space model: hierarchical tree of “nodes” (variables, methods, objects)
  • Subscriptions: client subscribes to a node; server pushes changes
  • Security: certs, encryption, authentication
  • Transport: typically TCP on port 4840 (binary) or HTTPS (less common)

PLCs running OPC UA Server expose their internal state as nodes:

Objects
└── Server
    └── ProductionLine1
        ├── Press42
        │   ├── Temperature      (value, double)
        │   ├── Pressure         (value, double)
        │   ├── State            (value, string)
        │   └── EmergencyStop    (method)
        └── Conveyor3
            └── Speed            (value, double)

Your bridge connects, finds relevant nodes, subscribes, gets value-change notifications.

Why the bridge

OPC UA is well-designed for OT but doesn’t fit cloud ingest well:

  • TCP-based, persistent connections
  • Cert-heavy security model
  • Server-push, not topic-routed
  • Limited tooling outside industrial vendors

MQTT is the cloud-friendly transport. Light, topic-based, decouples publishers from subscribers, runs anywhere.

The bridge: an OPC UA client that subscribes to nodes you care about, formats values, publishes to MQTT.

Bridge options in 2022

Commercial gateways:

  • HighByte Intelligence Hub — modern, GUI-driven, good UX. Per-deployment licensing.
  • Cirrus Link MQTT Distributor — Ignition-based, Sparkplug-native.
  • Siemens IoT2050 — Siemens hardware + bridge software.

Pros: GUI, vendor support, supported by industrial integrators. Cons: per-license costs, vendor lock-in to some extent.

Open-source / build-your-own:

  • Eclipse Milo (Java) — OPC UA SDK, write your own bridge
  • open62541 (C) — most common embedded OPC UA stack
  • Node-OPCUA (Node.js) — good for prototyping
  • gopcua/opcua (Go) — Go SDK
  • asyncua (Python) — Python SDK

For 5+ project deployments, commercial gateway might save engineering time. For 1-3 projects or custom needs, building one is straightforward.

A simple custom bridge in Go

package main

import (
    "context"
    "encoding/json"
    "log"
    "time"

    "github.com/gopcua/opcua"
    "github.com/gopcua/opcua/ua"
    mqtt "github.com/eclipse/paho.mqtt.golang"
)

type NodeMapping struct {
    OPCUANodeID string
    MQTTTopic   string
}

func main() {
    ctx := context.Background()
    opcClient, err := opcua.NewClient("opc.tcp://10.0.0.50:4840")
    if err != nil { log.Fatal(err) }
    if err := opcClient.Connect(ctx); err != nil { log.Fatal(err) }
    defer opcClient.Close()

    mqttClient := newMQTTClient(...)

    mappings := []NodeMapping{
        {"ns=2;s=ProductionLine1.Press42.Temperature", "factory/press42/temperature"},
        {"ns=2;s=ProductionLine1.Press42.Pressure",    "factory/press42/pressure"},
        {"ns=2;s=ProductionLine1.Conveyor3.Speed",     "factory/conveyor3/speed"},
    }

    sub, err := opcClient.SubscribeWithContext(ctx, &opcua.SubscriptionParameters{
        Interval: 500 * time.Millisecond,
    }, func(s *opcua.PublishNotificationData) {
        switch v := s.Value.(type) {
        case *ua.DataChangeNotification:
            for _, item := range v.MonitoredItems {
                handleChange(item, mappings, mqttClient)
            }
        }
    })
    if err != nil { log.Fatal(err) }
    defer sub.Cancel(ctx)

    for i, m := range mappings {
        id, _ := ua.ParseNodeID(m.OPCUANodeID)
        sub.Monitor(ctx, ua.TimestampsToReturnBoth, opcua.MonitoringParameters{
            NodeID:         id,
            ClientHandle:   uint32(i),
        })
    }

    select {} // run forever
}

func handleChange(item *ua.MonitoredItemNotification, mappings []NodeMapping, mqtt mqtt.Client) {
    idx := int(item.ClientHandle)
    topic := mappings[idx].MQTTTopic
    payload, _ := json.Marshal(map[string]any{
        "ts":    time.Now().UTC().Format(time.RFC3339),
        "value": item.Value.Value.Value(),
    })
    mqtt.Publish(topic, 1, false, payload)
}

200 lines, runnable on an edge gateway. Reliable for 100s of nodes.

Node selection

Don’t subscribe to every node. PLCs expose hundreds; most are internal state nobody needs externally. Curate.

The pattern that works:

  1. Browse the address space once via UaExpert (free OPC UA browser) — visual tree of nodes
  2. Document the mapping in YAML/JSON config: which OPC UA NodeID → which MQTT topic
  3. Subscribe only to the mapped nodes at runtime
  4. Add new nodes via config, not code
# bridge-config.yaml
mappings:
  - opcua_node: "ns=2;s=ProductionLine1.Press42.Temperature"
    mqtt_topic: "factory/press42/temperature"
    type: float
    sample_rate_ms: 500
  - opcua_node: "ns=2;s=ProductionLine1.Conveyor3.Speed"
    mqtt_topic: "factory/conveyor3/speed"
    type: float
    sample_rate_ms: 1000

OT and IT teams can collaborate on the YAML. OT says “we’ll expose these 20 nodes”; IT consumes the MQTT topics.

Security

OPC UA security has multiple modes:

  • None — no auth, no encryption. Lab/dev only.
  • Sign — auth + integrity but no encryption.
  • SignAndEncrypt — full TLS-equivalent.

For production, always SignAndEncrypt. The bridge needs a cert; the PLC needs to trust it. Coordinate with OT before deployment.

Auth options:

  • Anonymous — for dev only
  • Username/password — basic
  • Certificate-based — mutual auth, production grade

Common Pitfalls

Subscribing to everything. Floods MQTT, exhausts PLC bandwidth. Curate.

Sample rate too low. Subscribing at 50ms intervals when 1s is fine = wasted everything. Tune to what the metric actually needs.

No reconnect logic. OPC UA connections drop; bridge must reconnect. Most SDKs have it; configure it.

Translating OPC UA semantics to MQTT badly. E.g., OPC UA “quality” field is rich (good/bad/uncertain with reason codes). Squashing to “value only” loses information. Decide what to keep.

No node-name versioning. OT renames a node; bridge breaks silently. Use stable OPC UA NodeIDs, not browse paths.

Bridge as single point of failure. If the bridge dies, all data stops. Run two or have monitoring + auto-restart.

Wrapping Up

OPC UA → MQTT bridge is the universal “factory data into cloud” pattern. Commercial for fast-time-to-value, custom for control. Monday: Grafana dashboards for IIoT.