Skip to content
Merged
205 changes: 205 additions & 0 deletions src/Daqifi.Core.Tests/Firmware/Winc/FakeWincSerialPort.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
using Daqifi.Core.Firmware.Winc;

namespace Daqifi.Core.Tests.Firmware.Winc;

/// <summary>
/// A fake serial port that emulates the DAQiFi firmware's WINC bridge state machine, so the client
/// can be exercised end-to-end without hardware.
/// </summary>
/// <remarks>
/// This deliberately re-implements the firmware's parser (op-code wait, 12-byte header, XOR
/// validation, ACK/NACK, payload wait) rather than replaying canned bytes. That way a client that
/// frames a command incorrectly gets rejected here exactly as the device would reject it — the
/// point is to catch framing bugs the bench cannot.
/// </remarks>
internal sealed class FakeWincSerialPort : IWincSerialPort
{
private readonly Queue<byte> _toHost = new();
private readonly List<byte> _fromHost = [];

private State _state = State.WaitOpCode;
private byte[] _header = [];
private int _pendingPayload;

/// <summary>Register file the emulated WINC answers reads from.</summary>
internal Dictionary<uint, uint> Registers { get; } = [];

/// <summary>Memory the emulated WINC answers block reads from.</summary>
internal Dictionary<uint, byte[]> Blocks { get; } = [];

/// <summary>Every command header the host sent, in order.</summary>
internal List<byte[]> ReceivedHeaders { get; } = [];

/// <summary>Every block-write payload the host sent, in order.</summary>
internal List<byte[]> ReceivedPayloads { get; } = [];

/// <summary>Baud rates the host applied, in order.</summary>
internal List<int> BaudRateHistory { get; } = [];

/// <summary>When true, the emulated bridge NACKs the next block write.</summary>
internal bool FailNextBlockWrite { get; set; }

/// <summary>When true, the bridge does not answer the identify op code.</summary>
internal bool SuppressIdentityResponse { get; set; }

/// <summary>Number of times the host discarded its input buffer.</summary>
internal int DiscardCount { get; private set; }

internal bool WasDisposed { get; private set; }

public bool IsOpen { get; private set; }

private int _baudRate = 115200;

public int BaudRate
{
get => _baudRate;
set
{
_baudRate = value;
BaudRateHistory.Add(value);
}
}

public void Open() => IsOpen = true;

public void Close() => IsOpen = false;

public void DiscardInBuffer()
{
DiscardCount++;
_toHost.Clear();
}

public void Write(byte[] buffer, int offset, int count)
{
for (var i = 0; i < count; i++)
{
Feed(buffer[offset + i]);
}
}

public void ReadExactly(byte[] buffer, int offset, int count, TimeSpan timeout)
{
if (_toHost.Count < count)
{
throw new TimeoutException(
$"Fake port has {_toHost.Count} byte(s) buffered but {count} were requested.");
}

for (var i = 0; i < count; i++)
{
buffer[offset + i] = _toHost.Dequeue();
}
}

public void Dispose()
{
WasDisposed = true;
IsOpen = false;
}

/// <summary>Drives the emulated bridge one received byte at a time.</summary>
private void Feed(byte b)
{
_fromHost.Add(b);

switch (_state)
{
case State.WaitOpCode:
if (b == WincBridgeProtocol.IdentifyVariableBaud)
{
if (!SuppressIdentityResponse)
{
_toHost.Enqueue(WincBridgeProtocol.Response.IdVariableBaud);
}
}
else if (b == WincBridgeProtocol.StartCommand)
{
_state = State.WaitHeader;
_header = [];
}
// Any other op code is ignored, exactly as the firmware does.
break;

case State.WaitHeader:
_header = [.. _header, b];
if (_header.Length == WincBridgeProtocol.HeaderSize)
{
ReceivedHeaders.Add(_header);
HandleHeader();
}
break;

case State.WaitPayload:
_header = [.. _header, b];
if (_header.Length == _pendingPayload)
{
ReceivedPayloads.Add(_header);
_toHost.Enqueue(
FailNextBlockWrite
? WincBridgeProtocol.Response.Nack
: WincBridgeProtocol.Response.Ack);
FailNextBlockWrite = false;
_state = State.WaitOpCode;
}
break;
}
}

private void HandleHeader()
{
if (!WincBridgeProtocol.IsHeaderValid(_header))
{
_toHost.Enqueue(WincBridgeProtocol.Response.Nack);
_state = State.WaitOpCode;
return;
}

_toHost.Enqueue(WincBridgeProtocol.Response.Ack);

var command = (WincBridgeProtocol.Command)_header[0];
var size = (ushort)((_header[3] << 8) | _header[2]);
var address = ((uint)_header[7] << 24) | ((uint)_header[6] << 16) | ((uint)_header[5] << 8) | _header[4];

switch (command)
{
case WincBridgeProtocol.Command.ReadRegisterWithReturn:
var value = Registers.TryGetValue(address, out var v) ? v : 0u;
// Big-endian, matching the firmware.
_toHost.Enqueue((byte)(value >> 24));
_toHost.Enqueue((byte)(value >> 16));
_toHost.Enqueue((byte)(value >> 8));
_toHost.Enqueue((byte)value);
_state = State.WaitOpCode;
break;

case WincBridgeProtocol.Command.ReadBlock:
var block = Blocks.TryGetValue(address, out var data) ? data : new byte[size];
for (var i = 0; i < size; i++)
{
_toHost.Enqueue(i < block.Length ? block[i] : (byte)0);
}
_state = State.WaitOpCode;
break;

case WincBridgeProtocol.Command.WriteBlock:
_pendingPayload = size;
_header = [];
_state = State.WaitPayload;
break;

default:
// WriteRegister and Reconfigure produce no data beyond the header ACK.
_state = State.WaitOpCode;
break;
}
}

private enum State
{
WaitOpCode,
WaitHeader,
WaitPayload
}
}
192 changes: 192 additions & 0 deletions src/Daqifi.Core.Tests/Firmware/Winc/WincBridgeProtocolTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
using Daqifi.Core.Firmware.Winc;

namespace Daqifi.Core.Tests.Firmware.Winc;

/// <summary>
/// Framing tests for the WINC serial bridge wire format.
/// </summary>
/// <remarks>
/// These are the primary correctness net for the native flasher: the erase/program path cannot be
/// exercised on the bench, so the framing has to be right by construction. Expected byte sequences
/// are derived from the DAQiFi firmware's bridge parser
/// (<c>firmware/src/services/wifi_services/wifi_serial_bridge.c</c>), which is the code that
/// actually accepts or rejects these headers.
/// </remarks>
public class WincBridgeProtocolTests
{
[Fact]
public void BuildHeader_ProducesTwelveBytesWhoseXorIsZero()
{
// The firmware XORs all 12 bytes and requires 0 before it will ACK.
var header = WincBridgeProtocol.BuildHeader(
WincBridgeProtocol.Command.ReadBlock, 0x0100, 0xD0000, 0);

Assert.Equal(WincBridgeProtocol.HeaderSize, header.Length);

byte xor = 0;
foreach (var b in header)
{
xor ^= b;
}

Assert.Equal(0, xor);
Assert.True(WincBridgeProtocol.IsHeaderValid(header));
}

[Theory]
[InlineData((byte)0, (ushort)0, 0x1000u, 0u)] // ReadRegisterWithReturn
[InlineData((byte)1, (ushort)0, 0x10208u, 0x2Au)] // WriteRegister
[InlineData((byte)2, (ushort)2047, 0xD0000u, 0u)] // ReadBlock, max safe size
[InlineData((byte)3, (ushort)256, 0xD0000u, 0u)] // WriteBlock, one flash page
[InlineData((byte)5, (ushort)0, 0u, 500000u)] // Reconfigure to the fast baud
[InlineData((byte)1, (ushort)0xFFFF, 0xFFFFFFFFu, 0xFFFFFFFFu)] // all-ones saturation
public void BuildHeader_IsAlwaysAcceptedByTheFirmwareChecksumRule(
byte command, ushort size, uint address, uint value)
{
var header = WincBridgeProtocol.BuildHeader(
(WincBridgeProtocol.Command)command, size, address, value);

Assert.True(WincBridgeProtocol.IsHeaderValid(header));
}

[Fact]
public void BuildHeader_LaysOutCommandSizeAddressAndValueLittleEndian()
{
// Byte-for-byte against the firmware's ProcessHeader field extraction:
// size = [3]<<8 | [2]; addr = [7]<<24 | [6]<<16 | [5]<<8 | [4]; val likewise from [8..11].
var header = WincBridgeProtocol.BuildHeader(
WincBridgeProtocol.Command.WriteRegister,
size: 0x1234,
address: 0xAABBCCDD,
value: 0x11223344);

Assert.Equal((byte)WincBridgeProtocol.Command.WriteRegister, header[0]);

Assert.Equal(0x34, header[2]);
Assert.Equal(0x12, header[3]);

Assert.Equal(0xDD, header[4]);
Assert.Equal(0xCC, header[5]);
Assert.Equal(0xBB, header[6]);
Assert.Equal(0xAA, header[7]);

Assert.Equal(0x44, header[8]);
Assert.Equal(0x33, header[9]);
Assert.Equal(0x22, header[10]);
Assert.Equal(0x11, header[11]);
}

[Fact]
public void BuildHeader_ReconfigurePutsTheBaudRateInTheValueField()
{
// The baud change is the one command whose payload is the value field alone.
var header = WincBridgeProtocol.BuildHeader(
WincBridgeProtocol.Command.Reconfigure, 0, 0, 500000);

Assert.Equal(0x05, header[0]);
Assert.Equal(0x20, header[8]);
Assert.Equal(0xA1, header[9]);
Assert.Equal(0x07, header[10]);
Assert.Equal(0x00, header[11]);
Assert.True(WincBridgeProtocol.IsHeaderValid(header));
}

[Fact]
public void IsHeaderValid_RejectsASingleFlippedBit()
{
var header = WincBridgeProtocol.BuildHeader(
WincBridgeProtocol.Command.ReadBlock, 512, 0xD0000, 0);

header[6] ^= 0x01;

Assert.False(WincBridgeProtocol.IsHeaderValid(header));
}

[Theory]
[InlineData(0)]
[InlineData(11)]
[InlineData(13)]
public void IsHeaderValid_RejectsWrongLength(int length)
{
Assert.False(WincBridgeProtocol.IsHeaderValid(new byte[length]));
}

[Fact]
public void IsHeaderValid_RejectsNull()
{
Assert.False(WincBridgeProtocol.IsHeaderValid(null!));
}

[Fact]
public void ComputeChecksum_IgnoresTheExistingChecksumSlot()
{
// Byte 1 must not feed its own computation, or rebuilding a header would drift.
var header = WincBridgeProtocol.BuildHeader(
WincBridgeProtocol.Command.ReadBlock, 128, 0x1000, 0);

var recomputed = WincBridgeProtocol.ComputeChecksum(header);

Assert.Equal(header[1], recomputed);
}

[Fact]
public void ComputeChecksum_RejectsWrongLengthHeaders()
{
Assert.Throws<ArgumentException>(() => WincBridgeProtocol.ComputeChecksum(new byte[5]));
}

[Fact]
public void DecodeRegisterValue_ReadsBigEndian_OppositeOfTheHeaderFields()
{
// The firmware writes the register value MSB-first, unlike the little-endian header —
// getting this backwards is the single easiest way to misread every register.
var value = WincBridgeProtocol.DecodeRegisterValue([0x00, 0x10, 0x03, 0xA0]);

Assert.Equal(0x001003A0u, value);
}

[Fact]
public void DecodeRegisterValue_HandlesTheFullRange()
{
Assert.Equal(0xFFFFFFFFu, WincBridgeProtocol.DecodeRegisterValue([0xFF, 0xFF, 0xFF, 0xFF]));
Assert.Equal(0u, WincBridgeProtocol.DecodeRegisterValue([0x00, 0x00, 0x00, 0x00]));
}

[Theory]
[InlineData(0)]
[InlineData(3)]
[InlineData(5)]
public void DecodeRegisterValue_RejectsWrongLength(int length)
{
Assert.Throws<ArgumentException>(() => WincBridgeProtocol.DecodeRegisterValue(new byte[length]));
}

[Fact]
public void OpCodesAndResponses_MatchTheFirmwareConstants()
{
// Pinned against wifi_serial_bridge.c; a silent drift here would break every exchange.
Assert.Equal(0x12, WincBridgeProtocol.IdentifyVariableBaud);
Assert.Equal(0x13, WincBridgeProtocol.IdentifyFixedBaud);
Assert.Equal(0xA5, WincBridgeProtocol.StartCommand);

Assert.Equal(0x5A, WincBridgeProtocol.Response.Nack);
Assert.Equal(0x5B, WincBridgeProtocol.Response.IdVariableBaud);
Assert.Equal(0x5C, WincBridgeProtocol.Response.IdFixedBaud);
Assert.Equal(0xAC, WincBridgeProtocol.Response.Ack);

Assert.Equal(0, (byte)WincBridgeProtocol.Command.ReadRegisterWithReturn);
Assert.Equal(1, (byte)WincBridgeProtocol.Command.WriteRegister);
Assert.Equal(2, (byte)WincBridgeProtocol.Command.ReadBlock);
Assert.Equal(3, (byte)WincBridgeProtocol.Command.WriteBlock);
Assert.Equal(5, (byte)WincBridgeProtocol.Command.Reconfigure);
}

[Fact]
public void MaxReadBlockSize_StaysBelowTheFirmwareCommandBuffer()
{
// The device's read loop never terminates at or above its 2048-byte buffer, so 2047 is a
// hard ceiling rather than a tuning choice.
Assert.Equal(2047, WincBridgeProtocol.MaxReadBlockSize);
Assert.True(WincBridgeProtocol.MaxReadBlockSize < 2048);
}
}
Loading
Loading