Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 19 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

<img width="1292" height="759" alt="image" src=".github/assets/omodsim-trafficview.png" />
Expand Down Expand Up @@ -247,14 +258,14 @@ Below are the methods for installing the OpenModSim for different OS
## <img src="docs/icons/logo_windows.svg" width="16" height="16"> 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`

## <img src="docs/icons/logo_apple.svg" width="16" height="16"> 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.

Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down
11 changes: 11 additions & 0 deletions README.md.in
Original file line number Diff line number Diff line change
Expand Up @@ -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

<img width="1292" height="759" alt="image" src=".github/assets/omodsim-trafficview.png" />
Expand Down
2 changes: 1 addition & 1 deletion src/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
38 changes: 26 additions & 12 deletions src/applogger.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions src/cmake/sources.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@ set(SOURCES

# Header files
set(HEADERS
enronmodbus.h
ansimenu.h
ansiutils.h
application.h
Expand Down
4 changes: 2 additions & 2 deletions src/controls/modbusmessagewidget.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -406,15 +406,15 @@ void ModbusMessageWidget::update()
{
auto req = reinterpret_cast<const WriteSingleRegisterRequest*>(_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);
}
else
{
auto resp = reinterpret_cast<const WriteSingleRegisterResponse*>(_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);
}
Expand Down
86 changes: 60 additions & 26 deletions src/controls/outputdatawidget.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
#include <QGuiApplication>
#include <QPainter>
#include <QInputDialog>
#include <QSortFilterProxyModel>
#include <QSignalBlocker>
#include <climits>
#include "../styles/appcolors.h"
#include "apppreferences.h"
Expand Down Expand Up @@ -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
Expand All @@ -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())
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -248,8 +269,13 @@ QVector<quint16> OutputDataListModel::values() const
///
void OutputDataListModel::clear()
{
_mapItems.clear();
updateData(QModbusDataUnit());
beginResetModel();
{
QSignalBlocker blocker(this);
_mapItems.clear();
updateData(QModbusDataUnit());
}
endResetModel();
}

///
Expand Down Expand Up @@ -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<int>() << Qt::DisplayRole);
emit dataChanged(index(first_changed), index(last_changed), QVector<int>() << Qt::DisplayRole << ValueStartRole);
}
}
}
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -773,7 +803,7 @@ void OutputDataWidget::setDataViewColumnsDistance(int value)
///
int OutputDataWidget::rowCount() const
{
return _listModel->rowCount();
return _visibleModel->rowCount();
}

///
Expand All @@ -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());
Expand Down Expand Up @@ -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);
}

Expand Down Expand Up @@ -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);
}

///
Expand Down Expand Up @@ -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<quint16>(
Expand Down Expand Up @@ -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:
Expand All @@ -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:
Expand Down
Loading
Loading