diff --git a/docs/drivers/AbServer-Test-Fixture.md b/docs/drivers/AbServer-Test-Fixture.md index d0530fa..a656d3c 100644 --- a/docs/drivers/AbServer-Test-Fixture.md +++ b/docs/drivers/AbServer-Test-Fixture.md @@ -116,38 +116,101 @@ No smoke test for: The driver implements all of these + they have unit coverage, but the only end-to-end path `ab_server` validates today is atomic `ReadAsync`. +## Logix Emulate golden-box tier + +Rockwell Studio 5000 Logix Emulate sits **above** ab_server in fidelity + +**below** real hardware. When an operator has Emulate running on a +reachable Windows box + sets two env vars, the suite promotes several +behaviours from unit-only to end-to-end wire-level coverage: + +```powershell +$env:AB_SERVER_PROFILE = 'emulate' +$env:AB_SERVER_ENDPOINT = ':44818' +dotnet test tests\ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests +``` + +With `AB_SERVER_PROFILE` unset or `abserver`, the Emulate-tier classes +skip cleanly + the ab_server Docker fixture runs as usual. + +| Gap this fixture doc calls out | ab_server | Logix Emulate | Real hardware | +|---|---|---|---| +| UDT / CIP Template Object (task #194) | no | **yes** | yes | +| ALMD alarm projection (task #177) | no | **yes** | yes | +| `@tags` Symbol Object walk with `Program:` scope | partial | **yes** | yes | +| Add-On Instructions | no | **yes** | yes | +| GuardLogix safety-partition write rejection | no | **yes** (Emulate 5580) | yes | +| CompactLogix narrow ConnectionSize enforcement | no | **yes** (5370 firmware) | yes | +| EtherNet/IP embedded-switch behaviour | no | no | yes | +| Redundant chassis failover (1756-RM) | no | no | yes | +| Motion control timing | no | no | yes | + +**Tests that promote to Emulate** (gated on `AB_SERVER_PROFILE=emulate` +via `AbServerProfileGate.SkipUnless`): + +- `AbCipEmulateUdtReadTests.WholeUdt_read_decodes_each_member_at_its_Template_Object_offset` + — #194 whole-UDT optimization, verified against real Template Object + bytes +- `AbCipEmulateAlmdTests.Real_ALMD_raise_fires_OnAlarmEvent_through_the_driver_projection` + — #177 ALMD projection, verified against the real ALMD instruction + +**Required Studio 5000 project state** is documented in +[`tests/…/AbCip.IntegrationTests/LogixProject/README.md`](../../tests/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests/LogixProject/README.md); +the `.L5X` export lands there once the Emulate PC is on-site + the +project is authored. + +**Costs to accept**: + +- **Rockwell TechConnect or per-seat license** — not redistributable; + not CI-runnable. Each operator licenses their own Emulate install. +- **Windows-only + Hyper-V conflict** — Emulate can't coexist with + Docker Desktop's WSL 2 backend on the same OS, same way TwinCAT XAR + can't (see `docs/v2/dev-environment.md` §Integration host). +- **Manual lifecycle** — no `docker compose up` equivalent; operator + opens Emulate, loads the L5X, clicks Run. The L5X in the repo keeps + project state reproducible, runtime-start is human. + ## When to trust ab_server, when to reach for a rig -| Question | ab_server | Unit tests | Lab rig | -| --- | --- | --- | --- | -| "Does the driver talk CIP at all?" | yes | - | - | -| "Is my atomic read path wired correctly?" | yes | yes | yes | -| "Does whole-UDT grouping work?" | no | yes | yes (required) | -| "Do ALMD alarms raise + clear?" | no | yes | yes (required) | -| "Is Micro800 unconnected-only enforced wire-side?" | no (emulated as CLX) | partial | yes (required) | -| "Does GuardLogix reject non-safety writes on safety tags?" | no | no | yes (required) | -| "Does CompactLogix refuse oversized ConnectionSize?" | no | partial | yes (required) | -| "Does BOOL-in-DINT RMW race against concurrent writers?" | no | yes | yes (stress) | +| Question | ab_server | Unit tests | Logix Emulate | Lab rig | +| --- | --- | --- | --- | --- | +| "Does the driver talk CIP at all?" | yes | - | yes | - | +| "Is my atomic read path wired correctly?" | yes | yes | yes | yes | +| "Does whole-UDT grouping work?" | no | yes | **yes** | yes | +| "Do ALMD alarms raise + clear?" | no | yes | **yes** | yes | +| "Is Micro800 unconnected-only enforced wire-side?" | no (emulated as CLX) | partial | yes | yes (required) | +| "Does GuardLogix reject non-safety writes on safety tags?" | no | no | yes (Emulate 5580) | yes | +| "Does CompactLogix refuse oversized ConnectionSize?" | no | partial | yes (5370 firmware) | yes | +| "Does BOOL-in-DINT RMW race against concurrent writers?" | no | yes | partial | yes (stress) | +| "Does EtherNet/IP embedded-switch behave correctly?" | no | no | no | yes (required) | +| "Does redundant-chassis failover work?" | no | no | no | yes (required) | ## Follow-up candidates If integration-level UDT / alarm / quirk proof becomes a shipping gate, the options are roughly: -1. **Extend `ab_server`** upstream — the project accepts PRs + already carries - a CIP framing layer that UDT emulation could plug into. -2. **Swap in a richer simulator** — e.g. - [OpenPLC](https://www.openplcproject.com/) or pycomm3's test harness, if - either exposes UDTs over EtherNet/IP in a way libplctag can consume. -3. **Stand up a lab rig** — physical `1756-L7x` / `5069-L3x` / `2080-LC30` / - `1756-L8xS` controllers running Rockwell Studio 5000 projects; wire into - CI via a self-hosted runner. The only path that covers safety + narrow - ConnectionSize + real ALMD execution. +1. **Logix Emulate golden-box tier** (scaffolded; see the section above) — + highest-fidelity path short of real hardware. Closes UDT / ALMD / AOI / + optimized-DB gaps in one license + one Windows PC. +2. **Extend `ab_server`** upstream — the project accepts PRs + already + carries a CIP framing layer that UDT emulation could plug into. +3. **Stand up a lab rig** — physical `1756-L7x` / `5069-L3x` / `2080-LC30` + / `1756-L8xS` controllers. The only path that covers safety partitions + across nodes, redundant chassis, embedded-switch behaviour, and motion + timing. See also: - `tests/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests/AbServerFixture.cs` - `tests/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests/AbServerProfile.cs` +- `tests/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests/AbServerProfileGate.cs` + — `AB_SERVER_PROFILE` tier gate - `tests/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests/AbCipReadSmokeTests.cs` +- `tests/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests/Docker/` — ab_server + image + compose +- `tests/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests/Emulate/` — Logix + Emulate tier tests +- `tests/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests/LogixProject/README.md` + — L5X project state the Emulate tier expects - `docs/v2/test-data-sources.md` §2 — the broader test-data-source picking rationale this fixture slots into diff --git a/docs/v2/dev-environment.md b/docs/v2/dev-environment.md index 327565c..8b6d67b 100644 --- a/docs/v2/dev-environment.md +++ b/docs/v2/dev-environment.md @@ -149,6 +149,7 @@ Dev credentials in this inventory are convenience defaults, not secrets. Change | **S7 fixture — `otopcua-python-snap7:1.0`** | S7 driver integration tests | Docker image (local build, `python-snap7>=2.0`; see `tests/ZB.MOM.WW.OtOpcUa.Driver.S7.IntegrationTests/Docker/`); 1 compose profile: `s7_1500` | 1102 (non-privileged; driver honours `S7DriverOptions.Port`) | n/a | Developer (per machine) | | **OPC UA Client fixture — `mcr.microsoft.com/iotedge/opc-plc:2.14.10`** | OpcUaClient driver integration tests | Docker image (Microsoft-maintained, pinned; see `tests/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.IntegrationTests/Docker/`) | 50000 (OPC UA) | Anonymous (`--daa` off); auto-accept certs (`--aa`) | Developer (per machine) | | **TwinCAT XAR runtime VM** | TwinCAT ADS testing (per `test-data-sources.md` §5; Beckhoff XAR cannot coexist with Hyper-V on the same OS) | Hyper-V VM with Windows + TwinCAT XAR installed under 7-day renewable trial | 48898 (ADS over TCP) | TwinCAT default route credentials configured per Beckhoff docs | Integration host admin | +| **Rockwell Studio 5000 Logix Emulate** | AB CIP golden-box tier — closes UDT / ALMD / AOI / GuardLogix-safety / CompactLogix-ConnectionSize gaps the ab_server simulator can't cover. Loads the L5X project documented at `tests/.../AbCip.IntegrationTests/LogixProject/README.md`. Tests gated on `AB_SERVER_PROFILE=emulate` + `AB_SERVER_ENDPOINT=:44818`; see `docs/drivers/AbServer-Test-Fixture.md` §Logix Emulate golden-box tier | Windows-only install; **Hyper-V conflict** — can't coexist with Docker Desktop's WSL 2 backend on the same OS, same story as TwinCAT XAR. Runs on a dedicated Windows PC reachable on the LAN | 44818 (CIP / EtherNet/IP) | None required at the CIP layer; Studio 5000 project credentials per Rockwell install | Integration host admin (license + install); Developer (per session — open Emulate, load L5X, click Run) | | **FOCAS TCP stub** (`Driver.Focas.TestStub`) | FOCAS functional testing (per `test-data-sources.md` §6) | Local .NET 10 console app from this repo | 8193 (FOCAS) | n/a | Developer / integration host (run on demand) | | **FOCAS FaultShim** (`Driver.Focas.FaultShim`) | FOCAS native-fault injection (per `test-data-sources.md` §6) | Test-only native DLL named `Fwlib64.dll`, loaded via DLL search path in the test fixture | n/a (in-process) | n/a | Developer / integration host (test-only) | diff --git a/tests/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests/AbServerProfileGate.cs b/tests/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests/AbServerProfileGate.cs new file mode 100644 index 0000000..e4ac2dc --- /dev/null +++ b/tests/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests/AbServerProfileGate.cs @@ -0,0 +1,52 @@ +using Xunit; + +namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests; + +/// +/// Runtime gate that lets an integration-test class declare which target-server tier +/// it requires. Reads AB_SERVER_PROFILE from the environment; tests call +/// with the profile names they support + skip otherwise. +/// +/// +/// Two tiers today: +/// +/// abserver (default) — the Dockerized libplctag ab_server +/// simulator. Covers atomic reads / writes / basic discovery across the four +/// families (ControlLogix / CompactLogix / Micro800 / GuardLogix). +/// emulate — Rockwell Studio 5000 Logix Emulate on an operator's +/// Windows box, exposed via AB_SERVER_ENDPOINT. Adds real UDT / ALMD / +/// AOI / Program-scoped-tag coverage that ab_server can't emulate. Tier-gated +/// because Emulate is per-seat licensed + Windows-only + manually launched; +/// a stock `dotnet test` run against ab_server must skip Emulate-only classes +/// cleanly. +/// +/// Tests assert their target tier at the top of each [Fact] / +/// [Theory] body, mirroring the MODBUS_SIM_PROFILE gate pattern in +/// tests/.../Modbus.IntegrationTests/DL205/DL205StringQuirkTests.cs. +/// +public static class AbServerProfileGate +{ + public const string Default = "abserver"; + public const string Emulate = "emulate"; + + /// Active profile from AB_SERVER_PROFILE; defaults to . + public static string CurrentProfile => + Environment.GetEnvironmentVariable("AB_SERVER_PROFILE") is { Length: > 0 } raw + ? raw.Trim().ToLowerInvariant() + : Default; + + /// + /// Skip the calling test via Assert.Skip when + /// isn't in . Case-insensitive match. + /// + public static void SkipUnless(params string[] requiredProfiles) + { + foreach (var p in requiredProfiles) + if (string.Equals(p, CurrentProfile, StringComparison.OrdinalIgnoreCase)) + return; + Assert.Skip( + $"Test requires AB_SERVER_PROFILE in {{{string.Join(", ", requiredProfiles)}}}; " + + $"current value is '{CurrentProfile}'. " + + $"Set AB_SERVER_PROFILE=emulate + point AB_SERVER_ENDPOINT at a Logix Emulate instance to run the golden-box-tier tests."); + } +} diff --git a/tests/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests/Emulate/AbCipEmulateAlmdTests.cs b/tests/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests/Emulate/AbCipEmulateAlmdTests.cs new file mode 100644 index 0000000..0624b88 --- /dev/null +++ b/tests/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests/Emulate/AbCipEmulateAlmdTests.cs @@ -0,0 +1,105 @@ +using Shouldly; +using Xunit; +using ZB.MOM.WW.OtOpcUa.Core.Abstractions; +using ZB.MOM.WW.OtOpcUa.Driver.AbCip; + +namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests.Emulate; + +/// +/// Golden-box-tier ALMD alarm projection tests against Logix Emulate. +/// Promotes the feature-flagged ALMD projection (task #177) from unit-only coverage +/// (AbCipAlarmProjectionTests with faked InFaulted sequences) to end-to-end +/// wire-level coverage — Emulate runs the real ALMD instruction, with real +/// rising-edge semantics on InFaulted + Ack, so the driver's poll-based +/// projection gets validated against the actual behaviour shops running FT View see. +/// +/// +/// Required Emulate project state (see LogixProject/README.md): +/// +/// Controller-scope ALMD tag HighTempAlarm — a standard ALMD instruction +/// with default member set (In, InFaulted, Acked, +/// Severity, Cfg_ProgTime, …). +/// A periodic task that drives HighTempAlarm.In false→true→false at a +/// cadence the operator can script via a one-shot routine (e.g. a +/// SimulateAlarm bit the test case pulses through +/// IWritable.WriteAsync). +/// Operator writes 1 to SimulateAlarm to trigger the rising +/// edge on HighTempAlarm.In; ladder uses that as the alarm input. +/// +/// Runs only when AB_SERVER_PROFILE=emulate. ab_server has no ALMD +/// instruction + no alarm subsystem, so this tier-gated class couldn't produce a +/// meaningful result against the default simulator. +/// +[Collection("AbServerEmulate")] +[Trait("Category", "Integration")] +[Trait("Tier", "Emulate")] +public sealed class AbCipEmulateAlmdTests +{ + [AbServerFact] + public async Task Real_ALMD_raise_fires_OnAlarmEvent_through_the_driver_projection() + { + AbServerProfileGate.SkipUnless(AbServerProfileGate.Emulate); + + var endpoint = Environment.GetEnvironmentVariable("AB_SERVER_ENDPOINT") + ?? throw new InvalidOperationException( + "AB_SERVER_ENDPOINT must be set to the Logix Emulate instance when AB_SERVER_PROFILE=emulate."); + + var options = new AbCipDriverOptions + { + Devices = [new AbCipDeviceOptions($"ab://{endpoint}/1,0")], + EnableAlarmProjection = true, + AlarmPollInterval = TimeSpan.FromMilliseconds(200), + Tags = [ + new AbCipTagDefinition( + Name: "HighTempAlarm", + DeviceHostAddress: $"ab://{endpoint}/1,0", + TagPath: "HighTempAlarm", + DataType: AbCipDataType.Structure, + Members: [ + new AbCipStructureMember("InFaulted", AbCipDataType.DInt), + new AbCipStructureMember("Acked", AbCipDataType.DInt), + new AbCipStructureMember("Severity", AbCipDataType.DInt), + new AbCipStructureMember("In", AbCipDataType.DInt), + ]), + // The "simulate the alarm input" bit the ladder watches. + new AbCipTagDefinition( + Name: "SimulateAlarm", + DeviceHostAddress: $"ab://{endpoint}/1,0", + TagPath: "SimulateAlarm", + DataType: AbCipDataType.Bool, + Writable: true), + ], + }; + + await using var drv = new AbCipDriver(options, driverInstanceId: "emulate-almd"); + await drv.InitializeAsync("{}", TestContext.Current.CancellationToken); + + var raised = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + drv.OnAlarmEvent += (_, e) => + { + if (e.Message.Contains("raised")) raised.TrySetResult(e); + }; + + var sub = await drv.SubscribeAlarmsAsync( + ["HighTempAlarm"], TestContext.Current.CancellationToken); + + // Pulse the input bit the ladder watches, then wait for the driver's poll loop + // to see InFaulted rise + fire the raise event. + _ = await drv.WriteAsync( + [new WriteRequest("SimulateAlarm", true)], + TestContext.Current.CancellationToken); + + var got = await Task.WhenAny(raised.Task, Task.Delay(TimeSpan.FromSeconds(5))); + got.ShouldBe(raised.Task, "driver must surface the ALMD raise within 5 s of the ladder-driven edge"); + var args = await raised.Task; + args.SourceNodeId.ShouldBe("HighTempAlarm"); + args.AlarmType.ShouldBe("ALMD"); + + await drv.UnsubscribeAlarmsAsync(sub, TestContext.Current.CancellationToken); + + // Reset the bit so the next test run starts from a known state. + _ = await drv.WriteAsync( + [new WriteRequest("SimulateAlarm", false)], + TestContext.Current.CancellationToken); + } +} diff --git a/tests/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests/Emulate/AbCipEmulateUdtReadTests.cs b/tests/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests/Emulate/AbCipEmulateUdtReadTests.cs new file mode 100644 index 0000000..3386629 --- /dev/null +++ b/tests/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests/Emulate/AbCipEmulateUdtReadTests.cs @@ -0,0 +1,81 @@ +using Shouldly; +using Xunit; +using ZB.MOM.WW.OtOpcUa.Core.Abstractions; +using ZB.MOM.WW.OtOpcUa.Driver.AbCip; + +namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests.Emulate; + +/// +/// Golden-box-tier UDT read tests against Rockwell Studio 5000 Logix Emulate. +/// Promotes the whole-UDT-read optimization (task #194) from unit-only coverage +/// (golden Template Object byte buffers + ) +/// to end-to-end wire-level coverage — Emulate's firmware speaks the same CIP +/// Template Object responses real hardware does, so the member-offset math + the +/// AbCipUdtReadPlanner grouping get validated against production semantics. +/// +/// +/// Required Emulate project state (see LogixProject/README.md +/// for the L5X export that seeds this; ship the project once Emulate is on the +/// integration host): +/// +/// UDT Motor_UDT with members Speed : DINT, Torque : REAL, +/// Status : DINT — the member set +/// uses as its declared-layout golden reference. +/// Controller-scope tag Motor1 : Motor_UDT with seed values +/// Speed=1800, Torque=42.5f, Status=0x0001. +/// +/// Runs only when AB_SERVER_PROFILE=emulate. With ab_server +/// (the default), skips cleanly — ab_server lacks UDT / Template Object emulation +/// so this wire-level test couldn't pass against it regardless. +/// +[Collection("AbServerEmulate")] +[Trait("Category", "Integration")] +[Trait("Tier", "Emulate")] +public sealed class AbCipEmulateUdtReadTests +{ + [AbServerFact] + public async Task WholeUdt_read_decodes_each_member_at_its_Template_Object_offset() + { + AbServerProfileGate.SkipUnless(AbServerProfileGate.Emulate); + + var endpoint = Environment.GetEnvironmentVariable("AB_SERVER_ENDPOINT") + ?? throw new InvalidOperationException( + "AB_SERVER_ENDPOINT must be set to the Logix Emulate instance " + + "(e.g. '10.0.0.42:44818') when AB_SERVER_PROFILE=emulate."); + + var options = new AbCipDriverOptions + { + Devices = [new AbCipDeviceOptions($"ab://{endpoint}/1,0")], + Tags = [ + new AbCipTagDefinition( + Name: "Motor1", + DeviceHostAddress: $"ab://{endpoint}/1,0", + TagPath: "Motor1", + DataType: AbCipDataType.Structure, + Members: [ + new AbCipStructureMember("Speed", AbCipDataType.DInt), + new AbCipStructureMember("Torque", AbCipDataType.Real), + new AbCipStructureMember("Status", AbCipDataType.DInt), + ]), + ], + Timeout = TimeSpan.FromSeconds(5), + }; + + await using var drv = new AbCipDriver(options, driverInstanceId: "emulate-udt-smoke"); + await drv.InitializeAsync("{}", TestContext.Current.CancellationToken); + + // Whole-UDT read optimization from task #194: one libplctag read on the + // parent tag, three decodes from the buffer at member offsets. Asserts + // Emulate's Template Object response matches what AbCipUdtMemberLayout + // computes from the declared member set. + var snapshots = await drv.ReadAsync( + ["Motor1.Speed", "Motor1.Torque", "Motor1.Status"], + TestContext.Current.CancellationToken); + + snapshots.Count.ShouldBe(3); + foreach (var s in snapshots) s.StatusCode.ShouldBe(AbCipStatusMapper.Good); + Convert.ToInt32(snapshots[0].Value).ShouldBe(1800); + Convert.ToSingle(snapshots[1].Value).ShouldBe(42.5f, tolerance: 0.001f); + Convert.ToInt32(snapshots[2].Value).ShouldBe(0x0001); + } +} diff --git a/tests/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests/LogixProject/README.md b/tests/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests/LogixProject/README.md new file mode 100644 index 0000000..a7dc6dd --- /dev/null +++ b/tests/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests/LogixProject/README.md @@ -0,0 +1,102 @@ +# Logix Emulate project stub + +This folder holds the Studio 5000 project that Logix Emulate loads when running +the Emulate-tier integration tests +(`tests/.../AbCip.IntegrationTests/Emulate/*.cs`, gated on +`AB_SERVER_PROFILE=emulate`). + +**Status today**: stub. The actual `.L5X` export isn't committed yet; once +the Emulate PC is running + a project with the required state exists, +export to L5X + drop it here as `OtOpcUaAbCipFixture.L5X`. + +## Why L5X, not .ACD + +Studio 5000 ships two save formats: `.ACD` (binary, the runtime project) +and `.L5X` (XML export). Ship the L5X because: + +- Text format — reviewable in PR diffs, diffable in git +- Reproducible import across Studio 5000 versions +- Doesn't carry per-installation state (license watermarks, revision history) + +Reconstruction workflow: Studio 5000 → open project → File → Save As +→ `.L5X`. On a fresh Emulate install: File → Open → select the L5X → it +rebuilds the ACD from the XML. + +## Required project state + +The Emulate-tier tests rely on this exact tag / UDT set. Missing any of +these makes the dependent test fail loudly (TagNotFound, wrong value, +wrong type), not skip silently — Emulate is a tier above the Docker +simulator; operators who opted into it get opt-in-level coverage +expectations. + +### UDT definitions + +| UDT name | Members | Notes | +|---|---|---| +| `Motor_UDT` | `Speed : DINT`, `Torque : REAL`, `Status : DINT` | Matches `AbCipUdtMemberLayoutTests` declared-layout golden. Member order fixed — Logix Template Object offsets depend on it | + +### Controller tags + +| Tag | Type | Seed value | Purpose | +|---|---|---|---| +| `Motor1` | `Motor_UDT` | `{Speed=1800, Torque=42.5, Status=0x0001}` | `AbCipEmulateUdtReadTests.WholeUdt_read_decodes_each_member_at_its_Template_Object_offset` | +| `HighTempAlarm` | `ALMD` | default ALMD config, `In` tied to `SimulateAlarm` bit | `AbCipEmulateAlmdTests.Real_ALMD_raise_fires_OnAlarmEvent_through_the_driver_projection` | +| `SimulateAlarm` | `BOOL` | `0` | Operator-writable bit the ladder routes into `HighTempAlarm.In` — gives the test a clean way to drive the alarm edge without scripting Emulate directly | + +### Program structure + +- One periodic task `MainTask` @ 100 ms +- One program `MainProgram` +- One routine `MainRoutine` (Ladder) with a single rung: + `XIC SimulateAlarm OTE HighTempAlarm.In` + +That's enough ladder for `SimulateAlarm := 1` to raise the alarm + for +`SimulateAlarm := 0` to clear it. + +## Tier-level behaviours this project enables + +Coverage the existing Dockerized `ab_server` fixture can't produce — +each verified by an `Emulate/*Tests.cs` class gated on +`AB_SERVER_PROFILE=emulate`: + +- **CIP Template Object round-trip** — real Logix template bytes, + reads produce the same offset layout the CIP Symbol Object decoder + + `AbCipUdtMemberLayout` expect. +- **ALMD rising-edge semantics** — real Logix ALMD instruction fires + `InFaulted` / `Acked` transitions at cycle boundaries, not at our + unit-test fake's timer boundaries. +- **Optimized vs unoptimized DB behaviour** — Logix 5380/5580 series + runs the Studio 5000 project with optimized-DB-equivalent member + access; the driver's read path exercises that wire surface. + +Not in scope even with Emulate — needs real hardware: + +- EtherNet/IP embedded-switch behaviour (Stratix 5700, 1756-EN4TR) +- CIP Safety across partitions (Emulate 5580 emulates safety within + the chassis but not across nodes) +- Redundant chassis failover (1756-RM) +- Motion control timing +- High-speed discrete-input scheduling + +## How to run the Emulate-tier tests + +On the dev box: + +```powershell +$env:AB_SERVER_PROFILE = 'emulate' +$env:AB_SERVER_ENDPOINT = '10.0.0.42:44818' # replace with the Emulate PC IP +dotnet test tests\ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests +``` + +With `AB_SERVER_PROFILE` unset or `abserver`, the `Emulate/*Tests.cs` +classes skip cleanly; ab_server-backed tests run as usual. + +## See also + +- [`docs/drivers/AbServer-Test-Fixture.md`](../../../docs/drivers/AbServer-Test-Fixture.md) + §Logix Emulate golden-box tier — coverage map +- [`docs/v2/dev-environment.md`](../../../docs/v2/dev-environment.md) + §Integration host — license + networking notes +- Studio 5000 Logix Designer + Logix Emulate product pages on the + Rockwell TechConnect portal (licensed; internal link only). diff --git a/tests/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests.csproj b/tests/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests.csproj index 3f97c87..6a84f02 100644 --- a/tests/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests.csproj +++ b/tests/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests.csproj @@ -25,6 +25,7 @@ +