namespace RioJoy.Core.Protocol;
///
/// Streaming receive-side framing state machine, fed raw bytes from the serial
/// port. Faithful port of the framing loop in ReadCommBlock
/// (riovjoy2.cpp#L860); see docs/PROTOCOL.md §2.
///
/// Rules:
///
/// - Outside a packet, a byte in the command range starts a packet whose
/// total length is 1 + payloadLen + 1 (command + payload + checksum).
/// - Outside a packet, any other byte is reported as a
/// (ACK/NAK/RESTART/IDLE or garbage).
/// - Mid-packet, a byte with the high bit set aborts the packet and
/// resynchronizes (); that byte is
/// discarded, matching the legacy continue.
/// - When the final (checksum) byte arrives, a
/// is emitted with
/// set from a real checksum compare.
///
///
/// Each input byte produces at most one event, so the per-byte
/// returns a single optional event.
/// This type is not thread-safe; drive it from one reader.
///
public sealed class PacketParser
{
// Max framed packet = command(1) + AnalogReply payload(10) + checksum(1) = 12.
private const int MaxPacketBytes = 16;
private readonly byte[] _buffer = new byte[MaxPacketBytes];
// Bytes still expected to complete the current packet (0 = not in a packet).
private int _remaining;
// Count of bytes stored in _buffer for the current packet (incl. command).
private int _count;
/// True while a packet is partially received.
public bool InPacket => _remaining != 0;
/// Discard any in-progress packet and reset to the idle state.
public void Reset()
{
_remaining = 0;
_count = 0;
}
///
/// Feed one received byte. Returns and sets
/// when this byte produced an event; otherwise returns
/// (the byte was consumed into an in-progress packet).
///
public bool Feed(byte ch, out RioRxEvent ev)
{
if (_remaining != 0)
{
// Mid-packet. A high-bit byte is a framing error → abort and resync.
if ((ch & 0x80) != 0)
{
Reset();
ev = RioRxEvent.ForFramingError(ch);
return true;
}
_buffer[_count++] = ch;
_remaining--;
if (_remaining == 0)
{
ev = CompletePacket();
return true;
}
ev = default;
return false;
}
// Not in a packet.
if (RioCommandTable.IsCommand(ch))
{
_count = 0;
_buffer[_count++] = ch;
_remaining = RioCommandTable.PayloadLength(ch) + 1; // + checksum byte
ev = default;
return false;
}
// A byte outside framing: control character (ACK/NAK/RESTART/IDLE) or garbage.
ev = RioRxEvent.ForControl(ch);
return true;
}
///
/// Feed the first received bytes of
/// , invoking for each
/// event produced, in order.
///
public void Feed(byte[] data, int count, Action onEvent)
{
if (data is null) throw new ArgumentNullException(nameof(data));
if (onEvent is null) throw new ArgumentNullException(nameof(onEvent));
for (int i = 0; i < count; i++)
{
if (Feed(data[i], out RioRxEvent ev))
onEvent(ev);
}
}
private RioRxEvent CompletePacket()
{
// _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, 0, bodyLen);
bool checksumValid = computed == receivedChecksum;
var command = (RioCommand)_buffer[0];
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();
return RioRxEvent.ForPacket(packet, checksumValid);
}
}