Files

8.5 KiB

Modbus Driver

In-process native-protocol driver that exposes Modbus-TCP devices as OPC UA variable nodes. It runs inside the OtOpcUa server's .NET 10 AnyCPU process and speaks Modbus-TCP directly over a socket — no gateway, no sidecar, no bitness constraint. Modbus has no discovery protocol and no native push model, so the address space is built entirely from pre-declared tags and subscriptions are a polling overlay on top of IReadable.

For the driver spec (capability surface, config shape, byte-order matrix), see docs/v2/driver-specs.md §2. For the manual test client, see Driver.Modbus.Cli.md. For the integration fixture coverage map, see Modbus-Test-Fixture.md.

Project Layout

Project Role
src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Modbus/ The driver — ModbusDriver plus the ModbusTcpTransport socket layer, the connectivity probe, and the auto-prohibition planner.
src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Addressing/ Shared address grammar — ModbusAddressParser and the ModbusRegion / ModbusDataType / ModbusByteOrder / ModbusFamily enums. Lives in its own assembly so the Admin UI and the parser can speak about addresses without a transport dependency.
src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Contracts/ ModbusDriverOptions + ModbusTagDefinition config records bound from the driver's DriverConfig JSON.

Capability Surface

ModbusDriver : IDriver, ITagDiscovery, IReadable, IWritable, ISubscribable, IHostConnectivityProbe, IPerCallHostResolver, IDisposable, IAsyncDisposable (Driver.Modbus/ModbusDriver.cs). There is no IAlarmSource and no IHistoryProvider — the Modbus protocol expresses neither, so those capabilities are out of scope by design.

Capability Implementation entry point Notes
ITagDiscovery DiscoverAsync Emits one Modbus/{tag} variable per pre-declared tag; Modbus has no browse protocol, so the driver returns exactly the configured Tags.
IReadable ReadAsyncReadOneAsync / ReadCoalescedAsync FC01/FC02 for coils, FC03/FC04 for registers; auto-chunks reads past the per-device cap.
IWritable WriteAsyncWriteOneAsync FC05/FC15 for coils, FC06/FC16 for registers; BitInRegister writes do a per-register read-modify-write under a lock. DiscreteInputs / InputRegisters are read-only and return BadNotWritable.
ISubscribable SubscribeAsync driven by the shared PollGroupEngine No native push — subscriptions become per-tag polling groups with an optional per-tag Deadband filter.
IHostConnectivityProbe ProbeLoopAsync + GetHostStatuses Periodic cheap FC03 at Probe.ProbeAddress; HostName is the Host:Port string surfaced to the Admin UI.
IPerCallHostResolver ResolveHost Routes each call to a per-slave breaker key (Host:Port/unit{UnitId}) so a dead RTU slave behind a multi-unit gateway opens its own breaker.

Addressing Model

Every exposed register is a pre-declared ModbusTagDefinition (Region, Address, DataType, ByteOrder, …). Tag spreadsheets are typically authored as address strings parsed by ModbusAddressParser at config-bind time; the grammar is <region><offset>[.<bit>][:<type>[<len>]][:<order>][:<count>]:

Form Example Meaning
Modicon digits 40001 / 400001 Holding register 0 (5- or 6-digit form), default Int16
Mnemonic prefix HR1 / IR1 / C100 / DI5 Region prefix + 1-based register number
Bit suffix 40001.5 Bit 5 of holding register 0 (BitInRegister)
Explicit type 40001:F / 40001:STR20 Float32 / 20-char ASCII string
Word order 40001:F:CDAB Float32 with word-swap byte order
Array 40001:F:5 Float32[5] (consumes HR[0..9])

The four regions (Coils, DiscreteInputs, InputRegisters, HoldingRegisters) map directly to function-code selection. The type codes are aligned with Wonderware DASMBTCP and the Ignition Modbus driver so pasted tag sheets translate without manual rewriting.

Byte/word order is the most common production misconfiguration. The four ModbusByteOrder mnemonics — ABCD (BigEndian, spec default), CDAB (WordSwap), BADC (ByteSwap), DCBA (FullReverse) — describe how bytes A/B/C/D appear across consecutive registers when decoding a multi-register value.

Device Profiles

ModbusDriverOptions.Family selects a parser family-native branch (ModbusFamily):

  • Generic (default) — only Modicon (4xxxx) and mnemonic (HR1, C100) forms are accepted.
  • DL205 — AutomationDirect DirectLOGIC. V-memory (octal) → HoldingRegisters, Y/C → Coils, X/SP → DiscreteInputs. Strings can be packed low-byte-first via ModbusTagDefinition.StringByteOrder (the grammar can't express this — see ModbusStringByteOrder).
  • MELSEC — Mitsubishi. D-registers → HoldingRegisters, X → DiscreteInputs, Y/M → Coils; the MelsecSubFamily selector switches Q/L/iQR (hex) vs FX (octal) X/Y interpretation.

Per-family register caps are honoured through MaxRegistersPerRead / MaxRegistersPerWrite / MaxCoilsPerRead (e.g. DL205/DL260 cap reads at 128, Mitsubishi Q at 64); the driver auto-chunks larger reads into consecutive requests.

Coalesced Reads + Auto-Prohibition

When MaxReadGap > 0 the read planner (ReadCoalescedAsync) groups tags in the same (UnitId, Region), sorts by address, and merges near-adjacent register spans (gap ≤ MaxReadGap, total span ≤ the read cap) into a single FC03/FC04 PDU, then slices the response back into per-tag values. If a coalesced read hits a Modbus exception (illegal/protected register), the offending range is recorded as auto-prohibited so the planner stops re-coalescing across it; the surviving members fall back to per-tag reads in the same scan. Setting AutoProhibitReprobeInterval starts a background loop that periodically retries prohibited ranges and uses bisection to narrow a multi-register prohibition down to the actual offending register(s). Per-tag escape hatch: ModbusTagDefinition.CoalesceProhibited.

Configuration

ModbusDriverOptions (Driver.Modbus.Contracts/ModbusDriverOptions.cs) binds from the driver's DriverConfig JSON. Key fields:

  • EndpointHost, Port (default 502), UnitId, Timeout. Per-tag UnitId overrides drive multi-slave gateway topology.
  • Tags — the pre-declared ModbusTagDefinition list; this is the address space.
  • Probe — connectivity-probe interval / timeout / probe register (default register 0).
  • Read/write capsMaxRegistersPerRead (125), MaxRegistersPerWrite (123), MaxCoilsPerRead (2000), plus MaxReadGap and AutoProhibitReprobeInterval for coalescing.
  • Function-code overridesUseFC15ForSingleCoilWrites, UseFC16ForSingleRegisterWrites for PLCs that only accept multi-write codes.
  • ResilienceAutoReconnect, KeepAlive, IdleDisconnectTimeout, Reconnect backoff, and WriteOnChangeOnly redundant-write suppression.

Full per-field descriptions live in ModbusDriverOptions.cs. The JSON skeleton is reproduced in docs/v2/driver-specs.md §2.

Testing

  • Unit teststests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Tests/ (driver behaviour via a fake transport) and tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Addressing.Tests/ (the address grammar).
  • Integration teststests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/ run against the Docker Modbus simulator fixture. See Modbus-Test-Fixture.md for the coverage map and the MODBUS_SIM_ENDPOINT wiring.
  • Manual clientDriver.Modbus.Cli.md.

Operational Notes

  • Wrong-endian readings are silently plausible. A byte-order misconfiguration produces a wrong number, not a Bad quality code — surface byte-order mismatches as data-validation alerts, not status codes (see docs/v2/driver-specs.md §2).
  • WriteOnChangeOnly + write-only tags — the suppression cache is only invalidated by a read that returns a divergent value. A tag that is never subscribed/polled never refreshes its cache entry, so a re-asserted value can be suppressed indefinitely. Subscribe every tag that needs deterministic re-writes, or leave the option off.
  • Auto-prohibited ranges are visible via GetAutoProhibitedRanges and logged on first occurrence / on clear — use them to find protected register holes in a device's map.