Skip to content
Snippets Groups Projects
ds.cpp 8.44 KiB
// Copyright (C) 2024 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0

#include "ds.h"

#include <QJsonDocument>
#include <QJsonObject>
#include <QScreen>

namespace PackageFromDesignStudio {
using namespace Qt::Literals;
constexpr auto designStudioReady = "designStudioReady"_L1;
constexpr auto projectData = "projectData"_L1;
constexpr auto stopRunningProject = "stopRunningProject"_L1;
}; // namespace PackageFromDesignStudio

namespace PackageToDesignStudio {
using namespace Qt::Literals;
constexpr auto deviceInfo = "deviceInfo"_L1;
constexpr auto projectReceivingProgress = "projectReceivingProgress"_L1;
constexpr auto projectStarting = "projectStarting"_L1;
constexpr auto projectStarted = "projectRunning"_L1;
constexpr auto projectStopped = "projectStopped"_L1;
constexpr auto projectLogs = "projectLogs"_L1;
}; // namespace PackageToDesignStudio

DesignStudio::DesignStudio(QWebSocket *socket, const QString &deviceID, QObject *parent)
    : QObject(parent)
    , m_deviceID(deviceID)
    , m_socket(socket)
{
    initPingPong();
    initSocket();
    startPingPong();
    m_projectStallTimer.setInterval(5000);
    m_projectStallTimer.setSingleShot(true);
    connect(&m_projectStallTimer, &QTimer::timeout, this, [this]() {
        qDebug() << "Project is stalled. Closing the connection.";
        m_socket->close();
        m_socket->abort();
    });

    m_speedCalculator.setInterval(1000);
    m_speedCalculator.setSingleShot(false);
    connect(&m_speedCalculator, &QTimer::timeout, this, [this]() {
        quint64 elapsedTime = m_projectNotificationTime.msecsTo(QTime::currentTime()) / 1000;
        int receiveSpeed = elapsedTime > 0 ? m_projectData.size() / elapsedTime / 1024 : 0;
        qDebug() << "Receive speed" << receiveSpeed << "KB/s (" << receiveSpeed / 1024 << "MB/s )"
                 << "percentage" << m_lastPercentage << "%";
    });
}

void DesignStudio::initPingPong()
{
    m_pingTimer.setInterval(1000);
    m_pongTimer.setInterval(30000);
    m_pongTimer.setSingleShot(true);
    m_pingTimer.setSingleShot(true);

    connect(&m_pingTimer, &QTimer::timeout, this, [this]() {
        m_socket->ping();
        startPingPong();
    });

    connect(m_socket.data(),
            &QWebSocket::pong,
            this,
            [this](quint64 elapsedTime, const QByteArray &) {
                if (elapsedTime > 1000)
                    qWarning() << "Design Studio pong is too slow:" << elapsedTime
                               << "ms. Newtork issue?";
                else if (elapsedTime > 500)
                    qWarning() << "Design Studio pong is slow:" << elapsedTime << "ms";

                m_pongTimer.stop();
                m_pingTimer.start();
            });

    connect(&m_pongTimer, &QTimer::timeout, this, [this]() {
        qDebug() << "Design Studio" << m_designStudioID
                 << "is not responding. Closing the connection.";
        m_socket->close();
        m_socket->abort();
    });
}

void DesignStudio::stopPingPong()
{
    m_pingTimer.stop();
    m_pongTimer.stop();
}

void DesignStudio::startPingPong()
{
    m_pingTimer.start();
}

void DesignStudio::initSocket()
{
    connect(m_socket.data(), &QWebSocket::disconnected, this, [this]() {
        qDebug() << "Design Studio" << m_designStudioID << "disconnected";
        m_pingTimer.stop();
        m_pongTimer.stop();
        m_projectStallTimer.stop();
        m_speedCalculator.stop();
        m_projectData.clear();
        emit disconnected(m_designStudioID);
    });

    connect(m_socket.data(),
            &QWebSocket::textMessageReceived,
            this,
            &DesignStudio::processTextMessage);

    connect(m_socket.data(),
            &QWebSocket::binaryMessageReceived,
            this,
            &DesignStudio::processBinaryMessage);
}

void DesignStudio::disconnect()
{
    m_socket->close();
    m_socket->abort();
}

QString DesignStudio::ipv4Addr() const
{
    return m_socket->peerAddress().toString();
}

QString DesignStudio::id() const
{
    return m_designStudioID;
}

void DesignStudio::sendDeviceInfo()
{
    const QRect screenGeometry = QGuiApplication::primaryScreen()->geometry();
    QJsonObject deviceInfo;
    deviceInfo["screenHeight"] = screenGeometry.height();
    deviceInfo["screenWidth"] = screenGeometry.width();
    deviceInfo["os"] = QSysInfo::prettyProductName();
    deviceInfo["osVersion"] = QSysInfo::productVersion();
    deviceInfo["architecture"] = QSysInfo::currentCpuArchitecture();
    deviceInfo["deviceId"] = m_deviceID;
    deviceInfo["appVersion"] = QString(CMAKE_VAR_GIT_VERSION);

    qDebug() << "Sending device info to Design Studio" << deviceInfo;

    sendData(PackageToDesignStudio::deviceInfo, deviceInfo);
}

void DesignStudio::sendData(const QLatin1String &dataType, const QJsonValue &data)
{
    QJsonObject message;
    message["dataType"] = dataType;
    message["data"] = data;

    m_socket->sendTextMessage(QJsonDocument(message).toJson(QJsonDocument::Compact));
}

void DesignStudio::processTextMessage(const QString &message)
{
    QJsonParseError jsonError;
    const QJsonDocument jsonDoc = QJsonDocument::fromJson(message.toLatin1(), &jsonError);

    if (jsonError.error != QJsonParseError::NoError) {
        qDebug() << "Failed to parse JSON message:" << jsonError.errorString() << message;
        return;
    }

    const QJsonObject jsonObj = jsonDoc.object();
    if (!jsonObj.contains("dataType")) {
        qDebug() << "Invalid JSON message:" << jsonObj;
        return;
    }

    const QString dataType = jsonObj.value("dataType").toString();
    const QJsonObject data = jsonObj.value("data").toObject();

    if (dataType == PackageFromDesignStudio::designStudioReady) {
        const QString newDesignStudioId = data["designStudioID"].toString();
        const int commVersion = data["commVersion"].toInt();
        qDebug() << "Design Studio ready with ID" << newDesignStudioId << "and comm version"
                 << commVersion;
        m_designStudioID = newDesignStudioId;
        sendDeviceInfo();
        emit idReceived(newDesignStudioId, ipv4Addr());
    } else if (dataType == PackageFromDesignStudio::projectData) {
        m_incomingProjectSize = data["projectSize"].toInt();
        const QString qtVersion = data["qtVersion"].toString();

        auto qcompare = QString::compare(qtVersion, QT_VERSION_STR, Qt::CaseInsensitive);
        if (qcompare != 0) {
            qDebug() << "Qt version mismatch. Expected" << QT_VERSION_STR " or lower, but got"
                     << qtVersion << ". Project may not work correctly.";
            emit projectVersionMismatch(QT_VERSION_STR, qtVersion);
        }

        qDebug() << "Project is expected with size" << m_incomingProjectSize << "and Qt version"
                 << qtVersion;
        m_projectData.clear();
        m_projectNotificationTime = QTime::currentTime();
        m_speedCalculator.start();
        m_projectStallTimer.start();

        stopPingPong();
        emit projectIncoming(m_designStudioID, m_incomingProjectSize);
    } else if (dataType == PackageFromDesignStudio::stopRunningProject) {
        qDebug() << "Stop running project requested";
        m_projectData.clear();
        m_speedCalculator.stop();
        m_projectStallTimer.stop();
        emit projectStopRequested(m_designStudioID);
    } else {
        qDebug() << "Unkown JSON message type:" << dataType;
    }
}

void DesignStudio::processBinaryMessage(const QByteArray &data)
{
    m_projectStallTimer.start();
    m_projectData.append(data);
    const bool isProjectComplete = m_projectData.size() == m_incomingProjectSize;

    int percentage = m_projectData.size() * 100 / m_incomingProjectSize;

    if (m_lastPercentage != percentage) {
        emit projectIncomingProgress(m_designStudioID, percentage);
        m_lastPercentage = percentage;
        sendData(PackageToDesignStudio::projectReceivingProgress, percentage);
    }

    if (isProjectComplete) {
        startPingPong();
        sendProjectStarting();
        emit projectReceived(m_designStudioID, m_projectData);
        m_speedCalculator.stop();
        m_projectStallTimer.stop();
    }
}

void DesignStudio::sendProjectStarting()
{
    sendData(PackageToDesignStudio::projectStarting);
}

void DesignStudio::sendProjectStarted()
{
    sendData(PackageToDesignStudio::projectStarted);
}

void DesignStudio::sendProjectStopped()
{
    sendData(PackageToDesignStudio::projectStopped);
}

void DesignStudio::sendProjectLogs(const QString &logs)
{
    sendData(PackageToDesignStudio::projectLogs, logs);
}