Files
lmxopcua/docs/Driver.Modbus.Cli.md
Joseph Doherty 969b0847a1 docs: update path references for module-folder reorganization
Rewrite src/ and tests/ project paths in docs, CLAUDE.md, README.md, and
test-fixture READMEs to the new module-folder layout (Core/Server/Drivers/
Client/Tooling). References to retired v1 projects (Galaxy.Host/Proxy/Shared,
the legacy monolithic test projects) are left untouched.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 02:10:29 -04:00

5.9 KiB

otopcua-modbus-cli — Modbus-TCP test client

Ad-hoc probe / read / write / subscribe tool for talking to Modbus-TCP devices through the same ModbusDriver the OtOpcUa server uses. Mirrors the v1 OPC UA otopcua-cli shape so the muscle memory carries over: drop to a shell, point at a PLC, watch registers move.

First of four driver test-client CLIs (Modbus → AB CIP → AB Legacy → S7 → TwinCAT). Built on the shared ZB.MOM.WW.OtOpcUa.Driver.Cli.Common library so each downstream CLI inherits verbose/log wiring + snapshot formatting without copy-paste.

Build + run

dotnet build src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Cli
dotnet run --project src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Cli -- --help

Or publish a self-contained binary:

dotnet publish src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Cli -c Release -o publish/modbus-cli
publish/modbus-cli/otopcua-modbus-cli.exe --help

Common flags

Every command accepts:

Flag Default Purpose
-h / --host required Modbus-TCP server hostname or IP
-p / --port 502 TCP port
-U / --unit-id 1 Modbus unit / slave ID
--timeout-ms 2000 Per-PDU timeout
--disable-reconnect off Turn off mid-transaction reconnect-and-retry
--verbose off Serilog debug output

Commands

probe — is the PLC up?

Connects, reads one holding register, prints driver health. Fastest sanity check after swapping a network cable or deploying a new device.

otopcua-modbus-cli probe -h 192.168.1.10
otopcua-modbus-cli probe -h 192.168.1.10 --probe-address 100    # device locks HR[0]

read — single register / coil / string

Synthesises a one-tag driver config on the fly from --region + --address

  • --type flags.
# Holding register as UInt16
otopcua-modbus-cli read -h 192.168.1.10 -r HoldingRegisters -a 100 -t UInt16

# Float32 with word-swap (CDAB) — common on Siemens / some AB families
otopcua-modbus-cli read -h 192.168.1.10 -r HoldingRegisters -a 200 -t Float32 --byte-order WordSwap

# Single bit out of a packed holding register
otopcua-modbus-cli read -h 192.168.1.10 -r HoldingRegisters -a 10 -t BitInRegister --bit-index 3

# 40-char ASCII string — DirectLOGIC packs the first char in the low byte
otopcua-modbus-cli read -h 192.168.1.10 -r HoldingRegisters -a 300 -t String --string-length 40 --string-byte-order LowByteFirst

# Discrete input / coil
otopcua-modbus-cli read -h 192.168.1.10 -r DiscreteInputs -a 5 -t Bool

write — single value

Same flag shape as read plus -v / --value. Values parse per --type using invariant culture (period as decimal separator). Booleans accept true/false/1/0/yes/no/on/off.

otopcua-modbus-cli write -h 192.168.1.10 -r HoldingRegisters -a 100 -t UInt16 -v 42
otopcua-modbus-cli write -h 192.168.1.10 -r HoldingRegisters -a 200 -t Float32 -v 3.14
otopcua-modbus-cli write -h 192.168.1.10 -r Coils -a 5 -t Bool -v on

Writes are non-idempotent by default — a timeout after the device already applied the write will NOT auto-retry. This matches the driver's production contract (plan decisions #44 + #45).

subscribe — watch a register until Ctrl+C

Uses the driver's ISubscribable surface (polling under the hood via PollGroupEngine). Prints every data-change event with a timestamp.

otopcua-modbus-cli subscribe -h 192.168.1.10 -r HoldingRegisters -a 100 -t Int16 -i 500

Output format

  • probe / read emit a multi-line per-tag block: Tag / Value / Status / Source Time / Server Time.
  • write emits one line: Write <tag>: 0x... (Good | BadCommunicationError | …).
  • subscribe emits one line per change: [HH:mm:ss.fff] <tag> = <value> (<status>).

Status codes are rendered as 0xXXXXXXXX (Name) for the OPC UA shortlist (Good, BadCommunicationError, BadTimeout, BadNodeIdUnknown, BadTypeMismatch, Uncertain, …). Unknown codes fall back to bare hex.

Typical workflows

"Is the PLC alive?"probe.

"Does my recipe write land?"write + read back against the same address.

"Why is tag X flipping?"subscribe + wait for the operator scenario.

"What's the right byte order for this family?"read with --byte-order BigEndian, then with --byte-order WordSwap. The one that gives plausible values is the correct one for that device.

v2 addressing grammar

The driver accepts the industry-standard tag-address grammar so you can paste tag spreadsheets from Wonderware / Kepware / Ignition without per-row manual translation. Full reference + grammar rules: docs/v2/modbus-addressing.md.

Quick examples:

40001                  HoldingRegisters[0], Int16
400001                 same, 6-digit form
40001:F                Float32
40001:F:CDAB           Float32 word-swapped
40001:STR20            20-char ASCII string
40001:S:5              Int16[5] array (3-field shorthand)
40001:F:CDAB:10        Float32[10] with explicit word-swap (4-field strict)
40001.5                bit 5 of HR[0]
HR1:I                  Int32 via mnemonic region prefix (matches Wonderware)
C100                   Coil 100 (mnemonic, 1-based)
V2000:F:CDAB           DL205 V-memory at PDU 1024 + Float32 + word-swap (Family=DL205)
D100:I                 MELSEC D-register 100, Int32 (Family=MELSEC)

Type-code reminder (post-#146): :I is Int32 (matches Wonderware DASMBTCP + Ignition HRI). The explicit Int16 code is :S. Bare HR/IR with no type still defaults to Int16. Pre-#146 codes :DI / :L / :UDI / :UL / :LI / :ULI / :LBCD are removed; configs that use them get a clear "Unknown type code" diagnostic at parse time.

In DriverConfig JSON, set the per-tag addressString field instead of the structured region + address + dataType fields. Both styles can coexist within one driver instance.