Replaced the "ab_server PCCC upstream-broken" skip gate with the actual
root cause: libplctag's ab_server rejects empty CIP routing paths at the
unconnected-send layer before the PCCC dispatcher runs. Real SLC/
MicroLogix/PLC-5 hardware accepts empty paths (no backplane); ab_server
does not. With `/1,0` in place, N (Int16), F (Float32), and L (Int32)
file reads + writes round-trip cleanly across all three compose profiles.
## Fixture changes
- `AbLegacyServerFixture.cs`:
- Drop `AB_LEGACY_TRUST_WIRE` env var + the reachable-but-untrusted
skip branch. Fixture now only skips on TCP unreachability.
- Add `AB_LEGACY_CIP_PATH` env var (default `1,0`) + expose `CipPath`
property. Set `AB_LEGACY_CIP_PATH=` (empty) against real hardware.
- Shorter skip messages on the `[AbLegacyFact]` / `[AbLegacyTheory]`
attributes — one reason: endpoint not reachable.
- `AbLegacyReadSmokeTests.cs`:
- Device URI built from `sim.CipPath` instead of hardcoded empty path.
- New `AB_LEGACY_COMPOSE_PROFILE` env var filters the parametric
theory to the running container's family. Only one container binds
`:44818` at a time, so cross-family params would otherwise fail.
- `Slc500_write_then_read_round_trip` skips cleanly when the running
profile isn't `slc500`.
## E2E + seed + docs
- `scripts/e2e/test-ablegacy.ps1` — drop the `AB_LEGACY_TRUST_WIRE`
skip gate; synopsis calls out the `/1,0` vs empty cip-path split
between the Docker fixture and real hardware.
- `scripts/e2e/e2e-config.sample.json` — sample gateway flipped from
the hardware placeholder (`192.168.1.10`) to the Docker fixture
(`127.0.0.1/1,0`); comment rewritten.
- `scripts/e2e/README.md` — AB Legacy expected-matrix row goes from
SKIP to PASS.
- `scripts/smoke/seed-ablegacy-smoke.sql` — default HostAddress points
at the Docker fixture + header / footer text reflect the new state.
- `tests/.../Docker/README.md` — "Known limitations" section rewritten
to describe the cip-path gate (not a dispatcher gap); env-var table
picks up `AB_LEGACY_CIP_PATH` + `AB_LEGACY_COMPOSE_PROFILE`.
- `docs/drivers/AbLegacy-Test-Fixture.md` + `docs/drivers/README.md`
+ `docs/DriverClis.md` — flip status from blocked to functional;
residual bit-file-write gap (B3:0/5 → 0x803D0000) documented.
## Residual gap
Bit-file writes (`B3:0/5` style) surface `0x803D0000` against
`ab_server --plc=SLC500`; bit reads work. Non-blocking for smoke
coverage — N/F/L round-trip is enough. Real hardware / RSEmulate 500
for bit-write fidelity. Documented in `Docker/README.md` §"Known
limitations" + the `AbLegacy-Test-Fixture.md` follow-ups list.
## Verified
- Full-solution build: 0 errors, 334 pre-existing warnings.
- Integration suite passes per-profile with
`AB_LEGACY_COMPOSE_PROFILE=<slc500|micrologix|plc5>` + matching
compose container up.
- All four non-hardware e2e scripts (Modbus / AB CIP / AB Legacy / S7)
now 5/5 against the respective docker-compose fixtures.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
93 lines
4.6 KiB
Markdown
93 lines
4.6 KiB
Markdown
# Driver test-client CLIs
|
|
|
|
Five 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) |
|
|
|
|
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).
|
|
|
|
## Shared infrastructure
|
|
|
|
All five CLIs depend on `src/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 sixth CLI (hypothetical Galaxy / FOCAS) 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 suite. 122 unit tests cumulative
|
|
(16 shared-lib + 106 across the five CLIs) — run
|
|
`dotnet test tests/ZB.MOM.WW.OtOpcUa.Driver.Cli.Common.Tests` +
|
|
`tests/ZB.MOM.WW.OtOpcUa.Driver.*.Cli.Tests` to re-verify.
|