Files
CydandClaude Fable 5 7995c0b1c1 vRIO: virtual RIO cockpit device emulator
Speaks the device side of the RIO serial protocol (per riojoy's
PROTOCOL.md) on a COM port, behind an interactive replica of the
profile editor's cockpit panel: click cells to press buttons/keys,
drag the encoder gauges to move the five analog axes, and watch
host-commanded lamp states (incl. flash modes) light the cells.

Device behavior grounded in the real v4.2 firmware dump: version 4.2,
4-retry NAK budget ending in RESTART, and an optional emulation of the
analog reply-wedge latch leak for exercising host recovery watchdogs.

Verified: 33 unit tests, plus an interop harness driving RIOJoy's
actual RioSerialLink against VRioDevice over an in-memory transport
(version/check/analog/lamp/button/keypad/reset all round-trip).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 07:39:38 -05:00

100 lines
3.5 KiB
C#

using VRio.Core.Protocol;
using Xunit;
namespace VRio.Core.Tests;
public class ProtocolTests
{
[Fact]
public void Checksum_is_low7_of_sum_of_low7()
{
// 0x88 & 0x7F = 0x08, + 0x05 = 0x0D.
Assert.Equal(0x0D, RioChecksum.Compute(new byte[] { 0x88, 0x05 }));
}
[Theory]
[InlineData(0)]
[InlineData(1)]
[InlineData(-1)]
[InlineData(8191)]
[InlineData(-8192)]
[InlineData(1234)]
[InlineData(-4321)]
public void AnalogCodec_round_trips_and_stays_7bit(short value)
{
AnalogCodec.Split(value, out byte low, out byte high);
Assert.Equal(0, low & 0x80); // payload bytes must keep the high bit clear
Assert.Equal(0, high & 0x80);
Assert.Equal(value, AnalogCodec.Combine(low, high));
}
[Fact]
public void Builder_output_parses_back_with_valid_checksum()
{
byte[] wire = PacketBuilder.AnalogReply(100, -200, 300, -8192, 8191);
var parser = new PacketParser();
RioRxEvent? got = null;
foreach (byte b in wire)
if (parser.Feed(b, out RioRxEvent ev))
got = ev;
Assert.NotNull(got);
Assert.Equal(RioRxKind.Packet, got!.Value.Kind);
Assert.True(got.Value.ChecksumValid);
Assert.Equal(RioCommand.AnalogReply, got.Value.Packet.Command);
byte[] p = got.Value.Packet.Payload;
Assert.Equal(100, AnalogCodec.Combine(p[0], p[1]));
Assert.Equal(-200, AnalogCodec.Combine(p[2], p[3]));
Assert.Equal(300, AnalogCodec.Combine(p[4], p[5]));
Assert.Equal(-8192, AnalogCodec.Combine(p[6], p[7]));
Assert.Equal(8191, AnalogCodec.Combine(p[8], p[9]));
}
[Fact]
public void Parser_reports_control_bytes_outside_framing()
{
var parser = new PacketParser();
Assert.True(parser.Feed((byte)RioControl.Ack, out RioRxEvent ev));
Assert.Equal(RioRxKind.ControlByte, ev.Kind);
Assert.Equal((byte)RioControl.Ack, ev.Byte);
}
[Fact]
public void Parser_resyncs_on_high_bit_byte_mid_packet()
{
var parser = new PacketParser();
// Start a LampRequest (2-byte payload), then interrupt with a new command byte.
Assert.False(parser.Feed(0x84, out _));
Assert.True(parser.Feed(0xFF, out RioRxEvent err)); // IDLE mid-packet = framing error
Assert.Equal(RioRxKind.FramingError, err.Kind);
Assert.False(parser.InPacket);
// A clean packet right after must still parse.
byte[] wire = PacketBuilder.VersionReply(1, 2);
RioRxEvent? got = null;
foreach (byte b in wire)
if (parser.Feed(b, out RioRxEvent ev))
got = ev;
Assert.Equal(RioRxKind.Packet, got!.Value.Kind);
Assert.True(got.Value.ChecksumValid);
}
[Theory]
[InlineData(0x00, LampBrightness.Off, LampFlash.Solid)]
[InlineData(0x14, LampBrightness.Dim, LampFlash.Solid)]
[InlineData(0x3C, LampBrightness.Bright, LampFlash.Solid)]
[InlineData(0x3D, LampBrightness.Bright, LampFlash.FlashSlow)]
[InlineData(0x16, LampBrightness.Dim, LampFlash.FlashMed)]
[InlineData(0x04, LampBrightness.Dim, LampFlash.Solid)] // field 1 only
[InlineData(0x30, LampBrightness.Bright, LampFlash.Solid)] // field 2 only
public void LampState_decodes_brightness_and_flash(byte state, LampBrightness brightness, LampFlash flash)
{
Assert.Equal(brightness, RioLampState.Brightness(state));
Assert.Equal(flash, RioLampState.Flash(state));
}
}