diff --git a/src/RioJoy.Core/Calibration/AxisCalibrator.cs b/src/RioJoy.Core/Calibration/AxisCalibrator.cs index d05a708..be5ca99 100644 --- a/src/RioJoy.Core/Calibration/AxisCalibrator.cs +++ b/src/RioJoy.Core/Calibration/AxisCalibrator.cs @@ -200,7 +200,19 @@ public sealed class AxisCalibrator // 16838 (not 16383) and a +2 nudge are deliberate legacy anti-snap tweaks. _joystickXLast = lJx > 0 ? 16838 - ((lJx + 2) * sRightRate) : AxisOutputs.Center; } - // lJx == 0 leaves _joystickXLast unchanged (legacy behavior). + else + { + // Deliberate divergence from the legacy port, same reasoning as the + // throttle detent above: the legacy held the previous output on a raw + // of exactly 0. A real pot jitters and never rests at exact 0, so the + // hold was invisible on hardware - but vRIO's pad deadzone emits + // sustained exact zeros on release, and the hold latched the last + // in-motion output indefinitely: the ship kept turning at whatever + // rate the stick commanded the instant before release (bench + // 2026-08-01, Descent 3 drifting on yaw/pitch with the stick + // centered). Exact center in must be exact center out. + _joystickXLast = AxisOutputs.Center; + } return _config.InvertX ? AxisOutputs.Max - _joystickXLast : _joystickXLast; } @@ -223,7 +235,12 @@ public sealed class AxisCalibrator lJy -= DeadzoneJoystick; _joystickYLast = lJy > 0 ? AxisOutputs.Center - (lJy * sDownRate) : AxisOutputs.Center; } - // lJy == 0 leaves _joystickYLast unchanged (legacy behavior). + else + { + // Exact 0 centers rather than holding the previous output - see the + // matching branch in JoystickX for the full story. + _joystickYLast = AxisOutputs.Center; + } return _config.InvertY ? AxisOutputs.Max - _joystickYLast : _joystickYLast; } diff --git a/tests/RioJoy.Core.Tests/Calibration/AxisCalibratorTests.cs b/tests/RioJoy.Core.Tests/Calibration/AxisCalibratorTests.cs index 7496c54..6189fbe 100644 --- a/tests/RioJoy.Core.Tests/Calibration/AxisCalibratorTests.cs +++ b/tests/RioJoy.Core.Tests/Calibration/AxisCalibratorTests.cs @@ -163,4 +163,31 @@ public class AxisCalibratorTests cal.Update(Report(throttle: -400)); // power-on with lever pushed: becomes the start Assert.Equal(0, cal.Update(Report(throttle: -100)).Z); // above max → re-based, rest } + + [Fact] + public void Joystick_ReleaseToExactZero_Centers_NotHeld() + { + // Regression: the legacy left the output unchanged on a raw of exactly 0, + // which a jittering pot never produces - but vRIO's pad deadzone does, + // sustained, on every release. The held output latched the last in-motion + // value and the ship kept turning with the stick centered (bench + // 2026-08-01). A fast release goes straight from deflected to exact 0 + // with no intermediate sample; both axes must land on center. + var cal = new AxisCalibrator(); + cal.Update(Report(x: -3000, y: 2000)); // hard over + AxisOutputs o = cal.Update(Report(x: 0, y: 0)); // released, deadzoned to exact 0 + Assert.Equal(AxisOutputs.Center, o.X); + Assert.Equal(AxisOutputs.Center, o.Y); + } + + [Fact] + public void Joystick_ExactZero_StaysCentered_AcrossRepeatedPolls() + { + // The latch fed forward: once held off-center, every subsequent exact-0 + // poll (one per 55 ms, forever) kept it there. Center must be stable. + var cal = new AxisCalibrator(); + cal.Update(Report(x: 5000)); + for (int i = 0; i < 5; i++) + Assert.Equal(AxisOutputs.Center, cal.Update(Report(x: 0)).X); + } }