Phase 4: axis calibration + plasma display (RioJoy.Core)
Port the analog calibration math and the plasma/VFD command set: - Calibration/: AxisCalibrator ports UpdateThrottle/UpdatePadal/UpdateJoystick (riovjoy2.cpp#L1504+) — throttle deadzone + ratchet field, pedal deadzones, joystick X/Y auto-ranging from observed min/max, rudder mixing (enableZR), and the per-axis invert flags. Stateful (start positions, last outputs, observed extremes) like the legacy globals, with the RIOcmd axis resets. Final outputs clamp to the documented 0..32766 range (also guards a legacy compounding quirk). AxisOutputs carries the six values; IJoystickSink gains SetAxis(JoyAxis,value) so calibrated axes reach the HID feeder. - Plasma/: PlasmaCommands builds the CPlasma ESC sequences (clear/cursor/font/ attr/box draw+fill/text) + GetFontSize + the PlasmaPosText auto-fit/centering; PlasmaDisplay writes them over the secondary COM transport. - tests: 21 new xUnit tests (105 total) for throttle proportional/saturation/ invert, pedal ZR mix vs. direct, joystick centering/direction/invert/reset, the output clamp, and plasma byte sequences/font sizes/auto-fit positioning. Hardware verification of axis feel + plasma output remains; the game-specific PlasmaScoreDraw layout is deferred to profile content (Phase 5/7). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,118 @@
|
||||
using RioJoy.Core.Calibration;
|
||||
using RioJoy.Core.Protocol;
|
||||
using Xunit;
|
||||
|
||||
namespace RioJoy.Core.Tests.Calibration;
|
||||
|
||||
public class AxisCalibratorTests
|
||||
{
|
||||
// AnalogReport(throttle, leftPedal, rightPedal, joystickY, joystickX)
|
||||
private static AnalogReport Report(
|
||||
short throttle = 0, short left = 0, short right = 0, short y = 0, short x = 0) =>
|
||||
new(throttle, left, right, y, x);
|
||||
|
||||
// --- Throttle (Z) --------------------------------------------------------
|
||||
|
||||
[Fact]
|
||||
public void Throttle_BelowStart_IsProportional()
|
||||
{
|
||||
var cal = new AxisCalibrator();
|
||||
cal.ResetThrottle();
|
||||
|
||||
// At-rest after reset: throttle equals its tracked start ⇒ centered at 0.
|
||||
Assert.Equal(0, cal.Update(Report(throttle: 0)).Z);
|
||||
|
||||
// Pulled back 400 of 800 → half range × 32 = 16000.
|
||||
Assert.Equal(16000, cal.Update(Report(throttle: -400)).Z);
|
||||
|
||||
// Full back (800) → 32000.
|
||||
Assert.Equal(32000, cal.Update(Report(throttle: -800)).Z);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Throttle_OutOfRange_Saturates()
|
||||
{
|
||||
Assert.Equal(0, new AxisCalibrator().Update(Report(throttle: 900)).Z); // > +range
|
||||
Assert.Equal(32000, new AxisCalibrator().Update(Report(throttle: -900)).Z); // < -range
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Throttle_Invert_Flips()
|
||||
{
|
||||
var cal = new AxisCalibrator(new AxisCalibrationConfig { InvertZ = true });
|
||||
// throttle=900 → raw 0 → inverted to Max.
|
||||
Assert.Equal(AxisOutputs.Max, cal.Update(Report(throttle: 900)).Z);
|
||||
}
|
||||
|
||||
// --- Pedals (Rx / Ry / Rz) ----------------------------------------------
|
||||
|
||||
[Fact]
|
||||
public void Pedals_EnableZR_CentersRxRy_AndMixesRudder()
|
||||
{
|
||||
var cal = new AxisCalibrator(); // EnableZR default true
|
||||
AxisOutputs o = cal.Update(Report(left: 0, right: 0));
|
||||
|
||||
Assert.Equal(AxisOutputs.Center, o.Rx);
|
||||
Assert.Equal(AxisOutputs.Center, o.Ry);
|
||||
Assert.Equal(AxisOutputs.Center, o.Rz); // rudder with both pedals at rest
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Pedals_DisableZR_DriveRxDirectly_AndCenterRz()
|
||||
{
|
||||
var cal = new AxisCalibrator(new AxisCalibrationConfig { EnableZR = false });
|
||||
|
||||
// First sample sets the auto-range start; second (less depressed) produces a value.
|
||||
cal.Update(Report(left: -100));
|
||||
AxisOutputs o = cal.Update(Report(left: 0));
|
||||
|
||||
// lP = (-100) - 0 = -100 → ×1000/500 = -200 → |·|×32 = 6400.
|
||||
Assert.Equal(6400, o.Rx);
|
||||
Assert.Equal(AxisOutputs.Center, o.Rz); // Rz centered when ZR disabled
|
||||
}
|
||||
|
||||
// --- Joystick (X / Y) ----------------------------------------------------
|
||||
|
||||
[Fact]
|
||||
public void Joystick_AtZero_IsCentered()
|
||||
{
|
||||
AxisOutputs o = new AxisCalibrator().Update(Report(x: 0, y: 0));
|
||||
Assert.Equal(AxisOutputs.Center, o.X);
|
||||
Assert.Equal(AxisOutputs.Center, o.Y);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void JoystickX_RightAndLeft_MoveOppositeSidesOfCenter()
|
||||
{
|
||||
// Right (positive raw) → below center; with auto-range seeded by this sample.
|
||||
Assert.Equal(52, new AxisCalibrator().Update(Report(x: 80)).X);
|
||||
// Left (negative raw) → above center.
|
||||
Assert.Equal(32733, new AxisCalibrator().Update(Report(x: -80)).X);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void JoystickX_Invert_Flips()
|
||||
{
|
||||
var cal = new AxisCalibrator(new AxisCalibrationConfig { InvertX = true });
|
||||
Assert.Equal(AxisOutputs.Max - 52, cal.Update(Report(x: 80)).X);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ResetHorizontalJoystick_RecentersX()
|
||||
{
|
||||
var cal = new AxisCalibrator();
|
||||
Assert.Equal(52, cal.Update(Report(x: 80)).X);
|
||||
|
||||
cal.ResetHorizontalJoystick();
|
||||
// After reset, X holds center until the stick next moves off zero.
|
||||
Assert.Equal(AxisOutputs.Center, cal.Update(Report(x: 0)).X);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Outputs_AreClampedToAxisRange()
|
||||
{
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,8 @@ internal sealed class RecordingSink : IInputSink, IJoystickSink, ILampSink, IRio
|
||||
|
||||
public void SetHat(RioHat position) => Log.Add($"Hat({position})");
|
||||
|
||||
public void SetAxis(RioJoy.Core.Calibration.JoyAxis axis, int value) => Log.Add($"Axis({axis},{value})");
|
||||
|
||||
public void SetLamp(int address, byte lampState) => Log.Add($"Lamp(0x{address:X2},0x{lampState:X2})");
|
||||
|
||||
public void Execute(RioCommandCode command) => Log.Add($"Cmd({command})");
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
using RioJoy.Core.Plasma;
|
||||
using Xunit;
|
||||
|
||||
namespace RioJoy.Core.Tests.Plasma;
|
||||
|
||||
public class PlasmaCommandsTests
|
||||
{
|
||||
[Fact]
|
||||
public void EscSequences_AreCorrect()
|
||||
{
|
||||
Assert.Equal(new byte[] { 27, (byte)'@' }, PlasmaCommands.Clear());
|
||||
Assert.Equal(new byte[] { 27, (byte)'L' }, PlasmaCommands.CursorHome());
|
||||
Assert.Equal(new byte[] { 27, (byte)'R', 10 }, PlasmaCommands.CursorX(10));
|
||||
Assert.Equal(new byte[] { 27, (byte)'Q', 20 }, PlasmaCommands.CursorY(20));
|
||||
Assert.Equal(new byte[] { 27, (byte)'H', 3 }, PlasmaCommands.FontAttr(3));
|
||||
Assert.Equal(new byte[] { 27, (byte)'K', 5 }, PlasmaCommands.Font(5));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Box_DrawAndFill_HaveExpectedLayout()
|
||||
{
|
||||
Assert.Equal(new byte[] { 27, (byte)'X', 1, 2, 3, 4 }, PlasmaCommands.BoxDraw(1, 2, 3, 4));
|
||||
// Fill inserts a leading 0 parameter.
|
||||
Assert.Equal(new byte[] { 27, (byte)'x', 0, 1, 2, 3, 4 }, PlasmaCommands.BoxFill(1, 2, 3, 4));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Text_EncodesOneBytePerChar()
|
||||
{
|
||||
Assert.Equal(new byte[] { (byte)'A', (byte)'B', (byte)'C' }, PlasmaCommands.Text("ABC"));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(0, 5, 7)]
|
||||
[InlineData(3, 5, 7)]
|
||||
[InlineData(4, 10, 14)]
|
||||
[InlineData(5, 10, 14)]
|
||||
[InlineData(7, 5, 7)]
|
||||
public void GetFontSize_MatchesLegacy(int font, int w, int h)
|
||||
{
|
||||
Assert.Equal(new FontSize(w, h), PlasmaCommands.GetFontSize(font));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ResolvePosText_ShortText_PicksFont5_AndCenters()
|
||||
{
|
||||
// "HELLO" (5) is non-score and ≤9 → font 5 (10×14). Centered around (56,15).
|
||||
(byte x, byte y, byte font, int len) = PlasmaCommands.ResolvePosText("HELLO", 0, 0, 0);
|
||||
Assert.Equal(5, font);
|
||||
Assert.Equal(5, len);
|
||||
Assert.Equal(31, x); // 56 - (5*10/2)
|
||||
Assert.Equal(8, y); // 15 - (14/2)
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ResolvePosText_LongText_Font2_AndCapsLength()
|
||||
{
|
||||
string text = new string('1', 24);
|
||||
(byte x, byte y, byte font, int len) = PlasmaCommands.ResolvePosText(text, 0, 0, 0);
|
||||
Assert.Equal(2, font);
|
||||
Assert.Equal(20, len); // capped
|
||||
Assert.Equal(6, x); // 56 - (20*5/2)
|
||||
Assert.Equal(12, y); // 15 - (7/2)
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ResolvePosText_ScoreFont_AndExplicitPosition_Unchanged()
|
||||
{
|
||||
// font 2 (score) keeps its font; non-zero position is not re-centered.
|
||||
(byte x, byte y, byte font, int len) = PlasmaCommands.ResolvePosText("AB", 5, 6, 2);
|
||||
Assert.Equal(2, font);
|
||||
Assert.Equal(2, len);
|
||||
Assert.Equal(5, x);
|
||||
Assert.Equal(6, y);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user