One Typed gRPC API for Every Industrial Protocol
Every industrial protocol — Modbus, OPC UA, EtherNet/IP, Siemens S7, DNP3, BACnet — has its own wire format, addressing scheme, and quirks. The conventional answer is to write (or buy) a different client library for each one. Your SCADA has a Modbus client, an OPC UA client, a CIP client, an S7 client. Each is a dependency, a learning curve, and a place for protocol bugs to hide independently.
Voltrus Gateway takes a different shape: one binary that speaks all of them, exposed behind a single typed gRPC API. Your application talks to one client. The gateway drives the device.
The Problem with One Client per Protocol
- Repeated logic. Connect/read/write/subscribe gets re-implemented per protocol in every SCADA, HMI, and edge collector.
- Bugs multiply. A Modbus addressing bug and an S7 addressing bug are fixed in two different places, by two different people, on two different schedules.
- Types get lost. A CIP
DINT, an OPC UA string, and a signed Modbus register all tend to get flattened to a JSON number by the time they reach application code. - Polling everywhere. Without native subscriptions, every client runs its own polling loop and timers.
One Surface: Five RPCs
The entire gateway is five RPCs. They're the same regardless of which protocol sits underneath:
rpc OpenSession(ConnectRequest) -> ConnectResponse
rpc ReadTags(ReadTagsRequest) -> ReadTagsResponse
rpc WriteTag(WriteTagRequest) -> WriteResult
rpc SubscribeTags(SubscribeRequest) -> stream TagUpdateBatch
rpc Discover(DiscoverRequest) -> DiscoverResponse
Open a session to a device, read or write typed tags, subscribe for live streamed updates, and discover the tag namespace — all through one client.
Type-Preserving Values
The decisive design choice is that tag values are carried as a typed oneof, not a flattened number:
message TagValue {
oneof value {
bool v_bool; int32 v_int32; int64 v_int64;
uint32 v_uint32; float v_float; double v_double;
string v_string; bytes v_bytes;
}
string source_type; // e.g. "CIP.DINT", "OPCUA.String"
}
A CIP DINT arrives as an int32 with source_type "CIP.DINT". An OPC UA string arrives as a string. Your application branches on type rather than guessing byte order or scale factors. The same value read over Modbus, OPC UA, or EtherNet/IP reaches the client in the same shape — the gateway absorbs each protocol's encoding quirks.
Server-Streamed Subscriptions
SubscribeTags returns a server stream of TagUpdateBatch. There is no client-side polling loop to maintain — the gateway pushes deltas as devices change. For protocols like OPC UA that already have native subscriptions, the gateway bridges them transparently; for protocols like Modbus that don't, the gateway runs the poll internally and streams only changes.
One Canonical Implementation
The gateway runs the same mt-* Rust crates that power the Voltrus explorer apps. That makes the gateway the single canonical implementation of each protocol in the stack. A protocol bug fixed in the mt-modbus engine is fixed for the explorer, the gateway, and anything else built on it — once, everywhere.
When This Architecture Pays Off
- Custom collectors and cloud pipelines that need to reach many device types through one client.
- SCADA backends that want to stop re-implementing protocol clients and focus on SCADA concerns.
- Edge services running on Linux containers where a Windows OPC server isn't an option.
Stop Writing a Client per Protocol
Voltrus Gateway exposes Modbus, OPC UA, EtherNet/IP, S7, DNP3, and BACnet behind one typed gRPC API. Self-hosted, cross-platform, one-time per host.
Explore Voltrus Gateway →The Bottom Line
One typed gRPC API for every industrial protocol replaces a stack of per-protocol clients with a single canonical surface. Type-preserving values stop data from being flattened; server-streamed subscriptions stop you from writing polling loops; shared mt-* engines stop protocol bugs from multiplying. The gateway is the layer where protocol complexity ends and your application begins.