Fix SendInput crash; dim lit lamps after reset; harden the link
SendInput declared its parameter as Span<INPUT>, which cannot be marshalled (MarshalDirectiveException) — the first keyboard/mouse output threw on the serial read thread and killed the link. Pass a blittable INPUT[] instead; add a regression test. Also: defend the runtime so a faulty output sink can never tear down the serial link (output is best-effort), and dim lit lamps on the first reply from the board rather than in Start() — lamp commands sent in the first ms after the DTR reset (on port open) are dropped before the board has booted. Verified end-to-end against the partial RIO on COM1 (keypad types to the PC, joystick feeds, comms stay up; lit MFD buttons come up dim). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -70,12 +70,8 @@ public sealed class SendInputSink : IInputSink
|
||||
Send(input);
|
||||
}
|
||||
|
||||
private static void Send(INPUT input)
|
||||
{
|
||||
Span<INPUT> inputs = stackalloc INPUT[1];
|
||||
inputs[0] = input;
|
||||
SendInput(1, inputs, Marshal.SizeOf<INPUT>());
|
||||
}
|
||||
private static void Send(INPUT input) =>
|
||||
SendInput(1, new[] { input }, Marshal.SizeOf<INPUT>());
|
||||
|
||||
// --- Win32 interop --------------------------------------------------------
|
||||
|
||||
@@ -130,7 +126,7 @@ public sealed class SendInputSink : IInputSink
|
||||
}
|
||||
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
private static extern uint SendInput(uint nInputs, Span<INPUT> pInputs, int cbSize);
|
||||
private static extern uint SendInput(uint nInputs, INPUT[] pInputs, int cbSize);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern uint MapVirtualKey(uint uCode, uint uMapType);
|
||||
|
||||
@@ -20,6 +20,7 @@ public sealed class RioRuntime : IRioCommandSink, IDisposable
|
||||
private readonly InputRouter _router;
|
||||
private readonly ILampSink _lamp;
|
||||
private bool _started;
|
||||
private bool _lampsInitialized;
|
||||
|
||||
public RioRuntime(
|
||||
RioSerialLink link,
|
||||
@@ -61,18 +62,23 @@ public sealed class RioRuntime : IRioCommandSink, IDisposable
|
||||
/// <summary>Raised for each board/lamp status item in a <see cref="RioCommand.CheckReply"/>.</summary>
|
||||
public event Action<CheckStatus>? CheckReceived;
|
||||
|
||||
/// <summary>Subscribe to the link and set all lamps to their idle state.</summary>
|
||||
/// <summary>
|
||||
/// Subscribe to the link. Lit lamps are dimmed on the first reply from the board
|
||||
/// (<see cref="EnsureLampsInitialized"/>) rather than here: lamp commands sent in
|
||||
/// the first milliseconds after the DTR reset (on port open) are dropped before
|
||||
/// the board finishes booting, so the idle-dim state never takes.
|
||||
/// </summary>
|
||||
public void Start()
|
||||
{
|
||||
if (_started)
|
||||
return;
|
||||
_started = true;
|
||||
_lampsInitialized = false;
|
||||
|
||||
_link.PacketReceived += OnPacket;
|
||||
_link.AnalogReceived += OnAnalog;
|
||||
_link.VersionReceived += OnVersion;
|
||||
_link.CheckReceived += OnCheck;
|
||||
_router.InitializeLamps();
|
||||
}
|
||||
|
||||
/// <summary>Unsubscribe from the link.</summary>
|
||||
@@ -97,19 +103,37 @@ public sealed class RioRuntime : IRioCommandSink, IDisposable
|
||||
/// <summary>Invoke a RIO command directly (e.g. from the tray menu).</summary>
|
||||
public void Trigger(RioCommandCode command) => ((IRioCommandSink)this).Execute(command);
|
||||
|
||||
// Once the board replies (alive after the reset), set every lit button to dim.
|
||||
private void EnsureLampsInitialized()
|
||||
{
|
||||
if (_lampsInitialized)
|
||||
return;
|
||||
_lampsInitialized = true;
|
||||
_router.InitializeLamps();
|
||||
}
|
||||
|
||||
private void OnAnalog(AnalogReport report)
|
||||
{
|
||||
EnsureLampsInitialized();
|
||||
AxisOutputs axes = _calibrator.Update(report);
|
||||
_joystick.SetAxis(JoyAxis.X, axes.X);
|
||||
_joystick.SetAxis(JoyAxis.Y, axes.Y);
|
||||
_joystick.SetAxis(JoyAxis.Z, axes.Z);
|
||||
_joystick.SetAxis(JoyAxis.Rx, axes.Rx);
|
||||
_joystick.SetAxis(JoyAxis.Ry, axes.Ry);
|
||||
_joystick.SetAxis(JoyAxis.Rz, axes.Rz);
|
||||
try
|
||||
{
|
||||
_joystick.SetAxis(JoyAxis.X, axes.X);
|
||||
_joystick.SetAxis(JoyAxis.Y, axes.Y);
|
||||
_joystick.SetAxis(JoyAxis.Z, axes.Z);
|
||||
_joystick.SetAxis(JoyAxis.Rx, axes.Rx);
|
||||
_joystick.SetAxis(JoyAxis.Ry, axes.Ry);
|
||||
_joystick.SetAxis(JoyAxis.Rz, axes.Rz);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Output is best-effort: a faulty joystick sink must not kill the link.
|
||||
}
|
||||
}
|
||||
|
||||
private void OnPacket(RioPacket packet)
|
||||
{
|
||||
EnsureLampsInitialized();
|
||||
ReadOnlySpan<byte> p = packet.Payload.Span;
|
||||
switch (packet.Command)
|
||||
{
|
||||
@@ -131,10 +155,17 @@ public sealed class RioRuntime : IRioCommandSink, IDisposable
|
||||
// Route the event through the input pipeline and notify any live-activity listener.
|
||||
private void Activity(int address, bool pressed)
|
||||
{
|
||||
if (pressed)
|
||||
_router.Press(address);
|
||||
else
|
||||
_router.Release(address);
|
||||
try
|
||||
{
|
||||
if (pressed)
|
||||
_router.Press(address);
|
||||
else
|
||||
_router.Release(address);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Output is best-effort: a faulty sink must never kill the serial link.
|
||||
}
|
||||
|
||||
// Light the button on the RIO even when it is unmapped (the router only lamps
|
||||
// mapped lamp-buttons), so the editor's check-function workflow always responds.
|
||||
|
||||
Reference in New Issue
Block a user