/*! * * 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 AgentInterface.cpp * \author (original) Stephen Quong * \date (original) 24-May-2026 * */ #include "AgentInterface.h" #include #include #include static void qRegister() { qRegisterMetaType("AgentMessage::MsgId"); } Q_COREAPP_STARTUP_FUNCTION(qRegister) /*! * \brief AgentInterface::AgentInterface * \details Constructor * \param parent - optional QObject parent */ AgentInterface::AgentInterface(QObject *parent) : QObject(parent) { connect(&_socket, &QLocalSocket::connected, this, &AgentInterface::onConnected); connect(&_socket, &QLocalSocket::disconnected, this, &AgentInterface::onDisconnected); connect(&_socket, &QLocalSocket::errorOccurred, this, &AgentInterface::onError); connect(&_socket, &QLocalSocket::readyRead, this, &AgentInterface::onReadyRead); _reconnectTimer.setSingleShot(false); connect(&_reconnectTimer, &QTimer::timeout, this, &AgentInterface::onReconnectTimer); } /*! * \brief AgentInterface::init * \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) { if (_init) { return false; } _init = true; _socketPath = socketPath; _reconnectTimer.setInterval(reconnectIntervalMs); connectToServer(); return true; } /*! * \brief AgentInterface::init * \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) { if (!init(socketPath, reconnectIntervalMs)) { return false; } initThread(thread); return true; } /*! * \brief AgentInterface::send * \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); _socket.write(frame); _socket.flush(); return true; } /*! * \brief AgentInterface::quit * \details Moves this object back to the main thread for safe destruction. */ void AgentInterface::quit() { quitThread(); } /*! * \brief AgentInterface::connectToServer * \details Initiates a connection to the stored socket path. */ void AgentInterface::connectToServer() { _socket.connectToServer(_socketPath, QLocalSocket::ReadWrite); } /*! * \brief AgentInterface::initThread * \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) { Q_ASSERT_X(QThread::currentThread() == qApp->thread(), __func__, "AgentInterface::init must be called from the main thread"); thread.setObjectName(QString("%1_Thread").arg(metaObject()->className())); connect(qApp, &QCoreApplication::aboutToQuit, this, &AgentInterface::quit); moveToThread(&thread); thread.start(); } /*! * \brief AgentInterface::quitThread * \details Moves this object back to the main thread. */ void AgentInterface::quitThread() { if (QThread::currentThread() != qApp->thread()) { moveToThread(qApp->thread()); } } /*! * \brief AgentInterface::onConnected * \details Stops the reconnect timer and emits didConnect(). */ void AgentInterface::onConnected() { qDebug().noquote() << "Agent socket connected to" << _socketPath; _reconnectTimer.stop(); emit didConnect(); } /*! * \brief AgentInterface::onDisconnected * \details Clears inbound parser state, starts the reconnect timer, and emits didDisconnect(). */ void AgentInterface::onDisconnected() { qDebug().noquote() << "Agent socket disconnected — retrying in" << _reconnectTimer.interval() / 1000 << "s"; _rxBuf.clear(); _rxMsg.reset(); _reconnectTimer.start(); emit didDisconnect(); } /*! * \brief AgentInterface::onError * \details Logs the socket error and starts the reconnect timer if not already running. * \param error - the socket error code */ void AgentInterface::onError(QLocalSocket::LocalSocketError error) { qDebug().noquote() << "Agent socket error:" << _socket.errorString() << QString("(%1)").arg(error); if (!_reconnectTimer.isActive()) { _reconnectTimer.start(); } } /*! * \brief AgentInterface::onReadyRead * \details Drains incoming bytes through the AgentMessage parser and emits * didMessageReceive() for each complete frame. */ void AgentInterface::onReadyRead() { _rxBuf.append(_socket.readAll()); AgentMessage::FeedResult result; do { result = _rxMsg.feed(_rxBuf); if (result == AgentMessage::FeedResult::Complete) { emit didMessageReceive(_rxMsg.msgId(), _rxMsg.sequence(), _rxMsg.payload()); _rxMsg.reset(); } } while (result == AgentMessage::FeedResult::Complete && !_rxBuf.isEmpty()); } /*! * \brief AgentInterface::onReconnectTimer * \details Attempts to reconnect if the socket is in the unconnected state. */ void AgentInterface::onReconnectTimer() { if (_socket.state() != QLocalSocket::UnconnectedState) { return; } qDebug().noquote() << "Agent socket reconnecting to" << _socketPath; connectToServer(); }