#Requires -Version 7.0 <# .SYNOPSIS End-to-end CLI test for the OPC UA Client (gateway) driver bridged through the OtOpcUa server. Stages: probe, read, subscribe, topology-change. .DESCRIPTION The OPC UA Client driver reads from an upstream OPC UA server (default: Microsoft's opc-plc simulator on opc.tcp://localhost:50000) and re-exposes its address space through the local OtOpcUa server. This script drives the bridged path end-to-end via `otopcua-cli`. Four stages: 1. Probe — otopcua-cli connect succeeds against the OtOpcUa server; confirms the gateway is up. 2. Bridged read — otopcua-cli read on the bridged NodeId returns a Good value with a non-null payload; proves the IReadable.ReadAsync path round-trips through the driver to the upstream simulator. 3. Subscribe — otopcua-cli subscribe observes a data change within N seconds (opc-plc's StepUp ticks once per second by default, so this should always see a change). 4. Topology change — assert the auto-reimport-on-ModelChangeEvent path is wired up. We can't easily fire a real upstream model change without elevated opc-plc access, so this stage prints the option settings + asserts the driver's diagnostic surface reflects WatchModelChanges is enabled (or skips with INFO when the upstream doesn't expose ModelChangeEventType). Requires: - a running OtOpcUa server whose config DB has an OpcUaClient DriverInstance bound to opc-plc (or another upstream server) - the upstream OPC UA simulator reachable at $UpstreamUrl - a Tag bridged from upstream NodeId $UpstreamNodeId to local $BridgedNodeId .PARAMETER OpcUaUrl Endpoint URL of the OtOpcUa server. Default opc.tcp://localhost:4840. .PARAMETER UpstreamUrl Endpoint URL of the upstream OPC UA server (for documentation; the bridge itself is wired in the OtOpcUa server config). Default opc.tcp://localhost:50000. .PARAMETER BridgedNodeId Local NodeId the OtOpcUa server exposes for the upstream tag. Required — set per your server config (e.g. 'ns=2;s=/warsaw/opc-plc/StepUp'). .PARAMETER UpstreamNodeId The upstream NodeId being bridged (informational only; default 'ns=3;s=StepUp' which is opc-plc's monotonically-increasing UInt32). .PARAMETER ChangeWaitSec How long the subscribe stage waits for a data-change. Default 10s. .PARAMETER ReverseConnect When set, the script asserts the gateway is configured for reverse-connect (server-initiated) mode. The OtOpcUa server's DriverConfig for the OpcUaClient instance must already have ReverseConnect.Enabled=true + ListenerUrl set; this script doesn't reconfigure the driver, only verifies the bridged path still reads end-to-end with the listener up. The reverse-connect topology is opaque to the downstream OPC UA client (us), so the read assertion is identical to the dial-mode path — the value of running the script in this mode is to catch regressions where reverse-connect breaks the post-init capability surface. .PARAMETER ReverseListenerUrl Documentation-only. The listener URL the gateway is expected to be bound to when -ReverseConnect is set; printed in the run banner so operators can cross-check their server config. Default opc.tcp://0.0.0.0:4844. .EXAMPLE .\test-opcuaclient.ps1 -BridgedNodeId "ns=2;s=/warsaw/opc-plc/StepUp" .EXAMPLE # OT-DMZ deployment: the upstream dials the gateway. The script flow is the # same — we still drive the bridged read through the OtOpcUa server — but the # banner reflects the reverse-connect topology. .\test-opcuaclient.ps1 -BridgedNodeId "ns=2;s=/warsaw/opc-plc/StepUp" -ReverseConnect #> param( [string]$OpcUaUrl = "opc.tcp://localhost:4840", [string]$UpstreamUrl = "opc.tcp://localhost:50000", [Parameter(Mandatory)] [string]$BridgedNodeId, [string]$UpstreamNodeId = "ns=3;s=StepUp", [int]$ChangeWaitSec = 10, [switch]$ReverseConnect, [string]$ReverseListenerUrl = "opc.tcp://0.0.0.0:4844", # PR-12: HistoryReadEvents passthrough check. Requires the upstream to be running # in alarm-history mode (opc-plc --alm) AND the OtOpcUa server to expose a notifier # node bridged to the upstream's events source. The CLI doesn't have a dedicated # event-history command yet; this stage runs a regular historyread against the # bridged notifier and confirms the gateway round-trips the request without # surfacing BadHistoryOperationUnsupported, which would indicate the filter-aware # ReadEventsAsync path lost wiring. [switch]$HistoryEvents, [string]$EventsNotifierNodeId = "i=2253", # PR-14: upstream-redundancy probe. Passes the primary + secondary URLs # straight through to the gateway driver via DriverConfig (operator must have # already wired Redundancy.Enabled=true on the OpcUaClient instance — this # script doesn't reconfigure the driver, only verifies the bridged read still # works while both upstreams are reachable, and that the driver's redundancy # diagnostics are non-null). Stage is no-op when neither URL is provided. [string]$PrimaryUrl, [string]$SecondaryUrl ) $ErrorActionPreference = "Stop" . "$PSScriptRoot/_common.ps1" $opcUaCli = Get-CliInvocation ` -ProjectFolder "src/ZB.MOM.WW.OtOpcUa.Client.CLI" ` -ExeName "otopcua-cli" if ($ReverseConnect) { Write-Host "[INFO] -ReverseConnect set: gateway is expected to be bound to listener $ReverseListenerUrl" Write-Host "[INFO] Upstream OPC UA server should be configured with --rc=$ReverseListenerUrl (or equivalent on a real server)" } $results = @() # Stage 1: probe $results += Test-Probe ` -Name "OpcUaClient probe" ` -Cmd $opcUaCli ` -Args @("connect", "-u", $OpcUaUrl) # Stage 2: bridged read $results += Test-Probe ` -Name "OpcUaClient bridged read" ` -Cmd $opcUaCli ` -Args @("read", "-u", $OpcUaUrl, "-n", $BridgedNodeId) # Stage 3: subscribe-sees-change Write-Host "[INFO] Subscribing to $BridgedNodeId for ${ChangeWaitSec}s..." $subResults = & $opcUaCli.Cmd @($opcUaCli.Args + @( "subscribe", "-u", $OpcUaUrl, "-n", $BridgedNodeId, "-i", "500", "--duration", "$ChangeWaitSec")) if ($LASTEXITCODE -eq 0 -and $subResults -match "DataChange|StepUp|value=") { $results += [pscustomobject]@{ Stage = "Subscribe-sees-change"; Status = "PASS" } } else { $results += [pscustomobject]@{ Stage = "Subscribe-sees-change"; Status = "FAIL" } } # Stage 4: topology change (auto-reimport on ModelChangeEvent) # # The OPC UA Client driver subscribes to BaseModelChangeEventType on the # upstream Server node (i=2253) at the end of InitializeAsync, then debounces # events over OpcUaClientDriverOptions.ModelChangeDebounce (default 5s) and # triggers ReinitializeAsync. # # Driving a real upstream ModelChangeEvent from outside the simulator is # upstream-specific: # - opc-plc: invoke OpcPlc.AddSlowNode via OPC UA Call (requires a session # directly to opc-plc, not via the gateway, since the gateway exposes # mirrored read/write paths only for variables — methods are mirrored # under PR-9 but call permissions on the simulator's namespace may # not allow downstream invocation). # - production server: deploy a topology-change to the upstream server + # observe the local re-import. # # This stage is therefore documentation-only by default. Set # $env:OPCUACLIENT_TOPOLOGY_TRIGGER_CMD to a command that drives a real # topology change on the upstream and we'll execute it + wait for the # debounced re-import. $triggerCmd = $env:OPCUACLIENT_TOPOLOGY_TRIGGER_CMD if ($triggerCmd) { Write-Host "[INFO] Driving topology change via: $triggerCmd" & cmd.exe /c $triggerCmd Start-Sleep -Seconds 8 # debounce window + re-import duration # After re-import the bridged node should still be readable (or, if # the upstream removed the node, the read should return BadNodeIdUnknown). # Either way the gateway must remain healthy. $results += Test-Probe ` -Name "Topology-change re-read" ` -Cmd $opcUaCli ` -Args @("read", "-u", $OpcUaUrl, "-n", $BridgedNodeId) } else { Write-Host "[INFO] Topology-change stage skipped (set OPCUACLIENT_TOPOLOGY_TRIGGER_CMD to drive a real upstream model change)." $results += [pscustomobject]@{ Stage = "Topology-change"; Status = "SKIP" } } # Stage 5 (gated): HistoryReadEvents passthrough # # PR-12 lands the filter-aware IHistoryProvider.ReadEventsAsync overload on the # OPC UA Client driver. End-to-end coverage requires: # (a) the upstream in alarm-history mode (opc-plc --alm or a real server); # (b) the OtOpcUa server forwarding HistoryReadEvents to the gateway driver. # Gated behind -HistoryEvents because the default opc-plc fixture image isn't # launched with --alm. When set, the stage issues a historyread against the # bridged notifier ($EventsNotifierNodeId) and confirms the gateway returns # the request without BadHistoryOperationUnsupported. # Stage 6 (gated): upstream-redundancy probe (PR-14) # # When -PrimaryUrl + -SecondaryUrl are both supplied, the script runs an extra # read against the bridged NodeId and reports whether the gateway is still # answering. The actual ServiceLevel-driven failover is observable only on the # server side (driver-diagnostics RPC reports RedundancyFailoverCount); this # stage is a smoke check that the bridged path keeps round-tripping while # both upstreams are reachable. Drive a real failover by writing to the # primary's ServiceLevel node from outside this script. if ($PrimaryUrl -and $SecondaryUrl) { Write-Host "[INFO] Upstream redundancy probe: primary=$PrimaryUrl secondary=$SecondaryUrl" $results += Test-Probe ` -Name "OpcUaClient redundancy bridged-read" ` -Cmd $opcUaCli ` -Args @("read", "-u", $OpcUaUrl, "-n", $BridgedNodeId) } else { if (-not $PrimaryUrl -and -not $SecondaryUrl) { Write-Host "[INFO] Upstream redundancy stage skipped (set -PrimaryUrl and -SecondaryUrl to enable)." $results += [pscustomobject]@{ Stage = "Upstream-redundancy"; Status = "SKIP" } } } if ($HistoryEvents) { Write-Host "[INFO] HistoryEvents stage: issuing historyread against $EventsNotifierNodeId" $start = (Get-Date).ToUniversalTime().AddMinutes(-30).ToString("o") $end = (Get-Date).ToUniversalTime().AddMinutes(1).ToString("o") $eventOut = & $opcUaCli.Cmd @($opcUaCli.Args + @( "historyread", "-u", $OpcUaUrl, "-n", $EventsNotifierNodeId, "--start", $start, "--end", $end)) if ($LASTEXITCODE -eq 0 -and $eventOut -notmatch "BadHistoryOperationUnsupported") { $results += [pscustomobject]@{ Stage = "HistoryReadEvents"; Status = "PASS" } } elseif ($eventOut -match "BadHistoryOperationUnsupported") { Write-Host "[INFO] Upstream returned BadHistoryOperationUnsupported — re-run with --alm + a notifier that has event history." $results += [pscustomobject]@{ Stage = "HistoryReadEvents"; Status = "SKIP" } } else { $results += [pscustomobject]@{ Stage = "HistoryReadEvents"; Status = "FAIL" } } } Write-Host "" Write-Host "=== test-opcuaclient.ps1 results ===" $results | Format-Table -AutoSize $failed = $results | Where-Object { $_.Status -eq "FAIL" } if ($failed) { exit 1 } exit 0