Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
31 KiB
Native AB transport gate results
ab_server suites (T17)
Every ab_server integration test now runs once per transport (TransportMatrix in each integration
project). Run 2026-10-07 against 10.100.0.35, one compose profile at a time
(AbCip /opt/otopcua-abcip, AB_SERVER_COMPOSE_PROFILE=<p>; AbLegacy ~/otopcua-ablegacy,
AB_LEGACY_COMPOSE_PROFILE=<p>); every stack was brought down afterwards.
- Image:
otopcua-ab-server:libplctag-release(sha256:19e814747d85…, built 2026-08-01; ab_server from libplctagv2.7.0+ patches 0001/0002). Host compose files md5-identical to the repo's. - Commit: fixes
82d011c7+2171fc8c, test matrix8c68694d.
Counts are pass / fail / skip for the transport-parameterised rows only. Each AbCip run also passed the
6 transport-independent AbServerProfileTests. The AbCip skips are the 3 Emulate-tier tests
(AB_SERVER_PROFILE=emulate only). The AbLegacy skips are Slc500_write_then_read…, which skips itself
on both transports when the slc500 port (:44819) is down.
| Profile | Libplctag | Native | Native ⊇ Libplctag |
|---|---|---|---|
| controllogix | 1 / 0 / 3 | 1 / 0 / 3 | yes |
| compactlogix | 1 / 0 / 3 | 1 / 0 / 3 | yes |
| micro800 | 0 / 1 / 3 | 1 / 0 / 3 | yes (libplctag fails, see below) |
| guardlogix | 1 / 0 / 3 | 1 / 0 / 3 | yes |
| slc500 | 2 / 0 / 0 | 2 / 0 / 0 | yes |
| micrologix | 1 / 0 / 1 | 1 / 0 / 1 | yes |
| plc5 | 1 / 0 / 1 | 1 / 0 / 1 | yes |
Native-only failures found and fixed:
82d011c7(controllogix): RegisterSession carried a non-zero sender context, and ab_server rejects that with encapsulation status 1. Every native session failed.2171fc8c(slc500): ab_server answers Execute PCCC with a fixed requestor id (07 3D F3 45 43 50 21), so the requestor-echo check rejected every native PCCC reply. Native now sends libplctag's identity.
Skips added: none. The PLC-5 integration test only reads N7, so the PLC-5 Unsupported paths (non-N reads, all writes) and status-bit sub-element writes are never exercised by the suite.
micro800: the smoke test used to send /1,0 for every family, and both transports failed against
--plc=Micro800. It now uses an empty path for Micro800. With the empty path, native passes. libplctag
still fails with BadCommunicationError, because the driver's libplctag runtime passes Path = "" and
libplctag answers that with ErrorBadData. A null path works (checked with the capture client). That
defect is pre-existing and libplctag-side; it is recorded, not fixed, here.
Differential vs ab_server (T18)
Run 2026-10-07 on branch feat/native-ab-transport, harness
tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests/Differential/, against ab_server
(controllogix profile only, /opt/otopcua-abcip on 10.100.0.35):
AB_DIFF_ENDPOINT=10.100.0.35:44818 AB_DIFF_COUNT_OPENS=1 \
dotnet test tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests \
--filter "FullyQualifiedName~Differential" --logger "console;verbosity=detailed"
Target as the harness resolved it: 10.100.0.35:44818, path 1,0, family CompactLogix
(the defaults; libplctag attribute compactlogix, connection size 504).
Result: 7/7 passed, 0 unexpected divergences, 0 expected divergences.
| Check | Result |
|---|---|
Browse parity (@tags + program listings) |
9 tags via libplctag, 9 via native, identical as multisets |
| Template parity | vacuous: ab_server serves no UDTs, so 0 template ids. Task 21's press is the first real exercise |
| Read parity | 9/9 tags equal on status, raw buffer and decoded value |
Native Forward Opens over the run (AB_DIFF_COUNT_OPENS=1) |
1 |
| TCP connections to the target before the native phase | 0 (baseline 0). libplctag's session closed within 1 ms of the last handle being disposed |
| Read-only guard (offline, runs without the endpoint) | pass |
| Comparison falsifiability (offline) | pass |
| Tag | Type | libplctag status | native status | Equal? |
|---|---|---|---|---|
Motor_Run |
Bool | Ok | Ok | yes |
Line1_Count |
DInt | Ok | Ok | yes |
Pump_1_Speed |
Real | Ok | Ok | yes |
TestString |
String | Ok | Ok | yes |
TestSINT |
SInt | Ok | Ok | yes |
TestBOOL |
Bool | Ok | Ok | yes |
TestREAL |
Real | Ok | Ok | yes |
TestDINT |
DInt | Ok | Ok | yes |
TestArray |
DInt[16] | Ok | Ok | yes |
The values were made non-zero first. An all-zero simulator would make raw and value equality vacuous. Before the run, the simulator was seeded through a throwaway libplctag scratch program outside the repo. The harness itself never writes. Seeded values:
TestDINT= −123456789TestREAL= 3.14159Pump_1_Speed= 1234.5Line1_Count= 987654TestArray[i]= (i−8)·100003+7, which includes negativesTestString="Hello T18"
ab_server refuses 1-byte writes (ErrorBadParam), so TestBOOL, TestSINT and Motor_Run stay 0.
Their parity is therefore weaker evidence.
Wire capture of the same run. tcpdump ran in the ab_server container's network namespace
(nicolaka/netshoot), and the capture was decoded with tshark:
| TCP stream | Session | RegisterSession | Forward Open | Forward Close | Requests | Window (s, relative) |
|---|---|---|---|---|---|---|
| 12 | libplctag | 1 | 1 (Large, 0x5B) | 1 | 1× 0x55 listing, 18× 0x52 Read Fragmented | 0.243 → 0.336 |
| 13 | native | 1 | 1 (0x54) | 1 | 1× 0x55 listing, 18× 0x52 Read Fragmented | 0.349 → 0.452 |
- Exactly two sessions, with no overlap: a 13 ms gap between them.
- Zero write services on the wire: no 0x4D, 0x53 or 0x10.
- The 12 earlier sub-10 ms TCP connections are the
AbServerFact/AbServerTheoryreachability probes. xUnit runs them during discovery for the other suites, even under the filter. They targetAB_SERVER_ENDPOINT(default 10.100.0.35), neverAB_DIFF_ENDPOINT.
Explicit-list path. AB_DIFF_TAGS="TestDINT,NoSuchTag,TestArray,TestString": 4/4 equal.
NoSuchTag returned NotFound under both transports, and there was still exactly one native Forward Open.
Transport fixes found by the differential: none. One harness defect was found and fixed before
this result. libplctag closes a session as soon as its last handle is destroyed. The first version of the
libplctag phase created, read and disposed one handle at a time, so it opened a fresh session (and a
fresh Forward Open) per tag: 9+ sessions, which the netstat sampling showed. The libplctag phase now holds an anchor handle
on @tags for its whole duration, which mirrors the native anchor lease. That leaves the one-session
capture above.
Review round 1 re-run (press-safety fixes)
Re-run 2026-10-07 after the review-r1 fixes, with the default pacing. AB_SERVER_ENDPOINT=127.0.0.1:1
kept the sibling suites' discovery probes off the network:
AB_SERVER_ENDPOINT=127.0.0.1:1 AB_DIFF_ENDPOINT=10.100.0.35:44818 AB_DIFF_COUNT_OPENS=1 \
dotnet test tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests \
--filter "FullyQualifiedName~Differential" --logger "console;verbosity=detailed"
Result: 12/12 passed — 7 live tests and 5 offline safety tests — with 0 unexpected and 0 expected divergences.
| Check | Result |
|---|---|
| Settings | pace 20 ms, max tags 200, max templates 50 |
| Browse | 9 = 9 |
| Selection (array round-robin first) | 1 array + 8 scalars. ab_server has no UDTs, so there were no UDT members and the coverage check had nothing to require |
| Reads | 9/9 equal |
| Templates | 0 ids, not truncated. Not vacuous by the new rule: ab_server's STRING is not a structure, so browse found no structured tags |
| Native Forward Opens | 1 |
| TCP connections to the target before native | 0 (baseline 0) |
Wire capture of this run (tcpdump in the ab_server container's namespace, decoded with tshark):
| TCP stream | Session | Forward Open | Forward Close | Requests | Window (s) |
|---|---|---|---|---|---|
| 0 | libplctag | 1 (Large, 0x5B) | 1 | 1× 0x55, 18× 0x52 | 0.000 → 0.487 |
| 1 | native | 1 (0x54) | 1 | 1× 0x55, 18× 0x52 | 0.510 → 0.984 |
- No discovery probes.
- Zero write services: no 0x4D, 0x53, 0x4B or 0x10.
- The two sessions are 23 ms apart and do not overlap.
The Forward Opens differ between the transports, and that is expected. libplctag always uses a Large
Forward Open. Native uses a regular one because the CompactLogix profile's connection size (504) is at most 511. Task 21 will
see the same split on the press.
What changed for press safety (review r1):
- Stop on the first connection-level failure, on either transport. That covers a refused connect, RegisterSession or Forward Open, a timeout, and a lost session. Any status other than NotFound/NotAllowed/OutOfBounds/TooLarge/TooSmall/Unsupported/TypeMismatch aborts the whole run with the failing operation and status. Nothing is retried.
- The native phase is capped at one open attempt. It sets the new process-wide
CipConnection.OpenAttemptBudget, so a reconnect cannot reach the wire at all, even from inside an enumerator that swallows a per-program failure. - The run also aborts the moment
ForwardOpenCountpasses 1. This is checked after every browse entry, template and tag. - Offline pin: against a loopback listener that accepts and hangs up, the native phase aborts at browse after exactly one TCP connect. Removing the harness abort turns the test red. Removing both the abort and the budget turns the connect count red.
- Pacing:
AB_DIFF_PACE_MS(default 20) is applied before every harness-issued request: browse, each template read, each tag's create-read and read. Listing pages inside the enumerators are not paced. - Volume caps:
AB_DIFF_MAX_TEMPLATESdefaults to 50.AB_DIFF_MAX_TAGSandAB_DIFF_MAX_TEMPLATESmust be at most 500; a higher value fails fast instead of being clamped. - Selection: the default selection takes arrays (up to 20), UDT members (up to 20) and scalars round-robin, so any cap of 3 or more exercises all three kinds. The reads test fails if browse offered arrays or UDT members but the selection holds none.
- Template parity fails when browse found structured tags but no template id was compared, and treats an undecodable shape on either side as a mismatch.
- Write-capable integration tests now require
AB_SERVER_ALLOW_WRITES=1and refuse whileAB_DIFF_ENDPOINTis set. Those tests are the Logix Emulate ALMD test and the AbLegacy SLC500 N7:5 round trip. The ab_server controllogix matrix is read-only and unaffected: 15 passed and 11 skipped withAB_SERVER_COMPOSE_PROFILE=controllogix.
Latency harness proven on ab_server (T21 Step 1)
Differential/PollLatencyComparisonTests.cs (+ PollLatencyRun.cs) ran 2026-10-07 against ab_server
(controllogix image, host port 44828 because an unrelated outbound socket held 44818 on the docker host),
under the same --filter "FullyQualifiedName~Differential" invocation as the differential. The mechanics work:
the timed phase reuses the differential's browse, the anchors, the open budget and the barriers; 1 native Forward Open
in the latency phase; 30 passes per transport, 0 tag failures; native packs 100 concurrent reads into 6 requests per pass.
ab_server has 7 atomic scalars, so the 100-tag set was repeated cyclically (reported in the output).
| Family | libplctag p50 / p95 ms | native p50 / p95 ms | native requests/pass | Criterion |
|---|---|---|---|---|
| CompactLogix (504, the press default) | 7.7 / 10.9 | 22.1 / 25.3 | 6 | FAIL |
| ControlLogix (4002) | 7.3 / 9.1 | 7.7 / 9.2 | 2 | pass |
The CompactLogix result was a real finding, not a harness fault. Native opened a 504-byte connection there, while libplctag opened a Large one (see the T18 capture above), so native needed 3× the round trips per pass.
Fixed: the CompactLogix profile now defaults to 4002, so native opens Large like libplctag. A refusal still falls back to
a regular 504 open. Re-run 2026-10-07 23:30–23:32Z against the compactlogix compose profile on :44818 (3 distinct atomic
tags, repeated to 100), with a capture in the container's network namespace:
| Run | libplctag p50 / p95 ms | native p50 / p95 ms | native req/pass | Criterion |
|---|---|---|---|---|
| 1 | — | — | — | aborted in the latency libplctag phase (see below) |
| 2 | 7.6 / 9.6 | 7.5 / 10.8 | 2 | FAIL (limit 10.6) |
| 3 | 7.8 / 9.3 | 8.1 / 9.4 | 2 | PASS (limit 10.2) |
| 4 | 7.7 / 10.0 | 7.8 / 11.7 | 2 | FAIL (limit 11.1) |
| 5 | — | — | — | aborted in the latency libplctag phase |
The structural gap is gone: p50s are equal, and both transports now send 2 requests per pass. At ~10 ms the p95 margin is about 1 ms, which is within this network path's jitter, so the ab_server verdict varies from run to run.
Capture: every session opened with 0x5B (Large Forward Open) and was accepted (0xDB, status 0). That covers 15 sessions: four per complete invocation and three in the aborted run 5. Each session had one Forward Close (0x4E to Connection Manager 6/1). There were zero 0x4D/0x53/0x4B. Both aborts (runs 1 and 5) were the simulator: libplctag's ~2 KB packed request was split across TCP segments (1448 + 606), and ab_server answered with FIN. The harness aborted on the first connection-level failure, as designed. A real controller reassembles TCP; ab_server does not.
The T21 press result is recorded under ### Pass 3 — 2026-10-08 02:27–02:30 UTC (HEAD 8e2dbc67`, after the final-review fixes; same settings as pass 2) — with packet capture
17/17 passed. Browse 3162 = 3162 tags; 20 UDT templates identical; 200 tags read with 0 unexpected divergences (6 live); native 1 Forward Open, Forward Closes sent/acked 1/1; latency native p95 105.1 ms ≤ libplctag p95 197.3 ms ×1.10 = 217.1 ms → PASS. Raw output: pass 3.
Packet capture (tcpdump -i utun4 host 10.209.70.160 and port 44818, analysed with TCP sequence reassembly):
| Session | Order | Window (s) | CIP services seen (requests) | Forward Close target |
|---|---|---|---|---|
| libplctag differential | 1 | 0.0 – 82.5 | 0x03, 0x4C, 0x52, 0x55, 0x5B ×1, 0x4E ×1 | Connection Manager 6/1 |
| native differential | 2 | 82.5 – 126.3 | 0x03, 0x4C, 0x52, 0x55, 0x5B ×1, 0x4E ×1 (+ UnRegisterSession) | Connection Manager 6/1 |
| libplctag latency | 3 | 126.5 – 152.4 | 0x0A (MSP), 0x52, 0x5B ×1, 0x4E ×1 | Connection Manager 6/1 |
| native latency | 4 | 152.4 – 176.5 | 0x0A (MSP), 0x52, 0x5B ×1, 0x4E ×1 (+ UnRegisterSession) | Connection Manager 6/1 |
- Zero write-class services (no 0x4D Write Tag, 0x53 Write Tag Fragmented, 0x4B Execute PCCC, and no 0x4E to anything but the Connection Manager).
- Four sessions, strictly sequential — each starts after the previous one's last packet; never two at once.
- Every session opened exactly one Large Forward Open and closed it with one Forward Close.
Latency (T21)` and is not filled in here.
AdminUI /run (T19)
Run 2026-10-07 on the local docker-dev rig (OrbStack), against the ab_server controllogix fixture
on 10.100.0.35:44818. Every container ran the same freshly built image
(otopcua-host:dev sha256:91bffbb0…, checked per container because :9200 round-robins). The
modal was driven with the Chrome tools against http://localhost:9200/raw.
Authoring (UI).
- Created the AbCip driver
t19-abcip(DRV-8ea0f0a8cfde) in the MAIN cluster. - Configure → Transport = "Native (managed)" → Save.
DriverInstance.DriverConfigthen held"transport":"Native", a string. - Reopened Configure. The dropdown showed "Native (managed)". Changed only Timeout from 2 to 3 and
saved. The row then held
"transport":"Native"and"timeout":"00:00:03", so the save-reverts-transport defect is fixed live, not only in unit tests. - Created device
Device1plc1with hostab://10.100.0.35:44818/1,0(ControlLogix). Test Connect returnedOK · 62 ms. - Added the raw tag
TestDINT(Int32, read-only, tag pathTestDINT, DInt). - Seeded
TestDINT = 4242on the simulator so a non-zero value proves the read path. - Raw namespace:
ns=2, the shareds=OtOpcUaroot plus RawPath ids. UNS usesns=3. - RawPath:
t19-abcip/Device1plc1/TestDINT.
Leg 1: Native. The deploy sealed with acks=6. Both central nodes logged:
AbCip driver DRV-8ea0f0a8cfde initializing: transport=Native tagFactory=NativeAbCipTagFactory
CLI read on central-1 (:4840) and central-2 (:4841):
Node: ns=2;s=t19-abcip/Device1plc1/TestDINT
Value: 4242
Status: 0x00000000
The simulator value was then changed to 5150 through the libplctag CLI. The native-served node
followed it (Value: 5150, Status: 0x00000000, source time 23:02:09.60Z).
Leg 2: inverted gate (Native, dead port).
- Device host changed to
ab://10.100.0.35:44819/1,0. Nothing listens on 44819 (ss -ltncount 0). - The node went
BadNoCommunication(value retained). A CLI read failed withServiceResultException: [80050000](BadCommunicationError). The CLIreadthrows on a Bad status rather than printing it. - The central-1 log showed the native error text, raised from
NativeAbCipTagRuntime.ReadAsync:ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport.AbTransportException: TCP connect to 10.100.0.35:44819 failed: ConnectionRefused ---> System.Net.Sockets.SocketException (111): Connection refused - For contrast, the same dead port under libplctag logged different wording:
So the switch changes behaviour and is not inert.
ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport.AbTransportException: ErrorTimeout ---> libplctag.LibPlcTagException: ErrorTimeout
Leg 3: back to libplctag.
- Device restored to port 44818. Configure → Transport = "libplctag (default)" → Save. The row held
"transport":"Libplctag". - The redeploy sealed with
acks=6. Both centrals loggedinitializing: transport=Libplctag tagFactory=LibplctagTagFactory. - CLI reads on
:4840and:4841:Value: 5150,Status: 0x00000000.
Fix found by the gate. The first deploy logged no transport line. AbCipDriverFactoryExtensions.Register
took no ILoggerFactory, and DriverFactoryBootstrap registered AbCip without one, so every production
AbCip driver ran on NullLogger. That hid the new line and every existing AbCip warning or error. AbCip
now takes the host logger factory the same way AbLegacy and Modbus do. AbCipTransportInitLogTests is
falsified by reverting that wiring: 2/2 tests fail.
Cleanup.
- The
t19-abcipdriver, device and tag rows were deleted and the change redeployed (sealed,acks=6). Both centrals loggedstopped driver child DRV-8ea0f0a8cfde. - The rig has no test driver pointing at the fixture.
- The fixture was brought down (
docker compose --profile controllogix down). - The simulator's
TestDINTwas left at 5150 until teardown. The container is gone, so the value does not persist.
Real press (T21)
Target: Z-35162, production CompactLogix 5069-L320ERS2, 10.209.70.160:44818, path 1,0, family CompactLogix
(native connection size 4002, Large Forward Open). Read-only. User approved the press and each time window; VPN up;
no other client on this Mac connected to the press. Driven from the Mac over VPN (≈44 ms RTT).
Pass 1 — 2026-10-07 23:35–23:36 UTC (HEAD c306c6f6; 25 tags, 20 templates, pace 50 ms, latency 100×30)
12/14 passed. Safety held: packet capture (tcpdump on utun4) shows 4 sequential sessions, each one Large
Forward Open (0x5B) and one Forward Close (0x4E, Connection Manager 6/1, acked); services seen only 0x03, 0x0A, 0x4C,
0x52, 0x55, 0x5B, 0x4E; zero writes (no 0x4D/0x53/0x4B). Browse parity and all 20 UDT template shapes identical;
native Forward Opens = 1.
Failures, and what they led to:
- Read parity, 2 tags — live analog values moved between the sequential libplctag and native phases
(
WW_M4ActualTemp384.887 → 385.138). Harness defect: it could not tell drift from divergence. Fixed by a volatility probe (82a0c298, tightened element-wise in43b81b66): a value that moves between two libplctag reads is compared by status, length, type, shape and every element that held still. - Latency — native p95 142.8 ms vs libplctag 99.1 ms ×1.10 = 109.1 ms (FAIL), medians native 93.7 / libplctag 95.2 ms; three native outliers in 30 passes. Criterion unchanged; pass count raised to 50 and per-pass GC/RTT diagnostics added.
- Test host crashed at exit —
A callback was made on a garbage collected delegate … callback_func_ex. Root cause is in libplctag.NET 1.5.2: a failed tag create leaves its native callback registered andDispose()doesn't release it. Production-reachable on the default transport (a nonexistent tag name crashes the server node at stop/restart). Fixed in the adapters (6cd68762, hardened6ae62873); Gitea #578. - Found before pass 1, on ab_server: native CompactLogix opened a 504-byte connection (the family default libplctag
never honoured) and needed ~3× the round-trips. Native default changed to 4002 with the existing Large→regular
fallback (
c306c6f6).
Pass 2 — 2026-10-08 00:21–00:24 UTC (HEAD df776170, fresh build; 200 tags, 20 templates, pace 50 ms, latency 100×50)
17/17 passed.
| Check | Result |
|---|---|
| Browse parity | libplctag 3162 tags, native 3162 — identical sets |
| UDT templates | 20 compared (of 74 found, capped) — identical shapes |
| Read parity | 200 tags: 0 unexpected, 0 expected (TypeMismatch) divergences; 6 live tags (below) |
| Native Forward Opens | 1 per native phase |
| Native Forward Closes sent/acked | 1/1 per native phase; 0 connections left |
| Sessions | libplctag and native never connected at the same time |
| Writes | none — read-only source guard passes; grep -c WriteAsync = 0 over every Differential file |
| libplctag failed-create orphans | 15 destroyed by the #578 fix, 0 rooted — no crash at exit |
⚠️ Pass 2 has no independent packet capture (pass 3 below re-ran it with one): the operator's tcpdump had stopped at 23:45 UTC, before pass 2 began,
and this was not noticed until afterwards. The read-only evidence for pass 2 is the source guard and the harness's
own Forward Open/Close counters; pass 1's capture is the wire-level proof for the same code paths.
Live tags (value moved between the two libplctag reads; every element that held still matched native):
- live: WW_M4ActualTemp (lib 4 byte(s) 0CCDC243 = 389.60193; lib probe 4 byte(s) 7084C243 = 389.03467; native 4 byte(s) 4C47C243 = 388.557)
- live: Program:EM01_PowerUnit.__fout9FB1561FA00D73F5 (lib 20 byte(s) 0050C348218986402189A640DF76D9C021898640 = [400000,4.2042394,5.2042394,-6.7957606,4.2042394]; lib probe 20 byte(s) 0050C348E74B8640E74BA64019B4D9C0E74B8640 = [400000,4.1967654,5.1967654,-6.8032346,4.1967654]; native 20 byte(s) 0050C348BB328640BB32A64045CDD9C0BB328640 = [400000,4.1936927,5.1936927,-6.8063073,4.1936927])
- live: Program:EM02_Clamp.__fout2F67CD35C2302EE0 (lib 16 byte(s) BFB2BF379A9919BF00D68349CDCCCCBE = [2.285221E-05,-0.6,1080000,-0.4]; lib probe 16 byte(s) 5119313A9A9919BF00D68349CDCCCCBE = [0.00067557866,-0.6,1080000,-0.4]; native 16 byte(s) 1562C2B99A9919BF00D68349CDCCCCBE = [-0.0003707564,-0.6,1080000,-0.4])
- live: Program:ES03_EM01_Zone1.__fout25DE96F3D0E2B7BB (lib 16 byte(s) 00E14CC0CDDA75BF000000C08F2D2BC0 = [-3.201233,-0.9603699,-2,-2.6746557]; lib probe 16 byte(s) 009464C09A2589BF000000C0A84932C0 = [-3.5715332,-1.07146,-2,-2.7857456]; native 16 byte(s) 00A03AC034F35FBF000000C0A8B325C0 = [-2.9160156,-0.87480474,-2,-2.5890903])
- live: Program:ES03_EM01_Zone1.__fout92848E133B44AF24 (lib 32 byte(s) 40EDA94160F0D043000090C10022A14980256C41C656D143006A4F400000A8C1 = [21.240845,417.87793,-18,1320000,14.759155,418.67792,3.2408447,-21]; lib probe 32 byte(s) 6017AD412089D043000090C10022A14940D1654186EFD04300BB68400000A8C1 = [21.636414,417.0713,-18,1320000,14.363586,417.87128,3.6364136,-21]; native 32 byte(s) C047A841BE97CF43000090C10022A14980706F4124FECF43003E42400000A8C1 = [21.035034,415.1855,-18,1320000,14.964966,415.98547,3.0350342,-21])
- live: Program:ES03_EM01_Zone1.__fout6DC7745628658A9D (lib 8 byte(s) 00824E40CD34A53F = [3.2266846,1.2906739]; lib probe 8 byte(s) 008A6440CDD4B63F = [3.5709229,1.4283692]; native 8 byte(s) 000D3940670A943F = [2.8914185,1.1565675])
Test summary:
Passed ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests.Differential.DifferentialSafetyTests.Template_parity_fails_when_vacuous_or_undecodable [26 ms]
Passed ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests.Differential.DifferentialSafetyTests.A_failing_endpoint_aborts_the_native_phase_after_exactly_one_connect [2 s]
Passed ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests.Differential.DifferentialSafetyTests.The_default_selection_keeps_arrays_and_udt_members_at_any_cap [17 ms]
Passed ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests.Differential.DifferentialSafetyTests.Volume_knobs_fail_fast_above_the_ceiling_instead_of_clamping [4 ms]
Passed ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests.Differential.DifferentialSafetyTests.A_failing_endpoint_aborts_the_libplctag_phase_before_any_tag_is_read [6 s]
Passed ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests.Differential.PollLatencyComparisonTests.Tag_pick_takes_atomic_scalars_only_and_repeats_a_short_set_visibly [15 ms]
Passed ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests.Differential.PollLatencyComparisonTests.Native_poll_p95_is_within_ten_percent_of_libplctag [3 m 2 s]
Passed ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests.Differential.TransportDifferentialTests.The_native_side_closes_its_one_connection_with_exactly_one_forward_close [< 1 ms]
Passed ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests.Differential.TransportDifferentialTests.No_failed_libplctag_tag_is_left_with_a_live_native_callback [< 1 ms]
Passed ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests.Differential.TransportDifferentialTests.A_live_value_is_compared_structurally_and_a_stable_one_by_value [3 ms]
Passed ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests.Differential.TransportDifferentialTests.The_harness_is_read_only_by_construction [36 ms]
Passed ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests.Differential.TransportDifferentialTests.Browse_listings_decode_to_identical_tag_sets_through_both_enumerators [6 ms]
Passed ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests.Differential.TransportDifferentialTests.The_comparison_flags_every_divergence_and_excuses_only_a_native_type_mismatch [< 1 ms]
Passed ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests.Differential.TransportDifferentialTests.Every_template_decodes_to_an_identical_shape_through_both_readers [< 1 ms]
Passed ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests.Differential.TransportDifferentialTests.The_native_side_makes_exactly_one_forward_open_over_the_whole_run [< 1 ms]
Passed ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests.Differential.TransportDifferentialTests.Every_selected_tag_reads_identically_through_both_transports [3 ms]
Passed ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests.Differential.TransportDifferentialTests.Libplctag_and_native_are_never_connected_at_the_same_time [< 1 ms]
Total tests: 17
Passed: 17
Full raw dotnet test output: pass 1, pass 2.
Latency (T21)
Pass 2 (100 atomic tags × 50 passes per transport, 200 ms gap, all reads in a pass issued concurrently):
| Transport | Passes | p50 ms | p95 ms | min ms | max ms | CIP requests per pass (median) | Tag failures |
|---|---|---|---|---|---|---|---|
| libplctag | 50 | 107.0 | 139.5 | 57.9 | 164.8 | n/a | 0 |
| native | 50 | 100.3 | 105.8 | 94.8 | 107.5 | 2 | 0 |
Criterion: native p95 105.8 ms ≤ libplctag p95 139.5 ms × 1.10 = 153.4 ms → PASS. Native was faster at the median
and much steadier (max 107.5 ms vs 164.8 ms), packing 95 reads into 2 CIP requests per pass. Five pad/config tags that
libplctag refused at setup were removed from both sets (Local:*:I.Pad1, Local:5:C.CfgSize, Local:5:C.CfgIDNum).
Pass 1 (30 passes) failed this criterion on three native outliers (p95 142.8 vs limit 109.1; medians native 93.7 / libplctag 95.2) — see above.
Per-pass detail (pass 2):
| Pass | libplctag ms | libplctag GC 0/1/2 | native ms | native GC 0/1/2 | native requests | native RTT min/mean/max ms |
|---|---|---|---|---|---|---|
| 1 | 104.8 | 0/0/0 | 100.3 | 0/0/0 | 2 | 44.9/49.6/54.4 |
| 2 | 57.9 | 0/0/0 | 100.5 | 0/0/0 | 2 | 47.0/50.1/53.3 |
| 3 | 139.5 | 0/0/0 | 105.8 | 0/0/0 | 2 | 45.3/52.9/60.4 |
| 4 | 104.5 | 0/0/0 | 100.6 | 1/0/0 | 2 | 47.3/50.2/53.2 |
| 5 | 108.0 | 0/0/0 | 105.3 | 0/0/0 | 2 | 44.5/52.6/60.6 |
| 6 | 104.4 | 0/0/0 | 105.6 | 0/0/0 | 2 | 44.2/52.7/61.2 |
| 7 | 60.8 | 0/0/0 | 98.7 | 0/0/0 | 2 | 44.3/49.3/54.2 |
| 8 | 59.6 | 0/0/0 | 102.6 | 0/0/0 | 2 | 47.4/51.2/55.0 |
| 9 | 109.2 | 0/0/0 | 97.0 | 0/0/0 | 2 | 45.9/48.4/50.9 |
| 10 | 109.7 | 0/0/0 | 99.3 | 0/0/0 | 2 | 45.9/49.6/53.3 |
| 11 | 105.9 | 0/0/0 | 103.0 | 0/0/0 | 2 | 46.2/51.4/56.7 |
| 12 | 107.7 | 0/0/0 | 95.1 | 0/0/0 | 2 | 43.3/47.5/51.7 |
| 13 | 104.7 | 0/0/0 | 104.0 | 0/0/0 | 2 | 45.7/52.0/58.2 |
| 14 | 106.8 | 0/0/0 | 96.5 | 0/0/0 | 2 | 43.6/48.2/52.7 |
| 15 | 107.2 | 0/0/0 | 102.4 | 0/0/0 | 2 | 46.1/51.1/56.2 |
| 16 | 105.5 | 0/0/0 | 97.4 | 0/0/0 | 2 | 44.7/48.6/52.6 |
| 17 | 105.9 | 0/0/0 | 107.5 | 0/0/0 | 2 | 51.0/53.7/56.4 |
| 18 | 103.2 | 0/0/0 | 96.6 | 0/0/0 | 2 | 43.0/48.2/53.5 |
| 19 | 106.5 | 0/0/0 | 102.1 | 0/0/0 | 2 | 45.5/51.0/56.4 |
| 20 | 106.8 | 0/0/0 | 99.6 | 0/0/0 | 2 | 44.6/49.7/54.9 |
| 21 | 122.8 | 0/0/0 | 104.5 | 0/0/0 | 2 | 52.1/52.2/52.3 |
| 22 | 109.8 | 0/0/0 | 104.3 | 0/0/0 | 2 | 45.8/52.1/58.5 |
| 23 | 104.3 | 0/0/0 | 98.6 | 0/0/0 | 2 | 45.4/49.2/53.0 |
| 24 | 109.7 | 0/0/0 | 98.8 | 0/0/0 | 2 | 45.9/49.3/52.8 |
| 25 | 155.1 | 0/0/0 | 101.0 | 0/0/0 | 2 | 45.5/50.4/55.4 |
| 26 | 164.8 | 0/0/0 | 102.9 | 0/0/0 | 2 | 43.9/51.4/58.8 |
| 27 | 120.4 | 0/0/0 | 101.4 | 0/0/0 | 2 | 48.2/50.7/53.1 |
| 28 | 108.6 | 0/0/0 | 101.7 | 0/0/0 | 2 | 46.9/50.8/54.7 |
| 29 | 106.0 | 0/0/0 | 98.5 | 0/0/0 | 2 | 45.5/49.2/52.9 |
| 30 | 102.9 | 0/0/0 | 94.9 | 0/0/0 | 2 | 43.6/47.4/51.2 |
| 31 | 111.3 | 0/0/0 | 99.9 | 0/0/0 | 2 | 45.6/49.9/54.2 |
| 32 | 103.3 | 0/0/0 | 96.4 | 0/0/0 | 2 | 43.6/48.1/52.6 |
| 33 | 105.8 | 0/0/0 | 104.6 | 0/0/0 | 2 | 46.4/52.2/58.0 |
| 34 | 109.3 | 0/0/0 | 95.8 | 0/0/0 | 2 | 43.2/47.8/52.4 |
| 35 | 110.2 | 0/0/0 | 102.1 | 0/0/0 | 2 | 46.3/51.0/55.7 |
| 36 | 108.0 | 0/0/0 | 101.7 | 0/0/0 | 2 | 47.5/50.8/54.0 |
| 37 | 107.6 | 0/0/0 | 96.7 | 0/0/0 | 2 | 45.0/48.3/51.5 |
| 38 | 108.9 | 0/0/0 | 96.5 | 0/0/0 | 2 | 44.7/48.2/51.6 |
| 39 | 105.2 | 0/0/0 | 98.2 | 0/0/0 | 2 | 45.6/49.0/52.5 |
| 40 | 106.7 | 0/0/0 | 99.4 | 0/0/0 | 2 | 44.4/49.6/54.9 |
| 41 | 107.0 | 0/0/0 | 101.8 | 0/0/0 | 2 | 46.9/50.8/54.8 |
| 42 | 108.6 | 0/0/0 | 106.7 | 0/0/0 | 2 | 49.2/53.3/57.4 |
| 43 | 107.0 | 0/0/0 | 102.6 | 0/0/0 | 2 | 49.9/51.2/52.6 |
| 44 | 107.7 | 0/0/0 | 100.3 | 0/0/0 | 2 | 47.4/50.1/52.7 |
| 45 | 109.5 | 0/0/0 | 102.6 | 0/0/0 | 2 | 45.8/51.2/56.7 |
| 46 | 107.6 | 0/0/0 | 104.0 | 0/0/0 | 2 | 48.4/52.0/55.5 |
| 47 | 115.6 | 0/0/0 | 99.0 | 0/0/0 | 2 | 45.9/49.4/52.9 |
| 48 | 106.0 | 0/0/0 | 97.8 | 0/0/0 | 2 | 44.9/48.8/52.8 |
| 49 | 104.5 | 0/0/0 | 98.4 | 0/0/0 | 2 | 46.1/49.1/52.2 |
| 50 | 112.5 | 0/0/0 | 94.8 | 1/1/1 | 2 | 43.0/47.3/51.7 |