/*! * * 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 #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("SendAlways"), MsgAction::SendAlways }, { QStringLiteral("SendDelta"), MsgAction::SendDelta }, { QStringLiteral("Drop"), MsgAction::Drop }, }; static const QHash topicMap = { { QStringLiteral("HighPriority"), CloudConnectFrame::Type::HighPriority }, { QStringLiteral("NormalPriority"), CloudConnectFrame::Type::NormalPriority }, { QStringLiteral("DeviceLogFile"), CloudConnectFrame::Type::DeviceLogFile }, { QStringLiteral("TreatmentLogFile"), CloudConnectFrame::Type::TreatmentLogFile }, { QStringLiteral("CloudSyncLogFile"), CloudConnectFrame::Type::CloudSyncLogFile }, }; if (! QFile::exists(msgHandlingPath)) { qWarning().noquote() << "CloudConnect: handling INI" << msgHandlingPath << "does not exist"; return; } 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 msgCount = 0; for (const QString &group : msgHandlingIni.childGroups()) { bool ok = false; const Can::MsgId msgId = static_cast(group.toUInt(&ok, 16)); if (!ok) { qWarning().noquote() << QString("CloudConnect: could not convert group \"%1\" to MsgId").arg(group); continue; } msgHandlingIni.beginGroup(group); const QString msgIdStr = msgHandlingIni.value(QStringLiteral("msg_id")).toString(); const QString actionStr = msgHandlingIni.value(QStringLiteral("action")).toString().trimmed(); const QString topicStr = msgHandlingIni.value(QStringLiteral("topic")).toString().trimmed(); qInfo().noquote() << QString("%1 (0x%2): action=%3, topic=%4") .arg(msgIdStr).arg(QString("%1").arg(quint16(msgId), 4, 16, QChar('0')).toUpper()).arg(actionStr).arg(topicStr); msgHandlingIni.endGroup(); MsgHandling msgHandling; msgHandling.action = actionMap.value(actionStr, MsgAction::Drop); msgHandling.topic = topicMap.value(topicStr, CloudConnectFrame::Type::NormalPriority); if (actionStr.length() > 0 && !actionMap.contains(actionStr)) { qWarning().noquote() << QString("CloudConnect: unknown message action \"%1\" for msgId=0x%2 — defaulting to Drop") .arg(actionStr).arg(QString("%1").arg(quint16(msgId), 4, 16, QChar('0').toUpper())); } if (topicStr.length() > 0 && !topicMap.contains(topicStr)) { qWarning().noquote() << QString("CloudConnect: unknown message topic \"%1\" for msgId=0x%2 — defaulting to NormalPriority") .arg(topicStr).arg(QString("%1").arg(quint16(msgId), 4, 16, QChar('0').toUpper())); } _msgHandling.insert(msgId, msgHandling); msgCount++; } qInfo().noquote() << QString("CloudConnect: loaded message handling %1 (%2 entries)").arg(msgHandlingPath).arg(msgCount); } /*! * \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; } }