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); } }