#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" ) $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" } } 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