# `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 ```powershell 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: ```powershell 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. ```powershell 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. ```powershell # 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`. ```powershell 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. ```powershell 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 : 0x... (Good | BadCommunicationError | …)`. - `subscribe` emits one line per change: `[HH:mm:ss.fff] = ()`. 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.