chore: organize solution into module folders (Core/Server/Drivers/Client/Tooling)
Group all 69 projects into category subfolders under src/ and tests/ so the Rider Solution Explorer mirrors the module structure. Folders: Core, Server, Drivers (with a nested Driver CLIs subfolder), Client, Tooling. - Move every project folder on disk with git mv (history preserved as renames). - Recompute relative paths in 57 .csproj files: cross-category ProjectReferences, the lib/ HintPath+None refs in Driver.Historian.Wonderware, and the external mxaccessgw refs in Driver.Galaxy and its test project. - Rebuild ZB.MOM.WW.OtOpcUa.slnx with nested solution folders. - Re-prefix project paths in functional scripts (e2e, compliance, smoke SQL, integration, install). Build green (0 errors); unit tests pass. Docs left for a separate pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,121 @@
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
||||
using ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.PlcFamilies;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.IntegrationTests;
|
||||
|
||||
/// <summary>
|
||||
/// End-to-end smoke tests against the <c>ab_server</c> PCCC Docker container.
|
||||
/// Promotes the AB Legacy driver from unit-only coverage (<c>FakeAbLegacyTag</c>)
|
||||
/// to wire-level: real libplctag PCCC stack over real TCP against the ab_server
|
||||
/// simulator. Parametrised over all three families (SLC 500 / MicroLogix / PLC-5)
|
||||
/// via <c>[AbLegacyTheory]</c> + <c>[MemberData]</c>.
|
||||
/// </summary>
|
||||
[Collection(AbLegacyServerCollection.Name)]
|
||||
[Trait("Category", "Integration")]
|
||||
[Trait("Simulator", "ab_server-PCCC")]
|
||||
public sealed class AbLegacyReadSmokeTests(AbLegacyServerFixture sim)
|
||||
{
|
||||
// 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))]
|
||||
public async Task Driver_reads_seeded_N_file_from_ab_server_PCCC(AbLegacyServerProfile profile)
|
||||
{
|
||||
if (sim.SkipReason is not null) Assert.Skip(sim.SkipReason);
|
||||
|
||||
// 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)],
|
||||
Tags = [
|
||||
new AbLegacyTagDefinition(
|
||||
Name: "IntCounter",
|
||||
DeviceHostAddress: deviceUri,
|
||||
Address: "N7:0",
|
||||
DataType: AbLegacyDataType.Int),
|
||||
],
|
||||
Timeout = TimeSpan.FromSeconds(5),
|
||||
Probe = new AbLegacyProbeOptions { Enabled = false },
|
||||
}, driverInstanceId: $"ablegacy-smoke-{profile.Family}");
|
||||
|
||||
await drv.InitializeAsync("{}", TestContext.Current.CancellationToken);
|
||||
|
||||
var snapshots = await drv.ReadAsync(
|
||||
["IntCounter"], TestContext.Current.CancellationToken);
|
||||
|
||||
snapshots.Single().StatusCode.ShouldBe(AbLegacyStatusMapper.Good,
|
||||
$"N7:0 read must succeed against the {profile.Family} compose profile");
|
||||
drv.GetHealth().State.ShouldBe(DriverState.Healthy);
|
||||
}
|
||||
|
||||
[AbLegacyFact]
|
||||
public async Task Slc500_write_then_read_round_trip_on_N7_scratch_register()
|
||||
{
|
||||
if (sim.SkipReason is not null) Assert.Skip(sim.SkipReason);
|
||||
|
||||
// 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)],
|
||||
Tags = [
|
||||
new AbLegacyTagDefinition(
|
||||
Name: "Scratch",
|
||||
DeviceHostAddress: deviceUri,
|
||||
Address: "N7:5",
|
||||
DataType: AbLegacyDataType.Int,
|
||||
Writable: true),
|
||||
],
|
||||
Timeout = TimeSpan.FromSeconds(5),
|
||||
Probe = new AbLegacyProbeOptions { Enabled = false },
|
||||
}, driverInstanceId: "ablegacy-smoke-rw");
|
||||
|
||||
await drv.InitializeAsync("{}", TestContext.Current.CancellationToken);
|
||||
|
||||
const short probe = 0x1234;
|
||||
var writeResults = await drv.WriteAsync(
|
||||
[new WriteRequest("Scratch", probe)],
|
||||
TestContext.Current.CancellationToken);
|
||||
writeResults.Single().StatusCode.ShouldBe(AbLegacyStatusMapper.Good,
|
||||
"PCCC N7:5 write must succeed end-to-end");
|
||||
|
||||
var readResults = await drv.ReadAsync(
|
||||
["Scratch"], TestContext.Current.CancellationToken);
|
||||
readResults.Single().StatusCode.ShouldBe(AbLegacyStatusMapper.Good);
|
||||
Convert.ToInt32(readResults.Single().Value).ShouldBe(probe);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
using System.Net.Sockets;
|
||||
using Xunit;
|
||||
using Xunit.Sdk;
|
||||
using ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.PlcFamilies;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.IntegrationTests;
|
||||
|
||||
/// <summary>
|
||||
/// Reachability probe for the <c>ab_server</c> Docker container running in a PCCC
|
||||
/// plc mode (<c>SLC500</c> / <c>Micrologix</c> / <c>PLC/5</c>). Same container image
|
||||
/// the AB CIP integration suite uses — libplctag's <c>ab_server</c> supports both
|
||||
/// CIP + PCCC families from one binary. Tests skip via
|
||||
/// <see cref="AbLegacyFactAttribute"/> / <see cref="AbLegacyTheoryAttribute"/> when
|
||||
/// the port isn't live, so <c>dotnet test</c> stays green on a fresh clone without
|
||||
/// Docker running.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Env-var overrides:
|
||||
/// <list type="bullet">
|
||||
/// <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.
|
||||
/// </remarks>
|
||||
public sealed class AbLegacyServerFixture : IAsyncLifetime
|
||||
{
|
||||
private const string EndpointEnvVar = "AB_LEGACY_ENDPOINT";
|
||||
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()
|
||||
{
|
||||
if (Environment.GetEnvironmentVariable(EndpointEnvVar) is { Length: > 0 } raw)
|
||||
{
|
||||
var parts = raw.Split(':', 2);
|
||||
Host = parts[0];
|
||||
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);
|
||||
}
|
||||
|
||||
public ValueTask InitializeAsync() => ValueTask.CompletedTask;
|
||||
public ValueTask DisposeAsync() => ValueTask.CompletedTask;
|
||||
|
||||
/// <summary>
|
||||
/// Used by <see cref="AbLegacyFactAttribute"/> + <see cref="AbLegacyTheoryAttribute"/>
|
||||
/// during test-class construction — gates whether the test runs at all. Duplicates the
|
||||
/// fixture logic because attribute ctors fire before the collection fixture instance
|
||||
/// exists.
|
||||
/// </summary>
|
||||
public static bool IsServerAvailable()
|
||||
{
|
||||
var (host, port) = ResolveEndpoint();
|
||||
return ResolveSkipReason(host, port) is null;
|
||||
}
|
||||
|
||||
private static string? ResolveSkipReason(string host, int port)
|
||||
{
|
||||
if (!TcpProbe(host, port))
|
||||
{
|
||||
return $"AB Legacy PCCC endpoint at {host}:{port} not reachable within 2 s. " +
|
||||
$"Start the Docker container (docker compose -f Docker/docker-compose.yml " +
|
||||
$"--profile slc500 up -d), attach real hardware, or override {EndpointEnvVar}.";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static (string Host, int Port) ResolveEndpoint()
|
||||
{
|
||||
var raw = Environment.GetEnvironmentVariable(EndpointEnvVar);
|
||||
if (raw is null) return ("127.0.0.1", DefaultPort);
|
||||
var parts = raw.Split(':', 2);
|
||||
var port = parts.Length == 2 && int.TryParse(parts[1], out var p) ? p : DefaultPort;
|
||||
return (parts[0], port);
|
||||
}
|
||||
|
||||
private static bool TcpProbe(string host, int port)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var client = new TcpClient();
|
||||
var task = client.ConnectAsync(host, port);
|
||||
return task.Wait(TimeSpan.FromSeconds(2)) && client.Connected;
|
||||
}
|
||||
catch { return false; }
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Per-family marker for the PCCC-mode compose profile a given test targets. The
|
||||
/// compose file (<c>Docker/docker-compose.yml</c>) is the canonical source of truth
|
||||
/// for which <c>--plc</c> mode + tags each family seeds; this record just ties a
|
||||
/// family enum to its compose-profile name + operator-facing notes.
|
||||
/// </summary>
|
||||
public sealed record AbLegacyServerProfile(
|
||||
AbLegacyPlcFamily Family,
|
||||
string ComposeProfile,
|
||||
string Notes);
|
||||
|
||||
/// <summary>Canonical profiles covering every PCCC family the driver supports.</summary>
|
||||
public static class KnownProfiles
|
||||
{
|
||||
public static readonly AbLegacyServerProfile Slc500 = new(
|
||||
Family: AbLegacyPlcFamily.Slc500,
|
||||
ComposeProfile: "slc500",
|
||||
Notes: "SLC 500 / 5/05 family. ab_server SLC500 mode covers N/F/B/L files.");
|
||||
|
||||
public static readonly AbLegacyServerProfile MicroLogix = new(
|
||||
Family: AbLegacyPlcFamily.MicroLogix,
|
||||
ComposeProfile: "micrologix",
|
||||
Notes: "MicroLogix 1000 / 1100 / 1400. Shares N/F/B file-type coverage with SLC500; ST (ASCII strings) included.");
|
||||
|
||||
public static readonly AbLegacyServerProfile Plc5 = new(
|
||||
Family: AbLegacyPlcFamily.Plc5,
|
||||
ComposeProfile: "plc5",
|
||||
Notes: "PLC-5 family. ab_server PLC/5 mode covers N/F/B; per-family quirks on ST / timer file layouts unit-tested only.");
|
||||
|
||||
public static IReadOnlyList<AbLegacyServerProfile> All { get; } =
|
||||
[Slc500, MicroLogix, Plc5];
|
||||
|
||||
public static AbLegacyServerProfile ForFamily(AbLegacyPlcFamily family) =>
|
||||
All.FirstOrDefault(p => p.Family == family)
|
||||
?? throw new ArgumentOutOfRangeException(nameof(family), family, "No integration profile for this family.");
|
||||
}
|
||||
|
||||
[Xunit.CollectionDefinition(Name)]
|
||||
public sealed class AbLegacyServerCollection : Xunit.ICollectionFixture<AbLegacyServerFixture>
|
||||
{
|
||||
public const string Name = "AbLegacyServer";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// <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
|
||||
{
|
||||
public AbLegacyFactAttribute()
|
||||
{
|
||||
if (!AbLegacyServerFixture.IsServerAvailable())
|
||||
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.";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// <c>[Theory]</c>-equivalent with the same gate as <see cref="AbLegacyFactAttribute"/>.
|
||||
/// </summary>
|
||||
public sealed class AbLegacyTheoryAttribute : TheoryAttribute
|
||||
{
|
||||
public AbLegacyTheoryAttribute()
|
||||
{
|
||||
if (!AbLegacyServerFixture.IsServerAvailable())
|
||||
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.";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
# AB Legacy PCCC integration-test fixture — `ab_server` (Docker)
|
||||
|
||||
[libplctag](https://github.com/libplctag/libplctag)'s `ab_server` supports
|
||||
both CIP (ControlLogix / CompactLogix / Micro800) and PCCC (SLC 500 /
|
||||
MicroLogix / PLC-5) families from one binary. This fixture reuses the AB
|
||||
CIP Docker image (`otopcua-ab-server:libplctag-release`) with different
|
||||
`--plc` flags. No new Dockerfile needed — the compose file's `build:`
|
||||
block points at the AB CIP `Docker/` folder so `docker compose build`
|
||||
from here reuses the same multi-stage build.
|
||||
|
||||
**Docker is the only supported launch path**; a fresh clone needs Docker
|
||||
Desktop and nothing else.
|
||||
|
||||
| File | Purpose |
|
||||
|---|---|
|
||||
| [`docker-compose.yml`](docker-compose.yml) | Three per-family services (`slc500` / `micrologix` / `plc5`); all bind `:44818` |
|
||||
|
||||
## Run
|
||||
|
||||
From the repo root:
|
||||
|
||||
```powershell
|
||||
# SLC 500 family — widest PCCC coverage
|
||||
docker compose -f tests\ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.IntegrationTests\Docker\docker-compose.yml --profile slc500 up
|
||||
|
||||
# Per-family
|
||||
docker compose -f tests\...\Docker\docker-compose.yml --profile micrologix up
|
||||
docker compose -f tests\...\Docker\docker-compose.yml --profile plc5 up
|
||||
```
|
||||
|
||||
Detached + stop:
|
||||
|
||||
```powershell
|
||||
docker compose -f tests\...\Docker\docker-compose.yml --profile slc500 up -d
|
||||
docker compose -f tests\...\Docker\docker-compose.yml --profile slc500 down
|
||||
```
|
||||
|
||||
First run builds the `otopcua-ab-server:libplctag-release` image (~3-5
|
||||
min — clones libplctag + compiles `ab_server`). If the AB CIP fixture
|
||||
already built the image locally, docker reuses the cached layers + this
|
||||
runs in seconds. Only one family binds `:44818` at a time; to switch
|
||||
families stop the current service + start another.
|
||||
|
||||
## Endpoint
|
||||
|
||||
- Default: `localhost:44818` (EtherNet/IP standard)
|
||||
- Override with `AB_LEGACY_ENDPOINT=host:port` to point at a real SLC /
|
||||
MicroLogix / PLC-5 PLC on its native port.
|
||||
|
||||
## Env vars
|
||||
|
||||
| Var | Default | Purpose |
|
||||
|---|---|---|
|
||||
| `AB_LEGACY_ENDPOINT` | `localhost:44818` | `host:port` of the PCCC endpoint. |
|
||||
| `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, 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 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_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 when the listener isn't reachable. Tests use
|
||||
`[AbLegacyFact]` / `[AbLegacyTheory]` which check the same gate.
|
||||
|
||||
## What each family seeds
|
||||
|
||||
PCCC tag format is `<file>[<size>]` without a type suffix — file letter
|
||||
implies type:
|
||||
|
||||
- `N` = 16-bit signed integer
|
||||
- `F` = 32-bit IEEE 754 float
|
||||
- `B` = 1-bit boolean (stored as uint16, bit-addressable via `/n`)
|
||||
- `L` = 32-bit signed integer (SLC 5/05 V15+ only)
|
||||
- `ST` = 82-byte ASCII string (MicroLogix-specific extension)
|
||||
|
||||
| Family | Seeded tags | Notes |
|
||||
|---|---|---|
|
||||
| SLC 500 | `N7[10]`, `F8[10]`, `B3[10]`, `L19[10]` | Baseline; covers the four numeric file types a typical SLC project uses |
|
||||
| MicroLogix | `B3[10]`, `N7[10]`, `L19[10]` | No `F8` — MicroLogix 1000 has no float file; use L19 when scaled integers aren't enough |
|
||||
| PLC-5 | `N7[10]`, `F8[10]`, `B3[10]` | No `L` — PLC-5 predates the L file type; DINT equivalents went in integer files |
|
||||
|
||||
## Known limitations
|
||||
|
||||
### ab_server rejects empty CIP paths
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
### Bit-file writes on ab_server
|
||||
|
||||
`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)
|
||||
|
||||
- **Timer / Counter file decomposition** — PCCC T4 / C5 files contain
|
||||
three-field structs (`.ACC` / `.PRE` / `.DN`). Not in ab_server's
|
||||
scope; tests targeting `T4:0.ACC` stay unit-only.
|
||||
- **ST (ASCII string) files** — real MicroLogix ST files have a length
|
||||
field plus CRLF-sensitive semantics that don't round-trip cleanly.
|
||||
- **Indirect addressing** (`N7:[N10:5]`) — not in ab_server's scope.
|
||||
- **DF1 serial wire behaviour** — the whole ab_server path is TCP;
|
||||
DF1 radio / serial fidelity needs real hardware.
|
||||
|
||||
See [`docs/drivers/AbLegacy-Test-Fixture.md`](../../../docs/drivers/AbLegacy-Test-Fixture.md)
|
||||
for the full coverage map.
|
||||
|
||||
## References
|
||||
|
||||
- [libplctag on GitHub](https://github.com/libplctag/libplctag) — `ab_server`
|
||||
lives under `src/tools/ab_server/`
|
||||
- [`docs/drivers/AbLegacy-Test-Fixture.md`](../../../docs/drivers/AbLegacy-Test-Fixture.md)
|
||||
— coverage map + gap inventory
|
||||
- [`docs/v2/dev-environment.md`](../../../docs/v2/dev-environment.md)
|
||||
§Docker fixtures — full fixture inventory
|
||||
- [`../../ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests/Docker/`](../../ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests/Docker/)
|
||||
— the shared Dockerfile this compose file's `build:` block references
|
||||
@@ -0,0 +1,74 @@
|
||||
# AB Legacy PCCC integration-test fixture — ab_server in PCCC mode.
|
||||
#
|
||||
# Same image as the AB CIP fixture (otopcua-ab-server:libplctag-release).
|
||||
# The build context points at the AB CIP Docker folder one directory over
|
||||
# so `docker compose build` from here produces the same image if it
|
||||
# doesn't already exist; if it does, docker's cache reuses the layer.
|
||||
#
|
||||
# One service per PCCC family. All bind :44818 on the host; run one at a
|
||||
# time. PCCC tag format differs from CIP: `<file>[<size>]` without a
|
||||
# type suffix since the type is implicit in the file letter (N = INT,
|
||||
# F = REAL, B = bit-packed, L = DINT).
|
||||
#
|
||||
# Usage:
|
||||
# docker compose --profile slc500 up
|
||||
# docker compose --profile micrologix up
|
||||
# docker compose --profile plc5 up
|
||||
services:
|
||||
slc500:
|
||||
profiles: ["slc500"]
|
||||
build:
|
||||
context: ../../ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests/Docker
|
||||
dockerfile: Dockerfile
|
||||
image: otopcua-ab-server:libplctag-release
|
||||
container_name: otopcua-ab-server-slc500
|
||||
restart: "no"
|
||||
ports:
|
||||
- "44818:44818"
|
||||
command: [
|
||||
"ab_server",
|
||||
"--plc=SLC500",
|
||||
"--port=44818",
|
||||
"--tag=N7[10]",
|
||||
"--tag=F8[10]",
|
||||
"--tag=B3[10]",
|
||||
"--tag=L19[10]"
|
||||
]
|
||||
|
||||
micrologix:
|
||||
profiles: ["micrologix"]
|
||||
image: otopcua-ab-server:libplctag-release
|
||||
build:
|
||||
context: ../../ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests/Docker
|
||||
dockerfile: Dockerfile
|
||||
container_name: otopcua-ab-server-micrologix
|
||||
restart: "no"
|
||||
ports:
|
||||
- "44818:44818"
|
||||
command: [
|
||||
"ab_server",
|
||||
"--plc=Micrologix",
|
||||
"--port=44818",
|
||||
"--tag=B3[10]",
|
||||
"--tag=N7[10]",
|
||||
"--tag=L19[10]"
|
||||
]
|
||||
|
||||
plc5:
|
||||
profiles: ["plc5"]
|
||||
image: otopcua-ab-server:libplctag-release
|
||||
build:
|
||||
context: ../../ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests/Docker
|
||||
dockerfile: Dockerfile
|
||||
container_name: otopcua-ab-server-plc5
|
||||
restart: "no"
|
||||
ports:
|
||||
- "44818:44818"
|
||||
command: [
|
||||
"ab_server",
|
||||
"--plc=PLC/5",
|
||||
"--port=44818",
|
||||
"--tag=N7[10]",
|
||||
"--tag=F8[10]",
|
||||
"--tag=B3[10]"
|
||||
]
|
||||
@@ -0,0 +1,35 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<IsPackable>false</IsPackable>
|
||||
<IsTestProject>true</IsTestProject>
|
||||
<RootNamespace>ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.IntegrationTests</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="xunit.v3" Version="1.1.0"/>
|
||||
<PackageReference Include="Shouldly" Version="4.3.0"/>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0"/>
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="3.0.2">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\src\Drivers\ZB.MOM.WW.OtOpcUa.Driver.AbLegacy\ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.csproj"/>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Update="Docker\**\*" CopyToOutputDirectory="PreserveNewest"/>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<NuGetAuditSuppress Include="https://github.com/advisories/GHSA-37gx-xxp4-5rgx"/>
|
||||
<NuGetAuditSuppress Include="https://github.com/advisories/GHSA-w3x6-4m5h-cxqf"/>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
Reference in New Issue
Block a user