a7c27e978f
Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
800 lines
57 KiB
Markdown
800 lines
57 KiB
Markdown
# 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
|
||
libplctag `v2.7.0` + patches 0001/0002). Host compose files md5-identical to the repo's.
|
||
- **Commit:** fixes `82d011c7` + `2171fc8c`, test matrix `8c68694d`.
|
||
|
||
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):
|
||
|
||
```bash
|
||
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` = −123456789
|
||
- `TestREAL` = 3.14159
|
||
- `Pump_1_Speed` = 1234.5
|
||
- `Line1_Count` = 987654
|
||
- `TestArray[i]` = (i−8)·100003+7, which includes negatives
|
||
- `TestString` = `"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`/`AbServerTheory` reachability probes. xUnit runs them during discovery for the *other* suites, even under the filter. They target `AB_SERVER_ENDPOINT` (default 10.100.0.35), never `AB_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:
|
||
|
||
```bash
|
||
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 `ForwardOpenCount` passes 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_TEMPLATES` defaults to 50. `AB_DIFF_MAX_TAGS` and `AB_DIFF_MAX_TEMPLATES` must 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=1` and refuse while
|
||
`AB_DIFF_ENDPOINT` is 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 with
|
||
`AB_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](2026-10-07-native-ab-transport-t21/t21-pass3.txt).
|
||
|
||
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.DriverConfig` then 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 `Device1plc1` with host `ab://10.100.0.35:44818/1,0` (ControlLogix). Test Connect
|
||
returned `OK · 62 ms`.
|
||
- Added the raw tag `TestDINT` (Int32, read-only, tag path `TestDINT`, DInt).
|
||
- Seeded `TestDINT = 4242` on the simulator so a non-zero value proves the read path.
|
||
- Raw namespace: `ns=2`, the shared `s=OtOpcUa` root plus RawPath ids. UNS uses `ns=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 -ltn` count 0).
|
||
- The node went `BadNoCommunication` (value retained). A CLI read failed with
|
||
`ServiceResultException: [80050000]` (BadCommunicationError). The CLI `read` throws 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:
|
||
```
|
||
ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport.AbTransportException: ErrorTimeout
|
||
---> libplctag.LibPlcTagException: ErrorTimeout
|
||
```
|
||
So the switch changes behaviour and is not inert.
|
||
|
||
**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 logged
|
||
`initializing: transport=Libplctag tagFactory=LibplctagTagFactory`.
|
||
- CLI reads on `:4840` and `: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-abcip` driver, device and tag rows were deleted and the change redeployed (sealed,
|
||
`acks=6`). Both centrals logged `stopped 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 `TestDINT` was 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_M4ActualTemp` 384.887 → 385.138). Harness defect: it could not tell drift from divergence. Fixed by a volatility
|
||
probe (`82a0c298`, tightened element-wise in `43b81b66`): 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 and `Dispose()` 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`, hardened `6ae62873`); 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](2026-10-07-native-ab-transport-t21/t21-pass1.txt), [pass 2](2026-10-07-native-ab-transport-t21/t21-pass2.txt).
|
||
|
||
## 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 |
|
||
|
||
```
|
||
|
||
## hw4 evidence batch — 2026-10-09 (Gitea #592; #582, #572, ab_server bit write)
|
||
|
||
Branch `hw4-evidence`, harness `Differential/EvidenceTests.cs` (same folder, same read-only source/IL guard, extra opt-in
|
||
env gates so a standard `Differential` run never repeats them). Press: Z-35162, CompactLogix 5069-L320ERS2, VPN, pace 50 ms,
|
||
one short-lived native session per leg (1 Forward Open, 1 Forward Close acked), reads only. A diagnostics-only tap
|
||
(`ConnectionDiagnostics.ExchangeObserver`, null in production, exceptions swallowed) lets the harness see each CIP request and
|
||
its raw reply; no packet capture was used because `tcpdump` needs sudo on this Mac.
|
||
|
||
### Real-controller scalar writes (user-approved one-off, 2026-10-09)
|
||
|
||
Done by the controller session, not by this harness, through the AbCip CLI on the **native** transport against Z-35162. The
|
||
tags were chosen because a census of the L5X export shows no routine references them and they are absent from the Galaxy map.
|
||
Every write and read-back returned Good, and each tag was restored:
|
||
|
||
| Tag | Type | Sequence |
|
||
|---|---|---|
|
||
| `test2` | DINT | 0 -> 4242 -> 0 |
|
||
| `Program:UN01_Press.xxtest` | BOOL | false -> true -> false |
|
||
| `Program:ES03_EM03_Zone3._ff_test` | REAL | 0 -> 42.5 -> 0 |
|
||
|
||
This is the first native write on a real controller: scalar DINT/BOOL/REAL Write Tag is proven on a CompactLogix. The user's
|
||
write approval covered only that one-off and is consumed; every other leg of this batch was read-only. Still unproven on
|
||
hardware: fragmented wide-struct writes (#567), array writes, STRING writes, UDT member writes.
|
||
|
||
### #582 - mixed STRING / array / scalar Multiple Service Packet (PASS)
|
||
|
||
`AB_EVID_MIX=1 AB_DIFF_ENDPOINT=10.209.70.160:44818 AB_DIFF_PACE_MS=50` -> `EvidenceTests.Mixed_string_array_and_scalar_reads_travel_as_one_multiple_service_packet`
|
||
(raw output: [hw4-mix-msp.txt](2026-10-07-native-ab-transport-t21/hw4-mix-msp.txt)). Five reads were issued concurrently on one native session,
|
||
five times (>= 1 s apart), bracketed by sequential single reads of the same tags:
|
||
|
||
| Tag | Type | Value (single before / all 5 batches / single after) |
|
||
|---|---|---|
|
||
| `Program:UN01_Press.Sts_CycleTime_STR` | STRING | `'2:23:37'` throughout (matches the L5X export) |
|
||
| `Program:EM01_PowerUnit._SEQ_EP[1]` | DINT element | `300` throughout |
|
||
| `Program:EM01_PowerUnit._SEQ_EP` | DINT[32] | `[0,300,0 x30]` throughout |
|
||
| `test2` | DINT | `0` |
|
||
| `Program:UN01_Press.xxtest` | BOOL | `False` |
|
||
|
||
- **5/5 attempts put all five reads in ONE channel send**: a single 204 B Multiple Service Packet (service 0x0A) answered by a
|
||
273 B reply with `embedded=5`, outer status 0x00, embedded statuses all 0x00 with data lengths 92 B (STRING), 6 B, 130 B
|
||
(32 x DINT + type header), 6 B, 3 B. No embedded 0x06, no fragment re-fetch, `RequestsSent` delta = 1 each time.
|
||
- Every batch value equalled both bracketing singles, so reply packing/offsets decode correctly for a mix of 3-byte, 6-byte,
|
||
92-byte and 130-byte replies. The values are stable (static config/test tags), so equality is not a coincidence of drift.
|
||
- The tags are non-zero where it matters (the string and the array element), so decode equality is not vacuous; `test2` /
|
||
`xxtest` are zero by design and are only there to pin the scalar slots.
|
||
- On `ab_server` (loopback) the same harness showed the pump can wake mid-queue and split a batch (attempt 1 of 5: an MSP of 4
|
||
plus a bare read). That is a scheduling race, not a packing defect, which is why the leg repeats the batch and requires at
|
||
least one whole-batch MSP; on the press all five attempts were whole.
|
||
|
||
### #572 - does a real controller list symbol instances out of order? (observed: no)
|
||
|
||
`AB_EVID_BROWSE=1` -> `EvidenceTests.Symbol_listing_pages_are_ascending_on_a_real_controller` (raw: [hw4-listing-order.txt](2026-10-07-native-ab-transport-t21/hw4-listing-order.txt)).
|
||
One native `@tags` browse of Z-35162 through the production enumerator (default paging and pacing): 3162 tags, **41 listing
|
||
pages** (controller scope 8 pages, then each of the 15 programs, up to 6 pages for `ES03_TemperatureControl`).
|
||
|
||
- **0 non-ascending pages.** Within every page the instance ids were strictly ascending, and each page's first id was above the
|
||
previous page's maximum (e.g. controller scope 1..105, 106..207, ... 670..671).
|
||
- Instance ids are not contiguous: ids can be skipped (e.g. `EM02_Clamp` page ends `...,281,282,288,289`; `UN01_Press` page 1
|
||
ends at 109 and page 2 starts at 111). The reader's "next start = highest id + 1" rule handles that and the out-of-order case
|
||
alike; this is evidence only, no code changed.
|
||
- One empty page (`Program:Auxiliary`, 0 entries) was observed and handled.
|
||
|
||
### ab_server PCCC bit-file write (`B3:0/5`): the documented gap does not reproduce (no patch)
|
||
|
||
Provenance: `AbLegacy.IntegrationTests/Docker/README.md` ("Bit-file writes on ab_server", May 2026, libplctag + `--plc=SLC500`,
|
||
before the native transport) recorded `B3:0/5` -> `0x803D0000`. New `AbLegacyBitWriteTests` (writes `B3:0` = 0x0A0A, sets
|
||
bit 5, clears bit 3, asserts the whole word each time so neighbour bits are proven preserved) against local ab_server
|
||
(libplctag v2.7.0 + patches 0001/0002, OrbStack, `AB_LEGACY_ENDPOINT` pinned to a lane-local port):
|
||
|
||
| Family | Libplctag | Native |
|
||
|---|---|---|
|
||
| SLC500 | pass | pass |
|
||
| MicroLogix | pass | pass |
|
||
| PLC-5 | pass | `BadNotSupported` (0x803D0000) |
|
||
|
||
- **Finding:** the originally documented gap does not reproduce on the current fixture and driver; the cause of the original
|
||
failure is undetermined. No ab_server patch or Dockerfile change is needed. The driver does a per-parent-word read-modify-write
|
||
(typed read, typed write of the word), not a masked FNC 0xAB write (ab_server does implement 0xAB for 16-bit elements).
|
||
- The PLC-5 native `0x803D0000` is a **separate, intended** behaviour that merely shares the status code: all native PLC-5
|
||
writes are refused before the wire (#571, `NativeAbLegacyTagRuntime.WriteRefusal`). The test pins it. It is not the original
|
||
observation.
|
||
- No fix, so no revert-to-red proof; the test would fail if the word RMW clobbered neighbour bits.
|
||
|
||
## #611 UDT member-write gate — 2026-10-09 (user-approved, native transport)
|
||
|
||
Target: Z-35162, controller tag `_EmptyTemperatureSegment` (`UDT_TemperatureProfile_Segment`; no routine, alias or
|
||
trend references it in the 2026-09-11 L5X export; not in the Galaxy map). Tool: AbCip CLI from master `6c6800e5`,
|
||
`ab://10.209.70.160/1,0`, family CompactLogix, default (native) transport. Each leg: write, snapshot all ten members,
|
||
restore, snapshot again.
|
||
|
||
| Member | Type | Before | Wrote | Read back | Restored | Other 9 members |
|
||
|---|---|---|---|---|---|---|
|
||
| `ID` | DINT | 0 | 4242 | 4242 | 0 | unchanged (0/false) |
|
||
| `Enable` | BIT (BOOL member) | false | true | true | false | unchanged |
|
||
| `HoldTime` | REAL | 0 | 42.5 | 42.5 | 0 | unchanged |
|
||
|
||
Every write and read returned Good (`0x00000000`); the final snapshot equals the pre-gate snapshot. Not covered:
|
||
sibling-bit preservation inside one hidden host SINT (this UDT has a single BIT member) and the libplctag
|
||
member-write path. Raw output: the session scratchpad `hw4/gate611.txt`.
|
||
|
||
## AbLegacy on a real MicroLogix 1400 — 2026-10-09 (Z-33323, idle, user-directed)
|
||
|
||
Press selection: Z-33470 was mid-cycle (`N10:150/0` = `B3:0/4` = true) and was left alone; Z-33323 read idle on both, with the pump input `I:0/0` off. The other North presses time out on TCP from the VPN (ICMP answers). AbLegacy CLI from master, `ab://10.201.44.231/`, `-P MicroLogix`.
|
||
|
||
```
|
||
=== 10.201.44.231
|
||
ST14:10 Value: "Z-33323" Status: 0x00000000 (Good)
|
||
N10:150/0 Value: false Status: 0x00000000 (Good)
|
||
B3:0/4 Value: false Status: 0x00000000 (Good)
|
||
I:0/0 Value: <null> Status: 0x80050000 (BadCommunicationError)
|
||
I:0/1 Value: <null> Status: 0x80050000 (BadCommunicationError)
|
||
=== 10.201.44.249
|
||
ST14:10 Value: "Z-33470" Status: 0x00000000 (Good)
|
||
N10:150/0 Value: true Status: 0x00000000 (Good)
|
||
B3:0/4 Value: true Status: 0x00000000 (Good)
|
||
I:0/0 Value: <null> Status: 0x80050000 (BadCommunicationError)
|
||
I:0/1 Value: <null> Status: 0x80050000 (BadCommunicationError)
|
||
|
||
IDLE CHECK N10:150/0=false 0x00000000 (Good) B3:0/4=false 0x00000000 (Good)
|
||
S:0 Int | lib: <null> 0x80050000 (BadCommunicationError) | nat: 4 0x00000000 (Good)
|
||
S:57 Int | lib: <null> 0x80050000 (BadCommunicationError) | nat: 1400 0x00000000 (Good)
|
||
S:4 Int | lib: <null> 0x80050000 (BadCommunicationError) | nat: 9952 0x00000000 (Good)
|
||
N9:0 Int | lib: 2331 0x00000000 (Good) | nat: 2331 0x00000000 (Good)
|
||
N9:4 Int | lib: 3867 0x00000000 (Good) | nat: 3867 0x00000000 (Good)
|
||
N7:255 Int | lib: 32000 0x00000000 (Good) | nat: 32000 0x00000000 (Good)
|
||
F8:226 Float | lib: 2.1 0x00000000 (Good) | nat: 2.1 0x00000000 (Good)
|
||
F8:214 Float | lib: 200 0x00000000 (Good) | nat: 200 0x00000000 (Good)
|
||
B3:24 Int | lib: 1 0x00000000 (Good) | nat: 1 0x00000000 (Good)
|
||
B3:24/0 Bit | lib: true 0x00000000 (Good) | nat: true 0x00000000 (Good)
|
||
B3:28 Int | lib: 2309 0x00000000 (Good) | nat: 2309 0x00000000 (Good)
|
||
B3:28/8 Bit | lib: true 0x00000000 (Good) | nat: true 0x00000000 (Good)
|
||
B3:28/9 Bit | lib: false 0x00000000 (Good) | nat: false 0x00000000 (Good)
|
||
B3:132/14 Bit | lib: true 0x00000000 (Good) | nat: true 0x00000000 (Good)
|
||
T4:15.PRE Int | lib: 6000 0x00000000 (Good) | nat: 6000 0x00000000 (Good)
|
||
T4:15.ACC Int | lib: 6000 0x00000000 (Good) | nat: 6000 0x00000000 (Good)
|
||
T4:15.DN Bit | lib: true 0x00000000 (Good) | nat: true 0x00000000 (Good)
|
||
T4:93.PRE Int | lib: 7000 0x00000000 (Good) | nat: 7000 0x00000000 (Good)
|
||
T4:93.DN Bit | lib: false 0x00000000 (Good) | nat: false 0x00000000 (Good)
|
||
T4:14.PRE Int | lib: 3600 0x00000000 (Good) | nat: 3600 0x00000000 (Good)
|
||
T4:14.ACC Int | lib: 2922 0x00000000 (Good) | nat: 2922 0x00000000 (Good)
|
||
T4:11.PRE Int | lib: 32767 0x00000000 (Good) | nat: 32767 0x00000000 (Good)
|
||
T40:3.PRE Int | lib: 500 0x00000000 (Good) | nat: 500 0x00000000 (Good)
|
||
T40:12.PRE Int | lib: 500 0x00000000 (Good) | nat: 500 0x00000000 (Good)
|
||
C5:0.PRE Int | lib: 999 0x00000000 (Good) | nat: 999 0x00000000 (Good)
|
||
C5:0.ACC Int | lib: 111 0x00000000 (Good) | nat: 111 0x00000000 (Good)
|
||
C5:8.DN Bit | lib: true 0x00000000 (Good) | nat: true 0x00000000 (Good)
|
||
C5:1.PRE Int | lib: 32767 0x00000000 (Good) | nat: 32767 0x00000000 (Good)
|
||
R6:0.LEN Int | lib: 32 0x00000000 (Good) | nat: 32 0x00000000 (Good)
|
||
R6:0.POS Int | lib: 0 0x00000000 (Good) | nat: 0 0x00000000 (Good)
|
||
R6:0.DN Bit | lib: <null> 0x80050000 (BadCommunicationError) | nat: false 0x00000000 (Good)
|
||
ST14:10 String | lib: "Z-33323" 0x00000000 (Good) | nat: "Z-33323" 0x00000000 (Good)
|
||
ST14:0 String | lib: "EQ_TEST_LARGE_MOLD" 0x00000000 (Good) | nat: "EQ_TEST_LARGE_MOLD" 0x00000000 (Good)
|
||
O:0 Int | lib: <null> 0x80050000 (BadCommunicationError) | nat: 0 0x00000000 (Good)
|
||
O:0/3 Bit | lib: <null> 0x80050000 (BadCommunicationError) | nat: false 0x00000000 (Good)
|
||
I:0 Int | lib: <null> 0x80050000 (BadCommunicationError) | nat: -29376 0x00000000 (Good)
|
||
I:0/0 Bit | lib: <null> 0x80050000 (BadCommunicationError) | nat: false 0x00000000 (Good)
|
||
IDLE RECHECK N10:150/0=false 0x00000000 (Good)
|
||
|
||
pre: cycle=falseG pump=falseG
|
||
-- N7:23 write 4242: 0x00000000 (Good) -> N7:22=0G N7:23=4242G N7:24=0G
|
||
-- N7:23 restore 0: 0x00000000 (Good) -> N7:23=0G
|
||
-- F8:190 write 42.5: 0x00000000 (Good) -> F8:190=42.5G
|
||
-- F8:190 restore 0: 0x00000000 (Good) -> F8:190=0G
|
||
-- B3:200/5 set: 0x00000000 (Good) -> B3:200=32G (exp 32)
|
||
-- B3:200/3 set: 0x00000000 (Good) -> B3:200=40G (exp 40)
|
||
-- B3:200/5 clear: 0x00000000 (Good) -> B3:200=8G (exp 8)
|
||
-- B3:200/3 clear: 0x00000000 (Good) -> B3:200=0G (exp 0)
|
||
-- neighbours: B3:199=0G B3:201=0G
|
||
post: cycle=falseG pump=falseG N7:23=0G F8:190=0G B3:200=0G
|
||
```
|
||
|
||
### 2026-10-09 (later) — libplctag S/O/I + control status bits fixed (#659), re-read on Z-33323
|
||
|
||
Root cause, from libplctag v2.6.0 `src/protocols/ab/pccc.c` (the native core inside `libplctag.NativeImport` 1.0.41, pulled by the `libplctag` 1.5.2 package): `parse_pccc_file_num` (L881) requires a digit after the file letter and `parse_pccc_file_type` (L828-835) only treats `S` as the status file when a digit follows, so `S:0`/`O:0`/`I:0` never parse; `sub_element_lookup` (L936-1064) has only `con`/`len`/`pos` for R files, and `parse_pccc_bit_num` (L1162) refuses a `/bit` on a non-2-byte element, so neither `R6:0.DN` nor `R6:0/13` parses. The libplctag adapter now translates (`LibplctagLegacyName`): `S:n`→`S2:n`, `O:n`→`O0:n`, `I:n`→`I1:n` (the native encoder's implicit file numbers) and an unknown status bit → `R6:0.CON/<bit>`. Press idle beforehand (`N10:150/0` = `B3:0/4` = false, Native) and afterwards (both false again). AbLegacy CLI built from branch `i659`, reads only, one short session per read, ≥ 1.2 s apart:
|
||
|
||
```
|
||
S:0 Int Libplctag value=4 status=0x00000000 (Good)
|
||
S:0 Int Native value=4 status=0x00000000 (Good)
|
||
S:57 Int Libplctag value=1400 status=0x00000000 (Good)
|
||
S:57 Int Native value=1400 status=0x00000000 (Good)
|
||
S:4 Int Libplctag value=12688 status=0x00000000 (Good)
|
||
S:4 Int Native value=-31956 status=0x00000000 (Good)
|
||
O:0 Int Libplctag value=0 status=0x00000000 (Good)
|
||
O:0 Int Native value=0 status=0x00000000 (Good)
|
||
O:0/3 Bit Libplctag value=false status=0x00000000 (Good)
|
||
O:0/3 Bit Native value=false status=0x00000000 (Good)
|
||
I:0 Int Libplctag value=-29376 status=0x00000000 (Good)
|
||
I:0 Int Native value=-29376 status=0x00000000 (Good)
|
||
I:0/0 Bit Libplctag value=false status=0x00000000 (Good)
|
||
I:0/0 Bit Native value=false status=0x00000000 (Good)
|
||
I:0/1 Bit Libplctag value=false status=0x00000000 (Good)
|
||
I:0/1 Bit Native value=false status=0x00000000 (Good)
|
||
R6:0.DN Bit Libplctag value=false status=0x00000000 (Good)
|
||
R6:0.DN Bit Native value=false status=0x00000000 (Good)
|
||
R6:0.EN Bit Libplctag value=false status=0x00000000 (Good)
|
||
R6:0.EN Bit Native value=false status=0x00000000 (Good)
|
||
R6:0.LEN Int Libplctag value=32 status=0x00000000 (Good)
|
||
R6:0.LEN Int Native value=32 status=0x00000000 (Good)
|
||
N9:0 Int Libplctag value=2331 status=0x00000000 (Good)
|
||
N9:0 Int Native value=2331 status=0x00000000 (Good)
|
||
F8:226 Float Libplctag value=2.1 status=0x00000000 (Good)
|
||
F8:226 Float Native value=2.1 status=0x00000000 (Good)
|
||
B3:28 Int Libplctag value=2309 status=0x00000000 (Good)
|
||
B3:28 Int Native value=2309 status=0x00000000 (Good)
|
||
T4:15.PRE Int Libplctag value=6000 status=0x00000000 (Good)
|
||
T4:15.PRE Int Native value=6000 status=0x00000000 (Good)
|
||
C5:8.DN Bit Libplctag value=true status=0x00000000 (Good)
|
||
C5:8.DN Bit Native value=true status=0x00000000 (Good)
|
||
ST14:10 String Libplctag value="Z-33323" status=0x00000000 (Good)
|
||
ST14:10 String Native value="Z-33323" status=0x00000000 (Good)
|
||
05:31:09 I:0/6 Libplctag Value: true | Status: 0x00000000 (Good)
|
||
05:31:11 I:0/6 Native Value: true | Status: 0x00000000 (Good)
|
||
05:31:13 I:0/15 Libplctag Value: true | Status: 0x00000000 (Good)
|
||
05:31:16 I:0/15 Native Value: true | Status: 0x00000000 (Good)
|
||
05:31:18 I:0/7 Libplctag Value: false | Status: 0x00000000 (Good)
|
||
05:31:20 I:0/7 Native Value: false | Status: 0x00000000 (Good)
|
||
05:31:22 S:4 Libplctag Value: 17831 | Status: 0x00000000 (Good)
|
||
05:31:24 S:4 Native Value: -26549 | Status: 0x00000000 (Good)
|
||
05:31:26 S:4 Libplctag Value: -4646 | Status: 0x00000000 (Good)
|
||
05:31:28 S:4 Native Value: 17335 | Status: 0x00000000 (Good)
|
||
```
|
||
|
||
Every address reads identically on both transports. `S:4` is the free-running clock, so its value differs read to read on each transport (both Good). `I:0` = -29376 = 0x8D40, and bits `/6` and `/15` read true on both while `/0`, `/1`, `/7` read false — the bit is extracted from the word libplctag read, not defaulted. ab_server has no S/O/I files, so this real-controller read is the only wire evidence; the translation is unit-pinned against a model of the `pccc.c` parser (`LibplctagLegacyNameTests`).
|
||
|
||
Caveats: `R6:0.DN` and `R6:0.EN` read `false` on both transports, so this read does not discriminate the control-word bit number (a wrong bit would very likely also read false) — the R bit mapping rests on the native table and the `pccc.c` parser model, not on this wire evidence. Writes: on libplctag every address the translation changes (`S:`/`O:`/`I:`, R/UA status bits) is refused `BadNotSupported` (#659 review), so this fix adds no libplctag write path.
|
||
|
||
## hw5 — #566 idle-connection close and a native STRING write on Z-35162 — 2026-10-09 (user-approved)
|
||
|
||
Press: Z-35162, CompactLogix 5069-L320ERS2 rev 34.11, `ab://10.209.70.160/1,0`, family CompactLogix, Mac VPN. The user
|
||
approved exactly two things: (A) one read-only idle-connection session for #566 and (B) native array and STRING writes on
|
||
logic-unreferenced tags. Harness: `AbCip.IntegrationTests/HardwareGate/` (its own namespace, outside the read-only
|
||
`Differential` guard), both tests env-gated. No packet capture (`tcpdump` needs sudo); the diagnostics tap
|
||
(`ConnectionDiagnostics.ExchangeObserver`, now also fed the Forward Open reply so the granted RPIs are visible) and the
|
||
transport's exception text are the evidence.
|
||
|
||
### A. #566 — what a real Logix does with a class-3 connection idle past its timeout: it drops TCP, no CIP status
|
||
|
||
`AB_DIFF_ENDPOINT=10.209.70.160:44818 AB_EVID_IDLE=1 AB_EVID_IDLE_SECONDS=60` →
|
||
`IdleConnectionEvidenceTests` (raw: [hw5-idle566.txt](2026-10-07-native-ab-transport-t21/hw5-idle566.txt)). One private
|
||
pool, keep-alive pushed out to 1 h so nothing touched the connection, open-attempt budget 2, three Read Tag requests of
|
||
`test2` (DINT) and nothing else. 10:04:49Z–10:05:54Z UTC.
|
||
|
||
| t (s) | Event |
|
||
|---|---|
|
||
| 0.26 | Large Forward Open (0x5B) granted: O→T API = T→O API = 2 000 000 µs (the requested RPI, unchanged). Multiplier 1 (×8) ⇒ connection timeout **16 s** |
|
||
| 0.32 | read 1: general status 0x00 |
|
||
| 0.32 – 60.33 | idle, no traffic (3.75 × the timeout) |
|
||
| 60.33 | read 2: **failed in 9 ms on the write**: `IOException: Broken pipe` ← `SocketException(Shutdown)` → `AbTransportException(BadConnection)` → OPC UA `BadCommunicationError` (0x80050000). No CIP reply, no encapsulation status, `ConnectionsClosedByTarget` 0 |
|
||
| 62.47 | read 3: fresh Large Forward Open granted (new O→T id), read general 0x00 — **recovered on the next request**, open attempts 2/2 |
|
||
| 62.56 | lease released: Forward Close sent/acked 1/1 |
|
||
|
||
- **Answer to #566: neither 0x0107 nor 0x0203.** The controller closed the TCP session; the next request's write hit a dead
|
||
socket. EPIPE on the *first* write after the idle (not a successful write followed by EOF) means the close was already
|
||
complete on the Mac's side, which is consistent with an RST (or FIN + RST) from the controller; without a capture the
|
||
segment type and the exact moment within the 60 s window are not observed — only that it happened after the 16 s
|
||
timeout and before 60 s.
|
||
- **The transport handles it correctly as it stands, so no production code changed.** The failed write faults the session
|
||
(`EncapsulationClient.Fault`, `IsConnected` false), the failing request alone fails `BadConnection` (fast, not an
|
||
exchange timeout), the driver maps that to `BadCommunicationError` and evicts the runtime / reports `Degraded` for that
|
||
poll, and the next send reopens through the Forward Open throttle. The 0x0107/0x0203 recycle (#566) stays: it is
|
||
correct for a target that keeps TCP up, and harmless here (not exercised).
|
||
- **In production this path needs more than idleness.** The keep-alive fires at half the expiry (8 s), so a connection the
|
||
driver holds never idles 16 s; the shape above arises after a controller restart / re-download or a network gap.
|
||
- **Pinned offline:** `CipConnectionTests.A_target_that_resets_an_idle_session_fails_the_next_request_fast_and_the_one_after_reopens`
|
||
replays the shape (a `FakeEipServer.ResetAllConnections` RST with no request in flight): `BadConnection` in < 4 s,
|
||
`ConnectionsClosedByTarget` 0, one Forward Open before / two after. Proven able to fail: with `EnsureOpenAsync` made to
|
||
reuse a non-connected session it goes red (the reopen never happens).
|
||
|
||
### B. Native writes: STRING PASS; array NOT RUN (no tag qualifies)
|
||
|
||
**STRING — `Program:ES03_TemperatureControl._StrTmp` (PASS, restored byte-exact).** Census: refs 0, extrefs 0, not in the
|
||
Galaxy map, program not HMI/Wonderware; raw grep of `SN3244.L5X` finds the name only at its declaration (line 69426).
|
||
`AB_HW_WRITE_GATEWAY=ab://10.209.70.160/1,0 AB_HW_WRITE_APPROVED=1 AB_HW_WRITE_STRING_TAG=Program:ES03_TemperatureControl._StrTmp`
|
||
→ `StringWriteGateTests`, through the production `AbCipDriver` on the native transport, 10:10:54Z–10:11:29Z UTC (raw:
|
||
[hw5-string-write.txt](2026-10-07-native-ab-transport-t21/hw5-string-write.txt)). A read-only dry run (`AB_HW_WRITE_DRYRUN=1`)
|
||
first confirmed the preflight. The raw bytes come from the tap on each Read Tag Fragmented reply.
|
||
|
||
| Step | Status | Value | Raw (type, handle, LEN, DATA…) |
|
||
|---|---|---|---|
|
||
| snapshot 1 | Good | `ES03_TemperatureControl:Fault` | `A002 CE0F 1D000000 4553…6C74 00…` (92 B) |
|
||
| snapshot 2 (+30 s) | Good | same | identical to snapshot 1 |
|
||
| preflight | — | — | built-in STRING (0x02A0 / handle 0x0FCE / 88 B), LEN 29, every byte past LEN zero |
|
||
| write `OTOPCUA-TEST` | Good (0x00000000) | — | — |
|
||
| read back | Good | `OTOPCUA-TEST` | `A002 CE0F 0C000000 4F544F504355412D54455354 00…` |
|
||
| restore original | Good | — | — |
|
||
| read back | Good | `ES03_TemperatureControl:Fault` | **byte-equal to snapshot 1** |
|
||
| final snapshot (+2 s) | Good | same | byte-equal to snapshot 1 |
|
||
|
||
The original matches the L5X export's value. The write is one unfragmented Write Tag (88 B of data plus the path is far
|
||
below the 4000-byte request limit of the Large Forward Open).
|
||
|
||
**Array — not run; no candidate passes the brief's own rule.** The only logic-unreferenced, Read/Write atomic arrays in the
|
||
census are `Program:EM01_PowerUnit._SEQ_EP` and `Program:EM02_Clamp._SEQ_EP` (DINT[32]), and the raw grep shows they are
|
||
**not** unreferenced: aliases `_SEQ_EP01_AutoCycle → _SEQ_EP[1]` (EM01) and `_SEQ_EP01_Jog / _SEQ_EP02_Auto /
|
||
_SEQ_EP03_Stopping → _SEQ_EP[1..3]` (EM02) are the step registers of the power-unit auto cycle and the clamp jog / auto /
|
||
stopping sequencers, used by dozens of rungs (`MOV(…,_SEQ_EP02_Auto)`, `EQU(_SEQ_EP01_AutoCycle,300)`…). The census counts
|
||
references by tag name, so references through an alias were invisible to it. A whole-array write would rewrite live
|
||
sequencer state, and even a single unaliased element fails the "name appears only at its declaration" check. The other
|
||
arrays are referenced (`_DisableStates`, `_ModeTransitions`, `_ModeNames`, `_VacuumCalibration_*`, `Stored`,
|
||
`_monitorID`), `ExternalAccess="Read Only"` (`_SEQ_UP`, `Inp_CycleTime`) or framework lookup tables (`Inf_LookupFwkB`). No
|
||
array write was made. An element write needs a tag the user picks (or an unaliased element they approve explicitly).
|
||
|
||
**#567 (fragmented wide-struct write) — still cannot be done here, and a DINT[32] would not have done it.** AbCip has no
|
||
whole-array write at the driver layer (array tags are read-only, `DriverArrayWriteSupport`); and at the transport layer a
|
||
128-byte array (or this 88-byte STRING) is a single Write Tag: `LogixTagClient.WriteAsync` fragments only when the request
|
||
exceeds `MaxRequestBytes` (4000 B on the Large Forward Open this controller grants). A fragmented write needs a value of
|
||
roughly 4 KB.
|
||
|
||
### Native array-element write — 2026-10-09 (user-approved, Z-35162)
|
||
|
||
`Program:EM01_PowerUnit._SEQ_EP` (DINT[32]). L5X check: its only alias is `_SEQ_EP01_AutoCycle -> _SEQ_EP[1]`; no routine references `_SEQ_EP` directly (no index, COP or FLL); no cross-program reference. AbCip CLI (native) from master `98c00904`, one element per read, two snapshots 30 s apart before writing. `[1]` = -1 is the idle auto-cycle step.
|
||
|
||
```
|
||
SNAP1: 0 -1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
|
||
SNAP2: 0 -1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
|
||
WRITE [31]=4242: Write Program:EM01_PowerUnit._SEQ_EP[31]: 0x00000000 (Good)
|
||
AFTER: 0 -1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 4242
|
||
RESTORE [31]=0: Write Program:EM01_PowerUnit._SEQ_EP[31]: 0x00000000 (Good)
|
||
FINAL: 0 -1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
|
||
```
|