diff --git a/README.md b/README.md index b133effa..002c2f65 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,17 @@ The following simulations are available: Decrement - simulate register from High Limit to Low Limit with a given Step ``` +# Enron Modbus + +Enable **Enron Modbus (32-bit registers)** in **Modbus Definitions → General**. +The setting is saved with the project. + +Select **Enron Registers Int32** or **Enron Registers Float32** and enter the full protocol address +and a count. For example, address **7001**, count **2** exposes **7001–7002**. + +Supported 32-bit ranges are **5001–5999** and **7001–7999**. Choose the display +format separately in the toolbar. Event and history logs are not supported. + # Modbus Logging image @@ -247,14 +258,14 @@ Below are the methods for installing the OpenModSim for different OS ## Microsoft Windows Run the installer: -- For 32-bit Windows: `qt5-omodsim2-2.1-dev.gd72e3cb3_x86.exe` -- For 64-bit Windows: `qt5-omodsim2-2.1-dev.gd72e3cb3_x64.exe` or `qt6-omodsim2-2.1-dev.gd72e3cb3_x64.exe` +- For 32-bit Windows: `qt5-omodsim2-2.2.0_x86.exe` +- For 64-bit Windows: `qt5-omodsim2-2.2.0_x64.exe` or `qt6-omodsim2-2.2.0_x64.exe` ## macOS ### Install Open the DMG package: ```bash -open qt6-omodsim2-2.1-dev.gd72e3cb3_arm64.dmg +open qt6-omodsim2-2.2.0_arm64.dmg ``` Then drag `Open ModSim 2.app` to the `Applications` folder. @@ -278,11 +289,11 @@ rm -rf "/Applications/Open ModSim 2.app" ### Install Install the DEB package from the command line: ```bash -sudo apt install ./qt6-omodsim2_2.1-dev.gd72e3cb3-1_amd64.deb +sudo apt install ./qt6-omodsim2_2.2.0-1_amd64.deb ``` or if you want to use Qt5 libraries: ```bash -sudo apt install ./qt5-omodsim2_2.1-dev.gd72e3cb3-1_amd64.deb +sudo apt install ./qt5-omodsim2_2.2.0-1_amd64.deb ``` ### Remove @@ -299,7 +310,7 @@ sudo apt remove qt5-omodsim2 ### Install Install the RPM package from the command line: ```bash -sudo dnf install ./qt6-omodsim2-2.1-dev.gd72e3cb3-1.x86_64.rpm +sudo dnf install ./qt6-omodsim2-2.2.0-1.x86_64.rpm ``` ### Remove @@ -312,7 +323,7 @@ sudo dnf remove qt6-omodsim2 ### Install Install the RPM package from the command line as root user: ```bash -apt-get install ./qt6-omodsim2-2.1-dev.gd72e3cb3-1.x86_64.rpm +apt-get install ./qt6-omodsim2-2.2.0-1.x86_64.rpm ``` ### Remove @@ -329,7 +340,7 @@ sudo rpm --import qt6-omodsim2.rpm.pubkey ``` Install the RPM package using Zypper: ```bash -sudo zypper install ./qt6-omodsim2-2.1-dev.gd72e3cb3-1.x86_64.rpm +sudo zypper install ./qt6-omodsim2-2.2.0-1.x86_64.rpm ``` ### Remove diff --git a/README.md.in b/README.md.in index 2ce824db..99358157 100644 --- a/README.md.in +++ b/README.md.in @@ -47,6 +47,17 @@ The following simulations are available: Decrement - simulate register from High Limit to Low Limit with a given Step ``` +# Enron Modbus + +Enable **Enron Modbus (32-bit registers)** in **Modbus Definitions → General**. +The setting is saved with the project. + +Select **Enron Registers Int32** or **Enron Registers Float32** and enter the full protocol address +and a count. For example, address **7001**, count **2** exposes **7001–7002**. + +Supported 32-bit ranges are **5001–5999** and **7001–7999**. Choose the display +format separately in the toolbar. Event and history logs are not supported. + # Modbus Logging image diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 415a3132..15385f25 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -2,7 +2,7 @@ cmake_minimum_required(VERSION 3.28.4) set(CMAKE_MIN_REQ_VERSION ${CMAKE_MINIMUM_REQUIRED_VERSION}) project(omodsim - VERSION 2.1.0 + VERSION 2.2.0 DESCRIPTION "An Open Source Modbus Slave (Server) Utility" LANGUAGES CXX) diff --git a/src/applogger.cpp b/src/applogger.cpp index a8487b09..230ff69e 100644 --- a/src/applogger.cpp +++ b/src/applogger.cpp @@ -22,6 +22,26 @@ namespace { +/// +/// \brief Logs logical Enron registers instead of their backing-word storage. +/// +void logUnitMap(bool added, quint8 deviceId, QModbusDataUnit::RegisterType type, + quint16 address, quint16 length, EnronModbus::RegisterType enronType) +{ + QString name = registerTypeName(type); + if (enronType != EnronModbus::RegisterType::None) { + name = enronType == EnronModbus::RegisterType::Group5x + ? QCoreApplication::translate("DataMapDataModel", "Enron Registers Int32") + : QCoreApplication::translate("DataMapDataModel", "Enron Registers Float32"); + address = EnronModbus::registerAddress(enronType, address); + length = (length + 1) / 2; + } + const QString text = added + ? QCoreApplication::translate("MainWindow", "Address space added: unit %1, %2, starting address %3, length %4") + : QCoreApplication::translate("MainWindow", "Address space removed: unit %1, %2, starting address %3, length %4"); + qInfo(lcApp) << text.arg(deviceId).arg(name).arg(address).arg(length); +} + /// /// \brief connectionAddress /// \param cd @@ -295,21 +315,15 @@ void AppLogger::setupModbusMultiServerLogging(ModbusMultiServer& server, QObject }, Qt::DirectConnection); QObject::connect(&server, &ModbusMultiServer::unitMapAdded, context, - [](QUuid /*id*/, quint8 deviceId, QModbusDataUnit::RegisterType type, - quint16 addr, quint16 len) { - qInfo(lcApp) << QCoreApplication::translate( - "MainWindow", - "Address space added: unit %1, %2, starting address %3, length %4") - .arg(deviceId).arg(registerTypeName(type)).arg(addr).arg(len); + [](QUuid, quint8 deviceId, QModbusDataUnit::RegisterType type, + quint16 addr, quint16 len, EnronModbus::RegisterType enronType) { + logUnitMap(true, deviceId, type, addr, len, enronType); }, Qt::DirectConnection); QObject::connect(&server, &ModbusMultiServer::unitMapRemoved, context, - [](QUuid /*id*/, quint8 deviceId, QModbusDataUnit::RegisterType type, - quint16 addr, quint16 len) { - qInfo(lcApp) << QCoreApplication::translate( - "MainWindow", - "Address space removed: unit %1, %2, starting address %3, length %4") - .arg(deviceId).arg(registerTypeName(type)).arg(addr).arg(len); + [](QUuid, quint8 deviceId, QModbusDataUnit::RegisterType type, + quint16 addr, quint16 len, EnronModbus::RegisterType enronType) { + logUnitMap(false, deviceId, type, addr, len, enronType); }, Qt::DirectConnection); QObject::connect(&server, &ModbusMultiServer::descriptionChanged, context, diff --git a/src/cmake/sources.cmake b/src/cmake/sources.cmake index ce3b5c65..940c1ba6 100644 --- a/src/cmake/sources.cmake +++ b/src/cmake/sources.cmake @@ -136,6 +136,7 @@ set(SOURCES # Header files set(HEADERS + enronmodbus.h ansimenu.h ansiutils.h application.h diff --git a/src/controls/modbusmessagewidget.cpp b/src/controls/modbusmessagewidget.cpp index eb173b19..b0b48768 100644 --- a/src/controls/modbusmessagewidget.cpp +++ b/src/controls/modbusmessagewidget.cpp @@ -406,7 +406,7 @@ void ModbusMessageWidget::update() { auto req = reinterpret_cast(_mm.get()); const auto registerAddress = req->isValid() ? formatUInt16Value(_dataType, _showLeadingZeros, req->address()) + QString(" %1").arg(addrBase): "??"; - const auto registerValue = req->isValid() ? formatUInt16Value(_dataType, _showLeadingZeros, req->value()) : "??"; + const auto registerValue = req->isValid() ? formatUInt16Array(_dataType, _showLeadingZeros, req->adu()->pdu().data().mid(2), _byteOrder) : "??"; addField(tr("Register Address"), registerAddress, dataColor); addField(tr("Register Value"), registerValue, dataColor); } @@ -414,7 +414,7 @@ void ModbusMessageWidget::update() { auto resp = reinterpret_cast(_mm.get()); const auto registerAddress = resp->isValid() ? formatUInt16Value(_dataType, _showLeadingZeros, resp->address()) + QString(" %1").arg(addrBase) : "??"; - const auto registerValue = resp->isValid() ? formatUInt16Value(_dataType, _showLeadingZeros, resp->value()) : "??"; + const auto registerValue = resp->isValid() ? formatUInt16Array(_dataType, _showLeadingZeros, resp->adu()->pdu().data().mid(2), _byteOrder) : "??"; addField(tr("Register Address"), registerAddress, dataColor); addField(tr("Register Value"), registerValue, dataColor); } diff --git a/src/controls/outputdatawidget.cpp b/src/controls/outputdatawidget.cpp index e836c9fe..a8035477 100644 --- a/src/controls/outputdatawidget.cpp +++ b/src/controls/outputdatawidget.cpp @@ -10,6 +10,8 @@ #include #include #include +#include +#include #include #include "../styles/appcolors.h" #include "apppreferences.h" @@ -58,6 +60,26 @@ int OutputDataListModel::rowCount(const QModelIndex&) const return _parentWidget->_displayDefinition.Length; } +/// +/// \brief Formats Enron labels as protocol addresses without changing internal item keys. +/// +QString OutputDataListModel::displayAddress(quint32 address) const +{ + const auto& dd = _parentWidget->_displayDefinition; + const bool hex = AppPreferences::instance().globalHexView(); + if (dd.EnronType != EnronModbus::RegisterType::None) { + const int word = int(address) - (_parentWidget->_zeroBasedAddress ? 0 : 1); + const int logical = EnronModbus::registerAddress(dd.EnronType, word); + QString label = hex ? QStringLiteral("0x%1").arg(logical, 4, 16, QLatin1Char('0')).toUpper() + : QString::number(logical); + if (registersCount(_parentWidget->dataType()) == 1) + label += (word - EnronModbus::baseAddress(dd.EnronType)) % 2 ? QStringLiteral(".L") : QStringLiteral(".H"); + return label; + } + return formatAddress(dd.PointType, address, _parentWidget->_addrSpace, hex, + _parentWidget->_zeroBasedAddress ? AddressBase::Base0 : AddressBase::Base1); +} + /// /// \brief OutputDataListModel::data /// \param index @@ -72,9 +94,6 @@ QVariant OutputDataListModel::data(const QModelIndex& index, int role) const } const auto row = index.row(); - const auto pointType = _parentWidget->_displayDefinition.PointType; - const auto addrSpace = _parentWidget->_addrSpace; - const auto hexAddresses = AppPreferences::instance().globalHexView(); const auto itemData = _mapItems.find(row); if (itemData == _mapItems.end()) @@ -108,17 +127,19 @@ QVariant OutputDataListModel::data(const QModelIndex& index, int role) const } } const auto pad = _columnsDistance - descr.length(); - return formatAddress(pointType, itemData->Address, addrSpace, hexAddresses, - _parentWidget->_zeroBasedAddress ? AddressBase::Base0 : AddressBase::Base1) + + return displayAddress(itemData->Address) + QStringLiteral(": ") + itemData->ValueStr + descr + QStringLiteral(" ").repeated(pad); } + case ValueStartRole: + return int(_parentWidget->_displayDefinition.EnronType == EnronModbus::RegisterType::None + || row % registersCount(_parentWidget->dataType()) == 0); + case CaptureRole: return QString(itemData->ValueStr).remove('<').remove('>'); case AddressStringRole: - return formatAddress(pointType, itemData->Address, addrSpace, hexAddresses, - _parentWidget->_zeroBasedAddress ? AddressBase::Base0 : AddressBase::Base1); + return displayAddress(itemData->Address); case AddressRole: return itemData->Address; @@ -248,8 +269,13 @@ QVector OutputDataListModel::values() const /// void OutputDataListModel::clear() { - _mapItems.clear(); - updateData(QModbusDataUnit()); + beginResetModel(); + { + QSignalBlocker blocker(this); + _mapItems.clear(); + updateData(QModbusDataUnit()); + } + endResetModel(); } /// @@ -390,7 +416,7 @@ void OutputDataListModel::updateData(const QModbusDataUnit& data) // Notify changed data only if (first_changed != -1) { - emit dataChanged(index(first_changed), index(last_changed), QVector() << Qt::DisplayRole); + emit dataChanged(index(first_changed), index(last_changed), QVector() << Qt::DisplayRole << ValueStartRole); } } } @@ -457,7 +483,11 @@ OutputDataWidget::OutputDataWidget(QWidget *parent) : ui->listView->setMovement(QListView::Free); ui->listView->setWrapping(true); ui->listView->setFlow(QListView::TopToBottom); - ui->listView->setModel(_listModel.get()); + _visibleModel = new QSortFilterProxyModel(this); + _visibleModel->setSourceModel(_listModel.get()); + _visibleModel->setFilterRole(ValueStartRole); + _visibleModel->setFilterFixedString(QStringLiteral("1")); + ui->listView->setModel(_visibleModel); ui->listView->setItemDelegate(new DataDelegate( this )); setFont(defaultMonospaceFont()); setAutoFillBackground(true); @@ -773,7 +803,7 @@ void OutputDataWidget::setDataViewColumnsDistance(int value) /// int OutputDataWidget::rowCount() const { - return _listModel->rowCount(); + return _visibleModel->rowCount(); } /// @@ -793,11 +823,11 @@ int OutputDataWidget::paint(const QRect& rc, QPainter& painter, int startRow) int cx = rc.left(); int maxWidth = 0; - const int total = _listModel->rowCount(); + const int total = _visibleModel->rowCount(); for (int i = startRow; i < total; ++i) { - const auto text = _listModel->data(_listModel->index(i), Qt::DisplayRole).toString().trimmed(); + const auto text = _visibleModel->data(_visibleModel->index(i, 0), Qt::DisplayRole).toString().trimmed(); auto rcItem = painter.boundingRect(cx, cy, rc.right() - cx, rc.bottom() - cy, Qt::TextSingleLine, text); maxWidth = qMax(maxWidth, rcItem.width()); @@ -1072,7 +1102,8 @@ void OutputDataWidget::rebuildFindMatches(const QString& text) for (int row = 0; row < rows; ++row) { const QModelIndex idx = _listModel->index(row, 0); - if (_listModel->data(idx, ValueRole).toUInt() == targetValue) + if (_visibleModel->mapFromSource(idx).isValid() + && _listModel->data(idx, ValueRole).toUInt() == targetValue) _findRows.append(row); } @@ -1118,8 +1149,9 @@ void OutputDataWidget::focusCurrentFindMatch() if (!idx.isValid()) return; - ui->listView->setCurrentIndex(idx); - ui->listView->scrollTo(idx, QAbstractItemView::PositionAtCenter); + const auto visibleIndex = _visibleModel->mapFromSource(idx); + ui->listView->setCurrentIndex(visibleIndex); + ui->listView->scrollTo(visibleIndex, QAbstractItemView::PositionAtCenter); } /// @@ -1320,18 +1352,19 @@ static QIcon drawRemoveColorIcon(int size = 16) /// void OutputDataWidget::on_listView_customContextMenuRequested(const QPoint &pos) { - QModelIndex index = ui->listView->indexAt(pos); - if (!index.isValid()) + const auto visibleIndex = ui->listView->indexAt(pos); + if (!visibleIndex.isValid()) return; + const auto index = _visibleModel->mapToSource(visibleIndex); QMenu menu(this); const auto idx = getValueIndex(index); const auto address = _listModel->data(idx, AddressStringRole).toString(); QAction* writeValueAction = menu.addAction(tr("Set Value of %1").arg(address)); - connect(writeValueAction, &QAction::triggered, this, [this, index](){ - emit ui->listView->doubleClicked(index); + connect(writeValueAction, &QAction::triggered, this, [this, visibleIndex](){ + emit ui->listView->doubleClicked(visibleIndex); }); const quint16 addrKey = static_cast( @@ -1405,7 +1438,8 @@ void OutputDataWidget::on_listView_doubleClicked(const QModelIndex& index) /// QModelIndex OutputDataWidget::getValueIndex(const QModelIndex& index) const { - QModelIndex idx = index; + const auto sourceIndex = index.model() == _visibleModel ? _visibleModel->mapToSource(index) : index; + QModelIndex idx = sourceIndex; switch(_displayDefinition.PointType) { case QModbusDataUnit::HoldingRegisters: @@ -1416,15 +1450,15 @@ QModelIndex OutputDataWidget::getValueIndex(const QModelIndex& index) const case DataType::Float32: case DataType::Int32: case DataType::UInt32: - if(index.row() % 2) - idx = _listModel->index(index.row() - 1); + if(sourceIndex.row() % 2) + idx = _listModel->index(sourceIndex.row() - 1); break; case DataType::Float64: case DataType::Int64: case DataType::UInt64: - if(index.row() % 4) - idx = _listModel->index(index.row() - index.row() % 4); + if(sourceIndex.row() % 4) + idx = _listModel->index(sourceIndex.row() - sourceIndex.row() % 4); break; default: diff --git a/src/controls/outputdatawidget.h b/src/controls/outputdatawidget.h index 84331235..1df676ce 100644 --- a/src/controls/outputdatawidget.h +++ b/src/controls/outputdatawidget.h @@ -22,6 +22,7 @@ class QLabel; class QPainter; class QTimer; +class QSortFilterProxyModel; namespace Ui { class OutputDataWidget; @@ -37,7 +38,8 @@ enum OutputDataRole ValueRole = Qt::UserRole + 6, ColorRole = Qt::UserRole + 7, FindMatchRole = Qt::UserRole + 8, - FindCurrentRole = Qt::UserRole + 9 + FindCurrentRole = Qt::UserRole + 9, + ValueStartRole = Qt::UserRole + 10 }; class OutputDataWidget; @@ -90,6 +92,7 @@ class OutputDataListModel : public QAbstractListModel private: SimulationIconType simulationIcon(int row) const; + QString displayAddress(quint32 address) const; private: struct ItemData @@ -237,6 +240,7 @@ private slots: AddressSpace _addrSpace = AddressSpace::Addr6Digits; AddressColorMap _colorMap; QSharedPointer _listModel; + QSortFilterProxyModel* _visibleModel = nullptr; QString _findText; QVector _findRows; int _currentFindIndex = -1; diff --git a/src/controls/pointtypecombobox.cpp b/src/controls/pointtypecombobox.cpp index 13d4fe23..c56422e7 100644 --- a/src/controls/pointtypecombobox.cpp +++ b/src/controls/pointtypecombobox.cpp @@ -36,6 +36,12 @@ void PointTypeComboBox::changeEvent(QEvent* event) { for(int i = 0; i < count(); i++) { + const auto enron = static_cast(itemData(i, Qt::UserRole + 1).toInt()); + if (enron != EnronModbus::RegisterType::None) { + setItemText(i, enron == EnronModbus::RegisterType::Group5x + ? tr("Enron Registers Int32 (Read-Write)") : tr("Enron Registers Float32 (Read-Write)")); + continue; + } switch(itemData(i).value()) { case QModbusDataUnit::Coils: @@ -90,3 +96,51 @@ void PointTypeComboBox::on_currentIndexChanged(int index) emit pointTypeChanged(itemData(index).value()); } + +/// +/// \brief Returns the Enron group selected for the holding-register table. +/// +EnronModbus::RegisterType PointTypeComboBox::currentEnronType() const +{ + return static_cast(currentData(Qt::UserRole + 1).toInt()); +} + +/// +/// \brief Selects an Enron group without introducing a separate Modbus table. +/// +void PointTypeComboBox::setCurrentEnronType(EnronModbus::RegisterType type) +{ + if (type == EnronModbus::RegisterType::None) { + if (currentEnronType() != EnronModbus::RegisterType::None) + setCurrentPointType(currentPointType()); + return; + } + const int index = findData(int(type), Qt::UserRole + 1); + if (index >= 0) + setCurrentIndex(index); +} + +/// +/// \brief Adds or removes Enron choices to match the server protocol. +/// +void PointTypeComboBox::setEnronEnabled(bool enabled) +{ + const bool present = findData(int(EnronModbus::RegisterType::Group5x), Qt::UserRole + 1) >= 0; + if (enabled == present) + return; + + if (enabled) { + addItem(tr("Enron Registers Int32 (Read-Write)"), QModbusDataUnit::HoldingRegisters); + setItemData(count() - 1, int(EnronModbus::RegisterType::Group5x), Qt::UserRole + 1); + addItem(tr("Enron Registers Float32 (Read-Write)"), QModbusDataUnit::HoldingRegisters); + setItemData(count() - 1, int(EnronModbus::RegisterType::Group7x), Qt::UserRole + 1); + return; + } + + if (currentEnronType() != EnronModbus::RegisterType::None) + setCurrentPointType(QModbusDataUnit::HoldingRegisters); + for (int i = count() - 1; i >= 0; --i) { + if (itemData(i, Qt::UserRole + 1).toInt() != 0) + removeItem(i); + } +} diff --git a/src/controls/pointtypecombobox.h b/src/controls/pointtypecombobox.h index 2b971787..cea2b6e3 100644 --- a/src/controls/pointtypecombobox.h +++ b/src/controls/pointtypecombobox.h @@ -11,6 +11,7 @@ #include #include +#include "enronmodbus.h" /// /// \brief The PointTypeComboBox class @@ -24,6 +25,19 @@ class PointTypeComboBox : public QComboBox QModbusDataUnit::RegisterType currentPointType() const; void setCurrentPointType(QModbusDataUnit::RegisterType pointType); + /// + /// \brief Returns the Enron group selected for the holding-register table. + /// + EnronModbus::RegisterType currentEnronType() const; + /// + /// \brief Selects an Enron group without introducing a separate Modbus table. + /// + void setCurrentEnronType(EnronModbus::RegisterType type); + /// + /// \brief Adds or removes Enron choices to match the server protocol. + /// + void setEnronEnabled(bool enabled); + signals: void pointTypeChanged(QModbusDataUnit::RegisterType value); diff --git a/src/datamapdatamodel.cpp b/src/datamapdatamodel.cpp index 5972685b..968073fe 100644 --- a/src/datamapdatamodel.cpp +++ b/src/datamapdatamodel.cpp @@ -93,10 +93,9 @@ bool rangesOverlap(quint16 firstStart, int firstLength, quint16 secondStart, int /// /// \brief timestampForKey /// -QDateTime timestampForKey(ModbusMultiServer& server, const ItemMapKey& key, DataType type) +QDateTime timestampForKey(ModbusMultiServer& server, const ItemMapKey& key, int count) { QDateTime result; - const int count = registersCount(type); for (int i = 0; i < count; ++i) { const auto timestamp = server.timestamp(key.DeviceId, key.Type, static_cast(key.Address + i)); if (timestamp.isValid() && (!result.isValid() || timestamp > result)) @@ -206,7 +205,63 @@ QString formatValue(QModbusDataUnit::RegisterType regType, DataMapDataModel::DataMapDataModel(ModbusMultiServer& server, QObject* parent) : QAbstractTableModel(parent) , _server(server) + , _enronEnabled(server.getModbusDefinitions().EnronModbus) +{ + connect(&_server, &ModbusMultiServer::definitionsChanged, this, &DataMapDataModel::onDefinitionsChanged); +} + +/// +/// \brief Refreshes Enron choices and labels when the server protocol changes. +/// +void DataMapDataModel::onDefinitionsChanged(const ModbusDefinitions& definitions) +{ + if (_enronEnabled == definitions.EnronModbus) + return; + _enronEnabled = definitions.EnronModbus; + for (auto it = _data.cbegin(); it != _data.cend(); ++it) + registerEntry(it.key(), it.value()); + if (!_keys.isEmpty()) + emit dataChanged(index(0, 0), index(_keys.size() - 1, ColCount - 1)); + emit enronEnabledChanged(_enronEnabled); +} + +/// +/// \brief Retains saved Enron metadata while ordinary Modbus is active. +/// +EnronModbus::RegisterType DataMapDataModel::effectiveEnronType(const DataMapEntry& entry) const { + return _enronEnabled ? entry.EnronType : EnronModbus::RegisterType::None; +} + +/// +/// \brief Maps register choices to native tables and Enron backing-word ranges. +/// +bool DataMapDataModel::changeRegisterType(ItemMapKey& key, DataMapEntry& entry, const QVariant& value, int role) const +{ + auto enron = EnronModbus::RegisterType::None; + if (role == DataMapRole::EnronType) + enron = static_cast(value.toInt()); + else if (value.toString() == tr("Enron Registers Int32")) + enron = EnronModbus::RegisterType::Group5x; + else if (value.toString() == tr("Enron Registers Float32")) + enron = EnronModbus::RegisterType::Group7x; + if (role == DataMapRole::EnronType || enron != EnronModbus::RegisterType::None) { + if (!_enronEnabled || (enron != EnronModbus::RegisterType::Group5x && enron != EnronModbus::RegisterType::Group7x)) + return false; + key.Type = QModbusDataUnit::HoldingRegisters; + if (entry.EnronType != enron) { + key.Address = EnronModbus::baseAddress(enron); + entry.type = enron == EnronModbus::RegisterType::Group5x ? DataType::Int32 : DataType::Float32; + entry.order = RegisterOrder::MSRF; + entry.byteOrder = ByteOrder::Direct; + } + entry.EnronType = enron; + } else { + entry.EnronType = EnronModbus::RegisterType::None; + key.Type = role == DataMapRole::TypeValue ? static_cast(value.toInt()) + : stringToRegisterType(value.toString()); + } + return true; } /// @@ -261,6 +316,11 @@ QVariant DataMapDataModel::data(const QModelIndex& index, int role) const return {}; } + if (role == DataMapRole::EnronEnabled) + return _enronEnabled; + if (role == DataMapRole::EnronType) + return int(effectiveEnronType(e)); + switch (col) { case ColUnit: if (role == Qt::DisplayRole || role == Qt::EditRole) @@ -271,14 +331,21 @@ QVariant DataMapDataModel::data(const QModelIndex& index, int role) const break; case ColType: - if (role == Qt::DisplayRole || role == Qt::EditRole) + if (role == Qt::DisplayRole || role == Qt::EditRole) { + const auto enron = effectiveEnronType(e); + if (enron != EnronModbus::RegisterType::None) + return enron == EnronModbus::RegisterType::Group5x ? tr("Enron Registers Int32") : tr("Enron Registers Float32"); return registerTypeToString(key.Type); + } if (role == DataMapRole::TypeValue) return static_cast(key.Type); break; case ColAddress: - if (role == Qt::DisplayRole || role == Qt::EditRole) - return addressToDisplay(key.Address, _zeroBased); + if (role == Qt::DisplayRole || role == Qt::EditRole) { + const auto enron = effectiveEnronType(e); + return enron == EnronModbus::RegisterType::None ? addressToDisplay(key.Address, _zeroBased) + : QString::number(EnronModbus::registerAddress(enron, key.Address)); + } break; case ColDataType: @@ -434,7 +501,7 @@ bool DataMapDataModel::setData(const QModelIndex& index, const QVariant& value, _server.setData(oldKey.DeviceId, unit, WriteSource::User); entry.value = regs[0]; - entry.timestamp = timestampForKey(_server, oldKey, entry.type); + entry.timestamp = timestampForKey(_server, oldKey, entry.registerCount()); _data[oldKey] = entry; } // Always refresh the cell (restores previous value if parse failed) @@ -481,24 +548,36 @@ bool DataMapDataModel::setData(const QModelIndex& index, const QVariant& value, } // ── Key columns: Unit / Type / Address ──────────────────────────────────── - else if ((col == ColUnit || col == ColType || col == ColAddress) && (role == Qt::EditRole || role == Qt::DisplayRole || role == DataMapRole::TypeValue)) { + else if ((col == ColUnit || col == ColType || col == ColAddress) && (role == Qt::EditRole || role == Qt::DisplayRole || role == DataMapRole::TypeValue || role == DataMapRole::EnronType)) { ItemMapKey newKey = oldKey; if (col == ColUnit) { newKey.DeviceId = static_cast(value.toString().toUShort()); } else if (col == ColType) { - if (role == DataMapRole::TypeValue) - newKey.Type = static_cast(value.toInt()); - else - newKey.Type = stringToRegisterType(value.toString()); + if (!changeRegisterType(newKey, entry, value, role)) { + _inSetData = false; + return false; + } + } else if (effectiveEnronType(entry) != EnronModbus::RegisterType::None) { + bool ok; + const int address = value.toString().toInt(&ok); + const int base = EnronModbus::baseAddress(entry.EnronType); + if (!ok || address < base || address >= base + EnronModbus::RangeLength) { + _inSetData = false; + return false; + } + newKey.Address = EnronModbus::wordAddress(entry.EnronType, address); } else { newKey.Address = addressFromDisplay(value.toString(), _zeroBased); + if (newKey.Address != oldKey.Address) + entry.EnronType = EnronModbus::RegisterType::None; } if (newKey.DeviceId == oldKey.DeviceId && newKey.Type == oldKey.Type && newKey.Address == oldKey.Address) { - // No actual change - still refresh display (in case role was redundant write) + _data[oldKey] = entry; + registerEntry(oldKey, entry); emit dataChanged(createIndex(row, ColUnit), createIndex(row, ColTimestamp)); handled = true; } else { @@ -517,7 +596,7 @@ bool DataMapDataModel::setData(const QModelIndex& index, const QVariant& value, const auto unit = _server.data(newKey.DeviceId, newKey.Type, newKey.Address, 1); entry.value = unit.isValid() ? static_cast(unit.value(0)) : 0; - entry.timestamp = timestampForKey(_server, newKey, entry.type); + entry.timestamp = timestampForKey(_server, newKey, entry.registerCount()); entry.comment = _server.description(newKey.DeviceId, newKey.Type, newKey.Address); _keys[row] = newKey; @@ -527,7 +606,7 @@ bool DataMapDataModel::setData(const QModelIndex& index, const QVariant& value, // Timestamp for a new key/type can become available only after // registerEntry() has updated backend unit maps. if (!_data[newKey].timestamp.isValid()) { - const auto backendTsAfterRegister = timestampForKey(_server, newKey, _data[newKey].type); + const auto backendTsAfterRegister = timestampForKey(_server, newKey, _data[newKey].registerCount()); if (backendTsAfterRegister.isValid()) _data[newKey].timestamp = backendTsAfterRegister; } @@ -744,7 +823,16 @@ ItemMapKey DataMapDataModel::lastKey() const void DataMapDataModel::addEntry(const ItemMapKey& key, const DataMapEntry& entry) { DataMapEntry normalizedEntry = entry; - const auto backendTs = timestampForKey(_server, key, normalizedEntry.type); + if (key.Type != QModbusDataUnit::HoldingRegisters + || normalizedEntry.EnronType < EnronModbus::RegisterType::None + || normalizedEntry.EnronType > EnronModbus::RegisterType::Group7x) + normalizedEntry.EnronType = EnronModbus::RegisterType::None; + if (normalizedEntry.EnronType != EnronModbus::RegisterType::None) { + const int base = EnronModbus::baseAddress(normalizedEntry.EnronType); + if (key.Address < base || key.Address >= base + 2 * EnronModbus::RangeLength || (key.Address - base) % 2) + normalizedEntry.EnronType = EnronModbus::RegisterType::None; + } + const auto backendTs = timestampForKey(_server, key, normalizedEntry.registerCount()); if (backendTs.isValid()) { normalizedEntry.timestamp = backendTs; } else if (normalizedEntry.timestamp.isValid()) { @@ -778,7 +866,7 @@ void DataMapDataModel::addEntry(const ItemMapKey& key, const DataMapEntry& entry // For freshly added keys, backend timestamp can become available only after // the unit map is registered in registerEntry(). if (!normalizedEntry.timestamp.isValid()) { - const auto backendTsAfterRegister = timestampForKey(_server, key, normalizedEntry.type); + const auto backendTsAfterRegister = timestampForKey(_server, key, normalizedEntry.registerCount()); if (backendTsAfterRegister.isValid()) { _data[key].timestamp = backendTsAfterRegister; const int insertedRow = rowForKey(key); @@ -803,7 +891,7 @@ void DataMapDataModel::removeEntries(QList sourceRows) endRemoveRows(); if (!hasAnyRowWithKey(key)) { - const auto count = static_cast(registersCount(_data.value(key).type)); + const auto count = static_cast(_data.value(key).registerCount()); unregisterEntry(key); _data.remove(key); _server.clearUnmappedData(key.DeviceId, key.Type, key.Address, count); @@ -833,7 +921,7 @@ void DataMapDataModel::applyMbDataChange(quint8 deviceId, const QModbusDataUnit& if (it == _data.end()) continue; - if (!rangesOverlap(key.Address, registersCount(it->type), dataStart, dataCount)) + if (!rangesOverlap(key.Address, it->registerCount(), dataStart, dataCount)) continue; const int offset = static_cast(key.Address) - static_cast(dataStart); @@ -872,10 +960,10 @@ void DataMapDataModel::applyTimestampChange(quint8 deviceId, QModbusDataUnit::Re if (it == _data.end()) continue; - if (!rangesOverlap(key.Address, registersCount(it->type), address, 1)) + if (!rangesOverlap(key.Address, it->registerCount(), address, 1)) continue; - const auto rowTimestamp = timestampForKey(_server, key, it->type); + const auto rowTimestamp = timestampForKey(_server, key, it->registerCount()); if (it->timestamp == rowTimestamp) continue; @@ -900,7 +988,7 @@ void DataMapDataModel::refreshTimestamps() if (it == _data.end()) continue; - const auto rowTimestamp = timestampForKey(_server, key, it->type); + const auto rowTimestamp = timestampForKey(_server, key, it->registerCount()); if (it->timestamp == rowTimestamp) continue; @@ -1005,7 +1093,7 @@ void DataMapDataModel::registerEntry(const ItemMapKey& key, const DataMapEntry& _server.addDeviceId(key.DeviceId); } _server.addUnitMap(_uuids[key], key.DeviceId, key.Type, - key.Address, registersCount(entry.type)); + key.Address, entry.registerCount(), effectiveEnronType(entry)); } /// @@ -1042,10 +1130,11 @@ void DataMapFilterProxy::setFilterUnit(int unit) } /// -/// \brief DataMapFilterProxy::setFilterTypeIndex +/// \brief Selects all rows (0), ordinary tables (1–4), or Enron groups (5–6). /// void DataMapFilterProxy::setFilterTypeIndex(int index) { + index = qBound(0, index, 6); if (_filterTypeIndex == index) return; _filterTypeIndex = index; invalidateFilter(); @@ -1065,6 +1154,11 @@ bool DataMapFilterProxy::filterAcceptsRow(int sourceRow, const QModelIndex& /*so return false; if (_filterTypeIndex != 0) { + const int enron = src->data(src->index(sourceRow, ColType), DataMapRole::EnronType).toInt(); + if (_filterTypeIndex >= 5) + return enron == _filterTypeIndex - 4; + if (enron != 0) + return false; static const QModbusDataUnit::RegisterType types[] = { QModbusDataUnit::Coils, QModbusDataUnit::DiscreteInputs, diff --git a/src/datamapdatamodel.h b/src/datamapdatamodel.h index 5bbba128..9624cc74 100644 --- a/src/datamapdatamodel.h +++ b/src/datamapdatamodel.h @@ -21,6 +21,7 @@ #include "modbusmultiserver.h" #include "controls/outputtypes.h" #include "enums.h" +#include "enronmodbus.h" /// /// \brief Column indices for the DataMap table (shared between model and delegates) @@ -47,6 +48,8 @@ namespace DataMapRole { constexpr int Address = Qt::UserRole + 2; // ColUnit: Address as int constexpr int TypeValue = Qt::UserRole + 3; // ColType: RegisterType as int constexpr int RowColor = Qt::UserRole + 4; // any column: row highlight color as QColor + constexpr int EnronType = Qt::UserRole + 5; + constexpr int EnronEnabled = Qt::UserRole + 6; // Qt::UserRole on ColValue: raw quint16 value } @@ -62,6 +65,14 @@ struct DataMapEntry ByteOrder byteOrder = ByteOrder::Direct; QDateTime timestamp; QColor color; + EnronModbus::RegisterType EnronType = EnronModbus::RegisterType::None; + + /// + /// \brief Keeps both words of an Enron register mapped even in a 16-bit display format. + /// + int registerCount() const { + return qMax(EnronType == EnronModbus::RegisterType::None ? 1 : 2, registersCount(type)); + } }; /// @@ -117,7 +128,16 @@ class DataMapDataModel : public QAbstractTableModel // Refresh Unit and Address column display void refreshUnitAndAddressColumns(); +signals: + /// + /// \brief Refreshes protocol-dependent register choices in map editors and filters. + /// + void enronEnabledChanged(bool enabled); + private: + void onDefinitionsChanged(const ModbusDefinitions& definitions); + bool changeRegisterType(ItemMapKey& key, DataMapEntry& entry, const QVariant& value, int role) const; + EnronModbus::RegisterType effectiveEnronType(const DataMapEntry& entry) const; int rowForKey(const ItemMapKey& key) const; bool hasAnyRowWithKey(const ItemMapKey& key, int exceptRow = -1) const; void registerEntry(const ItemMapKey& key, const DataMapEntry& entry); @@ -127,6 +147,7 @@ class DataMapDataModel : public QAbstractTableModel QList _keys; QMap _data; QMap _uuids; + bool _enronEnabled = false; bool _inSetData = false; bool _zeroBased = false; bool _hexView = false; @@ -143,7 +164,10 @@ class DataMapFilterProxy : public QSortFilterProxyModel explicit DataMapFilterProxy(QObject* parent = nullptr); void setFilterUnit(int unit); // 0 = show all - void setFilterTypeIndex(int index); // 0 = show all, 1-4 = Coils/Discrete/Input/Holding + /// + /// \brief Selects all rows (0), ordinary tables (1–4), or Enron groups (5–6). + /// + void setFilterTypeIndex(int index); protected: bool filterAcceptsRow(int sourceRow, const QModelIndex& sourceParent) const override; diff --git a/src/dialogs/dialogmodbusdefinitions.cpp b/src/dialogs/dialogmodbusdefinitions.cpp index b2879ce0..b66b432e 100644 --- a/src/dialogs/dialogmodbusdefinitions.cpp +++ b/src/dialogs/dialogmodbusdefinitions.cpp @@ -133,6 +133,7 @@ void DialogModbusDefinitions::apply() { ModbusDefinitions defs; defs.AddrSpace = ui->comboBoxAddrSpace->currentAddressSpace(); + defs.EnronModbus = ui->checkBoxEnronModbus->isChecked(); defs.UseGlobalUnitMap = ui->checkBoxGlobalMap->isChecked(); defs.AutoAddRegistersOnRequest = ui->checkBoxAutoAddRegistersOnRequest->isChecked(); defs.ErrorSimulations.setResponseIncorrectId(ui->checkBoxErrIncorrentId->isChecked()); @@ -156,6 +157,7 @@ void DialogModbusDefinitions::apply() /// void DialogModbusDefinitions::updateModbusDefinitions(const ModbusDefinitions& defs) { + ui->checkBoxEnronModbus->setChecked(defs.EnronModbus); ui->checkBoxGlobalMap->setChecked(defs.UseGlobalUnitMap); ui->checkBoxAutoAddRegistersOnRequest->setChecked(defs.AutoAddRegistersOnRequest); ui->comboBoxAddrSpace->setCurrentAddressSpace(defs.AddrSpace); diff --git a/src/dialogs/dialogmodbusdefinitions.ui b/src/dialogs/dialogmodbusdefinitions.ui index e9357cd4..88e45730 100644 --- a/src/dialogs/dialogmodbusdefinitions.ui +++ b/src/dialogs/dialogmodbusdefinitions.ui @@ -58,6 +58,16 @@ + + + + Select Enron Int32 or Float32 registers and enter the full protocol address, starting at 5001 or 7001. Select the display format separately in the toolbar. + + + Enable Enron Modbus (32-bit registers) + + + diff --git a/src/dialogs/dialogwriteregister.cpp b/src/dialogs/dialogwriteregister.cpp index a886c0c7..e0be8c1b 100644 --- a/src/dialogs/dialogwriteregister.cpp +++ b/src/dialogs/dialogwriteregister.cpp @@ -71,7 +71,17 @@ DialogWriteRegister::DialogWriteRegister(ModbusWriteParams& params, QModbusDataU ui->lineEditAddress->setLeadingZeroes(hexView || params.LeadingZeros); ui->lineEditAddress->setInputMode(hexView ? NumericLineEdit::HexMode : NumericLineEdit::Int32Mode); ui->lineEditAddress->setInputRange(ModbusLimits::addressRange(params.AddrSpace, params.ZeroBasedAddress)); - ui->lineEditAddress->setValue(params.Address); + if (params.EnronType == EnronModbus::RegisterType::None) { + ui->lineEditAddress->setValue(params.Address); + } else { + const int word = params.Address - (params.ZeroBasedAddress ? 0 : 1); + _enronWordOffset = (word - EnronModbus::baseAddress(params.EnronType)) % 2; + const int base = EnronModbus::baseAddress(params.EnronType); + ui->lineEditAddress->setInputRange(base, base + EnronModbus::RangeLength - 1); + ui->lineEditAddress->setValue(EnronModbus::registerAddress(params.EnronType, word)); + if (registersCount(params.DataMode) == 1) + ui->labelAddress->setText(_enronWordOffset ? tr("Address (low word):") : tr("Address (high word):")); + } if(_dataSimulator != nullptr) { @@ -204,7 +214,7 @@ void DialogWriteRegister::changeEvent(QEvent* event) /// void DialogWriteRegister::accept() { - _writeParams.Address = ui->lineEditAddress->value(); + _writeParams.Address = registerAddress() + (_writeParams.ZeroBasedAddress ? 0 : 1); _writeParams.Value = ui->lineEditValue->value(); _writeParams.DeviceId = ui->lineEditNode->value(); @@ -247,7 +257,7 @@ void DialogWriteRegister::updateValue() if(srv == nullptr) return; const quint8 deviceId = ui->lineEditNode->value(); - const int simAddr = ui->lineEditAddress->value() - (_writeParams.ZeroBasedAddress ? 0 : 1); + const int simAddr = registerAddress(); const int count = registersCount(_writeParams.DataMode); const auto regs = srv->data(deviceId, _type, simAddr, count).values(); const auto value = makeValue(regs, _writeParams.DataMode, _writeParams.RegOrder, _writeParams.Order); @@ -256,14 +266,13 @@ void DialogWriteRegister::updateValue() /// /// \brief DialogWriteRegister::on_lineEditAddress_valueChanged -/// \param value /// -void DialogWriteRegister::on_lineEditAddress_valueChanged(const QVariant& value) +void DialogWriteRegister::on_lineEditAddress_valueChanged(const QVariant&) { if(_dataSimulator != nullptr) { const quint8 deviceId = ui->lineEditNode->value(); - const int simAddr = value.toInt() - (_writeParams.ZeroBasedAddress ? 0 : 1); + const int simAddr = registerAddress(); _simParams = _dataSimulator->simulationParams(deviceId, _type, simAddr); if(!_dataSimulator->canStartSimulation(_writeParams.DataMode, deviceId, _type, simAddr)) { _simParams.Mode = SimulationMode::Disabled; @@ -282,7 +291,7 @@ void DialogWriteRegister::on_lineEditNode_valueChanged(const QVariant& value) if(_dataSimulator != nullptr) { const quint8 deviceId = value.toUInt(); - const int simAddr = ui->lineEditAddress->value() - (_writeParams.ZeroBasedAddress ? 0 : 1); + const int simAddr = registerAddress(); _simParams = _dataSimulator->simulationParams(deviceId, _type, simAddr); if(!_dataSimulator->canStartSimulation(_writeParams.DataMode, deviceId, _type, simAddr)) { _simParams.Mode = SimulationMode::Disabled; @@ -303,7 +312,7 @@ void DialogWriteRegister::on_pushButtonSimulation_clicked() if(dlg.exec() == QDialog::Accepted) { const quint8 deviceId = ui->lineEditNode->value(); - const int simAddr = ui->lineEditAddress->value() - (_writeParams.ZeroBasedAddress ? 0 : 1); + const int simAddr = registerAddress(); if(_simParams.Mode == SimulationMode::Off) _dataSimulator->stopSimulation(deviceId, _type, simAddr); else @@ -312,3 +321,14 @@ void DialogWriteRegister::on_pushButtonSimulation_clicked() } } + +/// +/// \brief Resolves the address editor to the word address used by writes and simulation. +/// +int DialogWriteRegister::registerAddress() const +{ + const int address = ui->lineEditAddress->value(); + if (_writeParams.EnronType != EnronModbus::RegisterType::None) + return EnronModbus::wordAddress(_writeParams.EnronType, address) + _enronWordOffset; + return address - (_writeParams.ZeroBasedAddress ? 0 : 1); +} diff --git a/src/dialogs/dialogwriteregister.h b/src/dialogs/dialogwriteregister.h index e38ac17a..b44c6f2f 100644 --- a/src/dialogs/dialogwriteregister.h +++ b/src/dialogs/dialogwriteregister.h @@ -46,6 +46,7 @@ private slots: private: void updateSimulationButton(); + int registerAddress() const; void updateValue(); private: @@ -54,6 +55,7 @@ private slots: private: ModbusWriteParams& _writeParams; QModbusDataUnit::RegisterType _type; + int _enronWordOffset = 0; ModbusSimulationParams _simParams; DataSimulator* _dataSimulator; }; diff --git a/src/displaydefinition.h b/src/displaydefinition.h index 06f09f6e..d73b017a 100644 --- a/src/displaydefinition.h +++ b/src/displaydefinition.h @@ -14,6 +14,7 @@ #include #include #include "modbuslimits.h" +#include "enronmodbus.h" #include "modbusserver.h" #include "scriptsettings.h" @@ -27,6 +28,7 @@ struct DataViewDefinitions quint16 PointAddress = 1; QModbusDataUnit::RegisterType PointType = QModbusDataUnit::HoldingRegisters; quint16 Length = 100; + EnronModbus::RegisterType EnronType = EnronModbus::RegisterType::None; quint16 DataViewColumnsDistance = 16; bool LeadingZeros = true; @@ -38,6 +40,9 @@ struct DataViewDefinitions // address base, which this struct does not know. FormDataView clamps it // against the current base when the definition is applied. PointType = qBound(QModbusDataUnit::DiscreteInputs, PointType, QModbusDataUnit::HoldingRegisters); + if (PointType != QModbusDataUnit::HoldingRegisters + || EnronType < EnronModbus::RegisterType::None || EnronType > EnronModbus::RegisterType::Group7x) + EnronType = EnronModbus::RegisterType::None; Length = qBound(ModbusLimits::lengthRange().from(), Length, static_cast(ModbusLimits::lengthRange().to())); @@ -51,6 +56,7 @@ struct DataViewDefinitions && PointAddress == o.PointAddress && PointType == o.PointType && Length == o.Length + && EnronType == o.EnronType && DataViewColumnsDistance == o.DataViewColumnsDistance && LeadingZeros == o.LeadingZeros; } @@ -146,6 +152,7 @@ inline QSettings& operator <<(QSettings& out, const DataViewDefinitions& dd) out.setValue("DataViewDefinitions/DeviceId", dd.DeviceId); out.setValue("DataViewDefinitions/PointAddress", dd.PointAddress); out.setValue("DataViewDefinitions/PointType", dd.PointType); + out.setValue("DataViewDefinitions/EnronType", static_cast(dd.EnronType)); out.setValue("DataViewDefinitions/Length", dd.Length); out.setValue("DataViewDefinitions/DataViewColumnSpace", dd.DataViewColumnsDistance); out.setValue("DataViewDefinitions/LeadingZeros", dd.LeadingZeros); @@ -194,6 +201,7 @@ inline QSettings& operator >>(QSettings& in, DataViewDefinitions& dd) dd.DeviceId = in.value("DataViewDefinitions/DeviceId", 1).toUInt(); dd.PointAddress = in.value("DataViewDefinitions/PointAddress", 1).toUInt(); dd.PointType = (QModbusDataUnit::RegisterType)in.value("DataViewDefinitions/PointType", 4).toUInt(); + dd.EnronType = static_cast(in.value("DataViewDefinitions/EnronType", 0).toInt()); dd.Length = in.value("DataViewDefinitions/Length", 100).toUInt(); dd.DataViewColumnsDistance = in.value("DataViewDefinitions/DataViewColumnSpace", 16).toUInt(); dd.LeadingZeros = in.value("DataViewDefinitions/LeadingZeros", true).toBool(); @@ -247,6 +255,8 @@ inline QXmlStreamWriter& operator <<(QXmlStreamWriter& xml, const DataViewDefini xml.writeAttribute("DeviceId", QString::number(dd.DeviceId)); xml.writeAttribute("PointType", enumToString(dd.PointType)); xml.writeAttribute("PointAddress", QString::number(dd.PointAddress)); + if (dd.EnronType != EnronModbus::RegisterType::None) + xml.writeAttribute("EnronType", QString::number(static_cast(dd.EnronType))); xml.writeAttribute("Length", QString::number(dd.Length)); xml.writeAttribute("DataViewColumnsDistance", QString::number(dd.DataViewColumnsDistance)); xml.writeAttribute("LeadingZeros", boolToString(dd.LeadingZeros)); @@ -314,6 +324,8 @@ inline QXmlStreamReader& operator >>(QXmlStreamReader& xml, DataViewDefinitions& if (ok) dd.PointAddress = pointAddress; } + dd.EnronType = static_cast(attributes.value("EnronType").toInt()); + if (attributes.hasAttribute("Length")) { bool ok; const quint16 length = attributes.value("Length").toUShort(&ok); if (ok) dd.Length = length; diff --git a/src/enronmodbus.h b/src/enronmodbus.h new file mode 100644 index 00000000..0945c2bc --- /dev/null +++ b/src/enronmodbus.h @@ -0,0 +1,62 @@ +// SPDX-FileCopyrightText: 2026 OpenModSim contributors +// SPDX-License-Identifier: MIT + +/// +/// \file enronmodbus.h +/// \brief Defines the standard Enron 32-bit register ranges. +/// + +#ifndef ENRONMODBUS_H +#define ENRONMODBUS_H + +#include +#include + +namespace EnronModbus { + +constexpr quint16 LongBase = 5001; +constexpr quint16 FloatBase = 7001; +constexpr quint16 RangeLength = 999; + +enum class RegisterType { None, Group5x, Group7x }; + +/// +/// \brief Returns the first protocol address of the selected Enron register group. +/// +inline quint16 baseAddress(RegisterType type) +{ + return type == RegisterType::Group7x ? FloatBase : LongBase; +} + +/// +/// \brief Resolves a logical Enron register to its first backing word. +/// +inline int wordAddress(RegisterType type, int address) +{ + const int base = baseAddress(type); + return base + 2 * (address - base); +} + +/// +/// \brief Resolves a backing word to its logical Enron register. +/// +inline int registerAddress(RegisterType type, int address) +{ + const int base = baseAddress(type); + return base + (address - base) / 2; +} + +/// +/// \brief Identifies addresses with four-byte register values on the wire. +/// +inline bool is32BitAddress(quint16 address) +{ + return (address >= LongBase && address < LongBase + RangeLength) + || (address >= FloatBase && address < FloatBase + RangeLength); +} + +} + +Q_DECLARE_METATYPE(EnronModbus::RegisterType) + +#endif // ENRONMODBUS_H diff --git a/src/formdatamapview.cpp b/src/formdatamapview.cpp index 666ce7de..79cac403 100644 --- a/src/formdatamapview.cpp +++ b/src/formdatamapview.cpp @@ -14,6 +14,7 @@ #include "formdatamapview.h" #include "modbusmessages/modbusmessages.h" #include "controls/numericlineedit.h" +#include "controls/pointtypecombobox.h" #include "formatutils.h" #include "themedicons.h" #include "ui_formdatamapview.h" @@ -38,6 +39,16 @@ constexpr int RoleType = DataMapRole::Type; constexpr int RoleAddress = DataMapRole::Address; constexpr int RoleTypeValue = DataMapRole::TypeValue; +/// +/// \brief Resolves the backing map model for editors reached through a filter proxy. +/// +DataMapDataModel* dataMapSourceModel(const QModelIndex& index) +{ + const auto* proxy = qobject_cast(index.model()); + return proxy ? qobject_cast(proxy->sourceModel()) + : const_cast(qobject_cast(index.model())); +} + constexpr auto NumericEditorInitializedProperty = "_omodsim_numericEditorInitialized"; /// @@ -144,30 +155,35 @@ class TypeItemDelegate : public QStyledItemDelegate public: using QStyledItemDelegate::QStyledItemDelegate; - QWidget* createEditor(QWidget* parent, const QStyleOptionViewItem&, const QModelIndex&) const override + /// + /// \brief Shares protocol-dependent register choices with the data view. + /// + QWidget* createEditor(QWidget* parent, const QStyleOptionViewItem&, const QModelIndex& index) const override { - auto* cb = new QComboBox(parent); - cb->addItem(tr("Coils"), static_cast(QModbusDataUnit::Coils)); - cb->addItem(tr("Discrete Inputs"), static_cast(QModbusDataUnit::DiscreteInputs)); - cb->addItem(tr("Input Registers"), static_cast(QModbusDataUnit::InputRegisters)); - cb->addItem(tr("Holding Registers"),static_cast(QModbusDataUnit::HoldingRegisters)); + auto* cb = new PointTypeComboBox(parent); + cb->setEnronEnabled(index.data(DataMapRole::EnronEnabled).toBool()); + if (auto* source = dataMapSourceModel(index)) + connect(source, &DataMapDataModel::enronEnabledChanged, cb, &PointTypeComboBox::setEnronEnabled); return cb; } void setEditorData(QWidget* editor, const QModelIndex& index) const override { - auto* cb = qobject_cast(editor); + auto* cb = qobject_cast(editor); if (!cb) return; - const int value = index.data(RoleTypeValue).toInt(); - const int idx = cb->findData(value); - if (idx >= 0) cb->setCurrentIndex(idx); + cb->setCurrentPointType(static_cast(index.data(RoleTypeValue).toInt())); + cb->setCurrentEnronType(static_cast(index.data(DataMapRole::EnronType).toInt())); } void setModelData(QWidget* editor, QAbstractItemModel* model, const QModelIndex& index) const override { - auto* cb = qobject_cast(editor); + auto* cb = qobject_cast(editor); if (!cb) return; - model->setData(index, cb->currentData(), RoleTypeValue); + const auto enron = cb->currentEnronType(); + if (enron != EnronModbus::RegisterType::None) + model->setData(index, int(enron), DataMapRole::EnronType); + else + model->setData(index, int(cb->currentPointType()), RoleTypeValue); } void updateEditorGeometry(QWidget* editor, const QStyleOptionViewItem& option, const QModelIndex&) const override @@ -461,8 +477,7 @@ class AddressItemDelegate : public QStyledItemDelegate static DataMapDataModel* sourceModel(const QModelIndex& index) { - auto* proxy = qobject_cast(index.model()); - return proxy ? qobject_cast(proxy->sourceModel()) : nullptr; + return dataMapSourceModel(index); } static bool zeroBasedFromIndex(const QModelIndex& index) @@ -492,7 +507,13 @@ class AddressItemDelegate : public QStyledItemDelegate const bool zeroBased = zeroBasedFromIndex(index); auto* editor = new NumericLineEdit(parent); editor->setInputMode(NumericLineEdit::UInt32Mode); - editor->setInputRange(zeroBased ? 0 : 1, zeroBased ? 65535 : 65536); + const auto enron = static_cast(index.data(DataMapRole::EnronType).toInt()); + if (enron == EnronModbus::RegisterType::None) { + editor->setInputRange(zeroBased ? 0 : 1, zeroBased ? 65535 : 65536); + } else { + const quint32 base = EnronModbus::baseAddress(enron); + editor->setInputRange(base, base + EnronModbus::RangeLength - 1); + } editor->setHexView(hexViewFromIndex(index)); return editor; } @@ -504,10 +525,7 @@ class AddressItemDelegate : public QStyledItemDelegate auto* le = qobject_cast(editor); if (!le) return; - const quint16 rawAddr = static_cast( - index.siblingAtColumn(ColUnit).data(RoleAddress).toUInt()); - const bool zeroBased = zeroBasedFromIndex(index); - le->setValue(zeroBased ? rawAddr : static_cast(rawAddr) + 1); + le->setValue(index.data(Qt::EditRole).toUInt()); markNumericEditorInitialized(editor); } @@ -862,7 +880,7 @@ ProjectAddressSpaceRanges FormDataMapView::addressSpaceRanges() const key.DeviceId, key.Type, key.Address, - static_cast(registersCount(entry.type)) + static_cast(entry.registerCount()) }); } @@ -1245,18 +1263,8 @@ void FormDataMapView::changeEvent(QEvent* event) if (_filterUnitLabel) _filterUnitLabel->setText(tr("Unit:")); if (_filterTypeLabel) _filterTypeLabel->setText(tr("Data Type:")); if (_filterUnitSpin) _filterUnitSpin->setSpecialValueText(tr("All")); - if (_filterTypeCombo) { - const int idx = _filterTypeCombo->currentIndex(); - // Block signals to avoid spurious filter updates while repopulating - const QSignalBlocker blocker(_filterTypeCombo); - _filterTypeCombo->clear(); - _filterTypeCombo->addItem(tr("All Types")); - _filterTypeCombo->addItem(tr("Coils")); - _filterTypeCombo->addItem(tr("Discrete Inputs")); - _filterTypeCombo->addItem(tr("Input Registers")); - _filterTypeCombo->addItem(tr("Holding Registers")); - _filterTypeCombo->setCurrentIndex(idx); - } + if (_filterTypeCombo) + updateTypeFilters(_mbMultiServer.getModbusDefinitions().EnronModbus); if (_model) { // Force column headers to repaint with the new language @@ -1386,6 +1394,9 @@ int FormDataMapView::addRowAndReturnSourceRow(int referenceSourceRow) entry.type = referenceEntry.type; entry.order = referenceEntry.order; entry.byteOrder = referenceEntry.byteOrder; + entry.EnronType = referenceEntry.EnronType; + if (entry.EnronType != EnronModbus::RegisterType::None) + key.Address = qMin(referenceKey.Address + 2, EnronModbus::baseAddress(entry.EnronType) + 2 * (EnronModbus::RangeLength - 1)); } else if (!_model->isEmpty()) { const ItemMapKey last = _model->lastKey(); const auto& lastEntry = _model->entries()[last]; @@ -1395,12 +1406,19 @@ int FormDataMapView::addRowAndReturnSourceRow(int referenceSourceRow) entry.type = lastEntry.type; entry.order = lastEntry.order; entry.byteOrder = lastEntry.byteOrder; + entry.EnronType = lastEntry.EnronType; + if (entry.EnronType != EnronModbus::RegisterType::None) + key.Address = qMin(last.Address + 2, EnronModbus::baseAddress(entry.EnronType) + 2 * (EnronModbus::RangeLength - 1)); } // Skip duplicate keys while (_model->contains(key)) { - if (key.Address < 0xFFFF) ++key.Address; - else break; + const int stride = entry.EnronType == EnronModbus::RegisterType::None ? 1 : 2; + const int limit = entry.EnronType == EnronModbus::RegisterType::None ? 0xffff + : EnronModbus::baseAddress(entry.EnronType) + 2 * (EnronModbus::RangeLength - 1); + if (int(key.Address) + stride > limit) + break; + key.Address += stride; } entry.timestamp = _mbMultiServer.timestamp(key.DeviceId, key.Type, key.Address); @@ -1524,25 +1542,36 @@ void FormDataMapView::processRequest(quint8 deviceId, QModbusDataUnit::RegisterT { if (count == 0 || count > 2000) return; - const QModbusDataUnit unit = _mbMultiServer.data(deviceId, type, startAddress, count); + const bool enronEnabled = _mbMultiServer.getModbusDefinitions().EnronModbus; bool addedEntries = false; for (quint16 i = 0; i < count; ++i) { - const ItemMapKey key{ deviceId, type, static_cast(startAddress + i) }; - const quint16 value = unit.isValid() ? static_cast(unit.value(i)) : 0; + const quint32 address = quint32(startAddress) + i; + if (address > 0xffff) + break; + auto enron = EnronModbus::RegisterType::None; + if (enronEnabled && type == QModbusDataUnit::HoldingRegisters && EnronModbus::is32BitAddress(address)) + enron = address < EnronModbus::FloatBase ? EnronModbus::RegisterType::Group5x : EnronModbus::RegisterType::Group7x; + const quint16 word = enron == EnronModbus::RegisterType::None ? quint16(address) + : quint16(EnronModbus::wordAddress(enron, address)); + const ItemMapKey key{deviceId, type, word}; + const auto unit = _mbMultiServer.data(deviceId, type, word, enron == EnronModbus::RegisterType::None ? 1 : 2); + const quint16 value = unit.isValid() ? quint16(unit.value(0)) : 0; if (_model->contains(key)) { - const QModbusDataUnit singleUnit(type, startAddress + i, QVector{value}); - _model->applyMbDataChange(deviceId, singleUnit); + _model->applyMbDataChange(deviceId, unit); } else if (_autoAddOnRequest) { DataMapEntry entry; entry.value = value; entry.type = (type == QModbusDataUnit::Coils || type == QModbusDataUnit::DiscreteInputs) ? DataType::Binary : DataType::Int16; + entry.EnronType = enron; + if (enron != EnronModbus::RegisterType::None) + entry.type = enron == EnronModbus::RegisterType::Group5x ? DataType::Int32 : DataType::Float32; entry.order = RegisterOrder::MSRF; entry.byteOrder = ByteOrder::Direct; - entry.timestamp = _mbMultiServer.timestamp(deviceId, type, static_cast(startAddress + i)); + entry.timestamp = _mbMultiServer.timestamp(deviceId, type, word); _model->addEntry(key, entry); addedEntries = true; } @@ -1591,11 +1620,8 @@ void FormDataMapView::setupToolBar() _filterUnitSpin->setFixedWidth(60); _filterTypeCombo = new QComboBox; - _filterTypeCombo->addItem(tr("All Types")); - _filterTypeCombo->addItem(tr("Coils")); - _filterTypeCombo->addItem(tr("Discrete Inputs")); - _filterTypeCombo->addItem(tr("Input Registers")); - _filterTypeCombo->addItem(tr("Holding Registers")); + _filterTypeCombo->setObjectName("filterTypeCombo"); + updateTypeFilters(_mbMultiServer.getModbusDefinitions().EnronModbus); _filterTypeCombo->setFixedWidth(130); auto* filterWidget = new QWidget(ui->toolBar); @@ -1618,11 +1644,34 @@ void FormDataMapView::setupToolBar() this, [this](int val) { _proxy->setFilterUnit(val); updateActionState(); }); } +/// +/// \brief Keeps filter groups consistent with the active server protocol. +/// +void FormDataMapView::updateTypeFilters(bool enronEnabled) +{ + const int previous = _filterTypeCombo->currentIndex(); + const QSignalBlocker blocker(_filterTypeCombo); + _filterTypeCombo->clear(); + _filterTypeCombo->addItem(tr("All Types")); + _filterTypeCombo->addItem(tr("Coils")); + _filterTypeCombo->addItem(tr("Discrete Inputs")); + _filterTypeCombo->addItem(tr("Input Registers")); + _filterTypeCombo->addItem(tr("Holding Registers")); + if (enronEnabled) { + _filterTypeCombo->addItem(tr("Enron Registers Int32")); + _filterTypeCombo->addItem(tr("Enron Registers Float32")); + } + const int selected = previous >= 0 && previous < _filterTypeCombo->count() ? previous : 0; + _filterTypeCombo->setCurrentIndex(selected); + _proxy->setFilterTypeIndex(selected); +} + /// /// \brief FormDataMapView::setupServerConnections /// void FormDataMapView::setupServerConnections() { + connect(_model, &DataMapDataModel::enronEnabledChanged, this, &FormDataMapView::updateTypeFilters); connect(&_mbMultiServer, &ModbusMultiServer::requestOnConnection, this, &FormDataMapView::on_mbRequest); connect(&_mbMultiServer, &ModbusMultiServer::dataChanged, diff --git a/src/formdatamapview.h b/src/formdatamapview.h index 357aedde..2279bb53 100644 --- a/src/formdatamapview.h +++ b/src/formdatamapview.h @@ -95,6 +95,7 @@ private slots: void processRequest(quint8 deviceId, QModbusDataUnit::RegisterType type, quint16 startAddress, quint16 count); void refreshThemeDependentRowColors(); void setupToolBar(); + void updateTypeFilters(bool enronEnabled); void setupServerConnections(); void updateWindowIcon(); void ensureColumnHeadersFit(); @@ -179,6 +180,8 @@ inline QXmlStreamWriter& operator <<(QXmlStreamWriter& xml, FormDataMapView* frm xml.writeAttribute("Type", QString::number(key.Type)); xml.writeAttribute("Address", QString::number(key.Address)); xml.writeAttribute("DataType", enumToString(e.type)); + if (e.EnronType != EnronModbus::RegisterType::None) + xml.writeAttribute("EnronType", QString::number(int(e.EnronType))); if (isMultiRegisterType(e.type)) xml.writeAttribute("Order", enumToString(e.order)); if (key.Type == QModbusDataUnit::HoldingRegisters || @@ -273,6 +276,7 @@ inline QXmlStreamReader& operator >>(QXmlStreamReader& xml, FormDataMapView* frm DataMapEntry entry; entry.value = attrs.value("Value").toUShort(); + entry.EnronType = static_cast(attrs.value("EnronType").toInt()); entry.type = enumFromString(attrs.value("DataType").toString(), DataType::Int16); entry.order = enumFromString(attrs.value("Order").toString(), RegisterOrder::MSRF); entry.byteOrder = enumFromString(attrs.value("ByteOrder").toString(), ByteOrder::Direct); diff --git a/src/formdataview.cpp b/src/formdataview.cpp index a1fc6dd1..a6e9b66c 100644 --- a/src/formdataview.cpp +++ b/src/formdataview.cpp @@ -82,6 +82,7 @@ FormDataView::FormDataView(ModbusMultiServer& server, DataSimulator* simulator, server.addDeviceId(ui->lineEditDeviceId->value()); const auto mbDefs = _mbMultiServer.getModbusDefinitions(); + ui->comboBoxModbusPointType->setEnronEnabled(mbDefs.EnronModbus); ui->lineEditAddress->setLeadingZeroes(false); ui->lineEditAddress->setInputRange(ModbusLimits::addressRange(mbDefs.AddrSpace, false)); @@ -239,6 +240,7 @@ void FormDataView::changeEvent(QEvent* e) { ui->retranslateUi(this); updateStatus(); + updateSettingsControls(); } QWidget::changeEvent(e); @@ -275,6 +277,11 @@ DataViewDefinitions FormDataView::displayDefinition() const dd.PointAddress = ui->lineEditAddress->value(); dd.PointType = ui->comboBoxModbusPointType->currentPointType(); dd.Length = ui->lineEditLength->value(); + dd.EnronType = _enronType; + if (_enronType != EnronModbus::RegisterType::None) { + dd.PointAddress = EnronModbus::wordAddress(_enronType, dd.PointAddress) + (zeroBasedAddress() ? 0 : 1); + dd.Length *= 2; + } dd.DataViewColumnsDistance = ui->outputWidget->dataViewColumnsDistance(); dd.LeadingZeros = ui->checkBoxLeadingZeros->isChecked(); @@ -290,39 +297,70 @@ void FormDataView::setDisplayDefinition(const DataViewDefinitions& dd) if(!dd.FormName.isEmpty()) setWindowTitle(dd.FormName); - const auto defs = _mbMultiServer.getModbusDefinitions(); - const auto addressRange = ModbusLimits::addressRange(defs.AddrSpace, zeroBasedAddress()); - const auto pointAddress = static_cast(qBound(addressRange.from(), static_cast(dd.PointAddress), addressRange.to())); - ui->lineEditDeviceId->setLeadingZeroes(false); ui->lineEditDeviceId->setValue(dd.DeviceId); - { - QSignalBlocker b(ui->lineEditAddress); - ui->lineEditAddress->setLeadingZeroes(false); - ui->lineEditAddress->setInputRange(addressRange); - ui->lineEditAddress->setValue(pointAddress); - } - { - QSignalBlocker b(ui->lineEditLength); - ui->lineEditLength->setLeadingZeroes(false); - ui->lineEditLength->setInputRange(lengthRangeForPointType(pointAddress, zeroBasedAddress(), defs.AddrSpace, dd.PointType)); - ui->lineEditLength->setValue(dd.Length); - } { QSignalBlocker b(ui->checkBoxLeadingZeros); ui->checkBoxLeadingZeros->setChecked(dd.LeadingZeros); } - { - QSignalBlocker b(ui->comboBoxModbusPointType); - ui->comboBoxModbusPointType->setCurrentPointType(dd.PointType); - } + updateRegisterControls(dd); ui->outputWidget->setDataViewColumnsDistance(dd.DataViewColumnsDistance); updateSettingsControls(); emit definitionChanged(); } +/// +/// \brief Limits logical Enron input to the selected register group. +/// +QRange FormDataView::registerAddressRange() const +{ + if (_enronType == EnronModbus::RegisterType::None) + return ModbusLimits::addressRange(_mbMultiServer.getModbusDefinitions().AddrSpace, zeroBasedAddress()); + const int base = EnronModbus::baseAddress(_enronType); + return {base, base + EnronModbus::RangeLength - 1}; +} + +/// +/// \brief Bounds the visible count before converting Enron registers to backing words. +/// +QRange FormDataView::registerLengthRange(int address) const +{ + if (_enronType == EnronModbus::RegisterType::None) + return lengthRangeForPointType(address, zeroBasedAddress(), _mbMultiServer.getModbusDefinitions().AddrSpace, + ui->comboBoxModbusPointType->currentPointType()); + return {1, qMin(100, registerAddressRange().to() - address + 1)}; +} + +/// +/// \brief Presents logical Enron addresses and counts while retaining word-based storage definitions. +/// +void FormDataView::updateRegisterControls(const DataViewDefinitions& dd) +{ + QSignalBlocker addressBlocker(ui->lineEditAddress); + QSignalBlocker lengthBlocker(ui->lineEditLength); + QSignalBlocker typeBlocker(ui->comboBoxModbusPointType); + _enronType = dd.PointType == QModbusDataUnit::HoldingRegisters ? dd.EnronType : EnronModbus::RegisterType::None; + ui->comboBoxModbusPointType->setCurrentPointType(dd.PointType); + ui->comboBoxModbusPointType->setCurrentEnronType(_enronType); + int address = dd.PointAddress; + int count = dd.Length; + if (_enronType != EnronModbus::RegisterType::None) { + // Both address bases identify the same aligned word pair. + address = EnronModbus::registerAddress(_enronType, address); + count = qMax(1, count / 2); + } + const auto range = registerAddressRange(); + address = qBound(range.from(), address, range.to()); + ui->lineEditAddress->setLeadingZeroes(false); + ui->lineEditAddress->setInputRange(range); + ui->lineEditAddress->setValue(address); + ui->lineEditLength->setLeadingZeroes(false); + ui->lineEditLength->setInputRange(registerLengthRange(address)); + ui->lineEditLength->setValue(qBound(1, count, registerLengthRange(address).to())); +} + /// /// \brief FormDataView::dataType /// \return @@ -347,7 +385,7 @@ RegisterOrder FormDataView::registerOrder() const /// bool FormDataView::zeroBasedAddress() const { - return ui->lineEditAddress->range().from() == 0; + return _zeroBasedAddress; } /// @@ -363,6 +401,8 @@ void FormDataView::setAddressBase(AddressBase base) auto dd = displayDefinition(); dd.PointAddress = zeroBased ? qMax(0, dd.PointAddress - 1) : qMax(1, dd.PointAddress + 1); + _zeroBasedAddress = zeroBased; + // Switch address base first so setDisplayDefinitionSilent() uses the new base // when recalculating input ranges. { @@ -798,10 +838,6 @@ void FormDataView::setDisplayDefinitionSilent(const DataViewDefinitions& dd) if(!dd.FormName.isEmpty()) setWindowTitle(dd.FormName); - const auto defs = _mbMultiServer.getModbusDefinitions(); - const auto addressRange = ModbusLimits::addressRange(defs.AddrSpace, zeroBasedAddress()); - const auto pointAddress = static_cast(qBound(addressRange.from(), static_cast(dd.PointAddress), addressRange.to())); - // Handle device ID change (reference counting in server). { QSignalBlocker b(ui->lineEditDeviceId); @@ -816,27 +852,12 @@ void FormDataView::setDisplayDefinitionSilent(const DataViewDefinitions& dd) ui->lineEditDeviceId->setValue(dd.DeviceId); } - { - QSignalBlocker b(ui->lineEditAddress); - ui->lineEditAddress->setLeadingZeroes(false); - ui->lineEditAddress->setInputRange(addressRange); - ui->lineEditAddress->setValue(pointAddress); - } - { - QSignalBlocker b(ui->lineEditLength); - ui->lineEditLength->setLeadingZeroes(false); - ui->lineEditLength->setInputRange(lengthRangeForPointType(pointAddress, zeroBasedAddress(), defs.AddrSpace, dd.PointType)); - ui->lineEditLength->setValue(dd.Length); - } - { - QSignalBlocker b(ui->comboBoxModbusPointType); - ui->comboBoxModbusPointType->setCurrentPointType(dd.PointType); - } { QSignalBlocker b(ui->checkBoxLeadingZeros); ui->checkBoxLeadingZeros->setChecked(dd.LeadingZeros); } + updateRegisterControls(dd); ui->outputWidget->setDataViewColumnsDistance(dd.DataViewColumnsDistance); updateSettingsControls(); @@ -961,18 +982,10 @@ void FormDataView::showFind() /// void FormDataView::on_lineEditAddress_valueChanged(const QVariant&) { - const auto defs = _mbMultiServer.getModbusDefinitions(); - const bool zeroBased = zeroBasedAddress(); - const int address = ui->lineEditAddress->value(); - const auto lenRange = lengthRangeForPointType(address, zeroBased, defs.AddrSpace, ui->comboBoxModbusPointType->currentPointType()); - - ui->lineEditLength->setInputRange(lenRange); - if(ui->lineEditLength->value() > lenRange.to()) { - ui->lineEditLength->setValue(lenRange.to()); - ui->lineEditLength->update(); - } - - emit definitionChanged(); + const auto range = registerLengthRange(ui->lineEditAddress->value()); + ui->lineEditLength->setInputRange(range); + ui->lineEditLength->setValue(qBound(range.from(), ui->lineEditLength->value(), range.to())); + emit definitionChanged(); } /// @@ -1000,20 +1013,20 @@ void FormDataView::on_lineEditDeviceId_valueChanged(const QVariant& oldValue, co /// void FormDataView::on_comboBoxModbusPointType_pointTypeChanged(QModbusDataUnit::RegisterType type) { - const auto defs = _mbMultiServer.getModbusDefinitions(); - const bool zeroBased = zeroBasedAddress(); - const int address = ui->lineEditAddress->value(); - const auto lenRange = lengthRangeForPointType(address, zeroBased, defs.AddrSpace, type); - - ui->lineEditLength->setInputRange(lenRange); - if(ui->lineEditLength->value() > lenRange.to()) { - ui->lineEditLength->setValue(lenRange.to()); - ui->lineEditLength->update(); + auto dd = displayDefinition(); + const auto selected = ui->comboBoxModbusPointType->currentEnronType(); + const bool groupChanged = selected != _enronType; + if (selected != EnronModbus::RegisterType::None && selected != _enronType) { + dd.PointAddress = EnronModbus::baseAddress(selected) + (zeroBasedAddress() ? 0 : 1); + if (_enronType == EnronModbus::RegisterType::None) + dd.Length = qMin(dd.Length, 100) * 2; } - - emit definitionChanged(); + dd.EnronType = selected; + dd.PointType = type; + setDisplayDefinition(dd); + if (groupChanged && selected != EnronModbus::RegisterType::None) + setDataType(selected == EnronModbus::RegisterType::Group5x ? DataType::Int32 : DataType::Float32); emit pointTypeChanged(type); - updateDisplayBar(); } /// @@ -1034,7 +1047,7 @@ void FormDataView::onDefinitionChanged() const auto addr = dd.PointAddress - (zeroBasedAddress() ? 0 : 1); const auto formId = dataFormId(this); if(!formId.isNull()) { - _mbMultiServer.addUnitMap(formId, dd.DeviceId, dd.PointType, addr, dd.Length); + _mbMultiServer.addUnitMap(formId, dd.DeviceId, dd.PointType, addr, dd.Length, dd.EnronType); _initialMapSynced = true; } @@ -1084,6 +1097,7 @@ void FormDataView::on_outputWidget_itemDoubleClicked(quint16 addr, const QVarian params.ZeroBasedAddress = zeroBasedAddress(); params.LeadingZeros = dd.LeadingZeros; params.Server = &_mbMultiServer; + params.EnronType = _enronType; switch(pointType) { @@ -1149,14 +1163,14 @@ void FormDataView::refreshDisplayedData() /// void FormDataView::on_mbDefinitionsChanged(const ModbusDefinitions& defs) { - const auto dd = displayDefinition(); - const auto addr = dd.PointAddress - (zeroBasedAddress() ? 0 : 1); - ui->lineEditAddress->setInputRange(ModbusLimits::addressRange(defs.AddrSpace, zeroBasedAddress())); - ui->lineEditLength->setInputRange(lengthRangeForPointType(dd.PointAddress, zeroBasedAddress(), defs.AddrSpace, dd.PointType)); - ui->outputWidget->setup(dd, zeroBasedAddress(), defs.AddrSpace, - _dataSimulator->simulationMap(), _mbMultiServer.data(dd.DeviceId, dd.PointType, addr, dd.Length)); - syncDescriptionsFromServer(); - reapplyFind(); + auto dd = displayDefinition(); + { + QSignalBlocker blocker(ui->comboBoxModbusPointType); + ui->comboBoxModbusPointType->setEnronEnabled(defs.EnronModbus); + } + if (!defs.EnronModbus) + dd.EnronType = EnronModbus::RegisterType::None; + setDisplayDefinition(dd); } /// @@ -1308,6 +1322,9 @@ void FormDataView::setupDisplayBar() /// void FormDataView::updateSettingsControls() { + const bool enron = _enronType != EnronModbus::RegisterType::None; + ui->labelAddress->setText(enron ? tr("Enron Address:") : tr("Starting Address:")); + ui->labelLength->setText(enron ? tr("Register Count:") : tr("Length:")); { QSignalBlocker blocker(ui->checkBoxLeadingZeros); ui->checkBoxLeadingZeros->setChecked(displayDefinition().LeadingZeros); @@ -1393,6 +1410,8 @@ inline QXmlStreamWriter& operator <<(QXmlStreamWriter& xml, FormDataView* frm) xml.writeAttribute("DeviceId", QString::number(dd.DeviceId)); xml.writeAttribute("PointType", enumToString(dd.PointType)); xml.writeAttribute("PointAddress", QString::number(dd.PointAddress)); + if (dd.EnronType != EnronModbus::RegisterType::None) + xml.writeAttribute("EnronType", QString::number(static_cast(dd.EnronType))); xml.writeAttribute("Length", QString::number(dd.Length)); xml.writeAttribute("DataViewColumnsDistance", QString::number(dd.DataViewColumnsDistance)); xml.writeAttribute("LeadingZeros", boolToString(dd.LeadingZeros)); diff --git a/src/formdataview.h b/src/formdataview.h index aafa2011..062b92dd 100644 --- a/src/formdataview.h +++ b/src/formdataview.h @@ -150,6 +150,9 @@ private slots: void on_dataViewFlushTimeout(); private: + void updateRegisterControls(const DataViewDefinitions& dd); + QRange registerAddressRange() const; + QRange registerLengthRange(int address) const; void updateStatus(); void refreshDisplayedData(); void reapplyFind(); @@ -170,6 +173,8 @@ private slots: ModbusMultiServer& _mbMultiServer; DataSimulator* _dataSimulator; bool _initialMapSynced = false; + bool _zeroBasedAddress = false; + EnronModbus::RegisterType _enronType = EnronModbus::RegisterType::None; FindReplaceBar* _findReplaceBar = nullptr; QTimer* _dataViewFlushTimer = nullptr; @@ -246,10 +251,10 @@ inline QSettings& operator >>(QSettings& in, FormDataView* frm) if(isMinimized) wnd->setWindowState(Qt::WindowMinimized); if(isMaximized) wnd->setWindowState(Qt::WindowMaximized); + frm->setDisplayDefinition(displayDefinition); frm->setDataType(dataType); frm->setRegisterOrder(regOrder); frm->setByteOrder(byteOrder); - frm->setDisplayDefinition(displayDefinition); frm->setCodepage(in.value("Codepage").toString()); for(auto it = descriptionMap.cbegin(); it != descriptionMap.cend(); ++it) diff --git a/src/formdataview.ui b/src/formdataview.ui index 393602cf..526f7de6 100644 --- a/src/formdataview.ui +++ b/src/formdataview.ui @@ -255,7 +255,7 @@ - Data Type: + Register Type: Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter diff --git a/src/legacyprojectparser.cpp b/src/legacyprojectparser.cpp index c5884349..aa3af417 100644 --- a/src/legacyprojectparser.cpp +++ b/src/legacyprojectparser.cpp @@ -39,6 +39,8 @@ DataViewDefinitions readDataViewDefinitions(QXmlStreamReader& xml, const QString dd.PointType = enumFromString(attributes.value("PointType").toString()); } + dd.EnronType = static_cast(attributes.value("EnronType").toInt()); + if (attributes.hasAttribute("PointAddress")) { bool ok; const quint16 pointAddress = attributes.value("PointAddress").toUShort(&ok); diff --git a/src/main.cpp b/src/main.cpp index 43b3c091..203bb9e4 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -23,18 +23,6 @@ #include "styles/macappstyle.h" #endif -#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) -namespace { - static const int reg1 = qRegisterMetaType(); - static const int reg2 = qRegisterMetaType(); - static const int reg3 = qRegisterMetaType(); - static const int reg4 = qRegisterMetaType(); - static const int reg5 = qRegisterMetaType>(); - static const int reg6 = qRegisterMetaType(); - static const int reg7 = qRegisterMetaType>(); -} -#endif - /// /// \brief isConsoleOutputAvailable /// \return diff --git a/src/modbusdefinitions.h b/src/modbusdefinitions.h index 72c9a687..f4e5ce25 100644 --- a/src/modbusdefinitions.h +++ b/src/modbusdefinitions.h @@ -18,6 +18,7 @@ struct ModbusDefinitions { AddressSpace AddrSpace = AddressSpace::Addr6Digits; + bool EnronModbus = false; bool UseGlobalUnitMap = false; bool AutoAddRegistersOnRequest = false; ModbusErrorSimulations ErrorSimulations; @@ -42,6 +43,7 @@ inline QSettings& operator <<(QSettings& out, const ModbusDefinitions& defs) out << defs.ErrorSimulations; out << defs.AddrSpace; + out.setValue("EnronModbus", defs.EnronModbus); out.setValue("UseGlobalUnitMap", defs.UseGlobalUnitMap); out.setValue("AutoAddRegistersOnRequest", defs.AutoAddRegistersOnRequest); @@ -62,6 +64,7 @@ inline QSettings& operator >>(QSettings& in, ModbusDefinitions& defs) in >> defs.ErrorSimulations; in >> defs.AddrSpace; + defs.EnronModbus = in.value("EnronModbus", false).toBool(); defs.UseGlobalUnitMap = in.value("UseGlobalUnitMap", false).toBool(); defs.AutoAddRegistersOnRequest = in.value("AutoAddRegistersOnRequest", false).toBool(); @@ -82,6 +85,7 @@ inline QXmlStreamWriter& operator <<(QXmlStreamWriter& xml, const ModbusDefiniti xml.writeStartElement("ModbusDefinitions"); xml.writeAttribute("AddrSpace", enumToString(definitions.AddrSpace)); + xml.writeAttribute("EnronModbus", boolToString(definitions.EnronModbus)); xml.writeAttribute("UseGlobalUnitMap", boolToString(definitions.UseGlobalUnitMap)); xml.writeAttribute("AutoAddRegistersOnRequest", boolToString(definitions.AutoAddRegistersOnRequest)); @@ -106,6 +110,8 @@ inline QXmlStreamReader& operator >>(QXmlStreamReader& xml, ModbusDefinitions& d definitions.AddrSpace = enumFromString(attributes.value("AddrSpace").toString()); } + definitions.EnronModbus = stringToBool(attributes.value("EnronModbus").toString()); + if (attributes.hasAttribute("UseGlobalUnitMap")) { definitions.UseGlobalUnitMap = stringToBool(attributes.value("UseGlobalUnitMap").toString()); } diff --git a/src/modbusmessages/writesingleregister.h b/src/modbusmessages/writesingleregister.h index 57a59526..fb47ecee 100644 --- a/src/modbusmessages/writesingleregister.h +++ b/src/modbusmessages/writesingleregister.h @@ -10,6 +10,7 @@ #define WRITESINGLEREGISTER_H #include "modbusmessage.h" +#include "enronmodbus.h" /// /// \brief The WriteSingleRegisterRequest class @@ -48,7 +49,8 @@ class WriteSingleRegisterRequest : public ModbusMessage /// \return /// bool isValid() const override { - return ModbusMessage::isValid() && (isException() ? true : (dataSize() == 4)); + return ModbusMessage::isValid() && (isException() || dataSize() == 4 + || (dataSize() == 6 && EnronModbus::is32BitAddress(address()))); } /// @@ -60,11 +62,13 @@ class WriteSingleRegisterRequest : public ModbusMessage } /// - /// \brief value - /// \return + /// \brief Returns the unsigned wire value, including both words of an Enron register. /// - quint16 value() const { - return makeUInt16(at(3), at(2), ByteOrder::Direct); + quint32 value() const { + const quint16 high = makeUInt16(at(3), at(2), ByteOrder::Direct); + if (dataSize() == 6) + return (quint32(high) << 16) | makeUInt16(at(5), at(4), ByteOrder::Direct); + return high; } }; @@ -105,7 +109,8 @@ class WriteSingleRegisterResponse : public ModbusMessage /// \return /// bool isValid() const override { - return ModbusMessage::isValid() && dataSize() == 4; + return ModbusMessage::isValid() && (dataSize() == 4 + || (dataSize() == 6 && EnronModbus::is32BitAddress(address()))); } /// @@ -117,11 +122,13 @@ class WriteSingleRegisterResponse : public ModbusMessage } /// - /// \brief value - /// \return + /// \brief Returns the unsigned wire value, including both words of an Enron register. /// - quint16 value() const { - return makeUInt16(at(3), at(2), ByteOrder::Direct); + quint32 value() const { + const quint16 high = makeUInt16(at(3), at(2), ByteOrder::Direct); + if (dataSize() == 6) + return (quint32(high) << 16) | makeUInt16(at(5), at(4), ByteOrder::Direct); + return high; } }; diff --git a/src/modbusmultiserver.cpp b/src/modbusmultiserver.cpp index 00d8039e..f006b2e1 100644 --- a/src/modbusmultiserver.cpp +++ b/src/modbusmultiserver.cpp @@ -53,6 +53,14 @@ ModbusMultiServer::ModbusMultiServer(QObject *parent) : QObject{parent} ,_workerThread(new QThread(this)) { + qRegisterMetaType(); + qRegisterMetaType(); + qRegisterMetaType(); + qRegisterMetaType(); + qRegisterMetaType>(); + qRegisterMetaType(); + qRegisterMetaType>(); + qRegisterMetaType("EnronModbus::RegisterType"); qRegisterMetaType("WriteSource"); qRegisterMetaType("ModbusClientInfo"); @@ -159,18 +167,21 @@ void ModbusMultiServer::setUseGlobalUnitMap(bool use) } /// -/// \brief ModbusMultiServer::addUnitMap -/// \param id -/// \param pointType -/// \param pointAddress -/// \param length +/// \brief Registers backing words while preserving the logical Enron group for notifications. +/// \param id Identifier of the owning view or map entry. +/// \param deviceId Modbus device identifier. +/// \param pointType Backing Modbus table. +/// \param pointAddress Starting backing-word address. +/// \param length Number of backing words. +/// \param enronType Logical Enron group, or None for an ordinary table. /// -void ModbusMultiServer::addUnitMap(QUuid id, quint8 deviceId, QModbusDataUnit::RegisterType pointType, quint16 pointAddress, quint16 length) +void ModbusMultiServer::addUnitMap(QUuid id, quint8 deviceId, QModbusDataUnit::RegisterType pointType, + quint16 pointAddress, quint16 length, EnronModbus::RegisterType enronType) { if(QThread::currentThread() != _workerThread) { - QMetaObject::invokeMethod(this, [this, id, deviceId, pointType, pointAddress, length]() { - addUnitMap(id, deviceId, pointType, pointAddress, length); + QMetaObject::invokeMethod(this, [this, id, deviceId, pointType, pointAddress, length, enronType]() { + addUnitMap(id, deviceId, pointType, pointAddress, length, enronType); }, Qt::BlockingQueuedConnection); return; } @@ -180,10 +191,16 @@ void ModbusMultiServer::addUnitMap(QUuid id, quint8 deviceId, QModbusDataUnit::R _modbusDataUnitMaps[deviceId].setAddressSpace(_definitions.AddrSpace); } + const auto key = qMakePair(deviceId, id); + const bool groupChanged = _unitMapEnronTypes.value(key) != enronType; + if (enronType == EnronModbus::RegisterType::None) + _unitMapEnronTypes.remove(key); + else + _unitMapEnronTypes.insert(key, enronType); const bool changed = _modbusDataUnitMaps[deviceId].addUnitMap(id, pointType, pointAddress, length); reconfigureServers(); - if (changed) - emit unitMapAdded(id, deviceId, pointType, pointAddress, length); + if (changed || groupChanged) + emit unitMapAdded(id, deviceId, pointType, pointAddress, length, enronType); } /// @@ -200,6 +217,7 @@ void ModbusMultiServer::removeUnitMap(QUuid id, quint8 deviceId) return; } + const auto enronType = _unitMapEnronTypes.take(qMakePair(deviceId, id)); QModbusDataUnit removedUnit; const bool hasRemovedUnit = _modbusDataUnitMaps[deviceId].unitMap(id, removedUnit); const bool changed = _modbusDataUnitMaps[deviceId].removeUnitMap(id); @@ -207,7 +225,7 @@ void ModbusMultiServer::removeUnitMap(QUuid id, quint8 deviceId) if (changed && hasRemovedUnit) { emit unitMapRemoved(id, deviceId, removedUnit.registerType(), static_cast(removedUnit.startAddress()), - static_cast(removedUnit.valueCount())); + static_cast(removedUnit.valueCount()), enronType); } } @@ -1206,6 +1224,7 @@ void ModbusMultiServer::clearAddressSpace() } _modbusDataUnitMaps.clear(); + _unitMapEnronTypes.clear(); } /// diff --git a/src/modbusmultiserver.h b/src/modbusmultiserver.h index b169ff71..78f9701e 100644 --- a/src/modbusmultiserver.h +++ b/src/modbusmultiserver.h @@ -14,6 +14,7 @@ #include #include #include "modbusdataunitmap.h" +#include "enronmodbus.h" #include "modbuswriteparams.h" #include "connectiondetails.h" #include "modbusmessage.h" @@ -46,7 +47,18 @@ class ModbusMultiServer final : public QObject bool useGlobalUnitMap() const; void setUseGlobalUnitMap(bool use); - void addUnitMap(QUuid id, quint8 deviceId, QModbusDataUnit::RegisterType pointType, quint16 pointAddress, quint16 length); + /// + /// \brief Registers backing words while preserving the logical Enron group for notifications. + /// \param id Identifier of the owning view or map entry. + /// \param deviceId Modbus device identifier. + /// \param pointType Backing Modbus table. + /// \param pointAddress Starting backing-word address. + /// \param length Number of backing words. + /// \param enronType Logical Enron group, or None for an ordinary table. + /// + void addUnitMap(QUuid id, quint8 deviceId, QModbusDataUnit::RegisterType pointType, + quint16 pointAddress, quint16 length, + EnronModbus::RegisterType enronType = EnronModbus::RegisterType::None); void removeUnitMap(QUuid id, quint8 deviceId); QList registeredDeviceIds() const; @@ -160,8 +172,8 @@ class ModbusMultiServer final : public QObject void deviceIdAdded(quint8 deviceId); void deviceIdRemoved(quint8 deviceId); - void unitMapAdded(QUuid id, quint8 deviceId, QModbusDataUnit::RegisterType pointType, quint16 pointAddress, quint16 length); - void unitMapRemoved(QUuid id, quint8 deviceId, QModbusDataUnit::RegisterType pointType, quint16 pointAddress, quint16 length); + void unitMapAdded(QUuid id, quint8 deviceId, QModbusDataUnit::RegisterType pointType, quint16 pointAddress, quint16 length, EnronModbus::RegisterType enronType); + void unitMapRemoved(QUuid id, quint8 deviceId, QModbusDataUnit::RegisterType pointType, quint16 pointAddress, quint16 length, EnronModbus::RegisterType enronType); private slots: void on_clientConnected(const QString& clientAddress, quint16 clientPort); @@ -189,6 +201,7 @@ private slots: RequestHandlerPtr _requestHandler; std::atomic _snifferMode{false}; QMap _modbusDataUnitMaps; + QMap, EnronModbus::RegisterType> _unitMapEnronTypes; QList> _modbusServerList; }; diff --git a/src/modbusrtuserialserver.cpp b/src/modbusrtuserialserver.cpp index b75e94bf..4a330f03 100644 --- a/src/modbusrtuserialserver.cpp +++ b/src/modbusrtuserialserver.cpp @@ -10,6 +10,7 @@ #include #include #include "modbusrtuserialserver.h" +#include "enronmodbus.h" #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) #include @@ -263,7 +264,14 @@ void ModbusRtuSerialServer::on_readyRead() if (processesBroadcast()) event |= QModbusCommEvent::ReceiveFlag::BroadcastReceived; - const int pduSizeWithoutFcode = QModbusRequest::calculateDataSize(adu.pdu()); + int pduSizeWithoutFcode = QModbusRequest::calculateDataSize(adu.pdu()); + if (mbDef.EnronModbus && adu.pdu().functionCode() == QModbusPdu::WriteSingleRegister + && adu.pdu().dataSize() >= 2) { + quint16 address; + adu.pdu().decodeData(&address); + if (EnronModbus::is32BitAddress(address)) + pduSizeWithoutFcode = 6; + } // server address byte + function code byte + PDU size + 2 bytes CRC if ((pduSizeWithoutFcode < 0) || ((2 + pduSizeWithoutFcode + 2) != adu.rawSize())) { diff --git a/src/modbusrtutcpserver.cpp b/src/modbusrtutcpserver.cpp index 6354e4b8..20617746 100644 --- a/src/modbusrtutcpserver.cpp +++ b/src/modbusrtutcpserver.cpp @@ -17,6 +17,7 @@ #include #include "qmodbusadurtu.h" #include "modbusrtutcpserver.h" +#include "enronmodbus.h" namespace { @@ -28,7 +29,7 @@ constexpr int MaxRtuAduSize = 256; /// \param data /// \return /// -int expectedRequestSize(const QByteArray& data) +int expectedRequestSize(const QByteArray& data, bool enron) { if (data.size() < 2) return 0; @@ -41,10 +42,15 @@ int expectedRequestSize(const QByteArray& data) case 0x03: case 0x04: case 0x05: - case 0x06: case 0x08: return 8; + case 0x06: + if (data.size() < 4) + return 0; + return enron && EnronModbus::is32BitAddress( + (quint16(quint8(data.at(2))) << 8) | quint8(data.at(3))) ? 10 : 8; + case 0x07: case 0x0b: case 0x0c: @@ -88,7 +94,7 @@ int expectedRequestSize(const QByteArray& data) /// \param offset /// \return /// -int validFrameSizeAt(const QByteArray& buffer, int offset) +int validFrameSizeAt(const QByteArray& buffer, int offset, bool enron) { const QByteArray data = buffer.mid(offset); if (data.size() < QModbusAduRtu::MinRtuFrameSize || quint8(data.at(0)) > 247 @@ -96,7 +102,7 @@ int validFrameSizeAt(const QByteArray& buffer, int offset) return 0; } - const int expectedSize = expectedRequestSize(data); + const int expectedSize = expectedRequestSize(data, enron); if (expectedSize > 0) { if (expectedSize > MaxRtuAduSize || data.size() < expectedSize) @@ -124,12 +130,12 @@ int validFrameSizeAt(const QByteArray& buffer, int offset) /// \param frameSize /// \return /// -int findValidFrame(const QByteArray& buffer, int firstOffset, int& frameSize) +int findValidFrame(const QByteArray& buffer, int firstOffset, int& frameSize, bool enron) { for (int offset = firstOffset; offset + QModbusAduRtu::MinRtuFrameSize <= buffer.size(); ++offset) { - frameSize = validFrameSizeAt(buffer, offset); + frameSize = validFrameSizeAt(buffer, offset, enron); if (frameSize > 0) return offset; } @@ -278,7 +284,7 @@ bool ModbusRtuTcpServer::takeNextFrame(QByteArray& buffer, QByteArray& frame) continue; } - const int expectedSize = expectedRequestSize(buffer); + const int expectedSize = expectedRequestSize(buffer, getDefinitions().EnronModbus); if (expectedSize == 0) return false; @@ -302,7 +308,7 @@ bool ModbusRtuTcpServer::takeNextFrame(QByteArray& buffer, QByteArray& frame) } int recoveredSize = 0; - const int recoveredOffset = findValidFrame(buffer, 1, recoveredSize); + const int recoveredOffset = findValidFrame(buffer, 1, recoveredSize, getDefinitions().EnronModbus); if (recoveredOffset >= 0) { buffer.remove(0, recoveredOffset); @@ -313,7 +319,7 @@ bool ModbusRtuTcpServer::takeNextFrame(QByteArray& buffer, QByteArray& frame) continue; } - const int unknownFrameSize = validFrameSizeAt(buffer, 0); + const int unknownFrameSize = validFrameSizeAt(buffer, 0, getDefinitions().EnronModbus); if (unknownFrameSize > 0) { frame = buffer.left(unknownFrameSize); @@ -322,7 +328,7 @@ bool ModbusRtuTcpServer::takeNextFrame(QByteArray& buffer, QByteArray& frame) } int recoveredSize = 0; - const int recoveredOffset = findValidFrame(buffer, 1, recoveredSize); + const int recoveredOffset = findValidFrame(buffer, 1, recoveredSize, getDefinitions().EnronModbus); if (recoveredOffset >= 0) { buffer.remove(0, recoveredOffset); diff --git a/src/modbusserver.cpp b/src/modbusserver.cpp index fa315b94..4ec8bc83 100644 --- a/src/modbusserver.cpp +++ b/src/modbusserver.cpp @@ -11,6 +11,8 @@ #include #include #include "modbusserver.h" +#include "enronmodbus.h" +#include "modbuslimits.h" Q_LOGGING_CATEGORY(QT_MODBUS, "qt.modbus") Q_LOGGING_CATEGORY(QT_MODBUS_LOW, "qt.modbus.lowlevel") @@ -739,6 +741,30 @@ QModbusResponse ModbusServer::processReadInputRegistersRequest(const QModbusRequ return readBytes(request, QModbusDataUnit::InputRegisters, serverAddress); } +/// +/// \brief Maps Enron register addresses to pairs of words in the shared data store. +/// +QModbusDataUnit ModbusServer::requestRegisters(QModbusDataUnit::RegisterType type, + quint16 address, quint16 count) const +{ + if (!_definitions.EnronModbus) + return QModbusDataUnit(type, address, count); + + const quint32 end = quint32(address) + count; + if (end > quint32(ModbusLimits::addressSpaceSize(_definitions.AddrSpace))) + return {}; + + for (const quint16 base : {EnronModbus::LongBase, EnronModbus::FloatBase}) { + const quint32 limit = base + EnronModbus::RangeLength; + if (address < limit && end > base) { + if (address < base || end > limit) + return {}; + return QModbusDataUnit(type, base + 2 * (address - base), 2 * count); + } + } + return QModbusDataUnit(type, address, count); +} + /// /// \brief ModbusServer::readBytes /// \param request @@ -761,14 +787,16 @@ QModbusResponse ModbusServer::readBytes(const QModbusPdu &request, QModbusDataUn QModbusExceptionResponse::IllegalDataValue); } - // Get the requested range out of the registers. - QModbusDataUnit unit(unitType, address, count); - if (!data(&unit, serverAddress)) { + QModbusDataUnit unit = requestRegisters(unitType, address, count); + if (unit.valueCount() > 125) + return QModbusExceptionResponse(request.functionCode(), QModbusExceptionResponse::IllegalDataValue); + + if (!unit.isValid() || !data(&unit, serverAddress)) { return QModbusExceptionResponse(request.functionCode(), QModbusExceptionResponse::IllegalDataAddress); } - return QModbusResponse(request.functionCode(), quint8(count * 2), unit.values()); + return QModbusResponse(request.functionCode(), quint8(unit.valueCount() * 2), unit.values()); } /// @@ -790,7 +818,28 @@ QModbusResponse ModbusServer::processWriteSingleCoilRequest(const QModbusRequest /// QModbusResponse ModbusServer::processWriteSingleRegisterRequest(const QModbusRequest &request, int serverAddress) { - return writeSingle(request, QModbusDataUnit::HoldingRegisters, serverAddress); + if (!_definitions.EnronModbus || request.dataSize() < 2) + return writeSingle(request, QModbusDataUnit::HoldingRegisters, serverAddress); + + quint16 address; + request.decodeData(&address); + QModbusDataUnit unit = requestRegisters(QModbusDataUnit::HoldingRegisters, address, 1); + if (!unit.isValid()) + return QModbusExceptionResponse(request.functionCode(), QModbusExceptionResponse::IllegalDataAddress); + if (unit.valueCount() == 1) + return writeSingle(request, QModbusDataUnit::HoldingRegisters, serverAddress); + + if (request.dataSize() != 6) + return QModbusExceptionResponse(request.functionCode(), QModbusExceptionResponse::IllegalDataValue); + if (!data(&unit, serverAddress)) + return QModbusExceptionResponse(request.functionCode(), QModbusExceptionResponse::IllegalDataAddress); + + quint16 high, low; + request.decodeData(&address, &high, &low); + unit.setValues({high, low}); + if (!setData(unit, serverAddress)) + return QModbusExceptionResponse(request.functionCode(), QModbusExceptionResponse::ServerDeviceFailure); + return QModbusResponse(request.functionCode(), request.data()); } /// @@ -1061,19 +1110,25 @@ QModbusResponse ModbusServer::processWriteMultipleRegistersRequest(const QModbus quint8 byteCount; request.decodeData(&address, &numberOfRegisters, &byteCount); - // byte count does not match number of data bytes following or register count - if ((byteCount != (request.dataSize() - 5 )) || (byteCount != (numberOfRegisters * 2))) { + if (numberOfRegisters == 0) + return QModbusExceptionResponse(request.functionCode(), QModbusExceptionResponse::IllegalDataValue); + + QModbusDataUnit registers = requestRegisters(QModbusDataUnit::HoldingRegisters, address, numberOfRegisters); + + if (!registers.isValid()) + return QModbusExceptionResponse(request.functionCode(), QModbusExceptionResponse::IllegalDataAddress); + + // Enron quantities count logical registers rather than backing words. + if ((byteCount != (request.dataSize() - 5 )) || (byteCount != (registers.valueCount() * 2))) { return QModbusExceptionResponse(request.functionCode(), QModbusExceptionResponse::IllegalDataValue); } - if ((numberOfRegisters < 0x0001) || (numberOfRegisters > 0x007B)) { + if ((numberOfRegisters < 0x0001) || (registers.valueCount() > 0x007B)) { return QModbusExceptionResponse(request.functionCode(), QModbusExceptionResponse::IllegalDataValue); } - // Get the requested range out of the registers. - QModbusDataUnit registers(QModbusDataUnit::HoldingRegisters, address, numberOfRegisters); if (!data(®isters, serverAddress)) { return QModbusExceptionResponse(request.functionCode(), QModbusExceptionResponse::IllegalDataAddress); @@ -1084,7 +1139,7 @@ QModbusResponse ModbusServer::processWriteMultipleRegistersRequest(const QModbus QVector values; quint16 tmp; - for (int i = 0; i < numberOfRegisters; i++) { + for (int i = 0; i < int(registers.valueCount()); i++) { stream >> tmp; values.append(tmp); } @@ -1144,6 +1199,12 @@ QModbusResponse ModbusServer::processMaskWriteRegisterRequest(const QModbusReque quint16 address, andMask, orMask; request.decodeData(&address, &andMask, &orMask); + const auto unit = requestRegisters(QModbusDataUnit::HoldingRegisters, address, 1); + if (!unit.isValid()) + return QModbusExceptionResponse(request.functionCode(), QModbusExceptionResponse::IllegalDataAddress); + if (unit.valueCount() != 1) + return QModbusExceptionResponse(request.functionCode(), QModbusExceptionResponse::IllegalFunction); + quint16 reg; if (!data(QModbusDataUnit::HoldingRegisters, address, ®, serverAddress)) { return QModbusExceptionResponse(request.functionCode(), @@ -1172,22 +1233,27 @@ QModbusResponse ModbusServer::processReadWriteMultipleRegistersRequest(const QMo request.decodeData(&readStartAddress, &readQuantity, &writeStartAddress, &writeQuantity, &byteCount); - // byte count does not match number of data bytes following or register count - if ((byteCount != (request.dataSize() - 9 )) || (byteCount != (writeQuantity * 2))) { + if (readQuantity == 0 || writeQuantity == 0) + return QModbusExceptionResponse(request.functionCode(), QModbusExceptionResponse::IllegalDataValue); + + QModbusDataUnit writeRegisters = requestRegisters(QModbusDataUnit::HoldingRegisters, writeStartAddress, writeQuantity); + QModbusDataUnit readRegisters = requestRegisters(QModbusDataUnit::HoldingRegisters, readStartAddress, readQuantity); + + if (!writeRegisters.isValid() || !readRegisters.isValid()) + return QModbusExceptionResponse(request.functionCode(), QModbusExceptionResponse::IllegalDataAddress); + + // Enron quantities count logical registers rather than backing words. + if ((byteCount != (request.dataSize() - 9 )) || (byteCount != (writeRegisters.valueCount() * 2))) { return QModbusExceptionResponse(request.functionCode(), QModbusExceptionResponse::IllegalDataValue); } - if ((readQuantity < 0x0001) || (readQuantity > 0x007B) - || (writeQuantity < 0x0001) || (writeQuantity > 0x0079)) { + if ((readQuantity < 0x0001) || (readRegisters.valueCount() > 125) + || (writeQuantity < 0x0001) || (writeRegisters.valueCount() > 121)) { return QModbusExceptionResponse(request.functionCode(), QModbusExceptionResponse::IllegalDataValue); } - // According to spec, write operation is executed before the read operation - // Get the requested range out of the registers. - QModbusDataUnit writeRegisters(QModbusDataUnit::HoldingRegisters, writeStartAddress, - writeQuantity); if (!data(&writeRegisters, serverAddress)) { return QModbusExceptionResponse(request.functionCode(), QModbusExceptionResponse::IllegalDataAddress); @@ -1198,7 +1264,7 @@ QModbusResponse ModbusServer::processReadWriteMultipleRegistersRequest(const QMo QVector values; quint16 tmp; - for (int i = 0; i < writeQuantity; i++) { + for (int i = 0; i < int(writeRegisters.valueCount()); i++) { stream >> tmp; values.append(tmp); } @@ -1210,15 +1276,12 @@ QModbusResponse ModbusServer::processReadWriteMultipleRegistersRequest(const QMo QModbusExceptionResponse::ServerDeviceFailure); } - // Get the requested range out of the registers. - QModbusDataUnit readRegisters(QModbusDataUnit::HoldingRegisters, readStartAddress, - readQuantity); if (!data(&readRegisters, serverAddress)) { return QModbusExceptionResponse(request.functionCode(), QModbusExceptionResponse::IllegalDataAddress); } - return QModbusResponse(request.functionCode(), quint8(readQuantity * 2), + return QModbusResponse(request.functionCode(), quint8(readRegisters.valueCount() * 2), readRegisters.values()); } @@ -1234,12 +1297,23 @@ QModbusResponse ModbusServer::processReadFifoQueueRequest(const QModbusRequest & quint16 address; request.decodeData(&address); + const auto unit = requestRegisters(QModbusDataUnit::HoldingRegisters, address, 1); + if (!unit.isValid()) + return QModbusExceptionResponse(request.functionCode(), QModbusExceptionResponse::IllegalDataAddress); + if (unit.valueCount() != 1) + return QModbusExceptionResponse(request.functionCode(), QModbusExceptionResponse::IllegalFunction); + quint16 fifoCount; if (!data(QModbusDataUnit::HoldingRegisters, address, &fifoCount, serverAddress)) { return QModbusExceptionResponse(request.functionCode(), QModbusExceptionResponse::IllegalDataAddress); } + if (_definitions.EnronModbus + && quint32(address) + 1 + fifoCount > quint32(ModbusLimits::addressSpaceSize(_definitions.AddrSpace))) { + return QModbusExceptionResponse(request.functionCode(), QModbusExceptionResponse::IllegalDataAddress); + } + if (fifoCount > 31u) { return QModbusExceptionResponse(request.functionCode(), QModbusExceptionResponse::IllegalDataValue); diff --git a/src/modbusserver.h b/src/modbusserver.h index eb9bdc07..12ae9c91 100644 --- a/src/modbusserver.h +++ b/src/modbusserver.h @@ -168,6 +168,8 @@ class ModbusServer : public QObject virtual QModbusResponse processRequest(const QModbusPdu &request, int serverAddress); virtual QModbusResponse processPrivateRequest(const QModbusPdu &request, int serverAddress); + QModbusDataUnit requestRegisters(QModbusDataUnit::RegisterType type, quint16 address, quint16 count) const; + void resetCommunicationCounters(int serverAddress) { _counters[serverAddress].fill(0u); } void incrementCounter(ModbusServer::Counter counter, int serverAddress) { _counters[serverAddress][counter]++; } diff --git a/src/modbustcpserver.cpp b/src/modbustcpserver.cpp index 9259cb00..a3910a61 100644 --- a/src/modbustcpserver.cpp +++ b/src/modbustcpserver.cpp @@ -156,7 +156,14 @@ void ModbusTcpServer::on_newConnection() } QModbusRequest request; - input >> request; + if (mbDef.EnronModbus && bytesPdu > 0 + && quint8(buffer->at(mbpaHeaderSize)) == QModbusPdu::WriteSingleRegister) { + // Qt's PDU stream decoder assumes a two-byte FC06 value. + request = QModbusRequest(QModbusPdu::WriteSingleRegister, + buffer->mid(mbpaHeaderSize + 1, bytesPdu - 1)); + } else { + input >> request; + } buffer->remove(0, current); diff --git a/src/modbuswriteparams.h b/src/modbuswriteparams.h index 07ae3b83..697f065b 100644 --- a/src/modbuswriteparams.h +++ b/src/modbuswriteparams.h @@ -11,6 +11,7 @@ #include #include "enums.h" +#include "enronmodbus.h" class ModbusMultiServer; @@ -30,6 +31,7 @@ struct ModbusWriteParams bool ZeroBasedAddress; bool LeadingZeros = false; ModbusMultiServer* Server = nullptr; + EnronModbus::RegisterType EnronType = EnronModbus::RegisterType::None; }; Q_DECLARE_METATYPE(ModbusWriteParams) diff --git a/src/res/translations/omodsim_it.qm b/src/res/translations/omodsim_it.qm index 02b26565..8fa4c7df 100644 Binary files a/src/res/translations/omodsim_it.qm and b/src/res/translations/omodsim_it.qm differ diff --git a/src/res/translations/omodsim_it.ts b/src/res/translations/omodsim_it.ts index 338ef008..00cc0f8b 100644 --- a/src/res/translations/omodsim_it.ts +++ b/src/res/translations/omodsim_it.ts @@ -217,14 +217,14 @@ ByteOrder - - + + Direct Diretto - - + + Swapped Scambiato @@ -232,12 +232,12 @@ ByteOrderItemDelegate - + Use bytes in register order Usa i byte nell'ordine del registro - + Swap bytes inside each 16-bit register Scambia byte all'interno di ciascun registro a 16 bit @@ -367,47 +367,61 @@ Registri blocco - + + + + Enron Registers Int32 + Registri Enron Int32 + + + + + + Enron Registers Float32 + Registri Enron Float32 + + + Unit Unità - + Type Tipo - + Address Indirizzo - + Data Type Tipo di dati - + Register Order Ordine registro - + Byte Order Ordine byte - + Comment Commento - + Value Valore - + Timestamp Data/ora @@ -415,62 +429,62 @@ DataTypeItemDelegate - + 1-bit value (Coils / Discrete Inputs) Valore a 1 bit (bobine/ingressi discreti) - + 16-bit register value shown as 16 binary digits Valore del registro a 16 bit visualizzato come 16 cifre binarie - + Unsigned 16-bit integer (0 … 65535) Intero a 16 bit senza segno (0...65535) - + Signed 16-bit integer (−32768 … 32767) Intero a 16 bit con segno (−32768 … 32767) - + 16-bit value displayed as hexadecimal Valore a 16 bit visualizzato come esadecimale - + 16-bit value displayed as ANSI character Valore a 16 bit visualizzato come carattere ANSI - + IEEE 754 single-precision float (2 registers) IEEE 754 virgola mobile precisione singola (2 registri) - + IEEE 754 double-precision float (4 registers) IEEE 754 virgola mobile doppia precisione (4 registri) - + Signed 32-bit integer (2 registers) Intero a 32 bit con segno (2 registri) - + Unsigned 32-bit integer (2 registers) Intero a 32 bit senza segno (2 registri) - + Signed 64-bit integer (4 registers) Intero a 64 bit con segno (4 registri) - + Unsigned 64-bit integer (4 registers) Intero a 64 bit senza segno (4 registri) @@ -1070,48 +1084,58 @@ Vuoi aprire la pagina di download? Aggiungi automaticamente registri su richiesta - + + Select Enron Int32 or Float32 registers and enter the full protocol address, starting at 5001 or 7001. Select the display format separately in the toolbar. + + + + + Enable Enron Modbus (32-bit registers) + + + + Error Simulations Simulazioni di errori - + Response with incorrect IDs Risposta con ID errati - + Response with ILLEGAL FUNCTION Risposta con FUNZIONE ILLEGALE - + Response with DEVICE BUSY Risposta con DISPOSITIVO OCCUPATO - + Response with incorrect CRC (Serial Line only) Risposta con CRC errato (solo linea seriale) - + Response with delay Risposta con ritardo - - + + ms ms - + Response with random delay from 0 to Risposta con ritardo casuale da 0 a - + No response Nessuna risposta @@ -1723,6 +1747,16 @@ Vuoi aprire la pagina di download? DialogWriteRegister + + + Address (low word): + + + + + Address (high word): + + Write Register @@ -1745,7 +1779,7 @@ Vuoi aprire la pagina di download? - + Auto Simulation: OFF Simulazione automatica: OFF @@ -1755,18 +1789,18 @@ Vuoi aprire la pagina di download? Modello bit: - + Value, (HEX): Valore, (ESA): - + Value, (ANSI): Valore, (ANSI): - - + + Auto Simulation: ON Simulazione automatica: ON @@ -1916,74 +1950,73 @@ Vuoi aprire la pagina di download? - + Clear Table Cancella tabella - + Clear all visible rows from the table? Vuoi cancellare tutte le righe visibili dalla tabella? - + 1-based base 1 - + 0-based base 0 - + Remove Color Rimuovi colore - + Yellow Giallo - + Cyan Ciano - + Magenta Magenta - + LightGreen Verde chiaro - + Orange Arancio - + LightBlue Azzurro - + LightGray Grigio chiaro - - - + + All Types Tutti i tipi - + Address Base: %1 Unit Filter: %2 Type Filter: %3 @@ -1992,58 +2025,81 @@ Filtro unità: %2 Tipo filtro: %3 - - + Coils Bobine - - + Discrete Inputs Ingressi discreti - - + Input Registers Registri ingresso - - + Holding Registers Registri blocco - - + + Enron Registers Int32 + Registri Enron Int32 + + + + Enron Registers Float32 + Registri Enron Float32 + + + + Unit: Unità: - - + + Data Type: Tipo di dati: - - - + + + All Tutto FormDataView + + + Register Type: + + + + + Enron Address: + + + + + Register Count: + + + Starting Address: Indirizzo iniziale: + Length: Lunghezza: @@ -2057,11 +2113,6 @@ Tipo filtro: %3 Column Distance: Distanza colonna: - - - Data Type: - Tipo di dati: - Leading Zeros for Unsigned @@ -2192,17 +2243,17 @@ Tipo filtro: %3 Scambia byte (0xAB → 0xBA) - + 0-based base 0 - + 1-based base 1 - + Address Base: %1 Starting Address: %2 Length: %3 @@ -2211,7 +2262,7 @@ Indirizzo iniziale: %2 Lunghezza: %3 - + Unit Identifier: %1 Data Type: %2 @@ -2663,7 +2714,7 @@ Vuoi aprire la pagina di download? Imposta - + Script Script @@ -3000,314 +3051,314 @@ Verifica attentamente il risultato dell'unione. Senza titolo - + Address Base Base indirizzo - + Server connected: %1 Server connesso: %1 - + Server disconnected: %1 Server disconnesso: %1 - + Modbus client connected: %1 -> %2 Client Modbus connesso: %1 -> %2 - + Modbus client disconnected: %1 -> %2 Client Modbus disconnesso: %1 -> %2 - + [Unit %1] %2 [Unità %1] %2 - + Address space added: unit %1, %2, starting address %3, length %4 Spazio indirizzi aggiunto: unità %1, %2, indirizzo iniziale %3, lunghezza %4 - + <empty> <vuoto> - + Manual write: unit %1, %2, starting address %3, value %4 Scrittura manuale: unità %1, %2, indirizzo iniziale %3, valore %4 - + Client write: %1 -> unit %2, %3, starting address %4, value %5 Scrittura client: %1 -> unità %2, %3, indirizzo iniziale %4, valore %5 - + unknown client client sconosciuto - + Address comment changed: unit %1, %2, address %3: '%4' Commento all'indirizzo modificato: unità %1, %2, indirizzo %3: '%4' - + Auto simulation enabled (%1): unit %2, %3, address %4 Simulazione automatica abilitata (%1): unità %2, %3, indirizzo %4 - + Auto simulation disabled: unit %1, %2, address %3 Simulazione automatica disabilitata: unità %1, %2, indirizzo %3 - + [%1 values: %2%3] [%1 valori: %2%3] - + Modbus RTU over TCP/IP RTU Modbus su TCP/IP - + Traffic Traffico - + Map Mappa - + Form Modulo - + <null> <null> - + <untitled> <senza titolo> - + %1 '%2' %1 '%2' - + Font Font - + FontZoom FontZoom - + BackgroundColor ColoreSfondo - + ForegroundColor Colore primo piano - + AddressColor IndirizzoColore - + CommentColor ColoreCommento - + CheckForUpdates ControllaAggiornamentiu - + ShowWelcomeDialog Visualizza finestra di benvenuto - + Language Lingua - + ScriptFont Font script - + CodeAutoComplete CodiceCompletamentoAutomatico - + AutoShowConsoleOutput VisualizzaAutomaticamenteOutputConsole - + ConsoleMaxLines LineeMaxConsole - + DataView.ColumnsDistance DataView.ColumnsDistance - + DataView.LeadingZeros DataView.LeadingZeros - + TrafficView.LogLimit TrafficView.LogLimit - + TrafficView.AutoScroll TrafficView.AutoScroll - + ThemeMode Modalità tema - + true vero - + false falso - + enabled abilitato - + disabled disabilitato - + Unit ID added: %1 ID unità aggiunto: %1 - + Unit ID removed: %1 ID unità rimosso: %1 - + Address space removed: unit %1, %2, starting address %3, length %4 Spazio indirizzi rimosso: unità %1, %2, indirizzo iniziale %3, lunghezza %4 - + Project opened: %1 Progetto aperto: %1 - + <unsaved> <non salvato> - + Project closed: %1 Progetto chiuso: %1 - + Project saved: %1 Progetto salvato: %1 - + Project save failed: %1 (%2) Salvataggio del progetto non riuscito: %1 (%2) - + Project load failed: %1 (%2) Caricamento del progetto non riuscito: %1 (%2) - + Form created: %1 Modulo creato: %1 - + Form opened: %1 Modulo aperto: %1 - + Form closed: %1 Modulo chiuso: %1 - + Form deleted: %1 Modulo eliminato: %1 - + Preference changed: %1: %2 -> %3 La preferenza è stata modificata: %1: %2 -> %3 - + %1 changed: %2 -> %3 %1 modificato: %2 -> %3 - + Script started: %1 (mode: %2, interval: %3 ms) Script avviato: %1 (modalità: %2, intervallo: %3 ms) - + Script started: %1 (mode: %2) Script avviato: %1 (modalità: %2) - + Form renamed: '%1' -> '%2' Modulo rinominato: '%1' -> '%2' @@ -3317,19 +3368,19 @@ Verifica attentamente il risultato dell'unione. Passa ad un altro pannello - + 1-based base 1 - + 0-based base 0 - + Hex View Vista esadecimale @@ -3344,7 +3395,7 @@ Verifica attentamente il risultato dell'unione. Nessun progetto recente - + Data Dati @@ -3354,7 +3405,7 @@ Verifica attentamente il risultato dell'unione. File JavaScript (*.js);;Tutti i file (*) - + Script stopped: %1 Script interrotto: %1 @@ -3734,12 +3785,12 @@ Verifica attentamente il risultato dell'unione. ModbusMultiServer - + An incorrect device ID was specified (%1) È stato specificato un ID dispositivo errato (%1) - + Connection error. %1 Errore di connessione.%1 @@ -3770,12 +3821,12 @@ Verifica attentamente il risultato dell'unione. ModbusRtuTcpServer - + Could not write response to client Impossibile scrivere la risposta nel client - + Invalid connection settings for RTU over TCP/IP communication specified. Per la comunicazione RTU su TCP/IP sono state specificate impostazioni di connessione non valide. @@ -3783,7 +3834,7 @@ Verifica attentamente il risultato dell'unione. ModbusTcpServer - + Invalid connection settings for TCP communication specified. Per la comunicazione TCP sono state specificate impostazioni di connessione non valide. @@ -3791,67 +3842,67 @@ Verifica attentamente il risultato dell'unione. OutputDataWidget - + Set Value of %1 Imposta il valore di %1 - + Add Description Aggiungi descrizione - + Edit Description Modifica descrizione - + %1: Enter Description %1: inserisci la descrizione - + Remove Color Rimuovi colore - + Yellow Giallo - + Cyan Ciano - + Magenta Magenta - + LightGreen Verde chiaro - + Orange Arancio - + LightBlue Azzurro - + LightGray Grigio chiaro - + Zoom: %1% Zoom: %1% @@ -3890,26 +3941,38 @@ Verifica attentamente il risultato dell'unione. PointTypeComboBox - + + Enron Registers Int32 (Read-Write) + Registri Enron Int32 (lettura-scrittura) + + + + + Enron Registers Float32 (Read-Write) + Registri Enron Float32 (lettura-scrittura) + + + + Coils (Read-Write) Bobine (lettura-scrittura) - + Discrete Inputs (Read Only) Ingressi discreti (sola lettura) - + Holding Registers (Read-Write) Registri blocco (lettura-scrittura) - + Input Registers (Read Only) Registri ingresso (sola lettura) @@ -4029,47 +4092,47 @@ Verifica attentamente il risultato dell'unione. QModbusDevice - + Referenced serial device does not exist. Il dispositivo seriale di riferimento non esiste. - + Cannot open serial device due to permissions. Impossibile aprire il dispositivo seriale a causa delle autorizzazioni. - + Cannot open serial device. Impossibile aprire il dispositivo seriale. - + Write error. Errore di scrittura. - + Read error. Errore di lettura. - + Resource error. Errore di risorsa. - + Device operation is not supported error. Il funzionamento del dispositivo non è supportato errore. - + Timeout error. Errore timeout. - + Unknown error. Errore sconosciuto. @@ -4077,12 +4140,12 @@ Verifica attentamente il risultato dell'unione. QModbusRtuSerialServer - + Requesting serial port is closed La richiesta della porta seriale è chiusa - + Could not write response to client Impossibile scrivere la risposta nel client @@ -4090,12 +4153,12 @@ Verifica attentamente il risultato dell'unione. QModbusTcpServer - + Requesting socket is closed Il socket richiedente è chiuso - + Could not write response to client Impossibile scrivere la risposta nel client @@ -4157,12 +4220,12 @@ Verifica attentamente il risultato dell'unione. RegistryOrderItemDelegate - + Most Significant Register First - big-endian word order Primo registro più significativo: ordine delle parole big-endian - + Least Significant Register First - little-endian word order Primo registro meno significativo: ordine delle parole little-endian @@ -4170,14 +4233,14 @@ Verifica attentamente il risultato dell'unione. RunModeComboBox - + Once Una volta - + Periodically @@ -4372,29 +4435,6 @@ Verifica attentamente il risultato dell'unione. Vuoi cancellare tutti i messaggi dal registro del traffico? - - TypeItemDelegate - - - Coils - Bobine - - - - Discrete Inputs - Ingressi discreti - - - - Input Registers - Registri ingresso - - - - Holding Registers - Registri blocco - - UpdateChecker diff --git a/src/res/translations/omodsim_ru.qm b/src/res/translations/omodsim_ru.qm index 2dafe8c7..6dd67407 100644 Binary files a/src/res/translations/omodsim_ru.qm and b/src/res/translations/omodsim_ru.qm differ diff --git a/src/res/translations/omodsim_ru.ts b/src/res/translations/omodsim_ru.ts index e14da724..2e1a1953 100644 --- a/src/res/translations/omodsim_ru.ts +++ b/src/res/translations/omodsim_ru.ts @@ -217,14 +217,14 @@ ByteOrder - - + + Direct Прямой - - + + Swapped Обратный @@ -232,12 +232,12 @@ ByteOrderItemDelegate - + Use bytes in register order Использовать байты в порядке регистра - + Swap bytes inside each 16-bit register Поменять байты местами внутри каждого 16-битного регистра @@ -367,110 +367,124 @@ Регистры хранения - + Unit Устройство - + Type Тип - + Address Адрес - + Data Type Тип данных - + Register Order Порядок регистров - + Byte Order Порядок байт - + Comment Комментарий - + Value Значение - + Timestamp Время + + + + + Enron Registers Int32 + Регистры Enron Int32 + + + + + + Enron Registers Float32 + Регистры Enron Float32 + DataTypeItemDelegate - + 1-bit value (Coils / Discrete Inputs) 1-битное значение (Флаги / Дискретные входы) - + 16-bit register value shown as 16 binary digits 16-битное значение регистра в виде 16 двоичных цифр - + Unsigned 16-bit integer (0 … 65535) 16-битное целое без знака (0 … 65535) - + Signed 16-bit integer (−32768 … 32767) 16-битное целое со знаком (-32768 … 32767) - + 16-bit value displayed as hexadecimal 16-битное значение в шестнадцатеричном формате - + 16-bit value displayed as ANSI character 16-битное значение в виде символа ANSI - + IEEE 754 single-precision float (2 registers) Число с плавающей точкой одинарной точности IEEE 754 (2 регистра) - + IEEE 754 double-precision float (4 registers) Число с плавающей точкой двойной точности IEEE 754 (4 регистра) - + Signed 32-bit integer (2 registers) Знаковое 32-битное целое (2 регистра) - + Unsigned 32-bit integer (2 registers) Беззнаковое 32-битное целое (2 регистра) - + Signed 64-bit integer (4 registers) Знаковое 64-битное целое (4 регистра) - + Unsigned 64-bit integer (4 registers) Беззнаковое 64-битное целое (4 регистра) @@ -1044,6 +1058,16 @@ Open the download page? DialogModbusDefinitions + + + Enable Enron Modbus (32-bit registers) + Включить Enron Modbus (32-битные регистры) + + + + Select Enron Int32 or Float32 registers and enter the full protocol address, starting at 5001 or 7001. Select the display format separately in the toolbar. + Выберите регистры Enron Int32 или Float32 и задавайте полный адрес протокола, начиная с 5001 или 7001. Формат отображения выбирайте отдельно на панели инструментов. + Modbus Definitions @@ -1070,48 +1094,48 @@ Open the download page? Автоматически добавлять регистры по запросу - + Error Simulations Симуляция ошибок - + Response with incorrect IDs Отвечать некорректным ID - + Response with ILLEGAL FUNCTION Отвечать ошибкой ILLEGAL FUNCTION - + Response with DEVICE BUSY Отвечать ошибкой DEVICE BUSY - + Response with incorrect CRC (Serial Line only) Отвечать с некорректной CRC (только для последовательного интерфейса) - + Response with delay Отвечать с задержкой - - + + ms мс - + Response with random delay from 0 to Отвечать со случайной задержкой от 0 до - + No response Не отвечать на запросы @@ -1723,6 +1747,16 @@ Open the download page? DialogWriteRegister + + + Address (low word): + Адрес (младшее слово): + + + + Address (high word): + Адрес (старшее слово): + Write Register @@ -1745,7 +1779,7 @@ Open the download page? - + Auto Simulation: OFF Симуляция: Выкл @@ -1755,18 +1789,18 @@ Open the download page? Битовый шаблон: - + Value, (HEX): Значение, (HEX): - + Value, (ANSI): Значение, (ANSI): - - + + Auto Simulation: ON Симуляция: Вкл @@ -1916,74 +1950,73 @@ Open the download page? - + Clear Table Очистить таблицу - + Clear all visible rows from the table? Удалить все видимые строки из таблицы? - + 1-based 1-первый - + 0-based 0-первый - + Remove Color Удалить цвет - + Yellow Желтый - + Cyan Циан - + Magenta Пурпурный - + LightGreen Светло-зеленый - + Orange Оранжевый - + LightBlue Светло-синий - + LightGray Светло-серый - - - + + All Types Все типы - + Address Base: %1 Unit Filter: %2 Type Filter: %3 @@ -1992,58 +2025,81 @@ Type Filter: %3 Фильтр типов: %3 - - + Coils Регистры флагов - - + Discrete Inputs Дискретные входы - - + Input Registers Регистры ввода - - + Holding Registers Регистры хранения - - + + Unit: Устройство: - - + + Data Type: Тип данных: - - - + + + All Все + + + Enron Registers Int32 + Регистры Enron Int32 + + + + Enron Registers Float32 + Регистры Enron Float32 + FormDataView + + + Register Type: + Тип регистров: + + + + Enron Address: + Адрес Enron: + + + + Register Count: + Количество регистров: + + Starting Address: Начальный адрес: + Length: Количество: @@ -2057,11 +2113,6 @@ Type Filter: %3 Column Distance: Расстояние между столбцами: - - - Data Type: - Тип данных: - Leading Zeros for Unsigned @@ -2192,17 +2243,17 @@ Type Filter: %3 Перестановка байт (0xAB → 0xBA) - + 0-based 0-первый - + 1-based 1-первый - + Address Base: %1 Starting Address: %2 Length: %3 @@ -2211,7 +2262,7 @@ Length: %3 Длина: %3 - + Unit Identifier: %1 Data Type: %2 @@ -2663,7 +2714,7 @@ Open the download page? Настройка - + Script Скрипт @@ -3000,314 +3051,314 @@ Please verify the merge result carefully. Без имени - + Address Base Адресация - + Server connected: %1 Сервер подключён: %1 - + Server disconnected: %1 Сервер отключён: %1 - + Modbus client connected: %1 -> %2 Клиент Modbus подключён: %1 -> %2 - + Modbus client disconnected: %1 -> %2 Клиент Modbus отключён: %1 -> %2 - + [Unit %1] %2 [Устройство %1] %2 - + Address space added: unit %1, %2, starting address %3, length %4 Добавлено адресное пространство: устройство %1, %2, начальный адрес %3, длина %4 - + <empty> <пусто> - + Manual write: unit %1, %2, starting address %3, value %4 Ручная запись: устройство %1, %2, начальный адрес %3, значение %4 - + Client write: %1 -> unit %2, %3, starting address %4, value %5 Запись клиентом: %1 -> устройство %2, %3, начальный адрес %4, значение %5 - + unknown client неизвестный клиент - + Address comment changed: unit %1, %2, address %3: '%4' Изменён комментарий адреса: устройство %1, %2, адрес %3: '%4' - + Auto simulation enabled (%1): unit %2, %3, address %4 Автосимуляция включена (%1): устройство %2, %3, адрес %4 - + Auto simulation disabled: unit %1, %2, address %3 Автосимуляция отключена: устройство %1, %2, адрес %3 - + [%1 values: %2%3] [%1 значений: %2%3] - + Modbus RTU over TCP/IP Modbus RTU по TCP/IP - + Traffic Трафик - + Map Карта - + Form Форма - + <null> <null> - + <untitled> <без имени> - + %1 '%2' %1 '%2' - + Font Шрифт - + FontZoom Масштаб шрифта - + BackgroundColor Цвет фона - + ForegroundColor Цвет текста - + AddressColor Цвет адреса - + CommentColor Цвет комментария - + CheckForUpdates Проверять обновления - + ShowWelcomeDialog Показывать диалог приветствия - + Language Язык - + ScriptFont Шрифт скрипта - + CodeAutoComplete Автодополнение кода - + AutoShowConsoleOutput Автоматически показывать консоль вывода - + ConsoleMaxLines Ограничение вывода консоли - + DataView.ColumnsDistance Данные.Расстояние между столбцами - + DataView.LeadingZeros Данные.Ведущие нули для беззнаковых - + TrafficView.LogLimit Просмотр трафика.Лимит лога - + TrafficView.AutoScroll Просмотр трафика.Автопрокрутка - + ThemeMode Тема оформления - + true истина - + false ложь - + enabled вкл. - + disabled выкл. - + Unit ID added: %1 Устройство добавлено: %1 - + Unit ID removed: %1 Устройство удалено: %1 - + Address space removed: unit %1, %2, starting address %3, length %4 Удалено адресное пространство: устройство %1, %2, начальный адрес %3, длина %4 - + Project opened: %1 Проект открыт: %1 - + <unsaved> <несохранённый> - + Project closed: %1 Проект закрыт: %1 - + Project saved: %1 Проект сохранен: %1 - + Project save failed: %1 (%2) Ошибка сохранения проекта: %1 (%2) - + Project load failed: %1 (%2) Не удалось загрузить проект: %1 (%2) - + Form created: %1 Форма создана: %1 - + Form opened: %1 Форма открыта: %1 - + Form closed: %1 Форма закрыта: %1 - + Form deleted: %1 Форма удалена: %1 - + Preference changed: %1: %2 -> %3 Настройки изменены: %1: %2 -> %3 - + %1 changed: %2 -> %3 %1 изменено: %2 -> %3 - + Script started: %1 (mode: %2, interval: %3 ms) Скрипт запущен: %1 (режим: %2, интервал: %3 мс) - + Script started: %1 (mode: %2) Скрипт запущен: %1 (режим: %2) - + Form renamed: '%1' -> '%2' Форма переименована: '%1' -> '%2' @@ -3317,19 +3368,19 @@ Please verify the merge result carefully. Переместить на другую панель - + 1-based 1-первый - + 0-based 0-первый - + Hex View Шестнадцатиричный режим @@ -3344,7 +3395,7 @@ Please verify the merge result carefully. Нет недавних проектов - + Data Данные @@ -3354,7 +3405,7 @@ Please verify the merge result carefully. Файлы JavaScript (*.js);;Все файлы (*) - + Script stopped: %1 Скрипт остановлен: %1 @@ -3734,12 +3785,12 @@ Please verify the merge result carefully. ModbusMultiServer - + An incorrect device ID was specified (%1) Некорректное значение номера устройства (%1) - + Connection error. %1 Ошибка подключения. %1 @@ -3770,12 +3821,12 @@ Please verify the merge result carefully. ModbusRtuTcpServer - + Could not write response to client Не удалось создать ответ клиенту - + Invalid connection settings for RTU over TCP/IP communication specified. Указаны неверные параметры подключения RTU через TCP/IP. @@ -3783,7 +3834,7 @@ Please verify the merge result carefully. ModbusTcpServer - + Invalid connection settings for TCP communication specified. Указаны некорректные настройки для TCP соединения. @@ -3791,67 +3842,67 @@ Please verify the merge result carefully. OutputDataWidget - + Set Value of %1 Записать значение по адресу %1 - + Add Description Добавить описание - + Edit Description Редактировать описание - + %1: Enter Description %1: Введите описание - + Remove Color Удалить цвет - + Yellow Желтый - + Cyan Циан - + Magenta Пурпурный - + LightGreen Светло-зеленый - + Orange Оранжевый - + LightBlue Светло-синий - + LightGray Светло-серый - + Zoom: %1% Масштаб: %1% @@ -3890,26 +3941,38 @@ Please verify the merge result carefully. PointTypeComboBox - + + Enron Registers Int32 (Read-Write) + Регистры Enron Int32 (чтение-запись) + + + + + Enron Registers Float32 (Read-Write) + Регистры Enron Float32 (чтение-запись) + + + + Coils (Read-Write) Регистры флагов (чтение-запись) - + Discrete Inputs (Read Only) Дискретные входы (чтение) - + Holding Registers (Read-Write) Регистры хранения (чтение-запись) - + Input Registers (Read Only) Регистры ввода (чтение) @@ -4029,47 +4092,47 @@ Please verify the merge result carefully. QModbusDevice - + Referenced serial device does not exist. Указанное устройство не существует. - + Cannot open serial device due to permissions. Невозможно открыть устройство из-за разрешений доступа. - + Cannot open serial device. Невозможно открыть устройство. - + Write error. Ошибка записи. - + Read error. Ошибка чтения. - + Resource error. Ошибка ресурса. - + Device operation is not supported error. Работа устройства не поддерживается. - + Timeout error. Время ожидания истекло. - + Unknown error. Неизвестная ошибка. @@ -4077,12 +4140,12 @@ Please verify the merge result carefully. QModbusRtuSerialServer - + Requesting serial port is closed Запрашиваемый последовательный порт закрыт - + Could not write response to client Не удалось создать ответ клиенту @@ -4090,12 +4153,12 @@ Please verify the merge result carefully. QModbusTcpServer - + Requesting socket is closed Запрашивающий сокет закрыт - + Could not write response to client Не удалось создать ответ клиенту @@ -4157,12 +4220,12 @@ Please verify the merge result carefully. RegistryOrderItemDelegate - + Most Significant Register First - big-endian word order Старший регистр первый - порядок слов big-endian - + Least Significant Register First - little-endian word order Младший регистр первый - порядок слов little-endian @@ -4170,14 +4233,14 @@ Please verify the merge result carefully. RunModeComboBox - + Once Однократно - + Periodically @@ -4372,29 +4435,6 @@ Please verify the merge result carefully. Удалить все сообщения из журнала трафика? - - TypeItemDelegate - - - Coils - Регистры флагов - - - - Discrete Inputs - Дискретные входы - - - - Input Registers - Регистры ввода - - - - Holding Registers - Регистры хранения - - UpdateChecker diff --git a/src/res/translations/omodsim_zh_CN.qm b/src/res/translations/omodsim_zh_CN.qm index d15b580d..9ac7d87c 100644 Binary files a/src/res/translations/omodsim_zh_CN.qm and b/src/res/translations/omodsim_zh_CN.qm differ diff --git a/src/res/translations/omodsim_zh_CN.ts b/src/res/translations/omodsim_zh_CN.ts index 3139aa1c..e9a52bd8 100644 --- a/src/res/translations/omodsim_zh_CN.ts +++ b/src/res/translations/omodsim_zh_CN.ts @@ -217,14 +217,14 @@ ByteOrder - - + + Direct 直接 - - + + Swapped 交换 @@ -232,12 +232,12 @@ ByteOrderItemDelegate - + Use bytes in register order 按寄存器顺序使用字节 - + Swap bytes inside each 16-bit register 交换每个 16 位寄存器内的字节 @@ -367,47 +367,61 @@ 保持寄存器 - + + + + Enron Registers Int32 + Enron Int32 寄存器 + + + + + + Enron Registers Float32 + Enron Float32 寄存器 + + + Unit 单元 - + Type 类型 - + Address 地址 - + Data Type 数据类型 - + Register Order 寄存器顺序 - + Byte Order 字节顺序 - + Comment 注释 - + Value 值 - + Timestamp 时间戳 @@ -415,62 +429,62 @@ DataTypeItemDelegate - + 1-bit value (Coils / Discrete Inputs) 1位值(线圈/离散输入) - + 16-bit register value shown as 16 binary digits 16位寄存器值,显示为16位二进制数字 - + Unsigned 16-bit integer (0 … 65535) 无符号16位整数(0 … 65535) - + Signed 16-bit integer (−32768 … 32767) 有符号16位整数(-32768 … 32767) - + 16-bit value displayed as hexadecimal 16位值,以十六进制显示 - + 16-bit value displayed as ANSI character 16位值,以ANSI字符显示 - + IEEE 754 single-precision float (2 registers) IEEE 754单精度浮点数(2个寄存器) - + IEEE 754 double-precision float (4 registers) IEEE 754双精度浮点数(4个寄存器) - + Signed 32-bit integer (2 registers) 有符号32位整数(2个寄存器) - + Unsigned 32-bit integer (2 registers) 无符号32位整数(2个寄存器) - + Signed 64-bit integer (4 registers) 有符号64位整数(4个寄存器) - + Unsigned 64-bit integer (4 registers) 无符号64位整数(4个寄存器) @@ -1070,48 +1084,58 @@ Open the download page? 根据请求自动添加寄存器 - + + Select Enron Int32 or Float32 registers and enter the full protocol address, starting at 5001 or 7001. Select the display format separately in the toolbar. + + + + + Enable Enron Modbus (32-bit registers) + + + + Error Simulations 错误模拟 - + Response with incorrect IDs 带有错误 ID 的响应 - + Response with ILLEGAL FUNCTION 响应非法功能 - + Response with DEVICE BUSY 响应 DEVICE BUSY - + Response with incorrect CRC (Serial Line only) 响应带有错误的 CRC(仅限串行线路) - + Response with delay 延迟响应 - - + + ms 毫秒 - + Response with random delay from 0 to 响应随机延迟从 0 到 - + No response 没有响应 @@ -1723,6 +1747,16 @@ Open the download page? DialogWriteRegister + + + Address (low word): + + + + + Address (high word): + + Write Register @@ -1745,7 +1779,7 @@ Open the download page? - + Auto Simulation: OFF 自动模拟:关闭 @@ -1755,18 +1789,18 @@ Open the download page? 位模式: - + Value, (HEX): 值(十六进制): - + Value, (ANSI): 值,(ANSI): - - + + Auto Simulation: ON 自动模拟:开启 @@ -1916,74 +1950,73 @@ Open the download page? - + Clear Table 清除表格 - + Clear all visible rows from the table? 是否清除表格中所有可见行? - + 1-based 基地址为-1 - + 0-based 基地址为-0 - + Remove Color 去除颜色 - + Yellow 黄色 - + Cyan 青色 - + Magenta 品红色 - + LightGreen 浅绿色 - + Orange 橙色 - + LightBlue 浅蓝色 - + LightGray 浅灰色 - - - + + All Types 所有类型 - + Address Base: %1 Unit Filter: %2 Type Filter: %3 @@ -1992,58 +2025,81 @@ Type Filter: %3 类型过滤器:%3 - - + Coils 线圈 - - + Discrete Inputs 离散输入 - - + Input Registers 输入寄存器 - - + Holding Registers 保持寄存器 - - + + Enron Registers Int32 + Enron Int32 寄存器 + + + + Enron Registers Float32 + Enron Float32 寄存器 + + + + Unit: 单元: - - + + Data Type: 数据类型: - - - + + + All 全部 FormDataView + + + Register Type: + + + + + Enron Address: + + + + + Register Count: + + + Starting Address: 起始地址: + Length: 长度: @@ -2057,11 +2113,6 @@ Type Filter: %3 Column Distance: 列距: - - - Data Type: - 数据类型: - Leading Zeros for Unsigned @@ -2192,17 +2243,17 @@ Type Filter: %3 交换字节(0xAB → 0xBA) - + 0-based 基地址为-0 - + 1-based 基地址为-1 - + Address Base: %1 Starting Address: %2 Length: %3 @@ -2211,7 +2262,7 @@ Length: %3 长度:%3 - + Unit Identifier: %1 Data Type: %2 @@ -2663,7 +2714,7 @@ Open the download page? 设置 - + Script 脚本 @@ -3001,314 +3052,314 @@ Please verify the merge result carefully. 未命名 - + Address Base 地址基准 - + Server connected: %1 服务器已连接:%1 - + Server disconnected: %1 服务器已断开:%1 - + Modbus client connected: %1 -> %2 Modbus客户端已连接:%1 → %2 - + Modbus client disconnected: %1 -> %2 Modbus客户端已断开:%1 → %2 - + [Unit %1] %2 [单元 %1] %2 - + Address space added: unit %1, %2, starting address %3, length %4 地址空间已添加:单元 %1, %2, 起始地址 %3, 长度 %4 - + <empty> <空> - + Manual write: unit %1, %2, starting address %3, value %4 手动写入:单元 %1, %2, 起始地址 %3, 值 %4 - + Client write: %1 -> unit %2, %3, starting address %4, value %5 客户端写入:%1 → 单元 %2, %3, 起始地址 %4, 值 %5 - + unknown client 未知客户端 - + Address comment changed: unit %1, %2, address %3: '%4' 地址注释已更改:单元 %1, %2, 地址 %3: '%4' - + Auto simulation enabled (%1): unit %2, %3, address %4 自动仿真已启用(%1):单元 %2, %3, 地址 %4 - + Auto simulation disabled: unit %1, %2, address %3 自动仿真已禁用:单元 %1, %2, 地址 %3 - + [%1 values: %2%3] [%1 values: %2%3] - + Modbus RTU over TCP/IP 通过 TCP/IP 的 Modbus RTU - + Traffic 流量 - + Map 映射 - + Form 窗体 - + <null> <null> - + <untitled> <未命名> - + %1 '%2' %1 '%2' - + Font 字体 - + FontZoom 字体缩放 - + BackgroundColor 背景颜色 - + ForegroundColor 前景颜色 - + AddressColor 地址颜色 - + CommentColor 注释颜色 - + CheckForUpdates 检查更新 - + ShowWelcomeDialog 显示欢迎对话框 - + Language 语言 - + ScriptFont 脚本字体 - + CodeAutoComplete 代码自动补全 - + AutoShowConsoleOutput 自动显示控制台输出 - + ConsoleMaxLines 控制台最大行数 - + DataView.ColumnsDistance 数据视图.列距 - + DataView.LeadingZeros 数据视图.前导零 - + TrafficView.LogLimit 流量视图.日志限制 - + TrafficView.AutoScroll 流量视图.自动滚动 - + ThemeMode 主题模式 - + true 是 - + false 否 - + enabled 已启用 - + disabled 已禁用 - + Unit ID added: %1 单元ID已添加:%1 - + Unit ID removed: %1 单元ID已删除:%1 - + Address space removed: unit %1, %2, starting address %3, length %4 地址空间已删除:单元 %1, %2, 起始地址 %3, 长度 %4 - + Project opened: %1 项目已打开:%1 - + <unsaved> <未保存> - + Project closed: %1 项目已关闭:%1 - + Project saved: %1 项目已保存:%1 - + Project save failed: %1 (%2) 项目保存失败:%1 (%2) - + Project load failed: %1 (%2) 项目加载失败:%1(%2) - + Form created: %1 窗体已创建:%1 - + Form opened: %1 窗体已打开:%1 - + Form closed: %1 窗体已关闭:%1 - + Form deleted: %1 窗体已删除:%1 - + Preference changed: %1: %2 -> %3 首选项已更改:%1:%2 → %3 - + %1 changed: %2 -> %3 %1 已更改:%2 → %3 - + Script started: %1 (mode: %2, interval: %3 ms) 脚本已启动:%1(模式:%2,间隔:%3 毫秒) - + Script started: %1 (mode: %2) 脚本已启动:%1(模式:%2) - + Form renamed: '%1' -> '%2' 窗体已重命名:'%1' -> '%2' @@ -3318,19 +3369,19 @@ Please verify the merge result carefully. 移至其他面板 - + 1-based 基地址为-1 - + 0-based 基地址为-0 - + Hex View 十六进制视图 @@ -3345,7 +3396,7 @@ Please verify the merge result carefully. 没有最近项目 - + Data 数据 @@ -3355,7 +3406,7 @@ Please verify the merge result carefully. JavaScript文件 (*.js);;所有文件 (*) - + Script stopped: %1 脚本已停止:%1 @@ -3735,12 +3786,12 @@ Please verify the merge result carefully. ModbusMultiServer - + An incorrect device ID was specified (%1) 指定了错误的设备地址(%1) - + Connection error. %1 连接错误 %1 @@ -3771,12 +3822,12 @@ Please verify the merge result carefully. ModbusRtuTcpServer - + Could not write response to client 无法写入对客户端的响应 - + Invalid connection settings for RTU over TCP/IP communication specified. 指定的 RTU over TCP/IP 通信连接设置无效。 @@ -3784,7 +3835,7 @@ Please verify the merge result carefully. ModbusTcpServer - + Invalid connection settings for TCP communication specified. 指定的 TCP 通讯连接设定无效。 @@ -3792,67 +3843,67 @@ Please verify the merge result carefully. OutputDataWidget - + Set Value of %1 将值写入 %1 - + Add Description 添加描述 - + Edit Description 编辑描述 - + %1: Enter Description %1:输入描述 - + Remove Color 去除颜色 - + Yellow 黄色 - + Cyan 青色 - + Magenta 品红色 - + LightGreen 浅绿色 - + Orange 橙色 - + LightBlue 浅蓝色 - + LightGray 浅灰色 - + Zoom: %1% 缩放:%1% @@ -3891,26 +3942,38 @@ Please verify the merge result carefully. PointTypeComboBox - + + Enron Registers Int32 (Read-Write) + Enron Int32 寄存器(读写) + + + + + Enron Registers Float32 (Read-Write) + Enron Float32 寄存器(读写) + + + + Coils (Read-Write) 线圈(读写) - + Discrete Inputs (Read Only) 离散输入(只读) - + Holding Registers (Read-Write) 保持寄存器(读写) - + Input Registers (Read Only) 输入寄存器(只读) @@ -4030,47 +4093,47 @@ Please verify the merge result carefully. QModbusDevice - + Referenced serial device does not exist. 指定的串行设备不存在。 - + Cannot open serial device due to permissions. 由于权限问题,无法打开串行设备。 - + Cannot open serial device. 无法打开串行设备。 - + Write error. 写入错误。 - + Read error. 读取错误。 - + Resource error. 资源错误。 - + Device operation is not supported error. 设备操作不支持。 - + Timeout error. 超时错误。 - + Unknown error. 未知错误。 @@ -4078,12 +4141,12 @@ Please verify the merge result carefully. QModbusRtuSerialServer - + Requesting serial port is closed 请求的串口已关闭 - + Could not write response to client 无法写入对客户端的响应 @@ -4091,12 +4154,12 @@ Please verify the merge result carefully. QModbusTcpServer - + Requesting socket is closed 请求的套接字已关闭 - + Could not write response to client 无法写入对客户端的响应 @@ -4158,12 +4221,12 @@ Please verify the merge result carefully. RegistryOrderItemDelegate - + Most Significant Register First - big-endian word order 高位寄存器优先 - 大端字节序 - + Least Significant Register First - little-endian word order 低位寄存器优先 - 小端字节序 @@ -4171,14 +4234,14 @@ Please verify the merge result carefully. RunModeComboBox - + Once 一次 - + Periodically @@ -4373,29 +4436,6 @@ Please verify the merge result carefully. 确定要清除通信记录中的所有消息吗? - - TypeItemDelegate - - - Coils - 线圈 - - - - Discrete Inputs - 离散输入 - - - - Input Registers - 输入寄存器 - - - - Holding Registers - 保持寄存器 - - UpdateChecker diff --git a/src/res/translations/omodsim_zh_TW.qm b/src/res/translations/omodsim_zh_TW.qm index c1f1475d..0aba39be 100644 Binary files a/src/res/translations/omodsim_zh_TW.qm and b/src/res/translations/omodsim_zh_TW.qm differ diff --git a/src/res/translations/omodsim_zh_TW.ts b/src/res/translations/omodsim_zh_TW.ts index 2f1028e5..1e1966f7 100644 --- a/src/res/translations/omodsim_zh_TW.ts +++ b/src/res/translations/omodsim_zh_TW.ts @@ -217,14 +217,14 @@ ByteOrder - - + + Direct 直接 - - + + Swapped 交換 @@ -232,12 +232,12 @@ ByteOrderItemDelegate - + Use bytes in register order 依暫存器順序使用位元組 - + Swap bytes inside each 16-bit register 交換每個 16 位元暫存器內的位元組 @@ -367,47 +367,61 @@ 保持暫存器 - + + + + Enron Registers Int32 + Enron Int32 暫存器 + + + + + + Enron Registers Float32 + Enron Float32 暫存器 + + + Unit 單元 - + Type 類型 - + Address 地址 - + Data Type 資料類型 - + Register Order 暫存器順序 - + Byte Order 位元組順序 - + Comment 注釋 - + Value 值 - + Timestamp 時間戳記 @@ -415,62 +429,62 @@ DataTypeItemDelegate - + 1-bit value (Coils / Discrete Inputs) 1位元值(線圈/離散輸入) - + 16-bit register value shown as 16 binary digits 16位元暫存器值,以16位元二進位數字顯示 - + Unsigned 16-bit integer (0 … 65535) 無符號16位元整數(0 … 65535) - + Signed 16-bit integer (−32768 … 32767) 有符號16位元整數(-32768 … 32767) - + 16-bit value displayed as hexadecimal 16位元值,以十六進位顯示 - + 16-bit value displayed as ANSI character 16位元值,以ANSI字元顯示 - + IEEE 754 single-precision float (2 registers) IEEE 754單精度浮點數(2個暫存器) - + IEEE 754 double-precision float (4 registers) IEEE 754雙精度浮點數(4個暫存器) - + Signed 32-bit integer (2 registers) 有符號32位元整數(2個暫存器) - + Unsigned 32-bit integer (2 registers) 無符號32位元整數(2個暫存器) - + Signed 64-bit integer (4 registers) 有符號64位元整數(4個暫存器) - + Unsigned 64-bit integer (4 registers) 無符號64位元整數(4個暫存器) @@ -1070,48 +1084,58 @@ Open the download page? 依請求自動新增暫存器 - + + Select Enron Int32 or Float32 registers and enter the full protocol address, starting at 5001 or 7001. Select the display format separately in the toolbar. + + + + + Enable Enron Modbus (32-bit registers) + + + + Error Simulations 錯誤模擬 - + Response with incorrect IDs 帶有錯誤 ID 的響應 - + Response with ILLEGAL FUNCTION 響應非法功能 - + Response with DEVICE BUSY 響應 DEVICE BUSY - + Response with incorrect CRC (Serial Line only) 響應帶有錯誤的 CRC(僅限序列線路) - + Response with delay 延遲響應 - - + + ms 毫秒 - + Response with random delay from 0 to 響應隨機延遲從 0 到 - + No response 沒有響應 @@ -1723,6 +1747,16 @@ Open the download page? DialogWriteRegister + + + Address (low word): + + + + + Address (high word): + + Write Register @@ -1745,7 +1779,7 @@ Open the download page? - + Auto Simulation: OFF 自動模擬:關閉 @@ -1755,18 +1789,18 @@ Open the download page? 位元模式: - + Value, (HEX): 值(十六進位): - + Value, (ANSI): 值,(ANSI): - - + + Auto Simulation: ON 自動模擬:開啟 @@ -1916,74 +1950,73 @@ Open the download page? - + Clear Table 清除表格 - + Clear all visible rows from the table? 是否清除表格中所有可見行? - + 1-based 基地址為-1 - + 0-based 基地址為-0 - + Remove Color 移除顏色 - + Yellow 黃色 - + Cyan 青色 - + Magenta 品紅色 - + LightGreen 淺綠色 - + Orange 橙色 - + LightBlue 淺藍色 - + LightGray 淺灰色 - - - + + All Types 所有類型 - + Address Base: %1 Unit Filter: %2 Type Filter: %3 @@ -1992,58 +2025,81 @@ Type Filter: %3 類型過濾器:%3 - - + Coils 線圈 - - + Discrete Inputs 離散輸入 - - + Input Registers 輸入暫存器 - - + Holding Registers 保持暫存器 - - + + Enron Registers Int32 + Enron Int32 暫存器 + + + + Enron Registers Float32 + Enron Float32 暫存器 + + + + Unit: 單元: - - + + Data Type: 資料類型: - - - + + + All 全部 FormDataView + + + Register Type: + + + + + Enron Address: + + + + + Register Count: + + + Starting Address: 起始地址: + Length: 長度: @@ -2057,11 +2113,6 @@ Type Filter: %3 Column Distance: 欄距: - - - Data Type: - 資料類型: - Leading Zeros for Unsigned @@ -2192,17 +2243,17 @@ Type Filter: %3 交換位元組(0xAB → 0xBA) - + 0-based 基地址為-0 - + 1-based 基地址為-1 - + Address Base: %1 Starting Address: %2 Length: %3 @@ -2211,7 +2262,7 @@ Length: %3 長度:%3 - + Unit Identifier: %1 Data Type: %2 @@ -2663,7 +2714,7 @@ Open the download page? 設定 - + Script 腳本 @@ -3001,314 +3052,314 @@ Please verify the merge result carefully. 未命名 - + Address Base 地址基準 - + Server connected: %1 伺服器已連接:%1 - + Server disconnected: %1 伺服器已斷開:%1 - + Modbus client connected: %1 -> %2 Modbus用戶端已連接:%1 → %2 - + Modbus client disconnected: %1 -> %2 Modbus用戶端已斷開:%1 → %2 - + [Unit %1] %2 [單元 %1] %2 - + Address space added: unit %1, %2, starting address %3, length %4 地址空間已新增:單元 %1, %2, 起始地址 %3, 長度 %4 - + <empty> <空> - + Manual write: unit %1, %2, starting address %3, value %4 手動寫入:單元 %1, %2, 起始地址 %3, 値 %4 - + Client write: %1 -> unit %2, %3, starting address %4, value %5 用戶端寫入:%1 → 單元 %2, %3, 起始地址 %4, 値 %5 - + unknown client 未知用戶端 - + Address comment changed: unit %1, %2, address %3: '%4' 地址注釋已更改:單元 %1, %2, 地址 %3: '%4' - + Auto simulation enabled (%1): unit %2, %3, address %4 自動模擬已啟用(%1):單元 %2, %3, 地址 %4 - + Auto simulation disabled: unit %1, %2, address %3 自動模擬已停用:單元 %1, %2, 地址 %3 - + [%1 values: %2%3] [%1 values: %2%3] - + Modbus RTU over TCP/IP 透過 TCP/IP 的 Modbus RTU - + Traffic 流量 - + Map 映射 - + Form 表單 - + <null> <null> - + <untitled> <未命名> - + %1 '%2' %1 '%2' - + Font 字型 - + FontZoom 字體縮放 - + BackgroundColor 背景顏色 - + ForegroundColor 前景顏色 - + AddressColor 地址顏色 - + CommentColor 注釋顏色 - + CheckForUpdates 檢查更新 - + ShowWelcomeDialog 顯示歡迎對話框 - + Language 語言 - + ScriptFont 腳本字體 - + CodeAutoComplete 程式碼自動補全 - + AutoShowConsoleOutput 自動顯示主控台輸出 - + ConsoleMaxLines 主控台最大行數 - + DataView.ColumnsDistance 資料檢視.欄距 - + DataView.LeadingZeros 資料檢視.前導零 - + TrafficView.LogLimit 流量檢視.日誌限制 - + TrafficView.AutoScroll 流量檢視.自動捲動 - + ThemeMode 主題模式 - + true 是 - + false 否 - + enabled 已啟用 - + disabled 已停用 - + Unit ID added: %1 單元ID已新增:%1 - + Unit ID removed: %1 單元ID已移除:%1 - + Address space removed: unit %1, %2, starting address %3, length %4 地址空間已移除:單元 %1, %2, 起始地址 %3, 長度 %4 - + Project opened: %1 專案已開啟:%1 - + <unsaved> <未儲存> - + Project closed: %1 專案已關閉:%1 - + Project saved: %1 項目已儲存:%1 - + Project save failed: %1 (%2) 專案保存失敗:%1 (%2) - + Project load failed: %1 (%2) 專案載入失敗:%1(%2) - + Form created: %1 表單已建立:%1 - + Form opened: %1 表單已開啟:%1 - + Form closed: %1 表單已關閉:%1 - + Form deleted: %1 表單已刪除:%1 - + Preference changed: %1: %2 -> %3 偏好設定已更改:%1:%2 → %3 - + %1 changed: %2 -> %3 %1 已更改:%2 → %3 - + Script started: %1 (mode: %2, interval: %3 ms) 腳本已啟動:%1(模式:%2,間隔:%3 毫秒) - + Script started: %1 (mode: %2) 腳本已啟動:%1(模式:%2) - + Form renamed: '%1' -> '%2' 表單已重命名:'%1' -> '%2' @@ -3318,19 +3369,19 @@ Please verify the merge result carefully. 移至其他面板 - + 1-based 基地址為-1 - + 0-based 基地址為-0 - + Hex View 十六進位檢視 @@ -3345,7 +3396,7 @@ Please verify the merge result carefully. 無最近專案 - + Data 資料 @@ -3355,7 +3406,7 @@ Please verify the merge result carefully. JavaScript檔案 (*.js);;所有檔案 (*) - + Script stopped: %1 腳本已停止:%1 @@ -3735,12 +3786,12 @@ Please verify the merge result carefully. ModbusMultiServer - + An incorrect device ID was specified (%1) 指定了錯誤的裝置地址(%1) - + Connection error. %1 連線錯誤 %1 @@ -3771,12 +3822,12 @@ Please verify the merge result carefully. ModbusRtuTcpServer - + Could not write response to client 無法寫入對客戶端的響應 - + Invalid connection settings for RTU over TCP/IP communication specified. 指定的 RTU over TCP/IP 通訊連線設定無效。 @@ -3784,7 +3835,7 @@ Please verify the merge result carefully. ModbusTcpServer - + Invalid connection settings for TCP communication specified. 指定的 TCP 通訊連線設定無效。 @@ -3792,67 +3843,67 @@ Please verify the merge result carefully. OutputDataWidget - + Set Value of %1 設定 %1 的值 - + Add Description 新增描述 - + Edit Description 編輯描述 - + %1: Enter Description %1:輸入描述 - + Remove Color 移除顏色 - + Yellow 黃色 - + Cyan 青色 - + Magenta 品紅色 - + LightGreen 淺綠色 - + Orange 橙色 - + LightBlue 淺藍色 - + LightGray 淺灰色 - + Zoom: %1% 縮放:%1% @@ -3891,26 +3942,38 @@ Please verify the merge result carefully. PointTypeComboBox - + + Enron Registers Int32 (Read-Write) + Enron Int32 暫存器(讀寫) + + + + + Enron Registers Float32 (Read-Write) + Enron Float32 暫存器(讀寫) + + + + Coils (Read-Write) 線圈(讀寫) - + Discrete Inputs (Read Only) 離散輸入(唯讀) - + Holding Registers (Read-Write) 保持暫存器(讀寫) - + Input Registers (Read Only) 輸入暫存器(唯讀) @@ -4030,47 +4093,47 @@ Please verify the merge result carefully. QModbusDevice - + Referenced serial device does not exist. 指定的序列裝置不存在。 - + Cannot open serial device due to permissions. 由於許可權問題,無法開啟序列裝置。 - + Cannot open serial device. 無法開啟序列裝置。 - + Write error. 寫入錯誤。 - + Read error. 讀取錯誤。 - + Resource error. 資源錯誤。 - + Device operation is not supported error. 裝置操作不支援。 - + Timeout error. 超時錯誤。 - + Unknown error. 未知錯誤。 @@ -4078,12 +4141,12 @@ Please verify the merge result carefully. QModbusRtuSerialServer - + Requesting serial port is closed 請求的序列埠已關閉 - + Could not write response to client 無法寫入對客戶端的響應 @@ -4091,12 +4154,12 @@ Please verify the merge result carefully. QModbusTcpServer - + Requesting socket is closed 請求的套接字已關閉 - + Could not write response to client 無法寫入對客戶端的響應 @@ -4158,12 +4221,12 @@ Please verify the merge result carefully. RegistryOrderItemDelegate - + Most Significant Register First - big-endian word order 高位暫存器優先 - 大端字節序 - + Least Significant Register First - little-endian word order 低位暫存器優先 - 小端字節序 @@ -4171,14 +4234,14 @@ Please verify the merge result carefully. RunModeComboBox - + Once 一次 - + Periodically @@ -4373,29 +4436,6 @@ Please verify the merge result carefully. 確定要清除通訊記錄中的所有訊息嗎? - - TypeItemDelegate - - - Coils - 線圈 - - - - Discrete Inputs - 離散輸入 - - - - Input Registers - 輸入暫存器 - - - - Holding Registers - 保持暫存器 - - UpdateChecker diff --git a/src/tests/test_appproject.cpp b/src/tests/test_appproject.cpp index 8448581f..6c8845cc 100644 --- a/src/tests/test_appproject.cpp +++ b/src/tests/test_appproject.cpp @@ -7,18 +7,34 @@ /// #include +#include +#include +#include +#include +#include +#include +#include +#include +#include #include #include #include #include #include "appproject.h" +#include "applogger.h" #include "apppreferences.h" #include "application.h" #include "controls/mdiareaex.h" +#include "controls/pointtypecombobox.h" +#include "controls/numericlineedit.h" +#include "controls/outputdatawidget.h" +#include "dialogs/dialogwriteregister.h" #include "controls/projecttreewidget.h" #include "datasimulator.h" +#include "dialogs/dialogmodbusdefinitions.h" #include "formdataview.h" +#include "formdatamapview.h" #include "mainwindow.h" #include "modbusmultiserver.h" #include "projectformmetadata.h" @@ -28,6 +44,21 @@ class TestAppProject : public QObject Q_OBJECT private slots: + void logsEnronAddressSpaces_data(); + void logsEnronAddressSpaces(); + void mapsEnronRequests_data(); + void mapsEnronRequests(); + void editsAndRestoresEnronDataMap(); + void togglesEnronRegisterChoices(); + void compactsEnronRowsAcrossFormatChanges(); + void enronDisplayFormats_data(); + void enronDisplayFormats(); + void enronRegisterChoices_data(); + void enronRegisterChoices(); + void roundTripsEnronRegisterChoices(); + void editsEnronRegisterByLogicalAddress(); + void configuresEnronMode(); + void roundTripsEnronMode(); void createsAndNumbersEveryFormKind(); void closesReopensAndDeletesCanonicalForm(); void respectsDeletionLock(); @@ -194,7 +225,596 @@ QStringList commentPlacements(const QString& path) } -/// \brief Verifies creation, UUID assignment and per-kind display counters. +/// \brief Keeps ordinary tables distinguishable from Enron maps at the same backing addresses. +void TestAppProject::logsEnronAddressSpaces_data() +{ + QTest::addColumn("group"); + QTest::newRow("holding") << 0; + QTest::newRow("Int32") << 1; + QTest::newRow("Float32") << 2; +} + +/// \brief Logs logical groups, addresses and counts on both registration and removal. +void TestAppProject::logsEnronAddressSpaces() +{ + QFETCH(int, group); + ModbusMultiServer server; + QObject context; + AppLogger::setupModbusMultiServerLogging(server, &context); + auto definitions = server.getModbusDefinitions(); + definitions.EnronModbus = true; + server.setModbusDefinitions(definitions); + const auto enron = static_cast(group); + const int address = group == 2 ? 7003 : 5003; + const int logicalAddress = group ? address - 1 : address; + const int count = group ? 2 : 4; + const QString name = group + ? QCoreApplication::translate("DataMapDataModel", group == 1 ? "Enron Registers Int32" : "Enron Registers Float32") + : registerTypeName(QModbusDataUnit::HoldingRegisters); + const auto id = QUuid::createUuid(); + const QString added = QCoreApplication::translate("MainWindow", + "Address space added: unit %1, %2, starting address %3, length %4") + .arg(1).arg(name).arg(logicalAddress).arg(count); + QTest::ignoreMessage(QtInfoMsg, QRegularExpression(QRegularExpression::escape(added))); + server.addUnitMap(id, 1, QModbusDataUnit::HoldingRegisters, address, 4, enron); + QVERIFY(server.data(1, QModbusDataUnit::HoldingRegisters, address, 4).isValid()); + const QString removed = QCoreApplication::translate("MainWindow", + "Address space removed: unit %1, %2, starting address %3, length %4") + .arg(1).arg(name).arg(logicalAddress).arg(count); + QTest::ignoreMessage(QtInfoMsg, QRegularExpression(QRegularExpression::escape(removed))); + server.removeUnitMap(id, 1); + const QString ordinaryAdded = QCoreApplication::translate("MainWindow", + "Address space added: unit %1, %2, starting address %3, length %4") + .arg(1).arg(registerTypeName(QModbusDataUnit::HoldingRegisters)).arg(address).arg(4); + QTest::ignoreMessage(QtInfoMsg, QRegularExpression(QRegularExpression::escape(ordinaryAdded))); + server.addUnitMap(id, 1, QModbusDataUnit::HoldingRegisters, address, 4); + server.clearAddressSpace(); + QTest::ignoreMessage(QtInfoMsg, QRegularExpression(QRegularExpression::escape(ordinaryAdded))); + server.addUnitMap(id, 1, QModbusDataUnit::HoldingRegisters, address, 4); + server.removeUnitMap(id, 1); +} + +/// \brief Covers request mapping for both Enron ranges and ordinary Modbus. +void TestAppProject::mapsEnronRequests_data() +{ + QTest::addColumn("enabled"); + QTest::addColumn("base"); + QTest::addColumn("group"); + QTest::newRow("ordinary") << false << 7001 << 0; + QTest::newRow("5x") << true << 5001 << 1; + QTest::newRow("7x") << true << 7001 << 2; +} + +/// \brief Maps consecutive protocol addresses and appends the next native register. +void TestAppProject::mapsEnronRequests() +{ + QFETCH(bool, enabled); + QFETCH(int, base); + QFETCH(int, group); + ProjectFixture fixture; + auto definitions = fixture.Server.getModbusDefinitions(); + definitions.EnronModbus = enabled; + fixture.Server.setModbusDefinitions(definitions); + auto* form = qobject_cast(fixture.Project.createMdiChild(ProjectFormKind::DataMap)); + QVERIFY(form); + form->setAutoAddOnRequest(true); + auto* model = form->findChild(); + auto* table = form->findChild("tableView"); + QVERIFY(model); + QVERIFY(table); + fixture.Server.addUnitMap(QUuid::createUuid(), 1, QModbusDataUnit::HoldingRegisters, base, 6); + const QModbusDataUnit unit(QModbusDataUnit::HoldingRegisters, base, + QVector{0x1234, 0x5678, 0x2345, 0x6789, 0, 0}); + fixture.Server.setData(1, unit, WriteSource::User); + const auto request = ModbusMessage::create(QModbusRequest(QModbusPdu::ReadHoldingRegisters, + quint16(base), quint16(2)), ModbusMessage::Tcp, 1, 1, QDateTime::currentDateTime(), true); + fixture.Server.requestOnConnection({}, request); + QCOMPARE(model->rowCount(), 2); + QCOMPARE(model->keyForRow(0).Address, base); + QCOMPARE(model->keyForRow(1).Address, base + (enabled ? 2 : 1)); + QCOMPARE(model->index(1, ColType).data(DataMapRole::EnronType).toInt(), group); + QCOMPARE(model->index(1, ColAddress).data().toInt(), base + 1 + (enabled || form->zeroBasedAddress() ? 0 : 1)); + table->setCurrentIndex(table->model()->index(1, ColType)); + QVERIFY(QMetaObject::invokeMethod(form, "on_actionAdd_triggered", Qt::DirectConnection)); + QCOMPARE(model->rowCount(), 3); + QCOMPARE(model->keyForRow(2).Address, base + (enabled ? 4 : 2)); + QCOMPARE(model->index(2, ColType).data(DataMapRole::EnronType).toInt(), group); + fixture.Server.requestOnConnection({}, request); + QCOMPARE(model->rowCount(), 3); +} + +/// \brief Preserves Enron groups and protocol addresses through table editing and project reload. +void TestAppProject::editsAndRestoresEnronDataMap() +{ + ProjectFixture fixture; + auto* form = qobject_cast(fixture.Project.createMdiChild(ProjectFormKind::DataMap)); + QVERIFY(form); + auto* model = form->findChild(); + auto* table = form->findChild("tableView"); + auto* filter = form->findChild("filterTypeCombo"); + QVERIFY(model); + QVERIFY(table); + QVERIFY(filter); + model->addEntry({1, QModbusDataUnit::HoldingRegisters, 0}, {}); + const auto typeIndex = table->model()->index(0, ColType); + auto* delegate = table->itemDelegateForColumn(ColType); + QScopedPointer editor(delegate->createEditor(table, {}, typeIndex)); + auto* types = qobject_cast(editor.data()); + QVERIFY(types); + QCOMPARE(types->count(), 4); + QCOMPARE(filter->count(), 5); + + auto definitions = fixture.Server.getModbusDefinitions(); + definitions.EnronModbus = true; + fixture.Server.setModbusDefinitions(definitions); + QTRY_COMPARE(types->count(), 6); + QCOMPARE(filter->count(), 7); + QCOMPARE(filter->itemText(5), QCoreApplication::translate("FormDataMapView", "Enron Registers Int32")); + QCOMPARE(filter->itemText(6), QCoreApplication::translate("FormDataMapView", "Enron Registers Float32")); + types->setCurrentEnronType(EnronModbus::RegisterType::Group7x); + delegate->setModelData(types, table->model(), typeIndex); + QCOMPARE(model->index(0, ColType).data(DataMapRole::EnronType).toInt(), 2); + QCOMPARE(model->index(0, ColAddress).data().toInt(), 7001); + + const auto addressIndex = table->model()->index(0, ColAddress); + auto* addressDelegate = table->itemDelegateForColumn(ColAddress); + QScopedPointer addressEditor(addressDelegate->createEditor(table, {}, addressIndex)); + auto* address = qobject_cast(addressEditor.data()); + QVERIFY(address); + addressDelegate->setEditorData(address, addressIndex); + QCOMPARE(address->range().from(), 7001); + QCOMPARE(address->range().to(), 7999); + QCOMPARE(address->value(), 7001); + address->setValue(7002); + addressDelegate->setModelData(address, table->model(), addressIndex); + QCOMPARE(model->keyForRow(0).Address, 7003); + QCOMPARE(model->index(0, ColAddress).data().toInt(), 7002); + QVERIFY(model->setData(model->index(0, ColDataType), "UInt32")); + QVERIFY(model->setData(model->index(0, ColValue), "305419896")); + + QTemporaryDir files; + const auto saved = files.filePath("enron-map.omsim"); + QVERIFY(fixture.Project.saveProject(saved)); + filter->setCurrentIndex(6); + QCOMPARE(table->model()->rowCount(), 1); + definitions.EnronModbus = false; + fixture.Server.setModbusDefinitions(definitions); + QTRY_COMPARE(types->count(), 4); + QCOMPARE(filter->count(), 5); + QCOMPARE(filter->currentIndex(), 0); + QCOMPARE(table->model()->rowCount(), 1); + definitions.EnronModbus = true; + fixture.Server.setModbusDefinitions(definitions); + QTRY_COMPARE(types->count(), 6); + QCOMPARE(model->index(0, ColAddress).data().toInt(), 7002); + editor.reset(); + addressEditor.reset(); + + fixture.Project.closeProject(); + QVERIFY(fixture.Project.loadProject(saved).Success); + auto* restored = fixture.Mdi.findChild(); + QVERIFY(restored); + auto* restoredModel = restored->findChild(); + QVERIFY(restoredModel); + QCOMPARE(restoredModel->rowCount(), 1); + QCOMPARE(restoredModel->index(0, ColType).data(DataMapRole::EnronType).toInt(), 2); + QCOMPARE(restoredModel->index(0, ColAddress).data().toInt(), 7002); + QCOMPARE(restoredModel->keyForRow(0).Address, 7003); + QCOMPARE(restoredModel->index(0, ColDataType).data().toString(), QString("UInt32")); + const auto words = fixture.Server.data(1, QModbusDataUnit::HoldingRegisters, 7003, 2); + QCOMPARE(words.value(0), quint16(0x1234)); + QCOMPARE(words.value(1), quint16(0x5678)); +} + +/// \brief Changes the available choices without duplicates or losing ordinary selections. +void TestAppProject::togglesEnronRegisterChoices() +{ + PointTypeComboBox combo; + QCOMPARE(combo.count(), 4); + combo.setCurrentPointType(QModbusDataUnit::InputRegisters); + combo.setCurrentEnronType(EnronModbus::RegisterType::Group7x); + QCOMPARE(combo.currentPointType(), QModbusDataUnit::InputRegisters); + QCOMPARE(combo.currentEnronType(), EnronModbus::RegisterType::None); + combo.setEnronEnabled(false); + QCOMPARE(combo.count(), 4); + + for (auto mode : {EnronModbus::RegisterType::Group5x, EnronModbus::RegisterType::Group7x}) { + combo.setCurrentPointType(QModbusDataUnit::InputRegisters); + combo.setEnronEnabled(true); + combo.setEnronEnabled(true); + QCOMPARE(combo.count(), 6); + QCOMPARE(combo.currentPointType(), QModbusDataUnit::InputRegisters); + combo.setCurrentEnronType(mode); + QCOMPARE(combo.currentEnronType(), mode); + QCOMPARE(combo.currentText(), mode == EnronModbus::RegisterType::Group5x + ? QCoreApplication::translate("PointTypeComboBox", "Enron Registers Int32 (Read-Write)") + : QCoreApplication::translate("PointTypeComboBox", "Enron Registers Float32 (Read-Write)")); + combo.setEnronEnabled(false); + combo.setEnronEnabled(false); + QCOMPARE(combo.count(), 4); + QCOMPARE(combo.currentPointType(), QModbusDataUnit::HoldingRegisters); + QCOMPARE(combo.currentEnronType(), EnronModbus::RegisterType::None); + QCOMPARE(combo.findData(int(mode), Qt::UserRole + 1), -1); + } +} + +/// \brief Rebuilds visible rows across display widths without changing the stored range. +void TestAppProject::compactsEnronRowsAcrossFormatChanges() +{ + ProjectFixture fixture; + auto definitions = fixture.Server.getModbusDefinitions(); + definitions.EnronModbus = true; + fixture.Server.setModbusDefinitions(definitions); + auto* form = qobject_cast(fixture.Project.createMdiChild(ProjectFormKind::Data)); + QVERIFY(form); + form->findChild("comboBoxModbusPointType")->setCurrentEnronType(EnronModbus::RegisterType::Group5x); + form->findChild("lineEditLength")->setValue(3); + auto* output = form->findChild("outputWidget"); + auto* view = output->findChild("listView"); + QCOMPARE(form->displayDefinition().Length, quint16(6)); + QCOMPARE(view->model()->rowCount(), 3); + form->setDataType(DataType::UInt64); + QCOMPARE(view->model()->rowCount(), 2); + QVERIFY(view->model()->data(view->model()->index(1, 0), CaptureRole).toString().trimmed().isEmpty()); + form->setDataType(DataType::UInt16); + QCOMPARE(view->model()->rowCount(), 6); + form->setDataType(DataType::UInt32); + QCOMPARE(view->model()->rowCount(), 3); + QCOMPARE(form->displayDefinition().Length, quint16(6)); + QCOMPARE(form->data().size(), 6); + definitions.EnronModbus = false; + fixture.Server.setModbusDefinitions(definitions); + QTRY_COMPARE(form->displayDefinition().EnronType, EnronModbus::RegisterType::None); + QCOMPARE(view->model()->rowCount(), 6); +} + +/// \brief Covers every display action independently of the selected Enron group. +void TestAppProject::enronDisplayFormats_data() +{ + QTest::addColumn("group"); + QTest::addColumn("type"); + QTest::addColumn("action"); + const QList> formats = { + {DataType::Binary, "actionDisplayBinary"}, {DataType::Hex, "actionDisplayHex"}, + {DataType::Ansi, "actionDisplayAnsi"}, {DataType::Int16, "actionDisplayInt16"}, + {DataType::UInt16, "actionDisplayUInt16"}, {DataType::Int32, "actionDisplayInt32"}, + {DataType::UInt32, "actionDisplayUInt32"}, {DataType::Int64, "actionDisplayInt64"}, + {DataType::UInt64, "actionDisplayUInt64"}, {DataType::Float32, "actionDisplayFloat32"}, + {DataType::Float64, "actionDisplayFloat64"} + }; + for (int group : {1, 2}) { + for (const auto& format : formats) { + const auto name = QString("%1-%2").arg(group).arg(format.second); + QTest::newRow(qPrintable(name)) << group << format.first << format.second; + } + } +} + +/// \brief Reinterprets shared register bits without changing their group, address or storage. +void TestAppProject::enronDisplayFormats() +{ + QFETCH(int, group); + QFETCH(DataType, type); + QFETCH(QString, action); + auto& prefs = AppPreferences::instance(); + const bool previousHex = prefs.globalHexView(); + const auto restoreHex = qScopeGuard([&prefs, previousHex] { prefs.setGlobalHexView(previousHex); }); + prefs.setGlobalHexView(false); + ProjectFixture fixture; + auto definitions = fixture.Server.getModbusDefinitions(); + definitions.EnronModbus = true; + fixture.Server.setModbusDefinitions(definitions); + auto* form = qobject_cast(fixture.Project.createMdiChild(ProjectFormKind::Data)); + QVERIFY(form); + form->setAddressBase(AddressBase::Base0); + auto* combo = form->findChild("comboBoxModbusPointType"); + const auto enron = static_cast(group); + combo->setCurrentEnronType(enron); + form->findChild("lineEditLength")->setValue(2); + auto* displayAction = form->findChild(action); + QVERIFY(displayAction); + QVERIFY(displayAction->isEnabled()); + displayAction->trigger(); + QCOMPARE(form->dataType(), type); + QCOMPARE(form->displayDefinition().EnronType, enron); + QCOMPARE(form->findChild("lineEditAddress")->value(), int(EnronModbus::baseAddress(enron))); + const QVector words{0x3f80, 0, 0x4000, 0}; + fixture.Server.setData(1, QModbusDataUnit(QModbusDataUnit::HoldingRegisters, + EnronModbus::baseAddress(enron), words), WriteSource::User); + auto* model = form->findChild(); + QVERIFY(model); + QTRY_COMPARE(form->data(), words); + auto* output = form->findChild("outputWidget"); + auto* view = output->findChild("listView"); + QVERIFY(view); + const int visibleRows = 4 / registersCount(type); + QCOMPARE(view->model()->rowCount(), visibleRows); + QCOMPARE(output->rowCount(), visibleRows); + QSet visibleAddresses; + for (int row = 0; row < visibleRows; ++row) + visibleAddresses.insert(view->model()->data(view->model()->index(row, 0), AddressStringRole).toString()); + QCOMPARE(visibleAddresses.size(), visibleRows); + QImage page(1000, 1000, QImage::Format_ARGB32); + QPainter painter(&page); + QCOMPARE(output->paint(page.rect(), painter, 0), visibleRows); + const auto index = model->index(0); + QVERIFY(!model->data(index, CaptureRole).toString().trimmed().isEmpty()); + const QString protocolAddress = QString::number(EnronModbus::baseAddress(enron)); + const bool wordFormat = registersCount(type) == 1; + QCOMPARE(model->data(index, AddressStringRole).toString(), protocolAddress + + (wordFormat ? QStringLiteral(".H") : QString())); + if (wordFormat) { + QCOMPARE(model->data(model->index(1), AddressStringRole).toString(), protocolAddress + QStringLiteral(".L")); + QCOMPARE(model->data(index, ValueRole).toUInt(), uint(0x3f80)); + } else if (type == DataType::Float32) { + QCOMPARE(model->data(index, ValueRole).toFloat(), 1.0f); + QCOMPARE(model->data(model->index(2), ValueRole).toFloat(), 2.0f); + } else if (type == DataType::Float64) { + QCOMPARE(model->data(index, ValueRole).toDouble(), 0x1.000004p-7); + } else if (registersCount(type) == 2) { + QCOMPARE(model->data(index, ValueRole).toUInt(), uint(0x3f800000)); + } else { + QCOMPARE(model->data(index, ValueRole).toULongLong(), qulonglong(0x3f80000040000000)); + } + if (type == DataType::Float32) { + output->applyFindByValue(QStringLiteral("2")); + QVERIFY(output->findNextByValue(QStringLiteral("2"))); + QCOMPARE(view->currentIndex().row(), 1); + output->clearFindByValue(); + } + QObject::disconnect(output, &OutputDataWidget::itemDoubleClicked, nullptr, nullptr); + QSignalSpy editSpy(output, &OutputDataWidget::itemDoubleClicked); + QVERIFY(QMetaObject::invokeMethod(output, "on_listView_doubleClicked", Qt::DirectConnection, + Q_ARG(QModelIndex, view->model()->index(visibleRows - 1, 0)))); + QCOMPARE(editSpy.count(), 1); + QCOMPARE(editSpy.first().at(0).toUInt(), uint(EnronModbus::baseAddress(enron) + + (visibleRows - 1) * registersCount(type))); + if (type == DataType::Float32) + QCOMPARE(editSpy.first().at(1).toFloat(), 2.0f); + form->setRegisterOrder(RegisterOrder::LSRF); + QCOMPARE(form->registerOrder(), RegisterOrder::LSRF); + form->setByteOrder(ByteOrder::Swapped); + QCOMPARE(form->byteOrder(), ByteOrder::Swapped); + prefs.setGlobalHexView(true); + QTRY_COMPARE(model->data(index, AddressStringRole).toString(), QStringLiteral("0x%1").arg(EnronModbus::baseAddress(enron), 4, 16, QLatin1Char('0')).toUpper() + + (wordFormat ? QStringLiteral(".H") : QString())); + QCOMPARE(form->findChild("lineEditAddress")->value(), int(EnronModbus::baseAddress(enron))); +} + +/// \brief Exercises logical register input for both Enron groups and address bases. +void TestAppProject::enronRegisterChoices_data() +{ + QTest::addColumn("mode"); + QTest::addColumn("zeroBased"); + for (int mode : {1, 2}) { + QTest::newRow(mode == 1 ? "int-base0" : "float-base0") << mode << true; + QTest::newRow(mode == 1 ? "int-base1" : "float-base1") << mode << false; + } +} + +/// \brief Keeps UI counts and labels logical while editing the correct backing words. +void TestAppProject::enronRegisterChoices() +{ + QFETCH(int, mode); + QFETCH(bool, zeroBased); + auto& prefs = AppPreferences::instance(); + const bool previousHex = prefs.globalHexView(); + const auto restoreHex = qScopeGuard([&prefs, previousHex] { prefs.setGlobalHexView(previousHex); }); + prefs.setGlobalHexView(false); + ProjectFixture fixture; + auto* form = qobject_cast(fixture.Project.createMdiChild(ProjectFormKind::Data)); + QVERIFY(form); + form->setAddressBase(zeroBased ? AddressBase::Base0 : AddressBase::Base1); + auto* combo = form->findChild("comboBoxModbusPointType"); + auto* address = form->findChild("lineEditAddress"); + auto* length = form->findChild("lineEditLength"); + QVERIFY(combo); + const auto enron = static_cast(mode); + QCOMPARE(combo->count(), 4); + QCOMPARE(combo->findData(mode, Qt::UserRole + 1), -1); + auto definitions = fixture.Server.getModbusDefinitions(); + definitions.EnronModbus = true; + fixture.Server.setModbusDefinitions(definitions); + QTRY_COMPARE(combo->count(), 6); + QVERIFY(combo->findData(mode, Qt::UserRole + 1) >= 0); + QSignalSpy maps(&fixture.Server, &ModbusMultiServer::unitMapAdded); + combo->setCurrentEnronType(enron); + QTRY_VERIFY(!maps.isEmpty()); + QCOMPARE(maps.last().last().value(), enron); + const int base = EnronModbus::baseAddress(enron); + QCOMPARE(address->value(), base); + QCOMPARE(address->range().from(), base); + address->setValue(base + 1); + length->setValue(2); + const auto dd = form->displayDefinition(); + QCOMPARE(dd.EnronType, enron); + QCOMPARE(dd.PointAddress, quint16(base + 2 + (zeroBased ? 0 : 1))); + QCOMPARE(dd.Length, quint16(4)); + QCOMPARE(form->dataType(), mode == 1 ? DataType::Int32 : DataType::Float32); + form->setDataType(DataType::UInt16); + QCOMPARE(form->dataType(), DataType::UInt16); + form->setByteOrder(ByteOrder::Swapped); + QCOMPARE(form->byteOrder(), ByteOrder::Swapped); + form->setRegisterOrder(RegisterOrder::LSRF); + QCOMPARE(form->registerOrder(), RegisterOrder::LSRF); + form->setDataType(DataType::UInt32); + auto* model = form->findChild(); + QVERIFY(model); + QCOMPARE(model->data(model->index(0), AddressStringRole).toString(), QString::number(base + 1)); + QCOMPARE(model->data(model->index(2), AddressStringRole).toString(), QString::number(base + 2)); + QSignalSpy definitionSpy(form, &FormDataView::definitionChanged); + definitions.AddrSpace = AddressSpace::Addr5Digits; + fixture.Server.setModbusDefinitions(definitions); + QTRY_VERIFY(!definitionSpy.isEmpty()); + QCOMPARE(model->data(model->index(0), AddressStringRole).toString(), QString::number(base + 1)); + QCOMPARE(form->dataType(), DataType::UInt32); + QCOMPARE(form->byteOrder(), ByteOrder::Swapped); + QCOMPARE(form->registerOrder(), RegisterOrder::LSRF); + QCOMPARE(model->data(model->index(2), AddressRole).toUInt(), uint(base + 4 + (zeroBased ? 0 : 1))); + + form->setAddressBase(zeroBased ? AddressBase::Base1 : AddressBase::Base0); + QCOMPARE(address->value(), base + 1); + form->setAddressBase(zeroBased ? AddressBase::Base0 : AddressBase::Base1); + address->setValue(base + EnronModbus::RangeLength - 1); + QCOMPARE(length->range().to(), 1); + QCOMPARE(length->value(), 1); + QCOMPARE(form->displayDefinition().Length, quint16(2)); + + definitions.EnronModbus = false; + fixture.Server.setModbusDefinitions(definitions); + QTRY_COMPARE(form->displayDefinition().EnronType, EnronModbus::RegisterType::None); + QCOMPARE(combo->count(), 4); + QCOMPARE(combo->findData(mode, Qt::UserRole + 1), -1); + QCOMPARE(form->displayDefinition().Length, quint16(2)); + QCOMPARE(combo->currentPointType(), QModbusDataUnit::HoldingRegisters); +} + +/// \brief Preserves the Enron selection and logical range in project and settings serialization. +void TestAppProject::roundTripsEnronRegisterChoices() +{ + ProjectFixture fixture; + auto definitions = fixture.Server.getModbusDefinitions(); + definitions.EnronModbus = true; + fixture.Server.setModbusDefinitions(definitions); + auto* form = qobject_cast(fixture.Project.createMdiChild(ProjectFormKind::Data)); + QVERIFY(form); + form->setAddressBase(AddressBase::Base0); + form->findChild("comboBoxModbusPointType")->setCurrentEnronType(EnronModbus::RegisterType::Group7x); + form->findChild("lineEditAddress")->setValue(7002); + form->findChild("lineEditLength")->setValue(2); + form->setDataType(DataType::UInt32); + form->setRegisterOrder(RegisterOrder::LSRF); + form->setByteOrder(ByteOrder::Swapped); + const auto expected = form->displayDefinition(); + QTemporaryDir files; + QSettings settings(files.filePath("view.ini"), QSettings::IniFormat); + settings << expected; + DataViewDefinitions restored; + settings >> restored; + QCOMPARE(restored, expected); + QSettings viewSettings(files.filePath("form.ini"), QSettings::IniFormat); + form->saveSettings(viewSettings); + auto* settingsView = qobject_cast(fixture.Project.createMdiChild(ProjectFormKind::Data)); + QVERIFY(settingsView); + settingsView->loadSettings(viewSettings); + QCOMPARE(settingsView->dataType(), DataType::UInt32); + QCOMPARE(settingsView->registerOrder(), RegisterOrder::LSRF); + QCOMPARE(settingsView->byteOrder(), ByteOrder::Swapped); + fixture.Project.deleteForm(settingsView); + const auto saved = files.filePath("enron-view.omsim"); + QVERIFY(fixture.Project.saveProject(saved)); + fixture.Project.closeProject(); + definitions.EnronModbus = false; + fixture.Server.setModbusDefinitions(definitions); + QVERIFY(fixture.Project.loadProject(saved).Success); + const auto forms = fixture.Project.forms(ProjectFormKind::Data); + QCOMPARE(forms.size(), 1); + auto* loaded = qobject_cast(forms.first()); + QVERIFY(loaded); + QCOMPARE(loaded->displayDefinition().EnronType, EnronModbus::RegisterType::Group7x); + QCOMPARE(loaded->displayDefinition().Length, quint16(4)); + QCOMPARE(loaded->findChild("lineEditAddress")->value(), 7002); + QCOMPARE(loaded->findChild("lineEditLength")->value(), 2); + QCOMPARE(loaded->dataType(), DataType::UInt32); + QCOMPARE(loaded->registerOrder(), RegisterOrder::LSRF); + QCOMPARE(loaded->byteOrder(), ByteOrder::Swapped); + auto* combo = loaded->findChild("comboBoxModbusPointType"); + QCOMPARE(combo->count(), 4); + // Project settings are applied after the data views have been constructed. + definitions.EnronModbus = true; + fixture.Server.setModbusDefinitions(definitions); + QTRY_COMPARE(combo->currentEnronType(), EnronModbus::RegisterType::Group7x); + QCOMPARE(combo->count(), 6); + QCOMPARE(loaded->findChild("lineEditAddress")->value(), 7002); +} + +/// \brief Converts a logical address edited in the write dialog back to an independent word pair. +void TestAppProject::editsEnronRegisterByLogicalAddress() +{ + ModbusMultiServer server; + server.addUnitMap(QUuid::createUuid(), 1, QModbusDataUnit::HoldingRegisters, 7001, 6); + ModbusWriteParams params; + params.DeviceId = 1; + params.Address = 7004; + params.ZeroBasedAddress = false; + params.DataMode = DataType::Float32; + params.Value = 1.0f; + params.Order = ByteOrder::Direct; + params.EnronType = EnronModbus::RegisterType::Group7x; + params.Server = &server; + DialogWriteRegister dialog(params, QModbusDataUnit::HoldingRegisters, false, nullptr, nullptr); + auto* address = dialog.findChild("lineEditAddress"); + QCOMPARE(address->value(), 7002); + address->setValue(7003); + dialog.findChild("lineEditValue")->setValue(2.0f); + dialog.accept(); + QCOMPARE(params.Address, quint16(7006)); + server.writeRegister(QModbusDataUnit::HoldingRegisters, params); + QCOMPARE(server.data(1, QModbusDataUnit::HoldingRegisters, 7001, 6).values(), + QVector({0, 0, 0, 0, 0x4000, 0})); + params.DataMode = DataType::UInt16; + params.Address = 7003; + params.Value = 0; + DialogWriteRegister lowWord(params, QModbusDataUnit::HoldingRegisters, false, nullptr, nullptr); + auto* lowAddress = lowWord.findChild("lineEditAddress"); + QCOMPARE(lowAddress->value(), 7001); + lowAddress->setValue(7002); + lowWord.findChild("lineEditValue")->setValue(0xabcd); + lowWord.accept(); + QCOMPARE(params.Address, quint16(7005)); + server.writeRegister(QModbusDataUnit::HoldingRegisters, params); + QCOMPARE(server.data(1, QModbusDataUnit::HoldingRegisters, 7001, 6).values(), + QVector({0, 0, 0, 0xabcd, 0x4000, 0})); +} + +/// \brief Applies Enron mode through server settings and preserves it when edits are cancelled. +void TestAppProject::configuresEnronMode() +{ + ModbusMultiServer server; + DialogModbusDefinitions dialog(server); + auto* checkbox = dialog.findChild("checkBoxEnronModbus"); + auto* buttons = dialog.findChild("buttonBox"); + QVERIFY(checkbox); + QVERIFY(buttons); + QVERIFY(!checkbox->isChecked()); + checkbox->setChecked(true); + QVERIFY(buttons->button(QDialogButtonBox::Apply)->isEnabled()); + buttons->button(QDialogButtonBox::Apply)->click(); + QVERIFY(server.getModbusDefinitions().EnronModbus); + QVERIFY(!buttons->button(QDialogButtonBox::Apply)->isEnabled()); + + DialogModbusDefinitions cancelled(server); + auto* restored = cancelled.findChild("checkBoxEnronModbus"); + QVERIFY(restored->isChecked()); + restored->setChecked(false); + cancelled.reject(); + QVERIFY(server.getModbusDefinitions().EnronModbus); + + checkbox->setChecked(false); + dialog.accept(); + QVERIFY(!server.getModbusDefinitions().EnronModbus); +} + +/// \brief Restores the Enron server setting when reopening a project. +void TestAppProject::roundTripsEnronMode() +{ + ProjectFixture fixture; + QTemporaryDir files; + const auto source = files.filePath("source.omsim"); + const auto saved = files.filePath("enron.omsim"); + writeProjectFile(source, kAnnotatedProject); + QVERIFY(fixture.Project.loadProject(source).Success); + auto definitions = fixture.Server.getModbusDefinitions(); + definitions.EnronModbus = true; + fixture.Server.setModbusDefinitions(definitions); + QVERIFY(fixture.Project.saveProject(saved)); + fixture.Project.closeProject(); + definitions.EnronModbus = false; + fixture.Host.mbMultiServer().setModbusDefinitions(definitions); + QVERIFY(fixture.Project.loadProject(saved).Success); + QVERIFY(fixture.Host.mbMultiServer().getModbusDefinitions().EnronModbus); +} + void TestAppProject::createsAndNumbersEveryFormKind() { ProjectFixture fixture; diff --git a/src/tests/test_datamapdatamodel.cpp b/src/tests/test_datamapdatamodel.cpp index 9cae7f6a..a17cc6c5 100644 --- a/src/tests/test_datamapdatamodel.cpp +++ b/src/tests/test_datamapdatamodel.cpp @@ -47,6 +47,9 @@ class TestDataMapDataModel : public QObject Q_OBJECT private slots: + void editsEnronRegisters_data(); + void editsEnronRegisters(); + void filtersEnronGroups(); void removingRowReleasesDeviceId(); void removingRowAfterDataTypeChangeReleasesDeviceId(); void removingDuplicateKeyRowsReleasesDeviceId(); @@ -56,6 +59,110 @@ private slots: void removingRowKeepsDataCoveredByDataViewRange(); }; +/// \brief Covers both Enron groups independently of the ordinary address base. +void TestDataMapDataModel::editsEnronRegisters_data() +{ + QTest::addColumn("group"); + QTest::addColumn("zeroBased"); + QTest::newRow("5x-base0") << 1 << true; + QTest::newRow("5x-base1") << 1 << false; + QTest::newRow("7x-base0") << 2 << true; + QTest::newRow("7x-base1") << 2 << false; +} + +/// \brief Keeps protocol addresses, native widths and values consistent across edits and protocol toggles. +void TestDataMapDataModel::editsEnronRegisters() +{ + QFETCH(int, group); + QFETCH(bool, zeroBased); + ModbusMultiServer server; + auto definitions = server.getModbusDefinitions(); + definitions.EnronModbus = true; + server.setModbusDefinitions(definitions); + DataMapDataModel model(server); + model.setZeroBased(zeroBased); + model.addEntry(holdingKey(0), {}); + const auto enron = static_cast(group); + const int base = EnronModbus::baseAddress(enron); + QSignalSpy mapAdded(&server, &ModbusMultiServer::unitMapAdded); + const auto typeIndex = model.index(0, ColType); + QVERIFY(model.setData(typeIndex, group, DataMapRole::EnronType)); + QCOMPARE(model.data(typeIndex, DataMapRole::EnronType).toInt(), group); + QTRY_COMPARE(mapAdded.last().last().value(), enron); + QCOMPARE(model.data(typeIndex).toString(), group == 1 ? QString("Enron Registers Int32") : QString("Enron Registers Float32")); + QCOMPARE(model.keyForRow(0).Type, QModbusDataUnit::HoldingRegisters); + QCOMPARE(model.keyForRow(0).Address, quint16(base)); + QCOMPARE(model.data(model.index(0, ColAddress)).toInt(), base); + QVERIFY(server.data(1, QModbusDataUnit::HoldingRegisters, base, 2).isValid()); + QVERIFY(model.setData(model.index(0, ColAddress), QString::number(base + 1))); + QCOMPARE(model.keyForRow(0).Address, quint16(base + 2)); + QCOMPARE(model.data(model.index(0, ColAddress)).toInt(), base + 1); + QVERIFY(model.setData(model.index(0, ColDataType), "UInt32")); + QVERIFY(model.setData(model.index(0, ColValue), uint(0x12345678))); + QCOMPARE(server.data(1, QModbusDataUnit::HoldingRegisters, base + 2, 2).values(), + QVector({0x1234, 0x5678})); + QVERIFY(model.setData(model.index(0, ColDataType), "UInt16")); + QVERIFY(server.data(1, QModbusDataUnit::HoldingRegisters, base + 2, 2).isValid()); + QVERIFY(!model.setData(model.index(0, ColAddress), QString::number(base - 1))); + QVERIFY(!model.setData(model.index(0, ColAddress), QString::number(base + 999))); + QVERIFY(!model.setData(model.index(0, ColAddress), "invalid")); + QVERIFY(!model.setData(typeIndex, 99, DataMapRole::EnronType)); + QCOMPARE(model.keyForRow(0).Address, quint16(base + 2)); + QCOMPARE(server.data(1, QModbusDataUnit::HoldingRegisters, base + 2, 2).values(), + QVector({0x1234, 0x5678})); + definitions.EnronModbus = false; + server.setModbusDefinitions(definitions); + QTRY_VERIFY(!model.data(typeIndex, DataMapRole::EnronEnabled).toBool()); + QCOMPARE(model.data(typeIndex, DataMapRole::EnronType).toInt(), 0); + QTRY_COMPARE(mapAdded.last().last().value(), EnronModbus::RegisterType::None); + QVERIFY(!model.setData(typeIndex, group, DataMapRole::EnronType)); + definitions.EnronModbus = true; + server.setModbusDefinitions(definitions); + QTRY_COMPARE(model.data(typeIndex, DataMapRole::EnronType).toInt(), group); + QTRY_COMPARE(mapAdded.last().last().value(), enron); + QCOMPARE(model.data(model.index(0, ColAddress)).toInt(), base + 1); + QCOMPARE(model.data(model.index(0, ColDataType)).toString(), QStringLiteral("UInt16")); + QCOMPARE(server.data(1, QModbusDataUnit::HoldingRegisters, base + 2, 2).values(), + QVector({0x1234, 0x5678})); + QVERIFY(model.setData(typeIndex, int(QModbusDataUnit::HoldingRegisters), DataMapRole::TypeValue)); + QCOMPARE(model.data(typeIndex, DataMapRole::EnronType).toInt(), 0); + QCOMPARE(model.data(model.index(0, ColAddress)).toInt(), base + 2 + (zeroBased ? 0 : 1)); +} + +/// \brief Filters Enron groups separately while retaining ordinary table filters. +void TestDataMapDataModel::filtersEnronGroups() +{ + ModbusMultiServer server; + auto definitions = server.getModbusDefinitions(); + definitions.EnronModbus = true; + server.setModbusDefinitions(definitions); + DataMapDataModel model(server); + for (auto group : {EnronModbus::RegisterType::Group5x, EnronModbus::RegisterType::Group7x}) { + DataMapEntry entry; + entry.EnronType = group; + model.addEntry(holdingKey(EnronModbus::baseAddress(group)), entry); + } + model.addEntry(holdingKey(10), {}); + DataMapFilterProxy proxy; + proxy.setSourceModel(&model); + QCOMPARE(proxy.rowCount(), 3); + proxy.setFilterTypeIndex(5); + QCOMPARE(proxy.rowCount(), 1); + QCOMPARE(proxy.index(0, ColType).data(DataMapRole::EnronType).toInt(), 1); + proxy.setFilterTypeIndex(6); + QCOMPARE(proxy.rowCount(), 1); + QCOMPARE(proxy.index(0, ColType).data(DataMapRole::EnronType).toInt(), 2); + proxy.setFilterTypeIndex(4); + QCOMPARE(proxy.rowCount(), 1); + definitions.EnronModbus = false; + server.setModbusDefinitions(definitions); + QTRY_COMPARE(proxy.rowCount(), 3); + proxy.setFilterTypeIndex(5); + QCOMPARE(proxy.rowCount(), 0); + proxy.setFilterTypeIndex(-1); + QCOMPARE(proxy.rowCount(), 3); +} + void TestDataMapDataModel::removingRowReleasesDeviceId() { ModbusMultiServer server; diff --git a/src/tests/test_modbusmessages.cpp b/src/tests/test_modbusmessages.cpp index 758174a1..0f8cf4e5 100644 --- a/src/tests/test_modbusmessages.cpp +++ b/src/tests/test_modbusmessages.cpp @@ -248,6 +248,17 @@ void TestModbusMessages::writeSingleRegister() WriteSingleRegisterResponse badResp(response(QModbusPdu::WriteSingleRegister, be16(7)), kProto, 1, 0, kTs); QVERIFY(!badResp.isValid()); + + const auto payload = QByteArray::fromHex("1b593f800000"); + WriteSingleRegisterRequest enronReq(request(QModbusPdu::WriteSingleRegister, payload), kProto, 1, 0, kTs); + WriteSingleRegisterResponse enronResp(response(QModbusPdu::WriteSingleRegister, payload), kProto, 1, 0, kTs); + QVERIFY(enronReq.isValid()); + QVERIFY(enronResp.isValid()); + QCOMPARE(enronReq.value(), quint32(0x3f800000)); + QCOMPARE(enronResp.value(), quint32(0x3f800000)); + const auto invalidPayload = QByteArray::fromHex("00073f800000"); + QVERIFY(!WriteSingleRegisterRequest(request(QModbusPdu::WriteSingleRegister, invalidPayload), kProto, 1, 0, kTs).isValid()); + QVERIFY(!WriteSingleRegisterResponse(response(QModbusPdu::WriteSingleRegister, invalidPayload), kProto, 1, 0, kTs).isValid()); } void TestModbusMessages::writeMultipleCoils() diff --git a/src/tests/test_modbusmultiserver.cpp b/src/tests/test_modbusmultiserver.cpp index 77274b7e..469454d7 100644 --- a/src/tests/test_modbusmultiserver.cpp +++ b/src/tests/test_modbusmultiserver.cpp @@ -79,6 +79,7 @@ class TestModbusMultiServer : public QObject Q_OBJECT private slots: + void deliversQueuedDefinitionsAndData(); void mergesDescriptionMaps(); void replacesDescriptionMaps(); void mergesTimestampMaps(); @@ -115,6 +116,37 @@ private slots: static void prepare(ModbusMultiServer& server, quint8 deviceId, quint16 length = 300); }; +/// +/// \brief Delivers worker-thread updates without registrations supplied by the application entry point. +/// +void TestModbusMultiServer::deliversQueuedDefinitionsAndData() +{ + ModbusMultiServer server; + QObject receiver; + ModbusDefinitions receivedDefinitions; + QPair receivedData; + connect(&server, &ModbusMultiServer::definitionsChanged, &receiver, + [&receivedDefinitions](const ModbusDefinitions& defs) { receivedDefinitions = defs; }); + connect(&server, &ModbusMultiServer::dataChanged, &receiver, + [&receivedData](quint8 deviceId, const QModbusDataUnit& data) { receivedData = qMakePair(deviceId, data); }); + + auto definitions = server.getModbusDefinitions(); + definitions.EnronModbus = true; + server.setModbusDefinitions(definitions); + QTRY_VERIFY(receivedDefinitions.EnronModbus); + definitions.EnronModbus = false; + server.setModbusDefinitions(definitions); + QTRY_VERIFY(!receivedDefinitions.EnronModbus); + + server.addUnitMap(QUuid::createUuid(), 3, QModbusDataUnit::HoldingRegisters, 5001, 2); + const auto data = unit(QModbusDataUnit::HoldingRegisters, 5001, {0x1234, 0x5678}); + server.setData(3, data, WriteSource::User); + QTRY_COMPARE(receivedData.second.values(), data.values()); + QCOMPARE(receivedData.first, quint8(3)); + QCOMPARE(receivedData.second.registerType(), data.registerType()); + QCOMPARE(receivedData.second.startAddress(), data.startAddress()); +} + void TestModbusMultiServer::mergesDescriptionMaps() { ModbusMultiServer server; diff --git a/src/tests/test_modbusserver.cpp b/src/tests/test_modbusserver.cpp index 7a0c4b82..1f653b37 100644 --- a/src/tests/test_modbusserver.cpp +++ b/src/tests/test_modbusserver.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #include "modbusserver.h" @@ -88,6 +89,12 @@ class TestModbusServer : public QObject Q_OBJECT private slots: + void enronAddressSpace_data(); + void enronAddressSpace(); + void enronRequests_data(); + void enronRequests(); + void enronLimitsAndMapping(); + void persistsEnronMode(); void managesAddressesStateAndErrors(); void validatesOptions(); void readsAndWritesMapData(); @@ -101,6 +108,233 @@ private slots: void exercisesHeaderUtilities(); }; +/// \brief Covers both address capacities outside the fixed 32-bit Enron ranges. +void TestModbusServer::enronAddressSpace_data() +{ + QTest::addColumn("space"); + QTest::addColumn("address"); + QTest::addColumn("count"); + QTest::addColumn("allowed"); + for (auto space : {AddressSpace::Addr5Digits, AddressSpace::Addr6Digits}) { + const int capacity = space == AddressSpace::Addr5Digits ? 9999 : 65536; + for (int address : {0, 4000, 6000, 8000, 9998, 9999, 10000, 65534, 65535}) { + for (int count : {1, 2}) { + const auto name = QString("%1-%2-%3").arg(capacity).arg(address).arg(count); + QTest::newRow(qPrintable(name)) << space << address << count << (address + count <= capacity); + } + } + } +} + +/// \brief Applies the selected address capacity consistently to register reads and writes. +void TestModbusServer::enronAddressSpace() +{ + QFETCH(AddressSpace, space); + QFETCH(int, address); + QFETCH(int, count); + QFETCH(bool, allowed); + TestableModbusServer server; + server.addServerAddress(1); + ModbusDefinitions definitions; + definitions.EnronModbus = true; + definitions.AddrSpace = space; + server.setDefinitions(definitions); + ModbusDataUnitMap map; + for (auto type : {QModbusDataUnit::HoldingRegisters, QModbusDataUnit::InputRegisters}) + map.addUnitMap(QUuid::createUuid(), type, address, qMin(count, 65536 - address)); + server.setMap(map, 1); + + for (auto code : {QModbusPdu::ReadHoldingRegisters, QModbusPdu::ReadInputRegisters}) { + const auto response = server.processRequest(QModbusRequest(code, quint16(address), quint16(count)), 1); + QCOMPARE(!response.isException(), allowed); + if (allowed) + QCOMPARE(response.data(), QByteArray(1, char(count * 2)) + QByteArray(count * 2, '\0')); + else + QCOMPARE(response.exceptionCode(), QModbusPdu::IllegalDataAddress); + } + + const QByteArray writePayload = words({quint16(address), quint16(count)}) + + QByteArray(1, char(count * 2)) + QByteArray(count * 2, '\0'); + const auto multiple = server.processRequest(QModbusRequest(QModbusPdu::WriteMultipleRegisters, writePayload), 1); + QCOMPARE(!multiple.isException(), allowed); + if (!allowed) + QCOMPARE(multiple.exceptionCode(), QModbusPdu::IllegalDataAddress); + + const auto combinedPayload = words({quint16(address), quint16(count), quint16(address), quint16(count)}) + + QByteArray(1, char(count * 2)) + QByteArray(count * 2, '\0'); + const auto combined = server.processRequest(QModbusRequest(QModbusPdu::ReadWriteMultipleRegisters, combinedPayload), 1); + QCOMPARE(!combined.isException(), allowed); + if (!allowed) + QCOMPARE(combined.exceptionCode(), QModbusPdu::IllegalDataAddress); + + if (count == 1) { + const auto single = server.processRequest(QModbusRequest(QModbusPdu::WriteSingleRegister, quint16(address), quint16(0x1234)), 1); + QCOMPARE(!single.isException(), allowed); + if (allowed) { + QModbusDataUnit stored(QModbusDataUnit::HoldingRegisters, address, 1); + QVERIFY(server.data(&stored, 1)); + QCOMPARE(stored.value(0), quint16(0x1234)); + } else { + QCOMPARE(single.exceptionCode(), QModbusPdu::IllegalDataAddress); + } + const auto mask = server.processRequest(QModbusRequest(QModbusPdu::MaskWriteRegister, + quint16(address), quint16(0xffff), quint16(0)), 1); + QCOMPARE(!mask.isException(), allowed); + if (!allowed) + QCOMPARE(mask.exceptionCode(), QModbusPdu::IllegalDataAddress); + } + + if (count == 2 && address + count <= 65536) { + QVERIFY(server.setData(QModbusDataUnit(QModbusDataUnit::HoldingRegisters, address, {1, 0x55}), 1)); + const auto fifo = server.processRequest(QModbusRequest(QModbusPdu::ReadFifoQueue, quint16(address)), 1); + QCOMPARE(!fifo.isException(), allowed); + if (allowed) + QCOMPARE(fifo.data(), QByteArray::fromHex("000400010055")); + else + QCOMPARE(fifo.exceptionCode(), QModbusPdu::IllegalDataAddress); + } + + if (space == AddressSpace::Addr5Digits && address == 10000 && count == 1) { + definitions.AddrSpace = AddressSpace::Addr6Digits; + server.setDefinitions(definitions); + const QModbusRequest read(QModbusPdu::ReadHoldingRegisters, quint16(address), quint16(1)); + QVERIFY(!server.processRequest(read, 1).isException()); + definitions.AddrSpace = AddressSpace::Addr5Digits; + server.setDefinitions(definitions); + QCOMPARE(server.processRequest(read, 1).exceptionCode(), QModbusPdu::IllegalDataAddress); + } +} + +/// \brief Covers Enron wire payloads and validation independently of the transport. +void TestModbusServer::enronRequests_data() +{ + QTest::addColumn("enabled"); + QTest::addColumn("requestHex"); + QTest::addColumn("responseHex"); + + QTest::newRow("standard-default") << false << QByteArray("0313890002") << QByteArray("030411223344"); + QTest::newRow("long-pair") << true << QByteArray("0313890002") << QByteArray("03081122334455667788"); + QTest::newRow("second-long") << true << QByteArray("03138a0001") << QByteArray("030455667788"); + QTest::newRow("float-pair") << true << QByteArray("031b590002") << QByteArray("03083f80000040000000"); + QTest::newRow("input-registers") << true << QByteArray("0413890001") << QByteArray("040400000000"); + QTest::newRow("short-registers") << true << QByteArray("030bb90002") << QByteArray("030412345678"); + QTest::newRow("single-long") << true << QByteArray("06138a12345678") << QByteArray("06138a12345678"); + QTest::newRow("multiple-floats") << true << QByteArray("101b590002083f80000040000000") << QByteArray("101b590002"); + QTest::newRow("combined") << true << QByteArray("1713890002138a000104abcdef01") << QByteArray("170811223344abcdef01"); + QTest::newRow("single-short") << true << QByteArray("060bb9abcd") << QByteArray("060bb9abcd"); + QTest::newRow("multiple-short") << true << QByteArray("100bb900020412345678") << QByteArray("100bb90002"); + QTest::newRow("missing-long-word") << true << QByteArray("0613891234") << QByteArray("8603"); + QTest::newRow("trailing-single-data") << true << QByteArray("0613891234567800") << QByteArray("8603"); + QTest::newRow("wrong-byte-count") << true << QByteArray("1013890001021234") << QByteArray("9003"); + QTest::newRow("truncated-multiple") << true << QByteArray("1013890001041234") << QByteArray("9003"); + QTest::newRow("crosses-long-start") << true << QByteArray("0313880002") << QByteArray("8302"); + QTest::newRow("crosses-long-end") << true << QByteArray("03176f0002") << QByteArray("8302"); + QTest::newRow("crosses-float-end") << true << QByteArray("031f3f0002") << QByteArray("8302"); + QTest::newRow("crosses-write-end") << true << QByteArray("10176f0002080000000000000000") << QByteArray("9002"); + QTest::newRow("crosses-combined-read-end") << true << QByteArray("17176f0002138900010400000000") << QByteArray("9702"); + QTest::newRow("zero-write-quantity") << true << QByteArray("101389000000") << QByteArray("9003"); + QTest::newRow("zero-quantity") << true << QByteArray("0313890000") << QByteArray("8303"); + QTest::newRow("read-over-limit") << true << QByteArray("031389003f") << QByteArray("8303"); + QTest::newRow("mask-long") << true << QByteArray("161389ffff0000") << QByteArray("9601"); + QTest::newRow("fifo-long") << true << QByteArray("181389") << QByteArray("9801"); + QTest::newRow("coil-address-unaffected") << true << QByteArray("0113890001") << QByteArray("010100"); +} + +/// \brief Checks that Enron quantities preserve independent 32-bit values. +void TestModbusServer::enronRequests() +{ + QFETCH(bool, enabled); + QFETCH(QByteArray, requestHex); + QFETCH(QByteArray, responseHex); + TestableModbusServer server; + server.addServerAddress(1); + ModbusDataUnitMap map; + for (auto type : {QModbusDataUnit::HoldingRegisters, QModbusDataUnit::InputRegisters, QModbusDataUnit::Coils}) + map.addUnitMap(QUuid::createUuid(), type, 0, 10000); + server.setMap(map, 1); + ModbusDefinitions definitions; + definitions.EnronModbus = enabled; + server.setDefinitions(definitions); + QVERIFY(server.setData(QModbusDataUnit(QModbusDataUnit::HoldingRegisters, 5001, {0x1122, 0x3344, 0x5566, 0x7788}), 1)); + QVERIFY(server.setData(QModbusDataUnit(QModbusDataUnit::HoldingRegisters, 7001, {0x3f80, 0, 0x4000, 0}), 1)); + QVERIFY(server.setData(QModbusDataUnit(QModbusDataUnit::HoldingRegisters, 3001, {0x1234, 0x5678}), 1)); + + const QByteArray bytes = QByteArray::fromHex(requestHex); + const auto code = QModbusPdu::FunctionCode(quint8(bytes.at(0))); + const auto response = server.processRequest(QModbusRequest(code, bytes.mid(1)), 1); + QByteArray actual(1, char(response.functionCode() | (response.isException() ? 0x80 : 0))); + actual += response.data(); + QCOMPARE(actual.toHex(), responseHex); + if (!response.isException() && code == QModbusPdu::WriteSingleRegister && bytes.size() == 7) { + QModbusDataUnit stored(QModbusDataUnit::HoldingRegisters, 5001, 4); + QVERIFY(server.data(&stored, 1)); + QCOMPARE(stored.values(), QVector({0x1122, 0x3344, 0x1234, 0x5678})); + } +} + +/// \brief Checks PDU limits, range endpoints, missing maps and runtime mode changes. +void TestModbusServer::enronLimitsAndMapping() +{ + TestableModbusServer server; + server.addServerAddress(1); + ModbusDataUnitMap map; + map.addUnitMap(QUuid::createUuid(), QModbusDataUnit::HoldingRegisters, 5001, 1998); + map.addUnitMap(QUuid::createUuid(), QModbusDataUnit::HoldingRegisters, 7001, 1998); + server.setMap(map, 1); + ModbusDefinitions definitions; + definitions.EnronModbus = true; + server.setDefinitions(definitions); + for (quint16 address : {5001, 5999, 7001, 7999}) { + const auto response = server.processRequest(QModbusRequest(QModbusPdu::WriteSingleRegister, address, quint16(0x1234), quint16(0x5678)), 1); + QVERIFY(!response.isException()); + QCOMPARE(server.processRequest(QModbusRequest(QModbusPdu::ReadHoldingRegisters, address, quint16(1)), 1).data(), QByteArray::fromHex("0412345678")); + } + const QModbusRequest read(QModbusPdu::ReadHoldingRegisters, quint16(5001), quint16(62)); + QCOMPARE(server.processRequest(read, 1).dataSize(), 249); + const QModbusRequest write(QModbusPdu::WriteMultipleRegisters, QByteArray::fromHex("1389003df4") + QByteArray(244, '\0')); + QVERIFY(!server.processRequest(write, 1).isException()); + const QModbusRequest tooLarge(QModbusPdu::WriteMultipleRegisters, QByteArray::fromHex("1389003ef8") + QByteArray(248, '\0')); + QCOMPARE(server.processRequest(tooLarge, 1).exceptionCode(), QModbusPdu::IllegalDataValue); + const QModbusRequest missing(QModbusPdu::ReadHoldingRegisters, quint16(9000), quint16(1)); + QCOMPARE(server.processRequest(missing, 1).exceptionCode(), QModbusPdu::IllegalDataAddress); + definitions.EnronModbus = false; + server.setDefinitions(definitions); + QCOMPARE(server.processRequest(read, 1).dataSize(), 125); +} + +/// \brief Keeps Enron mode across settings and XML round trips while defaulting legacy projects to standard Modbus. +void TestModbusServer::persistsEnronMode() +{ + QTemporaryDir dir; + QVERIFY(dir.isValid()); + QSettings settings(dir.filePath("settings.ini"), QSettings::IniFormat); + ModbusDefinitions original; + QVERIFY(!original.EnronModbus); + original.EnronModbus = true; + settings << original; + ModbusDefinitions loaded; + settings >> loaded; + QVERIFY(loaded.EnronModbus); + + QByteArray bytes; + QXmlStreamWriter writer(&bytes); + writer << original; + QXmlStreamReader reader(bytes); + QVERIFY(reader.readNextStartElement()); + loaded.EnronModbus = false; + reader >> loaded; + QVERIFY(!reader.hasError()); + QVERIFY(loaded.EnronModbus); + QXmlStreamReader legacy(""); + QVERIFY(legacy.readNextStartElement()); + legacy >> loaded; + QVERIFY(!loaded.EnronModbus); + settings.clear(); + loaded.EnronModbus = true; + settings >> loaded; + QVERIFY(!loaded.EnronModbus); +} + void TestModbusServer::managesAddressesStateAndErrors() { TestableModbusServer server; diff --git a/src/tests/test_modbustransports.cpp b/src/tests/test_modbustransports.cpp index 92c8bb68..4f75d54c 100644 --- a/src/tests/test_modbustransports.cpp +++ b/src/tests/test_modbustransports.cpp @@ -145,6 +145,8 @@ class TestModbusTransports : public QObject Q_OBJECT private slots: + void servesEnronRequests_data(); + void servesEnronRequests(); void servesModbusTcpRequests(); void simulatesModbusTcpErrors(); void rejectsOccupiedTcpPort(); @@ -159,6 +161,53 @@ private slots: void sniffsRtuSerialOnPseudoTerminal(); }; +/// \brief Exercises Enron packet framing on both network transports. +void TestModbusTransports::servesEnronRequests_data() +{ + QTest::addColumn("rtu"); + QTest::newRow("tcp") << false; + QTest::newRow("rtu-over-tcp") << true; +} + +/// \brief Verifies fragmented 32-bit writes followed by coalesced reads on the same connection. +void TestModbusTransports::servesEnronRequests() +{ + QFETCH(bool, rtu); + ModbusTcpServer tcpServer; + ModbusRtuTcpServer rtuServer; + ModbusServer& server = rtu ? static_cast(rtuServer) : static_cast(tcpServer); + const quint16 port = startServer(server); + QVERIFY(port != 0); + ModbusDefinitions definitions; + definitions.EnronModbus = true; + server.setDefinitions(definitions); + ModbusDataUnitMap map; + map.addUnitMap(QUuid::createUuid(), QModbusDataUnit::HoldingRegisters, 7001, 4); + server.setMap(map, 1); + QTcpSocket socket; + socket.connectToHost(QHostAddress::LocalHost, port); + QVERIFY(socket.waitForConnected(1000)); + QModbusRequest write(QModbusPdu::WriteSingleRegister, quint16(7002), quint16(0x3f80), quint16(0)); + const QByteArray frame = rtu ? rtuFrame(1, write) : tcpFrame(1, 1, write); + socket.write(frame.left(frame.size() - 2)); + QVERIFY(socket.waitForBytesWritten(1000)); + QTest::qWait(10); + socket.write(frame.right(2)); + const auto response = receive(socket); + QCOMPARE(response, frame); + QModbusDataUnit stored(QModbusDataUnit::HoldingRegisters, 7001, 4); + QVERIFY(server.data(&stored, 1)); + QCOMPARE(stored.values(), QVector({0, 0, 0x3f80, 0})); + + const QModbusRequest read(QModbusPdu::ReadHoldingRegisters, quint16(7001), quint16(2)); + const auto readFrame = rtu ? rtuFrame(1, read) : tcpFrame(2, 1, read); + socket.write(readFrame + readFrame); + const QByteArray values = QByteArray::fromHex("08000000003f800000"); + const QModbusRequest expected(QModbusPdu::ReadHoldingRegisters, values); + const auto expectedFrame = rtu ? rtuFrame(1, expected) : tcpFrame(2, 1, expected); + QCOMPARE(receive(socket), expectedFrame + expectedFrame); +} + void TestModbusTransports::servesModbusTcpRequests() { ModbusTcpServer server; @@ -651,6 +700,22 @@ void TestModbusTransports::servesRtuSerialOnPseudoTerminal() QVERIFY(receivedSpy.count() >= 8); QVERIFY(sentSpy.count() >= 3); + server.setValue(ModbusServer::ListenOnlyMode, false, 1); + definitions.EnronModbus = true; + server.setDefinitions(definitions); + ModbusDataUnitMap enronMap; + enronMap.addUnitMap(QUuid::createUuid(), QModbusDataUnit::HoldingRegisters, 7001, 2); + server.setMap(enronMap, 1); + const auto enronWrite = rtuFrame(1, QModbusRequest(QModbusPdu::WriteSingleRegister, + quint16(7001), quint16(0x3f80), quint16(0))); + QVERIFY(send(masterFd, enronWrite)); + QCOMPARE(receive(masterFd), enronWrite); + const auto enronRead = rtuFrame(1, QModbusRequest(QModbusPdu::ReadHoldingRegisters, + quint16(7001), quint16(1))); + QVERIFY(send(masterFd, enronRead)); + QCOMPARE(receive(masterFd), rtuFrame(1, QModbusRequest(QModbusPdu::ReadHoldingRegisters, + QByteArray::fromHex("043f800000")))); + server.disconnectDevice(); QCOMPARE(server.state(), QModbusDevice::UnconnectedState); ::close(masterFd);