From 9a20e7cbea82f31acd1976879b36b0e3a15e216e Mon Sep 17 00:00:00 2001
From: Burak Hancerli <burak.hancerli@qt.io>
Date: Fri, 17 Nov 2023 13:35:22 +0100
Subject: [PATCH] add: network connection between DS and DV

---
 src/backend.cpp        |  90 +++++++++++++++++++++++++--
 src/backend.h          |   7 ++-
 src/dsConnector.cpp    | 108 ++++++++++++++++++++++++--------
 src/dsConnector.h      |  29 +++++++--
 src/projectManager.cpp | 138 +++++++++++++++++++++--------------------
 ui/Network.qml         |  30 ++++++++-
 6 files changed, 294 insertions(+), 108 deletions(-)

diff --git a/src/backend.cpp b/src/backend.cpp
index a1ebd29..8a1fcca 100644
--- a/src/backend.cpp
+++ b/src/backend.cpp
@@ -80,12 +80,6 @@ void Backend::initialize()
             this,
             &Backend::popupTextChanged);
 
-    connect(&m_designStudioConnector,
-            &DesignStudioConnector::networkStatusUpdated,
-            this,
-            &Backend::networkUpdated);
-    m_designStudioConnector.initialize();
-
     qDebug("Initialization complete");
 }
 
@@ -103,6 +97,90 @@ void Backend::updatePopup(const QString &text, bool indeterminate)
     QEventLoop().processEvents(QEventLoop::AllEvents, 100);
 }
 
+bool Backend::connectDesignStudio()
+{
+    if (m_designStudioConnector) {
+        qDebug() << "Design Studio Connector is already initialized";
+        return true;
+    }
+
+    qDebug() << "Initializing Design Studio Connector";
+
+    connect(
+        &m_dsConnectorThread,
+        &QThread::started,
+        this,
+        [&] {
+            m_designStudioConnector.reset(new DesignStudioConnector);
+            connect(m_designStudioConnector.data(),
+                    &DesignStudioConnector::networkStatusUpdated,
+                    this,
+                    &Backend::networkUpdated);
+
+            connect(m_designStudioConnector.data(),
+                    &DesignStudioConnector::projectReceived,
+                    &m_projectManager,
+                    [this](const QByteArray &projectData) {
+                        qDebug() << "Project received from Design Studio";
+                        emit popupOpen();
+                        updatePopup("Unpacking project...");
+                        qDebug() << "Project data size: " << projectData.size();
+                        const QString projectPath = m_projectManager.unpackProject(projectData);
+
+                        if (projectPath.isEmpty()) {
+                            showWarning("Could not unpack project. Please check the logs for more "
+                                        "information.");
+                            emit popupClose();
+                            return;
+                        }
+
+                        qDebug() << "Project unpacked to " << projectPath;
+                        updatePopup("Running project...");
+
+                        if (!m_projectManager.runProject(projectPath, "UntitledProject35")) {
+                            showWarning("Could not run project. Please check the logs for more "
+                                        "information.");
+                        } else {
+                            m_projectManager.showAppWindow();
+                        }
+                        emit popupClose();
+                    });
+            if (!m_designStudioConnector->initialize()) {
+                showWarning("Could initialize server. Please check the logs for more information.");
+                m_designStudioConnector.reset();
+            }
+        },
+        Qt::DirectConnection);
+
+    m_dsConnectorThread.start();
+
+    qDebug() << "Design Studio Connector is initialized";
+    return true;
+}
+
+void Backend::disconnectDesignStudio()
+{
+    if (!m_designStudioConnector) {
+        qDebug() << "Design Studio Connector is not initialized";
+        return;
+    }
+
+    qDebug() << "Disconnecting from Design Studio";
+
+    connect(
+        &m_dsConnectorThread,
+        &QThread::finished,
+        this,
+        [&] {
+            qDebug() << "Design Studio Connector is disconnected";
+            m_designStudioConnector.reset();
+        },
+        Qt::DirectConnection);
+
+    m_dsConnectorThread.quit();
+    m_dsConnectorThread.wait();
+}
+
 void Backend::runDemoProject(const QString &projectName)
 {
     qDebug() << "Checking if demo project is cached for " << projectName;
diff --git a/src/backend.h b/src/backend.h
index 6227665..9f3d7c6 100644
--- a/src/backend.h
+++ b/src/backend.h
@@ -26,6 +26,8 @@
 #ifndef DV_ANDROID_H
 #define DV_ANDROID_H
 
+#include <QThread>
+
 #include "dsConnector.h"
 #include "projectManager.h"
 #include "serviceConnector.h"
@@ -58,7 +60,8 @@ private:
     QString m_userHash;
     ServiceConnector m_serviceConnector;
     ProjectManager m_projectManager;
-    DesignStudioConnector m_designStudioConnector;
+    QScopedPointer<DesignStudioConnector> m_designStudioConnector;
+    QThread m_dsConnectorThread;
 
     // member functions
     void showWarning(const QString &message);
@@ -83,6 +86,8 @@ public slots:
     void runDemoProject(const QString &projectName);
     void clearDemoCaches();
     void registerUser(const QUrl &url);
+    bool connectDesignStudio();
+    void disconnectDesignStudio();
 };
 
 #endif // DV_ANDROID_H
diff --git a/src/dsConnector.cpp b/src/dsConnector.cpp
index 726555f..93785da 100644
--- a/src/dsConnector.cpp
+++ b/src/dsConnector.cpp
@@ -27,57 +27,111 @@
 
 #include <QNetworkInterface>
 
-DesignStudioConnector::DesignStudioConnector(QObject *parent)
-    : QObject(parent)
-    , m_socket(this)
-    , m_broadcastTimer(this)
-{}
+void DesignStudioConnector::receiveProject()
+{
+    qDebug() << "Reading data from Design Studio";
+    emit networkStatusUpdated("Reading data from Design Studio");
+    QByteArray data = m_tcpSocket->readAll();
+    qDebug() << "Data from Design Studio:" << data;
+    // if (data.startsWith(":qmlrc_project_starts:")) {
+    //     qDebug() << "Received qmlrc_project";
+    //     emit networkStatusUpdated("Received qmlrc_project");
+    //     m_projectData.append(data.mid(21, data.size() - 21));
+    //     m_receivingData = true;
+    // } else if (data.contains(":qmlrc_project_ends:")) {
+    //     qDebug() << "Received qmlrc_project_ends";
+    //     emit networkStatusUpdated("Received qmlrc_project_ends");
+    //     m_projectData.append(data.mid(0, data.size() - 20));
+    //     emit projectReceived(m_projectData);
+    //     m_projectData.clear();
+    //     m_receivingData = false;
+    // } else if (m_receivingData) {
+    //     qDebug() << "Received data:" << data;
+    //     emit networkStatusUpdated("Receiving project data");
+    //     m_projectData.append(data);
+    // } else {
+    //     qDebug() << "Received unknown data:" << data;
+    //     emit networkStatusUpdated("Received unknown data");
+    // }
+    m_projectData.append(data);
+}
 
-bool DesignStudioConnector::initialize()
+bool DesignStudioConnector::initTcpServer()
 {
-    // const bool retVal = m_socket.bind(39000);
+    const bool retVal = m_tcpServer.listen(QHostAddress::Any, m_tcpPort);
 
-    // if (!retVal) {
-    //     qDebug() << "Failed to bind socket";
-    //     emit networkStatusUpdated("Failed to bind socket on port 39000");
-    //     return false;
-    // }
+    if (!retVal) {
+        qDebug() << "Failed to listen on port " << m_tcpPort;
+        emit networkStatusUpdated("Failed to bind on port " + QString::number(m_tcpPort));
+        return false;
+    }
+
+    qDebug() << "Listening on port " << m_tcpPort;
+    connect(&m_tcpServer, &QTcpServer::newConnection, this, [this]() {
+        qDebug() << "New connection from Design Studio";
+        emit networkStatusUpdated("Design Studio is connected.\n Waiting for project...");
+        m_tcpSocket.reset(m_tcpServer.nextPendingConnection());
+        m_broadcastTimer.stop();
+
+        connect(m_tcpSocket.data(), &QTcpSocket::disconnected, this, [this]() {
+            qDebug() << "Disconnected from Design Studio";
+            emit networkStatusUpdated("\nLocal IP: " + m_ipv4Addr
+                                      + "\nWaiting for Design Studio to connect...");
+            m_broadcastTimer.start();
+            m_receivingData = false;
+            emit projectReceived(m_projectData);
+            m_projectData.clear();
+        });
+
+        connect(m_tcpSocket.data(),
+                &QTcpSocket::readyRead,
+                this,
+                &DesignStudioConnector::receiveProject);
+    });
+
+    return true;
+}
 
+void DesignStudioConnector::initBroadcast()
+{
     // get ipv4 address
-    QString ipv4Addr;
     const QList<QHostAddress> list = QNetworkInterface::allAddresses();
     for (const QHostAddress &address : list) {
         if (address.protocol() == QAbstractSocket::IPv4Protocol
             && address != QHostAddress::LocalHost) {
             qDebug() << "Found IPv4 address:" << address.toString();
-            ipv4Addr = address.toString();
+            m_ipv4Addr = address.toString();
             break;
         }
     }
 
-    // connect(&m_socket, &QUdpSocket::readyRead, this, &DesignStudioConnector::readPendingDatagrams);
-    qDebug() << "Advertising from" << ipv4Addr;
-    emit networkStatusUpdated("\nLocal IP: " + ipv4Addr
+    qDebug() << "Advertising from" << m_ipv4Addr;
+    emit networkStatusUpdated("\nLocal IP: " + m_ipv4Addr
                               + "\nWaiting for Design Studio to connect...");
 
     // Broadcast a message to the network in each 2 seconds
     // to let the Design Studio know that we are here
     connect(&m_broadcastTimer, &QTimer::timeout, this, [this]() {
-        QByteArray datagram = "Qt Design Viewer";
-        m_socket.writeDatagram(datagram.data(), datagram.size(), QHostAddress::Broadcast, 39000);
+        QByteArray datagram = "qt_ui_viewer_at:" + m_ipv4Addr.toUtf8() + ":"
+                              + QByteArray::number(m_tcpPort);
+
+        m_udpSocket.writeDatagram(datagram.data(),
+                                  datagram.size(),
+                                  QHostAddress::Broadcast,
+                                  m_udpPort);
+
         qDebug() << "Broadcasting datagram:" << datagram;
     });
+
     m_broadcastTimer.setSingleShot(false);
     m_broadcastTimer.start(2000);
-    return true;
 }
 
-void DesignStudioConnector::readPendingDatagrams()
+bool DesignStudioConnector::initialize()
 {
-    while (m_socket.hasPendingDatagrams()) {
-        QByteArray datagram;
-        datagram.resize(m_socket.pendingDatagramSize());
-        m_socket.readDatagram(datagram.data(), datagram.size());
-        qDebug() << "Received datagram:" << datagram;
-    }
+    if (!initTcpServer())
+        return false;
+
+    initBroadcast();
+    return true;
 }
diff --git a/src/dsConnector.h b/src/dsConnector.h
index 5857e38..5e577cc 100644
--- a/src/dsConnector.h
+++ b/src/dsConnector.h
@@ -27,6 +27,8 @@
 #define DSCONNECTOR_H
 
 #include <QObject>
+#include <QTcpServer>
+#include <QTcpSocket>
 #include <QTimer>
 #include <QUdpSocket>
 
@@ -34,18 +36,35 @@ class DesignStudioConnector : public QObject
 {
     Q_OBJECT
 public:
-    explicit DesignStudioConnector(QObject *parent = nullptr);
+    explicit DesignStudioConnector(QObject *parent = nullptr)
+        : QObject(parent)
+    {}
     bool initialize();
 
-private slots:
-    void readPendingDatagrams();
-
 private:
-    QUdpSocket m_socket;
+    // Tcp connection members
+    QTcpServer m_tcpServer;
+    QScopedPointer<QTcpSocket> m_tcpSocket;
+    const quint32 m_tcpPort = 40000;
+
+    // Udp connection members
+    QUdpSocket m_udpSocket;
     QTimer m_broadcastTimer;
+    QString m_ipv4Addr;
+    const quint32 m_udpPort = 39000;
+
+    // Other members
+    QByteArray m_projectData;
+    bool m_receivingData;
+
+    // Member functions
+    bool initTcpServer();
+    void initBroadcast();
+    void receiveProject();
 
 signals:
     void networkStatusUpdated(QString);
+    void projectReceived(QByteArray);
 };
 
 #endif // DSCONNECTOR_H
diff --git a/src/projectManager.cpp b/src/projectManager.cpp
index fc0249d..f9c25f1 100644
--- a/src/projectManager.cpp
+++ b/src/projectManager.cpp
@@ -213,11 +213,16 @@ bool ProjectManager::runProject(const QString &projectPath, const QString &proje
 
     QUrl mainQmlUrl = QUrl::fromUserInput(mainQmlFilePath);
     QFile file(mainQmlUrl.path());
-    // QFile file(mainQmlUrl.path().prepend(":"));
     if (!file.open(QIODevice::ReadOnly)) {
-        qCritical() << "Could not open mainQmlfile for reading! " << file.fileName() << ": "
-                    << file.errorString();
-        return false;
+        qWarning() << "Could not open mainQmlfile for reading! " << file.fileName() << ": "
+                   << file.errorString();
+        qWarning() << "Trying to open it as a resource file.";
+        file.setFileName(mainQmlUrl.path().prepend(":"));
+        if (!file.open(QIODevice::ReadOnly)) {
+            qCritical() << "Could not open mainQmlfile for reading! " << file.fileName() << ": "
+                        << file.errorString();
+            return false;
+        }
     }
 
     qDebug() << "Looking for qtquickcontrols2File in " << projectPath;
@@ -226,75 +231,76 @@ bool ProjectManager::runProject(const QString &projectPath, const QString &proje
     if (!qtquickcontrols2File.isEmpty()) {
         qDebug() << "Found qtquickcontrols2File: " << qtquickcontrols2File;
         qputenv("QT_QUICK_CONTROLS_CONF", qtquickcontrols2File.toLatin1());
-    }
+        }
 
-    qDebug() << "Initializing the qmlEngine";
-    m_qmlEngine.reset(new QQmlEngine);
-    m_qmlEngine->clearComponentCache();
+        qDebug() << "Initializing the qmlEngine";
+        m_qmlEngine.reset(new QQmlEngine);
+        m_qmlEngine->clearComponentCache();
 
-    qDebug("Adding import paths");
-    for (const QString &importPath : importPaths) {
-        qDebug() << "-- Import path: " << importPath;
-        m_qmlEngine->addImportPath(importPath);
-    }
+        qDebug("Adding import paths");
+        for (const QString &importPath : importPaths) {
+            qDebug() << "-- Import path: " << importPath;
+            m_qmlEngine->addImportPath(importPath);
+        }
 
-    QObject::connect(m_qmlEngine.data(),
-                     &QQmlEngine::warnings,
-                     this,
-                     [&](const QList<QQmlError> &warnings) {
-                         for (const auto &warning : warnings) {
-                             qWarning() << warning.toString();
-                         }
-                     });
-
-    emit projectStateChanged("Loading project...");
-    QEventLoop().processEvents(QEventLoop::AllEvents, 1000);
-
-    qDebug() << "Loading mainQmlUrl: " << mainQmlUrl.toString();
-    m_qmlComponent.reset(new QQmlComponent(m_qmlEngine.data()));
-    m_qmlComponent->loadUrl(mainQmlUrl);
-
-    qDebug() << "Waiting for qmlComponent to load";
-    while (m_qmlComponent->isLoading())
-        QCoreApplication::processEvents();
-
-    qDebug() << "Checking if m_qmlComponent is ready";
-    if (!m_qmlComponent->isReady()) {
-        qCritical() << "m_qmlComponent is not ready. Reason:" << m_qmlComponent->errorString();
-        return false;
-    }
-    qDebug() << "Creating top level object";
-    QObject *topLevel = m_qmlComponent->create();
-    if (!topLevel && m_qmlComponent->isError()) {
-        qCritical() << "Error while creating Qml m_qmlComponent:" << m_qmlComponent->errorString();
-        return false;
-    }
+        QObject::connect(m_qmlEngine.data(),
+                         &QQmlEngine::warnings,
+                         this,
+                         [&](const QList<QQmlError> &warnings) {
+                             for (const auto &warning : warnings) {
+                                 qWarning() << warning.toString();
+                             }
+                         });
+
+        emit projectStateChanged("Loading project...");
+        QEventLoop().processEvents(QEventLoop::AllEvents, 1000);
+
+        qDebug() << "Loading mainQmlUrl: " << mainQmlUrl.toString();
+        m_qmlComponent.reset(new QQmlComponent(m_qmlEngine.data()));
+        m_qmlComponent->loadUrl(mainQmlUrl);
+
+        qDebug() << "Waiting for qmlComponent to load";
+        while (m_qmlComponent->isLoading())
+            QCoreApplication::processEvents();
+
+        qDebug() << "Checking if m_qmlComponent is ready";
+        if (!m_qmlComponent->isReady()) {
+            qCritical() << "m_qmlComponent is not ready. Reason:" << m_qmlComponent->errorString();
+            return false;
+        }
+        qDebug() << "Creating top level object";
+        QObject *topLevel = m_qmlComponent->create();
+        if (!topLevel && m_qmlComponent->isError()) {
+            qCritical() << "Error while creating Qml m_qmlComponent:"
+                        << m_qmlComponent->errorString();
+            return false;
+        }
 
-    emit projectStateChanged("Setting up the quickWindow...");
-    QEventLoop().processEvents(QEventLoop::AllEvents, 1000);
+        emit projectStateChanged("Setting up the quickWindow...");
+        QEventLoop().processEvents(QEventLoop::AllEvents, 1000);
 
-    qDebug() << "Setting up the quickWindow";
-    m_quickWindow.reset(qobject_cast<QQuickWindow *>(topLevel));
-    if (m_quickWindow) {
-        qDebug() << "Running with incubator controller";
-        m_qmlEngine->setIncubationController(m_quickWindow->incubationController());
-    } else {
-        qWarning() << "Top level object is not a QQuickWindow. Trying QQuickView...";
+        qDebug() << "Setting up the quickWindow";
+        m_quickWindow.reset(qobject_cast<QQuickWindow *>(topLevel));
+        if (m_quickWindow) {
+            qDebug() << "Running with incubator controller";
+            m_qmlEngine->setIncubationController(m_quickWindow->incubationController());
+        } else {
+            qWarning() << "Top level object is not a QQuickWindow. Trying QQuickView...";
 
-        QQuickItem *contentItem = qobject_cast<QQuickItem *>(topLevel);
-        if (!contentItem) {
-            qCritical() << "Top level object cannot be casted to QQuickItem. Aborting.";
-            return false;
-        }
+            QQuickItem *contentItem = qobject_cast<QQuickItem *>(topLevel);
+            if (!contentItem) {
+                qCritical() << "Top level object cannot be casted to QQuickItem. Aborting.";
+                return false;
+            }
 
-        qDebug() << "Initializing QQuickView";
-        QQuickView *view = new QQuickView(m_qmlEngine.data(), nullptr);
-        m_quickWindow.reset(view);
-        view->setContent(mainQmlUrl, m_qmlComponent.data(), contentItem);
-        view->setResizeMode(QQuickView::SizeViewToRootObject);
-        m_quickWindow->setBaseSize(QSize(contentItem->width(), contentItem->height()));
-    }
-    return true;
+            qDebug() << "Initializing QQuickView";
+            QQuickView *view = new QQuickView(m_qmlEngine.data(), nullptr);
+            m_quickWindow.reset(view);
+            view->setContent(mainQmlUrl, m_qmlComponent.data(), contentItem);
+            view->setResizeMode(QQuickView::SizeViewToRootObject);
+            m_quickWindow->setBaseSize(QSize(contentItem->width(), contentItem->height()));
+        }
+        return true;
 }
 
 bool ProjectManager::cacheProject(const QByteArray &projectData, const QJsonObject &projectInfo)
diff --git a/ui/Network.qml b/ui/Network.qml
index 88b3130..e2ec0f4 100644
--- a/ui/Network.qml
+++ b/ui/Network.qml
@@ -22,17 +22,18 @@ Item {
             id: infoHeader
             Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
             horizontalAlignment: "AlignHCenter"
-            text: qsTr("Design Studio connection")
+            text: qsTr("Design Studio Connection Menu")
         }
 
         Label {
-            id: status
+            id: statusLabel
             Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
             horizontalAlignment: "AlignHCenter"
+            text: "Click the button below to start\nadvertising this device on the network"
             Connections {
                 target: backend
                 function onNetworkUpdated(newStatus){
-                    status.text = newStatus
+                    statusLabel.text = newStatus
                 }
             }
         }
@@ -42,5 +43,28 @@ Item {
             Layout.fillHeight: true
             Layout.fillWidth: true
         }
+
+        Button {
+            property bool isRunning: false
+            id: startConnection
+            Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
+            Layout.fillWidth: true
+            height: 100
+            text: qsTr("Start Connection")
+            onClicked: {
+                if (isRunning) {
+                    backend.disconnectDesignStudio()
+                    isRunning = false
+                    text = qsTr("Start Connection")
+                    statusLabel.text = "Click the button below to start\nadvertising this device on the network"
+                } else {
+                    let retval = backend.connectDesignStudio()
+                    if (retval) {
+                        text = qsTr("Stop Connection")
+                        isRunning = true
+                    }
+                }
+            }
+        }
     }
 }
-- 
GitLab