Author SHA1 Message Date
CydandClaude Fable 5 0674cf5ba4 Drop unused Microsoft.Bcl.HashCode, fix README typo
HashCode.Combine is called nowhere; the package (and its DLL in the dist
zip) was dead weight since the Core projects were created.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 20:40:14 -05:00
CydandClaude Fable 5 90bf6723dc vRIO builds in the vPLASMA display
The plasma glass now lives in vRIO's encoder strip: PlasmaCanvas grew a
dot-pitch parameter (default keeps the standalone glass pixel-identical)
and is compile-linked into VRio.App at pitch 3, parked at the strip's
left slot. The axis gauges move to the grid's right edge, the strip is
as tall as the glass, and the status text sits between them with the
axis readout split onto two lines. The button grid also compacts: rows
shift up past the layout's empty row 0 and the spare bottom row is
trimmed (the shared CockpitLayout is untouched, so coordinates still
match RIOJoy's map).

Control strip: the COM row labels are now the device names -- vRIO and
vPLASMA -- coloured by port status (green open, gray closed), replacing
the port-status line. The plasma row auto-opens COM12 at startup like
the RIO row does COM11; one Rescan refreshes both pickers. Plasma log
lines share the wire log under a vPLASMA: prefix, and the standalone
gestures carry over (double-click self-test, right-click reset).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 15:30:51 -05:00
CydandClaude Fable 5 359e1c0a40 vRIO auto-opens COM11 at startup when present
The usual port (device end of the COM1 pair) is selected and opened
automatically at launch. Missing or busy just logs to the wire log --
no modal at startup -- and leaves the manual picker in charge.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 08:25:28 -05:00
CydandClaude Fable 5 c6e0522408 PlasmaNew: replica goes USB-CDC (Win x64 hosts), park pending dump
The cockpit PCs are now Win x64, so the replica needs no real RS-232 — a
native-USB MCU presenting as a USB CDC virtual COM port is transparent to
the host (opens COMx, can't tell it isn't a UART). Records the consequences
(baud cosmetic, timing instant, pin the COM number, LEDs need their own
power) and marks the hardware/protocol thread parked until the U3 EPROM
firmware dump (or Babcock programming manual) is in hand.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 21:10:34 -05:00
CydandClaude Fable 5 9a1c8478bf Add PlasmaNew: plasma display reverse-engineering reference
Historical reference for recreating the cockpit plasma display in hardware.
The original Babcock plasma panels are failing; the end goal is a drop-in
replica — a modern microcontroller driving a 128x32 LED array that reads the
same RS-232 serial bus and speaks the same command protocol as the original
PD01D221. vPLASMA (the C# app) is the executable spec and test oracle for
that firmware.

Contents:
- PD01D221.pdf — Babcock datasheet (doc 9200-0109 Rev A)
- Board photos: controller overview, Cherry silkscreen, EPROM/SRAM, JP1
- README.md — full investigation: hardware ID (stock Babcock PD01D221,
  Cherry PCB 4317-C), chip inventory, recovered ESC command protocol, the
  JP1 config-jumper map (MODA/MODB hardwired to expanded mode), and the
  firmware-dump plan.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 20:50:35 -05:00
13 changed files with 447 additions and 61 deletions
Binary file not shown.
+206
View File
@@ -0,0 +1,206 @@
# PlasmaNew — reverse-engineering the real cockpit plasma display
Working notes and reference material for the cockpit plasma display.
**End goal: a hardware replica.** The original Babcock plasma panels are
starting to fail and are effectively irreplaceable. The plan is to drive a
modern **128 × 32 LED array** with a **modern microcontroller** that reads
the same RS-232 serial bus and speaks the same command protocol as the
original PD01D221 — a drop-in replacement, functionally identical from the
host's side, with none of the plasma physics or high voltage.
[vPLASMA](../src/VPlasma.App/) (the C# app in this repo) is the software
counterpart and serves the replica directly: it is an **executable
specification** of the display's behavior and a **test oracle**. Every
command semantic pinned down in `VPlasmaDevice` ports straight to the
replica's firmware, and the same differential-test rig (real panel vs.
vPLASMA) validates the replica. vPLASMA today is built from *observed
traffic* (the game's driver + a factory test tool); grounding it in the
*actual hardware* — protocol, fonts, and timing — feeds both the emulator
and the replacement firmware.
## What the display is
A **commercial off-the-shelf Babcock Display Products Division PD01D221**
"128 × 32 dot-matrix, gas-plasma display with controller and DC-DC
converter," with an RS-232C serial interface and a dedicated microprocessor
for refresh and the user interface. Built by **Cherry** (PCB assembly
**4317-C**, Made in Taiwan, © 1994). See [`PD01D221.pdf`](PD01D221.pdf)
(Babcock doc 9200-0109 Rev A).
Product family (the suffix letter = how much is on the board):
| Model | Contents |
|-------|----------|
| PD01**B**22B | 128×32 panel + driver electronics only (host refreshes it) |
| PD01**F**221 | + on-board DC-DC converter |
| PD01**D**221 | **+ controller: RS-232C, dedicated microprocessor** ← this unit |
**VWE used it stock — no custom fonts or bitmaps were installed.** So the
display's behavior is entirely the standard Babcock PD-series firmware, and
the `ESC P` "graphics" the game drew were rendered at runtime by the game,
not preloaded. Nothing on the display is VWE-specific.
## Board inventory
Chip IDs read from the photos below.
| Ref | Part | Role |
|-----|------|------|
| U1 | **Motorola MC68HC11D0** (44-pin QFP, mask 1C17F, wk 28/94) | ROMless HC11 MCU — the controller. Runs from external bus in expanded mode. |
| U3 | **TI TMS27PC512** (PLCC-32, 150 ns, Singapore) | **64 KB OTP EPROM = the firmware** (stock Babcock code + fonts). Standard 27C512. |
| U2 | QFP ~100-pin, label **"35GWP004 REV A 3994"** | Custom Cherry display/scan **ASIC** (wk 39/94). Drives the HV stage. *Not* the firmware. |
| U4 | **Mosel MS62256L-10** | 32 KB SRAM — frame buffer / scratch. |
| U7 | **Supertex HV7708** | 32-channel high-voltage plasma driver (more HV off-frame). |
| U5 | **Maxim MAX202CWE** | RS-232 transceiver — the serial interface. |
| — | **MAX707** | Reset / watchdog supervisor. |
| Y1 | **7.3728 MHz** crystal | E-clock = 1.8432 MHz; gives exact standard baud rates. |
Memory picture: ROMless HC11 + external 64 KB EPROM (code + fonts) + 32 KB
SRAM + custom scan ASIC + HV drivers. A 64 KB program EPROM for a 128×32
panel implies far more feature set than the game ever used.
## Reference photos
| File | Shows |
|------|-------|
| [`mpul-2026-07-07-152834.jpeg`](mpul-2026-07-07-152834.jpeg) | Controller overview: MC68HC11D0 (U1), the "35GWP004" ASIC (U2), HV7708 (U7), MAX202, MAX707. |
| [`silkscreenl-2026-07-07-152841.jpeg`](silkscreenl-2026-07-07-152841.jpeg) | Cherry silkscreen: PCB **4317-C**, © 1994, "Made in Taiwan". |
| [`unknown-2026-07-07-153818.jpeg`](unknown-2026-07-07-153818.jpeg) | The **TMS27PC512 EPROM** (U3, initially unidentified), Mosel SRAM (U4), HC11. |
| [`jumpers-2026-07-07-163733.jpeg`](jumpers-2026-07-07-163733.jpeg) | The **JP1** config header next to the HC11. |
## Datasheet-confirmed facts (`PD01D221.pdf`, doc 9200-0109 Rev A)
- Serial format **8N1**, baud **jumper-selectable 4800 / 9600 / 19.2K /
38.4K** (the game uses 9600).
- "Choice of standard fonts and styles" (= `ESC K` / `ESC H`); "program
custom characters" (a custom-char download command — **exists but VWE
didn't use it**); "graphic input commands / overlays" (= `ESC P`).
- Serial is **bidirectional**. Connector **J1**: pin 2 TxD (display→host),
pin 3 RxD (host→display), pin 4 CTS, pin 8 DTR ("display ready"), pin 5
GND. The game drove it write-only (flow control disabled, TxD ignored),
so vPLASMA's listen-only model is faithful.
- Also carries an 8-bit **parallel** port (J2), unused by the game.
- **The datasheet does *not* contain the `ESC` command table.** That's a
separate Babcock programming/user manual, which is **not available online**
(checked general web, datasheetarchive, bitsavers, archive.org, resellers;
only this datasheet was ever digitized). Sources for it: ask Babcock
directly (La Mirada CA, (714) 994-6500, babcockinc.com), or reconstruct it
from the dump + the sources we already have.
## Command protocol recovered so far
From the game driver (`TeslaRel410\CODE\RP\MUNGA_L4\L4PLASMA.CPP`) and the
factory test tool (`…\VWETEST\VGLTEST\PLASMA.EXE`). Full grammar lives in
[`../src/VPlasma.Core/Protocol/PlasmaProtocol.cs`](../src/VPlasma.Core/Protocol/PlasmaProtocol.cs).
| Bytes | Meaning |
|-------|---------|
| `ESC @` | Clear screen, reset text state |
| `ESC L` | Home cursor |
| `ESC G n` | Cursor mode (00/FF hidden, 01 steady, 03 flashing) |
| `ESC K n` | Font select (07; FF = default) |
| `ESC H n` | Text attributes (intensity / underline / reverse / flash) |
| `ESC P s y x w h data…` | Graphics write: MSB = leftmost pixel |
| BS / HT / LF / VT / CR | Cursor motion |
The Babcock manual (or a firmware dump) would fill in exact operand
encodings, tab stops, the `ESC P` "screen" byte, and any commands the game
never used.
## JP1 configuration header
Traced pin-by-pin (see the jumper photo). **JP1 is firmware-read
configuration, not CPU mode select** — each shunt ties a GP port pin the
firmware polls at boot. Shunt to GND = logic 0.
| JP1 pos | HC11 pin | Function |
|---------|----------|----------|
| 1 | pin 24 / PA0 | Baud select bit 0 |
| 2 | pin 22 / PA2 | Baud select bit 1 |
| 3 | pin 21 / PA3 | Option (unknown) |
| 4 | pin 15 / PD5 | Option (unknown) |
| 5 | pin 14 / PD4 | Option (unknown) |
| 6 | pin 13 / PD3 | Option (unknown) |
| 7 | J2 SEL → +5 V | Parallel interface select |
Positions 12 = the datasheet's baud "JUMPER 1 / JUMPER 2." Positions 36
are four unknown firmware option bits — candidates for a hidden factory
self-test / diagnostic mode.
HC11 pin map cross-checked while tracing: PD0PD5 = pins 1015, PA0PA7 =
pins 2417 (descending).
**MODA/MODB are hardwired high (expanded mode) through a diode to +5 V — not
jumper-selectable.** So bootstrap mode cannot be entered by moving a jumper;
it needs a mode-pin override. (Exact diode circuit still to be characterized.)
## Firmware-dump plan
Goal: get the 64 KB EPROM image, disassemble the HC11 code to recover the
full command table + font bitmaps + timing, then differential-test vPLASMA
against the real panel on identical byte streams. The recovered spec feeds
**both** vPLASMA and the replacement firmware.
1. **Free, no-solder — hunt for a diagnostic mode.** Capture J1 TxD while
power-cycling normally (may emit a banner/version), then step the four
unknown config jumpers (PA3, PD5, PD4, PD3) through combinations watching
TxD for a factory self-test or ROM dump.
2. **Serial bootstrap (conditional).** Bootstrap needs MODA = MODB = 0 at the
reset edge; they're pulled to +5 V via a diode. If that circuit has a
series resistor (or a diode-OR node), pull both low during a reset pulse
and run the standard **Motorola AN1060** dump loader out J1 — no cutting.
If hard-tied, a single trace cut/lift is needed. *Blocked on the diode
details.*
3. **Reliable fallback — read the EPROM directly.** PLCC-32 test clip on U3
with the HC11 held in reset, or hot-air U3 off and read it in a 27C512
adapter. Guaranteed image.
Safety: the panel runs on a few hundred volts from the on-board DC-DC. Keep
all work in the logic corner (HC11 / EPROM / MAX202); never probe the HV
section or the panel connector while powered.
## Open items
- Characterize the MODA/MODB diode circuit → decide if serial bootstrap is a
tack-a-wire job or needs a trace cut.
- Capture J1 TxD across config-jumper combinations (path 1).
- Obtain the Babcock PD01D programming manual, **or** dump the U3 EPROM.
- Once we have the command table + fonts: fold into `VPlasmaDevice`, replace
the public-domain 5×7 stand-in with the real Babcock glyphs, and
differential-test against the hardware.
- **Prototype the replica.** A modern MCU (RP2040 / ESP32 / Teensy) reads the
command stream into the same command parser and drives a 128×32 LED matrix
from the same frame buffer — the per-pixel lit / half-intensity / flash
flags in `VPlasmaDevice` map directly onto PWM brightness + blink. An amber
matrix best mimics the neon-orange plasma; for a true cockpit swap, match
the original active area (~12.75" × 3.15", ~0.1" pitch = 128×32).
## Replica interface — USB, not RS-232
The cockpit PCs are now **Win x64**, so the replica likely needs **no real
serial port**: a native-USB MCU presenting as a **USB CDC virtual COM port**
is transparent — the host opens `COMx` and can't tell it isn't a UART. This
deletes the RS-232 transceiver and connector from the BOM. Consequences:
- **Baud is cosmetic** over USB CDC (the 9600/… setting is accepted as a
no-op; the two baud-select jumpers need no hardware equivalent).
- **Timing becomes instant** rather than ~1 ms/byte — harmless for a display,
and vPLASMA can still throttle to mimic the original for differential tests.
- **Pin the COM number** the host expects (original was COM2) in Device
Manager so it drops in with no host-side config change.
- **DTR/RTS still cross** the CDC link if any host logic ever needs them (the
game didn't use flow control).
- **Power gotcha:** USB alone can't drive the LED array at full brightness —
use USB for data + a **separate DC feed** for the LEDs (or USB-C PD).
Transparency assumes the host reaches the display as a Windows `COMx`
endpoint — e.g. DOSBox-X `serial2=directserial realport:COMx`, which a USB
CDC port satisfies perfectly. Confirm the current drive path.
## Status
**Parked pending a firmware dump.** The software emulator (vPLASMA) is built
and released; this hardware/protocol thread is blocked on getting the U3
EPROM image (or the Babcock programming manual). Resume at the dump plan
above once a dump is in hand.
Binary file not shown.

After

Width:  |  Height:  |  Size: 675 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 896 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 943 KiB

+10 -6
View File
@@ -96,7 +96,7 @@ The two apps need a crossed serial link. Install a
(e.g. `COM1 ⇄ COM11`), then:
1. Run `VRio.App`, pick `COM11`, **Open**.
2. RIOJoy will always poit to `COM1`.
2. RIOJoy will always point to `COM1`.
RIOJoy's DTR open-pulse shows up in the wire log (DSR handshake), its ~55 ms
analog polling drives the "analog polls served" counter, and every click on
@@ -107,11 +107,15 @@ a null-modem cable work the same way.
The cockpit's second serial device is the **plasma display**: a 128×32
dot-matrix panel on COM2 (9600 8N1, no flow control) that the game draws
mission text and status graphics on. `VPlasma.App` is its software replica:
a bare-glass window that opens **COM12** (the device end of the plasma's
null-modem pair) on startup — retrying while the port is missing or busy,
port status in the title bar — decodes the display's command stream, and
renders the dot matrix in plasma orange, text mode included.
mission text and status graphics on. The software replica comes in two
forms. **Built into vRIO**: the panel's encoder strip hosts the glass at
top left, with its own port row in the control strip (label colour = port
status; auto-opens **COM12** at startup when present). And standalone,
`VPlasma.App`: a bare-glass window that opens **COM12** (the device end of
the plasma's null-modem pair) on startup — retrying while the port is
missing or busy, port status in the title bar. Both decode the display's
command stream and render the dot matrix in plasma orange, text mode
included; only one can hold COM12 at a time.
The command set was recovered from two Tesla 4.10 artifacts:
`CODE\RP\MUNGA_L4\L4PLASMA.CPP` (the game's driver — it renders everything
+19 -10
View File
@@ -11,10 +11,14 @@ namespace VPlasma.App;
/// </summary>
internal sealed class PlasmaCanvas : Control
{
private const int DotPitch = 5; // 4 px dot + 1 px gap → a 640×160 dot field
private const int DotSize = 4;
private const int Bezel = 4;
// Dot geometry: an N px pitch renders (N1) px dots with a 1 px gap. The
// standalone glass uses the default pitch 5 (4 px dots, a 640×160 dot
// field); vRIO embeds the same control at pitch 3 to fit its encoder strip.
private readonly int _dotPitch;
private readonly int _dotSize;
private static readonly Color BezelColor = Color.FromArgb(20, 18, 16);
private static readonly Color GlassColor = Color.FromArgb(26, 14, 6);
private static readonly Color UnlitDot = Color.FromArgb(46, 24, 10);
@@ -30,14 +34,14 @@ internal sealed class PlasmaCanvas : Control
private PlasmaCursorMode _cursorMode;
private int _cellWidth = 6, _cellHeight = 8;
public PlasmaCanvas()
public PlasmaCanvas(int dotPitch = 5)
{
_dotPitch = dotPitch;
_dotSize = dotPitch - 1;
SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.UserPaint |
ControlStyles.OptimizedDoubleBuffer | ControlStyles.FixedWidth |
ControlStyles.FixedHeight, true);
Size = new Size(
VPlasmaDevice.Width * DotPitch + 2 * Bezel,
VPlasmaDevice.Height * DotPitch + 2 * Bezel);
Size = SizeFor(dotPitch);
BackColor = BezelColor;
_blinkTimer.Tick += (_, _) =>
@@ -49,6 +53,11 @@ internal sealed class PlasmaCanvas : Control
_blinkTimer.Start();
}
/// <summary>Control size at a given dot pitch: the dot field plus bezel.</summary>
public static Size SizeFor(int dotPitch) => new(
VPlasmaDevice.Width * dotPitch + 2 * Bezel,
VPlasmaDevice.Height * dotPitch + 2 * Bezel);
/// <summary>Snapshot the device state and repaint. Call on the UI thread.</summary>
public void UpdateFrom(VPlasmaDevice device)
{
@@ -77,7 +86,7 @@ internal sealed class PlasmaCanvas : Control
using (var glass = new SolidBrush(GlassColor))
g.FillRectangle(glass, Bezel - 4, Bezel - 4,
VPlasmaDevice.Width * DotPitch + 7, VPlasmaDevice.Height * DotPitch + 7);
VPlasmaDevice.Width * _dotPitch + 7, VPlasmaDevice.Height * _dotPitch + 7);
using var unlit = new SolidBrush(UnlitDot);
using var lit = new SolidBrush(LitDot);
@@ -86,7 +95,7 @@ internal sealed class PlasmaCanvas : Control
for (int y = 0; y < VPlasmaDevice.Height; ++y)
{
int rowOffset = y * VPlasmaDevice.Width;
int py = Bezel + y * DotPitch;
int py = Bezel + y * _dotPitch;
for (int x = 0; x < VPlasmaDevice.Width; ++x)
{
byte dot = _frame[rowOffset + x];
@@ -96,7 +105,7 @@ internal sealed class PlasmaCanvas : Control
{
brush = (dot & VPlasmaDevice.PixelHalf) != 0 ? half : lit;
}
g.FillRectangle(brush, Bezel + x * DotPitch, py, DotSize, DotSize);
g.FillRectangle(brush, Bezel + x * _dotPitch, py, _dotSize, _dotSize);
}
}
@@ -108,7 +117,7 @@ internal sealed class PlasmaCanvas : Control
int cy = _cursor.Row * _cellHeight + _cellHeight - 1;
if (cy < VPlasmaDevice.Height)
for (int i = 0; i < _cellWidth && cx + i < VPlasmaDevice.Width; ++i)
g.FillRectangle(lit, Bezel + (cx + i) * DotPitch, Bezel + cy * DotPitch, DotSize, DotSize);
g.FillRectangle(lit, Bezel + (cx + i) * _dotPitch, Bezel + cy * _dotPitch, _dotSize, _dotSize);
}
}
-1
View File
@@ -12,7 +12,6 @@
<ItemGroup>
<PackageReference Include="System.Memory" Version="4.5.5" />
<PackageReference Include="Microsoft.Bcl.HashCode" Version="1.1.1" />
<PackageReference Include="PolySharp" Version="1.14.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
+163 -25
View File
@@ -1,5 +1,7 @@
using System.Diagnostics;
using System.IO.Ports;
using VPlasma.App;
using VPlasma.Core.Device;
using VRio.Core.Device;
using VRio.Core.Input;
@@ -8,15 +10,40 @@ namespace VRio.App;
/// <summary>
/// vRIO main window: the interactive cockpit panel on the left (the same
/// functional map RIOJoy's profile editor shows) and a control strip on the
/// right — COM port, device settings, and a live wire log. Open the COM port,
/// point RIOJoy at the other end of the null-modem pair, and every click here
/// arrives at RIOJoy exactly as if the physical cockpit sent it.
/// right — COM ports, device settings, and a live wire log. The panel's
/// encoder strip also hosts the built-in plasma glass (the vPLASMA display
/// emulator) on its own COM port. At startup the usual ports
/// (<see cref="PreferredPort"/>, <see cref="PreferredPlasmaPort"/>) are
/// opened automatically when available; otherwise open them by hand. Point
/// RIOJoy at the other end of the null-modem pair, and every click here
/// arrives at RIOJoy exactly as if the physical cockpit sent it; point the
/// game's COM2 passthrough at the plasma pair and the glass lights up.
/// </summary>
internal sealed class MainForm : Form
{
/// <summary>
/// vRIO's usual port: the device end of the COM1⇄COM11 com0com pair.
/// Auto-opened at startup when present and free; the picker still allows
/// any other port.
/// </summary>
private const string PreferredPort = "COM11";
/// <summary>
/// The built-in plasma's usual port: the device end of the COM2⇄COM12
/// com0com pair (the game's COM2 passthrough opens the other end).
/// </summary>
private const string PreferredPlasmaPort = "COM12";
private readonly VRioDevice _device = new();
private readonly VRioSerialService _service;
private readonly VPlasmaDevice _plasmaDevice = new();
private readonly VPlasmaSerialService _plasmaService;
private readonly PanelCanvas _canvas = new();
private readonly PlasmaCanvas _plasmaCanvas = new(PanelCanvas.PlasmaDotPitch)
{
Location = PanelCanvas.PlasmaSlot,
};
private int _selfTestPage;
private readonly InputRouter _router;
private readonly XInputGamepad _gamepad = new();
private readonly KeyboardLampMirror _lampMirror;
@@ -25,6 +52,16 @@ internal sealed class MainForm : Form
private readonly string _bindingsPath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "vRIO", "bindings.txt");
// Port rows: one line per built-in device — a name label whose colour is
// the port status (green = open, gray = closed), the port picker, and an
// Open/Close button. The one Rescan button refreshes both pickers.
private readonly Label _rioLabel = new()
{
Text = "vRIO",
Location = new Point(12, 15),
AutoSize = true,
ForeColor = Color.Gray,
};
private readonly ComboBox _portBox = new()
{
Location = new Point(80, 12),
@@ -33,13 +70,20 @@ internal sealed class MainForm : Form
};
private readonly Button _rescan = new() { Text = "Rescan", Location = new Point(214, 11), Width = 52 };
private readonly Button _openClose = new() { Text = "Open", Location = new Point(272, 11), Width = 46 };
private readonly Label _linkStatus = new()
private readonly Label _plasmaLabel = new()
{
Text = "Port closed.",
Location = new Point(12, 42),
Text = "vPLASMA",
Location = new Point(12, 45),
AutoSize = true,
ForeColor = Color.Gray,
};
private readonly ComboBox _plasmaPortBox = new()
{
Location = new Point(80, 42),
Width = 128,
DropDownStyle = ComboBoxStyle.DropDownList,
};
private readonly Button _plasmaOpenClose = new() { Text = "Open", Location = new Point(272, 41), Width = 46 };
private readonly CheckBox _spring = new() { Text = "Stick springs back to center", Location = new Point(10, 24), AutoSize = true, Checked = true };
private readonly Button _centerAxes = new() { Text = "Center all axes", Location = new Point(10, 52), Width = 140, Height = 26 };
@@ -103,7 +147,8 @@ internal sealed class MainForm : Form
AutoSize = true,
ForeColor = Color.Gray,
Text = "Left-click a cell: momentary press. Right-click: latch it down. " +
"Drag the X/Y box and the Z / L / R gauges to move the axes.",
"Drag the X/Y box and the Z / L / R gauges to move the axes. " +
"Double-click the plasma glass to cycle its self-test pages; right-click it to reset.",
};
private readonly TextBox _logBox = new()
@@ -144,11 +189,14 @@ internal sealed class MainForm : Form
KeyPreview = true; // form-level key routing for the input bindings
_service = new VRioSerialService(_device);
_plasmaService = new VPlasmaSerialService(_plasmaDevice);
_router = new InputRouter(_device);
_lampMirror = new KeyboardLampMirror(_device);
// Panel canvas, scrolled if the window is smaller than the grid.
// Panel canvas, scrolled if the window is smaller than the grid. The
// plasma glass rides along as a child in the encoder strip's left slot.
var scroller = new Panel { Dock = DockStyle.Fill, AutoScroll = true, BackColor = Color.FromArgb(28, 28, 28) };
_canvas.Controls.Add(_plasmaCanvas);
scroller.Controls.Add(_canvas);
Controls.Add(scroller);
@@ -177,8 +225,22 @@ internal sealed class MainForm : Form
_service.HostHandshake += high => RunOnUi(() =>
PrependLog(high ? "Host raised DTR (board-reset handshake)" : "Host dropped DTR"));
// Built-in plasma glass: a pure listener on its own port, same gestures
// as the standalone vPLASMA — double-click cycles the self-test pages,
// right-click resets the display to its power-on state.
_plasmaDevice.Updated += () => RunOnUi(() => _plasmaCanvas.UpdateFrom(_plasmaDevice));
_plasmaService.Logged += line => RunOnUi(() => PrependLog($"vPLASMA: {line}"));
_plasmaService.ConnectionChanged += open => RunOnUi(() => OnPlasmaConnectionChanged(open));
_plasmaCanvas.DoubleClick += (_, _) => RunPlasmaSelfTest();
_plasmaCanvas.MouseClick += (_, e) =>
{
if (e.Button == MouseButtons.Right)
_plasmaDevice.Reset();
};
_rescan.Click += (_, _) => RefreshPorts();
_openClose.Click += (_, _) => ToggleOpen();
_plasmaOpenClose.Click += (_, _) => TogglePlasmaOpen();
_spring.CheckedChanged += (_, _) => _canvas.StickSpringsBack = _spring.Checked;
_centerAxes.Click += (_, _) =>
{
@@ -242,12 +304,15 @@ internal sealed class MainForm : Form
_lampMirror.Dispose();
_rawKeyboard.Dispose();
_service.Dispose();
_plasmaService.Dispose();
};
RefreshPorts();
UpdateStatus();
PrependLog("vRIO ready. Open a COM port, then point RIOJoy at the other end of the pair.");
LoadBindings();
_plasmaCanvas.UpdateFrom(_plasmaDevice); // paint the power-on frame
AutoOpenPreferredPorts();
}
private Panel BuildControlStrip()
@@ -265,11 +330,13 @@ internal sealed class MainForm : Form
BorderStyle = BorderStyle.FixedSingle,
};
panel.Controls.Add(new Label { Text = "COM port:", Location = new Point(12, 15), AutoSize = true });
panel.Controls.Add(_rioLabel);
panel.Controls.Add(_portBox);
panel.Controls.Add(_rescan);
panel.Controls.Add(_openClose);
panel.Controls.Add(_linkStatus);
panel.Controls.Add(_plasmaLabel);
panel.Controls.Add(_plasmaPortBox);
panel.Controls.Add(_plasmaOpenClose);
var device = new GroupBox { Text = "Device", Location = new Point(12, 68), Size = new Size(306, 88) };
device.Controls.AddRange(new Control[] { _spring, _centerAxes, _lampsOff });
@@ -296,26 +363,80 @@ internal sealed class MainForm : Form
private void RefreshPorts()
{
string? current = _portBox.SelectedItem?.ToString();
_portBox.Items.Clear();
foreach (string name in SerialPort.GetPortNames().Distinct().OrderBy(n => n, StringComparer.OrdinalIgnoreCase))
_portBox.Items.Add(name);
string[] names = SerialPort.GetPortNames().Distinct()
.OrderBy(n => n, StringComparer.OrdinalIgnoreCase).ToArray();
// An open service's picker is disabled and shows the served port;
// leave it alone so the display can't drift from reality.
if (!_service.IsOpen)
RefreshPicker(_portBox, names);
if (!_plasmaService.IsOpen)
RefreshPicker(_plasmaPortBox, names);
}
if (_portBox.Items.Count == 0)
private static void RefreshPicker(ComboBox box, string[] portNames)
{
string? current = box.SelectedItem?.ToString();
box.Items.Clear();
foreach (string name in portNames)
box.Items.Add(name);
if (box.Items.Count == 0)
return;
int idx = current is null ? -1 : _portBox.Items.IndexOf(current);
_portBox.SelectedIndex = idx >= 0 ? idx : 0;
int idx = current is null ? -1 : box.Items.IndexOf(current);
box.SelectedIndex = idx >= 0 ? idx : 0;
}
/// <summary>
/// Startup convenience: if a device's usual port exists, select it and try
/// to open it. Failures (port missing, or busy because another vRIO/app
/// holds it) just log — no modal box at launch — and leave the manual
/// pickers in charge.
/// </summary>
private void AutoOpenPreferredPorts()
{
AutoOpen(_portBox, PreferredPort, _service.Open);
AutoOpen(_plasmaPortBox, PreferredPlasmaPort, _plasmaService.Open);
}
private void AutoOpen(ComboBox box, string port, Action<string> open)
{
int idx = box.Items.IndexOf(port);
if (idx < 0)
{
PrependLog($"{port} not present — pick a port and open it manually.");
return;
}
box.SelectedIndex = idx;
try
{
open(port);
}
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException or InvalidOperationException or ArgumentException)
{
PrependLog($"{port} is present but could not be opened ({ex.Message.TrimEnd('.')}) — open it manually once it frees up.");
}
}
private void ToggleOpen()
{
if (_service.IsOpen)
{
_service.Close();
return;
}
else
OpenFromPicker(_portBox, _service.Open);
}
if (_portBox.SelectedItem?.ToString() is not { } port)
private void TogglePlasmaOpen()
{
if (_plasmaService.IsOpen)
_plasmaService.Close();
else
OpenFromPicker(_plasmaPortBox, _plasmaService.Open);
}
private void OpenFromPicker(ComboBox box, Action<string> open)
{
if (box.SelectedItem?.ToString() is not { } port)
{
MessageBox.Show(this, "No COM port selected. On a single PC, install a com0com virtual " +
"null-modem pair and open one end here.", "vRIO", MessageBoxButtons.OK, MessageBoxIcon.Information);
@@ -324,7 +445,7 @@ internal sealed class MainForm : Form
try
{
_service.Open(port);
open(port);
}
catch (Exception ex)
{
@@ -336,12 +457,29 @@ internal sealed class MainForm : Form
private void OnConnectionChanged(bool open)
{
_openClose.Text = open ? "Close" : "Open";
_portBox.Enabled = _rescan.Enabled = !open;
_linkStatus.Text = open ? $"Serving {_service.PortName} @ 9600 8N1." : "Port closed.";
_linkStatus.ForeColor = open ? Color.ForestGreen : Color.Gray;
_portBox.Enabled = !open;
_rioLabel.ForeColor = open ? Color.ForestGreen : Color.Gray;
UpdateStatus();
}
private void OnPlasmaConnectionChanged(bool open)
{
_plasmaOpenClose.Text = open ? "Close" : "Open";
_plasmaPortBox.Enabled = !open;
_plasmaLabel.ForeColor = open ? Color.ForestGreen : Color.Gray;
}
/// <summary>
/// Feed the next canned self-test page through the plasma's wire parser —
/// the same end-to-end exercise the standalone vPLASMA runs on double-click.
/// </summary>
private void RunPlasmaSelfTest()
{
byte[] bytes = PlasmaSelfTest.BuildPage(_selfTestPage);
_plasmaDevice.OnReceived(bytes, bytes.Length);
_selfTestPage = (_selfTestPage + 1) % PlasmaSelfTest.PageCount;
}
// ---- Keyboard / gamepad input -------------------------------------------
/// <summary>
+44 -18
View File
@@ -1,3 +1,4 @@
using VPlasma.App;
using VRio.Core.Device;
using VRio.Core.Panel;
using VRio.Core.Protocol;
@@ -16,6 +17,10 @@ namespace VRio.App;
/// fill toward full travel (negative on the wire for the throttle, positive
/// for the pedals), and the stick covers ±80 around its center.
///
/// <para>The strip's left slot hosts the built-in plasma glass: MainForm parks
/// its <see cref="PlasmaCanvas"/> child at <see cref="PlasmaSlot"/>, and the
/// strip height and gauge positions derive from the glass size.</para>
///
/// <para>Left-click = momentary press (release on mouse-up). Right-click =
/// latch the button down / release it (handy for testing holds).</para>
/// </summary>
@@ -24,16 +29,27 @@ internal sealed class PanelCanvas : Control
// Cell geometry — identical to RIOJoy's PanelView so the two panels align.
private const int CellW = 66;
private const int CellH = 34;
private const int TopStrip = 78; // encoder strip: gauges end at 74, small gap to the grid
private static readonly IReadOnlyList<PanelButton> AllButtons = CockpitLayout.Buttons();
// Encoder-strip geometry (same placement as RIOJoy's PanelView): Z gauge,
// the pedal gauges L / R, and the X/Y stick box.
// First grid row any group occupies (the shared layout leaves row 0
// empty). Rendering shifts all rows up by this, so the button grid starts
// right under the encoder strip instead of a blank cell row below it.
private static readonly int FirstRow = CockpitLayout.Groups.Min(g => g.OriginRow);
// Encoder-strip geometry. The built-in plasma glass parks at the strip's
// left edge (MainForm adds its PlasmaCanvas as a child at PlasmaSlot),
// the axis gauges — Z, the pedal gauges L / R, and the X/Y stick box —
// hug the grid's right edge, and the green status text fills the gap
// between them. The strip is exactly as tall as the glass.
internal const int PlasmaDotPitch = 3; // 2 px dots: the 128×32 glass fits the strip
internal static readonly Size PlasmaSize = PlasmaCanvas.SizeFor(PlasmaDotPitch);
private const int StripTop = 6;
private const int StripH = 68;
internal static readonly Point PlasmaSlot = new(6, StripTop);
private static readonly int StripH = PlasmaSize.Height;
private static readonly int TopStrip = StripTop + StripH + 6; // button grid starts under the strip
private const int Bar = 30;
private static readonly int BaseX = 6 * CellW - 100;
private static readonly int BaseX = GridWidth() - 6 - 200; // the gauge cluster spans 200 px, right-aligned
private static readonly Rectangle BoxZ = new(BaseX, StripTop, Bar, StripH);
private static readonly Rectangle BoxL = new(BaseX + 34, StripTop, Bar, StripH);
private static readonly Rectangle BoxR = new(BaseX + 34 + 82 - Bar, StripTop, Bar, StripH);
@@ -109,19 +125,27 @@ internal sealed class PanelCanvas : Control
base.Dispose(disposing);
}
// Split from GridSize so the static gauge layout can use the width
// without touching TopStrip (which initializes after AllButtons).
private static int GridWidth()
{
int maxCol = 0;
foreach (PanelButton b in AllButtons)
if (b.Col > maxCol) maxCol = b.Col;
return (maxCol + 1) * CellW + 6;
}
private static Size GridSize()
{
int maxCol = 0, maxRow = 0;
int maxRow = 0;
foreach (PanelButton b in AllButtons)
{
if (b.Col > maxCol) maxCol = b.Col;
if (b.Row > maxRow) maxRow = b.Row;
}
return new Size((maxCol + 1) * CellW + 6, TopStrip + (maxRow + 2) * CellH);
// The last used row plus a small margin under the lowest group frame.
return new Size(GridWidth(), TopStrip + (maxRow - FirstRow + 1) * CellH + 6);
}
private static Rectangle Cell(int col, int row) =>
new(col * CellW + 2, TopStrip + row * CellH + 2, CellW - 4, CellH - 4);
new(col * CellW + 2, TopStrip + (row - FirstRow) * CellH + 2, CellW - 4, CellH - 4);
// ---- Painting ----------------------------------------------------------
@@ -142,7 +166,7 @@ internal sealed class PanelCanvas : Control
{
var frame = new Rectangle(
grp.OriginCol * CellW + 1,
TopStrip + grp.OriginRow * CellH + 1,
TopStrip + (grp.OriginRow - FirstRow) * CellH + 1,
grp.Cols * CellW,
(grp.Rows + 1) * CellH);
g.DrawRectangle(groupPen, frame);
@@ -288,10 +312,12 @@ internal sealed class PanelCanvas : Control
new Rectangle(BoxXY.X, BoxXY.Bottom - 16, BoxXY.Width, 14), Color.Gainsboro,
TextFormatFlags.HorizontalCenter | TextFormatFlags.Bottom);
// The mockup's green status/instruction area, right of the gauges; the
// live axis readout sits directly under the status lines (painted per
// frame, so drags track).
var statusRect = new Rectangle(BoxXY.Right + 16, StripTop, Width - BoxXY.Right - 24, TopStrip - StripTop - 6);
// The mockup's green status/instruction area, between the plasma glass
// and the gauges; the live axis readout sits directly under the status
// lines (painted per frame, so drags track).
var statusRect = new Rectangle(
PlasmaSlot.X + PlasmaSize.Width + 12, StripTop,
BoxZ.Left - PlasmaSlot.X - PlasmaSize.Width - 24, StripH);
if (statusRect.Width > 60)
{
using var statusFont = new Font("Consolas", 8f);
@@ -307,11 +333,11 @@ internal sealed class PanelCanvas : Control
short WireAxis(RioAxis a) => WireAxisProvider?.Invoke(a) ?? Axis(a);
string readout =
$"Z {WireAxis(RioAxis.Throttle),4} L {WireAxis(RioAxis.LeftPedal),3} R {WireAxis(RioAxis.RightPedal),3} " +
$"Z {WireAxis(RioAxis.Throttle),4} L {WireAxis(RioAxis.LeftPedal),3} R {WireAxis(RioAxis.RightPedal),3}\n" +
$"X {WireAxis(RioAxis.JoystickX),3} Y {WireAxis(RioAxis.JoystickY),3}";
TextRenderer.DrawText(g, readout, statusFont,
new Rectangle(statusRect.X, readoutTop, statusRect.Width, statusRect.Bottom - readoutTop), green,
TextFormatFlags.Left | TextFormatFlags.Top | TextFormatFlags.SingleLine);
TextFormatFlags.Left | TextFormatFlags.Top);
}
}
+5
View File
@@ -17,6 +17,11 @@
<ItemGroup>
<ProjectReference Include="..\VRio.Core\VRio.Core.csproj" />
<!-- The built-in plasma glass: vPLASMA's device/protocol core, plus its
canvas control compiled in directly (shared file, not a library —
VPlasma.Core stays UI-free). -->
<ProjectReference Include="..\VPlasma.Core\VPlasma.Core.csproj" />
<Compile Include="..\VPlasma.App\PlasmaCanvas.cs" Link="PlasmaCanvas.cs" />
</ItemGroup>
<!-- Stamp commit date + short sha into InformationalVersion; the window
-1
View File
@@ -12,7 +12,6 @@
<ItemGroup>
<PackageReference Include="System.Memory" Version="4.5.5" />
<PackageReference Include="Microsoft.Bcl.HashCode" Version="1.1.1" />
<PackageReference Include="PolySharp" Version="1.14.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>