calibrator: exact center in must be exact center out

The legacy joystick math left the output unchanged when the raw sample was
exactly 0 - a case a jittering pot never produces, so on real hardware the hold
was invisible. vRIO's pad deadzone produces it constantly: every release of the
bench gamepad shapes the residual to exactly 0.0 and holds it there, one sample
per 55 ms poll. The calibrator held the last in-motion output against that
stream indefinitely, so the virtual pad froze off-center at whatever the stick
commanded the instant before release.

Flown, that read as a Descent 3 pod drifting on yaw and pitch with the stick
centered, stopping only when the release was slow enough to land an
intermediate sample inside the +/-5 band. It survived a full day of suspects -
the game's deadzone, its axis map, its focus handling, com0com, the serial
protocol - because every layer below the calibrator was correct: the freeze was
visible in joy.cpl itself, and the game's own control trace showed heading
latched at -0.38 while the wire carried perfect zeros.

Same divergence from the legacy port, same reasoning, as the throttle detent
fix above it: zero means centered, not "no information". Both axes; regression
tests pin the fast-release case and center stability across repeated zero
polls.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Cyd
2026-08-01 20:49:25 -05:00
co-authored by Claude Opus 5
parent bdd30678e3
commit 870a8a257d
2 changed files with 46 additions and 2 deletions
+19 -2
View File
@@ -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;
}
@@ -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);
}
}