Index: LeahiRt/CMakeLists.txt =================================================================== diff -u -r6f9f24886c73d92b380e4c3afd82c0b19559d42d -r401d8ad0e5c070f9c6ecb41ed3ba25ba3d1c69a0 --- LeahiRt/CMakeLists.txt (.../CMakeLists.txt) (revision 6f9f24886c73d92b380e4c3afd82c0b19559d42d) +++ LeahiRt/CMakeLists.txt (.../CMakeLists.txt) (revision 401d8ad0e5c070f9c6ecb41ed3ba25ba3d1c69a0) @@ -11,8 +11,8 @@ set(CMAKE_AUTOMOC ON) -find_package(QT NAMES Qt6 Qt5 REQUIRED COMPONENTS Core WebSockets) -find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Core WebSockets) +find_package(QT NAMES Qt6 Qt5 REQUIRED COMPONENTS Core Network) +find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Core Network) find_package(Comms HINTS ${CMAKE_CURRENT_SOURCE_DIR}/../lib/Comms REQUIRED) find_package(MsgUtils HINTS ${CMAKE_CURRENT_SOURCE_DIR}/../lib/MsgUtils REQUIRED) @@ -38,6 +38,6 @@ target_link_libraries(${PROJECT_NAME} PRIVATE Comms MsgUtils - Qt${QT_VERSION_MAJOR}::WebSockets Qt${QT_VERSION_MAJOR}::Core + Qt${QT_VERSION_MAJOR}::Network ) Index: LeahiRt/LeahiRtController.cpp =================================================================== diff -u -rb71228d6701cbd89b6691c8e5bbb891550b6290c -r401d8ad0e5c070f9c6ecb41ed3ba25ba3d1c69a0 --- LeahiRt/LeahiRtController.cpp (.../LeahiRtController.cpp) (revision b71228d6701cbd89b6691c8e5bbb891550b6290c) +++ LeahiRt/LeahiRtController.cpp (.../LeahiRtController.cpp) (revision 401d8ad0e5c070f9c6ecb41ed3ba25ba3d1c69a0) @@ -1,50 +1,44 @@ -#include -#include // SQ +#include #include #include "LeahiRtController.h" #include "LeahiMsgDefs.h" -LeahiRtController::LeahiRtController(QObject *parent) : +LeahiRtController::LeahiRtController(const QString &configPath, QObject *parent) : QObject(parent), + _settings(configPath, QSettings::IniFormat), _canInterface(), _canThread(this), - _clientSocket(QString(), QWebSocketProtocol::VersionLatest, this), _msgBuilder(this) - // SQ _protoInterface(), - // SQ _protoThread(this) { _canInterface.init(_canThread); connect(&_canInterface, &Can::CanInterface::didFrameReceive, this, &LeahiRtController::onFrameReceive); - - // SQ _protoInterface.init(_protoThread); - // SQ connect(this, &LeahiRtController::didCanMessageReceive, &_protoInterface, &proto::ProtoInterface::onCanMessageReceive); - - connect(&_clientSocket, &QWebSocket::connected, [&]() { qDebug().noquote() << "Socket connected"; }); } LeahiRtController::~LeahiRtController() { - _clientSocket.close(); + _canThread.quit(); + _canThread.wait(); + + _agentThread.quit(); + _agentThread.wait(); } -void LeahiRtController::openSocket(const QString host, const unsigned int port) +void LeahiRtController::connectToAgent() { - _clientSocket.close(); - _clientSocket.open(QUrl(QString("ws://%1:%2").arg(host).arg(port))); + const QString socketPath = _settings.value("Socket/LocalSocketName", "/tmp/leahi_rt.sock").toString(); + const int reconnectIntervalMs = _settings.value("Socket/ReconnectIntervalMs", 5000).toInt(); + _agentInterface.init(socketPath, reconnectIntervalMs, _agentThread); } void LeahiRtController::onFrameReceive(const QCanBusFrame frame) { const Can::CanId canId = Can::CanId(frame.frameId()); Can::Message &msg = _messages[canId]; - // construct the message from the received frame and determine if a complete message has been received if (_msgBuilder.buildMessage(frame.payload(), msg, canId) && msg.isComplete()) { - // SQ Q_EMIT didCanMessageReceive(QDateTime::currentDateTime(), msg); - // if (_clientSocket.isValid()) { - qDebug().noquote() << QString("Received message with MsgId=0x%1").arg(QString("%1").arg(msg.msgId, 4, 16, QChar('0')).toUpper()); - _clientSocket.sendBinaryMessage(leahi::canMessageToProtobufByteArray(QDateTime::currentDateTime(), QStringLiteral("test_device"), msg)); - // } + qDebug().noquote() << QString("Received message with MsgId=0x%1").arg(QString("%1").arg(msg.msgId, 4, 16, QChar('0')).toUpper()); + const QByteArray payload = leahi::canMessageToProtobufByteArray(QDateTime::currentDateTime(), QStringLiteral("test_device"), msg); + _agentInterface.send(AgentMessage::MsgId::ClinicalData, _txSequence++, payload); msg.clear(); } } Index: LeahiRt/LeahiRtController.h =================================================================== diff -u -r6f9f24886c73d92b380e4c3afd82c0b19559d42d -r401d8ad0e5c070f9c6ecb41ed3ba25ba3d1c69a0 --- LeahiRt/LeahiRtController.h (.../LeahiRtController.h) (revision 6f9f24886c73d92b380e4c3afd82c0b19559d42d) +++ LeahiRt/LeahiRtController.h (.../LeahiRtController.h) (revision 401d8ad0e5c070f9c6ecb41ed3ba25ba3d1c69a0) @@ -4,13 +4,15 @@ #include #include #include +#include +#include #include -#include +#include "AgentInterface.h" +#include "AgentMessage.h" #include "CanInterface.h" #include "CanMessage.h" #include "MessageBuilder.h" -// SQ #include "ProtoInterface.h" using namespace Can; @@ -19,19 +21,20 @@ Q_OBJECT public: - explicit LeahiRtController(QObject *parent = nullptr); + explicit LeahiRtController(const QString &configPath, QObject *parent = nullptr); ~LeahiRtController(); - void openSocket(const QString host, const unsigned int port=80); + void connectToAgent(); private: + QSettings _settings; Can::CanInterface _canInterface; QThread _canThread; - QWebSocket _clientSocket; Can::MessageBuilder _msgBuilder; QMap _messages; - // SQ proto::ProtoInterface _protoInterface; - // SQ QThread _protoThread; + AgentInterface _agentInterface; + QThread _agentThread; + quint16 _txSequence = 0; Q_SIGNALS: void didCanMessageReceive(const QDateTime timestamp, const Can::Message msg); Index: LeahiRt/main.cpp =================================================================== diff -u -r6f9f24886c73d92b380e4c3afd82c0b19559d42d -r401d8ad0e5c070f9c6ecb41ed3ba25ba3d1c69a0 --- LeahiRt/main.cpp (.../main.cpp) (revision 6f9f24886c73d92b380e4c3afd82c0b19559d42d) +++ LeahiRt/main.cpp (.../main.cpp) (revision 401d8ad0e5c070f9c6ecb41ed3ba25ba3d1c69a0) @@ -1,7 +1,8 @@ -#include +#include +#include #include #include -#include +#include #include #include "LeahiRtController.h" @@ -33,9 +34,23 @@ QCoreApplication app(argc, argv); app.setApplicationName("LeahiRt"); app.setApplicationVersion("1.0"); + + QCommandLineParser parser; + parser.setApplicationDescription("Leahi Real-time Cloud Data Transmission daemon."); + parser.addHelpOption(); + parser.addVersionOption(); - LeahiRtController rtController; - rtController.openSocket("localhost", 80); + QCommandLineOption configOption( + {"c", "config"}, + "Path to the configuration INI file.", + "config", + QDir(app.applicationDirPath()).filePath("config/LeahiRt.ini") + ); + parser.addOption(configOption); + parser.process(app); + LeahiRtController rtController(parser.value(configOption)); + rtController.connectToServer(); + return app.exec(); } Index: lib/Comms/CMakeLists.txt =================================================================== diff -u -rfe400f72d9b81e4b496fffd58d87120485bda5e6 -r401d8ad0e5c070f9c6ecb41ed3ba25ba3d1c69a0 --- lib/Comms/CMakeLists.txt (.../CMakeLists.txt) (revision fe400f72d9b81e4b496fffd58d87120485bda5e6) +++ lib/Comms/CMakeLists.txt (.../CMakeLists.txt) (revision 401d8ad0e5c070f9c6ecb41ed3ba25ba3d1c69a0) @@ -11,13 +11,13 @@ find_package(MsgUtils HINTS ${CMAKE_CURRENT_SOURCE_DIR}/../MsgUtils REQUIRED) set(INCLUDES - include/AgentMessage.h + include/AgentInterface.h include/CanInterface.h include/ProtoInterface.h ) set(SRCS - src/AgentMessage.cpp + src/AgentInterface.cpp src/CanInterface.cpp src/ProtoInterface.cpp ) Index: lib/Comms/include/AgentInterface.h =================================================================== diff -u -r088513e6ea7bad08b4fb7862127c726eabad18fd -r401d8ad0e5c070f9c6ecb41ed3ba25ba3d1c69a0 --- lib/Comms/include/AgentInterface.h (.../AgentInterface.h) (revision 088513e6ea7bad08b4fb7862127c726eabad18fd) +++ lib/Comms/include/AgentInterface.h (.../AgentInterface.h) (revision 401d8ad0e5c070f9c6ecb41ed3ba25ba3d1c69a0) @@ -22,45 +22,84 @@ #include "AgentMessage.h" /*! - * \brief UDS interface to the Connectivity Agent - * \details Manages the QLocalSocket connection to the Connectivity Agent + * \brief UDS interface to the Connectivity Agent. + * \details Manages the QLocalSocket connection to the Connectivity Agent, * including automatic reconnection on disconnect or error. - * Outbound: call send() to write an AgentMessage frame to the socket. - * Inbound: emits didMessageReceive() for each complete parsed frame. + * + * \b Outbound: call send() to build and write an AgentMessage frame + * to the socket. Returns false if the socket is not connected. + * + * \b Inbound: raw bytes received from the socket are fed through + * an AgentMessage parser. When a complete frame is assembled the + * didMessageReceive() signal is emitted with the decoded fields. + * + * Call init() with the socket path, reconnect interval, and an optional + * QThread to run the interface on a dedicated thread. The class handles + * all reconnect attempts internally. */ class AgentInterface : public QObject { Q_OBJECT public: - AgentInterface(QObject *parent = nullptr); + /*! + * \brief Construct an AgentInterface. + * \param parent Optional QObject parent. + */ + explicit AgentInterface(QObject *parent = nullptr); + /*! + * \brief Initialize and connect to the Connectivity Agent UDS socket. + * \details Stores the socket path and reconnect interval, then initiates + * the first connection attempt. Reconnection is handled automatically + * on disconnect or error. + * \param socketPath Path to the Unix domain socket. + * \param reconnectIntervalMs Interval in milliseconds between reconnect attempts. + */ bool init(const QString &socketPath, int reconnectIntervalMs); + + /*! + * \brief Initialize on a dedicated thread and connect to the Connectivity Agent UDS socket. + * \details Calls init() then moves this object onto \p thread. + * Must be called from the main thread. + * \param socketPath Path to the Unix domain socket. + * \param reconnectIntervalMs Interval in milliseconds between reconnect attempts. + * \param thread The thread to move this object onto. + */ bool init(const QString &socketPath, int reconnectIntervalMs, QThread &thread); + + /*! + * \brief Send a message to the Connectivity Agent. + * \details Builds a wire-ready AgentMessage frame and writes it to the socket. + * \param msgId Message identifier for Agent MQTT routing. + * \param sequence Caller-managed sequence number. + * \param payload Message payload. Pass an empty QByteArray for zero-length frames. + * \return true if the frame was written to the socket, false if not connected. + */ bool send(AgentMessage::MsgId msgId, quint16 sequence, const QByteArray &payload = {}); public Q_SLOTS: + /*! + * \brief Quit the interface and move back to the main thread for destruction. + */ void quit(); Q_SIGNALS: /*! - * \brief didMessageReceive - * \details Emitted when a complete inbound AgentMessage frame has been parsed. - * \param msgId - message identifier from the frame header - * \param sequence - sequence number from the frame header - * \param payload - decoded payload bytes, empty for zero-length frames + * \brief Emitted when a complete inbound AgentMessage frame has been parsed. + * \param msgId Message identifier from the frame header. + * \param sequence Sequence number from the frame header. + * \param payload Decoded payload bytes, empty for zero-length frames. */ void didMessageReceive(AgentMessage::MsgId msgId, quint16 sequence, QByteArray payload); /*! - * \brief didConnect - * \details Emitted when the socket connects successfully. + * \brief Emitted when the socket connects successfully. */ void didConnect(); /*! - * \brief didDisconnect - * \details Emitted when the socket disconnects. + * \brief Emitted when the socket disconnects. */ void didDisconnect(); Fisheye: Tag 401d8ad0e5c070f9c6ecb41ed3ba25ba3d1c69a0 refers to a dead (removed) revision in file `lib/Comms/include/AgentMessage.h'. Fisheye: No comparison available. Pass `N' to diff? Index: lib/Comms/src/AgentInterface.cpp =================================================================== diff -u -rccc40a7e73e9ee5d2a5fb56f3f2bea4f8294900f -r401d8ad0e5c070f9c6ecb41ed3ba25ba3d1c69a0 --- lib/Comms/src/AgentInterface.cpp (.../AgentInterface.cpp) (revision ccc40a7e73e9ee5d2a5fb56f3f2bea4f8294900f) +++ lib/Comms/src/AgentInterface.cpp (.../AgentInterface.cpp) (revision 401d8ad0e5c070f9c6ecb41ed3ba25ba3d1c69a0) @@ -21,8 +21,8 @@ /*! * \brief AgentInterface::AgentInterface - * \details Constructor - * \param parent - optional QObject parent + * \details Constructor. Wires all socket and timer signal/slot connections. + * \param parent Optional QObject parent. */ AgentInterface::AgentInterface(QObject *parent) : QObject(parent) { @@ -37,11 +37,11 @@ /*! * \brief AgentInterface::init - * \details Stores the socket path and reconnect interval and initiates the - * first connection attempt. Guards against double-initialisation. - * \param socketPath - path to the Unix domain socket - * \param reconnectIntervalMs - milliseconds between reconnect attempts - * \return true on success, false if already initialised + * \details Stores the socket path and reconnect interval, then initiates the first + * connection attempt. Guards against double-initialisation. + * \param socketPath Path to the Unix domain socket. + * \param reconnectIntervalMs Interval in milliseconds between reconnect attempts. + * \return true on success, false if already initialised. */ bool AgentInterface::init(const QString &socketPath, int reconnectIntervalMs) { @@ -58,12 +58,12 @@ /*! * \brief AgentInterface::init - * \details Calls init() then moves this object onto vThread. - * Must be called from the main thread. - * \param socketPath - path to the Unix domain socket - * \param reconnectIntervalMs - milliseconds between reconnect attempts - * \param thread - the thread to move this object onto - * \return true on success, false if already initialised + * \details Calls init() then moves this object onto \p thread. Must be called + * from the main thread. + * \param socketPath Path to the Unix domain socket. + * \param reconnectIntervalMs Interval in milliseconds between reconnect attempts. + * \param thread The thread to move this object onto. + * \return true on success, false if already initialised. */ bool AgentInterface::init(const QString &socketPath, int reconnectIntervalMs, QThread &thread) { @@ -76,11 +76,11 @@ /*! * \brief AgentInterface::send - * \details Builds an AgentMessage frame and writes it to the socket. - * \param msgId - message identifier for Agent MQTT topic - * \param sequence - caller-managed sequence number - * \param payload - message payload; pass empty for zero-length frames - * \return true if written to the socket, false if not connected + * \details Builds a wire-ready AgentMessage frame and writes it to the socket. + * \param msgId Message identifier for Agent MQTT routing. + * \param sequence Caller-managed sequence number. + * \param payload Message payload. Pass an empty QByteArray for zero-length frames. + * \return true if the frame was written to the socket, false if not connected. */ bool AgentInterface::send(AgentMessage::MsgId msgId, quint16 sequence, const QByteArray &payload) { @@ -107,9 +107,9 @@ /*! * \brief AgentInterface::initThread - * \details Moves this object onto vThread and starts it. - * Must be called from the main thread. - * \param thread - the thread to move this object onto + * \details Names the thread, connects aboutToQuit to quit(), moves this object + * onto the thread, and starts it. Must be called from the main thread. + * \param thread The thread to move this object onto. */ void AgentInterface::initThread(QThread &thread) { @@ -123,7 +123,8 @@ /*! * \brief AgentInterface::quitThread - * \details Moves this object back to the main thread. + * \details Moves this object back to the main thread so it can be safely + * destroyed by its owner. */ void AgentInterface::quitThread() { @@ -159,7 +160,7 @@ /*! * \brief AgentInterface::onError * \details Logs the socket error and starts the reconnect timer if not already running. - * \param error - the socket error code + * \param error The socket error code. */ void AgentInterface::onError(QLocalSocket::LocalSocketError error) { @@ -171,8 +172,8 @@ /*! * \brief AgentInterface::onReadyRead - * \details Drains incoming bytes through the AgentMessage parser and emits - * didMessageReceive() for each complete frame. + * \details Appends incoming bytes to the receive buffer and drains it through + * the AgentMessage parser. Emits didMessageReceive() for each complete frame. */ void AgentInterface::onReadyRead() { Fisheye: Tag 401d8ad0e5c070f9c6ecb41ed3ba25ba3d1c69a0 refers to a dead (removed) revision in file `lib/Comms/src/AgentMessage.cpp'. Fisheye: No comparison available. Pass `N' to diff? Index: lib/MsgUtils/CMakeLists.txt =================================================================== diff -u -rb71228d6701cbd89b6691c8e5bbb891550b6290c -r401d8ad0e5c070f9c6ecb41ed3ba25ba3d1c69a0 --- lib/MsgUtils/CMakeLists.txt (.../CMakeLists.txt) (revision b71228d6701cbd89b6691c8e5bbb891550b6290c) +++ lib/MsgUtils/CMakeLists.txt (.../CMakeLists.txt) (revision 401d8ad0e5c070f9c6ecb41ed3ba25ba3d1c69a0) @@ -34,6 +34,7 @@ set(LEAHI_MSG_CONF ${CMAKE_CURRENT_SOURCE_DIR}/../../data/LeahiUnhandled.conf) set(INCLUDES + include/AgentMessage.h include/CanMessage.h include/crc.h # include/DenaliLogUtils.h @@ -50,6 +51,7 @@ ) set(SRCS + src/AgentMessage.cpp src/crc.cpp # src/DenaliLogUtils.cpp # src/encryption.cpp Index: lib/MsgUtils/include/AgentMessage.h =================================================================== diff -u -rccc40a7e73e9ee5d2a5fb56f3f2bea4f8294900f -r401d8ad0e5c070f9c6ecb41ed3ba25ba3d1c69a0 --- lib/MsgUtils/include/AgentMessage.h (.../AgentMessage.h) (revision ccc40a7e73e9ee5d2a5fb56f3f2bea4f8294900f) +++ lib/MsgUtils/include/AgentMessage.h (.../AgentMessage.h) (revision 401d8ad0e5c070f9c6ecb41ed3ba25ba3d1c69a0) @@ -15,101 +15,175 @@ #include /*! - * \brief LeahiRt to Connectivity Agent message framing - * \details Transport-agnostic binary framing. build() makes a wire-ready frame; - * feed() parses inbound bytes (see FeedResult). On Complete, read - * msgId()/sequence()/payload(), then reset(). + * \brief LeahiRt to Connectivity Agent message framing. + * \details Implements the binary message framing protocol + * between the LeahiRt application and the Agent application. * - * Frame layout — header only (payload_length == 0): + * 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 * - * Frame layout — with payload (payload_length > 0): - * + * \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: CRC-16/CCITT (poly 0x1021, init 0xFFFF, no reflection). - * Payload CRC: CRC-32/ISO-HDLC (IEEE 802.3, reflected poly 0xEDB88320). + * 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 topic identifier carried in every frame header - * \details The Connectivity Agent uses this value to determine the MQTT topic. + * \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, - Diagnostic = 0x0002, - Ack = 0x0003, - Alarms = 0x0004, - Audit = 0x0005, - DeviceLogFile = 0x0006, - TreatmentLogFile = 0x0007, - CloudSyncLogFile = 0x0008, + 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 + * \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. + 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 AgentMessage::msgId - * \details Message identifier of the last complete frame. - * Valid only after feed() returns FeedResult::Complete. - * \return MsgId of the last complete frame + * \brief Message identifier of the last successfully parsed frame. + * \note Valid only after feed() returns FeedResult::Complete. */ MsgId msgId() const { return _rxMsgId; } /*! - * \brief AgentMessage::sequence - * \details Sequence number of the last complete frame. - * Valid only after feed() returns FeedResult::Complete. - * \return sequence number of the last complete frame + * \brief Sequence number of the last successfully parsed frame. + * \note Valid only after feed() returns FeedResult::Complete. */ quint16 sequence() const { return _rxSequence; } /*! - * \brief AgentMessage::payload - * \details Payload bytes of the last complete frame. Empty for zero-length frames. - * Valid only after feed() returns FeedResult::Complete. - * \return payload of the last complete frame + * \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: + + 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; - static constexpr quint8 SYNC[SYNC_SIZE] = {0xAA, 0x55}; - static constexpr int HEADER_SIZE = 12; - static constexpr int MSGID_SIZE = 2; - static constexpr int SEQUENCE_SIZE = 2; - static constexpr int HEADER_CRC_SIZE = 2; - static constexpr int PAYLOAD_CRC_SIZE = 4; - static constexpr quint32 MAX_PAYLOAD_LEN = 64 * 1024; + 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; - MsgId _rxMsgId = MsgId::ClinicalData; - quint16 _rxSequence = 0; - quint32 _rxPayloadLen = 0; - QByteArray _rxPayload; + 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. }; Index: lib/MsgUtils/src/AgentMessage.cpp =================================================================== diff -u -rccc40a7e73e9ee5d2a5fb56f3f2bea4f8294900f -r401d8ad0e5c070f9c6ecb41ed3ba25ba3d1c69a0 --- lib/MsgUtils/src/AgentMessage.cpp (.../AgentMessage.cpp) (revision ccc40a7e73e9ee5d2a5fb56f3f2bea4f8294900f) +++ lib/MsgUtils/src/AgentMessage.cpp (.../AgentMessage.cpp) (revision 401d8ad0e5c070f9c6ecb41ed3ba25ba3d1c69a0) @@ -1,31 +1,10 @@ -/*! - * - * 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()); @@ -43,30 +22,39 @@ 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); + if (payloadLen == 0) { + return msg; } + 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 +// \details 1. Bytes are consumed when: scanning for the sync word, a complete header is parsed, +// or a full frame (header + payload) is parsed. +// 2. Header and payload will try to be read in the same feed() call when possible, but they +// may also be parsed across multiple feed() calls if the input buffer is fragmented. +// The caller is responsible for accumulating incoming bytes into the buffer and invoking feed() +// each time new bytes arrive. +// 3. If a header fails CRC validation, only the sync word is discarded. The payload section is +// skipped and the remaining bytes stay in the buffer to be re-scanned on the next feed() call. +// 4. If a payload fails CRC validation, only the header is discarded (pos is not advanced past +// the payload). The payload bytes stay in the buffer to be re-scanned on the next feed() call. +// 5. bytes parameter is modified in-place to remove the consumed bytes. The caller is responsible for +// appending new incoming bytes to the buffer and invoking feed() again. +// 6. If PayloadError is returned, the caller can immediately call feed() again in order to try +// to search for a header in the remaining payload bytes. +// If HeaderError is returned, the caller can call feed() again if there is any data remaining in the +// buffer to search for a header in the remaining bytes. // --------------------------------------------------------------------------- - -/*! - * \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; @@ -134,11 +122,6 @@ 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(); @@ -148,14 +131,10 @@ _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 - */ +// --------------------------------------------------------------------------- +// CRC-16/CCITT (poly 0x1021, init 0xFFFF, no reflection, no final XOR) +// Check value for "123456789": 0x29B1 +// --------------------------------------------------------------------------- quint16 AgentMessage::crc16ccitt(const quint8 *data, int len) { quint16 crc = 0xFFFF; @@ -173,15 +152,11 @@ 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 - */ +// --------------------------------------------------------------------------- +// CRC-32/ISO-HDLC (IEEE 802.3, reflected poly 0xEDB88320, +// init 0xFFFFFFFF, input/output reflected, final XOR 0xFFFFFFFF) +// Check value for "123456789": 0xCBF43926 +// --------------------------------------------------------------------------- quint32 AgentMessage::crc32isohdlc(const quint8 *data, int len) { quint32 crc = 0xFFFFFFFF;