Digital Factory

Industrial connectors for Unreal Engine 5.8

Plant data,
as Blueprint tags.

Five connectors that read live values from the equipment you already run — PLCs, brokers and OPC UA servers — and publish them into Unreal as named tags you can bind to. No middleware, no bridge service, no polling loop to write.

DIGITAL FACTORY TAG REGISTRY 0 updates
TagValueQualityUpdated
5Protocols, sold separately
0Plugin dependencies
43Automated tests per build
C++Full source included

In every connector

The parts nobody enjoys writing twice

Tag registry

A Game Instance Subsystem holding every value the connector has seen. Bind On Tag Updated, or read a tag by name. Connectors are kept alive by the registry, so one created in a Blueprint survives without being stored in a variable.

Reconnect that behaves

Fixed or exponential backoff with a delay ceiling, an optional attempt limit, and jitter — so a hundred clients pointed at one PLC don't retry in lockstep as it reboots. An outage reports twice: when it starts and when it gives up.

Credentials by reference

A config stores where a secret lives — an environment variable name or a file path — never the secret. Configs get exported, diffed and committed; a password inside one leaks the first time that happens.

Deduplication

Suppress broadcasts when a value hasn't changed, with a numeric deadband to absorb sensor noise. The deadband compares against the last broadcast value, so a reading creeping just under the threshold still reports eventually.

Writing, not just reading

Every connector sends as well as receives — Modbus writes registers and coils, OPC UA writes nodes, MQTT and AMQP publish, REST issues a request on demand. Writes are queued from the game thread and executed on the worker, so a Blueprint never blocks on a network round trip, and the call reports whether it was accepted rather than pretending it was delivered.

Runtime log level

Set verbosity from Blueprint using the familiar debug/info/warn/error/critical names. Works in packaged builds, where there is no console to type into.

Readable source

Full C++ ships with every connector, commented to explain why rather than what. Automated tests come with it — run them yourself from the Session Frontend.

Before you buy

What these connectors don't do

Stated here rather than discovered after purchase.

  • Win64 only. The vendored libraries are built for that platform.
  • MQTT and AMQP have no TLS. Credentials travel in the clear. Fine against a broker on the same machine, unsuitable for a plant network. OPC UA is the exception — it supports encrypted channels.
  • Values are stored as double. Integers above 253 lose precision, which affects OPC UA Int64 and Sparkplug long metrics.
  • Tag quality is only as good as the source. It defaults to Unknown, because Modbus, REST, MQTT and AMQP carry no quality field and claiming otherwise would invent data. OPC UA reports it properly, mapped from the server's status code. Nothing sets Stale yet, and a tag that stops arriving altogether keeps its last value.
  • OPC UA converts scalars only. Arrays and structs are skipped with a warning.
  • AMQP is 0-9-1, not 1.0. It speaks to RabbitMQ, not to Azure Service Bus or Solace.
  • Sparkplug B is decode-only. Reading works; issuing NCMD or DCMD commands does not, because that needs a protobuf encoder and this release only has a decoder. Raw MQTT publishing is available on the same connector — it is Sparkplug's command semantics specifically that are absent.

Connector

Modbus TCP

Polls coils, discrete inputs, holding registers and input registers from a Modbus TCP device using a register map you define, and publishes each entry as a tag. Writes single coils and single registers back.

Function codesRead 0x01 0x02 0x03 0x04, write 0x05 0x06
FramingMBAP over Unreal's own sockets
Data typesUInt16, Int16, UInt32, Int32, Float32, with configurable word order
Read strategyAdjacent registers coalesced into as few requests as the device allows
WritesQueued from the game thread, executed on the worker, echo verified
AuthenticationNone — the protocol carries no identity
Third-party codeNone redistributed
// One entry in the register map
Address       2        // zero-based protocol address
Type          Float32
Word Order    HighFirst
Tag Name      oven.temp_c
Function      ReadHoldingRegisters
Watch the addressing. This connector uses zero-based protocol addresses. The 4xxxx display convention starts at 40001 — there is no 40000 — so holding register 40001 is address 0, and the conversion is subtract 40001 rather than 40000. Vendors are inconsistent about this, and some document the raw address already. If every tag reads like its neighbour, the base is the first thing to check.

Testing without hardware

Tools/modbus_sim.py ships with the plugin: a Python server that serves changing values and prints what it served — decoded back out of the response bytes rather than recomputed, so the console and Unreal genuinely agree rather than merely appearing to. Standard library only, so there is nothing to install.

It answers reads and writes both. A written value is remembered from then on, so the register stops moving and reads back what you wrote: a simulator that accepted a write and carried on generating would be indistinguishable from one that discarded it.

Public simulators such as diagslave also work, but their register tables start at zero and stay there — which looks exactly like a broken decode.

python modbus_sim.py

# listening on
127.0.0.1:502

# holding / input registers
0      UInt16    counter, +1/s
1      Int16     sine, -1000..1000
2-3    Float32   20.0..25.0, high word first
4-5    UInt32    +10/s, high word first

# coils / discrete inputs
0      toggles once per second
1      always on
2      always off

Screenshots

Connector

REST

Polls a JSON-over-HTTP endpoint on an interval and flattens the response into tags. For the MES, historian or line-controller that already exposes an API.

TransportUnreal's own HTTP stack
FlatteningNested objects to dotted names, arrays to bracketed indices
Root pathScope the walk to a nested object with a dotted path
NullsPublished as an empty value, never dropped — so a source that stops reporting is distinguishable from one that hasn't changed
SendingSend Request — one POST or PUT on demand, outside the polling loop, reusing the configured endpoint and credentials
AuthenticationBasic, Bearer token, or API key in a named header
Third-party codeNone redistributed
// Response
{ "line1": { "sensors": [ { "temp": 20.4 } ],
            "vibration": null } }

// Tags produced
line1.sensors[0].temp   20.4
line1.vibration         (empty, quality Unknown)
A null is a reading, not an absence. Dropping it would leave the previous value in the registry, so an endpoint that stopped reporting would look identical to one whose value simply had not changed. The quality stays Unknown rather than Bad: plenty of systems use null to mean something specific — a drive at standstill, a probe not fitted — and calling that a fault would be our inference, not the source's statement.

Testing without a server of your own

Tools/rest_sim.py ships with the plugin: a JSON endpoint serving changing plant telemetry. Standard library only, so there is nothing to install on a machine you would rather not install anything on.

It is built around the awkward cases rather than the tidy ones — nested objects and an array for dotted and indexed names, every value type in one payload, a nested path worth aiming Response Root Path at, and optional bearer-token auth that refuses anything else with a 401 naming the header it wanted.

One sensor reports null for eight seconds each minute while the rest of the document keeps going, so you can watch a single tag go empty beside its neighbours. That case is invisible in a connector that drops nulls, which is exactly why it is worth being able to produce on demand.

Any other JSON endpoint works too. Prefer this one for anything you intend to keep: a public API rate-limits, changes its schema without telling you, and eventually stops existing.

# no auth
python rest_sim.py

# require Authorization: Bearer s3cret
python rest_sim.py --token s3cret

# listening on
http://127.0.0.1:18080

# endpoints
/                 the whole document
/api/telemetry    the same, for a config
                  that wants a path
/health           {"ok": true}, no auth

# a scoped walk: set Response Root
# Path to plant.line1 and the prefix
# disappears from every tag name

Screenshots

Connector

MQTT

Subscribes to broker topics and publishes arriving messages as tags, deriving tag names from the topic hierarchy. Decodes Sparkplug B payloads where your edge nodes publish them.

ClientEclipse Paho MQTT C, under the Eclipse Distribution License 1.0
Payload formatsRaw, JSON, and Sparkplug B
Sparkplug BMetric aliases resolved against birth messages; sequence gaps reported; alias table dropped when a node reports death
Tag namingGroup, node, device and metric — so two devices publishing the same metric name don't collide
PublishingPublish and Publish Bytes — topic, payload, QoS 0–2, optional retain
AuthenticationUsername and password
EncryptionNone in this release
// Subscribe to the whole namespace, not just data
Topic Filter     spBv1.0/#
Payload Format   SparkplugB

// DDATA carries an alias, never a name.
// Names arrive only in birth messages, so
// subscribing to data alone leaves every
// value permanently unidentifiable.
Sparkplug decoding is verified against an independent encoder. The decoder's own tests would pass even if a field number had been misread from the specification, because fixture and decoder would share the mistake. A separate Python encoder, checked byte-for-byte against Eclipse Tahu's schema, produces the payloads used to test it.

Screenshots

Connector

AMQP

Consumes a queue on a RabbitMQ broker and publishes each message as tags, taking the tag name from the message's routing key.

Protocol versionAMQP 0-9-1
Libraryrabbitmq-c, under the MIT licence
ModelConsumes an existing queue; declares no queues or exchanges of its own
Payload formatsRaw and JSON
PublishingPublish — exchange, routing key, body, optional persistence
AcknowledgementAutomatic, or manual with a prefetch limit when losing a message matters
AuthenticationUsername and password
EncryptionNone in this release — TLS sources are excluded from the build entirely
This is AMQP 0-9-1, not AMQP 1.0. They share a name and very little else. If your broker is Azure Service Bus, Solace or ActiveMQ speaking 1.0, this connector will not talk to it. If it is RabbitMQ, it will.

Screenshots

Connector

OPC UA

Connects to OPC UA servers and publishes node values as tags. Subscribes by default, with polling available for servers that limit subscription counts. Implements an OPC UA client using the open62541 stack.

Stackopen62541 v1.5.6, under MPL-2.0
Data deliverySubscriptions with configurable publishing interval, sampling interval and queue size; polling as a fallback
Security modesNone, Sign, and Sign & Encrypt
Security policiesBasic256Sha256, Aes128Sha256RsaOaep, Aes256Sha256RsaPss
WritingWrite Bool, Write Number, Write String — the numeric type is stated by the caller, because the server checks it
QualityMapped from the server's status code by severity, including the informational Good_ sub-codes
IdentityAnonymous, or username and password; client certificate with trust list and optional revocation lists
CertificatesGenerate Self Signed Certificate in Blueprint — OPC UA authenticates both ends, so a client needs one of its own
Crypto libraryThe OpenSSL that ships with Unreal Engine; nothing new is redistributed
Value conversionScalars only; arrays and structs are skipped with a warning
// Prosys Simulation Server defaults
Endpoint  opc.tcp://HOST:53530/OPCUA/SimulationServer
Security  SignAndEncrypt
Policy    Basic256Sha256
Node Id   ns=3;i=1001
Use the host name, not localhost. An OPC UA client connects to whatever the server advertises in its endpoint list, which is its own host name. Pointing at localhost can connect and then stall on the redirect.
Deprecated policies are not offered. Basic128Rsa15 and Basic256 are absent by choice — the stack is built without them. Password-protected private keys are also unsupported, since the password would have to live somewhere the config could reach it, which defeats protecting the key.

Screenshots

Licensing

What you're redistributing

Every connector ships full C++ source and a licence inventory naming each third-party component, its version, its upstream commit and its terms. Nothing here requires your project to be licensed under anything.

ConnectorThird-party componentLicence
Modbus TCPNone
RESTNone
AMQPrabbitmq-cMIT
MQTTEclipse Paho MQTT CEclipse Distribution License 1.0 (BSD-3-Clause)
OPC UAopen62541MPL-2.0 — file-level, does not extend to your code

No GPL or LGPL anywhere

Audited specifically, because copyleft of that strength would prevent redistribution inside a proprietary project at all. Two licence documents in the tree contain the words "GNU General Public License" — both inside a definition of the term "Secondary License", not as a dependency.

MPL-2.0 is file-level

Its obligations attach to the open62541 files themselves and do not extend to code that merely links against them — the decisive difference from GPL. The sources ship unmodified, with SHA-256 hashes published so you can verify that.

Built with AI assistance

A material portion of the C++ was written with a large language model, and the Fab listings are tagged accordingly. Every connector has been verified against real servers and brokers, not only against its own tests.