diff --git a/context/glass-cockpit.md b/context/glass-cockpit.md index 5dff570..6254292 100644 --- a/context/glass-cockpit.md +++ b/context/glass-cockpit.md @@ -156,6 +156,26 @@ want bare. From then on those windows are frameless **and pinned** — with no c nothing to drag them by, which is the point: the arrangement is finished. Delete the flag to get the frame back. +### `monitor:` — bind a window to a DISPLAY, not to pixels (2026-08-06) [T2 live] + +Instead of an `x,y,w,h` rect, a line may name a display device: + +``` +Secondary / Radar=monitor:DISPLAY4,bare +VGA Port A=monitor:DISPLAY2,bare +``` + +The resolver (`ResolveMonitorSpec` + `MonScanProc`, `L4GLASSWIN`) matches the full device name OR +its tail, so `monitor:DISPLAY4` works without typing `\\.\`; a bare integer is a 0-based index in +enumeration order. The surface is CENTRED on that monitor. This is what makes a fixed rig +survivable: **desktop rects move** when a display is re-detected, a USB graphics adapter +re-enumerates or someone changes a resolution, and a pixel rect then silently puts a panel on the +wrong glass — a device binding does not. Required for [[pod-hardware]] §ALPHA-MR, where a +mis-bound window means a picture on the wrong CRT with no error anywhere. + +`,bare` **implies frameless** (it used to need `,noframe` too, and a "bare" window came back +656×519 WITH a caption — a real bug found on the cab). + Options are comma-separated after the numbers and **unknown ones are ignored**, so an older build reading a newer file loses the option but never the line — the `bindings.txt` grammar rule applied here. @@ -183,6 +203,15 @@ appears in the cfg like any panel and honours `,noframe`. Verified: a drag wrote at 321,222. NB the plasma window blits directly every frame (`GetDC`+`StretchDIBits`), so unlike the panels it has no `WM_TIMER` focus-throttle to worry about. +**Turning it OFF: `L4PLASMA=NONE` (also `OFF`/`0`, 2026-08-06) [T2].** `L4GREND` creates a +marquee whenever `L4PLASMA` is set at all (`SCREEN` → the desktop window, anything else → a real +`PlasmaDisplay` on that serial port), and the GLASS profile force-defaults it to `SCREEN` — so on +a rig with no marquee wired, leaving the variable unset is NOT enough; the profile fills it back +in and a plasma window lands on the cab's glass. The `NONE`/`OFF`/`0` spelling nulls the string +before the branch, so no external display is constructed at all. The boot banner reads +`GLASS (PadRIO; plasma off [L4PLASMA])` when it takes, and the absence of the +`[plasmawin] desktop plasma display up` line is the runtime proof. + **Verified 2026-07-26 [T2]:** `BT_RIOBANK_LOG=1` dumps every bank's rects; `scratchpad/checkbank.py` reports the per-bank census and proves no address is SHADOWED (has a point no earlier button covers); `scratchpad/clickbank.py` then posts a real click at every @@ -214,7 +243,10 @@ window. - **World aspect** under the letterbox is the view rect's OWN aspect — the client no longer enters into it. - **`-fit`** (alias `-windowed-fullscreen`): borderless `WS_POPUP` over the monitor, canvas - letterboxed inside. Verified on a 3440×1440 ultrawide. + letterboxed inside. Verified on a 3440×1440 ultrawide. **`BT_FIT=1` is the env spelling** + (2026-08-06) — same flag, settable from `environ.ini`, so a fixed rig keeps its borderless main + view however it is launched instead of depending on one launcher's command line. Targets + `MONITOR_DEFAULTTOPRIMARY`, i.e. the PRIMARY display. [[pod-hardware]] §ALPHA-MR ⚠ **Ordering trap found live:** the letterbox flag was first set at device creation, but the first `WM_SIZE` arrives BEFORE the device exists — a `-fit` boot logged `aspect=3.14` (the diff --git a/context/pod-hardware.md b/context/pod-hardware.md index fff6e25..6f87796 100644 --- a/context/pod-hardware.md +++ b/context/pod-hardware.md @@ -217,12 +217,21 @@ colour channel of one shared palettized framebuffer. Mechanism, end to end: mono-tinted window on its own Windows display (see §MFD PANELS ON REAL HARDWARE), which is right when every panel has its own output. **On splitter-wired glass it is wrong** — three monitors would share one Windows display and each would show only its channel's share of a single tinted -image. Driving original splitter hardware needs a CHANNEL-COMPOSITE mode: extract three planes -into ONE 640x480 RGB image with pure (255,0,0)/(0,255,0)/(0,0,255) tints, one window per VGA -output. `SVGA16::ExpandPlaneToBGRA` already does the per-plane extraction with a tint, so the -composite is a small addition. OPEN: which way Nick's crash cart is wired (Windows shows three -separate 640x480 displays there, which suggests per-panel outputs via the Trigger 6 USB adapter, -not a splitter) — settle it by eye before building. +image. + +**`BT_POD_RGB=1` — the CHANNEL-COMPOSITE mode (built + field-verified 2026-08-06) [T2].** It +collapses the six panel windows into **three**: two VGA-port windows plus the radar. Each port +window composites its group's planes into ONE 640×480 BGRA image, each member ORed into its own +channel mask (`0x00FF0000` / `0x0000FF00` / `0x000000FF`), so the analog splitter downstream hands +each mono CRT exactly its picture. Grouping follows `L4GAUGE.CFG`: *VGA Port A* = Heat + Comm + +Mfd2, *VGA Port B* = Mfd1 + Mfd3; the radar keeps real colour on its own port. +`SVGA16::ExpandPlaneToBGRA` does the per-plane extraction, and `BT_POD_RGB` implies +`BT_POD_SURFACES`. Two companion gates came out of the bring-up: **`BT_POD_CHANMAP==,…`** +remaps a surface's colour line when a cab's harness disagrees with the 1995 config (it did — see +§ALPHA-MR), and **`BT_POD_IDENT`** / **`BT_POD_CHANTEST`** paint identifying/cycling test patterns +so you can tell which physical CRT is which without trusting the desktop layout. +**SETTLED:** Nick's cart IS splitter-wired — its three 640×480 "displays" are VGA outputs, each +feeding a three-monitor splitter, and the composite is what lit the glass correctly. ## MFD PANELS ON REAL HARDWARE — the bring-up path (2026-08-06) [T2 local / T4 on-pod] Nick's crash cart (pod hardware + Chrome Remote Desktop on a burner account) is the first chance @@ -262,6 +271,59 @@ at native size CENTRED, not scaled — a scale-to-fit option is the obvious foll Chrome Remote Desktop holds the CONSOLE session (it should — an RDP session would get a virtual display and light nothing); and the RIO serial input, which is a separate task from the displays. +## ALPHA-MR — the verified cart config, FROZEN (2026-08-06) [T2, field-verified by eye] +The bring-up above landed: real pictures on real pod glass, then user-confirmed correct +("ok this is correct"). What that took, and where it now lives permanently. + +**The rig.** Win10 IoT Enterprise LTSC on the cart; Intel HD630 + a "Trigger 6 External Graphics" +USB adapter. Four console displays: **DISPLAY3 800×600 PRIMARY at 0,0** (main view), plus +**DISPLAY4 @800,122**, **DISPLAY2 @1440,123**, **DISPLAY1 @2080,124** (each 640×480). Physical +cab: five mono MFD CRTs (two left stacked, two right stacked, one centre loose on the desk) plus +the colour radar LCD in the yellow-button frame. + +**The mapping** (`glass_layout.cfg`, `monitor:` binding — see [[glass-cockpit]]): +``` +Secondary / Radar = monitor:DISPLAY4,bare # colour LCD, yellow-button frame +VGA Port A = monitor:DISPLAY2,bare # RGB triple: Heat + Comm + Mfd2 +VGA Port B = monitor:DISPLAY1,bare # RGB triple: Mfd1 + Mfd3 +``` +Three windows, not six — under `BT_POD_RGB` each VGA-port window carries three MFDs on its three +colour planes (§THE RGB SPLIT). Desktop-coordinate order says NOTHING about physical cab position: +the first assignment, made by sorting monitors left-to-right, was wrong. Only eyes on the glass +settle it. + +**Two deviations from the 1995 config, both real, both this cab's wiring:** +- **`BT_POD_CHANMAP=Comm=blue,Heat=red`** — kills/deaths and coolant loops came up on each + other's panel. `L4GAUGE.CFG` has Comm on red and Heat on blue; THIS cab's splitter harness + wires them the other way. The env override remaps a port's colour channel without touching the + authentic config file. [T2] +- **`BT_GAUGE_SEC_ROT=0`** — the radar here is a LANDSCAPE LCD, so the 640×480 source needs no + rotation. The 1995 pod's 270° rotation existed because its secondary CRT was mounted PORTRAIT; + it is a mounting fact, not a format fact. (En route: 180° looked plausible and was upside down.) + +**Frozen in `content/environ.ini`**, not in a launcher — `scratchpad/pod/podprofile.ini` is the +block, `scratchpad/pod/mergeprofile.ps1` merges it idempotently between markers. environ.ini is +read before anything touches the environment ([[build-and-run]]), and the real environment still +WINS, so a .bat can override any of it for a one-off. The cart's `runpod.bat` now sets only +`BT_LOG`/`BT_GLASS_LOG` and the `-egg`; `runpod_env.bat` keeps the all-inline version as the +fallback. Two gates were added for this: +- **`BT_FIT=1`** — the env spelling of `-fit`, so the borderless main view survives however the + game is started (shortcut, scheduled task, autostart) instead of riding on one launcher's + command line. It fits `MONITOR_DEFAULTTOPRIMARY` = DISPLAY3, which is the main-view panel. +- **`L4PLASMA=NONE`** (also `OFF`/`0`) — no marquee at all. Needed because the GLASS profile + force-defaults `L4PLASMA=SCREEN`, so simply leaving it unset still puts a desktop plasma window + on the pod's glass. The boot banner reports `plasma off [L4PLASMA]` when it takes. +Verified on build 4.11.813 with the launcher carrying none of it: `[boot] environ.ini: 9 +setting(s) applied`, `[cockpit] -fit: borderless 800x600`, `[glasswin] radar rotation 0 (none)`, +and all three surfaces on their intended `\\.\DISPLAYn`. + +**Working on the cart, remotely.** SSH over Tailscale lands in **session 0**, which has a dummy +"WinDisc" display and CANNOT see or enumerate session 1's windows — GUI work must go through +`schtasks /run /tn BT411Run` (task registered `/IT`), and window enumeration over SSH silently +returns nothing rather than failing. Kill `btl4.exe` BEFORE scp'ing a new exe or the file is +locked. The receipts in `podrun.log` are the remote eyes; a missing `DEBUG_STREAM` line in an +otherwise-logging run is real evidence that code path did not execute. + ## The 1995 player manual — alignment audit (2026-07-18) [T1, primary source] `reference/manual/Tesla40_BT_manual.pdf` (34pp, from Nick). CONFIRMS the reconstruction on every checked control behavior: diff --git a/engine/MUNGA_L4/L4GLASSWIN.cpp b/engine/MUNGA_L4/L4GLASSWIN.cpp index cbab125..ded2c53 100644 --- a/engine/MUNGA_L4/L4GLASSWIN.cpp +++ b/engine/MUNGA_L4/L4GLASSWIN.cpp @@ -123,6 +123,10 @@ struct GWin const char *groupPort[3]; const char *groupAlt[3]; // the Eng twin, or NULL int groupCount; + + char monitorName[40]; // the PHYSICAL monitor this window landed on + // (\.\DISPLAYn), stamped at creation; shown + // by BT_POD_IDENT so the cab can be mapped. }; static GWin gWins[8]; @@ -253,17 +257,26 @@ static void // (0x15 sits on the left column -- Cyd listed 0x10-0x14; move it if wanted.) static const int bottomAddrs[4] = { 0x16, 0x17, 0x1F, 0x1E }; + // The SURFACE SIZE follows the rotation: a quarter turn (1 or 3) transposes + // the 640x480 source to 480x640 portrait -- the pod's rotated CRT -- while + // 0 (none) and 2 (180) keep it LANDSCAPE. Sizing this independently of the + // rotation squashed the picture into the wrong aspect on a landscape + // replacement panel (crash cart, 2026-08-06). + const int quarterTurn = (gRadarRot == 1 || gRadarRot == 3); + const int surfW = quarterTurn ? RadarSurfW : RadarSurfH; // 480 : 640 + const int surfH = quarterTurn ? RadarSurfH : RadarSurfW; // 640 : 480 + BTRioBankMetrics metrics; - BTRioBankMetricsFor(RadarSurfW, RadarSurfH, &metrics); + BTRioBankMetricsFor(surfW, surfH, &metrics); BTRioBank bank; - BTRioBankLayout(BTRioBankRadar, 0, 0, RadarSurfW, RadarSurfH, + BTRioBankLayout(BTRioBankRadar, 0, 0, surfW, surfH, 0x10, 0x18, &metrics, bottomAddrs, &bank); BTRioBankDump("Secondary / Radar", &bank); int dx = -bank.boundsX, dy = -bank.boundsY; AdoptBank(w, bank, ClrYellow, dx, dy); - SetRect(&w.surfaceRect, dx, dy, dx + RadarSurfW, dy + RadarSurfH); + SetRect(&w.surfaceRect, dx, dy, dx + surfW, dy + surfH); w.clientW = bank.boundsW; w.clientH = bank.boundsH; } @@ -426,6 +439,116 @@ static const char *layoutFileName = "glass_layout.cfg"; // The Flight Controls window (no surface at all) is not created. // BT_POD_SURFACES=1 -> every display window goes bare // glass_layout.cfg "=x,y,bare" -> just that one (mixed rigs) +// BT_POD_CHANMAP -- per-cart channel override, e.g. +// BT_POD_CHANMAP=Comm=blue,Heat=red +// The 1995 config authors which colour line each surface rides (L4GAUGE.CFG), +// but a rebuilt cab can have its RGB splitter wired to different panels than +// the original -- on Nick's cart the coolant (Heat) and hot-box (Comm) screens +// came out swapped, which is a WIRING difference, not a software bug. Rather +// than edit authentic config, override the channel here, per rig. +// Returns 0/1/2 (red/green/blue) for a named port, or -1 if unmapped. +static int + PodChannelOverride(const char *portName) +{ + static int loaded = 0; + static char names[8][20]; + static int chans[8]; + static int count = 0; + if (!loaded) + { + loaded = 1; + const char *e = getenv("BT_POD_CHANMAP"); + while (e != NULL && *e && count < 8) + { + while (*e == ' ' || *e == ',') ++e; + const char *eq = strchr(e, '='); + if (eq == NULL) break; + int n = 0; + for (const char *p = e; p < eq && n < 19; ++p) names[count][n++] = *p; + names[count][n] = 0; + const char *v = eq + 1; + int ch = -1; + if (_strnicmp(v, "red", 3) == 0) ch = L4GraphicsPort::RedChannel; + else if (_strnicmp(v, "green", 5) == 0) ch = L4GraphicsPort::GreenChannel; + else if (_strnicmp(v, "blue", 4) == 0) ch = L4GraphicsPort::BlueChannel; + if (ch >= 0) + { + chans[count] = ch; + DEBUG_STREAM << "[glasswin] channel override: " << names[count] + << " -> " << (ch == L4GraphicsPort::RedChannel ? "red" + : ch == L4GraphicsPort::GreenChannel ? "green" : "blue") + << std::endl << std::flush; + ++count; + } + const char *nx = strchr(v, ','); + if (nx == NULL) break; + e = nx + 1; + } + } + for (int i = 0; i < count; ++i) + if (portName != NULL && _stricmp(names[i], portName) == 0) + return chans[i]; + return -1; +} + +// The channel a surface actually rides on THIS rig: the override if given, +// else what the pod config authored. +static int + ResolvedChannelOf(const char *portName, L4GraphicsPort *port) +{ + int ov = PodChannelOverride(portName); + if (ov >= 0) + return ov; + return (port != NULL) ? (port->GetEnableID() & 0x7F) : -1; +} + +// BT_POD_IDENT=1 -- draw each surface's identity ON the glass (title, monitor, +// and in RGB mode the channel->surface map). The only reliable way to learn +// which physical cab panel a Windows display drives. +static int + PodIdentMode() +{ + static int m = -1; + if (m < 0) + { + const char *e = getenv("BT_POD_IDENT"); + m = (e != NULL && e[0] != 0 && e[0] != '0') ? 1 : 0; + } + return m; +} + +// BT_POD_CHANTEST=<seconds> -- CHANNEL WALK, the instrument for mapping +// splitter-wired glass. On a cab, three mono CRTs share one VGA port through +// its R/G/B lines, so "which panel is which" cannot be read off Windows: the +// desktop sees ONE display for three monitors. This drives ONE channel at a +// time -- all-red, then all-green, then all-blue, N seconds each, with the +// channel name drawn huge -- so the answer is simply: watch which panel lights +// up, and when. Readable even over a rough livestream. +// Returns the dwell in seconds (0 = off). +static int + PodChanTestSeconds() +{ + static int s = -1; + if (s < 0) + { + const char *e = getenv("BT_POD_CHANTEST"); + s = (e != NULL && e[0] != 0) ? atoi(e) : 0; + if (s < 0) s = 0; + if (e != NULL && e[0] != 0 && s == 0) s = 3; // "1"/"on" -> 3s default + } + return s; +} + +// Which channel is lit right now (0=red 1=green 2=blue), from the wall clock. +static int + PodChanTestPhase() +{ + int dwell = PodChanTestSeconds(); + if (dwell <= 0) + return -1; + return (int)((GetTickCount() / (DWORD)(dwell * 1000)) % 3); +} + // BT_POD_RGB=1 -- the pod's REAL video wiring (see pod-hardware.md §THE RGB // SPLIT): one VGA port per window, its R/G/B analog lines split to three // monochrome MFD monitors. Implies pod surface mode. @@ -609,6 +732,73 @@ void } // Restore saved positions over the just-computed pod-faithful defaults. Called +//--------------------------------------------------------------------------- +// MONITOR BINDING (2026-08-06) -- "monitor:<name-or-index>" instead of x,y. +// +// Absolute desktop coordinates do not survive a reboot on a pod: Windows +// renumbers the virtual desktop whenever adapters enumerate differently (a USB +// display adapter is especially prone), so a layout pinned to 800,122 can land +// on the wrong glass -- or off-screen -- after a power cycle. Binding to the +// MONITOR instead and resolving its rect at startup is stable across boots. +// Heat MFD=monitor:\.\DISPLAY4,bare (device name -- what the receipts print) +// Heat MFD=monitor:2,bare (index into the enumeration order) +// The surface is centred on that monitor; an exact-size panel fills it. +//--------------------------------------------------------------------------- +struct MonScan { int index; const char *want; int wantIndex; RECT rect; int found; }; + +static BOOL CALLBACK + MonScanProc(HMONITOR mon, HDC, LPRECT, LPARAM param) +{ + MonScan *sc = (MonScan *)param; + MONITORINFOEXA mi; + memset(&mi, 0, sizeof(mi)); + mi.cbSize = sizeof(mi); + if (GetMonitorInfoA(mon, (MONITORINFO *)&mi)) + { + int hit = 0; + if (sc->want != NULL) + { + // Match the full device name OR just its tail, so a hand-written + // cfg can say `monitor:DISPLAY4` and skip the \\.\ prefix entirely + // (backslashes in a config file are a foot-gun -- every shell, + // editor and copy-paste path mangles them differently). + size_t dl = strlen(mi.szDevice), wl = strlen(sc->want); + if (_stricmp(mi.szDevice, sc->want) == 0) + hit = 1; + else if (wl > 0 && wl <= dl + && _stricmp(mi.szDevice + (dl - wl), sc->want) == 0) + hit = 1; + } + if (!hit && sc->wantIndex >= 0 && sc->index == sc->wantIndex) + hit = 1; + if (hit && !sc->found) + { + sc->rect = mi.rcMonitor; + sc->found = 1; + } + } + ++sc->index; + return TRUE; +} + +// Resolve "monitor:<spec>" to a rect. Returns 1 on success. +static int + ResolveMonitorSpec(const char *spec, RECT *out) +{ + while (*spec == ' ' || *spec == ' ') ++spec; + MonScan sc; + memset(&sc, 0, sizeof(sc)); + sc.wantIndex = -1; + if (spec[0] >= '0' && spec[0] <= '9') + sc.wantIndex = atoi(spec); + else + sc.want = spec; + EnumDisplayMonitors(NULL, NULL, MonScanProc, (LPARAM)&sc); + if (sc.found) + *out = sc.rect; + return sc.found; +} + // from Create AFTER ComputeLayout, so any window not named in the file keeps its // computed spot. Restored windows are flagged so the WM_TIMER re-snap (which // re-runs ComputeLayout once the main window appears) leaves them alone. @@ -646,7 +836,34 @@ static void *end = '\0'; int x = 0, y = 0, w = 0, h = 0; - if (sscanf(eq + 1, "%d,%d,%d,%d", &x, &y, &w, &h) < 2) + int haveXY = (sscanf(eq + 1, "%d,%d,%d,%d", &x, &y, &w, &h) >= 2); + int viaMonitor = 0; + if (!haveXY) + { + // "monitor:<name|index>" -- boot-stable binding (see the resolver). + const char *v = eq + 1; + while (*v == ' ' || *v == '\t') ++v; + if (_strnicmp(v, "monitor:", 8) == 0 || _strnicmp(v, "mon:", 4) == 0) + { + char spec[64]; + const char *p2 = strchr(v, ':') + 1; + int n2 = 0; + while (*p2 && *p2 != ',' && n2 < (int)sizeof(spec) - 1) spec[n2++] = *p2++; + spec[n2] = 0; + RECT mr; + if (ResolveMonitorSpec(spec, &mr)) + { + x = mr.left; y = mr.top; + w = mr.right - mr.left; h = mr.bottom - mr.top; + haveXY = 1; + viaMonitor = 1; + } + else + DEBUG_STREAM << "[glasswin] monitor '" << spec + << "' not found -- leaving computed placement\n" << std::flush; + } + } + if (!haveXY) continue; // Trailing options after the numbers, comma-separated. Unknown ones @@ -666,11 +883,30 @@ static void GWin *gw = FindGWinByTitle(s); if (gw == NULL) continue; - gw->wantX = x; - gw->wantY = y; - gw->noFrame = noframe; + // "bare" is a pod panel: it always implies frameless. (Without this a + // cfg line saying only ",bare" silently re-framed a window that pod + // surface mode had already cropped -- caught on the bench 2026-08-06.) + gw->noFrame = (noframe || bare) ? 1 : 0; if (bare && gw->buttonCount > 0) MakeBareSurface(*gw); // per-window pod panel (mixed rigs) + if (viaMonitor) + { + // x,y,w,h is the MONITOR rect -- centre this surface on it (an + // exact-size pod panel fills it edge to edge). + int sw = gw->clientW > 0 ? gw->clientW : (gw->surfaceRect.right - gw->surfaceRect.left); + int sh = gw->clientH > 0 ? gw->clientH : (gw->surfaceRect.bottom - gw->surfaceRect.top); + gw->wantX = x + ((w - sw) / 2); + gw->wantY = y + ((h - sh) / 2); + DEBUG_STREAM << "[glasswin] '" << gw->title + << "' bound to monitor " << x << "," << y << " " << w << "x" << h + << " -> window at " << gw->wantX << "," << gw->wantY + << std::endl << std::flush; + } + else + { + gw->wantX = x; + gw->wantY = y; + } gw->restored = 1; ++restored; } @@ -841,6 +1077,19 @@ static void mask = 0xFF; // index the low (palette) byte palId = SVGA16::SecondaryPalette; } + // RGB mode: the base surface rides a CHANNEL, so its tint is that pure + // primary -- resolved live (override first), never the build-time guess. + if (w->groupCount > 0 && tint >= 0) + { + switch (ResolvedChannelOf(w->portPrimary, port)) + { + case L4GraphicsPort::RedChannel: tint = (int)0x00FF0000; break; + case L4GraphicsPort::GreenChannel: tint = (int)0x0000FF00; break; + case L4GraphicsPort::BlueChannel: tint = (int)0x000000FF; break; + default: break; + } + } + int ow = 0, oh = 0; svga->ExpandPlaneToBGRA(mask, palId, tint, w->rotate, gStage, &ow, &oh); @@ -880,9 +1129,9 @@ static void if (ga != NULL && ga->GetEnableID() != L4GraphicsPort::BlankColor) gp = ga; } - const int ch = gp->GetEnableID(); + const int ch = ResolvedChannelOf(w->groupPort[gi], gp); unsigned long chMask; - switch (ch & 0x7F) + switch (ch) { case L4GraphicsPort::RedChannel: chMask = 0x00FF0000ul; break; case L4GraphicsPort::GreenChannel: chMask = 0x0000FF00ul; break; @@ -1069,6 +1318,113 @@ static void // (NO full-face flash overlay here either -- same wrongness as the // L4VB16 one, removed 2026-08-03: the buttons are big rects tucked // under the surface by design, and the protruding edge is the lamp.) + + // ---- CHANNEL WALK (BT_POD_CHANTEST) --------------------------------- + // Overwrite the whole surface with ONE channel at full intensity, so + // exactly one monitor of a splitter-fed trio lights. Cycles R->G->B. + { + const int phase = PodChanTestPhase(); + if (phase >= 0) + { + static const COLORREF chColor[3] = + { RGB(255, 0, 0), RGB(0, 255, 0), RGB(0, 0, 255) }; + static const char *chName[3] = { "RED", "GREEN", "BLUE" }; + HBRUSH fill = CreateSolidBrush(chColor[phase]); + FillRect(dc, &w->surfaceRect, fill); + DeleteObject(fill); + + // Name the channel AND what the pod authors onto it here, so a + // lit panel identifies itself completely: "GREEN = Mfd2". + const char *who = "-"; + GaugeRenderer *cgr = BTResolveGaugeRenderer(); + if (cgr != NULL) + { + const char *cand[4]; int nc = 0; + cand[nc++] = w->portPrimary; + for (int gi = 0; gi < w->groupCount && nc < 4; ++gi) + cand[nc++] = w->groupPort[gi]; + for (int i = 0; i < nc; ++i) + { + L4GraphicsPort *pp = + static_cast<L4GraphicsPort*>(cgr->GetGraphicsPort(cand[i])); + if (pp != NULL && (pp->GetEnableID() & 0x7F) == phase) + { who = cand[i]; break; } + } + } + char big[96]; + sprintf(big, "%s %s = %s", w->monitorName, chName[phase], who); + RECT br = w->surfaceRect; + br.top += (br.bottom - br.top) / 3; + SetBkMode(dc, TRANSPARENT); + DrawLabelText(dc, big, titleFont, &br, RGB(0, 0, 0), + DT_CENTER | DT_SINGLELINE | DT_VCENTER); + } + } + + // ---- PANEL IDENTIFY (BT_POD_IDENT=1) -------------------------------- + // Pod bring-up: nobody can tell which PHYSICAL panel a Windows display + // is, and desktop coordinate order says nothing about where a panel + // sits in the cab -- guessing that mapping is exactly how it came out + // wrong. So label each surface ON ITS OWN GLASS with what it is, which + // monitor it came from, and (in RGB mode) the channel->surface map. + // Walk the pod, read the panels, write the layout from what you SAW. + if (PodIdentMode()) + { + char line1[96], line2[160]; + sprintf(line1, "%s", w->title); + if (w->groupCount > 0) + { + // name the channels so a splitter-fed trio is self-describing + char parts[128]; + parts[0] = 0; + GaugeRenderer *igr = BTResolveGaugeRenderer(); + const char *names[4]; int chans[4]; int nn = 0; + names[nn] = w->portPrimary; chans[nn] = -1; ++nn; + for (int gi = 0; gi < w->groupCount && nn < 4; ++gi) + { names[nn] = w->groupPort[gi]; chans[nn] = -1; ++nn; } + for (int i = 0; i < nn; ++i) + { + const char *chName = "?"; + if (igr != NULL) + { + L4GraphicsPort *pp = + static_cast<L4GraphicsPort*>(igr->GetGraphicsPort(names[i])); + if (pp != NULL) + switch (pp->GetEnableID() & 0x7F) + { + case L4GraphicsPort::RedChannel: chName = "RED"; break; + case L4GraphicsPort::GreenChannel: chName = "GREEN"; break; + case L4GraphicsPort::BlueChannel: chName = "BLUE"; break; + case L4GraphicsPort::AllChannels: chName = "RGB"; break; + default: chName = "blank"; break; + } + } + char one[48]; + sprintf(one, "%s%s=%s", (i ? " " : ""), chName, names[i]); + if (strlen(parts) + strlen(one) < sizeof(parts) - 1) + strcat(parts, one); + } + sprintf(line2, "%s %s", w->monitorName, parts); + } + else + { + sprintf(line2, "%s surface=%s", w->monitorName, + w->portPrimary ? w->portPrimary : "-"); + } + + RECT lr = w->surfaceRect; + lr.bottom = lr.top + 64; + HBRUSH back = CreateSolidBrush(RGB(0, 0, 0)); + FillRect(dc, &lr, back); + DeleteObject(back); + SetBkMode(dc, TRANSPARENT); + RECT t1 = lr; t1.top += 4; t1.bottom = t1.top + 30; + DrawLabelText(dc, line1, titleFont, &t1, RGB(255, 255, 0), + DT_LEFT | DT_SINGLELINE | DT_VCENTER); + RECT t2 = lr; t2.top += 32; t2.bottom = t2.top + 28; + DrawLabelText(dc, line2, buttonFont, &t2, RGB(0, 255, 255), + DT_LEFT | DT_SINGLELINE | DT_VCENTER); + } } BitBlt(winDC, 0, 0, client.right - client.left, client.bottom - client.top, @@ -1205,11 +1561,23 @@ void if (gWinCount != 0) return; // already up - // BT_GAUGE_SEC_ROT: the portrait-CRT unrotation direction (shared with the - // D3D dev path; default 3 = CW = user-verified upright). + // BT_GAUGE_SEC_ROT: how far to turn the secondary/radar surface. + // 0 = none, 1 = 90 CCW, 2 = 180, 3 = 90 CW (default -- the pod's + // portrait CRT, user-verified upright). + // 0 and 2 keep the picture LANDSCAPE, which a modern replacement panel + // needs. (Until 2026-08-06 this read only '1' and forced everything else + // to 3, so BT_GAUGE_SEC_ROT=2 was silently ignored -- caught on the cart + // when a 180 request kept coming back portrait.) { const char *rv = getenv("BT_GAUGE_SEC_ROT"); - gRadarRot = (rv != NULL && rv[0] == '1') ? 1 : 3; + if (rv != NULL && rv[0] >= '0' && rv[0] <= '3') + gRadarRot = rv[0] - '0'; + else + gRadarRot = 3; + DEBUG_STREAM << "[glasswin] radar rotation " << gRadarRot + << (gRadarRot == 3 ? " (90 CW)" : gRadarRot == 1 ? " (90 CCW)" + : gRadarRot == 2 ? " (180)" : " (none)") + << std::endl << std::flush; } memset(latched, 0, sizeof(latched)); @@ -1348,6 +1716,8 @@ void if (mon != NULL && GetMonitorInfoA(mon, (MONITORINFO *)&mi)) { devName = mi.szDevice; + strncpy(w.monitorName, mi.szDevice, sizeof(w.monitorName) - 1); + w.monitorName[sizeof(w.monitorName) - 1] = 0; mx = mi.rcMonitor.left; my = mi.rcMonitor.top; mw = mi.rcMonitor.right - mi.rcMonitor.left; mh = mi.rcMonitor.bottom - mi.rcMonitor.top; diff --git a/engine/MUNGA_L4/L4GREND.cpp b/engine/MUNGA_L4/L4GREND.cpp index 63965e6..22e7e53 100644 --- a/engine/MUNGA_L4/L4GREND.cpp +++ b/engine/MUNGA_L4/L4GREND.cpp @@ -90,6 +90,23 @@ L4GaugeRenderer::L4GaugeRenderer(bool windowed, int *secondaryIndex, int *aux1In char *plasma_string = getenv("L4PLASMA"); + // + // L4PLASMA=NONE / OFF / 0 -- no plasma marquee at all (2026-08-06). + // A real cab drives the 128x32 marquee over serial; a rebuilt cart may + // not have one wired, and the GLASS profile force-defaults L4PLASMA to + // SCREEN, which puts a desktop plasma window on the pod's glass where + // nothing should be. Leaving the variable UNSET is not an option + // there (the profile fills it in), so give "off" an explicit spelling. + // + if (plasma_string != NULL + && (_stricmp(plasma_string, "NONE") == 0 + || _stricmp(plasma_string, "OFF") == 0 + || strcmp(plasma_string, "0") == 0)) + { + Tell("Plasma display disabled (L4PLASMA=none)\n"); + plasma_string = NULL; + } + if (plasma_string != NULL) { //Win32 Serial support: ADB 06/30/07 diff --git a/engine/MUNGA_L4/L4VB16.cpp b/engine/MUNGA_L4/L4VB16.cpp index 4da69a6..879b5ef 100644 --- a/engine/MUNGA_L4/L4VB16.cpp +++ b/engine/MUNGA_L4/L4VB16.cpp @@ -739,6 +739,34 @@ void SVGA16::ExpandPlaneToBGRA(int mask, int paletteID, int monoTint, int rotate return; } + // 180 degrees (rotate 2): dimensions UNCHANGED -- the case a landscape + // replacement panel needs when the 1995 glass was a portrait CRT mounted + // the other way up. Added 2026-08-06 from the crash cart, whose radar is a + // landscape LCD: 90 CW left the picture portrait and overhanging its + // monitor, and a further 90 CW is this. + if (rotateQuadrant == 2) + { + for (int oy = 0; oy < h; oy++) + { + unsigned long *d = dst + oy * w; + for (int ox = 0; ox < w; ox++) + { + Word s = base[(h - 1 - oy) * w + (w - 1 - ox)]; + if (monoTint < 0) + { + PaletteTriplet *pe = &(pal->paletteData.Color[s & mask]); + d[ox] = ((unsigned long)pe->Red << 16) | + ((unsigned long)pe->Green << 8) | (unsigned long)pe->Blue; + } + else + d[ox] = (s & mask) ? (unsigned long)monoTint : 0UL; + } + } + if (outW) *outW = w; + if (outH) *outH = h; + return; + } + // 90-degree rotation: output is transposed (ow = h, oh = w). rotate 3 = CW, // rotate 1 = CCW (the DrawDevSurface convention; BT_GAUGE_SEC_ROT picks it). int ow = h, oh = w; diff --git a/game/btl4main.cpp b/game/btl4main.cpp index 138b1bd..cd05208 100644 --- a/game/btl4main.cpp +++ b/game/btl4main.cpp @@ -407,6 +407,18 @@ static const char *gBTCwdFixNote = NULL; // int gBTFitDisplay = 0; +// +// L4PLASMA=NONE/OFF/0 means "no marquee at all" (L4GREND.cpp reads the same +// spellings). Only used to keep the boot banner honest. +// +static bool BTPlasmaDisabled(void) +{ + const char *p = getenv("L4PLASMA"); + return p != NULL && (_stricmp(p, "NONE") == 0 || + _stricmp(p, "OFF") == 0 || + strcmp(p, "0") == 0); +} + static void BTEnsureContentDirectory(void) { if (GetFileAttributesA("BTL4.RES") != INVALID_FILE_ATTRIBUTES) @@ -848,6 +860,18 @@ int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine { gBTFitDisplay = 1; } + // + // BT_FIT=1 -- the env spelling of -fit, so a POD can freeze it in + // content\environ.ini alongside the rest of its rig config instead of + // depending on every launcher passing the flag (2026-08-06, crash cart: + // the main view has to fill its 800x600 panel on every boot, however the + // game gets started -- shortcut, autostart or scheduled task). + // + { + const char *fitEnv = getenv("BT_FIT"); + if (fitEnv != NULL && fitEnv[0] != '\0' && fitEnv[0] != '0') + gBTFitDisplay = 1; + } // BT_JOYCONFIG=1: the generic-joystick capture wizard (flight sticks / // HOTAS / pedals -- L4JOY.h). Console prompts detect which device/axis @@ -1145,7 +1169,9 @@ int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine std::cout << "[boot] platform profile: " << (fe_menu_mode ? "MENU (front end -- no profile applied)" : gBTPlatformPod ? "POD (RIO cockpit input; multi-surface gauges/MFDs via pod hardware or explicit L4GAUGE)" - : gBTPlatformGlass ? "GLASS (PadRIO + plasma window)" + : gBTPlatformGlass ? (BTPlasmaDisabled() + ? "GLASS (PadRIO; plasma off [L4PLASMA])" + : "GLASS (PadRIO + plasma window)") : "DEV (single window + keyboard)"); if (!fe_menu_mode) std::cout << " [secondary displays: " << kGlassLayoutName[glassLayout] << "]"; diff --git a/scratchpad/pod/glass_layout_ident.cfg b/scratchpad/pod/glass_layout_ident.cfg new file mode 100644 index 0000000..0e42804 --- /dev/null +++ b/scratchpad/pod/glass_layout_ident.cfg @@ -0,0 +1,8 @@ +# IDENTIFY PASS -- assignment here is arbitrary on purpose. Each panel prints +# its own name + monitor, so we map the cab from what the GLASS says, not from +# desktop coordinate order (which is what got it wrong the first time). +# +# Bound by MONITOR IDENTITY, not x,y: survives reboots and adapter renumbering. +VGA Port A=monitor:DISPLAY4,bare +VGA Port B=monitor:DISPLAY2,bare +Secondary / Radar=monitor:DISPLAY1,bare diff --git a/scratchpad/pod/glass_layout_swap.cfg b/scratchpad/pod/glass_layout_swap.cfg new file mode 100644 index 0000000..5d312b4 --- /dev/null +++ b/scratchpad/pod/glass_layout_swap.cfg @@ -0,0 +1,8 @@ +# Corrected from the cab photo (2026-08-06): DISPLAY4 feeds the COLOUR RADAR +# LCD -- the RGB composite was landing there and showing all three channels +# overlaid. Colour surface to the colour panel; the RGB composites to the +# displays that feed the mono CRT groups. Which CRT group is DISPLAY2 vs +# DISPLAY1 is still unknown -- the channel test settles it. +Secondary / Radar=monitor:DISPLAY4,bare +VGA Port A=monitor:DISPLAY2,bare +VGA Port B=monitor:DISPLAY1,bare diff --git a/scratchpad/pod/listwins.ps1 b/scratchpad/pod/listwins.ps1 new file mode 100644 index 0000000..65649f1 --- /dev/null +++ b/scratchpad/pod/listwins.ps1 @@ -0,0 +1,25 @@ +Add-Type @" +using System;using System.Text;using System.Runtime.InteropServices; +public class W{ + [DllImport("user32.dll")] public static extern bool EnumWindows(EnumWindowsProc cb,IntPtr p); + public delegate bool EnumWindowsProc(IntPtr h,IntPtr p); + [DllImport("user32.dll")] public static extern int GetWindowText(IntPtr h,StringBuilder s,int c); + [DllImport("user32.dll")] public static extern bool IsWindowVisible(IntPtr h); + [DllImport("user32.dll")] public static extern uint GetWindowThreadProcessId(IntPtr h,out uint p); + [DllImport("user32.dll")] public static extern bool GetWindowRect(IntPtr h,out R r); + public struct R{public int L,T,Rr,B;} +} +"@ +$target = (Get-Process btl4 -ErrorAction SilentlyContinue).Id +if (-not $target) { Write-Output "btl4 NOT RUNNING"; exit } +[W]::EnumWindows({ param($h,$l) + $o = 0 + [W]::GetWindowThreadProcessId($h, [ref]$o) | Out-Null + if ($o -eq $target -and [W]::IsWindowVisible($h)) { + $sb = New-Object System.Text.StringBuilder 256 + [W]::GetWindowText($h, $sb, 256) | Out-Null + $r = New-Object 'W+R' + [W]::GetWindowRect($h, [ref]$r) | Out-Null + Write-Output ("WIN '" + $sb.ToString() + "' at " + $r.L + "," + $r.T + " " + ($r.Rr-$r.L) + "x" + ($r.B-$r.T)) + } + return $true }, [IntPtr]::Zero) | Out-Null diff --git a/scratchpad/pod/mergeprofile.ps1 b/scratchpad/pod/mergeprofile.ps1 new file mode 100644 index 0000000..87e5b27 --- /dev/null +++ b/scratchpad/pod/mergeprofile.ps1 @@ -0,0 +1,21 @@ +# Merge the POD PROFILE block into the cart's content\environ.ini, idempotently. +# Any previous block between the two markers is replaced, so re-running is safe. +$ini = 'C:\bt411\BT411_4.11.801\content\environ.ini' +$prof = 'C:\bt411\BT411_4.11.801\content\podprofile.ini' + +$block = Get-Content $prof +$keep = @() +if (Test-Path $ini) { + $inBlock = $false + foreach ($line in (Get-Content $ini)) { + if ($line -match '^# ==== BT411 POD PROFILE') { $inBlock = $true; continue } + if ($line -match '^# ==== END BT411 POD PROFILE') { $inBlock = $false; continue } + if (-not $inBlock) { $keep += $line } + } +} +# Trim trailing blanks so repeated merges do not grow the file. +while ($keep.Count -gt 0 -and $keep[-1].Trim() -eq '') { $keep = $keep[0..($keep.Count-2)] } + +($keep + @('') + $block) | Set-Content $ini -Encoding ascii +Write-Output "environ.ini: $((Get-Content $ini).Count) lines, profile keys:" +Get-Content $ini | Select-String '^(BT_|L4)' | ForEach-Object { $_.Line } diff --git a/scratchpad/pod/podprofile.ini b/scratchpad/pod/podprofile.ini new file mode 100644 index 0000000..9eebaae --- /dev/null +++ b/scratchpad/pod/podprofile.ini @@ -0,0 +1,36 @@ +# ==== BT411 POD PROFILE (ALPHA-MR crash cart) ================================ +# Frozen 2026-08-06 from the verified-good bring-up. Everything a launcher +# used to have to pass now lives here, so the rig comes up correct however +# the game is started -- shortcut, scheduled task or autostart. +# Physical mapping + the RGB-split decode: context/pod-hardware.md. +# +# The layout itself (which surface sits on which monitor) is in +# glass_layout.cfg beside this file, loaded by BT_GLASS_LAYOUT=load: +# Secondary / Radar = monitor:DISPLAY4 (colour LCD in the yellow frame) +# VGA Port A = monitor:DISPLAY2 (RGB triple: Heat + Comm + Mfd2) +# VGA Port B = monitor:DISPLAY1 (RGB triple: Mfd1 + Mfd3) + +BT_GLASS=1 +BT_GLASS_PANELS=1 + +# ONE VGA port's R/G/B lines drive THREE mono MFDs -- composite the planes. +BT_POD_RGB=1 + +# THIS cab's splitter wires Comm and Heat to different colour lines than the +# 1995 L4GAUGE.CFG assumed (kills/deaths and coolant loops came up swapped). +BT_POD_CHANMAP=Comm=blue,Heat=red + +# The radar panel here is a LANDSCAPE LCD; the 1995 pod rotated only because +# its CRT was mounted portrait. 0 = leave the 640x480 source alone. +BT_GAUGE_SEC_ROT=0 + +BT_GLASS_LAYOUT=load +BT_START_INSIDE=1 + +# Main view fills its 800x600 panel (DISPLAY3, the primary) with no border. +BT_FIT=1 + +# No 128x32 marquee wired on this cart -- and the glass profile would +# otherwise default L4PLASMA to SCREEN and drop a plasma window on the glass. +L4PLASMA=NONE +# ==== END BT411 POD PROFILE ================================================== diff --git a/scratchpad/pod/runpod.bat b/scratchpad/pod/runpod.bat index b05ed13..045d63d 100644 --- a/scratchpad/pod/runpod.bat +++ b/scratchpad/pod/runpod.bat @@ -1,12 +1,9 @@ @echo off -REM BT411 pod launch -- console session (started via schtasks /IT). +REM BT411 pod -- ALPHA-MR. The rig config is FROZEN in content\environ.ini +REM (glass panels, RGB split, channel map, radar rotation, -fit, no plasma), +REM so this launcher carries nothing but the log switches and the mission. +REM Fallback if environ.ini is ever lost: runpod_env.bat sets it all inline. cd /d C:\bt411\BT411_4.11.801\content -set BT_GLASS=1 -set BT_GLASS_PANELS=1 -set BT_POD_RGB=1 -set BT_GLASS_LAYOUT=load set BT_GLASS_LOG=1 -set BT_START_INSIDE=1 -set BT_FE_SOLO=1 set BT_LOG=podrun.log -start "" ..\build\Release\btl4.exe +start "" ..\build\Release\btl4.exe -egg PODTEST.EGG diff --git a/scratchpad/pod/runpod_env.bat b/scratchpad/pod/runpod_env.bat new file mode 100644 index 0000000..4f23d74 --- /dev/null +++ b/scratchpad/pod/runpod_env.bat @@ -0,0 +1,16 @@ +@echo off +REM BT411 pod -- ALPHA-MR working config. +REM layout : radar on DISPLAY4 (the colour LCD), RGB composites on 2 and 1 +REM fix 1 : Comm/Heat ride swapped colour lines on THIS cab's splitter +REM fix 2 : radar panel is landscape -> UNROTATED landscape, not the pod portrait CRT +cd /d C:\bt411\BT411_4.11.801\content +set BT_GLASS=1 +set BT_GLASS_PANELS=1 +set BT_POD_RGB=1 +set BT_POD_CHANMAP=Comm=blue,Heat=red +set BT_GAUGE_SEC_ROT=0 +set BT_GLASS_LAYOUT=load +set BT_GLASS_LOG=1 +set BT_START_INSIDE=1 +set BT_LOG=podrun.log +start "" ..\build\Release\btl4.exe -egg PODTEST.EGG diff --git a/tools/podbringup.bat b/tools/podbringup.bat new file mode 100644 index 0000000..4244b95 --- /dev/null +++ b/tools/podbringup.bat @@ -0,0 +1,101 @@ +@echo off +REM =========================================================================== +REM podbringup.bat -- ONE double-click pod bring-up. BT411. +REM +REM 1. self-elevates (accept the UAC prompt -- that is the only interaction) +REM 2. installs the built-in Windows OpenSSH SERVER +REM 3. authorizes ONE key (the laptop's bt411 key) and locks its ACL +REM 4. opens SSH on private/domain profiles ONLY (never the public internet) +REM 5. probes the display/GPU/monitor/serial topology +REM 6. sends the results BACK over Tailscale -- no copying by hand +REM +REM Undo (admin cmd): +REM sc stop sshd & sc config sshd start= disabled +REM del C:\ProgramData\ssh\administrators_authorized_keys +REM netsh advfirewall firewall delete rule name="BT411 SSH" +REM =========================================================================== + +REM ---- 1. elevate ----------------------------------------------------------- +net session >nul 2>&1 +if %errorlevel% neq 0 ( + echo Requesting administrator rights -- click YES on the prompt... + powershell -Command "Start-Process -FilePath '%~f0' -Verb RunAs" + exit /b +) + +setlocal enabledelayedexpansion +set LAPTOP=desktop-ae1su9u +set TS=C:\PROGRA~1\Tailscale\tailscale.exe +set REPORT=%~dp0podbringup_report.txt +set DXOUT=%~dp0podbringup_dxdiag.txt + +echo ==================== BT411 POD BRING-UP ====================> "%REPORT%" +echo host=%COMPUTERNAME% user=%USERNAME% session=%SESSIONNAME%>> "%REPORT%" +ver >> "%REPORT%" +echo.>> "%REPORT%" + +REM ---- 2. OpenSSH server ---------------------------------------------------- +echo [1/5] installing OpenSSH server (this can take a minute)... +echo ---- OpenSSH install ---->> "%REPORT%" +dism /online /quiet /norestart /add-capability /capabilityname:OpenSSH.Server~~~~0.0.1.0 >> "%REPORT%" 2>&1 +sc config sshd start= auto >> "%REPORT%" 2>&1 +net start sshd >> "%REPORT%" 2>&1 +sc query sshd | findstr /i "STATE" >> "%REPORT%" 2>&1 + +REM ---- 3. authorize the laptop key ----------------------------------------- +echo [2/5] authorizing the BT411 key... +if not exist C:\ProgramData\ssh mkdir C:\ProgramData\ssh +set AKF=C:\ProgramData\ssh\administrators_authorized_keys +findstr /c:"bt411-claude-code@laptop" "%AKF%" >nul 2>&1 +if errorlevel 1 ( + echo ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICmGkinDbZMwQ/UhwqU/oECUIeAN+pBjLgsekj41di7W bt411-claude-code@laptop>> "%AKF%" +) +icacls "%AKF%" /inheritance:r /grant "SYSTEM:F" /grant "BUILTIN\Administrators:F" >> "%REPORT%" 2>&1 +echo ---- authorized_keys ---->> "%REPORT%" +type "%AKF%" >> "%REPORT%" 2>&1 + +REM ---- 4. firewall (private/domain only) ------------------------------------ +echo [3/5] firewall rule (private/tailnet only)... +netsh advfirewall firewall show rule name="BT411 SSH" >nul 2>&1 +if errorlevel 1 ( + netsh advfirewall firewall add rule name="BT411 SSH" dir=in action=allow protocol=TCP localport=22 profile=private,domain >> "%REPORT%" 2>&1 +) + +REM ---- 5. topology probe ---------------------------------------------------- +echo [4/5] probing displays / GPUs / monitors / serial ports... +echo.>> "%REPORT%" +echo ---- OS ---->> "%REPORT%" +wmic os get Caption,Version,BuildNumber,OSArchitecture /format:list >> "%REPORT%" 2>&1 +echo ---- GPUs ---->> "%REPORT%" +wmic path Win32_VideoController get Name,DriverVersion,DriverDate,CurrentHorizontalResolution,CurrentVerticalResolution /format:list >> "%REPORT%" 2>&1 +echo ---- Monitors ---->> "%REPORT%" +wmic path Win32_DesktopMonitor get DeviceID,Name,ScreenWidth,ScreenHeight,Availability /format:list >> "%REPORT%" 2>&1 +echo ---- Serial ports (RIO) ---->> "%REPORT%" +wmic path Win32_SerialPort get DeviceID,Name /format:list >> "%REPORT%" 2>&1 +echo ---- Tailscale ---->> "%REPORT%" +"%TS%" ip -4 >> "%REPORT%" 2>&1 +echo ---- CONNECT WITH ---->> "%REPORT%" +echo ssh -i ~/.ssh/bt411_pod %USERNAME%@bt411-pod>> "%REPORT%" + +echo [5/5] dxdiag (monitor rects + D3D caps, ~30s)... +dxdiag /whql:off /t "%DXOUT%" + +REM ---- 6. send it all back -------------------------------------------------- +echo Sending results back to %LAPTOP% over Tailscale... +"%TS%" file cp "%REPORT%" "%DXOUT%" %LAPTOP%: +if errorlevel 1 ( + echo. + echo ! Taildrop send failed -- right-click these two files and use + echo "Send with Tailscale" instead: + echo %REPORT% + echo %DXOUT% +) else ( + echo ...sent. +) + +echo. +echo ================= DONE ================= +echo ssh user: %USERNAME% host: bt411-pod +type "%REPORT%" | findstr /i "STATE ssh-ed25519 Caption" +echo. +pause