vRIO: virtual RIO cockpit device emulator
Speaks the device side of the RIO serial protocol (per riojoy's PROTOCOL.md) on a COM port, behind an interactive replica of the profile editor's cockpit panel: click cells to press buttons/keys, drag the encoder gauges to move the five analog axes, and watch host-commanded lamp states (incl. flash modes) light the cells. Device behavior grounded in the real v4.2 firmware dump: version 4.2, 4-retry NAK budget ending in RESTART, and an optional emulation of the analog reply-wedge latch leak for exercising host recovery watchdogs. Verified: 33 unit tests, plus an interop harness driving RIOJoy's actual RioSerialLink against VRioDevice over an in-memory transport (version/check/analog/lamp/button/keypad/reset all round-trip). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,140 @@
|
||||
using VRio.Core.Device;
|
||||
|
||||
namespace VRio.Core.Panel;
|
||||
|
||||
/// <summary>Shape of a panel group.</summary>
|
||||
public enum PanelGroupKind
|
||||
{
|
||||
/// <summary>A 4×2 MFD button cluster (lamp buttons).</summary>
|
||||
Mfd,
|
||||
|
||||
/// <summary>A vertical 1×8 board column (lamp buttons).</summary>
|
||||
Column,
|
||||
|
||||
/// <summary>A 4×4 keypad (no lamps).</summary>
|
||||
Keypad,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// One functional group on the cockpit control panel: a titled block of address
|
||||
/// buttons laid out row-major in <see cref="Rows"/> × <see cref="Cols"/>.
|
||||
/// <see cref="OriginCol"/>/<see cref="OriginRow"/> place the block on the panel
|
||||
/// in button-cell units.
|
||||
/// </summary>
|
||||
public sealed record PanelGroup(
|
||||
string Title,
|
||||
PanelGroupKind Kind,
|
||||
int Cols,
|
||||
int Rows,
|
||||
bool LampCapable,
|
||||
int OriginCol,
|
||||
int OriginRow,
|
||||
IReadOnlyList<int?> Addresses);
|
||||
|
||||
/// <summary>One address button placed on the panel (absolute cell coordinates).</summary>
|
||||
public sealed record PanelButton(int Address, PanelGroup Group, int Col, int Row, bool LampCapable);
|
||||
|
||||
/// <summary>
|
||||
/// The cockpit control panel's logical layout — the same functional grouping
|
||||
/// RIOJoy's profile editor draws (its <c>CockpitPanel</c>, derived from the
|
||||
/// original Win32 RIO control-panel mockup): five MFD clusters, four board
|
||||
/// columns, and the two later-added 4×4 keypads. Every RIO address
|
||||
/// 0x00–0x47 / 0x50–0x6F is placed exactly once. Keeping the byte-for-byte same
|
||||
/// layout means a button on vRIO sits where the same button sits in RIOJoy's
|
||||
/// editor — pressing one lights the other.
|
||||
/// </summary>
|
||||
public static class CockpitLayout
|
||||
{
|
||||
// Helper: 4×2 MFD address block, top row then bottom row (addresses descend).
|
||||
private static int?[] Mfd(int hi) => new int?[]
|
||||
{
|
||||
hi, hi - 1, hi - 2, hi - 3,
|
||||
hi - 4, hi - 5, hi - 6, hi - 7,
|
||||
};
|
||||
|
||||
// Helper: vertical 1×8 column from a base address upward.
|
||||
private static int?[] Col8(int baseAddr) => new int?[]
|
||||
{
|
||||
baseAddr, baseAddr + 1, baseAddr + 2, baseAddr + 3,
|
||||
baseAddr + 4, baseAddr + 5, baseAddr + 6, baseAddr + 7,
|
||||
};
|
||||
|
||||
// Helper: 4×4 keypad. Physical keys 1 2 3 C / 4 5 6 D / 7 8 9 E / A 0 B F map
|
||||
// to address = base + that hex digit.
|
||||
private static int?[] Keypad(int baseAddr)
|
||||
{
|
||||
int[] digits = { 0x1, 0x2, 0x3, 0xC, 0x4, 0x5, 0x6, 0xD, 0x7, 0x8, 0x9, 0xE, 0xA, 0x0, 0xB, 0xF };
|
||||
var a = new int?[16];
|
||||
for (int i = 0; i < 16; i++) a[i] = baseAddr + digits[i];
|
||||
return a;
|
||||
}
|
||||
|
||||
/// <summary>All panel groups, positioned for rendering.</summary>
|
||||
public static IReadOnlyList<PanelGroup> Groups { get; } = new[]
|
||||
{
|
||||
// Upper MFD row.
|
||||
new PanelGroup("Upper Left MFD", PanelGroupKind.Mfd, 4, 2, true, 0, 1, Mfd(0x2F)),
|
||||
new PanelGroup("Upper Middle MFD", PanelGroupKind.Mfd, 4, 2, true, 4, 1, Mfd(0x27)),
|
||||
new PanelGroup("Upper Right MFD", PanelGroupKind.Mfd, 4, 2, true, 8, 1, Mfd(0x37)),
|
||||
|
||||
// Lower MFD row.
|
||||
new PanelGroup("Lower Left MFD", PanelGroupKind.Mfd, 4, 2, true, 0, 5, Mfd(0x0F)),
|
||||
new PanelGroup("Lower Right MFD", PanelGroupKind.Mfd, 4, 2, true, 8, 5, Mfd(0x07)),
|
||||
|
||||
// Board columns.
|
||||
new PanelGroup("Throttle", PanelGroupKind.Column, 1, 8, true, 0, 9, Col8(0x38)),
|
||||
new PanelGroup("Secondary", PanelGroupKind.Column, 1, 8, true, 4, 9, Col8(0x10)),
|
||||
new PanelGroup("Screen", PanelGroupKind.Column, 1, 8, true, 6, 9, Col8(0x18)),
|
||||
new PanelGroup("Joystick", PanelGroupKind.Column, 1, 8, true, 11, 9, Col8(0x40)),
|
||||
|
||||
// Keypads (no lamps): internal in the middle, external between the
|
||||
// Secondary/Screen columns and the Joystick column.
|
||||
new PanelGroup("Internal Keypad", PanelGroupKind.Keypad, 4, 4, false, 4, 4, Keypad(0x50)),
|
||||
new PanelGroup("External Keypad", PanelGroupKind.Keypad, 4, 4, false, 7, 9, Keypad(0x60)),
|
||||
};
|
||||
|
||||
/// <summary>Flatten the groups to positioned <see cref="PanelButton"/>s (absolute cells).</summary>
|
||||
public static IReadOnlyList<PanelButton> Buttons()
|
||||
{
|
||||
var buttons = new List<PanelButton>();
|
||||
foreach (PanelGroup g in Groups)
|
||||
{
|
||||
for (int i = 0; i < g.Addresses.Count; i++)
|
||||
{
|
||||
if (g.Addresses[i] is not { } addr) continue;
|
||||
int localCol = i % g.Cols;
|
||||
int localRow = i / g.Cols;
|
||||
// +1 row leaves space under the title.
|
||||
buttons.Add(new PanelButton(addr, g, g.OriginCol + localCol, g.OriginRow + 1 + localRow, g.LampCapable));
|
||||
}
|
||||
}
|
||||
return buttons;
|
||||
}
|
||||
|
||||
// Named physical controls (from the Win32RIO control-panel mockup); every
|
||||
// other lamp button is known only by its hex address.
|
||||
private static readonly Dictionary<int, string> Named = new()
|
||||
{
|
||||
[0x3D] = "Panic",
|
||||
[0x3F] = "Throttle",
|
||||
[0x40] = "Main",
|
||||
[0x41] = "Hat Back",
|
||||
[0x42] = "Hat Up",
|
||||
[0x43] = "Hat Right",
|
||||
[0x44] = "Hat Left",
|
||||
[0x45] = "Pinky",
|
||||
[0x46] = "Middle",
|
||||
[0x47] = "Upper",
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// The label printed on the physical control: keypad keys carry their hex
|
||||
/// digit, a few controls have names, the rest is null (show the address).
|
||||
/// </summary>
|
||||
public static string? PhysicalLabel(int address)
|
||||
{
|
||||
if (RioAddressSpace.IsKeypad(address))
|
||||
return (address & 0x0F).ToString("X1");
|
||||
return Named.TryGetValue(address, out string? name) ? name : null;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user