1201d5fcfb
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.
93 lines
7.9 KiB
Markdown
93 lines
7.9 KiB
Markdown
# 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.
|