Mirrors the v1 otopcua-cli value prop (ad-hoc shell-level PLC validation) for
the Modbus-TCP driver, and lays down the shared scaffolding that AB CIP, AB
Legacy, S7, and TwinCAT CLIs will build on.
New projects:
- src/ZB.MOM.WW.OtOpcUa.Driver.Cli.Common/ — DriverCommandBase (verbose
flag + Serilog config) + SnapshotFormatter (single-tag + table +
write-result renders with invariant-culture value formatting + OPC UA
status-code shortnames + UTC-normalised timestamps).
- src/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Cli/ — otopcua-modbus-cli executable.
Commands: probe, read, write, subscribe. ModbusCommandBase carries the
host/port/unit-id flags + builds ModbusDriverOptions with Probe.Enabled
=false (CLI runs are one-shot; driver-internal keep-alive would race).
Commands + coverage:
- probe single FC03 + GetHealth() + pretty-print
- read region × address × type synth into one driver tag
- write same shape + --value parsed per --type
- subscribe polled-subscription stream until Ctrl+C
Tests (38 total):
- 16 SnapshotFormatterTests covering: status-code shortnames, unknown
codes fall back to hex, null value + timestamp placeholders, bool
lowercase, float invariant culture, string quoting, write-result shape,
aligned table columns, mismatched-length rejection, UTC normalisation.
- 22 Modbus CLI tests:
· ReadCommandTests.SynthesiseTagName (5 theory cases)
· WriteCommandParseValueTests (17 cases: bool aliases, unknown rejected,
Int16 bounds, UInt16/Bcd16 type, Float32/64 invariant culture,
String passthrough, BitInRegister, Int32 MinValue, non-numeric reject)
Wiring:
- ZB.MOM.WW.OtOpcUa.slnx grew 4 entries (2 src + 2 tests).
- docs/Driver.Modbus.Cli.md — operator-facing runbook with examples per
command + output format + typical workflows.
Regression: full-solution build clean; shared-lib tests 16/0, Modbus CLI tests
22/0.
Next up: repeat the pattern for AB CIP (shares ~40% more with Modbus via
libplctag), then AB Legacy, S7, TwinCAT. The shared base stays as-is unless
one of those exposes a gap the Modbus-first pass missed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
4.4 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/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Cli
dotnet run --project src/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Cli -- --help
Or publish a self-contained binary:
dotnet publish src/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
--typeflags.
# 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/reademit a multi-line per-tag block:Tag / Value / Status / Source Time / Server Time.writeemits one line:Write <tag>: 0x... (Good | BadCommunicationError | …).subscribeemits 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.