From 1eded793afe4d5007cce430e433ab71a66e32b28 Mon Sep 17 00:00:00 2001 From: Cyd Date: Mon, 6 Jul 2026 13:13:06 -0500 Subject: [PATCH] CheckRequest: send the real board's test-mode handshake The host waits up to 5s after CheckRequest for TestModeChange ENTER (8C 01 0D) before anything else, and sends no requests until the matching EXIT (8C 00 0C) arrives; vRIO jumped straight to the CheckReply dump, so hosts logged "RIO never came back from check request" and skipped the version exchange. Bracket the per-board BoardOk replies with enter/exit, byte-for-byte what the real v4.2 board sends on the wire tap. Co-Authored-By: Claude Fable 5 --- README.md | 12 +++++++----- src/VRio.Core/Device/VRioDevice.cs | 11 +++++++++-- tests/VRio.Core.Tests/VRioDeviceTests.cs | 21 +++++++++++++++++---- 3 files changed, 33 insertions(+), 11 deletions(-) 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]