Realistic axis travel, wire-log fixes, compact layout
- Axes now span the hardware windows from RIOJoy's calibrator instead of the full 14-bit wire range: throttle rests at 0 and travels to -800 (forward runs negative, matching the ratchet math), spring-loaded pedals rest at 0 and press to +500, stick covers +/-80 around center (new RioAxisRange in VRio.Core documents the provenance). - Stick X sign flipped: RIOJoy maps negative samples to the high half of its output axis, so dragging right now lowers raw X. - Removed the Rz mix bar - the real RIO has no such indicator. - Wire log: newest entries on top with the bound trimming the oldest lines, and fixed the anchoring bug that let the box grow past the window edge (the panel got its height only after children snapshotted their anchors), which also hid the Clear log button. - Tighter layout: axis readout sits under the status lines, encoder strip shrunk to fit the gauges, and the window sizes itself to the canvas + control strip. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+4
-1
@@ -8,4 +8,7 @@
|
||||
*.suo
|
||||
|
||||
# Test results
|
||||
TestResults/
|
||||
TestResults/
|
||||
|
||||
# Distributable archives (built locally, not tracked)
|
||||
dist/
|
||||
@@ -15,6 +15,11 @@ controls:
|
||||
or `KeyPressed`/`Released` on the wire). **Right-click** — latch it down.
|
||||
- **Drag** the X/Y box and the Z / L / R gauges — the five analog axes,
|
||||
returned by the next `AnalogReply` (14-bit signed, 7-bit-pair packed).
|
||||
Gauges span each axis' realistic hardware travel, not the full 14-bit
|
||||
range: Z/L/R rest at raw 0 at the gauge bottom and travel to −800
|
||||
(throttle — forward travel runs negative, matching RIOJoy's ratchet) or
|
||||
+500 (spring-loaded pedals), and the stick covers ±80 around center —
|
||||
the windows RIOJoy's calibrator expects.
|
||||
- Cells shade to the **lamp state the host commands** (`LampRequest`:
|
||||
off / dim / bright, with slow/med/fast flash), so RIOJoy's press-feedback
|
||||
lights the on-screen panel just like the real buttons.
|
||||
|
||||
+27
-10
@@ -90,7 +90,9 @@ internal sealed class MainForm : Form
|
||||
public MainForm()
|
||||
{
|
||||
Text = "vRIO — Virtual RIO cockpit device";
|
||||
ClientSize = new Size(1150, 800);
|
||||
// Fit the window to its content: the cockpit canvas plus the 330px
|
||||
// control strip, with just enough height for the strip's log area.
|
||||
ClientSize = new Size(_canvas.Width + 332, Math.Max(_canvas.Height, 640));
|
||||
MinimumSize = new Size(1000, 620);
|
||||
StartPosition = FormStartPosition.CenterScreen;
|
||||
|
||||
@@ -113,11 +115,11 @@ internal sealed class MainForm : Form
|
||||
// Device / service events arrive on worker threads; marshal to the UI.
|
||||
_device.LampChanged += (_, _) => RunOnUi(_canvas.Invalidate);
|
||||
_device.AxesChanged += () => RunOnUi(_canvas.Invalidate);
|
||||
_device.Logged += line => RunOnUi(() => AppendLog(line));
|
||||
_service.Logged += line => RunOnUi(() => AppendLog(line));
|
||||
_device.Logged += line => RunOnUi(() => PrependLog(line));
|
||||
_service.Logged += line => RunOnUi(() => PrependLog(line));
|
||||
_service.ConnectionChanged += open => RunOnUi(() => OnConnectionChanged(open));
|
||||
_service.HostHandshake += high => RunOnUi(() =>
|
||||
AppendLog(high ? "Host raised DTR (board-reset handshake)" : "Host dropped DTR"));
|
||||
PrependLog(high ? "Host raised DTR (board-reset handshake)" : "Host dropped DTR"));
|
||||
|
||||
_rescan.Click += (_, _) => RefreshPorts();
|
||||
_openClose.Click += (_, _) => ToggleOpen();
|
||||
@@ -155,7 +157,7 @@ internal sealed class MainForm : Form
|
||||
|
||||
RefreshPorts();
|
||||
UpdateStatus();
|
||||
AppendLog("vRIO ready. Open a COM port, then point RIOJoy at the other end of the pair.");
|
||||
PrependLog("vRIO ready. Open a COM port, then point RIOJoy at the other end of the pair.");
|
||||
}
|
||||
|
||||
private Panel BuildControlStrip()
|
||||
@@ -164,6 +166,11 @@ internal sealed class MainForm : Form
|
||||
{
|
||||
Dock = DockStyle.Right,
|
||||
Width = 330,
|
||||
// Height must be set before the anchored children are added:
|
||||
// Anchor offsets are captured against the parent's current size,
|
||||
// and the 100px default would pin the log box's bottom far below
|
||||
// the panel, making it grow past the window instead of scrolling.
|
||||
Height = ClientSize.Height,
|
||||
Padding = new Padding(8),
|
||||
BorderStyle = BorderStyle.FixedSingle,
|
||||
};
|
||||
@@ -265,13 +272,23 @@ internal sealed class MainForm : Form
|
||||
: "PORT CLOSED\nOpen a COM port to go live.";
|
||||
}
|
||||
|
||||
private void AppendLog(string line)
|
||||
private void PrependLog(string line)
|
||||
{
|
||||
// Bound the log so an all-day session doesn't grow without limit.
|
||||
if (_logBox.TextLength > 120_000)
|
||||
_logBox.Text = _logBox.Text.Substring(_logBox.TextLength - 60_000);
|
||||
// Newest entry on top; older lines flow downward and eventually fall
|
||||
// off the bounded end, so the visible top is always current.
|
||||
string text = $"[{_clock.Elapsed.TotalSeconds,8:F2}s] {line}{Environment.NewLine}" + _logBox.Text;
|
||||
|
||||
_logBox.AppendText($"[{_clock.Elapsed.TotalSeconds,8:F2}s] {line}{Environment.NewLine}");
|
||||
// Bound the log so an all-day session doesn't grow without limit;
|
||||
// cut at a line boundary so the tail isn't half an entry.
|
||||
if (text.Length > 120_000)
|
||||
{
|
||||
int cut = text.LastIndexOf(Environment.NewLine, 60_000, StringComparison.Ordinal);
|
||||
text = text.Substring(0, cut < 0 ? 60_000 : cut + Environment.NewLine.Length);
|
||||
}
|
||||
|
||||
_logBox.Text = text;
|
||||
_logBox.SelectionStart = 0;
|
||||
_logBox.ScrollToCaret();
|
||||
}
|
||||
|
||||
private void RunOnUi(Action action)
|
||||
|
||||
+47
-34
@@ -10,7 +10,11 @@ namespace VRio.App;
|
||||
/// here the cells are the <em>physical controls</em>: pressing one emits a
|
||||
/// button/keypad packet on the wire, and the fill shade tracks the lamp state
|
||||
/// the host last commanded (including flash modes). The encoder strip is live
|
||||
/// too — drag the X/Y box and the Z/L/R gauges to move the analog axes.
|
||||
/// too — drag the X/Y box and the Z/L/R gauges to move the analog axes. The
|
||||
/// gauges span each axis' realistic hardware travel (<see cref="RioAxisRange"/>),
|
||||
/// not the full 14-bit wire range: Z/L/R rest at raw 0 at the gauge bottom and
|
||||
/// fill toward full travel (negative on the wire for the throttle, positive
|
||||
/// for the pedals), and the stick covers ±80 around its center.
|
||||
///
|
||||
/// <para>Left-click = momentary press (release on mouse-up). Right-click =
|
||||
/// latch the button down / release it (handy for testing holds).</para>
|
||||
@@ -20,12 +24,12 @@ internal sealed class PanelCanvas : Control
|
||||
// Cell geometry — identical to RIOJoy's PanelView so the two panels align.
|
||||
private const int CellW = 66;
|
||||
private const int CellH = 34;
|
||||
private const int TopStrip = 108;
|
||||
private const int TopStrip = 78; // encoder strip: gauges end at 74, small gap to the grid
|
||||
|
||||
private static readonly IReadOnlyList<PanelButton> AllButtons = CockpitLayout.Buttons();
|
||||
|
||||
// Encoder-strip geometry (same placement as RIOJoy's PanelView): Z gauge,
|
||||
// the pedals' "U" (L / R arms + Rz bar), and the X/Y stick box.
|
||||
// the pedal gauges L / R, and the X/Y stick box.
|
||||
private const int StripTop = 6;
|
||||
private const int StripH = 68;
|
||||
private const int Bar = 30;
|
||||
@@ -33,7 +37,6 @@ internal sealed class PanelCanvas : Control
|
||||
private static readonly Rectangle BoxZ = new(BaseX, StripTop, Bar, StripH);
|
||||
private static readonly Rectangle BoxL = new(BaseX + 34, StripTop, Bar, StripH);
|
||||
private static readonly Rectangle BoxR = new(BaseX + 34 + 82 - Bar, StripTop, Bar, StripH);
|
||||
private static readonly Rectangle BoxRz = new(BaseX + 34, StripTop + StripH, 82, Bar);
|
||||
private static readonly Rectangle BoxXY = new(BaseX + 120, StripTop, 80, StripH);
|
||||
|
||||
private readonly System.Windows.Forms.Timer _flashTimer = new() { Interval = 100 };
|
||||
@@ -247,19 +250,11 @@ internal sealed class PanelCanvas : Control
|
||||
|
||||
short Axis(RioAxis a) => AxisProvider?.Invoke(a) ?? (short)0;
|
||||
|
||||
// Vertical gauges: Z (throttle) and the pedal arms L / R.
|
||||
DrawVGauge(g, pen, bg, fillBrush, labelFont, BoxZ, "Z", Axis(RioAxis.Throttle));
|
||||
DrawVGauge(g, pen, bg, fillBrush, labelFont, BoxL, "L", Axis(RioAxis.LeftPedal));
|
||||
DrawVGauge(g, pen, bg, fillBrush, labelFont, BoxR, "R", Axis(RioAxis.RightPedal));
|
||||
|
||||
// Rz bar: the PC-side rudder mix ((R − L) / 2) — display only.
|
||||
g.FillRectangle(bg, BoxRz);
|
||||
g.DrawRectangle(pen, BoxRz);
|
||||
int mix = (Axis(RioAxis.RightPedal) - Axis(RioAxis.LeftPedal)) / 2;
|
||||
int mx = BoxRz.X + (int)((mix - (float)AnalogCodec.Min) / (AnalogCodec.Max - AnalogCodec.Min) * BoxRz.Width);
|
||||
g.FillRectangle(fillBrush, Math.Max(BoxRz.X + 1, Math.Min(mx - 2, BoxRz.Right - 4)), BoxRz.Y + 1, 3, BoxRz.Height - 2);
|
||||
TextRenderer.DrawText(g, "Rz", labelFont, BoxRz, Color.Gainsboro,
|
||||
TextFormatFlags.HorizontalCenter | TextFormatFlags.VerticalCenter);
|
||||
// Vertical gauges: Z (throttle) and the pedals L / R. Each rests at
|
||||
// raw 0 (gauge bottom) and fills toward full travel at the top.
|
||||
DrawVGauge(g, pen, bg, fillBrush, labelFont, BoxZ, "Z", Axis(RioAxis.Throttle), RioAxisRange.ThrottleFull);
|
||||
DrawVGauge(g, pen, bg, fillBrush, labelFont, BoxL, "L", Axis(RioAxis.LeftPedal), RioAxisRange.PedalFull);
|
||||
DrawVGauge(g, pen, bg, fillBrush, labelFont, BoxR, "R", Axis(RioAxis.RightPedal), RioAxisRange.PedalFull);
|
||||
|
||||
// X/Y stick box with a crosshair at the current position.
|
||||
g.FillRectangle(bg, BoxXY);
|
||||
@@ -275,31 +270,39 @@ internal sealed class PanelCanvas : Control
|
||||
new Rectangle(BoxXY.X, BoxXY.Bottom - 16, BoxXY.Width, 14), Color.Gainsboro,
|
||||
TextFormatFlags.HorizontalCenter | TextFormatFlags.Bottom);
|
||||
|
||||
// The mockup's green status/instruction area, right of the gauges, with a
|
||||
// live axis readout on its last line (painted per frame, so drags track).
|
||||
// The mockup's green status/instruction area, right of the gauges; the
|
||||
// live axis readout sits directly under the status lines (painted per
|
||||
// frame, so drags track).
|
||||
var statusRect = new Rectangle(BoxXY.Right + 16, StripTop, Width - BoxXY.Right - 24, TopStrip - StripTop - 6);
|
||||
if (statusRect.Width > 60)
|
||||
{
|
||||
using var statusFont = new Font("Consolas", 8f);
|
||||
var green = Color.FromArgb(120, 220, 120);
|
||||
int readoutTop = statusRect.Y;
|
||||
if (_statusText.Length > 0)
|
||||
{
|
||||
TextRenderer.DrawText(g, _statusText, statusFont, statusRect, green,
|
||||
TextFormatFlags.Left | TextFormatFlags.Top | TextFormatFlags.WordBreak);
|
||||
readoutTop += TextRenderer.MeasureText(g, _statusText, statusFont,
|
||||
new Size(statusRect.Width, 0), TextFormatFlags.WordBreak).Height + 2;
|
||||
}
|
||||
|
||||
string readout =
|
||||
$"Z {Axis(RioAxis.Throttle),6} L {Axis(RioAxis.LeftPedal),6} R {Axis(RioAxis.RightPedal),6} " +
|
||||
$"X {Axis(RioAxis.JoystickX),6} Y {Axis(RioAxis.JoystickY),6}";
|
||||
TextRenderer.DrawText(g, readout, statusFont, statusRect, green,
|
||||
TextFormatFlags.Left | TextFormatFlags.Bottom | TextFormatFlags.SingleLine);
|
||||
$"Z {Axis(RioAxis.Throttle),4} L {Axis(RioAxis.LeftPedal),3} R {Axis(RioAxis.RightPedal),3} " +
|
||||
$"X {Axis(RioAxis.JoystickX),3} Y {Axis(RioAxis.JoystickY),3}";
|
||||
TextRenderer.DrawText(g, readout, statusFont,
|
||||
new Rectangle(statusRect.X, readoutTop, statusRect.Width, statusRect.Bottom - readoutTop), green,
|
||||
TextFormatFlags.Left | TextFormatFlags.Top | TextFormatFlags.SingleLine);
|
||||
}
|
||||
}
|
||||
|
||||
private static void DrawVGauge(Graphics g, Pen pen, Brush bg, Brush fill, Font labelFont,
|
||||
Rectangle box, string label, short value)
|
||||
Rectangle box, string label, short value, int full)
|
||||
{
|
||||
g.FillRectangle(bg, box);
|
||||
// Fill upward from the bottom, proportional to the normalized value.
|
||||
float norm = (value - (float)AnalogCodec.Min) / (AnalogCodec.Max - AnalogCodec.Min);
|
||||
// Fill upward from the raw-0 rest position at the bottom, proportional
|
||||
// to how far the control has travelled toward `full` (either sign).
|
||||
float norm = Math.Max(0f, Math.Min(1f, value / (float)full));
|
||||
int h = (int)(norm * (box.Height - 2));
|
||||
if (h > 0)
|
||||
g.FillRectangle(fill, box.X + 1, box.Bottom - 1 - h, box.Width - 2, h);
|
||||
@@ -310,13 +313,20 @@ internal sealed class PanelCanvas : Control
|
||||
|
||||
private static PointF StickToPoint(short x, short y)
|
||||
{
|
||||
float nx = (x - (float)AnalogCodec.Min) / (AnalogCodec.Max - AnalogCodec.Min);
|
||||
float ny = (y - (float)AnalogCodec.Min) / (AnalogCodec.Max - AnalogCodec.Min);
|
||||
// Raw X runs opposite the screen direction: stick right = negative on
|
||||
// the wire (RIOJoy's calibrator maps negative samples to the high half
|
||||
// of its output axis), so mirror the value for display.
|
||||
float nx = Norm(-x, RioAxisRange.JoystickExtent);
|
||||
float ny = Norm(y, RioAxisRange.JoystickExtent);
|
||||
return new PointF(
|
||||
BoxXY.X + 1 + nx * (BoxXY.Width - 2),
|
||||
BoxXY.Bottom - 1 - ny * (BoxXY.Height - 2)); // up = positive Y
|
||||
}
|
||||
|
||||
/// <summary>Raw axis value → 0..1 across its ±extent travel, clamped.</summary>
|
||||
private static float Norm(int value, int extent) =>
|
||||
Math.Max(0f, Math.Min(1f, (value + extent) / (2f * extent)));
|
||||
|
||||
// ---- Interaction -------------------------------------------------------
|
||||
|
||||
protected override void OnMouseDown(MouseEventArgs e)
|
||||
@@ -444,8 +454,10 @@ internal sealed class PanelCanvas : Control
|
||||
|
||||
private void UpdateStick(Point p)
|
||||
{
|
||||
short x = FromNorm((p.X - BoxXY.X) / (float)BoxXY.Width);
|
||||
short y = FromNorm((BoxXY.Bottom - p.Y) / (float)BoxXY.Height);
|
||||
// Dragging right lowers raw X — the wire sign is opposite the screen
|
||||
// direction (see StickToPoint).
|
||||
short x = FromNorm((BoxXY.Right - p.X) / (float)BoxXY.Width, RioAxisRange.JoystickExtent);
|
||||
short y = FromNorm((BoxXY.Bottom - p.Y) / (float)BoxXY.Height, RioAxisRange.JoystickExtent);
|
||||
AxisMoved?.Invoke(RioAxis.JoystickX, x);
|
||||
AxisMoved?.Invoke(RioAxis.JoystickY, y);
|
||||
}
|
||||
@@ -458,13 +470,14 @@ internal sealed class PanelCanvas : Control
|
||||
RioAxis.LeftPedal => BoxL,
|
||||
_ => BoxR,
|
||||
};
|
||||
short v = FromNorm((box.Bottom - p.Y) / (float)box.Height);
|
||||
AxisMoved?.Invoke(axis, v);
|
||||
// Gauge bottom = rest (raw 0), top = full travel (sign per axis).
|
||||
float norm = Math.Max(0f, Math.Min(1f, (box.Bottom - p.Y) / (float)box.Height));
|
||||
AxisMoved?.Invoke(axis, (short)(norm * RioAxisRange.Full(axis)));
|
||||
}
|
||||
|
||||
private static short FromNorm(float norm)
|
||||
private static short FromNorm(float norm, int extent)
|
||||
{
|
||||
norm = Math.Max(0f, Math.Min(1f, norm));
|
||||
return (short)(AnalogCodec.Min + norm * (AnalogCodec.Max - AnalogCodec.Min));
|
||||
return (short)(-extent + norm * 2 * extent);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,3 +78,42 @@ public enum RioAxis
|
||||
JoystickY = 3,
|
||||
JoystickX = 4,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Realistic travel of each axis on the physical cockpit, measured from the
|
||||
/// rest position (raw 0). The wire format can carry 14-bit signed values, but
|
||||
/// the hardware only uses a small slice of it, and each control has a
|
||||
/// direction — from RIOJoy's calibrator (RioJoy.Core <c>AxisCalibrator</c>,
|
||||
/// ported from riovjoy2.cpp):
|
||||
/// <list type="bullet">
|
||||
/// <item>The throttle lever's startup position is unknown, so RIOJoy
|
||||
/// ratchets its start from the highest sample seen and measures forward
|
||||
/// travel as start − sample, up to 800 counts — forward travel runs
|
||||
/// <em>negative</em> on the wire.</item>
|
||||
/// <item>The pedals are spring-loaded with a predictable rest; RIOJoy takes
|
||||
/// the lowest sample as rest and a press raises the value, up to 500.</item>
|
||||
/// <item>The joystick is self-centering; RIOJoy auto-ranges from observed
|
||||
/// min/max, seeded at ±80 either side of center.</item>
|
||||
/// </list>
|
||||
/// Values a vRIO front-end produces should stay inside these windows to look
|
||||
/// like real hardware.
|
||||
/// </summary>
|
||||
public static class RioAxisRange
|
||||
{
|
||||
/// <summary>Throttle raw value at full forward travel (rest 0; travel runs negative).</summary>
|
||||
public const int ThrottleFull = -800;
|
||||
|
||||
/// <summary>Pedal raw value at a full press (rest 0; spring-loaded).</summary>
|
||||
public const int PedalFull = 500;
|
||||
|
||||
/// <summary>Joystick full deflection either side of its 0 center.</summary>
|
||||
public const int JoystickExtent = 80;
|
||||
|
||||
/// <summary>Raw value at full travel from rest — the sign is the wire direction.</summary>
|
||||
public static int Full(RioAxis axis) => axis switch
|
||||
{
|
||||
RioAxis.Throttle => ThrottleFull,
|
||||
RioAxis.LeftPedal or RioAxis.RightPedal => PedalFull,
|
||||
_ => JoystickExtent,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user