# 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/L-file `N7:0/3`, B-file `B3:0/0`, and I/O image files `I:0/0`/`O:1/2`) do a per-parent-word read-modify-write under a lock. An Input-image (`I`) write is forwarded to the PLC and may be rejected by the device (the device drives the input image from physical inputs); the driver surfaces the device's real PCCC status in that case. 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.

**Bit-within-word writes** — B-file (`B3:0/0`), I/O image (`I:0/0`, `O:1/2`),
N-file (`N7:0/3`), and L-file bit addresses all use the same parent-word
read-modify-write (RMW) path: the driver reads the parent word, flips the target
bit, and writes the word back, under a per-parent lock. The lock serialises the
driver's own concurrent bit-writers against the same word, but cannot guard
against the PLC program writing that word between the driver's read and write —
this is an inherent limitation of any software-level RMW. Input-image (`I`) bit
writes are forwarded to the PLC as-is; the device drives the input image from
physical inputs and may reject the write.

## 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).

## 1-D array support

An AB Legacy tag becomes a **1-D OPC UA array node** when its `TagConfig` JSON carries
`"isArray": true` and `"arrayLength": N` (N ≥ 1, capped at 256).  The canonical rule:
`isArray: true` + `arrayLength >= 1` → array; `isArray: false` (any length) → scalar.

**Read mechanism** — the driver issues a **PCCC multi-element file read** via libplctag.
The tag's file address (e.g. `N7:0`) is extended by `N` element count so libplctag fetches
`N` consecutive PCCC file elements in one EIP/PCCC request.  The response is decoded into
N values of the declared `AbLegacyDataType`.  Maximum element count is 256 (PCCC payload
limit).

This closes the previously-noted limitation ("single-element addressing today") for the
array read path.  Array *writes* remain a follow-up.

**Unit test coverage** — `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/` covers
the multi-element read path via the fake tag runtime.  The AB Legacy integration fixture is
down during normal dev.

**Live-verify** — integration-fixture-gated (requires the AB Legacy Docker fixture).

**Deferrals** — array *writes*, multi-dimensional arrays, per-element historization.
`arrayLength > 256` is rejected at init with an explicit error (PCCC payload cap).

See [Uns.md §Array tags](../Uns.md#array-tags-1-d) for the cross-driver coverage matrix
and the UI authoring flow.

## 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.
