Files
BT411/tools/podprobe.ps1
T
Joe DiPrimaandClaude Opus 5 990ad52563 podprobe: emit boot-STABLE panel identities + ready-to-paste monitor🆔 lines
Adds a section resolving each screen's panel identity via the SAME Win32 API the
game uses (EnumDisplayDevices on the display's MONITOR child) rather than the
WmiMonitorID query above -- if probe and engine read different sources the
printed fragments could fail to match what the engine tests.  Verified on the
dev box: probe and engine emit byte-identical identities.

Answers the open [T3] multi-panel question WITHOUT deploying a build (pure OS
data).  When one EDID code appears on SEVERAL panels (identical MFD models) it
detects the collision and emits the per-connector UID form instead.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018SgmXGNMXavXiafKXf9MDC
2026-08-08 15:58:12 -05:00

258 lines
12 KiB
PowerShell

# podprobe.ps1 -- pod / crash-cart display + I/O topology probe.
#
# Run ON THE POD PC (PowerShell, no install, no admin):
# powershell -ExecutionPolicy Bypass -File podprobe.ps1
#
# Prints and writes podprobe.txt: GPUs, every monitor with its VIRTUAL DESKTOP
# rect (the coordinates glass_layout.cfg needs), EDID make/model (to identify
# the original pod panels), serial ports (the RIO board), and a PROPOSED
# glass_layout.cfg mapping the six pod surfaces onto the non-primary monitors.
#
# Then, in the game's working directory:
# copy the proposed glass_layout.cfg in
# set BT_GLASS=1 & set BT_GLASS_PANELS=1 & set BT_POD_SURFACES=1
# set BT_GLASS_LAYOUT=load (add =save to persist drags instead)
# btl4.exe -platform pod
# The log prints one "[glasswin] '<title>' ... -> monitor \\.\DISPLAYn" line per
# surface: that is the receipt that a picture landed on the right panel.
$ErrorActionPreference = 'Continue'
$out = New-Object System.Collections.ArrayList
function W($s) { [void]$out.Add($s); Write-Host $s }
W "==================== BT411 POD PROBE ===================="
W ("host : {0}" -f $env:COMPUTERNAME)
W ("when : {0}" -f (Get-Date -Format 'yyyy-MM-dd HH:mm:ss'))
W ("windows : {0}" -f (Get-CimInstance Win32_OperatingSystem).Caption)
W ""
W "---- WILL OUR BUILD EVEN RUN HERE? ----"
try {
$os = Get-CimInstance Win32_OperatingSystem
$ver = [Version]$os.Version
W (" OS version : {0} (build {1})" -f $os.Version, $os.BuildNumber)
W (" architecture: {0}" -f $os.OSArchitecture)
if ($ver.Major -lt 6) {
W " *** STOP: this is Windows XP/2003-era. btl4.exe is built with a modern MSVC"
W " toolset and WILL NOT LAUNCH here (CRT + API requirements). Options:"
W " (a) put a newer Windows on the cart, or (b) rebuild with the v141_xp"
W " toolset (VS2017), or (c) drive the panels from a modern PC instead."
} elseif ($ver.Major -eq 6 -and $ver.Minor -lt 1) {
W " *** WARNING: Vista-era. Untested; Win7 SP1+ is the practical minimum."
} else {
W " OK: modern enough for the current build."
}
} catch { W " (OS query failed: $_)" }
W ""
W "---- GPUs ----"
try {
Get-CimInstance Win32_VideoController | ForEach-Object {
W (" {0}" -f $_.Name)
W (" driver {0} ({1}) mode {2}x{3} ram {4} MB" -f `
$_.DriverVersion, $_.DriverDate, $_.CurrentHorizontalResolution,
$_.CurrentVerticalResolution, [int]($_.AdapterRAM / 1MB))
}
} catch { W " (video controller query failed: $_)" }
W ""
W "---- MONITORS (virtual-desktop rects -- these are the layout coordinates) ----"
Add-Type -AssemblyName System.Windows.Forms | Out-Null
$screens = [System.Windows.Forms.Screen]::AllScreens
$i = 0
foreach ($s in $screens) {
$b = $s.Bounds
W (" [{0}] {1}{2} x={3} y={4} {5}x{6} bpp={7}" -f `
$i, $s.DeviceName, $(if ($s.Primary) { " *PRIMARY*" } else { "" }),
$b.X, $b.Y, $b.Width, $b.Height, $s.BitsPerPixel)
$i++
}
W (" total screens: {0}" -f $screens.Count)
W ""
W "---- EDID identity (which physical panel is which) ----"
try {
Get-CimInstance -Namespace root\wmi -ClassName WmiMonitorID | ForEach-Object {
$mk = -join ($_.ManufacturerName | Where-Object { $_ -ne 0 } | ForEach-Object { [char]$_ })
$nm = -join ($_.UserFriendlyName | Where-Object { $_ -ne 0 } | ForEach-Object { [char]$_ })
$sn = -join ($_.SerialNumberID | Where-Object { $_ -ne 0 } | ForEach-Object { [char]$_ })
W (" {0} | make={1} model='{2}' serial={3} year={4}" -f `
$_.InstanceName, $mk, $nm, $sn, $_.YearOfManufacture)
}
} catch { W " (EDID query failed -- normal over some remote sessions: $_)" }
W ""
# ---------------------------------------------------------------------------
# BOOT-STABLE panel identity. Windows renumbers \\.\DISPLAYn and reorders the
# enumeration when a panel is power-cycled or re-cabled, so binding the pod by
# index or device name is fragile (Nick, 2026-08-07: "the order changed ... even
# if the visual desktop tool looks the same"). Bind by the panel's own EDID
# identity instead: glass_layout.cfg accepts `monitor:id:<fragment>`.
#
# This deliberately calls the SAME Win32 API the game does (EnumDisplayDevices
# on the display's MONITOR child) rather than the WmiMonitorID above -- if the
# probe and the engine read different sources, the fragments printed here might
# not be what the engine actually matches against.
# ---------------------------------------------------------------------------
W "---- BOOT-STABLE panel identity (paste these into glass_layout.cfg) ----"
try {
if (-not ("BTDisp" -as [type])) {
Add-Type -TypeDefinition @'
using System;
using System.Runtime.InteropServices;
public class BTDisp {
[StructLayout(LayoutKind.Sequential, CharSet=CharSet.Ansi)]
public struct DISPLAY_DEVICE {
public int cb;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst=32)] public string DeviceName;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst=128)] public string DeviceString;
public int StateFlags;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst=128)] public string DeviceID;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst=128)] public string DeviceKey;
}
[DllImport("user32.dll", CharSet=CharSet.Ansi)]
public static extern bool EnumDisplayDevicesA(string dev, uint num, ref DISPLAY_DEVICE dd, uint flags);
public static string MonitorId(string display) {
DISPLAY_DEVICE dd = new DISPLAY_DEVICE();
dd.cb = Marshal.SizeOf(typeof(DISPLAY_DEVICE));
// 0x1 = EDD_GET_DEVICE_INTERFACE_NAME (richer path, includes connector UID)
if (!EnumDisplayDevicesA(display, 0, ref dd, 0x1)) {
dd = new DISPLAY_DEVICE();
dd.cb = Marshal.SizeOf(typeof(DISPLAY_DEVICE));
if (!EnumDisplayDevicesA(display, 0, ref dd, 0)) return "";
}
return dd.DeviceID;
}
}
'@
}
$codes = @{}
$rows = @()
foreach ($s in [System.Windows.Forms.Screen]::AllScreens) {
$sid = [BTDisp]::MonitorId($s.DeviceName)
# EDID PnP code = 3 letters + 4 hex digits (AUO10ED, DEL4231)
$code = ""
if ($sid -match '[\\#\?]([A-Za-z]{3}[0-9A-Fa-f]{4})[\\#\?]') { $code = $Matches[1] }
elseif ($sid -match '([A-Za-z]{3}[0-9A-Fa-f]{4})') { $code = $Matches[1] }
$rows += [pscustomobject]@{ Dev=$s.DeviceName; Prim=$s.Primary; Sid=$sid; Code=$code
X=$s.Bounds.X; Y=$s.Bounds.Y; W=$s.Bounds.Width; H=$s.Bounds.Height }
if ($code -ne "") { $codes[$code] = 1 + $(if ($codes.ContainsKey($code)) { $codes[$code] } else { 0 }) }
}
foreach ($r in $rows) {
W (" {0}{1} {2},{3} {4}x{5}" -f $r.Dev, $(if ($r.Prim) { " *PRIMARY*" } else { "" }), $r.X, $r.Y, $r.W, $r.H)
W (" stable-id : {0}" -f $(if ($r.Sid) { $r.Sid } else { "(unavailable)" }))
if ($r.Code -ne "" -and $codes[$r.Code] -gt 1) {
# Same model on more than one output: the EDID code alone is ambiguous.
# The connector UID in the tail is what separates them.
$uid = ""
if ($r.Sid -match '(UID[0-9]+)') { $uid = $Matches[1] }
if ($uid -ne "") {
W (" cfg form : monitor:id:{0} <-- code '{1}' is on {2} panels, so use the UID" -f $uid, $r.Code, $codes[$r.Code])
} else {
W (" cfg form : (AMBIGUOUS -- '{0}' appears on {1} panels and no UID found;" -f $r.Code, $codes[$r.Code])
W " use a longer unique substring of stable-id above)"
}
} elseif ($r.Code -ne "") {
W (" cfg form : monitor:id:{0}" -f $r.Code)
} else {
W " cfg form : (no EDID code parsed -- use a substring of stable-id)"
}
}
$dupes = ($codes.GetEnumerator() | Where-Object { $_.Value -gt 1 } | Measure-Object).Count
if ($dupes -gt 0) {
W ""
W (" NOTE: {0} EDID code(s) appear on more than one panel (identical models)." -f $dupes)
W " Those lines use the connector UID instead, which is per-output."
}
W ""
W " Run this again AFTER a reboot or a panel power-cycle: the device names"
W " and ordering above may move, but stable-id / cfg form must NOT."
} catch { W " (stable-identity probe failed: $_)" }
W ""
W "---- SERIAL PORTS (the RIO board lives on one of these) ----"
try {
$sp = Get-CimInstance Win32_SerialPort
if ($sp) { $sp | ForEach-Object { W (" {0} {1}" -f $_.DeviceID, $_.Name) } }
else {
$reg = 'HKLM:\HARDWARE\DEVICEMAP\SERIALCOMM'
if (Test-Path $reg) {
(Get-ItemProperty $reg).PSObject.Properties |
Where-Object { $_.Name -notlike 'PS*' } |
ForEach-Object { W (" {0} -> {1}" -f $_.Name, $_.Value) }
} else { W " (none found)" }
}
} catch { W " (serial query failed: $_)" }
W ""
W "---- REMOTE SESSION ----"
W (" SESSIONNAME={0} (a console session drives the real panels; an RDP" -f $env:SESSIONNAME)
W " session gets a VIRTUAL display and will NOT light the pod monitors."
W " Chrome Remote Desktop attaches to the CONSOLE -- that is what we want."
W ""
# ---------------- proposed layout ----------------
# Pod surface roles, in the order the cab reads them. Sizes are the native
# surface sizes the game creates in BT_POD_SURFACES mode.
$roles = @(
@{ title = 'Heat MFD'; w = 640; h = 480; note = 'upper LEFT (coolant)' },
@{ title = 'Engineering'; w = 640; h = 480; note = 'upper CENTER (engineering)' },
@{ title = 'Comm MFD'; w = 640; h = 480; note = 'upper RIGHT (hot box / comm)' },
@{ title = 'Left Weapons'; w = 640; h = 480; note = 'lower LEFT (weapons)' },
@{ title = 'Right Weapons'; w = 640; h = 480; note = 'lower RIGHT (weapons)' },
@{ title = 'Secondary / Radar'; w = 480; h = 640; note = 'secondary screen (radar, portrait)' }
)
$targets = @($screens | Where-Object { -not $_.Primary } | Sort-Object { $_.Bounds.Y }, { $_.Bounds.X })
W "---- PROPOSED glass_layout.cfg ----"
if ($targets.Count -eq 0) {
W " ! No non-primary monitors found. Either the pod panels are not attached to"
W " this PC, or this is a remote/virtual display session. Nothing to map yet."
} else {
W (" {0} non-primary monitor(s) available; assigning in top-to-bottom, left-to-right order." -f $targets.Count)
if ($targets.Count -lt $roles.Count) {
W (" ! Fewer panels than surfaces -- the last {0} surface(s) stay on the primary display." -f ($roles.Count - $targets.Count))
}
}
$cfg = New-Object System.Collections.ArrayList
[void]$cfg.Add('# BT411 glass cockpit window layout -- generated by tools/podprobe.ps1')
[void]$cfg.Add('# "<title>=x,y,w,h,noframe,bare" (bare = surface only, no desktop buttons)')
[void]$cfg.Add('# Coordinates are VIRTUAL DESKTOP pixels; run the probe again after any')
[void]$cfg.Add('# display rearrangement, because Windows renumbers the desktop origin.')
for ($r = 0; $r -lt $roles.Count; $r++) {
$role = $roles[$r]
if ($r -lt $targets.Count) {
$b = $targets[$r].Bounds
# centre the surface on the panel (exact fit when the panel is 640x480)
$x = $b.X + [int](($b.Width - $role.w) / 2)
$y = $b.Y + [int](($b.Height - $role.h) / 2)
$line = "{0}={1},{2},{3},{4},noframe,bare" -f $role.title, $x, $y, $role.w, $role.h
[void]$cfg.Add($line)
W (" {0,-18} -> {1} ({2}x{3}) at {4},{5} [{6}]" -f `
$role.title, $targets[$r].DeviceName, $b.Width, $b.Height, $x, $y, $role.note)
} else {
[void]$cfg.Add(("# {0}=<x>,<y>,{1},{2},noframe,bare # no panel assigned" -f $role.title, $role.w, $role.h))
}
}
$cfgPath = Join-Path (Get-Location) 'glass_layout.cfg.proposed'
$cfg | Set-Content -Path $cfgPath -Encoding ASCII
W ""
W (" wrote {0}" -f $cfgPath)
W " -> review it, rename to glass_layout.cfg in the game's working directory."
W ""
W "---- NOTES ----"
W " * The pod's main 3D view is the GAME's own window (not a glass panel); put it"
W " on the main-view monitor with the normal window/fullscreen controls."
W " * 640x480 panels: if Windows does not offer that mode, the surface still"
W " renders 640x480 inside whatever mode the panel runs -- centred, not scaled."
W " * The 1995 rig spanned the five MFDs as ONE 1280x480 surface via NVIDIA"
W " Horizontal Span, which modern drivers removed. This per-panel window path"
W " replaces it and needs no special driver."
$txt = Join-Path (Get-Location) 'podprobe.txt'
$out | Set-Content -Path $txt -Encoding ASCII
Write-Host ""
Write-Host ("full report written to {0}" -f $txt)