diff --git a/playbook/shopfloor-setup/gea-shopfloor-waxtrace/scripts/Backup-FormtracepakSettings.ps1 b/playbook/shopfloor-setup/gea-shopfloor-waxtrace/scripts/Backup-FormtracepakSettings.ps1 index d481f0b..8ccf370 100755 --- a/playbook/shopfloor-setup/gea-shopfloor-waxtrace/scripts/Backup-FormtracepakSettings.ps1 +++ b/playbook/shopfloor-setup/gea-shopfloor-waxtrace/scripts/Backup-FormtracepakSettings.ps1 @@ -149,6 +149,28 @@ $RegistryRoots = @( "HKLM:\SOFTWARE\WOW6432Node\FORMPAK" "HKCU:\SOFTWARE\FORMPAK" "HKLM:\SOFTWARE\SURFPAK" +) + +# Sweep every loaded user hive under HKEY_USERS for Mitutoyo/FORMTRACEPAK/ +# FORMPAK/SURFPAK subkeys. Wax/Trace bays run as a domain "Shopfloor" account, +# whose HKCU prefs are NOT visible via HKCU:\ when the script runs as +# SupportUser / SYSTEM. As long as the Shopfloor user has a logged-in session +# (interactive or disconnected) the hive is mounted under HKEY_USERS\. +# Skip the special SIDs (S-1-5-18 = SYSTEM, S-1-5-19/20 = LocalService / +# NetworkService) and the *_Classes subhives. +$userHiveSubpaths = @('Software\Mitutoyo','Software\FORMTRACEPAK','Software\FORMPAK','Software\SURFPAK') +foreach ($u in (Get-ChildItem 'Registry::HKEY_USERS' -ErrorAction SilentlyContinue)) { + $sid = $u.PSChildName + if ($sid -notmatch '^S-1-5-21-' ) { continue } # only real user SIDs + if ($sid -match '_Classes$') { continue } + foreach ($sp in $userHiveSubpaths) { + $candidate = "Registry::HKEY_USERS\$sid\$sp" + if (Test-Path -LiteralPath $candidate) { + $RegistryRoots += $candidate + } + } +} +$RegistryRoots = $RegistryRoots + @( "HKCU:\SOFTWARE\SURFPAK" ) @@ -304,8 +326,13 @@ foreach ($root in $RegistryRoots) { $safeName = ($root -replace '[:\\]', '_').Trim('_') $regFilePath = Join-Path $regStage "${safeName}.reg" - $nativeRoot = $root -replace '^HKLM:', 'HKEY_LOCAL_MACHINE' ` - -replace '^HKCU:', 'HKEY_CURRENT_USER' + # Normalize the PSPath-style root into a form reg.exe accepts. + # HKLM:\... -> HKEY_LOCAL_MACHINE\... + # HKCU:\... -> HKEY_CURRENT_USER\... + # Registry::HKEY_USERS\\... -> HKEY_USERS\\... + $nativeRoot = $root -replace '^HKLM:\\', 'HKEY_LOCAL_MACHINE\' ` + -replace '^HKCU:\\', 'HKEY_CURRENT_USER\' ` + -replace '^Registry::', '' try { $proc = Start-Process -FilePath 'reg.exe' ` -ArgumentList "export `"$nativeRoot`" `"$regFilePath`" /y" ` @@ -359,47 +386,154 @@ if ($regCsvRows.Count -gt 0) { -NoTypeInformation -Encoding UTF8 } -# ---------------------------------------------------------------------- 5. Detect installed version ---------------------------------------------------------------------- +# ---------------------------------------------------------------------- 5. Detect installed version + model ---------------------------------------------------------------------- +# +# Evidence sources, established empirically by installing FormTracePak v6.213 +# on the win11 VM and probing the resulting registry / disk layout +# (2026-05-24): +# +# - bay-config.csv: authoritative for the per-asset marketing version +# ("6.213", "6.103", ...) AND model ("AVANT", "CV-4500", "CV-3200"). +# This is the source of truth for the 15 known wax/trace bays. +# +# - Formtracepak.exe VersionInfo (FileVersion / ProductVersion): the +# binary version on disk (e.g. 6.2.0.51). Does NOT match the +# bay-config marketing format directly (Mitutoyo numbers their +# binary releases differently from their MSI release labels), but is +# concrete on-disk evidence the install is real. +# +# - HKLM:\SOFTWARE\WOW6432Node\Mitutoyo\FORMPAK\Config\device map\DeviceName: +# the active controller / model picked at install time. Real values +# include "FORMTRACER Avant", "CV-4500", "CV-3200", "Contracer CV-3200". +# The Mitutoyo "Surfpak\FormMes\MachineInfo\Machine\Machine*" rows are +# installer templates listing every supported machine; ignore them. +# +# - Mitutoyo uninstall registry entries: useless for version. The +# "Formtracepak" uninstall entry has DisplayVersion = '' (empty), +# and the WinUSB driver-package entries falsely match a broad +# "Mitutoyo" regex with bogus dates like "01/26/2014 1.0.0.0". +# Not consulted. +# +# manifest.json now records both the bay-config-predicted values and the +# on-disk evidence, plus a Match flag so any drift between what's +# supposed to be installed and what actually is gets surfaced at restore +# time without needing a separate audit. -$detectedVersion = 'Unknown' -$uninstallPaths = @( - 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall', - 'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall', - 'HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall' -) -foreach ($uPath in $uninstallPaths) { - if (-not (Test-Path $uPath)) { continue } - $entries = Get-ChildItem -Path $uPath -ErrorAction SilentlyContinue - foreach ($entry in $entries) { +# --- bay-config.csv predicted values --- +$bayConfigVersion = $null +$bayConfigModel = $null +$bayConfigSource = $null +if ($AssetNumber) { + $bayCfg = @( + (Join-Path $scriptDir 'bay-config.csv'), + 'C:\WaxTrace-Install\bay-config.csv', + 'Y:\installers-post\waxtrace\bay-config.csv' + ) | Where-Object { $_ -and (Test-Path -LiteralPath $_) } | Select-Object -First 1 + if ($bayCfg) { try { - $props = Get-ItemProperty -Path $entry.PSPath -ErrorAction SilentlyContinue - if ($props.DisplayName -and $props.DisplayName -match 'FORMTRACEPAK|FormTrace') { - $detectedVersion = $props.DisplayVersion - break + $row = Import-Csv -LiteralPath $bayCfg | + Where-Object { $_.asset_tag -ieq $AssetNumber } | Select-Object -First 1 + if ($row) { + $bayConfigVersion = $row.ftpak_version + $bayConfigModel = $row.model + $bayConfigSource = $bayCfg } } catch { } } - if ($detectedVersion -ne 'Unknown') { break } +} + +# --- Formtracepak.exe FileVersion --- +$installedExeVersion = $null +$installedExePath = $null +$exeCandidates = @( + "${env:ProgramFiles(x86)}\MitutoyoApp\Formtracepak\Formtracepak.exe" + "${env:ProgramFiles}\MitutoyoApp\Formtracepak\Formtracepak.exe" + "${env:ProgramFiles(x86)}\Mitutoyo\Formtracepak\Formtracepak.exe" + "${env:ProgramFiles}\Mitutoyo\Formtracepak\Formtracepak.exe" + 'C:\Mitutoyo\Formtracepak\Formtracepak.exe' + 'D:\Mitutoyo\Formtracepak\Formtracepak.exe' +) +foreach ($exe in $exeCandidates) { + if (Test-Path -LiteralPath $exe) { + try { + $vi = (Get-Item -LiteralPath $exe).VersionInfo + $installedExeVersion = if ($vi.FileVersion) { $vi.FileVersion.Trim() } elseif ($vi.ProductVersion) { $vi.ProductVersion.Trim() } else { $null } + $installedExePath = $exe + break + } catch { } + } +} + +# --- Active device-map DeviceName + normalized model --- +$installedDeviceName = $null +$installedModelNormalized = $null +$deviceMapKey = 'HKLM:\SOFTWARE\WOW6432Node\Mitutoyo\FORMPAK\Config\device map' +if (Test-Path -LiteralPath $deviceMapKey) { + try { + $dm = Get-ItemProperty -LiteralPath $deviceMapKey -ErrorAction Stop + if ($dm.PSObject.Properties['DeviceName']) { + $installedDeviceName = [string]$dm.DeviceName + } + } catch { } +} +# Normalize DeviceName to bay-config.csv notation (AVANT / CV-4500 / CV-3200 / ...). +if ($installedDeviceName) { + switch -Regex ($installedDeviceName) { + '(?i)FORMTRACER\s*Avant|^AVANT$' { $installedModelNormalized = 'AVANT'; break } + '(?i)CV-?4500' { $installedModelNormalized = 'CV-4500'; break } + '(?i)CV-?3200' { $installedModelNormalized = 'CV-3200'; break } + '(?i)CV-?3100' { $installedModelNormalized = 'CV-3100'; break } + '(?i)CV-?4100' { $installedModelNormalized = 'CV-4100'; break } + '(?i)CV-?4200' { $installedModelNormalized = 'CV-4200'; break } + '(?i)Contracer|^CS-' { $installedModelNormalized = 'Contracer'; break } + '(?i)Surftest|^SV-' { $installedModelNormalized = 'Surftest'; break } + default { $installedModelNormalized = $installedDeviceName } + } +} + +# --- Reconcile + log --- +$bayConfigMatch = $true +if ($bayConfigModel -and $installedModelNormalized -and ($bayConfigModel -ine $installedModelNormalized)) { + $bayConfigMatch = $false +} +$detectedVersion = if ($bayConfigVersion) { $bayConfigVersion } elseif ($installedExeVersion) { $installedExeVersion } else { 'Unknown' } +$detectedModel = if ($bayConfigModel) { $bayConfigModel } elseif ($installedModelNormalized) { $installedModelNormalized } else { 'Unknown' } + +Write-Host "Bay config lookup : $(if ($bayConfigSource) { "$bayConfigSource (asset='$AssetNumber' -> ver='$bayConfigVersion' model='$bayConfigModel')" } else { 'asset not in any bay-config.csv' })" +Write-Host "Installed exe : $(if ($installedExePath) { "$installedExePath FileVersion=$installedExeVersion" } else { 'no Formtracepak.exe found on disk' })" +Write-Host "Installed device-map : $(if ($installedDeviceName) { "DeviceName='$installedDeviceName' -> normalized '$installedModelNormalized'" } else { "$deviceMapKey not present" })" +if (-not $bayConfigMatch) { + Write-Warning "Bay-config says model='$bayConfigModel' but device-map says '$installedModelNormalized' - DRIFT (recapture the bay-config.csv or the live install before restoring to a new bay)" } # ---------------------------------------------------------------------- Write manifest ---------------------------------------------------------------------- Write-Host "[4/4] Writing manifest..." -ForegroundColor Cyan $manifest = [PSCustomObject]@{ - SourceComputer = $env:COMPUTERNAME - BackupTimestamp = (Get-Date -Format 'o') - FormtracepakVersion = $detectedVersion - OSVersion = [System.Environment]::OSVersion.VersionString - PowerShellVersion = $PSVersionTable.PSVersion.ToString() - IncludesConfig = ($counters.Config -gt 0) - IncludesData = ($counters.Data -gt 0) - IncludesBinaries = ($counters.Binaries -gt 0) - IncludesRegistry = ($counters.RegKeys -gt 0) - ConfigFileCount = $counters.Config - DataFileCount = $counters.Data - BinaryFileCount = $counters.Binaries - RegistryValueCount = $counters.RegKeys - ErrorCount = $counters.Errors + SourceComputer = $env:COMPUTERNAME + AssetNumber = $AssetNumber + BackupTimestamp = (Get-Date -Format 'o') + FormtracepakVersion = $detectedVersion + Model = $detectedModel + BayConfigVersion = $bayConfigVersion + BayConfigModel = $bayConfigModel + BayConfigSource = $bayConfigSource + InstalledExeVersion = $installedExeVersion + InstalledExePath = $installedExePath + InstalledDeviceName = $installedDeviceName + InstalledModelNormalized= $installedModelNormalized + BayConfigMatch = $bayConfigMatch + OSVersion = [System.Environment]::OSVersion.VersionString + PowerShellVersion = $PSVersionTable.PSVersion.ToString() + IncludesConfig = ($counters.Config -gt 0) + IncludesData = ($counters.Data -gt 0) + IncludesBinaries = ($counters.Binaries -gt 0) + IncludesRegistry = ($counters.RegKeys -gt 0) + ConfigFileCount = $counters.Config + DataFileCount = $counters.Data + BinaryFileCount = $counters.Binaries + RegistryValueCount = $counters.RegKeys + ErrorCount = $counters.Errors } $manifest | ConvertTo-Json -Depth 3 |