Files
pxe-server/playbook/shopfloor-setup/Shopfloor/lib/Show-IntuneDeviceQR.ps1
cproudlock 04db6d6605 Generic build for other GE sites: no WJ data, one shopfloor type, Windows USB builder
Everything West Jefferson specific moved out to a separate repo
(pxe-site-wj): NTLARS/UDC/heat-treat backups keyed by WJ machine numbers,
CMM and Wax/Trace bay lists, WJ OpenText profiles, site-config.json, the
WJ preinstall list, and the WJ shopfloor PC types (Collections,
NoCollections, Common, Keyence, CMM, Genspect, HeatTreat, Wax/Trace,
Display, PartMarker) with GE-Enforce, machine numbers and the desktop
layout. The part-marker bench tools moved to their own repo. Dead
scripts removed: build-proxmox-iso, deploy-bios, sync_hardware_models,
mirror-from-gold, pull-bios, startnet-template.cmd.

Shopfloor is one generic type, gea-shopfloor-standard, with today's
timing:
- PPKG at first logon (unattend Order 10, unchanged).
- Install-AppList.ps1 runs C:\PreInstall\preinstall.json during setup.
- A new SYSTEM task, GE Shopfloor Post-PPKG, runs postppkg.json once
  after the post-PPKG reboot. It also clears AutoLogonCount after the
  ShopFloor handover (the Users-level monitor can be denied that HKLM
  write) and removes the Intune Sync task when done.
- Both lists ship empty. A site adds entries and installers.
- WinPE choice 3 goes straight to the PPKG menu. Staging, fetch and heal
  carry only the generic payload. The webapp's shopfloor sub-menu editor
  is gone, and the imaging tiles name the new stages.
- The playbook deploys the empty lists only if missing. It now deploys
  the PowerShell 7 MSI the shopfloor unattend installs, which it never
  did before.

Blancco site settings move into the webapp (Updates > Blancco releases):
business location, licensing Wi-Fi SSID and password. They are rendered
into the served preferences.xml and applied to every staged release, and
are seeded from site-vars (site_name, blancco_wifi_ssid,
blancco_wifi_password) only while none exist. The repo XML carries no
site values; the plaintext Wi-Fi password is gone from it.

New site_name and site_timezone settings: report and evidence-pack site
name, server clock and webapp time zone. No more West Jefferson defaults
in webapp/config.py or the dashboard title. Sync-BPRT.ps1 gets the
server address filled in at download instead of WJ's GE address.

USB on Windows:
- scripts/Build-PxeUsb.ps1 runs the same checks as build-usb.sh. It
  writes one FAT32 CIDATA partition (ISO files plus patched grub.cfg plus
  payload) with diskpart and reads every file back by SHA-256.
- Tested with Windows PowerShell 5.1 in the win11 VM, and the layout
  installs and first-boots in the pxetest VM.
- scripts/stage-usb-share.sh stages the build folder on a share.
- site-content/ is optional for both builders.

KB docs/kb/How_To_Deploy_PXE_Server_New_Site.html: new-site deployment,
Windows only, with a site-vars.yml guide.

Checks: webapp tests pass (5 suites); the unattend lint and playbook
syntax checks pass; every .ps1 parses; there are no WJ hosts, IPs, names
or PC types in playbook/, webapp/, scripts/ or autoinstall/.
2026-10-16 20:37:22 -04:00

208 lines
8.1 KiB
PowerShell

# Show-IntuneDeviceQR.ps1 - Standalone version of the QR-code panel from
# Monitor-IntuneProgress.ps1, extracted for use at sites that only need the
# device-ID display (no DSC monitoring, no Intune sync triggers, no reboot
# orchestration).
#
# Polls dsregcmd /status every 15s until the device reports an Azure AD
# DeviceId, then renders that ID as a half-block QR code in the console
# along with the literal text. Stays open until the user presses a key.
#
# REQUIREMENTS
# - Windows PowerShell 5.1 (or PS 7) with .NET Framework available
# - QRCoder.dll alongside this script (or at one of the fallback paths
# below). Single ~75 KB native dependency, no internet needed.
#
# USAGE
# powershell.exe -NoProfile -ExecutionPolicy Bypass -File Show-IntuneDeviceQR.ps1
#
# Or double-click via a one-line .bat:
# powershell.exe -NoProfile -ExecutionPolicy Bypass -NoExit -File "%~dp0Show-IntuneDeviceQR.ps1"
[CmdletBinding()]
param(
[int]$PollSeconds = 15,
[string]$QRCoderDllPath = ''
)
$ErrorActionPreference = 'Continue'
# ----------------------------------------------------------------------------
# Locate QRCoder.dll. Search order:
# 1. -QRCoderDllPath if explicitly passed
# 2. Next to this script ($PSScriptRoot\QRCoder.dll)
# 3. Current working directory
# 4. The standard staging path (C:\Enrollment\shopfloor-setup\Shopfloor)
# ----------------------------------------------------------------------------
function Resolve-QRCoderDll {
param([string]$Override)
$candidates = @()
if ($Override) { $candidates += $Override }
if ($PSScriptRoot) { $candidates += (Join-Path $PSScriptRoot 'QRCoder.dll') }
$candidates += (Join-Path (Get-Location) 'QRCoder.dll')
$candidates += 'C:\Enrollment\shopfloor-setup\Shopfloor\QRCoder.dll'
foreach ($c in $candidates) {
if ($c -and (Test-Path -LiteralPath $c)) { return (Resolve-Path -LiteralPath $c).Path }
}
return $null
}
# ----------------------------------------------------------------------------
# Get the AAD DeviceId from dsregcmd. Returns $null until the device is
# Azure AD joined AND dsregcmd has populated the field.
# ----------------------------------------------------------------------------
function Get-AadDeviceId {
try {
$dsreg = dsregcmd /status 2>&1
} catch {
return $null
}
$line = $dsreg | Select-String 'DeviceId' | Select-Object -First 1
if (-not $line) { return $null }
$parts = $line.ToString().Split(':')
if ($parts.Count -lt 2) { return $null }
$id = $parts[1].Trim()
if ([string]::IsNullOrWhiteSpace($id)) { return $null }
return $id
}
# ----------------------------------------------------------------------------
# Half-block QR renderer. Each output character is 1 module wide, 2 modules
# tall, using:
# U+2588 = both top and bottom modules set
# U+2580 = top only
# U+2584 = bottom only
# space = neither
# Cuts QR height in half vs full-block rendering. Adds a 4-module quiet
# zone manually since QRCoder's ModuleMatrix excludes it by default.
# ----------------------------------------------------------------------------
function Format-QrHalfBlocks {
param([string]$Payload, [string]$DllPath, [int]$IndentSpaces = 8)
Add-Type -Path $DllPath
$gen = New-Object QRCoder.QRCodeGenerator
$data = $gen.CreateQrCode($Payload, [QRCoder.QRCodeGenerator+ECCLevel]::L)
$matrix = $data.ModuleMatrix
$size = $matrix.Count
$pad = 4
$total = $size + 2 * $pad
$upper = [char]0x2580
$lower = [char]0x2584
$full = [char]0x2588
$left = ' ' * $IndentSpaces
$lines = New-Object System.Collections.Generic.List[string]
for ($y = 0; $y -lt $total; $y += 2) {
$sb = New-Object System.Text.StringBuilder
[void]$sb.Append($left)
for ($x = 0; $x -lt $total; $x++) {
$mx = $x - $pad
$my1 = $y - $pad
$my2 = $y + 1 - $pad
$top = ($my1 -ge 0 -and $my1 -lt $size -and $mx -ge 0 -and $mx -lt $size -and $matrix[$my1].Get($mx))
$bot = ($my2 -ge 0 -and $my2 -lt $size -and $mx -ge 0 -and $mx -lt $size -and $matrix[$my2].Get($mx))
if ($top -and $bot) { [void]$sb.Append($full) }
elseif ($top) { [void]$sb.Append($upper) }
elseif ($bot) { [void]$sb.Append($lower) }
else { [void]$sb.Append(' ') }
}
$lines.Add($sb.ToString())
}
return $lines
}
# ----------------------------------------------------------------------------
# Bigger console so the QR + text frame fits in one viewport.
# ----------------------------------------------------------------------------
try {
$rui = $Host.UI.RawUI
$maxH = $rui.MaxPhysicalWindowSize.Height
$targetWindow = [Math]::Min(58, [int]$maxH)
$targetBuffer = [Math]::Max($targetWindow, 200)
$bs = $rui.BufferSize
if ($bs.Height -lt $targetBuffer) { $bs.Height = $targetBuffer; $rui.BufferSize = $bs }
$ws = $rui.WindowSize
if ($ws.Height -lt $targetWindow) { $ws.Height = $targetWindow; $rui.WindowSize = $ws }
} catch {}
# ----------------------------------------------------------------------------
# Wait for QRCoder.dll
# ----------------------------------------------------------------------------
$dllPath = Resolve-QRCoderDll -Override $QRCoderDllPath
# Mark-of-the-Web: when files arrive via SMB / zip download / email,
# Windows attaches a Zone.Identifier alternate data stream that flags the
# file as "from another computer". Add-Type then refuses to load the DLL
# with "Operation is not supported" / "Could not load file or assembly".
# Unblock-File silently strips the ADS. Best-effort: errors are non-fatal
# (file may already be unblocked, or the user may not have write access).
if ($dllPath) {
try { Unblock-File -LiteralPath $dllPath -ErrorAction SilentlyContinue } catch {}
}
if ($PSCommandPath) {
try { Unblock-File -LiteralPath $PSCommandPath -ErrorAction SilentlyContinue } catch {}
}
if (-not $dllPath) {
Write-Host ""
Write-Host "ERROR: QRCoder.dll not found." -ForegroundColor Red
Write-Host "Searched (in order):"
if ($QRCoderDllPath) { Write-Host " - $QRCoderDllPath (-QRCoderDllPath)" }
if ($PSScriptRoot) { Write-Host " - $(Join-Path $PSScriptRoot 'QRCoder.dll')" }
Write-Host " - $(Join-Path (Get-Location) 'QRCoder.dll')"
Write-Host " - C:\Enrollment\shopfloor-setup\Shopfloor\QRCoder.dll"
Write-Host ""
Write-Host "Drop QRCoder.dll alongside this script and re-run." -ForegroundColor Yellow
exit 2
}
# ----------------------------------------------------------------------------
# Poll loop - retry every $PollSeconds until DeviceId appears
# ----------------------------------------------------------------------------
Write-Host ""
Write-Host " Waiting for Azure AD device registration..." -ForegroundColor Cyan
Write-Host " Polling dsregcmd every $PollSeconds s. Press Ctrl+C to abort." -ForegroundColor DarkGray
Write-Host ""
$attempt = 0
while ($true) {
$attempt++
$deviceId = Get-AadDeviceId
if ($deviceId) { break }
Write-Host (" [{0}] attempt {1,3} no DeviceId yet, retrying in {2}s..." -f (Get-Date -Format 'HH:mm:ss'), $attempt, $PollSeconds) -ForegroundColor DarkGray
Start-Sleep -Seconds $PollSeconds
}
# ----------------------------------------------------------------------------
# Render
# ----------------------------------------------------------------------------
Clear-Host
Write-Host ""
Write-Host " GE Aerospace -- Intune Device ID" -ForegroundColor Cyan
Write-Host " $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')" -ForegroundColor DarkGray
Write-Host ""
Write-Host " Device ID: $deviceId" -ForegroundColor Green
Write-Host ""
try {
$qrLines = Format-QrHalfBlocks -Payload $deviceId -DllPath $dllPath
foreach ($l in $qrLines) { Write-Host $l }
} catch {
Write-Host " (QR generation failed: $($_.Exception.Message))" -ForegroundColor Red
}
Write-Host ""
Write-Host " Scan with phone camera or Intune admin app." -ForegroundColor DarkGray
Write-Host ""
Write-Host " Press any key to close..." -ForegroundColor Yellow
try { [void][Console]::ReadKey($true) } catch { [void](Read-Host) }
exit 0