/*! * * 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 "LeahiMsgProtoUtils.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 msgHandlingPath - path to the message handling INI * \param parent - optional QObject parent */ LeahiRtController::LeahiRtController(const QString &configPath, const QString &msgHandlingPath, QObject *parent) : QObject(parent), _settings(configPath, QSettings::IniFormat), _canInterface(), _canThread(this), _dispatcher(this) { loadMsgHandling(msgHandlingPath); _canInterface.init(_canThread); connect(&_canInterface, &Can::CanInterface::didFrameReceive, this, &LeahiRtController::onFrameReceive); connect(&_dispatcher, &Can::MessageDispatcher::didActionReceive, this, &LeahiRtController::onMessageReceive); connect(&_agentInterface, &AgentInterface::didDisconnect, this, &LeahiRtController::onAgentDisconnect); } /*! * \brief LeahiRtController::~LeahiRtController * \details Destructor. Stops and joins the CAN and Agent worker threads. */ LeahiRtController::~LeahiRtController() { _canThread.quit(); _canThread.wait(); _agentThread.quit(); _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::loadMsgHandling * \details Parses message handling INI and populates _msgHandling. * \note Unknown msg action values default to Drop; unknown topic strings default to ClinicalData. * \param msgHandlingPath - path to the message handling INI file */ void LeahiRtController::loadMsgHandling(const QString &msgHandlingPath) { static const QHash actionMap = { { QStringLiteral("send_always"), MsgAction::SendAlways }, { QStringLiteral("send_delta"), MsgAction::SendDelta }, { QStringLiteral("drop"), MsgAction::Drop }, }; static const QHash topicMap = { { QStringLiteral("ClinicalData"), AgentMessage::MsgId::ClinicalData }, { QStringLiteral("Diagnostic"), AgentMessage::MsgId::Diagnostic }, { QStringLiteral("Ack"), AgentMessage::MsgId::Ack }, { QStringLiteral("Alarms"), AgentMessage::MsgId::Alarms }, { QStringLiteral("Audit"), AgentMessage::MsgId::Audit }, { QStringLiteral("DeviceLogFile"), AgentMessage::MsgId::DeviceLogFile }, { QStringLiteral("TreatmentLogFile"), AgentMessage::MsgId::TreatmentLogFile }, { QStringLiteral("CloudSyncLogFile"), AgentMessage::MsgId::CloudSyncLogFile }, }; QSettings msgHandlingIni(msgHandlingPath, QSettings::IniFormat); if (msgHandlingIni.status() != QSettings::NoError) { qWarning().noquote() << "LeahiRt: could not read message handling INI" << msgHandlingPath << "— all messages will be dropped"; return; } int loaded = 0; for (const QString &group : msgHandlingIni.childGroups()) { bool ok = false; const Can::MsgId msgId = static_cast(group.toUInt(&ok, 16)); if (!ok) { continue; } msgHandlingIni.beginGroup(group); const QString actionStr = msgHandlingIni.value(QStringLiteral("action"), QStringLiteral("drop")).toString().trimmed(); const QString topicStr = msgHandlingIni.value(QStringLiteral("topic")).toString().trimmed(); msgHandlingIni.endGroup(); MsgHandling msgHandling; msgHandling.action = actionMap.value(actionStr, MsgAction::Drop); msgHandling.topic = topicMap.value(topicStr, AgentMessage::MsgId::ClinicalData); if (!actionMap.contains(actionStr)) { qWarning().noquote() << QString("LeahiRt: unknown message action \"%1\" for msgId=0x%2 — defaulting to drop") .arg(actionStr).arg(msgId, 4, 16, QChar('0')); } _msgHandling.insert(msgId, msgHandling); loaded++; } qInfo().noquote() << QString("LeahiRt: loaded message handling %1 (%2 entries)").arg(msgHandlingPath).arg(loaded); } /*! * \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) { _dispatcher.onFrameReceive(Can::CanId(frame.frameId()), frame.payload()); } /*! * \brief LeahiRtController::onMessageReceive * \details Applies the message handling policy from LeahiRtMsgHandling.ini: drops, * forwards unconditionally, or forwards only on payload change. Uses the * section's topic to set the AgentMessage frame msg_id. * \param msg - the reassembled message */ void LeahiRtController::onMessageReceive(const Can::Message &msg) { const auto it = _msgHandling.constFind(msg.msgId); if (it == _msgHandling.constEnd()) { qInfo().noquote() << QString("LeahiRt: no action defined for %1 (0x%2), dropping") .arg(leahi::msgIdString(leahi::MsgId(msg.msgId))).arg(msg.msgId, 4, 16, QChar('0')); return; } if (it->action == MsgAction::Drop) { qInfo().noquote() << QString("LeahiRt: dropping message %1 (0x%2)") .arg(leahi::msgIdString(leahi::MsgId(msg.msgId))).arg(msg.msgId, 4, 16, QChar('0')); return; } auto &[received, cachedMsg] = _msgCache[msg.msgId]; if (it->action == MsgAction::SendDelta && received && cachedMsg.data.chopped(1) == msg.data.chopped(1)) { qInfo().noquote() << QString("LeahiRt: received message %1 (0x%2) did not changed from previous, dropping") .arg(leahi::msgIdString(leahi::MsgId(msg.msgId))).arg(msg.msgId, 4, 16, QChar('0')); return; } const QByteArray payload = leahi::canMessageToProtobufByteArray( QDateTime::currentDateTime(), QStringLiteral("test_device"), msg); _agentInterface.send(it->topic, _txSequence++, payload); received = true; cachedMsg = msg; } /*! * \brief LeahiRtController::onAgentDisconnect * \details Resets the received flag on all cache entries so that send_delta * messages are treated as new on the next connection. */ void LeahiRtController::onAgentDisconnect() { for (auto &[received, msg] : _msgCache) { received = false; } }