diff --git a/playbook/shopfloor-setup/Shopfloor/lib/Get-PCProfile.ps1 b/playbook/shopfloor-setup/Shopfloor/lib/Get-PCProfile.ps1
index 09d54a6..db2c38b 100644
--- a/playbook/shopfloor-setup/Shopfloor/lib/Get-PCProfile.ps1
+++ b/playbook/shopfloor-setup/Shopfloor/lib/Get-PCProfile.ps1
@@ -113,6 +113,28 @@ if ($siteConfig -and $siteConfig.pcProfiles -and $profileKey) {
}
}
}
+ # Subtype-strip fallback: a compound key like "gea-shopfloor-cmm-doda" (DODA
+ # CMM bays set pc-subtype.txt=doda) has no profile of its own. Fall back to
+ # the BARE pc-type ("gea-shopfloor-cmm" -> "CMM"). Without this the profile
+ # resolves to $null and callers silently use hardcoded defaults (wrong
+ # desktop icons + unwanted startup items).
+ if (-not $pcProfile -and $pcSubtype) {
+ $bare = $pcType
+ $pcProfile = $siteConfig.pcProfiles.$bare
+ if (-not $pcProfile) {
+ foreach ($g in $pcProfileAliasGroups) {
+ if ($g -icontains $bare) {
+ foreach ($alias in $g) {
+ if ($alias -ieq $bare) { continue }
+ $candidate = $siteConfig.pcProfiles.$alias
+ if ($candidate) { $pcProfile = $candidate; break }
+ }
+ break
+ }
+ }
+ }
+ if ($pcProfile) { $profileKey = $bare }
+ }
}
if ($pcProfile) {
diff --git a/playbook/shopfloor-setup/gea-shopfloor-cmm/09-Setup-CMM.ps1 b/playbook/shopfloor-setup/gea-shopfloor-cmm/09-Setup-CMM.ps1
index 4bf9d7b..bb7c442 100644
--- a/playbook/shopfloor-setup/gea-shopfloor-cmm/09-Setup-CMM.ps1
+++ b/playbook/shopfloor-setup/gea-shopfloor-cmm/09-Setup-CMM.ps1
@@ -193,6 +193,27 @@ try {
Write-CMMLog "Granted Users + Authenticated Users Modify on $frontEnd"
} catch { Write-CMMLog "Failed to ensure ${frontEnd}: $_" "WARN" }
+# ============================================================================
+# Step 2.5c: Active Setup - seed goCMM 2.12 DataFolder per-user for ALL users
+# ============================================================================
+# goCMM 2.12 stores its shared-data-dir in HKCU\Software\General Electric\goCMM
+# \DataFolder (decompiled: RegistrySettings uses Registry.CurrentUser - PER USER,
+# not HKLM). Setting it at imaging only touches the imaging user (SupportUser);
+# the ShopFloor operator's HKCU would be empty -> goCMM prompts them to pick the
+# folder on first launch. Active Setup runs StubPath once in EACH user's context
+# at first logon, so every user (SupportUser, ShopFloor, future) gets
+# DataFolder=C:\geaofi\. Bump Version to re-push. (Uses Set-ItemProperty in the
+# stub, not reg.exe, to avoid the trailing-backslash quoting trap.)
+$asKey = 'HKLM:\SOFTWARE\Microsoft\Active Setup\Installed Components\{8F3B2A10-7C4D-4E9A-B1F6-0A9C5D2E7B43}'
+try {
+ New-Item -Path $asKey -Force | Out-Null
+ Set-ItemProperty -Path $asKey -Name '(default)' -Value 'goCMM DataFolder (per-user)'
+ Set-ItemProperty -Path $asKey -Name 'Version' -Value '1'
+ $stub = 'powershell.exe -NoProfile -WindowStyle Hidden -Command "New-Item -Path ''HKCU:\SOFTWARE\General Electric\goCMM'' -Force | Out-Null; Set-ItemProperty -Path ''HKCU:\SOFTWARE\General Electric\goCMM'' -Name DataFolder -Value ''C:\geaofi\''"'
+ Set-ItemProperty -Path $asKey -Name 'StubPath' -Value $stub
+ Write-CMMLog "Seeded Active Setup: goCMM DataFolder=C:\geaofi\ (per-user, all users)"
+} catch { Write-CMMLog "Failed to seed Active Setup goCMM DataFolder: $_" 'WARN' }
+
# ============================================================================
# Step 2.6: First-run-as-admin for each installed PC-DMIS version
# ============================================================================
diff --git a/playbook/shopfloor-setup/gea-shopfloor-cmm/scripts/Convert-goCMMSettings.ps1 b/playbook/shopfloor-setup/gea-shopfloor-cmm/scripts/Convert-goCMMSettings.ps1
new file mode 100644
index 0000000..07b8e13
--- /dev/null
+++ b/playbook/shopfloor-setup/gea-shopfloor-cmm/scripts/Convert-goCMMSettings.ps1
@@ -0,0 +1,195 @@
+<#
+Convert-goCMMSettings.ps1
+
+Convert a legacy goCMM 1.1 ApplicationSettings.xml (C:\geaofi\) to the goCMM
+2.12.3 goCMMSettings.xml schema.
+
+The two are different SettingsModel schemas (different filename too). This is a
+TEMPLATE-based converter: new-only fields get defaults, the per-bay values are
+carried over from the old XML (+ optional overrides). Decisions baked in
+(2026-06-19): access-control left blank, operator key normalized to "Operator",
+cal intervals/restart use template defaults (old values NOT carried), Slack/Andon
+/Quindos/Emx dropped.
+
+Program paths default to the goCMM 2.12 convention (C:\geaofi\goCMM PC-DMIS
+Programs\) - override with -ResetProgram / -ProbeCalibrationProgram if the .PRG
+files live elsewhere. Those .PRG files MUST exist at the path written here.
+
+Usage:
+ .\Convert-goCMMSettings.ps1 -OldXml C:\geaofi\ApplicationSettings.xml `
+ -OutFile C:\geaofi\goCMMSettings.xml -PcdmisVersion 2016 -CmmId CMM10
+#>
+param(
+ [Parameter(Mandatory=$true)][string]$OldXml,
+ [Parameter(Mandatory=$true)][string]$OutFile,
+ [string]$PcdmisVersion, # 2016 | 2019 | 2026 (or full "PC-DMIS ... 64-bit")
+ [string]$CmmId, # e.g. CMM10 - used for the cal-program name default
+ [string]$PartGroup, # optional override (friendly S:\ or UNC); else from old XML
+ [string]$ResetProgram, # optional override
+ [string]$ProbeCalibrationProgram # optional override
+)
+$ErrorActionPreference = 'Stop'
+
+if (-not (Test-Path -LiteralPath $OldXml)) { throw "Old XML not found: $OldXml" }
+[xml]$old = Get-Content -LiteralPath $OldXml -Raw
+
+# --- part-group share canonicalization (FQDN + \SHARED, matches goCMM 2.12) ---
+function Canon([string]$pg) {
+ if (-not $pg) { return $pg }
+ $pg = $pg -replace '(?i)^S:\\', '\\tsgwp00525.wjs.geaerospace.net\SHARED\'
+ $pg = [regex]::Replace($pg, '(?i)\\\\tsgwp00525(?:\.[A-Za-z0-9.\-]+)?\\shared(?=\\|$)', '\\tsgwp00525.wjs.geaerospace.net\SHARED')
+ return $pg
+}
+
+# --- resolve per-bay values (override -> old XML -> default) ---
+# NOTE: read attributes with GetAttribute() - .Name collides with XmlElement.Name.
+$pg = $PartGroup
+if (-not $pg) {
+ $pgNode = $old.SettingsModel.PartGroups.PartGroup
+ if ($pgNode) { $pg = $pgNode.GetAttribute('FullName'); if (-not $pg) { $pg = $pgNode.FullName } }
+}
+$pg = Canon $pg
+
+# NB: PowerShell vars are case-insensitive - do NOT name this $cmmid (collides with the $CmmId param).
+$machineCmmId = $old.SettingsModel.MachineDefinition.CMMID
+if (-not $machineCmmId -and $CmmId) { $machineCmmId = "WJRP/$CmmId" }
+
+# SelectedVersionString from -PcdmisVersion
+$verMap = @{ '2016' = 'PC-DMIS 2016.0 64-bit'; '2019' = 'PC-DMIS 2019 R2 64-bit'; '2026' = 'PC-DMIS 2026.1 64-bit' }
+$selVer = ''
+if ($PcdmisVersion) { $selVer = if ($verMap.ContainsKey($PcdmisVersion)) { $verMap[$PcdmisVersion] } else { $PcdmisVersion } }
+
+# bool carryovers (old flat flags -> new IsEnabled)
+function OldBool($node, $default='false') { $v = $node; if ($null -eq $v -or $v -eq '') { $default } else { ([string]$v).ToLower() } }
+$pcdmisEnabled = OldBool $old.SettingsModel.UsingPcDmis 'true'
+$modusEnabled = OldBool $old.SettingsModel.UsingModus 'false'
+$winState = if ($old.SettingsModel.DefaultWindowState) { $old.SettingsModel.DefaultWindowState } else { 'Maximized' }
+$allowSel = OldBool $old.SettingsModel.AllowOperationProgramSelection 'true'
+$visExec = OldBool $old.SettingsModel.PcDmisSettings.VisibleDuringExecution 'true'
+$cmmMode = if ($old.SettingsModel.PcDmisSettings.CMMMode) { $old.SettingsModel.PcDmisSettings.CMMMode } else { 'Online' }
+$sizeTol = if ($old.SettingsModel.MachineDefinition.ProbeCalSizeTol) { $old.SettingsModel.MachineDefinition.ProbeCalSizeTol } else { '0.0005' }
+$formTol = if ($old.SettingsModel.MachineDefinition.ProbeCalFormTol) { $old.SettingsModel.MachineDefinition.ProbeCalFormTol } else { '0.0005' }
+$hasRotary = OldBool $old.SettingsModel.MachineDefinition.MachineHasRotaryTable 'false'
+
+# program paths (override -> goCMM 2.12 convention default)
+$progDir = 'C:\geaofi\goCMM PC-DMIS Programs'
+if (-not $ResetProgram) { $ResetProgram = "$progDir\MachineResetProgram.prg" }
+if (-not $ProbeCalibrationProgram) {
+ $calName = if ($CmmId) { "${CmmId}_Cal_All_Probes_rev2.PRG" } else { 'Cal_All_Probes_rev2.PRG' }
+ $ProbeCalibrationProgram = "$progDir\$calName"
+}
+
+# --- operator inputs (old KeyLabel + NonTBIInputs -> new OperatorInput; OPER -> Operator) ---
+$inputs = @()
+$kl = $old.SettingsModel.OperatorInputs.KeyLabel
+if ($kl) { $inputs += [pscustomobject]@{ Key = $kl.GetAttribute('Name'); Display = $kl.GetAttribute('Display') } }
+foreach ($oi in $old.SettingsModel.OperatorInputs.NonTBIInputs.OperatorInput) {
+ if ($oi -is [System.Xml.XmlElement]) { $inputs += [pscustomobject]@{ Key = $oi.GetAttribute('Name'); Display = $oi.GetAttribute('Display') } }
+}
+if (-not $inputs) {
+ $inputs = @([pscustomobject]@{Key='SERNO';Display='Serial Number'}, [pscustomobject]@{Key='Operator';Display='Operator'})
+}
+$opXml = ($inputs | ForEach-Object {
+ $k = if ($_.Key -ieq 'OPER') { 'Operator' } else { $_.Key } # decision #4
+ $d = if ($_.Key -ieq 'OPER') { 'Operator' } else { $_.Display }
+ " `n $k`n $d`n `n "
+}) -join "`n"
+
+function X([string]$s){ [System.Security.SecurityElement]::Escape($s) }
+
+$xml = @"
+
+
+ true
+ PCDMIS
+
+
+ $(X $pg)
+
+
+
+
+$opXml
+
+ false
+ $(X $winState)
+ $allowSel
+
+
+ $pcdmisEnabled
+ $visExec
+ $(X $ResetProgram)
+
+ false
+ false
+ $(X $ProbeCalibrationProgram)
+
+
+
+ false
+ false
+ 1000
+ true
+ ProgramCount
+ false
+ $(X $selVer)
+ 5
+ 5
+ 0
+ false
+ false
+
+
+
+
+ $modusEnabled
+ true
+
+
+ false
+ false
+
+
+
+
+ -Organiser
+
+
+ $(X $machineCmmId)
+
+
+ false
+ $sizeTol
+ $formTol
+
+ 72
+ 0
+
+
+ 24
+ 0
+
+ $hasRotary
+ false
+ $(X $cmmMode)
+
+
+
+
+
+
+
+"@
+
+$dir = Split-Path -Parent $OutFile
+if ($dir -and -not (Test-Path $dir)) { New-Item -ItemType Directory -Path $dir -Force | Out-Null }
+# validate well-formed before writing
+[void][xml]$xml
+Set-Content -LiteralPath $OutFile -Value $xml -Encoding UTF8
+Write-Host "Wrote $OutFile"
+Write-Host " PartGroup = $pg"
+Write-Host " CMMID = $machineCmmId"
+Write-Host " SelectedVersion = $selVer"
+Write-Host " ResetProgram = $ResetProgram"
+Write-Host " ProbeCalProgram = $ProbeCalibrationProgram"
+Write-Host "NOTE: confirm those .PRG files exist at the paths above (-ResetProgram / -ProbeCalibrationProgram to override)."