Index: LeahiRt/LeahiRtController.cpp =================================================================== diff -u -rf8c0a0b1c19dc386b2f88484d53db022270690a7 -r088513e6ea7bad08b4fb7862127c726eabad18fd --- LeahiRt/LeahiRtController.cpp (.../LeahiRtController.cpp) (revision f8c0a0b1c19dc386b2f88484d53db022270690a7) +++ LeahiRt/LeahiRtController.cpp (.../LeahiRtController.cpp) (revision 088513e6ea7bad08b4fb7862127c726eabad18fd) @@ -1,20 +1,44 @@ +/*! + * + * 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 LeahiRtController.cpp + * \author (original) Stephen Quong + * \date (original) 24-May-2026 + * + */ #include #include #include "LeahiRtController.h" #include "LeahiMsgDefs.h" +/*! + * \brief LeahiRtController::LeahiRtController + * \details Constructor. Starts the CAN interface and wires the frame→dispatcher→ + * completed-message pipeline. + * \param configPath - path to the settings file + * \param parent - optional QObject parent + */ LeahiRtController::LeahiRtController(const QString &configPath, QObject *parent) : QObject(parent), _settings(configPath, QSettings::IniFormat), _canInterface(), _canThread(this), - _msgBuilder(this) + _dispatcher(this) { _canInterface.init(_canThread); connect(&_canInterface, &Can::CanInterface::didFrameReceive, this, &LeahiRtController::onFrameReceive); + connect(&_dispatcher, &Can::MessageDispatcher::didActionReceive, this, &LeahiRtController::onMessageReceive); } +/*! + * \brief LeahiRtController::~LeahiRtController + * \details Destructor. Stops and joins the CAN and Agent worker threads. + */ LeahiRtController::~LeahiRtController() { _canThread.quit(); @@ -24,21 +48,50 @@ _agentThread.wait(); } +/*! + * \brief LeahiRtController::connectToAgent + * \details Initialises the AgentInterface using the socket path and reconnect + * interval from the settings file. + */ void LeahiRtController::connectToAgent() { 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); } +/*! + * \brief LeahiRtController::onFrameReceive + * \details Unpacks a CAN frame and feeds it to the dispatcher, which reassembles + * multi-frame messages per CAN id. + * \param frame - the received CAN frame + */ void LeahiRtController::onFrameReceive(const QCanBusFrame frame) { - const Can::CanId canId = Can::CanId(frame.frameId()); - Can::Message &msg = _messages[canId]; - if (_msgBuilder.buildMessage(frame.payload(), msg, canId) && msg.isComplete()) { - 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(); - } + _dispatcher.onFrameReceive(Can::CanId(frame.frameId()), frame.payload()); } + +/*! + * \brief LeahiRtController::onMessageReceive + * \details Converts a fully reassembled message to protobuf and forwards it to + * the Agent. + * \param msg - the reassembled message + */ +void LeahiRtController::onMessageReceive(const Can::Message &msg) +{ + Can::Message &cachedMsg = _msgCache[msg.msgId]; + qDebug().noquote() << QString("Received message with MsgId=0x%1").arg(QString("%1").arg(msg.msgId, 4, 16, QChar('0')).toUpper()); + + // only send the message if the data is different + // SQ if (cachedMsg.data != msg.data) { + // SQ if (msg.msgId == leahi::MSG_ID_TD_OP_MODE_DATA) { + const QByteArray payload = leahi::canMessageToProtobufByteArray( + QDateTime::currentDateTime(), + QStringLiteral("test_device"), + msg); + _agentInterface.send(AgentMessage::MsgId::ClinicalData, _txSequence++, payload); + // SQ } + // SQ } + cachedMsg = msg; + msg.dump(); +} Index: LeahiRt/LeahiRtController.h =================================================================== diff -u -rf8c0a0b1c19dc386b2f88484d53db022270690a7 -r088513e6ea7bad08b4fb7862127c726eabad18fd --- LeahiRt/LeahiRtController.h (.../LeahiRtController.h) (revision f8c0a0b1c19dc386b2f88484d53db022270690a7) +++ LeahiRt/LeahiRtController.h (.../LeahiRtController.h) (revision 088513e6ea7bad08b4fb7862127c726eabad18fd) @@ -1,3 +1,15 @@ +/*! + * + * 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 LeahiRtController.h + * \author (original) Stephen Quong + * \date (original) 24-May-2026 + * + */ #pragma once #include @@ -12,10 +24,17 @@ #include "AgentMessage.h" #include "CanInterface.h" #include "CanMessage.h" -#include "MessageBuilder.h" +#include "MessageDispatcher.h" using namespace Can; +/*! + * \brief Real-time CAN to cloud controller + * \details Owns the CAN→cloud pipeline. CanInterface receives frames, the + * MessageDispatcher reassembles them per CAN id, and completed messages + * are converted to protobuf and forwarded to the Connectivity Agent + * over the AgentInterface. + */ class LeahiRtController : public QObject { Q_OBJECT @@ -24,21 +43,44 @@ explicit LeahiRtController(const QString &configPath, QObject *parent = nullptr); ~LeahiRtController(); + /*! + * \brief connectToAgent + * \details Initialises the AgentInterface using the socket path and reconnect + * interval from the settings file. + */ void connectToAgent(); private: QSettings _settings; Can::CanInterface _canInterface; QThread _canThread; - Can::MessageBuilder _msgBuilder; - QMap _messages; + Can::MessageDispatcher _dispatcher; + QMap _msgCache; AgentInterface _agentInterface; QThread _agentThread; quint16 _txSequence = 0; Q_SIGNALS: + /*! + * \brief didCanMessageReceive + * \details Emitted when a complete CAN message has been reassembled. + * \param timestamp - time the message was completed + * \param msg - the completed message + */ void didCanMessageReceive(const QDateTime timestamp, const Can::Message msg); private Q_SLOTS: + /*! + * \brief onFrameReceive + * \details Unpacks a CAN frame and feeds it to the dispatcher for reassembly. + * \param frame - the received CAN frame + */ void onFrameReceive(const QCanBusFrame frame); + + /*! + * \brief onMessageReceive + * \details Converts a completed message to protobuf and forwards it to the Agent. + * \param msg - the reassembled message + */ + void onMessageReceive(const Can::Message &msg); }; Fisheye: Tag 088513e6ea7bad08b4fb7862127c726eabad18fd refers to a dead (removed) revision in file `SDDs/CloudSyncRt.puml'. Fisheye: No comparison available. Pass `N' to diff? Index: SDDs/RealtimeDataTransfer.puml =================================================================== diff -u --- SDDs/RealtimeDataTransfer.puml (revision 0) +++ SDDs/RealtimeDataTransfer.puml (revision 088513e6ea7bad08b4fb7862127c726eabad18fd) @@ -0,0 +1,14 @@ +@startuml CloudSync Real-time + + participant CANDumpPlayer as CANDumpPlayer + participant CANBus as CANBus + participant CloudSyncRt as CloudSyncRt + participant CloudSyncRtServer as CloudSyncRtServer + + + -> CANDumpPlayer: Input CAN dump file + CANDumpPlayer -> CANBus: Push messages onto CAN bus + CANBus -> CloudSyncRt: Read message from CAN bus + CloudSyncRt -> CloudSyncRt: Convert CAN messages to Protobuf messages + CloudSyncRt -> CloudSyncRtServer: Send Protobuf messages to CloudSyncRt Server +@enduml Index: lib/Comms/include/AgentInterface.h =================================================================== diff -u -rf8c0a0b1c19dc386b2f88484d53db022270690a7 -r088513e6ea7bad08b4fb7862127c726eabad18fd --- lib/Comms/include/AgentInterface.h (.../AgentInterface.h) (revision f8c0a0b1c19dc386b2f88484d53db022270690a7) +++ lib/Comms/include/AgentInterface.h (.../AgentInterface.h) (revision 088513e6ea7bad08b4fb7862127c726eabad18fd) @@ -22,84 +22,45 @@ #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. - * - * \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. + * Outbound: call send() to write an AgentMessage frame to the socket. + * Inbound: emits didMessageReceive() for each complete parsed frame. */ class AgentInterface : public QObject { Q_OBJECT public: - /*! - * \brief Construct an AgentInterface. - * \param parent Optional QObject parent. - */ - explicit AgentInterface(QObject *parent = nullptr); + 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 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 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 */ void didMessageReceive(AgentMessage::MsgId msgId, quint16 sequence, QByteArray payload); /*! - * \brief Emitted when the socket connects successfully. + * \brief didConnect + * \details Emitted when the socket connects successfully. */ void didConnect(); /*! - * \brief Emitted when the socket disconnects. + * \brief didDisconnect + * \details Emitted when the socket disconnects. */ void didDisconnect(); Index: lib/Comms/src/AgentInterface.cpp =================================================================== diff -u -rf8c0a0b1c19dc386b2f88484d53db022270690a7 -r088513e6ea7bad08b4fb7862127c726eabad18fd --- lib/Comms/src/AgentInterface.cpp (.../AgentInterface.cpp) (revision f8c0a0b1c19dc386b2f88484d53db022270690a7) +++ lib/Comms/src/AgentInterface.cpp (.../AgentInterface.cpp) (revision 088513e6ea7bad08b4fb7862127c726eabad18fd) @@ -21,8 +21,8 @@ /*! * \brief AgentInterface::AgentInterface - * \details Constructor. Wires all socket and timer signal/slot connections. - * \param parent Optional QObject parent. + * \details Constructor + * \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, 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. + * \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 */ bool AgentInterface::init(const QString &socketPath, int reconnectIntervalMs) { @@ -58,12 +58,12 @@ /*! * \brief AgentInterface::init - * \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. + * \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 */ bool AgentInterface::init(const QString &socketPath, int reconnectIntervalMs, QThread &thread) { @@ -76,18 +76,23 @@ /*! * \brief AgentInterface::send - * \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. + * \details Builds an 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 empty for zero-length frames + * \return true if written to the socket, false if not connected */ bool AgentInterface::send(AgentMessage::MsgId msgId, quint16 sequence, const QByteArray &payload) { if (_socket.state() != QLocalSocket::ConnectedState) { return false; } const QByteArray frame = AgentMessage::build(msgId, sequence, payload); + QString output; + for (auto i = 0; i < frame.size(); i++) { + output += QStringLiteral(" 0x") + QString("%1").arg(uint8_t(frame.at(i)), 2, 16, QChar('0')).toUpper(); + } + qDebug().noquote() << QString("frame =%1").arg(output); _socket.write(frame); _socket.flush(); return true; @@ -107,9 +112,9 @@ /*! * \brief AgentInterface::initThread - * \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. + * \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 */ void AgentInterface::initThread(QThread &thread) { @@ -123,8 +128,7 @@ /*! * \brief AgentInterface::quitThread - * \details Moves this object back to the main thread so it can be safely - * destroyed by its owner. + * \details Moves this object back to the main thread. */ void AgentInterface::quitThread() { @@ -160,7 +164,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) { @@ -172,8 +176,8 @@ /*! * \brief AgentInterface::onReadyRead - * \details Appends incoming bytes to the receive buffer and drains it through - * the AgentMessage parser. Emits didMessageReceive() for each complete frame. + * \details Drains incoming bytes through the AgentMessage parser and emits + * didMessageReceive() for each complete frame. */ void AgentInterface::onReadyRead() { Index: lib/MsgUtils/CMakeLists.txt =================================================================== diff -u -r6cdf791210cfa0d96514094d33510e639f9bc0b6 -r088513e6ea7bad08b4fb7862127c726eabad18fd --- lib/MsgUtils/CMakeLists.txt (.../CMakeLists.txt) (revision 6cdf791210cfa0d96514094d33510e639f9bc0b6) +++ lib/MsgUtils/CMakeLists.txt (.../CMakeLists.txt) (revision 088513e6ea7bad08b4fb7862127c726eabad18fd) @@ -10,7 +10,6 @@ endif() find_package(Protobuf REQUIRED) -find_package(absl REQUIRED) find_package(QT NAMES Qt6 Qt5 REQUIRED COMPONENTS Core) find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Core Network SerialBus) @@ -37,7 +36,7 @@ include/FrameInterface.h include/main.h include/MessageBuilder.h - # include/MessageDispatcher.h + include/MessageDispatcher.h # include/MessageGlobals.h # include/MessageInterpreter.h # include/qrcodegen.h @@ -52,7 +51,7 @@ src/format.cpp src/FrameInterface.cpp src/MessageBuilder.cpp - # src/MessageDispatcher.cpp + src/MessageDispatcher.cpp # src/MessageInterpreter.cpp # src/qrcodegen.cpp src/types.cpp @@ -100,9 +99,6 @@ target_link_libraries(${PROJECT_NAME} PUBLIC ${Protobuf_LIBRARIES} - absl::log - absl::log_internal_check_op - absl::log_internal_message Qt${QT_VERSION_MAJOR}::Core Qt${QT_VERSION_MAJOR}::Network Qt${QT_VERSION_MAJOR}::SerialBus Index: lib/MsgUtils/include/AgentMessage.h =================================================================== diff -u -rf8c0a0b1c19dc386b2f88484d53db022270690a7 -r088513e6ea7bad08b4fb7862127c726eabad18fd --- lib/MsgUtils/include/AgentMessage.h (.../AgentMessage.h) (revision f8c0a0b1c19dc386b2f88484d53db022270690a7) +++ lib/MsgUtils/include/AgentMessage.h (.../AgentMessage.h) (revision 088513e6ea7bad08b4fb7862127c726eabad18fd) @@ -15,175 +15,101 @@ #include /*! - * \brief LeahiRt to Connectivity Agent message framing. - * \details Implements the binary message framing protocol - * between the LeahiRt application and the Agent application. + * \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(). * - * 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.). + * Frame layout — header only (payload_length == 0): * - * \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 + * Frame layout — with payload (payload_length > 0): + * * ┌── 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). + * Header CRC: CRC-16/CCITT (poly 0x1021, init 0xFFFF, no reflection). + * Payload CRC: 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. + * \brief MQTT routing identifier carried in every frame header + * \details The Connectivity Agent uses this value to determine the MQTT topic. */ 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 + ClinicalData = 0x0001, + Diagnostic = 0x0002, + Ack = 0x0003, + Alarms = 0x0004, + Audit = 0x0005, + DeviceLogFile = 0x0006, + TreatmentLogFile = 0x0007, + CloudSyncLogFile = 0x0008, }; /*! - * \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 Message identifier of the last successfully parsed frame. - * \note Valid only after feed() returns FeedResult::Complete. + * \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 */ MsgId msgId() const { return _rxMsgId; } /*! - * \brief Sequence number of the last successfully parsed frame. - * \note Valid only after feed() returns FeedResult::Complete. + * \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 */ 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. + * \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 */ 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. - */ +private: 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. + 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; - 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. + QByteArray _headerBuf; + MsgId _rxMsgId = MsgId::ClinicalData; + quint16 _rxSequence = 0; + quint32 _rxPayloadLen = 0; + QByteArray _rxPayload; }; Index: lib/MsgUtils/include/types.h =================================================================== diff -u -rcfc0df719cb5033078d0cac45ce0f6243810f2e7 -r088513e6ea7bad08b4fb7862127c726eabad18fd --- lib/MsgUtils/include/types.h (.../types.h) (revision cfc0df719cb5033078d0cac45ce0f6243810f2e7) +++ lib/MsgUtils/include/types.h (.../types.h) (revision 088513e6ea7bad08b4fb7862127c726eabad18fd) @@ -122,7 +122,9 @@ qint8 bytes[sizeof(qint8)]; }; + using BOOL = U32; + static bool floatCompare(float f1, float f2); template < typename T > Index: lib/MsgUtils/src/AgentMessage.cpp =================================================================== diff -u -rf8c0a0b1c19dc386b2f88484d53db022270690a7 -r088513e6ea7bad08b4fb7862127c726eabad18fd --- lib/MsgUtils/src/AgentMessage.cpp (.../AgentMessage.cpp) (revision f8c0a0b1c19dc386b2f88484d53db022270690a7) +++ lib/MsgUtils/src/AgentMessage.cpp (.../AgentMessage.cpp) (revision 088513e6ea7bad08b4fb7862127c726eabad18fd) @@ -1,10 +1,31 @@ +/*! + * + * 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 routing + * \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()); @@ -22,39 +43,30 @@ const quint16 hCrc = crc16ccitt(header, HEADER_SIZE - HEADER_CRC_SIZE); qToBigEndian(hCrc, header + HEADER_SIZE - HEADER_CRC_SIZE); - if (payloadLen == 0) { - return msg; + 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); } - 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; @@ -122,6 +134,11 @@ 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(); @@ -131,10 +148,14 @@ _rxPayload.clear(); } -// --------------------------------------------------------------------------- -// CRC-16/CCITT (poly 0x1021, init 0xFFFF, no reflection, no final XOR) -// Check value for "123456789": 0x29B1 -// --------------------------------------------------------------------------- +/*! + * \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; @@ -152,11 +173,15 @@ return crc; } -// --------------------------------------------------------------------------- -// CRC-32/ISO-HDLC (IEEE 802.3, reflected poly 0xEDB88320, -// init 0xFFFFFFFF, input/output reflected, final XOR 0xFFFFFFFF) -// Check value for "123456789": 0xCBF43926 -// --------------------------------------------------------------------------- +/*! + * \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; Index: lib/MsgUtils/src/MessageBuilder.cpp =================================================================== diff -u -r2c00c6e743844c9a71fa03ce5a5c436ef3836484 -r088513e6ea7bad08b4fb7862127c726eabad18fd --- lib/MsgUtils/src/MessageBuilder.cpp (.../MessageBuilder.cpp) (revision 2c00c6e743844c9a71fa03ce5a5c436ef3836484) +++ lib/MsgUtils/src/MessageBuilder.cpp (.../MessageBuilder.cpp) (revision 088513e6ea7bad08b4fb7862127c726eabad18fd) @@ -361,11 +361,9 @@ */ quint16 MessageBuilder::getActionId(QByteArray &vPayload) { - MsgId_Bytes mMsgId; - int index = 0; - Types::getValue<>(vPayload, index, mMsgId); + MsgId mMsgId = vPayload.mid(0, eLenActionId).toHex().toUInt(0,16); vPayload = vPayload.mid(eLenActionId); - return mMsgId.value; + return mMsgId; } /*! Index: scripts/MsgUtils/msgutils/MsgData.py =================================================================== diff -u -r2202de40fe8377f73b17cd3ab3d432b167dd5b66 -r088513e6ea7bad08b4fb7862127c726eabad18fd --- scripts/MsgUtils/msgutils/MsgData.py (.../MsgData.py) (revision 2202de40fe8377f73b17cd3ab3d432b167dd5b66) +++ scripts/MsgUtils/msgutils/MsgData.py (.../MsgData.py) (revision 088513e6ea7bad08b4fb7862127c726eabad18fd) @@ -146,10 +146,10 @@ if not line and row_data is not None: # check to see if data has already been collected for this msg_id_value if row_data['msg_id_value'] in self.data: - print(f"WARNING: found message ({self.data[msg_id_value]['msg_id_hex_string']}) with same ID ({self.data[row_data['msg_id_value']]['msg_id']}) will be replaced by {row_data['msg_id']} for value {row_data['msg_id_hex_string']}") + print(f"WARNING: found message with same ID ({self.data[row_data['msg_id_value']]['msg_id_hex_string']}) - {self.data[row_data['msg_id_value']]['msg_id']} will be replaced by {row_data['msg_id']} for value {row_data['msg_id_hex_string']}") for item in self.data.values(): if item['msg_name'] == row_data['msg_name']: - print(f"WARNING: found message ({item['msg_id_hex_string']}, {row_data['msg_id_hex_string']}) with same name ({row_data['msg_name']}), skipping message") + print(f"WARNING: found message {item['msg_id_hex_string']} with same name ({row_data['msg_name']}) as new message{row_data['msg_id_hex_string']}, skipping message") row_data = None break if row_data is not None: @@ -178,7 +178,7 @@ # message name (first line of message section) elif row_data is not None and row_data['msg_id'] == '': row_data['raw'] += line + '\n' - row_data['msg_id'] = 'MSG_ID_' + '_'.join(word.upper() for word in line.split('_')) + row_data['msg_id'] = line if re.fullmatch(name_regex, line): row_data['msg_name'] = self.__message_name(row_data['msg_id']) else: Index: scripts/MsgUtils/msgutils/templates/MsgDefs_cpp.jinja =================================================================== diff -u -rbde1243eb2ff6af2868f6c6ad0cb4f5760aaf68b -r088513e6ea7bad08b4fb7862127c726eabad18fd --- scripts/MsgUtils/msgutils/templates/MsgDefs_cpp.jinja (.../MsgDefs_cpp.jinja) (revision bde1243eb2ff6af2868f6c6ad0cb4f5760aaf68b) +++ scripts/MsgUtils/msgutils/templates/MsgDefs_cpp.jinja (.../MsgDefs_cpp.jinja) (revision 088513e6ea7bad08b4fb7862127c726eabad18fd) @@ -93,19 +93,20 @@ void {{ msg['msg_name'] }}Payload::dump() const { - QStringList params; + QStringList paramList; {%- for field in msg['payload'] %} {%- if field['type'] != "union" %} - params << QString("{{ field['name'] }}=%1").arg({{ field['name'] }}.value); + paramList << QString("{{ field['name'] }}=%1").arg({{ field['name'] }}.value); {%- endif %} {%- endfor %} - qDebug().noquote() << QString("{{ msg['msg_name'] }}Payload: %1").arg(params.count() ? params.join(", ") : ""); + qDebug().noquote() << QString("{{ msg['msg_name'] }}Payload: %1").arg(paramList.count() ? paramList.join(", ") : ""); } {%- endfor %} +{%- if proto is defined %} QByteArray canMessageToProtobufByteArray(const QDateTime ×tamp, const QString &deviceSerialNum, const Can::Message &msg) { - static const auto updateHeader = [&](messages::Header *header) { + const auto updateHeader = [&](messages::Header *header) { if (header) { const auto msecs = timestamp.toMSecsSinceEpoch(); header->set_deviceserialnum(deviceSerialNum.toStdString()); @@ -129,9 +130,9 @@ payload.dump(); auto proto = payload.toProtobuf(); updateHeader(proto.mutable_header()); - std::ostringstream out; - (void)proto.SerializeToOstream(&out); - return out.str().c_str(); + std::string out; + (void)proto.SerializeToString(&out); + return QByteArray(out.data(), static_cast(out.size())); break; } {%- endfor %} @@ -141,6 +142,7 @@ } return QByteArray(); } +{%- endif %} {%- if cpp_namespace is defined and cpp_namespace is not none %} } // namespace {{ cpp_namespace }} Index: scripts/MsgUtils/msgutils/templates/MsgDefs_h.jinja =================================================================== diff -u -rbde1243eb2ff6af2868f6c6ad0cb4f5760aaf68b -r088513e6ea7bad08b4fb7862127c726eabad18fd --- scripts/MsgUtils/msgutils/templates/MsgDefs_h.jinja (.../MsgDefs_h.jinja) (revision bde1243eb2ff6af2868f6c6ad0cb4f5760aaf68b) +++ scripts/MsgUtils/msgutils/templates/MsgDefs_h.jinja (.../MsgDefs_h.jinja) (revision 088513e6ea7bad08b4fb7862127c726eabad18fd) @@ -65,7 +65,11 @@ // payload: {{ msg_cpp.field_list(msg_id_value) | join(", ") }} struct {{ msg['msg_name'] }}Payload { {%- for field in msg['cpp_struct']['payload'] %} - {{ field['cpp_type'] }} {{ field['name'] }}; +{%- if field['type'] == "union" %} + union {{ field['name'] }}; +{%- else %} + Types::{{ field['type'] }} {{ field['name'] }}; +{%- endif %} {%- endfor %} {%- if msg['cpp_struct']['payload'] | length %} {% endif %} @@ -88,9 +92,11 @@ void dump() const; }; {%- endfor %} -{%- if cpp_namespace is defined and cpp_namespace is not none %} +{%- if proto is defined %} QByteArray canMessageToProtobufByteArray(const QDateTime ×tamp, const QString &deviceSerialNumber, const Can::Message &msg); +{%- endif %} +{%- if cpp_namespace is defined and cpp_namespace is not none %} } // namespace {{ cpp_namespace }} {%- endif %} Index: scripts/MsgUtils/msgutils/templates/MsgDefs_proto.jinja =================================================================== diff -u -rbde1243eb2ff6af2868f6c6ad0cb4f5760aaf68b -r088513e6ea7bad08b4fb7862127c726eabad18fd --- scripts/MsgUtils/msgutils/templates/MsgDefs_proto.jinja (.../MsgDefs_proto.jinja) (revision bde1243eb2ff6af2868f6c6ad0cb4f5760aaf68b) +++ scripts/MsgUtils/msgutils/templates/MsgDefs_proto.jinja (.../MsgDefs_proto.jinja) (revision 088513e6ea7bad08b4fb7862127c726eabad18fd) @@ -7,12 +7,19 @@ import "google/protobuf/timestamp.proto"; +// Header is required as the first field (i.e. 'Header header = 1;') of every message. message Header { string deviceSerialNum = 1; google.protobuf.Timestamp timestamp = 2; uint32 msgId = 3; - uint32 sequence = 4; + int32 sequence = 4; } + +// Envelope is a minimal wrapper used to extract the Header from serialized messages without knowing the +// concrete message type at parse time. Every message requires Header at field 1. +message Envelope { + Header header = 1; +} {%- for (msg_id, msg) in msg_proto.data.items() %} // {{ msg['msg_id'] }} ({{ msg['msg_id_hex_string'] }}) Index: scripts/MsgUtils/pyproject.toml =================================================================== diff -u -rbde1243eb2ff6af2868f6c6ad0cb4f5760aaf68b -r088513e6ea7bad08b4fb7862127c726eabad18fd --- scripts/MsgUtils/pyproject.toml (.../pyproject.toml) (revision bde1243eb2ff6af2868f6c6ad0cb4f5760aaf68b) +++ scripts/MsgUtils/pyproject.toml (.../pyproject.toml) (revision 088513e6ea7bad08b4fb7862127c726eabad18fd) @@ -6,7 +6,7 @@ name = "msgutils" version = "1.0.0" description = "Diality Message Utilities" -requires-python = ">=3.9" +requires-python = ">=3.8" dependencies = ["jinja2"] [project.scripts]