/*! * * Copyright (c) 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 DCSSimController.cpp * \author (original) Stephen Quong * \date (original) 30-Jul-2026 * */ #include #include #include #include #include #include "DCSSimController.h" #include "LeahiMsgDefs.h" #include "LeahiMsgProtoUtils.h" /*! * \brief DCSSimController::DCSSimController * \details Constructor * \param port TCP port to listen on. * \param parent QObject parent. */ DCSSimController::DCSSimController(quint16 port, QObject *parent) : QObject(parent), _port(port) { connect(&_server, &QTcpServer::newConnection, this, &DCSSimController::onNewConnection); } /*! * \brief DCSSimController::listen * \details Starts the controller listening on all interfaces. * \return true on success, false if the port cannot be bound. */ bool DCSSimController::listen() { if (!_server.listen(QHostAddress::Any, _port)) { qCritical().noquote() << "DCSSimController: cannot listen on port" << _port << "—" << _server.errorString(); return false; } qInfo().noquote() << "DCSSimController: listening on port" << _port; return true; } /*! * \brief DCSSimController::onNewConnection * \details Accepts the pending connection, if another client is not already connect. */ void DCSSimController::onNewConnection() { if (_client != nullptr) { qWarning().noquote() << "DCSSimController: connection refused, client already connected"; _server.nextPendingConnection()->deleteLater(); return; } _client = _server.nextPendingConnection(); qInfo().noquote() << "DCSSimController: client connected from" << _client->peerAddress().toString(); connect(_client, &QTcpSocket::readyRead, this, &DCSSimController::onReadyRead); connect(_client, &QTcpSocket::disconnected, this, &DCSSimController::onDisconnected); } /*! * \brief DCSSimController::onDisconnected * \details Perform housekeeping on client disconnect. */ void DCSSimController::onDisconnected() { qInfo().noquote() << "DCSSimController: client disconnected after" << _publishCount << "messages"; _client->deleteLater(); _client = nullptr; _rxBuf.clear(); } /*! * \brief DCSSimController::onReadyRead * \details Handler for incoming data from the socket. */ void DCSSimController::onReadyRead() { _rxBuf.append(_client->readAll()); Mqtt::ReadState state = Mqtt::ReadState::Incomplete; do { Mqtt::Packet packet; state = Mqtt::readPacket(_rxBuf, packet); if (state == Mqtt::ReadState::Complete) { handlePacket(packet); } else if (state == Mqtt::ReadState::Error) { qWarning().noquote() << "DCSSimController: client read error, dropping the connection"; _rxBuf.clear(); _client->abort(); return; } } while (state == Mqtt::ReadState::Complete && !_rxBuf.isEmpty() && _client != nullptr); } /*! * \brief DCSSimController::handlePacketn * \details Incoming MQTT message handler. * \param packet The decoded MQTT message. */ void DCSSimController::handlePacket(const Mqtt::Packet &packet) { switch (packet.type) { case Mqtt::PacketType::Connect: handleConnectPacket(packet); break; case Mqtt::PacketType::Publish: handlePublishPacket(packet); break; case Mqtt::PacketType::PingReq: _client->write(Mqtt::buildPingResp()); break; case Mqtt::PacketType::Disconnect: qInfo().noquote() << "DCSSimController: DISCONNECT requested from client"; _client->disconnectFromHost(); break; default: qWarning().noquote() << "DCSSimController: received unexpected" << Mqtt::typeName(packet.type) << "from client"; break; } } /*! * \brief DCSSimController::handleConnectPacket * \details Handle received CONNECT message. * \param packet The decoded message. */ void DCSSimController::handleConnectPacket(const Mqtt::Packet &packet) { qInfo().noquote() << QString("DCSSimController: CONNECT requested: clientId=%1 keepAlive=%2s cleanSession=%3") .arg(packet.clientId) .arg(packet.keepAliveSecs) .arg(packet.cleanSession ? "true" : "false"); _client->write(Mqtt::buildConnAck(Mqtt::ConnAckCode::Accepted)); qInfo().noquote() << "DCSSimController: CONNACK sent"; } /*! * \brief DCSSimController::handlePublishPacket * \details Handle received PUBLISH message. * \param packet The decoded message. */ void DCSSimController::handlePublishPacket(const Mqtt::Packet &packet) { ++_publishCount; qInfo().noquote() << QString("DCSSimController: PUBLISH: topic=%1 packetId=%2 qos=%3 bytes=%4") .arg(packet.topic) .arg(packet.packetId) .arg(packet.qos) .arg(packet.payload.size()); if (packet.qos == 1) { _client->write(Mqtt::buildPubAck(packet.packetId)); qInfo().noquote() << QString("DCSSimController: PUBACK sent (packetId=%1)").arg(packet.packetId); } dumpPayload(packet.payload); } /*! * \brief DCSSimController::dumpPayload * \details Decodes a payload containing a serialized protobuf message and prints it as JSON. * \param payload Serialized protobuf bytes from the PUBLISH message. */ void DCSSimController::dumpPayload(const QByteArray &payload) { // The Envelope header is field 1 of every typed message, so parsing the // payload as an Envelope reads the header without knowing the real type yet. leahi::messages::Envelope envelope; if (!envelope.ParseFromArray(payload.constData(), static_cast(payload.size()))) { qWarning().noquote() << "DCSSimController: could not parse the Envelope header, payload dropped"; return; } const leahi::messages::Header &header = envelope.header(); const quint16 msgId = static_cast(header.msgid()); qInfo().noquote() << QString("DCSSimController: msgId=0x%1 (%2) serial=%3 seq=%4") .arg(msgId, 4, 16, QChar('0')) .arg(leahi::msgIdString(static_cast(msgId))) .arg(QString::fromStdString(header.deviceserialnum())) .arg(header.sequence()); const std::string &typeName = leahi::msgIdToProtoName(msgId); if (typeName.empty()) { qWarning().noquote() << QString("DCSSimController: unknown message with msgId=0x%1") .arg(msgId, 4, 16, QChar('0')); return; } const google::protobuf::Descriptor *descriptor = google::protobuf::DescriptorPool::generated_pool()->FindMessageTypeByName(typeName); if (descriptor == nullptr) { qWarning().noquote() << "DCSSimController: no descriptor for" << QString::fromStdString(typeName); return; } google::protobuf::DynamicMessageFactory factory; std::unique_ptr body(factory.GetPrototype(descriptor)->New()); if (!body->ParseFromArray(payload.constData(), static_cast(payload.size()))) { qWarning().noquote() << "DCSSimController: could not parse" << QString::fromStdString(typeName); return; } std::string json; google::protobuf::util::JsonPrintOptions options; options.add_whitespace = true; options.always_print_primitive_fields = true; google::protobuf::util::MessageToJsonString(*body, &json, options); qDebug().noquote() << QString::fromStdString(typeName) << ":" << Qt::endl << QString::fromStdString(json); }