//{{copyright}} //{{version}} //{{license}} #pragma once #include "../generic/crc.h" #include #include #include #include /** * @namespace sdi_ToolBox::comm::uBinaryFrame * @brief Utilities for robust binary frame serialization, deserialization, and protocol handling. * * This namespace provides classes and constants to manage binary communication frames with automatic * escaping, CRC calculation, and protocol validation. The supported frame format is: * [SOF1][SOF2][SIZE][PAYLOAD][CRC16][EOF] * where special bytes are escaped as needed. * * Main components: * - @ref FrameReceiver : Receives and extracts a complete binary frame from a byte stream. * - @ref MessageParser : Parses and validates a binary frame, providing typed access to the payload. * - @ref MessageBuilder : Builds a binary message with CRC calculation and escaping. * - @ref FramePacker : Packs a message into a complete frame ready for transmission (header, CRC, escaping, etc.). */ namespace sdi_ToolBox::comm::uBinaryFrame { constexpr uint8_t SOF1_ = 0xAA; // Start of Frame byte (0b10101010 - useful for bus synchronisation) constexpr uint8_t SOF2_ = 0x55; // Start of Frame byte (0b01010101 - useful for bus synchronisation) constexpr uint8_t EOF_ = 0x03; // End of Frame byte (End of Text code in ASCII table) constexpr uint8_t ESC_ = 0x1B; // Escape byte (Escape code in ASCII table) //-------------------------------------------------------------- /** * @class FrameReceiver * @brief Stateful receiver for extracting a complete binary frame from a byte stream. * * Handles frame synchronization, byte un-escaping, and buffering. * The frame is considered complete when the protocol's end-of-frame byte is detected. * * @tparam BUFFER_SIZE Maximum size of the internal receive buffer. * * Usage: * - Call push(byte) for each received byte. * - When push() returns true or isFrameAvailable() is true, use getFrame() to access the frame data. * * @code * FrameReceiver<128> receiver; * for (uint8_t byte : incomingBytes) { * if (receiver.push(byte)) { * auto frame = receiver.getFrame(); * // Process frame... * * receiver.reset(); * } * } * @endcode */ template class FrameReceiver { enum class State : uint8_t { WAIT_SOF1, WAIT_SOF2, RECEIVE_DATA, WAIT_EOF, ESCAPE_NEXT, END, }; public: FrameReceiver() = default; // Default constructor ~FrameReceiver() = default; // Default destructor FrameReceiver(const FrameReceiver &obj) = default; // Copy constructor FrameReceiver(FrameReceiver &&obj) noexcept = default; // Move constructor FrameReceiver &operator=(const FrameReceiver &obj) = default; // Copy assignment operator FrameReceiver &operator=(FrameReceiver &&obj) noexcept = default; // Move assignment operator /** * @brief Resets the receiver state and clears the internal buffer. * * After calling this method, the receiver is ready to process a new frame. */ void reset(); /** * @brief Pushes a byte into the receiver buffer and updates the state machine. * @param byte The received byte to process. * @return true if a complete frame has been received and is available, false otherwise. * * @note If a frame is available, use getFrame() to access its content. */ [[nodiscard]] bool push(uint8_t byte); /** * @brief Checks if a complete frame is available in the buffer. * @return true if a frame is available, false otherwise. */ [[nodiscard]] bool isFrameAvailable() const; /** * @brief Returns a span to the complete frame data. * @return A span containing the frame bytes, or an empty span if no frame is available. * * @note The returned span is only valid until the next reset() or push(). */ [[nodiscard]] std::span getFrame() const; protected: std::array m_buffer{}; // Receiver buffer size_t m_index = 0; // Current index in the buffer State m_state = State::WAIT_SOF1; // Current state of the receiver private: void appendByte(uint8_t byte); // Append a byte to the buffer }; //-------------------------------------------------------------- template inline void FrameReceiver::reset() { m_index = 0; m_state = State::WAIT_SOF1; } //-------------------------------------------------------------- template inline bool FrameReceiver::push(const uint8_t byte) { switch (m_state) { // Initial state: waiting for SOF1 case State::WAIT_SOF1: { if (byte == SOF1_) { m_index = 0; m_state = State::WAIT_SOF2; // Next byte should be SOF2 } break; } // Waiting for SOF2 case State::WAIT_SOF2: { if (byte == SOF2_) m_state = State::RECEIVE_DATA; // Start receiving data else m_state = State::WAIT_SOF1; // Abort frame reception break; } // Receiving data case State::RECEIVE_DATA: { if (byte == ESC_) m_state = State::ESCAPE_NEXT; // Next byte is escaped else if (byte == SOF1_) m_state = State::WAIT_SOF2; // Restart frame reception else if (byte == SOF2_) m_state = State::WAIT_SOF1; // Abort frame reception else if (byte == EOF_) { m_state = State::END; // Frame complete return true; } else appendByte(byte); break; } // Handling escaped byte case State::ESCAPE_NEXT: { appendByte(byte); m_state = State::RECEIVE_DATA; // Return to data reception break; } // Frame complete case State::END: default: return true; } return false; } //-------------------------------------------------------------- template inline bool FrameReceiver::isFrameAvailable() const { return m_state == State::END; } //-------------------------------------------------------------- template inline std::span FrameReceiver::getFrame() const { if (m_state == State::END) return { m_buffer.data(), m_index }; return {}; } //-------------------------------------------------------------- template inline void FrameReceiver::appendByte(const uint8_t byte) { if (m_index < BUFFER_SIZE) m_buffer[m_index++] = byte; } //-------------------------------------------------------------- /* --- */ //-------------------------------------------------------------- /** * @class MessageParser * @brief Parses and validates a binary frame, providing typed access to the payload. * * Checks frame structure, size, CRC, and end-of-frame marker. * Allows extraction of typed values or raw data from the payload. * * Usage: * - Construct with a frame buffer. * - Check validity with isValid() or getStatus(). * - Use get(offset, value) to extract typed data from the payload. * * @code * MessageParser parser(frame); * if (parser.isValid()) { * uint16_t id; * parser.get(0, id); * std::array data; * parser.get(2, std::span(data)); * // Use id and data... * } * @endcode */ class MessageParser { public: enum class MessageStatus : uint8_t { VALID, INVALID, BAD_SIZE, BAD_CRC, }; public: MessageParser() = delete; // Default constructor ~MessageParser() = default; // Default destructor MessageParser(const MessageParser &obj) = default; // Copy constructor MessageParser(MessageParser &&obj) noexcept = default; // Move constructor MessageParser &operator=(const MessageParser &obj) = default; // Copy assignment operator MessageParser &operator=(MessageParser &&obj) noexcept = default; // Move assignment operator /** * @brief Constructs a MessageParser and checks the integrity of the provided frame. * @param buffer The buffer containing the frame to parse. */ explicit MessageParser(std::span buffer); /** * @brief Checks if the parsed message is valid. * @return true if the message is valid, false otherwise. */ [[nodiscard]] bool isValid() const; /** * @brief Gets the current message validity status. * @return The status as a MessageStatus enum value. */ [[nodiscard]] MessageStatus getStatus() const; /** * @brief Gets the message payload buffer. * @return A span containing the payload bytes. */ [[nodiscard]] std::span getBuffer() const; /** * @brief Gets the size of the message payload. * @return The size of the payload in bytes. */ [[nodiscard]] size_t size() const; // Get message payload size /** * @brief Extracts a value of type T from the message payload at the specified offset. * @tparam T The type of the value to extract (POD type or std::span). * @param offset The offset in the payload to read from. * @param value Reference to store the extracted value. * @return The new offset after reading the value, or the original offset if reading failed. * * @note No exception is thrown. If extraction fails, value is not modified. */ template [[nodiscard]] size_t get(size_t offset, T &value) const; protected: std::span m_buffer; // Message buffer MessageStatus m_messageStatus = MessageStatus::INVALID; // Message validity status private: void messageCheck(); // Check message integrity }; //-------------------------------------------------------------- /* Constructor */ inline MessageParser::MessageParser(const std::span buffer) { m_buffer = buffer; messageCheck(); } //-------------------------------------------------------------- inline bool MessageParser::isValid() const { return m_messageStatus == MessageStatus::VALID; } //-------------------------------------------------------------- /* Get message validity status */ inline MessageParser::MessageStatus MessageParser::getStatus() const { return m_messageStatus; } //-------------------------------------------------------------- /* Get message payload buffer */ inline std::span MessageParser::getBuffer() const { return m_buffer; } //-------------------------------------------------------------- /* Get message payload size */ inline size_t MessageParser::size() const { return m_buffer.size(); } //-------------------------------------------------------------- template inline size_t MessageParser::get(size_t offset, T &value) const { static_assert((std::is_standard_layout_v && std::is_trivial_v) || // POD type std::is_same_v> || // std::span type std::is_same_v); // std::string type if constexpr (std::is_standard_layout_v && std::is_trivial_v) { // POD type constexpr size_t typeSize = sizeof(T); if (offset + typeSize <= m_buffer.size()) { std::memcpy(&value, m_buffer.data() + offset, typeSize); return offset + typeSize; // Return new offset } } else if constexpr (std::is_same_v>) { // std::span type if (offset + value.size() <= m_buffer.size()) { std::memcpy(value.data(), m_buffer.data() + offset, value.size()); return offset + value.size(); // Return new offset } } else if constexpr (std::is_same_v) { // std::string type uint8_t strSize; offset = get(offset, strSize); value.resize(strSize); std::span strSpan(reinterpret_cast(value.data()), strSize); offset = get(offset, strSpan); } return offset; // Unable to read value, return original offset } //-------------------------------------------------------------- /* Check message integrity */ inline void MessageParser::messageCheck() { /* Message format : [SOF1(1)][SOF2(1)][SIZE(1)][PAYLOAD(n)][CRC16(2)][EOF(1)] */ // Check SIZE const auto size = m_buffer[0]; if (constexpr size_t CRCSize = 2; m_buffer.size() != CRCSize + size + sizeof(size)) { m_messageStatus = MessageStatus::BAD_SIZE; return; } // Check CRC16 const auto loCRC = *(m_buffer.rbegin() + 1); const auto hiCRC = *m_buffer.rbegin(); const uint16_t frameCRC = static_cast(hiCRC << 8) | static_cast(loCRC); const uint16_t computedCRC = generic::CRC16_modbus::computeCRC(m_buffer.subspan(1, m_buffer.size() - 3)); if (frameCRC != computedCRC) { m_messageStatus = MessageStatus::BAD_CRC; return; } m_buffer = m_buffer.subspan(1, size); m_messageStatus = MessageStatus::VALID; } //-------------------------------------------------------------- /* --- */ //-------------------------------------------------------------- /** * @class MessageBuilderHelper * @brief Utility class to simplify the usage of MessageBuilder templates. * * This helper provides convenient methods and type aliases to work with MessageBuilder * instances without exposing template parameters in function signatures. */ class MessageBuilderHelper { public: MessageBuilderHelper() = default; // Default constructor virtual ~MessageBuilderHelper() = default; // Default destructor MessageBuilderHelper(const MessageBuilderHelper &obj) = default; // Copy constructor MessageBuilderHelper(MessageBuilderHelper &&obj) noexcept = default; // Move constructor MessageBuilderHelper &operator=(const MessageBuilderHelper &obj) = default; // Copy assignment operator MessageBuilderHelper &operator=(MessageBuilderHelper &&obj) noexcept = default; // Move assignment operator /** * @brief Gets the current message buffer as a span. * @return A span containing the current message bytes. */ [[nodiscard]] virtual std::span getBuffer() const = 0; /** * @brief Checks if the message buffer is full. * @return true if the buffer is full, false otherwise. */ [[nodiscard]] virtual bool isFull() const = 0; /** * @brief Gets the current size of the message buffer. * @return The number of bytes currently in the message buffer. */ [[nodiscard]] virtual size_t getBufferSize() const = 0; /** * @brief Gets the current CRC16 value of the message. * @return The current CRC16 value. */ [[nodiscard]] virtual uint16_t getCRC() const = 0; }; //-------------------------------------------------------------- /* --- */ //-------------------------------------------------------------- /** * @class MessageBuilder * @brief Builds a binary message payload with CRC calculation and escaping support. * * Supports appending POD types or byte spans, and computes CRC incrementally. * Provides methods to reset, append, and retrieve the current buffer and CRC. * * @tparam BUFFER_SIZE Maximum size of the internal message buffer. * * Usage: * - Use append() or operator<< to add data. * - Retrieve the buffer with getBuffer() and CRC with getCRC(). * * @code * MessageBuilder<64> builder; * builder << uint16_t(0x1234) << std::span(data, dataLen); * auto buffer = builder.getBuffer(); * auto crc = builder.getCRC(); * @endcode */ template class MessageBuilder : public MessageBuilderHelper { public: MessageBuilder(); // Default constructor virtual ~MessageBuilder() = default; // Default destructor MessageBuilder(const MessageBuilder &obj) = default; // Copy constructor MessageBuilder(MessageBuilder &&obj) noexcept = default; // Move constructor MessageBuilder &operator=(const MessageBuilder &obj) = default; // Copy assignment operator MessageBuilder &operator=(MessageBuilder &&obj) noexcept = default; // Move assignment operator /** * @brief Resets the message builder to an empty state. * @return Reference to the MessageBuilder for chaining. */ MessageBuilder &reset(); // Reset the message builder /** * @brief Appends a value of type T to the message buffer. * @tparam T The type of the value to append (POD type or std::span). * @param value The value to append. * @return Reference to the MessageBuilder for chaining. * * @note If appending the value would exceed the buffer size, it is ignored. */ template MessageBuilder &append(const T &value); // Append value of type T to the message /** * @brief Overloads the << operator to append a value of type T to the message buffer. * @tparam T The type of the value to append (POD type or std::span). * @param value The value to append. * @return Reference to the MessageBuilder for chaining. * * @note If appending the value would exceed the buffer size, it is ignored. */ template MessageBuilder &operator<<(const T &value); [[nodiscard]] std::span getBuffer() const override; [[nodiscard]] bool isFull() const override; [[nodiscard]] size_t getBufferSize() const override; [[nodiscard]] uint16_t getCRC() const override; protected: std::array m_buffer{}; // Receiver buffer size_t m_index = 0; // Current index in the buffer generic::CRC16_modbus m_CRC; // Current CRC value }; //-------------------------------------------------------------- template inline MessageBuilder::MessageBuilder() { m_CRC.init(); } //-------------------------------------------------------------- template inline MessageBuilder &MessageBuilder::reset() { m_index = 0; m_CRC.init(); return *this; // Return reference to self for chaining } //-------------------------------------------------------------- template template inline MessageBuilder &MessageBuilder::append(const T &value) { static_assert((std::is_standard_layout_v && std::is_trivial_v) || // POD type std::is_same_v> || // std::span type std::is_same_v> || // std::span type std::is_same_v || // std::string type std::is_same_v); // const std::string type if constexpr (std::is_standard_layout_v && std::is_trivial_v) { // POD type constexpr size_t typeSize = sizeof(T); if (m_index + typeSize <= BUFFER_SIZE) { std::array dataBuffer; std::memcpy(dataBuffer.data(), &value, typeSize); std::memcpy(m_buffer.data() + m_index, dataBuffer.data(), dataBuffer.size()); m_CRC.update(dataBuffer); m_index += typeSize; } } else if constexpr (std::is_same_v> || std::is_same_v>) { // std::span type if (m_index + value.size() <= BUFFER_SIZE) { std::memcpy(m_buffer.data() + m_index, value.data(), value.size()); m_CRC.update(value); m_index += value.size(); } } else if constexpr (std::is_same_v || std::is_same_v) { // std::string type if (m_index + (value.size() + sizeof(uint8_t)) <= BUFFER_SIZE) { append(static_cast(value.size())); std::memcpy(m_buffer.data() + m_index, value.data(), value.size()); m_CRC.update(std::span(reinterpret_cast(value.data()), value.size())); m_index += value.size(); } } return *this; // Return reference to self for chaining } //-------------------------------------------------------------- template template inline MessageBuilder &MessageBuilder::operator<<(const T &value) { append(value); return *this; // Return reference to self for chaining } //-------------------------------------------------------------- template inline std::span MessageBuilder::getBuffer() const { return { m_buffer.data(), m_index }; } //-------------------------------------------------------------- template inline bool MessageBuilder::isFull() const { return m_index >= BUFFER_SIZE; } //-------------------------------------------------------------- template inline size_t MessageBuilder::getBufferSize() const { return m_index; } //-------------------------------------------------------------- template inline uint16_t MessageBuilder::getCRC() const { return m_CRC.getCRC(); } //-------------------------------------------------------------- /* --- */ //-------------------------------------------------------------- /** * @class FramePacker * @brief Packs a message payload into a complete binary frame ready for transmission. * * Adds protocol headers, escapes special bytes, appends CRC and end-of-frame marker. * Ensures the packed frame fits in the provided buffer size. * * @tparam BUFFER_SIZE Maximum size of the output frame buffer. * * Usage: * - Call packMessage(payload, crc) to build the frame. * - Use isValid() to check for overflow. * - Retrieve the packed frame with getPackedFrame(). * * @code * FramePacker<256> packer; * if (packer.packMessage(payload, crc)) { * auto frame = packer.getPackedFrame(); * // Send frame... * } * @endcode */ template class FramePacker { static constexpr size_t CRC_SIZE = 4; // CRC16(2 x2) --> (CRC is 4 bytes to support escaped CRC) public: FramePacker() = default; // Default constructor explicit FramePacker(std::span message, uint16_t crc); // Constructor ~FramePacker() = default; // Default destructor FramePacker(const FramePacker &obj) = default; // Copy constructor FramePacker(FramePacker &&obj) noexcept = default; // Move constructor FramePacker &operator=(const FramePacker &obj) = default; // Copy assignment operator FramePacker &operator=(FramePacker &&obj) noexcept = default; // Move assignment operator /** * @brief Packs the input message into a complete frame with headers, CRC, and escaping. * @param message The message payload to pack. * @param crc The CRC16 value to append to the frame. * @return true if packing was successful without overflow, false otherwise. */ bool packMessage(std::span message, uint16_t crc); /** * @brief Checks if the packed frame is valid (no overflow occurred). * @return true if the packed frame is valid, false otherwise. */ [[nodiscard]] bool isValid() const; /** * @brief Gets the packed frame buffer as a span. * @return A span containing the packed frame bytes. */ [[nodiscard]] std::span getPackedFrame() const; protected: std::array m_frame{}; // Output buffer size_t m_index = 0; // Current index in output buffer std::array m_CRCBuffer{}; // CRC buffer with escaping size_t m_CRCSize = 0; // Current size of CRC buffer with escaping bool m_overflow = false; // Flag indicating if overflow occurred during packing private: void appendByte(uint8_t byte); // Append byte with escaping to output buffer void appendRawByte(uint8_t byte); // Append raw byte to output buffer void packCRC(uint16_t crc); // Pack CRC into CRC buffer with escaping }; //-------------------------------------------------------------- template inline FramePacker::FramePacker(const std::span message, const uint16_t crc) { packMessage(message, crc); } //-------------------------------------------------------------- template inline bool FramePacker::packMessage(const std::span message, const uint16_t crc) { // Reset state m_index = 0; m_overflow = false; // Prepare CRC buffer with escaping packCRC(crc); // Append header appendRawByte(SOF1_); // Append SOF1 appendRawByte(SOF2_); // Append SOF2 appendByte(static_cast(message.size())); // Append payload size // Append payload for (const auto &byte : message) appendByte(byte); // Append CRC for (size_t i = 0; i < m_CRCSize; i++) appendRawByte(m_CRCBuffer[i]); // Append EOF appendRawByte(EOF_); return !m_overflow; // Return true if packing was successful } //-------------------------------------------------------------- template inline bool FramePacker::isValid() const { return !m_overflow; } //-------------------------------------------------------------- template inline std::span FramePacker::getPackedFrame() const { return std::span(m_frame.data(), m_index); } //-------------------------------------------------------------- template inline void FramePacker::appendByte(const uint8_t byte) { if (byte == SOF1_ || byte == SOF2_ || byte == EOF_ || byte == ESC_) { appendRawByte(ESC_); // Append escape byte } appendRawByte(byte); // Append actual byte } //-------------------------------------------------------------- template inline void FramePacker::appendRawByte(const uint8_t byte) { if (m_index < BUFFER_SIZE) m_frame[m_index++] = byte; else m_overflow = true; } //-------------------------------------------------------------- template inline void FramePacker::packCRC(const uint16_t crc) { m_CRCSize = 0; const uint8_t loCRC = static_cast(crc & 0x00FF); const uint8_t hiCRC = static_cast((crc >> 8) & 0x00FF); if (loCRC == SOF1_ || loCRC == SOF2_ || loCRC == EOF_ || loCRC == ESC_) m_CRCBuffer[m_CRCSize++] = ESC_; m_CRCBuffer[m_CRCSize++] = loCRC; if (hiCRC == SOF1_ || hiCRC == SOF2_ || hiCRC == EOF_ || hiCRC == ESC_) m_CRCBuffer[m_CRCSize++] = ESC_; m_CRCBuffer[m_CRCSize++] = hiCRC; } //-------------------------------------------------------------- } // namespace sdi_ToolBox::comm::uBinaryFrame