From 094b8aa2bbb8b90dff9da199873cbc6b36549eb6 Mon Sep 17 00:00:00 2001 From: Thomas Vanbesien Date: Wed, 11 Mar 2026 15:25:11 +0100 Subject: Refactor: three XPL2 ports, EchoServer class, dark debug console, qmllint clean - Xpl2Client: replace single port with commandPort/imagingPort/statusPort (9110/9111/9112) - Mock server: extract EchoServer class from lambda-based listenOn() - Demo: dark debug console matching BobinkQtOpcUa style, 2-column connection layout - Fix qmlls module resolution via IMPORT_PATH in qt_add_qml_module() - Add pragma ComponentBehavior: Bound, fix all qmllint warnings --- CMakeLists.txt | 5 +- demo/CMakeLists.txt | 6 +- demo/Main.qml | 292 +++++++++++++++++++++++++++------------------ mock-server/CMakeLists.txt | 2 +- mock-server/EchoServer.cpp | 50 ++++++++ mock-server/EchoServer.h | 24 ++++ mock-server/main.cpp | 55 +-------- src/Xpl2Client.cpp | 159 ++++++++++++++++-------- src/Xpl2Client.h | 66 ++++++---- 9 files changed, 415 insertions(+), 244 deletions(-) create mode 100644 mock-server/EchoServer.cpp create mode 100644 mock-server/EchoServer.h diff --git a/CMakeLists.txt b/CMakeLists.txt index e49cf1b..6562716 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -10,10 +10,9 @@ qt_standard_project_setup() qt_policy(SET QTP0001 NEW) # QML import path so Qt Creator / qmlls can resolve modules. -list(APPEND QML_IMPORT_PATH "${PROJECT_BINARY_DIR}/qml") set(QML_IMPORT_PATH - "${QML_IMPORT_PATH}" - CACHE STRING "" FORCE) + "${CMAKE_CURRENT_BINARY_DIR}/qml" + CACHE STRING "Path to locally built QML modules") # Generate .qmlls.ini for QML language server set(QT_QML_GENERATE_QMLLS_INI diff --git a/demo/CMakeLists.txt b/demo/CMakeLists.txt index f85639b..dd46473 100644 --- a/demo/CMakeLists.txt +++ b/demo/CMakeLists.txt @@ -7,7 +7,11 @@ qt_add_qml_module( VERSION 1.0 QML_FILES - Main.qml) + Main.qml + # IMPORT_PATH sets QT_QML_IMPORT_PATH on the target, which qmlls reads to + # resolve imports like "import Xpl2" in .qmlls.ini. + IMPORT_PATH + "${PROJECT_BINARY_DIR}/qml") set_target_properties(QtXpl2Demo PROPERTIES RUNTIME_OUTPUT_DIRECTORY "${PROJECT_BINARY_DIR}/bin") diff --git a/demo/Main.qml b/demo/Main.qml index 82b2573..fe9f885 100644 --- a/demo/Main.qml +++ b/demo/Main.qml @@ -1,3 +1,5 @@ +pragma ComponentBehavior: Bound + import QtQuick import QtQuick.Controls import QtQuick.Layouts @@ -6,170 +8,232 @@ import Xpl2 ApplicationWindow { id: root - width: 800 height: 600 - visible: true title: "XPL2 Demo" + visible: true + width: 800 Connections { - target: Xpl2Client - function onConnectedChanged() { - log(Xpl2Client.connected ? "Connected" : "Disconnected"); + debugConsole.appendLog(Xpl2Client.connected ? "Connected" : + "Disconnected"); } - function onResponseReceived(response: string) { - log("← " + response); - responseModel.append({ - text: response - }); + function onErrorOccurred(error: string) { + debugConsole.appendLog("ERROR: " + error); } - function onErrorOccurred(error: string) { - log("ERROR: " + error); + function onResponseReceived(response: string) { + debugConsole.appendLog("Received: " + response); + responseModel.append({ + text: response + }); } function onStatusMessage(message: string) { - log(message); + debugConsole.appendLog(message); } - } - function log(msg: string) { - let ts = new Date().toLocaleTimeString(Qt.locale(), "HH:mm:ss.zzz"); - logModel.append({ - text: "[" + ts + "] " + msg - }); - logView.positionViewAtEnd(); + target: Xpl2Client } ColumnLayout { anchors.fill: parent - anchors.margins: 16 - spacing: 12 + spacing: 0 - // --- Connection --- - GroupBox { - title: "Connection" + ColumnLayout { + Layout.fillHeight: true Layout.fillWidth: true + Layout.margins: 16 + spacing: 12 - GridLayout { - columns: 4 - anchors.fill: parent + // --- Connection --- + GroupBox { + Layout.fillWidth: true + title: "Connection" - Label { - text: "Host:" - } - TextField { - id: hostField - text: Xpl2Client.host - Layout.fillWidth: true - onEditingFinished: Xpl2Client.host = text - } + GridLayout { + anchors.fill: parent + columns: 2 - Label { - text: "Port:" - } - TextField { - id: portField - text: Xpl2Client.port - implicitWidth: 80 - validator: IntValidator { - bottom: 1 - top: 65535 + Label { + text: "Host:" } - onEditingFinished: Xpl2Client.port = parseInt(text) - } - Button { - text: Xpl2Client.connected ? "Disconnect" : "Connect" - Layout.columnSpan: 4 - Layout.alignment: Qt.AlignRight - onClicked: { - if (Xpl2Client.connected) + TextField { + id: hostField + + Layout.fillWidth: true + text: Xpl2Client.host + + onEditingFinished: Xpl2Client.host = text + } + + Label { + text: "Command port:" + } + + TextField { + id: commandPortField + + Layout.fillWidth: true + text: Xpl2Client.commandPort + + validator: IntValidator { + bottom: 1 + top: 65535 + } + + onEditingFinished: Xpl2Client.commandPort = parseInt( + text) + } + + Label { + text: "Imaging port:" + } + + TextField { + id: imagingPortField + + Layout.fillWidth: true + text: Xpl2Client.imagingPort + + validator: IntValidator { + bottom: 1 + top: 65535 + } + + onEditingFinished: Xpl2Client.imagingPort = parseInt( + text) + } + + Label { + text: "Status port:" + } + + TextField { + id: statusPortField + + Layout.fillWidth: true + text: Xpl2Client.statusPort + + validator: IntValidator { + bottom: 1 + top: 65535 + } + + onEditingFinished: Xpl2Client.statusPort = parseInt( + text) + } + + Button { + Layout.alignment: Qt.AlignRight + Layout.columnSpan: 2 + text: Xpl2Client.connected ? "Disconnect" : "Connect" + + onClicked: { + if (Xpl2Client.connected) Xpl2Client.disconnectFromServer(); - else + else Xpl2Client.connectToServer(); + } } } } - } - // --- Send Command --- - GroupBox { - title: "Command" - Layout.fillWidth: true - enabled: Xpl2Client.connected + // --- Send Command --- + GroupBox { + Layout.fillWidth: true + enabled: Xpl2Client.connected + title: "Command" - RowLayout { - anchors.fill: parent + RowLayout { + anchors.fill: parent - TextField { - id: cmdField - placeholderText: "Enter command…" - Layout.fillWidth: true - onAccepted: sendBtn.clicked() - } + TextField { + id: cmdField + + Layout.fillWidth: true + placeholderText: "Enter command…" + + onAccepted: sendBtn.clicked() + } - Button { - id: sendBtn - text: "Send" - onClicked: { - if (cmdField.text.length > 0) { - Xpl2Client.sendCommand(cmdField.text); - cmdField.text = ""; + Button { + id: sendBtn + + text: "Send" + + onClicked: { + if (cmdField.text.length > 0) { + Xpl2Client.sendCommand(cmdField.text); + cmdField.text = ""; + } } } } } - } - // --- Responses --- - GroupBox { - title: "Responses" - Layout.fillWidth: true - Layout.fillHeight: true + // --- Responses --- + GroupBox { + Layout.fillHeight: true + Layout.fillWidth: true + title: "Responses" - ListView { - id: responseView - anchors.fill: parent - clip: true - model: ListModel { - id: responseModel - } + ListView { + id: responseView - delegate: Text { - required property string text - width: responseView.width - wrapMode: Text.Wrap - font.family: "monospace" - font.pixelSize: 13 - text: model.text + anchors.fill: parent + clip: true + + delegate: Text { + id: responseDelegate + + required property string text + + font.family: "monospace" + font.pixelSize: 13 + text: responseDelegate.text + width: responseView.width + wrapMode: Text.Wrap + } + model: ListModel { + id: responseModel + + } } } } - // --- Debug Log --- - GroupBox { - title: "Log" + // --- Debug Console --- + Rectangle { + id: debugConsole + + function appendLog(msg) { + let ts = new Date().toLocaleTimeString(Qt.locale(), "HH:mm:ss"); + debugLog.text += "[" + ts + "] " + msg + "\n"; + debugLog.cursorPosition = debugLog.text.length; + } + Layout.fillWidth: true - Layout.preferredHeight: 160 + Layout.preferredHeight: 120 + border.color: "#444" + color: "#1e1e1e" + radius: 4 - ListView { - id: logView + ScrollView { anchors.fill: parent - clip: true - model: ListModel { - id: logModel - } + anchors.margins: 4 + + TextArea { + id: debugLog - delegate: Text { - required property string text - width: logView.width - wrapMode: Text.Wrap + background: null + color: "#cccccc" font.family: "monospace" - font.pixelSize: 12 - color: "#555" - text: model.text + font.pointSize: 9 + readOnly: true + wrapMode: TextEdit.Wrap } } } diff --git a/mock-server/CMakeLists.txt b/mock-server/CMakeLists.txt index 52bfabb..f5d54f8 100644 --- a/mock-server/CMakeLists.txt +++ b/mock-server/CMakeLists.txt @@ -1,4 +1,4 @@ -qt_add_executable(Xpl2MockServer main.cpp) +qt_add_executable(Xpl2MockServer main.cpp EchoServer.h EchoServer.cpp) set_target_properties(Xpl2MockServer PROPERTIES RUNTIME_OUTPUT_DIRECTORY "${PROJECT_BINARY_DIR}/bin") diff --git a/mock-server/EchoServer.cpp b/mock-server/EchoServer.cpp new file mode 100644 index 0000000..43fc6f9 --- /dev/null +++ b/mock-server/EchoServer.cpp @@ -0,0 +1,50 @@ +/** + * @file EchoServer.cpp + * @brief Simple TCP echo server for a single port. + */ +#include "EchoServer.h" + +#include + +EchoServer::EchoServer (quint16 port, const char *name, QObject *parent) + : QTcpServer (parent), m_name (name), m_port (port) +{ + connect (this, &QTcpServer::newConnection, this, + &EchoServer::onNewConnection); + + if (!listen (QHostAddress::Any, port)) + qCritical ("Failed to listen on %s port %d: %s", m_name, m_port, + qPrintable (errorString ())); + else + qInfo ("Listening on %s port %d", m_name, m_port); +} + +void +EchoServer::onNewConnection () +{ + while (auto *sock = nextPendingConnection ()) + { + qInfo ("[%s:%d] client connected", m_name, m_port); + connect (sock, &QTcpSocket::readyRead, this, + &EchoServer::onClientReadyRead); + connect (sock, &QTcpSocket::disconnected, this, + &EchoServer::onClientDisconnected); + } +} + +void +EchoServer::onClientReadyRead () +{ + auto *sock = qobject_cast (sender ()); + QByteArray data = sock->readAll (); + qInfo ("[%s:%d] echo %lld bytes", m_name, m_port, data.size ()); + sock->write (data); +} + +void +EchoServer::onClientDisconnected () +{ + auto *sock = qobject_cast (sender ()); + qInfo ("[%s:%d] client disconnected", m_name, m_port); + sock->deleteLater (); +} diff --git a/mock-server/EchoServer.h b/mock-server/EchoServer.h new file mode 100644 index 0000000..2e51b1e --- /dev/null +++ b/mock-server/EchoServer.h @@ -0,0 +1,24 @@ +/** + * @file EchoServer.h + * @brief Simple TCP echo server for a single port. + */ +#pragma once + +#include + +class EchoServer : public QTcpServer +{ + Q_OBJECT + +public: + EchoServer (quint16 port, const char *name, QObject *parent = nullptr); + +private slots: + void onNewConnection (); + void onClientReadyRead (); + void onClientDisconnected (); + +private: + const char *m_name; + quint16 m_port; +}; diff --git a/mock-server/main.cpp b/mock-server/main.cpp index 4818d6f..0694e4d 100644 --- a/mock-server/main.cpp +++ b/mock-server/main.cpp @@ -2,63 +2,18 @@ * @file main.cpp * @brief Mock XPL2 server — echoes back on three ports. */ -#include -#include -#include - -static QTcpServer * -listenOn (quint16 port, const char *name, QObject *parent) -{ - auto *server = new QTcpServer (parent); - - QObject::connect ( - server, &QTcpServer::newConnection, server, - [=] () - { - while (auto *sock = server->nextPendingConnection ()) - { - qInfo ("[%s:%d] client connected", name, port); - - QObject::connect (sock, &QTcpSocket::readyRead, sock, - [=] () - { - QByteArray data = sock->readAll (); - qInfo ("[%s:%d] echo %lld bytes", name, - port, data.size ()); - sock->write (data); - }); +#include "EchoServer.h" - QObject::connect (sock, &QTcpSocket::disconnected, sock, - [=] () - { - qInfo ("[%s:%d] client disconnected", name, - port); - sock->deleteLater (); - }); - } - }); - - if (!server->listen (QHostAddress::Any, port)) - { - qCritical ("Failed to listen on %s port %d: %s", name, port, - qPrintable (server->errorString ())); - } - else - { - qInfo ("Listening on %s port %d", name, port); - } - - return server; -} +#include int main (int argc, char *argv[]) { QCoreApplication app (argc, argv); - listenOn (9110, "Command", &app); - listenOn (9111, "Imaging", &app); - listenOn (9112, "Status", &app); + new EchoServer (9110, "Command", &app); + new EchoServer (9111, "Imaging", &app); + new EchoServer (9112, "Status", &app); return app.exec (); } diff --git a/src/Xpl2Client.cpp b/src/Xpl2Client.cpp index db53792..8ed032f 100644 --- a/src/Xpl2Client.cpp +++ b/src/Xpl2Client.cpp @@ -4,83 +4,142 @@ */ #include "Xpl2Client.h" -Xpl2Client::Xpl2Client(QObject *parent) : QObject(parent) { - connect(&m_socket, &QTcpSocket::connected, this, &Xpl2Client::onConnected); - connect(&m_socket, &QTcpSocket::disconnected, this, - &Xpl2Client::onDisconnected); - connect(&m_socket, &QTcpSocket::readyRead, this, &Xpl2Client::onReadyRead); - connect(&m_socket, &QAbstractSocket::errorOccurred, this, - &Xpl2Client::onErrorOccurred); +Xpl2Client::Xpl2Client (QObject *parent) : QObject (parent) +{ + connect (&m_socket, &QTcpSocket::connected, this, &Xpl2Client::onConnected); + connect (&m_socket, &QTcpSocket::disconnected, this, + &Xpl2Client::onDisconnected); + connect (&m_socket, &QTcpSocket::readyRead, this, &Xpl2Client::onReadyRead); + connect (&m_socket, &QAbstractSocket::errorOccurred, this, + &Xpl2Client::onErrorOccurred); } -QString Xpl2Client::host() const { return m_host; } +QString +Xpl2Client::host () const +{ + return m_host; +} -void Xpl2Client::setHost(const QString &host) { +void +Xpl2Client::setHost (const QString &host) +{ if (m_host == host) return; m_host = host; - emit hostChanged(); + emit hostChanged (); } -quint16 Xpl2Client::port() const { return m_port; } +quint16 +Xpl2Client::commandPort () const +{ + return m_commandPort; +} -void Xpl2Client::setPort(quint16 port) { - if (m_port == port) +void +Xpl2Client::setCommandPort (quint16 port) +{ + if (m_commandPort == port) return; - m_port = port; - emit portChanged(); + m_commandPort = port; + emit commandPortChanged (); } -bool Xpl2Client::isConnected() const { - return m_socket.state() == QAbstractSocket::ConnectedState; +quint16 +Xpl2Client::imagingPort () const +{ + return m_imagingPort; } -void Xpl2Client::connectToServer() { - if (isConnected()) { - emit statusMessage(QStringLiteral("Already connected")); +void +Xpl2Client::setImagingPort (quint16 port) +{ + if (m_imagingPort == port) return; - } - emit statusMessage( - QStringLiteral("Connecting to %1:%2…").arg(m_host).arg(m_port)); - m_socket.connectToHost(m_host, m_port); + m_imagingPort = port; + emit imagingPortChanged (); } -void Xpl2Client::disconnectFromServer() { - if (!isConnected()) +quint16 +Xpl2Client::statusPort () const +{ + return m_statusPort; +} + +void +Xpl2Client::setStatusPort (quint16 port) +{ + if (m_statusPort == port) return; - m_socket.disconnectFromHost(); + m_statusPort = port; + emit statusPortChanged (); +} + +bool +Xpl2Client::isConnected () const +{ + return m_socket.state () == QAbstractSocket::ConnectedState; +} + +void +Xpl2Client::connectToServer () +{ + if (isConnected ()) + { + emit statusMessage (QStringLiteral ("Already connected")); + return; + } + emit statusMessage (QStringLiteral ("Connecting to %1:%2…") + .arg (m_host) + .arg (m_commandPort)); + m_socket.connectToHost (m_host, m_commandPort); } -void Xpl2Client::sendCommand(const QString &command) { - if (!isConnected()) { - emit errorOccurred(QStringLiteral("Not connected")); +void +Xpl2Client::disconnectFromServer () +{ + if (!isConnected ()) return; - } - QByteArray data = command.toUtf8() + '\n'; - m_socket.write(data); - emit statusMessage(QStringLiteral("Sent: %1").arg(command)); + m_socket.disconnectFromHost (); +} + +void +Xpl2Client::sendCommand (const QString &command) +{ + if (!isConnected ()) + { + emit errorOccurred (QStringLiteral ("Not connected")); + return; + } + QByteArray data = command.toUtf8 () + '\n'; + m_socket.write (data); + emit statusMessage (QStringLiteral ("Sent: %1").arg (command)); } -void Xpl2Client::onConnected() { - emit connectedChanged(); - emit statusMessage(QStringLiteral("Connected to %1:%2") - .arg(m_socket.peerName()) - .arg(m_socket.peerPort())); +void +Xpl2Client::onConnected () +{ + emit connectedChanged (); } -void Xpl2Client::onDisconnected() { - emit connectedChanged(); - emit statusMessage(QStringLiteral("Disconnected")); +void +Xpl2Client::onDisconnected () +{ + emit connectedChanged (); } -void Xpl2Client::onReadyRead() { - while (m_socket.canReadLine()) { - QString line = QString::fromUtf8(m_socket.readLine()).trimmed(); - emit responseReceived(line); - } +void +Xpl2Client::onReadyRead () +{ + while (m_socket.canReadLine ()) + { + QString line = QString::fromUtf8 (m_socket.readLine ()).trimmed (); + emit responseReceived (line); + } } -void Xpl2Client::onErrorOccurred(QAbstractSocket::SocketError error) { - Q_UNUSED(error) - emit errorOccurred(m_socket.errorString()); +void +Xpl2Client::onErrorOccurred (QAbstractSocket::SocketError error) +{ + Q_UNUSED (error) + emit errorOccurred (m_socket.errorString ()); } diff --git a/src/Xpl2Client.h b/src/Xpl2Client.h index f052962..d339bc3 100644 --- a/src/Xpl2Client.h +++ b/src/Xpl2Client.h @@ -8,46 +8,62 @@ #include #include -class Xpl2Client : public QObject { +class Xpl2Client : public QObject +{ Q_OBJECT QML_ELEMENT QML_SINGLETON - Q_PROPERTY(QString host READ host WRITE setHost NOTIFY hostChanged) - Q_PROPERTY(quint16 port READ port WRITE setPort NOTIFY portChanged) - Q_PROPERTY(bool connected READ isConnected NOTIFY connectedChanged) + Q_PROPERTY (QString host READ host WRITE setHost NOTIFY hostChanged) + Q_PROPERTY (quint16 commandPort READ commandPort WRITE setCommandPort NOTIFY + commandPortChanged) + Q_PROPERTY (quint16 imagingPort READ imagingPort WRITE setImagingPort NOTIFY + imagingPortChanged) + Q_PROPERTY (quint16 statusPort READ statusPort WRITE setStatusPort NOTIFY + statusPortChanged) + Q_PROPERTY (bool connected READ isConnected NOTIFY connectedChanged) public: - explicit Xpl2Client(QObject *parent = nullptr); + explicit Xpl2Client (QObject *parent = nullptr); - QString host() const; - void setHost(const QString &host); + QString host () const; + void setHost (const QString &host); - quint16 port() const; - void setPort(quint16 port); + quint16 commandPort () const; + void setCommandPort (quint16 port); - bool isConnected() const; + quint16 imagingPort () const; + void setImagingPort (quint16 port); - Q_INVOKABLE void connectToServer(); - Q_INVOKABLE void disconnectFromServer(); - Q_INVOKABLE void sendCommand(const QString &command); + quint16 statusPort () const; + void setStatusPort (quint16 port); + + bool isConnected () const; + + Q_INVOKABLE void connectToServer (); + Q_INVOKABLE void disconnectFromServer (); + Q_INVOKABLE void sendCommand (const QString &command); signals: - void hostChanged(); - void portChanged(); - void connectedChanged(); - void responseReceived(const QString &response); - void errorOccurred(const QString &error); - void statusMessage(const QString &message); + void hostChanged (); + void commandPortChanged (); + void imagingPortChanged (); + void statusPortChanged (); + void connectedChanged (); + void responseReceived (const QString &response); + void errorOccurred (const QString &error); + void statusMessage (const QString &message); private slots: - void onConnected(); - void onDisconnected(); - void onReadyRead(); - void onErrorOccurred(QAbstractSocket::SocketError error); + void onConnected (); + void onDisconnected (); + void onReadyRead (); + void onErrorOccurred (QAbstractSocket::SocketError error); private: QTcpSocket m_socket; - QString m_host = QStringLiteral("127.0.0.1"); - quint16 m_port = 5000; + QString m_host = QStringLiteral ("127.0.0.1"); + quint16 m_commandPort = 9110; + quint16 m_imagingPort = 9111; + quint16 m_statusPort = 9112; }; -- cgit v1.2.3