Wire carries stick-up as negative; Invert Y opts back to physical

The old Invert joystick Y box (checked by default) is now the wire's
native convention: full up = -80 in AnalogReply with the flag off.
Checking the renamed Invert Y sends the physical direction (up = +80).

Control strip cleanup: test-mode buttons and the firmware selector are
gone (the device still reports 4.2), Keyboard/Gamepad/Invert Y share
one line, the help text drops the bindings sentence, and the wire log
gets the reclaimed height.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Cyd
2026-07-07 09:47:25 -05:00
co-authored by Claude Fable 5
parent 391c53f294
commit 007d15e668
3 changed files with 48 additions and 44 deletions
+22 -35
View File
@@ -41,35 +41,31 @@ internal sealed class MainForm : Form
ForeColor = Color.Gray,
};
private readonly NumericUpDown _verMajor = new() { Location = new Point(80, 24), Width = 44, Minimum = 0, Maximum = 127, Value = 4 };
private readonly NumericUpDown _verMinor = new() { Location = new Point(140, 24), Width = 44, Minimum = 0, Maximum = 127, Value = 2 };
private readonly CheckBox _spring = new() { Text = "Stick springs back to center", Location = new Point(10, 56), AutoSize = true, Checked = true };
private readonly Button _centerAxes = new() { Text = "Center all axes", Location = new Point(10, 82), Width = 140, Height = 26 };
private readonly Button _lampsOff = new() { Text = "All lamps off", Location = new Point(156, 82), Width = 140, Height = 26 };
private readonly Button _testEnter = new() { Text = "Enter test mode", Location = new Point(10, 114), Width = 140, Height = 26 };
private readonly Button _testExit = new() { Text = "Exit test mode", Location = new Point(156, 114), Width = 140, Height = 26 };
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 };
private readonly Button _lampsOff = new() { Text = "All lamps off", Location = new Point(156, 52), Width = 140, Height = 26 };
private readonly CheckBox _kbInput = new() { Text = "Keyboard", Location = new Point(10, 22), AutoSize = true, Checked = true };
private readonly CheckBox _padInput = new() { Text = "Xbox gamepad", Location = new Point(120, 22), AutoSize = true, Checked = true };
private readonly CheckBox _invertY = new() { Text = "Invert joystick Y", Location = new Point(10, 46), AutoSize = true, Checked = true };
private readonly CheckBox _padInput = new() { Text = "Gamepad", Location = new Point(100, 22), AutoSize = true, Checked = true };
private readonly CheckBox _invertY = new() { Text = "Invert Y", Location = new Point(190, 22), AutoSize = true };
private readonly Label _padStatus = new()
{
Text = "No controller detected.",
Location = new Point(10, 70),
Location = new Point(10, 46),
AutoSize = true,
ForeColor = Color.Gray,
};
private readonly Button _reloadBindings = new() { Text = "Reload bindings", Location = new Point(10, 92), Width = 140, Height = 26 };
private readonly Button _editBindings = new() { Text = "Edit bindings…", Location = new Point(156, 92), Width = 140, Height = 26 };
private readonly Button _reloadBindings = new() { Text = "Reload bindings", Location = new Point(10, 68), Width = 140, Height = 26 };
private readonly Button _editBindings = new() { Text = "Edit bindings…", Location = new Point(156, 68), Width = 140, Height = 26 };
private readonly CheckBox _kbLights = new()
{
Text = "Mirror lamps on RGB keyboard (Dynamic Lighting)",
Location = new Point(10, 124),
Location = new Point(10, 100),
AutoSize = true,
};
private readonly ComboBox _kbLightsTarget = new()
{
Location = new Point(10, 146),
Location = new Point(10, 122),
Width = 286,
DropDownStyle = ComboBoxStyle.DropDownList,
Enabled = false, // populated while the mirror is on
@@ -77,12 +73,12 @@ internal sealed class MainForm : Form
private readonly Label _kbInputLabel = new()
{
Text = "Capture keyboard (no window focus needed):",
Location = new Point(10, 168),
Location = new Point(10, 144),
AutoSize = true,
};
private readonly ComboBox _kbInputTarget = new()
{
Location = new Point(10, 186),
Location = new Point(10, 162),
Width = 286,
DropDownStyle = ComboBoxStyle.DropDownList,
};
@@ -95,25 +91,24 @@ internal sealed class MainForm : Form
private readonly Label _counters = new()
{
Location = new Point(12, 458),
Location = new Point(12, 372),
AutoSize = true,
Font = new Font("Consolas", 8f),
};
private readonly Label _help = new()
{
Location = new Point(12, 514),
Location = new Point(12, 428),
MaximumSize = new Size(306, 0),
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. " +
"Keyboard and Xbox-pad input follow the bindings file (Edit bindings…).",
"Drag the X/Y box and the Z / L / R gauges to move the axes.",
};
private readonly TextBox _logBox = new()
{
Location = new Point(12, 598),
Location = new Point(12, 498),
Multiline = true,
ReadOnly = true,
ScrollBars = ScrollBars.Both, // long wire lines don't wrap — scroll to read
@@ -184,8 +179,6 @@ internal sealed class MainForm : Form
_rescan.Click += (_, _) => RefreshPorts();
_openClose.Click += (_, _) => ToggleOpen();
_verMajor.ValueChanged += (_, _) => _device.VersionMajor = (byte)_verMajor.Value;
_verMinor.ValueChanged += (_, _) => _device.VersionMinor = (byte)_verMinor.Value;
_spring.CheckedChanged += (_, _) => _canvas.StickSpringsBack = _spring.Checked;
_centerAxes.Click += (_, _) =>
{
@@ -198,8 +191,6 @@ internal sealed class MainForm : Form
_device.ClearLamps();
_canvas.Invalidate();
};
_testEnter.Click += (_, _) => _device.SendTestMode(1);
_testExit.Click += (_, _) => _device.SendTestMode(0);
_clearLog.Click += (_, _) => _logBox.Clear();
_kbInput.CheckedChanged += (_, _) =>
@@ -280,21 +271,17 @@ internal sealed class MainForm : Form
panel.Controls.Add(_openClose);
panel.Controls.Add(_linkStatus);
var device = new GroupBox { Text = "Device", Location = new Point(12, 68), Size = new Size(306, 150) };
device.Controls.Add(new Label { Text = "Firmware:", Location = new Point(10, 27), AutoSize = true });
device.Controls.Add(_verMajor);
device.Controls.Add(new Label { Text = ".", Location = new Point(127, 27), AutoSize = true });
device.Controls.Add(_verMinor);
device.Controls.AddRange(new Control[] { _spring, _centerAxes, _lampsOff, _testEnter, _testExit });
var device = new GroupBox { Text = "Device", Location = new Point(12, 68), Size = new Size(306, 88) };
device.Controls.AddRange(new Control[] { _spring, _centerAxes, _lampsOff });
panel.Controls.Add(device);
var input = new GroupBox { Text = "Input", Location = new Point(12, 224), Size = new Size(306, 226) };
var input = new GroupBox { Text = "Input", Location = new Point(12, 162), Size = new Size(306, 202) };
input.Controls.AddRange(new Control[] { _kbInput, _padInput, _invertY, _padStatus, _reloadBindings, _editBindings, _kbLights, _kbLightsTarget, _kbInputLabel, _kbInputTarget });
panel.Controls.Add(input);
panel.Controls.Add(_counters);
panel.Controls.Add(_help);
panel.Controls.Add(new Label { Text = "Wire log:", Location = new Point(12, 580), AutoSize = true });
panel.Controls.Add(new Label { Text = "Wire log:", Location = new Point(12, 480), AutoSize = true });
_logBox.Size = new Size(306, ClientSize.Height - _logBox.Top - 44);
panel.Controls.Add(_logBox);
@@ -359,11 +346,11 @@ internal sealed class MainForm : Form
/// <summary>
/// Keys route to the panel unless the user is in a control that needs
/// them (port list, firmware spinners, log box scrolling).
/// them (port list, log box scrolling).
/// </summary>
private bool KeyboardRoutingActive =>
_kbInput.Checked &&
!_portBox.ContainsFocus && !_verMajor.ContainsFocus && !_verMinor.ContainsFocus && !_logBox.ContainsFocus;
!_portBox.ContainsFocus && !_logBox.ContainsFocus;
protected override void OnHandleCreated(EventArgs e)
{
+9 -6
View File
@@ -58,9 +58,11 @@ public sealed class VRioDevice
public byte VersionMinor { get; set; } = 2;
/// <summary>
/// Negate joystick Y in AnalogReply. Only the host sees the flip — local
/// state (<see cref="GetAxis"/>, the panel's dot and readout) keeps the
/// physical stick direction.
/// The wire natively carries stick-up as negative (full up = 80 in
/// AnalogReply); set this to send the physical direction (up = positive)
/// instead. Only the host sees the difference — local state
/// (<see cref="GetAxis"/>, the panel's dot) keeps the physical stick
/// direction either way.
/// </summary>
public bool InvertJoystickY { get; set; }
@@ -110,7 +112,8 @@ public sealed class VRioDevice
/// <summary>
/// Axis value as the next AnalogReply will carry it — same as
/// <see cref="GetAxis"/> except for the <see cref="InvertJoystickY"/> flip.
/// <see cref="GetAxis"/> except joystick Y follows the wire convention
/// (see <see cref="InvertJoystickY"/>).
/// </summary>
public short GetWireAxis(RioAxis axis)
{
@@ -119,8 +122,8 @@ public sealed class VRioDevice
}
private short WireY(short y) => InvertJoystickY
? (short)Math.Min(AnalogCodec.Max, -y) // clamp: -Min (8192) is one past the 14-bit Max
: y;
? y
: (short)Math.Min(AnalogCodec.Max, -y); // clamp: -Min (8192) is one past the 14-bit Max
/// <summary>
/// Move an axis. Values are clamped to the 14-bit signed range the wire
+17 -3
View File
@@ -61,15 +61,15 @@ public class VRioDeviceTests
Assert.Equal(1000, AnalogCodec.Combine(p[0], p[1])); // throttle
Assert.Equal(0, AnalogCodec.Combine(p[2], p[3])); // left pedal
Assert.Equal(0, AnalogCodec.Combine(p[4], p[5])); // right pedal
Assert.Equal(3000, AnalogCodec.Combine(p[6], p[7])); // joystick Y
Assert.Equal(-3000, AnalogCodec.Combine(p[6], p[7])); // joystick Y (up is negative on the wire)
Assert.Equal(-5000, AnalogCodec.Combine(p[8], p[9])); // joystick X
Assert.Equal(1, device.AnalogRequests);
}
[Fact]
public void InvertJoystickY_flips_the_wire_but_not_local_state()
public void Joystick_Y_is_negated_on_the_wire_but_not_locally()
{
var device = new VRioDevice { InvertJoystickY = true };
var device = new VRioDevice();
var wire = new Wire(device);
device.SetAxis(RioAxis.JoystickY, 3000);
@@ -88,6 +88,20 @@ public class VRioDeviceTests
Assert.Equal(AnalogCodec.Max, AnalogCodec.Combine(p[6], p[7]));
}
[Fact]
public void InvertJoystickY_sends_the_physical_direction_instead()
{
var device = new VRioDevice { InvertJoystickY = true };
var wire = new Wire(device);
device.SetAxis(RioAxis.JoystickY, 3000);
Send(device, PacketBuilder.Build(RioCommand.AnalogRequest));
byte[] p = Assert.Single(wire.Packets).Payload;
Assert.Equal(3000, AnalogCodec.Combine(p[6], p[7]));
Assert.Equal(3000, device.GetWireAxis(RioAxis.JoystickY));
}
[Fact]
public void VersionRequest_reports_configured_firmware()
{