Fix pedal gauges frozen under ZR mix: feed L/R from pre-mix pedal readouts
The editor's L/R gauges read Rx/Ry, but EnableZR (on in real profiles) folds the pedals into Rz and pins Rx/Ry to center, so the bars never moved with the physical pedals. AxisCalibrator now exposes LeftPedalOutput/RightPedalOutput (the calibrated pre-mix positions) and RioRuntime.AxesUpdated carries an AxisReadout (the six virtual axes + both pedals); the strip draws L/R from the pedal readouts, Z/Rz/X/Y from the axes. With ZR off the readouts equal Rx/Ry, so nothing changes there. Verified by screenshot: Rx/Ry centered while L=75% and R=25% render correctly. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+4
-3
@@ -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
|
||||
|
||||
@@ -70,6 +70,16 @@ public sealed class AxisCalibrator
|
||||
|
||||
private static int Clamp(int v) => RioJoy.Core.Compat.Net48Math.Clamp(v, 0, AxisOutputs.Max);
|
||||
|
||||
/// <summary>
|
||||
/// The calibrated left-pedal position from the last <see cref="Update"/>
|
||||
/// (<c>0..32766</c>) — always tracks the pedal, even when
|
||||
/// <see cref="AxisCalibrationConfig.EnableZR"/> centers <see cref="AxisOutputs.Rx"/>.
|
||||
/// </summary>
|
||||
public int LeftPedalOutput => Clamp(_leftPedalLast);
|
||||
|
||||
/// <summary>Right-pedal counterpart of <see cref="LeftPedalOutput"/>.</summary>
|
||||
public int RightPedalOutput => Clamp(_rightPedalLast);
|
||||
|
||||
private int Throttle(int throttle)
|
||||
{
|
||||
if (throttle < -RangeThrottle || throttle > RangeThrottle)
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
namespace RioJoy.Core.Calibration;
|
||||
|
||||
/// <summary>
|
||||
/// 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 (<see cref="AxisCalibrationConfig.EnableZR"/>)
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public readonly struct AxisReadout
|
||||
{
|
||||
/// <summary>The six virtual-joystick outputs (what the HID device receives).</summary>
|
||||
public AxisOutputs Axes { get; }
|
||||
|
||||
/// <summary>Calibrated left-pedal position (<c>0..32766</c>), before the ZR mix.</summary>
|
||||
public int LeftPedal { get; }
|
||||
|
||||
/// <summary>Calibrated right-pedal position (<c>0..32766</c>), before the ZR mix.</summary>
|
||||
public int RightPedal { get; }
|
||||
|
||||
public AxisReadout(AxisOutputs axes, int leftPedal, int rightPedal)
|
||||
{
|
||||
Axes = axes;
|
||||
LeftPedal = leftPedal;
|
||||
RightPedal = rightPedal;
|
||||
}
|
||||
}
|
||||
@@ -57,11 +57,11 @@ public sealed class RioRuntime : IRioCommandSink, IDisposable
|
||||
public event Action<int, bool>? ButtonActivity;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public event Action<AxisOutputs>? AxesUpdated;
|
||||
public event Action<AxisReadout>? AxesUpdated;
|
||||
|
||||
/// <summary>Raised with the firmware version when a <see cref="RioCommand.VersionReply"/> arrives.</summary>
|
||||
public event Action<VersionInfo>? 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)
|
||||
|
||||
@@ -32,7 +32,7 @@ internal sealed class PanelCanvas : Control
|
||||
public Func<JoyStickSetting, bool>? CalibrationProvider { get; set; }
|
||||
|
||||
private readonly HashSet<int> _livePressed = new();
|
||||
private AxisOutputs? _axes;
|
||||
private AxisReadout? _axes;
|
||||
|
||||
public int? SelectedAddress { get; private set; }
|
||||
|
||||
@@ -46,14 +46,16 @@ internal sealed class PanelCanvas : Control
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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));
|
||||
|
||||
@@ -75,7 +75,7 @@ public static class PanelView
|
||||
Func<int, string?> function,
|
||||
Func<int, bool> lit,
|
||||
Func<int, bool> 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);
|
||||
}
|
||||
|
||||
@@ -466,8 +466,8 @@ public sealed class ProfileEditorForm : Form
|
||||
public void ShowLiveActivity(int address, bool pressed) =>
|
||||
RunOnUi(() => _canvas.SetLivePressed(address, pressed));
|
||||
|
||||
/// <summary>Show the live calibrated axis values on the encoder gauges (serial thread).</summary>
|
||||
public void ShowAxes(AxisOutputs axes) =>
|
||||
/// <summary>Show the live calibrated readout on the encoder gauges (serial thread).</summary>
|
||||
public void ShowAxes(AxisReadout axes) =>
|
||||
RunOnUi(() => _canvas.SetAxes(axes));
|
||||
|
||||
/// <summary>Show the firmware version from a RIO version reply (serial thread).</summary>
|
||||
|
||||
@@ -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) ----------------------------------------------------
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user