Modbus, OPC UA & EtherNet/IP in One Binary: One Protocol Layer for the Edge
A modern plant doesn't run one protocol. It runs Modbus on the power meters, OPC UA on the newer PLCs, EtherNet/IP on the Allen-Bradley gear, S7 on the Siemens line, DNP3 on the SCADA outstations, BACnet on the building automation, and MQTT toward the cloud. Reaching all of them used to mean seven clients, seven mental models, and seven sets of edge-case bugs.
Voltrus Gateway puts all seven behind a single gRPC API, driven by one Rust binary. This piece walks through what "unified" means in practice, and where the gateway stops and the rest of an edge stack begins.
The Seven Protocols, One Mental Model
Every protocol in the gateway answers the same questions the same way: how do I connect, how do I read, how do I write, how do I subscribe, how do I discover tags. The docs describe the five RPCs in full, but the shape is uniform:
| Protocol | Read | Write | Subscribe | Typical Port |
|---|---|---|---|---|
| Modbus TCP | ✓ | ✓ | ✓ (gateway-polled) | 502 |
| OPC UA | ✓ | ✓ | ✓ (native) | 4840 |
| EtherNet/IP (CIP) | ✓ | ✓ | ✓ | 44818 |
| Siemens S7 | ✓ | ✓ | ✓ | 102 |
| BACnet/IP | ✓ | ✓ | ✓ | 47808 |
| DNP3 | ✓ | ✓ | ✓ | 20000 |
For protocols with native subscriptions (OPC UA most prominently), the gateway bridges them straight through. For protocols without (Modbus, BACnet), the gateway runs the poll internally and streams only changes, so your client code looks identical either way.
The mt-* Engines
Each protocol is implemented in its own mt-* Rust crate: mt-modbus, mt-opcua, mt-enip, mt-s7, mt-dnp3, mt-bacnet. These are the same engines that power the Voltrus explorer apps. The gateway isn't a second implementation: it's the canonical one, exposed over gRPC.
mt-enip is fixed for the EtherNet/IP Explorer desktop app, for the gateway, and for anything else built on the engine. Protocol complexity lives in one place per protocol, audited and battle-tested by both surfaces.
Read / Write / Subscribe Parity
"Unified" only means something if the parity is real. Voltrus Gateway ships full read, write, and subscribe on the six device protocols (MQTT adds publish, read and subscribe), not "read on most, write on a few." A holding-register write on Modbus, a tag write on OPC UA, and a coil write on a CIP controller all go through the same WriteTag RPC with a typed value. The gateway encodes the value correctly for the target protocol; your client doesn't know or care which one.
Discover, Don't Guess
Half the work in talking to a new device is figuring out what tags it actually exposes. Discover browses the device's tag namespace over the same gRPC surface (OPC UA's address space, a CIP controller's symbol table, an S7 CPU's data blocks), so you don't need a separate browsing tool to map out a PLC before you can read it.
What a Protocol Layer Should Handle
Translating between OT and IT protocols is more than reformatting bytes. Four jobs belong in the layer that holds the device connection, so that every consumer does not redo them:
- Type conversion. A 32-bit float on Modbus is two 16-bit registers whose word order depends on the vendor (see Modbus floating-point byte order). A BACnet Present-Value and a CIP
DINTeach have their own shape. The gateway returns a typed value plus a source type such asCIP.DINT, so the client branches on type instead of guessing. - Quality. OPC UA status codes and BACnet status flags say whether a value can be trusted. Downstream code should see "bad" as bad, not as the last good number.
- Timestamps. Modbus carries none on the wire, so something has to stamp a value when it is read. Decide where that happens and do it once.
- Tag naming. Holding register 40001 means nothing to an operator. Mapping it to a name like
boiler.outlet.temperaturebelongs in configuration next to the connection, not scattered across scripts.
What to Keep Out of It
The gateway's job is the device connection and the typed API. Store-and-forward buffering, history, dashboards and a message broker are better handled by whatever consumes the data, and the arithmetic shows why you want to be deliberate about where they live. A remote site with 100 tags read once a second produces 8.64 million data points a day. If the uplink drops for a weekend, whichever component buffers that has to survive it without filling the disk, and it has to send alarms ahead of bulk history when the link returns. Deadband filtering (do not send a value that has not changed) cuts that volume a lot, and it is best done before data leaves the site.
A Minimal Edge Stack
For plain protocol translation, a minimal stack covers most sites with three parts on one small machine:
- Protocol layer. Voltrus Gateway holds the connections to the PLCs, meters and controllers.
- A consumer with a local store. A small collector or SCADA that subscribes over gRPC and writes to an embedded database such as SQLite, so an outage costs you nothing but delay.
- Northbound transport. MQTT or HTTP out to the cloud or the central SCADA.
The alternative stack that vendors like to sell adds a local MQTT broker, a flow engine, a time-series database and a dashboard server, each its own service. Each one is another thing to patch, monitor and restart on a box that may be hours away, and when the site stops reporting you have four suspects instead of one. That stack earns its keep when you need a local HMI that works without the uplink, several independent consumers of the same data, months of local history, or non-programmers building logic. If none of those apply, start minimal. For reaching a remote box when something does go wrong, see Voltrus Remote.
MQTT on the Same Surface
MQTT ships in the same binary and joins the same gRPC surface: publish, read and subscribe bridged into the unified tag model. Adding a protocol doesn't add a client; it adds a connect option.
Seven Protocols, One Binary
Voltrus Gateway drives Modbus, OPC UA, EtherNet/IP, S7, DNP3, BACnet, and MQTT behind one typed gRPC API. Linux and Windows, self-hosted, one-time per host.
Explore Voltrus Gateway →