One Typed gRPC API for Every Industrial Protocol

August 9, 2026 · Category: Guide · Tags: gRPC, Architecture, Modbus, OPC UA, EtherNet/IP

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

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.

Why this matters: "the value is 230" is useless if you don't know whether it's volts, a raw count, or half of a 32-bit float split across two registers. Typed values + source type make the data self-describing.

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

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.