diff --git a/README.md b/README.md
index 9107596..7d90502 100644
--- a/README.md
+++ b/README.md
@@ -50,11 +50,13 @@ device behavior grounded in the **real v4.2 firmware dump**
9600 8N1), never closer. A virtual null-modem has no UART, so unpaced
writes would land at the host in microsecond bursts no real board could
produce; vRIO's writer thread schedules each byte against a monotonic
- slot deadline instead, so e.g. the 45-byte CheckRequest response takes
- the same ~47 ms it takes real hardware.
-- `CheckRequest` → one `BoardOk` CheckReply per board (the 11 boards from the
- legacy firmware's table). `VersionRequest` → configurable version,
- default **4.2**.
+ slot deadline instead, so e.g. the 51-byte CheckRequest response takes
+ the same ~53 ms it takes real hardware.
+- `CheckRequest` → the real board's init handshake: `TestModeChange` **enter**,
+ one `BoardOk` CheckReply per board (the 11 boards from the legacy firmware's
+ table), then `TestModeChange` **exit**. Hosts wait (≤5 s per step) on both
+ test-mode packets and send nothing while test mode is active, so the exit is
+ mandatory. `VersionRequest` → configurable version, default **4.2**.
- `ResetRequest` re-zeroes the targeted axis (or all).
- A NAK re-sends the last event up to **4 times**, then gives up with a
RESTART byte — the real board's retry budget.
diff --git a/src/VRio.Core/Device/VRioDevice.cs b/src/VRio.Core/Device/VRioDevice.cs
index 9fdbdb6..b2c4f1c 100644
--- a/src/VRio.Core/Device/VRioDevice.cs
+++ b/src/VRio.Core/Device/VRioDevice.cs
@@ -13,7 +13,9 @@ namespace VRio.Core.Device;
/// Wire behavior (mirroring what RIOJoy expects from the real board):
///
/// - ACKs every well-formed inbound packet, NAKs one with a bad checksum.
-/// - CheckRequest → a BoardOk CheckReply per known board.
+/// - CheckRequest → TestModeChange ENTER, a BoardOk CheckReply per known
+/// board (the self-test stream), then TestModeChange EXIT — the init
+/// handshake the real board performs and the game waits (≤5s per step) on.
/// - VersionRequest → VersionReply with the configured firmware version
/// (default 4.2, matching the real board's dumped EPROM).
/// - AnalogRequest → AnalogReply with the current five axis values.
@@ -239,9 +241,14 @@ public sealed class VRioDevice
switch (packet.Command)
{
case RioCommand.CheckRequest:
- Logged?.Invoke("RX CheckRequest → all boards OK");
+ // Init handshake (verified against a real v4.2 board tap): the
+ // host waits ≤5s for TestModeChange ENTER before anything else,
+ // and sends no requests at all until the matching EXIT arrives.
+ Logged?.Invoke("RX CheckRequest → test mode enter, all boards OK, exit");
+ Send(PacketBuilder.TestModeChange(1));
foreach ((byte number, string _) in RioAddressSpace.Boards)
Send(PacketBuilder.CheckReply(RioStatusType.BoardOk, number));
+ Send(PacketBuilder.TestModeChange(0));
break;
case RioCommand.VersionRequest:
diff --git a/tests/VRio.Core.Tests/VRioDeviceTests.cs b/tests/VRio.Core.Tests/VRioDeviceTests.cs
index 28ac905..a98688b 100644
--- a/tests/VRio.Core.Tests/VRioDeviceTests.cs
+++ b/tests/VRio.Core.Tests/VRioDeviceTests.cs
@@ -79,22 +79,35 @@ public class VRioDeviceTests
}
[Fact]
- public void CheckRequest_reports_every_board_ok()
+ public void CheckRequest_enters_test_mode_reports_boards_then_exits()
{
var device = new VRioDevice();
var wire = new Wire(device);
Send(device, PacketBuilder.Build(RioCommand.CheckRequest));
- Assert.Equal(RioAddressSpace.Boards.Count, wire.Packets.Count);
- Assert.All(wire.Packets, p =>
+ // The init handshake: TestModeChange ENTER, one CheckReply per board,
+ // TestModeChange EXIT. The game waits on both test-mode packets and
+ // stays mute forever if the EXIT never arrives.
+ Assert.Equal(RioAddressSpace.Boards.Count + 2, wire.Packets.Count);
+
+ RioPacket enter = wire.Packets[0];
+ Assert.Equal(RioCommand.TestModeChange, enter.Command);
+ Assert.Equal(new byte[] { 1 }, enter.Payload);
+
+ RioPacket exit = wire.Packets[wire.Packets.Count - 1];
+ Assert.Equal(RioCommand.TestModeChange, exit.Command);
+ Assert.Equal(new byte[] { 0 }, exit.Payload);
+
+ var checks = wire.Packets.GetRange(1, RioAddressSpace.Boards.Count);
+ Assert.All(checks, p =>
{
Assert.Equal(RioCommand.CheckReply, p.Command);
Assert.Equal((byte)RioStatusType.BoardOk, p.Payload[0]);
});
Assert.Equal(
RioAddressSpace.Boards.Select(b => b.Number),
- wire.Packets.Select(p => p.Payload[1]));
+ checks.Select(p => p.Payload[1]));
}
[Fact]