aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.clang-format1
-rw-r--r--.gitignore8
-rw-r--r--.qmlformat.ini10
-rw-r--r--CMakeLists.txt30
-rw-r--r--demo/CMakeLists.txt15
-rw-r--r--demo/Main.qml177
-rw-r--r--demo/main.cpp23
-rw-r--r--docs/protocol.pdfbin0 -> 493446 bytes
-rw-r--r--src/CMakeLists.txt17
-rw-r--r--src/Xpl2Client.cpp86
-rw-r--r--src/Xpl2Client.h53
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
new file mode 100644
index 0000000..0bf0bb4
--- /dev/null
+++ b/docs/protocol.pdf
Binary files differ
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;
+};