fix(abcip): unblock the controller-browse gate, and fix the decoder it exposed (#468)
The AbCip controller browse could never be verified end to end: libplctag's ab_server answers CIP service 0x55 (Get Instance Attribute List) on the Symbol Object class with "unsupported", so the `@tags` walk had no backend. #468's own definition of done offered a sim that serves `@tags` as an alternative to real hardware; this is that. The fixture now applies a vendored patch teaching ab_server to serve the Symbol Object listing, with partial-transfer paging. Correctness is checked against libplctag's own `list_tags_logix` rather than against our decoder — our decoder is the thing under test, so its agreement would prove nothing. The clone is also pinned to a concrete tag: the Dockerfile called `--branch release` "pinned" while tracking a branch that moves. That immediately exposed a real defect. CipSymbolObjectDecoder advanced to an even byte boundary after each symbol name, but entries are packed with no padding — libplctag walks them by sizeof(entry) + string_len, and a captured reply confirms it. Any odd-length name therefore desynchronised every entry behind it, and odd-length names are ordinary. In the live browse below, `Motor_Run` is 9 bytes and first on the wire, so the old decoder would have shown that one tag and silently dropped the other eight. Every existing multi-entry test passed throughout, because the test builder padded exactly as the decoder did: the two agreed with each other and neither agreed with a controller. The builder no longer pads, and the layout is pinned by a captured reply (CipSymbolObjectWireCaptureTests) instead of by a second statement of the assumption. Restoring the alignment step reddens exactly the three new tests and none of the 29 others. Live gate on docker-dev, all three of #468's criteria: - populated tree — all nine seeded tags render as leaves, in wire order - refresh re-captures - leaf commit — Pump_1_Speed + TestDINT saved with their CIP tag paths The gate also surfaced two things it was not asked about: browse-commit drops the discovered data type, so a browsed REAL commits as a DINT and the driver decodes it as one (filed as #536), and Test Connect fails against this fixture regardless of the device because it probes `@raw_cpu_type` (documented). TwinCAT and FOCAS still have no browsable backend; #468's AbCip half is closed, theirs is not. Claude-Session: https://claude.ai/code/session_015p7wGqy3YpZNCpDzTpGMKo
This commit is contained in:
@@ -12,6 +12,47 @@ clone needs Docker Desktop and nothing else.
|
||||
|---|---|
|
||||
| [`Dockerfile`](Dockerfile) | Multi-stage: build from libplctag at pinned tag → copy binary into `debian:bookworm-slim` runtime image |
|
||||
| [`docker-compose.yml`](docker-compose.yml) | One service per family (`controllogix` / `compactlogix` / `micro800` / `guardlogix`); all bind `:44818` |
|
||||
| [`patches/`](patches/) | Applied to the libplctag checkout before the build — see [Local patches](#local-patches) |
|
||||
|
||||
## Local patches
|
||||
|
||||
`patches/0001-ab_server-serve-cip-symbol-object-listing.patch` teaches `ab_server`
|
||||
to answer **CIP service 0x55 (Get Instance Attribute List) on class 0x6B**, the
|
||||
Symbol Object listing a client reads through the `@tags` pseudo-tag to enumerate
|
||||
a controller's symbol table. Upstream answers `unsupported`, which is what left
|
||||
the AbCip controller browse unverifiable (Gitea #468) — the browse binding was
|
||||
proven alive, but no backend ever returned a tag.
|
||||
|
||||
Two things worth knowing before touching it:
|
||||
|
||||
- **The reply layout is fixed to the attribute set libplctag asks for** —
|
||||
`{0x02 symbol type, 0x07 element size, 0x08 array dimensions, 0x01 symbol
|
||||
name}`. A request for a different set is refused rather than answered with a
|
||||
differently-shaped reply the client would misparse.
|
||||
- **Entries are packed with no padding.** The next entry begins on the byte
|
||||
after the last name byte, even on an odd offset. This is not a detail of the
|
||||
fixture: it is what a controller does, and assuming otherwise is exactly the
|
||||
decoder defect the gate found.
|
||||
|
||||
Correctness is checked against **libplctag's own `list_tags_logix`** client, not
|
||||
against our decoder — our decoder is the thing under test, so agreeing with it
|
||||
would prove nothing. `list_tags_logix` is the reference implementation exercised
|
||||
against real Logix hardware:
|
||||
|
||||
```bash
|
||||
git clone --depth 1 --branch v2.7.0 https://github.com/libplctag/libplctag.git
|
||||
cd libplctag && cmake -S . -B build -DCMAKE_BUILD_TYPE=Release && cmake --build build --parallel
|
||||
./build/bin_dist/list_tags_logix 10.100.0.35 1,0 # expect all nine tags, "SUCCESS!"
|
||||
```
|
||||
|
||||
Partial-transfer paging is exercised the same way by starting `ab_server` with a
|
||||
few hundred tags: all of them should come back, with no duplicates and no gaps
|
||||
at the page boundaries.
|
||||
|
||||
**Bumping `LIBPLCTAG_TAG` means re-checking the patch against the new tag.** It
|
||||
is a concrete tag rather than the `release` branch precisely so that this stays
|
||||
a deliberate act — the branch moves, and the Dockerfile previously called it
|
||||
"pinned" while tracking it.
|
||||
|
||||
## Run
|
||||
|
||||
@@ -64,7 +105,7 @@ place means updating both.
|
||||
|
||||
| Family | Seeded tags | Notes |
|
||||
|---|---|---|
|
||||
| ControlLogix | `TestDINT` `TestREAL` `TestBOOL` `TestSINT` `TestString` `TestArray` | Widest-coverage; PR 9 baseline. UDT emulation missing from ab_server |
|
||||
| ControlLogix | `TestDINT` `TestREAL` `TestBOOL` `TestSINT` `TestString` `TestArray` `Pump_1_Speed` `Line1_Count` `Motor_Run` | Widest-coverage; PR 9 baseline. UDT emulation missing from ab_server. The last three serve the controller-browse gate and are additive — the read/write suites key off the `Test*` tags. `Motor_Run` (9), `Line1_Count` (11) and `TestArray` (9) have **odd-length names on purpose**: entries are unpadded, so an odd name puts every following entry on an odd offset, and a decoder that assumes even alignment loses everything behind it |
|
||||
| CompactLogix | `TestDINT` `TestREAL` `TestBOOL` | Narrow ConnectionSize cap enforced driver-side; ab_server accepts any size |
|
||||
| Micro800 | `TestDINT` `TestREAL` | ab_server has no `micro800` mode; falls back to `controllogix` emulation |
|
||||
| GuardLogix | `TestDINT` `SafetyDINT_S` | ab_server has no safety subsystem; `_S` suffix triggers driver-side classification only |
|
||||
@@ -73,7 +114,17 @@ place means updating both.
|
||||
|
||||
- **No UDT / CIP Template Object emulation** — `ab_server` covers atomic
|
||||
types only. UDT reads + task #194 whole-UDT optimization verify via
|
||||
unit tests with golden byte buffers.
|
||||
unit tests with golden byte buffers. The Symbol Object patch above lists
|
||||
symbols, including their types; it does not add Template Object support, so a
|
||||
browsed UDT still cannot be expanded into members here.
|
||||
- **No `@raw_cpu_type`** — the driver's Test Connect probe falls back to that
|
||||
pseudo-tag when a device has no authored tags, and `ab_server` answers
|
||||
`ErrorBadParam`. So **Test Connect reports failure against this fixture even
|
||||
when the device is fine**, and browse works regardless. Author one tag and the
|
||||
probe uses it instead.
|
||||
- **Controller-scope listing only** — a program-scope listing arrives with a
|
||||
leading symbolic segment naming the program, and `ab_server` has no notion of
|
||||
programs. `Program:`-scoped tags cannot be browsed here.
|
||||
- **Family-specific quirks trust driver-side code** — ab_server emulates
|
||||
a generic Logix CPU; the ConnectionSize cap, empty-path unconnected
|
||||
mode, and safety-partition write rejection all need lab rigs for
|
||||
|
||||
Reference in New Issue
Block a user