diff --git a/docs/PLAN.md b/docs/PLAN.md index c52925a..b78d3d3 100644 --- a/docs/PLAN.md +++ b/docs/PLAN.md @@ -281,11 +281,12 @@ Replaces the legacy Google-Sheet → `.data` → GIMP → Script-Fu pipeline context-sensitive picker (keyboard key by name via `KeyCatalog`, joystick Button N, hat direction, mouse/RIO-command enum); modifiers enable only for keyboard. Opened from the tray ("Edit profile…"); Save persists the profile. The encoder - gauges are **live**: `RioRuntime.AxesUpdated` streams the calibrated axis values - (post invert/deadzone/mix — exactly what the virtual joystick receives) into the + gauges are **live**: `RioRuntime.AxesUpdated` streams an `AxisReadout` (the six + virtual-joystick outputs plus the calibrated pre-mix pedal positions) into the strip at the analog poll rate — Z and the L/R pedals fill bottom-up, Rz deflects from its center tick, and the X/Y box tracks the stick as a dot (pure fraction - math in `RioJoy.Core.Editing.AxisGauges`, unit-tested). + math in `RioJoy.Core.Editing.AxisGauges`, unit-tested). L/R read the pedals + from before the ZR mix, since the mix pins Rx/Ry to center. ⏳ Still to refine: showing each button's assigned key as a caption, grouping a button's two bank addresses, and clone-from-existing. - The unified profile JSON supersedes both `RIO.ini` and the Google Sheet, with diff --git a/src/RioJoy.Core/Calibration/AxisCalibrator.cs b/src/RioJoy.Core/Calibration/AxisCalibrator.cs index d285c9c..b67b0f4 100644 --- a/src/RioJoy.Core/Calibration/AxisCalibrator.cs +++ b/src/RioJoy.Core/Calibration/AxisCalibrator.cs @@ -70,6 +70,16 @@ public sealed class AxisCalibrator private static int Clamp(int v) => RioJoy.Core.Compat.Net48Math.Clamp(v, 0, AxisOutputs.Max); + /// + /// The calibrated left-pedal position from the last + /// (0..32766) — always tracks the pedal, even when + /// centers . + /// + public int LeftPedalOutput => Clamp(_leftPedalLast); + + /// Right-pedal counterpart of . + public int RightPedalOutput => Clamp(_rightPedalLast); + private int Throttle(int throttle) { if (throttle < -RangeThrottle || throttle > RangeThrottle) diff --git a/src/RioJoy.Core/Calibration/AxisReadout.cs b/src/RioJoy.Core/Calibration/AxisReadout.cs new file mode 100644 index 0000000..dea4019 --- /dev/null +++ b/src/RioJoy.Core/Calibration/AxisReadout.cs @@ -0,0 +1,27 @@ +namespace RioJoy.Core.Calibration; + +/// +/// One live calibration sample for gauge displays: the six virtual-joystick axis +/// outputs plus the calibrated pedal positions. The pedals are surfaced +/// separately because the ZR mix () +/// folds them into Rz and pins Rx/Ry to center — a pedal gauge fed from the axes +/// alone would freeze while the physical pedals move. +/// +public readonly struct AxisReadout +{ + /// The six virtual-joystick outputs (what the HID device receives). + public AxisOutputs Axes { get; } + + /// Calibrated left-pedal position (0..32766), before the ZR mix. + public int LeftPedal { get; } + + /// Calibrated right-pedal position (0..32766), before the ZR mix. + public int RightPedal { get; } + + public AxisReadout(AxisOutputs axes, int leftPedal, int rightPedal) + { + Axes = axes; + LeftPedal = leftPedal; + RightPedal = rightPedal; + } +} diff --git a/src/RioJoy.Core/RioRuntime.cs b/src/RioJoy.Core/RioRuntime.cs index 1744b73..d57b5d3 100644 --- a/src/RioJoy.Core/RioRuntime.cs +++ b/src/RioJoy.Core/RioRuntime.cs @@ -57,11 +57,11 @@ public sealed class RioRuntime : IRioCommandSink, IDisposable public event Action? ButtonActivity; /// - /// Raised with the six calibrated axis values after every analog reply — - /// independent of the joystick sink, so the editor can show live gauges even - /// while input routing is suppressed. + /// Raised with the calibrated readout (virtual axes + pre-mix pedal positions) + /// after every analog reply — independent of the joystick sink, so the editor + /// can show live gauges even while input routing is suppressed. /// - public event Action? AxesUpdated; + public event Action? AxesUpdated; /// Raised with the firmware version when a arrives. public event Action? VersionReceived; @@ -137,7 +137,8 @@ public sealed class RioRuntime : IRioCommandSink, IDisposable // Output is best-effort: a faulty joystick sink must not kill the link. } - AxesUpdated?.Invoke(axes); + AxesUpdated?.Invoke(new AxisReadout( + axes, _calibrator.LeftPedalOutput, _calibrator.RightPedalOutput)); } private void OnPacket(RioPacket packet) diff --git a/src/RioJoy.Tray/Editor/PanelCanvas.cs b/src/RioJoy.Tray/Editor/PanelCanvas.cs index 8a32e20..9b6429a 100644 --- a/src/RioJoy.Tray/Editor/PanelCanvas.cs +++ b/src/RioJoy.Tray/Editor/PanelCanvas.cs @@ -32,7 +32,7 @@ internal sealed class PanelCanvas : Control public Func? CalibrationProvider { get; set; } private readonly HashSet _livePressed = new(); - private AxisOutputs? _axes; + private AxisReadout? _axes; public int? SelectedAddress { get; private set; } @@ -46,14 +46,16 @@ internal sealed class PanelCanvas : Control } /// - /// Show the latest calibrated axis values on the encoder-gauge strip. Arrives at + /// Show the latest calibrated readout on the encoder-gauge strip. Arrives at /// the analog poll rate (~18 Hz), so only the strip is repainted, and only when a /// value actually changed. /// - public void SetAxes(AxisOutputs axes) + public void SetAxes(AxisReadout axes) { - if (_axes is { } p && p.X == axes.X && p.Y == axes.Y && p.Z == axes.Z && - p.Rx == axes.Rx && p.Ry == axes.Ry && p.Rz == axes.Rz) + if (_axes is { } p && + p.Axes.X == axes.Axes.X && p.Axes.Y == axes.Axes.Y && p.Axes.Z == axes.Axes.Z && + p.Axes.Rx == axes.Axes.Rx && p.Axes.Ry == axes.Axes.Ry && p.Axes.Rz == axes.Axes.Rz && + p.LeftPedal == axes.LeftPedal && p.RightPedal == axes.RightPedal) return; _axes = axes; Invalidate(new Rectangle(0, 0, Width, PanelView.TopStrip)); diff --git a/src/RioJoy.Tray/Editor/PanelView.cs b/src/RioJoy.Tray/Editor/PanelView.cs index fc931fc..f842c6d 100644 --- a/src/RioJoy.Tray/Editor/PanelView.cs +++ b/src/RioJoy.Tray/Editor/PanelView.cs @@ -75,7 +75,7 @@ public static class PanelView Func function, Func lit, Func live, - AxisOutputs? axes, + AxisReadout? axes, int? selected) { g.Clear(Color.FromArgb(28, 28, 28)); @@ -152,7 +152,7 @@ public static class PanelView } } - private static void DrawEncoderStrip(Graphics g, Font font, AxisOutputs? axes) + private static void DrawEncoderStrip(Graphics g, Font font, AxisReadout? axes) { using var pen = new Pen(Color.FromArgb(120, 160, 120)); using var gauge = new SolidBrush(Color.FromArgb(20, 40, 20)); @@ -181,12 +181,13 @@ public static class PanelView g.FillRectangle(gauge, box); // Live values from the RIO's analog stream (null when the link is down). - // The gauges show the calibrated outputs — post invert/deadzone/mix — i.e. - // exactly what the virtual joystick receives. + // Z/Rz/X/Y show the virtual-joystick outputs; L and R show the calibrated + // pedal positions from before the ZR mix, which pins Rx/Ry to center — the + // pedal gauges must track the physical pedals either way. if (axes is { } a) { - // Z (throttle) and the L/R pedals (Rx/Ry) fill bottom-up. - foreach (var (box, value) in new[] { (zBox, a.Z), (lBox, a.Rx), (rBox, a.Ry) }) + // Z (throttle) and the L/R pedals fill bottom-up. + foreach (var (box, value) in new[] { (zBox, a.Axes.Z), (lBox, a.LeftPedal), (rBox, a.RightPedal) }) { int h = (int)Math.Round(box.Height * AxisGauges.Fraction(value)); if (h > 0) @@ -194,7 +195,7 @@ public static class PanelView } // Rz (mixed rudder) deflects left/right from the bar's center. - double d = AxisGauges.Deflection(a.Rz); + double d = AxisGauges.Deflection(a.Axes.Rz); int cx = rzBox.X + rzBox.Width / 2; int w = (int)Math.Round(rzBox.Width / 2.0 * Math.Abs(d)); if (w > 0) @@ -217,8 +218,8 @@ public static class PanelView // (0 = left/top, matching the HID axis convention). if (axes is { } live) { - int px = xyBox.X + (int)Math.Round(AxisGauges.Fraction(live.X) * (xyBox.Width - 1)); - int py = xyBox.Y + (int)Math.Round(AxisGauges.Fraction(live.Y) * (xyBox.Height - 1)); + int px = xyBox.X + (int)Math.Round(AxisGauges.Fraction(live.Axes.X) * (xyBox.Width - 1)); + int py = xyBox.Y + (int)Math.Round(AxisGauges.Fraction(live.Axes.Y) * (xyBox.Height - 1)); using var dot = new SolidBrush(Color.FromArgb(150, 230, 150)); g.FillEllipse(dot, px - 3, py - 3, 7, 7); } diff --git a/src/RioJoy.Tray/Editor/ProfileEditorForm.cs b/src/RioJoy.Tray/Editor/ProfileEditorForm.cs index b9b0344..25d185a 100644 --- a/src/RioJoy.Tray/Editor/ProfileEditorForm.cs +++ b/src/RioJoy.Tray/Editor/ProfileEditorForm.cs @@ -466,8 +466,8 @@ public sealed class ProfileEditorForm : Form public void ShowLiveActivity(int address, bool pressed) => RunOnUi(() => _canvas.SetLivePressed(address, pressed)); - /// Show the live calibrated axis values on the encoder gauges (serial thread). - public void ShowAxes(AxisOutputs axes) => + /// Show the live calibrated readout on the encoder gauges (serial thread). + public void ShowAxes(AxisReadout axes) => RunOnUi(() => _canvas.SetAxes(axes)); /// Show the firmware version from a RIO version reply (serial thread). diff --git a/tests/RioJoy.Core.Tests/Calibration/AxisCalibratorTests.cs b/tests/RioJoy.Core.Tests/Calibration/AxisCalibratorTests.cs index b8e1456..5eab45f 100644 --- a/tests/RioJoy.Core.Tests/Calibration/AxisCalibratorTests.cs +++ b/tests/RioJoy.Core.Tests/Calibration/AxisCalibratorTests.cs @@ -69,6 +69,26 @@ public class AxisCalibratorTests // 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 + + // The pedal readouts mirror Rx/Ry when there is no mix. + Assert.Equal(o.Rx, cal.LeftPedalOutput); + Assert.Equal(o.Ry, cal.RightPedalOutput); + } + + [Fact] + public void PedalOutputs_TrackPedals_EvenWithZrMix() + { + var cal = new AxisCalibrator(); // EnableZR default true + + cal.Update(Report(left: -100, right: -100)); + AxisOutputs o = cal.Update(Report(left: 0, right: 0)); + + // The mix centers Rx/Ry, but the pedal readouts still track the pedals + // (same math as the ZR-off Rx/Ry: 6400 for this deflection). + Assert.Equal(AxisOutputs.Center, o.Rx); + Assert.Equal(AxisOutputs.Center, o.Ry); + Assert.Equal(6400, cal.LeftPedalOutput); + Assert.Equal(6400, cal.RightPedalOutput); } // --- Joystick (X / Y) ---------------------------------------------------- diff --git a/tests/RioJoy.Core.Tests/RioRuntimeTests.cs b/tests/RioJoy.Core.Tests/RioRuntimeTests.cs index 9505f56..f606c03 100644 --- a/tests/RioJoy.Core.Tests/RioRuntimeTests.cs +++ b/tests/RioJoy.Core.Tests/RioRuntimeTests.cs @@ -1,5 +1,6 @@ using System.Diagnostics; using AxisOutputs = RioJoy.Core.Calibration.AxisOutputs; +using AxisReadout = RioJoy.Core.Calibration.AxisReadout; using RioJoy.Core; using RioJoy.Core.Mapping; using RioJoy.Core.Protocol; @@ -81,19 +82,22 @@ public class RioRuntimeTests var recorder = new RecordingSink(); using var runtime = new RioRuntime(link, new RioInputMap(), recorder, recorder); - AxisOutputs? seen = null; + AxisReadout? seen = null; runtime.AxesUpdated += axes => seen = axes; runtime.Start(); using var cts = new CancellationTokenSource(); Task run = link.RunAsync(cts.Token); - // All raw axes at zero → joystick X/Y centered, same values the sink receives. + // All raw axes at zero → joystick X/Y centered, same values the sink receives; + // both pedals at rest → pedal readouts empty. fake.Enqueue(PacketBuilder.Build(RioCommand.AnalogReply, new byte[10])); await WaitUntilAsync(() => seen is not null); - Assert.Equal(AxisOutputs.Center, seen!.Value.X); - Assert.Equal(AxisOutputs.Center, seen.Value.Y); + Assert.Equal(AxisOutputs.Center, seen!.Value.Axes.X); + Assert.Equal(AxisOutputs.Center, seen.Value.Axes.Y); + Assert.Equal(0, seen.Value.LeftPedal); + Assert.Equal(0, seen.Value.RightPedal); cts.Cancel(); await run;