Files
TeslaRel410/restoration/PlasmaNew/FS-plasma-bench.ps1
T
CydandClaude Fable 5 bd082d8c4a PlasmaNew: ESC X/x box commands in the replica + MW4 bench stream
Implements the firmware-disasm-confirmed Firestorm box protocol in the
MatrixPortal replica (ESC X outline / ESC x mode-fill via a BOXOP operand
collector, plus the ESC Y region op), updates FIRMWARE.md with the findings,
and adds FS-plasma-bench.ps1 -- a simulated MW4 serial stream for benching
the display without a pod.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 12:02:37 -05:00

248 lines
10 KiB
PowerShell

<#
plasma-bench.ps1 -- Bench-test driver for the MW4/FireStorm cockpit plasma score display.
Replays the exact byte sequences MW4.exe sends to the plasma display on COM2
(class CPlasma over C232Comm; sources: mw4\Code\MW4\CRIOMAIN.CPP + c232comm.cpp).
Port settings (c232comm.cpp Open()): 9600 baud, 8 data bits, no parity, 1 stop bit,
no CTS/RTS/XON flow control; DTR is pulsed high at open then dropped ~10 ms later
(DTR_CONTROL_ENABLE -> DTR_CONTROL_DISABLE + Sleep(10)). Bytes go out raw, unframed.
Command set (ESC = 0x1B), CRIOMAIN.CPP lines 2998-3139:
ESC @ clear screen (PlasmaClear)
ESC G n cursor mode (PlasmaCursor: 0=off 1=on 2=flash)
ESC R n cursor X (PlasmaCursorX)
ESC Q n cursor Y (PlasmaCursorY)
ESC H n font attribute (PlasmaFontAttr: 0=normal 1=half 2=line 4=reverse 8=flash)
ESC K n font select (PlasmaFont: 0-3,6,7 = 5x7 4,5 = 10x14)
ESC X l t r b draw box outline (PlasmaBoxDraw)
ESC x 0 l t r b fill/blank box (PlasmaBoxFill)
<ascii bytes> text at current cursor (PlasmaText)
Game sequence replayed here (PlasmaDisplay, CRIOMAIN.CPP:3281):
startup clear + cursor off (CPlasma ctor)
state 1 ready centered 9-space wipe, then centered callsign
(<=9 chars -> font 5 [10x14]; 10-20 chars -> font 2 [5x7])
state 2 start score panel: fill(27,19,93,30), boxes (27,19,42,30)+(42,19,93,30);
rank in left box (x=33 one digit / x=29 two), score right-aligned
ending at x=85-5*len; both font 2, y=20. Later updates redraw only
the field that changed (no boxes) -- use -CountUp to exercise that.
state 0 end clear (-ClearAtEnd)
Examples:
.\plasma-bench.ps1 -Port COM5 -Callsign JEFF -Rank 3 -Score 4200
.\plasma-bench.ps1 -Port COM5 -Callsign JEFF -CountUp 5 -CountUpInterval 1.5
.\plasma-bench.ps1 -DryRun # print the byte stream, touch no port
#>
param(
[string]$Port = 'COM2',
[int]$Baud = 9600,
[string]$Callsign = 'FStest6789012',
[ValidateRange(1, 99)][int]$Rank = 1,
[ValidateRange(0, 99999999)][int]$Score = 1250,
[double]$DelaySeconds = 5, # callsign hold time before the score panel appears
[int]$CountUp = 0, # extra score updates after the first draw (simulates in-game scoring)
[double]$CountUpInterval = 2, # seconds between count-up updates
[int]$CountUpStep = 150, # score increment per update
[switch]$ClearAtEnd, # cursor-off + clear before closing (game does this at mission end)
[switch]$DtrPulse, # replicate the game's DTR pulse at open (some USB CDC devices choke on it)
[switch]$HexDump, # print every packet as hex as it is sent
[switch]$DryRun # no serial port; just print the packets (implies -HexDump)
)
$ErrorActionPreference = 'Stop'
$ESC = [byte]0x1B
if ($DryRun) { $HexDump = $true }
# --- serial plumbing (C232Comm) ---------------------------------------------
$script:SerialPort = $null
function New-PlasmaPort {
$sp = New-Object System.IO.Ports.SerialPort $Port, $Baud,
([System.IO.Ports.Parity]::None), 8, ([System.IO.Ports.StopBits]::One)
$sp.Handshake = [System.IO.Ports.Handshake]::None
$sp.WriteTimeout = 1000
return $sp
}
function Open-Plasma {
if ($DryRun) { return }
# DEFAULT: plain open, modem lines untouched. The plasma protocol is data bytes
# only; the game's DTR pulse (DTR_CONTROL_ENABLE at open -> DISABLE after
# Sleep(10), c232comm.cpp) is generic port-open code, not display protocol, and
# a failed modem-line request on a USB CDC device (usbser.sys) faults the .NET
# serial stream and can knock the device off the bus. -DtrPulse opts back in
# for a real RS-232 wiring that expects it.
$sp = New-PlasmaPort
if ($DtrPulse) {
try {
$sp.RtsEnable = $false # fRtsControl = 0
$sp.DtrEnable = $true # DTR_CONTROL_ENABLE at open...
$sp.Open()
Start-Sleep -Milliseconds 10
$sp.DtrEnable = $false # ...then DTR_CONTROL_DISABLE after Sleep(10)
} catch {
Write-Warning "DTR pulse failed ($($_.Exception.Message.Trim())); reopening without touching modem lines."
try { $sp.Dispose() } catch { }
# wait for the device to settle / re-enumerate if the failed request upset it
$deadline = (Get-Date).AddSeconds(10)
while (((Get-Date) -lt $deadline) -and
([System.IO.Ports.SerialPort]::GetPortNames() -notcontains $Port)) {
Start-Sleep -Milliseconds 250
}
Start-Sleep -Milliseconds 500
$sp = New-PlasmaPort
$sp.Open()
}
} else {
$sp.Open() # .NET defaults: DTR/RTS not asserted
}
Start-Sleep -Milliseconds 200 # let USB CDC settle before the first write
$script:SerialPort = $sp
}
function Send-Packet {
# CPlasma::SendPacket -> txComLoop: one raw write per command, no framing
param([byte[]]$Bytes)
if ($HexDump) {
$hex = ($Bytes | ForEach-Object { '{0:X2}' -f $_ }) -join ' '
$txt = -join ($Bytes | ForEach-Object { if ($_ -ge 0x20 -and $_ -le 0x7E) { [char]$_ } else { '.' } })
Write-Host (' TX {0,-42} {1}' -f $hex, $txt)
}
if ($script:SerialPort) { $script:SerialPort.Write($Bytes, 0, $Bytes.Length) }
}
# --- CPlasma command primitives (CRIOMAIN.CPP:2998-3097) --------------------
function Plasma-Clear { Send-Packet ([byte[]]($ESC, 0x40)) } # ESC @
function Plasma-Cursor { param([byte]$n) Send-Packet ([byte[]]($ESC, 0x47, $n)) } # ESC G
function Plasma-CursorX { param([byte]$n) Send-Packet ([byte[]]($ESC, 0x52, $n)) } # ESC R
function Plasma-CursorY { param([byte]$n) Send-Packet ([byte[]]($ESC, 0x51, $n)) } # ESC Q
function Plasma-FontAttr { param([byte]$n) Send-Packet ([byte[]]($ESC, 0x48, $n)) } # ESC H
function Plasma-Font { param([byte]$n) Send-Packet ([byte[]]($ESC, 0x4B, $n)) } # ESC K
function Plasma-BoxDraw {
param([int]$l, [int]$t, [int]$r, [int]$b)
Send-Packet ([byte[]]($ESC, 0x58, $l, $t, $r, $b)) # ESC X
}
function Plasma-BoxFill {
param([int]$l, [int]$t, [int]$r, [int]$b)
Send-Packet ([byte[]]($ESC, 0x78, 0, $l, $t, $r, $b)) # ESC x 0
}
function Get-FontSize {
# CPlasma::GetFontSize: fonts 0-3,6,7 = 5x7; fonts 4,5 = 10x14
param([int]$Font)
if ($Font -eq 4 -or $Font -eq 5) { return @{ W = 10; H = 14 } }
return @{ W = 5; H = 7 }
}
function Plasma-PosText {
# CPlasma::PlasmaPosText, including its auto font selection + centering math
param([string]$Text, [int]$X = 0, [int]$Y = 0, [int]$Attr = 0, [int]$Font = 5)
if (-not $Text) { return }
$len = $Text.Length
if ($len -le 0) { return }
if ($Font -ne 2) { # auto-pick: big font up to 9 chars, else small, cap 20
if ($len -le 9) { $Font = 5 }
elseif ($len -le 20) { $Font = 2 }
else { $Font = 2; $len = 20 }
} elseif ($len -gt 20) { $len = 20 }
$size = Get-FontSize $Font
if ($X -eq 0 -and $Y -eq 0) { # center on the 112-wide panel (C++ integer division)
$X = 56 - [math]::Floor(($len * $size.W) / 2)
$Y = 15 - [math]::Floor($size.H / 2)
}
Plasma-CursorX $X
Plasma-CursorY $Y
Plasma-FontAttr $Attr
Plasma-Font $Font
Send-Packet ([System.Text.Encoding]::ASCII.GetBytes($Text.Substring(0, $len)))
}
# --- game-level draws (CRIOMAIN.CPP:3141-3308) -------------------------------
$script:OldRank = $null # m_nOldRank / m_nOldScore (INT_MIN after "ready")
$script:OldScore = $null
function Plasma-ScoreDraw {
# CPlasma::PlasmaScoreDraw: boxes on first draw, then changed-fields-only updates
param([string]$RankStr, [string]$ScoreStr, [bool]$BoxFlag)
if ($BoxFlag) {
Plasma-BoxFill 27 19 93 30
Plasma-BoxDraw 27 19 42 30
Plasma-BoxDraw 42 19 93 30
}
if ($RankStr.Length -ge 1 -and $RankStr.Length -le 2 -and
$ScoreStr.Length -ge 1 -and $ScoreStr.Length -le 8) {
if ($script:OldRank -ne [int]$RankStr) {
Plasma-PosText ' ' 29 20 0 2
if ($RankStr.Length -eq 1) { Plasma-PosText $RankStr 33 20 0 2 }
else { Plasma-PosText $RankStr 29 20 0 2 }
}
if ($script:OldScore -ne [int]$ScoreStr) {
Plasma-PosText ' ' 44 20 0 2
Plasma-PosText $ScoreStr (85 - ($ScoreStr.Length * 5)) 20 0 2
}
} else {
Write-Warning "Rank must be 1-2 chars and score 1-8 chars; skipping draw (game does the same)."
}
$script:OldRank = [int]$RankStr
$script:OldScore = [int]$ScoreStr
}
# --- main: replay the game's display sequence --------------------------------
if ($Callsign.Length -gt 20) { $Callsign = $Callsign.Substring(0, 20) }
try {
if (-not $DryRun) { Write-Host "Opening $Port @ $Baud 8N1 (no flow control)..." }
Open-Plasma
# CPlasma ctor: clear + cursor off
Write-Host '[startup] clear + cursor off'
Plasma-Clear
Plasma-Cursor 0
# state 1 "ready": 9-space wipe then the callsign, both auto-centered
Write-Host "[ready] callsign '$Callsign'"
Plasma-PosText ' '
Plasma-PosText $Callsign
Write-Host ("[wait] {0}s" -f $DelaySeconds)
if (-not $DryRun) { Start-Sleep -Seconds $DelaySeconds }
# state 2 "start": first score draw includes the panel boxes
Write-Host "[score] rank $Rank, score $Score (panel + fields)"
Plasma-ScoreDraw "$Rank" "$Score" $true
for ($i = 1; $i -le $CountUp; $i++) {
if (-not $DryRun) { Start-Sleep -Seconds $CountUpInterval }
$Score += $CountUpStep
Write-Host "[score] update ${i}: score $Score (changed fields only)"
Plasma-ScoreDraw "$Rank" "$Score" $false
}
if ($ClearAtEnd) {
# CPlasma dtor: cursor off + clear
Write-Host '[end] cursor off + clear'
Plasma-Cursor 0
Plasma-Clear
}
Write-Host 'Done.'
}
finally {
if ($script:SerialPort) {
$script:SerialPort.Close()
$script:SerialPort.Dispose()
}
}