Phase 8A (1/2): de-Span the serial layer, swap JSON to Newtonsoft

Prepares RioJoy.Core for the net40 (Windows XP) target, which has no
System.Memory, ValueTask, or System.Text.Json:
- IRioTransport and the whole protocol/framing layer now use byte[] +
  Task (RioPacket.Payload, PacketParser/Builder, RioChecksum, replies,
  AnalogReport, RioHidReport). At 9600 baud Span bought nothing; the
  SerialPortTransport bridge copies disappear entirely.
- ConfigStore/OverlayTemplateStore switch to Newtonsoft 13 with the
  same conventions (indented, PascalCase, string enums, null-skipping);
  verified against the real STJ-written config.json and regions.json
  (load + round-trip). System.Memory and System.Text.Json packages
  dropped.

275 tests green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Cyd
2026-07-11 20:36:19 -05:00
co-authored by Claude Fable 5
parent 02abfa8a14
commit 3b2af7b79a
21 changed files with 104 additions and 93 deletions
@@ -29,12 +29,12 @@ public class RioHidReportTests
{
var report = new RioHidReport();
report.SetAxis(JoyAxis.Z, 0x1234);
Assert.Equal(0x34, report.Bytes[4]);
Assert.Equal(0x12, report.Bytes[5]);
Assert.Equal(0x34, report.ToArray()[4]);
Assert.Equal(0x12, report.ToArray()[5]);
report.SetAxis(JoyAxis.Z, 70000); // over max → clamped to 32767
Assert.Equal(0xFF, report.Bytes[4]);
Assert.Equal(0x7F, report.Bytes[5]);
Assert.Equal(0xFF, report.ToArray()[4]);
Assert.Equal(0x7F, report.ToArray()[5]);
}
[Theory]
@@ -47,7 +47,7 @@ public class RioHidReportTests
{
var report = new RioHidReport();
report.SetHat(hat);
Assert.Equal(expected, report.Bytes[12] & 0x0F);
Assert.Equal(expected, report.ToArray()[12] & 0x0F);
}
[Theory]
@@ -59,10 +59,10 @@ public class RioHidReportTests
{
var report = new RioHidReport();
report.SetButton(button, pressed: true);
Assert.Equal(mask, report.Bytes[byteIndex]);
Assert.Equal(mask, report.ToArray()[byteIndex]);
report.SetButton(button, pressed: false);
Assert.Equal(0, report.Bytes[byteIndex]);
Assert.Equal(0, report.ToArray()[byteIndex]);
}
[Fact]
@@ -80,6 +80,6 @@ public class RioHidReportTests
report.SetButton(1, true);
report.SetButton(2, true);
report.SetButton(1, false);
Assert.Equal(0x02, report.Bytes[13]); // only button 2 remains
Assert.Equal(0x02, report.ToArray()[13]); // only button 2 remains
}
}
@@ -41,7 +41,7 @@ public class PacketBuilderTests
byte[] packet = PacketBuilder.Build(RioCommand.AnalogReply, payload);
Assert.Equal((byte)RioCommand.AnalogReply, packet[0]);
Assert.Equal(payload, packet.AsSpan(1, packet.Length - 2).ToArray());
Assert.Equal(RioChecksum.Compute(packet.AsSpan(0, packet.Length - 1)), packet[^1]);
Assert.Equal(payload, packet.Skip(1).Take(packet.Length - 2).ToArray());
Assert.Equal(RioChecksum.Compute(packet, 0, packet.Length - 1), packet[packet.Length - 1]);
}
}
@@ -5,7 +5,7 @@ namespace RioJoy.Core.Tests.Protocol;
public class PacketParserTests
{
private static List<RioRxEvent> FeedAll(PacketParser parser, ReadOnlySpan<byte> data)
private static List<RioRxEvent> FeedAll(PacketParser parser, byte[] data)
{
var events = new List<RioRxEvent>();
foreach (byte b in data)
@@ -8,7 +8,7 @@ public class RioChecksumTests
[Fact]
public void Empty_IsZero()
{
Assert.Equal(0, RioChecksum.Compute(ReadOnlySpan<byte>.Empty));
Assert.Equal(0, RioChecksum.Compute(new byte[0]));
}
[Fact]
@@ -23,13 +23,13 @@ internal sealed class FakeTransport : IRioTransport
/// <summary>Signal that no more inbound data will arrive (transport closed).</summary>
public void CompleteIncoming() => _incoming.Writer.TryComplete();
public async ValueTask<int> ReadAsync(Memory<byte> buffer, CancellationToken cancellationToken)
public async Task<int> ReadAsync(byte[] buffer, CancellationToken cancellationToken)
{
while (await _incoming.Reader.WaitToReadAsync(cancellationToken).ConfigureAwait(false))
{
if (_incoming.Reader.TryRead(out byte[]? chunk))
{
chunk.AsSpan().CopyTo(buffer.Span);
Array.Copy(chunk, buffer, chunk.Length);
return chunk.Length;
}
}
@@ -37,10 +37,10 @@ internal sealed class FakeTransport : IRioTransport
return 0; // completed
}
public ValueTask WriteAsync(ReadOnlyMemory<byte> data, CancellationToken cancellationToken)
public Task WriteAsync(byte[] data, CancellationToken cancellationToken)
{
_writes.Writer.TryWrite(data.ToArray());
return default; // net48 has no ValueTask.CompletedTask; default(ValueTask) is the completed task
_writes.Writer.TryWrite((byte[])data.Clone());
return Task.CompletedTask;
}
/// <summary>Read the next outbound write, failing if none arrives in time.</summary>