vPLASMA: companion app emulating the cockpit plasma display

The cockpit's second serial device joins vRIO: the 128x32 dot-matrix
plasma on COM2 (9600 8N1) that the game draws mission text and status
graphics on. VPlasma.App opens the device end of a COM port, decodes
the display's command stream, and renders the matrix in plasma orange.

The command set is recovered from two Tesla 4.10 artifacts: the game's
driver (L4PLASMA.CPP — ESC P packed-bitmap row writes, ESC G 0 cursor
hide at boot) and the factory test tool PLASMA.EXE, whose data segment
pairs each command literal with its printf description (BS/HT/LF/VT/CR
motion, ESC @ clear, ESC L home, ESC G cursor, ESC K fonts, ESC H
attributes: intensity/underline/reverse/flash). Text renders through
the classic public-domain 5x7 set standing in for the lost ROM glyphs;
fonts 0-3 give 21x4 cells, fonts 4-7 the doubled 10x2. A Self test
cycles banner/charset/graphics pages through the same parser the wire
feeds, and the wire log shows every decoded command.

Verified end-to-end over the second com0com pair (host writing COM2,
vPLASMA listening on COM12). The verify skill gains the cross-process
combo-box lesson: string-carrying CB_* messages hang across processes,
so select ports by locally computed index via CB_SETCURSEL.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Cyd
2026-07-07 14:22:00 -05:00
co-authored by Claude Fable 5
parent 767473d1cf
commit a1b7dae3da
17 changed files with 1747 additions and 4 deletions
@@ -0,0 +1,25 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net48</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<LangVersion>latest</LangVersion>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
<PackageReference Include="xunit" Version="2.9.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
<PackageReference Include="PolySharp" Version="1.14.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\VPlasma.Core\VPlasma.Core.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,307 @@
using VPlasma.Core.Device;
using Xunit;
namespace VPlasma.Core.Tests;
public class VPlasmaDeviceTests
{
private const byte Esc = 0x1B;
private static void Feed(VPlasmaDevice device, params byte[] bytes)
=> device.OnReceived(bytes, bytes.Length);
private static void Feed(VPlasmaDevice device, IEnumerable<byte> bytes)
{
byte[] arr = bytes.ToArray();
device.OnReceived(arr, arr.Length);
}
private static byte Pixel(VPlasmaDevice device, int x, int y)
{
var frame = new byte[VPlasmaDevice.Width * VPlasmaDevice.Height];
device.CopyFrame(frame);
return frame[y * VPlasmaDevice.Width + x];
}
/// <summary>A full-width ESC P row the way L4PLASMA.CPP sends one.</summary>
private static byte[] GraphicsRow(int y, params byte[] data)
{
var row = new List<byte> { Esc, (byte)'P', 0, (byte)y, 0, (byte)data.Length, 1 };
row.AddRange(data);
return row.ToArray();
}
// ---- graphics writes -------------------------------------------------
[Fact]
public void GraphicsRow_SetsPixelsMsbFirst()
{
var device = new VPlasmaDevice();
Feed(device, GraphicsRow(5, 0x80, 0x01)); // xbyte 0..1
Assert.Equal(VPlasmaDevice.PixelLit, Pixel(device, 0, 5)); // MSB of byte 0
Assert.Equal(0, Pixel(device, 1, 5));
Assert.Equal(0, Pixel(device, 14, 5));
Assert.Equal(VPlasmaDevice.PixelLit, Pixel(device, 15, 5)); // LSB of byte 1
Assert.Equal(0, Pixel(device, 0, 4));
Assert.Equal(0, Pixel(device, 0, 6));
Assert.Equal(1, device.GraphicsRows);
}
[Fact]
public void GraphicsRow_SurvivesAnyChunkBoundary()
{
byte[] wire = GraphicsRow(3, Enumerable.Repeat((byte)0xFF, 16).ToArray());
// Split the same command at every possible boundary.
for (int split = 1; split < wire.Length; ++split)
{
var device = new VPlasmaDevice();
device.OnReceived(wire, split);
byte[] rest = wire.Skip(split).ToArray();
device.OnReceived(rest, rest.Length);
Assert.Equal(VPlasmaDevice.PixelLit, Pixel(device, 0, 3));
Assert.Equal(VPlasmaDevice.PixelLit, Pixel(device, 127, 3));
Assert.Equal(1, device.GraphicsRows);
}
}
[Fact]
public void GraphicsBlock_MultipleRowsAdvanceY()
{
var device = new VPlasmaDevice();
// screen 0, y=10, xbyte=0, 1 byte/row, 3 rows.
Feed(device, Esc, (byte)'P', 0, 10, 0, 1, 3, 0x80, 0x80, 0x80);
Assert.Equal(VPlasmaDevice.PixelLit, Pixel(device, 0, 10));
Assert.Equal(VPlasmaDevice.PixelLit, Pixel(device, 0, 11));
Assert.Equal(VPlasmaDevice.PixelLit, Pixel(device, 0, 12));
Assert.Equal(0, Pixel(device, 0, 13));
Assert.Equal(3, device.GraphicsRows);
}
[Fact]
public void GraphicsWrite_HonorsByteColumnOffset()
{
var device = new VPlasmaDevice();
Feed(device, Esc, (byte)'P', 0, 0, 2, 1, 1, 0xFF); // xbyte=2 → x 16..23
Assert.Equal(0, Pixel(device, 15, 0));
Assert.Equal(VPlasmaDevice.PixelLit, Pixel(device, 16, 0));
Assert.Equal(VPlasmaDevice.PixelLit, Pixel(device, 23, 0));
Assert.Equal(0, Pixel(device, 24, 0));
}
[Fact]
public void GraphicsWrite_OutOfRangeRowIsConsumedNotDrawn()
{
var device = new VPlasmaDevice();
Feed(device, GraphicsRow(40, Enumerable.Repeat((byte)0xFF, 16).ToArray()));
Feed(device, (byte)'!'); // parser must be back in text mode
var frame = new byte[VPlasmaDevice.Width * VPlasmaDevice.Height];
device.CopyFrame(frame);
Assert.Equal(1, device.TextCharsDrawn);
Assert.Equal(0, device.GraphicsRows);
}
[Fact]
public void GraphicsWrite_OverwritesTextAttributes()
{
var device = new VPlasmaDevice();
Feed(device, Esc, (byte)'H', 4); // flashing text
Feed(device, (byte)'H'); // glyph col 0 is a full bar at x=0
Assert.Equal(VPlasmaDevice.PixelLit | VPlasmaDevice.PixelFlash, Pixel(device, 0, 0));
Feed(device, GraphicsRow(0, 0x80)); // repaint that row from the wire
Assert.Equal(VPlasmaDevice.PixelLit, Pixel(device, 0, 0));
}
// ---- text mode ---------------------------------------------------------
[Fact]
public void Text_DrawsGlyphAndAdvancesCursor()
{
var device = new VPlasmaDevice();
Feed(device, (byte)'H'); // 5×7 'H': column 0 = 0x7F (all seven rows)
Assert.Equal(VPlasmaDevice.PixelLit, Pixel(device, 0, 0));
Assert.Equal(VPlasmaDevice.PixelLit, Pixel(device, 0, 6));
Assert.Equal(0, Pixel(device, 0, 7)); // gap row
Assert.Equal(0, Pixel(device, 5, 0)); // gap column
Assert.Equal(new VPlasmaDevice.Point(1, 0), device.CursorCell);
Assert.Equal(1, device.TextCharsDrawn);
}
[Fact]
public void ControlChars_MoveTheCursor()
{
var device = new VPlasmaDevice();
Feed(device, 0x09, 0x09, 0x09); // HT ×3
Assert.Equal(new VPlasmaDevice.Point(3, 0), device.CursorCell);
Feed(device, 0x08); // BS
Assert.Equal(new VPlasmaDevice.Point(2, 0), device.CursorCell);
Feed(device, 0x0A); // LF
Assert.Equal(new VPlasmaDevice.Point(2, 1), device.CursorCell);
Feed(device, 0x0D); // CR
Assert.Equal(new VPlasmaDevice.Point(0, 1), device.CursorCell);
Feed(device, 0x0B); // VT
Assert.Equal(new VPlasmaDevice.Point(0, 0), device.CursorCell);
Feed(device, 0x0B); // VT off the top wraps to the bottom row
Assert.Equal(new VPlasmaDevice.Point(0, 3), device.CursorCell);
}
[Fact]
public void Text_WrapsAtRowAndScreenEnd()
{
var device = new VPlasmaDevice();
Feed(device, Enumerable.Repeat((byte)'X', 22)); // one full 21-cell row + 1
Assert.Equal(new VPlasmaDevice.Point(1, 1), device.CursorCell);
Feed(device, Enumerable.Repeat((byte)'X', 21 * 3 - 1)); // exactly to the end
Assert.Equal(new VPlasmaDevice.Point(0, 0), device.CursorCell); // wrapped to top
}
[Fact]
public void EscAt_ClearsScreenAndResetsTextState()
{
var device = new VPlasmaDevice();
Feed(device, Esc, (byte)'K', 4, Esc, (byte)'H', 1, (byte)'H');
Feed(device, Esc, (byte)'@');
Assert.Equal(0, Pixel(device, 0, 0));
Assert.Equal(new VPlasmaDevice.Point(0, 0), device.CursorCell);
Assert.Equal(0, device.Font);
Assert.Equal(PlasmaAttributes.None, device.Attributes);
}
[Fact]
public void EscL_HomesCursorWithoutClearing()
{
var device = new VPlasmaDevice();
Feed(device, (byte)'H', Esc, (byte)'L');
Assert.Equal(new VPlasmaDevice.Point(0, 0), device.CursorCell);
Assert.Equal(VPlasmaDevice.PixelLit, Pixel(device, 0, 0)); // glyph survives
}
[Theory]
[InlineData(0x00, PlasmaCursorMode.Hidden)] // the game's cursor-off
[InlineData(0xFF, PlasmaCursorMode.Hidden)] // the test tool's hide
[InlineData(0x01, PlasmaCursorMode.Steady)]
[InlineData(0x03, PlasmaCursorMode.Flashing)]
public void EscG_SetsCursorMode(byte operand, PlasmaCursorMode expected)
{
var device = new VPlasmaDevice();
Feed(device, Esc, (byte)'G', operand);
Assert.Equal(expected, device.CursorMode);
}
[Fact]
public void EscK_SelectsFontGrids()
{
var device = new VPlasmaDevice();
Feed(device, Esc, (byte)'K', 4); // large: 12×16 cells
Assert.Equal(4, device.Font);
Assert.Equal(12, device.CellWidth);
Assert.Equal(16, device.CellHeight);
Feed(device, (byte)'A'); // large glyph: 'A' col 1 (0x11) row 0 → 2×2 dots at (2..3, 0..1)
Assert.Equal(VPlasmaDevice.PixelLit, Pixel(device, 2, 0));
Assert.Equal(VPlasmaDevice.PixelLit, Pixel(device, 3, 1));
Feed(device, Esc, (byte)'K', 0xFF); // FF → default font 0
Assert.Equal(0, device.Font);
Assert.Equal(6, device.CellWidth);
}
[Fact]
public void EscH_AppliesAttributes()
{
var device = new VPlasmaDevice();
Feed(device, Esc, (byte)'H', 1, (byte)'H'); // half intensity
Assert.Equal(VPlasmaDevice.PixelLit | VPlasmaDevice.PixelHalf, Pixel(device, 0, 0));
Feed(device, Esc, (byte)'H', 2, (byte)'H'); // underline: gap row lit
Assert.Equal(VPlasmaDevice.PixelLit, Pixel(device, 6, 7));
Feed(device, Esc, (byte)'H', 3, (byte)' '); // reverse: a space renders solid
Assert.Equal(VPlasmaDevice.PixelLit, Pixel(device, 12, 0));
Assert.Equal(VPlasmaDevice.PixelLit, Pixel(device, 17, 7));
Feed(device, Esc, (byte)'H', 0xFF, (byte)'H'); // defaults restored
Assert.Equal(VPlasmaDevice.PixelLit, Pixel(device, 18, 0));
Assert.Equal(0, Pixel(device, 18, 7));
Assert.Equal(PlasmaAttributes.None, device.Attributes);
}
// ---- robustness ----------------------------------------------------------
[Fact]
public void UnknownEscape_IsConsumedAndTextResumes()
{
var device = new VPlasmaDevice();
Feed(device, Esc, (byte)'Z', (byte)'H');
Assert.Equal(1, device.TextCharsDrawn);
Assert.Equal(VPlasmaDevice.PixelLit, Pixel(device, 0, 0));
}
[Fact]
public void GameStartupSequence_HidesCursor()
{
// L4PLASMA.CPP's constructor sends exactly ESC 'G' 0x00.
var device = new VPlasmaDevice();
Assert.Equal(PlasmaCursorMode.Steady, device.CursorMode); // power-on default
Feed(device, 27, (byte)'G', 0x00);
Assert.Equal(PlasmaCursorMode.Hidden, device.CursorMode);
}
[Fact]
public void SelfTestPages_ParseWithoutUnknownCommands()
{
for (int page = 0; page < PlasmaSelfTest.PageCount; ++page)
{
var device = new VPlasmaDevice();
var complaints = new List<string>();
device.Logged += line =>
{
if (line.StartsWith("Unknown", StringComparison.Ordinal)
|| line.StartsWith("Unhandled", StringComparison.Ordinal))
complaints.Add(line);
};
byte[] bytes = PlasmaSelfTest.BuildPage(page);
device.OnReceived(bytes, bytes.Length);
Assert.Empty(complaints);
Assert.True(device.BytesReceived > 0);
}
}
[Fact]
public void Reset_RestoresPowerOnState()
{
var device = new VPlasmaDevice();
Feed(device, Esc, (byte)'K', 5, Esc, (byte)'H', 4, Esc, (byte)'G', 0, (byte)'H');
device.Reset();
Assert.Equal(0, Pixel(device, 0, 0));
Assert.Equal(0, device.Font);
Assert.Equal(PlasmaAttributes.None, device.Attributes);
Assert.Equal(PlasmaCursorMode.Steady, device.CursorMode);
Assert.Equal(new VPlasmaDevice.Point(0, 0), device.CursorCell);
}
}