/*! * * 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 "LeahiMsgProtoUtils.h" #include #include #include #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: handleMessage(_rxMsg); _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::handleMessage * \details Decodes a received frame and prints its typed body as JSON, generically * via the protobuf descriptor pool (msgId -> message name -> dynamic parse). * \param msg The complete AgentMessage frame. */ void AgentSimController::handleMessage(const AgentMessage &msg) { const QByteArray payload = msg.payload(); logMessage(msg.msgId(), msg.sequence(), payload); // Read the Header (field 1, shared by every typed message) to learn the msgId. leahi::messages::Envelope envelope; if (!envelope.ParseFromArray(payload.constData(), payload.size())) { qWarning().noquote() << "AgentSim: could not parse Envelope header — frame dropped"; return; } const leahi::messages::Header &header = envelope.header(); // Identify the concrete message type from the msgId and decode it generically. const std::string &typeName = leahi::msgIdToProtoName(static_cast(header.msgid())); if (typeName.empty()) { qWarning().noquote() << QString("AgentSim: unknown msgId=0x%1 — cannot decode typed body") .arg(header.msgid(), 4, 16, QChar('0')); return; } const google::protobuf::Descriptor *desc = google::protobuf::DescriptorPool::generated_pool()->FindMessageTypeByName(typeName); if (desc == nullptr) { qWarning().noquote() << "AgentSim: no descriptor for" << QString::fromStdString(typeName); return; } google::protobuf::DynamicMessageFactory factory; std::unique_ptr body(factory.GetPrototype(desc)->New()); if (!body->ParseFromArray(payload.constData(), payload.size())) { qWarning().noquote() << "AgentSim: could not parse" << QString::fromStdString(typeName); return; } std::string json; google::protobuf::util::JsonPrintOptions opts; opts.add_whitespace = true; opts.always_print_primitive_fields = true; google::protobuf::util::MessageToJsonString(*body, &json, opts); // protobuf renders msgId as a decimal integer; rewrite it as 0x + 4-digit hex. QString jsonStr = QString::fromStdString(json); jsonStr.replace(QString("\"msgId\": %1").arg(header.msgid()), QString("\"msgId\": \"0x%1\"").arg(header.msgid(), 4, 16, QChar('0'))); qDebug().noquote() << QString::fromStdString(typeName) << ":" << Qt::endl << jsonStr; } /*! * \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()); }