/*! * * Copyright (c) 2024-2026 Diality Inc. - All Rights Reserved. * \copyright * THIS CODE MAY NOT BE COPIED OR REPRODUCED IN ANY FORM, IN PART OR IN * WHOLE, WITHOUT THE EXPLICIT PERMISSION OF THE COPYRIGHT OWNER. * * \file AgentMessage.h * \author (original) Stephen Quong * \date (original) 24-May-2026 * */ #pragma once #include /*! * \brief LeahiRt to Connectivity Agent message framing. * \details Implements the binary message framing protocol * between the LeahiRt application and the Agent application. * * The class is transport-agnostic — it has no dependency on any * socket or I/O class and can be used with any byte-stream transport * (QLocalSocket, QTcpSocket, CAN bus, test harness, etc.). * * \b Outbound: AgentMessage::build() constructs a complete wire-ready * frame from a message ID, sequence number, and optional payload. * * \b Inbound: feed() accepts raw bytes from the transport. The caller * invokes feed() each time bytes arrive and inspects the returned * FeedResult. When FeedResult::Complete is returned the decoded * message is available via msgId(), sequence(), and payload(). * reset() must be called before feeding the next message. * * \b Frame \b layout \b — \b header \b only \b (payload_length = 0): * \code * Byte: 0 1 2 3 4 5 6-9 10 11 * ┌─────────┬────────┬────────┬───────────┬────────┐ * │ AA 55 │ msg_id │ seq_num│ pay_length│hdr_crc │ * │ sync │uint16BE│uint16BE│ uint32 BE │uint16BE│ * └─────────┴────────┴────────┴───────────┴────────┘ * \endcode * * \b Frame \b layout \b — \b with \b payload \b (payload_length > 0): * \code * ┌── 12-byte header ──┬── N bytes payload ──┬── pay_crc (4 B) ──┐ * │ (see above) │ uint8[] │ CRC-32/ISO-HDLC │ * └────────────────────┴─────────────────────┴───────────────────┘ * \endcode * * Header CRC uses CRC-16/CCITT (poly 0x1021, init 0xFFFF, no reflection). * Payload CRC uses CRC-32/ISO-HDLC (IEEE 802.3, reflected poly 0xEDB88320). */ class AgentMessage { public: /*! * \brief MQTT routing identifier carried in every frame header. * \details The Connectivity Agent uses this value as a key into its * routing table to determine the MQTT topic for the message. */ enum class MsgId : quint16 { ClinicalData = 0x0001, ///< Clinical data → MQTT topic: clinical Diagnostic = 0x0002, ///< Diagnostic data → MQTT topic: service Ack = 0x0003, ///< Acknowledgement → response to LeahiRt Alarms = 0x0004, ///< Alarm data → MQTT topic: alarms Audit = 0x0005, ///< Audit data → MQTT topic: audit DeviceLogFile = 0x0006, ///< Device log file → MQTT topic: log TreatmentLogFile = 0x0007, ///< Treatment log → MQTT topic: tx_log CloudSyncLogFile = 0x0008, ///< CloudSync log → MQTT topic: cs_log }; /*! * \brief Result returned by feed() after processing each byte chunk. */ enum class FeedResult { Incomplete, ///< More bytes needed — continue feeding. Complete, ///< Full valid frame assembled — read accessors, then call reset(). HeaderError, ///< Header CRC mismatch — frame dropped, state reset automatically. PayloadError, ///< Payload CRC mismatch or oversized payload — frame dropped, state reset automatically. }; /*! * \brief Build a complete wire-ready frame. * \details Constructs the 12-byte header, computes the CRC-16/CCITT header * integrity field, and when payload is non-empty appends the payload * followed by its CRC-32/ISO-HDLC integrity field. * \param msgId Message identifier used by the Agent for MQTT routing. * \param sequence Caller-managed sequence number for message tracking and ACK correlation. * \param payload Optional message payload. Pass an empty QByteArray for zero-length * control frames (e.g. ACK). Payload must not exceed 64 KB. * \return QByteArray containing the complete frame ready to write to the transport. */ static QByteArray build(MsgId msgId, quint16 sequence, const QByteArray &payload = {}); /*! * \brief Feed raw bytes from the transport into the inbound state machine. * \details Consumes only the bytes needed to advance or complete the current * frame, then returns. Any bytes beyond what the current frame * requires are left in \p bytes so the caller can pass the same * buffer back to feed() to begin parsing the next frame. * * The caller owns the buffer and is responsible for draining it: * \code * QByteArray buf = _socket->readAll(); * AgentMessage::FeedResult result; * do { * result = _msg.feed(buf); * if (result == AgentMessage::FeedResult::Complete) { * handleMessage(_msg.msgId(), _msg.sequence(), _msg.payload()); * _msg.reset(); * } * } while (result == AgentMessage::FeedResult::Complete && !buf.isEmpty()); * \endcode * * \param bytes Buffer of raw bytes from the transport. Bytes consumed by * this call are removed from the front of the buffer. * \return FeedResult indicating the parser outcome for this call. */ FeedResult feed(QByteArray &bytes); /*! * \brief Message identifier of the last successfully parsed frame. * \note Valid only after feed() returns FeedResult::Complete. */ MsgId msgId() const { return _rxMsgId; } /*! * \brief Sequence number of the last successfully parsed frame. * \note Valid only after feed() returns FeedResult::Complete. */ quint16 sequence() const { return _rxSequence; } /*! * \brief Payload of the last successfully parsed frame. * \details Returns an empty QByteArray for zero-length frames. * \note Valid only after feed() returns FeedResult::Complete. */ QByteArray payload() const { return _rxPayload; } /*! * \brief Reset inbound parser state. * \details Clears all accumulated header bytes and decoded fields, returning * the parser to its initial sync-scanning state. Must be called by * the caller after consuming a Complete frame. Error results call * reset() internally. The caller's buffer is unaffected — bytes * already removed from it by feed() are not restored. */ void reset(); private: /*! * \brief Compute a CRC-16/CCITT integrity value. * \details Parameters: poly 0x1021, init 0xFFFF, no input or output * reflection, no final XOR. Used for header integrity. * Verify implementation with check value: crc16ccitt("123456789", 9) == 0x29B1. * \param data Pointer to the input data bytes. * \param len Number of bytes to process. * \return 16-bit CRC value. */ static quint16 crc16ccitt(const quint8 *data, int len); /*! * \brief Compute a CRC-32/ISO-HDLC integrity value. * \details Parameters: reflected poly 0xEDB88320 (IEEE 802.3), init 0xFFFFFFFF, * input and output reflected, final XOR 0xFFFFFFFF. Used for payload integrity. * Verify implementation with check value: crc32isohdlc("123456789", 9) == 0xCBF43926. * \param data Pointer to the input data bytes. * \param len Number of bytes to process. * \return 32-bit CRC value. */ static quint32 crc32isohdlc(const quint8 *data, int len); static constexpr int SYNC_SIZE = 2; ///< Sync word length in bytes. static constexpr quint8 SYNC[SYNC_SIZE] = {0xAA, 0x55}; ///< Sync word byte sequence. static constexpr int HEADER_SIZE = 12; ///< Fixed header length in bytes. static constexpr int MSGID_SIZE = 2; ///< msg_id field length in bytes. static constexpr int SEQUENCE_SIZE = 2; ///< sequence_num field length in bytes. static constexpr int HEADER_CRC_SIZE = 2; ///< Header CRC field length in bytes. static constexpr int PAYLOAD_CRC_SIZE = 4; ///< Payload CRC field length in bytes. static constexpr quint32 MAX_PAYLOAD_LEN = 64 * 1024; ///< Maximum permitted payload length. QByteArray _headerBuf; ///< Partial header bytes accumulated across feed() calls. MsgId _rxMsgId = MsgId::ClinicalData; ///< Parsed msg_id, valid after header is complete. quint16 _rxSequence = 0; ///< Parsed sequence number, valid after header is complete. quint32 _rxPayloadLen = 0; ///< Parsed payload_length, valid after header is complete. QByteArray _rxPayload; ///< Decoded payload of the last complete frame. };