# AB Legacy Driver In-process native-protocol driver that exposes legacy Allen-Bradley PLCs — **SLC 500**, **MicroLogix**, **PLC-5**, and Logix-via-PCCC — as OPC UA nodes. It runs inside the OtOpcUa server's .NET 10 AnyCPU process and speaks PCCC over EtherNet/IP through the same libplctag.NET wrapper as the AB CIP driver, but addresses data by **file** (data-table) rather than by symbolic tag. One driver instance can serve many devices; per-device routing is keyed on the canonical `ab://gateway[:port]/cip-path` host-address string. PCCC has no native push model, so subscriptions are a polling overlay on top of `IReadable`. For the driver spec (capability surface, config shape, payload limits), see [docs/v2/driver-specs.md §4](../v2/driver-specs.md). For the manual test client, see [Driver.AbLegacy.Cli.md](../Driver.AbLegacy.Cli.md). For the integration fixture coverage map, see [AbLegacy-Test-Fixture.md](AbLegacy-Test-Fixture.md). ## Project Layout | Project | Role | |---------|------| | `src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/` | The driver — `AbLegacyDriver`, the libplctag runtime wrapper, the PCCC file-address parser (`AbLegacyAddress`), the host-address parser, and the status mapper. | | `src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Contracts/` | `AbLegacyDriverOptions`, `AbLegacyDeviceOptions`, `AbLegacyTagDefinition`, and the `AbLegacyDataType` / `AbLegacyPlcFamily` / `AbLegacyPlcFamilyProfile` records bound from the driver's `DriverConfig` JSON. | Per family the `AbLegacyPlcFamilyProfile` supplies the libplctag `plc` attribute, default CIP path, max-payload bytes, and the `SupportsStringFile` / `SupportsLongFile` capability flags. MicroLogix uses direct EIP (empty default path); MicroLogix and PLC-5 don't ship L-files; PLC-5 predates them entirely. Tag types are validated against the device's profile at init time — declaring a `Long` or `String` tag on a family that can't support it fails fast with a clear message. ## Capability Surface `AbLegacyDriver : IDriver, IReadable, IWritable, ITagDiscovery, ISubscribable, IHostConnectivityProbe, IPerCallHostResolver, IDisposable, IAsyncDisposable` (`Driver.AbLegacy/AbLegacyDriver.cs`). There is **no `IAlarmSource`** — unlike the AB CIP driver, PCCC has no ALMD instruction to project, so alarms are out of scope. | Capability | Implementation entry point | Notes | |------------|---------------------------|-------| | `ITagDiscovery` | `DiscoverAsync` | Emits pre-declared tags under per-device folders. Tags are single-element today (`IsArray` hard-wired false); multi-element file ranges are a tracked follow-up. | | `IReadable` | `ReadAsync` | Per-tag reads serialized per cached runtime under a lock (a libplctag `Tag` handle is not concurrency-safe across the server read path + poll loop). | | `IWritable` | `WriteAsync` | Bit-within-word writes (N-file `N7:0/3`, B-file bits) do a per-parent-word read-modify-write under a lock. Non-writable tags return `BadNotWritable`. | | `ISubscribable` | `SubscribeAsync` driven by the shared `PollGroupEngine` | No push model — subscriptions become polling groups. | | `IHostConnectivityProbe` | `ProbeLoopAsync` + `GetHostStatuses` | One probe loop per device reading `Probe.ProbeAddress`; transitions log Warning (down) / Information (recover). | | `IPerCallHostResolver` | `ResolveHost` | Routes each call to the tag's `DeviceHostAddress`; unknown references fall back to the first device, never throwing (per the interface contract). | ## Addressing Model Per-device host addresses are the canonical `ab://gateway[:port]/cip-path` form parsed by `AbLegacyHostAddress.TryParse`. When the parsed CIP path is empty the family profile's default path is used (e.g. SLC 500 gets `1,0`; MicroLogix stays empty for direct EIP). Tags carry a PCCC **file address** parsed by `AbLegacyAddress` (`AbLegacyAddress.cs`) — file letter + file number + word number, with an optional bit index (`/N`) or structured sub-element (`.ACC`, `.PRE`, …). The string is passed straight through to libplctag's `name=` attribute; the parser validates shape and surfaces the pieces for driver-side routing (e.g. deciding a bit needs read-modify-write): | Form | Meaning | |------|---------| | `N7:0` | Integer file 7, word 0 (signed 16-bit) | | `F8:0` | Float file 8, word 0 (32-bit IEEE-754) | | `B3:0/0` | Bit file 3, word 0, bit 0 | | `L9:0` | Long-integer file (SLC 5/05+, 32-bit) | | `ST9:0` | String file (82-byte fixed-length) | | `T4:0.ACC` / `C5:0.PRE` | Timer / counter sub-element | | `I:0/0` / `O:1/2` / `S:1` | Input / output / status system files (no file number) | `AbLegacyDataType` covers the corresponding PCCC types: `Bit`, `Int` (N), `Long` (L), `Float` (F), `AnalogInt` (A), `String` (ST), and the `TimerElement` / `CounterElement` / `ControlElement` sub-element families. The parser enforces PCCC structural rules — bit-addressing only on 16/32-bit element files, sub-elements only on T/C/R files, no file number on I/O/S — rejecting malformed addresses before they reach libplctag. ## Configuration `AbLegacyDriverOptions` (`Driver.AbLegacy.Contracts/AbLegacyDriverOptions.cs`) binds from the driver's `DriverConfig` JSON: - **`Devices`** — one `AbLegacyDeviceOptions` per PLC (`HostAddress`, `PlcFamily`, optional `DeviceName`). - **`Tags`** — pre-declared `AbLegacyTagDefinition` list (`Name`, `DeviceHostAddress`, `Address`, `DataType`, `Writable`, `WriteIdempotent`). - **`Probe`** — connectivity-probe `Enabled` / `Interval` / `Timeout` / `ProbeAddress`. Full per-field descriptions live in the contracts assembly. The JSON skeleton is reproduced in [docs/v2/driver-specs.md §4](../v2/driver-specs.md). ## Testing - **Unit tests** — `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/` cover the driver, the PCCC address parser, and the host-address parser via fake tag runtimes. - **Integration tests** — `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.IntegrationTests/` run against the AB Legacy Docker fixture. See [AbLegacy-Test-Fixture.md](AbLegacy-Test-Fixture.md) for the coverage map. - **Manual client** — [Driver.AbLegacy.Cli.md](../Driver.AbLegacy.Cli.md). ## Operational Notes - **Native heap is invisible to the GC.** As with AB CIP, `GetMemoryFootprint()` reports CLR allocations only; watch whole-process RSS and use `ReinitializeAsync` to recycle libplctag handles. - **PCCC reconnect is more expensive than CIP** — legacy PLCs have no connection multiplexing, so the resilience pipeline should use longer backoff than for AB CIP (see [docs/v2/driver-specs.md §4](../v2/driver-specs.md)). - **Single-element addressing today** — a PCCC file is inherently an array (an N7 file is up to 256 words), but the current tag surface addresses one element per tag; range-spanning tags must be enumerated element-by-element until multi-element addressing lands.