# Driver test-client CLIs

Six shell-level ad-hoc validation tools, one per native-protocol driver family.
Each mirrors the v1 `otopcua-cli` shape (probe / read / write / subscribe) against
the **same driver** the OtOpcUa server uses — so "does the CLI see it?" and
"does the server see it?" are the same question.

| CLI | Protocol | Docs |
|---|---|---|
| `otopcua-modbus-cli` | Modbus-TCP | [Driver.Modbus.Cli.md](Driver.Modbus.Cli.md) |
| `otopcua-abcip-cli` | CIP / EtherNet-IP (Logix symbolic) | [Driver.AbCip.Cli.md](Driver.AbCip.Cli.md) |
| `otopcua-ablegacy-cli` | PCCC (SLC / MicroLogix / PLC-5) | [Driver.AbLegacy.Cli.md](Driver.AbLegacy.Cli.md) |
| `otopcua-s7-cli` | S7comm / ISO-on-TCP | [Driver.S7.Cli.md](Driver.S7.Cli.md) |
| `otopcua-twincat-cli` | Beckhoff ADS | [Driver.TwinCAT.Cli.md](Driver.TwinCAT.Cli.md) |
| `otopcua-focas-cli` | Fanuc FOCAS/2 (CNC) | [Driver.FOCAS.Cli.md](Driver.FOCAS.Cli.md) |

The OPC UA client CLI lives separately and predates this suite —
see [Client.CLI.md](Client.CLI.md) for `otopcua-cli`.

## Shared commands

Every driver CLI exposes the same four verbs:

- **`probe`** — open a session, read one sentinel tag, print driver health.
  Fastest "is the device talking?" check.
- **`read`** — synthesise a one-tag driver config from `--type` / `--address`
  (or `--tag` / `--symbol`) flags, read once, print the snapshot. No extra
  config file needed.
- **`write`** — same shape plus `--value`. Values parse per `--type` using
  invariant culture. Booleans accept `true` / `false` / `1` / `0` / `yes` /
  `no` / `on` / `off`. Writes are **non-idempotent by default** — a timeout
  after the device already applied the write will not auto-retry (plan
  decisions #44, #45).
- **`subscribe`** — long-running data-change stream until Ctrl+C. Uses native
  push where available (TwinCAT ADS notifications) and falls back to polling
  (`PollGroupEngine`) where the protocol has no push (Modbus, AB, S7, FOCAS).

The TwinCAT CLI adds a fifth verb, **`browse`** — it walks the controller's
symbol table via the driver's `DiscoverAsync` path and prints every symbol the
atomic-type mapper recognises. No other driver CLI ships `browse`.

## Shared infrastructure

All six CLIs depend on `src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.Cli.Common/`:

- `DriverCommandBase` — `--verbose` + Serilog configuration + the abstract
  `Timeout` surface every protocol-specific base overrides with its own
  default.
- `SnapshotFormatter` — consistent output across every CLI: tag / value /
  status / source-time / server-time for single reads, a 4-column table for
  batches, `Write <tag>: 0x... (Name)` for writes, and one line per change
  event for subscriptions. OPC UA status codes render as `0xXXXXXXXX (Name)`
  with a shortlist for `Good` / `Bad*` / `Uncertain`; unknown codes fall
  back to hex.

Writing a seventh CLI (hypothetical Galaxy / OPC UA Client) costs roughly
150 lines: a `{Family}CommandBase` + four thin command classes that hand
their flag values to the already-shipped driver.

## Typical cross-CLI workflows

- **Commissioning a new device** — `probe` first, then `read` a known-good
  tag. If the device is up + talking the protocol, both pass; if the tag is
  wrong you'll see the read fail with a protocol-specific error.
- **Reproducing a production bug** — `subscribe` to the tag the bug report
  names, then have the operator run the scenario. You get an HH:mm:ss.fff
  timeline of exactly when each value changed.
- **Validating a recipe write** — `write` + `read` back. If the server's
  write path would have done anything different, the CLI would have too.
- **Byte-order / word-swap debugging** — `read` with one `--byte-order`,
  then the other. The plausible result identifies the correct setting
  for that device family. (Modbus, S7.)

## Known gaps

- **AB Legacy cip-path quirk** — libplctag's ab_server requires a
  non-empty CIP routing path before forwarding to the PCCC dispatcher.
  Pass `--gateway "ab://127.0.0.1:44818/1,0"` against the Docker
  fixture; real SLC / MicroLogix / PLC-5 hardware accepts an empty
  path (`ab://host:44818/`). Bit-file writes (`B3:0/5`) still surface
  `0x803D0000` against ab_server — route operator-critical bit writes
  to real hardware until upstream fixes this.
- **S7 PUT/GET communication** must be enabled in TIA Portal for any
  S7-1200/1500. See [Driver.S7.Cli.md](Driver.S7.Cli.md).
- **TwinCAT AMS router** must be reachable (local XAR, standalone Router
  NuGet, or authorised remote route). See
  [Driver.TwinCAT.Cli.md](Driver.TwinCAT.Cli.md).
- **Structure / UDT writes** are refused by the AB CIP + TwinCAT CLIs —
  whole-UDT writes need a declared member layout that belongs in a real
  driver config, not a one-shot flag.

## Tracking

Tasks #249 / #250 / #251 shipped the original five. The FOCAS CLI followed
alongside the Tier-C isolation work on task #220. Every CLI — FOCAS included —
ships its own unit-test project under `tests/Drivers/Cli/`, alongside the shared
`tests/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.Cli.Common.Tests`. Re-verify with
`dotnet test tests/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.Cli.Common.Tests` and
each per-family `tests/Drivers/Cli/...Cli.Tests` project.
