c0fbe74693
Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
327 lines
26 KiB
Markdown
327 lines
26 KiB
Markdown
# 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) |
|
|
| `I:017/07` | **PLC-5 only:** `I:`/`O:` word and bit indices are **octal** (#244); a digit 8/9 is a parse error |
|
|
| `RTC:0.HR` / `HSC:0.ACC` | **MicroLogix only:** function files RTC, HSC, DLS, MMI, PTO, PWM, STI, EII, IOS, BHI (#245) |
|
|
| `N7:[N7:0]` / `N[N7:0]:5` | Indirect (index word read from the PLC) — resolved at read/write time (#646) |
|
|
|
|
`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.
|
|
|
|
### Family-specific address forms (#244 / #245 / #247)
|
|
|
|
`AbLegacyAddress.TryParse(text, family)` is family-aware; the driver passes the device's `PlcFamily`
|
|
(the one-argument overload is the family-neutral grammar: decimal, no function files).
|
|
|
|
- **PLC-5 octal I/O (#244).** RSLogix 5 shows `I:`/`O:` word and bit indices in octal, so `I:017/07` is word 15,
|
|
bit 7 and `I:017/17` is bit 15. Only `I`/`O` on PLC-5 (`OctalIoAddressing` on the family profile) — SLC,
|
|
MicroLogix and Logix-PCCC stay decimal, and PLC-5 `N`/`B`/... files stay decimal. The record stores decimal and
|
|
`ToLibplctagName()` **emits decimal**, so the libplctag `name=` string and the string the native transport
|
|
re-parses carry identical numbers. This is not a double conversion: libplctag's `pccc.c`
|
|
(`parse_pccc_elem_num` / `parse_pccc_bit_num`) parses decimal numerals only (a FIXME there says octal is
|
|
unsupported), so the driver must convert. Pinned by `Plc5_io_indices_are_octal` (`I:017/17` -> `I:15/15`).
|
|
The connectivity-probe address is normalised the same way — by both the driver's probe loop and the Test Connect probe, through one helper (`AbLegacyProbeAddress.Normalise`, #660); an unparseable Test Connect probe address fails cleanly ("not a valid AbLegacy address") instead of reaching the transport. Normalisation yields the canonical name; the libplctag `S:0` → `S2:0` translation (#659) happens downstream in the runtime, so the two compose.
|
|
- ⚠️ **Behaviour change / migration.** Before this change a PLC-5 `I:`/`O:` address was passed to libplctag as
|
|
written, i.e. read as **decimal**. An address copied from RSLogix in octal (e.g. `I:017`) therefore read the
|
|
wrong word, and an authored decimal such as `I:1/12` now means bit 10 (octal 12). A previously "working"
|
|
address containing the digits 8 or 9 (`I:018`, `O:1/9`) is no longer valid on PLC-5. Re-author any PLC-5
|
|
I/O tags in RSLogix octal. Such a tag is refused (see below), never silently misread.
|
|
- **A malformed / family-invalid address** (also: a function-file letter off MicroLogix) reads and writes
|
|
`BadNodeIdUnknown` for that tag only, creates no runtime, leaves driver health alone, and logs **one**
|
|
Warning per tag. The same check (`AbLegacyAddress.Validate`) runs in the AdminUI tag editor (family-independent:
|
|
rejects only what *no* family accepts) and the deploy gate (`EquipmentTagConfigInspector`, using the device's
|
|
`DeviceConfig.PlcFamily`, absent = SLC 500, unrecognised = unknown) — so `I:018` on a PLC-5 device warns at
|
|
deploy (an error under `TagConfigValidationMode.Error`), and the editor is never stricter than the gate.
|
|
- **MicroLogix function files (#245).** `RTC`, `HSC`, `DLS`, `MMI`, `PTO`, `PWM`, `STI`, `EII`, `IOS`, `BHI`
|
|
(single fixed instance — no file number — with a `.SUBELEMENT`; no bit index) parse on MicroLogix devices only.
|
|
`AbLegacyFunctionFile.SubElementType` carries the sub-element catalogue (flags → Boolean, words → Int32; unknown
|
|
members default to Int32). ⚠️ **Wire-unverified on both transports:** there is no capture and no simulator
|
|
coverage; the libplctag path passes the name through and the native transport has no file-type code for them, so
|
|
it refuses the tag as `Unsupported` (`BadNotSupported` to the client; see Wire evidence).
|
|
- **Indirect addressing (#247 parse, #646 runtime).** `N7:[N7:0]` (word from another address) and `N[N7:0]:5` (file
|
|
from another address) parse, one level deep (the inner address must be direct, and carries the family's octal rule).
|
|
At runtime the driver reads the inner address first (same runtime factory, so libplctag and Native alike), requires
|
|
an integer in range (word/element `0..255`, or `0..999` on PLC-5/LogixPccc; file `0..999`), composes the concrete address and runs the normal
|
|
read/write path against it. **The index is re-read on every read/write - never cached** - and the previous
|
|
resolution's handle is released when the index moves. A bad index (negative, too large, float/string source) is
|
|
`BadOutOfRange` for that tag only (no health impact); an inner-read failure propagates its own status (a wire
|
|
failure also degrades health like any read). **Writes** resolve then write and are **not atomic**: the index can
|
|
change between the two PLC accesses. Bit RMW writes (`B3:[N7:0]/4`) resolve first, then RMW the concrete word.
|
|
The Native transport never sees the bracket form (it receives the composed name), so N-file indirect stays inside
|
|
the wire-proven envelope; other file types are as (un)verified as their direct forms. Live-gated on `ab_server`
|
|
by `AbLegacyIndirectAddressingTests` (`N7:0=5`, `N7:5=42`, `N7:[N7:0]` reads 42).
|
|
|
|
## 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`.
|
|
- **`Transport`** — absent (default ⇒ `Libplctag`), `Native` (opt-in), or `Libplctag`; see [Transport](#transport-transport-option).
|
|
|
|
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).
|
|
|
|
## Transport (`transport` option)
|
|
|
|
Same switch as AB CIP: `transport` = absent \| `Native` \| `Libplctag`, a string (case-insensitive on read,
|
|
written as `"Libplctag"` / `"Native"`; "use the default" is encoded by omitting the key). `Native` is a managed
|
|
PCCC-over-EtherNet/IP client (`Driver.Ab.Transport`).
|
|
|
|
**Default resolution** (one place, `AbLegacyTransportResolver`, used by the driver, the Test Connect probe and
|
|
the CLI): an absent key resolves to **`Libplctag` for every PLC family** — unchanged from before the native
|
|
transport existed. **`Native` is opt-in** (final review I3; since 2026-10-09 it is proven on one real MicroLogix 1400 across
|
|
S/N/F/B/T/C/R/ST/O/I reads and N/F/B-bit writes, but not on SLC, PLC-5 or `logixpccc` — see
|
|
[Wire evidence](#wire-evidence-native-pccc)). An explicit value always wins:
|
|
explicit `Native` with a PLC-5 / `LogixPccc` device is allowed (an operator choice) and logs one Warning per
|
|
driver instance naming the limitation. (AbCip's node-level `Drivers:AbCip:DefaultTransport` has no AbLegacy
|
|
counterpart — the AbLegacy default is libplctag and is not node-configurable.)
|
|
|
|
A change takes effect when the driver instance is constructed: on a deploy that changes `DriverConfig`
|
|
(which recreates it) **and on every process start**, including the restart that installs this build (see the
|
|
release note). The
|
|
AdminUI form's **Transport** dropdown offers "Default (libplctag)" (omits the key on save), "Native (managed,
|
|
opt-in)" and "libplctag". The driver logs `transport=<effective> (default|configured)` on every init, e.g.
|
|
`transport=Libplctag (default)`. CLI: `--transport default|native|libplctag` (default `default` = libplctag for
|
|
every `--plc-type`). See
|
|
[AbCip.md §Transport](AbCip.md#transport-transport-option) for the shared behaviour (one pooled connection
|
|
per device, Forward Open throttle, routing).
|
|
|
|
Native routing: SLC500/MicroLogix/PLC-5 are direct; `logixpccc` uses UnconnectedSend and its SLC framing is
|
|
**unverified**.
|
|
|
|
### Release note — native PCCC transport, opt-in (final review I3)
|
|
|
|
- **The default is unchanged: libplctag.** A driver config with no `transport` key runs `Libplctag`, for
|
|
every PLC family, exactly as before this release. The upgrade restart moves nothing. (An interim build on
|
|
this branch defaulted SLC 500 / MicroLogix to Native; that was reverted before merge.)
|
|
- **`"transport": "Native"` opts a driver into the managed PCCC-over-EtherNet/IP client** (`Driver.Ab.Transport`).
|
|
It takes effect when the driver is constructed — a deploy that changes `DriverConfig`, or a process start —
|
|
not on `ReinitializeAsync`. The AdminUI **Transport** dropdown authors it; remove the key (or pick
|
|
"Default (libplctag)") to go back.
|
|
- **Known limits of Native** (why it is opt-in):
|
|
- ⚠️ **Real-hardware evidence is one MicroLogix 1400** (Z-33323, 2026-10-09) — see
|
|
[Wire evidence](#wire-evidence-native-pccc): S (incl. `S:0`), N, F, B, T, C, R, ST, O, I reads, T/C/R
|
|
sub-elements and status bits, and N/F/B-bit writes all correct. **L and A files, function files, SLC 5/05,
|
|
PLC-5 and `logixpccc` are unverified.** Opt in per driver after checking it against the real controller.
|
|
- ⚠️ **Status-bit writes are refused** — `T4:0.DN`, `T4:0.TT`, `T4:0.EN`, `C5:0.CU`, `C5:0.DN`, `R6:0.EN`
|
|
and the rest return **`BadNotSupported`** whatever the tag's authored data type (the refusal keys on the
|
|
address); nothing is written to the PLC; health is unchanged. The driver answers the refusal **before it
|
|
creates the tag's runtime**, so a first-touch write sends nothing at all — not even the init read, which
|
|
against a controller without that file used to surface as `BadCommunicationError` + Degraded health (#593
|
|
follow-up). PLC-5 writes are refused the same way. libplctag performs a masked bit write here; a native
|
|
read-modify-write of the control word would race the PLC updating TT/DN. Reads of status bits are unaffected.
|
|
- ⚠️ **On libplctag (the default) a status-bit write is NOT refused** — it goes to the controller as a write to the
|
|
timer/counter element (libplctag's masked bit-tag write; not exercised on a wire — `ab_server` has no T/C/R
|
|
files). This covers the status bits libplctag parses itself (`T` `EN/TT/DN`, `C` `CU/CD/DN/OV/UN`); the ones it
|
|
has no mnemonic for — every `R` status bit and `C…UA` — **are** refused on libplctag too (`BadNotSupported`, #659). Nothing in authoring prevents it: the address is free text,
|
|
any `dataType` may be chosen (a status bit is naturally authored `Bit`), and `writable` defaults to `true`.
|
|
Author status-bit tags with `"writable": false` unless writing them is genuinely intended.
|
|
- **PLC-5 native is N-file reads only**; every other PLC-5 operation is refused with `BadNotSupported`.
|
|
`LogixPccc` framing is unverified. Explicit Native on either logs a Warning at init.
|
|
- ST (string) byte order is verified on a real MicroLogix 1400 (`ST14:10` = `"Z-33323"`).
|
|
- Unlike AbCip, the native AbLegacy runtime has **no element-type check**, so a mis-authored same-width
|
|
type reads as reinterpreted data (as on libplctag), not `BadTypeMismatch`.
|
|
- Releasing a native tag (evict, reinitialize, shutdown) never blocks on the connection close: the lease
|
|
is released in the background (`ConnectionLease.ReleaseInBackground`), the same shape as native AbCip
|
|
(#579). A re-create cannot overlap the close, because the pool holds the successor connection behind it.
|
|
|
|
### Wire evidence (native PCCC)
|
|
|
|
**Real controller (2026-10-09, user-directed):** MicroLogix 1400 **Z-33323** (`ab://10.201.44.231/`, empty path),
|
|
chosen because it was idle (`N10:150/0` cycle-in-progress and `B3:0/4` false, pump input `I:0/0` off, `O:0` = 0
|
|
before and after). Read pass, each address on libplctag then native (raw output in
|
|
[the gate-results doc](../plans/2026-10-07-native-ab-transport-gate-results.md)):
|
|
|
|
| Address(es) | libplctag | Native |
|
|
|---|---|---|
|
|
| `S:0` (default probe), `S:57` (= 1400), `S:4` | Good, equal (4, 1400, clock) since #659; **BadCommunicationError** before | Good (4, 1400, clock) |
|
|
| `N9:0`, `N9:4`, `N7:255` | Good | Good, equal |
|
|
| `F8:226` (2.1), `F8:214` (200.0) | Good | Good, equal |
|
|
| `B3:24`, `B3:28` (0x0905), bits `/0`, `/8`, `/9`, `B3:132/14` | Good | Good, equal |
|
|
| `T4:15/93/14/11.PRE`, `.ACC`, `.DN`; `T40:3/12.PRE` | Good | Good, equal |
|
|
| `C5:0.PRE/.ACC`, `C5:1.PRE`, `C5:8.DN` | Good | Good, equal |
|
|
| `R6:0.LEN`, `.POS` | Good | Good, equal |
|
|
| `R6:0.DN` (and `.EN`) | Good, equal since #659; **BadCommunicationError** before | Good |
|
|
| `ST14:10` (`"Z-33323"`), `ST14:0` | Good | Good, equal — **ST byte order correct** |
|
|
| `O:0`, `O:0/3`, `I:0`, `I:0/0`, `I:0/1`, `I:0/6`, `I:0/15` | Good, equal since #659; **BadCommunicationError** before | Good |
|
|
|
|
Native writes on the same idle press, each restored to 0 and verified: `N7:23` 0→4242→0 (neighbours `N7:22/24`
|
|
unchanged), `F8:190` 0→42.5→0, `B3:200` bit RMW `/5` set → 32, `/3` set → 40, `/5` clear → 8, `/3` clear → 0
|
|
(neighbours `B3:199/201` unchanged). Addresses vetted against the sibling Z-33756 program export (no ladder,
|
|
symbol, HMI, panel or migration reference) and Z-33323's own symbol table, and read 0 beforehand.
|
|
**libplctag `S`/`O`/`I` and control status bits — fixed (#659).** Before the fix libplctag (the default) returned
|
|
`BadCommunicationError` for `S:`, `O:`, `I:` and `R6:0.DN`/`.EN` on this controller, so the default probe `S:0`
|
|
failed on real hardware. Root cause, from libplctag 2.6.0's `src/protocols/ab/pccc.c` (the core bundled by the
|
|
`libplctag` 1.5.2 package): `parse_pccc_file_num` requires a digit after the file letter (and `S` is only the
|
|
status file when a digit follows it), and its sub-element table has only `con`/`len`/`pos` for R files — a bare
|
|
`R6:0/13` is refused too, because a `/bit` is only allowed on a 2-byte element. The libplctag adapter
|
|
(`LibplctagLegacyName`) now translates the canonical address into the form that parser accepts — `S:n` → `S2:n`,
|
|
`O:n` → `O0:n`, `I:n` → `I1:n` (the same default file numbers the native encoder uses, on every family) and a status
|
|
bit libplctag lacks (`R` `EN/EU/DN/EM/ER/UL/IN/FD`, `C` `UA`) → `R6:0.CON/<bit>` (control word + bit, bit numbers
|
|
from the native table). The authored grammar is unchanged; `S2:0`/`O0:0`/`I1:0` stay rejected by the parser (one
|
|
spelling per address). Re-run on the same idle press 2026-10-09: every address above reads **identical** on both
|
|
transports (`S:4` is a free-running clock and differs read to read on each). **The translation is read-side only — it does not widen the write surface:** before it, libplctag could not create a
|
|
tag for `S:`/`O:`/`I:` or for an `R`/`C…UA` status bit, so no write to one ever reached the PLC. Now that the
|
|
translated names parse, a write would drive a physical output, a status-file word or (masked) a control word. So on
|
|
libplctag **every write whose address the translation changes** — `S:`, `O:`, `I:` (word and bit writes; a bit write
|
|
such as `O:0/3` is refused ahead of the parent-word read-modify-write) and the rewritten status bits (`R`
|
|
`EN/EU/DN/EM/ER/UL/IN/FD`, `C` `UA`) — is refused **`BadNotSupported` before a runtime is created**: nothing read or
|
|
sent, health unchanged (`LibplctagLegacyTagRuntime.WriteRefusal`, asked through `IAbLegacyTagFactory.WriteRefusal`, the
|
|
same plumbing as the native refusal). Those writes never worked on libplctag, so nothing is lost; the native
|
|
transport (opt-in) does write `S`/`O`/`I`. T/C status bits libplctag parses itself keep their pre-existing write behaviour (note above).
|
|
Not tested on hardware: L and A files (no press has them), function files, PLC-5, SLC, `logixpccc`.
|
|
|
|
Simulator evidence:
|
|
|
|
| What | Evidence |
|
|
|---|---|
|
|
| N-file read (`N7:0`) | libplctag captures `slc500-read-n7`, `micrologix-read-n7`, `plc5-read-n7` (codec byte parity) + native `AbLegacyReadSmokeTests` read on all three profiles |
|
|
| N-file write (`N7:5`) | capture `slc500-write-n7` + native `AbLegacyReadSmokeTests` write/read round trip (SLC 500) |
|
|
| F-file read (`F8`) | one capture, `slc500-read-f8` (codec parity only; no native F write on a wire) |
|
|
| S (incl. `S:0`), B, T, C, R, ST, O, I | **real MicroLogix 1400 (above)**; L and A: none |
|
|
| MicroLogix function files (RTC/HSC/...), PLC-5 octal I/O | **none** — parsed only; native refuses function files as Unsupported |
|
|
| Bit-within-word write (`B3:0/5`) | `ab_server` SLC500 + MicroLogix, both transports (`AbLegacyBitWriteTests`): the driver RMWs the parent word, only the addressed bit changes (the parent-word read-modify-write is not atomic against ladder writes to the same word). PLC-5 native refuses it (`BadNotSupported`, #571) |
|
|
| Indirect addressing (`N7:[N7:0]`) | `ab_server` N-file, both transports (#646) - resolved by the driver, the transport sees the composed address |
|
|
| T/C/R sub-elements (`.PRE`/`.ACC`/`.LEN`/`.POS`, status bits) | **real MicroLogix 1400 (above)** |
|
|
|
|
### Native known gaps
|
|
|
|
- ⚠️ **One real controller.** A MicroLogix 1400 (Z-33323) passed reads across S/N/F/B/T/C/R/ST/O/I and N/F/B-bit
|
|
writes; SLC 5/05, PLC-5, `logixpccc`, L/A files and function files are unverified. See
|
|
[Wire evidence](#wire-evidence-native-pccc).
|
|
- **PLC-5 native is N-file reads only.** Every other file type and all PLC-5 writes are refused before the
|
|
wire and reach the client as `BadNotSupported` (handle kept, health unchanged; #571 closed). Native is opt-in
|
|
for every family; an explicit Native on PLC-5 / `logixpccc` logs a Warning at init.
|
|
- Status-bit sub-element writes (e.g. `T4:0.DN`, `C5:0.DN`) are Unsupported natively (they would clobber the
|
|
control word); nothing goes on the wire — the driver refuses before creating the tag's runtime, so not even an
|
|
init read is sent (`NativeAbLegacyTagRuntime.WriteRefusal` is the single predicate, asked through
|
|
`IAbLegacyTagFactory.WriteRefusal`). Pinned offline by `NativeAbLegacyRefusedWriteTests` and on `ab_server` by
|
|
`AbLegacyUnsupportedPathTests` (whose SLC500 fixture has no T/C/R files, which is what makes it discriminating). Sub-element arrays are Unsupported natively. L-file bit indexes
|
|
above 15 are allowed.
|
|
- Capture recipe for golden frames: `scripts/ab-capture/capture.sh` (see
|
|
[AbLegacy-Test-Fixture.md](AbLegacy-Test-Fixture.md)). Follow-up issues: #564 to #577.
|
|
|
|
## 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** (libplctag by default; the opt-in
|
|
native transport issues the same typed read, chunked under the PCCC payload limit).
|
|
The tag's file address (e.g. `N7:0`) is extended by `N` element count so the transport 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. The opt-in native transport also writes multi-element arrays (chunked); on libplctag
|
|
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* on libplctag (the opt-in native transport writes them), 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, libplctag's native heap does not show up in CLR allocation figures; watch whole-process RSS and use `ReinitializeAsync` to recycle libplctag handles. (`IDriver.GetMemoryFootprint()` was deleted as unconsumed in Gitea #525.)
|
|
- **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.
|