Adds a filter-aware overload of IHistoryProvider.ReadEventsAsync that carries EventFilter SelectClauses + WhereClause, and implements it on the OPC UA Client driver via Session.HistoryReadAsync + ReadEventDetails. The change is additive (default-impl returns NotSupportedException) so the existing Galaxy.Proxy.GalaxyProxyDriver implementation keeps compiling against the fixed-field overload — no cross-driver refactor required. * Core.Abstractions: new EventHistoryRequest / SimpleAttributeSpec / ContentFilterSpec records mirror the OPC UA wire shape transport-neutrally. HistoricalEventBatch / HistoricalEventRow carry an open-ended Fields bag keyed by SimpleAttributeSpec.FieldName so server-side dispatch can re-align with the client's wire-side SelectClause order. * OpcUaClient driver: new ReadEventsAsync(fullReference, EventHistoryRequest, ct) builds an EventFilter, calls Session.HistoryReadAsync, and unwraps HistoryEvent.Events into HistoricalEventBatch rows. Default SelectClause set matches BuildHistoryEvent on the server side. ContentFilter bytes are decoded through the live session's MessageContext (passthrough — the driver does not evaluate filters). * Unit tests: 7 new tests cover SelectClause translation, default-clause fallback, malformed where-clause swallowing, uninitialized-driver guard, null-request guard, and IHistoryProvider default fallback. * Integration scaffold: build-only [Fact] gated on opc-plc --alm; flips to green when the fixture image is upgraded. * Docs: HistoryRead Events section in docs/drivers/OpcUaClient.md plus a cross-link from Client.CLI.md historyread page. * E2E: -HistoryEvents switch on scripts/e2e/test-opcuaclient.ps1 confirms the gateway round-trips HistoryReadEvents without BadHistoryOperationUnsupported (gated; defaults to skip). Closes #284
211 lines
9.7 KiB
PowerShell
211 lines
9.7 KiB
PowerShell
#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"
|
|
)
|
|
|
|
$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.
|
|
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
|