/*! * * 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 CloudConnectController.cpp * \author (original) Stephen Quong * \date (original) 24-May-2026 * */ #include #include #include "CloudConnectController.h" #include "LeahiMsgProtoUtils.h" /*! * \brief CloudConnectController::CloudConnectController * \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 */ CloudConnectController::CloudConnectController(const QString &configPath, const QString &msgHandlingPath, QObject *parent) : QObject(parent), _settings(configPath, QSettings::IniFormat), _canInterface(), _canThread(this), _dispatcher(this), _appServer(this) { loadMsgHandling(msgHandlingPath); _canInterface.init(_canThread); connect(&_canInterface, &Can::CanInterface::didFrameReceive, this, &CloudConnectController::onFrameReceive); connect(&_dispatcher, &Can::MessageDispatcher::didActionReceive, this, &CloudConnectController::onMessageReceive); connect(&_agentInterface, &CloudConnectClient::didDisconnect, this, &CloudConnectController::onAgentDisconnect); } /*! * \brief CloudConnectController::~CloudConnectController * \details Destructor. Stops and joins the CAN and Agent worker threads. */ CloudConnectController::~CloudConnectController() { _canThread.quit(); _canThread.wait(); _agentThread.quit(); _agentThread.wait(); } /*! * \brief CloudConnectController::connectToAgent * \details Initialises the CloudConnectClient using the socket path and reconnect * interval from the settings file. */ void CloudConnectController::connectToAgent() { const QString socketPath = _settings.value("Socket/AgentSocketName", "/tmp/cloudconnect_agent.sock").toString(); const int reconnectIntervalMs = _settings.value("Socket/ReconnectIntervalMs", 5000).toInt(); _agentInterface.init(socketPath, reconnectIntervalMs, _agentThread); } /*! * \brief CloudConnectController::listenForApp * \details Starts the local-socket server that the Luis application connects to, * using the app socket path from the settings file. * \return true if the server bound successfully, false otherwise. */ bool CloudConnectController::listenForApp() { const QString appSocketPath = _settings.value("Socket/AppSocketName", "/tmp/cloudconnect_app.sock").toString(); return _appServer.listen(appSocketPath); } /*! * \brief CloudConnectController::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 CloudConnectController::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"), CloudConnectFrame::MsgId::ClinicalData }, { QStringLiteral("Diagnostic"), CloudConnectFrame::MsgId::Diagnostic }, { QStringLiteral("Ack"), CloudConnectFrame::MsgId::Ack }, { QStringLiteral("Alarms"), CloudConnectFrame::MsgId::Alarms }, { QStringLiteral("Audit"), CloudConnectFrame::MsgId::Audit }, { QStringLiteral("DeviceLogFile"), CloudConnectFrame::MsgId::DeviceLogFile }, { QStringLiteral("TreatmentLogFile"), CloudConnectFrame::MsgId::TreatmentLogFile }, { QStringLiteral("CloudSyncLogFile"), CloudConnectFrame::MsgId::CloudSyncLogFile }, }; QSettings msgHandlingIni(msgHandlingPath, QSettings::IniFormat); if (msgHandlingIni.status() != QSettings::NoError) { qWarning().noquote() << "CloudConnect: 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, CloudConnectFrame::MsgId::ClinicalData); if (!actionMap.contains(actionStr)) { qWarning().noquote() << QString("CloudConnect: 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("CloudConnect: loaded message handling %1 (%2 entries)").arg(msgHandlingPath).arg(loaded); } /*! * \brief CloudConnectController::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 CloudConnectController::onFrameReceive(const QCanBusFrame frame) { _dispatcher.onFrameReceive(Can::CanId(frame.frameId()), frame.payload()); } /*! * \brief CloudConnectController::onMessageReceive * \details Applies the message handling policy from LeahiMsgHandling.ini: drops, * forwards unconditionally, or forwards only on payload change. Uses the * section's topic to set the CloudConnectFrame frame msg_id. * \param msg - the reassembled message */ void CloudConnectController::onMessageReceive(const Can::Message &msg) { const auto it = _msgHandling.constFind(msg.msgId); if (it == _msgHandling.constEnd()) { qInfo().noquote() << QString("CloudConnect: 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("CloudConnect: 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("CloudConnect: 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); const quint16 sequence = _txSequence++; _agentInterface.send(it->topic, sequence, payload); _appServer.send(it->topic, sequence, payload); received = true; cachedMsg = msg; } /*! * \brief CloudConnectController::onAgentDisconnect * \details Resets the received flag on all cache entries so that send_delta * messages are treated as new on the next connection. */ void CloudConnectController::onAgentDisconnect() { for (auto &[received, msg] : _msgCache) { received = false; } }