# AB Legacy diagnostic counters Per-device diagnostic counters surface as auto-generated read-only OPC UA variables under each device's synthetic `_Diagnostics/` folder. HMIs can bind directly without going through a separate diagnostics RPC. Mirrors the AB CIP `_System/` pattern from PR abcip-4.3. Closes #253 (PR ablegacy-10). ## The seven counters Each device managed by the `AbLegacyDriver` exposes seven read-only nodes under `AbLegacy//_Diagnostics/`: | Name | Type | Semantics | |---|---|---| | `RequestCount` | Int64 | Total `ReadAsync` requests issued against this device. One increment per non-diagnostic reference per call, success or failure. | | `ResponseCount` | Int64 | Successful read responses. | | `ErrorCount` | Int64 | Failed read responses (any non-Good status). | | `RetryCount` | Int64 | Retry attempts beyond the first per the PR 9 retry loop. A single read with two retries adds two. | | `LastErrorCode` | Int32 | Most recent libplctag status code on a failed read; `0` when no error has been seen since the last reset. | | `LastErrorMessage` | String | Most recent libplctag error message on a failed read; empty when no error has been seen since the last reset. | | `CommFailures` | Int64 | Count of read failures mapped to `BadCommunicationError`. Spans transient libplctag throws + retried-out chains so operators see a single "wire fell off" counter. | **Address shape**: `_Diagnostics//` — e.g. `_Diagnostics/ab://10.0.0.5/1,0/RequestCount`. The `` segment is the canonical `ab://host[:port]/cip-path` string from `AbLegacyDeviceOptions.HostAddress`. The browse path looks like `AbLegacy//_Diagnostics/` — the same shape as a user-config tag node, just under a reserved sibling folder. ## Reset behaviour | Trigger | Effect | |---|---| | `ReinitializeAsync` | Every counter for every device resets to zero, plus `LastErrorMessage` clears to empty. | | `ShutdownAsync` | Same as Reinitialize — counters drop with the device map. | | Driver process restart | Counters start at zero. | | Probe transition Stopped→Running | **No automatic reset** — counters are cumulative across reconnect events so operators can spot intermittent links by watching `CommFailures` keep climbing. | There is no in-process "reset" RPC at the time of writing. If you need to clear counters without a redeploy, kick a `ReinitializeAsync` from the Admin RPC surface — the driver re-EnsureDevice's each host so the freshly registered counters start at zero. ## What does *not* increment counters Reads against `_Diagnostics//` are **driver-local observability**, not field traffic — they short-circuit before the libplctag dispatch and do NOT increment `RequestCount` or any other counter. Otherwise a 1 Hz HMI poll of `RequestCount` would make the counter chase its own tail. Writes against `_Diagnostics/*` are rejected with `BadNotWritable` because every diagnostic node is `SecurityClassification.ViewOnly` — a misbehaving SCADA template can't accidentally clobber the diagnostic surface. ## Collision with user tags User-config tags must not shadow the seven reserved diagnostic names and must not live under the synthetic `_Diagnostics/` folder. Both shapes are rejected at `InitializeAsync` time with a clear `InvalidOperationException`: - A tag named `RequestCount` (or any of the other six reserved names) is rejected because it would silently never resolve at read time — the diagnostics short-circuit wins. - A tag whose `Address` starts with `_Diagnostics/` is rejected because the whole prefix is owned by the auto-emitted counters. Pick a different name (`SiteRequestCount`, `MachineRequestCount`) or a different address path (real PCCC files like `N7:0`). ## HMI binding examples ### OPC UA Client CLI ```powershell dotnet run --project src/ZB.MOM.WW.OtOpcUa.Client.CLI -- read ` -u opc.tcp://localhost:4840 ` -n "ns=2;s=AbLegacy/ab://10.0.0.5/1,0/_Diagnostics/RequestCount" ``` ### AB Legacy CLI (driver-direct, no OPC UA layer) ```powershell dotnet run --project src/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Cli -- read ` -g "ab://10.0.0.5/1,0" -P Slc500 ` --address "_Diagnostics/RequestCount" ``` The driver-direct path lets you sanity-check the counter without standing up an OPC UA server — useful when triaging a wire-level issue on the bench. ### Subscription pattern Subscribe to all seven counters at a slow rate (e.g. 5–10 s) on a long-lived overview dashboard, plus a faster rate (1 s) on `LastErrorMessage` / `LastErrorCode` when actively debugging a flapping link. The diagnostics short-circuit makes every read O(1) — there's no penalty for fast polling of the counter itself, only the OPC UA subscription bookkeeping. ## Cross-references - [`AbLegacyDiagnosticTags.cs`](../../src/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/AbLegacyDiagnosticTags.cs) — counter store + read short-circuit - [`AbLegacyDriver.cs`](../../src/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/AbLegacyDriver.cs) — increment sites in `ReadAsync`, discovery emission in `DiscoverAsync` - [`AbLegacy-Test-Fixture.md`](AbLegacy-Test-Fixture.md) — `AbLegacyDiagnosticsTests` + collision-rejection contract - [AB CIP `_System/` parallel](../../src/ZB.MOM.WW.OtOpcUa.Driver.AbCip/AbCipSystemTagSource.cs) — same pattern with the CIP-specific six entries (incl. writeable `_RefreshTagDb` trigger)