From 8a984792ce157abbca115300d8238119bfad5f9b Mon Sep 17 00:00:00 2001 From: Alexandr Ananev Date: Wed, 4 Nov 2026 21:54:33 +0700 Subject: [PATCH 1/5] feat: Add opt-in Enron Modbus register support Persist the server mode, support 32-bit Enron register requests across transports, and honor the selected 5/6-digit address space. Add protocol, transport, settings and project regression coverage for issue #141. --- README.md | 25 +++ README.md.in | 25 +++ src/cmake/sources.cmake | 1 + src/controls/modbusmessagewidget.cpp | 4 +- src/dialogs/dialogmodbusdefinitions.cpp | 2 + src/dialogs/dialogmodbusdefinitions.ui | 10 + src/enronmodbus.h | 31 +++ src/modbusdefinitions.h | 6 + src/modbusmessages/writesingleregister.h | 27 ++- src/modbusrtuserialserver.cpp | 10 +- src/modbusrtutcpserver.cpp | 26 ++- src/modbusserver.cpp | 122 +++++++++--- src/modbusserver.h | 2 + src/modbustcpserver.cpp | 9 +- src/res/translations/omodsim_it.ts | 70 ++++--- src/res/translations/omodsim_ru.qm | Bin 77615 -> 78678 bytes src/res/translations/omodsim_ru.ts | 70 ++++--- src/res/translations/omodsim_zh_CN.ts | 70 ++++--- src/res/translations/omodsim_zh_TW.ts | 70 ++++--- src/tests/test_appproject.cpp | 54 ++++++ src/tests/test_modbusmessages.cpp | 11 ++ src/tests/test_modbusserver.cpp | 234 +++++++++++++++++++++++ src/tests/test_modbustransports.cpp | 65 +++++++ 23 files changed, 776 insertions(+), 168 deletions(-) create mode 100644 src/enronmodbus.h diff --git a/README.md b/README.md index b133effa..df2fe7f2 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,31 @@ 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 applies to all server transports and is saved with the project. +Standard Modbus remains the default for existing projects. + +Enron protocol addresses are used directly: 5001–5999 contain 32-bit integers, +and 7001–7999 contain IEEE 754 floats. Other register addresses retain 16-bit +values within the selected 5/6-digit address space. Requests exceeding that +capacity or crossing a 16/32-bit range boundary are rejected. The address-space +setting does not shift or scale the Enron ranges. +Coils and discrete inputs retain their usual addressing. + +The existing tables and script API store 16-bit words. For either 32-bit range, +Enron address `A` uses table words `B + 2 * (A - B)` and the next word, where +`B` is 5001 or 7001. Select zero-based table addresses. For example, Enron float +7002 uses table words 7003–7004; set them to `0x3F80` and `0x0000` for 1.0. +Use a 32-bit table display format with the high word and high byte first. + +Functions 03, 04, 06, 16 and 23 support these register widths. A single read can +return up to 62 32-bit values; function 16 can write up to 61, and function 23 +can write up to 60. Mask writes and FIFO reads are unavailable for 32-bit +addresses. Enron event logs, historical records and device-specific extensions +are outside this implementation. + # Modbus Logging image diff --git a/README.md.in b/README.md.in index 2ce824db..000029f0 100644 --- a/README.md.in +++ b/README.md.in @@ -47,6 +47,31 @@ 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 applies to all server transports and is saved with the project. +Standard Modbus remains the default for existing projects. + +Enron protocol addresses are used directly: 5001–5999 contain 32-bit integers, +and 7001–7999 contain IEEE 754 floats. Other register addresses retain 16-bit +values within the selected 5/6-digit address space. Requests exceeding that +capacity or crossing a 16/32-bit range boundary are rejected. The address-space +setting does not shift or scale the Enron ranges. +Coils and discrete inputs retain their usual addressing. + +The existing tables and script API store 16-bit words. For either 32-bit range, +Enron address `A` uses table words `B + 2 * (A - B)` and the next word, where +`B` is 5001 or 7001. Select zero-based table addresses. For example, Enron float +7002 uses table words 7003–7004; set them to `0x3F80` and `0x0000` for 1.0. +Use a 32-bit table display format with the high word and high byte first. + +Functions 03, 04, 06, 16 and 23 support these register widths. A single read can +return up to 62 32-bit values; function 16 can write up to 61, and function 23 +can write up to 60. Mask writes and FIFO reads are unavailable for 32-bit +addresses. Enron event logs, historical records and device-specific extensions +are outside this implementation. + # Modbus Logging image 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/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..380d254a 100644 --- a/src/dialogs/dialogmodbusdefinitions.ui +++ b/src/dialogs/dialogmodbusdefinitions.ui @@ -58,6 +58,16 @@ + + + + Enron addresses 5001–5999 and 7001–7999 are 32-bit; other addresses are 16-bit. The selected 5/6-digit address space limits the available addresses. Each 32-bit value uses two table words: base + 2 × (address − base), high word first. Use zero-based table addresses. + + + Enable Enron Modbus (32-bit registers) + + + diff --git a/src/enronmodbus.h b/src/enronmodbus.h new file mode 100644 index 00000000..fd66e84d --- /dev/null +++ b/src/enronmodbus.h @@ -0,0 +1,31 @@ +// 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 + +namespace EnronModbus { + +constexpr quint16 LongBase = 5001; +constexpr quint16 FloatBase = 7001; +constexpr quint16 RangeLength = 999; + +/// +/// \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); +} + +} + +#endif // ENRONMODBUS_H 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/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/res/translations/omodsim_it.ts b/src/res/translations/omodsim_it.ts index 338ef008..3e7947b8 100644 --- a/src/res/translations/omodsim_it.ts +++ b/src/res/translations/omodsim_it.ts @@ -1070,48 +1070,58 @@ Vuoi aprire la pagina di download? Aggiungi automaticamente registri su richiesta - + + Enron addresses 5001–5999 and 7001–7999 are 32-bit; other addresses are 16-bit. The selected 5/6-digit address space limits the available addresses. Each 32-bit value uses two table words: base + 2 × (address − base), high word first. Use zero-based table addresses. + + + + + 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 @@ -2192,17 +2202,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 +2221,7 @@ Indirizzo iniziale: %2 Lunghezza: %3 - + Unit Identifier: %1 Data Type: %2 @@ -3770,12 +3780,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 +3793,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. @@ -4029,47 +4039,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 +4087,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 +4100,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 diff --git a/src/res/translations/omodsim_ru.qm b/src/res/translations/omodsim_ru.qm index 2dafe8c7bdd897a0084ee7843b1c17e1b6a921a6..8cfda6858de99357e27998539ed1e41ee9b7ddef 100644 GIT binary patch delta 6639 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 Ye-8G~%z77`-pCjc+GzKg5s{bv4`ddyxBvhE delta 5659 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 x{$dTTu{`e|%>Tb=dHIqH4RcbsHsA8J DialogModbusDefinitions + + + Enable Enron Modbus (32-bit registers) + Включить Enron Modbus (32-битные регистры) + + + + Enron addresses 5001–5999 and 7001–7999 are 32-bit; other addresses are 16-bit. The selected 5/6-digit address space limits the available addresses. Each 32-bit value uses two table words: base + 2 × (address − base), high word first. Use zero-based table addresses. + Адреса Enron 5001–5999 и 7001–7999 — 32-битные, остальные — 16-битные. Доступные адреса ограничены выбранным 5/6-значным адресным пространством. Каждое 32-битное значение занимает два слова таблицы: база + 2 × (адрес − база), старшее слово первым. Используйте адреса таблицы с нуля. + Modbus Definitions @@ -1070,48 +1080,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 Не отвечать на запросы @@ -2192,17 +2202,17 @@ Type Filter: %3 Перестановка байт (0xAB → 0xBA) - + 0-based 0-первый - + 1-based 1-первый - + Address Base: %1 Starting Address: %2 Length: %3 @@ -2211,7 +2221,7 @@ Length: %3 Длина: %3 - + Unit Identifier: %1 Data Type: %2 @@ -3770,12 +3780,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 +3793,7 @@ Please verify the merge result carefully. ModbusTcpServer - + Invalid connection settings for TCP communication specified. Указаны некорректные настройки для TCP соединения. @@ -4029,47 +4039,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 +4087,12 @@ Please verify the merge result carefully. QModbusRtuSerialServer - + Requesting serial port is closed Запрашиваемый последовательный порт закрыт - + Could not write response to client Не удалось создать ответ клиенту @@ -4090,12 +4100,12 @@ Please verify the merge result carefully. QModbusTcpServer - + Requesting socket is closed Запрашивающий сокет закрыт - + Could not write response to client Не удалось создать ответ клиенту diff --git a/src/res/translations/omodsim_zh_CN.ts b/src/res/translations/omodsim_zh_CN.ts index 3139aa1c..54bd5b74 100644 --- a/src/res/translations/omodsim_zh_CN.ts +++ b/src/res/translations/omodsim_zh_CN.ts @@ -1070,48 +1070,58 @@ Open the download page? 根据请求自动添加寄存器 - + + Enron addresses 5001–5999 and 7001–7999 are 32-bit; other addresses are 16-bit. The selected 5/6-digit address space limits the available addresses. Each 32-bit value uses two table words: base + 2 × (address − base), high word first. Use zero-based table addresses. + + + + + 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 没有响应 @@ -2192,17 +2202,17 @@ Type Filter: %3 交换字节(0xAB → 0xBA) - + 0-based 基地址为-0 - + 1-based 基地址为-1 - + Address Base: %1 Starting Address: %2 Length: %3 @@ -2211,7 +2221,7 @@ Length: %3 长度:%3 - + Unit Identifier: %1 Data Type: %2 @@ -3771,12 +3781,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 +3794,7 @@ Please verify the merge result carefully. ModbusTcpServer - + Invalid connection settings for TCP communication specified. 指定的 TCP 通讯连接设定无效。 @@ -4030,47 +4040,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 +4088,12 @@ Please verify the merge result carefully. QModbusRtuSerialServer - + Requesting serial port is closed 请求的串口已关闭 - + Could not write response to client 无法写入对客户端的响应 @@ -4091,12 +4101,12 @@ Please verify the merge result carefully. QModbusTcpServer - + Requesting socket is closed 请求的套接字已关闭 - + Could not write response to client 无法写入对客户端的响应 diff --git a/src/res/translations/omodsim_zh_TW.ts b/src/res/translations/omodsim_zh_TW.ts index 2f1028e5..0046a353 100644 --- a/src/res/translations/omodsim_zh_TW.ts +++ b/src/res/translations/omodsim_zh_TW.ts @@ -1070,48 +1070,58 @@ Open the download page? 依請求自動新增暫存器 - + + Enron addresses 5001–5999 and 7001–7999 are 32-bit; other addresses are 16-bit. The selected 5/6-digit address space limits the available addresses. Each 32-bit value uses two table words: base + 2 × (address − base), high word first. Use zero-based table addresses. + + + + + 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 沒有響應 @@ -2192,17 +2202,17 @@ Type Filter: %3 交換位元組(0xAB → 0xBA) - + 0-based 基地址為-0 - + 1-based 基地址為-1 - + Address Base: %1 Starting Address: %2 Length: %3 @@ -2211,7 +2221,7 @@ Length: %3 長度:%3 - + Unit Identifier: %1 Data Type: %2 @@ -3771,12 +3781,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 +3794,7 @@ Please verify the merge result carefully. ModbusTcpServer - + Invalid connection settings for TCP communication specified. 指定的 TCP 通訊連線設定無效。 @@ -4030,47 +4040,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 +4088,12 @@ Please verify the merge result carefully. QModbusRtuSerialServer - + Requesting serial port is closed 請求的序列埠已關閉 - + Could not write response to client 無法寫入對客戶端的響應 @@ -4091,12 +4101,12 @@ Please verify the merge result carefully. QModbusTcpServer - + Requesting socket is closed 請求的套接字已關閉 - + Could not write response to client 無法寫入對客戶端的響應 diff --git a/src/tests/test_appproject.cpp b/src/tests/test_appproject.cpp index 8448581f..5bb022a4 100644 --- a/src/tests/test_appproject.cpp +++ b/src/tests/test_appproject.cpp @@ -7,6 +7,9 @@ /// #include +#include +#include +#include #include #include #include @@ -18,6 +21,7 @@ #include "controls/mdiareaex.h" #include "controls/projecttreewidget.h" #include "datasimulator.h" +#include "dialogs/dialogmodbusdefinitions.h" #include "formdataview.h" #include "mainwindow.h" #include "modbusmultiserver.h" @@ -28,6 +32,8 @@ class TestAppProject : public QObject Q_OBJECT private slots: + void configuresEnronMode(); + void roundTripsEnronMode(); void createsAndNumbersEveryFormKind(); void closesReopensAndDeletesCanonicalForm(); void respectsDeletionLock(); @@ -195,6 +201,54 @@ QStringList commentPlacements(const QString& path) } /// \brief Verifies creation, UUID assignment and per-kind display counters. +/// \brief Applies Enron mode through server settings and preserves it when edits are cancelled. +void TestAppProject::configuresEnronMode() +{ + ModbusMultiServer server; + DialogModbusDefinitions dialog(server); + auto* checkbox = dialog.findChild("checkBoxEnronModbus"); + auto* buttons = dialog.findChild("buttonBox"); + QVERIFY(checkbox); + QVERIFY(buttons); + QVERIFY(!checkbox->isChecked()); + checkbox->setChecked(true); + QVERIFY(buttons->button(QDialogButtonBox::Apply)->isEnabled()); + buttons->button(QDialogButtonBox::Apply)->click(); + QVERIFY(server.getModbusDefinitions().EnronModbus); + QVERIFY(!buttons->button(QDialogButtonBox::Apply)->isEnabled()); + + DialogModbusDefinitions cancelled(server); + auto* restored = cancelled.findChild("checkBoxEnronModbus"); + QVERIFY(restored->isChecked()); + restored->setChecked(false); + cancelled.reject(); + QVERIFY(server.getModbusDefinitions().EnronModbus); + + checkbox->setChecked(false); + dialog.accept(); + QVERIFY(!server.getModbusDefinitions().EnronModbus); +} + +/// \brief Restores the Enron server setting when reopening a project. +void TestAppProject::roundTripsEnronMode() +{ + ProjectFixture fixture; + QTemporaryDir files; + const auto source = files.filePath("source.omsim"); + const auto saved = files.filePath("enron.omsim"); + writeProjectFile(source, kAnnotatedProject); + QVERIFY(fixture.Project.loadProject(source).Success); + auto definitions = fixture.Server.getModbusDefinitions(); + definitions.EnronModbus = true; + fixture.Server.setModbusDefinitions(definitions); + QVERIFY(fixture.Project.saveProject(saved)); + fixture.Project.closeProject(); + definitions.EnronModbus = false; + fixture.Host.mbMultiServer().setModbusDefinitions(definitions); + QVERIFY(fixture.Project.loadProject(saved).Success); + QVERIFY(fixture.Host.mbMultiServer().getModbusDefinitions().EnronModbus); +} + void TestAppProject::createsAndNumbersEveryFormKind() { ProjectFixture fixture; diff --git a/src/tests/test_modbusmessages.cpp b/src/tests/test_modbusmessages.cpp index 758174a1..0f8cf4e5 100644 --- a/src/tests/test_modbusmessages.cpp +++ b/src/tests/test_modbusmessages.cpp @@ -248,6 +248,17 @@ void TestModbusMessages::writeSingleRegister() WriteSingleRegisterResponse badResp(response(QModbusPdu::WriteSingleRegister, be16(7)), kProto, 1, 0, kTs); QVERIFY(!badResp.isValid()); + + const auto payload = QByteArray::fromHex("1b593f800000"); + WriteSingleRegisterRequest enronReq(request(QModbusPdu::WriteSingleRegister, payload), kProto, 1, 0, kTs); + WriteSingleRegisterResponse enronResp(response(QModbusPdu::WriteSingleRegister, payload), kProto, 1, 0, kTs); + QVERIFY(enronReq.isValid()); + QVERIFY(enronResp.isValid()); + QCOMPARE(enronReq.value(), quint32(0x3f800000)); + QCOMPARE(enronResp.value(), quint32(0x3f800000)); + const auto invalidPayload = QByteArray::fromHex("00073f800000"); + QVERIFY(!WriteSingleRegisterRequest(request(QModbusPdu::WriteSingleRegister, invalidPayload), kProto, 1, 0, kTs).isValid()); + QVERIFY(!WriteSingleRegisterResponse(response(QModbusPdu::WriteSingleRegister, invalidPayload), kProto, 1, 0, kTs).isValid()); } void TestModbusMessages::writeMultipleCoils() diff --git a/src/tests/test_modbusserver.cpp b/src/tests/test_modbusserver.cpp index 7a0c4b82..1f653b37 100644 --- a/src/tests/test_modbusserver.cpp +++ b/src/tests/test_modbusserver.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #include "modbusserver.h" @@ -88,6 +89,12 @@ class TestModbusServer : public QObject Q_OBJECT private slots: + void enronAddressSpace_data(); + void enronAddressSpace(); + void enronRequests_data(); + void enronRequests(); + void enronLimitsAndMapping(); + void persistsEnronMode(); void managesAddressesStateAndErrors(); void validatesOptions(); void readsAndWritesMapData(); @@ -101,6 +108,233 @@ private slots: void exercisesHeaderUtilities(); }; +/// \brief Covers both address capacities outside the fixed 32-bit Enron ranges. +void TestModbusServer::enronAddressSpace_data() +{ + QTest::addColumn("space"); + QTest::addColumn("address"); + QTest::addColumn("count"); + QTest::addColumn("allowed"); + for (auto space : {AddressSpace::Addr5Digits, AddressSpace::Addr6Digits}) { + const int capacity = space == AddressSpace::Addr5Digits ? 9999 : 65536; + for (int address : {0, 4000, 6000, 8000, 9998, 9999, 10000, 65534, 65535}) { + for (int count : {1, 2}) { + const auto name = QString("%1-%2-%3").arg(capacity).arg(address).arg(count); + QTest::newRow(qPrintable(name)) << space << address << count << (address + count <= capacity); + } + } + } +} + +/// \brief Applies the selected address capacity consistently to register reads and writes. +void TestModbusServer::enronAddressSpace() +{ + QFETCH(AddressSpace, space); + QFETCH(int, address); + QFETCH(int, count); + QFETCH(bool, allowed); + TestableModbusServer server; + server.addServerAddress(1); + ModbusDefinitions definitions; + definitions.EnronModbus = true; + definitions.AddrSpace = space; + server.setDefinitions(definitions); + ModbusDataUnitMap map; + for (auto type : {QModbusDataUnit::HoldingRegisters, QModbusDataUnit::InputRegisters}) + map.addUnitMap(QUuid::createUuid(), type, address, qMin(count, 65536 - address)); + server.setMap(map, 1); + + for (auto code : {QModbusPdu::ReadHoldingRegisters, QModbusPdu::ReadInputRegisters}) { + const auto response = server.processRequest(QModbusRequest(code, quint16(address), quint16(count)), 1); + QCOMPARE(!response.isException(), allowed); + if (allowed) + QCOMPARE(response.data(), QByteArray(1, char(count * 2)) + QByteArray(count * 2, '\0')); + else + QCOMPARE(response.exceptionCode(), QModbusPdu::IllegalDataAddress); + } + + const QByteArray writePayload = words({quint16(address), quint16(count)}) + + QByteArray(1, char(count * 2)) + QByteArray(count * 2, '\0'); + const auto multiple = server.processRequest(QModbusRequest(QModbusPdu::WriteMultipleRegisters, writePayload), 1); + QCOMPARE(!multiple.isException(), allowed); + if (!allowed) + QCOMPARE(multiple.exceptionCode(), QModbusPdu::IllegalDataAddress); + + const auto combinedPayload = words({quint16(address), quint16(count), quint16(address), quint16(count)}) + + QByteArray(1, char(count * 2)) + QByteArray(count * 2, '\0'); + const auto combined = server.processRequest(QModbusRequest(QModbusPdu::ReadWriteMultipleRegisters, combinedPayload), 1); + QCOMPARE(!combined.isException(), allowed); + if (!allowed) + QCOMPARE(combined.exceptionCode(), QModbusPdu::IllegalDataAddress); + + if (count == 1) { + const auto single = server.processRequest(QModbusRequest(QModbusPdu::WriteSingleRegister, quint16(address), quint16(0x1234)), 1); + QCOMPARE(!single.isException(), allowed); + if (allowed) { + QModbusDataUnit stored(QModbusDataUnit::HoldingRegisters, address, 1); + QVERIFY(server.data(&stored, 1)); + QCOMPARE(stored.value(0), quint16(0x1234)); + } else { + QCOMPARE(single.exceptionCode(), QModbusPdu::IllegalDataAddress); + } + const auto mask = server.processRequest(QModbusRequest(QModbusPdu::MaskWriteRegister, + quint16(address), quint16(0xffff), quint16(0)), 1); + QCOMPARE(!mask.isException(), allowed); + if (!allowed) + QCOMPARE(mask.exceptionCode(), QModbusPdu::IllegalDataAddress); + } + + if (count == 2 && address + count <= 65536) { + QVERIFY(server.setData(QModbusDataUnit(QModbusDataUnit::HoldingRegisters, address, {1, 0x55}), 1)); + const auto fifo = server.processRequest(QModbusRequest(QModbusPdu::ReadFifoQueue, quint16(address)), 1); + QCOMPARE(!fifo.isException(), allowed); + if (allowed) + QCOMPARE(fifo.data(), QByteArray::fromHex("000400010055")); + else + QCOMPARE(fifo.exceptionCode(), QModbusPdu::IllegalDataAddress); + } + + if (space == AddressSpace::Addr5Digits && address == 10000 && count == 1) { + definitions.AddrSpace = AddressSpace::Addr6Digits; + server.setDefinitions(definitions); + const QModbusRequest read(QModbusPdu::ReadHoldingRegisters, quint16(address), quint16(1)); + QVERIFY(!server.processRequest(read, 1).isException()); + definitions.AddrSpace = AddressSpace::Addr5Digits; + server.setDefinitions(definitions); + QCOMPARE(server.processRequest(read, 1).exceptionCode(), QModbusPdu::IllegalDataAddress); + } +} + +/// \brief Covers Enron wire payloads and validation independently of the transport. +void TestModbusServer::enronRequests_data() +{ + QTest::addColumn("enabled"); + QTest::addColumn("requestHex"); + QTest::addColumn("responseHex"); + + QTest::newRow("standard-default") << false << QByteArray("0313890002") << QByteArray("030411223344"); + QTest::newRow("long-pair") << true << QByteArray("0313890002") << QByteArray("03081122334455667788"); + QTest::newRow("second-long") << true << QByteArray("03138a0001") << QByteArray("030455667788"); + QTest::newRow("float-pair") << true << QByteArray("031b590002") << QByteArray("03083f80000040000000"); + QTest::newRow("input-registers") << true << QByteArray("0413890001") << QByteArray("040400000000"); + QTest::newRow("short-registers") << true << QByteArray("030bb90002") << QByteArray("030412345678"); + QTest::newRow("single-long") << true << QByteArray("06138a12345678") << QByteArray("06138a12345678"); + QTest::newRow("multiple-floats") << true << QByteArray("101b590002083f80000040000000") << QByteArray("101b590002"); + QTest::newRow("combined") << true << QByteArray("1713890002138a000104abcdef01") << QByteArray("170811223344abcdef01"); + QTest::newRow("single-short") << true << QByteArray("060bb9abcd") << QByteArray("060bb9abcd"); + QTest::newRow("multiple-short") << true << QByteArray("100bb900020412345678") << QByteArray("100bb90002"); + QTest::newRow("missing-long-word") << true << QByteArray("0613891234") << QByteArray("8603"); + QTest::newRow("trailing-single-data") << true << QByteArray("0613891234567800") << QByteArray("8603"); + QTest::newRow("wrong-byte-count") << true << QByteArray("1013890001021234") << QByteArray("9003"); + QTest::newRow("truncated-multiple") << true << QByteArray("1013890001041234") << QByteArray("9003"); + QTest::newRow("crosses-long-start") << true << QByteArray("0313880002") << QByteArray("8302"); + QTest::newRow("crosses-long-end") << true << QByteArray("03176f0002") << QByteArray("8302"); + QTest::newRow("crosses-float-end") << true << QByteArray("031f3f0002") << QByteArray("8302"); + QTest::newRow("crosses-write-end") << true << QByteArray("10176f0002080000000000000000") << QByteArray("9002"); + QTest::newRow("crosses-combined-read-end") << true << QByteArray("17176f0002138900010400000000") << QByteArray("9702"); + QTest::newRow("zero-write-quantity") << true << QByteArray("101389000000") << QByteArray("9003"); + QTest::newRow("zero-quantity") << true << QByteArray("0313890000") << QByteArray("8303"); + QTest::newRow("read-over-limit") << true << QByteArray("031389003f") << QByteArray("8303"); + QTest::newRow("mask-long") << true << QByteArray("161389ffff0000") << QByteArray("9601"); + QTest::newRow("fifo-long") << true << QByteArray("181389") << QByteArray("9801"); + QTest::newRow("coil-address-unaffected") << true << QByteArray("0113890001") << QByteArray("010100"); +} + +/// \brief Checks that Enron quantities preserve independent 32-bit values. +void TestModbusServer::enronRequests() +{ + QFETCH(bool, enabled); + QFETCH(QByteArray, requestHex); + QFETCH(QByteArray, responseHex); + TestableModbusServer server; + server.addServerAddress(1); + ModbusDataUnitMap map; + for (auto type : {QModbusDataUnit::HoldingRegisters, QModbusDataUnit::InputRegisters, QModbusDataUnit::Coils}) + map.addUnitMap(QUuid::createUuid(), type, 0, 10000); + server.setMap(map, 1); + ModbusDefinitions definitions; + definitions.EnronModbus = enabled; + server.setDefinitions(definitions); + QVERIFY(server.setData(QModbusDataUnit(QModbusDataUnit::HoldingRegisters, 5001, {0x1122, 0x3344, 0x5566, 0x7788}), 1)); + QVERIFY(server.setData(QModbusDataUnit(QModbusDataUnit::HoldingRegisters, 7001, {0x3f80, 0, 0x4000, 0}), 1)); + QVERIFY(server.setData(QModbusDataUnit(QModbusDataUnit::HoldingRegisters, 3001, {0x1234, 0x5678}), 1)); + + const QByteArray bytes = QByteArray::fromHex(requestHex); + const auto code = QModbusPdu::FunctionCode(quint8(bytes.at(0))); + const auto response = server.processRequest(QModbusRequest(code, bytes.mid(1)), 1); + QByteArray actual(1, char(response.functionCode() | (response.isException() ? 0x80 : 0))); + actual += response.data(); + QCOMPARE(actual.toHex(), responseHex); + if (!response.isException() && code == QModbusPdu::WriteSingleRegister && bytes.size() == 7) { + QModbusDataUnit stored(QModbusDataUnit::HoldingRegisters, 5001, 4); + QVERIFY(server.data(&stored, 1)); + QCOMPARE(stored.values(), QVector({0x1122, 0x3344, 0x1234, 0x5678})); + } +} + +/// \brief Checks PDU limits, range endpoints, missing maps and runtime mode changes. +void TestModbusServer::enronLimitsAndMapping() +{ + TestableModbusServer server; + server.addServerAddress(1); + ModbusDataUnitMap map; + map.addUnitMap(QUuid::createUuid(), QModbusDataUnit::HoldingRegisters, 5001, 1998); + map.addUnitMap(QUuid::createUuid(), QModbusDataUnit::HoldingRegisters, 7001, 1998); + server.setMap(map, 1); + ModbusDefinitions definitions; + definitions.EnronModbus = true; + server.setDefinitions(definitions); + for (quint16 address : {5001, 5999, 7001, 7999}) { + const auto response = server.processRequest(QModbusRequest(QModbusPdu::WriteSingleRegister, address, quint16(0x1234), quint16(0x5678)), 1); + QVERIFY(!response.isException()); + QCOMPARE(server.processRequest(QModbusRequest(QModbusPdu::ReadHoldingRegisters, address, quint16(1)), 1).data(), QByteArray::fromHex("0412345678")); + } + const QModbusRequest read(QModbusPdu::ReadHoldingRegisters, quint16(5001), quint16(62)); + QCOMPARE(server.processRequest(read, 1).dataSize(), 249); + const QModbusRequest write(QModbusPdu::WriteMultipleRegisters, QByteArray::fromHex("1389003df4") + QByteArray(244, '\0')); + QVERIFY(!server.processRequest(write, 1).isException()); + const QModbusRequest tooLarge(QModbusPdu::WriteMultipleRegisters, QByteArray::fromHex("1389003ef8") + QByteArray(248, '\0')); + QCOMPARE(server.processRequest(tooLarge, 1).exceptionCode(), QModbusPdu::IllegalDataValue); + const QModbusRequest missing(QModbusPdu::ReadHoldingRegisters, quint16(9000), quint16(1)); + QCOMPARE(server.processRequest(missing, 1).exceptionCode(), QModbusPdu::IllegalDataAddress); + definitions.EnronModbus = false; + server.setDefinitions(definitions); + QCOMPARE(server.processRequest(read, 1).dataSize(), 125); +} + +/// \brief Keeps Enron mode across settings and XML round trips while defaulting legacy projects to standard Modbus. +void TestModbusServer::persistsEnronMode() +{ + QTemporaryDir dir; + QVERIFY(dir.isValid()); + QSettings settings(dir.filePath("settings.ini"), QSettings::IniFormat); + ModbusDefinitions original; + QVERIFY(!original.EnronModbus); + original.EnronModbus = true; + settings << original; + ModbusDefinitions loaded; + settings >> loaded; + QVERIFY(loaded.EnronModbus); + + QByteArray bytes; + QXmlStreamWriter writer(&bytes); + writer << original; + QXmlStreamReader reader(bytes); + QVERIFY(reader.readNextStartElement()); + loaded.EnronModbus = false; + reader >> loaded; + QVERIFY(!reader.hasError()); + QVERIFY(loaded.EnronModbus); + QXmlStreamReader legacy(""); + QVERIFY(legacy.readNextStartElement()); + legacy >> loaded; + QVERIFY(!loaded.EnronModbus); + settings.clear(); + loaded.EnronModbus = true; + settings >> loaded; + QVERIFY(!loaded.EnronModbus); +} + void TestModbusServer::managesAddressesStateAndErrors() { TestableModbusServer server; diff --git a/src/tests/test_modbustransports.cpp b/src/tests/test_modbustransports.cpp index 92c8bb68..4f75d54c 100644 --- a/src/tests/test_modbustransports.cpp +++ b/src/tests/test_modbustransports.cpp @@ -145,6 +145,8 @@ class TestModbusTransports : public QObject Q_OBJECT private slots: + void servesEnronRequests_data(); + void servesEnronRequests(); void servesModbusTcpRequests(); void simulatesModbusTcpErrors(); void rejectsOccupiedTcpPort(); @@ -159,6 +161,53 @@ private slots: void sniffsRtuSerialOnPseudoTerminal(); }; +/// \brief Exercises Enron packet framing on both network transports. +void TestModbusTransports::servesEnronRequests_data() +{ + QTest::addColumn("rtu"); + QTest::newRow("tcp") << false; + QTest::newRow("rtu-over-tcp") << true; +} + +/// \brief Verifies fragmented 32-bit writes followed by coalesced reads on the same connection. +void TestModbusTransports::servesEnronRequests() +{ + QFETCH(bool, rtu); + ModbusTcpServer tcpServer; + ModbusRtuTcpServer rtuServer; + ModbusServer& server = rtu ? static_cast(rtuServer) : static_cast(tcpServer); + const quint16 port = startServer(server); + QVERIFY(port != 0); + ModbusDefinitions definitions; + definitions.EnronModbus = true; + server.setDefinitions(definitions); + ModbusDataUnitMap map; + map.addUnitMap(QUuid::createUuid(), QModbusDataUnit::HoldingRegisters, 7001, 4); + server.setMap(map, 1); + QTcpSocket socket; + socket.connectToHost(QHostAddress::LocalHost, port); + QVERIFY(socket.waitForConnected(1000)); + QModbusRequest write(QModbusPdu::WriteSingleRegister, quint16(7002), quint16(0x3f80), quint16(0)); + const QByteArray frame = rtu ? rtuFrame(1, write) : tcpFrame(1, 1, write); + socket.write(frame.left(frame.size() - 2)); + QVERIFY(socket.waitForBytesWritten(1000)); + QTest::qWait(10); + socket.write(frame.right(2)); + const auto response = receive(socket); + QCOMPARE(response, frame); + QModbusDataUnit stored(QModbusDataUnit::HoldingRegisters, 7001, 4); + QVERIFY(server.data(&stored, 1)); + QCOMPARE(stored.values(), QVector({0, 0, 0x3f80, 0})); + + const QModbusRequest read(QModbusPdu::ReadHoldingRegisters, quint16(7001), quint16(2)); + const auto readFrame = rtu ? rtuFrame(1, read) : tcpFrame(2, 1, read); + socket.write(readFrame + readFrame); + const QByteArray values = QByteArray::fromHex("08000000003f800000"); + const QModbusRequest expected(QModbusPdu::ReadHoldingRegisters, values); + const auto expectedFrame = rtu ? rtuFrame(1, expected) : tcpFrame(2, 1, expected); + QCOMPARE(receive(socket), expectedFrame + expectedFrame); +} + void TestModbusTransports::servesModbusTcpRequests() { ModbusTcpServer server; @@ -651,6 +700,22 @@ void TestModbusTransports::servesRtuSerialOnPseudoTerminal() QVERIFY(receivedSpy.count() >= 8); QVERIFY(sentSpy.count() >= 3); + server.setValue(ModbusServer::ListenOnlyMode, false, 1); + definitions.EnronModbus = true; + server.setDefinitions(definitions); + ModbusDataUnitMap enronMap; + enronMap.addUnitMap(QUuid::createUuid(), QModbusDataUnit::HoldingRegisters, 7001, 2); + server.setMap(enronMap, 1); + const auto enronWrite = rtuFrame(1, QModbusRequest(QModbusPdu::WriteSingleRegister, + quint16(7001), quint16(0x3f80), quint16(0))); + QVERIFY(send(masterFd, enronWrite)); + QCOMPARE(receive(masterFd), enronWrite); + const auto enronRead = rtuFrame(1, QModbusRequest(QModbusPdu::ReadHoldingRegisters, + quint16(7001), quint16(1))); + QVERIFY(send(masterFd, enronRead)); + QCOMPARE(receive(masterFd), rtuFrame(1, QModbusRequest(QModbusPdu::ReadHoldingRegisters, + QByteArray::fromHex("043f800000")))); + server.disconnectDevice(); QCOMPARE(server.state(), QModbusDevice::UnconnectedState); ::close(masterFd); From c31a867eea724a1546d1b508def831572dfdd1ac Mon Sep 17 00:00:00 2001 From: Alexandr Ananev Date: Wed, 4 Nov 2026 21:55:52 +0700 Subject: [PATCH 2/5] chore: Update version to 2.2.0 in CMakeLists and README --- README.md | 16 ++++++++-------- src/CMakeLists.txt | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index df2fe7f2..5eb57c01 100644 --- a/README.md +++ b/README.md @@ -272,14 +272,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. @@ -303,11 +303,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 @@ -324,7 +324,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 @@ -337,7 +337,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 @@ -354,7 +354,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/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) From 92c48c2495285bc08c96168c7e949a7f7e99e90f Mon Sep 17 00:00:00 2001 From: Alexandr Ananev Date: Thu, 5 Nov 2026 09:51:49 +0700 Subject: [PATCH 3/5] feat: Integrate Enron register groups into data views and maps --- README.md | 28 +- README.md.in | 28 +- src/controls/outputdatawidget.cpp | 86 +++-- src/controls/outputdatawidget.h | 6 +- src/controls/pointtypecombobox.cpp | 54 +++ src/controls/pointtypecombobox.h | 14 + src/datamapdatamodel.cpp | 138 +++++-- src/datamapdatamodel.h | 26 +- src/dialogs/dialogmodbusdefinitions.ui | 2 +- src/dialogs/dialogwriteregister.cpp | 36 +- src/dialogs/dialogwriteregister.h | 2 + src/displaydefinition.h | 12 + src/enronmodbus.h | 28 ++ src/formdatamapview.cpp | 139 ++++--- src/formdatamapview.h | 4 + src/formdataview.cpp | 165 ++++---- src/formdataview.h | 7 +- src/formdataview.ui | 2 +- src/legacyprojectparser.cpp | 2 + src/modbuswriteparams.h | 2 + src/res/translations/omodsim_it.ts | 258 +++++++------ src/res/translations/omodsim_ru.qm | Bin 78678 -> 79382 bytes src/res/translations/omodsim_ru.ts | 260 +++++++------ src/res/translations/omodsim_zh_CN.ts | 258 +++++++------ src/res/translations/omodsim_zh_TW.ts | 258 +++++++------ src/tests/test_appproject.cpp | 507 ++++++++++++++++++++++++- src/tests/test_datamapdatamodel.cpp | 102 +++++ 27 files changed, 1740 insertions(+), 684 deletions(-) diff --git a/README.md b/README.md index 5eb57c01..bab6c0df 100644 --- a/README.md +++ b/README.md @@ -50,27 +50,13 @@ The following simulations are available: # Enron Modbus Enable **Enron Modbus (32-bit registers)** in **Modbus Definitions → General**. -The setting applies to all server transports and is saved with the project. -Standard Modbus remains the default for existing projects. - -Enron protocol addresses are used directly: 5001–5999 contain 32-bit integers, -and 7001–7999 contain IEEE 754 floats. Other register addresses retain 16-bit -values within the selected 5/6-digit address space. Requests exceeding that -capacity or crossing a 16/32-bit range boundary are rejected. The address-space -setting does not shift or scale the Enron ranges. -Coils and discrete inputs retain their usual addressing. - -The existing tables and script API store 16-bit words. For either 32-bit range, -Enron address `A` uses table words `B + 2 * (A - B)` and the next word, where -`B` is 5001 or 7001. Select zero-based table addresses. For example, Enron float -7002 uses table words 7003–7004; set them to `0x3F80` and `0x0000` for 1.0. -Use a 32-bit table display format with the high word and high byte first. - -Functions 03, 04, 06, 16 and 23 support these register widths. A single read can -return up to 62 32-bit values; function 16 can write up to 61, and function 23 -can write up to 60. Mask writes and FIFO reads are unavailable for 32-bit -addresses. Enron event logs, historical records and device-specific extensions -are outside this implementation. +The setting is saved with the project. + +Select **Enron Registers 5x** or **7x** 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/README.md.in b/README.md.in index 000029f0..cfaaeb33 100644 --- a/README.md.in +++ b/README.md.in @@ -50,27 +50,13 @@ The following simulations are available: # Enron Modbus Enable **Enron Modbus (32-bit registers)** in **Modbus Definitions → General**. -The setting applies to all server transports and is saved with the project. -Standard Modbus remains the default for existing projects. - -Enron protocol addresses are used directly: 5001–5999 contain 32-bit integers, -and 7001–7999 contain IEEE 754 floats. Other register addresses retain 16-bit -values within the selected 5/6-digit address space. Requests exceeding that -capacity or crossing a 16/32-bit range boundary are rejected. The address-space -setting does not shift or scale the Enron ranges. -Coils and discrete inputs retain their usual addressing. - -The existing tables and script API store 16-bit words. For either 32-bit range, -Enron address `A` uses table words `B + 2 * (A - B)` and the next word, where -`B` is 5001 or 7001. Select zero-based table addresses. For example, Enron float -7002 uses table words 7003–7004; set them to `0x3F80` and `0x0000` for 1.0. -Use a 32-bit table display format with the high word and high byte first. - -Functions 03, 04, 06, 16 and 23 support these register widths. A single read can -return up to 62 32-bit values; function 16 can write up to 61, and function 23 -can write up to 60. Mask writes and FIFO reads are unavailable for 32-bit -addresses. Enron event logs, historical records and device-specific extensions -are outside this implementation. +The setting is saved with the project. + +Select **Enron Registers 5x** or **7x** 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/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..884d8494 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 5x (Read-Write)") : tr("Enron Registers 7x (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 5x (Read-Write)"), QModbusDataUnit::HoldingRegisters); + setItemData(count() - 1, int(EnronModbus::RegisterType::Group5x), Qt::UserRole + 1); + addItem(tr("Enron Registers 7x (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..a2502155 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,61 @@ 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; + 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 5x")) + enron = EnronModbus::RegisterType::Group5x; + else if (value.toString() == tr("Enron Registers 7x")) + 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 +314,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 +329,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 5x") : tr("Enron Registers 7x"); 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 +499,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 +546,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 +594,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 +604,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 +821,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 +864,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 +889,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 +919,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 +958,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 +986,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 +1091,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()); } /// @@ -1042,10 +1128,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 +1152,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.ui b/src/dialogs/dialogmodbusdefinitions.ui index 380d254a..3995840f 100644 --- a/src/dialogs/dialogmodbusdefinitions.ui +++ b/src/dialogs/dialogmodbusdefinitions.ui @@ -61,7 +61,7 @@ - Enron addresses 5001–5999 and 7001–7999 are 32-bit; other addresses are 16-bit. The selected 5/6-digit address space limits the available addresses. Each 32-bit value uses two table words: base + 2 × (address − base), high word first. Use zero-based table addresses. + Select Enron registers 5x or 7x 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 index fd66e84d..ecfd152a 100644 --- a/src/enronmodbus.h +++ b/src/enronmodbus.h @@ -17,6 +17,34 @@ 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. /// diff --git a/src/formdatamapview.cpp b/src/formdatamapview.cpp index 666ce7de..1ed0d33d 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 5x")); + _filterTypeCombo->addItem(tr("Enron Registers 7x")); + } + 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..add73664 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(); } /// @@ -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/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.ts b/src/res/translations/omodsim_it.ts index 3e7947b8..728dd5e3 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,59 @@ Registri blocco - + + + Enron Registers 5x + + + + + + Enron Registers 7x + + + + 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 +427,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) @@ -1071,7 +1083,7 @@ Vuoi aprire la pagina di download? - Enron addresses 5001–5999 and 7001–7999 are 32-bit; other addresses are 16-bit. The selected 5/6-digit address space limits the available addresses. Each 32-bit value uses two table words: base + 2 × (address − base), high word first. Use zero-based table addresses. + Select Enron registers 5x or 7x and enter the full protocol address, starting at 5001 or 7001. Select the display format separately in the toolbar. @@ -1733,6 +1745,16 @@ Vuoi aprire la pagina di download? DialogWriteRegister + + + Address (low word): + + + + + Address (high word): + + Write Register @@ -1755,7 +1777,7 @@ Vuoi aprire la pagina di download? - + Auto Simulation: OFF Simulazione automatica: OFF @@ -1765,18 +1787,18 @@ Vuoi aprire la pagina di download? Modello bit: - + Value, (HEX): Valore, (ESA): - + Value, (ANSI): Valore, (ANSI): - - + + Auto Simulation: ON Simulazione automatica: ON @@ -1926,74 +1948,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 @@ -2002,58 +2023,81 @@ Filtro unità: %2 Tipo filtro: %3 - - + Coils Bobine - - + Discrete Inputs Ingressi discreti - - + Input Registers Registri ingresso - - + Holding Registers Registri blocco - - + + Enron Registers 5x + + + + + Enron Registers 7x + + + + + Unit: Unità: - - + + Data Type: Tipo di dati: - - - + + + All Tutto FormDataView + + + Register Type: + + + + + Enron Address: + + + + + Register Count: + + + Starting Address: Indirizzo iniziale: + Length: Lunghezza: @@ -2067,11 +2111,6 @@ Tipo filtro: %3 Column Distance: Distanza colonna: - - - Data Type: - Tipo di dati: - Leading Zeros for Unsigned @@ -2202,17 +2241,17 @@ Tipo filtro: %3 Scambia byte (0xAB → 0xBA) - + 0-based base 0 - + 1-based base 1 - + Address Base: %1 Starting Address: %2 Length: %3 @@ -2221,7 +2260,7 @@ Indirizzo iniziale: %2 Lunghezza: %3 - + Unit Identifier: %1 Data Type: %2 @@ -3801,67 +3840,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% @@ -3900,26 +3939,38 @@ Verifica attentamente il risultato dell'unione. PointTypeComboBox - + + Enron Registers 5x (Read-Write) + + + + + + Enron Registers 7x (Read-Write) + + + + + 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) @@ -4167,12 +4218,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 @@ -4382,29 +4433,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 8cfda6858de99357e27998539ed1e41ee9b7ddef..2eacbc75784661e6ccd6f48ae7efde3cb054d65f 100644 GIT binary patch delta 7364 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 diff --git a/src/res/translations/omodsim_ru.ts b/src/res/translations/omodsim_ru.ts index 67945554..37873b98 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,122 @@ Регистры хранения - + Unit Устройство - + Type Тип - + Address Адрес - + Data Type Тип данных - + Register Order Порядок регистров - + Byte Order Порядок байт - + Comment Комментарий - + Value Значение - + Timestamp Время + + + + Enron Registers 5x + Регистры Enron 5x + + + + + Enron Registers 7x + Регистры Enron 7x + 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 регистра) @@ -1051,8 +1063,8 @@ Open the download page? - Enron addresses 5001–5999 and 7001–7999 are 32-bit; other addresses are 16-bit. The selected 5/6-digit address space limits the available addresses. Each 32-bit value uses two table words: base + 2 × (address − base), high word first. Use zero-based table addresses. - Адреса Enron 5001–5999 и 7001–7999 — 32-битные, остальные — 16-битные. Доступные адреса ограничены выбранным 5/6-значным адресным пространством. Каждое 32-битное значение занимает два слова таблицы: база + 2 × (адрес − база), старшее слово первым. Используйте адреса таблицы с нуля. + Select Enron registers 5x or 7x and enter the full protocol address, starting at 5001 or 7001. Select the display format separately in the toolbar. + Выберите регистры Enron 5x или 7x и задавайте полный адрес протокола, начиная с 5001 или 7001. Формат отображения выбирайте отдельно на панели инструментов. @@ -1733,6 +1745,16 @@ Open the download page? DialogWriteRegister + + + Address (low word): + Адрес (младшее слово): + + + + Address (high word): + Адрес (старшее слово): + Write Register @@ -1755,7 +1777,7 @@ Open the download page? - + Auto Simulation: OFF Симуляция: Выкл @@ -1765,18 +1787,18 @@ Open the download page? Битовый шаблон: - + Value, (HEX): Значение, (HEX): - + Value, (ANSI): Значение, (ANSI): - - + + Auto Simulation: ON Симуляция: Вкл @@ -1926,74 +1948,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 @@ -2002,58 +2023,81 @@ Type Filter: %3 Фильтр типов: %3 - - + Coils Регистры флагов - - + Discrete Inputs Дискретные входы - - + Input Registers Регистры ввода - - + Holding Registers Регистры хранения - - + + Unit: Устройство: - - + + Data Type: Тип данных: - - - + + + All Все + + + Enron Registers 5x + Регистры Enron 5x + + + + Enron Registers 7x + Регистры Enron 7x + FormDataView + + + Register Type: + Тип регистров: + + + + Enron Address: + Адрес Enron: + + + + Register Count: + Количество регистров: + + Starting Address: Начальный адрес: + Length: Количество: @@ -2067,11 +2111,6 @@ Type Filter: %3 Column Distance: Расстояние между столбцами: - - - Data Type: - Тип данных: - Leading Zeros for Unsigned @@ -2202,17 +2241,17 @@ Type Filter: %3 Перестановка байт (0xAB → 0xBA) - + 0-based 0-первый - + 1-based 1-первый - + Address Base: %1 Starting Address: %2 Length: %3 @@ -2221,7 +2260,7 @@ Length: %3 Длина: %3 - + Unit Identifier: %1 Data Type: %2 @@ -3801,67 +3840,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% @@ -3900,26 +3939,38 @@ Please verify the merge result carefully. PointTypeComboBox - + + Enron Registers 5x (Read-Write) + Регистры Enron 5x (чтение-запись) + + + + + Enron Registers 7x (Read-Write) + Регистры Enron 7x (чтение-запись) + + + + Coils (Read-Write) Регистры флагов (чтение-запись) - + Discrete Inputs (Read Only) Дискретные входы (чтение) - + Holding Registers (Read-Write) Регистры хранения (чтение-запись) - + Input Registers (Read Only) Регистры ввода (чтение) @@ -4167,12 +4218,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 @@ -4382,29 +4433,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.ts b/src/res/translations/omodsim_zh_CN.ts index 54bd5b74..5eff6368 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,59 @@ 保持寄存器 - + + + Enron Registers 5x + + + + + + Enron Registers 7x + + + + Unit 单元 - + Type 类型 - + Address 地址 - + Data Type 数据类型 - + Register Order 寄存器顺序 - + Byte Order 字节顺序 - + Comment 注释 - + Value 值 - + Timestamp 时间戳 @@ -415,62 +427,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个寄存器) @@ -1071,7 +1083,7 @@ Open the download page? - Enron addresses 5001–5999 and 7001–7999 are 32-bit; other addresses are 16-bit. The selected 5/6-digit address space limits the available addresses. Each 32-bit value uses two table words: base + 2 × (address − base), high word first. Use zero-based table addresses. + Select Enron registers 5x or 7x and enter the full protocol address, starting at 5001 or 7001. Select the display format separately in the toolbar. @@ -1733,6 +1745,16 @@ Open the download page? DialogWriteRegister + + + Address (low word): + + + + + Address (high word): + + Write Register @@ -1755,7 +1777,7 @@ Open the download page? - + Auto Simulation: OFF 自动模拟:关闭 @@ -1765,18 +1787,18 @@ Open the download page? 位模式: - + Value, (HEX): 值(十六进制): - + Value, (ANSI): 值,(ANSI): - - + + Auto Simulation: ON 自动模拟:开启 @@ -1926,74 +1948,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 @@ -2002,58 +2023,81 @@ Type Filter: %3 类型过滤器:%3 - - + Coils 线圈 - - + Discrete Inputs 离散输入 - - + Input Registers 输入寄存器 - - + Holding Registers 保持寄存器 - - + + Enron Registers 5x + + + + + Enron Registers 7x + + + + + Unit: 单元: - - + + Data Type: 数据类型: - - - + + + All 全部 FormDataView + + + Register Type: + + + + + Enron Address: + + + + + Register Count: + + + Starting Address: 起始地址: + Length: 长度: @@ -2067,11 +2111,6 @@ Type Filter: %3 Column Distance: 列距: - - - Data Type: - 数据类型: - Leading Zeros for Unsigned @@ -2202,17 +2241,17 @@ Type Filter: %3 交换字节(0xAB → 0xBA) - + 0-based 基地址为-0 - + 1-based 基地址为-1 - + Address Base: %1 Starting Address: %2 Length: %3 @@ -2221,7 +2260,7 @@ Length: %3 长度:%3 - + Unit Identifier: %1 Data Type: %2 @@ -3802,67 +3841,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% @@ -3901,26 +3940,38 @@ Please verify the merge result carefully. PointTypeComboBox - + + Enron Registers 5x (Read-Write) + + + + + + Enron Registers 7x (Read-Write) + + + + + Coils (Read-Write) 线圈(读写) - + Discrete Inputs (Read Only) 离散输入(只读) - + Holding Registers (Read-Write) 保持寄存器(读写) - + Input Registers (Read Only) 输入寄存器(只读) @@ -4168,12 +4219,12 @@ Please verify the merge result carefully. RegistryOrderItemDelegate - + Most Significant Register First - big-endian word order 高位寄存器优先 - 大端字节序 - + Least Significant Register First - little-endian word order 低位寄存器优先 - 小端字节序 @@ -4383,29 +4434,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.ts b/src/res/translations/omodsim_zh_TW.ts index 0046a353..1f8c0243 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,59 @@ 保持暫存器 - + + + Enron Registers 5x + + + + + + Enron Registers 7x + + + + Unit 單元 - + Type 類型 - + Address 地址 - + Data Type 資料類型 - + Register Order 暫存器順序 - + Byte Order 位元組順序 - + Comment 注釋 - + Value 值 - + Timestamp 時間戳記 @@ -415,62 +427,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個暫存器) @@ -1071,7 +1083,7 @@ Open the download page? - Enron addresses 5001–5999 and 7001–7999 are 32-bit; other addresses are 16-bit. The selected 5/6-digit address space limits the available addresses. Each 32-bit value uses two table words: base + 2 × (address − base), high word first. Use zero-based table addresses. + Select Enron registers 5x or 7x and enter the full protocol address, starting at 5001 or 7001. Select the display format separately in the toolbar. @@ -1733,6 +1745,16 @@ Open the download page? DialogWriteRegister + + + Address (low word): + + + + + Address (high word): + + Write Register @@ -1755,7 +1777,7 @@ Open the download page? - + Auto Simulation: OFF 自動模擬:關閉 @@ -1765,18 +1787,18 @@ Open the download page? 位元模式: - + Value, (HEX): 值(十六進位): - + Value, (ANSI): 值,(ANSI): - - + + Auto Simulation: ON 自動模擬:開啟 @@ -1926,74 +1948,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 @@ -2002,58 +2023,81 @@ Type Filter: %3 類型過濾器:%3 - - + Coils 線圈 - - + Discrete Inputs 離散輸入 - - + Input Registers 輸入暫存器 - - + Holding Registers 保持暫存器 - - + + Enron Registers 5x + + + + + Enron Registers 7x + + + + + Unit: 單元: - - + + Data Type: 資料類型: - - - + + + All 全部 FormDataView + + + Register Type: + + + + + Enron Address: + + + + + Register Count: + + + Starting Address: 起始地址: + Length: 長度: @@ -2067,11 +2111,6 @@ Type Filter: %3 Column Distance: 欄距: - - - Data Type: - 資料類型: - Leading Zeros for Unsigned @@ -2202,17 +2241,17 @@ Type Filter: %3 交換位元組(0xAB → 0xBA) - + 0-based 基地址為-0 - + 1-based 基地址為-1 - + Address Base: %1 Starting Address: %2 Length: %3 @@ -2221,7 +2260,7 @@ Length: %3 長度:%3 - + Unit Identifier: %1 Data Type: %2 @@ -3802,67 +3841,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% @@ -3901,26 +3940,38 @@ Please verify the merge result carefully. PointTypeComboBox - + + Enron Registers 5x (Read-Write) + + + + + + Enron Registers 7x (Read-Write) + + + + + Coils (Read-Write) 線圈(讀寫) - + Discrete Inputs (Read Only) 離散輸入(唯讀) - + Holding Registers (Read-Write) 保持暫存器(讀寫) - + Input Registers (Read Only) 輸入暫存器(唯讀) @@ -4168,12 +4219,12 @@ Please verify the merge result carefully. RegistryOrderItemDelegate - + Most Significant Register First - big-endian word order 高位暫存器優先 - 大端字節序 - + Least Significant Register First - little-endian word order 低位暫存器優先 - 小端字節序 @@ -4383,29 +4434,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 5bb022a4..13127cb4 100644 --- a/src/tests/test_appproject.cpp +++ b/src/tests/test_appproject.cpp @@ -7,6 +7,11 @@ /// #include +#include +#include +#include +#include +#include #include #include #include @@ -19,10 +24,15 @@ #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" @@ -32,6 +42,17 @@ class TestAppProject : public QObject Q_OBJECT private slots: + 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(); @@ -200,7 +221,491 @@ QStringList commentPlacements(const QString& path) } -/// \brief Verifies creation, UUID assignment and per-kind display counters. +/// \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); + 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); + combo.setEnronEnabled(false); + combo.setEnronEnabled(false); + QCOMPARE(combo.count(), 4); + QCOMPARE(combo.currentPointType(), QModbusDataUnit::HoldingRegisters); + QCOMPARE(combo.currentEnronType(), EnronModbus::RegisterType::None); + QCOMPARE(combo.findData(int(mode), Qt::UserRole + 1), -1); + } +} + +/// \brief Rebuilds visible rows across display widths without changing the stored range. +void TestAppProject::compactsEnronRowsAcrossFormatChanges() +{ + ProjectFixture fixture; + auto definitions = fixture.Server.getModbusDefinitions(); + definitions.EnronModbus = true; + fixture.Server.setModbusDefinitions(definitions); + auto* form = qobject_cast(fixture.Project.createMdiChild(ProjectFormKind::Data)); + QVERIFY(form); + form->findChild("comboBoxModbusPointType")->setCurrentEnronType(EnronModbus::RegisterType::Group5x); + form->findChild("lineEditLength")->setValue(3); + auto* output = form->findChild("outputWidget"); + auto* view = output->findChild("listView"); + QCOMPARE(form->displayDefinition().Length, quint16(6)); + QCOMPARE(view->model()->rowCount(), 3); + form->setDataType(DataType::UInt64); + QCOMPARE(view->model()->rowCount(), 2); + QVERIFY(view->model()->data(view->model()->index(1, 0), CaptureRole).toString().trimmed().isEmpty()); + form->setDataType(DataType::UInt16); + QCOMPARE(view->model()->rowCount(), 6); + form->setDataType(DataType::UInt32); + QCOMPARE(view->model()->rowCount(), 3); + QCOMPARE(form->displayDefinition().Length, quint16(6)); + QCOMPARE(form->data().size(), 6); + definitions.EnronModbus = false; + fixture.Server.setModbusDefinitions(definitions); + QTRY_COMPARE(form->displayDefinition().EnronType, EnronModbus::RegisterType::None); + QCOMPARE(view->model()->rowCount(), 6); +} + +/// \brief Covers every display action independently of the selected Enron group. +void TestAppProject::enronDisplayFormats_data() +{ + QTest::addColumn("group"); + QTest::addColumn("type"); + QTest::addColumn("action"); + const QList> formats = { + {DataType::Binary, "actionDisplayBinary"}, {DataType::Hex, "actionDisplayHex"}, + {DataType::Ansi, "actionDisplayAnsi"}, {DataType::Int16, "actionDisplayInt16"}, + {DataType::UInt16, "actionDisplayUInt16"}, {DataType::Int32, "actionDisplayInt32"}, + {DataType::UInt32, "actionDisplayUInt32"}, {DataType::Int64, "actionDisplayInt64"}, + {DataType::UInt64, "actionDisplayUInt64"}, {DataType::Float32, "actionDisplayFloat32"}, + {DataType::Float64, "actionDisplayFloat64"} + }; + for (int group : {1, 2}) { + for (const auto& format : formats) { + const auto name = QString("%1-%2").arg(group).arg(format.second); + QTest::newRow(qPrintable(name)) << group << format.first << format.second; + } + } +} + +/// \brief Reinterprets shared register bits without changing their group, address or storage. +void TestAppProject::enronDisplayFormats() +{ + QFETCH(int, group); + QFETCH(DataType, type); + QFETCH(QString, action); + auto& prefs = AppPreferences::instance(); + const bool previousHex = prefs.globalHexView(); + const auto restoreHex = qScopeGuard([&prefs, previousHex] { prefs.setGlobalHexView(previousHex); }); + prefs.setGlobalHexView(false); + ProjectFixture fixture; + auto definitions = fixture.Server.getModbusDefinitions(); + definitions.EnronModbus = true; + fixture.Server.setModbusDefinitions(definitions); + auto* form = qobject_cast(fixture.Project.createMdiChild(ProjectFormKind::Data)); + QVERIFY(form); + form->setAddressBase(AddressBase::Base0); + auto* combo = form->findChild("comboBoxModbusPointType"); + const auto enron = static_cast(group); + combo->setCurrentEnronType(enron); + form->findChild("lineEditLength")->setValue(2); + auto* displayAction = form->findChild(action); + QVERIFY(displayAction); + QVERIFY(displayAction->isEnabled()); + displayAction->trigger(); + QCOMPARE(form->dataType(), type); + QCOMPARE(form->displayDefinition().EnronType, enron); + QCOMPARE(form->findChild("lineEditAddress")->value(), int(EnronModbus::baseAddress(enron))); + const QVector words{0x3f80, 0, 0x4000, 0}; + fixture.Server.setData(1, QModbusDataUnit(QModbusDataUnit::HoldingRegisters, + EnronModbus::baseAddress(enron), words), WriteSource::User); + auto* model = form->findChild(); + QVERIFY(model); + QTRY_COMPARE(form->data(), words); + auto* output = form->findChild("outputWidget"); + auto* view = output->findChild("listView"); + QVERIFY(view); + const int visibleRows = 4 / registersCount(type); + QCOMPARE(view->model()->rowCount(), visibleRows); + QCOMPARE(output->rowCount(), visibleRows); + QSet visibleAddresses; + for (int row = 0; row < visibleRows; ++row) + visibleAddresses.insert(view->model()->data(view->model()->index(row, 0), AddressStringRole).toString()); + QCOMPARE(visibleAddresses.size(), visibleRows); + QImage page(1000, 1000, QImage::Format_ARGB32); + QPainter painter(&page); + QCOMPARE(output->paint(page.rect(), painter, 0), visibleRows); + const auto index = model->index(0); + QVERIFY(!model->data(index, CaptureRole).toString().trimmed().isEmpty()); + const QString protocolAddress = QString::number(EnronModbus::baseAddress(enron)); + const bool wordFormat = registersCount(type) == 1; + QCOMPARE(model->data(index, AddressStringRole).toString(), protocolAddress + + (wordFormat ? QStringLiteral(".H") : QString())); + if (wordFormat) { + QCOMPARE(model->data(model->index(1), AddressStringRole).toString(), protocolAddress + QStringLiteral(".L")); + QCOMPARE(model->data(index, ValueRole).toUInt(), uint(0x3f80)); + } else if (type == DataType::Float32) { + QCOMPARE(model->data(index, ValueRole).toFloat(), 1.0f); + QCOMPARE(model->data(model->index(2), ValueRole).toFloat(), 2.0f); + } else if (type == DataType::Float64) { + QCOMPARE(model->data(index, ValueRole).toDouble(), 0x1.000004p-7); + } else if (registersCount(type) == 2) { + QCOMPARE(model->data(index, ValueRole).toUInt(), uint(0x3f800000)); + } else { + QCOMPARE(model->data(index, ValueRole).toULongLong(), qulonglong(0x3f80000040000000)); + } + if (type == DataType::Float32) { + output->applyFindByValue(QStringLiteral("2")); + QVERIFY(output->findNextByValue(QStringLiteral("2"))); + QCOMPARE(view->currentIndex().row(), 1); + output->clearFindByValue(); + } + QObject::disconnect(output, &OutputDataWidget::itemDoubleClicked, nullptr, nullptr); + QSignalSpy editSpy(output, &OutputDataWidget::itemDoubleClicked); + QVERIFY(QMetaObject::invokeMethod(output, "on_listView_doubleClicked", Qt::DirectConnection, + Q_ARG(QModelIndex, view->model()->index(visibleRows - 1, 0)))); + QCOMPARE(editSpy.count(), 1); + QCOMPARE(editSpy.first().at(0).toUInt(), uint(EnronModbus::baseAddress(enron) + + (visibleRows - 1) * registersCount(type))); + if (type == DataType::Float32) + QCOMPARE(editSpy.first().at(1).toFloat(), 2.0f); + form->setRegisterOrder(RegisterOrder::LSRF); + QCOMPARE(form->registerOrder(), RegisterOrder::LSRF); + form->setByteOrder(ByteOrder::Swapped); + QCOMPARE(form->byteOrder(), ByteOrder::Swapped); + prefs.setGlobalHexView(true); + QTRY_COMPARE(model->data(index, AddressStringRole).toString(), QStringLiteral("0x%1").arg(EnronModbus::baseAddress(enron), 4, 16, QLatin1Char('0')).toUpper() + + (wordFormat ? QStringLiteral(".H") : QString())); + QCOMPARE(form->findChild("lineEditAddress")->value(), int(EnronModbus::baseAddress(enron))); +} + +/// \brief Exercises logical register input for both Enron groups and address bases. +void TestAppProject::enronRegisterChoices_data() +{ + QTest::addColumn("mode"); + QTest::addColumn("zeroBased"); + for (int mode : {1, 2}) { + QTest::newRow(mode == 1 ? "int-base0" : "float-base0") << mode << true; + QTest::newRow(mode == 1 ? "int-base1" : "float-base1") << mode << false; + } +} + +/// \brief Keeps UI counts and labels logical while editing the correct backing words. +void TestAppProject::enronRegisterChoices() +{ + QFETCH(int, mode); + QFETCH(bool, zeroBased); + auto& prefs = AppPreferences::instance(); + const bool previousHex = prefs.globalHexView(); + const auto restoreHex = qScopeGuard([&prefs, previousHex] { prefs.setGlobalHexView(previousHex); }); + prefs.setGlobalHexView(false); + ProjectFixture fixture; + auto* form = qobject_cast(fixture.Project.createMdiChild(ProjectFormKind::Data)); + QVERIFY(form); + form->setAddressBase(zeroBased ? AddressBase::Base0 : AddressBase::Base1); + auto* combo = form->findChild("comboBoxModbusPointType"); + auto* address = form->findChild("lineEditAddress"); + auto* length = form->findChild("lineEditLength"); + QVERIFY(combo); + const auto enron = static_cast(mode); + QCOMPARE(combo->count(), 4); + QCOMPARE(combo->findData(mode, Qt::UserRole + 1), -1); + auto definitions = fixture.Server.getModbusDefinitions(); + definitions.EnronModbus = true; + fixture.Server.setModbusDefinitions(definitions); + QTRY_COMPARE(combo->count(), 6); + QVERIFY(combo->findData(mode, Qt::UserRole + 1) >= 0); + combo->setCurrentEnronType(enron); + const int base = EnronModbus::baseAddress(enron); + QCOMPARE(address->value(), base); + QCOMPARE(address->range().from(), base); + address->setValue(base + 1); + length->setValue(2); + const auto dd = form->displayDefinition(); + QCOMPARE(dd.EnronType, enron); + QCOMPARE(dd.PointAddress, quint16(base + 2 + (zeroBased ? 0 : 1))); + QCOMPARE(dd.Length, quint16(4)); + QCOMPARE(form->dataType(), mode == 1 ? DataType::Int32 : DataType::Float32); + form->setDataType(DataType::UInt16); + QCOMPARE(form->dataType(), DataType::UInt16); + form->setByteOrder(ByteOrder::Swapped); + QCOMPARE(form->byteOrder(), ByteOrder::Swapped); + form->setRegisterOrder(RegisterOrder::LSRF); + QCOMPARE(form->registerOrder(), RegisterOrder::LSRF); + form->setDataType(DataType::UInt32); + auto* model = form->findChild(); + QVERIFY(model); + QCOMPARE(model->data(model->index(0), AddressStringRole).toString(), QString::number(base + 1)); + QCOMPARE(model->data(model->index(2), AddressStringRole).toString(), QString::number(base + 2)); + QSignalSpy definitionSpy(form, &FormDataView::definitionChanged); + definitions.AddrSpace = AddressSpace::Addr5Digits; + fixture.Server.setModbusDefinitions(definitions); + QTRY_VERIFY(!definitionSpy.isEmpty()); + QCOMPARE(model->data(model->index(0), AddressStringRole).toString(), QString::number(base + 1)); + QCOMPARE(form->dataType(), DataType::UInt32); + QCOMPARE(form->byteOrder(), ByteOrder::Swapped); + QCOMPARE(form->registerOrder(), RegisterOrder::LSRF); + QCOMPARE(model->data(model->index(2), AddressRole).toUInt(), uint(base + 4 + (zeroBased ? 0 : 1))); + + form->setAddressBase(zeroBased ? AddressBase::Base1 : AddressBase::Base0); + QCOMPARE(address->value(), base + 1); + form->setAddressBase(zeroBased ? AddressBase::Base0 : AddressBase::Base1); + address->setValue(base + EnronModbus::RangeLength - 1); + QCOMPARE(length->range().to(), 1); + QCOMPARE(length->value(), 1); + QCOMPARE(form->displayDefinition().Length, quint16(2)); + + definitions.EnronModbus = false; + fixture.Server.setModbusDefinitions(definitions); + QTRY_COMPARE(form->displayDefinition().EnronType, EnronModbus::RegisterType::None); + QCOMPARE(combo->count(), 4); + QCOMPARE(combo->findData(mode, Qt::UserRole + 1), -1); + QCOMPARE(form->displayDefinition().Length, quint16(2)); + QCOMPARE(combo->currentPointType(), QModbusDataUnit::HoldingRegisters); +} + +/// \brief Preserves the Enron selection and logical range in project and settings serialization. +void TestAppProject::roundTripsEnronRegisterChoices() +{ + ProjectFixture fixture; + auto definitions = fixture.Server.getModbusDefinitions(); + definitions.EnronModbus = true; + fixture.Server.setModbusDefinitions(definitions); + auto* form = qobject_cast(fixture.Project.createMdiChild(ProjectFormKind::Data)); + QVERIFY(form); + form->setAddressBase(AddressBase::Base0); + form->findChild("comboBoxModbusPointType")->setCurrentEnronType(EnronModbus::RegisterType::Group7x); + form->findChild("lineEditAddress")->setValue(7002); + form->findChild("lineEditLength")->setValue(2); + form->setDataType(DataType::UInt32); + form->setRegisterOrder(RegisterOrder::LSRF); + form->setByteOrder(ByteOrder::Swapped); + const auto expected = form->displayDefinition(); + QTemporaryDir files; + QSettings settings(files.filePath("view.ini"), QSettings::IniFormat); + settings << expected; + DataViewDefinitions restored; + settings >> restored; + QCOMPARE(restored, expected); + QSettings viewSettings(files.filePath("form.ini"), QSettings::IniFormat); + form->saveSettings(viewSettings); + auto* settingsView = qobject_cast(fixture.Project.createMdiChild(ProjectFormKind::Data)); + QVERIFY(settingsView); + settingsView->loadSettings(viewSettings); + QCOMPARE(settingsView->dataType(), DataType::UInt32); + QCOMPARE(settingsView->registerOrder(), RegisterOrder::LSRF); + QCOMPARE(settingsView->byteOrder(), ByteOrder::Swapped); + fixture.Project.deleteForm(settingsView); + const auto saved = files.filePath("enron-view.omsim"); + QVERIFY(fixture.Project.saveProject(saved)); + fixture.Project.closeProject(); + definitions.EnronModbus = false; + fixture.Server.setModbusDefinitions(definitions); + QVERIFY(fixture.Project.loadProject(saved).Success); + const auto forms = fixture.Project.forms(ProjectFormKind::Data); + QCOMPARE(forms.size(), 1); + auto* loaded = qobject_cast(forms.first()); + QVERIFY(loaded); + QCOMPARE(loaded->displayDefinition().EnronType, EnronModbus::RegisterType::Group7x); + QCOMPARE(loaded->displayDefinition().Length, quint16(4)); + QCOMPARE(loaded->findChild("lineEditAddress")->value(), 7002); + QCOMPARE(loaded->findChild("lineEditLength")->value(), 2); + QCOMPARE(loaded->dataType(), DataType::UInt32); + QCOMPARE(loaded->registerOrder(), RegisterOrder::LSRF); + QCOMPARE(loaded->byteOrder(), ByteOrder::Swapped); + auto* combo = loaded->findChild("comboBoxModbusPointType"); + QCOMPARE(combo->count(), 4); + // Project settings are applied after the data views have been constructed. + definitions.EnronModbus = true; + fixture.Server.setModbusDefinitions(definitions); + QTRY_COMPARE(combo->currentEnronType(), EnronModbus::RegisterType::Group7x); + QCOMPARE(combo->count(), 6); + QCOMPARE(loaded->findChild("lineEditAddress")->value(), 7002); +} + +/// \brief Converts a logical address edited in the write dialog back to an independent word pair. +void TestAppProject::editsEnronRegisterByLogicalAddress() +{ + ModbusMultiServer server; + server.addUnitMap(QUuid::createUuid(), 1, QModbusDataUnit::HoldingRegisters, 7001, 6); + ModbusWriteParams params; + params.DeviceId = 1; + params.Address = 7004; + params.ZeroBasedAddress = false; + params.DataMode = DataType::Float32; + params.Value = 1.0f; + params.Order = ByteOrder::Direct; + params.EnronType = EnronModbus::RegisterType::Group7x; + params.Server = &server; + DialogWriteRegister dialog(params, QModbusDataUnit::HoldingRegisters, false, nullptr, nullptr); + auto* address = dialog.findChild("lineEditAddress"); + QCOMPARE(address->value(), 7002); + address->setValue(7003); + dialog.findChild("lineEditValue")->setValue(2.0f); + dialog.accept(); + QCOMPARE(params.Address, quint16(7006)); + server.writeRegister(QModbusDataUnit::HoldingRegisters, params); + QCOMPARE(server.data(1, QModbusDataUnit::HoldingRegisters, 7001, 6).values(), + QVector({0, 0, 0, 0, 0x4000, 0})); + params.DataMode = DataType::UInt16; + params.Address = 7003; + params.Value = 0; + DialogWriteRegister lowWord(params, QModbusDataUnit::HoldingRegisters, false, nullptr, nullptr); + auto* lowAddress = lowWord.findChild("lineEditAddress"); + QCOMPARE(lowAddress->value(), 7001); + lowAddress->setValue(7002); + lowWord.findChild("lineEditValue")->setValue(0xabcd); + lowWord.accept(); + QCOMPARE(params.Address, quint16(7005)); + server.writeRegister(QModbusDataUnit::HoldingRegisters, params); + QCOMPARE(server.data(1, QModbusDataUnit::HoldingRegisters, 7001, 6).values(), + QVector({0, 0, 0, 0xabcd, 0x4000, 0})); +} + /// \brief Applies Enron mode through server settings and preserves it when edits are cancelled. void TestAppProject::configuresEnronMode() { diff --git a/src/tests/test_datamapdatamodel.cpp b/src/tests/test_datamapdatamodel.cpp index 9cae7f6a..37049d3a 100644 --- a/src/tests/test_datamapdatamodel.cpp +++ b/src/tests/test_datamapdatamodel.cpp @@ -47,6 +47,9 @@ class TestDataMapDataModel : public QObject Q_OBJECT private slots: + void editsEnronRegisters_data(); + void editsEnronRegisters(); + void filtersEnronGroups(); void removingRowReleasesDeviceId(); void removingRowAfterDataTypeChangeReleasesDeviceId(); void removingDuplicateKeyRowsReleasesDeviceId(); @@ -56,6 +59,105 @@ private slots: void removingRowKeepsDataCoveredByDataViewRange(); }; +/// \brief Covers both Enron groups independently of the ordinary address base. +void TestDataMapDataModel::editsEnronRegisters_data() +{ + QTest::addColumn("group"); + QTest::addColumn("zeroBased"); + QTest::newRow("5x-base0") << 1 << true; + QTest::newRow("5x-base1") << 1 << false; + QTest::newRow("7x-base0") << 2 << true; + QTest::newRow("7x-base1") << 2 << false; +} + +/// \brief Keeps protocol addresses, native widths and values consistent across edits and protocol toggles. +void TestDataMapDataModel::editsEnronRegisters() +{ + QFETCH(int, group); + QFETCH(bool, zeroBased); + ModbusMultiServer server; + auto definitions = server.getModbusDefinitions(); + definitions.EnronModbus = true; + server.setModbusDefinitions(definitions); + DataMapDataModel model(server); + model.setZeroBased(zeroBased); + model.addEntry(holdingKey(0), {}); + const auto enron = static_cast(group); + const int base = EnronModbus::baseAddress(enron); + const auto typeIndex = model.index(0, ColType); + QVERIFY(model.setData(typeIndex, group, DataMapRole::EnronType)); + QCOMPARE(model.data(typeIndex, DataMapRole::EnronType).toInt(), group); + QCOMPARE(model.keyForRow(0).Type, QModbusDataUnit::HoldingRegisters); + QCOMPARE(model.keyForRow(0).Address, quint16(base)); + QCOMPARE(model.data(model.index(0, ColAddress)).toInt(), base); + QVERIFY(server.data(1, QModbusDataUnit::HoldingRegisters, base, 2).isValid()); + QVERIFY(model.setData(model.index(0, ColAddress), QString::number(base + 1))); + QCOMPARE(model.keyForRow(0).Address, quint16(base + 2)); + QCOMPARE(model.data(model.index(0, ColAddress)).toInt(), base + 1); + QVERIFY(model.setData(model.index(0, ColDataType), "UInt32")); + QVERIFY(model.setData(model.index(0, ColValue), uint(0x12345678))); + QCOMPARE(server.data(1, QModbusDataUnit::HoldingRegisters, base + 2, 2).values(), + QVector({0x1234, 0x5678})); + QVERIFY(model.setData(model.index(0, ColDataType), "UInt16")); + QVERIFY(server.data(1, QModbusDataUnit::HoldingRegisters, base + 2, 2).isValid()); + QVERIFY(!model.setData(model.index(0, ColAddress), QString::number(base - 1))); + QVERIFY(!model.setData(model.index(0, ColAddress), QString::number(base + 999))); + QVERIFY(!model.setData(model.index(0, ColAddress), "invalid")); + QVERIFY(!model.setData(typeIndex, 99, DataMapRole::EnronType)); + QCOMPARE(model.keyForRow(0).Address, quint16(base + 2)); + QCOMPARE(server.data(1, QModbusDataUnit::HoldingRegisters, base + 2, 2).values(), + QVector({0x1234, 0x5678})); + definitions.EnronModbus = false; + server.setModbusDefinitions(definitions); + QTRY_VERIFY(!model.data(typeIndex, DataMapRole::EnronEnabled).toBool()); + QCOMPARE(model.data(typeIndex, DataMapRole::EnronType).toInt(), 0); + QVERIFY(!model.setData(typeIndex, group, DataMapRole::EnronType)); + definitions.EnronModbus = true; + server.setModbusDefinitions(definitions); + QTRY_COMPARE(model.data(typeIndex, DataMapRole::EnronType).toInt(), group); + QCOMPARE(model.data(model.index(0, ColAddress)).toInt(), base + 1); + QCOMPARE(model.data(model.index(0, ColDataType)).toString(), QStringLiteral("UInt16")); + QCOMPARE(server.data(1, QModbusDataUnit::HoldingRegisters, base + 2, 2).values(), + QVector({0x1234, 0x5678})); + QVERIFY(model.setData(typeIndex, int(QModbusDataUnit::HoldingRegisters), DataMapRole::TypeValue)); + QCOMPARE(model.data(typeIndex, DataMapRole::EnronType).toInt(), 0); + QCOMPARE(model.data(model.index(0, ColAddress)).toInt(), base + 2 + (zeroBased ? 0 : 1)); +} + +/// \brief Filters Enron groups separately while retaining ordinary table filters. +void TestDataMapDataModel::filtersEnronGroups() +{ + ModbusMultiServer server; + auto definitions = server.getModbusDefinitions(); + definitions.EnronModbus = true; + server.setModbusDefinitions(definitions); + DataMapDataModel model(server); + for (auto group : {EnronModbus::RegisterType::Group5x, EnronModbus::RegisterType::Group7x}) { + DataMapEntry entry; + entry.EnronType = group; + model.addEntry(holdingKey(EnronModbus::baseAddress(group)), entry); + } + model.addEntry(holdingKey(10), {}); + DataMapFilterProxy proxy; + proxy.setSourceModel(&model); + QCOMPARE(proxy.rowCount(), 3); + proxy.setFilterTypeIndex(5); + QCOMPARE(proxy.rowCount(), 1); + QCOMPARE(proxy.index(0, ColType).data(DataMapRole::EnronType).toInt(), 1); + proxy.setFilterTypeIndex(6); + QCOMPARE(proxy.rowCount(), 1); + QCOMPARE(proxy.index(0, ColType).data(DataMapRole::EnronType).toInt(), 2); + proxy.setFilterTypeIndex(4); + QCOMPARE(proxy.rowCount(), 1); + definitions.EnronModbus = false; + server.setModbusDefinitions(definitions); + QTRY_COMPARE(proxy.rowCount(), 3); + proxy.setFilterTypeIndex(5); + QCOMPARE(proxy.rowCount(), 0); + proxy.setFilterTypeIndex(-1); + QCOMPARE(proxy.rowCount(), 3); +} + void TestDataMapDataModel::removingRowReleasesDeviceId() { ModbusMultiServer server; From e7aca302942e98fb83461cd6deda060f2d488ead Mon Sep 17 00:00:00 2001 From: Alexandr Ananev Date: Thu, 5 Nov 2026 10:13:18 +0700 Subject: [PATCH 4/5] fix: Show Enron data types in register labels and address space logs --- README.md | 2 +- README.md.in | 2 +- src/applogger.cpp | 38 +++-- src/controls/pointtypecombobox.cpp | 6 +- src/datamapdatamodel.cpp | 10 +- src/dialogs/dialogmodbusdefinitions.ui | 2 +- src/enronmodbus.h | 3 + src/formdatamapview.cpp | 4 +- src/formdataview.cpp | 2 +- src/modbusmultiserver.cpp | 34 +++-- src/modbusmultiserver.h | 19 ++- src/res/translations/omodsim_it.qm | Bin 79024 -> 79365 bytes src/res/translations/omodsim_it.ts | 198 ++++++++++++------------ src/res/translations/omodsim_ru.qm | Bin 79382 -> 79122 bytes src/res/translations/omodsim_ru.ts | 200 +++++++++++++------------ src/res/translations/omodsim_zh_CN.qm | Bin 59237 -> 59548 bytes src/res/translations/omodsim_zh_CN.ts | 198 ++++++++++++------------ src/res/translations/omodsim_zh_TW.qm | Bin 59387 -> 59698 bytes src/res/translations/omodsim_zh_TW.ts | 198 ++++++++++++------------ src/tests/test_appproject.cpp | 61 ++++++++ src/tests/test_datamapdatamodel.cpp | 5 + 21 files changed, 550 insertions(+), 432 deletions(-) diff --git a/README.md b/README.md index bab6c0df..002c2f65 100644 --- a/README.md +++ b/README.md @@ -52,7 +52,7 @@ The following simulations are available: Enable **Enron Modbus (32-bit registers)** in **Modbus Definitions → General**. The setting is saved with the project. -Select **Enron Registers 5x** or **7x** and enter the full protocol address +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 diff --git a/README.md.in b/README.md.in index cfaaeb33..99358157 100644 --- a/README.md.in +++ b/README.md.in @@ -52,7 +52,7 @@ The following simulations are available: Enable **Enron Modbus (32-bit registers)** in **Modbus Definitions → General**. The setting is saved with the project. -Select **Enron Registers 5x** or **7x** and enter the full protocol address +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 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/controls/pointtypecombobox.cpp b/src/controls/pointtypecombobox.cpp index 884d8494..c56422e7 100644 --- a/src/controls/pointtypecombobox.cpp +++ b/src/controls/pointtypecombobox.cpp @@ -39,7 +39,7 @@ void PointTypeComboBox::changeEvent(QEvent* event) 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 5x (Read-Write)") : tr("Enron Registers 7x (Read-Write)")); + ? tr("Enron Registers Int32 (Read-Write)") : tr("Enron Registers Float32 (Read-Write)")); continue; } switch(itemData(i).value()) @@ -130,9 +130,9 @@ void PointTypeComboBox::setEnronEnabled(bool enabled) return; if (enabled) { - addItem(tr("Enron Registers 5x (Read-Write)"), QModbusDataUnit::HoldingRegisters); + addItem(tr("Enron Registers Int32 (Read-Write)"), QModbusDataUnit::HoldingRegisters); setItemData(count() - 1, int(EnronModbus::RegisterType::Group5x), Qt::UserRole + 1); - addItem(tr("Enron Registers 7x (Read-Write)"), QModbusDataUnit::HoldingRegisters); + addItem(tr("Enron Registers Float32 (Read-Write)"), QModbusDataUnit::HoldingRegisters); setItemData(count() - 1, int(EnronModbus::RegisterType::Group7x), Qt::UserRole + 1); return; } diff --git a/src/datamapdatamodel.cpp b/src/datamapdatamodel.cpp index a2502155..968073fe 100644 --- a/src/datamapdatamodel.cpp +++ b/src/datamapdatamodel.cpp @@ -218,6 +218,8 @@ 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); @@ -239,9 +241,9 @@ bool DataMapDataModel::changeRegisterType(ItemMapKey& key, DataMapEntry& entry, auto enron = EnronModbus::RegisterType::None; if (role == DataMapRole::EnronType) enron = static_cast(value.toInt()); - else if (value.toString() == tr("Enron Registers 5x")) + else if (value.toString() == tr("Enron Registers Int32")) enron = EnronModbus::RegisterType::Group5x; - else if (value.toString() == tr("Enron Registers 7x")) + 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)) @@ -332,7 +334,7 @@ QVariant DataMapDataModel::data(const QModelIndex& index, int role) const 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 5x") : tr("Enron Registers 7x"); + 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); @@ -1091,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, entry.registerCount()); + key.Address, entry.registerCount(), effectiveEnronType(entry)); } /// diff --git a/src/dialogs/dialogmodbusdefinitions.ui b/src/dialogs/dialogmodbusdefinitions.ui index 3995840f..88e45730 100644 --- a/src/dialogs/dialogmodbusdefinitions.ui +++ b/src/dialogs/dialogmodbusdefinitions.ui @@ -61,7 +61,7 @@ - Select Enron registers 5x or 7x and enter the full protocol address, starting at 5001 or 7001. Select the display format separately in the toolbar. + 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/enronmodbus.h b/src/enronmodbus.h index ecfd152a..0945c2bc 100644 --- a/src/enronmodbus.h +++ b/src/enronmodbus.h @@ -10,6 +10,7 @@ #define ENRONMODBUS_H #include +#include namespace EnronModbus { @@ -56,4 +57,6 @@ inline bool is32BitAddress(quint16 address) } +Q_DECLARE_METATYPE(EnronModbus::RegisterType) + #endif // ENRONMODBUS_H diff --git a/src/formdatamapview.cpp b/src/formdatamapview.cpp index 1ed0d33d..79cac403 100644 --- a/src/formdatamapview.cpp +++ b/src/formdatamapview.cpp @@ -1658,8 +1658,8 @@ void FormDataMapView::updateTypeFilters(bool enronEnabled) _filterTypeCombo->addItem(tr("Input Registers")); _filterTypeCombo->addItem(tr("Holding Registers")); if (enronEnabled) { - _filterTypeCombo->addItem(tr("Enron Registers 5x")); - _filterTypeCombo->addItem(tr("Enron Registers 7x")); + _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); diff --git a/src/formdataview.cpp b/src/formdataview.cpp index add73664..a6e9b66c 100644 --- a/src/formdataview.cpp +++ b/src/formdataview.cpp @@ -1047,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; } diff --git a/src/modbusmultiserver.cpp b/src/modbusmultiserver.cpp index 00d8039e..c3786bac 100644 --- a/src/modbusmultiserver.cpp +++ b/src/modbusmultiserver.cpp @@ -53,6 +53,7 @@ ModbusMultiServer::ModbusMultiServer(QObject *parent) : QObject{parent} ,_workerThread(new QThread(this)) { + qRegisterMetaType("EnronModbus::RegisterType"); qRegisterMetaType("WriteSource"); qRegisterMetaType("ModbusClientInfo"); @@ -159,18 +160,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 +184,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 +210,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 +218,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 +1217,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/res/translations/omodsim_it.qm b/src/res/translations/omodsim_it.qm index 02b26565674e6395abcf8dd79490f7a833d3ac09..8fa4c7dfee048ac3912ca938eaaf471f117d5c52 100644 GIT binary patch delta 6779 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 delta 6326 zcmXY#c|c9+8^@n>&pCHH=iZxE*_W}6>{&9F$dawGCNe6LqM@|euQo|dB201+O7eI1o+POVkD2w8uzB zTY=9p&q7NgM;oFwbrSWP>!lwl#YQp~i-^XT5EYJwP-R3L*Aq>fN%Y@oa2-)`XE1{3 ztA~+{&_Fc3l&B4gE2%?--FmqVSvOZ`y_skA?{EL1bky8BX|*abS;s7 zhs4hb#GQh6PVFVG!oXX^RYK#ZyMd$hM9ZxVKZsIC5_cQwpN!H|7T{sJU64<^0>D!y}$H@iFz-I z?=p;Jn%C0Ac!@(@BzovbKWV!ff5ehg3_MvDMw5@gi|77AQ-6FUa(1U_zr!)- zts-X`BBFc|Is3LC@*7Fc>+yd6H*$`KAd`BK^TvHdWh^a>Rx#SlREf`NO<=46^}0+r8hO|vrx z6aCVb=1g8m6y8N*@<5sso(EH9kh}L6eE*(2ys$AhJM!57lISY-V0eH30WEI#2oa(u z-*5IFJ9|phXRM+Z zU7sQ`OrY9#Dk6_f^tL9NXiIB)_a{u@G>5)cB@=yH$1oNMt4|jgzU4Wh?vadmJQPm0 zhOwxsB6|PANJil$QMsRKQxl4ubz7o+6Vq)$ERmI*>D^&3(a9`k=<;gBzfCV@crheB zW6O-_iveevn30Eng7@1n<4(RKYRFBN{kXX;iWJVu)`jcuShj~ZD|Q>4yrV`V)pDv5rdAnSPr@2{Pb4coDu=m962 zn5!j*n=1==pGmZ%NEX5$B+~4ZtFkp;q z`=S>}Fk@wh01B{@9a|Dl)MJP26zoplzsO2fA>SXdl$F%h5)BwBafFY=nEtYhouJaN z`w|;GWH*H;@b&=N(}xa32TWxzzWhlv<-4r*9=;3Ak$t?34J`f2%JePah4D98)d8sZ z-}|f>%M-aSWi19G=a=TPR!iWF<`3C+JBJaYFkx-_Y#~PD$@Z=aB6_if9TXh_iAmz> zwGzYs|2{K^9Tf>pdf#OyMcpJi*2qpj1ygK}W@n{V!&zml{~t-ui4NupHSgWup6>x5mjffTPz}x4{F)1 zwNSBzo=t2!1A;zccah<}CwsI88>Kw39~hjFsIb&0Jzz^WZ$&i5vX=s}%;9|Y(hqy= zTftT)jU&2Z%3fcFAZroAUJte-YBiC)z0;iN+$8q?w`D{ju55i%95M1jws9)Hv#w?v zo$+1EBOH@hKs59|XEGh>X{`s>zqk>G9M4I5`^88-tr1w&dAJ^IXf9l-(>GHyj;GSMH~ zxD`ndAZ`b@((oMDoLgyl-qePR_)$gld?pv=S;Z0UYf3n(wA9}V;o>^L0dzaSPDI*F zF5VI2Hmu?j&)T8nC2%_je?S(R${i}dhmy9?NX9CPI~<2bpr8h9LX9@(GHtNo?`OE| zm~_Nqdx?GOxRYmch|WiF1(%*7v>drp!aOt*=eVhWV_r11q~ zx|xr4>`x5E^Rby=b6Y-c4hB4#%_nU*3m;#_{}~^MN;g(wMI*o42G5(W8p$xdBx;-Z z-Tyo#y0BYfQ#8Lvi5)Kp=MTl4Cwi5_XPw`O%6kXR(xEf>F_+KI$3%C+_+#A+HXI}I zz(oFd6a=_9jn7NoL$o`D&#Uf4jAukr;`5L4y1`J#@gM2p+-MPsHT8QB@h z$fRdRx0^5Wh5%mw@Fn(*#I*G0OY{jyj;ne7g^DLgn-lo5Um#J4HD8wY0l~MGzwFZ& zdHo50rSCo>_Xz&#N+-1H2l?yc77}9?&EHt&i3xh~w>n=(m;avsdp8Q**XP~^zi*g(%>?ID_CU0N;5-oi%c?Scd@5(!0%SN({~dXv7U7xiDo;E&h3N83`HnTaaUN+Y-w_Uz>plC*Q(EjKn!HKk zmcQgFxB4L*H_3PZ>;a$XD&L))i@0qoKib$1h0WYZMmJtwe5e{}Wxo9UVuY*p0D0LM zEz!XodD$*^*t(^>$_nB3vr>MmISxd=L*;i9q5AA)@~7#A$bZ@LPfaO^f4Q%MJI@l; z`YHH9Y0yBu!out|5@NWbWy4^c)OsuWHIF1RiB$}B=#6;pr5MwLL3}4FoD4m~_LE5{r}hFo=C>3ANREKE^4<$OY| zZK3qkJHWYYgO#2J4u7Lu_~I-vx(`b4aoga050!o`f>2AoDT5s$(7s~j>KYVf_L?%h zbszMJtTMhJ6}HV%W>mnt-5x75emq1guupk7a57SEKV@$A4Va=ynYX$CwM(r$VKX05 zwo-YnhY!)Pa;3hs%LQnlt+MPic0TEd^3vyxhM} zs9vr_9_V>Y^>*SdIQKQxCx7^YyY*SMYBp4{qDo@wQne;^A0$sn&^5EtcUBKjLo8E2 z^)T~EL_aR7e^bJ;lk3!O_mD4VDAXR`Y;gdeul5`Q%bVJ&7fg7HlfWnSg8dL{>ST%k zMX7zG(Oe{@ssp+rNV_gn>t8P+5t zpvhNfUxD2Ith(SheB1AYx-9Vm1n^Yfy!ipA#Qq5t%`El5xT*hbN<`&(B(dqbx^9dL zU22ZH&L6QDo1lJ`t3WrbQNOW=oo4={ZdeF)|C%SLRzP0mAwl)U4hM`ZL46Glm%l^M zSwXEw&Ir2B=i%v71ncx|L?=cHt$xM$=O2VtYgV9{i!)NsG;SSeht@G z3APWy;82@{;Tq___<%4xe=JeeTERYMD^c!V!Qs6bj%sfNhYvH5k28gdF-}B+t>BcI zi`e~1m@(Uv=;$iJIS~ujwG&+Om!j>=7UsDef}Q^myt=}}Z%q**tV57Yct(h@3jzNX zq7K93ymkpuSJLpoHX|9Czr^NCg_tSUL`9K8>|7FA&oHL9V#rBph1q4M8plMNeedh@Eh*Qzf!wDyYYgA)kb@QtW8j8lk)$E}Pi3 z!j<#@A~Sc1p{&GbOC-MEWF%vKNw~QOD))UW+;WP5LoO8Vn0JE)Hwm@VO^I4}5b7R+ z=Q*K1EF2M$BfNXb6TQh0Wml7tTs%aLKCBabODpQU5duP~*z5^ZHakgd+X_1!s1v)g zu;qX_i6e5w?vVw^=b>T`i$`c4g2cY_k%nK469@eXlbu;B4%Uyx4)2QgFFjDxzlbBd z1`?xGiDS1^;kf<{{D{)3me`vU^;4bWaHv#^F0(*!qUgG=KhdEc;#^A%nDJcV#v*Zv zBNn=`O$-YBgbC`z5KGvy=VXb!$B3a`{}GK16T?r!X95Gn2o}LMeY+Tu1ZUoVR!o@t zg(%cT+!{I;&Ri;PJ%{hN&ew}c-=3jq$Pstlh3$9t7t`utvmdL)v`=ulbr#~jg=l}| zR^q# zACnlXA0+6t`WCW@4=xUhO?otb6MZO}rLA zq_4xp!vOK)R1569yM}F%i}ULNlpDHPOd>jn8bT=)y3GP3<+m|2U8QbVA}fSBdW)8_8I=(*#g8AcdbCISk?AZhD(+2I87Aj&aUuwTcLd7N3 z5*rJ(O_3vT>bankFH1(ZW35xH<#2`LuM>Qr;SQs8CWhOz^{zTI10(Bnt@L+!NTSrW z&fW&6o1*(^yfslwxo%K>Iqrf_=mw9&gl6@+F}5(xub*|}sEH z*7J3~YdWDcw%5g^!^y&alKAC-ZmUx@GN!F=+Xo-q35V;_YL4O(YLAhOWoO;={hOeo zaR+rLIw66WozR`G|AfZ!ita%o7I;{xd;B8~HoUES5_k;pTC00K)d#6GNcZ+F1P!?+ zarY11yVr@tScq7ql|@3)uFko|&1+3_gKg4(PYAXdmfLQJl`_FDxlcm+KGTFXX{Nbj z(>gE9PqSnewNBhFFzpzVggyN&5}dn_Ff!O?xRL#L*h~K~feF`#Sm$@KWvZF~0p!S; AhX4Qo diff --git a/src/res/translations/omodsim_it.ts b/src/res/translations/omodsim_it.ts index 728dd5e3..00cc0f8b 100644 --- a/src/res/translations/omodsim_it.ts +++ b/src/res/translations/omodsim_it.ts @@ -367,59 +367,61 @@ Registri blocco - - - Enron Registers 5x - + + + + Enron Registers Int32 + Registri Enron Int32 - - - Enron Registers 7x - + + + + 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 @@ -1083,7 +1085,7 @@ Vuoi aprire la pagina di download? - Select Enron registers 5x or 7x and enter the full protocol address, starting at 5001 or 7001. Select the display format separately in the toolbar. + 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. @@ -2044,13 +2046,13 @@ Tipo filtro: %3 - Enron Registers 5x - + Enron Registers Int32 + Registri Enron Int32 - Enron Registers 7x - + Enron Registers Float32 + Registri Enron Float32 @@ -2712,7 +2714,7 @@ Vuoi aprire la pagina di download? Imposta - + Script Script @@ -3049,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' @@ -3366,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 @@ -3393,7 +3395,7 @@ Verifica attentamente il risultato dell'unione. Nessun progetto recente - + Data Dati @@ -3403,7 +3405,7 @@ Verifica attentamente il risultato dell'unione. File JavaScript (*.js);;Tutti i file (*) - + Script stopped: %1 Script interrotto: %1 @@ -3783,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 @@ -3941,14 +3943,14 @@ Verifica attentamente il risultato dell'unione. - Enron Registers 5x (Read-Write) - + Enron Registers Int32 (Read-Write) + Registri Enron Int32 (lettura-scrittura) - Enron Registers 7x (Read-Write) - + Enron Registers Float32 (Read-Write) + Registri Enron Float32 (lettura-scrittura) @@ -4231,14 +4233,14 @@ Verifica attentamente il risultato dell'unione. RunModeComboBox - + Once Una volta - + Periodically diff --git a/src/res/translations/omodsim_ru.qm b/src/res/translations/omodsim_ru.qm index 2eacbc75784661e6ccd6f48ae7efde3cb054d65f..6dd67407390f88fa8c7df852ca8121e621928aee 100644 GIT binary patch delta 6707 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 delta 6754 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 diff --git a/src/res/translations/omodsim_ru.ts b/src/res/translations/omodsim_ru.ts index 37873b98..2e1a1953 100644 --- a/src/res/translations/omodsim_ru.ts +++ b/src/res/translations/omodsim_ru.ts @@ -367,61 +367,63 @@ Регистры хранения - + Unit Устройство - + Type Тип - + Address Адрес - + Data Type Тип данных - + Register Order Порядок регистров - + Byte Order Порядок байт - + Comment Комментарий - + Value Значение - + Timestamp Время - - - Enron Registers 5x - Регистры Enron 5x + + + + Enron Registers Int32 + Регистры Enron Int32 - - - Enron Registers 7x - Регистры Enron 7x + + + + Enron Registers Float32 + Регистры Enron Float32 @@ -1063,8 +1065,8 @@ Open the download page? - Select Enron registers 5x or 7x and enter the full protocol address, starting at 5001 or 7001. Select the display format separately in the toolbar. - Выберите регистры Enron 5x или 7x и задавайте полный адрес протокола, начиная с 5001 или 7001. Формат отображения выбирайте отдельно на панели инструментов. + 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. Формат отображения выбирайте отдельно на панели инструментов. @@ -2063,13 +2065,13 @@ Type Filter: %3 - Enron Registers 5x - Регистры Enron 5x + Enron Registers Int32 + Регистры Enron Int32 - Enron Registers 7x - Регистры Enron 7x + Enron Registers Float32 + Регистры Enron Float32 @@ -2712,7 +2714,7 @@ Open the download page? Настройка - + Script Скрипт @@ -3049,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' @@ -3366,19 +3368,19 @@ Please verify the merge result carefully. Переместить на другую панель - + 1-based 1-первый - + 0-based 0-первый - + Hex View Шестнадцатиричный режим @@ -3393,7 +3395,7 @@ Please verify the merge result carefully. Нет недавних проектов - + Data Данные @@ -3403,7 +3405,7 @@ Please verify the merge result carefully. Файлы JavaScript (*.js);;Все файлы (*) - + Script stopped: %1 Скрипт остановлен: %1 @@ -3783,12 +3785,12 @@ Please verify the merge result carefully. ModbusMultiServer - + An incorrect device ID was specified (%1) Некорректное значение номера устройства (%1) - + Connection error. %1 Ошибка подключения. %1 @@ -3941,14 +3943,14 @@ Please verify the merge result carefully. - Enron Registers 5x (Read-Write) - Регистры Enron 5x (чтение-запись) + Enron Registers Int32 (Read-Write) + Регистры Enron Int32 (чтение-запись) - Enron Registers 7x (Read-Write) - Регистры Enron 7x (чтение-запись) + Enron Registers Float32 (Read-Write) + Регистры Enron Float32 (чтение-запись) @@ -4231,14 +4233,14 @@ Please verify the merge result carefully. RunModeComboBox - + Once Однократно - + Periodically diff --git a/src/res/translations/omodsim_zh_CN.qm b/src/res/translations/omodsim_zh_CN.qm index d15b580dae50d78a727127575b1b7125b1382a9f..9ac7d87c16e589450240ac532a851394e1a02b68 100644 GIT binary patch delta 6732 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 diff --git a/src/res/translations/omodsim_zh_CN.ts b/src/res/translations/omodsim_zh_CN.ts index 5eff6368..e9a52bd8 100644 --- a/src/res/translations/omodsim_zh_CN.ts +++ b/src/res/translations/omodsim_zh_CN.ts @@ -367,59 +367,61 @@ 保持寄存器 - - - Enron Registers 5x - + + + + Enron Registers Int32 + Enron Int32 寄存器 - - - Enron Registers 7x - + + + + Enron Registers Float32 + Enron Float32 寄存器 - + Unit 单元 - + Type 类型 - + Address 地址 - + Data Type 数据类型 - + Register Order 寄存器顺序 - + Byte Order 字节顺序 - + Comment 注释 - + Value 值 - + Timestamp 时间戳 @@ -1083,7 +1085,7 @@ Open the download page? - Select Enron registers 5x or 7x and enter the full protocol address, starting at 5001 or 7001. Select the display format separately in the toolbar. + 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. @@ -2044,13 +2046,13 @@ Type Filter: %3 - Enron Registers 5x - + Enron Registers Int32 + Enron Int32 寄存器 - Enron Registers 7x - + Enron Registers Float32 + Enron Float32 寄存器 @@ -2712,7 +2714,7 @@ Open the download page? 设置 - + Script 脚本 @@ -3050,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' @@ -3367,19 +3369,19 @@ Please verify the merge result carefully. 移至其他面板 - + 1-based 基地址为-1 - + 0-based 基地址为-0 - + Hex View 十六进制视图 @@ -3394,7 +3396,7 @@ Please verify the merge result carefully. 没有最近项目 - + Data 数据 @@ -3404,7 +3406,7 @@ Please verify the merge result carefully. JavaScript文件 (*.js);;所有文件 (*) - + Script stopped: %1 脚本已停止:%1 @@ -3784,12 +3786,12 @@ Please verify the merge result carefully. ModbusMultiServer - + An incorrect device ID was specified (%1) 指定了错误的设备地址(%1) - + Connection error. %1 连接错误 %1 @@ -3942,14 +3944,14 @@ Please verify the merge result carefully. - Enron Registers 5x (Read-Write) - + Enron Registers Int32 (Read-Write) + Enron Int32 寄存器(读写) - Enron Registers 7x (Read-Write) - + Enron Registers Float32 (Read-Write) + Enron Float32 寄存器(读写) @@ -4232,14 +4234,14 @@ Please verify the merge result carefully. RunModeComboBox - + Once 一次 - + Periodically diff --git a/src/res/translations/omodsim_zh_TW.qm b/src/res/translations/omodsim_zh_TW.qm index c1f1475dc7dbde22f8f2ccd9ca569a0463d5af11..0aba39be58d21f63e3d68cd87569204e64b3a3ed 100644 GIT binary patch delta 6698 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 bsm3(a-0Gi0nL;oyw`1+YCb@t9E-3yBe?ilE delta 6323 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保持暫存器 - - - Enron Registers 5x - + + + + Enron Registers Int32 + Enron Int32 暫存器 - - - Enron Registers 7x - + + + + Enron Registers Float32 + Enron Float32 暫存器 - + Unit 單元 - + Type 類型 - + Address 地址 - + Data Type 資料類型 - + Register Order 暫存器順序 - + Byte Order 位元組順序 - + Comment 注釋 - + Value 值 - + Timestamp 時間戳記 @@ -1083,7 +1085,7 @@ Open the download page? - Select Enron registers 5x or 7x and enter the full protocol address, starting at 5001 or 7001. Select the display format separately in the toolbar. + 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. @@ -2044,13 +2046,13 @@ Type Filter: %3 - Enron Registers 5x - + Enron Registers Int32 + Enron Int32 暫存器 - Enron Registers 7x - + Enron Registers Float32 + Enron Float32 暫存器 @@ -2712,7 +2714,7 @@ Open the download page? 設定 - + Script 腳本 @@ -3050,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' @@ -3367,19 +3369,19 @@ Please verify the merge result carefully. 移至其他面板 - + 1-based 基地址為-1 - + 0-based 基地址為-0 - + Hex View 十六進位檢視 @@ -3394,7 +3396,7 @@ Please verify the merge result carefully. 無最近專案 - + Data 資料 @@ -3404,7 +3406,7 @@ Please verify the merge result carefully. JavaScript檔案 (*.js);;所有檔案 (*) - + Script stopped: %1 腳本已停止:%1 @@ -3784,12 +3786,12 @@ Please verify the merge result carefully. ModbusMultiServer - + An incorrect device ID was specified (%1) 指定了錯誤的裝置地址(%1) - + Connection error. %1 連線錯誤 %1 @@ -3942,14 +3944,14 @@ Please verify the merge result carefully. - Enron Registers 5x (Read-Write) - + Enron Registers Int32 (Read-Write) + Enron Int32 暫存器(讀寫) - Enron Registers 7x (Read-Write) - + Enron Registers Float32 (Read-Write) + Enron Float32 暫存器(讀寫) @@ -4232,14 +4234,14 @@ Please verify the merge result carefully. RunModeComboBox - + Once 一次 - + Periodically diff --git a/src/tests/test_appproject.cpp b/src/tests/test_appproject.cpp index 13127cb4..6c8845cc 100644 --- a/src/tests/test_appproject.cpp +++ b/src/tests/test_appproject.cpp @@ -7,6 +7,7 @@ /// #include +#include #include #include #include @@ -21,6 +22,7 @@ #include #include "appproject.h" +#include "applogger.h" #include "apppreferences.h" #include "application.h" #include "controls/mdiareaex.h" @@ -42,6 +44,8 @@ class TestAppProject : public QObject Q_OBJECT private slots: + void logsEnronAddressSpaces_data(); + void logsEnronAddressSpaces(); void mapsEnronRequests_data(); void mapsEnronRequests(); void editsAndRestoresEnronDataMap(); @@ -221,6 +225,55 @@ QStringList commentPlacements(const QString& path) } +/// \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() { @@ -296,6 +349,8 @@ void TestAppProject::editsAndRestoresEnronDataMap() 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); @@ -371,6 +426,9 @@ void TestAppProject::togglesEnronRegisterChoices() 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); @@ -559,7 +617,10 @@ void TestAppProject::enronRegisterChoices() fixture.Server.setModbusDefinitions(definitions); QTRY_COMPARE(combo->count(), 6); QVERIFY(combo->findData(mode, Qt::UserRole + 1) >= 0); + QSignalSpy maps(&fixture.Server, &ModbusMultiServer::unitMapAdded); combo->setCurrentEnronType(enron); + QTRY_VERIFY(!maps.isEmpty()); + QCOMPARE(maps.last().last().value(), enron); const int base = EnronModbus::baseAddress(enron); QCOMPARE(address->value(), base); QCOMPARE(address->range().from(), base); diff --git a/src/tests/test_datamapdatamodel.cpp b/src/tests/test_datamapdatamodel.cpp index 37049d3a..a17cc6c5 100644 --- a/src/tests/test_datamapdatamodel.cpp +++ b/src/tests/test_datamapdatamodel.cpp @@ -84,9 +84,12 @@ void TestDataMapDataModel::editsEnronRegisters() model.addEntry(holdingKey(0), {}); const auto enron = static_cast(group); const int base = EnronModbus::baseAddress(enron); + QSignalSpy mapAdded(&server, &ModbusMultiServer::unitMapAdded); const auto typeIndex = model.index(0, ColType); QVERIFY(model.setData(typeIndex, group, DataMapRole::EnronType)); QCOMPARE(model.data(typeIndex, DataMapRole::EnronType).toInt(), group); + QTRY_COMPARE(mapAdded.last().last().value(), enron); + QCOMPARE(model.data(typeIndex).toString(), group == 1 ? QString("Enron Registers Int32") : QString("Enron Registers Float32")); QCOMPARE(model.keyForRow(0).Type, QModbusDataUnit::HoldingRegisters); QCOMPARE(model.keyForRow(0).Address, quint16(base)); QCOMPARE(model.data(model.index(0, ColAddress)).toInt(), base); @@ -111,10 +114,12 @@ void TestDataMapDataModel::editsEnronRegisters() server.setModbusDefinitions(definitions); QTRY_VERIFY(!model.data(typeIndex, DataMapRole::EnronEnabled).toBool()); QCOMPARE(model.data(typeIndex, DataMapRole::EnronType).toInt(), 0); + QTRY_COMPARE(mapAdded.last().last().value(), EnronModbus::RegisterType::None); QVERIFY(!model.setData(typeIndex, group, DataMapRole::EnronType)); definitions.EnronModbus = true; server.setModbusDefinitions(definitions); QTRY_COMPARE(model.data(typeIndex, DataMapRole::EnronType).toInt(), group); + QTRY_COMPARE(mapAdded.last().last().value(), enron); QCOMPARE(model.data(model.index(0, ColAddress)).toInt(), base + 1); QCOMPARE(model.data(model.index(0, ColDataType)).toString(), QStringLiteral("UInt16")); QCOMPARE(server.data(1, QModbusDataUnit::HoldingRegisters, base + 2, 2).values(), From 2a15e3717f32dfad6456df0d1dda23365a0bcdab Mon Sep 17 00:00:00 2001 From: Alexandr Ananev Date: Thu, 5 Nov 2026 10:36:13 +0700 Subject: [PATCH 5/5] fix: Register queued server signal types outside the application entry point --- src/main.cpp | 12 ----------- src/modbusmultiserver.cpp | 7 ++++++ src/tests/test_modbusmultiserver.cpp | 32 ++++++++++++++++++++++++++++ 3 files changed, 39 insertions(+), 12 deletions(-) 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/modbusmultiserver.cpp b/src/modbusmultiserver.cpp index c3786bac..f006b2e1 100644 --- a/src/modbusmultiserver.cpp +++ b/src/modbusmultiserver.cpp @@ -53,6 +53,13 @@ ModbusMultiServer::ModbusMultiServer(QObject *parent) : QObject{parent} ,_workerThread(new QThread(this)) { + qRegisterMetaType(); + qRegisterMetaType(); + qRegisterMetaType(); + qRegisterMetaType(); + qRegisterMetaType>(); + qRegisterMetaType(); + qRegisterMetaType>(); qRegisterMetaType("EnronModbus::RegisterType"); qRegisterMetaType("WriteSource"); qRegisterMetaType("ModbusClientInfo"); diff --git a/src/tests/test_modbusmultiserver.cpp b/src/tests/test_modbusmultiserver.cpp index 77274b7e..469454d7 100644 --- a/src/tests/test_modbusmultiserver.cpp +++ b/src/tests/test_modbusmultiserver.cpp @@ -79,6 +79,7 @@ class TestModbusMultiServer : public QObject Q_OBJECT private slots: + void deliversQueuedDefinitionsAndData(); void mergesDescriptionMaps(); void replacesDescriptionMaps(); void mergesTimestampMaps(); @@ -115,6 +116,37 @@ private slots: static void prepare(ModbusMultiServer& server, quint8 deviceId, quint16 length = 300); }; +/// +/// \brief Delivers worker-thread updates without registrations supplied by the application entry point. +/// +void TestModbusMultiServer::deliversQueuedDefinitionsAndData() +{ + ModbusMultiServer server; + QObject receiver; + ModbusDefinitions receivedDefinitions; + QPair receivedData; + connect(&server, &ModbusMultiServer::definitionsChanged, &receiver, + [&receivedDefinitions](const ModbusDefinitions& defs) { receivedDefinitions = defs; }); + connect(&server, &ModbusMultiServer::dataChanged, &receiver, + [&receivedData](quint8 deviceId, const QModbusDataUnit& data) { receivedData = qMakePair(deviceId, data); }); + + auto definitions = server.getModbusDefinitions(); + definitions.EnronModbus = true; + server.setModbusDefinitions(definitions); + QTRY_VERIFY(receivedDefinitions.EnronModbus); + definitions.EnronModbus = false; + server.setModbusDefinitions(definitions); + QTRY_VERIFY(!receivedDefinitions.EnronModbus); + + server.addUnitMap(QUuid::createUuid(), 3, QModbusDataUnit::HoldingRegisters, 5001, 2); + const auto data = unit(QModbusDataUnit::HoldingRegisters, 5001, {0x1234, 0x5678}); + server.setData(3, data, WriteSource::User); + QTRY_COMPARE(receivedData.second.values(), data.values()); + QCOMPARE(receivedData.first, quint8(3)); + QCOMPARE(receivedData.second.registerType(), data.registerType()); + QCOMPARE(receivedData.second.startAddress(), data.startAddress()); +} + void TestModbusMultiServer::mergesDescriptionMaps() { ModbusMultiServer server;