diff --git a/.gitignore b/.gitignore
index a8f3c8c2c0bebe67bcb6a4cc172c727e572f6777..8d36dec89c08783de08469f32f102abe6a944d60 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,3 +6,7 @@ src/ui/*.db-wal
 src/ui/share.qrc
 logcat.txt
 output.junit.xml
+src/ui/DesignViewer.qmlproject.qtds
+src/android/AndroidManifest.xml
+logcat.txt
+output.junit.xml
diff --git a/.gitmodules b/.gitmodules
index e3877e67162c697829984109305a8957250cb4aa..bda2b24c434e8f7e0af7b45711373810e0c2c9bf 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -1,12 +1,6 @@
-[submodule "qtquickdesigner-components"]
-path = 3rdparty/qtquickdesigner-components
-url = https://git.qt.io/design-studio/kit-dependencies/qt-quickdesigner-components.git
 [submodule "3rdparty/zxing-cpp"]
-	path = 3rdparty/zxing-cpp
-	url = https://github.com/zxing-cpp/zxing-cpp.git
+path = 3rdparty/zxing-cpp
+url = https://github.com/zxing-cpp/zxing-cpp.git
 [submodule "3rdparty/qtquickdesigner-components"]
-	path = 3rdparty/qtquickdesigner-components
-	url = https://git.qt.io/design-studio/kit-dependencies/qt-quickdesigner-components
-[submodule "3rdparty/googletest"]
-	path = 3rdparty/googletest
-	url = https://github.com/google/googletest.git
+path = 3rdparty/qtquickdesigner-components
+url = https://codereview.qt-project.org/qt-labs/qtquickdesigner-components
diff --git a/CMakeLists.txt b/CMakeLists.txt
index dd4c369cfc67c1492a28bb2d44abcaa47a983f62..d21fb8bd6c17737fff82e5828b74a58436886198 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -25,7 +25,7 @@ find_package(
 find_package(Qt6 REQUIRED COMPONENTS Core)
 qt_policy(SET QTP0002 NEW)
 
-set(QT_MINIMUM_VERSION 6.6.1)
+set(QT_MINIMUM_VERSION 6.7.0)
 if(QT_VERSION VERSION_LESS QT_MINIMUM_VERSION)
     message(FATAL_ERROR "Minimum supported Qt version: ${QT_MINIMUM_VERSION}")
 endif()
@@ -40,8 +40,8 @@ add_definitions( -DCMAKE_VAR_GIT_VERSION="${CMAKE_VAR_GIT_VERSION}" )
 add_definitions( -DCMAKE_VAR_QT_QUICK_COMPONENTS_VERSION="${CMAKE_VAR_QT_QUICK_COMPONENTS_VERSION}" )
 add_definitions( -DCMAKE_VAR_ZXING_VERSION="${CMAKE_VAR_ZXING_VERSION}" )
 
-add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/src)
 add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/3rdparty/zxing-cpp)
+add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/src)
 add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/tests)
 
 message(STATUS "PROJECT VERSION: ${CMAKE_VAR_GIT_VERSION}")
diff --git a/cicd/gitlab-ci.yml b/cicd/gitlab-ci.yml
index 8dd8ae478010e2b7cffbda83defed21d4659c81c..0b3765d5b392032689ad59bcd5889a3495657717 100644
--- a/cicd/gitlab-ci.yml
+++ b/cicd/gitlab-ci.yml
@@ -6,6 +6,11 @@ variables:
 
 workflow:
   name: 'Qt-${QDS_CI_QT_VERSION} - ${CI_COMMIT_MESSAGE}'
+  rules:
+    - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
+    - if: '$CI_COMMIT_BRANCH && $CI_OPEN_MERGE_REQUESTS'
+      when: never
+
 
 stages:
   - build
diff --git a/cicd/stages/build.yml b/cicd/stages/build.yml
index 4dd650528abbf8eaccb9a4dd1a9cfafdcea918f7..b6dde85720f82cac9ce50f451748dacfb4f31aaf 100644
--- a/cicd/stages/build.yml
+++ b/cicd/stages/build.yml
@@ -3,8 +3,8 @@
 build-android:
   extends: .pipeline_common
   stage: build
-  rules:
-    - if: $CI_PIPELINE_SOURCE == "push" || $CI_PIPELINE_SOURCE == "web"
+  # rules:
+  #   - if: $CI_PIPELINE_SOURCE == "push" || $CI_PIPELINE_SOURCE == "web"
   parallel:
     matrix:
       - QDS_CI_JOB_TARGET_ARCH: "arm64_v8a"
diff --git a/cicd/stages/test.yml b/cicd/stages/test.yml
index 5c9943ab8f28e626a0f01ec7a60487d22111c38e..4912a8ba331069d62ff16ab6d39469cd285a1593 100644
--- a/cicd/stages/test.yml
+++ b/cicd/stages/test.yml
@@ -28,7 +28,7 @@ test-x86_64:
         echo "Waiting for test to start"
         sleep 0.1
         counter=$((counter+1))
-        if [ $counter -gt 60 ]; then
+        if [ $counter -gt 180 ]; then
           echo "Test did not start in time"
           exit 1
         fi
@@ -38,11 +38,11 @@ test-x86_64:
     - |
       counter=0
       while [ -n "$(adb shell pidof -s io.qt.qtuiviewer.test)" ]; do
-        echo "Waiting for test to finish"
-        sleep 0.1
+        echo "Waiting for test to finish with PID ${PID_OF_TEST}"
+        sleep 1
         counter=$((counter+1))
-        if [ $counter -gt 60 ]; then
-          echo "Test did not finish in time"
+        if [ $counter -gt 180 ]; then
+          echo "Test did not finish in time or did not start"
           exit 1
         fi
       done
@@ -50,6 +50,8 @@ test-x86_64:
     - adb shell "run-as io.qt.qtuiviewer.test cat /data/data/io.qt.qtuiviewer.test/files/output.junitxml" > output.junit.xml
     - adb uninstall io.qt.qtuiviewer.test
     - mv output.junit.xml ${QDS_CI_JOB_TEST_RESULTS_PATH}/test.junit.xml
+    - chmod +x ${CI_PROJECT_DIR}/tests/scripts/get_test_result.sh
+    - ${CI_PROJECT_DIR}/tests/scripts/get_test_result.sh ${QDS_CI_JOB_TEST_RESULTS_PATH}/test.junit.xml
   artifacts:
     paths:
       - ${QDS_CI_JOB_TEST_RESULTS_PATH}/
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index d991bbb72ecd8afc5942927f3efc7f0f263f0367..5d4948f30840a9d54e2063105f198e09c60b1d7f 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -1,16 +1,21 @@
 find_package(
     Qt6
-    COMPONENTS Core Widgets Quick Gui Qml Multimedia MultimediaWidgets Concurrent
+    COMPONENTS Core Widgets Quick Gui Qml Multimedia MultimediaWidgets Concurrent Network WebSockets
     REQUIRED
 )
 
+find_package(QT NAMES Qt6 Qt5 REQUIRED COMPONENTS Core Network WebSockets)
+
 qt_add_executable(${PROJECT_NAME}
     backend/importdummy.qml
     backend/main.cpp
     backend/backend.cpp backend/backend.h
     backend/serviceconnector.cpp backend/serviceconnector.h
     backend/projectmanager.cpp backend/projectmanager.h
-    backend/dsconnector.cpp backend/dsconnector.h
+    backend/dsconnector/ds.cpp backend/dsconnector/ds.h
+    backend/dsconnector/dsdiscovery.cpp backend/dsconnector/dsdiscovery.h
+    backend/dsconnector/dsmanager.cpp backend/dsconnector/dsmanager.h
+    backend/dsconnector/tcpdatatypes.h
     backend/qrscanner.cpp backend/qrscanner.h
     ui/main.qml
     ../3rdparty/zxing-cpp/example/ZXingQtReader.h
@@ -30,7 +35,7 @@ qt_add_resources(${PROJECT_NAME} "qml"
         ui/main.qml
         ui/HomePage.qml
         ui/Logs.qml
-        ui/Network.qml
+        ui/DSManagement.qml
         ui/ExamplesPage.qml
         ui/AboutHeader.qml
         ui/SettingsPage.qml
@@ -41,7 +46,7 @@ target_link_libraries(${PROJECT_NAME} PRIVATE
     Qt6::Quick Qt6::Gui
     Qt6::Qml Qt6::GuiPrivate
     Qt6::Multimedia Qt6::MultimediaWidgets
-    Qt6::Concurrent
+    Qt6::Concurrent Qt6::Network Qt6::WebSockets
     ZXing::ZXing
 )
 
@@ -49,7 +54,10 @@ qt_add_library(qtuiviewerlib OBJECT
     EXCLUDE_FROM_ALL
     backend/projectmanager.cpp backend/projectmanager.h
     backend/serviceconnector.cpp backend/serviceconnector.h
-    backend/dsconnector.cpp backend/dsconnector.h
+    backend/dsconnector/ds.cpp backend/dsconnector/ds.h
+    backend/dsconnector/dsdiscovery.cpp backend/dsconnector/dsdiscovery.h
+    backend/dsconnector/dsmanager.cpp backend/dsconnector/dsmanager.h
+    backend/dsconnector/tcpdatatypes.h
 )
 
 target_link_libraries(qtuiviewerlib PRIVATE
@@ -61,6 +69,7 @@ target_link_libraries(qtuiviewerlib PRIVATE
     Qt6::Multimedia
     Qt6::MultimediaWidgets
     Qt6::Concurrent
+    Qt6::WebSockets
 )
 
 target_include_directories(qtuiviewerlib PUBLIC
@@ -76,5 +85,7 @@ set_property(TARGET ${PROJECT_NAME} PROPERTY QT_ANDROID_EXTRA_LIBS
     ${ANDROID_OPENSSL_PATH}/libssl_3.so
 )
 
-set_property(TARGET ${PROJECT_NAME} PROPERTY QT_ANDROID_TARGET_SDK_VERSION 34)
-qt6_import_qml_plugins(${PROJECT_NAME})
+# this needs to be increased with every new release
+set(GOOGLE_PLAY_APP_VERSION 27)
+# CMAKE_VAR_GIT_VERSION (coming from the top-level CMakeLists.txt) and GOOGLE_PLAY_APP_VERSION replaced in the following file
+configure_file(${CMAKE_CURRENT_SOURCE_DIR}/android/AndroidManifest.xml.in  ${CMAKE_CURRENT_SOURCE_DIR}/android/AndroidManifest.xml)
diff --git a/src/android/AndroidManifest.xml b/src/android/AndroidManifest.xml.in
similarity index 94%
rename from src/android/AndroidManifest.xml
rename to src/android/AndroidManifest.xml.in
index 0f8638c6a2ecc13b302492d1b76eb1dd5586171f..eec6de3c45a664ddbdb5f10c8e5be853af9ffae2 100644
--- a/src/android/AndroidManifest.xml
+++ b/src/android/AndroidManifest.xml.in
@@ -1,6 +1,7 @@
 <?xml version="1.0"?>
 <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="io.qt.qtuiviewer"
-    android:installLocation="auto" android:versionCode="27" android:versionName="1.0.0">
+    android:installLocation="auto" android:versionCode="@GOOGLE_PLAY_APP_VERSION@"
+    android:versionName="@CMAKE_VAR_GIT_VERSION@">
     <!-- %%INSERT_PERMISSIONS -->
     <!-- %%INSERT_FEATURES -->
     <supports-screens android:anyDensity="true" android:largeScreens="true"
diff --git a/src/backend/backend.cpp b/src/backend/backend.cpp
index 79a35b82f7ef4d4ceb831f17f81e5f87b21de830..6cbbec2fc09c649922376e97dbc38d2536e40b7b 100644
--- a/src/backend/backend.cpp
+++ b/src/backend/backend.cpp
@@ -24,46 +24,82 @@
 ****************************************************************************/
 
 #include "backend.h"
+#include "backend/constants.h"
 
 #include <QDesktopServices>
 #include <QEventLoop>
 #include <QFileInfo>
 #include <QGuiApplication>
+#include <QJniObject>
 #include <QJsonArray>
 #include <QJsonObject>
 #include <QSettings>
+#include <QSysInfo>
+#include <QTimer>
+
+// Q_DECLARE_JNI_CLASS(Secure, "android/provider/Settings$Secure");
+// Q_DECLARE_JNI_CLASS(String, "java/lang/String");
+// Q_DECLARE_JNI_CLASS(ContentResolver, "android/content/ContentResolver");
 
 Backend::Backend(QObject *parent)
     : QObject(parent)
 {
     // This will allow us to open the app with the QR code
     QDesktopServices::setUrlHandler("qtdesignviewer", this, "parseDesignViewerUrl");
+    QDesktopServices::setUrlHandler("qtdesignstudio", this, "parseDesignViewerUrl");
     QDesktopServices::setUrlHandler("https", this, "parseDesignViewerUrl");
-    connect(&m_dsConnectorThread,
-            &QThread::started,
-            this,
-            &Backend::initDesignStudioConnector,
-            Qt::DirectConnection);
-    m_dsConnectorThread.start();
 
-    connect(&m_serviceConnector,
-            &ServiceConnector::downloadProgress,
-            this,
-            &Backend::downloadProgress);
-
-    updateUserProjectList();
     // Initialize background update
-    connect(&m_backgroundTimer, &QTimer::timeout, this, &Backend::updateUserProjectList);
-    m_backgroundTimer.setInterval(1000 * 10);
-    enableBackgroundUpdate(updateInBackground());
+    connect(&m_projectListUpdateTimer, &QTimer::timeout, this, &Backend::updateUserProjectList);
+    m_projectListUpdateTimer.setInterval(1000 * 10);
+    m_projectListUpdateTimer.start();
+    updateUserProjectList();
+    initDesignStudioManager();
+
+    connect(qApp,
+            &QGuiApplication::applicationStateChanged,
+            this,
+            [this](Qt::ApplicationState state) {
+                qDebug() << "Application state changed to:" << state;
+                if (state == Qt::ApplicationState::ApplicationActive) {
+                    m_dsConnectorThread.start();
+                    m_projectListUpdateTimer.start();
+                } else if (state == Qt::ApplicationState::ApplicationSuspended) {
+                    m_dsConnectorThread.quit();
+                    m_projectListUpdateTimer.stop();
+                }
+            });
 
     const QRect screenGeometry = QGuiApplication::primaryScreen()->geometry();
+
+    // Get the unique device ID
+    auto context = QNativeInterface::QAndroidApplication::context();
+    // auto contentResolver = context.callMethod<QtJniTypes::ContentResolver>("getContentResolver");
+    // auto androidId = QtJniTypes::Secure::callStaticMethod<jstring>("getString",
+    //                                                                contentResolver,
+    //                                                                QStringLiteral("android_id"));
+    // const QByteArray serial = androidId.toString().toLatin1();
+
     qDebug() << "Qt Design Viewer";
     qDebug() << "System information:";
     qDebug() << "-- Qt version: " << QT_VERSION_STR;
     qDebug() << "-- OpenSSL support: " << QVariant(QSslSocket::supportsSsl()).toString();
     qDebug() << "-- Screen height: " << QString::number(screenGeometry.height());
     qDebug() << "-- Screen width: " << QString::number(screenGeometry.width());
+    qDebug() << "-- OS: " << QSysInfo::prettyProductName();
+    qDebug() << "-- OS version: " << QSysInfo::productVersion();
+    qDebug() << "-- Architecture: " << QSysInfo::currentCpuArchitecture();
+    qDebug() << "-- Boot ID: " << QSysInfo::bootUniqueId();
+    qDebug() << "-- Kernel type: " << QSysInfo::kernelType();
+    qDebug() << "-- Kernel version: " << QSysInfo::kernelVersion();
+    qDebug() << "-- Machine unique ID: " << QSysInfo::machineUniqueId();
+    qDebug() << "-- Product type: " << QSysInfo::productType();
+    qDebug() << "-- Product version: " << QSysInfo::productVersion();
+    qDebug() << "-- Build ABI: " << QSysInfo::buildAbi();
+    qDebug() << "-- Build CPU architecture: " << QSysInfo::buildCpuArchitecture();
+    // qDebug() << "-- Unique device ID: " << serial;
+
+    qDebug() << "Thread id backend:" << QThread::currentThreadId();
 }
 
 QString Backend::buildInfo() const
@@ -102,7 +138,7 @@ void Backend::setUpdateInBackground(const bool &enabled)
 
 void Backend::setAutoScaleProject(const bool &enabled)
 {
-    QSettings().setValue("system/autoScaleProject", enabled);
+    QSettings().setValue(Constants::Settings::ProjectManager::AutoScale, enabled);
     if (enabled) {
         qDebug() << "Auto scale project is enabled";
     } else {
@@ -112,23 +148,18 @@ void Backend::setAutoScaleProject(const bool &enabled)
 
 void Backend::setUserHash(const QString &userHash)
 {
-    QSettings().setValue("user/hash", userHash);
+    QSettings().setValue(Constants::Settings::Backend::UserHash, userHash);
     emit userHashChanged();
 }
 
-bool Backend::updateInBackground()
-{
-    return QSettings().value("system/updateInBackground", false).toBool();
-}
-
 bool Backend::autoScaleProject()
 {
-    return QSettings().value("system/autoScaleProject", true).toBool();
+    return QSettings().value(Constants::Settings::ProjectManager::AutoScale, true).toBool();
 }
 
 QString Backend::userHash()
 {
-    return QSettings().value("user/hash").toString();
+    return QSettings().value(Constants::Settings::Backend::UserHash).toString();
 }
 
 void Backend::updatePopup(const QString &text, bool indeterminate)
@@ -138,81 +169,65 @@ void Backend::updatePopup(const QString &text, bool indeterminate)
     QEventLoop().processEvents(QEventLoop::AllEvents, 1000);
 }
 
-void Backend::scanQrCode()
-{
-    m_qrScanner.reset(new QrScanner);
-    connect(m_qrScanner.data(), &QrScanner::qrCodeScanned, this, [&](const QString &qrCode) {
-        qDebug() << "QR code scanned:" << qrCode;
-        parseDesignViewerUrl(QUrl(qrCode));
-        m_qrScanner.reset();
-    });
-    m_qrScanner->scanQrCode();
-}
-
 void Backend::initializeProjectManager()
 {
-    m_projectManager.reset(new ProjectManager(autoScaleProject()));
-    connect(m_projectManager.data(), &ProjectManager::closingProject, this, [&] {
+    m_projectManager.reset(new ProjectManager);
+    connect(m_projectManager.get(), &ProjectManager::closingProject, this, [&] {
         emit popupClose();
         m_projectManager.reset();
     });
 }
 
-void Backend::initDesignStudioConnector()
+void Backend::initDesignStudioManager()
 {
-    m_designStudioConnector.reset(new DesignStudioConnector);
-
-    connect(m_designStudioConnector.data(),
-            &DesignStudioConnector::networkStatusUpdated,
-            this,
-            &Backend::networkUpdated);
-
-    connect(m_designStudioConnector.data(), &DesignStudioConnector::projectIncoming, this, [&] {
-        qDebug() << "Project incoming from Design Studio";
-        m_projectManager.reset();
-        emit popupOpen();
-        updatePopup("Receiving project...");
+    connect(&m_dsConnectorThread, &QThread::started, [this] {
+        qDebug() << "Design Studio Manager thread started";
+        m_designStudioManager.reset(new DesignStudioManager);
+
+        // signals goes from DS Manager to UI
+        connect(m_designStudioManager.get(),
+                &DesignStudioManager::registrationPinRequested,
+                this,
+                &Backend::pinRequested);
+
+        connect(m_designStudioManager.get(),
+                &DesignStudioManager::projectReceived,
+                this,
+                &Backend::runDsProject);
+
+        // signals goes from UI to DS Manager
+        connect(this,
+                &Backend::enterPin,
+                m_designStudioManager.get(),
+                &DesignStudioManager::enterPin);
     });
 
-    connect(
-        m_designStudioConnector.data(),
-        &DesignStudioConnector::projectReceived,
-        this,
-        [this](const QByteArray &projectData) {
-            qDebug() << "Project received from Design Studio";
-            initializeProjectManager();
-            emit popupOpen();
-            updatePopup("Unpacking project...");
-            qDebug() << "Project data size: " << projectData.size();
-            const QString projectPath = m_projectManager->unpackProject(projectData);
-
-            if (projectPath.isEmpty()) {
-                qCritical() << "Could not unpack project. Please check the logs for more "
-                               "information.";
-                emit popupClose();
-                return;
-            }
-
-            qDebug() << "Project unpacked to " << projectPath;
-            updatePopup("Running project...");
+    connect(&m_dsConnectorThread, &QThread::finished, this, [this] {
+        qDebug() << "Design Studio Manager thread finished";
+        m_designStudioManager.reset();
+    });
 
-            qDebug() << "Project received confirmation sent to Design Studio";
-            if (!m_projectManager->runProject(projectPath)) {
-                qCritical() << "Could not run project. Please check the logs for more "
-                               "information.";
-            } else {
-                m_projectManager->showAppWindow();
-            }
+    connect(&m_serviceConnector,
+            &ServiceConnector::downloadProgress,
+            this,
+            &Backend::downloadProgress);
+}
 
-            QMetaObject::invokeMethod(m_designStudioConnector.data(),
-                                      "sendProjectReceived",
-                                      Qt::QueuedConnection);
+void Backend::runDsProject(const QByteArray &projectData)
+{
+    initializeProjectManager();
+    emit popupOpen();
+    updatePopup("Unpacking project...");
+    QString projectPath = m_projectManager->unpackProject(projectData);
+    updatePopup("Running project...");
 
-            emit popupClose();
-        },
-        Qt::QueuedConnection);
+    if (!m_projectManager->runProject(projectPath))
+        qCritical() << "Could not run project. Please check the logs for more information.";
+    else {
+        m_projectManager->showAppWindow();
+    }
 
-    qDebug() << "Design Studio Connector is initialized";
+    emit popupClose();
 }
 
 void Backend::runDemoProject(const QString &projectName)
@@ -379,7 +394,6 @@ void Backend::updateUserProjectList()
     const QString userHash = Backend::userHash();
 
     if (userHash.isEmpty()) {
-        qWarning("User hash is not registered");
         return;
     }
 
@@ -416,27 +430,52 @@ void Backend::updateUserProjectList()
     emit projectListChanged();
 }
 
+void Backend::scanQrCode()
+{
+    m_qrScanner.reset(new QrScanner);
+    connect(m_qrScanner.get(), &QrScanner::qrCodeScanned, this, [&](const QString &qrCode) {
+        qDebug() << "QR code scanned:" << qrCode;
+        parseDesignViewerUrl(QUrl(qrCode));
+        m_qrScanner.reset();
+    });
+    m_qrScanner->scanQrCode();
+}
+
 void Backend::parseDesignViewerUrl(const QUrl &url)
 {
-    QString urlData = url.toString();
-    // urlData could be either a direct url to the project or a user hash
-    // If it is a user hash, we register the user and fetch the project list
-    // If it is a project url, we submit the url to the text field
-    // sample url: https://<url>/<project_name>.qmlrc
-    // sample user hash: qtdesignviewer://19f8907b6t84029384hs8djshdu38476
-    if (urlData.isEmpty())
-        return;
-    else if (urlData.startsWith("https//")) {
-        urlData.replace("https//", "https://");
-        emit urlUpdated(urlData);
-    } else if (urlData.startsWith("https://")) {
-        emit urlUpdated(urlData);
-    } else if (urlData.startsWith("qtdesignviewer://")) {
+    // url format could be one of the following:
+    // - https://<url>/<project_name>.qmlrc
+    // - qtdesignviewer://<user_hash>
+    // - qtdesignstudio://<ipv4_address>?<design_studio_id>
+
+    if (url.scheme() == "https") {
+        emit urlUpdated(url.toString());
+    } else if (url.scheme() == "qtdesignviewer") {
         qDebug() << "Registering user from QR code";
-        setUserHash(url.toString().remove("qtdesignviewer://"));
+        setUserHash(url.host());
         updateUserProjectList();
+    } else if (url.scheme() == "qtdesignstudio") {
+        qDebug() << "Connecting to Design Studio from QR code";
+
+        if (url.host().isEmpty()) {
+            qWarning() << "No Design Studio IP address found in the QR code";
+            return;
+        } else if (url.query().isEmpty()) {
+            qWarning() << "No Design Studio ID found in the QR code";
+            return;
+        }
+
+        const QString ipv4Addr = url.host();
+        const QString designStudioId = url.query();
+
+        QMetaObject::invokeMethod(m_designStudioManager.get(),
+                                  "designStudioFound",
+                                  Qt::QueuedConnection,
+                                  Q_ARG(QString, ipv4Addr),
+                                  Q_ARG(QString, designStudioId));
     } else {
-        qWarning() << "Unknown QR code data: " << urlData;
+        qWarning() << "Unknown QR code format";
+        qWarning() << "URL:" << url.toString();
     }
 }
 
diff --git a/src/backend/backend.h b/src/backend/backend.h
index dd8b563f66af7d6766028be8e872d9a8e57e08d0..38cc89c56b0be17d88d896964f5113b2086e8ea2 100644
--- a/src/backend/backend.h
+++ b/src/backend/backend.h
@@ -27,11 +27,13 @@
 #define DV_ANDROID_H
 
 #include <QThread>
+#include <QTimer>
 
-#include "dsconnector.h"
+#include "dsconnector/dsmanager.h"
 #include "projectmanager.h"
 #include "qrscanner.h"
 #include "serviceconnector.h"
+#include <memory>
 
 class Backend : public QObject
 {
@@ -57,12 +59,12 @@ private:
     // Other members
     ServiceConnector m_serviceConnector;
     QThread m_dsConnectorThread;
-    QScopedPointer<ProjectManager> m_projectManager;
-    QScopedPointer<DesignStudioConnector> m_designStudioConnector;
-    QScopedPointer<QrScanner> m_qrScanner;
+    std::unique_ptr<ProjectManager> m_projectManager;
+    std::unique_ptr<DesignStudioManager> m_designStudioManager;
+    std::unique_ptr<QrScanner> m_qrScanner;
 
     // Settings
-    QTimer m_backgroundTimer;
+    QTimer m_projectListUpdateTimer;
 
     // member functions
     void updatePopup(const QString &text, bool indeterminate = true);
@@ -83,8 +85,13 @@ signals:
     void popupOpen();
     void popupClose();
 
-    // UI signals - Network page
-    void networkUpdated(QString);
+    // UI signals - from DS Manager page
+    void pinRequested(const QString &id);
+    void pinPopupOpen();
+
+    // UI signals - from UI
+    void enterPin(const QString &deviceId, const QString &pin);
+    void connectToDesignStudio(const QString &url);
 
 public slots:
     QString buildInfo() const;
@@ -93,19 +100,18 @@ public slots:
     void runOnlineProject(const QString &url);
     void runUserProject(const QString &projectName, const QString &password);
     void runDemoProject(const QString &projectName);
+    void runDsProject(const QByteArray &projectData);
     void clearDemoCaches();
 
-    void initDesignStudioConnector();
+    void initDesignStudioManager();
 
     void parseDesignViewerUrl(const QUrl &url);
 
     // settings - setters
-    void setUpdateInBackground(const bool &enabled);
     void setAutoScaleProject(const bool &enabled);
     void setUserHash(const QString &userHash);
 
     // settings - getters
-    bool updateInBackground();
     bool autoScaleProject();
     QString userHash();
 
@@ -113,7 +119,6 @@ public slots:
 
 private slots:
     void initializeProjectManager();
-    void enableBackgroundUpdate(const bool &enabled);
     void updateUserProjectList();
 };
 
diff --git a/src/backend/constants.h b/src/backend/constants.h
new file mode 100644
index 0000000000000000000000000000000000000000..540341e6de378d4917a39edf4a11913861340e48
--- /dev/null
+++ b/src/backend/constants.h
@@ -0,0 +1,54 @@
+/****************************************************************************
+**
+** Copyright (C) 2024 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the Qt Design Viewer of the Qt Toolkit.
+**
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 3 as published by the Free Software
+** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-3.0.html.
+**
+****************************************************************************/
+
+#pragma once
+
+#include <QStandardPaths>
+#include <QString>
+
+namespace Constants {
+
+// Settings keys
+namespace Settings {
+namespace ProjectManager {
+const static QString AutoScale = "project/autoScale";
+} // namespace ProjectManager
+
+namespace Backend {
+const static QString UserHash = "user/hash";
+} // namespace Backend
+} // namespace Settings
+
+// Path Constants
+namespace Paths {
+const static QString WritableLocation = QStandardPaths::writableLocation(
+    QStandardPaths::AppDataLocation);
+const static QString ProjectCachePath = WritableLocation + "/projectCache";
+const static QString DemoProjectsPath = WritableLocation + "/demoProjects";
+const static QString ConfigPath = QStandardPaths::writableLocation(QStandardPaths::ConfigLocation);
+const static QString ConfigPathDsManager = ConfigPath + "/dsmanager.json";
+} // namespace Paths
+
+} // namespace Constants
diff --git a/src/backend/dsconnector.cpp b/src/backend/dsconnector.cpp
deleted file mode 100644
index b9f46eb576533ac5ae7d829c490201b24cf972dd..0000000000000000000000000000000000000000
--- a/src/backend/dsconnector.cpp
+++ /dev/null
@@ -1,128 +0,0 @@
-/****************************************************************************
-**
-** Copyright (C) 2023 The Qt Company Ltd.
-** Contact: https://www.qt.io/licensing/
-**
-** This file is part of the Qt Design Viewer of the Qt Toolkit.
-**
-** Commercial License Usage
-** Licensees holding valid commercial Qt licenses may use this file in
-** accordance with the commercial license agreement provided with the
-** Software or, alternatively, in accordance with the terms contained in
-** a written agreement between you and The Qt Company. For licensing terms
-** and conditions see https://www.qt.io/terms-conditions. For further
-** information use the contact form at https://www.qt.io/contact-us.
-**
-** GNU General Public License Usage
-** Alternatively, this file may be used under the terms of the GNU
-** General Public License version 3 as published by the Free Software
-** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
-** included in the packaging of this file. Please review the following
-** information to ensure the GNU General Public License requirements will
-** be met: https://www.gnu.org/licenses/gpl-3.0.html.
-**
-****************************************************************************/
-
-#include "dsconnector.h"
-
-#include <QNetworkInterface>
-
-DesignStudioConnector::DesignStudioConnector(const quint16 port, QObject *parent)
-    : QObject(parent)
-    , m_tcpPort(port)
-{
-    initTcpServer();
-}
-
-void DesignStudioConnector::receiveProject()
-{
-    QByteArray data = m_tcpSocket->readAll();
-
-    if (data.startsWith("qres")) {
-        qDebug() << "TCP:: Received project start delimeter";
-        m_projectData.clear();
-        m_projectData.append(data);
-        m_receivingData = true;
-        emit projectIncoming();
-    } else if (m_receivingData) {
-        if (data.contains("::qmlrc-end::")) {
-            qDebug() << "TCP:: Received project end delimeter";
-            m_projectData.append(data.mid(0, data.size() - 13));
-            emit projectReceived(m_projectData);
-            m_receivingData = false;
-        } else {
-            qDebug() << "TCP:: Received data sequence";
-            m_projectData.append(data);
-        }
-    } else {
-        qDebug() << "TCP:: Received unknown data:" << data;
-    }
-}
-
-void DesignStudioConnector::initTcpServer()
-{
-    const bool retVal = m_tcpServer.listen(QHostAddress::Any, m_tcpPort);
-
-    updateIpv4Addr();
-
-    if (!retVal) {
-        qDebug() << "Failed to listen on port " << m_tcpPort;
-        emit networkStatusUpdated("Failed to bind on port " + QString::number(m_tcpPort));
-        return;
-    }
-
-    qDebug() << "Listening on port " << m_tcpPort;
-    connect(&m_tcpServer, &QTcpServer::newConnection, this, &DesignStudioConnector::clientConnected);
-
-    m_ipUpdateTimer.setInterval(5000);
-    connect(&m_ipUpdateTimer, &QTimer::timeout, this, &DesignStudioConnector::updateIpv4Addr);
-    m_ipUpdateTimer.start();
-}
-
-void DesignStudioConnector::clientConnected()
-{
-    qDebug() << "New connection from Design Studio";
-    emit networkStatusUpdated("Qt Design Studio is connected.\n Waiting for project...");
-    m_tcpSocket.reset(m_tcpServer.nextPendingConnection());
-    m_projectData.clear();
-    m_ipUpdateTimer.stop();
-
-    connect(m_tcpSocket.data(), &QTcpSocket::disconnected, this, [&]() {
-        qDebug() << "Disconnected from Design Studio";
-        emit networkStatusUpdated("\nLocal IP: " + m_ipv4Addr
-                                  + "\nWaiting for Qt Design Studio to connect...");
-    });
-
-    connect(m_tcpSocket.data(),
-            &QTcpSocket::readyRead,
-            this,
-            &DesignStudioConnector::receiveProject);
-}
-
-void DesignStudioConnector::updateIpv4Addr()
-{
-    const QList<QHostAddress> list = QNetworkInterface::allAddresses();
-    for (const QHostAddress &address : list) {
-        if (address.protocol() == QAbstractSocket::IPv4Protocol
-            && address != QHostAddress::LocalHost) {
-            if (m_ipv4Addr != address.toString()) {
-                qDebug() << "Local IP: " << address.toString();
-            }
-            m_ipv4Addr = address.toString();
-            break;
-        }
-    }
-    emit networkStatusUpdated("Local IP: " + m_ipv4Addr
-                              + "\nWaiting for Design Studio to connect...");
-}
-
-void DesignStudioConnector::sendProjectReceived()
-{
-    if (!m_tcpSocket) {
-        qDebug() << "TCP:: Socket is not connected";
-        return;
-    }
-
-    m_tcpSocket->write("::qmlrc-received::");
-    m_tcpSocket->waitForBytesWritten(3000);
-}
diff --git a/src/backend/dsconnector/ds.cpp b/src/backend/dsconnector/ds.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..a3ca3e950cdf39cb01e0e777faab308edb4389eb
--- /dev/null
+++ b/src/backend/dsconnector/ds.cpp
@@ -0,0 +1,236 @@
+/****************************************************************************
+**
+** Copyright (C) 2024 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the Qt Design Viewer of the Qt Toolkit.
+**
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 3 as published by the Free Software
+** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-3.0.html.
+**
+****************************************************************************/
+
+#include "ds.h"
+
+#include <QDebug>
+#include <QGuiApplication>
+#include <QJsonDocument>
+#include <QJsonObject>
+#include <QRect>
+#include <QScreen>
+#include <QSysInfo>
+#include <QTimer>
+
+#include "tcpdatatypes.h"
+
+DesignStudio::DesignStudio(const QString &ipv4Addr,
+                           const quint16 port,
+                           const QString &designStudioId,
+                           const QString &deviceId,
+                           QObject *parent)
+    : QObject(parent)
+    , m_ipv4Addr(ipv4Addr)
+    , m_port(port)
+    , m_url("ws://" + m_ipv4Addr + ":" + QString::number(m_port))
+    , m_designStudioId(designStudioId)
+    , m_deviceId(deviceId)
+{
+    initPingPong();
+    initSocket();
+}
+
+void DesignStudio::initPingPong()
+{
+    connect(&m_pingTimer, &QTimer::timeout, this, [this]() {
+        m_socket.ping();
+        m_pongTimer.start(5000);
+    });
+
+    connect(&m_socket, &QWebSocket::pong, this, [this](quint64 elapsedTime, const QByteArray &) {
+        qDebug() << "Pong received from Design Studio" << m_designStudioId << "in" << elapsedTime
+                 << "ms";
+        m_pongTimer.stop();
+    });
+
+    connect(&m_pongTimer, &QTimer::timeout, this, [this]() {
+        qDebug() << "Design Studio" << m_designStudioId << "is not responding. Reconnecting.";
+        m_socket.close();
+    });
+}
+
+void DesignStudio::initSocket()
+{
+    connect(&m_socket, &QWebSocket::textMessageReceived, this, &DesignStudio::processTextMessage);
+    connect(&m_socket,
+            &QWebSocket::binaryMessageReceived,
+            this,
+            &DesignStudio::processBinaryMessage);
+    connect(&m_socket, &QWebSocket::disconnected, this, [this]() {
+        QTimer::singleShot(m_reconnectTimeout, this, [this]() { m_socket.open(m_url); });
+    });
+
+    connect(&m_socket, &QWebSocket::stateChanged, this, [this](QAbstractSocket::SocketState state) {
+        if (state == QAbstractSocket::ConnectedState) {
+            qDebug() << "Connected to Design Studio" << m_designStudioId;
+            m_socketWasConnected = true;
+            m_pingTimer.start(10000);
+            m_pongTimer.stop();
+        } else if (state == QAbstractSocket::UnconnectedState && m_socketWasConnected) {
+            qDebug() << "Disconnected from Design Studio" << m_designStudioId;
+            m_socketWasConnected = false;
+            m_pingTimer.stop();
+            m_pongTimer.stop();
+        }
+    });
+
+    m_socket.open(m_url);
+}
+
+QString DesignStudio::ipv4Addr() const
+{
+    return m_ipv4Addr;
+}
+
+quint16 DesignStudio::port() const
+{
+    return m_port;
+}
+
+QUrl DesignStudio::url() const
+{
+    return m_url;
+}
+
+QString DesignStudio::designStudioId() const
+{
+    return m_designStudioId;
+}
+
+QString DesignStudio::deviceId() const
+{
+    return m_deviceId;
+}
+
+bool DesignStudio::isRegistered() const
+{
+    return !m_deviceId.isEmpty();
+}
+
+bool DesignStudio::connected() const
+{
+    return m_socket.state() == QAbstractSocket::ConnectedState;
+}
+
+void DesignStudio::setIpAddress(const QString &ipv4Addr)
+{
+    m_socket.close();
+    m_ipv4Addr = ipv4Addr;
+    m_url = QUrl("ws://" + m_ipv4Addr + ":" + QString::number(m_port));
+    m_socket.open(m_url);
+}
+
+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::sendRegistrationPin(const QString &pin)
+{
+    qDebug() << "Sending registration PIN to Design Studio with pin" << pin;
+    sendData(PackageToDesignStudio::registrationPin, pin);
+}
+
+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();
+
+    if (dataType == PackageFromDesignStudio::designStudioReady) {
+        m_designStudioId = jsonObj.value("data").toString();
+        qDebug() << "Design Studio" << m_designStudioId << "is waiting for the info.";
+        sendDeviceInfo();
+    } else if (dataType == PackageFromDesignStudio::pinVerificationRequested) {
+        qDebug() << "Registration PIN requested by Design Studio";
+        emit registrationPinRequested(m_designStudioId);
+    } else if (dataType == PackageFromDesignStudio::pinVerificationFailed) {
+        qDebug() << "Registration PIN verification failed";
+        emit pinVerificationFailed(m_designStudioId);
+    } else if (dataType == PackageFromDesignStudio::deviceIdReceived) {
+        if (!m_deviceId.isEmpty()) {
+            qDebug() << "Registration ID already set. Ignoring new registration ID";
+            return;
+        }
+
+        m_deviceId = jsonObj.value("data").toString();
+        qDebug() << "Registration successful. Received ID:" << m_deviceId;
+        emit registrationSucceeded(m_designStudioId);
+        emit dsOnline(m_designStudioId, m_ipv4Addr);
+    } else if (dataType == PackageFromDesignStudio::deviceIdDeclined) {
+        qDebug() << "Design Studio unregistered";
+        m_deviceId.clear();
+        emit unregistered(m_designStudioId);
+    } else if (dataType == PackageFromDesignStudio::deviceIdAccepted) {
+        qDebug() << "Design Studio accepted the device ID. Connection alive.";
+        emit dsOnline(m_designStudioId, m_ipv4Addr);
+    } else if (dataType == PackageFromDesignStudio::projectData) {
+        qDebug() << "Project is expected";
+        emit projectIncoming();
+    } else {
+        qDebug() << "Unkown JSON message";
+    }
+}
+
+void DesignStudio::processBinaryMessage(const QByteArray &data)
+{
+    // this slot is only triggered when we're receiving a qmlrc project
+    qDebug() << "Binary message received (most probably a project)";
+    emit projectReceived(data);
+}
diff --git a/src/backend/dsconnector/ds.h b/src/backend/dsconnector/ds.h
new file mode 100644
index 0000000000000000000000000000000000000000..6c39361e5b41b2f6b2921df38f4b6777d68b883b
--- /dev/null
+++ b/src/backend/dsconnector/ds.h
@@ -0,0 +1,99 @@
+/****************************************************************************
+**
+** Copyright (C) 2024 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the Qt Design Viewer of the Qt Toolkit.
+**
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 3 as published by the Free Software
+** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-3.0.html.
+**
+****************************************************************************/
+
+#pragma once
+
+#include <QObject>
+#include <QTimer>
+#include <QWebSocket>
+#include <qjsonvalue.h>
+#include <qlatin1stringview.h>
+
+class DesignStudio : public QObject
+{
+    Q_OBJECT
+public:
+    DesignStudio(const QString &ipv4Addr,
+                 const quint16 port,
+                 const QString &designStudioId = QString(),
+                 const QString &deviceId = QString(),
+                 QObject *parent = nullptr);
+
+    // Getters
+    QString ipv4Addr() const;
+    QString designStudioId() const;
+    quint16 port() const;
+    QUrl url() const;
+    QString deviceId() const;
+    bool isRegistered() const;
+    bool connected() const;
+
+    // Setters
+    void setIpAddress(const QString &ipv4Addr);
+
+    // Send data
+    void sendRegistrationPin(const QString &pin);
+
+private:
+    // Network
+    QWebSocket m_socket;
+    QString m_ipv4Addr;
+    quint16 m_port;
+    QUrl m_url;
+    bool m_socketWasConnected = false;
+    QTimer m_pingTimer;
+    QTimer m_pongTimer;
+
+    // DS data
+    QString m_designStudioId;
+    QString m_deviceId;
+
+    // Settings
+    constexpr static int m_reconnectTimeout = 5000;
+
+    // DS comm
+    void sendDeviceInfo();
+    void sendData(const QLatin1String &dataType, const QJsonValue &data = QJsonValue());
+
+    // Internal
+    void initPingPong();
+    void initSocket();
+
+private slots:
+    void processTextMessage(const QString &data);
+    void processBinaryMessage(const QByteArray &data);
+
+signals:
+    // DS signals
+    void registrationPinRequested(const QString &id);
+    void pinVerificationFailed(const QString &id);
+    void registrationSucceeded(const QString &id);
+    void unregistered(const QString &id);
+
+    void dsOnline(const QString &id, const QString &ipv4Addr);
+
+    void projectIncoming();
+    void projectReceived(const QByteArray &data);
+};
diff --git a/src/backend/dsconnector/dsdiscovery.cpp b/src/backend/dsconnector/dsdiscovery.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..9c0fcc5e65c5db6b0cdf2c48aef6d0a8c43c01ab
--- /dev/null
+++ b/src/backend/dsconnector/dsdiscovery.cpp
@@ -0,0 +1,86 @@
+/****************************************************************************
+**
+** Copyright (C) 2024 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the Qt Design Viewer of the Qt Toolkit.
+**
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 3 as published by the Free Software
+** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-3.0.html.
+**
+****************************************************************************/
+
+#include "dsdiscovery.h"
+
+#include <QNetworkDatagram>
+#include <QNetworkInterface>
+
+#include <QJsonDocument>
+#include <QJsonObject>
+#include <QJsonParseError>
+#include <qjsondocument.h>
+
+DesignStudioDiscovery::DesignStudioDiscovery(QObject *parent)
+    : QObject(parent)
+{
+    const QList<QNetworkInterface> interfaces = QNetworkInterface::allInterfaces();
+    for (const QNetworkInterface &interface : interfaces) {
+        if (interface.flags().testFlag(QNetworkInterface::IsUp)
+            && interface.flags().testFlag(QNetworkInterface::IsRunning)
+            && !interface.flags().testFlag(QNetworkInterface::IsLoopBack)) {
+            for (const QNetworkAddressEntry &entry : interface.addressEntries()) {
+                if (entry.ip().protocol() == QAbstractSocket::IPv4Protocol) {
+                    const bool retVal = m_udpSocket.bind(QHostAddress::AnyIPv4,
+                                                         53452,
+                                                         QUdpSocket::ShareAddress);
+                    if (!retVal) {
+                        qWarning() << "UDP:: Failed to bind to port 53452";
+                    }
+                }
+            }
+        }
+    }
+
+    qDebug() << "UDP:: Listening on" << m_udpSocket.localAddress() << "port"
+             << m_udpSocket.localPort();
+    connect(&m_udpSocket, &QUdpSocket::readyRead, this, &DesignStudioDiscovery::onReadyRead);
+}
+
+void DesignStudioDiscovery::onReadyRead()
+{
+    while (m_udpSocket.hasPendingDatagrams()) {
+        const QNetworkDatagram datagram = m_udpSocket.receiveDatagram();
+        const QString message = datagram.data();
+        const QString ipv4Addr = datagram.senderAddress().toString();
+
+        QJsonParseError jsonError;
+        const QJsonDocument jsonDoc = QJsonDocument::fromJson(message.toUtf8(), &jsonError);
+
+        if (jsonError.error != QJsonParseError::NoError) {
+            qDebug() << "UDP:: Failed to parse JSON message:" << jsonError.errorString();
+            continue;
+        }
+
+        const QJsonObject jsonObj = jsonDoc.object();
+
+        if (!jsonObj.contains("name") && !jsonObj.contains("id")) {
+            qDebug() << "UDP:: Invalid JSON message:" << jsonObj;
+            continue;
+        }
+
+        emit designStudioFound(ipv4Addr, jsonObj.value("id").toString());
+    }
+}
diff --git a/src/backend/dsconnector.h b/src/backend/dsconnector/dsdiscovery.h
similarity index 58%
rename from src/backend/dsconnector.h
rename to src/backend/dsconnector/dsdiscovery.h
index 1068224b5932a7c93d6f0c949fb25c5f651a9141..369e924ba4f437c4dec6a5cea5161fe1fcb26c87 100644
--- a/src/backend/dsconnector.h
+++ b/src/backend/dsconnector/dsdiscovery.h
@@ -1,6 +1,6 @@
 /****************************************************************************
 **
-** Copyright (C) 2023 The Qt Company Ltd.
+** Copyright (C) 2024 The Qt Company Ltd.
 ** Contact: https://www.qt.io/licensing/
 **
 ** This file is part of the Qt Design Viewer of the Qt Toolkit.
@@ -23,48 +23,23 @@
 **
 ****************************************************************************/
 
-#ifndef DSCONNECTOR_H
-#define DSCONNECTOR_H
+#pragma once
 
 #include <QObject>
-#include <QTcpServer>
-#include <QTcpSocket>
-#include <QTimer>
 #include <QUdpSocket>
 
-class DesignStudioConnector : public QObject
+class DesignStudioDiscovery : public QObject
 {
     Q_OBJECT
 public:
-    explicit DesignStudioConnector(const quint16 port = 40000, QObject *parent = nullptr);
+    explicit DesignStudioDiscovery(QObject *parent = nullptr);
 
-public slots:
-    void sendProjectReceived();
-
-private:
-    // Tcp connection members
-    QTcpServer m_tcpServer;
-    QScopedPointer<QTcpSocket> m_tcpSocket;
-    const quint32 m_tcpPort;
-
-    // Udp connection members
-    QTimer m_ipUpdateTimer;
-    QString m_ipv4Addr;
-
-    // Other members
-    QByteArray m_projectData;
-    bool m_receivingData;
-
-    // Member functions
-    void initTcpServer();
-    void receiveProject();
-    void updateIpv4Addr();
-    void clientConnected();
+private slots:
+    void onReadyRead();
 
 signals:
-    void networkStatusUpdated(QString);
-    void projectReceived(QByteArray);
-    void projectIncoming();
-};
+    void designStudioFound(const QString &ipv4Addr, const QString &id);
 
-#endif // DSCONNECTOR_H
+private:
+    QUdpSocket m_udpSocket;
+};
diff --git a/src/backend/dsconnector/dsmanager.cpp b/src/backend/dsconnector/dsmanager.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..ebbfe214059dc2c14e72c07678fb971a51698494
--- /dev/null
+++ b/src/backend/dsconnector/dsmanager.cpp
@@ -0,0 +1,187 @@
+/****************************************************************************
+**
+** Copyright (C) 2023 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the Qt Design Viewer of the Qt Toolkit.
+**
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 3 as published by the Free Software
+** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-3.0.html.
+**
+****************************************************************************/
+
+#include "dsmanager.h"
+#include "../constants.h"
+#include "backend/dsconnector/ds.h"
+#include <memory>
+
+#include <QFile>
+#include <QJsonArray>
+#include <QJsonDocument>
+#include <QJsonObject>
+#include <QLoggingCategory>
+#include <QThread>
+#include <qdebug.h>
+#include <qjsonobject.h>
+#include <qobject.h>
+
+DesignStudioManager::DesignStudioManager(QObject *parent)
+    : QObject(parent)
+{
+    initializeRegisteredDesignStudios();
+
+    m_discovery = std::make_unique<DesignStudioDiscovery>();
+    connect(m_discovery.get(),
+            &DesignStudioDiscovery::designStudioFound,
+            this,
+            &DesignStudioManager::designStudioFound);
+
+    qDebug() << "Design Studio Manager initialized";
+    qDebug() << "Thread id dsmanger:" << QThread::currentThreadId();
+}
+
+void DesignStudioManager::initializeRegisteredDesignStudios()
+{
+    QFile file(Constants::Paths::ConfigPathDsManager);
+
+    if (!file.exists()) {
+        qDebug() << "Settings file not found.";
+        return;
+    }
+
+    file.open(QIODevice::ReadOnly);
+    QJsonDocument doc = QJsonDocument::fromJson(file.readAll());
+    file.close();
+
+    if (!doc.isObject()) {
+        qDebug() << "Settings file is not a JSON object.";
+        return;
+    }
+
+    QJsonObject obj = doc.object();
+    QJsonArray dsArray = obj["designStudios"].toArray();
+    for (const auto &ds : dsArray) {
+        QJsonObject dsObj = ds.toObject();
+        initDesignStudio(dsObj["ipv4Addr"].toString(),
+                         dsObj["designStudioId"].toString(),
+                         dsObj["deviceId"].toString());
+    }
+}
+
+void DesignStudioManager::initDesignStudio(const QString &ipv4Addr,
+                                           const QString &designStudioId,
+                                           const QString &deviceId)
+{
+    qDebug() << "Initializing Design Studio" << designStudioId << "with IPv4 address" << ipv4Addr;
+    auto ds = std::make_unique<DesignStudio>(ipv4Addr, 40000, designStudioId, deviceId);
+
+    connect(ds.get(),
+            &DesignStudio::registrationSucceeded,
+            this,
+            &DesignStudioManager::designStudioRegistered);
+
+    connect(ds.get(),
+            &DesignStudio::unregistered,
+            this,
+            &DesignStudioManager::designStudioUnregistered);
+
+    connect(ds.get(),
+            &DesignStudio::registrationPinRequested,
+            this,
+            &DesignStudioManager::registrationPinRequested);
+
+    connect(ds.get(), &DesignStudio::projectReceived, this, &DesignStudioManager::projectReceived);
+
+    m_designStudios.push_back(std::move(ds));
+}
+
+void DesignStudioManager::designStudioFound(const QString &ipv4Addr, const QString &id)
+{
+    qDebug() << "Design Studio found with IPv4 address" << ipv4Addr << "and ID" << id;
+    // check if the Design Studio is already in the list
+
+    for (const auto &ds : m_designStudios) {
+        if (ds->designStudioId() == id) {
+            if (ds->ipv4Addr() != ipv4Addr) {
+                qDebug() << "Design Studio" << id << "changed IP address from" << ds->ipv4Addr()
+                         << "to" << ipv4Addr;
+                ds->setIpAddress(ipv4Addr);
+            } else {
+                qDebug() << "Design Studio" << id << "already in the list";
+            }
+            return;
+        }
+    }
+
+    initDesignStudio(ipv4Addr, id);
+}
+
+void DesignStudioManager::updateConfigFile()
+{
+    QFile file(Constants::Paths::ConfigPathDsManager);
+    if (!file.open(QIODevice::ReadWrite | QIODevice::Truncate)) {
+        qDebug() << "Failed to open settings file for writing";
+        return;
+    }
+
+    QJsonObject rootObj;
+    QJsonArray dsArray;
+
+    for (const auto &d : m_designStudios) {
+        if (!d->isRegistered()) {
+            continue;
+        }
+
+        QJsonObject dsObj;
+        dsObj["ipv4Addr"] = d->ipv4Addr();
+        dsObj["designStudioId"] = d->designStudioId();
+        dsObj["deviceId"] = d->deviceId();
+        dsArray.append(dsObj);
+    }
+
+    rootObj["designStudios"] = dsArray;
+    file.write(QJsonDocument(rootObj).toJson());
+}
+
+void DesignStudioManager::designStudioRegistered(const QString &designStudioId)
+{
+    qDebug() << "Design Studio" << designStudioId << "registered. Updating settings.";
+    updateConfigFile();
+}
+
+void DesignStudioManager::designStudioUnregistered(const QString &designStudioId)
+{
+    qDebug() << "Design Studio" << designStudioId << "unregistered. Removing from settings.";
+    updateConfigFile();
+}
+
+void DesignStudioManager::enterPin(const QString &id, const QString &pin)
+{
+    DesignStudio *ds = nullptr;
+
+    for (const auto &d : m_designStudios) {
+        if (d->designStudioId() == id) {
+            ds = d.get();
+            break;
+        }
+    }
+
+    if (!ds) {
+        return;
+    }
+
+    ds->sendRegistrationPin(pin);
+}
diff --git a/src/backend/dsconnector/dsmanager.h b/src/backend/dsconnector/dsmanager.h
new file mode 100644
index 0000000000000000000000000000000000000000..824996d3b29d965e1428ceed7a0cb153e3729a0d
--- /dev/null
+++ b/src/backend/dsconnector/dsmanager.h
@@ -0,0 +1,69 @@
+/****************************************************************************
+**
+** Copyright (C) 2023 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the Qt Design Viewer of the Qt Toolkit.
+**
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 3 as published by the Free Software
+** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-3.0.html.
+**
+****************************************************************************/
+
+#ifndef DSCONNECTOR_H
+#define DSCONNECTOR_H
+
+#include <QObject>
+#include <qscopedpointer.h>
+
+#include "ds.h"
+#include "dsdiscovery.h"
+#include <memory>
+
+class DesignStudioManager : public QObject
+{
+    Q_OBJECT
+public:
+    explicit DesignStudioManager(QObject *parent = nullptr);
+
+public slots:
+    void enterPin(const QString &id, const QString &pin);
+    void designStudioFound(const QString &ipv4Addr, const QString &id);
+
+private slots:
+    void designStudioRegistered(const QString &id);
+    void designStudioUnregistered(const QString &id);
+
+private:
+    // Discovery object
+    std::unique_ptr<DesignStudioDiscovery> m_discovery;
+
+    // Discovered Design Studio instances
+    std::vector<std::unique_ptr<DesignStudio>> m_designStudios;
+
+    void initializeRegisteredDesignStudios();
+    void initDesignStudio(const QString &ipv4Addr,
+                          const QString &designStudioId,
+                          const QString &deviceId = QString());
+
+    void updateConfigFile();
+
+signals:
+    void registrationPinRequested(const QString &id);
+    void projectReceived(const QByteArray &project);
+};
+
+#endif // DSCONNECTOR_H
diff --git a/src/backend/dsconnector/tcpdatatypes.h b/src/backend/dsconnector/tcpdatatypes.h
new file mode 100644
index 0000000000000000000000000000000000000000..5001db5a7c651c54eaa6170eaf5e29d4776d5ae7
--- /dev/null
+++ b/src/backend/dsconnector/tcpdatatypes.h
@@ -0,0 +1,45 @@
+/****************************************************************************
+**
+** Copyright (C) 2024 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the Qt Design Tooling
+**
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 3 as published by the Free Software
+** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-3.0.html.
+**
+****************************************************************************/
+
+#pragma once
+
+#include <QLatin1String>
+
+namespace PackageFromDesignStudio {
+using namespace Qt::Literals;
+constexpr auto designStudioReady = "designStudioReady"_L1;
+constexpr auto pinVerificationRequested = "pinRequested"_L1;
+constexpr auto pinVerificationFailed = "pinVerificationFailed"_L1;
+constexpr auto deviceIdReceived = "deviceIdReceived"_L1;
+constexpr auto deviceIdDeclined = "deviceIdDeclined"_L1;
+constexpr auto deviceIdAccepted = "deviceIdAccepted"_L1;
+constexpr auto projectData = "projectData"_L1;
+}; // namespace PackageFromDesignStudio
+
+namespace PackageToDesignStudio {
+using namespace Qt::Literals;
+constexpr auto deviceInfo = "deviceInfo"_L1;
+constexpr auto registrationPin = "registrationPinResponse"_L1;
+}; // namespace PackageToDesignStudio
diff --git a/src/backend/projectmanager.cpp b/src/backend/projectmanager.cpp
index ed795814da3dada736e4bd375b0f88cfadaa240d..dbae903a6e3f54dea836e0526fe792119edef1d1 100644
--- a/src/backend/projectmanager.cpp
+++ b/src/backend/projectmanager.cpp
@@ -37,33 +37,36 @@
 #include <QRandomGenerator>
 #include <QRegularExpression>
 #include <QResource>
-#include <QStandardPaths>
+#include <QSettings>
 #include <QTemporaryDir>
 #include <QTemporaryFile>
+
 #if QT_VERSION >= QT_VERSION_CHECK(6, 6, 0)
 #include <QtCore/private/qzipreader_p.h>
 #else
 #include <QtGui/private/qzipreader_p.h>
 #endif
 
-ProjectManager::ProjectManager(const bool &autoScaleProject, QObject *parent)
+#include "constants.h"
+
+ProjectManager::ProjectManager(QObject *parent)
     : QObject(parent)
-    , m_projectCachePath(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation))
-    , m_demoProjectCachePath(m_projectCachePath + "/demoProjects")
-    , m_autoScaleProject(autoScaleProject)
+    , m_autoScaleProject(
+          QSettings().value(Constants::Settings::ProjectManager::AutoScale, true).toBool())
 {
     qDebug() << "ProjectManager created.";
-    qDebug() << "Project cache path: " << m_projectCachePath;
-    qDebug() << "Demo project cache path: " << m_demoProjectCachePath;
+    qDebug() << "Project cache path: " << Constants::Paths::ProjectCachePath;
+    qDebug() << "Demo project cache path: " << Constants::Paths::DemoProjectsPath;
+    qDebug() << "Auto scale project: " << m_autoScaleProject;
 
-    if (!QDir(m_projectCachePath).exists()) {
-        qDebug() << "Creating project cache path: " << m_projectCachePath;
-        QDir().mkpath(m_projectCachePath);
+    if (!QDir(Constants::Paths::ProjectCachePath).exists()) {
+        qDebug() << "Creating project cache path: " << Constants::Paths::ProjectCachePath;
+        QDir().mkpath(Constants::Paths::ProjectCachePath);
     }
 
-    if (!QDir(m_demoProjectCachePath).exists()) {
-        qDebug() << "Creating demo project cache path: " << m_demoProjectCachePath;
-        QDir().mkpath(m_demoProjectCachePath);
+    if (!QDir(Constants::Paths::DemoProjectsPath).exists()) {
+        qDebug() << "Creating demo project cache path: " << Constants::Paths::DemoProjectsPath;
+        QDir().mkpath(Constants::Paths::DemoProjectsPath);
     }
 }
 
@@ -324,7 +327,7 @@ bool ProjectManager::cacheProject(const QByteArray &projectData, const QJsonObje
 
     qDebug() << "Caching project " << projectId << " with last modified "
              << projectInfo.value("uploadTime");
-    const QString cachePath = m_projectCachePath + "/" + projectId;
+    const QString cachePath = Constants::Paths::ProjectCachePath + "/" + projectId;
 
     // remove old cache
     if (QDir(cachePath).exists()) {
@@ -370,7 +373,7 @@ bool ProjectManager::isProjectCached(const QJsonObject &projectInfo)
     const QString lastModified = projectInfo.value("uploadTime").toString();
 
     qDebug() << "Checking if project " << projectId << " is cached";
-    const QString cachePath = m_projectCachePath + "/" + projectId;
+    const QString cachePath = Constants::Paths::ProjectCachePath + "/" + projectId;
     if (!QDir(cachePath).exists()) {
         qDebug() << "Project " << projectId << " is not cached";
         return false;
@@ -404,7 +407,7 @@ void ProjectManager::clearCachedProject(const QJsonObject &projectInfo)
     const QString projectId = projectInfo.value("id").toString();
     qDebug() << "Clearing cache for project " << projectId;
 
-    const QString cachePath = m_projectCachePath + "/" + projectId;
+    const QString cachePath = Constants::Paths::ProjectCachePath + "/" + projectId;
     if (!QDir(cachePath).exists()) {
         qDebug() << "Project " << projectId << " is not cached";
     }
@@ -418,7 +421,7 @@ bool ProjectManager::runCachedProject(const QJsonObject &projectInfo)
     const QString projectId = projectInfo.value("id").toString();
     qDebug() << "Running cached project " << projectId;
 
-    const QString projectPath = m_projectCachePath + "/" + projectId;
+    const QString projectPath = Constants::Paths::ProjectCachePath + "/" + projectId;
     return runProject(projectPath);
 }
 
@@ -427,7 +430,7 @@ bool ProjectManager::cacheDemoProject(const QByteArray &projectData, const QJson
     // sample project info
     //[{"lastUpdate":1701947766739.9812,"name":"ClusterTutorial.qmlrc"}]
     const QString projectName = projectInfo.value("name").toString().remove(".qmlrc");
-    const QString demoProjectPath = m_demoProjectCachePath + "/" + projectName;
+    const QString demoProjectPath = Constants::Paths::DemoProjectsPath + "/" + projectName;
     qDebug() << "Caching demo project " << projectName << " to " << demoProjectPath;
 
     // remove old cache
@@ -476,8 +479,8 @@ bool ProjectManager::cacheDemoProject(const QByteArray &projectData, const QJson
 
 void ProjectManager::clearDemoCaches()
 {
-    qDebug() << "Clearing demo caches";
-    QDir(m_demoProjectCachePath).removeRecursively();
+    qDebug() << "Clearing demo caches:" << Constants::Paths::DemoProjectsPath;
+    QDir(Constants::Paths::DemoProjectsPath).removeRecursively();
 }
 
 bool ProjectManager::isDemoProjectCached(const QJsonObject &projectInfo)
@@ -485,7 +488,7 @@ bool ProjectManager::isDemoProjectCached(const QJsonObject &projectInfo)
     // sample project info
     //[{"lastUpdate":1701947766739.9812,"name":"ClusterTutorial.qmlrc"}]
     const QString projectName = projectInfo.value("name").toString().remove(".qmlrc");
-    const QString demoProjectPath = m_demoProjectCachePath + "/" + projectName;
+    const QString demoProjectPath = Constants::Paths::DemoProjectsPath + "/" + projectName;
     qDebug() << "Checking if demo project " << projectName << " is cached";
 
     if (!QDir(demoProjectPath).exists()) {
@@ -520,7 +523,7 @@ bool ProjectManager::isDemoProjectCached(const QJsonObject &projectInfo)
 
 bool ProjectManager::runDemoProject(const QString &projectName)
 {
-    const QString demoProjectPath = m_demoProjectCachePath + "/" + projectName;
+    const QString demoProjectPath = Constants::Paths::DemoProjectsPath + "/" + projectName;
     qDebug() << "Running demo project " << projectName << " from " << demoProjectPath;
     return runProject(demoProjectPath);
 }
diff --git a/src/backend/projectmanager.h b/src/backend/projectmanager.h
index 8c79fac1cf576f0f8a2e4f857ce193596257ecf2..354ba59c2efedeb1fd53f8a99b68bf1d0f679d21 100644
--- a/src/backend/projectmanager.h
+++ b/src/backend/projectmanager.h
@@ -37,7 +37,7 @@ class ProjectManager : public QObject
 {
     Q_OBJECT
 public:
-    explicit ProjectManager(const bool &autoScaleProject = true, QObject *parent = nullptr);
+    explicit ProjectManager(QObject *parent = nullptr);
     ~ProjectManager();
 
     QString unpackProject(const QByteArray &project, bool extractZip = false);
@@ -63,8 +63,6 @@ private:
     // Member variables
     QByteArray m_projectData;
     QString m_projectPath;
-    const QString m_projectCachePath;
-    const QString m_demoProjectCachePath;
     const bool m_autoScaleProject;
 
     // Qml related members
diff --git a/src/ui/DSManagement.qml b/src/ui/DSManagement.qml
new file mode 100644
index 0000000000000000000000000000000000000000..a73578e28797d01cde6c9469d8f3163554ab77de
--- /dev/null
+++ b/src/ui/DSManagement.qml
@@ -0,0 +1,35 @@
+import QtQuick
+import QtQuick.Controls 6.4
+import QtQuick.Layouts
+
+
+Item {
+    id: header
+
+    ColumnLayout {
+        spacing: 10
+        anchors.fill: parent
+        Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
+
+        Text {
+            Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
+            Layout.preferredWidth: parent.width
+            horizontalAlignment: "AlignHCenter"
+            wrapMode: Text.WordWrap
+            textFormat: Text.StyledText
+            text: "<p>
+                       Use Design Studio (File -> Device Management) to register you phone.
+                   <br>
+
+                   <p>
+                       If your phone is not discovered by Design Studio, or if it has been registered before but doesn't show up as online, you can scan the QR code shown on Design Studio.
+                   "
+        }
+
+        Button {
+            Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
+            text: "Scan QR Code"
+            onClicked: backend.scanQrCode()
+        }
+    }
+}
diff --git a/src/ui/HomePage.qml b/src/ui/HomePage.qml
index 428221b322da3ad65d3cb4eb1629c952731ff86c..9e15965b92beb21522133d050ff71fe5b8dee276 100644
--- a/src/ui/HomePage.qml
+++ b/src/ui/HomePage.qml
@@ -73,41 +73,6 @@ Item {
                 Layout.fillWidth: true
                 Layout.fillHeight: true
             }
-
-            Text {
-                id: dsInstructions
-                text: "Create a connection to  Qt Design Studio:
-    Goto File > Deploy Project to Android, and type in the following IP address:"
-                font.pixelSize: 12
-                horizontalAlignment: Text.AlignHCenter
-                verticalAlignment: Text.AlignVCenter
-                wrapMode: Text.WordWrap
-                Layout.fillWidth: true
-            }
-
-            Text {
-                id: ipAdress
-                text: "Waiting for backend to be initialized..."
-                font.pixelSize: 16
-                horizontalAlignment: Text.AlignHCenter
-                verticalAlignment: Text.AlignVCenter
-                wrapMode: Text.WordWrap
-                Layout.fillWidth: true
-                Connections {
-                    target: backend
-                    function onNetworkUpdated(newStatus){
-                        ipAdress.text = newStatus
-                    }
-                }
-            }
-
-            Item {
-                id: item5
-                Layout.preferredWidth: 10
-                Layout.preferredHeight: 10
-                Layout.fillWidth: true
-                Layout.fillHeight: true
-            }
         }
 
         Item {
@@ -265,11 +230,6 @@ Item {
                 target: sep2
                 visible: false
             }
-
-            PropertyChanges {
-                target: item5
-                visible: true
-            }
         },
         State {
             name: "State2"
@@ -295,11 +255,6 @@ Item {
                 visible: true
             }
 
-            PropertyChanges {
-                target: item5
-                visible: false
-            }
-
             PropertyChanges {
                 target: gridLayout
                 columns: 2
diff --git a/src/ui/Network.qml b/src/ui/Network.qml
deleted file mode 100644
index 1fc374c124c4575d45300e229e060d896069e676..0000000000000000000000000000000000000000
--- a/src/ui/Network.qml
+++ /dev/null
@@ -1,55 +0,0 @@
-import QtQuick
-import QtQuick.Controls 6.4
-import QtQuick.Layouts
-
-
-Item {
-    id: header
-
-    ColumnLayout {
-        anchors.fill: parent
-        Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
-
-        Item {
-            id: item2
-            width: 200
-            height: 200
-            Layout.preferredHeight: 10
-            Layout.fillWidth: true
-        }
-
-        Label {
-            id: infoHeader
-            Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
-            horizontalAlignment: "AlignHCenter"
-            text: qsTr("Connect to Qt Design Studio:
-                        \n1. Copy the Local IP.
-                        \n2. Open the project in Qt Design Studio (4.4 or later).
-                        \n3. Select File -> Deploy Project to Android.
-                        \n4. Paste the Local IP to Device IP.
-                        \n5. Select Connect.
-                        \n6. Select Send Project.")
-        }
-
-        Label {
-            id: statusLabel
-            Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
-            horizontalAlignment: "AlignHCenter"
-            text: "Waiting for backend to be initialized..."
-            Connections {
-                target: backend
-                function onNetworkUpdated(newStatus){
-                    statusLabel.text = newStatus
-                }
-            }
-        }
-
-        Item {
-            id: item3
-            width: 200
-            height: 200
-            Layout.preferredHeight: 10
-            Layout.fillWidth: true
-        }
-    }
-}
diff --git a/src/ui/SettingsPage.qml b/src/ui/SettingsPage.qml
index 894afb5ddd2b19d1859f62890d46d000d60e6b35..79cc624e858702a5767c9acbd319aafec2de7e63 100644
--- a/src/ui/SettingsPage.qml
+++ b/src/ui/SettingsPage.qml
@@ -20,27 +20,12 @@ Item {
             id: column2
             Layout.fillWidth: true
 
-            CheckBox {
-                id: checkBox
-                text: qsTr("Update user projects in the backgroud")
-                font.pointSize: 15
-                onCheckStateChanged: backend.setUpdateInBackground(checkState)
-                checkState: backend.updateInBackground() ? Qt.Checked : Qt.Unchecked
-            }
-
-            Text{
-                leftPadding: 45
-                text: qsTr("Checks new projects every 10 seconds")
-                font.pointSize: 12
-            }
-
            CheckBox {
                 id: checkBox2
                 text: qsTr("Auto scale the project")
                 font.pointSize: 15
                 onCheckStateChanged: backend.setAutoScaleProject(checkState)
                 checkState: backend.autoScaleProject() ? Qt.Checked : Qt.Unchecked
-
            }
 
            Text{
diff --git a/src/ui/content/images/ds_icon.png b/src/ui/content/images/ds_icon.png
new file mode 100644
index 0000000000000000000000000000000000000000..d565981cdae7b075f7565da7325e893950f75799
Binary files /dev/null and b/src/ui/content/images/ds_icon.png differ
diff --git a/src/ui/main.qml b/src/ui/main.qml
index bd782663f6485d7ae737c616f32cf57d0750d7e5..0093041475f8d50a6a9d9a9caf85addfd71aec25 100644
--- a/src/ui/main.qml
+++ b/src/ui/main.qml
@@ -103,7 +103,6 @@ Rectangle {
             Layout.fillWidth: true
         }
 
-
         ExamplesPage {
             id: examplesPage
             Layout.fillWidth: true
@@ -114,8 +113,8 @@ Rectangle {
             Layout.fillWidth: true
         }
 
-        Network {
-            id: networkPage
+        DSManagement {
+            id: dsManagementPage
             Layout.fillWidth: true
         }
 
@@ -133,12 +132,11 @@ Rectangle {
     states: [
         State {
             name: "vertical"
-            when: root.height >= 400
-
+            when: root.height >= 600
         },
         State {
             name: "horizontal"
-            when: root.height < 400
+            when: root.height < 600
 
             PropertyChanges {
                 target: qdsicon1
@@ -164,15 +162,14 @@ Rectangle {
         id: drawer
         width: 150
         height: root.height
+
         ScrollView {
             id: scrollview
             anchors.fill: parent
+            padding: 0
 
             ColumnLayout {
                 id: column
-                anchors.fill: drawer
-                anchors.rightMargin: 10
-                anchors.leftMargin: 10
                 width: Math.max(implicitWidth, drawer.availableWidth)
                 height: Math.max(implicitHeight, drawer.availableHeight)
 
@@ -214,8 +211,8 @@ Rectangle {
                 }
 
                 TabButton {
-                    id: network
-                    text: qsTr("Network")
+                    id: dsManagement
+                    text: qsTr("Design Studio")
                     Layout.fillWidth: true
                     checkable: true
                     autoExclusive: true
@@ -265,4 +262,110 @@ Rectangle {
             }
         }
     }
+
+    Popup {
+        property bool popupCloseReceived : false
+        id: popup
+        anchors.centerIn: parent
+        width: 300
+        height: 100
+        modal: true
+        focus: true
+        closePolicy: Popup.CloseOnEscape
+
+        onClosed: {
+            if (!popupCloseReceived) {
+                backend.popupInterrupted();
+            }
+        }
+
+        ColumnLayout {
+            anchors.fill: parent
+            Text {
+                id: popupText
+            }
+            Item {
+                id: name
+            }
+            ProgressBar {
+                id: popupProgressBar
+                Layout.fillWidth: true
+                to: 100
+            }
+        }
+
+        Connections {
+            target: backend
+            function onPopupOpen() {
+                popup.open()
+                popup.popupCloseReceived = false
+            }
+            function onPopupClose() {
+                popup.popupCloseReceived = true
+                popup.close()
+            }
+            function onPopupTextChanged(text) {
+                popupText.text = text
+            }
+            function onPopupProgressIndeterminateChanged(status) {
+                popupProgressBar.indeterminate = status
+            }
+            function onDownloadProgress(progress) {
+                popupProgressBar.value = progress
+            }
+        }
+    }
+
+    Popup {
+        property string deviceId: ""
+        id: pinPopup
+        anchors.centerIn: parent
+        width: 250
+        height: 250
+        visible: false
+        modal: true
+        focus: true
+        contentItem: Rectangle {
+            color: "lightgrey"
+            border.color: "black"
+            border.width: 2
+            radius: 10
+            Text {
+                text: "Enter 4 digit pin\nthat you see on Design Studio"
+                anchors.centerIn: parent
+                horizontalAlignment: Qt.AlignHCenter
+            }
+            TextField {
+                id: pinField
+                anchors.top: parent.top
+                anchors.topMargin: 20
+                anchors.horizontalCenter: parent.horizontalCenter
+                width: 100
+                height: 50
+                font.pixelSize: 20
+                inputMethodHints: Qt.ImhDigitsOnly
+                validator: IntValidator { bottom: 0; top: 9999 }
+            }
+
+            Button {
+                text: "Send PIN"
+                anchors.bottom: parent.bottom
+                anchors.bottomMargin: 20
+                anchors.horizontalCenter: parent.horizontalCenter
+                onClicked: {
+                    backend.enterPin(pinPopup.deviceId, pinField.text);
+                    pinPopup.close();
+                }
+            }
+        }
+
+        Connections {
+            target: backend
+            function onPinRequested(deviceId) {
+                console.log("Pin requested for device: " + deviceId)
+                pinPopup.visible = true
+                pinPopup.deviceId = deviceId
+            }
+        }
+    }
 }
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
index e290b1706a39bf5ddd04c4bec43ffece7df45ecb..d76888cff0b04461b549068d7d7dc1edf31590f3 100644
--- a/tests/CMakeLists.txt
+++ b/tests/CMakeLists.txt
@@ -1,18 +1,23 @@
 set(TARGET_NAME ${PROJECT_NAME}_test)
 
-find_package(Qt6 REQUIRED COMPONENTS Test Core Quick Gui Multimedia)
+find_package(Qt6 REQUIRED COMPONENTS Test Core Quick Gui Multimedia WebSockets)
 
 enable_testing(true)
 qt_add_executable(${TARGET_NAME}
-    tst_qtuiviewer.cpp
-    tst_qtuiviewer.h
+    tst_projectmanager.cpp tst_projectmanager.h
+    tst_serviceconnector.cpp tst_serviceconnector.h
+    tst_designstudio.cpp tst_designstudio.h
+    tst_dsdiscovery.cpp tst_dsdiscovery.h
+    tst_dsmanager.cpp tst_dsmanager.h
+    mock.h
+    main.cpp
 )
 
 add_test(NAME ${TARGET_NAME} COMMAND qtuiviewer_test)
 target_link_libraries(
     ${TARGET_NAME}
     PRIVATE
-    Qt::Test Qt::Core Qt::Quick Qt::Gui Qt::Multimedia
+    Qt::Test Qt::Core Qt::Quick Qt::Gui Qt::Multimedia Qt::WebSockets
     qtuiviewerlib
 )
 
diff --git a/tests/main.cpp b/tests/main.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..e991d917f12093112202e0debb16bd0ab17486c7
--- /dev/null
+++ b/tests/main.cpp
@@ -0,0 +1,103 @@
+/****************************************************************************
+**
+** Copyright (C) 2024 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the Qt Design Viewer of the Qt Toolkit.
+**
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 3 as published by the Free Software
+** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-3.0.html.
+**
+****************************************************************************/
+
+#include <QGuiApplication>
+#include <QTemporaryFile>
+#include <QTest>
+
+#include "tst_designstudio.h"
+#include "tst_dsdiscovery.h"
+#include "tst_projectmanager.h"
+#include "tst_serviceconnector.h"
+
+#define DEBUG_LINE qDebug() << "Debugline:" << __LINE__
+
+int main(int argc, char *argv[])
+{
+    QGuiApplication app(argc, argv);
+
+    QStringList initialArgs = app.arguments();
+    DEBUG_LINE << "Arguments:" << initialArgs;
+
+    QString outputFileName;
+    QString appName = initialArgs.first();
+    initialArgs.removeFirst();
+
+    if (initialArgs.contains("-o")) {
+        int index = initialArgs.indexOf("-o");
+        if (index < initialArgs.size() - 1) {
+            outputFileName = initialArgs.at(index + 1);
+            outputFileName.remove(",junitxml"); // remove the junitxml extension if it's provided
+            DEBUG_LINE << "Find output file name inside the args:" << outputFileName;
+
+            initialArgs.removeAt(index); // remove the -o flag
+            initialArgs.removeAt(index); // remove the output file name
+        }
+    }
+
+    if (outputFileName.isEmpty()) {
+        qDebug() << "Output file name not provided. Using the default name: output.junitxml";
+        outputFileName = "output.junitxml";
+    }
+
+    QFile outputFile{outputFileName};
+    if (!outputFile.open(QIODevice::Append)) {
+        DEBUG_LINE << "Failed to open the output file";
+        return 1;
+    }
+
+    int status = 0;
+    auto runTest = [&status, &outputFile, initialArgs, appName](QObject *obj) {
+        // temporary file to store the individual test results.
+        // we'll append the results to the output file.
+        QTemporaryFile file;
+        file.open();
+        DEBUG_LINE << "Temporary file:" << file.fileName();
+
+        QStringList args{"-o", QString(file.fileName()).append(",junitxml")};
+        args.prepend(appName);
+        args.append(initialArgs);
+
+        DEBUG_LINE << "Running test" << obj->metaObject()->className() << "with arguments:" << args;
+        status |= QTest::qExec(obj, args);
+
+        // append the test results to the output file
+        file.readLine(); // skip the first line because it's the xml header
+        outputFile.write(file.readAll());
+    };
+
+    outputFile.write("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
+    outputFile.write("<testsuites>\n");
+
+    runTest(new TestServiceConnector);
+    runTest(new TestProjectManager);
+    runTest(new TestDesignStudio);
+    runTest(new TestDesignStudioDiscovery);
+
+    outputFile.write("</testsuites>\n");
+
+    DEBUG_LINE << "Test suite finished with status:" << status;
+    return status;
+}
diff --git a/tests/mock.h b/tests/mock.h
new file mode 100644
index 0000000000000000000000000000000000000000..f7eb895c7154d5b7db6840aac91cc3fcb4c751f0
--- /dev/null
+++ b/tests/mock.h
@@ -0,0 +1,99 @@
+/****************************************************************************
+**
+** Copyright (C) 2024 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the Qt Design Viewer of the Qt Toolkit.
+**
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 3 as published by the Free Software
+** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-3.0.html.
+**
+****************************************************************************/
+
+#pragma once
+
+#include <QJsonDocument>
+#include <QJsonObject>
+#include <QObject>
+#include <QTimer>
+#include <QUdpSocket>
+#include <QWebSocket>
+#include <QWebSocketServer>
+#include <qtypes.h>
+
+#include "backend/dsconnector/tcpdatatypes.h"
+
+#define UDP_PORT 53452
+
+class DesignStudioMock : public QObject
+{
+    Q_OBJECT
+
+private:
+    // self advertisement over udp
+    QTimer m_discoveryTimer;
+    const QString m_id;
+
+    // websocket connection to design studio
+    QWebSocketServer m_webSocketServer;
+
+public:
+    DesignStudioMock(QObject *parent = nullptr, const QString &id = "123456")
+        : QObject(parent)
+        , m_id(id)
+        , m_webSocketServer("DesignStudioMock", QWebSocketServer::NonSecureMode, this)
+
+    {
+        connect(&m_discoveryTimer, &QTimer::timeout, [this] {
+            QUdpSocket udpSocket;
+
+            QJsonObject message;
+            message["name"] = "__designstudio__";
+            message["id"] = m_id;
+            QByteArray datagram = QJsonDocument(message).toJson(QJsonDocument::Compact);
+
+            udpSocket.writeDatagram(datagram, QHostAddress::LocalHost, UDP_PORT);
+            udpSocket.writeDatagram(datagram, QHostAddress::Broadcast, UDP_PORT);
+        });
+
+        m_discoveryTimer.setInterval(100);
+        m_discoveryTimer.start();
+
+        connect(&m_webSocketServer, &QWebSocketServer::newConnection, [this] {
+            QWebSocket *socket = m_webSocketServer.nextPendingConnection();
+            connect(socket,
+                    &QWebSocket::textMessageReceived,
+                    [this, socket](const QString &message) {
+                        QJsonObject json = QJsonDocument::fromJson(message.toUtf8()).object();
+                    });
+        });
+    }
+
+    void enableDiscovery(const bool enable)
+    {
+        if (enable) {
+            m_discoveryTimer.start();
+        } else {
+            m_discoveryTimer.stop();
+        }
+    }
+
+    void connectToDesignStudio();
+    void disconnectFromDesignStudio();
+
+signals:
+    void disconnected();
+};
diff --git a/tests/scripts/get_test_result.sh b/tests/scripts/get_test_result.sh
new file mode 100644
index 0000000000000000000000000000000000000000..c4758afc6b5c6c354fe81c08a4ad9f8fff3e69b9
--- /dev/null
+++ b/tests/scripts/get_test_result.sh
@@ -0,0 +1,28 @@
+#!/bin/bash
+
+# first argument is the path to the output.junit.xml file
+if [ $# -ne 1 ]; then
+    echo "Usage: $0 <path_to_output.junit.xml>"
+    exit 1
+fi
+
+UNIT_TEST_RESULT_FILE=$1
+
+# check if the file exists
+if [ ! -f "${UNIT_TEST_RESULT_FILE}" ]; then
+    echo "File ${UNIT_TEST_RESULT_FILE} not found!"
+    exit 1
+fi
+
+# check if the file is empty
+if [ ! -s "${UNIT_TEST_RESULT_FILE}" ]; then
+    echo "File ${UNIT_TEST_RESULT_FILE} is empty!"
+    exit 1
+fi
+
+RETVAL=$(grep -c "<failure" ${UNIT_TEST_RESULT_FILE})
+
+if [ ${RETVAL} -ne 0 ]; then
+    echo "Test failed!"
+    exit 1
+fi
diff --git a/tests/tst_designstudio.cpp b/tests/tst_designstudio.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..b22b4e632e4b6a0d50c3f88d0034d29a981fa842
--- /dev/null
+++ b/tests/tst_designstudio.cpp
@@ -0,0 +1,301 @@
+/****************************************************************************
+**
+** Copyright (C) 2024 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the Qt Design Viewer of the Qt Toolkit.
+**
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 3 as published by the Free Software
+** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-3.0.html.
+**
+****************************************************************************/
+
+#include "tst_designstudio.h"
+
+#include <QJsonDocument>
+#include <QJsonObject>
+#include <QLatin1String>
+#include <QSignalSpy>
+#include <QTest>
+#include <QWebSocket>
+#include <QWebSocketServer>
+
+#include "backend/dsconnector/ds.h"
+#include "backend/dsconnector/tcpdatatypes.h"
+
+QJsonObject createPackage(const QLatin1String &type, const QString &data)
+{
+    QJsonObject obj;
+    obj.insert("dataType", type.toString());
+    obj.insert("data", data);
+    return obj;
+}
+
+void TestDesignStudio::initTestCase()
+{
+    qDebug() << "Initialize TestDesignStudio";
+    ds = new DesignStudio("localhost", 40000, "id", "deviceId");
+    QVERIFY(ds != nullptr);
+}
+
+void TestDesignStudio::testIpv4Addr()
+{
+    QCOMPARE(ds->ipv4Addr(), QString("localhost"));
+}
+
+void TestDesignStudio::testId()
+{
+    QCOMPARE(ds->designStudioId(), QString("id"));
+}
+
+void TestDesignStudio::testPort()
+{
+    QCOMPARE(ds->port(), quint16(40000));
+}
+
+void TestDesignStudio::testUrl()
+{
+    QCOMPARE(ds->url(), QUrl("ws://localhost:40000"));
+}
+
+void TestDesignStudio::testDeviceId()
+{
+    QCOMPARE(ds->deviceId(), QString("deviceId"));
+}
+
+void TestDesignStudio::testConnected()
+{
+    QHostAddress addr = QHostAddress::LocalHost;
+    QWebSocketServer server("TestDesignStudio", QWebSocketServer::NonSecureMode);
+    QVERIFY(server.listen(addr, 40001));
+
+    DesignStudio ds(addr.toString(), 40001, "id", "deviceId");
+    QTRY_VERIFY(ds.connected());
+}
+
+void TestDesignStudio::testSendRegistrationPin()
+{
+    QWebSocketServer server("TestDesignStudio", QWebSocketServer::NonSecureMode);
+    QVERIFY(server.listen(QHostAddress::LocalHost, 40002));
+
+    DesignStudio ds("localhost", 40002, "id", "deviceId");
+    QTRY_VERIFY(ds.connected());
+
+    QWebSocket *socket = server.nextPendingConnection();
+    QVERIFY(socket != nullptr);
+
+    connect(socket, &QWebSocket::textMessageReceived, [](const QString &message) {
+        qDebug() << "Received message" << message;
+
+        QJsonObject obj = QJsonDocument::fromJson(message.toUtf8()).object();
+        QVERIFY(obj.contains("dataType"));
+        QVERIFY(obj.contains("data"));
+        QVERIFY(obj.value("dataType").toString() == PackageToDesignStudio::registrationPin);
+        QVERIFY(obj.value("data").toString() == "1234");
+    });
+
+    QSignalSpy spy(socket, &QWebSocket::textMessageReceived);
+    QVERIFY(spy.isValid());
+
+    ds.sendRegistrationPin("1234");
+    QTRY_COMPARE(spy.count(), 1);
+    QTest::qWait(1000);
+}
+
+void TestDesignStudio::testSendDeviceInfo()
+{
+    QWebSocketServer server("TestDesignStudio", QWebSocketServer::NonSecureMode);
+    QVERIFY(server.listen(QHostAddress::LocalHost, 40003));
+
+    DesignStudio ds("localhost", 40003, "id", "deviceId");
+    QTRY_VERIFY(ds.connected());
+
+    QWebSocket *socket = server.nextPendingConnection();
+    QVERIFY(socket != nullptr);
+
+    connect(socket, &QWebSocket::textMessageReceived, [](const QString &message) {
+        qDebug() << "Received message" << message;
+
+        QJsonObject obj = QJsonDocument::fromJson(message.toUtf8()).object();
+        QVERIFY(obj.contains("dataType"));
+        QVERIFY(obj.contains("data"));
+        QVERIFY(obj.value("dataType").toString() == PackageToDesignStudio::deviceInfo);
+        QVERIFY(obj.value("data").toObject().contains("screenHeight"));
+        QVERIFY(obj.value("data").toObject().contains("screenWidth"));
+        QVERIFY(obj.value("data").toObject().contains("os"));
+        QVERIFY(obj.value("data").toObject().contains("osVersion"));
+        QVERIFY(obj.value("data").toObject().contains("architecture"));
+        QVERIFY(obj.value("data").toObject().contains("deviceId"));
+        QVERIFY(obj.value("data").toObject().contains("appVersion"));
+    });
+
+    QSignalSpy spy(socket, &QWebSocket::textMessageReceived);
+    QVERIFY(spy.isValid());
+
+    const QJsonObject dsReadyPackage = createPackage(PackageFromDesignStudio::designStudioReady,
+                                                     "data");
+    socket->sendTextMessage(QJsonDocument(dsReadyPackage).toJson(QJsonDocument::Compact));
+
+    QTRY_COMPARE(spy.count(), 1);
+    QTest::qWait(500);
+}
+
+void TestDesignStudio::testRegistrationPinRequestedSignal()
+{
+    QWebSocketServer server("TestDesignStudio", QWebSocketServer::NonSecureMode);
+    QVERIFY(server.listen(QHostAddress::LocalHost, 40005));
+
+    DesignStudio ds("localhost", 40005, "id", "deviceId");
+    QTRY_VERIFY(ds.connected());
+
+    QWebSocket *socket = server.nextPendingConnection();
+    QVERIFY(socket != nullptr);
+
+    QSignalSpy spy(&ds, &DesignStudio::registrationPinRequested);
+    QVERIFY(spy.isValid());
+
+    const QJsonObject pinRequestedPackage
+        = createPackage(PackageFromDesignStudio::pinVerificationRequested, "data");
+    socket->sendTextMessage(QJsonDocument(pinRequestedPackage).toJson(QJsonDocument::Compact));
+
+    QTRY_COMPARE(spy.count(), 1);
+}
+
+void TestDesignStudio::testPinVerificationFailedSignal()
+{
+    QWebSocketServer server("TestDesignStudio", QWebSocketServer::NonSecureMode);
+    QVERIFY(server.listen(QHostAddress::LocalHost, 40006));
+
+    DesignStudio ds("localhost", 40006, "id", "deviceId");
+    QTRY_VERIFY(ds.connected());
+
+    QWebSocket *socket = server.nextPendingConnection();
+    QVERIFY(socket != nullptr);
+
+    QSignalSpy spy(&ds, &DesignStudio::pinVerificationFailed);
+    QVERIFY(spy.isValid());
+
+    const QJsonObject pinVerificationFailedPackage
+        = createPackage(PackageFromDesignStudio::pinVerificationFailed, "data");
+    socket->sendTextMessage(
+        QJsonDocument(pinVerificationFailedPackage).toJson(QJsonDocument::Compact));
+
+    QTRY_COMPARE(spy.count(), 1);
+}
+
+void TestDesignStudio::testRegistrationSuccessfulSignal()
+{
+    QWebSocketServer server("TestDesignStudio", QWebSocketServer::NonSecureMode);
+    QVERIFY(server.listen(QHostAddress::LocalHost, 40007));
+
+    DesignStudio ds("localhost", 40007, "id");
+    QTRY_VERIFY(ds.connected());
+
+    QWebSocket *socket = server.nextPendingConnection();
+    QVERIFY(socket != nullptr);
+
+    QSignalSpy spy(&ds, &DesignStudio::registrationSucceeded);
+    QVERIFY(spy.isValid());
+
+    const QJsonObject deviceIdReceivedPackage
+        = createPackage(PackageFromDesignStudio::deviceIdReceived, "data");
+    socket->sendTextMessage(QJsonDocument(deviceIdReceivedPackage).toJson(QJsonDocument::Compact));
+
+    QTRY_COMPARE(spy.count(), 1);
+}
+
+void TestDesignStudio::testUnregisteredSignal()
+{
+    QWebSocketServer server("TestDesignStudio", QWebSocketServer::NonSecureMode);
+    QVERIFY(server.listen(QHostAddress::LocalHost, 40009));
+
+    DesignStudio ds("localhost", 40009, "id", "deviceId");
+    QTRY_VERIFY(ds.connected());
+
+    QWebSocket *socket = server.nextPendingConnection();
+    QVERIFY(socket != nullptr);
+
+    QSignalSpy spy(&ds, &DesignStudio::unregistered);
+    QVERIFY(spy.isValid());
+
+    const QJsonObject deviceIdDeclinedPackage
+        = createPackage(PackageFromDesignStudio::deviceIdDeclined, "data");
+    socket->sendTextMessage(QJsonDocument(deviceIdDeclinedPackage).toJson(QJsonDocument::Compact));
+
+    QTRY_COMPARE(spy.count(), 1);
+}
+
+void TestDesignStudio::testConnectionAliveSignal()
+{
+    QWebSocketServer server("TestDesignStudio", QWebSocketServer::NonSecureMode);
+    QVERIFY(server.listen(QHostAddress::LocalHost, 40010));
+
+    DesignStudio ds("localhost", 40010, "id", "deviceId");
+    QTRY_VERIFY(ds.connected());
+
+    QWebSocket *socket = server.nextPendingConnection();
+    QVERIFY(socket != nullptr);
+
+    QSignalSpy spy(&ds, &DesignStudio::dsOnline);
+    QVERIFY(spy.isValid());
+
+    const QJsonObject dsReadyPackage = createPackage(PackageFromDesignStudio::deviceIdAccepted,
+                                                     "data");
+    socket->sendTextMessage(QJsonDocument(dsReadyPackage).toJson(QJsonDocument::Compact));
+
+    QTRY_COMPARE(spy.count(), 1);
+}
+
+void TestDesignStudio::testProjectIncomingSignal()
+{
+    QWebSocketServer server("TestDesignStudio", QWebSocketServer::NonSecureMode);
+    QVERIFY(server.listen(QHostAddress::LocalHost, 40011));
+
+    DesignStudio ds("localhost", 40011, "id", "deviceId");
+    QTRY_VERIFY(ds.connected());
+
+    QWebSocket *socket = server.nextPendingConnection();
+    QVERIFY(socket != nullptr);
+
+    QSignalSpy spy(&ds, &DesignStudio::projectIncoming);
+    QVERIFY(spy.isValid());
+
+    const QJsonObject projectIncomingPackage = createPackage(PackageFromDesignStudio::projectData,
+                                                             "data");
+    socket->sendTextMessage(QJsonDocument(projectIncomingPackage).toJson(QJsonDocument::Compact));
+
+    QTRY_COMPARE(spy.count(), 1);
+}
+
+void TestDesignStudio::testProjectReceivedSignal()
+{
+    QWebSocketServer server("TestDesignStudio", QWebSocketServer::NonSecureMode);
+    QVERIFY(server.listen(QHostAddress::LocalHost, 40012));
+
+    DesignStudio ds("localhost", 40012, "id", "deviceId");
+    QTRY_VERIFY(ds.connected());
+
+    QWebSocket *socket = server.nextPendingConnection();
+    QVERIFY(socket != nullptr);
+
+    QSignalSpy spy(&ds, &DesignStudio::projectReceived);
+    QVERIFY(spy.isValid());
+
+    const QString arbitraryData = "some arbitrary data";
+    socket->sendBinaryMessage(arbitraryData.toUtf8());
+
+    QTRY_COMPARE(spy.count(), 1);
+}
diff --git a/tests/tst_designstudio.h b/tests/tst_designstudio.h
new file mode 100644
index 0000000000000000000000000000000000000000..518d12ee9d17c50d5b0fefc85c888f5775226524
--- /dev/null
+++ b/tests/tst_designstudio.h
@@ -0,0 +1,62 @@
+/****************************************************************************
+**
+** Copyright (C) 2024 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the Qt Design Viewer of the Qt Toolkit.
+**
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 3 as published by the Free Software
+** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-3.0.html.
+**
+****************************************************************************/
+
+#pragma once
+
+#include "backend/dsconnector/ds.h"
+
+class TestDesignStudio : public QObject
+{
+    Q_OBJECT
+private:
+    DesignStudio *ds;
+
+private slots:
+    // init
+    void initTestCase();
+
+    // test getters
+    void testIpv4Addr();
+    void testId();
+    void testPort();
+    void testUrl();
+    void testDeviceId();
+
+    // test status check
+    void testConnected();
+
+    // test send data
+    void testSendRegistrationPin();
+    void testSendDeviceInfo();
+
+    // test signals
+    void testRegistrationPinRequestedSignal();
+    void testPinVerificationFailedSignal();
+    void testRegistrationSuccessfulSignal();
+    void testUnregisteredSignal();
+    void testConnectionAliveSignal();
+    void testProjectIncomingSignal();
+    void testProjectReceivedSignal();
+};
diff --git a/tests/tst_dsdiscovery.cpp b/tests/tst_dsdiscovery.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..e5d1a7acc97f87a4d9d6d21a4f52d97089edea13
--- /dev/null
+++ b/tests/tst_dsdiscovery.cpp
@@ -0,0 +1,67 @@
+/****************************************************************************
+**
+** Copyright (C) 2024 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the Qt Design Viewer of the Qt Toolkit.
+**
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 3 as published by the Free Software
+** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-3.0.html.
+**
+****************************************************************************/
+
+#include "tst_dsdiscovery.h"
+
+#include <QJsonArray>
+#include <QJsonDocument>
+#include <QJsonObject>
+#include <QSignalSpy>
+#include <QUdpSocket>
+
+#include "backend/dsconnector/dsdiscovery.h"
+
+#define UDP_PORT 53452
+
+void TestDesignStudioDiscovery::initTestCase()
+{
+    qDebug() << "Initialize TestDesignStudioDiscovery";
+
+    connect(&m_timer, &QTimer::timeout, [] {
+        QUdpSocket udpSocket;
+
+        QJsonObject message;
+        message["name"] = "__designstudio__";
+        message["id"] = "123456";
+        QByteArray datagram = QJsonDocument(message).toJson(QJsonDocument::Compact);
+
+        udpSocket.writeDatagram(datagram, QHostAddress::LocalHost, UDP_PORT);
+        udpSocket.writeDatagram(datagram, QHostAddress::Broadcast, UDP_PORT);
+    });
+}
+
+void TestDesignStudioDiscovery::testDesignStudioFoundSignal()
+{
+    qDebug() << "Test Design Studio Found Signal";
+
+    DesignStudioDiscovery dsDiscovery;
+    connect(&dsDiscovery, &DesignStudioDiscovery::designStudioFound, [](const QString &ipv4Addr) {
+        qDebug() << "Design Studio found at" << ipv4Addr;
+    });
+
+    m_timer.start(1000);
+
+    QSignalSpy spy(&dsDiscovery, &DesignStudioDiscovery::designStudioFound);
+}
diff --git a/tests/tst_dsdiscovery.h b/tests/tst_dsdiscovery.h
new file mode 100644
index 0000000000000000000000000000000000000000..d8f092a0964e77a868475c71d25443d9378ba483
--- /dev/null
+++ b/tests/tst_dsdiscovery.h
@@ -0,0 +1,41 @@
+/****************************************************************************
+**
+** Copyright (C) 2024 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the Qt Design Viewer of the Qt Toolkit.
+**
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 3 as published by the Free Software
+** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-3.0.html.
+**
+****************************************************************************/
+
+#pragma once
+
+#include <QTimer>
+
+class TestDesignStudioDiscovery : public QObject
+{
+    Q_OBJECT
+private:
+    QTimer m_timer;
+
+private slots:
+    void initTestCase();
+
+    // test signals
+    void testDesignStudioFoundSignal();
+};
diff --git a/tests/tst_dsmanager.cpp b/tests/tst_dsmanager.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..51f9a0c2f3077052127b5aea26c0c446166d3aa7
--- /dev/null
+++ b/tests/tst_dsmanager.cpp
@@ -0,0 +1,59 @@
+/****************************************************************************
+**
+** Copyright (C) 2024 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the Qt Design Viewer of the Qt Toolkit.
+**
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 3 as published by the Free Software
+** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-3.0.html.
+**
+****************************************************************************/
+
+#include "tst_dsmanager.h"
+
+#include <QJsonDocument>
+#include <QJsonObject>
+#include <QTest>
+#include <QUdpSocket>
+
+#include "backend/dsconnector/dsmanager.h"
+#include "mock.h"
+
+void TestDesignStudioManager::testEnterPin()
+{
+    // Test enterPin() function
+}
+
+void TestDesignStudioManager::testDesignStudioRegisteredSignal()
+{
+    // Test designStudioRegisteredSignal() function
+}
+
+void TestDesignStudioManager::testDesignStudioUnregisteredSignal()
+{
+    // Test designStudioUnregisteredSignal() function
+}
+
+void TestDesignStudioManager::testPinRequestedSignal()
+{
+    // Test pinRequestedSignal() function
+}
+
+void TestDesignStudioManager::testProjectReceivedSignal()
+{
+    // Test projectReceivedSignal() function
+}
diff --git a/tests/tst_dsmanager.h b/tests/tst_dsmanager.h
new file mode 100644
index 0000000000000000000000000000000000000000..07442f2a9a7aa4f6590b6f8f7ff86f632481d33d
--- /dev/null
+++ b/tests/tst_dsmanager.h
@@ -0,0 +1,44 @@
+/****************************************************************************
+**
+** Copyright (C) 2024 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the Qt Design Viewer of the Qt Toolkit.
+**
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 3 as published by the Free Software
+** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-3.0.html.
+**
+****************************************************************************/
+
+#pragma once
+
+#include <QObject>
+
+class TestDesignStudioManager : public QObject
+{
+    Q_OBJECT
+private:
+private slots:
+
+    // test setters
+    void testEnterPin();
+
+    // test signals
+    void testDesignStudioRegisteredSignal();
+    void testDesignStudioUnregisteredSignal();
+    void testPinRequestedSignal();
+    void testProjectReceivedSignal();
+};
diff --git a/tests/tst_qtuiviewer.cpp b/tests/tst_projectmanager.cpp
similarity index 53%
rename from tests/tst_qtuiviewer.cpp
rename to tests/tst_projectmanager.cpp
index 08499d8c8876a997c4b77181a206e2578280efac..c2caf9a080e29d6cda9a74bf7f72f8c01b70180e 100644
--- a/tests/tst_qtuiviewer.cpp
+++ b/tests/tst_projectmanager.cpp
@@ -23,28 +23,28 @@
 **
 ****************************************************************************/
 
-#include "tst_qtuiviewer.h"
+#include "tst_projectmanager.h"
 
-#include <QSignalSpy>
+#include <QJsonArray>
+#include <QTest>
 
-#include "backend/dsconnector.h"
 #include "backend/projectmanager.h"
 #include "backend/serviceconnector.h"
 
 #define DEMO_PROJECT_NAME "ClusterTutorial"
 
-void TestQtUiViewer::initTestCase()
+void TestProjectManager::initTestCase()
 {
-    qDebug() << "initTestCase";
+    qDebug() << "Initialize TestProjectManager";
     ServiceConnector sc;
-    m_demoProjectList = sc.fetchDemoList();
-    QVERIFY(m_demoProjectList.has_value());
+    std::optional<QJsonArray> demoProjectList = sc.fetchDemoList();
+    QVERIFY(demoProjectList.has_value());
 
     m_demoProjectData = sc.fetchDemo(DEMO_PROJECT_NAME);
     QVERIFY(m_demoProjectData.has_value());
 
     QJsonObject projectInfo;
-    for (auto project : m_demoProjectList.value()) {
+    for (auto project : demoProjectList.value()) {
         if (DEMO_PROJECT_NAME == project.toObject().value("name").toString().remove(".qmlrc")) {
             m_demoInfoCorrect = project.toObject();
             break;
@@ -73,39 +73,17 @@ void TestQtUiViewer::initTestCase()
                                               {"ttlDays", 31},
                                               {"uploadTime", "2023-10-27T13:58:22"},
                                               {"userHash", "12038740912873462987"}}};
-    qDebug() << "initTestCase done";
+    qDebug() << "TestProjectManager initialized";
 }
 
-void TestQtUiViewer::fetchDemoProjectList()
-{
-    QStringList projectNames{"ClusterTutorial.qmlrc",
-                             "CoffeeMachine.qmlrc",
-                             "EBikeDesign.qmlrc",
-                             "MaterialBundle.qmlrc",
-                             "SideMenu.qmlrc",
-                             "WebinarDemo.qmlrc"};
-
-    QStringList projectNamesFromJson;
-    for (const auto &project : m_demoProjectList.value()) {
-        projectNamesFromJson.append(project.toObject().value("name").toString());
-    }
-
-    QCOMPARE(projectNamesFromJson, projectNames);
-}
-
-void TestQtUiViewer::fetchDemoProject()
-{
-    QCOMPARE(m_demoProjectData.has_value(), true);
-}
-
-void TestQtUiViewer::cacheDemoProject()
+void TestProjectManager::cacheDemoProject()
 {
     ProjectManager pm;
 
     QCOMPARE(pm.cacheDemoProject(m_demoProjectData.value(), m_demoInfoCorrect), true);
 }
 
-void TestQtUiViewer::isDemoProjectCachedTrue()
+void TestProjectManager::isDemoProjectCachedTrue()
 {
     ProjectManager pm;
 
@@ -113,7 +91,7 @@ void TestQtUiViewer::isDemoProjectCachedTrue()
     QCOMPARE(pm.isDemoProjectCached(m_demoInfoCorrect), true);
 }
 
-void TestQtUiViewer::isDemoProjectCachedFalse()
+void TestProjectManager::isDemoProjectCachedFalse()
 {
     ProjectManager pm;
 
@@ -121,7 +99,7 @@ void TestQtUiViewer::isDemoProjectCachedFalse()
     QCOMPARE(pm.isDemoProjectCached(m_demoInfoIncorrect), false);
 }
 
-void TestQtUiViewer::clearDemoProjectCache()
+void TestProjectManager::clearDemoProjectCache()
 {
     ProjectManager pm;
     QVERIFY(pm.cacheDemoProject(m_demoProjectData.value(), m_demoInfoCorrect));
@@ -130,27 +108,27 @@ void TestQtUiViewer::clearDemoProjectCache()
     QCOMPARE(pm.isDemoProjectCached(m_demoInfoCorrect), false);
 }
 
-void TestQtUiViewer::cacheUserProject()
+void TestProjectManager::cacheUserProject()
 {
     ProjectManager pm;
     QCOMPARE(pm.cacheProject(m_demoProjectData.value(), m_userProjectInfoCorrect), true);
 }
 
-void TestQtUiViewer::isUserProjectCachedTrue()
+void TestProjectManager::isUserProjectCachedTrue()
 {
     ProjectManager pm;
     pm.cacheProject(m_demoProjectData.value(), m_userProjectInfoCorrect);
     QCOMPARE(pm.isProjectCached(m_userProjectInfoCorrect), true);
 }
 
-void TestQtUiViewer::isUserProjectCachedFalse()
+void TestProjectManager::isUserProjectCachedFalse()
 {
     ProjectManager pm;
     QVERIFY(pm.cacheProject(m_demoProjectData.value(), m_userProjectInfoCorrect));
     QCOMPARE(pm.isProjectCached(m_userProjectInfoIncorrect), false);
 }
 
-void TestQtUiViewer::clearUserProjectCache()
+void TestProjectManager::clearUserProjectCache()
 {
     ProjectManager pm;
     QVERIFY(pm.cacheProject(m_demoProjectData.value(), m_userProjectInfoCorrect));
@@ -160,113 +138,10 @@ void TestQtUiViewer::clearUserProjectCache()
     QCOMPARE(pm.isProjectCached(m_userProjectInfoCorrect), false);
 }
 
-void TestQtUiViewer::clearNonExistentUserProjectCache()
+void TestProjectManager::clearNonExistentUserProjectCache()
 {
     ProjectManager pm;
     QVERIFY(!pm.isProjectCached(m_userProjectInfoIncorrect));
     pm.clearCachedProject(m_userProjectInfoIncorrect);
     QCOMPARE(pm.isProjectCached(m_userProjectInfoIncorrect), false);
 }
-
-void TestQtUiViewer::initDsConnector()
-{
-    DesignStudioConnector dsc(40000);
-
-    QTcpSocket socket;
-    socket.connectToHost("localhost", 40000);
-
-    QVERIFY(socket.waitForConnected(1000));
-}
-
-void TestQtUiViewer::incomingProjectTrue()
-{
-    DesignStudioConnector dsc(40001);
-
-    QTcpSocket socket;
-    socket.connectToHost("localhost", 40001);
-
-    QVERIFY(socket.waitForConnected(1000));
-
-    // we'll write a sample project over the tcp connection
-    // and expect the DesignStudioConnector to emit the projectIncoming signal
-    QSignalSpy spy(&dsc, &DesignStudioConnector::projectIncoming);
-    QVERIFY(spy.isValid());
-
-    QByteArray projectData = "qres::qmlrc-start::";
-    socket.write(projectData);
-    QVERIFY(socket.waitForBytesWritten(1000));
-
-    QTRY_COMPARE(spy.count(), 1);
-}
-
-void TestQtUiViewer::incomingProjectFalse()
-{
-    DesignStudioConnector dsc(40002);
-
-    QTcpSocket socket;
-    socket.connectToHost("localhost", 40002);
-
-    QVERIFY(socket.waitForConnected(1000));
-
-    // we'll write a arbitrary data over the tcp connection
-    // and don't expect the DesignStudioConnector to emit the projectIncoming signal
-    QSignalSpy spy(&dsc, SIGNAL(projectIncoming()));
-
-    QByteArray projectData = "some arbitrary data";
-    socket.write(projectData);
-    QVERIFY(socket.waitForBytesWritten(1000));
-
-    QTRY_COMPARE(spy.count(), 0);
-}
-
-void TestQtUiViewer::receiveProject()
-{
-    DesignStudioConnector dsc(40003);
-
-    QTcpSocket socket;
-    socket.connectToHost("localhost", 40003);
-
-    QVERIFY(socket.waitForConnected(1000));
-
-    // we'll write a sample project over the tcp connection
-    // and expect the DesignStudioConnector to emit the projectReceived signal
-    QSignalSpy spy(&dsc, &DesignStudioConnector::projectReceived);
-    QVERIFY(spy.isValid());
-
-    QByteArray projectData = "qres::qmlrc-start::";
-    socket.write(projectData);
-    QVERIFY(socket.waitForBytesWritten(1000));
-
-    spy.wait(1000);
-
-    projectData = "Hello World::qmlrc-end::";
-    socket.write(projectData);
-    QVERIFY(socket.waitForBytesWritten(1000));
-
-    QTRY_COMPARE(spy.count(), 1);
-}
-
-void TestQtUiViewer::networkStatusUpdated()
-{
-    DesignStudioConnector dsc(40004);
-
-    QTcpSocket socket;
-
-    QSignalSpy spy(&dsc, &DesignStudioConnector::networkStatusUpdated);
-    QVERIFY(spy.isValid());
-
-    socket.connectToHost("localhost", 40004);
-    QVERIFY(socket.waitForConnected(1000));
-
-    spy.wait(500);
-    QTRY_COMPARE(spy.count(), 1);
-
-    socket.disconnectFromHost();
-    socket.waitForDisconnected(1000);
-    QVERIFY(socket.state() == QAbstractSocket::UnconnectedState);
-
-    spy.wait(500);
-    QTRY_COMPARE(spy.count(), 2);
-}
-
-QTEST_MAIN(TestQtUiViewer);
diff --git a/tests/tst_qtuiviewer.h b/tests/tst_projectmanager.h
similarity index 82%
rename from tests/tst_qtuiviewer.h
rename to tests/tst_projectmanager.h
index c158d378053baccfec8b52b34bb965ce015414a2..4e7635a356cc1ae3acd8ebf0e9507de3e8514752 100644
--- a/tests/tst_qtuiviewer.h
+++ b/tests/tst_projectmanager.h
@@ -23,15 +23,14 @@
 **
 ****************************************************************************/
 
-#include <QJsonArray>
+#pragma once
+
 #include <QJsonObject>
-#include <QtTest/QTest>
 
-class TestQtUiViewer : public QObject
+class TestProjectManager : public QObject
 {
     Q_OBJECT
 private:
-    std::optional<QJsonArray> m_demoProjectList;
     std::optional<QByteArray> m_demoProjectData;
     QJsonObject m_demoInfoCorrect;
     QJsonObject m_demoInfoIncorrect;
@@ -39,12 +38,9 @@ private:
     QJsonObject m_userProjectInfoIncorrect;
 
 private slots:
+    // init
     void initTestCase();
 
-    // service connector tests
-    void fetchDemoProjectList();
-    void fetchDemoProject();
-
     // project manager tests - demo project
     void cacheDemoProject();
     void isDemoProjectCachedTrue();
@@ -57,11 +53,4 @@ private slots:
     void isUserProjectCachedFalse();
     void clearUserProjectCache();
     void clearNonExistentUserProjectCache();
-
-    // ds connector tests
-    void initDsConnector();
-    void incomingProjectTrue();
-    void incomingProjectFalse();
-    void receiveProject();
-    void networkStatusUpdated();
 };
diff --git a/tests/tst_serviceconnector.cpp b/tests/tst_serviceconnector.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..9b6c66e9ea7a8a4fde24eb668f91f54bbbd58c39
--- /dev/null
+++ b/tests/tst_serviceconnector.cpp
@@ -0,0 +1,68 @@
+/****************************************************************************
+**
+** Copyright (C) 2024 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the Qt Design Viewer of the Qt Toolkit.
+**
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 3 as published by the Free Software
+** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-3.0.html.
+**
+****************************************************************************/
+
+#include "tst_serviceconnector.h"
+
+#include <QJsonObject>
+#include <QTest>
+
+#include "backend/serviceconnector.h"
+
+#define DEMO_PROJECT_NAME "ClusterTutorial"
+
+void TestServiceConnector::initTestCase()
+{
+    qDebug() << "Initialize TestServiceConnector";
+    ServiceConnector sc;
+    m_demoProjectList = sc.fetchDemoList();
+    QVERIFY(m_demoProjectList.has_value());
+
+    m_demoProjectData = sc.fetchDemo(DEMO_PROJECT_NAME);
+    QVERIFY(m_demoProjectData.has_value());
+
+    qDebug() << "TestServiceConnector initialized";
+}
+
+void TestServiceConnector::fetchDemoProjectList()
+{
+    QStringList projectNames{"ClusterTutorial.qmlrc",
+                             "CoffeeMachine.qmlrc",
+                             "EBikeDesign.qmlrc",
+                             "MaterialBundle.qmlrc",
+                             "SideMenu.qmlrc",
+                             "WebinarDemo.qmlrc"};
+
+    QStringList projectNamesFromJson;
+    for (const auto &project : m_demoProjectList.value()) {
+        projectNamesFromJson.append(project.toObject().value("name").toString());
+    }
+
+    QCOMPARE(projectNamesFromJson, projectNames);
+}
+
+void TestServiceConnector::fetchDemoProject()
+{
+    QCOMPARE(m_demoProjectData.has_value(), true);
+}
diff --git a/tests/tst_serviceconnector.h b/tests/tst_serviceconnector.h
new file mode 100644
index 0000000000000000000000000000000000000000..b0dfa99706193d3c3c100475fff8f03c732a9055
--- /dev/null
+++ b/tests/tst_serviceconnector.h
@@ -0,0 +1,43 @@
+/****************************************************************************
+**
+** Copyright (C) 2024 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the Qt Design Viewer of the Qt Toolkit.
+**
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 3 as published by the Free Software
+** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-3.0.html.
+**
+****************************************************************************/
+
+#pragma once
+
+#include <QJsonArray>
+
+class TestServiceConnector : public QObject
+{
+    Q_OBJECT
+private:
+    std::optional<QJsonArray> m_demoProjectList;
+    std::optional<QByteArray> m_demoProjectData;
+
+private slots:
+    void initTestCase();
+
+    // service connector tests
+    void fetchDemoProjectList();
+    void fetchDemoProject();
+};