diff --git a/.gitignore b/.gitignore
index 98b15e5..0745f08 100644
--- a/.gitignore
+++ b/.gitignore
@@ -8,4 +8,7 @@
*.suo
# Test results
-TestResults/
\ No newline at end of file
+TestResults/
+
+# Distributable archives (built locally, not tracked)
+dist/
\ No newline at end of file
diff --git a/README.md b/README.md
index 3e8ddbe..5295538 100644
--- a/README.md
+++ b/README.md
@@ -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.
diff --git a/src/VRio.App/MainForm.cs b/src/VRio.App/MainForm.cs
index e2100c7..014f036 100644
--- a/src/VRio.App/MainForm.cs
+++ b/src/VRio.App/MainForm.cs
@@ -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)
diff --git a/src/VRio.App/PanelCanvas.cs b/src/VRio.App/PanelCanvas.cs
index 19ffa23..aabae64 100644
--- a/src/VRio.App/PanelCanvas.cs
+++ b/src/VRio.App/PanelCanvas.cs
@@ -10,7 +10,11 @@ namespace VRio.App;
/// here the cells are the physical controls: 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 (),
+/// 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.
///
/// Left-click = momentary press (release on mouse-up). Right-click =
/// latch the button down / release it (handy for testing holds).
@@ -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 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
}
+ /// Raw axis value → 0..1 across its ±extent travel, clamped.
+ 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);
}
}
diff --git a/src/VRio.Core/Device/RioAddressSpace.cs b/src/VRio.Core/Device/RioAddressSpace.cs
index cf801a4..f43bb31 100644
--- a/src/VRio.Core/Device/RioAddressSpace.cs
+++ b/src/VRio.Core/Device/RioAddressSpace.cs
@@ -78,3 +78,42 @@ public enum RioAxis
JoystickY = 3,
JoystickX = 4,
}
+
+///
+/// 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 AxisCalibrator,
+/// ported from riovjoy2.cpp):
+///
+/// - 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
+/// negative on the wire.
+/// - 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.
+/// - The joystick is self-centering; RIOJoy auto-ranges from observed
+/// min/max, seeded at ±80 either side of center.
+///
+/// Values a vRIO front-end produces should stay inside these windows to look
+/// like real hardware.
+///
+public static class RioAxisRange
+{
+ /// Throttle raw value at full forward travel (rest 0; travel runs negative).
+ public const int ThrottleFull = -800;
+
+ /// Pedal raw value at a full press (rest 0; spring-loaded).
+ public const int PedalFull = 500;
+
+ /// Joystick full deflection either side of its 0 center.
+ public const int JoystickExtent = 80;
+
+ /// Raw value at full travel from rest — the sign is the wire direction.
+ public static int Full(RioAxis axis) => axis switch
+ {
+ RioAxis.Throttle => ThrottleFull,
+ RioAxis.LeftPedal or RioAxis.RightPedal => PedalFull,
+ _ => JoystickExtent,
+ };
+}