<# .SYNOPSIS Captures the native AVEVA client's StartQuery2 request bytes for analog/state summary queries (HCAL roadmap R1.8/R1.9) so the managed SDK's summary request shape can be decoded against ground truth instead of guessed. .DESCRIPTION Drives the .NET-Framework NativeTraceHarness against the live Historian with an IL-rewritten copy of aahClientManaged.dll whose ClientMessageEncoder.WriteMessage is instrumented to log every outgoing MDAS body (the same pipeline that produced every other proven request shape). For each candidate HistoryQueryArgs config it writes a per-config NDJSON capture under artifacts/reverse-engineering/instrumented-wcf-writemessage-summary/ (gitignored). The default matrix is: - baseline-full : RetrievalMode=Full (the known-good non-summary request) - analog-avg : RetrievalMode=Cyclic + ValueSelector=Average + Resolution - analog-min : RetrievalMode=Cyclic + ValueSelector=Minimum + Resolution - analog-agg-avg : RetrievalMode=Cyclic + AggregationType=Average + Resolution - state-summary : RetrievalMode=Cyclic + MaxStates>0 + Resolution Diff any candidate against baseline-full (scripts/decode-summary-capture.py) to read off the exact QueryType / SummaryType / AutoSummaryParameters bytes the native client sets for a summary, then implement the managed request against that. .NOTES Artifacts are diagnostic. Sanitize before copying anything into docs/ — never commit raw capture NDJSON, credentials, hostnames, or customer tag names. #> [CmdletBinding()] param( [string]$ServerName = "localhost", [int]$TcpPort = 32568, # SysTimeSec is the local data-bearing system tag (OtOpcUaParityTest_001.Counter is stale/empty). [string]$TagName = "SysTimeSec", [int]$LookbackMinutes = 240, [int]$MaxRows = 4, # 1-hour summary cycle in 100ns ticks (1h = 36,000,000,000 ticks). [uint64]$ResolutionTicks = 36000000000, [string]$Configuration = "Debug", # Restrict the run to a single named config from the matrix (default: run all). [string]$OnlyConfig = "", # Also instrument ReadMessage so each capture includes the incoming WCF response bodies # (the GetNextQueryResultBuffer2 pResultBuff summary rows). Decoded by decode-summary-response.py. [switch]$WithResponse ) $ErrorActionPreference = "Stop" $repoRoot = Split-Path -Parent $PSScriptRoot Set-Location $repoRoot $reProj = Join-Path $repoRoot "tools\AVEVA.Historian.ReverseEngineering\AVEVA.Historian.ReverseEngineering.csproj" $harnessProj = Join-Path $repoRoot "tools\AVEVA.Historian.NativeTraceHarness\AVEVA.Historian.NativeTraceHarness.csproj" $instrProj = Join-Path $repoRoot "tools\AVEVA.Historian.ReverseInstrumentation\AVEVA.Historian.ReverseInstrumentation.csproj" $captureDir = Join-Path $repoRoot "artifacts\reverse-engineering\instrumented-wcf-writemessage-summary" $currentCopy = Join-Path $captureDir "current-copy" $instrDll = Join-Path $captureDir "aahClientManaged.dll" Write-Host "== Building tooling ($Configuration) ==" -ForegroundColor Cyan dotnet build $reProj -c $Configuration --nologo -v q | Out-Null dotnet build $instrProj -c $Configuration --nologo -v q | Out-Null dotnet build $harnessProj -c $Configuration --nologo -v q | Out-Null $instrSourceDll = Get-ChildItem -Recurse (Join-Path $repoRoot "tools\AVEVA.Historian.ReverseInstrumentation\bin\$Configuration") ` -Filter "AVEVA.Historian.ReverseInstrumentation.dll" | Select-Object -First 1 -ExpandProperty FullName if (-not $instrSourceDll) { throw "ReverseInstrumentation.dll not found under bin\$Configuration." } Write-Host "== Instrumenting WriteMessage$(if ($WithResponse) { ' + ReadMessage' }) ==" -ForegroundColor Cyan New-Item -ItemType Directory -Force -Path $captureDir | Out-Null if ($WithResponse) { # Chain via a distinct intermediate file (reading+writing the same path drops the second # hook on the mixed-mode native image). Final dll carries both hooks with distinct Phase # strings: WCF.WriteMessage.Body and WCF.ReadMessage.Body. $writeOnly = Join-Path $captureDir "aahClientManaged.write.dll" dotnet run --no-build -c $Configuration --project $reProj -- ` instrument-wcf-writemessage (Join-Path $repoRoot "current\aahClientManaged.dll") $writeOnly | Out-Null dotnet run --no-build -c $Configuration --project $reProj -- ` instrument-wcf-readmessage $writeOnly $instrDll | Out-Null } else { dotnet run --no-build -c $Configuration --project $reProj -- ` instrument-wcf-writemessage (Join-Path $repoRoot "current\aahClientManaged.dll") $instrDll | Out-Null } Write-Host "== Staging current-copy ==" -ForegroundColor Cyan # Mirror current/ into current-copy, then overwrite the managed dll with the instrumented # build and drop the strong-named logger assembly alongside it so the injected call binds. robocopy (Join-Path $repoRoot "current") $currentCopy /MIR /NJH /NJS /NDL /NP /NC /NS | Out-Null Copy-Item -Force $instrDll (Join-Path $currentCopy "aahClientManaged.dll") Copy-Item -Force $instrSourceDll (Join-Path $currentCopy "AVEVA.Historian.ReverseInstrumentation.dll") # Candidate matrix: name + harness arg list. Summary configs all use Cyclic + a resolution; # the differentiator is which summary knob is set. $matrix = @( @{ Name = "baseline-full"; Args = @("--retrieval-mode", "Full") }, @{ Name = "analog-avg"; Args = @("--retrieval-mode", "Cyclic", "--value-selector", "Average", "--resolution-ticks", "$ResolutionTicks") }, @{ Name = "analog-min"; Args = @("--retrieval-mode", "Cyclic", "--value-selector", "Minimum", "--resolution-ticks", "$ResolutionTicks") }, @{ Name = "analog-max"; Args = @("--retrieval-mode", "Cyclic", "--value-selector", "Maximum", "--resolution-ticks", "$ResolutionTicks") }, @{ Name = "analog-integral"; Args = @("--retrieval-mode", "Cyclic", "--value-selector", "Integral", "--resolution-ticks", "$ResolutionTicks") }, @{ Name = "mode-integral"; Args = @("--retrieval-mode", "Integral", "--resolution-ticks", "$ResolutionTicks") }, @{ Name = "mode-twavg"; Args = @("--retrieval-mode", "TimeWeightedAverage", "--resolution-ticks", "$ResolutionTicks") }, @{ Name = "analog-agg-avg"; Args = @("--retrieval-mode", "Cyclic", "--aggregation-type", "Average", "--resolution-ticks", "$ResolutionTicks") }, @{ Name = "state-summary"; Args = @("--retrieval-mode", "Cyclic", "--max-states", "10", "--resolution-ticks", "$ResolutionTicks") } ) if ($OnlyConfig) { $matrix = $matrix | Where-Object { $_.Name -eq $OnlyConfig } } if (-not $matrix) { throw "No matrix entry named '$OnlyConfig'." } $harnessDll = Join-Path $currentCopy "aahClientManaged.dll" $summary = @() foreach ($cfg in $matrix) { $name = $cfg.Name $capturePath = Join-Path $captureDir "summary-capture-$name-latest.ndjson" if (Test-Path $capturePath) { Remove-Item -Force $capturePath } $env:AVEVA_HISTORIAN_RE_CAPTURE = $capturePath Write-Host "== Capturing: $name ==" -ForegroundColor Green $harnessArgs = @( "--scenario", "history", "--server-name", $ServerName, "--tcp-port", "$TcpPort", "--tag", $TagName, "--lookback-minutes", "$LookbackMinutes", "--max-rows", "$MaxRows", "--current-dir", $currentCopy, "--managed-dll-path", $harnessDll ) + $cfg.Args # Don't let a single config that errors (e.g. state summary on an analog tag) abort the # whole matrix, and don't treat dotnet's stderr noise as a terminating error. try { $prevEap = $ErrorActionPreference $ErrorActionPreference = "Continue" & dotnet run --no-build -c $Configuration --project $harnessProj -- @harnessArgs 2>&1 | Out-Null } catch { Write-Host " (config '$name' raised: $($_.Exception.Message))" -ForegroundColor Yellow } finally { $ErrorActionPreference = $prevEap } $recCount = if (Test-Path $capturePath) { (Get-Content $capturePath | Where-Object { $_.Trim() }).Count } else { 0 } Write-Host " -> $recCount records -> $capturePath" $summary += [pscustomobject]@{ Config = $name; Records = $recCount; Capture = $capturePath } } Remove-Item Env:\AVEVA_HISTORIAN_RE_CAPTURE -ErrorAction SilentlyContinue Write-Host "`n== Capture summary ==" -ForegroundColor Cyan $summary | Format-Table -AutoSize Write-Host "Decode with: python scripts\decode-summary-capture.py" -ForegroundColor Cyan