/*! * * 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.cpp * \author (original) Stephen Quong * \date (original) 24-May-2026 * */ #include "AgentMessage.h" #include // --------------------------------------------------------------------------- // Outbound // --------------------------------------------------------------------------- /*! * \brief AgentMessage::build * \details Builds a complete wire-ready frame with header and optional payload CRCs. * \param msgId - message identifier for Agent MQTT topic * \param sequence - caller-managed sequence number * \param payload - optional payload; pass empty for zero-length frames (e.g. Ack) * \return complete frame ready to write to the transport */ QByteArray AgentMessage::build(MsgId msgId, quint16 sequence, const QByteArray &payload) { const quint32 payloadLen = static_cast(payload.size()); // Header: sync(2) + msg_id(2) + sequence(2) + payload_length(4) + header_crc(2) = 12 bytes QByteArray msg(HEADER_SIZE, Qt::Uninitialized); quint8 *header = reinterpret_cast(msg.data()); header[0] = SYNC[0]; header[1] = SYNC[1]; qToBigEndian(static_cast(msgId), header + SYNC_SIZE); qToBigEndian(sequence, header + SYNC_SIZE + MSGID_SIZE); qToBigEndian(payloadLen, header + SYNC_SIZE + MSGID_SIZE + SEQUENCE_SIZE); const quint16 hCrc = crc16ccitt(header, HEADER_SIZE - HEADER_CRC_SIZE); qToBigEndian(hCrc, header + HEADER_SIZE - HEADER_CRC_SIZE); if (payloadLen > 0) { msg.append(payload); const quint32 crc = crc32isohdlc(reinterpret_cast(payload.constData()), payload.size()); QByteArray crcBytes(PAYLOAD_CRC_SIZE, Qt::Uninitialized); qToBigEndian(crc, reinterpret_cast(crcBytes.data())); msg.append(crcBytes); } return msg; } // --------------------------------------------------------------------------- // Inbound // --------------------------------------------------------------------------- /*! * \brief AgentMessage::feed * \details Feeds raw bytes into the inbound parser state machine. * Consumed bytes are removed from the front of the buffer. * On HeaderError or PayloadError the caller may call feed() again * immediately if the buffer is non-empty. * \param bytes - raw bytes from the transport; modified in-place * \return FeedResult indicating the parser outcome */ AgentMessage::FeedResult AgentMessage::feed(QByteArray &bytes) { int pos = 0; FeedResult result = FeedResult::Incomplete; // scan for a valid header — skipped when _headerBuf is already populated from a prior feed() call while (_headerBuf.size() == 0 && bytes.size() - pos >= HEADER_SIZE && result != FeedResult::HeaderError) { if (static_cast(bytes.at(pos)) == SYNC[0] && static_cast(bytes.at(pos + 1)) == SYNC[1]) { _headerBuf.append(bytes.constData() + pos, HEADER_SIZE); if (crc16ccitt(reinterpret_cast(_headerBuf.constData()), HEADER_SIZE - HEADER_CRC_SIZE) == qFromBigEndian(reinterpret_cast(_headerBuf.constData() + HEADER_SIZE - HEADER_CRC_SIZE))) { const quint8 *header = reinterpret_cast(_headerBuf.constData()); int header_pos = SYNC_SIZE; _rxMsgId = static_cast(qFromBigEndian(header + header_pos)); header_pos += MSGID_SIZE; _rxSequence = qFromBigEndian(header + header_pos); header_pos += SEQUENCE_SIZE; _rxPayloadLen = qFromBigEndian(header + header_pos); pos += HEADER_SIZE; } else { // TODO: log the header CRC failure _headerBuf.clear(); pos += SYNC_SIZE; result = FeedResult::HeaderError; } } else { ++pos; } } // process payload if a valid header has been accumulated if (result != FeedResult::HeaderError && _headerBuf.size() == HEADER_SIZE) { if (_rxPayloadLen == 0) { result = FeedResult::Complete; } else if (_rxPayloadLen > MAX_PAYLOAD_LEN) { // TODO: log the oversized payload _headerBuf.clear(); result = FeedResult::PayloadError; } else if (bytes.size() - pos >= static_cast(_rxPayloadLen) + PAYLOAD_CRC_SIZE) { const quint8 *payload = reinterpret_cast(bytes.constData() + pos); if (crc32isohdlc(payload, static_cast(_rxPayloadLen)) == qFromBigEndian(payload + _rxPayloadLen)) { _rxPayload = QByteArray(reinterpret_cast(payload), static_cast(_rxPayloadLen)); pos += static_cast(_rxPayloadLen) + PAYLOAD_CRC_SIZE; result = FeedResult::Complete; } else { // TODO: log the payload CRC failure _headerBuf.clear(); result = FeedResult::PayloadError; } } } // remove the consumed bytes from the input buffer bytes.remove(0, pos); return result; } /*! * \brief AgentMessage::reset * \details Resets the inbound parser to its initial sync-scanning state. * Must be called after consuming a Complete frame. */ void AgentMessage::reset() { _headerBuf.clear(); _rxMsgId = MsgId::ClinicalData; _rxSequence = 0; _rxPayloadLen = 0; _rxPayload.clear(); } /*! * \brief AgentMessage::crc16ccitt * \details CRC-16/CCITT: poly 0x1021, init 0xFFFF, no reflection, no final XOR. * Check value: crc16ccitt("123456789", 9) == 0x29B1. * \param data - input data bytes * \param len - number of bytes to process * \return 16-bit CRC value */ quint16 AgentMessage::crc16ccitt(const quint8 *data, int len) { quint16 crc = 0xFFFF; for (int i = 0; i < len; ++i) { crc ^= static_cast(data[i]) << 8; for (int bit = 0; bit < 8; ++bit) { if (crc & 0x8000) { crc = (crc << 1) ^ 0x1021; } else { crc <<= 1; } } } return crc; } /*! * \brief AgentMessage::crc32isohdlc * \details CRC-32/ISO-HDLC: reflected poly 0xEDB88320 (IEEE 802.3), init 0xFFFFFFFF, * input and output reflected, final XOR 0xFFFFFFFF. * Check value: crc32isohdlc("123456789", 9) == 0xCBF43926. * \param data - input data bytes * \param len - number of bytes to process * \return 32-bit CRC value */ quint32 AgentMessage::crc32isohdlc(const quint8 *data, int len) { quint32 crc = 0xFFFFFFFF; for (int i = 0; i < len; ++i) { crc ^= data[i]; for (int bit = 0; bit < 8; ++bit) { if (crc & 1) { crc = (crc >> 1) ^ 0xEDB88320; } else { crc >>= 1; } } } return crc ^ 0xFFFFFFFF; }