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:
@@ -0,0 +1,21 @@
|
||||
using RioJoy.Core.Output;
|
||||
using Xunit;
|
||||
|
||||
namespace RioJoy.Core.Tests.Output;
|
||||
|
||||
public class SendInputSinkTests
|
||||
{
|
||||
[Fact]
|
||||
public void SendInput_DoesNotThrow_OnMarshalling()
|
||||
{
|
||||
var sink = new SendInputSink();
|
||||
|
||||
// A zero relative mouse move is a no-op, but it exercises the same SendInput
|
||||
// INPUT[] marshalling path that previously threw MarshalDirectiveException with
|
||||
// a Span<INPUT> parameter — which killed the serial read thread when a button
|
||||
// with output enabled was pressed.
|
||||
Exception? ex = Record.Exception(() => sink.MouseMove(0, 0));
|
||||
|
||||
Assert.Null(ex);
|
||||
}
|
||||
}
|
||||
@@ -72,6 +72,40 @@ public class RioRuntimeTests
|
||||
await run;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task LitLamps_Dimmed_OnFirstBoardReply_NotOnStart()
|
||||
{
|
||||
var fake = new FakeTransport();
|
||||
var link = new RioSerialLink(fake, new RioSerialLinkOptions { AutoPollAnalog = false });
|
||||
var recorder = new RecordingSink();
|
||||
|
||||
var map = new RioInputMap { [0x05] = new RioMapEntry(0x9001) }; // lit joystick button
|
||||
using var runtime = new RioRuntime(link, map, recorder, recorder);
|
||||
runtime.Start();
|
||||
|
||||
using var cts = new CancellationTokenSource();
|
||||
Task run = link.RunAsync(cts.Token);
|
||||
|
||||
// Nothing is sent on Start: lamp commands would be dropped during the board's
|
||||
// post-reset boot, so no write happens until the board first replies.
|
||||
await Assert.ThrowsAnyAsync<OperationCanceledException>(
|
||||
() => fake.NextWriteAsync(TimeSpan.FromMilliseconds(150)));
|
||||
|
||||
// A reply proves the board is alive → the lit lamp is dimmed.
|
||||
fake.Enqueue(PacketBuilder.Build(RioCommand.AnalogReply, new byte[10]));
|
||||
|
||||
bool dimmed = false;
|
||||
for (int i = 0; i < 8 && !dimmed; i++)
|
||||
{
|
||||
byte[] w = await fake.NextWriteAsync(TimeSpan.FromSeconds(2));
|
||||
dimmed = w.Length >= 3 && w[0] == (byte)RioCommand.LampRequest && w[1] == 0x05;
|
||||
}
|
||||
Assert.True(dimmed, "expected a LampRequest dimming address 0x05 after the first reply");
|
||||
|
||||
cts.Cancel();
|
||||
await run;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ButtonActivity_FiresForPressAndRelease_EvenWhenUnmapped()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user