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:
@@ -43,10 +43,7 @@ public sealed class RioHidReport
|
||||
SetHat(RioHat.Centered);
|
||||
}
|
||||
|
||||
/// <summary>The current report bytes (length <see cref="Size"/>).</summary>
|
||||
public ReadOnlySpan<byte> Bytes => _buffer;
|
||||
|
||||
/// <summary>Copy of the current report bytes.</summary>
|
||||
/// <summary>Copy of the current report bytes (length <see cref="Size"/>).</summary>
|
||||
public byte[] ToArray() => (byte[])_buffer.Clone();
|
||||
|
||||
/// <summary>Set an axis value (clamped to 0..<see cref="AxisMax"/>), little-endian.</summary>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Converters;
|
||||
|
||||
namespace RioJoy.Core.Overlay;
|
||||
|
||||
@@ -11,24 +11,24 @@ namespace RioJoy.Core.Overlay;
|
||||
/// </summary>
|
||||
public static class OverlayTemplateStore
|
||||
{
|
||||
private static readonly JsonSerializerOptions Options = new()
|
||||
private static readonly JsonSerializerSettings Options = new()
|
||||
{
|
||||
WriteIndented = true,
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
|
||||
Converters = { new JsonStringEnumConverter() },
|
||||
Formatting = Formatting.Indented,
|
||||
NullValueHandling = NullValueHandling.Ignore,
|
||||
Converters = { new StringEnumConverter() },
|
||||
};
|
||||
|
||||
public static string Serialize(OverlayTemplate template)
|
||||
{
|
||||
if (template is null) throw new ArgumentNullException(nameof(template));
|
||||
return JsonSerializer.Serialize(template, Options);
|
||||
return JsonConvert.SerializeObject(template, Options);
|
||||
}
|
||||
|
||||
public static OverlayTemplate Deserialize(string json)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(json)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(json));
|
||||
return JsonSerializer.Deserialize<OverlayTemplate>(json, Options)
|
||||
?? throw new JsonException("Overlay template JSON deserialized to null.");
|
||||
return JsonConvert.DeserializeObject<OverlayTemplate>(json, Options)
|
||||
?? throw new JsonSerializationException("Overlay template JSON deserialized to null.");
|
||||
}
|
||||
|
||||
public static void Save(OverlayTemplate template, string path)
|
||||
|
||||
@@ -48,5 +48,5 @@ public sealed class PlasmaDisplay
|
||||
}
|
||||
|
||||
private Task WriteAsync(byte[] data, CancellationToken ct) =>
|
||||
_transport.WriteAsync(data, ct).AsTask();
|
||||
_transport.WriteAsync(data, ct);
|
||||
}
|
||||
|
||||
@@ -1,32 +1,36 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Converters;
|
||||
|
||||
namespace RioJoy.Core.Profiles;
|
||||
|
||||
/// <summary>
|
||||
/// Loads and saves <see cref="AppConfig"/> as JSON. Replaces the legacy
|
||||
/// SimpleIni single-file config with a profile library (see docs/PLAN.md).
|
||||
/// Newtonsoft rather than System.Text.Json so the net40 (Windows XP) flavor
|
||||
/// shares one serializer — and one on-disk format — with net48 (PLAN.md §8A).
|
||||
/// Conventions match the original STJ settings: indented, PascalCase names,
|
||||
/// enums as strings, nulls skipped — existing config.json files parse as-is.
|
||||
/// </summary>
|
||||
public static class ConfigStore
|
||||
{
|
||||
private static readonly JsonSerializerOptions Options = new()
|
||||
private static readonly JsonSerializerSettings Options = new()
|
||||
{
|
||||
WriteIndented = true,
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
|
||||
Converters = { new JsonStringEnumConverter() },
|
||||
Formatting = Formatting.Indented,
|
||||
NullValueHandling = NullValueHandling.Ignore,
|
||||
Converters = { new StringEnumConverter() },
|
||||
};
|
||||
|
||||
public static string Serialize(AppConfig config)
|
||||
{
|
||||
if (config is null) throw new ArgumentNullException(nameof(config));
|
||||
return JsonSerializer.Serialize(config, Options);
|
||||
return JsonConvert.SerializeObject(config, Options);
|
||||
}
|
||||
|
||||
public static AppConfig Deserialize(string json)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(json)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(json));
|
||||
return JsonSerializer.Deserialize<AppConfig>(json, Options)
|
||||
?? throw new JsonException("Config JSON deserialized to null.");
|
||||
return JsonConvert.DeserializeObject<AppConfig>(json, Options)
|
||||
?? throw new JsonSerializationException("Config JSON deserialized to null.");
|
||||
}
|
||||
|
||||
/// <summary>Save the config to <paramref name="path"/> (creating directories).</summary>
|
||||
|
||||
@@ -44,8 +44,9 @@ public readonly struct AnalogReport
|
||||
/// <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)
|
||||
public static bool TryParse(byte[] payload, out AnalogReport report)
|
||||
{
|
||||
if (payload is null) throw new ArgumentNullException(nameof(payload));
|
||||
if (payload.Length != RioCommandTable.PayloadLength(RioCommand.AnalogReply))
|
||||
throw new ArgumentException(
|
||||
$"AnalogReply payload must be {RioCommandTable.PayloadLength(RioCommand.AnalogReply)} bytes.",
|
||||
|
||||
@@ -13,8 +13,9 @@ public static class PacketBuilder
|
||||
/// <paramref name="payload"/>. The payload length must match the command's
|
||||
/// entry in the length table.
|
||||
/// </summary>
|
||||
public static byte[] Build(RioCommand command, ReadOnlySpan<byte> payload)
|
||||
public static byte[] Build(RioCommand command, byte[] payload)
|
||||
{
|
||||
if (payload is null) throw new ArgumentNullException(nameof(payload));
|
||||
int expected = RioCommandTable.PayloadLength(command);
|
||||
if (payload.Length != expected)
|
||||
throw new ArgumentException(
|
||||
@@ -23,13 +24,15 @@ public static class PacketBuilder
|
||||
|
||||
var packet = new byte[1 + expected + 1];
|
||||
packet[0] = (byte)command;
|
||||
payload.CopyTo(packet.AsSpan(1));
|
||||
packet[^1] = RioChecksum.Compute(packet.AsSpan(0, 1 + expected));
|
||||
Array.Copy(payload, 0, packet, 1, expected);
|
||||
packet[packet.Length - 1] = RioChecksum.Compute(packet, 0, 1 + expected);
|
||||
return packet;
|
||||
}
|
||||
|
||||
/// <summary>Build a zero-payload command packet (CheckRequest etc.).</summary>
|
||||
public static byte[] Build(RioCommand command) => Build(command, ReadOnlySpan<byte>.Empty);
|
||||
public static byte[] Build(RioCommand command) => Build(command, EmptyPayload);
|
||||
|
||||
private static readonly byte[] EmptyPayload = new byte[0];
|
||||
|
||||
// --- Convenience builders for the PC → RIO commands ---------------------
|
||||
|
||||
@@ -40,12 +43,12 @@ public static class PacketBuilder
|
||||
public static byte[] AnalogRequest() => Build(RioCommand.AnalogRequest);
|
||||
|
||||
public static byte[] ResetRequest(RioResetTarget target) =>
|
||||
Build(RioCommand.ResetRequest, stackalloc byte[] { (byte)target });
|
||||
Build(RioCommand.ResetRequest, new[] { (byte)target });
|
||||
|
||||
/// <summary>
|
||||
/// LampRequest: payload is <c>[lamp#, state]</c>. The state byte is a
|
||||
/// 7-bit lamp-state value (see <see cref="RioLampState"/>).
|
||||
/// </summary>
|
||||
public static byte[] LampRequest(byte lampNumber, byte state) =>
|
||||
Build(RioCommand.LampRequest, stackalloc byte[] { lampNumber, state });
|
||||
Build(RioCommand.LampRequest, new[] { lampNumber, state });
|
||||
}
|
||||
|
||||
@@ -92,15 +92,17 @@ public sealed class PacketParser
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Feed a chunk of received bytes, invoking <paramref name="onEvent"/> for
|
||||
/// each event produced, in order.
|
||||
/// Feed the first <paramref name="count"/> received bytes of
|
||||
/// <paramref name="data"/>, invoking <paramref name="onEvent"/> for each
|
||||
/// event produced, in order.
|
||||
/// </summary>
|
||||
public void Feed(ReadOnlySpan<byte> data, Action<RioRxEvent> onEvent)
|
||||
public void Feed(byte[] data, int count, Action<RioRxEvent> onEvent)
|
||||
{
|
||||
if (data is null) throw new ArgumentNullException(nameof(data));
|
||||
if (onEvent is null) throw new ArgumentNullException(nameof(onEvent));
|
||||
foreach (byte ch in data)
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
if (Feed(ch, out RioRxEvent ev))
|
||||
if (Feed(data[i], out RioRxEvent ev))
|
||||
onEvent(ev);
|
||||
}
|
||||
}
|
||||
@@ -110,11 +112,12 @@ public sealed class PacketParser
|
||||
// _buffer = [command][payload…][checksum]; _count includes all of them.
|
||||
int bodyLen = _count - 1; // command + payload (exclude checksum)
|
||||
byte receivedChecksum = _buffer[bodyLen];
|
||||
byte computed = RioChecksum.Compute(_buffer.AsSpan(0, bodyLen));
|
||||
byte computed = RioChecksum.Compute(_buffer, 0, bodyLen);
|
||||
bool checksumValid = computed == receivedChecksum;
|
||||
|
||||
var command = (RioCommand)_buffer[0];
|
||||
var payload = _buffer.AsSpan(1, bodyLen - 1).ToArray(); // copy out; buffer is reused
|
||||
var payload = new byte[bodyLen - 1]; // copy out; buffer is reused
|
||||
Array.Copy(_buffer, 1, payload, 0, payload.Length);
|
||||
var packet = new RioPacket(command, payload);
|
||||
|
||||
Reset();
|
||||
|
||||
@@ -9,15 +9,22 @@ namespace RioJoy.Core.Protocol;
|
||||
public static class RioChecksum
|
||||
{
|
||||
/// <summary>
|
||||
/// Compute the 7-bit checksum over <paramref name="commandAndPayload"/>
|
||||
/// (the command byte followed by its payload bytes — not the checksum byte).
|
||||
/// Compute the 7-bit checksum over the command byte followed by its payload
|
||||
/// bytes (not the checksum byte): <paramref name="count"/> bytes of
|
||||
/// <paramref name="commandAndPayload"/> starting at <paramref name="offset"/>.
|
||||
/// </summary>
|
||||
public static byte Compute(ReadOnlySpan<byte> commandAndPayload)
|
||||
public static byte Compute(byte[] commandAndPayload, int offset, int count)
|
||||
{
|
||||
if (commandAndPayload is null) throw new ArgumentNullException(nameof(commandAndPayload));
|
||||
|
||||
byte sum = 0;
|
||||
foreach (byte b in commandAndPayload)
|
||||
sum += (byte)(b & 0x7F);
|
||||
for (int i = offset; i < offset + count; i++)
|
||||
sum += (byte)(commandAndPayload[i] & 0x7F);
|
||||
|
||||
return (byte)(sum & 0x7F);
|
||||
}
|
||||
|
||||
/// <summary>Compute the checksum over all of <paramref name="commandAndPayload"/>.</summary>
|
||||
public static byte Compute(byte[] commandAndPayload) =>
|
||||
Compute(commandAndPayload, 0, commandAndPayload?.Length ?? 0);
|
||||
}
|
||||
|
||||
@@ -11,13 +11,14 @@ public readonly struct RioPacket
|
||||
public RioCommand Command { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The payload bytes (high bit always clear). Length matches
|
||||
/// <see cref="RioCommandTable.PayloadLength(RioCommand)"/>.
|
||||
/// The payload bytes (high bit always clear; treat as read-only). Length
|
||||
/// matches <see cref="RioCommandTable.PayloadLength(RioCommand)"/>.
|
||||
/// </summary>
|
||||
public ReadOnlyMemory<byte> Payload { get; }
|
||||
public byte[] Payload { get; }
|
||||
|
||||
public RioPacket(RioCommand command, ReadOnlyMemory<byte> payload)
|
||||
public RioPacket(RioCommand command, byte[] payload)
|
||||
{
|
||||
if (payload is null) throw new ArgumentNullException(nameof(payload));
|
||||
int expected = RioCommandTable.PayloadLength(command);
|
||||
if (payload.Length != expected)
|
||||
throw new ArgumentException(
|
||||
@@ -30,7 +31,7 @@ public readonly struct RioPacket
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
var hex = BitConverter.ToString(Payload.ToArray()).Replace("-", string.Empty);
|
||||
return Payload.IsEmpty ? Command.ToString() : $"{Command} [{hex}]";
|
||||
var hex = BitConverter.ToString(Payload).Replace("-", string.Empty);
|
||||
return Payload.Length == 0 ? Command.ToString() : $"{Command} [{hex}]";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ public readonly struct VersionInfo
|
||||
}
|
||||
|
||||
/// <summary>Decode a <see cref="RioCommand.VersionReply"/> payload (2 bytes).</summary>
|
||||
public static VersionInfo Parse(ReadOnlySpan<byte> payload)
|
||||
public static VersionInfo Parse(byte[] payload)
|
||||
{
|
||||
Require(payload, RioCommand.VersionReply);
|
||||
return new VersionInfo(payload[0], payload[1]);
|
||||
@@ -24,8 +24,9 @@ public readonly struct VersionInfo
|
||||
|
||||
public override string ToString() => $"{Major}.{Minor}";
|
||||
|
||||
internal static void Require(ReadOnlySpan<byte> payload, RioCommand command)
|
||||
internal static void Require(byte[] payload, RioCommand command)
|
||||
{
|
||||
if (payload is null) throw new ArgumentNullException(nameof(payload));
|
||||
int expected = RioCommandTable.PayloadLength(command);
|
||||
if (payload.Length != expected)
|
||||
throw new ArgumentException(
|
||||
@@ -51,7 +52,7 @@ public readonly struct CheckStatus
|
||||
}
|
||||
|
||||
/// <summary>Decode a <see cref="RioCommand.CheckReply"/> payload (2 bytes).</summary>
|
||||
public static CheckStatus Parse(ReadOnlySpan<byte> payload)
|
||||
public static CheckStatus Parse(byte[] payload)
|
||||
{
|
||||
VersionInfo.Require(payload, RioCommand.CheckReply);
|
||||
return new CheckStatus((RioStatusType)payload[0], payload[1]);
|
||||
|
||||
@@ -15,8 +15,7 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Nefarius.ViGEm.Client" Version="1.21.256" />
|
||||
<PackageReference Include="System.IO.Ports" Version="8.0.0" />
|
||||
<PackageReference Include="System.Memory" Version="4.5.5" />
|
||||
<PackageReference Include="System.Text.Json" Version="8.0.5" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
<PackageReference Include="Microsoft.Bcl.HashCode" Version="1.1.1" />
|
||||
<PackageReference Include="PolySharp" Version="1.14.1">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
|
||||
@@ -144,7 +144,7 @@ public sealed class RioRuntime : IRioCommandSink, IDisposable
|
||||
private void OnPacket(RioPacket packet)
|
||||
{
|
||||
EnsureLampsInitialized();
|
||||
ReadOnlySpan<byte> p = packet.Payload.Span;
|
||||
byte[] p = packet.Payload;
|
||||
switch (packet.Command)
|
||||
{
|
||||
case RioCommand.ButtonPressed when p[0] < RioAddress.ButtonCount:
|
||||
@@ -185,7 +185,7 @@ public sealed class RioRuntime : IRioCommandSink, IDisposable
|
||||
ButtonActivity?.Invoke(address, pressed);
|
||||
}
|
||||
|
||||
private static bool IsKeypad(ReadOnlySpan<byte> p) => p[0] is 0 or 1 && p[1] <= 0x0F;
|
||||
private static bool IsKeypad(byte[] p) => p[0] is 0 or 1 && p[1] <= 0x0F;
|
||||
|
||||
// IRioCommandSink: RIO commands routed from a button (RIOcmd port).
|
||||
void IRioCommandSink.Execute(RioCommandCode command)
|
||||
|
||||
@@ -15,8 +15,13 @@ public interface IRioTransport : IDisposable
|
||||
/// Read available bytes into <paramref name="buffer"/>. Returns the number of
|
||||
/// bytes read; a return of 0 indicates the transport has closed.
|
||||
/// </summary>
|
||||
ValueTask<int> ReadAsync(Memory<byte> buffer, CancellationToken cancellationToken);
|
||||
/// <remarks>
|
||||
/// Plain <c>byte[]</c>/<see cref="Task"/> rather than Memory/ValueTask: the
|
||||
/// link runs at 9600 baud, and the net40 (Windows XP) flavor has neither
|
||||
/// System.Memory nor ValueTask (see docs/PLAN.md §Phase 8).
|
||||
/// </remarks>
|
||||
Task<int> ReadAsync(byte[] buffer, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>Write all of <paramref name="data"/> to the transport.</summary>
|
||||
ValueTask WriteAsync(ReadOnlyMemory<byte> data, CancellationToken cancellationToken);
|
||||
Task WriteAsync(byte[] data, CancellationToken cancellationToken);
|
||||
}
|
||||
|
||||
@@ -79,7 +79,7 @@ public sealed class RioSerialLink
|
||||
}
|
||||
|
||||
/// <summary>Send a pre-built packet (see <see cref="PacketBuilder"/>) to the RIO.</summary>
|
||||
public async Task SendAsync(ReadOnlyMemory<byte> packet, CancellationToken cancellationToken = default)
|
||||
public async Task SendAsync(byte[] packet, CancellationToken cancellationToken = default)
|
||||
{
|
||||
await _writeLock.WaitAsync(cancellationToken).ConfigureAwait(false);
|
||||
try
|
||||
@@ -155,7 +155,7 @@ public sealed class RioSerialLink
|
||||
switch (packet.Command)
|
||||
{
|
||||
case RioCommand.AnalogReply:
|
||||
if (AnalogReport.TryParse(packet.Payload.Span, out AnalogReport report))
|
||||
if (AnalogReport.TryParse(packet.Payload, out AnalogReport report))
|
||||
{
|
||||
_sinceAnalog.Restart();
|
||||
AnalogReceived?.Invoke(report);
|
||||
@@ -163,11 +163,11 @@ public sealed class RioSerialLink
|
||||
break;
|
||||
|
||||
case RioCommand.VersionReply:
|
||||
VersionReceived?.Invoke(VersionInfo.Parse(packet.Payload.Span));
|
||||
VersionReceived?.Invoke(VersionInfo.Parse(packet.Payload));
|
||||
break;
|
||||
|
||||
case RioCommand.CheckReply:
|
||||
CheckReceived?.Invoke(CheckStatus.Parse(packet.Payload.Span));
|
||||
CheckReceived?.Invoke(CheckStatus.Parse(packet.Payload));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,21 +47,11 @@ public sealed class SerialPortTransport : IRioTransport
|
||||
|
||||
public string Description => $"{_port.PortName} @ {BaudRate} 8N1";
|
||||
|
||||
// net48's Stream has no Memory-based ReadAsync/WriteAsync overloads, so bridge
|
||||
// through a pooled array and copy into/out of the caller's Memory<byte>.
|
||||
public async ValueTask<int> ReadAsync(Memory<byte> buffer, CancellationToken cancellationToken)
|
||||
{
|
||||
byte[] tmp = new byte[buffer.Length];
|
||||
int read = await _stream.ReadAsync(tmp, 0, tmp.Length, cancellationToken).ConfigureAwait(false);
|
||||
new ReadOnlySpan<byte>(tmp, 0, read).CopyTo(buffer.Span);
|
||||
return read;
|
||||
}
|
||||
public Task<int> ReadAsync(byte[] buffer, CancellationToken cancellationToken) =>
|
||||
_stream.ReadAsync(buffer, 0, buffer.Length, cancellationToken);
|
||||
|
||||
public async ValueTask WriteAsync(ReadOnlyMemory<byte> data, CancellationToken cancellationToken)
|
||||
{
|
||||
byte[] tmp = data.ToArray();
|
||||
await _stream.WriteAsync(tmp, 0, tmp.Length, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
public Task WriteAsync(byte[] data, CancellationToken cancellationToken) =>
|
||||
_stream.WriteAsync(data, 0, data.Length, cancellationToken);
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -68,7 +68,7 @@ bool Changed(AnalogReport a, AnalogReport b) =>
|
||||
link.PacketReceived += p =>
|
||||
{
|
||||
packets++;
|
||||
ReadOnlySpan<byte> pl = p.Payload.Span;
|
||||
byte[] pl = p.Payload;
|
||||
switch (p.Command)
|
||||
{
|
||||
case RioCommand.ButtonPressed when pl[0] < RioAddress.ButtonCount:
|
||||
|
||||
Reference in New Issue
Block a user