From 467df25d5a7cd1d0f2fee7c2958a765b72c550c8 Mon Sep 17 00:00:00 2001 From: Reinder Feenstra Date: Sat, 8 Oct 2022 23:29:22 +0200 Subject: [PATCH] WIP: LNCV programming widget --- .gitmodules | 3 + client/CMakeLists.txt | 6 +- client/src/mainwindow.cpp | 12 + client/src/mainwindow.hpp | 1 + client/src/network/callmethod.hpp | 16 +- .../src/programming/lncv/lncvprogrammer.cpp | 640 ++++++++++++++++++ .../src/programming/lncv/lncvprogrammer.hpp | 102 +++ .../lncv/lncvprogramminglistmodel.cpp | 44 ++ .../lncv/lncvprogramminglistmodel.hpp | 42 ++ shared/data/lncv | 1 + .../src/traintastic/utils/standardpaths.cpp | 11 + .../src/traintastic/utils/standardpaths.hpp | 1 + shared/translations/en-us.txt | 17 + 13 files changed, 888 insertions(+), 8 deletions(-) create mode 100644 .gitmodules create mode 100644 client/src/programming/lncv/lncvprogrammer.cpp create mode 100644 client/src/programming/lncv/lncvprogrammer.hpp create mode 100644 client/src/programming/lncv/lncvprogramminglistmodel.cpp create mode 100644 client/src/programming/lncv/lncvprogramminglistmodel.hpp create mode 160000 shared/data/lncv diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 00000000..5ac13562 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "shared/data/lncv"] + path = shared/data/lncv + url = git@github.com:traintastic/lncv.git diff --git a/client/CMakeLists.txt b/client/CMakeLists.txt index 16cc4088..c7a3acd4 100644 --- a/client/CMakeLists.txt +++ b/client/CMakeLists.txt @@ -44,6 +44,8 @@ file(GLOB SOURCES "src/misc/*.cpp" "src/network/*.hpp" "src/network/*.cpp" + "src/programming/lncv/*.hpp" + "src/programming/lncv/*.cpp" "src/settings/*.hpp" "src/settings/*.cpp" "src/style/*.hpp" @@ -67,11 +69,11 @@ file(GLOB SOURCES "thirdparty/QtWaitingSpinner/*.hpp" "thirdparty/QtWaitingSpinner/*.cpp") -find_package(Qt5 COMPONENTS Widgets Network Svg REQUIRED) +find_package(Qt5 COMPONENTS Widgets Network Svg Xml REQUIRED) target_sources(traintastic-client PRIVATE ${SOURCES}) -target_link_libraries(traintastic-client PRIVATE Qt5::Widgets Qt5::Network Qt5::Svg) +target_link_libraries(traintastic-client PRIVATE Qt5::Widgets Qt5::Network Qt5::Svg Qt5::Xml) ### PLATFORM ### diff --git a/client/src/mainwindow.cpp b/client/src/mainwindow.cpp index 7b324ec8..c637b3f4 100644 --- a/client/src/mainwindow.cpp +++ b/client/src/mainwindow.cpp @@ -45,6 +45,7 @@ #include "network/object.hpp" #include "network/property.hpp" #include "network/method.hpp" +#include "programming/lncv/lncvprogrammer.hpp" #include "subwindow/objectsubwindow.hpp" #include "subwindow/boardsubwindow.hpp" #include "subwindow/throttlesubwindow.hpp" @@ -412,6 +413,16 @@ MainWindow::MainWindow(QWidget* parent) : if(Method* method = traintastic->getMethod("shutdown")) method->call(); }); + m_menuProgramming = menu->addMenu(Locale::tr("qtapp.mainmenu:programming")); + m_menuProgramming->addAction(Locale::tr("lncv_programmer:lncv_programmer") + "...", + [this]() + { + auto* window = new QMdiSubWindow(); + window->setWidget(new LNCVProgrammer(m_connection)); + window->setAttribute(Qt::WA_DeleteOnClose); + m_mdiArea->addSubWindow(window); + window->show(); + }); menu = menuBar()->addMenu(Locale::tr("qtapp.mainmenu:help")); menu->addAction(Locale::tr("qtapp.mainmenu:help"), @@ -773,6 +784,7 @@ void MainWindow::updateActions() m = m_connection->traintastic()->getMethod("shutdown"); m_actionServerShutdown->setEnabled(m && m->getAttributeBool(AttributeName::Enabled, false)); } + m_menuProgramming->setEnabled(haveWorld); setMenuEnabled(m_menuWorld, haveWorld); m_worldOnlineOfflineToolButton->setEnabled(haveWorld); diff --git a/client/src/mainwindow.hpp b/client/src/mainwindow.hpp index 557ea06c..507f6ff3 100644 --- a/client/src/mainwindow.hpp +++ b/client/src/mainwindow.hpp @@ -83,6 +83,7 @@ class MainWindow : public QMainWindow QAction* m_actionServerRestart; QAction* m_actionServerShutdown; QAction* m_actionServerLog; + QMenu* m_menuProgramming; // Main toolbar: QToolBar* m_toolbar; QToolButton* m_worldOnlineOfflineToolButton; diff --git a/client/src/network/callmethod.hpp b/client/src/network/callmethod.hpp index eace8620..9f9d86f7 100644 --- a/client/src/network/callmethod.hpp +++ b/client/src/network/callmethod.hpp @@ -3,7 +3,7 @@ * * This file is part of the traintastic source code. * - * Copyright (C) 2020-2021 Reinder Feenstra + * Copyright (C) 2020-2022 Reinder Feenstra * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License @@ -77,7 +77,8 @@ int callMethod(Connection& connection, Method& method, std::functionwrite(value_type_v); request->write(sizeof...(A)); // N arguments - writeArguments(*request, args...); + if constexpr(sizeof...(A) > 0) + writeArguments(*request, args...); connection.send(request, [&connection, callback](const std::shared_ptr message) @@ -101,7 +102,8 @@ int callMethodR(Method& method, std::functionwrite(value_type_v); request->write(sizeof...(A)); // N arguments - writeArguments(*request, args...); + if constexpr(sizeof...(A) > 0) + writeArguments(*request, args...); auto c = method.object().connection(); c->send(request, @@ -125,7 +127,8 @@ int callMethod(Method& method, std::function callback, request->write(ValueType::Invalid); request->write(sizeof...(A)); // N arguments - writeArguments(*request, args...); + if constexpr(sizeof...(A) > 0) + writeArguments(*request, args...); method.object().connection()->send(request, [callback=std::move(callback)](const std::shared_ptr message) @@ -137,7 +140,7 @@ int callMethod(Method& method, std::function callback, } template -void callMethod(Method& method, std::nullptr_t, A... args) +void callMethod(Method& method, std::nullptr_t = nullptr, A... args) { auto event = Message::newEvent(Message::Command::ObjectCallMethod); event->write(method.object().handle()); @@ -145,7 +148,8 @@ void callMethod(Method& method, std::nullptr_t, A... args) event->write(ValueType::Invalid); event->write(sizeof...(A)); // N arguments - writeArguments(*event, args...); + if constexpr(sizeof...(A) > 0) + writeArguments(*event, args...); method.object().connection()->send(event); } diff --git a/client/src/programming/lncv/lncvprogrammer.cpp b/client/src/programming/lncv/lncvprogrammer.cpp new file mode 100644 index 00000000..21fbe7f3 --- /dev/null +++ b/client/src/programming/lncv/lncvprogrammer.cpp @@ -0,0 +1,640 @@ +/** + * client/src/programming/lncv/lncvprogrammer.cpp + * + * This file is part of the traintastic source code. + * + * Copyright (C) 2022 Reinder Feenstra + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include "lncvprogrammer.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "lncvprogramminglistmodel.hpp" +#include "../../network/connection.hpp" +#include "../../network/callmethod.hpp" +#include "../../network/event.hpp" + +static QDomElement getElementByLanguage(const QList& list, const QString& language) +{ + for(const auto element : list) + if(element.attribute("language", "") == language) + return element; + + return {}; +} + +static QDomElement getElementByLanguage(const QDomElement& element, const QString& tagName) +{ + static const auto appLanguage = QString::fromStdU32String(Locale::instance->filename.filename().u32string()); + + QList list; + for(QDomElement e = element.firstChildElement(tagName); !e.isNull(); e = e.nextSiblingElement(tagName)) + list.append(e); + + QDomElement e = getElementByLanguage(list, appLanguage); + if(!e.isNull()) + return e; + + e = getElementByLanguage(list, "en-us"); + if(!e.isNull()) + return e; + + e = getElementByLanguage(list, ""); + if(!e.isNull()) + return e; + + return {}; +} + +static int getAttributeBool(const QDomElement element, const QString attribute, bool default_ = false) +{ + const auto s = element.attribute(attribute); + if(s == "true") + return true; + else if(s == "false") + return false; + return default_; +} + +static int getAttributeInt(const QDomElement element, const QString attribute, int default_ = 0, int min = std::numeric_limits::min(), int max = std::numeric_limits::max()) +{ + bool ok; + const int r = element.attribute(attribute).toInt(&ok); + return (ok && r >= min && r <= max) ? r : default_; +} + +static double getAttributeFloat(const QDomElement element, const QString attribute, double default_ = std::numeric_limits::quiet_NaN()) +{ + bool ok; + const double r = element.attribute(attribute).toDouble(&ok); + return ok ? r : default_; +} + +LNCVProgrammer::LNCVProgrammer(std::shared_ptr connection, QWidget* parent, Qt::WindowFlags f) + : QWidget(parent, f) + , m_connection{std::move(connection)} + , m_requestId{Connection::invalidRequestId} + , m_pages{new QStackedWidget(this)} + , m_statusBar{new QStatusBar(this)} + , m_interface{new QComboBox(this)} + , m_module{new QComboBox(this)} + , m_otherModule{new QSpinBox(this)} + , m_address{new QSpinBox(this)} + , m_broadcastAddress{new QCheckBox(Locale::tr("lncv_programmer:use_broadcast_address"), this)} + , m_lncvs{new QTableWidget(0, 3, this)} + , m_start{new QPushButton(Locale::tr("lncv_programmer:start"))} + , m_read{new QPushButton(Locale::tr("lncv_programmer:read"))} + , m_write{new QPushButton(Locale::tr("lncv_programmer:write"))} + , m_stop{new QPushButton(Locale::tr("lncv_programmer:stop"))} +{ + setWindowTitle(Locale::tr("lncv_programmer:lncv_programmer")); + + loadInterfaces(); + connect(m_interface, QOverload::of(&QComboBox::currentIndexChanged), this, &LNCVProgrammer::updateStartEnabled); + + loadModules(); + connect(m_module, QOverload::of(&QComboBox::currentIndexChanged), this, &LNCVProgrammer::moduleChanged); + + m_otherModule->setValue(0); + m_otherModule->setRange(moduleIdMin, moduleIdMax); + + m_address->setValue(1); + m_address->setRange(1, 65535); + + connect(m_broadcastAddress, &QCheckBox::toggled, this, &LNCVProgrammer::useBroadcastAddressChanged); + + connect(m_start, &QPushButton::clicked, + [this]() + { + loadLNCVs(); + m_pages->widget(pageStart)->setEnabled(false); + m_statusBar->showMessage(Locale::tr("lncv_programmer:sending_start")); + + if(const auto& world = m_connection->world()) + { + if(auto* getLNCVProgrammer = world->getMethod("get_lncv_programmer")) + { + callMethodR(*getLNCVProgrammer, + [this](const ObjectPtr& object, Message::ErrorCode /*ec*/) + { + if(object) + { + if(auto* onReadResponse = object->getEvent("on_read_response")) + { + connect(onReadResponse, &Event::fired, this, + [this](QVariantList arguments) + { + const bool success = arguments[0].toBool(); + const int lncv = arguments[1].toInt(); + const int value = arguments[2].toInt(); + + if(success) + { + m_statusBar->clearMessage(); + + if(auto it = m_lncvToRow.find(lncv); it != m_lncvToRow.end()) + { + auto* w = m_lncvs->cellWidget(it->second, columnValue); + if(auto* spinBox = dynamic_cast(w)) + { + spinBox->setEnabled(true); + spinBox->setValue(value); + } + else if(auto* doubleSpinBox = dynamic_cast(w)) + { + const double gain = doubleSpinBox->property("lncv_gain").toDouble(); + const double offset = doubleSpinBox->property("lncv_offset").toDouble(); + doubleSpinBox->setEnabled(true); + doubleSpinBox->setValue(value * gain + offset); + } + else if(auto* comboBox = dynamic_cast(w)) + { + comboBox->setEnabled(true); + const int itemCount = comboBox->count(); + for(int i = 0; i < itemCount; i++) + { + if(comboBox->itemData(i) == value) + { + comboBox->setCurrentIndex(i); + return; + } + } + comboBox->addItem(QString::number(value), value); + comboBox->setCurrentIndex(itemCount); + } + else + { + m_lncvs->setItem(it->second, columnValue, new QTableWidgetItem(QString::number(value))); + } + } + } + else + { + m_statusBar->showMessage(Locale::tr("lncv_programmer:reading_lncv_x_failed").arg(lncv), showMessageTimeout); + } + + setState(State::Idle); + }); + } + else + assert(false); + + if(auto* start = object->getMethod("start")) + { + m_object = object; + m_pages->setCurrentIndex(pageProgramming); + m_read->setEnabled(false); + m_write->setEnabled(false); + + const int moduleId = + (m_module->currentIndex() == m_module->count() - 1) + ? m_otherModule->value() + : m_module->currentData(roleModuleId).toInt(); + + setState(State::WaitForStart); + + callMethodR(*start, + [this](const bool sent, Message::ErrorCode /*ec*/) + { + if(sent) + { + m_statusBar->showMessage(Locale::tr("lncv_programmer:waiting_for_module_to_respond")); + } + else + { + m_statusBar->showMessage(Locale::tr("lncv_programmer:sending_start_failed"), showMessageTimeout); + reset(); + } + }, moduleId, m_address->value()); + } + else + assert(false); + } + }, m_interface->currentText()); + } + else + assert(false); + } + else + assert(false); + }); + + connect(m_read, &QPushButton::clicked, + [this]() + { + if(const int lncv = getSelectedLNCV(); lncv != -1) + { + m_statusBar->showMessage(Locale::tr("lncv_programmer:reading_lncv_x").arg(lncv)); + if(auto* read = m_object->getMethod("read")) + { + callMethod(*read, nullptr, lncv); + setState(State::WaitForRead); + } + } + }); + + connect(m_write, &QPushButton::clicked, + [this]() + { + if(const int lncv = getSelectedLNCV(); lncv != -1) + { + if(const int lncvValue = getSelectedLNCVValue(); lncvValue != -1) + { + m_statusBar->showMessage(Locale::tr("lncv_programmer:writing_lncv_x").arg(lncv)); + if(auto* write = m_object->getMethod("write")) + { + callMethod(*write, nullptr, lncv, lncvValue); + setState(State::WaitForWrite); + } + } + } + }); + + connect(m_stop, &QPushButton::clicked, + [this]() + { + if(auto* stop = m_object->getMethod("stop")) + { + m_statusBar->clearMessage(); + callMethod(*stop); + reset(); + } + else + assert(false); + }); + + m_lncvs->setHorizontalHeaderLabels({QString("LNCV"), Locale::tr("lncv_programmer:value"), Locale::tr("lncv_programmer:description")}); + m_lncvs->horizontalHeader()->setStretchLastSection(true); + m_lncvs->verticalHeader()->setVisible(false); + m_lncvs->setEditTriggers(QAbstractItemView::NoEditTriggers); + m_lncvs->setSelectionMode(QAbstractItemView::SingleSelection); + m_lncvs->setSelectionBehavior(QAbstractItemView::SelectRows); + connect(m_lncvs, &QTableWidget::currentItemChanged, + [this](QTableWidgetItem* /*current*/, QTableWidgetItem* /*previous*/) + { + updateReadWriteEnabled(); + }); + + // start page: + { + auto* form = new QFormLayout(); + form->addRow(Locale::tr("hardware:interface"), m_interface); + form->addRow(Locale::tr("lncv_programmer:module"), m_module); + form->addRow("", m_otherModule); + form->addRow(Locale::tr("hardware:address"), m_address); + form->addRow("", m_broadcastAddress); + form->addRow("", m_start); + auto* w = new QWidget(this); + w->setLayout(form); + m_pages->addWidget(w); + } + + // programming page: + { + auto* v = new QVBoxLayout(); + v->addWidget(m_lncvs); + + auto* h = new QHBoxLayout(); + h->addStretch(); + h->addWidget(m_read); + h->addWidget(m_write); + h->addWidget(m_stop); + h->addStretch(); + v->addLayout(h); + auto* w = new QWidget(this); + w->setLayout(v); + m_pages->addWidget(w); + } + + auto* l = new QVBoxLayout(); + l->setMargin(0); + l->addWidget(m_pages); + l->addWidget(m_statusBar); + setLayout(l); +} + +LNCVProgrammer::~LNCVProgrammer() +{ + if(m_requestId != Connection::invalidRequestId) + m_connection->cancelRequest(m_requestId); + m_object.reset(); +} + +void LNCVProgrammer::loadInterfaces() +{ + m_requestId = m_connection->getObject("world.lncv_programming_controllers", + [this](const ObjectPtr& object, Message::ErrorCode /*ec*/) + { + m_requestId = Connection::invalidRequestId; + + if(object) + m_requestId = m_connection->getTableModel(object, + [this](const TableModelPtr& table, Message::ErrorCode /*ec*/) + { + m_requestId = Connection::invalidRequestId; + + if(table) + m_interface->setModel(new LNCVProgrammingListModel(table, m_interface)); + }); + }); +} + +void LNCVProgrammer::loadModules() +{ + m_module->clear(); + m_module->addItem(""); + + QDir moduleDir(QString::fromStdString(getLNCVXMLPath().string())); + moduleDir.setNameFilters({"*.xml"}); + for(const QString& entry : moduleDir.entryList(QDir::Files | QDir::Readable, QDir::Name)) + { + const QString filename = moduleDir.absoluteFilePath(entry); + QFile file(filename); + if(!file.open(QIODevice::ReadOnly)) + continue; + + QDomDocument doc; + if(!doc.setContent(&file)) + continue; + + QDomElement lncvModule = doc.documentElement(); + if(lncvModule.tagName() != "lncvmodule" || !lncvModule.hasAttribute("id")) + continue; + + bool ok; + if(int moduleId = lncvModule.attribute("id").toInt(&ok); ok && moduleId >= moduleIdMin && moduleId <= moduleIdMax) + { + if(auto name = getElementByLanguage(lncvModule, "name"); !name.isNull()) + { + QString label{name.text()}; + + if(auto vendor = getElementByLanguage(lncvModule, "vendor"); !name.isNull()) + label.prepend(" ").prepend(vendor.text()); + + m_module->addItem(label, filename); + m_module->setItemData(m_module->count() - 1, moduleId, roleModuleId); + } + } + } + + m_module->addItem(Locale::tr("lncv_programmer:other_module")); + + moduleChanged(); +} + +void LNCVProgrammer::loadLNCVs() +{ + m_lncvs->setRowCount(0); + m_lncvToRow.clear(); + + if(const auto filename = m_module->currentData(roleFilename).toString(); !filename.isEmpty()) + { + QFile file(filename); + if(file.open(QIODevice::ReadOnly)) + { + QDomDocument doc; + if(doc.setContent(&file)) + { + QDomElement lncvModule = doc.documentElement(); + + bool ok; + QDomElement lncv = lncvModule.firstChildElement("lncv"); + while(!lncv.isNull()) + { + if(int number = lncv.attribute("lncv").toInt(&ok); ok && number >= lncvMin && number <= lncvMax) + { + const int row = m_lncvs->rowCount(); + m_lncvs->setRowCount(row + 1); + + m_lncvs->setItem(row, columnLNCV, new QTableWidgetItem(lncv.attribute("lncv"))); + m_lncvs->setItem(row, columnDescription, new QTableWidgetItem(getElementByLanguage(lncv, "name").text())); + + QWidget* w = nullptr; + if(QDomElement value = lncv.firstChildElement("value"); !value.isNull()) + { + if(getAttributeBool(value, "readonly")) + { + // no widget + } + else + { + const int defaultValue = getAttributeInt(value, "default", -1, lncvValueMin, lncvValueMax); + + QDomElement option = value.firstChildElement("option"); + if(!option.isNull()) + { + auto* cb = new QComboBox(this); + + while(!option.isNull()) + { + const int v = getAttributeInt(option, "value", -1, lncvValueMin, lncvValueMax); + if(v != -1) + { + auto name = getElementByLanguage(option, "name"); + if(!name.isNull()) + cb->addItem(name.text(), v); + else + cb->addItem(QString::number(v), v); + + if(defaultValue == v) + cb->setCurrentIndex(cb->count() - 1); + } + option = option.nextSiblingElement("option"); + } + w = cb; + } + else + { + const int min = getAttributeInt(value, "min", lncvValueMin, lncvValueMin, lncvValueMax); + const int max = getAttributeInt(value, "max", lncvValueMax, lncvValueMin, lncvValueMax); + double gain = getAttributeFloat(value, "gain"); + double offset = getAttributeFloat(value, "offset"); + double dummy; + if((std::isfinite(gain) && std::modf(gain, &dummy) != 0) || + (std::isfinite(offset) && std::modf(offset, &dummy) != 0)) + { + if(std::isnan(gain)) + gain = 1; + if(std::isnan(offset)) + offset = 0; + + auto* sb = new QDoubleSpinBox(this); + + sb->setProperty("lncv_gain", gain); + sb->setProperty("lncv_offset", offset); + + sb->setSingleStep(gain); + sb->setDecimals(-std::floor(std::log10(std::abs(gain)))); + + if(max >= min) + sb->setRange(min * gain + offset, max * gain + offset); + else + sb->setRange(lncvValueMin * gain + offset, lncvValueMax * gain + offset); + + if(auto unit = value.attribute("unit", ""); !unit.isEmpty()) + sb->setSuffix(unit.prepend(" ")); + + if(defaultValue != -1) + sb->setValue(defaultValue * gain + offset); + + w = sb; + } + else + { + auto* spinBox = new QSpinBox(this); + + if(max >= min) + spinBox->setRange(min, max); + else + spinBox->setRange(lncvValueMin, lncvValueMax); + + if(auto unit = value.attribute("unit", ""); !unit.isEmpty()) + spinBox->setSuffix(unit.prepend(" ")); + + if(defaultValue != -1) + spinBox->setValue(defaultValue); + + w = spinBox; + } + } + } + } + else + { + auto* sb = new QSpinBox(this); + sb->setRange(lncvValueMin, lncvValueMax); + w = sb; + } + + if(w) + { + m_lncvs->setCellWidget(row, columnValue, w); + w->setEnabled(false); + } + + m_lncvToRow[number] = row; + } + lncv = lncv.nextSiblingElement("lncv"); + } + } + } + } +} + +void LNCVProgrammer::moduleChanged() +{ + m_otherModule->setVisible(m_module->currentIndex() == m_module->count() - 1); + updateStartEnabled(); +} + +void LNCVProgrammer::useBroadcastAddressChanged() +{ + if(m_broadcastAddress->isChecked()) + { + m_lastAddress = m_address->value(); + m_address->setValue(broadcastAddress); + m_address->setEnabled(false); + } + else + { + m_address->setValue(m_lastAddress); + m_address->setEnabled(true); + } +} + +void LNCVProgrammer::updateStartEnabled() +{ + m_start->setEnabled( + m_interface->currentIndex() != 0 && m_interface->count() > 1 && + m_module->currentIndex() != 0); +} + +void LNCVProgrammer::updateReadWriteEnabled() +{ + const bool b = (getSelectedLNCV() != -1) && (m_state == State::Idle); + m_read->setEnabled(b); + m_write->setEnabled(b && getSelectedLNCVValue() != -1); +} + +void LNCVProgrammer::reset() +{ + m_pages->widget(pageStart)->setEnabled(true); + m_pages->setCurrentIndex(pageStart); + m_object.reset(); +} + +int LNCVProgrammer::getSelectedLNCV() const +{ + if(const auto* item = m_lncvs->item(m_lncvs->currentRow(), columnLNCV)) + { + bool ok; + if(int lncv = item->text().toInt(&ok); ok && lncv >= lncvMin && lncv <= lncvMax) + return lncv; + } + return -1; +} + +int LNCVProgrammer::getSelectedLNCVValue() const +{ + if(const auto* widget = m_lncvs->cellWidget(m_lncvs->currentRow(), columnValue)) + { + if(!widget->isEnabled()) + return -1; + + if(const auto* spinBox = dynamic_cast(widget)) + { + return spinBox->value(); + } + if(const auto* comboBox = dynamic_cast(widget)) + { + return comboBox->currentData().toInt(); + } + if(const auto* doubleSpinBox = dynamic_cast(widget)) + { + const double gain = doubleSpinBox->property("lncv_gain").toDouble(); + const double offset = doubleSpinBox->property("lncv_offset").toDouble(); + return std::round((doubleSpinBox->value() - offset) / gain); + } + } + else if(const auto* item = m_lncvs->item(m_lncvs->currentRow(), columnValue)) + { + bool ok; + if(int lncv = item->text().toInt(&ok); ok && lncv >= lncvMin && lncv <= lncvMax) + return lncv; + } + return -1; +} + +void LNCVProgrammer::setState(State value) +{ + m_state = value; + updateReadWriteEnabled(); +} diff --git a/client/src/programming/lncv/lncvprogrammer.hpp b/client/src/programming/lncv/lncvprogrammer.hpp new file mode 100644 index 00000000..c10df5ad --- /dev/null +++ b/client/src/programming/lncv/lncvprogrammer.hpp @@ -0,0 +1,102 @@ +/** + * client/src/programming/lncv/lncvprogrammer.hpp + * + * This file is part of the traintastic source code. + * + * Copyright (C) 2022 Reinder Feenstra + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef TRAINTASTIC_CLIENT_PROGRAMMING_LNCV_LNCVPROGRAMMER_HPP +#define TRAINTASTIC_CLIENT_PROGRAMMING_LNCV_LNCVPROGRAMMER_HPP + +#include +#include +#include "../../network/objectptr.hpp" + +class Connection; +class QStackedWidget; +class QStatusBar; +class QComboBox; +class QSpinBox; +class QCheckBox; +class QPushButton; +class QTableWidget; + +class LNCVProgrammer final : public QWidget +{ + private: + enum class State + { + Idle, + WaitForStart, + WaitForRead, + WaitForWrite, + }; + + static constexpr int pageStart = 0; + static constexpr int pageProgramming = 1; + static constexpr int roleFilename = Qt::UserRole + 0; + static constexpr int roleModuleId = Qt::UserRole + 1; + static constexpr int showMessageTimeout = 3000; + static constexpr int moduleIdMin = 0; + static constexpr int moduleIdMax = 65535; + static constexpr uint16_t broadcastAddress = 65535; + static constexpr int lncvMin = 0; + static constexpr int lncvMax = 655535; + static constexpr int lncvValueMin = 0; + static constexpr int lncvValueMax = 655535; + static constexpr int columnLNCV = 0; + static constexpr int columnValue = 1; + static constexpr int columnDescription = 2; + + std::shared_ptr m_connection; + int m_requestId; + ObjectPtr m_object; + QStackedWidget* m_pages; + QStatusBar* m_statusBar; + QComboBox* m_interface; + QComboBox* m_module; + QSpinBox* m_otherModule; + QSpinBox* m_address; + QCheckBox* m_broadcastAddress; + QTableWidget* m_lncvs; + QPushButton* m_start; + QPushButton* m_read; + QPushButton* m_write; + QPushButton* m_stop; + int m_lastAddress = 1; + State m_state = State::Idle; + std::unordered_map m_lncvToRow; + + void loadInterfaces(); + void loadModules(); + void loadLNCVs(); + void moduleChanged(); + void useBroadcastAddressChanged(); + void updateStartEnabled(); + void updateReadWriteEnabled(); + void reset(); + int getSelectedLNCV() const; + int getSelectedLNCVValue() const; + void setState(State value); + + public: + explicit LNCVProgrammer(std::shared_ptr, QWidget* parent = nullptr, Qt::WindowFlags f = Qt::WindowFlags()); + ~LNCVProgrammer() final; +}; + +#endif diff --git a/client/src/programming/lncv/lncvprogramminglistmodel.cpp b/client/src/programming/lncv/lncvprogramminglistmodel.cpp new file mode 100644 index 00000000..cf99cb43 --- /dev/null +++ b/client/src/programming/lncv/lncvprogramminglistmodel.cpp @@ -0,0 +1,44 @@ +/** + * client/src/programming/lncv/lncvprogramminglistmodel.cpp + * + * This file is part of the traintastic source code. + * + * Copyright (C) 2022 Reinder Feenstra + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include "lncvprogramminglistmodel.hpp" +#include "../../network/tablemodel.hpp" + +LNCVProgrammingListModel::LNCVProgrammingListModel(TableModelPtr tableModel, QObject* parent) + : QAbstractListModel(parent) + , m_tableModel{std::move(tableModel)} +{ + m_tableModel->setRegion(0, std::numeric_limits::max(), 0, std::numeric_limits::max()); +} + +int LNCVProgrammingListModel::rowCount(const QModelIndex& parent) const +{ + return 1 + m_tableModel->rowCount(parent); +} + +QVariant LNCVProgrammingListModel::data(const QModelIndex& index, int role) const +{ + if(index.row() == 0) + return role == Qt::DisplayRole ? QVariant("") : QVariant{}; + + return m_tableModel->data(QAbstractListModel::index(index.row() - 1, index.column()), role); +} diff --git a/client/src/programming/lncv/lncvprogramminglistmodel.hpp b/client/src/programming/lncv/lncvprogramminglistmodel.hpp new file mode 100644 index 00000000..7f9342da --- /dev/null +++ b/client/src/programming/lncv/lncvprogramminglistmodel.hpp @@ -0,0 +1,42 @@ +/** + * client/src/programming/lncv/lncvprogramminglistmodel.hpp + * + * This file is part of the traintastic source code. + * + * Copyright (C) 2022 Reinder Feenstra + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef TRAINTASTIC_CLIENT_PROGRAMMING_LNCV_LNCVPROGRAMMINGLISTMODEL_HPP +#define TRAINTASTIC_CLIENT_PROGRAMMING_LNCV_LNCVPROGRAMMINGLISTMODEL_HPP + +#include +#include "../../network/tablemodelptr.hpp" + +class LNCVProgrammingListModel : public QAbstractListModel +{ + private: + TableModelPtr m_tableModel; + + public: + explicit LNCVProgrammingListModel(TableModelPtr tableModel, QObject* parent = nullptr); + + int rowCount(const QModelIndex& parent = QModelIndex()) const final; + + QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const final; +}; + +#endif diff --git a/shared/data/lncv b/shared/data/lncv new file mode 160000 index 00000000..1ee4f267 --- /dev/null +++ b/shared/data/lncv @@ -0,0 +1 @@ +Subproject commit 1ee4f2678a35e8e42f204a2eb03935c9f4919c20 diff --git a/shared/src/traintastic/utils/standardpaths.cpp b/shared/src/traintastic/utils/standardpaths.cpp index f593214b..99aece74 100644 --- a/shared/src/traintastic/utils/standardpaths.cpp +++ b/shared/src/traintastic/utils/standardpaths.cpp @@ -72,3 +72,14 @@ std::filesystem::path getManualPath() return {}; #endif } + +std::filesystem::path getLNCVXMLPath() +{ +#ifdef WIN32 + return getProgramDataPath() / "traintastic" / "lncv"; +#elif defined(__linux__) + return "/opt/traintastic/lncv"; +#else + return std::filesystem::current_path() / "lncv"; +#endif +} diff --git a/shared/src/traintastic/utils/standardpaths.hpp b/shared/src/traintastic/utils/standardpaths.hpp index ecb2edbe..9724e70c 100644 --- a/shared/src/traintastic/utils/standardpaths.hpp +++ b/shared/src/traintastic/utils/standardpaths.hpp @@ -32,5 +32,6 @@ std::filesystem::path getLocalAppDataPath(); std::filesystem::path getLocalePath(); std::filesystem::path getManualPath(); +std::filesystem::path getLNCVXMLPath(); #endif diff --git a/shared/translations/en-us.txt b/shared/translations/en-us.txt index 27cf46ba..6391e3bc 100644 --- a/shared/translations/en-us.txt +++ b/shared/translations/en-us.txt @@ -227,6 +227,22 @@ list:move_down=Move down list:move_up=Move up list:remove=Remove +lncv_programmer:description=Description +lncv_programmer:lncv_programmer=LNCV programmer +lncv_programmer:module=Module +lncv_programmer:other_module=Other module +lncv_programmer:read=Read +lncv_programmer:reading_lncv_x=Reading LNCV %1 +lncv_programmer:reading_lncv_x_failed=Reading LNCV %1 failed +lncv_programmer:sending_start=Sending start +lncv_programmer:start=Start +lncv_programmer:stop=Stop +lncv_programmer:use_broadcast_address=Use broadcast address +lncv_programmer:value=Value +lncv_programmer:waiting_for_module_to_respond=Waiting for module to respond +lncv_programmer:write=Write +lncv_programmer:writing_lncv_x=Writing LNCV %1 + loconet_command_station:custom=Custom loconet_command_station:digikeijs_dr5000=Digikeijs DR5000 loconet_command_station:uhlenbrock_ibcom=Uhlenbrock IB-Com @@ -439,6 +455,7 @@ qtapp.mainmenu:load_world=Load world qtapp.mainmenu:new_world=New world qtapp.mainmenu:objects=Objects qtapp.mainmenu:power=Power +qtapp.mainmenu:programming=Programming qtapp.mainmenu:quit=Quit qtapp.mainmenu:restart_server=Restart server qtapp.mainmenu:restart_server_question=Are you sure you want to restart the server?