Index: tools/AgentSim/AgentSimController.cpp =================================================================== diff -u --- tools/AgentSim/AgentSimController.cpp (revision 0) +++ tools/AgentSim/AgentSimController.cpp (revision b1bab9e8d105f31bca82799af43ed354a4ab2df0) @@ -0,0 +1,139 @@ +/*! + * + * 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 AgentSimController.cpp + * \author (original) Stephen Quong + * \date (original) 24-May-2026 + * + */ +#include "AgentSimController.h" + +#include + +/*! + * \brief AgentSimController::AgentSimController + * \details Constructor. Loads the INI configuration and wires the server newConnection signal. + * \param configPath Path to the INI configuration file. + * \param parent Optional QObject parent. + */ +AgentSimController::AgentSimController(const QString &configPath, QObject *parent) + : QObject(parent), _settings(configPath, QSettings::IniFormat) +{ + connect(&_server, &QLocalServer::newConnection, this, &AgentSimController::onNewConnection); +} + +/*! + * \brief AgentSimController::listen + * \details Reads the socket path from the INI configuration, removes any stale + * socket file, then starts the server. + * \return true on success, false if the server could not bind. + */ +bool AgentSimController::listen() +{ + const QString socketPath = _settings.value("Socket/LocalSocketName", "/tmp/leahi_rt.sock").toString(); + QLocalServer::removeServer(socketPath); + if (!_server.listen(socketPath)) { + qCritical().noquote() << "AgentSim: failed to listen on" << socketPath << "—" << _server.errorString(); + return false; + } + qInfo().noquote() << "AgentSim: listening on" << socketPath; + return true; +} + +/*! + * \brief AgentSimController::onNewConnection + * \details Accepts the pending connection. If a client is already connected the + * new socket is discarded with a warning. + */ +void AgentSimController::onNewConnection() +{ + if (_client) { + qWarning().noquote() << "AgentSim: second connection attempt rejected — already connected"; + _server.nextPendingConnection()->deleteLater(); + return; + } + + _client = _server.nextPendingConnection(); + qInfo().noquote() << "AgentSim: client connected"; + + connect(_client, &QLocalSocket::readyRead, this, &AgentSimController::onReadyRead); + connect(_client, &QLocalSocket::disconnected, this, &AgentSimController::onDisconnected); +} + +/*! + * \brief AgentSimController::onDisconnected + * \details Releases the client socket and resets inbound parser state. + */ +void AgentSimController::onDisconnected() +{ + qInfo().noquote() << "AgentSim: client disconnected"; + _client->deleteLater(); + _client = nullptr; + _rxBuf.clear(); + _rxMsg.reset(); +} + +/*! + * \brief AgentSimController::onReadyRead + * \details Appends incoming bytes to the receive buffer and drains it through + * the AgentMessage parser. Logs each complete frame and sends an Ack. + */ +void AgentSimController::onReadyRead() +{ + _rxBuf.append(_client->readAll()); + + AgentMessage::FeedResult result; + do { + result = _rxMsg.feed(_rxBuf); + switch (result) { + case AgentMessage::FeedResult::Complete: + logMessage(_rxMsg.msgId(), _rxMsg.sequence(), _rxMsg.payload()); + sendAck(_rxMsg.sequence()); + _rxMsg.reset(); + break; + case AgentMessage::FeedResult::HeaderError: + qWarning().noquote() << "AgentSim: header CRC error — frame dropped"; + break; + case AgentMessage::FeedResult::PayloadError: + qWarning().noquote() << "AgentSim: payload CRC error — frame dropped"; + break; + case AgentMessage::FeedResult::Incomplete: + break; + } + } while (result == AgentMessage::FeedResult::Complete && !_rxBuf.isEmpty()); +} + +/*! + * \brief AgentSimController::logMessage + * \details Logs the message identifier, sequence number, and payload length. + * \param msgId Message identifier from the parsed frame. + * \param sequence Sequence number from the parsed frame. + * \param payload Payload bytes of the parsed frame. + */ +void AgentSimController::logMessage(AgentMessage::MsgId msgId, quint16 sequence, const QByteArray &payload) +{ + qInfo().noquote() << QString("AgentSim: rx MsgId=0x%1 seq=%2 payloadLen=%3") + .arg(static_cast(msgId), 4, 16, QChar('0')) + .arg(sequence) + .arg(payload.size()); +} + +/*! + * \brief AgentSimController::sendAck + * \details Builds an Ack frame carrying \p sequence and writes it to the client socket. + * \param sequence Sequence number to echo back in the Ack frame. + */ +void AgentSimController::sendAck(quint16 sequence) +{ + if (!_client || _client->state() != QLocalSocket::ConnectedState) { + return; + } + const QByteArray frame = AgentMessage::build(AgentMessage::MsgId::Ack, sequence); + _client->write(frame); + _client->flush(); + qInfo().noquote() << QString("AgentSim: tx MsgId=Ack seq=%1").arg(sequence); +} Index: tools/AgentSim/AgentSimController.h =================================================================== diff -u --- tools/AgentSim/AgentSimController.h (revision 0) +++ tools/AgentSim/AgentSimController.h (revision b1bab9e8d105f31bca82799af43ed354a4ab2df0) @@ -0,0 +1,63 @@ +/*! + * + * 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 AgentSimController.h + * \author (original) Stephen Quong + * \date (original) 24-May-2026 + * + */ +#pragma once + +#include +#include +#include +#include +#include +#include + +#include "AgentMessage.h" + +/*! + * \brief Simulates the server side of the LeahiRt ↔ Agent UDS connection. + * \details Listens on a Unix domain socket, accepts one client at a time, parses + * inbound AgentMessage frames, logs them, and replies with an Ack frame. + * Parser state is cleared automatically when the client disconnects. + */ +class AgentSimController : public QObject +{ + Q_OBJECT + +public: + /*! + * \brief Construct an AgentSimController. + * \param configPath Path to the INI configuration file. + * \param parent Optional QObject parent. + */ + explicit AgentSimController(const QString &configPath, QObject *parent = nullptr); + + /*! + * \brief Begin listening on the configured socket path. + * \details Removes any stale socket file before binding. + * \return true on success, false if the server could not bind. + */ + bool listen(); + +private Q_SLOTS: + void onNewConnection(); + void onDisconnected(); + void onReadyRead(); + +private: + void logMessage(AgentMessage::MsgId msgId, quint16 sequence, const QByteArray &payload); + void sendAck(quint16 sequence); + + QSettings _settings; + QLocalServer _server; + QLocalSocket *_client = nullptr; + QByteArray _rxBuf; + AgentMessage _rxMsg; +}; Index: tools/AgentSim/CMakeLists.txt =================================================================== diff -u --- tools/AgentSim/CMakeLists.txt (revision 0) +++ tools/AgentSim/CMakeLists.txt (revision b1bab9e8d105f31bca82799af43ed354a4ab2df0) @@ -0,0 +1,46 @@ +cmake_minimum_required(VERSION 3.16) + +project(AgentSim LANGUAGES CXX) + +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_INCLUDE_CURRENT_DIR ON) +set(CMAKE_DISABLE_IN_SOURCE_BUILD ON) +set(CMAKE_AUTOMOC ON) + +set(TOOLS_BIN ${CMAKE_CURRENT_SOURCE_DIR}/../bin) +file(MAKE_DIRECTORY ${TOOLS_BIN}) + +find_package(QT NAMES Qt6 Qt5 REQUIRED COMPONENTS Core Network) +find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Core Network) +find_package(MsgUtils HINTS ${CMAKE_CURRENT_SOURCE_DIR}/../../lib/MsgUtils REQUIRED) + +set(INCLUDES + AgentSimController.h +) + +set(SRCS + AgentSimController.cpp + main.cpp +) + +add_executable(${PROJECT_NAME} + ${INCLUDES} ${SRCS} +) + +target_link_libraries(${PROJECT_NAME} PRIVATE + MsgUtils + Qt${QT_VERSION_MAJOR}::Core + Qt${QT_VERSION_MAJOR}::Network +) + +set_target_properties(${PROJECT_NAME} PROPERTIES + RUNTIME_OUTPUT_DIRECTORY ${TOOLS_BIN} + ADDITIONAL_CLEAN_FILES ${TOOLS_BIN} +) + +install(TARGETS ${PROJECT_NAME} + BUNDLE DESTINATION . + RUNTIME DESTINATION ${TOOLS_BIN} + LIBRARY DESTINATION ${CMAKE_CURRENT_SOURCE_DIR}/lib +) Index: tools/AgentSim/config/AgentSim.ini =================================================================== diff -u --- tools/AgentSim/config/AgentSim.ini (revision 0) +++ tools/AgentSim/config/AgentSim.ini (revision b1bab9e8d105f31bca82799af43ed354a4ab2df0) @@ -0,0 +1,2 @@ +[Socket] +LocalSocketName=/tmp/leahi_rt.sock Index: tools/AgentSim/main.cpp =================================================================== diff -u --- tools/AgentSim/main.cpp (revision 0) +++ tools/AgentSim/main.cpp (revision b1bab9e8d105f31bca82799af43ed354a4ab2df0) @@ -0,0 +1,43 @@ +/*! + * + * 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 main.cpp + * \author (original) Stephen Quong + * \date (original) 24-May-2026 + * + */ +#include +#include +#include +#include + +#include "AgentSimController.h" + +int main(int argc, char *argv[]) +{ + QCoreApplication app(argc, argv); + app.setApplicationName("AgentSim"); + app.setApplicationVersion("1.0"); + + QCommandLineParser parser; + parser.setApplicationDescription("Simulates the Connectivity Agent UDS server for testing LeahiRt."); + parser.addHelpOption(); + parser.addVersionOption(); + + QCommandLineOption configOption({"c", "config"}, "Path to the configuration INI file.", "config", + QDir(app.applicationDirPath()).filePath("config/AgentSim.ini")); + parser.addOption(configOption); + + parser.process(app); + + AgentSimController controller(parser.value(configOption)); + if (!controller.listen()) { + return 1; + } + + return app.exec(); +}