Files
lmxopcua/scripts/e2e/test-focas.ps1
2026-04-26 05:45:13 -04:00

204 lines
9.0 KiB
PowerShell

#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 }