Phase 2: serial + RIO protocol core (RioJoy.Core) with unit tests
Port the RIO wire protocol from legacy/riovjoy2.cpp into testable C#: - Protocol/: command + length table, 7-bit checksum, packet builder, and a streaming receive-side framing state machine (PacketParser) that mirrors the legacy ReadCommBlock framing/resync (high-bit-mid-packet abort). Typed RIO->PC decodes: AnalogReport (14-bit sign-extend), VersionInfo, CheckStatus; lamp-state composition. - Serial/: RioSerialLink drives an async receive loop with ACK/NAK reply policy (legacy force-accept vs. opt-in VerifyInboundChecksum), the analog poll timer, and the >5s reset-recovery watchdog. IRioTransport abstracts the COM port; the SerialPort-backed transport does 9600 8N1 + DTR reset pulse, and acquire/release is just create/dispose (foundation for native-game serial yield). - tests/RioJoy.Core.Tests: 54 xUnit tests covering checksum, framing/resync, builder round-trips, analog sign-extension + sentinel rejection, lamp combos, and the read loop driven against an in-memory fake transport. Hardware verification (version/check/analog against a cabinet) remains; it can't be done off-device. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,74 @@
|
||||
namespace RioJoy.Core.Protocol;
|
||||
|
||||
/// <summary>
|
||||
/// The five raw analog axes decoded from an <see cref="RioCommand.AnalogReply"/>
|
||||
/// payload. Values are the 14-bit signed raw counts straight off the RIO, before
|
||||
/// any calibration/deadzone mapping (that math is Phase 4). Port of
|
||||
/// <c>AnalogEvent</c> / <c>CombinePair</c> (riovjoy2.cpp#L1116); see
|
||||
/// docs/PROTOCOL.md §4.
|
||||
/// </summary>
|
||||
public readonly struct AnalogReport
|
||||
{
|
||||
public short Throttle { get; }
|
||||
public short LeftPedal { get; }
|
||||
public short RightPedal { get; }
|
||||
public short JoystickY { get; }
|
||||
public short JoystickX { get; }
|
||||
|
||||
public AnalogReport(short throttle, short leftPedal, short rightPedal, short joystickY, short joystickX)
|
||||
{
|
||||
Throttle = throttle;
|
||||
LeftPedal = leftPedal;
|
||||
RightPedal = rightPedal;
|
||||
JoystickY = joystickY;
|
||||
JoystickX = joystickX;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Decode a 14-bit signed axis value from a (low, high) byte pair, each
|
||||
/// carrying 7 data bits. Port of <c>CombinePair</c> (riovjoy2.cpp#L1116):
|
||||
/// combine the 7-bit halves then sign-extend bit 13.
|
||||
/// </summary>
|
||||
public static short CombinePair(byte low, byte high)
|
||||
{
|
||||
int raw = (low & 0x7F) | (high << 7); // 14 bits
|
||||
if ((raw & 0x2000) != 0) // bit 13 set → negative
|
||||
raw |= ~0x3FFF; // sign-extend into the high bits
|
||||
return (short)raw;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Try to decode an <see cref="RioCommand.AnalogReply"/> payload (10 bytes:
|
||||
/// 5 axes × low,high in the order throttle, left pedal, right pedal,
|
||||
/// joystick Y, joystick X). Returns <see langword="false"/> if any byte equals
|
||||
/// <c>0xFE</c> (<see cref="RioControl.Restart"/>), which the RIO uses as an
|
||||
/// "invalid sample" sentinel — the legacy code ignores such replies.
|
||||
/// </summary>
|
||||
public static bool TryParse(ReadOnlySpan<byte> payload, out AnalogReport report)
|
||||
{
|
||||
if (payload.Length != RioCommandTable.PayloadLength(RioCommand.AnalogReply))
|
||||
throw new ArgumentException(
|
||||
$"AnalogReply payload must be {RioCommandTable.PayloadLength(RioCommand.AnalogReply)} bytes.",
|
||||
nameof(payload));
|
||||
|
||||
foreach (byte b in payload)
|
||||
{
|
||||
if (b == (byte)RioControl.Restart)
|
||||
{
|
||||
report = default;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
report = new AnalogReport(
|
||||
throttle: CombinePair(payload[0], payload[1]),
|
||||
leftPedal: CombinePair(payload[2], payload[3]),
|
||||
rightPedal: CombinePair(payload[4], payload[5]),
|
||||
joystickY: CombinePair(payload[6], payload[7]),
|
||||
joystickX: CombinePair(payload[8], payload[9]));
|
||||
return true;
|
||||
}
|
||||
|
||||
public override string ToString() =>
|
||||
$"T:{Throttle} L:{LeftPedal} R:{RightPedal} Y:{JoystickY} X:{JoystickX}";
|
||||
}
|
||||
Reference in New Issue
Block a user