diff options
| author | Thomas Vanbesien <tvanbesi@proton.me> | 2026-03-11 11:02:24 +0100 |
|---|---|---|
| committer | Thomas Vanbesien <tvanbesi@proton.me> | 2026-03-11 11:09:40 +0100 |
| commit | 35309337aacac2eac7d7215e526bddcf7647b8cf (patch) | |
| tree | 3626e5e5e17e23122deab1bf3ade0502872873f3 | |
| download | QtXpl2-35309337aacac2eac7d7215e526bddcf7647b8cf.tar.gz QtXpl2-35309337aacac2eac7d7215e526bddcf7647b8cf.zip | |
Initial project setup: Qt 6.10.2 TCP client library for Alchemie XPL2 protocol
- Static library (src/) with QML singleton Xpl2Client for TCP communication
- Qt Quick demo app (demo/) with connection UI, command input, and response log
| -rw-r--r-- | .clang-format | 1 | ||||
| -rw-r--r-- | .gitignore | 8 | ||||
| -rw-r--r-- | .qmlformat.ini | 10 | ||||
| -rw-r--r-- | CMakeLists.txt | 30 | ||||
| -rw-r--r-- | demo/CMakeLists.txt | 15 | ||||
| -rw-r--r-- | demo/Main.qml | 177 | ||||
| -rw-r--r-- | demo/main.cpp | 23 | ||||
| -rw-r--r-- | docs/protocol.pdf | bin | 0 -> 493446 bytes | |||
| -rw-r--r-- | src/CMakeLists.txt | 17 | ||||
| -rw-r--r-- | src/Xpl2Client.cpp | 86 | ||||
| -rw-r--r-- | src/Xpl2Client.h | 53 |
11 files changed, 420 insertions, 0 deletions
diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..a6cc54a --- /dev/null +++ b/.clang-format @@ -0,0 +1 @@ +BasedOnStyle: GNU diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1a33617 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +# IDE / editor +.qmlls.ini +.qtcreator +.cache + +# Build output +build +compile_commands.json diff --git a/.qmlformat.ini b/.qmlformat.ini new file mode 100644 index 0000000..f32994e --- /dev/null +++ b/.qmlformat.ini @@ -0,0 +1,10 @@ +[General] +IndentWidth=4 +MaxColumnWidth=80 +NewlineType=unix +NormalizeOrder=true +ObjectsSpacing=true +FunctionsSpacing=true +SemicolonRule=always +SortImports=false +UseTabs=false diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..bcb1f62 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,30 @@ +cmake_minimum_required(VERSION 3.16) +project(QtXpl2 LANGUAGES CXX) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) + +find_package(Qt6 6.10.2 REQUIRED COMPONENTS Core Network Qml Quick) +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) + +# Generate .qmlls.ini for QML language server +set(QT_QML_GENERATE_QMLLS_INI + ON + CACHE BOOL "") + +# Let the demo executable find the library at run time. +set(CMAKE_BUILD_RPATH "${PROJECT_BINARY_DIR}/qml/Xpl2") + +add_subdirectory(src) + +if(PROJECT_IS_TOP_LEVEL) + add_subdirectory(demo) +endif() diff --git a/demo/CMakeLists.txt b/demo/CMakeLists.txt new file mode 100644 index 0000000..f85639b --- /dev/null +++ b/demo/CMakeLists.txt @@ -0,0 +1,15 @@ +qt_add_executable(QtXpl2Demo main.cpp) + +qt_add_qml_module( + QtXpl2Demo + URI + QtXpl2Demo + VERSION + 1.0 + QML_FILES + Main.qml) + +set_target_properties(QtXpl2Demo PROPERTIES RUNTIME_OUTPUT_DIRECTORY + "${PROJECT_BINARY_DIR}/bin") + +target_link_libraries(QtXpl2Demo PRIVATE Qt6::Quick QtXpl2plugin) diff --git a/demo/Main.qml b/demo/Main.qml new file mode 100644 index 0000000..82b2573 --- /dev/null +++ b/demo/Main.qml @@ -0,0 +1,177 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Xpl2 + +ApplicationWindow { + id: root + + width: 800 + height: 600 + visible: true + title: "XPL2 Demo" + + Connections { + target: Xpl2Client + + function onConnectedChanged() { + log(Xpl2Client.connected ? "Connected" : "Disconnected"); + } + + function onResponseReceived(response: string) { + log("← " + response); + responseModel.append({ + text: response + }); + } + + function onErrorOccurred(error: string) { + log("ERROR: " + error); + } + + function onStatusMessage(message: string) { + log(message); + } + } + + function log(msg: string) { + let ts = new Date().toLocaleTimeString(Qt.locale(), "HH:mm:ss.zzz"); + logModel.append({ + text: "[" + ts + "] " + msg + }); + logView.positionViewAtEnd(); + } + + ColumnLayout { + anchors.fill: parent + anchors.margins: 16 + spacing: 12 + + // --- Connection --- + GroupBox { + title: "Connection" + Layout.fillWidth: true + + GridLayout { + columns: 4 + anchors.fill: parent + + Label { + text: "Host:" + } + TextField { + id: hostField + text: Xpl2Client.host + Layout.fillWidth: true + onEditingFinished: Xpl2Client.host = text + } + + Label { + text: "Port:" + } + TextField { + id: portField + text: Xpl2Client.port + implicitWidth: 80 + validator: IntValidator { + bottom: 1 + top: 65535 + } + onEditingFinished: Xpl2Client.port = parseInt(text) + } + + Button { + text: Xpl2Client.connected ? "Disconnect" : "Connect" + Layout.columnSpan: 4 + Layout.alignment: Qt.AlignRight + onClicked: { + if (Xpl2Client.connected) + Xpl2Client.disconnectFromServer(); + else + Xpl2Client.connectToServer(); + } + } + } + } + + // --- Send Command --- + GroupBox { + title: "Command" + Layout.fillWidth: true + enabled: Xpl2Client.connected + + RowLayout { + anchors.fill: parent + + TextField { + id: cmdField + placeholderText: "Enter command…" + Layout.fillWidth: true + onAccepted: sendBtn.clicked() + } + + 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 + + ListView { + id: responseView + anchors.fill: parent + clip: true + model: ListModel { + id: responseModel + } + + delegate: Text { + required property string text + width: responseView.width + wrapMode: Text.Wrap + font.family: "monospace" + font.pixelSize: 13 + text: model.text + } + } + } + + // --- Debug Log --- + GroupBox { + title: "Log" + Layout.fillWidth: true + Layout.preferredHeight: 160 + + ListView { + id: logView + anchors.fill: parent + clip: true + model: ListModel { + id: logModel + } + + delegate: Text { + required property string text + width: logView.width + wrapMode: Text.Wrap + font.family: "monospace" + font.pixelSize: 12 + color: "#555" + text: model.text + } + } + } + } +} diff --git a/demo/main.cpp b/demo/main.cpp new file mode 100644 index 0000000..64c4f22 --- /dev/null +++ b/demo/main.cpp @@ -0,0 +1,23 @@ +/** + * @file main.cpp + * @brief Entry point for the QtXpl2 demo application. + */ +#include <QGuiApplication> +#include <QQmlApplicationEngine> + +#include <QtQml/QQmlExtensionPlugin> +Q_IMPORT_QML_PLUGIN (Xpl2Plugin) + +int +main (int argc, char *argv[]) +{ + QGuiApplication app (argc, argv); + + QQmlApplicationEngine engine; + QObject::connect ( + &engine, &QQmlApplicationEngine::objectCreationFailed, &app, + [] () { QCoreApplication::exit (1); }, Qt::QueuedConnection); + + engine.loadFromModule ("QtXpl2Demo", "Main"); + return app.exec (); +} diff --git a/docs/protocol.pdf b/docs/protocol.pdf Binary files differnew file mode 100644 index 0000000..0bf0bb4 --- /dev/null +++ b/docs/protocol.pdf diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt new file mode 100644 index 0000000..3339013 --- /dev/null +++ b/src/CMakeLists.txt @@ -0,0 +1,17 @@ +qt_add_library(QtXpl2 STATIC) + +qt_add_qml_module( + QtXpl2 + URI + Xpl2 + VERSION + 1.0 + SOURCES + Xpl2Client.h + Xpl2Client.cpp + OUTPUT_DIRECTORY + "${PROJECT_BINARY_DIR}/qml/Xpl2") + +target_compile_features(QtXpl2 PRIVATE cxx_std_17) + +target_link_libraries(QtXpl2 PRIVATE Qt6::Core Qt6::Network Qt6::Quick) diff --git a/src/Xpl2Client.cpp b/src/Xpl2Client.cpp new file mode 100644 index 0000000..db53792 --- /dev/null +++ b/src/Xpl2Client.cpp @@ -0,0 +1,86 @@ +/** + * @file Xpl2Client.cpp + * @brief TCP client for the Alchemie XPL2 printhead protocol. + */ +#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); +} + +QString Xpl2Client::host() const { return m_host; } + +void Xpl2Client::setHost(const QString &host) { + if (m_host == host) + return; + m_host = host; + emit hostChanged(); +} + +quint16 Xpl2Client::port() const { return m_port; } + +void Xpl2Client::setPort(quint16 port) { + if (m_port == port) + return; + m_port = port; + emit portChanged(); +} + +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_port)); + m_socket.connectToHost(m_host, m_port); +} + +void Xpl2Client::disconnectFromServer() { + if (!isConnected()) + return; + 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::onDisconnected() { + emit connectedChanged(); + emit statusMessage(QStringLiteral("Disconnected")); +} + +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()); +} diff --git a/src/Xpl2Client.h b/src/Xpl2Client.h new file mode 100644 index 0000000..f052962 --- /dev/null +++ b/src/Xpl2Client.h @@ -0,0 +1,53 @@ +/** + * @file Xpl2Client.h + * @brief TCP client for the Alchemie XPL2 printhead protocol. + */ +#pragma once + +#include <QObject> +#include <QQmlEngine> +#include <QTcpSocket> + +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) + +public: + explicit Xpl2Client(QObject *parent = nullptr); + + QString host() const; + void setHost(const QString &host); + + quint16 port() const; + void setPort(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); + +private slots: + 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; +}; |
