diff --git a/src/VRio.App/MainForm.cs b/src/VRio.App/MainForm.cs
index 5fd415e..9da31a0 100644
--- a/src/VRio.App/MainForm.cs
+++ b/src/VRio.App/MainForm.cs
@@ -190,8 +190,8 @@ internal sealed class MainForm : Form
if (!_kbInput.Checked)
_router.ReleaseAllKeys();
};
- _invertY.CheckedChanged += (_, _) => _router.InvertJoystickY = _invertY.Checked;
- _router.InvertJoystickY = _invertY.Checked; // the field initializer raises no event
+ _invertY.CheckedChanged += (_, _) => _device.InvertJoystickY = _invertY.Checked;
+ _device.InvertJoystickY = _invertY.Checked; // the field initializer raises no event
_kbLights.CheckedChanged += (_, _) =>
{
_lampMirror.Enabled = _kbLights.Checked;
diff --git a/src/VRio.Core/Device/VRioDevice.cs b/src/VRio.Core/Device/VRioDevice.cs
index b2c4f1c..f169a19 100644
--- a/src/VRio.Core/Device/VRioDevice.cs
+++ b/src/VRio.Core/Device/VRioDevice.cs
@@ -57,6 +57,13 @@ public sealed class VRioDevice
/// Firmware version reported by VersionReply (real boards run 4.2).
public byte VersionMinor { get; set; } = 2;
+ ///
+ /// Negate joystick Y in AnalogReply. Only the host sees the flip — local
+ /// state (, the panel's dot and readout) keeps the
+ /// physical stick direction.
+ ///
+ public bool InvertJoystickY { get; set; }
+
///
/// When true, retry exhaustion leaves the analog reply path wedged (the
/// v4.2 latch-leak bug) until a host ResetRequest clears it.
@@ -274,6 +281,8 @@ public sealed class VRioDevice
y = _axes[(int)RioAxis.JoystickY];
x = _axes[(int)RioAxis.JoystickX];
}
+ if (InvertJoystickY) // clamp: -Min (8192) is one past the 14-bit Max
+ y = (short)Math.Min(AnalogCodec.Max, -y);
Send(PacketBuilder.AnalogReply(t, l, r, y, x));
break;
diff --git a/src/VRio.Core/Input/InputRouter.cs b/src/VRio.Core/Input/InputRouter.cs
index f00221b..fac9776 100644
--- a/src/VRio.Core/Input/InputRouter.cs
+++ b/src/VRio.Core/Input/InputRouter.cs
@@ -57,13 +57,6 @@ public sealed class InputRouter
/// A routed address went down (true) or came back up (false) — for panel highlighting.
public event Action? AddressHeldChanged;
- ///
- /// Flip the sign of the composed joystick Y before it reaches the device.
- /// Applies to all routed sources (keys and pad) alike; panel drags write
- /// the device directly and are unaffected.
- ///
- public bool InvertJoystickY { get; set; }
-
///
/// The active bindings. Assigning releases everything currently held
/// (keys, toggles, pad buttons) so a reload never strands a pressed input.
@@ -223,8 +216,6 @@ public sealed class InputRouter
if (b.Rate <= 0f)
total += Shape(_pad.Axis(b.Source), b);
}
- if (InvertJoystickY && axis == RioAxis.JoystickY)
- total = -total;
total = ClampNorm(axis, total);
short raw = (short)Math.Round(total * RioAxisRange.Full(axis));
diff --git a/tests/VRio.Core.Tests/InputRouterTests.cs b/tests/VRio.Core.Tests/InputRouterTests.cs
index 64d5863..2be571e 100644
--- a/tests/VRio.Core.Tests/InputRouterTests.cs
+++ b/tests/VRio.Core.Tests/InputRouterTests.cs
@@ -191,26 +191,6 @@ public class InputRouterTests
Assert.Equal(0, _device.GetAxis(RioAxis.JoystickY));
}
- [Fact]
- public void InvertJoystickY_flips_the_composed_value_for_all_sources()
- {
- UseProfile("key Up axis JoystickY deflect 1\npadaxis LeftStickY axis JoystickY");
- _router.InvertJoystickY = true;
-
- _router.KeyDown("Up");
- _router.Tick(0.016);
- Assert.Equal(-RioAxisRange.JoystickExtent, _device.GetAxis(RioAxis.JoystickY));
- _router.KeyUp("Up");
-
- _router.SetPadState(new PadState(PadButtons.None, 0, 0.5f, 0, 0, 0, 0));
- _router.Tick(0.016);
- Assert.Equal(-RioAxisRange.JoystickExtent / 2, _device.GetAxis(RioAxis.JoystickY));
-
- _router.InvertJoystickY = false; // toggling back re-sends the upright value
- _router.Tick(0.016);
- Assert.Equal(RioAxisRange.JoystickExtent / 2, _device.GetAxis(RioAxis.JoystickY));
- }
-
[Fact]
public void Rate_key_walks_the_throttle_and_position_sticks()
{
diff --git a/tests/VRio.Core.Tests/VRioDeviceTests.cs b/tests/VRio.Core.Tests/VRioDeviceTests.cs
index d8ee2da..38a17c9 100644
--- a/tests/VRio.Core.Tests/VRioDeviceTests.cs
+++ b/tests/VRio.Core.Tests/VRioDeviceTests.cs
@@ -66,6 +66,27 @@ public class VRioDeviceTests
Assert.Equal(1, device.AnalogRequests);
}
+ [Fact]
+ public void InvertJoystickY_flips_the_wire_but_not_local_state()
+ {
+ 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.GetAxis(RioAxis.JoystickY)); // the panel's view is unflipped
+
+ // Full negative deflection: -Min is one past Max, so it clamps rather than throws.
+ device.SetAxis(RioAxis.JoystickY, AnalogCodec.Min);
+ wire.Clear();
+ Send(device, PacketBuilder.Build(RioCommand.AnalogRequest));
+ p = Assert.Single(wire.Packets).Payload;
+ Assert.Equal(AnalogCodec.Max, AnalogCodec.Combine(p[6], p[7]));
+ }
+
[Fact]
public void VersionRequest_reports_configured_firmware()
{