Index: LeahiRt/LeahiRtController.cpp =================================================================== diff -u -r6f9f24886c73d92b380e4c3afd82c0b19559d42d -rccc40a7e73e9ee5d2a5fb56f3f2bea4f8294900f --- LeahiRt/LeahiRtController.cpp (.../LeahiRtController.cpp) (revision 6f9f24886c73d92b380e4c3afd82c0b19559d42d) +++ LeahiRt/LeahiRtController.cpp (.../LeahiRtController.cpp) (revision ccc40a7e73e9ee5d2a5fb56f3f2bea4f8294900f) @@ -1,52 +1,185 @@ -#include -#include // SQ +/*! + * + * 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 "AlphaOmega.h" // SQ #include "LeahiRtController.h" -#include "LeahiMsgDefs.h" +#include "LeahiMsgProtoUtils.h" -LeahiRtController::LeahiRtController(QObject *parent) : +/*! + * \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), - _clientSocket(QString(), QWebSocketProtocol::VersionLatest, this), - _msgBuilder(this) - // SQ _protoInterface(), - // SQ _protoThread(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); +} - // SQ _protoInterface.init(_protoThread); - // SQ connect(this, &LeahiRtController::didCanMessageReceive, &_protoInterface, &proto::ProtoInterface::onCanMessageReceive); +/*! + * \brief LeahiRtController::~LeahiRtController + * \details Destructor. Stops and joins the CAN and Agent worker threads. + */ +LeahiRtController::~LeahiRtController() +{ + _canThread.quit(); + _canThread.wait(); - connect(&_clientSocket, &QWebSocket::connected, [&]() { qDebug().noquote() << "Socket connected"; }); + _agentThread.quit(); + _agentThread.wait(); } -LeahiRtController::~LeahiRtController() +/*! + * \brief LeahiRtController::connectToAgent + * \details Initialises the AgentInterface using the socket path and reconnect + * interval from the settings file. + */ +void LeahiRtController::connectToAgent() { - _clientSocket.close(); + 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::openSocket(const QString host, const unsigned int port) +/*! + * \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) { - AO_BEGIN_END_CLASS(ao) // SQ - _clientSocket.close(); - _clientSocket.open(QUrl(QString("ws://%1:%2").arg(host).arg(port))); + 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) { - 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)); - // } - msg.clear(); + _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; + } +}