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>
393 lines
20 KiB
PowerShell
393 lines
20 KiB
PowerShell
#Requires -Version 7.0
|
|
<#
|
|
.SYNOPSIS
|
|
End-to-end CLI test for the OPC UA Client (gateway) driver bridged through
|
|
the OtOpcUa server.
|
|
|
|
.DESCRIPTION
|
|
The OpcUaClient driver is unique in the fleet — it's a gateway that connects
|
|
to ANOTHER OPC UA server and re-exposes its address space through the local
|
|
OtOpcUa server. So there's no protocol-specific driver CLI; both directions
|
|
of this test use `otopcua-cli` against two different endpoints:
|
|
|
|
remote = the upstream OPC UA server the driver connects to (opc-plc fixture
|
|
by default, opc.tcp://localhost:50000)
|
|
local = the OtOpcUa server itself, which mirrors remote nodes through the
|
|
OpcUaClient driver instance (opc.tcp://localhost:4840)
|
|
|
|
Eight stages cover the driver's full capability surface:
|
|
|
|
1. Remote probe — otopcua-cli connect to the upstream. Confirms the
|
|
simulator / target server is reachable and
|
|
speaking UA Secure Channel.
|
|
2. Remote read — otopcua-cli read of -RemoteNodeId on the upstream.
|
|
Captures the current value + confirms the node
|
|
exists. Baseline for the forward-bridge stage.
|
|
3. Forward bridge — otopcua-cli read of -BridgeNodeId on the LOCAL
|
|
server. Proves the driver discovered + mirrored
|
|
the remote node into the local address space and
|
|
the read path is live (IReadable via session).
|
|
4. Subscribe-sees-change — subscribe on local -BridgeNodeId in the
|
|
background. opc-plc's tickers (FastUInt1, StepUp)
|
|
mutate autonomously, so no driver poke is needed
|
|
— a data-change event should arrive within the
|
|
subscription window. Covers ISubscribable +
|
|
upstream subscription transfer.
|
|
5. Reverse bridge — otopcua-cli write to local -WritableBridgeNodeId,
|
|
then otopcua-cli read of -WritableRemoteNodeId
|
|
directly on the upstream. Confirms writes flow
|
|
through the driver to the remote (IWritable). Opt-
|
|
in — opc-plc default image has no writable nodes
|
|
without `--sn`; pass -WritableBridgeNodeId AND
|
|
-WritableRemoteNodeId to enable.
|
|
6. Browse mirror — otopcua-cli browse of the local -BridgeRootNodeId
|
|
at depth -BrowseDepth. Asserts at least
|
|
-BrowseMinNodes descendants appear. Covers
|
|
ITagDiscovery → local-namespace projection.
|
|
7. Alarm fires — otopcua-cli alarms subscription on local
|
|
-AlarmNodeId. opc-plc with `--alm` cycles a
|
|
TripAlarm autonomously; assert an Active alarm
|
|
event surfaces. Covers IAlarmSource → OPC UA A&E
|
|
projection. Opt-in via -AlarmNodeId.
|
|
8. History read — historyread on local -HistoryNodeId over a
|
|
lookback window. Covers IHistoryProvider →
|
|
upstream HistoryRead dispatch. Opt-in via
|
|
-HistoryNodeId. Note: opc-plc's default image
|
|
does not historize — a historizing upstream
|
|
(Prosys, UaExpert sample server) is required.
|
|
|
|
Prereqs:
|
|
|
|
1. Upstream OPC UA server reachable at -RemoteUrl. Default expects the
|
|
opc-plc Docker fixture (`tests/.../Driver.OpcUaClient.IntegrationTests/
|
|
Docker/docker-compose.yml`): `docker compose up -d` before running.
|
|
2. OtOpcUa server running at -OpcUaUrl with an OpcUaClient DriverInstance
|
|
in its Config DB whose EndpointUrl = -RemoteUrl. The server's
|
|
DiscoverAsync populates the mirrored namespace at startup; the
|
|
-BridgeNodeId / -BridgeRootNodeId you pass must correspond to whatever
|
|
NodeIds that discovery produced on your local server.
|
|
3. To exercise stages 5 / 7 / 8, the upstream must expose writable nodes /
|
|
alarm conditions / history. opc-plc alone doesn't cover all three — see
|
|
parameter docs below for the combinations that work with opc-plc.
|
|
|
|
.PARAMETER RemoteUrl
|
|
Upstream OPC UA server endpoint (the server the driver connects to).
|
|
Default matches the opc-plc Docker fixture — opc.tcp://localhost:50000.
|
|
|
|
.PARAMETER OpcUaUrl
|
|
Local OtOpcUa server endpoint. Default opc.tcp://localhost:4840.
|
|
|
|
.PARAMETER RemoteNodeId
|
|
NodeId on the upstream used for stages 1-2. Default ns=3;s=FastUInt1 — opc-plc
|
|
ticker that increments every 100 ms.
|
|
|
|
.PARAMETER BridgeNodeId
|
|
NodeId on the LOCAL server that mirrors -RemoteNodeId after the OpcUaClient
|
|
driver discovers it. Dev-specific — whatever the local DiscoverAsync produced
|
|
for the upstream node. No default; mandatory for stages 3-4.
|
|
|
|
.PARAMETER WritableRemoteNodeId
|
|
Writable NodeId on the upstream for the reverse-bridge stage. opc-plc's
|
|
default image has no writable nodes; add `--sn=1` to the compose command to
|
|
expose `ns=3;s=SlowUInt1` as writable (or similar per opc-plc docs). Omit to
|
|
skip stage 5.
|
|
|
|
.PARAMETER WritableBridgeNodeId
|
|
Matching local mirror of -WritableRemoteNodeId. Omit to skip stage 5.
|
|
|
|
.PARAMETER BridgeRootNodeId
|
|
Root NodeId on the local server under which the mirrored upstream sits. The
|
|
browse stage walks from this node down to -BrowseDepth. Default i=85
|
|
(ObjectsFolder) — works but produces a lot of output; pass a narrower root
|
|
for faster / more targeted coverage.
|
|
|
|
.PARAMETER BrowseDepth
|
|
Max depth for the browse stage. Default 3.
|
|
|
|
.PARAMETER BrowseMinNodes
|
|
Minimum number of descendants expected under -BridgeRootNodeId. Default 5.
|
|
|
|
.PARAMETER AlarmNodeId
|
|
NodeId of the ConditionType on the local server for the alarm-fires stage.
|
|
opc-plc with `--alm` exposes e.g. TripAlarm conditions; the local mirror path
|
|
of that condition goes here. Omit to skip stage 7.
|
|
|
|
.PARAMETER AlarmWaitSec
|
|
Seconds to wait for the alarm to cycle. opc-plc's TripAlarm fires on its own
|
|
cadence; 15 s usually covers one cycle. Default 15.
|
|
|
|
.PARAMETER HistoryNodeId
|
|
NodeId on the local server whose history to query. Omit to skip stage 8.
|
|
|
|
.PARAMETER HistoryLookbackSec
|
|
Seconds back from now to query history. Default 3600.
|
|
|
|
.PARAMETER ChangeWaitSec
|
|
Seconds the subscribe-sees-change stage waits for a natural ticker update.
|
|
opc-plc's FastUInt1 ticks every 100 ms so a short window suffices. Default 8.
|
|
|
|
.EXAMPLE
|
|
# Bare-minimum: stages 1-4 + browse, against the opc-plc compose fixture.
|
|
# Requires the local OtOpcUa server to have discovered opc-plc and placed
|
|
# FastUInt1 under (for example) ns=2;s=OpcUaClient/FastUInt1.
|
|
./scripts/e2e/test-opcuaclient.ps1 -BridgeNodeId "ns=2;s=OpcUaClient/FastUInt1"
|
|
|
|
.EXAMPLE
|
|
# Full matrix — all eight stages. Requires an opc-plc image with --sn (for
|
|
# writable) + --alm (for alarms; default compose has this) + a historizing
|
|
# upstream (opc-plc does not; Prosys does).
|
|
./scripts/e2e/test-opcuaclient.ps1 `
|
|
-BridgeNodeId "ns=2;s=OpcUaClient/FastUInt1" `
|
|
-WritableRemoteNodeId "ns=3;s=SlowUInt1" `
|
|
-WritableBridgeNodeId "ns=2;s=OpcUaClient/SlowUInt1" `
|
|
-BridgeRootNodeId "ns=2;s=OpcUaClient" `
|
|
-AlarmNodeId "ns=2;s=OpcUaClient/TripAlarm" `
|
|
-HistoryNodeId "ns=2;s=OpcUaClient/StepUp"
|
|
#>
|
|
|
|
param(
|
|
[string]$RemoteUrl = "opc.tcp://localhost:50000",
|
|
[string]$OpcUaUrl = "opc.tcp://localhost:4840",
|
|
[string]$RemoteNodeId = "ns=3;s=FastUInt1",
|
|
[Parameter(Mandatory)] [string]$BridgeNodeId,
|
|
[string]$WritableRemoteNodeId = "",
|
|
[string]$WritableBridgeNodeId = "",
|
|
[string]$BridgeRootNodeId = "i=85",
|
|
[int]$BrowseDepth = 3,
|
|
[int]$BrowseMinNodes = 5,
|
|
[string]$AlarmNodeId = "",
|
|
[int]$AlarmWaitSec = 15,
|
|
[string]$HistoryNodeId = "",
|
|
[int]$HistoryLookbackSec = 3600,
|
|
[int]$ChangeWaitSec = 8
|
|
)
|
|
|
|
$ErrorActionPreference = "Stop"
|
|
. "$PSScriptRoot/_common.ps1"
|
|
|
|
$opcUaCli = Get-CliInvocation `
|
|
-ProjectFolder "src/ZB.MOM.WW.OtOpcUa.Client.CLI" `
|
|
-ExeName "otopcua-cli"
|
|
|
|
$results = @()
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Stage 1 — Remote probe. `otopcua-cli connect` exits 0 when the Secure Channel
|
|
# + Session handshake to the upstream complete cleanly. A failure here means
|
|
# opc-plc isn't running or the endpoint is unreachable — nothing downstream is
|
|
# worth trying.
|
|
# ---------------------------------------------------------------------------
|
|
|
|
Write-Header "Remote probe"
|
|
$probe = Invoke-Cli -Cli $opcUaCli -Args @("connect", "-u", $RemoteUrl)
|
|
if ($probe.ExitCode -eq 0 -and $probe.Output -match "Connection successful") {
|
|
Write-Pass "upstream $RemoteUrl reachable + speaks UA"
|
|
$results += @{ Passed = $true }
|
|
} else {
|
|
Write-Fail "upstream connect failed (exit=$($probe.ExitCode))"
|
|
Write-Host $probe.Output
|
|
$results += @{ Passed = $false; Reason = "remote probe failed" }
|
|
# Fail fast: if the upstream is down every other stage will cascade.
|
|
Write-Summary -Title "OpcUaClient e2e" -Results $results
|
|
exit 1
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Stage 2 — Remote read. Pulls the current value of -RemoteNodeId directly from
|
|
# the upstream. Recorded for later stages to compare against, and confirms the
|
|
# chosen NodeId actually exists on this upstream.
|
|
# ---------------------------------------------------------------------------
|
|
|
|
Write-Header "Remote read"
|
|
$remoteRead = Invoke-Cli -Cli $opcUaCli -Args @("read", "-u", $RemoteUrl, "-n", $RemoteNodeId)
|
|
$remoteValue = $null
|
|
if ($remoteRead.ExitCode -eq 0 -and $remoteRead.Output -match "Value:\s+([^\r\n]+)") {
|
|
$remoteValue = $Matches[1].Trim()
|
|
Write-Pass "remote $RemoteNodeId = $remoteValue"
|
|
$results += @{ Passed = $true }
|
|
} else {
|
|
Write-Fail "remote read of $RemoteNodeId failed"
|
|
Write-Host $remoteRead.Output
|
|
$results += @{ Passed = $false; Reason = "remote read failed" }
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Stage 3 — Forward bridge. Read -BridgeNodeId on the LOCAL server. If the
|
|
# OpcUaClient driver is live + its discovery mapped -RemoteNodeId into the
|
|
# local namespace, this should return a Good value. For ticker nodes like
|
|
# FastUInt1 we don't require exact equality with stage 2 (the ticker has
|
|
# likely advanced between reads); a Good-status read is the real signal.
|
|
# ---------------------------------------------------------------------------
|
|
|
|
Write-Header "Forward bridge (remote → local)"
|
|
$localRead = Invoke-Cli -Cli $opcUaCli -Args @("read", "-u", $OpcUaUrl, "-n", $BridgeNodeId)
|
|
if ($localRead.ExitCode -eq 0 -and $localRead.Output -match "Status:\s+0x00000000" -and $localRead.Output -match "Value:\s+([^\r\n]+)") {
|
|
$localValue = $Matches[1].Trim()
|
|
Write-Pass "local bridge $BridgeNodeId = $localValue (remote was $remoteValue)"
|
|
$results += @{ Passed = $true }
|
|
} else {
|
|
Write-Fail "local bridge read failed — driver instance may not be configured or discovery hasn't run"
|
|
Write-Host $localRead.Output
|
|
$results += @{ Passed = $false; Reason = "forward bridge failed" }
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Stage 4 — Subscribe sees change. opc-plc's FastUInt1 ticks autonomously so we
|
|
# don't need to drive a write. A properly wired OpcUaClient driver forwards
|
|
# remote MonitoredItem data-change callbacks to the local server, which then
|
|
# publishes them to our subscribe client. If nothing arrives within the
|
|
# window, either the remote node isn't a ticker OR the upstream subscription
|
|
# chain is broken (probe state, keep-alive, SDK publish queue).
|
|
# ---------------------------------------------------------------------------
|
|
|
|
Write-Header "Subscribe sees change"
|
|
$stdout = New-TemporaryFile
|
|
$stderr = New-TemporaryFile
|
|
$subArgs = @($opcUaCli.PrefixArgs) + @(
|
|
"subscribe", "-u", $OpcUaUrl, "-n", $BridgeNodeId,
|
|
"-i", "200", "--duration", "$ChangeWaitSec")
|
|
$subProc = Start-Process -FilePath $opcUaCli.File `
|
|
-ArgumentList $subArgs -NoNewWindow -PassThru `
|
|
-RedirectStandardOutput $stdout.FullName `
|
|
-RedirectStandardError $stderr.FullName
|
|
Write-Info "subscription started (pid $($subProc.Id)) for ${ChangeWaitSec}s"
|
|
$subProc.WaitForExit(($ChangeWaitSec + 5) * 1000) | Out-Null
|
|
if (-not $subProc.HasExited) { Stop-Process -Id $subProc.Id -Force }
|
|
$subOut = (Get-Content $stdout.FullName -Raw) + (Get-Content $stderr.FullName -Raw)
|
|
Remove-Item $stdout.FullName, $stderr.FullName -ErrorAction SilentlyContinue
|
|
|
|
# SubscribeCommand prints `[timestamp] <NodeId> = <value> (0xNNNNNNNN)` per
|
|
# data-change event. 0x00000000 == Good; anything else is a non-Good status
|
|
# we intentionally don't count (a quality drop isn't a "saw the change").
|
|
$changeLines = @(($subOut -split "`n") | Where-Object { $_ -match "=\s+\S.*\(0x00000000\)" })
|
|
if ($changeLines.Count -gt 0) {
|
|
Write-Pass "$($changeLines.Count) data-change events observed on bridge"
|
|
$results += @{ Passed = $true }
|
|
} else {
|
|
Write-Fail "no data-change events in ${ChangeWaitSec}s — upstream node may be static, or subscription chain broken"
|
|
Write-Host $subOut
|
|
$results += @{ Passed = $false; Reason = "no data-change" }
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Stage 5 — Reverse bridge. Only runs when both writable NodeIds are supplied.
|
|
# Writes on the local bridge side, reads directly on the upstream to verify
|
|
# the write crossed the driver. 2s settle accounts for the driver's next poll
|
|
# (non-idempotent writes on upstream side may take a tick to propagate).
|
|
# ---------------------------------------------------------------------------
|
|
|
|
if ([string]::IsNullOrEmpty($WritableBridgeNodeId) -or [string]::IsNullOrEmpty($WritableRemoteNodeId)) {
|
|
Write-Header "Reverse bridge (local → remote)"
|
|
Write-Skip "WritableBridgeNodeId / WritableRemoteNodeId not supplied — opc-plc default has no writable nodes. Add --sn=N to the compose and re-run with both params set."
|
|
} else {
|
|
Write-Header "Reverse bridge (local → remote)"
|
|
$writeValue = Get-Random -Minimum 1 -Maximum 9999
|
|
$w = Invoke-Cli -Cli $opcUaCli -Args @(
|
|
"write", "-u", $OpcUaUrl, "-n", $WritableBridgeNodeId, "-v", "$writeValue")
|
|
if ($w.ExitCode -ne 0 -or $w.Output -notmatch "Write successful") {
|
|
Write-Fail "local-side write failed"
|
|
Write-Host $w.Output
|
|
$results += @{ Passed = $false; Reason = "reverse-bridge write failed" }
|
|
} else {
|
|
Write-Info "local write ok, waiting 2s for driver propagate"
|
|
Start-Sleep -Seconds 2
|
|
$r = Invoke-Cli -Cli $opcUaCli -Args @("read", "-u", $RemoteUrl, "-n", $WritableRemoteNodeId)
|
|
if ($r.ExitCode -eq 0 -and $r.Output -match "Value:\s+$([Regex]::Escape("$writeValue"))\b") {
|
|
Write-Pass "remote reads back $writeValue"
|
|
$results += @{ Passed = $true }
|
|
} else {
|
|
Write-Fail "remote value did not reflect $writeValue"
|
|
Write-Host $r.Output
|
|
$results += @{ Passed = $false; Reason = "reverse-bridge readback mismatch" }
|
|
}
|
|
}
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Stage 6 — Browse mirror. Walks -BridgeRootNodeId to -BrowseDepth levels. The
|
|
# BrowseCommand emits one line per encountered node; we count non-empty lines
|
|
# minus the root-summary line and compare against -BrowseMinNodes. A naked
|
|
# i=85 root always has something; a narrower dev-specific root is stricter.
|
|
# ---------------------------------------------------------------------------
|
|
|
|
Write-Header "Browse mirror"
|
|
$br = Invoke-Cli -Cli $opcUaCli -Args @(
|
|
"browse", "-u", $OpcUaUrl, "-n", $BridgeRootNodeId,
|
|
"-r", "-d", "$BrowseDepth")
|
|
if ($br.ExitCode -ne 0) {
|
|
Write-Fail "browse failed (exit=$($br.ExitCode))"
|
|
Write-Host $br.Output
|
|
$results += @{ Passed = $false; Reason = "browse failed" }
|
|
} else {
|
|
# BrowseCommand prints one line per node: `[Type] Name (NodeId: xxx)` with
|
|
# indentation for depth. Count every line carrying a NodeId marker.
|
|
$nodeLines = @(($br.Output -split "`n") | Where-Object { $_ -match "\(NodeId:" })
|
|
$count = $nodeLines.Count
|
|
if ($count -ge $BrowseMinNodes) {
|
|
Write-Pass "$count descendants under $BridgeRootNodeId (>= $BrowseMinNodes)"
|
|
$results += @{ Passed = $true }
|
|
} else {
|
|
Write-Fail "only $count descendants — expected >= $BrowseMinNodes"
|
|
Write-Host $br.Output
|
|
$results += @{ Passed = $false; Reason = "browse under-populated" }
|
|
}
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Stage 7 — Alarm fires. opc-plc with --alm (set in the compose) cycles a
|
|
# TripAlarm Condition autonomously. The local alarm subscription should
|
|
# surface at least one Active transition within the wait window. Opt-in:
|
|
# requires the user to know the local mirror of the upstream alarm Condition.
|
|
# ---------------------------------------------------------------------------
|
|
|
|
if ([string]::IsNullOrEmpty($AlarmNodeId)) {
|
|
Write-Header "Alarm fires"
|
|
Write-Skip "AlarmNodeId not supplied — skipping alarm stage"
|
|
} else {
|
|
Write-Header "Alarm fires"
|
|
$stdout = New-TemporaryFile
|
|
$stderr = New-TemporaryFile
|
|
$allArgs = @($opcUaCli.PrefixArgs) + @(
|
|
"alarms", "-u", $OpcUaUrl, "-n", $AlarmNodeId, "-i", "500", "--refresh")
|
|
$proc = Start-Process -FilePath $opcUaCli.File `
|
|
-ArgumentList $allArgs -NoNewWindow -PassThru `
|
|
-RedirectStandardOutput $stdout.FullName `
|
|
-RedirectStandardError $stderr.FullName
|
|
Write-Info "alarm subscription started (pid $($proc.Id)), waiting ${AlarmWaitSec}s for opc-plc alarm cycle"
|
|
Start-Sleep -Seconds $AlarmWaitSec
|
|
if (-not $proc.HasExited) { Stop-Process -Id $proc.Id -Force }
|
|
$out = (Get-Content $stdout.FullName -Raw) + (Get-Content $stderr.FullName -Raw)
|
|
Remove-Item $stdout.FullName, $stderr.FullName -ErrorAction SilentlyContinue
|
|
|
|
if ($out -match "ALARM\b" -and $out -match "Active\b") {
|
|
Write-Pass "alarm condition fired with Active state"
|
|
$results += @{ Passed = $true }
|
|
} else {
|
|
Write-Fail "no Active alarm event observed in ${AlarmWaitSec}s — check opc-plc compose has --alm + the AlarmNodeId is the local mirror of the upstream Condition"
|
|
Write-Host $out
|
|
$results += @{ Passed = $false; Reason = "no alarm event" }
|
|
}
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Stage 8 — History read. IHistoryProvider dispatch to the upstream's
|
|
# HistoryRead service. opc-plc does NOT historize by default, so this stage
|
|
# SKIPs when -HistoryNodeId is empty. Against a historizing upstream (Prosys,
|
|
# UA Expert sample server, AVEVA Historian) point -HistoryNodeId at the local
|
|
# mirror of a historized node.
|
|
# ---------------------------------------------------------------------------
|
|
|
|
if ([string]::IsNullOrEmpty($HistoryNodeId)) {
|
|
Write-Header "History read"
|
|
Write-Skip "HistoryNodeId not supplied — opc-plc default does not historize; supply a historized-upstream mirror NodeId to enable."
|
|
} else {
|
|
$results += Test-HistoryHasSamples `
|
|
-OpcUaCli $opcUaCli `
|
|
-OpcUaUrl $OpcUaUrl `
|
|
-NodeId $HistoryNodeId `
|
|
-LookbackSec $HistoryLookbackSec
|
|
}
|
|
|
|
Write-Summary -Title "OpcUaClient e2e" -Results $results
|
|
if ($results | Where-Object { -not $_.Passed }) { exit 1 }
|