From c16314c426caf9da48d3f834d2c1bbbb2407177a Mon Sep 17 00:00:00 2001 From: Michael Zillgith Date: Thu, 29 Jun 2023 16:54:54 +0100 Subject: [PATCH 01/79] - config file parser: added support for arrays of basic and complex data attributes including initialization (LIB61850-415) --- .../server/model/config_file_parser.c | 295 +++++++++++------- 1 file changed, 189 insertions(+), 106 deletions(-) diff --git a/src/iec61850/server/model/config_file_parser.c b/src/iec61850/server/model/config_file_parser.c index 8f2abeca..55a9f937 100644 --- a/src/iec61850/server/model/config_file_parser.c +++ b/src/iec61850/server/model/config_file_parser.c @@ -1,7 +1,7 @@ /* * config_file_parser.c * - * Copyright 2014-2022 Michael Zillgith + * Copyright 2014-2023 Michael Zillgith * * This file is part of libIEC61850. * @@ -28,6 +28,8 @@ #include "libiec61850_platform_includes.h" #include "stack_config.h" +#include + #define READ_BUFFER_MAX_SIZE 1024 static uint8_t lineBuffer[READ_BUFFER_MAX_SIZE]; @@ -114,6 +116,112 @@ ConfigFileParser_createModelFromConfigFileEx(const char* filename) return model; } +static bool +setValue(char* lineBuffer, DataAttribute* dataAttribute) +{ + char* valueIndicator = strchr((char*) lineBuffer, '='); + + if (valueIndicator != NULL) { + switch (dataAttribute->type) { + case IEC61850_UNICODE_STRING_255: + { + char* stringStart = valueIndicator + 2; + terminateString(stringStart, '"'); + dataAttribute->mmsValue = MmsValue_newMmsString(stringStart); + } + break; + + case IEC61850_VISIBLE_STRING_255: + case IEC61850_VISIBLE_STRING_129: + case IEC61850_VISIBLE_STRING_65: + case IEC61850_VISIBLE_STRING_64: + case IEC61850_VISIBLE_STRING_32: + case IEC61850_CURRENCY: + { + char* stringStart = valueIndicator + 2; + terminateString(stringStart, '"'); + dataAttribute->mmsValue = MmsValue_newVisibleString(stringStart); + } + break; + + case IEC61850_INT8: + case IEC61850_INT16: + case IEC61850_INT32: + case IEC61850_INT64: + case IEC61850_INT128: + case IEC61850_ENUMERATED: + { + int32_t intValue; + if (sscanf(valueIndicator + 1, "%i", &intValue) != 1) goto exit_error; + dataAttribute->mmsValue = MmsValue_newIntegerFromInt32(intValue); + } + break; + + case IEC61850_INT8U: + case IEC61850_INT16U: + case IEC61850_INT24U: + case IEC61850_INT32U: + { + uint32_t uintValue; + if (sscanf(valueIndicator + 1, "%u", &uintValue) != 1) goto exit_error; + dataAttribute->mmsValue = MmsValue_newUnsignedFromUint32(uintValue); + } + break; + + case IEC61850_FLOAT32: + { + float floatValue; + if (sscanf(valueIndicator + 1, "%f", &floatValue) != 1) goto exit_error; + dataAttribute->mmsValue = MmsValue_newFloat(floatValue); + } + break; + + case IEC61850_FLOAT64: + { + double doubleValue; + if (sscanf(valueIndicator + 1, "%lf", &doubleValue) != 1) goto exit_error; + dataAttribute->mmsValue = MmsValue_newDouble(doubleValue); + } + break; + + case IEC61850_BOOLEAN: + { + int boolean; + if (sscanf(valueIndicator + 1, "%i", &boolean) != 1) goto exit_error; + dataAttribute->mmsValue = MmsValue_newBoolean((bool) boolean); + } + break; + + case IEC61850_OPTFLDS: + { + int value; + if (sscanf(valueIndicator + 1, "%i", &value) != 1) goto exit_error; + dataAttribute->mmsValue = MmsValue_newBitString(-10); + MmsValue_setBitStringFromIntegerBigEndian(dataAttribute->mmsValue, value); + } + break; + + case IEC61850_TRGOPS: + { + int value; + if (sscanf(valueIndicator + 1, "%i", &value) != 1) goto exit_error; + dataAttribute->mmsValue = MmsValue_newBitString(-6); + MmsValue_setBitStringFromIntegerBigEndian(dataAttribute->mmsValue, value); + } + break; + + default: + break; + + } + } + + return true; + +exit_error: + return false; +} + IedModel* ConfigFileParser_createModelFromConfigFile(FileHandle fileHandle) { @@ -121,11 +229,14 @@ ConfigFileParser_createModelFromConfigFile(FileHandle fileHandle) bool stateInModel = false; int indendation = 0; + bool inArray = false; + bool inArrayElement = false; IedModel* model = NULL; LogicalDevice* currentLD = NULL; LogicalNode* currentLN = NULL; ModelNode* currentModelNode = NULL; + ModelNode* currentArrayNode = NULL; DataSet* currentDataSet = NULL; GSEControlBlock* currentGoCB = NULL; SVControlBlock* currentSMVCB = NULL; @@ -144,6 +255,18 @@ ConfigFileParser_createModelFromConfigFile(FileHandle fileHandle) if (bytesRead > 0) { lineBuffer[bytesRead] = 0; + /* trim trailing spaces */ + while (bytesRead > 1) { + bytesRead--; + + if (isspace(lineBuffer[bytesRead])) { + lineBuffer[bytesRead] = 0; + } + else { + break; + } + } + if (stateInModel) { if (StringUtils_startsWith((char*) lineBuffer, "}")) { @@ -161,11 +284,21 @@ ConfigFileParser_createModelFromConfigFile(FileHandle fileHandle) indendation = 3; } else if (indendation > 4) { + + if (inArrayElement && currentModelNode->parent == currentArrayNode) { + inArrayElement = false; + } + else { + indendation--; + } + + if (inArray && currentModelNode == currentArrayNode) { + inArray = false; + } + currentModelNode = currentModelNode->parent; - indendation--; } } - else if (indendation == 1) { if (StringUtils_startsWith((char*) lineBuffer, "LD")) { indendation = 2; @@ -210,7 +343,9 @@ ConfigFileParser_createModelFromConfigFile(FileHandle fileHandle) int arrayElements = 0; - sscanf((char*) lineBuffer, "DO(%129s %i)", nameString, &arrayElements); + if (sscanf((char*)lineBuffer, "DO(%129s %i)", nameString, &arrayElements) != 2) { + goto exit_error; + } currentModelNode = (ModelNode*) DataObject_create(nameString, (ModelNode*) currentLN, arrayElements); @@ -218,7 +353,10 @@ ConfigFileParser_createModelFromConfigFile(FileHandle fileHandle) else if (StringUtils_startsWith((char*) lineBuffer, "DS")) { indendation = 4; - sscanf((char*) lineBuffer, "DS(%129s)", nameString); + if (sscanf((char*)lineBuffer, "DS(%129s)", nameString) != 1) { + goto exit_error; + } + terminateString(nameString, ')'); currentDataSet = DataSet_create(nameString, currentLN); @@ -296,7 +434,6 @@ ConfigFileParser_createModelFromConfigFile(FileHandle fileHandle) nameString3, confRef, fixedOffs, minTime, maxTime); indendation = 4; - } else if (StringUtils_startsWith((char*) lineBuffer, "SMVC")) { uint32_t confRev; @@ -313,7 +450,6 @@ ConfigFileParser_createModelFromConfigFile(FileHandle fileHandle) currentSMVCB = SVControlBlock_create(nameString, currentLN, nameString2, nameString3, confRev, smpMod, smpRate, optFlds, (bool) isUnicast); indendation = 4; - } #if (CONFIG_IEC61850_SETTING_GROUPS == 1) else if (StringUtils_startsWith((char*) lineBuffer, "SG")) { @@ -357,6 +493,41 @@ ConfigFileParser_createModelFromConfigFile(FileHandle fileHandle) currentModelNode = (ModelNode*) DataObject_create(nameString, currentModelNode, arrayElements); } + else if (StringUtils_startsWith((char*) lineBuffer, "[")) { + if (inArray == false) { + goto exit_error; + } + + int arrayIndex; + + if (sscanf((char*)lineBuffer, "[%i]", &arrayIndex) != 1) { + goto exit_error; + } + + if (StringUtils_endsWith((char*)lineBuffer, ";")) { + /* array of basic data attribute */ + ModelNode* arrayElementNode = ModelNode_getChildWithIdx(currentArrayNode, arrayIndex); + + if (arrayElementNode) { + setValue((char*)lineBuffer, (DataAttribute*)arrayElementNode); + } + else { + goto exit_error; + } + + } + else if (StringUtils_endsWith((char*)lineBuffer, "{")) { + /* array of constructed data attribtute */ + currentModelNode = ModelNode_getChildWithIdx(currentArrayNode, arrayIndex); + + if (currentModelNode) { + inArrayElement = true; + } + else { + goto exit_error; + } + } + } else if (StringUtils_startsWith((char*) lineBuffer, "DA")) { int arrayElements = 0; @@ -366,108 +537,20 @@ ConfigFileParser_createModelFromConfigFile(FileHandle fileHandle) int triggerOptions = 0; uint32_t sAddr = 0; - sscanf((char*) lineBuffer, "DA(%129s %i %i %i %i %u)", nameString, &arrayElements, &attributeType, &functionalConstraint, &triggerOptions, &sAddr); + if (sscanf((char*)lineBuffer, "DA(%129s %i %i %i %i %u)", nameString, &arrayElements, &attributeType, &functionalConstraint, &triggerOptions, &sAddr) != 6) { + goto exit_error; + } DataAttribute* dataAttribute = DataAttribute_create(nameString, currentModelNode, (DataAttributeType) attributeType, (FunctionalConstraint) functionalConstraint, triggerOptions, arrayElements, sAddr); - char* valueIndicator = strchr((char*) lineBuffer, '='); - - if (valueIndicator != NULL) { - switch (dataAttribute->type) { - case IEC61850_UNICODE_STRING_255: - { - char* stringStart = valueIndicator + 2; - terminateString(stringStart, '"'); - dataAttribute->mmsValue = MmsValue_newMmsString(stringStart); - } - break; - - case IEC61850_VISIBLE_STRING_255: - case IEC61850_VISIBLE_STRING_129: - case IEC61850_VISIBLE_STRING_65: - case IEC61850_VISIBLE_STRING_64: - case IEC61850_VISIBLE_STRING_32: - case IEC61850_CURRENCY: - { - char* stringStart = valueIndicator + 2; - terminateString(stringStart, '"'); - dataAttribute->mmsValue = MmsValue_newVisibleString(stringStart); - } - break; - - case IEC61850_INT8: - case IEC61850_INT16: - case IEC61850_INT32: - case IEC61850_INT64: - case IEC61850_INT128: - case IEC61850_ENUMERATED: - { - int32_t intValue; - if (sscanf(valueIndicator + 1, "%i", &intValue) != 1) goto exit_error; - dataAttribute->mmsValue = MmsValue_newIntegerFromInt32(intValue); - } - break; - - case IEC61850_INT8U: - case IEC61850_INT16U: - case IEC61850_INT24U: - case IEC61850_INT32U: - { - uint32_t uintValue; - if (sscanf(valueIndicator + 1, "%u", &uintValue) != 1) goto exit_error; - dataAttribute->mmsValue = MmsValue_newUnsignedFromUint32(uintValue); - } - break; - - case IEC61850_FLOAT32: - { - float floatValue; - if (sscanf(valueIndicator + 1, "%f", &floatValue) != 1) goto exit_error; - dataAttribute->mmsValue = MmsValue_newFloat(floatValue); - } - break; - - case IEC61850_FLOAT64: - { - double doubleValue; - if (sscanf(valueIndicator + 1, "%lf", &doubleValue) != 1) goto exit_error; - dataAttribute->mmsValue = MmsValue_newDouble(doubleValue); - } - break; - - case IEC61850_BOOLEAN: - { - int boolean; - if (sscanf(valueIndicator + 1, "%i", &boolean) != 1) goto exit_error; - dataAttribute->mmsValue = MmsValue_newBoolean((bool) boolean); - } - break; - - case IEC61850_OPTFLDS: - { - int value; - if (sscanf(valueIndicator + 1, "%i", &value) != 1) goto exit_error; - dataAttribute->mmsValue = MmsValue_newBitString(-10); - MmsValue_setBitStringFromIntegerBigEndian(dataAttribute->mmsValue, value); - } - break; - - case IEC61850_TRGOPS: - { - int value; - if (sscanf(valueIndicator + 1, "%i", &value) != 1) goto exit_error; - dataAttribute->mmsValue = MmsValue_newBitString(-6); - MmsValue_setBitStringFromIntegerBigEndian(dataAttribute->mmsValue, value); - } - break; - - default: - break; - - } + if (arrayElements > 0) { + inArray = true; + currentArrayNode = (ModelNode*)dataAttribute; } + setValue((char*)lineBuffer, dataAttribute); + int lineLength = (int) strlen((char*) lineBuffer); if (lineBuffer[lineLength - 1] == '{') { @@ -541,8 +624,6 @@ ConfigFileParser_createModelFromConfigFile(FileHandle fileHandle) else goto exit_error; } - - } else { if (StringUtils_startsWith((char*) lineBuffer, "MODEL{")) { @@ -552,7 +633,9 @@ ConfigFileParser_createModelFromConfigFile(FileHandle fileHandle) indendation = 1; } else if (StringUtils_startsWith((char*) lineBuffer, "MODEL(")) { - sscanf((char*) lineBuffer, "MODEL(%129s)", nameString); + if (sscanf((char*)lineBuffer, "MODEL(%129s)", nameString) != 1) + goto exit_error; + terminateString(nameString, ')'); model = IedModel_create(nameString); stateInModel = true; From 665501c9faf57474a4d57ac2447e698ac2f4545b Mon Sep 17 00:00:00 2001 From: Michael Zillgith Date: Thu, 13 Jul 2023 19:36:43 +0100 Subject: [PATCH 02/79] - extended data model helper function to be able to be used in array elements --- src/iec61850/server/model/cdc.c | 87 ++++++++++++++++----------------- 1 file changed, 43 insertions(+), 44 deletions(-) diff --git a/src/iec61850/server/model/cdc.c b/src/iec61850/server/model/cdc.c index 122fa8bd..211e925e 100644 --- a/src/iec61850/server/model/cdc.c +++ b/src/iec61850/server/model/cdc.c @@ -3,7 +3,7 @@ * * Helper functions for the dynamic creation of Common Data Classes (CDCs) * - * Copyright 2014 Michael Zillgith + * Copyright 2014-2023 Michael Zillgith * * This file is part of libIEC61850. * @@ -35,7 +35,7 @@ DataAttribute* CAC_AnalogueValue_create(const char* name, ModelNode* parent, FunctionalConstraint fc, uint8_t triggerOptions, bool isIntegerNotFloat) { - DataAttribute* analogeValue = DataAttribute_create(name, parent, IEC61850_CONSTRUCTED, fc, triggerOptions, 0, 0); + DataAttribute* analogeValue = (name == NULL) ? (DataAttribute*)parent : DataAttribute_create(name, parent, IEC61850_CONSTRUCTED, fc, triggerOptions, 0, 0); if (isIntegerNotFloat) DataAttribute_create("i", (ModelNode*) analogeValue, IEC61850_INT32, fc, triggerOptions, 0, 0); @@ -48,7 +48,7 @@ CAC_AnalogueValue_create(const char* name, ModelNode* parent, FunctionalConstrai DataAttribute* CAC_ValWithTrans_create(const char* name, ModelNode* parent, FunctionalConstraint fc, uint8_t triggerOptions, bool hasTransientIndicator) { - DataAttribute* valWithTrans = DataAttribute_create(name, parent, IEC61850_CONSTRUCTED, fc, triggerOptions, 0, 0); + DataAttribute* valWithTrans = (name == NULL) ? (DataAttribute*)parent : DataAttribute_create(name, parent, IEC61850_CONSTRUCTED, fc, triggerOptions, 0, 0); DataAttribute_create("posVal", (ModelNode*) valWithTrans, IEC61850_INT8, fc, triggerOptions, 0, 0); @@ -64,7 +64,7 @@ CAC_ValWithTrans_create(const char* name, ModelNode* parent, FunctionalConstrain DataAttribute* CAC_Vector_create(const char* name, ModelNode* parent, uint32_t options, FunctionalConstraint fc, uint8_t triggerOptions) { - DataAttribute* vector = DataAttribute_create(name, parent, IEC61850_CONSTRUCTED, fc, triggerOptions, 0, 0); + DataAttribute* vector = (name == NULL) ? (DataAttribute*)parent : DataAttribute_create(name, parent, IEC61850_CONSTRUCTED, fc, triggerOptions, 0, 0); CAC_AnalogueValue_create("mag", (ModelNode*) vector, fc, triggerOptions, false); @@ -77,7 +77,7 @@ CAC_Vector_create(const char* name, ModelNode* parent, uint32_t options, Functio DataAttribute* CAC_Point_create(const char* name, ModelNode* parent, FunctionalConstraint fc, uint8_t triggerOptions, bool hasZVal) { - DataAttribute* point = DataAttribute_create(name, parent, IEC61850_CONSTRUCTED, fc, triggerOptions, 0, 0); + DataAttribute* point = (name == NULL) ? (DataAttribute*)parent : DataAttribute_create(name, parent, IEC61850_CONSTRUCTED, fc, triggerOptions, 0, 0); DataAttribute_create("xVal", (ModelNode*) point, IEC61850_FLOAT32, fc, triggerOptions, 0, 0); DataAttribute_create("yVal", (ModelNode*) point, IEC61850_FLOAT32, fc, triggerOptions, 0, 0); @@ -91,7 +91,7 @@ CAC_Point_create(const char* name, ModelNode* parent, FunctionalConstraint fc, u DataAttribute* CAC_ScaledValueConfig_create(const char* name, ModelNode* parent) { - DataAttribute* scaling = DataAttribute_create(name, parent, IEC61850_CONSTRUCTED, IEC61850_FC_CF, TRG_OPT_DATA_CHANGED, 0, 0); + DataAttribute* scaling = (name == NULL) ? (DataAttribute*)parent : DataAttribute_create(name, parent, IEC61850_CONSTRUCTED, IEC61850_FC_CF, TRG_OPT_DATA_CHANGED, 0, 0); DataAttribute_create("scaleFactor", (ModelNode*) scaling, IEC61850_FLOAT32, IEC61850_FC_CF, TRG_OPT_DATA_CHANGED, 0, 0); DataAttribute_create("offset", (ModelNode*) scaling, IEC61850_FLOAT32, IEC61850_FC_CF, TRG_OPT_DATA_CHANGED, 0, 0); @@ -102,7 +102,7 @@ CAC_ScaledValueConfig_create(const char* name, ModelNode* parent) DataAttribute* CAC_Unit_create(const char* name, ModelNode* parent, bool hasMagnitude) { - DataAttribute* unit = DataAttribute_create(name, parent, IEC61850_CONSTRUCTED, IEC61850_FC_CF, TRG_OPT_DATA_CHANGED, 0, 0); + DataAttribute* unit = (name == NULL) ? (DataAttribute*)parent : DataAttribute_create(name, parent, IEC61850_CONSTRUCTED, IEC61850_FC_CF, TRG_OPT_DATA_CHANGED, 0, 0); DataAttribute_create("SIUnit", (ModelNode*) unit, IEC61850_ENUMERATED, IEC61850_FC_CF, TRG_OPT_DATA_CHANGED, 0, 0); @@ -255,7 +255,7 @@ CDC_addStandardOptions(DataObject* dataObject, uint32_t options) DataObject* CDC_SPS_create(const char* dataObjectName, ModelNode* parent, uint32_t options) { - DataObject* newSPS = DataObject_create(dataObjectName, parent, 0); + DataObject* newSPS = (dataObjectName == NULL) ? (DataObject*)parent : DataObject_create(dataObjectName, parent, 0); CDC_addStatusToDataObject(newSPS, IEC61850_BOOLEAN); @@ -273,7 +273,7 @@ CDC_SPS_create(const char* dataObjectName, ModelNode* parent, uint32_t options) DataObject* CDC_DPS_create(const char* dataObjectName, ModelNode* parent, uint32_t options) { - DataObject* newDPS = DataObject_create(dataObjectName, parent, 0); + DataObject* newDPS = (dataObjectName == NULL) ? (DataObject*)parent : DataObject_create(dataObjectName, parent, 0); CDC_addStatusToDataObject(newDPS, IEC61850_CODEDENUM); @@ -291,7 +291,7 @@ CDC_DPS_create(const char* dataObjectName, ModelNode* parent, uint32_t options) DataObject* CDC_INS_create(const char* dataObjectName, ModelNode* parent, uint32_t options) { - DataObject* newINS = DataObject_create(dataObjectName, parent, 0); + DataObject* newINS = (dataObjectName == NULL) ? (DataObject*)parent : DataObject_create(dataObjectName, parent, 0); CDC_addStatusToDataObject(newINS, IEC61850_INT32); @@ -310,7 +310,7 @@ CDC_INS_create(const char* dataObjectName, ModelNode* parent, uint32_t options) DataObject* CDC_ENS_create(const char* dataObjectName, ModelNode* parent, uint32_t options) { - DataObject* newENS = DataObject_create(dataObjectName, parent, 0); + DataObject* newENS = (dataObjectName == NULL) ? (DataObject*)parent : DataObject_create(dataObjectName, parent, 0); CDC_addStatusToDataObject(newENS, IEC61850_ENUMERATED); @@ -328,7 +328,7 @@ CDC_ENS_create(const char* dataObjectName, ModelNode* parent, uint32_t options) DataObject* CDC_BCR_create(const char* dataObjectName, ModelNode* parent, uint32_t options) { - DataObject* newBCR = DataObject_create(dataObjectName, parent, 0); + DataObject* newBCR = (dataObjectName == NULL) ? (DataObject*)parent : DataObject_create(dataObjectName, parent, 0); DataAttribute_create("actVal", (ModelNode*) newBCR, IEC61850_INT64, IEC61850_FC_ST, TRG_OPT_DATA_CHANGED, 0, 0); @@ -359,7 +359,7 @@ CDC_BCR_create(const char* dataObjectName, ModelNode* parent, uint32_t options) DataObject* CDC_SEC_create(const char* dataObjectName, ModelNode* parent, uint32_t options) { - DataObject* newSEC = DataObject_create(dataObjectName, parent, 0); + DataObject* newSEC = (dataObjectName == NULL) ? (DataObject*)parent : DataObject_create(dataObjectName, parent, 0); DataAttribute_create("cnt", (ModelNode*) newSEC, IEC61850_INT32U, IEC61850_FC_ST, TRG_OPT_DATA_CHANGED, 0, 0); DataAttribute_create("sev", (ModelNode*) newSEC, IEC61850_ENUMERATED, IEC61850_FC_ST, 0, 0, 0); @@ -380,7 +380,7 @@ CDC_SEC_create(const char* dataObjectName, ModelNode* parent, uint32_t options) DataObject* CDC_VSS_create(const char* dataObjectName, ModelNode* parent, uint32_t options) { - DataObject* newSPS = DataObject_create(dataObjectName, parent, 0); + DataObject* newSPS = (dataObjectName == NULL) ? (DataObject*)parent : DataObject_create(dataObjectName, parent, 0); CDC_addStatusToDataObject(newSPS, IEC61850_VISIBLE_STRING_255); @@ -403,7 +403,7 @@ CDC_VSS_create(const char* dataObjectName, ModelNode* parent, uint32_t options) DataObject* CDC_MV_create(const char* dataObjectName, ModelNode* parent, uint32_t options, bool isIntegerNotFloat) { - DataObject* newMV = DataObject_create(dataObjectName, parent, 0); + DataObject* newMV = (dataObjectName == NULL) ? (DataObject*)parent : DataObject_create(dataObjectName, parent, 0); if (options & CDC_OPTION_INST_MAG) CAC_AnalogueValue_create("instMag", (ModelNode*) newMV, IEC61850_FC_MX, 0, isIntegerNotFloat); @@ -432,7 +432,7 @@ CDC_MV_create(const char* dataObjectName, ModelNode* parent, uint32_t options, b DataObject* CDC_CMV_create(const char* dataObjectName, ModelNode* parent, uint32_t options) { - DataObject* newMV = DataObject_create(dataObjectName, parent, 0); + DataObject* newMV = (dataObjectName == NULL) ? (DataObject*)parent : DataObject_create(dataObjectName, parent, 0); if (options & CDC_OPTION_INST_MAG) CAC_Vector_create("instCVal", (ModelNode*) newMV, options, IEC61850_FC_MX, 0); @@ -465,7 +465,7 @@ CDC_CMV_create(const char* dataObjectName, ModelNode* parent, uint32_t options) DataObject* CDC_SAV_create(const char* dataObjectName, ModelNode* parent, uint32_t options, bool isIntegerNotFloat) { - DataObject* newSAV = DataObject_create(dataObjectName, parent, 0); + DataObject* newSAV = (dataObjectName == NULL) ? (DataObject*)parent : DataObject_create(dataObjectName, parent, 0); CAC_AnalogueValue_create("instMag", (ModelNode*) newSAV, IEC61850_FC_MX, 0, isIntegerNotFloat); @@ -491,7 +491,7 @@ CDC_SAV_create(const char* dataObjectName, ModelNode* parent, uint32_t options, DataObject* CDC_HST_create(const char* dataObjectName, ModelNode* parent, uint32_t options, uint16_t maxPts) { - DataObject* newHST = DataObject_create(dataObjectName, parent, 0); + DataObject* newHST = (dataObjectName == NULL) ? (DataObject*)parent : DataObject_create(dataObjectName, parent, 0); DataAttribute_create("hstVal", (ModelNode*) newHST, IEC61850_INT32, IEC61850_FC_ST, TRG_OPT_DATA_CHANGED | TRG_OPT_DATA_UPDATE, maxPts, 0); @@ -577,7 +577,7 @@ addCommonControlAttributes(DataObject* dobj, uint32_t controlOptions) DataObject* CDC_SPC_create(const char* dataObjectName, ModelNode* parent, uint32_t options, uint32_t controlOptions) { - DataObject* newSPC = DataObject_create(dataObjectName, parent, 0); + DataObject* newSPC = (dataObjectName == NULL) ? (DataObject*)parent : DataObject_create(dataObjectName, parent, 0); addOriginatorAndCtlNumOptions((ModelNode*) newSPC, controlOptions); @@ -613,7 +613,7 @@ CDC_SPC_create(const char* dataObjectName, ModelNode* parent, uint32_t options, DataObject* CDC_DPC_create(const char* dataObjectName, ModelNode* parent, uint32_t options, uint32_t controlOptions) { - DataObject* newDPC = DataObject_create(dataObjectName, parent, 0); + DataObject* newDPC = (dataObjectName == NULL) ? (DataObject*)parent : DataObject_create(dataObjectName, parent, 0); addOriginatorAndCtlNumOptions((ModelNode*) newDPC, controlOptions); @@ -695,7 +695,7 @@ addControlStatusAttributesForAnalogControl(DataObject* dobj, uint32_t controlOpt DataObject* CDC_APC_create(const char* dataObjectName, ModelNode* parent, uint32_t options, uint32_t controlOptions, bool isIntegerNotFloat) { - DataObject* newAPC = DataObject_create(dataObjectName, parent, 0); + DataObject* newAPC = (dataObjectName == NULL) ? (DataObject*)parent : DataObject_create(dataObjectName, parent, 0); addControlStatusAttributesForAnalogControl(newAPC, controlOptions); @@ -728,7 +728,7 @@ CDC_APC_create(const char* dataObjectName, ModelNode* parent, uint32_t options, DataObject* CDC_INC_create(const char* dataObjectName, ModelNode* parent, uint32_t options, uint32_t controlOptions) { - DataObject* newINC = DataObject_create(dataObjectName, parent, 0); + DataObject* newINC = (dataObjectName == NULL) ? (DataObject*)parent : DataObject_create(dataObjectName, parent, 0); addOriginatorAndCtlNumOptions((ModelNode*) newINC, controlOptions); @@ -764,7 +764,7 @@ CDC_INC_create(const char* dataObjectName, ModelNode* parent, uint32_t options, DataObject* CDC_ENC_create(const char* dataObjectName, ModelNode* parent, uint32_t options, uint32_t controlOptions) { - DataObject* newENC = DataObject_create(dataObjectName, parent, 0); + DataObject* newENC = (dataObjectName == NULL) ? (DataObject*)parent : DataObject_create(dataObjectName, parent, 0); addOriginatorAndCtlNumOptions((ModelNode*) newENC, controlOptions); @@ -791,7 +791,7 @@ CDC_ENC_create(const char* dataObjectName, ModelNode* parent, uint32_t options, DataObject* CDC_BSC_create(const char* dataObjectName, ModelNode* parent, uint32_t options, uint32_t controlOptions, bool hasTransientIndicator) { - DataObject* newBSC = DataObject_create(dataObjectName, parent, 0); + DataObject* newBSC = (dataObjectName == NULL) ? (DataObject*)parent : DataObject_create(dataObjectName, parent, 0); addOriginatorAndCtlNumOptions((ModelNode*) newBSC, controlOptions); @@ -821,7 +821,7 @@ CDC_BSC_create(const char* dataObjectName, ModelNode* parent, uint32_t options, DataObject* CDC_ISC_create(const char* dataObjectName, ModelNode* parent, uint32_t options, uint32_t controlOptions, bool hasTransientIndicator) { - DataObject* newISC = DataObject_create(dataObjectName, parent, 0); + DataObject* newISC = (dataObjectName == NULL) ? (DataObject*)parent : DataObject_create(dataObjectName, parent, 0); addOriginatorAndCtlNumOptions((ModelNode*) newISC, controlOptions); @@ -855,7 +855,7 @@ CDC_ISC_create(const char* dataObjectName, ModelNode* parent, uint32_t options, DataObject* CDC_BAC_create(const char* dataObjectName, ModelNode* parent, uint32_t options, uint32_t controlOptions, bool isIntegerNotFloat) { - DataObject* newBAC = DataObject_create(dataObjectName, parent, 0); + DataObject* newBAC = (dataObjectName == NULL) ? (DataObject*)parent : DataObject_create(dataObjectName, parent, 0); addControlStatusAttributesForAnalogControl(newBAC, controlOptions); @@ -899,7 +899,7 @@ CDC_BAC_create(const char* dataObjectName, ModelNode* parent, uint32_t options, DataObject* CDC_LPL_create(const char* dataObjectName, ModelNode* parent, uint32_t options) { - DataObject* newLPL = DataObject_create(dataObjectName, parent, 0); + DataObject* newLPL = (dataObjectName == NULL) ? (DataObject*)parent : DataObject_create(dataObjectName, parent, 0); DataAttribute_create("vendor", (ModelNode*) newLPL, IEC61850_VISIBLE_STRING_255, IEC61850_FC_DC, 0, 0, 0); DataAttribute_create("swRev", (ModelNode*) newLPL, IEC61850_VISIBLE_STRING_255, IEC61850_FC_DC, 0, 0, 0); @@ -921,7 +921,7 @@ CDC_LPL_create(const char* dataObjectName, ModelNode* parent, uint32_t options) DataObject* CDC_DPL_create(const char* dataObjectName, ModelNode* parent, uint32_t options) { - DataObject* newDPL = DataObject_create(dataObjectName, parent, 0); + DataObject* newDPL = (dataObjectName == NULL) ? (DataObject*)parent : DataObject_create(dataObjectName, parent, 0); DataAttribute_create("vendor", (ModelNode*) newDPL, IEC61850_VISIBLE_STRING_255, IEC61850_FC_DC, 0, 0, 0); @@ -949,7 +949,7 @@ CDC_DPL_create(const char* dataObjectName, ModelNode* parent, uint32_t options) DataObject* CDC_ACD_create(const char* dataObjectName, ModelNode* parent, uint32_t options) { - DataObject* newACD = DataObject_create(dataObjectName, parent, 0); + DataObject* newACD = (dataObjectName == NULL) ? (DataObject*)parent : DataObject_create(dataObjectName, parent, 0); DataAttribute_create("general", (ModelNode*) newACD, IEC61850_BOOLEAN, IEC61850_FC_ST, TRG_OPT_DATA_CHANGED, 0, 0); DataAttribute_create("dirGeneral", (ModelNode*) newACD, IEC61850_ENUMERATED, IEC61850_FC_ST, TRG_OPT_DATA_CHANGED, 0, 0); @@ -984,7 +984,7 @@ CDC_ACD_create(const char* dataObjectName, ModelNode* parent, uint32_t options) DataObject* CDC_ACT_create(const char* dataObjectName, ModelNode* parent, uint32_t options) { - DataObject* newACT = DataObject_create(dataObjectName, parent, 0); + DataObject* newACT = (dataObjectName == NULL) ? (DataObject*)parent : DataObject_create(dataObjectName, parent, 0); DataAttribute_create("general", (ModelNode*) newACT, IEC61850_BOOLEAN, IEC61850_FC_ST, TRG_OPT_DATA_CHANGED, 0, 0); @@ -1010,7 +1010,7 @@ CDC_ACT_create(const char* dataObjectName, ModelNode* parent, uint32_t options) DataObject* CDC_WYE_create(const char* dataObjectName, ModelNode* parent, uint32_t options) { - DataObject* newWYE = DataObject_create(dataObjectName, parent, 0); + DataObject* newWYE = (dataObjectName == NULL) ? (DataObject*)parent : DataObject_create(dataObjectName, parent, 0); /* TODO check if some options should be masked */ /* TODO take care for GC_1 */ @@ -1033,7 +1033,7 @@ CDC_WYE_create(const char* dataObjectName, ModelNode* parent, uint32_t options) DataObject* CDC_DEL_create(const char* dataObjectName, ModelNode* parent, uint32_t options) { - DataObject* newDEL = DataObject_create(dataObjectName, parent, 0); + DataObject* newDEL = (dataObjectName == NULL) ? (DataObject*)parent : DataObject_create(dataObjectName, parent, 0); /* TODO check if some options should be masked */ CDC_CMV_create("phsAB", (ModelNode*) newDEL, options); @@ -1052,7 +1052,7 @@ CDC_DEL_create(const char* dataObjectName, ModelNode* parent, uint32_t options) DataObject* CDC_SPG_create(const char* dataObjectName, ModelNode* parent, uint32_t options) { - DataObject* newSPG = DataObject_create(dataObjectName, parent, 0); + DataObject* newSPG = (dataObjectName == NULL) ? (DataObject*)parent : DataObject_create(dataObjectName, parent, 0); DataAttribute_create("setVal", (ModelNode*) newSPG, IEC61850_BOOLEAN, IEC61850_FC_SP, TRG_OPT_DATA_CHANGED, 0, 0); @@ -1064,7 +1064,7 @@ CDC_SPG_create(const char* dataObjectName, ModelNode* parent, uint32_t options) DataObject* CDC_VSG_create(const char* dataObjectName, ModelNode* parent, uint32_t options) { - DataObject* newSPG = DataObject_create(dataObjectName, parent, 0); + DataObject* newSPG = (dataObjectName == NULL) ? (DataObject*)parent : DataObject_create(dataObjectName, parent, 0); DataAttribute_create("setVal", (ModelNode*) newSPG, IEC61850_VISIBLE_STRING_255, IEC61850_FC_SP, TRG_OPT_DATA_CHANGED, 0, 0); @@ -1077,7 +1077,7 @@ CDC_VSG_create(const char* dataObjectName, ModelNode* parent, uint32_t options) DataObject* CDC_ENG_create(const char* dataObjectName, ModelNode* parent, uint32_t options) { - DataObject* newENG = DataObject_create(dataObjectName, parent, 0); + DataObject* newENG = (dataObjectName == NULL) ? (DataObject*)parent : DataObject_create(dataObjectName, parent, 0); DataAttribute_create("setVal", (ModelNode*) newENG, IEC61850_ENUMERATED, IEC61850_FC_SP, TRG_OPT_DATA_CHANGED, 0, 0); @@ -1089,7 +1089,7 @@ CDC_ENG_create(const char* dataObjectName, ModelNode* parent, uint32_t options) DataObject* CDC_ING_create(const char* dataObjectName, ModelNode* parent, uint32_t options) { - DataObject* newING = DataObject_create(dataObjectName, parent, 0); + DataObject* newING = (dataObjectName == NULL) ? (DataObject*)parent : DataObject_create(dataObjectName, parent, 0); DataAttribute_create("setVal", (ModelNode*) newING, IEC61850_INT32, IEC61850_FC_SP, TRG_OPT_DATA_CHANGED, 0, 0); @@ -1114,7 +1114,7 @@ CDC_ING_create(const char* dataObjectName, ModelNode* parent, uint32_t options) DataObject* CDC_ASG_create(const char* dataObjectName, ModelNode* parent, uint32_t options, bool isIntegerNotFloat) { - DataObject* newASG = DataObject_create(dataObjectName, parent, 0); + DataObject* newASG = (dataObjectName == NULL) ? (DataObject*)parent : DataObject_create(dataObjectName, parent, 0); CAC_AnalogueValue_create("setMag", (ModelNode*) newASG, IEC61850_FC_SP, TRG_OPT_DATA_CHANGED, isIntegerNotFloat); @@ -1145,7 +1145,7 @@ CDC_ASG_create(const char* dataObjectName, ModelNode* parent, uint32_t options, DataObject* CDC_SPV_create(const char* dataObjectName, ModelNode* parent, uint32_t options, uint32_t controlOptions, uint32_t wpOptions, bool hasChaManRs) { - DataObject* newSPV = DataObject_create(dataObjectName, parent, 0); + DataObject* newSPV = (dataObjectName == NULL) ? (DataObject*)parent : DataObject_create(dataObjectName, parent, 0); if (hasChaManRs) CDC_SPC_create("chaManRs", (ModelNode*) newSPV, 0, CDC_CTL_MODEL_DIRECT_NORMAL); @@ -1196,7 +1196,7 @@ CDC_STV_create(const char* dataObjectName, ModelNode* parent, (void)controlOptions; /* TODO implement */ (void)wpOptions; /* TODO implement */ - DataObject* newSTV = DataObject_create(dataObjectName, parent, 0); + DataObject* newSTV = (dataObjectName == NULL) ? (DataObject*)parent : DataObject_create(dataObjectName, parent, 0); CDC_INS_create("actSt", (ModelNode*) newSTV, 0); @@ -1218,7 +1218,7 @@ CDC_ALM_create(const char* dataObjectName, ModelNode* parent, (void)controlOptions; /* TODO implement */ (void)wpOptions; /* TODO implement */ - DataObject* newALM = DataObject_create(dataObjectName, parent, 0); + DataObject* newALM = (dataObjectName == NULL) ? (DataObject*)parent : DataObject_create(dataObjectName, parent, 0); CDC_SPC_create("almAck", (ModelNode*) newALM, 0, CDC_CTL_MODEL_DIRECT_NORMAL | CDC_CTL_OPTION_ORIGIN); @@ -1244,7 +1244,7 @@ CDC_CMD_create(const char* dataObjectName, ModelNode* parent, (void)hasCmTm; /* TODO implement */ (void)hasCmCt; /* TODO implement */ - DataObject* newCMD = DataObject_create(dataObjectName, parent, 0); + DataObject* newCMD = (dataObjectName == NULL) ? (DataObject*)parent : DataObject_create(dataObjectName, parent, 0); CDC_INC_create("actSt", (ModelNode*) newCMD, 0, controlOptions); @@ -1272,7 +1272,7 @@ CDC_CTE_create(const char* dataObjectName, ModelNode* parent, { (void)controlOptions; /* TODO implement */ - DataObject* newCTE = DataObject_create(dataObjectName, parent, 0); + DataObject* newCTE = (dataObjectName == NULL) ? (DataObject*)parent : DataObject_create(dataObjectName, parent, 0); CDC_SPC_create("manRs", (ModelNode*) newCTE, 0, CDC_CTL_MODEL_DIRECT_NORMAL | CDC_CTL_OPTION_ORIGIN); @@ -1313,7 +1313,7 @@ CDC_TMS_create(const char* dataObjectName, ModelNode* parent, { (void)controlOptions; /* TODO implement */ - DataObject* newTMS = DataObject_create(dataObjectName, parent, 0); + DataObject* newTMS = (dataObjectName == NULL) ? (DataObject*)parent : DataObject_create(dataObjectName, parent, 0); CDC_SPC_create("manRs", (ModelNode*) newTMS, 0, CDC_CTL_MODEL_DIRECT_NORMAL | CDC_CTL_OPTION_ORIGIN); @@ -1344,4 +1344,3 @@ CDC_TMS_create(const char* dataObjectName, ModelNode* parent, return newTMS; } - From 3c918ee4e31eddf1ff2308363c700fccc6a4d247 Mon Sep 17 00:00:00 2001 From: Michael Zillgith Date: Fri, 28 Jul 2023 14:46:53 +0100 Subject: [PATCH 03/79] - IED server: added IedServer_ListObjectsAccessHandler callback to control LISTOBJECTS access rights (LIB61850-417) --- src/iec61850/inc/iec61850_server.h | 20 +++ .../inc_private/mms_mapping_internal.h | 3 + src/iec61850/server/impl/ied_server.c | 7 + src/iec61850/server/mms_mapping/mms_mapping.c | 137 +++++++++++++++++- src/mms/inc_private/mms_server_internal.h | 6 + src/mms/inc_private/mms_server_libinternal.h | 6 + .../iso_mms/server/mms_get_namelist_service.c | 36 +++-- src/mms/iso_mms/server/mms_server.c | 35 +++++ 8 files changed, 233 insertions(+), 17 deletions(-) diff --git a/src/iec61850/inc/iec61850_server.h b/src/iec61850/inc/iec61850_server.h index 3db3235e..94324a00 100644 --- a/src/iec61850/inc/iec61850_server.h +++ b/src/iec61850/inc/iec61850_server.h @@ -2007,6 +2007,26 @@ typedef bool LIB61850_API void IedServer_setDirectoryAccessHandler(IedServer self, IedServer_DirectoryAccessHandler handler, void* parameter); +/** + * \brief Callback that is called when a client is invoking a list objects service + * + * \param parameter user provided parameter + * \param connection client connection that is involved + * + * \return true to include the object in the service response, otherwise false + */ +typedef bool +(*IedServer_ListObjectsAccessHandler) (void* parameter, ClientConnection connection, LogicalDevice* ld, LogicalNode* ln, DataObject* dataObject, FunctionalConstraint fc); + +/** + * \brief Set a handler to control which objects are return by the list objects services + * + * \param handler the callback handler to be used + * \param parameter a user provided parameter that is passed to the handler. + */ +LIB61850_API void +IedServer_setListObjectsAccessHandler(IedServer self, IedServer_ListObjectsAccessHandler handler, void* parameter); + /**@}*/ /**@}*/ diff --git a/src/iec61850/inc_private/mms_mapping_internal.h b/src/iec61850/inc_private/mms_mapping_internal.h index 51714f11..6e3edb52 100644 --- a/src/iec61850/inc_private/mms_mapping_internal.h +++ b/src/iec61850/inc_private/mms_mapping_internal.h @@ -352,6 +352,9 @@ struct sMmsMapping { IedServer_DirectoryAccessHandler directoryAccessHandler; void* directoryAccessHandlerParameter; + + IedServer_ListObjectsAccessHandler listObjectsAccessHandler; + void* listObjectsAccessHandlerParameter; }; #endif /* MMS_MAPPING_INTERNAL_H_ */ diff --git a/src/iec61850/server/impl/ied_server.c b/src/iec61850/server/impl/ied_server.c index 27d40624..aa8e84cd 100644 --- a/src/iec61850/server/impl/ied_server.c +++ b/src/iec61850/server/impl/ied_server.c @@ -1967,3 +1967,10 @@ IedServer_setDirectoryAccessHandler(IedServer self, IedServer_DirectoryAccessHan self->mmsMapping->directoryAccessHandler = handler; self->mmsMapping->directoryAccessHandlerParameter = parameter; } + +void +IedServer_setListObjectsAccessHandler(IedServer self, IedServer_ListObjectsAccessHandler handler, void* parameter) +{ + self->mmsMapping->listObjectsAccessHandler = handler; + self->mmsMapping->listObjectsAccessHandlerParameter = parameter; +} diff --git a/src/iec61850/server/mms_mapping/mms_mapping.c b/src/iec61850/server/mms_mapping/mms_mapping.c index 2d76f39c..91e12f62 100644 --- a/src/iec61850/server/mms_mapping/mms_mapping.c +++ b/src/iec61850/server/mms_mapping/mms_mapping.c @@ -3314,6 +3314,122 @@ mmsConnectionHandler(void* parameter, MmsServerConnection connection, MmsServerE } } +static bool +mmsListObjectsAccessHandler(void* parameter, MmsDomain* domain, char* variableId, MmsServerConnection connection) +{ + MmsMapping* self = (MmsMapping*) parameter; + + if (DEBUG_IED_SERVER) + printf("IED_SERVER: mmsListObjectsAccessHandler: Requested %s\n", variableId); + + bool allowAccess = true; + + if (self->listObjectsAccessHandler) + { + char* separator = strchr(variableId, '$'); + +#if (CONFIG_IEC61850_SETTING_GROUPS == 1) + + if (separator) { + if (isFunctionalConstraint("SE", separator)) { + goto exit_function; + } + } + +#endif /* (CONFIG_IEC61850_SETTING_GROUPS == 1) */ + + char* ldName = MmsDomain_getName(domain); + + LogicalDevice* ld = IedModel_getDevice(self->model, ldName); + + if (ld) + { + FunctionalConstraint fc = IEC61850_FC_NONE; + + if (separator) { + fc = FunctionalConstraint_fromString(separator + 1); + + if (fc == IEC61850_FC_BR || fc == IEC61850_FC_US || + fc == IEC61850_FC_MS || fc == IEC61850_FC_RP || + fc == IEC61850_FC_LG || fc == IEC61850_FC_GO) + { + goto exit_function; + } + else + { + char str[65]; + + StringUtils_createStringFromBufferInBuffer(str, (uint8_t*) variableId, separator - variableId); + + LogicalNode* ln = LogicalDevice_getLogicalNode(ld, str); + + if (ln != NULL) + { + char* doStart = strchr(separator + 1, '$'); + + if (doStart != NULL) { + + char* doEnd = strchr(doStart + 1, '$'); + + if (doEnd == NULL) { + StringUtils_copyStringToBuffer(doStart + 1, str); + } + else { + doEnd--; + + StringUtils_createStringFromBufferInBuffer(str, (uint8_t*) (doStart + 1), doEnd - doStart); + } + + if (fc == IEC61850_FC_SP) { + if (!strcmp(str, "SGCB")) + goto exit_function; + } + + ModelNode* dobj = ModelNode_getChild((ModelNode*) ln, str); + + if (dobj != NULL) { + + if (dobj->modelType == DataObjectModelType) { + + ClientConnection clientConnection = private_IedServer_getClientConnectionByHandle(self->iedServer, + connection); + + allowAccess = self->listObjectsAccessHandler(self->listObjectsAccessHandlerParameter, clientConnection, ld, ln, (DataObject*) dobj, fc); + } + } + } + else { + /* no data object but with FC specified */ + + ClientConnection clientConnection = private_IedServer_getClientConnectionByHandle(self->iedServer, + connection); + + allowAccess = self->listObjectsAccessHandler(self->listObjectsAccessHandlerParameter, clientConnection, ld, ln, NULL, fc); + } + } + } + } + else { + LogicalNode* ln = LogicalDevice_getLogicalNode(ld, variableId); + + if (ln) { + /* only LN, no FC specified */ + + ClientConnection clientConnection = private_IedServer_getClientConnectionByHandle(self->iedServer, connection); + + allowAccess = self->listObjectsAccessHandler(self->listObjectsAccessHandlerParameter, clientConnection, ld, ln, NULL, fc); + } + } + } + else { + /* internal error ? - we should not end up here! */ + } + } + +exit_function: + return allowAccess; +} + static MmsDataAccessError mmsReadAccessHandler (void* parameter, MmsDomain* domain, char* variableId, MmsServerConnection connection, bool isDirectAccess) { @@ -3350,11 +3466,9 @@ mmsReadAccessHandler (void* parameter, MmsDomain* domain, char* variableId, MmsS LogicalDevice* ld = IedModel_getDevice(self->model, ldName); - if (ld != NULL) { - - char str[65]; - - FunctionalConstraint fc; + if (ld != NULL) + { + FunctionalConstraint fc = IEC61850_FC_NONE; if (separator != NULL) { fc = FunctionalConstraint_fromString(separator + 1); @@ -3367,6 +3481,8 @@ mmsReadAccessHandler (void* parameter, MmsDomain* domain, char* variableId, MmsS } else { + char str[65]; + StringUtils_createStringFromBufferInBuffer(str, (uint8_t*) variableId, separator - variableId); LogicalNode* ln = LogicalDevice_getLogicalNode(ld, str); @@ -3417,6 +3533,16 @@ mmsReadAccessHandler (void* parameter, MmsDomain* domain, char* variableId, MmsS } } } + else { + LogicalNode* ln = LogicalDevice_getLogicalNode(ld, variableId); + + if (ln != NULL) { + ClientConnection clientConnection = private_IedServer_getClientConnectionByHandle(self->iedServer, connection); + + return self->readAccessHandler(ld, ln, NULL, fc, clientConnection, + self->readAccessHandlerParameter); + } + } } return DATA_ACCESS_ERROR_OBJECT_ACCESS_UNSUPPORTED; @@ -3666,6 +3792,7 @@ MmsMapping_installHandlers(MmsMapping* self) MmsServer_installReadHandler(self->mmsServer, mmsReadHandler, (void*) self); MmsServer_installWriteHandler(self->mmsServer, mmsWriteHandler, (void*) self); MmsServer_installReadAccessHandler(self->mmsServer, mmsReadAccessHandler, (void*) self); + MmsServer_installListAccessHandler(self->mmsServer, mmsListObjectsAccessHandler, (void*) self); MmsServer_installConnectionHandler(self->mmsServer, mmsConnectionHandler, (void*) self); MmsServer_installVariableListAccessHandler(self->mmsServer, variableListAccessHandler, (void*) self); MmsServer_installGetNameListHandler(self->mmsServer, mmsGetNameListHandler, (void*) self); diff --git a/src/mms/inc_private/mms_server_internal.h b/src/mms/inc_private/mms_server_internal.h index ec70d359..606e0827 100644 --- a/src/mms/inc_private/mms_server_internal.h +++ b/src/mms/inc_private/mms_server_internal.h @@ -120,6 +120,9 @@ struct sMmsServer { MmsWriteVariableHandler writeHandler; void* writeHandlerParameter; + MmsListAccessHandler listAccessHandler; + void* listAccessHandlerParameter; + MmsConnectionHandler connectionHandler; void* connectionHandlerParameter; @@ -422,6 +425,9 @@ mmsServer_setValue(MmsServer self, MmsDomain* domain, char* itemId, MmsValue* va LIB61850_INTERNAL MmsValue* mmsServer_getValue(MmsServer self, MmsDomain* domain, char* itemId, MmsServerConnection connection, bool isDirectAccess); +LIB61850_INTERNAL bool +mmsServer_checkListAccess(MmsServer self, MmsDomain* domain, char* itemId, MmsServerConnection connection); + LIB61850_INTERNAL void mmsServer_createMmsWriteResponse(MmsServerConnection connection, uint32_t invokeId, ByteBuffer* response, int numberOfItems, MmsDataAccessError* accessResults); diff --git a/src/mms/inc_private/mms_server_libinternal.h b/src/mms/inc_private/mms_server_libinternal.h index 444989c9..da5de7a5 100644 --- a/src/mms/inc_private/mms_server_libinternal.h +++ b/src/mms/inc_private/mms_server_libinternal.h @@ -37,6 +37,9 @@ typedef MmsDataAccessError (*MmsWriteVariableHandler)(void* parameter, MmsDomain* domain, char* variableId, MmsValue* value, MmsServerConnection connection); +typedef bool (*MmsListAccessHandler) (void* parameter, MmsDomain* domain, + char* variableId, MmsServerConnection connection); + typedef void (*MmsConnectionHandler)(void* parameter, MmsServerConnection connection, MmsServerEvent event); @@ -63,6 +66,9 @@ LIB61850_INTERNAL void MmsServer_installWriteHandler(MmsServer self, MmsWriteVariableHandler, void* parameter); +LIB61850_INTERNAL void +MmsServer_installListAccessHandler(MmsServer self, MmsListAccessHandler listAccessHandler, void* parameter); + /** * A connection handler will be invoked whenever a new client connection is opened or closed */ diff --git a/src/mms/iso_mms/server/mms_get_namelist_service.c b/src/mms/iso_mms/server/mms_get_namelist_service.c index 2170f8c1..0a9c6f57 100644 --- a/src/mms/iso_mms/server/mms_get_namelist_service.c +++ b/src/mms/iso_mms/server/mms_get_namelist_service.c @@ -129,7 +129,7 @@ appendMmsSubVariable(char* name, char* child) } static LinkedList -addSubNamedVaribleNamesToList(LinkedList nameList, char* prefix, MmsVariableSpecification* variable) +addSubNamedVaribleNamesToList(MmsServerConnection connection, LinkedList nameList, MmsDomain* domain, char* prefix, MmsVariableSpecification* variable) { LinkedList listElement = nameList; @@ -158,14 +158,20 @@ addSubNamedVaribleNamesToList(LinkedList nameList, char* prefix, MmsVariableSpec #endif /* (CONFIG_MMS_SORT_NAME_LIST == 1) */ if (variableName) - { - listElement = LinkedList_insertAfter(listElement, variableName); + { + bool accessAllowed = mmsServer_checkListAccess(connection->server, domain, variableName, connection); + + if (accessAllowed) { + + listElement = LinkedList_insertAfter(listElement, variableName); #if (CONFIG_MMS_SORT_NAME_LIST == 1) - listElement = addSubNamedVaribleNamesToList(listElement, variableName, variables[index[i]]); + listElement = addSubNamedVaribleNamesToList(connection, listElement, domain, variableName, variables[index[i]]); #else - listElement = addSubNamedVaribleNamesToList(listElement, variableName, variables[i]); + listElement = addSubNamedVaribleNamesToList(connection, listElement, domain, variableName, variables[i]); #endif /* (CONFIG_MMS_SORT_NAME_LIST == 1) */ + + } } } @@ -233,7 +239,7 @@ getNameListDomainSpecific(MmsServerConnection connection, char* domainName) bool allowAccess = true; if (connection->server->getNameListHandler) { - allowAccess = connection->server->getNameListHandler(connection->server->getNameListHandlerParameter, MMS_GETNAMELIST_DATASETS, domain, connection); + allowAccess = connection->server->getNameListHandler(connection->server->getNameListHandlerParameter, MMS_GETNAMELIST_DATA, domain, connection); } if (allowAccess) { @@ -255,22 +261,28 @@ getNameListDomainSpecific(MmsServerConnection connection, char* domainName) for (i = 0; i < domain->namedVariablesCount; i++) { + bool accessAllowed = mmsServer_checkListAccess(connection->server, domain, variables[index[i]]->name, connection); + + if (accessAllowed) { + #if (CONFIG_MMS_SORT_NAME_LIST == 1) - element = LinkedList_insertAfter(element, StringUtils_copyString(variables[index[i]]->name)); + element = LinkedList_insertAfter(element, StringUtils_copyString(variables[index[i]]->name)); #else - element = LinkedList_insertAfter(element, StringUtils_copyString(variables[i]->name)); + element = LinkedList_insertAfter(element, StringUtils_copyString(variables[i]->name)); #endif #if (CONFIG_MMS_SUPPORT_FLATTED_NAME_SPACE == 1) #if (CONFIG_MMS_SORT_NAME_LIST == 1) - char* prefix = variables[index[i]]->name; - element = addSubNamedVaribleNamesToList(element, prefix, variables[index[i]]); + char* prefix = variables[index[i]]->name; + element = addSubNamedVaribleNamesToList(connection, element, domain, prefix, variables[index[i]]); #else - char* prefix = variables[i]->name; - element = addSubNamedVaribleNamesToList(element, prefix, variables[i]); + char* prefix = variables[i]->name; + element = addSubNamedVaribleNamesToList(connection, element, domain, prefix, variables[i]); #endif /* (CONFIG_MMS_SORT_NAME_LIST == 1) */ #endif /* (CONFIG_MMS_SUPPORT_FLATTED_NAME_SPACE == 1) */ + } + } #if (CONFIG_MMS_SORT_NAME_LIST == 1) diff --git a/src/mms/iso_mms/server/mms_server.c b/src/mms/iso_mms/server/mms_server.c index b9d4c798..3229b21e 100644 --- a/src/mms/iso_mms/server/mms_server.c +++ b/src/mms/iso_mms/server/mms_server.c @@ -351,6 +351,13 @@ MmsServer_installWriteHandler(MmsServer self, MmsWriteVariableHandler writeHandl self->writeHandlerParameter = parameter; } +void +MmsServer_installListAccessHandler(MmsServer self, MmsListAccessHandler listAccessHandler, void* parameter) +{ + self->listAccessHandler = listAccessHandler; + self->listAccessHandlerParameter = parameter; +} + void MmsServer_installConnectionHandler(MmsServer self, MmsConnectionHandler connectionHandler, void* parameter) { @@ -579,6 +586,34 @@ exit_function: return value; } +MmsDataAccessError +mmsServer_checkReadAccess(MmsServer self, MmsDomain* domain, char* itemId, MmsServerConnection connection, bool isDirectAccess) +{ + MmsDataAccessError accessError = DATA_ACCESS_ERROR_SUCCESS; + + printf("mmsServer_checkReadAccess(%s/%s)\n", domain->domainName, itemId); + + if (self->readAccessHandler) { + accessError = + self->readAccessHandler(self->readAccessHandlerParameter, (domain == (MmsDomain*) self->device) ? NULL : domain, + itemId, connection, isDirectAccess); + } + + return accessError; +} + +bool +mmsServer_checkListAccess(MmsServer self, MmsDomain* domain, char* itemId, MmsServerConnection connection) +{ + bool allowAccess = true; + + if (self->listAccessHandler) { + allowAccess = self->listAccessHandler(self->listAccessHandlerParameter, domain, itemId, connection); + } + + return allowAccess; +} + MmsDevice* MmsServer_getDevice(MmsServer self) { From 2467605e2358a17d9fc8ba0d24d3303fb8704796 Mon Sep 17 00:00:00 2001 From: Michael Zillgith Date: Fri, 28 Jul 2023 15:28:00 +0100 Subject: [PATCH 04/79] - IED server: apply LISTOBJECTS restrictions to get-variable-access-attributes servic (LIB61850-417) --- .../iso_mms/server/mms_get_namelist_service.c | 3 +++ .../server/mms_get_var_access_service.c | 24 +++++++++++++++---- 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/src/mms/iso_mms/server/mms_get_namelist_service.c b/src/mms/iso_mms/server/mms_get_namelist_service.c index 0a9c6f57..6d6c1568 100644 --- a/src/mms/iso_mms/server/mms_get_namelist_service.c +++ b/src/mms/iso_mms/server/mms_get_namelist_service.c @@ -172,6 +172,9 @@ addSubNamedVaribleNamesToList(MmsServerConnection connection, LinkedList nameLis #endif /* (CONFIG_MMS_SORT_NAME_LIST == 1) */ } + else { + GLOBAL_FREEMEM(variableName); + } } } diff --git a/src/mms/iso_mms/server/mms_get_var_access_service.c b/src/mms/iso_mms/server/mms_get_var_access_service.c index abdf6d02..d936701c 100644 --- a/src/mms/iso_mms/server/mms_get_var_access_service.c +++ b/src/mms/iso_mms/server/mms_get_var_access_service.c @@ -1,7 +1,7 @@ /* * mms_get_var_access_service.c * - * Copyright 2013 Michael Zillgith + * Copyright 2013-2023 Michael Zillgith * * This file is part of libIEC61850. * @@ -191,7 +191,8 @@ deleteVariableAccessAttributesResponse( getVarAccessAttr->typeSpecification.choice.structure.components.list.array = NULL; getVarAccessAttr->typeSpecification.choice.structure.components.list.count = 0; getVarAccessAttr->typeSpecification.choice.structure.components.list.size = 0; - } else if (getVarAccessAttr->typeSpecification.present == TypeSpecification_PR_array) { + } + else if (getVarAccessAttr->typeSpecification.present == TypeSpecification_PR_array) { GLOBAL_FREEMEM(getVarAccessAttr->typeSpecification.choice.array.numberOfElements.buf); getVarAccessAttr->typeSpecification.choice.array.numberOfElements.buf = NULL; getVarAccessAttr->typeSpecification.choice.array.numberOfElements.size = 0; @@ -215,8 +216,10 @@ createVariableAccessAttributesResponse( MmsVariableSpecification* namedVariable = NULL; + MmsDomain* domain = NULL; + if (domainId != NULL) { - MmsDomain* domain = MmsDevice_getDomain(device, domainId); + domain = MmsDevice_getDomain(device, domainId); if (domain == NULL) { if (DEBUG_MMS_SERVER) printf("MMS_SERVER: domain %s not known\n", domainId); @@ -233,9 +236,20 @@ createVariableAccessAttributesResponse( namedVariable = MmsDevice_getNamedVariable(device, nameId); #endif /* (CONFIG_MMS_SUPPORT_VMD_SCOPE_NAMED_VARIABLES == 1) */ - if (namedVariable == NULL) { - if (DEBUG_MMS_SERVER) printf("MMS_SERVER: named variable %s not known\n", nameId); + if (DEBUG_MMS_SERVER) printf("MMS_SERVER: named variable %s.%s not known\n", domainId, nameId); + + mmsMsg_createServiceErrorPdu(invokeId, response, + MMS_ERROR_ACCESS_OBJECT_NON_EXISTENT); + + goto exit_function; + } + + bool accessAllowed = mmsServer_checkListAccess(connection->server, domain, nameId, connection); + + if (!accessAllowed) { + if (DEBUG_MMS_SERVER) + printf("MMS_SERVER: named variable %s/%s not visible due to access restrictions\n", domainId, nameId); mmsMsg_createServiceErrorPdu(invokeId, response, MMS_ERROR_ACCESS_OBJECT_NON_EXISTENT); From efec5194a284069921fb37920a3ce033a9285454 Mon Sep 17 00:00:00 2001 From: Michael Zillgith Date: Fri, 28 Jul 2023 15:37:34 +0100 Subject: [PATCH 05/79] - replaced tabs by spaces in mms_get_var_access_service.c --- .../server/mms_get_var_access_service.c | 513 +++++++++--------- 1 file changed, 256 insertions(+), 257 deletions(-) diff --git a/src/mms/iso_mms/server/mms_get_var_access_service.c b/src/mms/iso_mms/server/mms_get_var_access_service.c index d936701c..840d90b5 100644 --- a/src/mms/iso_mms/server/mms_get_var_access_service.c +++ b/src/mms/iso_mms/server/mms_get_var_access_service.c @@ -32,250 +32,250 @@ static int createTypeSpecification ( - MmsVariableSpecification* namedVariable, - TypeSpecification_t* typeSpec) + MmsVariableSpecification* namedVariable, + TypeSpecification_t* typeSpec) { - if (namedVariable->type == MMS_ARRAY) { - typeSpec->present = TypeSpecification_PR_array; - - asn_long2INTEGER(&(typeSpec->choice.array.numberOfElements), - (long) namedVariable->typeSpec.array.elementCount); - - typeSpec->choice.array.packed = NULL; - typeSpec->choice.array.elementType = (TypeSpecification_t*) GLOBAL_CALLOC(1, sizeof(TypeSpecification_t)); - - createTypeSpecification(namedVariable->typeSpec.array.elementTypeSpec, - typeSpec->choice.array.elementType); - } - else if (namedVariable->type == MMS_STRUCTURE) { - - typeSpec->present = TypeSpecification_PR_structure; - - int componentCount = namedVariable->typeSpec.structure.elementCount; - - typeSpec->choice.structure.components.list.count = componentCount; - typeSpec->choice.structure.components.list.size = componentCount; - - typeSpec->choice.structure.components.list.array - = (StructComponent_t**) GLOBAL_CALLOC(componentCount, sizeof(StructComponent_t*)); - - int i; - - for (i = 0; i < componentCount; i++) { - - typeSpec->choice.structure.components.list.array[i] = - (StructComponent_t*) GLOBAL_CALLOC(1, sizeof(StructComponent_t)); - - typeSpec->choice.structure.components.list.array[i]->componentName = - (Identifier_t*) GLOBAL_CALLOC(1, sizeof(Identifier_t)); - - typeSpec->choice.structure.components.list.array[i]->componentName->buf = - (uint8_t*) StringUtils_copyString(namedVariable->typeSpec.structure.elements[i]->name); - - typeSpec->choice.structure.components.list.array[i]->componentName->size = - strlen(namedVariable->typeSpec.structure.elements[i]->name); - - typeSpec->choice.structure.components.list.array[i]->componentType = - (TypeSpecification_t*) GLOBAL_CALLOC(1, sizeof(TypeSpecification_t)); - - createTypeSpecification(namedVariable->typeSpec.structure.elements[i], - typeSpec->choice.structure.components.list.array[i]->componentType); - } - } - else { - - switch (namedVariable->type) { - case MMS_BOOLEAN: - typeSpec->present = TypeSpecification_PR_boolean; - break; - case MMS_BIT_STRING: - typeSpec->present = TypeSpecification_PR_bitstring; - typeSpec->choice.bitstring = namedVariable->typeSpec.bitString; - break; - case MMS_INTEGER: - typeSpec->present = TypeSpecification_PR_integer; - typeSpec->choice.integer = namedVariable->typeSpec.integer; - break; - case MMS_UNSIGNED: - typeSpec->present = TypeSpecification_PR_unsigned; - typeSpec->choice.Unsigned = namedVariable->typeSpec.unsignedInteger; - break; - case MMS_FLOAT: - typeSpec->present = TypeSpecification_PR_floatingpoint; - typeSpec->choice.floatingpoint.exponentwidth = - namedVariable->typeSpec.floatingpoint.exponentWidth; - typeSpec->choice.floatingpoint.formatwidth = - namedVariable->typeSpec.floatingpoint.formatWidth; - break; - case MMS_OCTET_STRING: - typeSpec->present = TypeSpecification_PR_octetstring; - typeSpec->choice.octetstring = namedVariable->typeSpec.octetString; - break; - case MMS_VISIBLE_STRING: - typeSpec->present = TypeSpecification_PR_visiblestring; - typeSpec->choice.visiblestring = namedVariable->typeSpec.visibleString; - break; - case MMS_STRING: - typeSpec->present = TypeSpecification_PR_mMSString; - typeSpec->choice.mMSString = namedVariable->typeSpec.mmsString; - break; - case MMS_UTC_TIME: - typeSpec->present = TypeSpecification_PR_utctime; - break; - case MMS_BINARY_TIME: - typeSpec->present = TypeSpecification_PR_binarytime; - - if (namedVariable->typeSpec.binaryTime == 6) - typeSpec->choice.binarytime = 1; - else - typeSpec->choice.binarytime = 0; - - break; - default: - if (DEBUG_MMS_SERVER) - printf("MMS-SERVER: Unsupported type %i!\n", namedVariable->type); - return -1; - break; - } - } - - return 1; + if (namedVariable->type == MMS_ARRAY) { + typeSpec->present = TypeSpecification_PR_array; + + asn_long2INTEGER(&(typeSpec->choice.array.numberOfElements), + (long) namedVariable->typeSpec.array.elementCount); + + typeSpec->choice.array.packed = NULL; + typeSpec->choice.array.elementType = (TypeSpecification_t*) GLOBAL_CALLOC(1, sizeof(TypeSpecification_t)); + + createTypeSpecification(namedVariable->typeSpec.array.elementTypeSpec, + typeSpec->choice.array.elementType); + } + else if (namedVariable->type == MMS_STRUCTURE) { + + typeSpec->present = TypeSpecification_PR_structure; + + int componentCount = namedVariable->typeSpec.structure.elementCount; + + typeSpec->choice.structure.components.list.count = componentCount; + typeSpec->choice.structure.components.list.size = componentCount; + + typeSpec->choice.structure.components.list.array + = (StructComponent_t**) GLOBAL_CALLOC(componentCount, sizeof(StructComponent_t*)); + + int i; + + for (i = 0; i < componentCount; i++) { + + typeSpec->choice.structure.components.list.array[i] = + (StructComponent_t*) GLOBAL_CALLOC(1, sizeof(StructComponent_t)); + + typeSpec->choice.structure.components.list.array[i]->componentName = + (Identifier_t*) GLOBAL_CALLOC(1, sizeof(Identifier_t)); + + typeSpec->choice.structure.components.list.array[i]->componentName->buf = + (uint8_t*) StringUtils_copyString(namedVariable->typeSpec.structure.elements[i]->name); + + typeSpec->choice.structure.components.list.array[i]->componentName->size = + strlen(namedVariable->typeSpec.structure.elements[i]->name); + + typeSpec->choice.structure.components.list.array[i]->componentType = + (TypeSpecification_t*) GLOBAL_CALLOC(1, sizeof(TypeSpecification_t)); + + createTypeSpecification(namedVariable->typeSpec.structure.elements[i], + typeSpec->choice.structure.components.list.array[i]->componentType); + } + } + else { + + switch (namedVariable->type) { + case MMS_BOOLEAN: + typeSpec->present = TypeSpecification_PR_boolean; + break; + case MMS_BIT_STRING: + typeSpec->present = TypeSpecification_PR_bitstring; + typeSpec->choice.bitstring = namedVariable->typeSpec.bitString; + break; + case MMS_INTEGER: + typeSpec->present = TypeSpecification_PR_integer; + typeSpec->choice.integer = namedVariable->typeSpec.integer; + break; + case MMS_UNSIGNED: + typeSpec->present = TypeSpecification_PR_unsigned; + typeSpec->choice.Unsigned = namedVariable->typeSpec.unsignedInteger; + break; + case MMS_FLOAT: + typeSpec->present = TypeSpecification_PR_floatingpoint; + typeSpec->choice.floatingpoint.exponentwidth = + namedVariable->typeSpec.floatingpoint.exponentWidth; + typeSpec->choice.floatingpoint.formatwidth = + namedVariable->typeSpec.floatingpoint.formatWidth; + break; + case MMS_OCTET_STRING: + typeSpec->present = TypeSpecification_PR_octetstring; + typeSpec->choice.octetstring = namedVariable->typeSpec.octetString; + break; + case MMS_VISIBLE_STRING: + typeSpec->present = TypeSpecification_PR_visiblestring; + typeSpec->choice.visiblestring = namedVariable->typeSpec.visibleString; + break; + case MMS_STRING: + typeSpec->present = TypeSpecification_PR_mMSString; + typeSpec->choice.mMSString = namedVariable->typeSpec.mmsString; + break; + case MMS_UTC_TIME: + typeSpec->present = TypeSpecification_PR_utctime; + break; + case MMS_BINARY_TIME: + typeSpec->present = TypeSpecification_PR_binarytime; + + if (namedVariable->typeSpec.binaryTime == 6) + typeSpec->choice.binarytime = 1; + else + typeSpec->choice.binarytime = 0; + + break; + default: + if (DEBUG_MMS_SERVER) + printf("MMS-SERVER: Unsupported type %i!\n", namedVariable->type); + return -1; + break; + } + } + + return 1; } static void freeTypeSpecRecursive(TypeSpecification_t* typeSpec) { - if (typeSpec->present == TypeSpecification_PR_structure) { - int elementCount = - typeSpec->choice.structure.components.list.count; - - int i; - - for (i = 0; i < elementCount; i++) { - GLOBAL_FREEMEM(typeSpec->choice.structure.components.list.array[i]->componentName->buf); - GLOBAL_FREEMEM(typeSpec->choice.structure.components.list.array[i]->componentName); - freeTypeSpecRecursive(typeSpec->choice.structure.components.list.array[i]->componentType); - GLOBAL_FREEMEM(typeSpec->choice.structure.components.list.array[i]->componentType); - GLOBAL_FREEMEM(typeSpec->choice.structure.components.list.array[i]); - } - - GLOBAL_FREEMEM(typeSpec->choice.structure.components.list.array); - } - else if (typeSpec->present == TypeSpecification_PR_array) { - GLOBAL_FREEMEM(typeSpec->choice.array.numberOfElements.buf); - freeTypeSpecRecursive(typeSpec->choice.array.elementType); - GLOBAL_FREEMEM(typeSpec->choice.array.elementType); - } + if (typeSpec->present == TypeSpecification_PR_structure) { + int elementCount = + typeSpec->choice.structure.components.list.count; + + int i; + + for (i = 0; i < elementCount; i++) { + GLOBAL_FREEMEM(typeSpec->choice.structure.components.list.array[i]->componentName->buf); + GLOBAL_FREEMEM(typeSpec->choice.structure.components.list.array[i]->componentName); + freeTypeSpecRecursive(typeSpec->choice.structure.components.list.array[i]->componentType); + GLOBAL_FREEMEM(typeSpec->choice.structure.components.list.array[i]->componentType); + GLOBAL_FREEMEM(typeSpec->choice.structure.components.list.array[i]); + } + + GLOBAL_FREEMEM(typeSpec->choice.structure.components.list.array); + } + else if (typeSpec->present == TypeSpecification_PR_array) { + GLOBAL_FREEMEM(typeSpec->choice.array.numberOfElements.buf); + freeTypeSpecRecursive(typeSpec->choice.array.elementType); + GLOBAL_FREEMEM(typeSpec->choice.array.elementType); + } } static void deleteVariableAccessAttributesResponse( - GetVariableAccessAttributesResponse_t* getVarAccessAttr) + GetVariableAccessAttributesResponse_t* getVarAccessAttr) { - if (getVarAccessAttr->typeSpecification.present == TypeSpecification_PR_structure) { - int count = getVarAccessAttr->typeSpecification.choice.structure.components.list.count; - - int i; - for (i = 0; i < count; i++) { - GLOBAL_FREEMEM(getVarAccessAttr->typeSpecification.choice.structure.components.list.array[i]->componentName->buf); - GLOBAL_FREEMEM(getVarAccessAttr->typeSpecification.choice.structure.components.list.array[i]->componentName); - TypeSpecification_t* typeSpec = - getVarAccessAttr->typeSpecification.choice.structure.components.list.array[i]->componentType; - freeTypeSpecRecursive(typeSpec); - GLOBAL_FREEMEM(typeSpec); - GLOBAL_FREEMEM(getVarAccessAttr->typeSpecification.choice.structure.components.list.array[i]); - } - - GLOBAL_FREEMEM(getVarAccessAttr->typeSpecification.choice.structure.components.list.array); - - getVarAccessAttr->typeSpecification.choice.structure.components.list.array = NULL; - getVarAccessAttr->typeSpecification.choice.structure.components.list.count = 0; - getVarAccessAttr->typeSpecification.choice.structure.components.list.size = 0; - } - else if (getVarAccessAttr->typeSpecification.present == TypeSpecification_PR_array) { - GLOBAL_FREEMEM(getVarAccessAttr->typeSpecification.choice.array.numberOfElements.buf); - getVarAccessAttr->typeSpecification.choice.array.numberOfElements.buf = NULL; - getVarAccessAttr->typeSpecification.choice.array.numberOfElements.size = 0; - freeTypeSpecRecursive(getVarAccessAttr->typeSpecification.choice.array.elementType); - - GLOBAL_FREEMEM(getVarAccessAttr->typeSpecification.choice.array.elementType); - - getVarAccessAttr->typeSpecification.choice.array.elementType = NULL; - } + if (getVarAccessAttr->typeSpecification.present == TypeSpecification_PR_structure) { + int count = getVarAccessAttr->typeSpecification.choice.structure.components.list.count; + + int i; + for (i = 0; i < count; i++) { + GLOBAL_FREEMEM(getVarAccessAttr->typeSpecification.choice.structure.components.list.array[i]->componentName->buf); + GLOBAL_FREEMEM(getVarAccessAttr->typeSpecification.choice.structure.components.list.array[i]->componentName); + TypeSpecification_t* typeSpec = + getVarAccessAttr->typeSpecification.choice.structure.components.list.array[i]->componentType; + freeTypeSpecRecursive(typeSpec); + GLOBAL_FREEMEM(typeSpec); + GLOBAL_FREEMEM(getVarAccessAttr->typeSpecification.choice.structure.components.list.array[i]); + } + + GLOBAL_FREEMEM(getVarAccessAttr->typeSpecification.choice.structure.components.list.array); + + getVarAccessAttr->typeSpecification.choice.structure.components.list.array = NULL; + getVarAccessAttr->typeSpecification.choice.structure.components.list.count = 0; + getVarAccessAttr->typeSpecification.choice.structure.components.list.size = 0; + } + else if (getVarAccessAttr->typeSpecification.present == TypeSpecification_PR_array) { + GLOBAL_FREEMEM(getVarAccessAttr->typeSpecification.choice.array.numberOfElements.buf); + getVarAccessAttr->typeSpecification.choice.array.numberOfElements.buf = NULL; + getVarAccessAttr->typeSpecification.choice.array.numberOfElements.size = 0; + freeTypeSpecRecursive(getVarAccessAttr->typeSpecification.choice.array.elementType); + + GLOBAL_FREEMEM(getVarAccessAttr->typeSpecification.choice.array.elementType); + + getVarAccessAttr->typeSpecification.choice.array.elementType = NULL; + } } static void createVariableAccessAttributesResponse( - MmsServerConnection connection, - char* domainId, - char* nameId, - int invokeId, - ByteBuffer* response) + MmsServerConnection connection, + char* domainId, + char* nameId, + int invokeId, + ByteBuffer* response) { - MmsDevice* device = MmsServer_getDevice(connection->server); + MmsDevice* device = MmsServer_getDevice(connection->server); - MmsVariableSpecification* namedVariable = NULL; + MmsVariableSpecification* namedVariable = NULL; - MmsDomain* domain = NULL; + MmsDomain* domain = NULL; - if (domainId != NULL) { - domain = MmsDevice_getDomain(device, domainId); + if (domainId != NULL) { + domain = MmsDevice_getDomain(device, domainId); - if (domain == NULL) { - if (DEBUG_MMS_SERVER) printf("MMS_SERVER: domain %s not known\n", domainId); + if (domain == NULL) { + if (DEBUG_MMS_SERVER) printf("MMS_SERVER: domain %s not known\n", domainId); - mmsMsg_createServiceErrorPdu(invokeId, response, - MMS_ERROR_ACCESS_OBJECT_NON_EXISTENT); - goto exit_function; - } + mmsMsg_createServiceErrorPdu(invokeId, response, + MMS_ERROR_ACCESS_OBJECT_NON_EXISTENT); + goto exit_function; + } - namedVariable = MmsDomain_getNamedVariable(domain, nameId); - } + namedVariable = MmsDomain_getNamedVariable(domain, nameId); + } #if (CONFIG_MMS_SUPPORT_VMD_SCOPE_NAMED_VARIABLES == 1) - else - namedVariable = MmsDevice_getNamedVariable(device, nameId); + else + namedVariable = MmsDevice_getNamedVariable(device, nameId); #endif /* (CONFIG_MMS_SUPPORT_VMD_SCOPE_NAMED_VARIABLES == 1) */ - if (namedVariable == NULL) { - if (DEBUG_MMS_SERVER) printf("MMS_SERVER: named variable %s.%s not known\n", domainId, nameId); + if (namedVariable == NULL) { + if (DEBUG_MMS_SERVER) printf("MMS_SERVER: named variable %s.%s not known\n", domainId, nameId); - mmsMsg_createServiceErrorPdu(invokeId, response, - MMS_ERROR_ACCESS_OBJECT_NON_EXISTENT); + mmsMsg_createServiceErrorPdu(invokeId, response, + MMS_ERROR_ACCESS_OBJECT_NON_EXISTENT); - goto exit_function; - } + goto exit_function; + } - bool accessAllowed = mmsServer_checkListAccess(connection->server, domain, nameId, connection); + bool accessAllowed = mmsServer_checkListAccess(connection->server, domain, nameId, connection); - if (!accessAllowed) { - if (DEBUG_MMS_SERVER) - printf("MMS_SERVER: named variable %s/%s not visible due to access restrictions\n", domainId, nameId); + if (!accessAllowed) { + if (DEBUG_MMS_SERVER) + printf("MMS_SERVER: named variable %s/%s not visible due to access restrictions\n", domainId, nameId); - mmsMsg_createServiceErrorPdu(invokeId, response, - MMS_ERROR_ACCESS_OBJECT_NON_EXISTENT); + mmsMsg_createServiceErrorPdu(invokeId, response, + MMS_ERROR_ACCESS_OBJECT_NON_EXISTENT); - goto exit_function; - } + goto exit_function; + } - MmsPdu_t* mmsPdu = mmsServer_createConfirmedResponse(invokeId); + MmsPdu_t* mmsPdu = mmsServer_createConfirmedResponse(invokeId); - mmsPdu->choice.confirmedResponsePdu.confirmedServiceResponse.present = - ConfirmedServiceResponse_PR_getVariableAccessAttributes; + mmsPdu->choice.confirmedResponsePdu.confirmedServiceResponse.present = + ConfirmedServiceResponse_PR_getVariableAccessAttributes; - GetVariableAccessAttributesResponse_t* getVarAccessAttr; + GetVariableAccessAttributesResponse_t* getVarAccessAttr; - getVarAccessAttr = &(mmsPdu->choice.confirmedResponsePdu. - confirmedServiceResponse.choice.getVariableAccessAttributes); + getVarAccessAttr = &(mmsPdu->choice.confirmedResponsePdu. + confirmedServiceResponse.choice.getVariableAccessAttributes); - getVarAccessAttr->mmsDeletable = 0; + getVarAccessAttr->mmsDeletable = 0; - createTypeSpecification(namedVariable, &getVarAccessAttr->typeSpecification); + createTypeSpecification(namedVariable, &getVarAccessAttr->typeSpecification); - asn_enc_rval_t rval = - der_encode(&asn_DEF_MmsPdu, mmsPdu, mmsServer_write_out, (void*) response); + asn_enc_rval_t rval = + der_encode(&asn_DEF_MmsPdu, mmsPdu, mmsServer_write_out, (void*) response); - if (rval.encoded == -1) { - response->size = 0; + if (rval.encoded == -1) { + response->size = 0; if (DEBUG_MMS_SERVER) printf("MMS getVariableAccessAttributes: message to large! send error PDU!\n"); @@ -284,81 +284,80 @@ createVariableAccessAttributesResponse( MMS_ERROR_SERVICE_OTHER); goto exit_function; - } + } - deleteVariableAccessAttributesResponse(getVarAccessAttr); + deleteVariableAccessAttributesResponse(getVarAccessAttr); - asn_DEF_MmsPdu.free_struct(&asn_DEF_MmsPdu, mmsPdu, 0); + asn_DEF_MmsPdu.free_struct(&asn_DEF_MmsPdu, mmsPdu, 0); exit_function: - return; + return; } int mmsServer_handleGetVariableAccessAttributesRequest( - MmsServerConnection connection, - uint8_t* buffer, int bufPos, int maxBufPos, - uint32_t invokeId, - ByteBuffer* response) + MmsServerConnection connection, + uint8_t* buffer, int bufPos, int maxBufPos, + uint32_t invokeId, + ByteBuffer* response) { - int retVal = 0; + int retVal = 0; - GetVariableAccessAttributesRequest_t* request = 0; + GetVariableAccessAttributesRequest_t* request = 0; - asn_dec_rval_t rval; /* Decoder return value */ + asn_dec_rval_t rval; /* Decoder return value */ - rval = ber_decode(NULL, &asn_DEF_GetVariableAccessAttributesRequest, - (void**) &request, buffer + bufPos, maxBufPos - bufPos); + rval = ber_decode(NULL, &asn_DEF_GetVariableAccessAttributesRequest, + (void**) &request, buffer + bufPos, maxBufPos - bufPos); - if (rval.code == RC_OK) { - if (request->present == GetVariableAccessAttributesRequest_PR_name) { - if (request->choice.name.present == ObjectName_PR_domainspecific) { - Identifier_t domainId = request->choice.name.choice.domainspecific.domainId; - Identifier_t nameId = request->choice.name.choice.domainspecific.itemId; + if (rval.code == RC_OK) { + if (request->present == GetVariableAccessAttributesRequest_PR_name) { + if (request->choice.name.present == ObjectName_PR_domainspecific) { + Identifier_t domainId = request->choice.name.choice.domainspecific.domainId; + Identifier_t nameId = request->choice.name.choice.domainspecific.itemId; - char* domainIdStr = StringUtils_createStringFromBuffer(domainId.buf, domainId.size); - char* nameIdStr = StringUtils_createStringFromBuffer(nameId.buf, nameId.size); + char* domainIdStr = StringUtils_createStringFromBuffer(domainId.buf, domainId.size); + char* nameIdStr = StringUtils_createStringFromBuffer(nameId.buf, nameId.size); - if (DEBUG_MMS_SERVER) - printf("MMS_SERVER: getVariableAccessAttributes domainId: %s nameId: %s\n", domainIdStr, nameIdStr); + if (DEBUG_MMS_SERVER) + printf("MMS_SERVER: getVariableAccessAttributes domainId: %s nameId: %s\n", domainIdStr, nameIdStr); - createVariableAccessAttributesResponse(connection, domainIdStr, nameIdStr, invokeId, response); + createVariableAccessAttributesResponse(connection, domainIdStr, nameIdStr, invokeId, response); - GLOBAL_FREEMEM(domainIdStr); - GLOBAL_FREEMEM(nameIdStr); - } + GLOBAL_FREEMEM(domainIdStr); + GLOBAL_FREEMEM(nameIdStr); + } #if (CONFIG_MMS_SUPPORT_VMD_SCOPE_NAMED_VARIABLES == 1) - else if (request->choice.name.present == ObjectName_PR_vmdspecific) { - Identifier_t nameId = request->choice.name.choice.vmdspecific; + else if (request->choice.name.present == ObjectName_PR_vmdspecific) { + Identifier_t nameId = request->choice.name.choice.vmdspecific; - char* nameIdStr = StringUtils_createStringFromBuffer(nameId.buf, nameId.size); + char* nameIdStr = StringUtils_createStringFromBuffer(nameId.buf, nameId.size); - if (DEBUG_MMS_SERVER) printf("MMS_SERVER: getVariableAccessAttributes (VMD specific) nameId: %s\n", nameIdStr); + if (DEBUG_MMS_SERVER) printf("MMS_SERVER: getVariableAccessAttributes (VMD specific) nameId: %s\n", nameIdStr); - createVariableAccessAttributesResponse(connection, NULL, nameIdStr, invokeId, response); + createVariableAccessAttributesResponse(connection, NULL, nameIdStr, invokeId, response); - GLOBAL_FREEMEM(nameIdStr); - } + GLOBAL_FREEMEM(nameIdStr); + } #endif /* (CONFIG_MMS_SUPPORT_VMD_SCOPE_NAMED_VARIABLES == 1) */ - else { - if (DEBUG_MMS_SERVER) printf("GetVariableAccessAttributesRequest with name other than domainspecific is not supported!\n"); - retVal = -1; - } - } - else { - if (DEBUG_MMS_SERVER) printf("GetVariableAccessAttributesRequest with address not supported!\n"); - retVal = -1; - } - } - else { - if (DEBUG_MMS_SERVER) printf("GetVariableAccessAttributesRequest parsing request failed!\n"); - retVal = -1; - } - - asn_DEF_GetVariableAccessAttributesRequest.free_struct(&asn_DEF_GetVariableAccessAttributesRequest, request, 0); - - return retVal; + else { + if (DEBUG_MMS_SERVER) printf("GetVariableAccessAttributesRequest with name other than domainspecific is not supported!\n"); + retVal = -1; + } + } + else { + if (DEBUG_MMS_SERVER) printf("GetVariableAccessAttributesRequest with address not supported!\n"); + retVal = -1; + } + } + else { + if (DEBUG_MMS_SERVER) printf("GetVariableAccessAttributesRequest parsing request failed!\n"); + retVal = -1; + } + + asn_DEF_GetVariableAccessAttributesRequest.free_struct(&asn_DEF_GetVariableAccessAttributesRequest, request, 0); + + return retVal; } #endif /* (MMS_GET_VARIABLE_ACCESS_ATTRIBUTES == 1) */ - From 98c04dfeda5c81a5f80ed4c9ce8ef713e93b323c Mon Sep 17 00:00:00 2001 From: Michael Zillgith Date: Fri, 28 Jul 2023 15:55:20 +0100 Subject: [PATCH 06/79] - added LISTOBEJCTS access control to server_example_access_control (LIB61850-417) --- .../server_example_access_control.c | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/examples/server_example_access_control/server_example_access_control.c b/examples/server_example_access_control/server_example_access_control.c index e0b789af..365576e8 100644 --- a/examples/server_example_access_control/server_example_access_control.c +++ b/examples/server_example_access_control/server_example_access_control.c @@ -146,6 +146,20 @@ readAccessHandler(LogicalDevice* ld, LogicalNode* ln, DataObject* dataObject, Fu return DATA_ACCESS_ERROR_SUCCESS; } +static bool +listObjectsAccessHandler(void* parameter, ClientConnection connection, LogicalDevice* ld, LogicalNode* ln, DataObject* dataObject, FunctionalConstraint fc) +{ + printf("list objects access to %s/%s.%s[%s]\n", ld->name, ln->name, dataObject ? dataObject->name : "-", FunctionalConstraint_toString(fc)); + + if (!strcmp(ln->name, "GGIO1")) { + if (dataObject && !strcmp(dataObject->name, "AnIn1")) { + return false; + } + } + + return true; +} + static bool directoryAccessHandler(void* parameter, ClientConnection connection, IedServer_DirectoryCategory category, LogicalDevice* logicalDevice) { @@ -258,6 +272,9 @@ main(int argc, char** argv) IedServer_setDirectoryAccessHandler(iedServer, directoryAccessHandler, NULL); + /* control visibility of data objects in directory (get-name-list) and variable description (get-variable-access-attributes) services */ + IedServer_setListObjectsAccessHandler(iedServer, listObjectsAccessHandler, NULL); + /* MMS server will be instructed to start listening for client connections. */ IedServer_start(iedServer, tcpPort); From 5a24981048dc063bc96c3f14400ff4b5bc2df093 Mon Sep 17 00:00:00 2001 From: Michael Zillgith Date: Sun, 30 Jul 2023 19:01:32 +0100 Subject: [PATCH 07/79] - IED server: added code to create SMVCBs with the dynamic model API (LIB61850-67) --- src/iec61850/server/model/dynamic_model.c | 33 +++++++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/src/iec61850/server/model/dynamic_model.c b/src/iec61850/server/model/dynamic_model.c index 790c9be3..68710320 100644 --- a/src/iec61850/server/model/dynamic_model.c +++ b/src/iec61850/server/model/dynamic_model.c @@ -166,7 +166,6 @@ IedModel_addSettingGroupControlBlock(IedModel* self, SettingGroupControlBlock* s } #endif /* (CONFIG_IEC61850_SETTING_GROUPS == 1) */ - static void IedModel_addGSEControlBlock(IedModel* self, GSEControlBlock* gcb) { @@ -175,13 +174,29 @@ IedModel_addGSEControlBlock(IedModel* self, GSEControlBlock* gcb) else { GSEControlBlock* lastGcb = self->gseCBs; - while (lastGcb->sibling != NULL) + while (lastGcb->sibling) lastGcb = lastGcb->sibling; lastGcb->sibling = gcb; } } +static void +IedModel_addSMVControlBlock(IedModel* self, SVControlBlock* smvcb) +{ + if (self->svCBs == NULL) { + self->svCBs = smvcb; + } + else { + SVControlBlock* lastSvCB = self->svCBs; + + while (lastSvCB->sibling) + lastSvCB = lastSvCB->sibling; + + lastSvCB->sibling = smvcb; + } +} + LogicalDevice* LogicalDevice_createEx(const char* inst, IedModel* parent, const char* ldName) { @@ -512,6 +527,14 @@ GSEControlBlock_create(const char* name, LogicalNode* parent, const char* appId, return self; } +static void +LogicalNode_addSMVControlBlock(LogicalNode* self, SVControlBlock* smvcb) +{ + IedModel* model = (IedModel*) self->parent->parent; + + IedModel_addSMVControlBlock(model, smvcb); +} + SVControlBlock* SVControlBlock_create(const char* name, LogicalNode* parent, const char* svID, const char* dataSet, uint32_t confRev, uint8_t smpMod, uint16_t smpRate, uint8_t optFlds, bool isUnicast) @@ -536,6 +559,12 @@ SVControlBlock_create(const char* name, LogicalNode* parent, const char* svID, c self->optFlds = optFlds; self->isUnicast = isUnicast; + + self->dstAddress = NULL; + self->sibling = NULL; + + if (parent) + LogicalNode_addSMVControlBlock(parent, self); } return self; From 405124b8d90d3243121363a1008a3a82b50a220f Mon Sep 17 00:00:00 2001 From: Michael Zillgith Date: Sun, 30 Jul 2023 19:04:39 +0100 Subject: [PATCH 08/79] - config file generator: added code to add SMVCBs to config files (LIB61850-67) --- .../scl/communication/ConnectedAP.java | 14 +++++- .../tools/DynamicModelGenerator.java | 48 +++++++++++++++++++ 2 files changed, 61 insertions(+), 1 deletion(-) diff --git a/tools/model_generator/src/com/libiec61850/scl/communication/ConnectedAP.java b/tools/model_generator/src/com/libiec61850/scl/communication/ConnectedAP.java index d9765b04..b3cf608d 100644 --- a/tools/model_generator/src/com/libiec61850/scl/communication/ConnectedAP.java +++ b/tools/model_generator/src/com/libiec61850/scl/communication/ConnectedAP.java @@ -85,7 +85,7 @@ public class ConnectedAP { public List getSmvs() { return smvs; } - + public GSE lookupGSE(String logicalDeviceName, String name) { for (GSE gse : this.getGses()) { @@ -97,6 +97,18 @@ public class ConnectedAP { return null; } + + public SMV lookupSMV(String logicalDeviceName, String name) { + + for (SMV smv : this.getSmvs()) { + if (smv.getLdInst().equals(logicalDeviceName)) { + if (smv.getCbName().equals(name)) + return smv; + } + } + + return null; + } public PhyComAddress lookupSMVAddress(String logicalDeviceName, String name) { diff --git a/tools/model_generator/src/com/libiec61850/tools/DynamicModelGenerator.java b/tools/model_generator/src/com/libiec61850/tools/DynamicModelGenerator.java index cd1d4593..e94ca7ab 100644 --- a/tools/model_generator/src/com/libiec61850/tools/DynamicModelGenerator.java +++ b/tools/model_generator/src/com/libiec61850/tools/DynamicModelGenerator.java @@ -37,6 +37,7 @@ import com.libiec61850.scl.SclParserException; import com.libiec61850.scl.communication.ConnectedAP; import com.libiec61850.scl.communication.GSE; import com.libiec61850.scl.communication.PhyComAddress; +import com.libiec61850.scl.communication.SMV; import com.libiec61850.scl.model.AccessPoint; import com.libiec61850.scl.model.DataAttribute; import com.libiec61850.scl.model.DataModelValue; @@ -51,6 +52,7 @@ import com.libiec61850.scl.model.LogicalDevice; import com.libiec61850.scl.model.LogicalNode; import com.libiec61850.scl.model.ReportControlBlock; import com.libiec61850.scl.model.ReportSettings; +import com.libiec61850.scl.model.SampledValueControl; import com.libiec61850.scl.model.Services; import com.libiec61850.scl.model.SettingControl; @@ -166,6 +168,52 @@ public class DynamicModelGenerator { for (Log log : logicalNode.getLogs()) output.println("LOG(" + log.getName() + ");"); + + for (SampledValueControl svcb : logicalNode.getSampledValueControlBlocks()) { + LogicalDevice ld = logicalNode.getParentLogicalDevice(); + + SMV smv = null; + PhyComAddress smvAddress = null; + + if (connectedAP != null) { + smv = connectedAP.lookupSMV(ld.getInst(), svcb.getName()); + + if (smv != null) + smvAddress = smv.getAddress(); + } + else + System.out.println("WARNING: IED \"" + ied.getName() + "\" has no connected access point!"); + + output.print("SMVC("); + output.print(svcb.getName() + " "); + output.print(svcb.getSmvID() + " "); + output.print(svcb.getDatSet() + " "); + output.print(svcb.getConfRev() + " "); + output.print("0" + " "); + output.print(svcb.getSmpRate() + " "); + output.print(svcb.getSmvOpts().getIntValue() + " "); + output.print(svcb.isMulticast() ? "0" : "1"); + output.print(")"); + + if (smvAddress != null) { + output.println("{"); + + output.print("PA("); + output.print(smvAddress.getVlanPriority() + " "); + output.print(smvAddress.getVlanId() + " "); + output.print(smvAddress.getAppId() + " "); + + for (int i = 0; i < 6; i++) + output.printf("%02x", smvAddress.getMacAddress()[i]); + + output.println(");"); + + output.println("}"); + } + else { + output.println(";"); + } + } for (GSEControl gcb : logicalNode.getGSEControlBlocks()) { LogicalDevice ld = logicalNode.getParentLogicalDevice(); From 0622bae0f6eda6611352621bdc094f8b99fefa72 Mon Sep 17 00:00:00 2001 From: Michael Zillgith Date: Sun, 30 Jul 2023 20:02:37 +0100 Subject: [PATCH 09/79] - config file generator/static model generator: added support for SampledValueControl.smpMod (LIB61850-67) --- examples/iec61850_9_2_LE_example/sv.icd | 2 +- .../scl/model/SampledValueControl.java | 31 +++++++--- .../src/com/libiec61850/scl/model/SmpMod.java | 56 +++++++++++++++++++ .../tools/DynamicModelGenerator.java | 2 +- .../tools/StaticModelGenerator.java | 4 +- 5 files changed, 85 insertions(+), 10 deletions(-) create mode 100644 tools/model_generator/src/com/libiec61850/scl/model/SmpMod.java diff --git a/examples/iec61850_9_2_LE_example/sv.icd b/examples/iec61850_9_2_LE_example/sv.icd index b52f8d92..af5fe962 100644 --- a/examples/iec61850_9_2_LE_example/sv.icd +++ b/examples/iec61850_9_2_LE_example/sv.icd @@ -93,7 +93,7 @@ SCL.xsd"> + smvID="xxxxMUnn01" smpRate="80" nofASDU="1" confRev="1" smpMod="SmpPerPeriod"> diff --git a/tools/model_generator/src/com/libiec61850/scl/model/SampledValueControl.java b/tools/model_generator/src/com/libiec61850/scl/model/SampledValueControl.java index f2f03b66..6ef8c29b 100644 --- a/tools/model_generator/src/com/libiec61850/scl/model/SampledValueControl.java +++ b/tools/model_generator/src/com/libiec61850/scl/model/SampledValueControl.java @@ -16,9 +16,8 @@ public class SampledValueControl { private int nofASDU; private boolean multicast = false; private SmvOpts smvOpts; - - - + private SmpMod smpMod = SmpMod.SMP_PER_PERIOD; + public SampledValueControl(Node smvControlNode) throws SclParserException { this.name = ParserUtils.parseAttribute(smvControlNode, "name"); this.desc = ParserUtils.parseAttribute(smvControlNode, "desc"); @@ -49,10 +48,25 @@ public class SampledValueControl { Node smvOptsNode = ParserUtils.getChildNodeWithTag(smvControlNode, "SmvOpts"); this.smvOpts = new SmvOpts(smvOptsNode); + + String smpModString = ParserUtils.parseAttribute(smvControlNode, "smpMod"); + + if (smpModString != null) { + if (smpModString.equals("SmpPerPeriod")) { + smpMod = SmpMod.SMP_PER_PERIOD; + } + else if (smpModString.equals("SmpPerSec")) { + smpMod = SmpMod.SMP_PER_SECOND; + } + else if (smpModString.equals("SecPerSmp")) { + smpMod = SmpMod.SEC_PER_SMP; + } + else { + throw new SclParserException(smvControlNode, "Invalid smpMod value " + smpModString); + } + } } - - - + public String getName() { return name; } @@ -88,5 +102,8 @@ public class SampledValueControl { public SmvOpts getSmvOpts() { return smvOpts; } - + + public SmpMod getSmpMod() { + return smpMod; + } } diff --git a/tools/model_generator/src/com/libiec61850/scl/model/SmpMod.java b/tools/model_generator/src/com/libiec61850/scl/model/SmpMod.java new file mode 100644 index 00000000..4de9f6eb --- /dev/null +++ b/tools/model_generator/src/com/libiec61850/scl/model/SmpMod.java @@ -0,0 +1,56 @@ +package com.libiec61850.scl.model; + +/* + * Copyright 2023 Michael Zillgith + * + * This file is part of libIEC61850. + * + * libIEC61850 is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * libIEC61850 is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with libIEC61850. If not, see . + * + * See COPYING file for the complete license text. + */ + +public enum SmpMod +{ + SMP_PER_PERIOD(0), + SMP_PER_SECOND(1), + SEC_PER_SMP(2); + + private int value; + + private SmpMod(int value) { + this.value = value; + } + + public int getValue() { + return value; + } + + public boolean compare(int i) + { + return (value == i); + } + + public static SmpMod fromValue(int val) + { + SmpMod[] errors = SmpMod.values(); + + for (int i = 0; i < errors.length; i++) { + if (errors[i].compare(val)) + return errors[i]; + } + + return SmpMod.SMP_PER_PERIOD; + } +} diff --git a/tools/model_generator/src/com/libiec61850/tools/DynamicModelGenerator.java b/tools/model_generator/src/com/libiec61850/tools/DynamicModelGenerator.java index e94ca7ab..0c05fd57 100644 --- a/tools/model_generator/src/com/libiec61850/tools/DynamicModelGenerator.java +++ b/tools/model_generator/src/com/libiec61850/tools/DynamicModelGenerator.java @@ -189,7 +189,7 @@ public class DynamicModelGenerator { output.print(svcb.getSmvID() + " "); output.print(svcb.getDatSet() + " "); output.print(svcb.getConfRev() + " "); - output.print("0" + " "); + output.print(svcb.getSmpMod().getValue() + " "); output.print(svcb.getSmpRate() + " "); output.print(svcb.getSmvOpts().getIntValue() + " "); output.print(svcb.isMulticast() ? "0" : "1"); diff --git a/tools/model_generator/src/com/libiec61850/tools/StaticModelGenerator.java b/tools/model_generator/src/com/libiec61850/tools/StaticModelGenerator.java index 4381aff0..a06313a0 100644 --- a/tools/model_generator/src/com/libiec61850/tools/StaticModelGenerator.java +++ b/tools/model_generator/src/com/libiec61850/tools/StaticModelGenerator.java @@ -1247,8 +1247,10 @@ public class StaticModelGenerator { svString += "NULL, "; svString += svCB.getSmvOpts().getIntValue() + ", "; + + svString += svCB.getSmpMod().getValue() + ", "; - svString += "0, " + svCB.getSmpRate() + ", "; + svString += svCB.getSmpRate() + ", "; svString += svCB.getConfRev() + ", "; From 15359059212929e1d2594a081c2a02aaf0363155 Mon Sep 17 00:00:00 2001 From: Michael Zillgith Date: Sun, 30 Jul 2023 20:06:42 +0100 Subject: [PATCH 10/79] - added updated compiled versions of config file and static model generator (LIB61850-67) --- tools/model_generator/genconfig.jar | Bin 97408 -> 100509 bytes tools/model_generator/genmodel.jar | Bin 98733 -> 100549 bytes 2 files changed, 0 insertions(+), 0 deletions(-) diff --git a/tools/model_generator/genconfig.jar b/tools/model_generator/genconfig.jar index 2c9ed1fc32bfed1a96eac95456e1f376101d23df..6f42a2ba6247a31d4ea4ed573b23e23305214280 100644 GIT binary patch delta 40581 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

>CojhY(G()abf3Sm<%HU}%UNlAqRU=HF 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