From b137a11c4b6f39b4ae6233ebe20c4c989c982d6f Mon Sep 17 00:00:00 2001 From: openhands Date: Mon, 30 Jun 2025 13:57:13 +0000 Subject: [PATCH 01/28] Initial commit with basic EIS Multiplexer Qt application --- CMakeLists.txt | 30 +++++++++++++++++++ channelwidget.cpp | 49 ++++++++++++++++++++++++++++++ channelwidget.h | 35 ++++++++++++++++++++++ main.cpp | 11 +++++++ mainwindow.cpp | 76 +++++++++++++++++++++++++++++++++++++++++++++++ mainwindow.h | 39 ++++++++++++++++++++++++ 6 files changed, 240 insertions(+) create mode 100644 CMakeLists.txt create mode 100644 channelwidget.cpp create mode 100644 channelwidget.h create mode 100644 main.cpp create mode 100644 mainwindow.cpp create mode 100644 mainwindow.h diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..05adc82 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,30 @@ + +cmake_minimum_required(VERSION 3.14) + +# Set the project name +project(eismultiplexer-qt) + +# Set C++ standard +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +# Find Qt6 +find_package(Qt6 REQUIRED COMPONENTS Widgets) + +# Add the libeismultiplexer library +add_subdirectory(../libeismultiplexer) + +# Add the application executable +add_executable(eismultiplexer-qt + main.cpp + mainwindow.cpp + mainwindow.h + channelwidget.cpp + channelwidget.h +) + +# Link Qt Widgets +target_link_libraries(eismultiplexer-qt Qt6::Widgets eismultiplexer) + +# Include directories +target_include_directories(eismultiplexer-qt PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) diff --git a/channelwidget.cpp b/channelwidget.cpp new file mode 100644 index 0000000..ce2366a --- /dev/null +++ b/channelwidget.cpp @@ -0,0 +1,49 @@ + + + +#include "channelwidget.h" +#include + +ChannelWidget::ChannelWidget(uint16_t deviceSerial, uint16_t channelNumber, struct eismultiplexer* multiplexer, + QWidget *parent) + : QWidget(parent), deviceSerial(deviceSerial), channelNumber(channelNumber), multiplexer(multiplexer) +{ + // Create layout + QHBoxLayout* layout = new QHBoxLayout(this); + + // Create label with device serial and channel number + label = new QLabel(QString("Device %1, Channel %2").arg(deviceSerial).arg(channelNumber), this); + layout->addWidget(label); + + // Create checkbox + checkbox = new QCheckBox(this); + layout->addWidget(checkbox); + + // Connect checkbox signal + connect(checkbox, &QCheckBox::toggled, this, &ChannelWidget::onChannelToggled); + + // Set layout + setLayout(layout); +} + +ChannelWidget::~ChannelWidget() +{ + // Nothing to clean up +} + +void ChannelWidget::onChannelToggled(bool checked) +{ + channel_t channelFlag = static_cast(1 << channelNumber); + if (checked) { + if (eismultiplexer_connect_channel(multiplexer, channelFlag) < 0) { + qWarning() << "Failed to connect channel" << channelNumber << "on device" << deviceSerial; + checkbox->setChecked(false); + } + } else { + if (eismultiplexer_disconnect_channel(multiplexer, channelFlag) < 0) { + qWarning() << "Failed to disconnect channel" << channelNumber << "on device" << deviceSerial; + checkbox->setChecked(true); + } + } +} + diff --git a/channelwidget.h b/channelwidget.h new file mode 100644 index 0000000..5a2a6b7 --- /dev/null +++ b/channelwidget.h @@ -0,0 +1,35 @@ + + + +#ifndef CHANNELWIDGET_H +#define CHANNELWIDGET_H + +#include +#include +#include +#include +#include + +class ChannelWidget : public QWidget +{ + Q_OBJECT + +public: + ChannelWidget(uint16_t deviceSerial, uint16_t channelNumber, struct eismultiplexer* multiplexer, + QWidget *parent = nullptr); + ~ChannelWidget(); + +private slots: + void onChannelToggled(bool checked); + +private: + uint16_t deviceSerial; + uint16_t channelNumber; + struct eismultiplexer* multiplexer; + QCheckBox* checkbox; + QLabel* label; +}; + +#endif // CHANNELWIDGET_H + + diff --git a/main.cpp b/main.cpp new file mode 100644 index 0000000..6bbb0be --- /dev/null +++ b/main.cpp @@ -0,0 +1,11 @@ + +#include +#include "mainwindow.h" + +int main(int argc, char *argv[]) +{ + QApplication app(argc, argv); + MainWindow window; + window.show(); + return app.exec(); +} diff --git a/mainwindow.cpp b/mainwindow.cpp new file mode 100644 index 0000000..a6da879 --- /dev/null +++ b/mainwindow.cpp @@ -0,0 +1,76 @@ + + +#include "mainwindow.h" +#include +#include + +MainWindow::MainWindow(QWidget *parent) + : QMainWindow(parent) +{ + setupUi(); + enumerateDevices(); +} + +MainWindow::~MainWindow() +{ + // Clean up all channel widgets + for (auto widget : channelWidgets) { + delete widget; + } +} + +void MainWindow::setupUi() +{ + // Create central widget and main layout + centralWidget = new QWidget(this); + mainLayout = new QVBoxLayout(centralWidget); + + // Create scroll area + scrollArea = new QScrollArea(this); + scrollContent = new QWidget(); + scrollLayout = new QVBoxLayout(scrollContent); + + // Set up scroll area properties + scrollArea->setWidget(scrollContent); + scrollArea->setWidgetResizable(true); + scrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + + // Add scroll area to main layout + mainLayout->addWidget(scrollArea); + + // Set central widget + setCentralWidget(centralWidget); + setWindowTitle("EIS Multiplexer Controller"); +} + +void MainWindow::enumerateDevices() +{ + size_t count = 0; + uint16_t* serials = eismultiplexer_list_available_devices(&count); + + if (!serials) { + qWarning() << "No EIS multiplexer devices found"; + return; + } + + for (size_t i = 0; i < count; i++) { + uint16_t serial = serials[i]; + struct eismultiplexer multiplexer; + if (eismultiplexer_connect(&multiplexer, serial) == 0) { + uint16_t channelCount = 0; + if (eismultiplexer_get_channel_count(&multiplexer, &channelCount) == 0) { + for (uint16_t channel = 0; channel < channelCount; channel++) { + ChannelWidget* widget = new ChannelWidget(serial, channel, &multiplexer); + channelWidgets.push_back(widget); + scrollLayout->addWidget(widget); + } + } + eismultiplexer_disconnect(&multiplexer); + } else { + qWarning() << "Failed to connect to device with serial" << serial; + } + } + + free(serials); +} + diff --git a/mainwindow.h b/mainwindow.h new file mode 100644 index 0000000..519cb37 --- /dev/null +++ b/mainwindow.h @@ -0,0 +1,39 @@ + + +#ifndef MAINWINDOW_H +#define MAINWINDOW_H + +#include +#include +#include +#include +#include +#include "channelwidget.h" + +QT_BEGIN_NAMESPACE +namespace Ui { class MainWindow; } +QT_END_NAMESPACE + +class MainWindow : public QMainWindow +{ + Q_OBJECT + +public: + MainWindow(QWidget *parent = nullptr); + ~MainWindow(); + +private: + void enumerateDevices(); + void setupUi(); + + QWidget *centralWidget; + QVBoxLayout *mainLayout; + QScrollArea *scrollArea; + QWidget *scrollContent; + QVBoxLayout *scrollLayout; + + std::vector channelWidgets; +}; + +#endif // MAINWINDOW_H + From 7b6e49f770a1327c0d70b29783fe6042eddbd3c6 Mon Sep 17 00:00:00 2001 From: openhands Date: Mon, 30 Jun 2025 14:05:15 +0000 Subject: [PATCH 02/28] Fixed vtable issues and enabled automoc in CMakeLists.txt --- CMakeLists.txt | 8 ++++++-- channelwidget.h | 6 +++--- mainwindow.h | 4 ++-- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 05adc82..8224829 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -11,8 +11,12 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON) # Find Qt6 find_package(Qt6 REQUIRED COMPONENTS Widgets) -# Add the libeismultiplexer library -add_subdirectory(../libeismultiplexer) +# Enable automoc for Qt meta-object compiler +set(CMAKE_AUTOMOC ON) + +# Include the libeismultiplexer library +include_directories(/workspace/libeismultiplexer) +link_directories(/workspace/libeismultiplexer/build) # Add the application executable add_executable(eismultiplexer-qt diff --git a/channelwidget.h b/channelwidget.h index 5a2a6b7..b3c5191 100644 --- a/channelwidget.h +++ b/channelwidget.h @@ -15,9 +15,9 @@ class ChannelWidget : public QWidget Q_OBJECT public: - ChannelWidget(uint16_t deviceSerial, uint16_t channelNumber, struct eismultiplexer* multiplexer, - QWidget *parent = nullptr); - ~ChannelWidget(); + explicit ChannelWidget(uint16_t deviceSerial, uint16_t channelNumber, struct eismultiplexer* multiplexer, + QWidget *parent = nullptr); + ~ChannelWidget() override; private slots: void onChannelToggled(bool checked); diff --git a/mainwindow.h b/mainwindow.h index 519cb37..c4d297b 100644 --- a/mainwindow.h +++ b/mainwindow.h @@ -19,8 +19,8 @@ class MainWindow : public QMainWindow Q_OBJECT public: - MainWindow(QWidget *parent = nullptr); - ~MainWindow(); + explicit MainWindow(QWidget *parent = nullptr); + ~MainWindow() override; private: void enumerateDevices(); From b2bd53be80f19a0c81e8e222aed487968f46f9a1 Mon Sep 17 00:00:00 2001 From: openhands Date: Mon, 30 Jun 2025 14:05:45 +0000 Subject: [PATCH 03/28] Add .gitignore file to exclude build directory --- .gitignore | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1e931b8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,14 @@ + +# Build directory +build/ + +# Qt creator user files +*.user +*.pro.user +*.opensdf +*.sdf + +# CMake cache +CMakeCache.txt +CMakeFiles/ +cmake_install.cmake From 64e01ea5997735c245b380656f0556e68b899c89 Mon Sep 17 00:00:00 2001 From: openhands Date: Mon, 30 Jun 2025 14:19:38 +0000 Subject: [PATCH 04/28] Added QMessageBox notifications for device connection errors and channel control errors --- channelwidget.cpp | 7 +++++++ channelwidget.h | 2 +- mainwindow.cpp | 7 ++++++- mainwindow.h | 2 ++ 4 files changed, 16 insertions(+), 2 deletions(-) diff --git a/channelwidget.cpp b/channelwidget.cpp index ce2366a..5615280 100644 --- a/channelwidget.cpp +++ b/channelwidget.cpp @@ -3,6 +3,7 @@ #include "channelwidget.h" #include +#include ChannelWidget::ChannelWidget(uint16_t deviceSerial, uint16_t channelNumber, struct eismultiplexer* multiplexer, QWidget *parent) @@ -36,13 +37,19 @@ void ChannelWidget::onChannelToggled(bool checked) channel_t channelFlag = static_cast(1 << channelNumber); if (checked) { if (eismultiplexer_connect_channel(multiplexer, channelFlag) < 0) { + QMessageBox::warning(this, tr("Connection Failed"), + tr("Failed to connect channel %1 on device %2").arg(channelNumber).arg(deviceSerial)); qWarning() << "Failed to connect channel" << channelNumber << "on device" << deviceSerial; checkbox->setChecked(false); + setEnabled(false); // Gray out the widget } } else { if (eismultiplexer_disconnect_channel(multiplexer, channelFlag) < 0) { + QMessageBox::warning(this, tr("Disconnection Failed"), + tr("Failed to disconnect channel %1 on device %2").arg(channelNumber).arg(deviceSerial)); qWarning() << "Failed to disconnect channel" << channelNumber << "on device" << deviceSerial; checkbox->setChecked(true); + setEnabled(false); // Gray out the widget } } } diff --git a/channelwidget.h b/channelwidget.h index b3c5191..02f427d 100644 --- a/channelwidget.h +++ b/channelwidget.h @@ -27,7 +27,7 @@ private: uint16_t channelNumber; struct eismultiplexer* multiplexer; QCheckBox* checkbox; - QLabel* label; + QLabel* label; // No need for tr() function, QObject already provides it }; #endif // CHANNELWIDGET_H diff --git a/mainwindow.cpp b/mainwindow.cpp index a6da879..38a0796 100644 --- a/mainwindow.cpp +++ b/mainwindow.cpp @@ -2,6 +2,7 @@ #include "mainwindow.h" #include +#include #include MainWindow::MainWindow(QWidget *parent) @@ -48,7 +49,9 @@ void MainWindow::enumerateDevices() size_t count = 0; uint16_t* serials = eismultiplexer_list_available_devices(&count); - if (!serials) { + if (!serials || count == 0) { + QMessageBox::warning(this, tr("No Devices Found"), + tr("No EIS multiplexer devices were found. Please connect a device and try again.")); qWarning() << "No EIS multiplexer devices found"; return; } @@ -67,6 +70,8 @@ void MainWindow::enumerateDevices() } eismultiplexer_disconnect(&multiplexer); } else { + QMessageBox::warning(this, tr("Connection Failed"), + tr("Failed to connect to device with serial %1").arg(serial)); qWarning() << "Failed to connect to device with serial" << serial; } } diff --git a/mainwindow.h b/mainwindow.h index c4d297b..999d9fd 100644 --- a/mainwindow.h +++ b/mainwindow.h @@ -22,6 +22,8 @@ public: explicit MainWindow(QWidget *parent = nullptr); ~MainWindow() override; + // No need for tr() function, QObject already provides it + private: void enumerateDevices(); void setupUi(); From 278db0b23fd301586f1a9a75020668db68f8c9bd Mon Sep 17 00:00:00 2001 From: openhands Date: Mon, 30 Jun 2025 16:33:38 +0200 Subject: [PATCH 05/28] remove build dir from git --- channelwidget.cpp | 20 ++++++++++++++++++++ channelwidget.h | 9 ++++++++- mainwindow.h | 3 ++- 3 files changed, 30 insertions(+), 2 deletions(-) diff --git a/channelwidget.cpp b/channelwidget.cpp index 5615280..b351fe6 100644 --- a/channelwidget.cpp +++ b/channelwidget.cpp @@ -32,8 +32,28 @@ ChannelWidget::~ChannelWidget() // Nothing to clean up } +uint16_t ChannelWidget::getDeviceSerial() const +{ + return deviceSerial; +} + +uint16_t ChannelWidget::getChannelNumber() const +{ + return channelNumber; +} + +bool ChannelWidget::isChecked() const +{ + return checkbox->isChecked(); +} + void ChannelWidget::onChannelToggled(bool checked) { + if (checked) { + // Emit signal before actually turning on the channel + emit channelAboutToBeTurnedOn(deviceSerial, channelNumber); + } + channel_t channelFlag = static_cast(1 << channelNumber); if (checked) { if (eismultiplexer_connect_channel(multiplexer, channelFlag) < 0) { diff --git a/channelwidget.h b/channelwidget.h index 02f427d..a1e2dc1 100644 --- a/channelwidget.h +++ b/channelwidget.h @@ -19,6 +19,13 @@ public: QWidget *parent = nullptr); ~ChannelWidget() override; + uint16_t getDeviceSerial() const; + uint16_t getChannelNumber() const; + bool isChecked() const; + +signals: + void channelAboutToBeTurnedOn(uint16_t deviceSerial, uint16_t channelNumber); + private slots: void onChannelToggled(bool checked); @@ -27,7 +34,7 @@ private: uint16_t channelNumber; struct eismultiplexer* multiplexer; QCheckBox* checkbox; - QLabel* label; // No need for tr() function, QObject already provides it + QLabel* label; }; #endif // CHANNELWIDGET_H diff --git a/mainwindow.h b/mainwindow.h index 999d9fd..4426a48 100644 --- a/mainwindow.h +++ b/mainwindow.h @@ -22,7 +22,8 @@ public: explicit MainWindow(QWidget *parent = nullptr); ~MainWindow() override; - // No need for tr() function, QObject already provides it +private slots: + void onChannelAboutToBeTurnedOn(uint16_t deviceSerial, uint16_t channelNumber); private: void enumerateDevices(); From d36d5e563a69d486ae3978efaff5d2572f9d5d9c Mon Sep 17 00:00:00 2001 From: Carl Philipp Klemm Date: Mon, 30 Jun 2025 16:59:48 +0200 Subject: [PATCH 06/28] Fix issues with api handling, use shared_ptr to clean up multiplexer structs --- CMakeLists.txt | 1 + channelwidget.cpp | 6 ++++-- channelwidget.h | 6 ++---- mainwindow.cpp | 18 +++++++++++------- 4 files changed, 18 insertions(+), 13 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 8224829..ed9bb60 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -26,6 +26,7 @@ add_executable(eismultiplexer-qt channelwidget.cpp channelwidget.h ) +target_compile_options(eismultiplexer-qt PUBLIC "-Wall") # Link Qt Widgets target_link_libraries(eismultiplexer-qt Qt6::Widgets eismultiplexer) diff --git a/channelwidget.cpp b/channelwidget.cpp index b351fe6..b5df245 100644 --- a/channelwidget.cpp +++ b/channelwidget.cpp @@ -56,18 +56,20 @@ void ChannelWidget::onChannelToggled(bool checked) channel_t channelFlag = static_cast(1 << channelNumber); if (checked) { - if (eismultiplexer_connect_channel(multiplexer, channelFlag) < 0) { + if (eismultiplexer_connect_channel(multiplexer.get(), channelFlag) < 0) { QMessageBox::warning(this, tr("Connection Failed"), tr("Failed to connect channel %1 on device %2").arg(channelNumber).arg(deviceSerial)); qWarning() << "Failed to connect channel" << channelNumber << "on device" << deviceSerial; + checkbox->blockSignals(true); checkbox->setChecked(false); setEnabled(false); // Gray out the widget } } else { - if (eismultiplexer_disconnect_channel(multiplexer, channelFlag) < 0) { + if (eismultiplexer_disconnect_channel(multiplexer.get(), channelFlag) < 0) { QMessageBox::warning(this, tr("Disconnection Failed"), tr("Failed to disconnect channel %1 on device %2").arg(channelNumber).arg(deviceSerial)); qWarning() << "Failed to disconnect channel" << channelNumber << "on device" << deviceSerial; + checkbox->blockSignals(true); checkbox->setChecked(true); setEnabled(false); // Gray out the widget } diff --git a/channelwidget.h b/channelwidget.h index a1e2dc1..0c10019 100644 --- a/channelwidget.h +++ b/channelwidget.h @@ -1,6 +1,3 @@ - - - #ifndef CHANNELWIDGET_H #define CHANNELWIDGET_H @@ -9,6 +6,7 @@ #include #include #include +#include class ChannelWidget : public QWidget { @@ -32,7 +30,7 @@ private slots: private: uint16_t deviceSerial; uint16_t channelNumber; - struct eismultiplexer* multiplexer; + std::shared_ptr multiplexer; QCheckBox* checkbox; QLabel* label; }; diff --git a/mainwindow.cpp b/mainwindow.cpp index 38a0796..6a97402 100644 --- a/mainwindow.cpp +++ b/mainwindow.cpp @@ -1,9 +1,8 @@ - - #include "mainwindow.h" #include #include #include +#include MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) @@ -44,6 +43,9 @@ void MainWindow::setupUi() setWindowTitle("EIS Multiplexer Controller"); } +void MainWindow::onChannelAboutToBeTurnedOn(uint16_t deviceSerial, uint16_t channelNumber) +{} + void MainWindow::enumerateDevices() { size_t count = 0; @@ -53,22 +55,24 @@ void MainWindow::enumerateDevices() QMessageBox::warning(this, tr("No Devices Found"), tr("No EIS multiplexer devices were found. Please connect a device and try again.")); qWarning() << "No EIS multiplexer devices found"; + close(); return; } for (size_t i = 0; i < count; i++) { uint16_t serial = serials[i]; - struct eismultiplexer multiplexer; - if (eismultiplexer_connect(&multiplexer, serial) == 0) { + std::shared_ptr multiplexer(new struct eismultiplexer); + if (eismultiplexer_connect(multiplexer.get(), serial) >= 0) { uint16_t channelCount = 0; - if (eismultiplexer_get_channel_count(&multiplexer, &channelCount) == 0) { + qDebug()<<"Adding channels from device "<= 0) { for (uint16_t channel = 0; channel < channelCount; channel++) { - ChannelWidget* widget = new ChannelWidget(serial, channel, &multiplexer); + ChannelWidget* widget = new ChannelWidget(serial, channel, multiplexer.get()); + qDebug()<<"Added widget from device "<addWidget(widget); } } - eismultiplexer_disconnect(&multiplexer); } else { QMessageBox::warning(this, tr("Connection Failed"), tr("Failed to connect to device with serial %1").arg(serial)); From 9e0d7a5d9d3fd01c01a4ae2802fbf88b788d52fa Mon Sep 17 00:00:00 2001 From: Carl Philipp Klemm Date: Thu, 3 Jul 2025 11:55:32 +0200 Subject: [PATCH 07/28] Use desinger form for main window, add ganged combo box --- CMakeLists.txt | 7 ++--- channelwidget.cpp | 56 +++++++++++++++++++++++--------------- channelwidget.h | 14 +++++++--- main.cpp | 1 - mainwindow.cpp | 53 +++++++++--------------------------- mainwindow.h | 34 ++++++++---------------- mainwindow.ui | 68 +++++++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 141 insertions(+), 92 deletions(-) create mode 100644 mainwindow.ui diff --git a/CMakeLists.txt b/CMakeLists.txt index ed9bb60..d4a23ba 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -11,8 +11,8 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON) # Find Qt6 find_package(Qt6 REQUIRED COMPONENTS Widgets) -# Enable automoc for Qt meta-object compiler set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTOUIC ON) # Include the libeismultiplexer library include_directories(/workspace/libeismultiplexer) @@ -21,10 +21,11 @@ link_directories(/workspace/libeismultiplexer/build) # Add the application executable add_executable(eismultiplexer-qt main.cpp - mainwindow.cpp - mainwindow.h channelwidget.cpp channelwidget.h + mainwindow.h + mainwindow.cpp + mainwindow.ui ) target_compile_options(eismultiplexer-qt PUBLIC "-Wall") diff --git a/channelwidget.cpp b/channelwidget.cpp index b5df245..53c9140 100644 --- a/channelwidget.cpp +++ b/channelwidget.cpp @@ -1,30 +1,42 @@ - - - #include "channelwidget.h" #include #include -ChannelWidget::ChannelWidget(uint16_t deviceSerial, uint16_t channelNumber, struct eismultiplexer* multiplexer, +ChannelWidget::ChannelWidget(uint16_t deviceSerial, uint16_t channelNumber, std::shared_ptr multiplexer, QWidget *parent) - : QWidget(parent), deviceSerial(deviceSerial), channelNumber(channelNumber), multiplexer(multiplexer) + : + QWidget(parent), + deviceSerial(deviceSerial), + channelNumber(channelNumber), + multiplexer(multiplexer), + checkbox("Enable"), + devicelabel(QString::asprintf("Device %04u", deviceSerial)), + channellabel(QString::asprintf("Channel %u", channelNumber)), + ganglabel("Ganged:") { - // Create layout - QHBoxLayout* layout = new QHBoxLayout(this); + hlayout.addLayout(&labellayout); + vlayout.addLayout(&hlayout); - // Create label with device serial and channel number - label = new QLabel(QString("Device %1, Channel %2").arg(deviceSerial).arg(channelNumber), this); - layout->addWidget(label); + labellayout.addWidget(&devicelabel); + labellayout.addWidget(&channellabel); - // Create checkbox - checkbox = new QCheckBox(this); - layout->addWidget(checkbox); + line.setGeometry(QRect(320, 150, 118, 3)); + line.setFrameShape(QFrame::HLine); + line.setFrameShadow(QFrame::Sunken); + vlayout.addWidget(&line); - // Connect checkbox signal - connect(checkbox, &QCheckBox::toggled, this, &ChannelWidget::onChannelToggled); + gangcombo.addItem("Unganged"); - // Set layout - setLayout(layout); + hlayout.addStretch(); + hlayout.addWidget(&ganglabel); + hlayout.addWidget(&gangcombo); + hlayout.addWidget(&checkbox); + connect(&checkbox, &QCheckBox::toggled, this, &ChannelWidget::onChannelToggled); + + setFixedHeight(96); + setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); + + setLayout(&vlayout); } ChannelWidget::~ChannelWidget() @@ -44,7 +56,7 @@ uint16_t ChannelWidget::getChannelNumber() const bool ChannelWidget::isChecked() const { - return checkbox->isChecked(); + return checkbox.isChecked(); } void ChannelWidget::onChannelToggled(bool checked) @@ -60,8 +72,8 @@ void ChannelWidget::onChannelToggled(bool checked) QMessageBox::warning(this, tr("Connection Failed"), tr("Failed to connect channel %1 on device %2").arg(channelNumber).arg(deviceSerial)); qWarning() << "Failed to connect channel" << channelNumber << "on device" << deviceSerial; - checkbox->blockSignals(true); - checkbox->setChecked(false); + checkbox.blockSignals(true); + checkbox.setChecked(false); setEnabled(false); // Gray out the widget } } else { @@ -69,8 +81,8 @@ void ChannelWidget::onChannelToggled(bool checked) QMessageBox::warning(this, tr("Disconnection Failed"), tr("Failed to disconnect channel %1 on device %2").arg(channelNumber).arg(deviceSerial)); qWarning() << "Failed to disconnect channel" << channelNumber << "on device" << deviceSerial; - checkbox->blockSignals(true); - checkbox->setChecked(true); + checkbox.blockSignals(true); + checkbox.setChecked(true); setEnabled(false); // Gray out the widget } } diff --git a/channelwidget.h b/channelwidget.h index 0c10019..a5e5d51 100644 --- a/channelwidget.h +++ b/channelwidget.h @@ -5,6 +5,7 @@ #include #include #include +#include #include #include @@ -13,7 +14,7 @@ class ChannelWidget : public QWidget Q_OBJECT public: - explicit ChannelWidget(uint16_t deviceSerial, uint16_t channelNumber, struct eismultiplexer* multiplexer, + explicit ChannelWidget(uint16_t deviceSerial, uint16_t channelNumber, std::shared_ptr multiplexer, QWidget *parent = nullptr); ~ChannelWidget() override; @@ -31,8 +32,15 @@ private: uint16_t deviceSerial; uint16_t channelNumber; std::shared_ptr multiplexer; - QCheckBox* checkbox; - QLabel* label; + QCheckBox checkbox; + QLabel devicelabel; + QLabel channellabel; + QLabel ganglabel; + QComboBox gangcombo; + QFrame line; + QVBoxLayout vlayout; + QHBoxLayout hlayout; + QVBoxLayout labellayout; }; #endif // CHANNELWIDGET_H diff --git a/main.cpp b/main.cpp index 6bbb0be..d198183 100644 --- a/main.cpp +++ b/main.cpp @@ -1,4 +1,3 @@ - #include #include "mainwindow.h" diff --git a/mainwindow.cpp b/mainwindow.cpp index 6a97402..a736260 100644 --- a/mainwindow.cpp +++ b/mainwindow.cpp @@ -1,51 +1,23 @@ -#include "mainwindow.h" -#include -#include #include -#include +#include +#include "mainwindow.h" +#include "ui_mainwindow.h" MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) + , ui(new Ui::MainWindow) { - setupUi(); + ui->setupUi(this); enumerateDevices(); + + connect(ui->actionQuit, &QAction::triggered, this, [this](){close();}); } MainWindow::~MainWindow() { - // Clean up all channel widgets - for (auto widget : channelWidgets) { - delete widget; - } + delete ui; } -void MainWindow::setupUi() -{ - // Create central widget and main layout - centralWidget = new QWidget(this); - mainLayout = new QVBoxLayout(centralWidget); - - // Create scroll area - scrollArea = new QScrollArea(this); - scrollContent = new QWidget(); - scrollLayout = new QVBoxLayout(scrollContent); - - // Set up scroll area properties - scrollArea->setWidget(scrollContent); - scrollArea->setWidgetResizable(true); - scrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); - - // Add scroll area to main layout - mainLayout->addWidget(scrollArea); - - // Set central widget - setCentralWidget(centralWidget); - setWindowTitle("EIS Multiplexer Controller"); -} - -void MainWindow::onChannelAboutToBeTurnedOn(uint16_t deviceSerial, uint16_t channelNumber) -{} - void MainWindow::enumerateDevices() { size_t count = 0; @@ -67,10 +39,10 @@ void MainWindow::enumerateDevices() qDebug()<<"Adding channels from device "<= 0) { for (uint16_t channel = 0; channel < channelCount; channel++) { - ChannelWidget* widget = new ChannelWidget(serial, channel, multiplexer.get()); + std::shared_ptr widget(new ChannelWidget(serial, channel, multiplexer)); qDebug()<<"Added widget from device "<addWidget(widget); + channels.push_back(widget); + ui->channelLayout->addWidget(widget.get()); } } } else { @@ -78,8 +50,9 @@ void MainWindow::enumerateDevices() tr("Failed to connect to device with serial %1").arg(serial)); qWarning() << "Failed to connect to device with serial" << serial; } + ui->channelLayout->addStretch(); } + ui->statusbar->showMessage("Ready"); free(serials); } - diff --git a/mainwindow.h b/mainwindow.h index 4426a48..166b03a 100644 --- a/mainwindow.h +++ b/mainwindow.h @@ -1,42 +1,30 @@ - - #ifndef MAINWINDOW_H #define MAINWINDOW_H #include -#include -#include -#include -#include +#include + #include "channelwidget.h" -QT_BEGIN_NAMESPACE -namespace Ui { class MainWindow; } -QT_END_NAMESPACE +namespace Ui { +class MainWindow; +} class MainWindow : public QMainWindow { Q_OBJECT + std::vector> channels; + Ui::MainWindow *ui; + +signals: + void channelStateChanged(uint16_t device, uint16_t channel); public: explicit MainWindow(QWidget *parent = nullptr); - ~MainWindow() override; - -private slots: - void onChannelAboutToBeTurnedOn(uint16_t deviceSerial, uint16_t channelNumber); + ~MainWindow(); private: void enumerateDevices(); - void setupUi(); - - QWidget *centralWidget; - QVBoxLayout *mainLayout; - QScrollArea *scrollArea; - QWidget *scrollContent; - QVBoxLayout *scrollLayout; - - std::vector channelWidgets; }; #endif // MAINWINDOW_H - diff --git a/mainwindow.ui b/mainwindow.ui new file mode 100644 index 0000000..53aca8f --- /dev/null +++ b/mainwindow.ui @@ -0,0 +1,68 @@ + + + MainWindow + + + + 0 + 0 + 800 + 600 + + + + EisMultiplexer-Qt + + + + + + + true + + + + + 0 + 0 + 788 + 537 + + + + + + + + + + + + + + + + 0 + 0 + 800 + 29 + + + + + File + + + + + + + + + Quit + + + + + + From 5c5efb50299c6bd47da700168f7fbb7a4a5786be Mon Sep 17 00:00:00 2001 From: Carl Philipp Klemm Date: Mon, 25 Aug 2025 17:00:16 +0200 Subject: [PATCH 08/28] Support windows --- CMakeLists.txt | 50 +++++++++++++------- channelwidget.cpp | 117 ++++++++++++++++++++++++---------------------- channelwidget.h | 43 ++++++++--------- main.cpp | 8 ++-- mainwindow.cpp | 86 +++++++++++++++++++--------------- mainwindow.h | 17 +++---- multiplexer.cpp | 16 +++++++ multiplexer.h | 22 +++++++++ 8 files changed, 215 insertions(+), 144 deletions(-) create mode 100644 multiplexer.cpp create mode 100644 multiplexer.h diff --git a/CMakeLists.txt b/CMakeLists.txt index d4a23ba..e86a2c3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,36 +1,50 @@ - cmake_minimum_required(VERSION 3.14) -# Set the project name -project(eismultiplexer-qt) +project(eismuliplexer-qt) + +set(CMAKE_PROJECT_VERSION_MAJOR 0) +set(CMAKE_PROJECT_VERSION_MINOR 9) +set(CMAKE_PROJECT_VERSION_PATCH 0) + +add_compile_definitions(VERSION_MAJOR=${CMAKE_PROJECT_VERSION_MAJOR}) +add_compile_definitions(VERSION_MINOR=${CMAKE_PROJECT_VERSION_MINOR}) +add_compile_definitions(VERSION_PATCH=${CMAKE_PROJECT_VERSION_PATCH}) + +if(CMAKE_HOST_SYSTEM_NAME MATCHES "Windows") + message(FATAL_ERROR "Windows builds have to be cross compiled on UNIX") +endif() + +message("Platform " ${CMAKE_SYSTEM_NAME}) +if(WIN32) + configure_file(${CMAKE_CURRENT_SOURCE_DIR}/scripts/release-win.sh ${CMAKE_CURRENT_BINARY_DIR}/release.sh @ONLY) + add_custom_target(package + COMMAND ${CMAKE_CURRENT_BINARY_DIR}/release.sh + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} + COMMENT "Createing release archive" + VERBATIM) +endif(WIN32) -# Set C++ standard set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) -# Find Qt6 +find_package(PkgConfig REQUIRED) +pkg_check_modules(EISMULIPLEXER REQUIRED eismuliplexer) find_package(Qt6 REQUIRED COMPONENTS Widgets) +find_package(Qt6 REQUIRED COMPONENTS Core) set(CMAKE_AUTOMOC ON) set(CMAKE_AUTOUIC ON) -# Include the libeismultiplexer library -include_directories(/workspace/libeismultiplexer) -link_directories(/workspace/libeismultiplexer/build) - -# Add the application executable -add_executable(eismultiplexer-qt +add_executable(${PROJECT_NAME} main.cpp channelwidget.cpp channelwidget.h mainwindow.h mainwindow.cpp mainwindow.ui + multiplexer.h + multiplexer.cpp ) -target_compile_options(eismultiplexer-qt PUBLIC "-Wall") - -# Link Qt Widgets -target_link_libraries(eismultiplexer-qt Qt6::Widgets eismultiplexer) - -# Include directories -target_include_directories(eismultiplexer-qt PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) +target_compile_options(${PROJECT_NAME} PUBLIC "-Wall") +target_link_libraries(${PROJECT_NAME} PRIVATE Qt6::Widgets Qt6::Core ${EISMULIPLEXER_LIBRARIES}) +#target_include_directories(${PROJECT_NAME} PUBLIC ${EISMULIPLEXER_INCLUDE_DIRS}) diff --git a/channelwidget.cpp b/channelwidget.cpp index 53c9140..ae259fc 100644 --- a/channelwidget.cpp +++ b/channelwidget.cpp @@ -2,89 +2,96 @@ #include #include -ChannelWidget::ChannelWidget(uint16_t deviceSerial, uint16_t channelNumber, std::shared_ptr multiplexer, +ChannelWidget::ChannelWidget(uint16_t deviceSerial, uint16_t channelNumber, + std::shared_ptr multiplexer, QWidget *parent) - : - QWidget(parent), - deviceSerial(deviceSerial), - channelNumber(channelNumber), - multiplexer(multiplexer), - checkbox("Enable"), - devicelabel(QString::asprintf("Device %04u", deviceSerial)), - channellabel(QString::asprintf("Channel %u", channelNumber)), - ganglabel("Ganged:") + : + QWidget(parent), + deviceSerial(deviceSerial), + channelNumber(channelNumber), + multiplexer(multiplexer), + checkbox("Enable"), + devicelabel(QString::asprintf("Device %04u", deviceSerial)), + channellabel(QString::asprintf("Channel %u", channelNumber)), + ganglabel("Ganged:") { - hlayout.addLayout(&labellayout); - vlayout.addLayout(&hlayout); + hlayout.addLayout(&labellayout); + vlayout.addLayout(&hlayout); - labellayout.addWidget(&devicelabel); - labellayout.addWidget(&channellabel); + labellayout.addWidget(&devicelabel); + labellayout.addWidget(&channellabel); - line.setGeometry(QRect(320, 150, 118, 3)); - line.setFrameShape(QFrame::HLine); - line.setFrameShadow(QFrame::Sunken); - vlayout.addWidget(&line); + line.setGeometry(QRect(320, 150, 118, 3)); + line.setFrameShape(QFrame::HLine); + line.setFrameShadow(QFrame::Sunken); + vlayout.addWidget(&line); - gangcombo.addItem("Unganged"); + gangcombo.addItem("Unganged"); - hlayout.addStretch(); - hlayout.addWidget(&ganglabel); - hlayout.addWidget(&gangcombo); - hlayout.addWidget(&checkbox); - connect(&checkbox, &QCheckBox::toggled, this, &ChannelWidget::onChannelToggled); + hlayout.addStretch(); + hlayout.addWidget(&ganglabel); + hlayout.addWidget(&gangcombo); + hlayout.addWidget(&checkbox); + connect(&checkbox, &QCheckBox::toggled, this, &ChannelWidget::onChannelToggled); - setFixedHeight(96); - setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); + setFixedHeight(96); + setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); - setLayout(&vlayout); + setLayout(&vlayout); } ChannelWidget::~ChannelWidget() { - // Nothing to clean up + // Nothing to clean up } uint16_t ChannelWidget::getDeviceSerial() const { - return deviceSerial; + return deviceSerial; } uint16_t ChannelWidget::getChannelNumber() const { - return channelNumber; + return channelNumber; } bool ChannelWidget::isChecked() const { - return checkbox.isChecked(); + return checkbox.isChecked(); } void ChannelWidget::onChannelToggled(bool checked) { - if (checked) { - // Emit signal before actually turning on the channel - emit channelAboutToBeTurnedOn(deviceSerial, channelNumber); - } + if (checked) + { + // Emit signal before actually turning on the channel + emit channelAboutToBeTurnedOn(deviceSerial, channelNumber); + } - channel_t channelFlag = static_cast(1 << channelNumber); - if (checked) { - if (eismultiplexer_connect_channel(multiplexer.get(), channelFlag) < 0) { - QMessageBox::warning(this, tr("Connection Failed"), - tr("Failed to connect channel %1 on device %2").arg(channelNumber).arg(deviceSerial)); - qWarning() << "Failed to connect channel" << channelNumber << "on device" << deviceSerial; - checkbox.blockSignals(true); - checkbox.setChecked(false); - setEnabled(false); // Gray out the widget - } - } else { - if (eismultiplexer_disconnect_channel(multiplexer.get(), channelFlag) < 0) { - QMessageBox::warning(this, tr("Disconnection Failed"), - tr("Failed to disconnect channel %1 on device %2").arg(channelNumber).arg(deviceSerial)); - qWarning() << "Failed to disconnect channel" << channelNumber << "on device" << deviceSerial; - checkbox.blockSignals(true); - checkbox.setChecked(true); - setEnabled(false); // Gray out the widget - } - } + channel_t channelFlag = static_cast(1 << channelNumber); + if (checked) + { + if (eismultiplexer_connect_channel(multiplexer.get(), channelFlag) < 0) + { + QMessageBox::warning(this, tr("Connection Failed"), + tr("Failed to connect channel %1 on device %2").arg(channelNumber).arg(deviceSerial)); + qWarning() << "Failed to connect channel" << channelNumber << "on device" << deviceSerial; + checkbox.blockSignals(true); + checkbox.setChecked(false); + setEnabled(false); // Gray out the widget + } + } + else + { + if (eismultiplexer_disconnect_channel(multiplexer.get(), channelFlag) < 0) + { + QMessageBox::warning(this, tr("Disconnection Failed"), + tr("Failed to disconnect channel %1 on device %2").arg(channelNumber).arg(deviceSerial)); + qWarning() << "Failed to disconnect channel" << channelNumber << "on device" << deviceSerial; + checkbox.blockSignals(true); + checkbox.setChecked(true); + setEnabled(false); // Gray out the widget + } + } } diff --git a/channelwidget.h b/channelwidget.h index a5e5d51..08762a6 100644 --- a/channelwidget.h +++ b/channelwidget.h @@ -11,36 +11,37 @@ class ChannelWidget : public QWidget { - Q_OBJECT + Q_OBJECT public: - explicit ChannelWidget(uint16_t deviceSerial, uint16_t channelNumber, std::shared_ptr multiplexer, - QWidget *parent = nullptr); - ~ChannelWidget() override; + explicit ChannelWidget(uint16_t deviceSerial, uint16_t channelNumber, + std::shared_ptr multiplexer, + QWidget *parent = nullptr); + ~ChannelWidget() override; - uint16_t getDeviceSerial() const; - uint16_t getChannelNumber() const; - bool isChecked() const; + uint16_t getDeviceSerial() const; + uint16_t getChannelNumber() const; + bool isChecked() const; signals: - void channelAboutToBeTurnedOn(uint16_t deviceSerial, uint16_t channelNumber); + void channelAboutToBeTurnedOn(uint16_t deviceSerial, uint16_t channelNumber); private slots: - void onChannelToggled(bool checked); + void onChannelToggled(bool checked); private: - uint16_t deviceSerial; - uint16_t channelNumber; - std::shared_ptr multiplexer; - QCheckBox checkbox; - QLabel devicelabel; - QLabel channellabel; - QLabel ganglabel; - QComboBox gangcombo; - QFrame line; - QVBoxLayout vlayout; - QHBoxLayout hlayout; - QVBoxLayout labellayout; + uint16_t deviceSerial; + uint16_t channelNumber; + std::shared_ptr multiplexer; + QCheckBox checkbox; + QLabel devicelabel; + QLabel channellabel; + QLabel ganglabel; + QComboBox gangcombo; + QFrame line; + QVBoxLayout vlayout; + QHBoxLayout hlayout; + QVBoxLayout labellayout; }; #endif // CHANNELWIDGET_H diff --git a/main.cpp b/main.cpp index d198183..fb4d5b4 100644 --- a/main.cpp +++ b/main.cpp @@ -3,8 +3,8 @@ int main(int argc, char *argv[]) { - QApplication app(argc, argv); - MainWindow window; - window.show(); - return app.exec(); + QApplication app(argc, argv); + MainWindow window; + window.show(); + return app.exec(); } diff --git a/mainwindow.cpp b/mainwindow.cpp index a736260..99a5739 100644 --- a/mainwindow.cpp +++ b/mainwindow.cpp @@ -4,55 +4,65 @@ #include "ui_mainwindow.h" MainWindow::MainWindow(QWidget *parent) - : QMainWindow(parent) - , ui(new Ui::MainWindow) + : QMainWindow(parent) + , ui(new Ui::MainWindow) { - ui->setupUi(this); - enumerateDevices(); + ui->setupUi(this); + enumerateDevices(); - connect(ui->actionQuit, &QAction::triggered, this, [this](){close();}); + connect(ui->actionQuit, &QAction::triggered, this, [this]() + { + close(); + }); } MainWindow::~MainWindow() { - delete ui; + delete ui; } void MainWindow::enumerateDevices() { - size_t count = 0; - uint16_t* serials = eismultiplexer_list_available_devices(&count); + size_t count = 0; + uint16_t* serials = eismultiplexer_list_available_devices(&count); - if (!serials || count == 0) { - QMessageBox::warning(this, tr("No Devices Found"), - tr("No EIS multiplexer devices were found. Please connect a device and try again.")); - qWarning() << "No EIS multiplexer devices found"; - close(); - return; - } + if (!serials || count == 0) + { + QMessageBox::warning(nullptr, tr("No Devices Found"), + tr("No EIS multiplexer devices were found. Please connect a device and try again.")); + qWarning() << "No EIS multiplexer devices found"; + exit(0); + return; + } - for (size_t i = 0; i < count; i++) { - uint16_t serial = serials[i]; - std::shared_ptr multiplexer(new struct eismultiplexer); - if (eismultiplexer_connect(multiplexer.get(), serial) >= 0) { - uint16_t channelCount = 0; - qDebug()<<"Adding channels from device "<= 0) { - for (uint16_t channel = 0; channel < channelCount; channel++) { - std::shared_ptr widget(new ChannelWidget(serial, channel, multiplexer)); - qDebug()<<"Added widget from device "<channelLayout->addWidget(widget.get()); - } - } - } else { - QMessageBox::warning(this, tr("Connection Failed"), - tr("Failed to connect to device with serial %1").arg(serial)); - qWarning() << "Failed to connect to device with serial" << serial; - } - ui->channelLayout->addStretch(); - } - ui->statusbar->showMessage("Ready"); + for (size_t i = 0; i < count; i++) + { + uint16_t serial = serials[i]; + std::shared_ptr multiplexer(new struct eismultiplexer); + if (eismultiplexer_connect(multiplexer.get(), serial) >= 0) + { + uint16_t channelCount = 0; + qDebug()<<"Adding channels from device "<= 0) + { + for (uint16_t channel = 0; channel < channelCount; channel++) + { + std::shared_ptr widget(new ChannelWidget(serial, channel, multiplexer)); + qDebug()<<"Added widget from device "<channelLayout->addWidget(widget.get()); + } + } + } + else + { + QMessageBox::warning(this, tr("Connection Failed"), + tr("Failed to connect to device with serial %1").arg(serial)); + qWarning() << "Failed to connect to device with serial" << serial; + } + ui->channelLayout->addStretch(); + } + ui->statusbar->showMessage("Ready"); - free(serials); + free(serials); } diff --git a/mainwindow.h b/mainwindow.h index 166b03a..cb1a19c 100644 --- a/mainwindow.h +++ b/mainwindow.h @@ -6,25 +6,26 @@ #include "channelwidget.h" -namespace Ui { +namespace Ui +{ class MainWindow; } class MainWindow : public QMainWindow { - Q_OBJECT - std::vector> channels; - Ui::MainWindow *ui; + Q_OBJECT + std::vector> channels; + Ui::MainWindow *ui; signals: - void channelStateChanged(uint16_t device, uint16_t channel); + void channelStateChanged(uint16_t device, uint16_t channel); public: - explicit MainWindow(QWidget *parent = nullptr); - ~MainWindow(); + explicit MainWindow(QWidget *parent = nullptr); + ~MainWindow(); private: - void enumerateDevices(); + void enumerateDevices(); }; #endif // MAINWINDOW_H diff --git a/multiplexer.cpp b/multiplexer.cpp new file mode 100644 index 0000000..3e1c8e0 --- /dev/null +++ b/multiplexer.cpp @@ -0,0 +1,16 @@ +#include "multiplexer.h" + +Multiplexer::Multiplexer(QObject *parent) + : QObject{parent} +{} + +Multiplexer::~Multiplexer() +{ + for(auto& multiplexer : multiplexers) + eismultiplexer_disconnect(multiplexer.get()); +} + +void Multiplexer::probe() +{ + +} diff --git a/multiplexer.h b/multiplexer.h new file mode 100644 index 0000000..32a04ec --- /dev/null +++ b/multiplexer.h @@ -0,0 +1,22 @@ +#ifndef MULTIPLEXER_H +#define MULTIPLEXER_H + +#include +#include "eismultiplexer.h" + +class Multiplexer : public QObject +{ + Q_OBJECT + std::vector> multiplexers; + std::vector channelStates; + +public: + explicit Multiplexer(QObject *parent = nullptr); + ~Multiplexer(); + void probe(); + +signals: + void foundDevice(std::shared_ptr); +}; + +#endif // MULTIPLEXER_H From e87470d14ebe9800acc5e4d63cfe667caa9efbcc Mon Sep 17 00:00:00 2001 From: Carl Philipp Klemm Date: Mon, 25 Aug 2025 17:01:34 +0200 Subject: [PATCH 09/28] inial commit --- CMakeLists.txt | 50 ++++++++++++++++++++++ channelwidget.cpp | 97 ++++++++++++++++++++++++++++++++++++++++++ channelwidget.h | 49 +++++++++++++++++++++ main.cpp | 10 +++++ mainwindow.cpp | 68 +++++++++++++++++++++++++++++ mainwindow.h | 31 ++++++++++++++ mainwindow.ui | 68 +++++++++++++++++++++++++++++ multiplexer.cpp | 16 +++++++ multiplexer.h | 22 ++++++++++ scripts/release-win.sh | 61 ++++++++++++++++++++++++++ 10 files changed, 472 insertions(+) create mode 100644 CMakeLists.txt create mode 100644 channelwidget.cpp create mode 100644 channelwidget.h create mode 100644 main.cpp create mode 100644 mainwindow.cpp create mode 100644 mainwindow.h create mode 100644 mainwindow.ui create mode 100644 multiplexer.cpp create mode 100644 multiplexer.h create mode 100755 scripts/release-win.sh diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..e86a2c3 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,50 @@ +cmake_minimum_required(VERSION 3.14) + +project(eismuliplexer-qt) + +set(CMAKE_PROJECT_VERSION_MAJOR 0) +set(CMAKE_PROJECT_VERSION_MINOR 9) +set(CMAKE_PROJECT_VERSION_PATCH 0) + +add_compile_definitions(VERSION_MAJOR=${CMAKE_PROJECT_VERSION_MAJOR}) +add_compile_definitions(VERSION_MINOR=${CMAKE_PROJECT_VERSION_MINOR}) +add_compile_definitions(VERSION_PATCH=${CMAKE_PROJECT_VERSION_PATCH}) + +if(CMAKE_HOST_SYSTEM_NAME MATCHES "Windows") + message(FATAL_ERROR "Windows builds have to be cross compiled on UNIX") +endif() + +message("Platform " ${CMAKE_SYSTEM_NAME}) +if(WIN32) + configure_file(${CMAKE_CURRENT_SOURCE_DIR}/scripts/release-win.sh ${CMAKE_CURRENT_BINARY_DIR}/release.sh @ONLY) + add_custom_target(package + COMMAND ${CMAKE_CURRENT_BINARY_DIR}/release.sh + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} + COMMENT "Createing release archive" + VERBATIM) +endif(WIN32) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +find_package(PkgConfig REQUIRED) +pkg_check_modules(EISMULIPLEXER REQUIRED eismuliplexer) +find_package(Qt6 REQUIRED COMPONENTS Widgets) +find_package(Qt6 REQUIRED COMPONENTS Core) + +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTOUIC ON) + +add_executable(${PROJECT_NAME} + main.cpp + channelwidget.cpp + channelwidget.h + mainwindow.h + mainwindow.cpp + mainwindow.ui + multiplexer.h + multiplexer.cpp +) +target_compile_options(${PROJECT_NAME} PUBLIC "-Wall") +target_link_libraries(${PROJECT_NAME} PRIVATE Qt6::Widgets Qt6::Core ${EISMULIPLEXER_LIBRARIES}) +#target_include_directories(${PROJECT_NAME} PUBLIC ${EISMULIPLEXER_INCLUDE_DIRS}) diff --git a/channelwidget.cpp b/channelwidget.cpp new file mode 100644 index 0000000..ae259fc --- /dev/null +++ b/channelwidget.cpp @@ -0,0 +1,97 @@ +#include "channelwidget.h" +#include +#include + +ChannelWidget::ChannelWidget(uint16_t deviceSerial, uint16_t channelNumber, + std::shared_ptr multiplexer, + QWidget *parent) + : + QWidget(parent), + deviceSerial(deviceSerial), + channelNumber(channelNumber), + multiplexer(multiplexer), + checkbox("Enable"), + devicelabel(QString::asprintf("Device %04u", deviceSerial)), + channellabel(QString::asprintf("Channel %u", channelNumber)), + ganglabel("Ganged:") +{ + hlayout.addLayout(&labellayout); + vlayout.addLayout(&hlayout); + + labellayout.addWidget(&devicelabel); + labellayout.addWidget(&channellabel); + + line.setGeometry(QRect(320, 150, 118, 3)); + line.setFrameShape(QFrame::HLine); + line.setFrameShadow(QFrame::Sunken); + vlayout.addWidget(&line); + + gangcombo.addItem("Unganged"); + + hlayout.addStretch(); + hlayout.addWidget(&ganglabel); + hlayout.addWidget(&gangcombo); + hlayout.addWidget(&checkbox); + connect(&checkbox, &QCheckBox::toggled, this, &ChannelWidget::onChannelToggled); + + setFixedHeight(96); + setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); + + setLayout(&vlayout); +} + +ChannelWidget::~ChannelWidget() +{ + // Nothing to clean up +} + +uint16_t ChannelWidget::getDeviceSerial() const +{ + return deviceSerial; +} + +uint16_t ChannelWidget::getChannelNumber() const +{ + return channelNumber; +} + +bool ChannelWidget::isChecked() const +{ + return checkbox.isChecked(); +} + +void ChannelWidget::onChannelToggled(bool checked) +{ + if (checked) + { + // Emit signal before actually turning on the channel + emit channelAboutToBeTurnedOn(deviceSerial, channelNumber); + } + + channel_t channelFlag = static_cast(1 << channelNumber); + if (checked) + { + if (eismultiplexer_connect_channel(multiplexer.get(), channelFlag) < 0) + { + QMessageBox::warning(this, tr("Connection Failed"), + tr("Failed to connect channel %1 on device %2").arg(channelNumber).arg(deviceSerial)); + qWarning() << "Failed to connect channel" << channelNumber << "on device" << deviceSerial; + checkbox.blockSignals(true); + checkbox.setChecked(false); + setEnabled(false); // Gray out the widget + } + } + else + { + if (eismultiplexer_disconnect_channel(multiplexer.get(), channelFlag) < 0) + { + QMessageBox::warning(this, tr("Disconnection Failed"), + tr("Failed to disconnect channel %1 on device %2").arg(channelNumber).arg(deviceSerial)); + qWarning() << "Failed to disconnect channel" << channelNumber << "on device" << deviceSerial; + checkbox.blockSignals(true); + checkbox.setChecked(true); + setEnabled(false); // Gray out the widget + } + } +} + diff --git a/channelwidget.h b/channelwidget.h new file mode 100644 index 0000000..08762a6 --- /dev/null +++ b/channelwidget.h @@ -0,0 +1,49 @@ +#ifndef CHANNELWIDGET_H +#define CHANNELWIDGET_H + +#include +#include +#include +#include +#include +#include +#include + +class ChannelWidget : public QWidget +{ + Q_OBJECT + +public: + explicit ChannelWidget(uint16_t deviceSerial, uint16_t channelNumber, + std::shared_ptr multiplexer, + QWidget *parent = nullptr); + ~ChannelWidget() override; + + uint16_t getDeviceSerial() const; + uint16_t getChannelNumber() const; + bool isChecked() const; + +signals: + void channelAboutToBeTurnedOn(uint16_t deviceSerial, uint16_t channelNumber); + +private slots: + void onChannelToggled(bool checked); + +private: + uint16_t deviceSerial; + uint16_t channelNumber; + std::shared_ptr multiplexer; + QCheckBox checkbox; + QLabel devicelabel; + QLabel channellabel; + QLabel ganglabel; + QComboBox gangcombo; + QFrame line; + QVBoxLayout vlayout; + QHBoxLayout hlayout; + QVBoxLayout labellayout; +}; + +#endif // CHANNELWIDGET_H + + diff --git a/main.cpp b/main.cpp new file mode 100644 index 0000000..fb4d5b4 --- /dev/null +++ b/main.cpp @@ -0,0 +1,10 @@ +#include +#include "mainwindow.h" + +int main(int argc, char *argv[]) +{ + QApplication app(argc, argv); + MainWindow window; + window.show(); + return app.exec(); +} diff --git a/mainwindow.cpp b/mainwindow.cpp new file mode 100644 index 0000000..99a5739 --- /dev/null +++ b/mainwindow.cpp @@ -0,0 +1,68 @@ +#include +#include +#include "mainwindow.h" +#include "ui_mainwindow.h" + +MainWindow::MainWindow(QWidget *parent) + : QMainWindow(parent) + , ui(new Ui::MainWindow) +{ + ui->setupUi(this); + enumerateDevices(); + + connect(ui->actionQuit, &QAction::triggered, this, [this]() + { + close(); + }); +} + +MainWindow::~MainWindow() +{ + delete ui; +} + +void MainWindow::enumerateDevices() +{ + size_t count = 0; + uint16_t* serials = eismultiplexer_list_available_devices(&count); + + if (!serials || count == 0) + { + QMessageBox::warning(nullptr, tr("No Devices Found"), + tr("No EIS multiplexer devices were found. Please connect a device and try again.")); + qWarning() << "No EIS multiplexer devices found"; + exit(0); + return; + } + + for (size_t i = 0; i < count; i++) + { + uint16_t serial = serials[i]; + std::shared_ptr multiplexer(new struct eismultiplexer); + if (eismultiplexer_connect(multiplexer.get(), serial) >= 0) + { + uint16_t channelCount = 0; + qDebug()<<"Adding channels from device "<= 0) + { + for (uint16_t channel = 0; channel < channelCount; channel++) + { + std::shared_ptr widget(new ChannelWidget(serial, channel, multiplexer)); + qDebug()<<"Added widget from device "<channelLayout->addWidget(widget.get()); + } + } + } + else + { + QMessageBox::warning(this, tr("Connection Failed"), + tr("Failed to connect to device with serial %1").arg(serial)); + qWarning() << "Failed to connect to device with serial" << serial; + } + ui->channelLayout->addStretch(); + } + ui->statusbar->showMessage("Ready"); + + free(serials); +} diff --git a/mainwindow.h b/mainwindow.h new file mode 100644 index 0000000..cb1a19c --- /dev/null +++ b/mainwindow.h @@ -0,0 +1,31 @@ +#ifndef MAINWINDOW_H +#define MAINWINDOW_H + +#include +#include + +#include "channelwidget.h" + +namespace Ui +{ +class MainWindow; +} + +class MainWindow : public QMainWindow +{ + Q_OBJECT + std::vector> channels; + Ui::MainWindow *ui; + +signals: + void channelStateChanged(uint16_t device, uint16_t channel); + +public: + explicit MainWindow(QWidget *parent = nullptr); + ~MainWindow(); + +private: + void enumerateDevices(); +}; + +#endif // MAINWINDOW_H diff --git a/mainwindow.ui b/mainwindow.ui new file mode 100644 index 0000000..53aca8f --- /dev/null +++ b/mainwindow.ui @@ -0,0 +1,68 @@ + + + MainWindow + + + + 0 + 0 + 800 + 600 + + + + EisMultiplexer-Qt + + + + + + + true + + + + + 0 + 0 + 788 + 537 + + + + + + + + + + + + + + + + 0 + 0 + 800 + 29 + + + + + File + + + + + + + + + Quit + + + + + + diff --git a/multiplexer.cpp b/multiplexer.cpp new file mode 100644 index 0000000..3e1c8e0 --- /dev/null +++ b/multiplexer.cpp @@ -0,0 +1,16 @@ +#include "multiplexer.h" + +Multiplexer::Multiplexer(QObject *parent) + : QObject{parent} +{} + +Multiplexer::~Multiplexer() +{ + for(auto& multiplexer : multiplexers) + eismultiplexer_disconnect(multiplexer.get()); +} + +void Multiplexer::probe() +{ + +} diff --git a/multiplexer.h b/multiplexer.h new file mode 100644 index 0000000..32a04ec --- /dev/null +++ b/multiplexer.h @@ -0,0 +1,22 @@ +#ifndef MULTIPLEXER_H +#define MULTIPLEXER_H + +#include +#include "eismultiplexer.h" + +class Multiplexer : public QObject +{ + Q_OBJECT + std::vector> multiplexers; + std::vector channelStates; + +public: + explicit Multiplexer(QObject *parent = nullptr); + ~Multiplexer(); + void probe(); + +signals: + void foundDevice(std::shared_ptr); +}; + +#endif // MULTIPLEXER_H diff --git a/scripts/release-win.sh b/scripts/release-win.sh new file mode 100755 index 0000000..f987f25 --- /dev/null +++ b/scripts/release-win.sh @@ -0,0 +1,61 @@ +#!/bin/bash -e +# +# libkissinference - an inference libary for kiss networks +# Copyright (C) 2024 Carl Philipp Klemm +# +# This file is part of libkissinference. +# +# libkissinference is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# libkissinference is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with libkissinference. If not, see . +# + +PROJECTNAME=@PROJECT_NAME@ +SYSTEMPROC=@CMAKE_SYSTEM_PROCESSOR@ +ROOTPATH=@CMAKE_FIND_ROOT_PATH@ +VERSION="@CMAKE_PROJECT_VERSION_MAJOR@.@CMAKE_PROJECT_VERSION_MINOR@.@CMAKE_PROJECT_VERSION_PATCH@" +BINARYDIR="@CMAKE_CURRENT_BINARY_DIR@" +SRCDIR="@CMAKE_CURRENT_SOURCE_DIR@" +RELDIRECTORY="$BINARYDIR/packaged/$VERSION/release" +ZIPNAME=$PROJECTNAME-$SYSTEMPROC-$VERSION + +rm $BINARYDIR/packaged/$ZIPNAME.zip || true +cd $BINARYDIR +install -d $RELDIRECTORY +cp eismuliplexer-qt.exe $RELDIRECTORY +cp $ROOTPATH/bin/libwinpthread-1.dll $RELDIRECTORY +cp $ROOTPATH/bin/libusb-1.0.dll $RELDIRECTORY +cp $ROOTPATH/bin/libssp-0.dll $RELDIRECTORY +cp $ROOTPATH/bin/libgcc_s_seh-1.dll $RELDIRECTORY +cp $ROOTPATH/bin/libstdc++-6.dll $RELDIRECTORY +cp $ROOTPATH/bin/Qt6Core.dll $RELDIRECTORY +cp $ROOTPATH/bin/Qt6Gui.dll $RELDIRECTORY +cp $ROOTPATH/bin/Qt6Widgets.dll $RELDIRECTORY +cp -r $ROOTPATH/lib/qt6/plugins/platforms $RELDIRECTORY +cp $ROOTPATH/lib/libeismultiplexer.dll $RELDIRECTORY +cp $ROOTPATH/bin/libpcre2-*-0.dll $RELDIRECTORY +cp $ROOTPATH/bin/zlib1.dll $RELDIRECTORY +cp $ROOTPATH/bin/libfreetype-6.dll $RELDIRECTORY +cp $ROOTPATH/bin/libpng16-16.dll $RELDIRECTORY +cp $ROOTPATH/bin/libzstd.dll $RELDIRECTORY +cp $ROOTPATH/bin/libharfbuzz-0.dll $RELDIRECTORY +cp $ROOTPATH/bin/libintl-8.dll $RELDIRECTORY +cp $ROOTPATH/bin/libgraphite2.dll $RELDIRECTORY +cp $ROOTPATH/bin/libglib-2.0-0.dll $RELDIRECTORY +cp $ROOTPATH/bin/libbz2-1.dll $RELDIRECTORY +cp $ROOTPATH/bin/libbrotlidec.dll $RELDIRECTORY +cp $ROOTPATH/bin/libbrotlicommon.dll $RELDIRECTORY +cp $ROOTPATH/bin/libiconv-2.dll $RELDIRECTORY +#cp $SRCDIR/README.md $RELDIRECTORY +cd $RELDIRECTORY/.. +rm $BINARYDIR/packaged/$ZIPNAME.zip || true +zip -r $BINARYDIR/packaged/$ZIPNAME.zip release From 316ce6c043df90d18f8313cf9ef9e7420722e4a8 Mon Sep 17 00:00:00 2001 From: Carl Philipp Klemm Date: Mon, 25 Aug 2025 17:50:18 +0200 Subject: [PATCH 10/28] Correct handling of eismultiplexer_connect return code --- mainwindow.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mainwindow.cpp b/mainwindow.cpp index 99a5739..a41892a 100644 --- a/mainwindow.cpp +++ b/mainwindow.cpp @@ -39,7 +39,7 @@ void MainWindow::enumerateDevices() { uint16_t serial = serials[i]; std::shared_ptr multiplexer(new struct eismultiplexer); - if (eismultiplexer_connect(multiplexer.get(), serial) >= 0) + if (!eismultiplexer_connect(multiplexer.get(), serial)) { uint16_t channelCount = 0; qDebug()<<"Adding channels from device "< Date: Thu, 18 Sep 2025 13:02:31 +0200 Subject: [PATCH 11/28] Enable the building of appimages on CI --- .gitea/workflows/build.yml | 46 +++++++++++++++++++++++++++++ CMakeLists.txt | 22 +++++++------- mainwindow.cpp | 5 ++-- resources/eismultiplexerqt.desktop | 7 +++++ resources/eismultiplexerqt.png | Bin 0 -> 40880 bytes 5 files changed, 67 insertions(+), 13 deletions(-) create mode 100644 .gitea/workflows/build.yml create mode 100644 resources/eismultiplexerqt.desktop create mode 100644 resources/eismultiplexerqt.png diff --git a/.gitea/workflows/build.yml b/.gitea/workflows/build.yml new file mode 100644 index 0000000..bb845d5 --- /dev/null +++ b/.gitea/workflows/build.yml @@ -0,0 +1,46 @@ +name: Build eismuliplexer for linux +run-name: Building eismuliplexer for linux +on: [push] + +jobs: + Build: + runs-on: ubuntu-22.04 + steps: + - name: Install dependancies + run: apt update; apt install -y libusb-1.0-0-dev cmake qt6-base-dev qt6-base-dev-tools qt6-tools-dev-tools libqt6widgets6 wget + - name: Check out repository code + uses: ischanx/checkout@8c80eac3058d03dc5301629e8f7d59ae255d6cc3 + - name: Checkout libeismultiplexer + run: git clone http://192.168.178.27/git/Eismultiplexer/libeismultiplexer.git + - name: Install Appimagetool + run: | + wget https://github.com/probonopd/go-appimage/releases/download/continuous/appimagetool-904-x86_64.AppImage + chmod +x appimagetool-904-x86_64.AppImage + mv appimagetool-904-x86_64.AppImage /usr/bin/appimagetool + - name: Version + id: libeismultiplexer version + run: | + cd ${{ gitea.workspace }}/libeismultiplexer + git fetch -a; + echo "tag=$(git describe --tags `git rev-list --tags --max-count=1`)" >> $GITHUB_OUTPUT + - name: Build and install libeismultiplexer + run: | + mkdir ${{ gitea.workspace }}/libeismultiplexer/build; cd ${{ gitea.workspace }}/libeismultiplexer/build + cmake -DCMAKE_BUILD_TYPE=Release -DGIT_TAG=${{ steps.version.outputs.tag }} .. + make + make install + - name: Build + run: | + mkdir ${{ gitea.workspace }}/build; cd ${{ gitea.workspace }}/build + cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=./install/usr .. + make + make install + - name: Create Appimage + run: | + cd ${{ gitea.workspace }}/build/ + appimagetool deploy install/usr/share/applications/eismultiplexerqt.desktop + appimagetool install + uses: actions/upload-artifact@v3 + with: + name: eismultiplexer-qt-appimage + path: ${{ gitea.workspace }}/build/*.appimage diff --git a/CMakeLists.txt b/CMakeLists.txt index e86a2c3..e3f9565 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,28 +1,25 @@ cmake_minimum_required(VERSION 3.14) -project(eismuliplexer-qt) +project(eismultiplexer-qt) -set(CMAKE_PROJECT_VERSION_MAJOR 0) -set(CMAKE_PROJECT_VERSION_MINOR 9) -set(CMAKE_PROJECT_VERSION_PATCH 0) +list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake/") +include(GitVersion) +get_version_from_git() +message("Building version ${PROJECT_VERSION}") add_compile_definitions(VERSION_MAJOR=${CMAKE_PROJECT_VERSION_MAJOR}) add_compile_definitions(VERSION_MINOR=${CMAKE_PROJECT_VERSION_MINOR}) add_compile_definitions(VERSION_PATCH=${CMAKE_PROJECT_VERSION_PATCH}) -if(CMAKE_HOST_SYSTEM_NAME MATCHES "Windows") - message(FATAL_ERROR "Windows builds have to be cross compiled on UNIX") -endif() - message("Platform " ${CMAKE_SYSTEM_NAME}) -if(WIN32) +if(WIN32 AND NOT CMAKE_HOST_SYSTEM_NAME MATCHES "Windows") configure_file(${CMAKE_CURRENT_SOURCE_DIR}/scripts/release-win.sh ${CMAKE_CURRENT_BINARY_DIR}/release.sh @ONLY) add_custom_target(package COMMAND ${CMAKE_CURRENT_BINARY_DIR}/release.sh WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} COMMENT "Createing release archive" VERBATIM) -endif(WIN32) +endif(WIN32 AND NOT CMAKE_HOST_SYSTEM_NAME MATCHES "Windows") set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) @@ -45,6 +42,9 @@ add_executable(${PROJECT_NAME} multiplexer.h multiplexer.cpp ) +set_target_properties(${PROJECT_NAME} PROPERTIES WIN32_EXECUTABLE ON) target_compile_options(${PROJECT_NAME} PUBLIC "-Wall") target_link_libraries(${PROJECT_NAME} PRIVATE Qt6::Widgets Qt6::Core ${EISMULIPLEXER_LIBRARIES}) -#target_include_directories(${PROJECT_NAME} PUBLIC ${EISMULIPLEXER_INCLUDE_DIRS}) +install(TARGETS ${PROJECT_NAME} DESTINATION bin) +install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/resources/eismultiplexerqt.desktop DESTINATION share/applications) +install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/resources/eismultiplexerqt.png DESTINATION share/icons/hicolor/256x256/apps) diff --git a/mainwindow.cpp b/mainwindow.cpp index a41892a..0b7ae5d 100644 --- a/mainwindow.cpp +++ b/mainwindow.cpp @@ -39,7 +39,8 @@ void MainWindow::enumerateDevices() { uint16_t serial = serials[i]; std::shared_ptr multiplexer(new struct eismultiplexer); - if (!eismultiplexer_connect(multiplexer.get(), serial)) + int ret = eismultiplexer_connect(multiplexer.get(), serial); + if (ret == 0) { uint16_t channelCount = 0; qDebug()<<"Adding channels from device "<channelLayout->addStretch(); } diff --git a/resources/eismultiplexerqt.desktop b/resources/eismultiplexerqt.desktop new file mode 100644 index 0000000..58f04ce --- /dev/null +++ b/resources/eismultiplexerqt.desktop @@ -0,0 +1,7 @@ +[Desktop Entry] +Categories=Qt;Science +Comment=Application to control RHD eismultiplexer devices +Icon=eismultiplexerqt +Name=EisMultiplexerQT +Exec=eismultiplexer-qt +Type=Application diff --git a/resources/eismultiplexerqt.png b/resources/eismultiplexerqt.png new file mode 100644 index 0000000000000000000000000000000000000000..6408f2e89aa0bf6f54c974dd119dc2e4fdce5a0c GIT binary patch literal 40880 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 literal 0 HcmV?d00001 From f43d78f28529b3a688fc464bdf5018ae1565d3c0 Mon Sep 17 00:00:00 2001 From: Carl Philipp Klemm Date: Thu, 18 Sep 2025 13:04:50 +0200 Subject: [PATCH 12/28] CI: correct syntax --- .gitea/workflows/build.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.gitea/workflows/build.yml b/.gitea/workflows/build.yml index bb845d5..d007f93 100644 --- a/.gitea/workflows/build.yml +++ b/.gitea/workflows/build.yml @@ -17,8 +17,8 @@ jobs: wget https://github.com/probonopd/go-appimage/releases/download/continuous/appimagetool-904-x86_64.AppImage chmod +x appimagetool-904-x86_64.AppImage mv appimagetool-904-x86_64.AppImage /usr/bin/appimagetool - - name: Version - id: libeismultiplexer version + - name: libeismultiplexer version + id: version run: | cd ${{ gitea.workspace }}/libeismultiplexer git fetch -a; @@ -40,6 +40,7 @@ jobs: cd ${{ gitea.workspace }}/build/ appimagetool deploy install/usr/share/applications/eismultiplexerqt.desktop appimagetool install + - name: Upload Appimage uses: actions/upload-artifact@v3 with: name: eismultiplexer-qt-appimage From 379342c0868d42bbeb4f03b8d5f908f22944db2c Mon Sep 17 00:00:00 2001 From: Carl Philipp Klemm Date: Thu, 18 Sep 2025 13:06:47 +0200 Subject: [PATCH 13/28] Add missing cmake file --- cmake/GitVersion.cmake | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 cmake/GitVersion.cmake diff --git a/cmake/GitVersion.cmake b/cmake/GitVersion.cmake new file mode 100644 index 0000000..7913c7e --- /dev/null +++ b/cmake/GitVersion.cmake @@ -0,0 +1,38 @@ +function(get_version_from_git) + find_package(Git REQUIRED) + + execute_process( + COMMAND ${GIT_EXECUTABLE} describe --tags --always + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} + OUTPUT_VARIABLE GIT_TAG + OUTPUT_STRIP_TRAILING_WHITESPACE + RESULT_VARIABLE GIT_RESULT + ) + + if(NOT GIT_RESULT EQUAL 0) + message(FATAL_ERROR "Failed to get git tag") + endif() + + execute_process( + COMMAND ${GIT_EXECUTABLE} rev-parse --short=7 HEAD + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} + OUTPUT_VARIABLE GIT_COMMIT_SHORT_HASH + OUTPUT_STRIP_TRAILING_WHITESPACE + ) + + string(REGEX REPLACE "^v" "" CLEAN_TAG "${GIT_TAG}") + if(CLEAN_TAG MATCHES "^([0-9]+)\\.([0-9]+)\\.([0-9]+)(-.*)?$") + + set(CMAKE_PROJECT_VERSION_MAJOR ${CMAKE_MATCH_1}) + set(CMAKE_PROJECT_VERSION_MAJOR ${CMAKE_MATCH_1} PARENT_SCOPE) + set(CMAKE_PROJECT_VERSION_MINOR ${CMAKE_MATCH_2}) + set(CMAKE_PROJECT_VERSION_MINOR ${CMAKE_MATCH_2} PARENT_SCOPE) + set(CMAKE_PROJECT_VERSION_PATCH ${CMAKE_MATCH_3}) + set(CMAKE_PROJECT_VERSION_PATCH ${CMAKE_MATCH_3} PARENT_SCOPE) + + set(PROJECT_VERSION "${CMAKE_MATCH_1}.${CMAKE_MATCH_2}.${CMAKE_MATCH_3}") + set(PROJECT_VERSION "${CMAKE_MATCH_1}.${CMAKE_MATCH_2}.${CMAKE_MATCH_3}" PARENT_SCOPE) + else() + message(FATAL_ERROR "Tag '${CLEAN_TAG}' does not match semver format") + endif() +endfunction() From c3afad46bc46e8258fe169b1a710021ce8b6f97b Mon Sep 17 00:00:00 2001 From: Carl Philipp Klemm Date: Thu, 18 Sep 2025 13:09:07 +0200 Subject: [PATCH 14/28] CI: fix version handling --- .gitea/workflows/build.yml | 12 +++++++++--- cmake/GitVersion.cmake | 36 ++++++++++++++++++------------------ 2 files changed, 27 insertions(+), 21 deletions(-) diff --git a/.gitea/workflows/build.yml b/.gitea/workflows/build.yml index d007f93..f925e9e 100644 --- a/.gitea/workflows/build.yml +++ b/.gitea/workflows/build.yml @@ -18,21 +18,27 @@ jobs: chmod +x appimagetool-904-x86_64.AppImage mv appimagetool-904-x86_64.AppImage /usr/bin/appimagetool - name: libeismultiplexer version - id: version + id: libeisversion run: | cd ${{ gitea.workspace }}/libeismultiplexer git fetch -a; echo "tag=$(git describe --tags `git rev-list --tags --max-count=1`)" >> $GITHUB_OUTPUT + - name: Version + id: version + run: | + cd ${{ gitea.workspace }} + git fetch -a; + echo "tag=$(git describe --tags `git rev-list --tags --max-count=1`)" >> $GITHUB_OUTPUT - name: Build and install libeismultiplexer run: | mkdir ${{ gitea.workspace }}/libeismultiplexer/build; cd ${{ gitea.workspace }}/libeismultiplexer/build - cmake -DCMAKE_BUILD_TYPE=Release -DGIT_TAG=${{ steps.version.outputs.tag }} .. + cmake -DCMAKE_BUILD_TYPE=Release -DGIT_TAG=${{ steps.libeisversion.outputs.tag }} .. make make install - name: Build run: | mkdir ${{ gitea.workspace }}/build; cd ${{ gitea.workspace }}/build - cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=./install/usr .. + cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=./install/usr -DGIT_TAG=${{ steps.version.outputs.tag }} .. make make install - name: Create Appimage diff --git a/cmake/GitVersion.cmake b/cmake/GitVersion.cmake index 7913c7e..b147005 100644 --- a/cmake/GitVersion.cmake +++ b/cmake/GitVersion.cmake @@ -1,25 +1,19 @@ function(get_version_from_git) find_package(Git REQUIRED) - execute_process( - COMMAND ${GIT_EXECUTABLE} describe --tags --always - WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} - OUTPUT_VARIABLE GIT_TAG - OUTPUT_STRIP_TRAILING_WHITESPACE - RESULT_VARIABLE GIT_RESULT - ) - - if(NOT GIT_RESULT EQUAL 0) - message(FATAL_ERROR "Failed to get git tag") + if(NOT GIT_TAG) + execute_process( + COMMAND ${GIT_EXECUTABLE} describe --tags --always + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} + OUTPUT_VARIABLE GIT_TAG + OUTPUT_STRIP_TRAILING_WHITESPACE + RESULT_VARIABLE GIT_RESULT + ) + if(NOT GIT_RESULT EQUAL 0) + message(FATAL_ERROR "Failed to get git tag") + endif() endif() - execute_process( - COMMAND ${GIT_EXECUTABLE} rev-parse --short=7 HEAD - WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} - OUTPUT_VARIABLE GIT_COMMIT_SHORT_HASH - OUTPUT_STRIP_TRAILING_WHITESPACE - ) - string(REGEX REPLACE "^v" "" CLEAN_TAG "${GIT_TAG}") if(CLEAN_TAG MATCHES "^([0-9]+)\\.([0-9]+)\\.([0-9]+)(-.*)?$") @@ -33,6 +27,12 @@ function(get_version_from_git) set(PROJECT_VERSION "${CMAKE_MATCH_1}.${CMAKE_MATCH_2}.${CMAKE_MATCH_3}") set(PROJECT_VERSION "${CMAKE_MATCH_1}.${CMAKE_MATCH_2}.${CMAKE_MATCH_3}" PARENT_SCOPE) else() - message(FATAL_ERROR "Tag '${CLEAN_TAG}' does not match semver format") + message(WARNING "Tag '${CLEAN_TAG}' does not match semver format") + set(CMAKE_PROJECT_VERSION_MAJOR 0) + set(CMAKE_PROJECT_VERSION_MAJOR 0 PARENT_SCOPE) + set(CMAKE_PROJECT_VERSION_MINOR 0) + set(CMAKE_PROJECT_VERSION_MINOR 0 PARENT_SCOPE) + set(CMAKE_PROJECT_VERSION_PATCH 0) + set(CMAKE_PROJECT_VERSION_PATCH 0 PARENT_SCOPE) endif() endfunction() From 24a4146577ba1b2b7a482392a13da5747877fd02 Mon Sep 17 00:00:00 2001 From: Carl Philipp Klemm Date: Thu, 18 Sep 2025 13:25:18 +0200 Subject: [PATCH 15/28] CI: add glx and mesa packages to work around broken qt package in ubuntu --- .gitea/workflows/build.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.gitea/workflows/build.yml b/.gitea/workflows/build.yml index f925e9e..2e6c552 100644 --- a/.gitea/workflows/build.yml +++ b/.gitea/workflows/build.yml @@ -7,7 +7,7 @@ jobs: runs-on: ubuntu-22.04 steps: - name: Install dependancies - run: apt update; apt install -y libusb-1.0-0-dev cmake qt6-base-dev qt6-base-dev-tools qt6-tools-dev-tools libqt6widgets6 wget + run: apt update; apt install -y libusb-1.0-0-dev cmake qt6-base-dev qt6-base-dev-tools qt6-tools-dev-tools libqt6widgets6 wget libglx-dev libgl1-mesa-dev - name: Check out repository code uses: ischanx/checkout@8c80eac3058d03dc5301629e8f7d59ae255d6cc3 - name: Checkout libeismultiplexer @@ -33,13 +33,13 @@ jobs: run: | mkdir ${{ gitea.workspace }}/libeismultiplexer/build; cd ${{ gitea.workspace }}/libeismultiplexer/build cmake -DCMAKE_BUILD_TYPE=Release -DGIT_TAG=${{ steps.libeisversion.outputs.tag }} .. - make + make -j $(nproc) make install - name: Build run: | mkdir ${{ gitea.workspace }}/build; cd ${{ gitea.workspace }}/build cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=./install/usr -DGIT_TAG=${{ steps.version.outputs.tag }} .. - make + make -j $(nproc) make install - name: Create Appimage run: | From 31f6a3e06f34a57b8f1dca11a3e5a23eedaac350 Mon Sep 17 00:00:00 2001 From: Carl Philipp Klemm Date: Thu, 18 Sep 2025 13:29:47 +0200 Subject: [PATCH 16/28] CI: use --appimage-extract-and-run --- .gitea/workflows/build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitea/workflows/build.yml b/.gitea/workflows/build.yml index 2e6c552..219e6ea 100644 --- a/.gitea/workflows/build.yml +++ b/.gitea/workflows/build.yml @@ -44,8 +44,8 @@ jobs: - name: Create Appimage run: | cd ${{ gitea.workspace }}/build/ - appimagetool deploy install/usr/share/applications/eismultiplexerqt.desktop - appimagetool install + appimagetool --appimage-extract-and-run deploy install/usr/share/applications/eismultiplexerqt.desktop + appimagetool --appimage-extract-and-run install - name: Upload Appimage uses: actions/upload-artifact@v3 with: From 8a3b08ef912ce68bcbcb63c564d60f17c55dad19 Mon Sep 17 00:00:00 2001 From: Carl Philipp Klemm Date: Thu, 18 Sep 2025 13:31:53 +0200 Subject: [PATCH 17/28] CI: correct upload-artifact command --- .gitea/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitea/workflows/build.yml b/.gitea/workflows/build.yml index 219e6ea..ae4640b 100644 --- a/.gitea/workflows/build.yml +++ b/.gitea/workflows/build.yml @@ -50,4 +50,4 @@ jobs: uses: actions/upload-artifact@v3 with: name: eismultiplexer-qt-appimage - path: ${{ gitea.workspace }}/build/*.appimage + path: ${{ gitea.workspace }}/build/*.AppImage From b2dc70f5e70346e74eb0713df8b94a58eb2330be Mon Sep 17 00:00:00 2001 From: Carl Philipp Klemm Date: Thu, 18 Sep 2025 13:39:23 +0200 Subject: [PATCH 18/28] CI: also install qpa plugins, so that they are hopefully included in the appimage --- .gitea/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitea/workflows/build.yml b/.gitea/workflows/build.yml index ae4640b..30b3025 100644 --- a/.gitea/workflows/build.yml +++ b/.gitea/workflows/build.yml @@ -7,7 +7,7 @@ jobs: runs-on: ubuntu-22.04 steps: - name: Install dependancies - run: apt update; apt install -y libusb-1.0-0-dev cmake qt6-base-dev qt6-base-dev-tools qt6-tools-dev-tools libqt6widgets6 wget libglx-dev libgl1-mesa-dev + run: apt update; apt install -y libusb-1.0-0-dev cmake qt6-base-dev qt6-base-dev-tools qt6-tools-dev-tools libqt6widgets6 wget libglx-dev libgl1-mesa-dev qt6-qpa-plugins - name: Check out repository code uses: ischanx/checkout@8c80eac3058d03dc5301629e8f7d59ae255d6cc3 - name: Checkout libeismultiplexer From e079642bbecf5bf78bf675b675c082562b42e574 Mon Sep 17 00:00:00 2001 From: Carl Philipp Klemm Date: Thu, 18 Sep 2025 13:52:39 +0200 Subject: [PATCH 19/28] CI: try ubuntu-latest --- .gitea/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitea/workflows/build.yml b/.gitea/workflows/build.yml index 30b3025..01949ad 100644 --- a/.gitea/workflows/build.yml +++ b/.gitea/workflows/build.yml @@ -4,7 +4,7 @@ on: [push] jobs: Build: - runs-on: ubuntu-22.04 + runs-on: ubuntu-latest steps: - name: Install dependancies run: apt update; apt install -y libusb-1.0-0-dev cmake qt6-base-dev qt6-base-dev-tools qt6-tools-dev-tools libqt6widgets6 wget libglx-dev libgl1-mesa-dev qt6-qpa-plugins From dc9b41246ad66f36cca76c67564f1523ad6e9fe9 Mon Sep 17 00:00:00 2001 From: Carl Philipp Klemm Date: Thu, 18 Sep 2025 13:57:58 +0200 Subject: [PATCH 20/28] CI: try standalone interpreted appimage --- .gitea/workflows/build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitea/workflows/build.yml b/.gitea/workflows/build.yml index 01949ad..340ac90 100644 --- a/.gitea/workflows/build.yml +++ b/.gitea/workflows/build.yml @@ -44,8 +44,8 @@ jobs: - name: Create Appimage run: | cd ${{ gitea.workspace }}/build/ - appimagetool --appimage-extract-and-run deploy install/usr/share/applications/eismultiplexerqt.desktop - appimagetool --appimage-extract-and-run install + appimagetool --appimage-extract-and-run -s deploy install/usr/share/applications/eismultiplexerqt.desktop + appimagetool --appimage-extract-and-run -s install - name: Upload Appimage uses: actions/upload-artifact@v3 with: From 67d673f3b4bbbd55419a90dae20f19cdb7af77eb Mon Sep 17 00:00:00 2001 From: Carl Philipp Klemm Date: Thu, 25 Sep 2025 16:39:27 +0200 Subject: [PATCH 21/28] Add windows build to CI, unlikely to work first try --- .gitea/workflows/build-win.yml | 44 ++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 .gitea/workflows/build-win.yml diff --git a/.gitea/workflows/build-win.yml b/.gitea/workflows/build-win.yml new file mode 100644 index 0000000..98c0919 --- /dev/null +++ b/.gitea/workflows/build-win.yml @@ -0,0 +1,44 @@ +name: Build eismuliplexer-qt for windows +run-name: Building eismuliplexer-qt for windows +on: [push] + +jobs: + Build: + runs-on: arch-qt + steps: + - name: Install dependancies + run: pacman -Sy; pacman -S --noconfirm mingw-w64-libusb mingw-w64-pkg-config + - name: Check out repository code + uses: ischanx/checkout@8c80eac3058d03dc5301629e8f7d59ae255d6cc3 + - name: Checkout libeismultiplexer + run: git clone http://192.168.178.27/git/Eismultiplexer/libeismultiplexer.git + - name: libeismultiplexer version + id: libeisversion + run: | + cd ${{ gitea.workspace }}/libeismultiplexer + git fetch -a; + echo "tag=$(git describe --tags `git rev-list --tags --max-count=1`)" >> $GITHUB_OUTPUT + - name: Version + id: version + run: | + cd ${{ gitea.workspace }} + git fetch -a; + echo "tag=$(git describe --tags `git rev-list --tags --max-count=1`)" >> $GITHUB_OUTPUT + - name: Build libeismultiplexer + run: | + mkdir ${{ gitea.workspace }}/libeismultiplexer/build + cd ${{ gitea.workspace }}/libeismultiplexer/build + cmake -DCMAKE_INSTALL_PREFIX=/usr/x86_64-w64-mingw32/ -DCMAKE_TOOLCHAIN_FILE=../crossW64.cmake -DCMAKE_BUILD_TYPE=Release -DGIT_TAG=${{ steps.libeisversion.outputs.tag }} .. + make + make install + - name: Build + run: | + mkdir ${{ gitea.workspace }}/build; cd ${{ gitea.workspace }}/build + cmake -DCMAKE_TOOLCHAIN_FILE=../crossW64.cmake -DCMAKE_BUILD_TYPE=Release -DGIT_TAG=${{ steps.version.outputs.tag }} .. + make -j $(nproc) + make package + - name: Upload Package + uses: actions/upload-artifact@v3 + with: + name: eismuliplexer-qt-${{ steps.version.outputs.tag }}-w64 + path: ${{ gitea.workspace }}/build/packaged/${{ steps.version.outputs.tag }}/release/* From acb9e520ae78c268c3a5c43e551732663cc0820b Mon Sep 17 00:00:00 2001 From: Carl Philipp Klemm Date: Thu, 25 Sep 2025 16:54:37 +0200 Subject: [PATCH 22/28] CI: change windows build image --- .gitea/workflows/build-win.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitea/workflows/build-win.yml b/.gitea/workflows/build-win.yml index 98c0919..72def03 100644 --- a/.gitea/workflows/build-win.yml +++ b/.gitea/workflows/build-win.yml @@ -4,7 +4,7 @@ on: [push] jobs: Build: - runs-on: arch-qt + runs-on: arch-mingw-qt steps: - name: Install dependancies run: pacman -Sy; pacman -S --noconfirm mingw-w64-libusb mingw-w64-pkg-config From 36d60961cd46ad5400b847ed7395fa5e738579e3 Mon Sep 17 00:00:00 2001 From: Carl Philipp Klemm Date: Thu, 25 Sep 2025 17:24:04 +0200 Subject: [PATCH 23/28] CI: windows ci needs nodejs for checkout --- .gitea/workflows/build-win.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitea/workflows/build-win.yml b/.gitea/workflows/build-win.yml index 72def03..1f24439 100644 --- a/.gitea/workflows/build-win.yml +++ b/.gitea/workflows/build-win.yml @@ -7,7 +7,7 @@ jobs: runs-on: arch-mingw-qt steps: - name: Install dependancies - run: pacman -Sy; pacman -S --noconfirm mingw-w64-libusb mingw-w64-pkg-config + run: pacman -Sy; pacman -S --noconfirm mingw-w64-libusb mingw-w64-pkg-config nodejs - name: Check out repository code uses: ischanx/checkout@8c80eac3058d03dc5301629e8f7d59ae255d6cc3 - name: Checkout libeismultiplexer From 7151e1852044b8a4e18c7aa5641c4bf46ed85367 Mon Sep 17 00:00:00 2001 From: Carl Philipp Klemm Date: Thu, 25 Sep 2025 17:25:24 +0200 Subject: [PATCH 24/28] add missing cross compile file --- .gitea/workflows/build-win.yml | 3 ++- crossW64.cmake | 8 ++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) create mode 100644 crossW64.cmake diff --git a/.gitea/workflows/build-win.yml b/.gitea/workflows/build-win.yml index 1f24439..b31829a 100644 --- a/.gitea/workflows/build-win.yml +++ b/.gitea/workflows/build-win.yml @@ -33,7 +33,8 @@ jobs: make install - name: Build run: | - mkdir ${{ gitea.workspace }}/build; cd ${{ gitea.workspace }}/build + mkdir ${{ gitea.workspace }}/build + cd ${{ gitea.workspace }}/build cmake -DCMAKE_TOOLCHAIN_FILE=../crossW64.cmake -DCMAKE_BUILD_TYPE=Release -DGIT_TAG=${{ steps.version.outputs.tag }} .. make -j $(nproc) make package diff --git a/crossW64.cmake b/crossW64.cmake new file mode 100644 index 0000000..1f3404f --- /dev/null +++ b/crossW64.cmake @@ -0,0 +1,8 @@ +set(CMAKE_SYSTEM_NAME Windows) +set(CMAKE_SYSTEM_PROCESSOR AMD64) +set(CMAKE_C_COMPILER x86_64-w64-mingw32-gcc) +set(CMAKE_CXX_COMPILER x86_64-w64-mingw32-g++) +set(CMAKE_FIND_ROOT_PATH /usr/x86_64-w64-mingw32) +set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) +set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) +set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) From 6c62ba763ac8a331be466dc9b34e8cd6414fe181 Mon Sep 17 00:00:00 2001 From: Carl Philipp Klemm Date: Thu, 25 Sep 2025 17:34:31 +0200 Subject: [PATCH 25/28] set PKG_CONFIG_PATH when cross compieling to find correct version of the eismultiplexer lib --- crossW64.cmake | 1 + 1 file changed, 1 insertion(+) diff --git a/crossW64.cmake b/crossW64.cmake index 1f3404f..0fab148 100644 --- a/crossW64.cmake +++ b/crossW64.cmake @@ -6,3 +6,4 @@ set(CMAKE_FIND_ROOT_PATH /usr/x86_64-w64-mingw32) set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) +set(ENV{PKG_CONFIG_PATH} "/usr/x86_64-w64-mingw32/lib/pkgconfig/") From 6b496e46ef3ca0190eb056fedfefc06adea3f7ae Mon Sep 17 00:00:00 2001 From: Carl Philipp Klemm Date: Thu, 25 Sep 2025 17:36:52 +0200 Subject: [PATCH 26/28] Packageing: correct windows executable name --- scripts/release-win.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/release-win.sh b/scripts/release-win.sh index f987f25..c022117 100755 --- a/scripts/release-win.sh +++ b/scripts/release-win.sh @@ -31,7 +31,7 @@ ZIPNAME=$PROJECTNAME-$SYSTEMPROC-$VERSION rm $BINARYDIR/packaged/$ZIPNAME.zip || true cd $BINARYDIR install -d $RELDIRECTORY -cp eismuliplexer-qt.exe $RELDIRECTORY +cp eismultiplexer-qt.exe $RELDIRECTORY cp $ROOTPATH/bin/libwinpthread-1.dll $RELDIRECTORY cp $ROOTPATH/bin/libusb-1.0.dll $RELDIRECTORY cp $ROOTPATH/bin/libssp-0.dll $RELDIRECTORY From b4f3eeae7004f383cc4b7691bcb170447018800e Mon Sep 17 00:00:00 2001 From: Carl Philipp Klemm Date: Thu, 25 Sep 2025 17:37:47 +0200 Subject: [PATCH 27/28] CI: zip is needed on windows for packageing release --- .gitea/workflows/build-win.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitea/workflows/build-win.yml b/.gitea/workflows/build-win.yml index b31829a..5c5835f 100644 --- a/.gitea/workflows/build-win.yml +++ b/.gitea/workflows/build-win.yml @@ -7,7 +7,7 @@ jobs: runs-on: arch-mingw-qt steps: - name: Install dependancies - run: pacman -Sy; pacman -S --noconfirm mingw-w64-libusb mingw-w64-pkg-config nodejs + run: pacman -Sy; pacman -S --noconfirm mingw-w64-libusb mingw-w64-pkg-config nodejs zip - name: Check out repository code uses: ischanx/checkout@8c80eac3058d03dc5301629e8f7d59ae255d6cc3 - name: Checkout libeismultiplexer From 7a6e6747567f5da0a11acca3d97f77ff89485eaf Mon Sep 17 00:00:00 2001 From: Carl Philipp Klemm Date: Thu, 25 Sep 2025 17:39:30 +0200 Subject: [PATCH 28/28] rename windows build job --- .gitea/workflows/build-win.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitea/workflows/build-win.yml b/.gitea/workflows/build-win.yml index 5c5835f..31120dd 100644 --- a/.gitea/workflows/build-win.yml +++ b/.gitea/workflows/build-win.yml @@ -3,7 +3,7 @@ run-name: Building eismuliplexer-qt for windows on: [push] jobs: - Build: + BuildWin: runs-on: arch-mingw-qt steps: - name: Install dependancies