Review round 1 on the golden frames:
- Re-captured logix-read-dint-array16 (element i = ((i+1)<<24)|(i+1)) and
logix-read-bool (= 1) so neither decode oracle is all zeros. ab_server rejects
libplctag's own BOOL write with CIP 0x03 (two data bytes for a one-byte BOOL),
so the BOOL is seeded by a raw UCMM Write Tag (client seed-bool).
- Committed the capture client as scripts/ab-capture/client (outside the slnx,
CPM off, libplctag 1.5.2 pinned); capture.sh gains a `client` subcommand.
- README documents simulator-only behaviour: zeroed reply sender context, STRING
typed D0 00 instead of A0 02 + handle, and the BOOL write refusal.
- capture.sh no longer claims UnregisterSession reaches the wire (session ends in RST).
- CaptureFileTests also check the CPF walk ends at the frame end and that every
reply service is its request service | 0x80.
17 .hex fixtures captured from libplctag 1.5.2 (native 2.6.0) against
otopcua-ab-server:libplctag-release, plus the CaptureFile loader, its
length/alternation tests, and the re-runnable scripts/ab-capture/capture.sh.
Observed and recorded in Captures/README.md "Routing observed":
- Micro800 is NOT UCMM under libplctag 2.6.0: it opens a Large Forward Open
(path 20 02 24 01) and reads connected; use_connected_msg=0 is ignored.
- SLC500/MicroLogix/PLC-5 send Execute PCCC 0x4B unconnected and unwrapped;
a configured path=1,0 never reaches the wire.
- Only Large FO 0x5B was observed; no 0x54 fallback occurred.
- LogixPccc is unreachable against ab_server (CIP 0x08); no fixture committed.
Removes the 'use UseNativeNotifications=false for arrays' workaround and
documents the raw-bytes array model shared by poll and notification: element
checks, STRING arrays, multi-dim flattening, 0-based publication, leading
sub-range for a shorter arrayLength, refusals (struct/UDT arrays, too long,
scalar over array), the 128 KiB native cap (BadOutOfRange), the bit-path fix,
and the 2026-10-07 read-only live gate on Z33856 / Z33733.
Live on Z33856 a UInt tag over .ARRFAULTCODE (ARRAY [0..12] OF UINT) made the
Beckhoff read throw - BadCommunicationError and a Degraded driver - while the
native path reported BadTypeMismatch. CheckRead now refuses a scalar declared
over an array (by category, so an alias of an array type is caught), pointing
at the array-tag / element-path alternatives.
A native notification for MAIN.Flags.3 / .ADSSTATE_RUN.0 was registered on the
literal WORD.N path, which ADS has no symbol for: live on the TC 2.11 Elma
cleaner Z33856 it failed with DeviceSymbolNotFound, and that registration
error tore down the whole subscribe call. The read path already reads the
parent word; the notification now does the same - it registers the parent as
raw bytes at its resolved width and reads the bit (TwinCATBitAccess), refusing
a bit outside the parent or a non-integer parent with BadTypeMismatch.
An array tag subscribed natively used to register only its declared scalar
(publishing element 0 at Good) and was then refused wholesale with
BadTypeMismatch. The driver now hands the authored ArrayLength to the client,
which registers the whole array as raw bytes and decodes every notification.
- TwinCATTypeGuard.CheckArray plans the layout from the RESOLVED array type:
element checked like a scalar notification (width / float kind / string
kind); STRING(n)/WSTRING(n) elements stride n+1 / 2(n+1); multi-dim arrays
flattened in PLC memory order; an authored ArrayLength shorter than the PLC
array reads the leading sub-range, a longer one is refused; struct / pointer
/ nested-array elements, non-array symbols and VAR_IN_OUT refs are refused.
- CheckNotificationSize caps a native array at 128 KiB (largest registration
proven on a live TC 2.11 target); above it the tag is refused BadOutOfRange.
- TwinCATArrayCodec decodes the bytes into the element-typed CLR array.
- The poll read uses the same plan + codec instead of the Beckhoff typed array
read, which failed live for most real arrays (exact CLR element type, rank-N
for multi-dim, member arrays sliced by the under-reported TC2 symbol size).
- TwinCATNotificationRefusedException carries the refusal's status; the type
mismatch derives from it and the driver isolates either per tag.
Found by the read-only live gate against the TwinCAT 2.11 Elma cleaners
Z33856 / Z33733:
- The Beckhoff library reports a TC2 dotted MEMBER symbol's ByteSize as if the
PLC's byte count were a bit count (STRING(80) -> 11, INT -> 1, UDINT -> 1)
while DataType.ByteSize is right (81/2/4, raw SYM_INFOBYNAMEEX says 81). The
previous commit's typeof(byte[]) read sliced by the symbol size, so
.OPC_MACHINEDATA.PROCESSSTATUS read "ready for o" at Good; the notification
width check would also have refused every numeric member. The shape now takes
the resolved type's size, and string reads go by index group/offset (or a
fresh handle for 0xF005) at that width.
- P_INITINPUT_ACTION.SLOTID is "STRING(20) (VAR_IN_OUT)" - a 4-byte reference -
but resolves to a plain STRING(20); a String tag read 21 bytes at the pointer
as text at Good. The symbol's own VAR_IN_OUT marker now classifies it
NonScalar -> BadTypeMismatch.