fix(ablegacy): refuse native status-bit and PLC-5 writes before creating the runtime (#593)

A first-touch native write to a T/C/R status-bit sub-element (T4:0.DN) created
and initialised the tag's runtime before the runtime's write refusal could fire;
the init is one READ, so against a device lacking the file (the ab_server
fixture has no T/C/R) it surfaced as BadCommunicationError + Degraded health
instead of BadNotSupported. The bit-RMW path was not involved: AbLegacyAddress
rejects sub-element + /N, so a status bit's BitIndex is always null.

NativeAbLegacyTagRuntime.WriteRefusal is now the single predicate (status bit
by address whatever the authored type; any PLC-5 write), asked by the driver via
IAbLegacyTagFactory.WriteRefusal (default null, so libplctag is unchanged)
before EnsureTagRuntimeAsync and ahead of the bit-RMW branch.

Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
This commit is contained in:
Joseph Doherty
2026-10-08 07:28:52 -04:00
parent 5a5f9b22c9
commit e863e9f0e2
7 changed files with 281 additions and 21 deletions
+15 -4
View File
@@ -142,9 +142,17 @@ Native routing: SLC500/MicroLogix/PLC-5 are direct; `logixpccc` uses Unconnected
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`**; nothing is written to the PLC; the handle is kept and health is
unchanged. 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.
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.
@@ -175,7 +183,10 @@ Everything below is against the `ab_server` simulator; no real PCCC controller h
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. Sub-element arrays are Unsupported natively. L-file bit indexes
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