# Golden frame captures — libplctag ⇄ ab_server These `.hex` files are the exact EtherNet/IP frames libplctag exchanged with the `ab_server` simulator. The native AB transport's codecs (plan Tasks 7 and 8) assert **byte parity** against them. > ⚠️ They prove parity with **libplctag**, not correctness against a controller. The independent > reference is the real-press captures in > `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/CipSymbolObjectRealControllerCaptureTests.cs` > and `CipTemplateObjectWireCaptureTests.cs`. ## Format One frame per line: `> ` = client→server, `< ` = server→client, then the full encapsulation frame (24-byte header + data) as uppercase hex with no spaces. `#` lines are comments; each file opens with a comment block naming its client, server, profile, tag string and what the frames are. `CaptureFile.Load` reads them; `CaptureFileTests` checks every frame's header length field against its data length and that requests and replies alternate. Values are **little-endian on the wire** and carry live per-session values — session handle, connection IDs, connection serial, PCCC TNS. A parity test must compare structure and the fields it builds, not those session-assigned values. ## How they were taken | | | |---|---| | Date | 2026-10-07 | | Repo commit | `a0897fda` (fixture sources identical to the files on the docker host, checked by md5) | | Client | **libplctag 1.5.2** (libplctag.NET NuGet, `Directory.Packages.props`) → libplctag.NativeImport 1.0.41 → native core **2.6.0** (read via `plc_tag_get_int_attribute(0,"version_*")`) | | Server image | **`otopcua-ab-server:libplctag-release`** (ab_server from libplctag `v2.7.0` + vendored patches 0001 `@tags` listing and 0002 tag-not-found → CIP 0x04; both confirmed live — see `logix-tags-listing.hex` / `logix-read-unknown-tag.hex`) | | AbCip compose | `/opt/otopcua-abcip` on `10.100.0.35` (= `tests/.../AbCip.IntegrationTests/Docker/`), host `:44818` | | AbLegacy compose | `~/otopcua-ablegacy` on `10.100.0.35` (= `tests/.../AbLegacy.IntegrationTests/Docker/`), host `:44819` slc500 / `:44820` micrologix / `:44821` plc5 | | Capture point | `tcpdump -i eth0 tcp port 44818` in a `nicolaka/netshoot:v0.13` sidecar sharing the simulator container's network namespace; `tshark` 4.2.4 in the same image; segments re-joined into frames by the 24-byte header length field | | Recipe | `scripts/ab-capture/capture.sh run …` then `capture.sh tohex …` | The client was a ~60-line console program over libplctag 1.5.2 that mirrors how `LibplctagTagRuntime` / `LibplctagLegacyTagRuntime` / `LibplctagTagEnumerator` build a tag (`Gateway`, `Path`, `PlcType`, `Protocol=ab_eip`, `Name`, `Timeout=5s`, `ElementCount` when > 1; no other attributes), then runs `InitializeAsync` + one `ReadAsync` or `WriteAsync`, disposes the tag and calls `plc_tag_shutdown` so the Forward Close reaches the wire. **One fresh session per capture**, one profile up at a time. Note that `InitializeAsync` itself performs a read, so a "read" capture holds two read exchanges and a "write" capture holds a read then the write. ### Per-file record All files: libplctag 1.5.2 (native 2.6.0), image `otopcua-ab-server:libplctag-release`. The tag string is the libplctag attribute set the wrapper was given. | File | Profile | Tag string | Frames | |---|---|---|---| | `logix-register-session.hex` | controllogix | `protocol=ab_eip&gateway=10.100.0.35:44818&path=1,0&plc=controllogix&name=TestDINT` | RegisterSession (frames 1–2 of the `logix-read-dint` session) | | `logix-forward-open.hex` | controllogix | same as above | Large Forward Open **0x5B** (frames 3–4) | | `logix-read-dint.hex` | controllogix | `…&plc=controllogix&name=TestDINT` | 2× connected Read Tag Fragmented 0x52 (value 123456) | | `logix-read-dint-array16.hex` | controllogix | `…&name=TestArray&elem_count=16` | 2× 0x52, 16 elements | | `logix-read-string.hex` | controllogix | `…&name=TestString` | 2× 0x52, STRING `"HelloAB"` | | `logix-read-bool.hex` | controllogix | `…&name=TestBOOL` | 2× 0x52, BOOL 0 | | `logix-write-dint.hex` | controllogix | `…&name=TestDINT` | 0x52 read, then Write Tag 0x4D of 123456 | | `logix-write-string.hex` | controllogix | `…&name=TestString` | 0x52 read, then 0x4D of `"HelloAB"` | | `logix-read-unknown-tag.hex` | controllogix | `…&name=NoSuchTag` | 0x52 → general status **0x04** (patch 0002); libplctag raised `ErrorNotFound`, no retry | | `logix-tags-listing.hex` | controllogix | `…&name=@tags` | Get Instance Attribute List **0x55** on Symbol class 0x6B (patch 0001); all 9 tags in one reply, status 0x00 | | `logix-forward-close.hex` | controllogix | `…&name=TestDINT` | Forward Close 0x4E (last 2 frames of the `logix-read-dint` session) | | `micro800-read-dint.hex` | micro800 | `protocol=ab_eip&gateway=10.100.0.35:44818&plc=micro800&name=TestDINT` | **Full session**: RegisterSession, Large FO, 2× 0x52 (value 987654), Forward Close | | `slc500-read-n7.hex` | slc500 | `protocol=ab_eip&gateway=10.100.0.35:44819&path=1,0&plc=slc500&name=N7:0` | Full session: RegisterSession, 2× Execute PCCC 0x4B / PCCC 0x0F-0xA2 (value 4321) | | `slc500-write-n7.hex` | slc500 | same | Full session: 0x0F-0xA2 read, then 0x0F-0xAA write of 4321 | | `slc500-read-f8.hex` | slc500 | `…:44819&path=1,0&plc=slc500&name=F8:0` | Full session: 2× 0x0F-0xA2 (value 3.5) | | `micrologix-read-n7.hex` | micrologix | `protocol=ab_eip&gateway=10.100.0.35:44820&plc=micrologix&name=N7:0` | Full session: 2× 0x0F-0xA2 (value 2468) | | `plc5-read-n7.hex` | plc5 | `protocol=ab_eip&gateway=10.100.0.35:44821&path=1,0&plc=plc5&name=N7:0` | Full session: 2× PCCC 0x0F-**0x01** PLC-5 typed read (value 1357) | Logix files other than `register-session`/`forward-open`/`forward-close` hold **only the operation's exchanges**; the handshake frames of their sessions are byte-for-byte the same shape as those three files. The Micro800 and PCCC files hold the **whole session** so their routing is visible in one place. No session in any family sent UnregisterSession (0x66). ## Routing observed What libplctag 2.6.0 actually put on the wire. Tasks 9, 12 and 14 take their `Routing` choice from this table. | Family | ForwardOpen | CIP request wrapped in Unconnected Send (0x52) | Route bytes | Requestor-id bytes (PCCC) | |---|---|---|---|---| | controllogix (`path=1,0`) | **yes** — Large FO 0x5B only (0x54 never sent; the 0x5B succeeded). FO itself is sent direct to the Connection Manager `20 06 24 01`, not wrapped. Connection path `01 00 20 02 24 01` (3 words), net params `0x42000FA2`, RPI 1 000 000 µs, transport `0xA3` | **no** — requests ride the connection (SendUnitData, item 0xB1) | `01 00` (port 1, slot 0), carried only inside the FO connection path | n/a | | micro800 (no path) | **yes** — Large FO 0x5B, connection path `20 02 24 01` (2 words, no port segment). ⚠️ Not UCMM; see concerns | **no** — connected | none | n/a | | slc500 (`path=1,0`) | **no** | **no** — Execute PCCC 0x4B sent unconnected (SendRRData, item 0xB2) straight to `20 67 24 01` (PCCC object) | **none — the `1,0` path was given but never appeared on the wire** | `07 3D F3 45 43 50 21` (length 7, vendor 0xF33D, serial 0x21504345) | | micrologix (no path) | **no** | **no** — as slc500 | none | `07 3D F3 45 43 50 21` | | plc5 (`path=1,0`) | **no** | **no** — as slc500 | **none — `1,0` not emitted** | `07 3D F3 45 43 50 21` | | logixpccc (`path=1,0`, against the controllogix profile) | **no** | **yes** — Unconnected Send 0x52 to `20 06 24 01`, priority/tick `0A 05`, embedded 0x4B (len 0x1C) | `01 00` (path size 1 word, reserved, then `01 00`) | `07 3D F3 45 43 50 21` | **logixpccc is not reachable**: ab_server's ControlLogix mode has no PCCC object and answers `CB 00 08 00` (service not supported). Its request shape above is read from the client side of that rejected exchange; **no `.hex` fixture was committed for it**, since a parity fixture whose reply is an error would teach nothing about the success path.