Files
lmxopcua/tests/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.IntegrationTests/TwinCatProject

TwinCAT XAR fixture project

This folder holds the TwinCAT 3 XAE project that the XAR VM runs for the integration-tests suite (tests/.../TwinCAT.IntegrationTests/*.cs).

Status today: stub. The .tsproj isn't committed yet; once the XAR VM is up + a project with the required state exists, export via File → Export + drop it here as OtOpcUaTwinCatFixture.tsproj + its PLC .library / .plcproj companions.

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

The smoke tests in TwinCAT3SmokeTests.cs depend on this exact GVL + PLC setup. Missing or renamed symbols surface as ADS DeviceSymbolNotFound or wrong-type read failures, not silent skips.

Global Variable List: GVL_Fixture

VAR_GLOBAL
    // Monotonically-increasing counter; MAIN increments each cycle.
    // Seed value 1234 picked so the smoke test can assert ">= 1234" without
    // synchronising with the initial cycle.
    nCounter   : DINT := 1234;

    // Scratch REAL for write-then-read round-trip test. Smoke test writes
    // 42.5 + reads back.
    rSetpoint  : REAL := 0.0;

    // Readable boolean with seed value TRUE. Reserved for future
    // expansion (e.g. discovery / symbol-browse tests).
    bFlag      : BOOL := TRUE;
END_VAR

PLC program: MAIN

PROGRAM MAIN
VAR
END_VAR

// One-line program: increment the fixture counter every cycle.
// The native-notification smoke test subscribes to GVL_Fixture.nCounter
// + observes the monotonic changes without a write path.
GVL_Fixture.nCounter := GVL_Fixture.nCounter + 1;

Task

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

Performance scenarios

PR 2.1 (ADS Sum-read / Sum-write) ships an opt-in perf-tier integration test (TwinCATSumCommandPerfTests.Driver_sum_read_1000_tags_beats_loop_baseline_by_5x) that reads 1000 DINTs in one shot and asserts the bulk path beats the per-tag loop by ≥ 5×. The fixture state required by that test is:

Global Variable List: GVL_Perf

VAR_GLOBAL
    // 1000-DINT array  exercised by the bulk Sum-read benchmark.
    aTags       : ARRAY[1..1000] OF DINT;
    fbPerfChurn : FB_PerfChurn;
END_VAR

The XAE-form GVL ships at PLC/GVLs/GVL_Perf.TcGVL; import it into the PLC project alongside GVL_Fixture.

POU: FB_PerfChurn

FUNCTION_BLOCK FB_PerfChurn
VAR
    nIndex : INT := 1;
END_VAR

GVL_Perf.aTags[nIndex] := GVL_Perf.aTags[nIndex] + 1;
nIndex := nIndex + 1;
IF nIndex > 1000 THEN
    nIndex := 1;
END_IF

The XAE-form POU ships at PLC/POUs/FB_PerfChurn.TcPOU. Wire it into MAIN so a value rotates each cycle:

PROGRAM MAIN
VAR
END_VAR

// existing GVL_Fixture line:
GVL_Fixture.nCounter := GVL_Fixture.nCounter + 1;

// PR 2.1  keep aTags moving so caches don't short-circuit the read.
GVL_Perf.fbPerfChurn();

Running the perf tier

$env:TWINCAT_TARGET_HOST  = '10.0.0.42'
$env:TWINCAT_TARGET_NETID = '5.23.91.23.1.1'
$env:TWINCAT_PERF         = '1'
dotnet test tests\ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.IntegrationTests `
    --filter "Category=Performance"

Without TWINCAT_PERF=1 the perf test skips via [TwinCATPerfFact] even when the runtime is reachable — perf runs are opt-in to keep the default integration pass fast.

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.

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.

Online-change test scenario

PR 2.3 (proactive Symbol-Version invalidation listener) ships an operator-gated integration test (TwinCATSymbolVersionTests.Driver_invalidates_handle_cache_on_symbol_version_bump) that verifies AdsTwinCATClient's AdsSymbolVersionChanged listener wipes the handle cache when the PLC re-initialises after an online change. The test polls for up to 60 s waiting for the operator to trigger the change from XAE.

The fixture state (GVL_Perf + aTags[1..1000]) is the same one used by the Sum-read perf test — no new project state required.

Manual workflow

With the XAR runtime live + the test process polling:

  1. Open the project in XAE on the dev box (or wherever XAE runs).
  2. Add a dummy variable to GVL_Perf — any new declaration triggers a symbol-table rebuild. Example: append bSymVerProbe : BOOL := FALSE; to the GVL.
  3. Login (Ctrl+F8) — XAE prompts to load the change.
  4. Activate Configuration (Yellow-arrow button, or TwinCAT → Activate Configuration). The runtime re-initialises; the symbol-version counter increments; AdsTwinCATClient.OnAdsSymbolVersionChanged fires; the handle cache wipes; the test polls observe SymbolVersionBumps > 0 + asserts the post-bump read recreates handles.

The test skips by default — opt in by setting TWINCAT_MANUAL_ONLINE_CHANGE=1 alongside the standard TWINCAT_TARGET_HOST / TWINCAT_TARGET_NETID env vars before kicking off the test run.

$env:TWINCAT_TARGET_HOST          = '10.0.0.42'
$env:TWINCAT_TARGET_NETID         = '5.23.91.23.1.1'
$env:TWINCAT_MANUAL_ONLINE_CHANGE = '1'
dotnet test tests\ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.IntegrationTests `
    --filter "FullyQualifiedName~TwinCATSymbolVersionTests"

How to run the TwinCAT-tier tests

On the dev box:

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

With any of those env vars unset, all three smoke tests skip cleanly via [TwinCATFact]; unit suite (TwinCAT.Tests) runs unchanged.

See also