#Requires -Version 7.0 <# .SYNOPSIS End-to-end CLI test for the FOCAS (Fanuc CNC) driver. .DESCRIPTION **Hardware-gated.** There is no public FOCAS simulator; the driver's FwlibFocasClient P/Invokes Fanuc's licensed Fwlib32.dll. Against a dev box without the DLL on PATH the test will skip with a clear message. Against a real CNC with the DLL present it runs probe / driver-loopback / server-bridge the same way the other scripts do. Set FOCAS_TRUST_WIRE=1 when -CncHost points at a real CNC to un-gate. .PARAMETER CncHost IP or hostname of the CNC. Default 127.0.0.1 — override for real runs. .PARAMETER CncPort FOCAS TCP port. Default 8193. .PARAMETER Address FOCAS address to exercise. Default R100 (PMC R-file register). .PARAMETER OpcUaUrl OtOpcUa server endpoint. .PARAMETER BridgeNodeId NodeId at which the server publishes the Address. .PARAMETER Write Issue #268 (F4-a) + #269 (F4-b) — opts the script into write stages. Without -Write the script runs read-only probe / loopback / bridge coverage. With -Write the script additionally exercises the F4-b cnc_wrparam + cnc_wrmacro round-trip stages against the configured -ParamAddress / -MacroAddress (default safe values). The wire writes fire only when FOCAS_TRUST_WIRE=1 (already gated above) AND the operator explicitly requests the write path. .PARAMETER ParamAddress Parameter address for the F4-b write stage (default PARAM:1815). Only used when -Write is supplied. Pick a parameter that's safe to scribble on for your CNC setup — the default is benign for a stock Fanuc 30i but every site differs. .PARAMETER MacroAddress Macro variable for the F4-b write stage (default MACRO:500). Macro writes are the lowest-risk write surface (no parameter-write switch needed, no MDI mode required) so this stage runs whenever -Write is supplied. .PARAMETER PmcBitAddress PMC bit address for the F4-c bit-write round-trip stage (default R100.3). Only fires when -Write is supplied AND the operator double-opts in via FOCAS_PMC_WRITE=1, mirroring the FOCAS_PARAM_WRITE gate. PMC writes have a higher blast radius than PARAM/MACRO (a mistargeted bit can move motion or latch a feedhold) so the gate is off by default — see docs/v2/focas-deployment.md "Write safety / PMC pre-checks". .PARAMETER CncPassword Issue #271 (F4-d) — optional CNC connection-level password emitted via cnc_wrunlockparam on connect. Required only when the controller gates parameter writes behind a password switch (16i + some 30i firmwares with parameter-protect on). Threaded through to every CLI invocation in the -Write stage as --cnc-password. PASSWORD INVARIANT: never logged — the CLI's Serilog config does not destructure this flag. See docs/v2/focas-deployment.md § "FOCAS password handling" for the no-log invariant + rotation runbook. #> param( [string]$CncHost = "127.0.0.1", [int]$CncPort = 8193, [string]$Address = "R100", [string]$OpcUaUrl = "opc.tcp://localhost:4840", [Parameter(Mandatory)] [string]$BridgeNodeId, [switch]$Write, [string]$ParamAddress = "PARAM:1815", [string]$MacroAddress = "MACRO:500", [string]$PmcBitAddress = "R100.3", [string]$CncPassword = "" ) $ErrorActionPreference = "Stop" . "$PSScriptRoot/_common.ps1" if (-not ($env:FOCAS_TRUST_WIRE -eq "1" -or $env:FOCAS_TRUST_WIRE -eq "true")) { Write-Skip "FOCAS_TRUST_WIRE not set — no public simulator exists (task #222 tracks the lab rig). Set =1 when -CncHost points at a real CNC with Fwlib32.dll on PATH." exit 0 } $focasCli = Get-CliInvocation ` -ProjectFolder "src/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Cli" ` -ExeName "otopcua-focas-cli" $opcUaCli = Get-CliInvocation ` -ProjectFolder "src/ZB.MOM.WW.OtOpcUa.Client.CLI" ` -ExeName "otopcua-cli" $commonFocas = @("-h", $CncHost, "-p", $CncPort) # F4-d (issue #271) — thread the CNC connection password through to every CLI # invocation. The CLI's --cnc-password flag emits cnc_wrunlockparam on connect # and the driver's per-call retry path re-issues unlock + retries once on # EW_PASSWD. PASSWORD INVARIANT: the password is NOT logged here. Write-Host # and Test-* helpers never destructure $commonFocas, but we still avoid # Write-Host'ing the array directly; the CLI's Serilog config also redacts. if (-not [string]::IsNullOrWhiteSpace($CncPassword)) { $commonFocas += @("--cnc-password", $CncPassword) } $results = @() $results += Test-Probe ` -Cli $focasCli ` -ProbeArgs (@("probe") + $commonFocas + @("-a", $Address, "--type", "Int16")) $writeValue = Get-Random -Minimum 1 -Maximum 9999 $results += Test-DriverLoopback ` -Cli $focasCli ` -WriteArgs (@("write") + $commonFocas + @("-a", $Address, "-t", "Int16", "-v", $writeValue)) ` -ReadArgs (@("read") + $commonFocas + @("-a", $Address, "-t", "Int16")) ` -ExpectedValue "$writeValue" $bridgeValue = Get-Random -Minimum 10000 -Maximum 19999 $results += Test-ServerBridge ` -DriverCli $focasCli ` -DriverWriteArgs (@("write") + $commonFocas + @("-a", $Address, "-t", "Int16", "-v", $bridgeValue)) ` -OpcUaCli $opcUaCli ` -OpcUaUrl $OpcUaUrl ` -OpcUaNodeId $BridgeNodeId ` -ExpectedValue "$bridgeValue" $reverseValue = Get-Random -Minimum 20000 -Maximum 29999 $results += Test-OpcUaWriteBridge ` -OpcUaCli $opcUaCli ` -OpcUaUrl $OpcUaUrl ` -OpcUaNodeId $BridgeNodeId ` -DriverCli $focasCli ` -DriverReadArgs (@("read") + $commonFocas + @("-a", $Address, "-t", "Int16")) ` -ExpectedValue "$reverseValue" $subValue = Get-Random -Minimum 30000 -Maximum 32766 $results += Test-SubscribeSeesChange ` -OpcUaCli $opcUaCli ` -OpcUaUrl $OpcUaUrl ` -OpcUaNodeId $BridgeNodeId ` -DriverCli $focasCli ` -DriverWriteArgs (@("write") + $commonFocas + @("-a", $Address, "-t", "Int16", "-v", $subValue)) ` -ExpectedValue "$subValue" if ($Write) { # F4-b — macro + parameter round-trip writes. Both stages use the same # write-then-read shape the existing PMC stages use; the per-tag value # comes back through Test-DriverLoopback's read step. # # Macro writes run unconditionally when -Write is supplied — no MDI / no # parameter-write switch dependency, lowest-risk write surface on a CNC. $macroValue = Get-Random -Minimum 100 -Maximum 9999 $results += Test-DriverLoopback ` -Cli $focasCli ` -WriteArgs (@("write") + $commonFocas + @("-a", $MacroAddress, "-t", "Int32", "-v", $macroValue)) ` -ReadArgs (@("read") + $commonFocas + @("-a", $MacroAddress, "-t", "Int32")) ` -ExpectedValue "$macroValue" # Parameter writes only fire when the operator double-opts in via # FOCAS_PARAM_WRITE=1. The CNC must be in MDI mode + parameter-write # switch enabled or every write returns EW_PASSWD (BadUserAccessDenied); # without an opt-in the script won't even attempt the write. F4-d will # land an OPC UA-side unlock workflow that lets this stage run without # the pendant. if ($env:FOCAS_PARAM_WRITE -eq "1" -or $env:FOCAS_PARAM_WRITE -eq "true") { $paramValue = Get-Random -Minimum 100 -Maximum 9999 $results += Test-DriverLoopback ` -Cli $focasCli ` -WriteArgs (@("write") + $commonFocas + @("-a", $ParamAddress, "-t", "Int32", "-v", $paramValue)) ` -ReadArgs (@("read") + $commonFocas + @("-a", $ParamAddress, "-t", "Int32")) ` -ExpectedValue "$paramValue" } else { Write-Host "[skip] FOCAS_PARAM_WRITE not set — parameter-write stage requires the CNC to be in MDI mode + parameter-write switch enabled (see docs/v2/focas-deployment.md 'Write safety')." } # F4-c — PMC bit round-trip. PMC writes have a higher blast radius # than PARAM/MACRO (a mistargeted bit can move motion or latch a # feedhold) so the stage is gated on a separate FOCAS_PMC_WRITE=1 # opt-in. The bit write exercises the driver's read-modify-write # path: write 'on' -> read returns 'on'; write 'off' -> read returns # 'off'. Both halves run so a regression in either branch is caught. if ($env:FOCAS_PMC_WRITE -eq "1" -or $env:FOCAS_PMC_WRITE -eq "true") { $results += Test-DriverLoopback ` -Cli $focasCli ` -WriteArgs (@("write") + $commonFocas + @("-a", $PmcBitAddress, "-t", "Bit", "-v", "on")) ` -ReadArgs (@("read") + $commonFocas + @("-a", $PmcBitAddress, "-t", "Bit")) ` -ExpectedValue "True" $results += Test-DriverLoopback ` -Cli $focasCli ` -WriteArgs (@("write") + $commonFocas + @("-a", $PmcBitAddress, "-t", "Bit", "-v", "off")) ` -ReadArgs (@("read") + $commonFocas + @("-a", $PmcBitAddress, "-t", "Bit")) ` -ExpectedValue "False" } else { Write-Host "[skip] FOCAS_PMC_WRITE not set — PMC bit-write round-trip is off by default because a mistargeted PMC bit can move motion or latch a feedhold (see docs/v2/focas-deployment.md 'PMC pre-checks')." } } Write-Summary -Title "FOCAS e2e" -Results $results if ($results | Where-Object { -not $_.Passed }) { exit 1 }