73fdb80157
Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
283 lines
22 KiB
Markdown
283 lines
22 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.
|
|
- ⚠️ **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: on a wire it is proven only for N-file reads and
|
|
writes and one F8 read — 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):
|
|
- ⚠️ **Wire evidence is narrow and simulator-only** — see [Wire evidence](#wire-evidence-native-pccc). Only
|
|
N-file reads and writes and one F8 read have touched a wire (`ab_server`). The **default probe address
|
|
`S:0`** and the **B, T, C, R, S, L, ST, A, O and I file types** — and the T/C/R sub-element offsets
|
|
(`.PRE`/`.ACC`/`.LEN`/`.POS`) — are encoded from the PCCC file-type table and are **unverified natively**.
|
|
No real SLC, MicroLogix or PLC-5 has been exercised. 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/control element (libplctag's bit-tag write; not exercised on a wire — `ab_server` has no T/C/R
|
|
files). 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 unverified** (no capture); string values may be byte-swapped.
|
|
- 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)
|
|
|
|
Everything below is against the `ab_server` simulator; no real PCCC controller has been exercised.
|
|
|
|
| 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. the default probe `S:0`), B, T, C, R, L, ST, A, O, I | **none** — encoded from the PCCC file-type table |
|
|
| MicroLogix function files (RTC/HSC/...), PLC-5 octal I/O | **none** — parsed only; native refuses function files as Unsupported |
|
|
| 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`), 6-byte T/C/R element layout | **none** |
|
|
|
|
### Native known gaps
|
|
|
|
- ⚠️ **Simulator-only, and narrow.** Only N-file reads/writes and one F8 read are on a wire (`ab_server`); see
|
|
[Wire evidence](#wire-evidence-native-pccc). No real SLC/MicroLogix/PLC-5 has been exercised; the real-press
|
|
gate (Task 21) was read-only and AbCip-only.
|
|
- **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.
|
|
- **ST (string) file layout is uncaptured**, so the string byte order is unverified natively.
|
|
- 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.
|