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
@@ -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
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