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.
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.
libplctag decodes whatever bytes the controller returns as the authored type and never
checks the CIP type code. Live read-only check on a production press: WW_ActualVacuum is a
REAL (L5X + the controller's symbol table agree) but the legacy Kepware export in the
ignitionoee repo types it Short; read as INT it returned Good -17346 while the real value
was 5.37. A scalar atomic read (not an array, not a bit-in-DINT) whose buffer size is not
the authored type's width now reads BadTypeMismatch with a warning naming both. Widths
observed live: SINT/BOOL 1, INT 2, DINT/REAL 4, LINT 8. IAbCipTagRuntime.GetSize() is a
default-interface member (-1 = unknown, never checked). Same-width mismatches (DINT vs
REAL) stay undetectable without the type code libplctag strips.
FOCAS.md: positions scale from each block's own base/exponent and
PositionDecimalPlaces is only the scale-less fallback; cnc_getfigure is gone;
Spindle Load/MotorSpeedPct are Float64 percentages (MaxRpm renamed); program
fields the CNC does not report read BadNoData; the #548 Makino 'AA' duplicates
were a 2-byte name decode (now AA6/AA7). New 'Wire decode corrections (2026-10)'
table lists each before/after with client-visible changes flagged, plus what
was deliberately not ported and why. Troubleshooting rows for BadNoData and
integer-macro BadTypeMismatch. Test-fixture doc follows the mock changes.