Files
lmxopcua/tests/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.IntegrationTests/TwinCatProject/README.md
Joseph Doherty 69e0d02c72 task-galaxy-e2e branch — non-FOCAS work-in-progress snapshot
Catch-all commit for pending work on the task-galaxy-e2e branch that
wasn't part of the FOCAS migration. Grouping by topic so future per-topic
commits can be cherry-picked if needed.

TwinCAT
- src/.../Driver.TwinCAT/AdsTwinCATClient.cs + TwinCATDriverFactoryExtensions.cs:
  factory-registration extensions + ADS client refinements.
- src/.../Driver.TwinCAT.Cli/Commands/BrowseCommand.cs: new browse command
  for the TwinCAT test-client CLI.
- tests/.../Driver.TwinCAT.IntegrationTests/TwinCAT3SmokeTests.cs + TwinCatProject/:
  fixture scaffold with a minimal POU + README pointing at the TCBSD/ESXi
  VM for e2e.
- docs/Driver.TwinCAT.Cli.md + docs/drivers/TwinCAT-Test-Fixture.md:
  documentation for the above.
- docs/v3/twincat-backlog.md: forward-looking backlog seed.

Admin UI + fleet status
- src/.../Admin/Components/Pages/Clusters/DriversTab.razor + Hosts.razor:
  UI refresh for fleet-status rendering.
- src/.../Admin/Hubs/FleetStatusHub.cs + FleetStatusPoller.cs +
  Admin/Program.cs: SignalR hub + poller plumbing for live fleet data.
- tests/.../Admin.Tests/FleetStatusPollerTests.cs: poller coverage.

Server + redundancy runtime (Phase 6.3 follow-ups)
- src/.../Server/Hosting/RedundancyPublisherHostedService.cs: HostedService
  that owns the RedundancyStatePublisher lifecycle + wires peer reachability.
- src/.../Server/Redundancy/ServerRedundancyNodeWriter.cs: OPC UA
  variable-node writer binding ServiceLevel + ServerUriArray to the
  publisher's events.
- src/.../Server/Program.cs + Server.csproj: hosted-service registration.
- tests/.../Server.Tests/ServerRedundancyNodeWriterTests.cs +
  Server.Tests.csproj: coverage for the above.

Configuration
- src/.../Configuration/Validation/DraftValidator.cs +
  tests/.../Configuration.Tests/DraftValidatorTests.cs: draft-validation
  refinements.

E2E scripts (shared infrastructure)
- scripts/e2e/README.md + _common.ps1 + test-all.ps1: shared helpers + the
  all-drivers test-all runner.
- scripts/e2e/test-opcuaclient.ps1: OPC UA Client e2e runner.

Docs
- docs/v2/implementation/phase-6-{1,2,3,4}*.md + exit-gate-phase-{3,7}.md:
  phase-gate + implementation doc updates.
- docs/v2/plan.md: top-level plan refresh.
- docs/v2/redundancy-interop-playbook.md: client interop playbook for the
  Phase 6.3 redundancy-runtime work.

Two orphan FOCAS docs remain on disk but deliberately unstaged —
docs/v2/focas-deployment.md and docs/v2/implementation/focas-simulator-plan.md
describe the now-retired Tier-C topology and should either be rewritten
or deleted in a follow-up.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 14:12:19 -04:00

9.7 KiB

TwinCAT XAR fixture project

This folder holds the TwinCAT 3 XAE project that the XAR VM (or TCBSD target) runs for the integration-tests suite and the broader browse / UDT / array / enum coverage exercised by the OPC UA driver.

Status today: the .sln / .tsproj / .plcproj wrappers still get generated per-workstation in XAE (GUIDs are install-specific), but every PLC object is committed as a standalone .TcGVL / .TcDUT / .TcPOU under PLC/{GVLs,DUTs,POUs}/. Reconstruction is "Add Existing Item" of each file into a fresh XAE project — see Importing the committed PLC objects below.

Why .tsproj, not the binary bootproject

TwinCAT ships two project forms: the XAE .tsproj (XML, source of truth) and the compiled bootproject that the XAR runtime actually loads. Ship the .tsproj because:

  • Text format — reviewable in PR diffs, diffable in git
  • Rebuildable across TC3 engineering versions (the XAE tool rebuilds the bootproject from .tsproj on "Activate Configuration")
  • Doesn't carry per-install state (target AmsNetId, source licensing)

Reconstruction workflow on the VM:

  1. Open TC3 XAE (Visual Studio shell)
  2. File → Open → OtOpcUaTwinCatFixture.tsproj
  3. Target system → the VM's AmsNetId (set in System Manager → Routes)
  4. Build → Build Solution (produces the bootproject)
  5. Activate Configuration → Run Mode (deploys to XAR + starts the runtime)

Required project state

TwinCAT3SmokeTests.cs ships 14 [TwinCATFact] methods plus a 16-case [TwinCATTheory] — the tests depend on the exact shapes below. Missing or renamed symbols surface as ADS DeviceSymbolNotFound or wrong-type read failures, not silent skips. Changed seed values will flip specific assertions — the load-bearing ones are called out inline.

Integration-test contract

The dependency surface spans GVL_Fixture, GVL_Primitives, GVL_Arrays, GVL_Enums, GVL_Plant, and MAIN.

GVL_Fixture (source: PLC/GVLs/GVL_Fixture.TcGVL) holds exactly three variables — nCounter : DINT := 1234, rSetpoint : REAL := 0.0, bFlag : BOOL := TRUE. MAIN (source: PLC/POUs/MAIN.TcPOU) increments nCounter every cycle so the native-notification test sees monotonic change without writing. Seeded values aren't reliable — PlcTask watchdog restarts reset them — so the read test only asserts a non-negative DINT.

GVL_Primitives.vWord := 16#BEEF — the bit-indexed BOOL test pins to bit 3 (set in 0xBEEF) and bit 4 (clear). Changing the seed flips that test.

GVL_Primitives numeric seeds — the primitive-type theory reads each of vSInt, vUSInt, vInt, vUInt, vDInt, vUDInt, vLInt, vULInt, vReal, vLReal, vString and asserts the exact seed value. Matches the declarations in PLC/GVLs/GVL_Primitives.TcGVL.

GVL_Arrays.aReal1D[5] — the array round-trip test writes + reads element index 5. The array must be writable; don't apply read-only attributes.

GVL_Plant.Line1.Stations[1].Axes[1].Motor.{Temperature, Running} — the nested-UDT test reads both (LREAL + BOOL). FB_LineSim must be driving the hierarchy (see MAIN) so values are alive.

GVL_Enums.currentAxisState — the browse test asserts this symbol surfaces by name.

Complex hierarchy (for browse / UDT / array / enum coverage)

All sources in PLC/. Commits are split one-object-per-file so XAE can "Add Existing Item" each into a fresh project (the .plcproj wrapper is environment-specific and not committed).

Type coverageGVL_Primitives has one of every ADS primitive: BOOL, BYTE, WORD, DWORD, LWORD, SINT, USINT, INT, UINT, DINT, UDINT, LINT, ULINT, REAL, LREAL, STRING(80), WSTRING(80), TIME, TOD, DATE, DT.

Array coverageGVL_Arrays has 1-D primitives, ARRAY[1..4,1..4] OF REAL, ARRAY[0..2,0..2,0..2] OF DINT, plus ARRAY[1..4] OF ST_Axis for per-element nested-struct browse.

Enum + alias coverageE_AxisState : DINT, E_Severity : INT, T_Temperature : LREAL, T_MeterPerSec : LREAL. GVL_Enums exposes each at the root so tests can assert EnumStrings / DataTypeDefinition rendering without walking the plant hierarchy.

5-level plant hierarchy — rooted at GVL_Plant.Line1 : ST_Line:

GVL_Plant.Line1                         (ST_Line)
    .Stations[1..3]                     (ARRAY OF ST_Station)
        .Axes[1..4]                     (ARRAY OF ST_Axis)
            .Motor                      (ST_Motor)
            .Encoder                    (ST_Encoder)
            .Commands                   (ST_AxisCommands)
            .TravelLog[1..8]            (ARRAY OF LREAL)
        .IO                             (ST_StationIO — 32x DI/DO, 8x AI/AO)
        .Alarms[1..16]                  (ARRAY OF ST_Alarm)
    .Recipe                             (ST_Recipe)
        .Steps[1..10]                   (ARRAY OF ST_RecipeStep)
        .SupportedSkus[1..4]            (ARRAY OF STRING)
    .Stats                              (ST_Stats)

Live value churnFB_LineSim + FB_AxisSim are called from MAIN every cycle: axes ramp position + derive velocity/accel, motor current/temperature track a sine, IO masks rotate, one alarm per station toggles each 32 cycles, stats counters increment. Subscription tests see real data-change notifications without an external writer.

Task

  • PlcTask — cyclic, 10 ms interval, priority 20
  • Assigned to MAIN

Runtime ID

  • TC3 PLC runtime 1 (AMS port 851) — the smoke-test fixture defaults to this. Use runtime 2 / port 852 only if the single runtime is already taken by another project on the same VM.

Importing the committed PLC objects

On a machine with a working TcXaeShell (or Visual Studio with the TC3 XAE integration):

  1. File → New → Project → TwinCAT XAE Project → name OtOpcUaTwinCatFixture. This lays down the .sln + .tsproj scaffolding.
  2. In the Solution Explorer, right-click the PLC node → Add New Item → Standard PLC Project → name PLC. Delete the auto-generated stub MAIN — the committed one will replace it.
  3. Right-click the PLC project's DUTs folder → Add → Existing Item … → multi-select every file under PLC/DUTs/ and Add As Link (so the repo stays the source of truth). Repeat for GVLs/PLC/GVLs/ and POUs/PLC/POUs/.
  4. Assign MAIN to PlcTask (cyclic, 10 ms, priority 20).
  5. Set the target system to the TCBSD / XAR VM via the AMS route, then Build → Build Solution + Activate Configuration → Run Mode.

If XAE complains about missing references while importing, add the DUTs before the GVLs (the structs are referenced by the plant GVL) and the enums/aliases first within DUTs.

XAR VM setup (one-time)

Full bootstrap lives in docs/v2/dev-environment.md. The TwinCAT-specific steps:

  1. Create the Hyper-V VM — Gen 2, Windows 10/11 64-bit, 4 GB RAM, 2 CPUs. External virtual switch so the dev box can reach <vm-ip>:48898.
  2. Install TwinCAT 3 XAE + XAR — free download from Beckhoff (www.beckhoff.com/en-en/products/automation/twincat/). Activate the 7-day trial on first boot.
  3. Note the VM's AmsNetId — shown in the TwinCAT system tray icon → Properties → AMS NetId (format like 5.23.91.23.1.1).
  4. Configure bilateral ADS route:
    • On the VM: System Manager → Routes → Add Route → dev box's AmsNetId + IP
    • On the dev box: edit %TC_INSTALLPATH%\Target\StaticRoutes.xml (or use the dev box's own TwinCAT System Manager if installed) to add the VM's AmsNetId + IP
  5. Import this project per the reconstruction workflow above.
  6. Hit Activate Configuration + Run Mode. The runtime starts; the system tray icon goes green; port 48898 is live.

License rotation

The XAR trial expires every 7 days. When it lapses:

  1. The runtime goes silent (red tray icon, ADS port 48898 stops responding to new connections).
  2. Integration tests skip with the reason message pointing at this folder's README.
  3. Operator runs C:\TwinCAT\3.1\Target\StartUp\TcActivate.exe /reactivate on the VM console (not RDP — the trial activation wants the interactive-login desktop).

Options to eliminate the manual step:

  • Scheduled task that runs the reactivate every 6 days at 02:00 — documented in the Beckhoff forums as working for some TC3 builds, not officially supported.
  • Paid runtime license (~$1k one-time per runtime, per CPU) — kills the rotation permanently, worth it if the integration host is long-lived.

How to run the TwinCAT-tier tests

On the dev box:

$env:TWINCAT_TARGET_NETID = '5.23.91.23.1.1'     # replace with the VM AmsNetId — REQUIRED
$env:TWINCAT_TARGET_HOST  = '10.0.0.42'          # replace with the VM IP (defaults to localhost)
# $env:TWINCAT_TARGET_PORT = '852'               # only if not using PLC runtime 1 (default 851)
dotnet test tests\ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.IntegrationTests

Only TWINCAT_TARGET_NETID is required — fixture gates on it specifically. TWINCAT_TARGET_HOST defaults to localhost when unset; TWINCAT_TARGET_PORT defaults to 851. With the NetID unset, the whole integration suite (~28 cases, both [TwinCATFact] and [TwinCATTheory]) skips cleanly; unit suite (TwinCAT.Tests) runs unchanged.

See also