Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0674cf5ba4 | ||
|
|
90bf6723dc | ||
|
|
359e1c0a40 | ||
|
|
c6e0522408 | ||
|
|
9a1c8478bf |
Binary file not shown.
@@ -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 (0–7; 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 1–2 = the datasheet's baud "JUMPER 1 / JUMPER 2." Positions 3–6
|
||||
are four unknown firmware option bits — candidates for a hidden factory
|
||||
self-test / diagnostic mode.
|
||||
|
||||
HC11 pin map cross-checked while tracing: PD0–PD5 = pins 10–15, PA0–PA7 =
|
||||
pins 24–17 (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 |
@@ -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
|
||||
|
||||
@@ -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 (N−1) 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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user