diff --git a/.gitignore b/.gitignore index f147edf..fab7372 100644 --- a/.gitignore +++ b/.gitignore @@ -1,52 +1,73 @@ -# C++ objects and libs -*.slo -*.lo -*.o +# This file is used to ignore files which are generated +# ---------------------------------------------------------------------------- + +*~ +*.autosave *.a -*.la -*.lai +*.core +*.moc +*.o +*.obj +*.orig +*.rej *.so *.so.* -*.dll -*.dylib - -# Qt-es -object_script.*.Release -object_script.*.Debug -*_plugin_import.cpp +*_pch.h.cpp +*_resource.rc +*.qm +.#* +*.*# +core +!core/ +tags +.DS_Store +.directory +*.debug +Makefile* +*.prl +*.app +moc_*.cpp +ui_*.h +qrc_*.cpp +Thumbs.db +*.res +*.rc /.qmake.cache /.qmake.stash -*.pro.user -*.pro.user.* -*.qbs.user -*.qbs.user.* -*.moc -moc_*.cpp -moc_*.h -qrc_*.cpp -ui_*.h -*.qmlc -*.jsc -Makefile* -*build-* -*.qm -*.prl -# Qt unit tests -target_wrapper.* +# qtcreator generated files +*.pro.user* -# QtCreator -*.autosave +# xemacs temporary files +*.flc -# QtCreator Qml -*.qmlproject.user -*.qmlproject.user.* +# Vim temporary files +.*.swp -# QtCreator CMake -CMakeLists.txt.user* +# Visual Studio generated files +*.ib_pdb_index +*.idb +*.ilk +*.pdb +*.sln +*.suo +*.vcproj +*vcproj.*.*.user +*.ncb +*.sdf +*.opensdf +*.vcxproj +*vcxproj.* -# QtCreator 4.8< compilation database -compile_commands.json +# MinGW generated files +*.Debug +*.Release + +# Python byte code +*.pyc + +# Binaries +# -------- +*.dll +*.exe -# QtCreator local machine specific files for imported projects -*creator.user* diff --git a/main.cpp b/main.cpp new file mode 100644 index 0000000..f3110e0 --- /dev/null +++ b/main.cpp @@ -0,0 +1,32 @@ +// Qt includes +#include +#include + +// local includes +#include "mainwindow.h" + +int main(int argc, char *argv[]) +{ + QApplication app{argc, argv}; + + qSetMessagePattern(QStringLiteral("%{time dd.MM.yyyy HH:mm:ss.zzz} " + "[" + "%{if-debug}D%{endif}" + "%{if-info}I%{endif}" + "%{if-warning}W%{endif}" + "%{if-critical}C%{endif}" + "%{if-fatal}F%{endif}" + "] " + "%{function}(): " + "%{message}")); + + QCoreApplication::setOrganizationDomain(QStringLiteral("brunner.ninja")); + QCoreApplication::setOrganizationName(QStringLiteral("feedc0de enterprises")); + QCoreApplication::setApplicationName(QStringLiteral("qmodbustester")); + QCoreApplication::setApplicationVersion(QStringLiteral("1.0")); + + MainWindow mainWindow; + mainWindow.show(); + + return app.exec(); +} diff --git a/mainwindow.cpp b/mainwindow.cpp new file mode 100644 index 0000000..1d74640 --- /dev/null +++ b/mainwindow.cpp @@ -0,0 +1,319 @@ +#include "mainwindow.h" +#include "ui_mainwindow.h" + +// Qt includes +#include +#include +#include +#include +#include +#include +#include + +// local includes +#include "modbustablemodel.h" + +// utilities +namespace { +QDebug operator<<(QDebug debug, QModbusDataUnit::RegisterType registerType); +QString toString(QModbusDataUnit::RegisterType registerType); +} // namespace + +MainWindow::MainWindow(QWidget *parent) : + QMainWindow{parent}, + m_ui{std::make_unique()}, + m_modbus{std::make_unique(this)}, + m_model{std::make_unique(this)} +{ + m_ui->setupUi(this); + + m_ui->spinBoxTimeout->setValue(m_modbus->timeout()); + connect(m_modbus.get(), &QModbusClient::timeoutChanged, m_ui->spinBoxTimeout, &QSpinBox::setValue); + connect(m_ui->spinBoxTimeout, &QSpinBox::valueChanged, m_modbus.get(), &QModbusClient::setTimeout); + + m_ui->spinBoxRetries->setValue(m_modbus->numberOfRetries()); + //connect(m_modbus.get(), &QModbusClient::numberOfRetriesChanged, m_ui->spinBoxRetries, &QSpinBox::setValue); + connect(m_ui->spinBoxRetries, &QSpinBox::valueChanged, m_modbus.get(), &QModbusClient::setNumberOfRetries); + + modbusStateChanged(m_modbus->state()); + + connect(m_modbus.get(), &QModbusClient::errorOccurred, + this, &MainWindow::modbusErrorOccured); + + connect(m_modbus.get(), &QModbusClient::stateChanged, + this, &MainWindow::modbusStateChanged); + + { + const auto addItem = [&](const auto &text, const auto &value){ + m_ui->comboBoxType->addItem(text, QVariant::fromValue(value)); + }; + addItem(tr("Discrete Inputs"), QModbusDataUnit::DiscreteInputs); + addItem(tr("Coils"), QModbusDataUnit::Coils); + addItem(tr("Input Registers"), QModbusDataUnit::InputRegisters); + addItem(tr("Holding Registers"), QModbusDataUnit::HoldingRegisters); + } + m_ui->comboBoxType->setCurrentIndex( + m_ui->comboBoxType->findData( + QVariant::fromValue(QModbusDataUnit::HoldingRegisters) + ) + ); + + connect(m_ui->pushButtonConnect, &QAbstractButton::pressed, this, &MainWindow::connectPressed); + connect(m_ui->pushButtonRequest, &QAbstractButton::pressed, this, &MainWindow::requestPressed); + + m_ui->tableView->setModel(m_model.get()); +} + +MainWindow::~MainWindow() = default; + +void MainWindow::connectPressed() +{ + if (m_reply) + { + QMessageBox::warning(this, + tr("Another request is still pending!"), + tr("Another request is still pending!")); + return; + } + + switch (const auto state = m_modbus->state()) + { + case QModbusDevice::ConnectedState: + m_modbus->disconnectDevice(); + break; + case QModbusDevice::UnconnectedState: + m_modbus->setConnectionParameter(QModbusDevice::NetworkAddressParameter, m_ui->lineEditServer->text()); + m_modbus->setConnectionParameter(QModbusDevice::NetworkPortParameter, m_ui->spinBoxPort->value()); + if (!m_modbus->connectDevice()) + { + + } + break; + default: + QMessageBox::warning(this, + tr("Modbus client is in wrong state"), + tr("Modbus client is in wrong state:\n\n%0") + .arg(QMetaEnum::fromType().valueToKey(state)) + ); + } +} + +void MainWindow::requestPressed() +{ + if (const auto state = m_modbus->state(); state != QModbusDevice::ConnectedState) + { + QMessageBox::warning(this, + tr("Modbus client is in wrong state"), + tr("Modbus client is in wrong state:\n\n%0") + .arg(QMetaEnum::fromType().valueToKey(state)) + ); + return; + } + + if (m_reply) + { + QMessageBox::warning(this, + tr("Another request is still pending!"), + tr("Another request is still pending!")); + return; + } + + const auto registerType = m_ui->comboBoxType->currentData(); + if (!registerType.isValid() || !registerType.canConvert()) + { + qDebug() << registerType << registerType.typeName() << registerType.canConvert() << registerType.canConvert(); + QMessageBox::warning(this, + tr("Invalid register type selected!"), + tr("Invalid register type selected!")); + return; + } + + m_timer.start(); + + QModbusDataUnit dataUnit(registerType.value(), m_ui->spinBoxRegister->value(), m_ui->spinBoxCount->value()); + qDebug() << m_ui->spinBoxSlave->value() << dataUnit.registerType() << dataUnit.startAddress() << dataUnit.valueCount(); + if (m_reply = std::unique_ptr(m_modbus->sendReadRequest(std::move(dataUnit), m_ui->spinBoxSlave->value()))) + { + updateRequestFields(); + + if (m_reply->isFinished()) + replyFinished(); + else + { + m_ui->labelRequestStatus->setText(tr("Pending...")); + if (!m_ui->checkBoxAutorefresh->isChecked()) + m_model->setResult({}); + connect(m_reply.get(), &QModbusReply::finished, this, &MainWindow::replyFinished); + } + } + else + { + m_ui->checkBoxAutorefresh->setChecked(false); + m_model->setResult({}); + QMessageBox::warning(this, + tr("Request sending failed!"), + tr("Request sending failed:\n\n%s").arg(m_modbus->errorString())); + } +} + +void MainWindow::modbusErrorOccured(int error) +{ + const auto typedError = QModbusDevice::Error(error); + qWarning() << typedError << m_modbus->errorString(); + + statusBar()->showMessage(tr("Modbus client failed with %0: %1") + .arg(typedError) + .arg(m_modbus->errorString()), + 5000); +} + +void MainWindow::modbusStateChanged(int state) +{ + qDebug() << QModbusDevice::State(state); + + m_ui->labelConnectionStatus->setText(QMetaEnum::fromType().valueToKey(state)); + + if (state == QModbusDevice::ConnectedState) + m_ui->pushButtonConnect->setText(tr("Disconnect")); + else if (state == QModbusDevice::UnconnectedState) + { + m_ui->pushButtonConnect->setText(tr("Connect")); + if (!m_reply) + m_ui->labelRequestStatus->setText(tr("Idle")); + } + + m_ui->lineEditServer->setEnabled(state == QModbusDevice::UnconnectedState); + m_ui->spinBoxPort->setEnabled(state == QModbusDevice::UnconnectedState); + m_ui->pushButtonConnect->setEnabled(state == QModbusDevice::ConnectedState || state == QModbusDevice::UnconnectedState); + m_ui->pushButtonRequest->setEnabled(state == QModbusDevice::ConnectedState); +} + +void MainWindow::replyFinished() +{ + Q_ASSERT(m_reply); + + if (!m_reply->isFinished()) + { + qWarning() << "not yet finished?!"; + return; + } + + const auto elapsed = m_timer.elapsed(); + + if (const QModbusDevice::Error error = m_reply->error(); error == QModbusDevice::NoError) + { + const auto result = m_reply->result(); + + if (result.isValid()) + { + m_model->setResult(result); + + m_ui->labelRequestStatus->setText(tr("Succeeded!")); + + statusBar()->showMessage(tr("Showing %0 from %1 to %2 (%3 registers) (took %4ms)") + .arg(toString(result.registerType())) + .arg(result.startAddress()) + .arg(result.startAddress() + result.valueCount()) + .arg(result.valueCount()) + .arg(elapsed), + 5000); + + if (m_ui->checkBoxAutorefresh->isChecked()) + QTimer::singleShot(m_ui->spinBoxDelay->value(), this, &MainWindow::requestPressed); + } + else + { + qWarning() << "result is invalid!"; + + m_model->setResult({}); + + m_ui->checkBoxAutorefresh->setChecked(false); + + m_ui->labelRequestStatus->setText(tr("Failed!")); + + const auto msg = tr("Request finished without any indication for an error but still the result is not valid! (took %0ms)") + .arg(elapsed); + + statusBar()->showMessage(msg, 5000); + + QMessageBox::warning(this, + tr("Request failed!"), + msg); + } + } + else + { + qWarning() << error << m_reply->errorString(); + + if (const auto result = m_reply->result(); result.isValid()) + { + qDebug() << "registerType =" << result.registerType(); + qDebug() << "startAddress =" << result.startAddress(); + qDebug() << "valueCount =" << result.valueCount(); + } + + m_model->setResult({}); + + m_ui->checkBoxAutorefresh->setChecked(false); + + m_ui->labelRequestStatus->setText(tr("Failed!")); + + const auto msg = tr("Request failed with %0: %1 (took %2ms)") + .arg(error) + .arg(m_reply->errorString()) + .arg(elapsed); + + statusBar()->showMessage(msg, 5000); + + if (m_ui->checkBoxShowMsgBoxes->isChecked()) + QMessageBox::warning(this, + tr("Request failed!"), + msg); + } + + m_reply = nullptr; + updateRequestFields(); +} + +void MainWindow::updateRequestFields() +{ + m_ui->spinBoxSlave->setEnabled(!m_reply); + m_ui->comboBoxType->setEnabled(!m_reply); + m_ui->spinBoxRegister->setEnabled(!m_reply); + m_ui->spinBoxCount->setEnabled(!m_reply); + m_ui->pushButtonRequest->setEnabled(m_modbus->state() == QModbusDevice::ConnectedState && !m_reply); +} + +namespace { +QDebug operator<<(QDebug debug, QModbusDataUnit::RegisterType registerType) +{ + QDebugStateSaver saver(debug); + + debug.nospace() << "QModbusDataUnit::RegisterType("; + + switch (registerType) + { + case QModbusDataUnit::RegisterType::Invalid: debug << "Invalid"; break; + case QModbusDataUnit::RegisterType::DiscreteInputs: debug << "DiscreteInputs"; break; + case QModbusDataUnit::RegisterType::Coils: debug << "Coils"; break; + case QModbusDataUnit::RegisterType::InputRegisters: debug << "InputRegisters"; break; + case QModbusDataUnit::RegisterType::HoldingRegisters: debug << "HoldingRegisters"; break; + default: debug << int(registerType); break; + } + + return debug << ')'; +} + +QString toString(QModbusDataUnit::RegisterType registerType) +{ + switch (registerType) + { + case QModbusDataUnit::RegisterType::Invalid: return MainWindow::tr("Invalid"); + case QModbusDataUnit::RegisterType::DiscreteInputs: return MainWindow::tr("DiscreteInputs"); + case QModbusDataUnit::RegisterType::Coils: return MainWindow::tr("Coils"); + case QModbusDataUnit::RegisterType::InputRegisters: return MainWindow::tr("InputRegisters"); + case QModbusDataUnit::RegisterType::HoldingRegisters: return MainWindow::tr("HoldingRegisters"); + default: return MainWindow::tr("Unknown RegisterType(%0)").arg(int(registerType)); + } +} +} // namespace diff --git a/mainwindow.h b/mainwindow.h new file mode 100644 index 0000000..923b370 --- /dev/null +++ b/mainwindow.h @@ -0,0 +1,41 @@ +#pragma once + +// system includes +#include + +// Qt includes +#include +#include + +// forward declares +class QModbusTcpClient; +class QModbusReply; +namespace Ui { class MainWindow; } +class ModbusTableModel; + +class MainWindow : public QMainWindow +{ + Q_OBJECT + +public: + explicit MainWindow(QWidget *parent = nullptr); + ~MainWindow() override; + +private slots: + void connectPressed(); + void requestPressed(); + + void modbusErrorOccured(int error); + void modbusStateChanged(int state); + + void replyFinished(); + +private: + void updateRequestFields(); + + const std::unique_ptr m_ui; + const std::unique_ptr m_modbus; + const std::unique_ptr m_model; + std::unique_ptr m_reply; + QElapsedTimer m_timer; +}; diff --git a/mainwindow.ui b/mainwindow.ui new file mode 100644 index 0000000..035844e --- /dev/null +++ b/mainwindow.ui @@ -0,0 +1,335 @@ + + + MainWindow + + + + 0 + 0 + 947 + 736 + + + + Qt Modbus Tester + + + + + + + + + <b>Connection:</b> + + + + + + + Server: + + + lineEditServer + + + + + + + 192.168.0.75 + + + + + + + Port: + + + spinBoxPort + + + + + + + 65535 + + + 502 + + + + + + + Slave: + + + spinBoxSlave + + + + + + + 65535 + + + 0 + + + + + + + Connect + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Connection status + + + + + + + + + + + <b>Error handling:</b> + + + + + + + Timeout: + + + spinBoxTimeout + + + + + + + 65535 + + + + + + + Retries: + + + spinBoxRetries + + + + + + + 65535 + + + + + + + Show messageboxes + + + true + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + <b>Data unit:</b> + + + + + + + Type: + + + comboBoxType + + + + + + + + + + Register: + + + spinBoxRegister + + + + + + + 65535 + + + + + + + Count: + + + spinBoxCount + + + + + + + 65535 + + + 100 + + + + + + + Autorefresh + + + + + + + Delay: + + + spinBoxDelay + + + + + + + false + + + ms + + + 65535 + + + + + + + Request + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Idle + + + + + + + + + + + + + + 0 + 0 + 947 + 20 + + + + + + + + + checkBoxAutorefresh + toggled(bool) + spinBoxDelay + setEnabled(bool) + + + 518 + 103 + + + 630 + 104 + + + + + diff --git a/modbustablemodel.cpp b/modbustablemodel.cpp new file mode 100644 index 0000000..7b33503 --- /dev/null +++ b/modbustablemodel.cpp @@ -0,0 +1,128 @@ +#include "modbustablemodel.h" + +namespace { +enum { + ColumnDecimal, + ColumnHex, + ColumnBinary, + ColumnAscii, + ColumnCount +}; +} + +void ModbusTableModel::setResult(const QModbusDataUnit &result) +{ + beginResetModel(); + m_result = result; + endResetModel(); +} + +QModelIndex ModbusTableModel::index(int row, int column, const QModelIndex &parent) const +{ + Q_ASSERT(!parent.isValid()); + return createIndex(row, column); +} + +QModelIndex ModbusTableModel::parent(const QModelIndex &child) const +{ + return {}; +} + +int ModbusTableModel::rowCount(const QModelIndex &parent) const +{ + Q_ASSERT(!parent.isValid()); + return m_result.isValid() ? m_result.valueCount() : 0; +} + +int ModbusTableModel::columnCount(const QModelIndex &parent) const +{ + Q_ASSERT(!parent.isValid()); + return ColumnCount; +} + +QVariant ModbusTableModel::data(const QModelIndex &index, int role) const +{ + Q_ASSERT(index.isValid()); + Q_ASSERT(m_result.isValid()); + Q_ASSERT(index.row() >= 0); + Q_ASSERT(index.row() < m_result.valueCount()); + Q_ASSERT(index.column() >= 0); + Q_ASSERT(index.column() < ColumnCount); + + const auto &values = m_result.values(); + Q_ASSERT(index.row() < values.size()); + + const auto &value = values[index.row()]; + + switch (role) + { + case Qt::DisplayRole: + switch (index.column()) + { + case ColumnDecimal: return QString::number(value); + case ColumnHex: return QString::number(value, 16).rightJustified(4, '0').insert(2, ' '); + case ColumnBinary: return QString::number(value, 2) .rightJustified(16, '0').insert(8, ' '); + case ColumnAscii: return QString{QChar{(value&0xFF00)>>8}} + QChar{value&0x00FF}; + } + __builtin_unreachable(); + break; + case Qt::EditRole: + switch (index.column()) + { + case ColumnDecimal: + case ColumnHex: + case ColumnBinary: + case ColumnAscii: + return value; + } + __builtin_unreachable(); + break; + } + + return {}; +} + +QVariant ModbusTableModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + switch (orientation) + { + case Qt::Horizontal: + Q_ASSERT(section >= 0); + Q_ASSERT(section < ColumnCount); + + switch (role) + { + case Qt::DisplayRole: + case Qt::EditRole: + switch (section) + { + case ColumnDecimal: return tr("Decimal"); + case ColumnHex: return tr("Hex"); + case ColumnBinary: return tr("Binary"); + case ColumnAscii: return tr("Ascii"); + } + __builtin_unreachable(); + break; + } + + break; + + case Qt::Vertical: + Q_ASSERT(m_result.isValid()); + Q_ASSERT(section >= 0); + Q_ASSERT(section < m_result.valueCount()); + + switch (role) + { + case Qt::DisplayRole: return QString::number(m_result.startAddress() + section); + case Qt::EditRole: return m_result.startAddress() + section; + } + + break; + + default: + __builtin_unreachable(); + } + + return {}; +} diff --git a/modbustablemodel.h b/modbustablemodel.h new file mode 100644 index 0000000..920a62d --- /dev/null +++ b/modbustablemodel.h @@ -0,0 +1,23 @@ +#pragma once + +// Qt includes +#include +#include + +class ModbusTableModel : public QAbstractTableModel +{ +public: + using QAbstractTableModel::QAbstractTableModel; + + void setResult(const QModbusDataUnit &result); + + QModelIndex index(int row, int column, const QModelIndex &parent) const override; + QModelIndex parent(const QModelIndex &child) const override; + int rowCount(const QModelIndex &parent) const override; + int columnCount(const QModelIndex &parent) const override; + QVariant data(const QModelIndex &index, int role) const override; + QVariant headerData(int section, Qt::Orientation orientation, int role) const override; + +private: + QModbusDataUnit m_result; +}; diff --git a/qtmodbustester.pro b/qtmodbustester.pro new file mode 100644 index 0000000..4bb2d8a --- /dev/null +++ b/qtmodbustester.pro @@ -0,0 +1,16 @@ +QT = core network serialbus gui widgets + +CONFIG += c++latest + +DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 + +SOURCES += main.cpp \ + mainwindow.cpp \ + modbustablemodel.cpp + +FORMS += \ + mainwindow.ui + +HEADERS += \ + mainwindow.h \ + modbustablemodel.h