namespace ErvProtocol { // 고정패킷 파서 (PC_ERV_Protocol.md Rev2.0) // 0xAA | CMD | DATA[240] | CRC16-MODBUS(LE) = 244B 고정 // 연결(소켓/시리얼)마다 1개 인스턴스. CRC 통과 프레임만 OnFrame 으로 전달. public sealed class FrameParser { public const byte STX = 0xAA; public const int DataLen = CtrlFrame.DataLen; // 240 public event Action? OnFrame; // (cmd, data[240]) public event Action? OnError; enum Step { Stx, Cmd, Data, CrcLo, CrcHi } Step _step = Step.Stx; byte _cmd, _crcLo; readonly byte[] _data = new byte[DataLen]; int _dataPos; public void Feed(ReadOnlySpan data) { foreach (var b in data) FeedByte(b); } public void FeedByte(byte b) { switch (_step) { case Step.Stx: if (b == STX) _step = Step.Cmd; break; case Step.Cmd: _cmd = b; _dataPos = 0; _step = Step.Data; break; case Step.Data: _data[_dataPos++] = b; if (_dataPos >= DataLen) _step = Step.CrcLo; break; case Step.CrcLo: _crcLo = b; _step = Step.CrcHi; break; case Step.CrcHi: ushort rxCrc = (ushort)(_crcLo | (b << 8)); _step = Step.Stx; Verify(rxCrc); break; } } // 프레임 동기 리셋 (시리얼 프레임 갭 발생 시 등) public void Reset() => _step = Step.Stx; void Verify(ushort rxCrc) { var buf = new byte[1 + DataLen]; // CMD + DATA[240] buf[0] = _cmd; Array.Copy(_data, 0, buf, 1, DataLen); ushort calc = Crc16.Modbus(buf, 0, buf.Length); if (calc != rxCrc) { OnError?.Invoke($"CRC ERR cmd=0x{_cmd:X2} calc=0x{calc:X4} rx=0x{rxCrc:X4}"); return; } var data = new byte[DataLen]; Array.Copy(_data, data, DataLen); OnFrame?.Invoke(_cmd, data); } } }