385 lines
14 KiB
Markdown
385 lines
14 KiB
Markdown
# 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)
|