Task #222 partial — unblock AB Legacy PCCC via cip-path workaround (5/5 stages) #223
@@ -68,11 +68,13 @@ values to the already-shipped driver.
|
||||
|
||||
## Known gaps
|
||||
|
||||
- **AB Legacy PCCC wire-level** against the ab_server Docker simulator is
|
||||
upstream-broken — see the "Known limitations" section in
|
||||
[Driver.AbLegacy.Cli.md](Driver.AbLegacy.Cli.md). Pointing the CLI at
|
||||
real SLC / MicroLogix / PLC-5 hardware or a RSEmulate 500 golden-box
|
||||
works as expected.
|
||||
- **AB Legacy cip-path quirk** — libplctag's ab_server requires a
|
||||
non-empty CIP routing path before forwarding to the PCCC dispatcher.
|
||||
Pass `--gateway "ab://127.0.0.1:44818/1,0"` against the Docker
|
||||
fixture; real SLC / MicroLogix / PLC-5 hardware accepts an empty
|
||||
path (`ab://host:44818/`). Bit-file writes (`B3:0/5`) still surface
|
||||
`0x803D0000` against ab_server — route operator-critical bit writes
|
||||
to real hardware until upstream fixes this.
|
||||
- **S7 PUT/GET communication** must be enabled in TIA Portal for any
|
||||
S7-1200/1500. See [Driver.S7.Cli.md](Driver.S7.Cli.md).
|
||||
- **TwinCAT AMS router** must be reachable (local XAR, standalone Router
|
||||
|
||||
@@ -93,11 +93,13 @@ cover the common ones but uncommon ones (`R` counters, `S` status files,
|
||||
|
||||
## Follow-up candidates
|
||||
|
||||
1. **Fix ab_server PCCC coverage upstream** — the scaffold lands the
|
||||
Docker infrastructure; the wire-level round-trip gap is in ab_server
|
||||
itself. Filing a patch to `libplctag/libplctag` to expand PCCC
|
||||
server-side opcode coverage would make the scaffolded smoke tests
|
||||
pass without a golden-box tier.
|
||||
1. **Expand ab_server PCCC coverage** — the smoke suite passes today
|
||||
for N (Int16), F (Float32), and L (Int32) files across SLC500 /
|
||||
MicroLogix / PLC-5 modes with the `/1,0` cip-path workaround in
|
||||
place. Known residual gap: bit-file writes (`B3:0/5`) surface
|
||||
`0x803D0000`. Contributing a patch to `libplctag/libplctag` to close
|
||||
this + documenting ab_server's empty-path rejection in its README
|
||||
would remove the last Docker-vs-hardware divergences.
|
||||
2. **Rockwell RSEmulate 500 golden-box tier** — Rockwell's real emulator
|
||||
for SLC/MicroLogix/PLC-5. Would close UDT-equivalent (integer-file
|
||||
indirection), timer/counter decomposition, and real ladder execution
|
||||
@@ -114,7 +116,8 @@ cover the common ones but uncommon ones (`R` counters, `S` status files,
|
||||
- `tests/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.IntegrationTests/AbLegacyServerFixture.cs`
|
||||
— TCP probe + skip attributes + env-var parsing
|
||||
- `tests/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.IntegrationTests/AbLegacyReadSmokeTests.cs`
|
||||
— three wire-level smoke tests (currently blocked by ab_server PCCC gap)
|
||||
— wire-level smoke tests; pass against the ab_server Docker fixture
|
||||
with `AB_LEGACY_COMPOSE_PROFILE` set to the running container
|
||||
- `tests/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.IntegrationTests/Docker/docker-compose.yml`
|
||||
— compose profiles reusing AB CIP Dockerfile
|
||||
- `tests/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.IntegrationTests/Docker/README.md`
|
||||
|
||||
@@ -44,7 +44,7 @@ Each driver has a dedicated fixture doc that lays out what the integration / uni
|
||||
- [AB CIP](AbServer-Test-Fixture.md) — Dockerized `ab_server` (multi-stage build from libplctag source); atomic-read smoke across 4 families; UDT / ALMD / family quirks unit-only
|
||||
- [Modbus](Modbus-Test-Fixture.md) — Dockerized `pymodbus` + per-family JSON profiles (4 compose profiles); best-covered driver, gaps are error-path-shaped
|
||||
- [Siemens S7](S7-Test-Fixture.md) — Dockerized `python-snap7` server; DB/MB read + write round-trip verified end-to-end on `:1102`
|
||||
- [AB Legacy](AbLegacy-Test-Fixture.md) — Docker scaffold via `ab_server` PCCC mode (task #224); wire-level round-trip currently blocked by ab_server's PCCC coverage gap, docs call out RSEmulate 500 + lab-rig resolution paths
|
||||
- [AB Legacy](AbLegacy-Test-Fixture.md) — Dockerized `ab_server` PCCC mode across SLC500 / MicroLogix / PLC-5 profiles (task #224); N/F/L-file round-trip verified end-to-end. `/1,0` cip-path required for the Docker fixture; real hardware uses empty. Residual gap: bit-file writes (`B3:0/5`) still surface BadState — real HW / RSEmulate 500 for those
|
||||
- [TwinCAT](TwinCAT-Test-Fixture.md) — XAR-VM integration scaffolding (task #221); three smoke tests skip when VM unreachable. Unit via `FakeTwinCATClient` with native-notification harness
|
||||
- [FOCAS](FOCAS-Test-Fixture.md) — no integration fixture, unit-only via `FakeFocasClient`; Tier C out-of-process isolation scoped but not shipped
|
||||
- [OPC UA Client](OpcUaClient-Test-Fixture.md) — no integration fixture, unit-only via mocked `Session`; loopback against this repo's own server is the obvious next step
|
||||
|
||||
@@ -132,7 +132,7 @@ section to skip it.
|
||||
|---|---|---|
|
||||
| Modbus | — | **PASS** (pymodbus fixture) |
|
||||
| AB CIP | — | **PASS** (ab_server fixture) |
|
||||
| AB Legacy | `AB_LEGACY_TRUST_WIRE=1` | **SKIP** (ab_server PCCC path upstream-broken — task #222) |
|
||||
| AB Legacy | — | **PASS** (ab_server SLC500/MicroLogix/PLC-5 profiles; `/1,0` cip-path required for the Docker fixture) |
|
||||
| S7 | — | **PASS** (python-snap7 fixture) |
|
||||
| FOCAS | `FOCAS_TRUST_WIRE=1` | **SKIP** (no public simulator — task #222 lab rig) |
|
||||
| TwinCAT | `TWINCAT_TRUST_WIRE=1` | **SKIP** (needs XAR or standalone Router — task #221) |
|
||||
|
||||
@@ -17,8 +17,8 @@
|
||||
},
|
||||
|
||||
"ablegacy": {
|
||||
"$comment": "Gated behind AB_LEGACY_TRUST_WIRE=1 — ab_server PCCC path upstream-broken, needs real SLC / MicroLogix / PLC-5 or RSEmulate 500.",
|
||||
"gateway": "ab://192.168.1.10/1,0",
|
||||
"$comment": "Works against ab_server --profile slc500 (Docker fixture) or real SLC/MicroLogix/PLC-5 hardware. `/1,0` cip-path is required for the Docker fixture; real hardware accepts an empty path — e.g. `ab://10.0.1.50:44818/`.",
|
||||
"gateway": "ab://127.0.0.1/1,0",
|
||||
"plcType": "Slc500",
|
||||
"address": "N7:5",
|
||||
"bridgeNodeId": "ns=2;s=AbLegacy/N7_5"
|
||||
|
||||
@@ -4,16 +4,19 @@
|
||||
End-to-end CLI test for the AB Legacy (PCCC) driver.
|
||||
|
||||
.DESCRIPTION
|
||||
**KNOWN-BROKEN upstream (ab_server PCCC dispatcher gap, verified 2026-04-21).**
|
||||
Works against real SLC / MicroLogix / PLC-5 hardware or a RSEmulate 500
|
||||
golden-box. Against the Docker ab_server the tests deliberately skip —
|
||||
same gate as tests/.../AbLegacy.IntegrationTests (AB_LEGACY_TRUST_WIRE=1).
|
||||
Runs against libplctag's ab_server PCCC Docker fixture (one of the
|
||||
slc500 / micrologix / plc5 compose profiles) or real SLC / MicroLogix /
|
||||
PLC-5 hardware. Five assertions: probe / driver-loopback / forward-
|
||||
bridge / reverse-bridge / subscribe-sees-change.
|
||||
|
||||
Five assertions: probe / driver-loopback / forward-bridge / reverse-bridge /
|
||||
subscribe-sees-change.
|
||||
ab_server enforces a non-empty CIP routing path (`/1,0`) before the
|
||||
PCCC dispatcher runs; real hardware accepts an empty path. The default
|
||||
$Gateway uses `/1,0` for the Docker fixture — pass `-Gateway
|
||||
"ab://host:44818/"` when pointing at a real SLC 5/05 / MicroLogix /
|
||||
PLC-5.
|
||||
|
||||
.PARAMETER Gateway
|
||||
ab://host[:port]/cip-path. Default ab://127.0.0.1/1,0.
|
||||
ab://host[:port]/cip-path. Default ab://127.0.0.1/1,0 (Docker fixture).
|
||||
|
||||
.PARAMETER PlcType
|
||||
Slc500 / MicroLogix / Plc5 / LogixPccc (default Slc500).
|
||||
@@ -39,13 +42,10 @@ param(
|
||||
$ErrorActionPreference = "Stop"
|
||||
. "$PSScriptRoot/_common.ps1"
|
||||
|
||||
# Skip-gate: the driver CLI's underlying AbLegacyServerFixture-equivalent
|
||||
# check — operators point at real hardware by setting AB_LEGACY_TRUST_WIRE=1.
|
||||
# Without the opt-in we skip (don't run against the known-broken ab_server).
|
||||
if (-not ($env:AB_LEGACY_TRUST_WIRE -eq "1" -or $env:AB_LEGACY_TRUST_WIRE -eq "true")) {
|
||||
Write-Skip "AB_LEGACY_TRUST_WIRE not set — skipping (ab_server PCCC is upstream-broken; set =1 against real hardware / RSEmulate)."
|
||||
exit 0
|
||||
}
|
||||
# ab_server PCCC works; the earlier "upstream-broken" gate is gone. The only
|
||||
# caveat: libplctag's ab_server rejects empty CIP paths, so $Gateway must
|
||||
# carry a non-empty path segment (default /1,0). Real SLC/PLC-5 hardware
|
||||
# accepts an empty path — use `ab://host:44818/` when pointing at real PLCs.
|
||||
|
||||
$abLegacyCli = Get-CliInvocation `
|
||||
-ProjectFolder "src/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Cli" `
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
-- AB Legacy e2e smoke seed — closes #213 (umbrella #209).
|
||||
--
|
||||
-- Hardware-gated. The ab_server PCCC dispatcher is upstream-broken (task #222
|
||||
-- tracks the lab rig + alternative fixtures), so verifying this seed end-to-end
|
||||
-- requires real SLC 500 / MicroLogix / PLC-5 hardware or an RSEmulate 500
|
||||
-- golden-box. The factory + seed ship here so the wiring is in place the
|
||||
-- moment real hardware becomes available — no server code changes needed.
|
||||
-- Works against the ab_server PCCC Docker fixture (one of the slc500 /
|
||||
-- micrologix / plc5 compose profiles) or real SLC 500 / MicroLogix / PLC-5
|
||||
-- hardware. Default HostAddress below points at the Docker fixture with a
|
||||
-- `/1,0` cip-path; libplctag's ab_server rejects empty paths before routing
|
||||
-- to the PCCC dispatcher. Real hardware uses an empty path — change the
|
||||
-- HostAddress to `ab://<plc-ip>:44818/` (note the trailing slash with nothing
|
||||
-- after) before running the seed for that setup.
|
||||
--
|
||||
-- Usage (once hardware is reachable):
|
||||
-- Usage:
|
||||
-- docker compose -f tests/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.IntegrationTests/Docker/docker-compose.yml --profile slc500 up -d
|
||||
-- sqlcmd -S "localhost,14330" -d OtOpcUaConfig -U sa -P "OtOpcUaDev_2026!" \
|
||||
-- -i scripts/smoke/seed-ablegacy-smoke.sql
|
||||
-- (Update the `HostAddress` below to point at the real gateway first.)
|
||||
|
||||
SET NOCOUNT ON;
|
||||
SET XACT_ABORT ON;
|
||||
@@ -83,7 +85,7 @@ VALUES (@Gen, @DrvId, @ClusterId, @NsId, 'ablegacy-smoke', 'AbLegacy', N'{
|
||||
"TimeoutMs": 2000,
|
||||
"Devices": [
|
||||
{
|
||||
"HostAddress": "ab://192.168.1.10/1,0",
|
||||
"HostAddress": "ab://127.0.0.1:44818/1,0",
|
||||
"PlcFamily": "Slc500",
|
||||
"DeviceName": "slc-500"
|
||||
}
|
||||
@@ -92,7 +94,7 @@ VALUES (@Gen, @DrvId, @ClusterId, @NsId, 'ablegacy-smoke', 'AbLegacy', N'{
|
||||
"Tags": [
|
||||
{
|
||||
"Name": "N7_5",
|
||||
"DeviceHostAddress": "ab://192.168.1.10/1,0",
|
||||
"DeviceHostAddress": "ab://127.0.0.1:44818/1,0",
|
||||
"Address": "N7:5",
|
||||
"DataType": "Int",
|
||||
"Writable": true,
|
||||
@@ -117,6 +119,7 @@ PRINT ' Cluster: ' + @ClusterId;
|
||||
PRINT ' Node: ' + @NodeId;
|
||||
PRINT ' Generation: ' + CONVERT(nvarchar(20), @Gen);
|
||||
PRINT '';
|
||||
PRINT 'NOTE: hardware-gated. ab_server PCCC is upstream-broken (#222). Point the';
|
||||
PRINT ' DriverConfig HostAddress at real SLC / MicroLogix / PLC-5 / RSEmulate';
|
||||
PRINT ' and run the e2e script with AB_LEGACY_TRUST_WIRE=1.';
|
||||
PRINT 'NOTE: default points at the ab_server slc500 Docker fixture with a /1,0';
|
||||
PRINT ' cip-path (required by ab_server). For real SLC/MicroLogix/PLC-5';
|
||||
PRINT ' hardware, edit the DriverConfig HostAddress to end with /<empty>';
|
||||
PRINT ' e.g. "ab://<plc-ip>:44818/" and re-run this seed.';
|
||||
|
||||
@@ -17,8 +17,22 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.IntegrationTests;
|
||||
[Trait("Simulator", "ab_server-PCCC")]
|
||||
public sealed class AbLegacyReadSmokeTests(AbLegacyServerFixture sim)
|
||||
{
|
||||
public static IEnumerable<object[]> Profiles =>
|
||||
KnownProfiles.All.Select(p => new object[] { p });
|
||||
// Only one ab_server container binds :44818 at a time and `--plc=SLC500` only
|
||||
// answers SLC-mode PCCC, etc. When `AB_LEGACY_COMPOSE_PROFILE` is set, the theory
|
||||
// filters to that profile alone so the suite matches the running container. Unset
|
||||
// (the default for real-hardware runs) parameterises across every family the driver
|
||||
// supports.
|
||||
public static IEnumerable<object[]> Profiles
|
||||
{
|
||||
get
|
||||
{
|
||||
var only = Environment.GetEnvironmentVariable("AB_LEGACY_COMPOSE_PROFILE");
|
||||
var profiles = KnownProfiles.All.Where(p =>
|
||||
string.IsNullOrEmpty(only) ||
|
||||
string.Equals(p.ComposeProfile, only, StringComparison.OrdinalIgnoreCase));
|
||||
return profiles.Select(p => new object[] { p });
|
||||
}
|
||||
}
|
||||
|
||||
[AbLegacyTheory]
|
||||
[MemberData(nameof(Profiles))]
|
||||
@@ -26,9 +40,11 @@ public sealed class AbLegacyReadSmokeTests(AbLegacyServerFixture sim)
|
||||
{
|
||||
if (sim.SkipReason is not null) Assert.Skip(sim.SkipReason);
|
||||
|
||||
// PCCC PLCs use empty cip-path, but AbLegacyHostAddress still requires the
|
||||
// /cip-path suffix to parse.
|
||||
var deviceUri = $"ab://{sim.Host}:{sim.Port}/";
|
||||
// PCCC semantics allow an empty cip-path (real SLC/PLC-5 hardware takes nothing
|
||||
// after the `/`), but libplctag's ab_server requires a non-empty path at the
|
||||
// CIP unconnected-send layer before the PCCC dispatcher runs. Default `1,0`
|
||||
// against the Docker fixture; set AB_LEGACY_CIP_PATH= (empty) against real HW.
|
||||
var deviceUri = $"ab://{sim.Host}:{sim.Port}/{sim.CipPath}";
|
||||
await using var drv = new AbLegacyDriver(new AbLegacyDriverOptions
|
||||
{
|
||||
Devices = [new AbLegacyDeviceOptions(deviceUri, profile.Family)],
|
||||
@@ -58,9 +74,21 @@ public sealed class AbLegacyReadSmokeTests(AbLegacyServerFixture sim)
|
||||
{
|
||||
if (sim.SkipReason is not null) Assert.Skip(sim.SkipReason);
|
||||
|
||||
// PCCC PLCs use empty cip-path, but AbLegacyHostAddress still requires the
|
||||
// /cip-path suffix to parse.
|
||||
var deviceUri = $"ab://{sim.Host}:{sim.Port}/";
|
||||
// Skip when the running compose profile isn't SLC500 — ab_server's `--plc=`
|
||||
// flag selects exactly one family per process, so a write against a plc5-mode
|
||||
// container with SLC500 semantics always fails at the wire.
|
||||
var only = Environment.GetEnvironmentVariable("AB_LEGACY_COMPOSE_PROFILE");
|
||||
if (!string.IsNullOrEmpty(only) &&
|
||||
!string.Equals(only, "slc500", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
Assert.Skip($"Test targets the SLC500 compose profile; AB_LEGACY_COMPOSE_PROFILE='{only}'.");
|
||||
}
|
||||
|
||||
// PCCC semantics allow an empty cip-path (real SLC/PLC-5 hardware takes nothing
|
||||
// after the `/`), but libplctag's ab_server requires a non-empty path at the
|
||||
// CIP unconnected-send layer before the PCCC dispatcher runs. Default `1,0`
|
||||
// against the Docker fixture; set AB_LEGACY_CIP_PATH= (empty) against real HW.
|
||||
var deviceUri = $"ab://{sim.Host}:{sim.Port}/{sim.CipPath}";
|
||||
await using var drv = new AbLegacyDriver(new AbLegacyDriverOptions
|
||||
{
|
||||
Devices = [new AbLegacyDeviceOptions(deviceUri, AbLegacyPlcFamily.Slc500)],
|
||||
|
||||
@@ -20,6 +20,12 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.IntegrationTests;
|
||||
/// <item><c>AB_LEGACY_ENDPOINT</c> — <c>host:port</c> of the PCCC-mode simulator.
|
||||
/// Defaults to <c>localhost:44818</c> (EtherNet/IP port; ab_server's PCCC
|
||||
/// emulation exposes PCCC-over-CIP on the same port as CIP itself).</item>
|
||||
/// <item><c>AB_LEGACY_CIP_PATH</c> — routing path appended to the <c>ab://host:port/</c>
|
||||
/// URI. Defaults to <c>1,0</c> (port-1/slot-0 backplane), required by ab_server
|
||||
/// which rejects unconnected Send_RR_Data with an empty path at the CIP layer
|
||||
/// before the PCCC dispatcher runs. Real SLC 5/05 / MicroLogix / PLC-5 hardware
|
||||
/// use an empty path — set <c>AB_LEGACY_CIP_PATH=</c> (empty) when pointing at
|
||||
/// real hardware.</item>
|
||||
/// </list>
|
||||
/// Distinct from <c>AB_SERVER_ENDPOINT</c> used by the AB CIP fixture so both
|
||||
/// can point at different containers simultaneously during a combined test run.
|
||||
@@ -27,24 +33,30 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.IntegrationTests;
|
||||
public sealed class AbLegacyServerFixture : IAsyncLifetime
|
||||
{
|
||||
private const string EndpointEnvVar = "AB_LEGACY_ENDPOINT";
|
||||
|
||||
/// <summary>
|
||||
/// Opt-in flag that promises the endpoint can actually round-trip PCCC reads/writes
|
||||
/// (real SLC 5/05 / MicroLogix 1100/1400 / PLC-5 hardware, or a RSEmulate 500
|
||||
/// golden-box per <c>docs/v2/lmx-followups.md</c>). Without this, the fixture assumes
|
||||
/// the endpoint is libplctag's <c>ab_server --plc=SLC500</c> Docker container — whose
|
||||
/// PCCC dispatcher is a known upstream gap — and skips cleanly rather than failing
|
||||
/// every test with <c>BadCommunicationError</c>.
|
||||
/// </summary>
|
||||
private const string TrustWireEnvVar = "AB_LEGACY_TRUST_WIRE";
|
||||
private const string CipPathEnvVar = "AB_LEGACY_CIP_PATH";
|
||||
|
||||
/// <summary>Standard EtherNet/IP port. PCCC-over-CIP rides on the same port as
|
||||
/// native CIP; the differentiator is the <c>--plc</c> flag ab_server was started
|
||||
/// with, not a different TCP listener.</summary>
|
||||
public const int DefaultPort = 44818;
|
||||
|
||||
/// <summary>
|
||||
/// ab_server rejects unconnected Send_RR_Data with an empty CIP routing path
|
||||
/// at the CIP layer — the PCCC dispatcher never runs. <c>1,0</c> is the generic
|
||||
/// port-1/slot-0 backplane path; any well-formed path passes the gate. Real
|
||||
/// hardware (SLC 5/05 / MicroLogix / PLC-5) uses an empty path because there's
|
||||
/// no backplane to cross, so point <c>AB_LEGACY_CIP_PATH=</c> (empty) at real
|
||||
/// hardware to exercise the authentic wire semantics.
|
||||
/// </summary>
|
||||
public const string DefaultCipPath = "1,0";
|
||||
|
||||
public string Host { get; } = "127.0.0.1";
|
||||
public int Port { get; } = DefaultPort;
|
||||
|
||||
/// <summary>CIP routing path portion of the device URI (after the <c>/</c> separator).
|
||||
/// May be empty when targeting real hardware; non-empty against ab_server.</summary>
|
||||
public string CipPath { get; } = DefaultCipPath;
|
||||
|
||||
public string? SkipReason { get; }
|
||||
|
||||
public AbLegacyServerFixture()
|
||||
@@ -56,6 +68,11 @@ public sealed class AbLegacyServerFixture : IAsyncLifetime
|
||||
if (parts.Length == 2 && int.TryParse(parts[1], out var p)) Port = p;
|
||||
}
|
||||
|
||||
// Empty override is intentional (real hardware); treat `null` as "not set, use
|
||||
// default" but preserve an explicit empty-string override.
|
||||
var cipOverride = Environment.GetEnvironmentVariable(CipPathEnvVar);
|
||||
if (cipOverride is not null) CipPath = cipOverride;
|
||||
|
||||
SkipReason = ResolveSkipReason(Host, Port);
|
||||
}
|
||||
|
||||
@@ -82,22 +99,6 @@ public sealed class AbLegacyServerFixture : IAsyncLifetime
|
||||
$"Start the Docker container (docker compose -f Docker/docker-compose.yml " +
|
||||
$"--profile slc500 up -d), attach real hardware, or override {EndpointEnvVar}.";
|
||||
}
|
||||
|
||||
// TCP reaches — but is the peer a real PLC (wire-trustworthy) or ab_server's PCCC
|
||||
// mode (dispatcher is upstream-broken, every read surfaces BadCommunicationError)?
|
||||
// We can't detect it at the wire without issuing a full libplctag session, so we
|
||||
// require an explicit opt-in for wire-level runs. See
|
||||
// `tests/.../Docker/README.md` §"Known limitations" for the upstream-tracking pointer.
|
||||
if (Environment.GetEnvironmentVariable(TrustWireEnvVar) is not { Length: > 0 } trust
|
||||
|| !(trust == "1" || string.Equals(trust, "true", StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
return $"AB Legacy endpoint at {host}:{port} is reachable but {TrustWireEnvVar} is not set. " +
|
||||
"ab_server's PCCC dispatcher is a known upstream gap (libplctag/libplctag), so by " +
|
||||
"default the integration suite assumes the simulator is in play and skips. Set " +
|
||||
$"{TrustWireEnvVar}=1 when pointing at real SLC 5/05 / MicroLogix 1100/1400 / PLC-5 " +
|
||||
"hardware or a RSEmulate 500 golden-box.";
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -166,7 +167,7 @@ public sealed class AbLegacyServerCollection : Xunit.ICollectionFixture<AbLegacy
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// <c>[Fact]</c>-equivalent that skips when the PCCC endpoint isn't wire-trustworthy.
|
||||
/// <c>[Fact]</c>-equivalent that skips when the PCCC endpoint isn't reachable.
|
||||
/// See <see cref="AbLegacyServerFixture"/> for the exact skip semantics.
|
||||
/// </summary>
|
||||
public sealed class AbLegacyFactAttribute : FactAttribute
|
||||
@@ -174,11 +175,9 @@ public sealed class AbLegacyFactAttribute : FactAttribute
|
||||
public AbLegacyFactAttribute()
|
||||
{
|
||||
if (!AbLegacyServerFixture.IsServerAvailable())
|
||||
Skip = "AB Legacy PCCC endpoint not wire-trustworthy. Either no simulator is " +
|
||||
"running, or the Docker ab_server is up but AB_LEGACY_TRUST_WIRE is not " +
|
||||
"set (ab_server's PCCC dispatcher is a known upstream gap). Set " +
|
||||
"AB_LEGACY_TRUST_WIRE=1 when pointing AB_LEGACY_ENDPOINT at real hardware " +
|
||||
"or a RSEmulate 500 golden-box.";
|
||||
Skip = "AB Legacy PCCC endpoint not reachable. Start the Docker fixture " +
|
||||
"(docker compose -f Docker/docker-compose.yml --profile slc500 up -d) " +
|
||||
"or point AB_LEGACY_ENDPOINT at real hardware.";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -190,10 +189,8 @@ public sealed class AbLegacyTheoryAttribute : TheoryAttribute
|
||||
public AbLegacyTheoryAttribute()
|
||||
{
|
||||
if (!AbLegacyServerFixture.IsServerAvailable())
|
||||
Skip = "AB Legacy PCCC endpoint not wire-trustworthy. Either no simulator is " +
|
||||
"running, or the Docker ab_server is up but AB_LEGACY_TRUST_WIRE is not " +
|
||||
"set (ab_server's PCCC dispatcher is a known upstream gap). Set " +
|
||||
"AB_LEGACY_TRUST_WIRE=1 when pointing AB_LEGACY_ENDPOINT at real hardware " +
|
||||
"or a RSEmulate 500 golden-box.";
|
||||
Skip = "AB Legacy PCCC endpoint not reachable. Start the Docker fixture " +
|
||||
"(docker compose -f Docker/docker-compose.yml --profile slc500 up -d) " +
|
||||
"or point AB_LEGACY_ENDPOINT at real hardware.";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,31 +52,33 @@ families stop the current service + start another.
|
||||
| Var | Default | Purpose |
|
||||
|---|---|---|
|
||||
| `AB_LEGACY_ENDPOINT` | `localhost:44818` | `host:port` of the PCCC endpoint. |
|
||||
| `AB_LEGACY_TRUST_WIRE` | *unset* | Opt-in promise that the endpoint is a real PLC or RSEmulate 500 golden-box (not ab_server). Required for integration tests to actually run; without it the tests skip with an upstream-gap message even when TCP reaches a listener. See the **Known limitations** section below. |
|
||||
| `AB_LEGACY_CIP_PATH` | `1,0` | CIP routing path portion of the `ab://host:port/<path>` URI. ab_server rejects empty paths at the CIP unconnected-send layer; real SLC/MicroLogix/PLC-5 hardware accepts empty (no backplane). Set to empty (`AB_LEGACY_CIP_PATH=`) when pointing at real hardware. |
|
||||
| `AB_LEGACY_COMPOSE_PROFILE` | *unset* | When set (e.g. `slc500`), the parametric theory filters to that profile. Only one compose container binds `:44818` at a time; set this to the profile currently up so the suite doesn't try to hit e.g. the Slc500 family against the PLC-5 container. Leave unset for real-hardware runs (all 3 families parameterize). |
|
||||
|
||||
## Run the integration tests
|
||||
|
||||
In a separate shell with a container up:
|
||||
In a separate shell with a container up, tell the suite which profile is
|
||||
running so only the matching theory-parameterization executes:
|
||||
|
||||
```powershell
|
||||
cd C:\Users\dohertj2\Desktop\lmxopcua
|
||||
$env:AB_LEGACY_COMPOSE_PROFILE = "slc500" # or "micrologix" / "plc5"
|
||||
dotnet test tests\ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.IntegrationTests
|
||||
```
|
||||
|
||||
Against the Docker ab_server the suite **skips** with a pointer to the
|
||||
upstream gap (see **Known limitations**). Against real SLC / MicroLogix /
|
||||
PLC-5 hardware or a RSEmulate 500 box:
|
||||
Against real SLC / MicroLogix / PLC-5 hardware, set the endpoint + an
|
||||
empty cip-path + leave the profile unset so all 3 parameterizations
|
||||
run (real PLCs answer any valid family):
|
||||
|
||||
```powershell
|
||||
$env:AB_LEGACY_ENDPOINT = "10.0.1.50:44818"
|
||||
$env:AB_LEGACY_TRUST_WIRE = "1"
|
||||
$env:AB_LEGACY_CIP_PATH = "" # empty — real hardware has no backplane
|
||||
dotnet test tests\ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.IntegrationTests
|
||||
```
|
||||
|
||||
`AbLegacyServerFixture` TCP-probes the endpoint at collection init and sets
|
||||
a skip reason that captures **both** cases: unreachable endpoint *and*
|
||||
reachable-but-wire-untrusted. Tests use `[AbLegacyFact]` / `[AbLegacyTheory]`
|
||||
which check the same gate.
|
||||
`AbLegacyServerFixture` TCP-probes the endpoint at collection init and
|
||||
sets a skip reason when the listener isn't reachable. Tests use
|
||||
`[AbLegacyFact]` / `[AbLegacyTheory]` which check the same gate.
|
||||
|
||||
## What each family seeds
|
||||
|
||||
@@ -97,41 +99,31 @@ implies type:
|
||||
|
||||
## Known limitations
|
||||
|
||||
### ab_server PCCC dispatcher (confirmed upstream gap, verified 2026-04-21)
|
||||
### ab_server rejects empty CIP paths
|
||||
|
||||
**ab_server accepts TCP at `:44818` but its PCCC dispatcher is not
|
||||
functional.** Running with `--plc=SLC500 --debug=5` shows no request
|
||||
logs when libplctag issues a read, and every read surfaces as
|
||||
`BadCommunicationError` (libplctag status `0x80050000`). This matches
|
||||
the libplctag docs' description of PCCC support as less-mature than
|
||||
CIP in the bundled `ab_server` tool.
|
||||
libplctag's `ab_server` enforces a non-empty CIP routing path at the
|
||||
unconnected-send layer before forwarding to the PCCC dispatcher; a
|
||||
client-side `ab://host:port/` with nothing after the `/` surfaces as
|
||||
`BadCommunicationError` (`0x80050000`) with no server-side log line.
|
||||
|
||||
**Fixture behavior.** To avoid a loud row of failing tests on the
|
||||
integration host every time someone `docker compose up`s the SLC500
|
||||
profile, `AbLegacyServerFixture` gates on a second env var
|
||||
`AB_LEGACY_TRUST_WIRE`. The matrix:
|
||||
Real SLC/PLC-5 hardware has no backplane routing, so an empty path is
|
||||
how field devices are addressed. The fixture defaults to `/1,0`
|
||||
(port-1/slot-0 — the conventional ControlLogix backplane path) which
|
||||
the ab_server accepts; operators targeting real hardware set
|
||||
`AB_LEGACY_CIP_PATH=` (empty) to exercise authentic wire semantics.
|
||||
|
||||
| Endpoint reachable? | `AB_LEGACY_TRUST_WIRE` set? | Result |
|
||||
|---|---|---|
|
||||
| No | — | Skip ("not reachable") |
|
||||
| Yes | No | **Skip ("ab_server PCCC gap")** |
|
||||
| Yes | `1` or `true` | **Run** |
|
||||
Previous versions of this README described PCCC as "upstream-broken" —
|
||||
the root cause turned out to be the cip-path gate above, not a gap in
|
||||
`pccc.c`. N-file (Int16), F-file (Float32), and L-file (Int32) round-
|
||||
trip cleanly across SLC500, MicroLogix, and PLC-5 modes.
|
||||
|
||||
The test bodies themselves are correct for real hardware — point
|
||||
`AB_LEGACY_ENDPOINT` at a real SLC 5/05 / MicroLogix 1100/1400 /
|
||||
PLC-5, set `AB_LEGACY_TRUST_WIRE=1`, and the smoke tests round-trip
|
||||
cleanly.
|
||||
### Bit-file writes on ab_server
|
||||
|
||||
Resolution paths (pick one):
|
||||
|
||||
1. **File an ab_server bug** in `libplctag/libplctag` to expand PCCC
|
||||
server-side coverage.
|
||||
2. **Golden-box tier** via Rockwell RSEmulate 500 — closer to real
|
||||
firmware, but license-gated + RSLinx-dependent. Set
|
||||
`AB_LEGACY_TRUST_WIRE=1` when the endpoint points at an Emulate
|
||||
box.
|
||||
3. **Lab rig** — used SLC 5/05 / MicroLogix 1100 on a dedicated
|
||||
network (task #222); the authoritative path.
|
||||
`B3:0/5`-style bit-in-boolean writes currently surface `0x803D0000`
|
||||
against `ab_server --plc=SLC500`; bit reads work. Non-blocking for the
|
||||
smoke suite (which targets N-file Int16 + F-file float reads), but
|
||||
bit-write fidelity isn't simulator-verified — route operator-critical
|
||||
bit writes to real hardware or RSEmulate 500 until upstream resolves.
|
||||
|
||||
### Other known gaps (unchanged from ab_server)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user