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

385 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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`
```st
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`
```st
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`
> **Note (PR 3.2 / #314)**: the probe loop also reads the four
> `TwinCAT_SystemInfoVarList` system symbols (`_AppInfo.AppName`,
> `_AppInfo.OnlineChangeCnt`, `_TaskInfo[1].CycleTime`, `_TaskInfo[1].LastExecTime`)
> per tick — they're exported by every TC3 PLC runtime, so no extra fixture state
> is required. `TwinCATDiagnosticsIntegrationTests` asserts they surface on
> `DriverHealth.Diagnostics`.
> **Note (PR 3.1 / #313)**: `GVL_Fixture.nCounter` doubles as the
> coalescing-test driver for `TwinCATMaxDelayTests`. The 10 ms cycle +
> per-cycle increment in `MAIN` means a no-coalescing subscriber sees ~100
> events / s; with `MaxDelayMs=500` the test asserts ≤ 3 events / s. No new
> project state required.
## 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`
```st
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`
```st
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:
```st
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
```powershell
$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.
## Complex hierarchy
PR 4.1 / #315 (nested-UDT browse via online type walker) exercises
`TwinCATTypeWalker.Walk` against a real PLC symbol graph. The fixture
state required:
### DUTs
- `PLC/DUTs/ST_NestedFlags.TcDUT` — mixed-atomic struct (BOOL / INT / REAL /
STRING) used for the per-member flatten coverage.
- `PLC/DUTs/ST_AlarmRecord.TcDUT` — small two-field struct used as the
element type of the cutoff array.
- `PLC/DUTs/ST_RecursiveCap.TcDUT` — struct with a `POINTER TO`
self-reference; verifies the walker's pointer-skip + cycle-guard
paths terminate without exploding the symbol stream.
### GVL: `GVL_Plant`
```st
VAR_GLOBAL
stFlags : ST_NestedFlags;
aAlarmRecords : ARRAY[1..2000] OF ST_AlarmRecord;
END_VAR
```
`stFlags` produces N atomic leaves where N = the number of `ST_NestedFlags`
fields. `aAlarmRecords` has 2000 elements which exceeds the default
`TwinCATDriverOptions.MaxArrayExpansion` (1024) — discovery surfaces it as
a single `IsArrayRoot` leaf rather than 4000 per-element rows. Lower the
cap on the driver instance to force per-element expansion (or raise it if
the operator wants the per-element view).
The XAE-form artefacts ship at `PLC/DUTs/*.TcDUT` + `PLC/GVLs/GVL_Plant.TcGVL`;
import them into the PLC project alongside `GVL_Fixture` + `GVL_Perf`.
The integration test that exercises this fixture lives at
`tests/.../TwinCATUdtBrowseTests.cs` and skips via `[TwinCATFact]` when
the XAR runtime isn't reachable.
## 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.
```powershell
$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"
```
## Alarm scenarios
PR 5.1 (#316) ships an opt-in TC3 EventLogger bridge. The driver's
`IAlarmSource` implementation surfaces alarms by opening a second
`AdsClient` against AMS port `110` (`AMSPORT_EVENTLOG`) and adding a
device notification on `ADSIGRP_TCEVENTLOG_ALARMS`. The decode is
best-effort because Beckhoff doesn't ship a managed `TcEventLogger`
wrapper (only C++ TcCOM headers); some fields surface as `Unknown`
until a follow-up PR lands a binary-protocol decoder. Spike output
captured at `docs/v3/twincat-eventlogger-spike.md`.
The integration test
(`TwinCATAlarmIntegrationTests.Driver_raises_alarm_event_when_PLC_logs_event`)
ships build-only in PR 5.1 — once the XAR project imports the GVL +
FB_AlarmHarness below, swap the `Assert.Skip` in the test body for the
live flow:
1. Init the driver with `EnableAlarms=true`.
2. `SubscribeAlarmsAsync([], ct)`.
3. `WriteAsync` to flip `GVL_Alarms.bTriggerEvent` from `FALSE` to
`TRUE``FB_AlarmHarness` sees the rising edge and calls
`FB_TcLogEvent` on the PLC side.
4. Assert `OnAlarmEvent` fires within `~5 s` with non-empty
`Source` + `Message`.
### Global Variable List: `GVL_Alarms`
```st
VAR_GLOBAL
bTriggerEvent : BOOL := FALSE;
bAcked : BOOL := FALSE;
nLastEventClass : DINT := 0;
nLastSeverity : USINT := 0;
fbAlarmHarness : FB_AlarmHarness;
END_VAR
```
The XAE-form GVL ships at `PLC/GVLs/GVL_Alarms.TcGVL`; import it
alongside the other fixture GVLs.
### POU: `FB_AlarmHarness`
```st
FUNCTION_BLOCK FB_AlarmHarness
VAR
fbTrigger : R_TRIG;
fbLogEvent : FB_TcLogEvent; // declared in Tc3_EventLogger
sMessage : STRING(255) := 'Integration-fixture EventLogger trigger';
END_VAR
fbTrigger(CLK := GVL_Alarms.bTriggerEvent);
IF fbTrigger.Q THEN
fbLogEvent.eSeverity := TcEventSeverity.Warning;
fbLogEvent.bConfirmable := TRUE;
fbLogEvent.Execute(bExecute := TRUE);
GVL_Alarms.nLastEventClass := 1;
GVL_Alarms.nLastSeverity := 100;
END_IF
fbLogEvent.Execute(bExecute := FALSE);
```
The XAE-form POU ships at `PLC/POUs/FB_AlarmHarness.TcPOU`. Wire it
into `MAIN`:
```st
GVL_Alarms.fbAlarmHarness();
```
### Event class IDs / severity buckets / cleared-on transitions
| Symbol | Value | Notes |
| --- | --- | --- |
| `nLastEventClass` | `DINT`, fixture-side echo (`1` after a rising edge) | Watch-window aid; the actual EventLogger event class is configured in the TC3 GUI per project. |
| `nLastSeverity` | `USINT`, fixed `100` after a rising edge | Maps to `AlarmSeverity.Medium` via `TwinCATAlarmSource.MapSeverity` (≤128 = Medium). |
| `bTriggerEvent` | `BOOL`, operator/test writes | Rising edge only — flip back to `FALSE` then `TRUE` to re-fire. |
| `bAcked` | `BOOL`, driver writes when `AcknowledgeAsync` runs | Cleared by next event raise. |
The TC3 EventLogger surfaces the cleared transition automatically when
`fbLogEvent.bConfirmable=TRUE` and an operator confirms; the driver
projects the clear as a second `OnAlarmEvent` with the same condition
id.
## How to run the TwinCAT-tier tests
On the dev box:
```powershell
$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
- [`docs/drivers/TwinCAT-Test-Fixture.md`](../../../docs/drivers/TwinCAT-Test-Fixture.md)
— coverage map
- [`docs/v2/dev-environment.md`](../../../docs/v2/dev-environment.md)
§Integration host — VM + route + license-rotation notes
- Beckhoff Information System → TwinCAT 3 → Product overview + ADS +
PLC reference (licensed; internal link only)