calibration: throttle reads rest (0), not full, at the detent

Faithful port of a legacy quirk made harmful by axis routing: _throttleLast
initialized to Center and the lT==0 hold path meant the first poll at rest
compounded 16383*32 -> clamped 32766 = FULL throttle until the lever moved
past the deadzone (under the old routing this held LeftTrigger at 255 -
fire-secondary - from power-on). lT==0 now zeroes like the deadzone branch
and _throttleLast initializes to 0, the sanctioned rest value ResetAll/
ResetThrottle already used. Three regression tests replace the one that
asserted the old behavior.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Cyd
2026-07-30 09:00:09 -05:00
co-authored by Claude Fable 5
parent 2f2438717a
commit 6967e66837
2 changed files with 57 additions and 19 deletions
@@ -128,11 +128,39 @@ public class AxisCalibratorTests
Assert.Equal(AxisOutputs.Center, cal.Update(Report(x: 0)).X);
}
// --- Detent regression (legacy quirk deliberately fixed) -----------------
[Fact]
public void Outputs_AreClampedToAxisRange()
public void Throttle_FirstPollAtRest_IsZero_NotPinned()
{
// The init-state throttle quirk yields a value far over range on the first
// poll at rest; the documented 0..Max clamp guards it.
Assert.Equal(AxisOutputs.Max, new AxisCalibrator().Update(Report(throttle: 0)).Z);
// Regression: the legacy Center init + detent-hold compounded through the
// ×32 rescale and pinned Z at Max on the very first poll at rest — full
// reverse on a Centered thumb route, full thrust on a UnipolarPositive one
// (Descent). Rest must read 0 with no reset and stay there.
var cal = new AxisCalibrator();
Assert.Equal(0, cal.Update(Report(throttle: 0)).Z);
Assert.Equal(0, cal.Update(Report(throttle: 0)).Z);
}
[Fact]
public void Throttle_ReleaseExactlyOntoStart_ReturnsToZero_NoSpike()
{
// Regression: a release landing exactly on the tracked start (lT == 0)
// used to hold the last moving value, which the ×32 rescale turned into a
// one-poll full-throttle spike until jitter broke it.
var cal = new AxisCalibrator();
Assert.Equal(0, cal.Update(Report(throttle: 0)).Z); // seed start at rest
Assert.Equal(16000, cal.Update(Report(throttle: -400)).Z); // pushed halfway
Assert.Equal(0, cal.Update(Report(throttle: 0)).Z); // release onto start
}
[Fact]
public void Throttle_SampleAboveRunningMax_ReadsAsRest()
{
// Samples above the tracked start re-base the auto-range (lT == 0 again)
// and must read as rest, not re-enter the legacy hold path.
var cal = new AxisCalibrator();
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
}
}