diff --git a/src/communication/modbuspoll.cpp b/src/communication/modbuspoll.cpp index 9121d1f6..33ea1296 100644 --- a/src/communication/modbuspoll.cpp +++ b/src/communication/modbuspoll.cpp @@ -5,7 +5,6 @@ #include "util/formatdatetime.h" #include "util/scopelogging.h" - ModbusPoll::ModbusPoll(SettingsModel* pSettingsModel, QObject* parent) : QObject(parent), _bPollActive(false) { _pPollTimer = new QTimer(this); @@ -107,7 +106,6 @@ void ModbusPoll::onReadDataResult(ResultDoubleList results) } } - /*! \brief Returns the AdapterManager owned by this instance. */ AdapterManager* ModbusPoll::adapterManager() const { diff --git a/src/dialogs/addregisterwidget.cpp b/src/dialogs/addregisterwidget.cpp index c3b14e7c..efe8b5b0 100644 --- a/src/dialogs/addregisterwidget.cpp +++ b/src/dialogs/addregisterwidget.cpp @@ -77,7 +77,8 @@ AddRegisterWidget::AddRegisterWidget(SettingsModel* pSettingsModel, } connect(_pUi->btnAdd, &QPushButton::clicked, this, &AddRegisterWidget::handleResultAccept); - connect(_pAdapterManager, &AdapterManager::buildExpressionResult, this, &AddRegisterWidget::onBuildExpressionResult); + connect(_pAdapterManager, &AdapterManager::buildExpressionResult, this, + &AddRegisterWidget::onBuildExpressionResult); _axisGroup.setExclusive(true); _axisGroup.addButton(_pUi->radioPrimary); diff --git a/src/importexport/projectfiledefinitions.h b/src/importexport/projectfiledefinitions.h index b7a5ffdf..84e76f7b 100644 --- a/src/importexport/projectfiledefinitions.h +++ b/src/importexport/projectfiledefinitions.h @@ -10,10 +10,26 @@ const char cModbusScopeTag[] = "modbusscope"; const char cModbusTag[] = "modbus"; const char cScopeTag[] = "scope"; const char cViewTag[] = "view"; +const char cConnectionTag[] = "connection"; const char cDeviceTag[] = "device"; const char cDeviceIdTag[] = "deviceid"; const char cDeviceNameTag[] = "name"; const char cLogTag[] = "log"; +const char cConnectionIdTag[] = "connectionid"; +const char cConnectionEnabledTag[] = "enabled"; +const char cConnectionTypeTag[] = "type"; +const char cIpTag[] = "ip"; +const char cPortTag[] = "port"; +const char cPortNameTag[] = "portname"; +const char cBaudrateTag[] = "baudrate"; +const char cParityTag[] = "parity"; +const char cDataBitsTag[] = "databits"; +const char cStopBitsTag[] = "stopbits"; +const char cSlaveIdTag[] = "slaveid"; +const char cTimeoutTag[] = "timeout"; +const char cConsecutiveMaxTag[] = "consecutivemax"; +const char cInt32LittleEndianTag[] = "int32littleendian"; +const char cPersistentConnectionTag[] = "persistentconnection"; const char cPollTimeTag[] = "polltime"; const char cAbsoluteTimesTag[] = "absolutetimes"; const char cLogToFileTag[] = "logtofile"; @@ -58,7 +74,9 @@ const char cAdapterSettingsKey[] = "settings"; const char cAdapterIdKey[] = "adapterId"; const char cAdapterKey[] = "adapter"; const char cIdJsonKey[] = "id"; +const char cConnectionsJsonKey[] = "connections"; const char cDevicesJsonKey[] = "devices"; +const char cConnectionTypeJsonKey[] = "connectiontype"; /* JSON constant values */ const quint32 cMinimumJsonVersion = 6; diff --git a/src/importexport/projectfilehandler.cpp b/src/importexport/projectfilehandler.cpp index d58757e8..3a6c9b63 100644 --- a/src/importexport/projectfilehandler.cpp +++ b/src/importexport/projectfilehandler.cpp @@ -4,6 +4,7 @@ #include "importexport/projectfiledata.h" #include "importexport/projectfilejsonexporter.h" #include "importexport/projectfilejsonparser.h" +#include "importexport/projectfilexmlparser.h" #include "models/device.h" #include "models/graphdatamodel.h" #include "models/guimodel.h" @@ -35,8 +36,23 @@ void ProjectFileHandler::openProjectFile(QString projectFilePath) QTextStream in(&file); QString projectFileContents = in.readAll(); - ProjectFileJsonParser jsonParser; - GeneralError parseErr = jsonParser.parseFile(projectFileContents, &loadedSettings); + GeneralError parseErr; + QString trimmed = projectFileContents.trimmed(); + if (trimmed.startsWith('{')) + { + ProjectFileJsonParser jsonParser; + parseErr = jsonParser.parseFile(projectFileContents, &loadedSettings); + } + else if (trimmed.startsWith('<')) + { + ProjectFileXmlParser xmlParser; + parseErr = + xmlParser.parseFile(projectFileContents, &loadedSettings, QFileInfo(projectFilePath).absolutePath()); + } + else + { + parseErr.reportError(tr("The file is not a valid MBS project file.")); + } if (parseErr.result()) { diff --git a/src/importexport/projectfilexmlparser.cpp b/src/importexport/projectfilexmlparser.cpp new file mode 100644 index 00000000..a8ceaa2a --- /dev/null +++ b/src/importexport/projectfilexmlparser.cpp @@ -0,0 +1,857 @@ + +#include "projectfilexmlparser.h" + +#include "importexport/projectfiledefinitions.h" + +#include +#include +#include + +using ProjectFileData::AdapterFileSettings; +using ProjectFileData::DeviceSettings; +using ProjectFileData::GeneralSettings; +using ProjectFileData::LogSettings; +using ProjectFileData::ProjectSettings; +using ProjectFileData::RegisterSettings; +using ProjectFileData::ScaleSettings; +using ProjectFileData::ScopeSettings; +using ProjectFileData::ViewSettings; +using ProjectFileData::YAxisSettings; + +ProjectFileXmlParser::ProjectFileXmlParser() : _dataLevel(0) +{ +} + +/*! + * \brief Parse a legacy XML MBS project file (data levels 3–5). + * \param fileContent Raw file contents. + * \param pSettings Output settings structure. + * \param projectBaseDir Absolute path of the directory containing the project file, + * used to resolve relative log-file paths. + * \return GeneralError — result() is true on success. + */ +GeneralError ProjectFileXmlParser::parseFile(const QString& fileContent, + ProjectSettings* pSettings, + const QString& projectBaseDir) +{ + GeneralError parseErr; + _projectBaseDir = projectBaseDir; + + QDomDocument::ParseResult result = + _domDocument.setContent(fileContent, QDomDocument::ParseOption::UseNamespaceProcessing); + if (!result) + { + parseErr.reportError(QString("Parse error at line %1, column %2:\n%3") + .arg(result.errorLine) + .arg(result.errorColumn) + .arg(result.errorMessage)); + return parseErr; + } + + QDomElement root = _domDocument.documentElement(); + if (root.tagName() != ProjectFileDefinitions::cModbusScopeTag) + { + parseErr.reportError(QString("The file is not a valid ModbusScope project file.")); + return parseErr; + } + + bool bRet; + QString strDataLevel = root.attribute(ProjectFileDefinitions::cDatalevelAttribute, "1"); + quint32 datalevel = strDataLevel.toUInt(&bRet); + + if (!bRet) + { + parseErr.reportError(QString("Data level (%1) is not a valid number").arg(strDataLevel)); + return parseErr; + } + + _dataLevel = datalevel; + if (datalevel < ProjectFileDefinitions::cMinimumDataLevel) + { + parseErr.reportError(QString("Data level %1 is not supported. Minimum datalevel is %2.\n\n" + "Try saving the project file with a previous version of ModbusScope.\n\n" + "Project file loading is aborted.") + .arg(datalevel) + .arg(ProjectFileDefinitions::cMinimumDataLevel)); + return parseErr; + } + + if (datalevel > ProjectFileDefinitions::cCurrentDataLevel) + { + parseErr.reportError(QString("Data level %1 is not supported. Only datalevel %2 or lower is supported.\n\n" + "Try upgrading ModbusScope to the latest version.\n\n" + "Project file loading is aborted.") + .arg(datalevel) + .arg(ProjectFileDefinitions::cCurrentDataLevel)); + return parseErr; + } + + QDomElement tag = root.firstChildElement(); + while (!tag.isNull()) + { + if (tag.tagName() == ProjectFileDefinitions::cModbusTag) + { + parseErr = parseModbusTag(tag, &pSettings->general); + if (!parseErr.result()) + { + break; + } + } + else if (tag.tagName() == ProjectFileDefinitions::cScopeTag) + { + parseErr = parseScopeTag(tag, &pSettings->scope); + if (!parseErr.result()) + { + break; + } + } + else if (tag.tagName() == ProjectFileDefinitions::cViewTag) + { + parseErr = parseViewTag(tag, &pSettings->view); + if (!parseErr.result()) + { + break; + } + } + + tag = tag.nextSiblingElement(); + } + + return parseErr; +} + +/*! + * \brief Parse the \c \ section, building both main-app settings and the adapter JSON blob. + */ +GeneralError ProjectFileXmlParser::parseModbusTag(const QDomElement& element, GeneralSettings* pGeneralSettings) +{ + GeneralError parseErr; + QJsonArray connectionsArray; + QJsonArray adapterDevicesArray; + + QDomElement child = element.firstChildElement(); + while (!child.isNull()) + { + if (child.tagName() == ProjectFileDefinitions::cConnectionTag) + { + QJsonObject connectionJson; + + if (_dataLevel < 5) + { + QJsonObject deviceJson; + DeviceSettings deviceSettings; + + parseErr = parseLegacyConnectionTag(child, &connectionJson, &deviceJson, &deviceSettings); + + if (parseErr.result() && connectionJson[ProjectFileDefinitions::cEnabledAttribute].toBool(true)) + { + pGeneralSettings->deviceSettings.append(deviceSettings); + adapterDevicesArray.append(deviceJson); + } + } + else + { + parseErr = parseConnectionTag(child, &connectionJson); + } + + if (!parseErr.result()) + { + break; + } + + connectionsArray.append(connectionJson); + } + else if (child.tagName() == ProjectFileDefinitions::cDeviceTag) + { + DeviceSettings deviceSettings; + QJsonObject deviceJson; + + parseErr = parseDeviceTag(child, &deviceSettings, &deviceJson); + if (!parseErr.result()) + { + break; + } + + pGeneralSettings->deviceSettings.append(deviceSettings); + adapterDevicesArray.append(deviceJson); + } + else if (child.tagName() == ProjectFileDefinitions::cLogTag) + { + parseErr = parseLogTag(child, &pGeneralSettings->logSettings); + if (!parseErr.result()) + { + break; + } + } + + child = child.nextSiblingElement(); + } + + if (parseErr.result()) + { + buildAdapterSettings(connectionsArray, adapterDevicesArray, pGeneralSettings); + } + + return parseErr; +} + +/*! + * \brief Parse a \c \ tag (data level 5) into a JSON object for the adapter blob. + */ +GeneralError ProjectFileXmlParser::parseConnectionTag(const QDomElement& element, QJsonObject* pConnectionJson) +{ + GeneralError parseErr; + QDomElement child = element.firstChildElement(); + while (!child.isNull() && parseErr.result()) + { + parseErr = parseConnectionFields(child, pConnectionJson); + child = child.nextSiblingElement(); + } + + return parseErr; +} + +/*! + * \brief Parse a legacy \c \ tag (data level < 5) which contains both + * connection settings and device settings (slaveid, consecutivemax, etc.). + */ +GeneralError ProjectFileXmlParser::parseLegacyConnectionTag(const QDomElement& element, + QJsonObject* pConnectionJson, + QJsonObject* pDeviceJson, + DeviceSettings* pDeviceSettings) +{ + GeneralError parseErr; + + /* In legacy format, each connection implicitly defines a device. + * The device gets a synthetic deviceId based on the connection id. */ + quint32 connectionId = 0; + quint8 slaveId = 1; + quint8 consecutiveMax = 125; + bool int32LittleEndian = true; + + QDomElement child = element.firstChildElement(); + while (!child.isNull()) + { + bool bRet; + + parseErr = parseConnectionFields(child, pConnectionJson); + if (!parseErr.result()) + { + break; + } + + if (child.tagName() == ProjectFileDefinitions::cSlaveIdTag) + { + slaveId = static_cast(child.text().toUInt(&bRet)); + if (!bRet) + { + parseErr.reportError(QString("Slave id ( %1 ) is not a valid number").arg(child.text())); + break; + } + } + else if (child.tagName() == ProjectFileDefinitions::cConsecutiveMaxTag) + { + consecutiveMax = static_cast(child.text().toUInt(&bRet)); + if (!bRet) + { + parseErr.reportError( + QString("Consecutive register maximum ( %1 ) is not a valid number").arg(child.text())); + break; + } + } + else if (child.tagName() == ProjectFileDefinitions::cInt32LittleEndianTag) + { + int32LittleEndian = (child.text().toLower().compare(ProjectFileDefinitions::cTrueValue) == 0); + } + else if (child.tagName() == ProjectFileDefinitions::cConnectionIdTag) + { + connectionId = child.text().toUInt(&bRet); + if (!bRet) + { + parseErr.reportError(QString("Connection Id (%1) is not a valid number").arg(child.text())); + break; + } + } + + child = child.nextSiblingElement(); + } + + if (parseErr.result()) + { + /* Build generic device settings for main app */ + pDeviceSettings->bDeviceId = true; + pDeviceSettings->deviceId = connectionId + 1; + + /* Build adapter device JSON blob */ + (*pDeviceJson)[ProjectFileDefinitions::cIdJsonKey] = static_cast(connectionId + 1); + (*pDeviceJson)[ProjectFileDefinitions::cConnectionIdTag] = static_cast(connectionId); + (*pDeviceJson)[ProjectFileDefinitions::cSlaveIdTag] = static_cast(slaveId); + (*pDeviceJson)[ProjectFileDefinitions::cConsecutiveMaxTag] = static_cast(consecutiveMax); + (*pDeviceJson)[ProjectFileDefinitions::cInt32LittleEndianTag] = int32LittleEndian; + } + + return parseErr; +} + +/*! + * \brief Parse a \c \ tag (data level 5) into both generic DeviceSettings and adapter JSON. + */ +GeneralError ProjectFileXmlParser::parseDeviceTag(const QDomElement& element, + DeviceSettings* pDeviceSettings, + QJsonObject* pDeviceJson) +{ + GeneralError parseErr; + + quint32 connectionId = 0; + quint8 slaveId = 1; + quint8 consecutiveMax = 125; + bool int32LittleEndian = true; + + QDomElement child = element.firstChildElement(); + while (!child.isNull()) + { + bool bRet; + if (child.tagName() == ProjectFileDefinitions::cDeviceIdTag) + { + pDeviceSettings->bDeviceId = true; + pDeviceSettings->deviceId = child.text().toUInt(&bRet); + if (!bRet) + { + parseErr.reportError(QString("Device Id (%1) is not a valid number").arg(child.text())); + break; + } + } + else if (child.tagName() == ProjectFileDefinitions::cDeviceNameTag) + { + pDeviceSettings->bName = true; + pDeviceSettings->name = child.text(); + } + else if (child.tagName() == ProjectFileDefinitions::cConnectionIdTag) + { + connectionId = child.text().toUInt(&bRet); + if (!bRet) + { + parseErr.reportError(QString("Connection Id (%1) is not a valid number").arg(child.text())); + break; + } + } + else if (child.tagName() == ProjectFileDefinitions::cSlaveIdTag) + { + slaveId = static_cast(child.text().toUInt(&bRet)); + if (!bRet) + { + parseErr.reportError(QString("Slave id ( %1 ) is not a valid number").arg(child.text())); + break; + } + } + else if (child.tagName() == ProjectFileDefinitions::cConsecutiveMaxTag) + { + consecutiveMax = static_cast(child.text().toUInt(&bRet)); + if (!bRet) + { + parseErr.reportError( + QString("Consecutive register maximum ( %1 ) is not a valid number").arg(child.text())); + break; + } + } + else if (child.tagName() == ProjectFileDefinitions::cInt32LittleEndianTag) + { + int32LittleEndian = (child.text().toLower().compare(ProjectFileDefinitions::cTrueValue) == 0); + } + + child = child.nextSiblingElement(); + } + + if (parseErr.result()) + { + (*pDeviceJson)[ProjectFileDefinitions::cIdJsonKey] = static_cast(pDeviceSettings->deviceId); + (*pDeviceJson)[ProjectFileDefinitions::cConnectionIdTag] = static_cast(connectionId); + (*pDeviceJson)[ProjectFileDefinitions::cSlaveIdTag] = static_cast(slaveId); + (*pDeviceJson)[ProjectFileDefinitions::cConsecutiveMaxTag] = static_cast(consecutiveMax); + (*pDeviceJson)[ProjectFileDefinitions::cInt32LittleEndianTag] = int32LittleEndian; + } + + return parseErr; +} + +/*! + * \brief Parse the \c \ tag. + */ +GeneralError ProjectFileXmlParser::parseLogTag(const QDomElement& element, LogSettings* pLogSettings) +{ + GeneralError parseErr; + QDomElement child = element.firstChildElement(); + while (!child.isNull()) + { + if (child.tagName() == ProjectFileDefinitions::cPollTimeTag) + { + bool bRet; + pLogSettings->bPollTime = true; + pLogSettings->pollTime = child.text().toUInt(&bRet); + if (!bRet) + { + parseErr.reportError(QString("Poll time ( %1 ) is not a valid number").arg(child.text())); + break; + } + } + else if (child.tagName() == ProjectFileDefinitions::cAbsoluteTimesTag) + { + pLogSettings->bAbsoluteTimes = (child.text().toLower().compare(ProjectFileDefinitions::cTrueValue) == 0); + } + else if (child.tagName() == ProjectFileDefinitions::cLogToFileTag) + { + parseErr = parseLogToFile(child, pLogSettings); + if (!parseErr.result()) + { + break; + } + } + + child = child.nextSiblingElement(); + } + + return parseErr; +} + +/*! + * \brief Parse the \c \ element. + */ +GeneralError ProjectFileXmlParser::parseLogToFile(const QDomElement& element, LogSettings* pLogSettings) +{ + GeneralError parseErr; + + QString enabled = element.attribute(ProjectFileDefinitions::cEnabledAttribute, ProjectFileDefinitions::cTrueValue); + pLogSettings->bLogToFile = (enabled.compare(ProjectFileDefinitions::cTrueValue, Qt::CaseInsensitive) == 0); + + QDomElement child = element.firstChildElement(); + while (!child.isNull()) + { + if (child.tagName() == ProjectFileDefinitions::cFilenameTag) + { + QFileInfo fileInfo = (_projectBaseDir.isEmpty() || QFileInfo(child.text()).isAbsolute()) + ? QFileInfo(child.text()) + : QFileInfo(QDir(_projectBaseDir), child.text()); + bool bValid = true; + + if (!fileInfo.isFile()) + { + if (fileInfo.exists() || !fileInfo.dir().exists()) + { + bValid = false; + } + } + + if (bValid) + { + pLogSettings->bLogToFileFile = true; + pLogSettings->logFile = fileInfo.filePath(); + } + } + + child = child.nextSiblingElement(); + } + + return parseErr; +} + +/*! + * \brief Parse the \c \ tag containing register definitions. + */ +GeneralError ProjectFileXmlParser::parseScopeTag(const QDomElement& element, ScopeSettings* pScopeSettings) +{ + GeneralError parseErr; + QDomElement child = element.firstChildElement(); + while (!child.isNull()) + { + if (child.tagName() == ProjectFileDefinitions::cRegisterTag) + { + RegisterSettings registerData; + parseErr = parseRegisterTag(child, ®isterData); + if (!parseErr.result()) + { + break; + } + + pScopeSettings->registerList.append(registerData); + } + + child = child.nextSiblingElement(); + } + + return parseErr; +} + +/*! + * \brief Parse a single \c \ element. + */ +GeneralError ProjectFileXmlParser::parseRegisterTag(const QDomElement& element, RegisterSettings* pRegisterSettings) +{ + GeneralError parseErr; + + QString active = element.attribute(ProjectFileDefinitions::cActiveAttribute, ProjectFileDefinitions::cTrueValue); + pRegisterSettings->bActive = (active.compare(ProjectFileDefinitions::cTrueValue, Qt::CaseInsensitive) == 0); + + QDomElement child = element.firstChildElement(); + while (!child.isNull()) + { + if (child.tagName() == ProjectFileDefinitions::cTextTag) + { + pRegisterSettings->text = child.text(); + } + else if (child.tagName() == ProjectFileDefinitions::cColorTag) + { + if (QColor::isValidColorName(child.text())) + { + pRegisterSettings->bColor = true; + pRegisterSettings->color = QColor(child.text()); + } + else + { + parseErr.reportError(QString("Color is not a valid color. Did you use correct color format? " + "Expecting #FF0000 (red)")); + break; + } + } + else if (child.tagName() == ProjectFileDefinitions::cValueAxisTag) + { + bool bOk = false; + quint32 axis = child.text().toUInt(&bOk); + if (bOk) + { + pRegisterSettings->valueAxis = axis; + } + else + { + parseErr.reportError(QString("Value axis (%1) is not a valid number").arg(child.text())); + break; + } + } + else if (child.tagName() == ProjectFileDefinitions::cExpressionTag) + { + pRegisterSettings->expression = child.text(); + } + + child = child.nextSiblingElement(); + } + + return parseErr; +} + +/*! + * \brief Parse the \c \ tag. + */ +GeneralError ProjectFileXmlParser::parseViewTag(const QDomElement& element, ViewSettings* pViewSettings) +{ + GeneralError parseErr; + QDomElement child = element.firstChildElement(); + while (!child.isNull()) + { + if (child.tagName() == ProjectFileDefinitions::cScaleTag) + { + parseErr = parseScaleTag(child, &pViewSettings->scaleSettings); + if (!parseErr.result()) + { + break; + } + } + + child = child.nextSiblingElement(); + } + + return parseErr; +} + +/*! + * \brief Parse the \c \ tag containing x-axis and y-axis settings. + */ +GeneralError ProjectFileXmlParser::parseScaleTag(const QDomElement& element, ScaleSettings* pScaleSettings) +{ + GeneralError parseErr; + QDomElement child = element.firstChildElement(); + while (!child.isNull()) + { + if (child.tagName() == ProjectFileDefinitions::cXaxisTag) + { + QString mode = child.attribute(ProjectFileDefinitions::cModeAttribute); + + if (mode.compare(ProjectFileDefinitions::cSlidingValue, Qt::CaseInsensitive) == 0) + { + pScaleSettings->xAxis.bSliding = true; + parseErr = parseScaleXAxis(child, pScaleSettings); + if (!parseErr.result()) + { + break; + } + } + else + { + pScaleSettings->xAxis.bSliding = false; + } + } + else if (child.tagName() == ProjectFileDefinitions::cYaxisTag) + { + QString axisString = child.attribute(ProjectFileDefinitions::cAxisAttribute); + bool bRet = false; + int axisId = axisString.toInt(&bRet); + if (!bRet) + { + axisId = 0; + } + YAxisSettings* yAxisSettings = (axisId == 1) ? &pScaleSettings->y2Axis : &pScaleSettings->yAxis; + + QString mode = child.attribute(ProjectFileDefinitions::cModeAttribute); + + if (mode.compare(ProjectFileDefinitions::cWindowAutoValue, Qt::CaseInsensitive) == 0) + { + yAxisSettings->bWindowScale = true; + } + else if (mode.compare(ProjectFileDefinitions::cMinmaxValue, Qt::CaseInsensitive) == 0) + { + yAxisSettings->bMinMax = true; + parseErr = parseScaleYAxis(child, yAxisSettings); + if (!parseErr.result()) + { + break; + } + } + } + + child = child.nextSiblingElement(); + } + + return parseErr; +} + +/*! + * \brief Parse the x-axis sliding interval from a \c \ element. + */ +GeneralError ProjectFileXmlParser::parseScaleXAxis(const QDomElement& element, ScaleSettings* pScaleSettings) +{ + GeneralError parseErr; + bool bSlidingInterval = false; + + QDomElement child = element.firstChildElement(); + while (!child.isNull()) + { + if (child.tagName() == ProjectFileDefinitions::cSlidingintervalTag) + { + bool bRet; + pScaleSettings->xAxis.slidingInterval = child.text().toUInt(&bRet); + if (bRet) + { + bSlidingInterval = true; + } + else + { + parseErr.reportError(QString("Scale (x-axis) has an incorrect sliding interval. " + "\"%1\" is not a valid number") + .arg(child.text())); + break; + } + } + + child = child.nextSiblingElement(); + } + + if (parseErr.result() && !bSlidingInterval) + { + parseErr.reportError( + QString("If x-axis has sliding window scaling then slidinginterval variable should be defined.")); + } + + return parseErr; +} + +/*! + * \brief Parse min/max values from a \c \ element. + */ +GeneralError ProjectFileXmlParser::parseScaleYAxis(const QDomElement& element, YAxisSettings* pYAxisSettings) +{ + GeneralError parseErr; + bool bMin = false; + bool bMax = false; + + QDomElement child = element.firstChildElement(); + while (!child.isNull()) + { + bool bRet; + if (child.tagName() == ProjectFileDefinitions::cMinTag) + { + pYAxisSettings->scaleMin = QLocale().toDouble(child.text(), &bRet); + if (bRet) + { + bMin = true; + } + else + { + parseErr.reportError( + QString("Scale (y-axis) has an incorrect minimum. \"%1\" is not a valid double").arg(child.text())); + break; + } + } + else if (child.tagName() == ProjectFileDefinitions::cMaxTag) + { + pYAxisSettings->scaleMax = QLocale().toDouble(child.text(), &bRet); + if (bRet) + { + bMax = true; + } + else + { + parseErr.reportError( + QString("Scale (y-axis) has an incorrect maximum. \"%1\" is not a valid double").arg(child.text())); + break; + } + } + + child = child.nextSiblingElement(); + } + + if (parseErr.result()) + { + if (!bMin) + { + parseErr.reportError(QString("If y-axis has min max scaling then min variable should be defined.")); + } + else if (!bMax) + { + parseErr.reportError(QString("If y-axis has min max scaling then max variable should be defined.")); + } + } + + return parseErr; +} + +/*! + * \brief Extract connection fields from a child element into a JSON object for the adapter blob. + * \return GeneralError — result() is false if a numeric field contains an invalid value. + */ +GeneralError ProjectFileXmlParser::parseConnectionFields(const QDomElement& child, QJsonObject* pConnectionJson) +{ + GeneralError parseErr; + bool bRet; + + if (child.tagName() == ProjectFileDefinitions::cConnectionIdTag) + { + quint32 val = child.text().toUInt(&bRet); + if (!bRet) + { + parseErr.reportError(QString("Connection id (%1) is not a valid number").arg(child.text())); + return parseErr; + } + (*pConnectionJson)[ProjectFileDefinitions::cIdJsonKey] = static_cast(val); + } + else if (child.tagName() == ProjectFileDefinitions::cConnectionEnabledTag) + { + (*pConnectionJson)[ProjectFileDefinitions::cEnabledAttribute] = + (child.text().toLower().compare(ProjectFileDefinitions::cTrueValue) == 0); + } + else if (child.tagName() == ProjectFileDefinitions::cConnectionTypeTag) + { + (*pConnectionJson)[ProjectFileDefinitions::cConnectionTypeJsonKey] = child.text(); + } + else if (child.tagName() == ProjectFileDefinitions::cIpTag) + { + (*pConnectionJson)[ProjectFileDefinitions::cIpTag] = child.text(); + } + else if (child.tagName() == ProjectFileDefinitions::cPortTag) + { + quint32 val = child.text().toUInt(&bRet); + if (!bRet) + { + parseErr.reportError(QString("Port (%1) is not a valid number").arg(child.text())); + return parseErr; + } + (*pConnectionJson)[ProjectFileDefinitions::cPortTag] = static_cast(val); + } + else if (child.tagName() == ProjectFileDefinitions::cPortNameTag) + { + (*pConnectionJson)[ProjectFileDefinitions::cPortNameTag] = child.text(); + } + else if (child.tagName() == ProjectFileDefinitions::cBaudrateTag) + { + quint32 val = child.text().toUInt(&bRet); + if (!bRet) + { + parseErr.reportError(QString("Baudrate (%1) is not a valid number").arg(child.text())); + return parseErr; + } + (*pConnectionJson)[ProjectFileDefinitions::cBaudrateTag] = static_cast(val); + } + else if (child.tagName() == ProjectFileDefinitions::cParityTag) + { + quint32 val = child.text().toUInt(&bRet); + if (!bRet) + { + parseErr.reportError(QString("Parity (%1) is not a valid number").arg(child.text())); + return parseErr; + } + (*pConnectionJson)[ProjectFileDefinitions::cParityTag] = static_cast(val); + } + else if (child.tagName() == ProjectFileDefinitions::cStopBitsTag) + { + quint32 val = child.text().toUInt(&bRet); + if (!bRet) + { + parseErr.reportError(QString("Stop bits (%1) is not a valid number").arg(child.text())); + return parseErr; + } + (*pConnectionJson)[ProjectFileDefinitions::cStopBitsTag] = static_cast(val); + } + else if (child.tagName() == ProjectFileDefinitions::cDataBitsTag) + { + quint32 val = child.text().toUInt(&bRet); + if (!bRet) + { + parseErr.reportError(QString("Data bits (%1) is not a valid number").arg(child.text())); + return parseErr; + } + (*pConnectionJson)[ProjectFileDefinitions::cDataBitsTag] = static_cast(val); + } + else if (child.tagName() == ProjectFileDefinitions::cTimeoutTag) + { + quint32 val = child.text().toUInt(&bRet); + if (!bRet) + { + parseErr.reportError(QString("Timeout (%1) is not a valid number").arg(child.text())); + return parseErr; + } + (*pConnectionJson)[ProjectFileDefinitions::cTimeoutTag] = static_cast(val); + } + else if (child.tagName() == ProjectFileDefinitions::cPersistentConnectionTag) + { + (*pConnectionJson)[ProjectFileDefinitions::cPersistentConnectionTag] = + (child.text().toLower().compare(ProjectFileDefinitions::cTrueValue) == 0); + } + + return parseErr; +} + +/*! + * \brief Construct the adapter settings entry from collected connection and device JSON arrays. + */ +void ProjectFileXmlParser::buildAdapterSettings(const QJsonArray& connectionsArray, + const QJsonArray& devicesArray, + GeneralSettings* pGeneralSettings) +{ + AdapterFileSettings adapterSettings; + adapterSettings.type = "modbus"; + + QJsonObject settingsObj; + settingsObj[ProjectFileDefinitions::cConnectionsJsonKey] = connectionsArray; + settingsObj[ProjectFileDefinitions::cDevicesJsonKey] = devicesArray; + adapterSettings.settings = settingsObj; + + pGeneralSettings->adapterList.append(adapterSettings); + const quint32 newAdapterIndex = static_cast(pGeneralSettings->adapterList.size() - 1); + + /* Assign adapter index and type to devices that have not yet been linked to an adapter */ + for (int i = 0; i < pGeneralSettings->deviceSettings.size(); i++) + { + if (pGeneralSettings->deviceSettings[i].adapterType.isEmpty()) + { + pGeneralSettings->deviceSettings[i].adapterId = newAdapterIndex; + pGeneralSettings->deviceSettings[i].adapterType = "modbus"; + } + } +} diff --git a/src/importexport/projectfilexmlparser.h b/src/importexport/projectfilexmlparser.h new file mode 100644 index 00000000..d010206f --- /dev/null +++ b/src/importexport/projectfilexmlparser.h @@ -0,0 +1,71 @@ +#ifndef PROJECTFILEXMLPARSER_H +#define PROJECTFILEXMLPARSER_H + +#include "importexport/generalerror.h" +#include "importexport/projectfiledata.h" + +#include +#include +#include +#include + +/*! + * \brief Parses a legacy XML-format MBS project file (data levels 3–5) and + * converts it into the same ProjectSettings structure used by the JSON parser. + * + * Connection and device settings from the XML \c \ section are converted + * into an adapter settings JSON blob so the result is indistinguishable from a + * file loaded via ProjectFileJsonParser. + */ +class ProjectFileXmlParser +{ +public: + ProjectFileXmlParser(); + + /*! + * \brief Parse a legacy XML MBS project file into ProjectSettings. + * \param fileContent Raw file contents. + * \param pSettings Output settings structure. + * \param projectBaseDir Absolute path of the directory containing the project file. + * Used to resolve relative log-file paths; pass an empty string + * to skip directory-existence validation for relative paths. + * \return GeneralError — result() is true on success. + */ + GeneralError parseFile(const QString& fileContent, + ProjectFileData::ProjectSettings* pSettings, + const QString& projectBaseDir = QString()); + +private: + GeneralError parseModbusTag(const QDomElement& element, ProjectFileData::GeneralSettings* pGeneralSettings); + + GeneralError parseConnectionTag(const QDomElement& element, QJsonObject* pConnectionJson); + GeneralError parseLegacyConnectionTag(const QDomElement& element, + QJsonObject* pConnectionJson, + QJsonObject* pDeviceJson, + ProjectFileData::DeviceSettings* pDeviceSettings); + GeneralError parseDeviceTag(const QDomElement& element, + ProjectFileData::DeviceSettings* pDeviceSettings, + QJsonObject* pDeviceJson); + GeneralError parseLogTag(const QDomElement& element, ProjectFileData::LogSettings* pLogSettings); + GeneralError parseLogToFile(const QDomElement& element, ProjectFileData::LogSettings* pLogSettings); + + GeneralError parseScopeTag(const QDomElement& element, ProjectFileData::ScopeSettings* pScopeSettings); + GeneralError parseRegisterTag(const QDomElement& element, ProjectFileData::RegisterSettings* pRegisterSettings); + + GeneralError parseViewTag(const QDomElement& element, ProjectFileData::ViewSettings* pViewSettings); + GeneralError parseScaleTag(const QDomElement& element, ProjectFileData::ScaleSettings* pScaleSettings); + GeneralError parseScaleXAxis(const QDomElement& element, ProjectFileData::ScaleSettings* pScaleSettings); + GeneralError parseScaleYAxis(const QDomElement& element, ProjectFileData::YAxisSettings* pYAxisSettings); + + GeneralError parseConnectionFields(const QDomElement& child, QJsonObject* pConnectionJson); + + void buildAdapterSettings(const QJsonArray& connectionsArray, + const QJsonArray& devicesArray, + ProjectFileData::GeneralSettings* pGeneralSettings); + + QDomDocument _domDocument; + quint32 _dataLevel; + QString _projectBaseDir; +}; + +#endif // PROJECTFILEXMLPARSER_H diff --git a/tests/importexport/CMakeLists.txt b/tests/importexport/CMakeLists.txt index cf9c1b33..f0196459 100644 --- a/tests/importexport/CMakeLists.txt +++ b/tests/importexport/CMakeLists.txt @@ -5,6 +5,7 @@ add_xtest(tst_datafileparser ${CMAKE_CURRENT_SOURCE_DIR}/csvdata.cpp) add_xtest_mock(tst_presethandler) add_xtest(tst_presetparser ${CMAKE_CURRENT_SOURCE_DIR}/presetfiletestdata.cpp) add_xtest(tst_projectfilejsonparser ${CMAKE_CURRENT_SOURCE_DIR}/projectfilejsontestdata.cpp) +add_xtest(tst_projectfilexmlparser ${CMAKE_CURRENT_SOURCE_DIR}/projectfilexmltestdata.cpp) add_xtest(tst_projectfilejsonexporter) add_xtest(tst_projectfilehandler) add_xtest(tst_settingsauto ${CMAKE_CURRENT_SOURCE_DIR}/csvdata.cpp) diff --git a/tests/importexport/projectfilexmltestdata.cpp b/tests/importexport/projectfilexmltestdata.cpp new file mode 100644 index 00000000..4baf6377 --- /dev/null +++ b/tests/importexport/projectfilexmltestdata.cpp @@ -0,0 +1,303 @@ + +#include "projectfilexmltestdata.h" + +// clang-format off + +QString ProjectFileXmlTestData::cTooLowDataLevel = QString( + " \n" + " \n" + " \n" +); + +QString ProjectFileXmlTestData::cTooHighDataLevel = QString( + " \n" + " \n" + " \n" +); + +QString ProjectFileXmlTestData::cDataLevel3Expressions = QString( + " \n" + " \n" + " \n" + " \n" + " Data point \n" + " \n" + " #ff0000 \n" + " \n" + " \n" + " Data point 2 \n" + " \n" + " #0000ff \n" + " \n" + " \n" + " Data point 3 \n" + " \n" + " \n" + " \n" + " \n" +); + +QString ProjectFileXmlTestData::cDataLevel5Connection = QString( + " \n" + " \n" + " \n" + " \n" + " true \n" + " 0 \n" + " tcp \n" + " 127.0.0.1 \n" + " 502 \n" + " COM1 \n" + " 115200 \n" + " 0 \n" + " 1 \n" + " 8 \n" + " 1000 \n" + " true \n" + " \n" + " \n" + " 1 \n" + " Device 1 \n" + " 0 \n" + " 3 \n" + " 100 \n" + " false \n" + " \n" + " \n" + " 250 \n" + " false \n" + " \n" + " \n" + " \n" + " \n" +); + +QString ProjectFileXmlTestData::cDataLevel5MixedMulti = QString( + " \n" + " \n" + " \n" + " \n" + " true \n" + " 0 \n" + " serial \n" + " 127.0.0.1 \n" + " 502 \n" + " COM10 \n" + " 38400 \n" + " 0 \n" + " 1 \n" + " 8 \n" + " 500 \n" + " true \n" + " \n" + " \n" + " true \n" + " 1 \n" + " tcp \n" + " 127.0.0.1 \n" + " 502 \n" + " COM1 \n" + " 115200 \n" + " 0 \n" + " 1 \n" + " 8 \n" + " 2000 \n" + " true \n" + " \n" + " \n" + " false \n" + " 2 \n" + " tcp \n" + " 127.0.0.1 \n" + " 502 \n" + " COM1 \n" + " 115200 \n" + " 0 \n" + " 1 \n" + " 8 \n" + " 1000 \n" + " true \n" + " \n" + " \n" + " 1 \n" + " Device 1 (serial 1) \n" + " 0 \n" + " 1 \n" + " 125 \n" + " true \n" + " \n" + " \n" + " 2 \n" + " Device 2 (serial 2) \n" + " 0 \n" + " 2 \n" + " 125 \n" + " true \n" + " \n" + " \n" + " 3 \n" + " Device 3 (TCP) \n" + " 1 \n" + " 1 \n" + " 125 \n" + " true \n" + " \n" + " \n" + " 250 \n" + " false \n" + " \n" + " \n" + " \n" + " \n" +); + +QString ProjectFileXmlTestData::cDataLevel4LegacyConnection = QString( + " \n" + " \n" + " \n" + " \n" + " true \n" + " 0 \n" + " tcp \n" + " 127.0.0.1 \n" + " 502 \n" + " COM1 \n" + " 115200 \n" + " 0 \n" + " 1 \n" + " 8 \n" + " 2 \n" + " 1002 \n" + " 122 \n" + " true \n" + " true \n" + " \n" + " \n" + " false \n" + " 1 \n" + " tcp \n" + " 127.0.0.1 \n" + " 502 \n" + " COM1 \n" + " 115200 \n" + " 0 \n" + " 1 \n" + " 8 \n" + " 3 \n" + " 1000 \n" + " 125 \n" + " true \n" + " true \n" + " \n" + " \n" + " \n" + " \n" +); + +QString ProjectFileXmlTestData::cLogSettings = QString( + " \n" + " \n" + " \n" + " \n" + " 750 \n" + " true \n" + " \n" + " \n" + " \n" + " \n" +); + +QString ProjectFileXmlTestData::cScaleSliding = QString( + " \n" + " \n" + " \n" + " \n" + " \n" + " 20 \n" + " \n" + " \n" + " \n" + " \n" +); + +QString ProjectFileXmlTestData::cScaleMinMax = QString( + " \n" + " \n" + " \n" + " \n" + " \n" + " 20 \n" + " \n" + " \n" + " 0 \n" + " 25,5 \n" + " \n" + " \n" + " \n" + " \n" +); + +QString ProjectFileXmlTestData::cScaleWindowAuto = QString( + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " 0 \n" + " 25,5 \n" + " \n" + " \n" + " \n" + " \n" +); + +QString ProjectFileXmlTestData::cValueAxis = QString( + " \n" + " \n" + " \n" + " \n" + " Data point \n" + " \n" + " 0 \n" + " \n" + " \n" + " Data point 2 \n" + " \n" + " 1 \n" + " \n" + " \n" + " Data point 3 \n" + " \n" + " \n" + " \n" + " \n" +); + +QString ProjectFileXmlTestData::cValueAxisInvalid = QString( + " \n" + " \n" + " \n" + " \n" + " Data point \n" + " \n" + " notanumber \n" + " \n" + " \n" + " \n" +); + +QString ProjectFileXmlTestData::cLogFileRelativePath = QString( + " \n" + " \n" + " \n" + " \n" + " \n" + " subdir/output.csv \n" + " \n" + " \n" + " \n" + " \n" +); + +// clang-format on diff --git a/tests/importexport/projectfilexmltestdata.h b/tests/importexport/projectfilexmltestdata.h new file mode 100644 index 00000000..dbb6122a --- /dev/null +++ b/tests/importexport/projectfilexmltestdata.h @@ -0,0 +1,26 @@ +#ifndef PROJECTFILEXMLTESTDATA_H +#define PROJECTFILEXMLTESTDATA_H + +#include + +class ProjectFileXmlTestData +{ +public: + static QString cTooLowDataLevel; + static QString cTooHighDataLevel; + + static QString cDataLevel3Expressions; + static QString cDataLevel5Connection; + static QString cDataLevel5MixedMulti; + static QString cDataLevel4LegacyConnection; + + static QString cLogSettings; + static QString cScaleSliding; + static QString cScaleMinMax; + static QString cScaleWindowAuto; + static QString cValueAxis; + static QString cValueAxisInvalid; + static QString cLogFileRelativePath; +}; + +#endif // PROJECTFILEXMLTESTDATA_H diff --git a/tests/importexport/tst_projectfilexmlparser.cpp b/tests/importexport/tst_projectfilexmlparser.cpp new file mode 100644 index 00000000..570e1d0f --- /dev/null +++ b/tests/importexport/tst_projectfilexmlparser.cpp @@ -0,0 +1,315 @@ + +#include "tst_projectfilexmlparser.h" + +#include "importexport/projectfilexmlparser.h" +#include "projectfilexmltestdata.h" + +#include +#include +#include +#include +#include +#include + +using ProjectFileData::ProjectSettings; + +void TestProjectFileXmlParser::init() +{ +} + +void TestProjectFileXmlParser::initTestCase() +{ + QLocale::setDefault(QLocale(QLocale::Dutch, QLocale::Belgium)); +} + +void TestProjectFileXmlParser::cleanup() +{ +} + +void TestProjectFileXmlParser::tooLowDataLevel() +{ + ProjectFileXmlParser parser; + ProjectSettings settings; + + GeneralError err = parser.parseFile(ProjectFileXmlTestData::cTooLowDataLevel, &settings); + QVERIFY(!err.result()); +} + +void TestProjectFileXmlParser::tooHighDataLevel() +{ + ProjectFileXmlParser parser; + ProjectSettings settings; + + GeneralError err = parser.parseFile(ProjectFileXmlTestData::cTooHighDataLevel, &settings); + QVERIFY(!err.result()); +} + +void TestProjectFileXmlParser::dataLevel3Expressions() +{ + ProjectFileXmlParser parser; + ProjectSettings settings; + + GeneralError err = parser.parseFile(ProjectFileXmlTestData::cDataLevel3Expressions, &settings); + QVERIFY(err.result()); + + QCOMPARE(settings.scope.registerList.size(), 3); + + QVERIFY(settings.scope.registerList[0].bActive); + QCOMPARE(settings.scope.registerList[0].text, QString("Data point")); + QCOMPARE(settings.scope.registerList[0].expression, QString("${40001}/2")); + QVERIFY(settings.scope.registerList[0].bColor); + QCOMPARE(settings.scope.registerList[0].color, QColor("#ff0000")); + + QVERIFY(settings.scope.registerList[1].bActive); + QCOMPARE(settings.scope.registerList[1].text, QString("Data point 2")); + QCOMPARE(settings.scope.registerList[1].expression, QString("${40002:s16b}")); + QVERIFY(settings.scope.registerList[1].bColor); + QCOMPARE(settings.scope.registerList[1].color, QColor("#0000ff")); + + QVERIFY(!settings.scope.registerList[2].bActive); + QCOMPARE(settings.scope.registerList[2].text, QString("Data point 3")); + QCOMPARE(settings.scope.registerList[2].expression, QString("${40003@2:s16b}*10")); +} + +void TestProjectFileXmlParser::dataLevel5Connection() +{ + ProjectFileXmlParser parser; + ProjectSettings settings; + + GeneralError err = parser.parseFile(ProjectFileXmlTestData::cDataLevel5Connection, &settings); + QVERIFY(err.result()); + + /* Adapter blob */ + QCOMPARE(settings.general.adapterList.size(), 1); + QCOMPARE(settings.general.adapterList[0].type, QString("modbus")); + + QJsonObject adapterSettings = settings.general.adapterList[0].settings; + QVERIFY(adapterSettings.contains("connections")); + QVERIFY(adapterSettings.contains("devices")); + + QJsonArray connections = adapterSettings["connections"].toArray(); + QCOMPARE(connections.size(), 1); + + QJsonObject conn0 = connections[0].toObject(); + QCOMPARE(conn0["id"].toInt(), 0); + QCOMPARE(conn0["enabled"].toBool(), true); + QCOMPARE(conn0["connectiontype"].toString(), QString("tcp")); + QCOMPARE(conn0["ip"].toString(), QString("127.0.0.1")); + QCOMPARE(conn0["port"].toInt(), 502); + QCOMPARE(conn0["portname"].toString(), QString("COM1")); + QCOMPARE(conn0["baudrate"].toInt(), 115200); + QCOMPARE(conn0["parity"].toInt(), 0); + QCOMPARE(conn0["stopbits"].toInt(), 1); + QCOMPARE(conn0["databits"].toInt(), 8); + QCOMPARE(conn0["timeout"].toInt(), 1000); + QVERIFY(conn0["persistentconnection"].toBool()); + + QJsonArray adapterDevices = adapterSettings["devices"].toArray(); + QCOMPARE(adapterDevices.size(), 1); + QJsonObject dev0 = adapterDevices[0].toObject(); + QCOMPARE(dev0["id"].toInt(), 1); + QCOMPARE(dev0["connectionid"].toInt(), 0); + QCOMPARE(dev0["slaveid"].toInt(), 3); + QCOMPARE(dev0["consecutivemax"].toInt(), 100); + QVERIFY(!dev0["int32littleendian"].toBool()); + + /* Generic devices */ + QCOMPARE(settings.general.deviceSettings.size(), 1); + QVERIFY(settings.general.deviceSettings[0].bDeviceId); + QCOMPARE(settings.general.deviceSettings[0].deviceId, static_cast(1)); + QCOMPARE(settings.general.deviceSettings[0].adapterId, static_cast(0)); + QCOMPARE(settings.general.deviceSettings[0].adapterType, QString("modbus")); + QVERIFY(settings.general.deviceSettings[0].bName); + QCOMPARE(settings.general.deviceSettings[0].name, QString("Device 1")); + + /* Log */ + QVERIFY(settings.general.logSettings.bPollTime); + QCOMPARE(settings.general.logSettings.pollTime, static_cast(250)); + QVERIFY(!settings.general.logSettings.bAbsoluteTimes); + QVERIFY(settings.general.logSettings.bLogToFile); +} + +void TestProjectFileXmlParser::dataLevel5MixedMulti() +{ + ProjectFileXmlParser parser; + ProjectSettings settings; + + GeneralError err = parser.parseFile(ProjectFileXmlTestData::cDataLevel5MixedMulti, &settings); + QVERIFY(err.result()); + + /* 3 connections, 3 devices */ + QJsonObject adapterSettings = settings.general.adapterList[0].settings; + QJsonArray connections = adapterSettings["connections"].toArray(); + QCOMPARE(connections.size(), 3); + + /* First connection: serial */ + QJsonObject conn0 = connections[0].toObject(); + QCOMPARE(conn0["connectiontype"].toString(), QString("serial")); + QCOMPARE(conn0["portname"].toString(), QString("COM10")); + QCOMPARE(conn0["baudrate"].toInt(), 38400); + QCOMPARE(conn0["timeout"].toInt(), 500); + + /* Third connection: disabled */ + QJsonObject conn2 = connections[2].toObject(); + QVERIFY(!conn2["enabled"].toBool()); + + /* 3 generic devices */ + QCOMPARE(settings.general.deviceSettings.size(), 3); + QCOMPARE(settings.general.deviceSettings[0].deviceId, static_cast(1)); + QCOMPARE(settings.general.deviceSettings[0].name, QString("Device 1 (serial 1)")); + QCOMPARE(settings.general.deviceSettings[1].deviceId, static_cast(2)); + QCOMPARE(settings.general.deviceSettings[1].name, QString("Device 2 (serial 2)")); + QCOMPARE(settings.general.deviceSettings[2].deviceId, static_cast(3)); + QCOMPARE(settings.general.deviceSettings[2].name, QString("Device 3 (TCP)")); + + /* Adapter devices */ + QJsonArray adapterDevices = adapterSettings["devices"].toArray(); + QCOMPARE(adapterDevices.size(), 3); + QCOMPARE(adapterDevices[0].toObject()["connectionid"].toInt(), 0); + QCOMPARE(adapterDevices[0].toObject()["slaveid"].toInt(), 1); + QCOMPARE(adapterDevices[2].toObject()["connectionid"].toInt(), 1); +} + +void TestProjectFileXmlParser::dataLevel4LegacyConnection() +{ + ProjectFileXmlParser parser; + ProjectSettings settings; + + GeneralError err = parser.parseFile(ProjectFileXmlTestData::cDataLevel4LegacyConnection, &settings); + QVERIFY(err.result()); + + /* Only enabled connections create devices in legacy mode */ + QCOMPARE(settings.general.deviceSettings.size(), 1); + QVERIFY(settings.general.deviceSettings[0].bDeviceId); + QCOMPARE(settings.general.deviceSettings[0].deviceId, static_cast(1)); + + /* Adapter blob should have both connections */ + QJsonObject adapterSettings = settings.general.adapterList[0].settings; + QJsonArray connections = adapterSettings["connections"].toArray(); + QCOMPARE(connections.size(), 2); + + /* First connection TCP */ + QJsonObject conn0 = connections[0].toObject(); + QCOMPARE(conn0["connectiontype"].toString(), QString("tcp")); + QCOMPARE(conn0["timeout"].toInt(), 1002); + + /* Only one adapter device (from the enabled connection) */ + QJsonArray adapterDevices = adapterSettings["devices"].toArray(); + QCOMPARE(adapterDevices.size(), 1); + QCOMPARE(adapterDevices[0].toObject()["slaveid"].toInt(), 2); + QCOMPARE(adapterDevices[0].toObject()["consecutivemax"].toInt(), 122); + QVERIFY(adapterDevices[0].toObject()["int32littleendian"].toBool()); +} + +void TestProjectFileXmlParser::logSettings() +{ + ProjectFileXmlParser parser; + ProjectSettings settings; + + GeneralError err = parser.parseFile(ProjectFileXmlTestData::cLogSettings, &settings); + QVERIFY(err.result()); + + QVERIFY(settings.general.logSettings.bPollTime); + QCOMPARE(settings.general.logSettings.pollTime, static_cast(750)); + QVERIFY(settings.general.logSettings.bAbsoluteTimes); + QVERIFY(!settings.general.logSettings.bLogToFile); +} + +void TestProjectFileXmlParser::scaleSliding() +{ + ProjectFileXmlParser parser; + ProjectSettings settings; + + GeneralError err = parser.parseFile(ProjectFileXmlTestData::cScaleSliding, &settings); + QVERIFY(err.result()); + + QVERIFY(settings.view.scaleSettings.xAxis.bSliding); + QCOMPARE(settings.view.scaleSettings.xAxis.slidingInterval, static_cast(20)); +} + +void TestProjectFileXmlParser::scaleMinMax() +{ + ProjectFileXmlParser parser; + ProjectSettings settings; + + GeneralError err = parser.parseFile(ProjectFileXmlTestData::cScaleMinMax, &settings); + QVERIFY(err.result()); + + QVERIFY(settings.view.scaleSettings.xAxis.bSliding); + QCOMPARE(settings.view.scaleSettings.xAxis.slidingInterval, static_cast(20)); + + QVERIFY(settings.view.scaleSettings.yAxis.bMinMax); + QVERIFY(qFuzzyIsNull(settings.view.scaleSettings.yAxis.scaleMin)); + QCOMPARE(settings.view.scaleSettings.yAxis.scaleMax, 25.5); +} + +void TestProjectFileXmlParser::scaleWindowAuto() +{ + ProjectFileXmlParser parser; + ProjectSettings settings; + + GeneralError err = parser.parseFile(ProjectFileXmlTestData::cScaleWindowAuto, &settings); + QVERIFY(err.result()); + + QVERIFY(settings.view.scaleSettings.yAxis.bWindowScale); + QVERIFY(!settings.view.scaleSettings.yAxis.bMinMax); + + QVERIFY(settings.view.scaleSettings.y2Axis.bMinMax); + QVERIFY(qFuzzyIsNull(settings.view.scaleSettings.y2Axis.scaleMin)); + QCOMPARE(settings.view.scaleSettings.y2Axis.scaleMax, 25.5); +} + +void TestProjectFileXmlParser::valueAxis() +{ + ProjectFileXmlParser parser; + ProjectSettings settings; + + GeneralError err = parser.parseFile(ProjectFileXmlTestData::cValueAxis, &settings); + QVERIFY(err.result()); + + QCOMPARE(settings.scope.registerList.size(), 3); + + QCOMPARE(settings.scope.registerList[0].valueAxis, static_cast(0)); + QCOMPARE(settings.scope.registerList[1].valueAxis, static_cast(1)); + QCOMPARE(settings.scope.registerList[2].valueAxis, static_cast(0)); +} + +void TestProjectFileXmlParser::valueAxisInvalid() +{ + ProjectFileXmlParser parser; + ProjectSettings settings; + + GeneralError err = parser.parseFile(ProjectFileXmlTestData::cValueAxisInvalid, &settings); + QVERIFY(!err.result()); +} + +void TestProjectFileXmlParser::logFileRelativePath() +{ + /* Create a temporary project directory that contains "subdir/" so the relative path is valid */ + QTemporaryDir tempDir; + QVERIFY(tempDir.isValid()); + QVERIFY(QDir(tempDir.path()).mkdir("subdir")); + + ProjectFileXmlParser parserWithBase; + ProjectSettings settingsWithBase; + GeneralError errWithBase = + parserWithBase.parseFile(ProjectFileXmlTestData::cLogFileRelativePath, &settingsWithBase, tempDir.path()); + QVERIFY(errWithBase.result()); + QVERIFY(settingsWithBase.general.logSettings.bLogToFileFile); + QVERIFY(!settingsWithBase.general.logSettings.logFile.isEmpty()); + + /* A different base dir without "subdir/" must reject the relative path */ + QTemporaryDir wrongDir; + QVERIFY(wrongDir.isValid()); + + ProjectFileXmlParser parserWrongBase; + ProjectSettings settingsWrongBase; + GeneralError errWrongBase = + parserWrongBase.parseFile(ProjectFileXmlTestData::cLogFileRelativePath, &settingsWrongBase, wrongDir.path()); + QVERIFY(errWrongBase.result()); + QVERIFY(!settingsWrongBase.general.logSettings.bLogToFileFile); +} + +QTEST_MAIN(TestProjectFileXmlParser) + +#include "tst_projectfilexmlparser.moc" diff --git a/tests/importexport/tst_projectfilexmlparser.h b/tests/importexport/tst_projectfilexmlparser.h new file mode 100644 index 00000000..55c887ee --- /dev/null +++ b/tests/importexport/tst_projectfilexmlparser.h @@ -0,0 +1,36 @@ + +#ifndef TST_PROJECTFILEXMLPARSER_H +#define TST_PROJECTFILEXMLPARSER_H + +#include + +class TestProjectFileXmlParser : public QObject +{ + Q_OBJECT +private slots: + void init(); + void initTestCase(); + void cleanup(); + + void tooLowDataLevel(); + void tooHighDataLevel(); + + void dataLevel3Expressions(); + + void dataLevel5Connection(); + void dataLevel5MixedMulti(); + void dataLevel4LegacyConnection(); + + void logSettings(); + + void scaleSliding(); + void scaleMinMax(); + void scaleWindowAuto(); + + void valueAxis(); + void valueAxisInvalid(); + + void logFileRelativePath(); +}; + +#endif // TST_PROJECTFILEXMLPARSER_H