diff --git a/include/iec61850.hpp b/include/iec61850.hpp index 63d7896..b94145a 100644 --- a/include/iec61850.hpp +++ b/include/iec61850.hpp @@ -145,6 +145,9 @@ class IEC61850Client void sendCommandAck (const std::string& label, ControlModel mode, bool terminated); + bool firstTimeConnect = true; + MmsValue* lastEntryId = nullptr; + private: std::shared_ptr > m_connections = nullptr; @@ -152,6 +155,7 @@ class IEC61850Client IEC61850ClientConnection* m_active_connection = nullptr; std::mutex m_activeConnectionMtx; + enum class ConnectionStatus { STARTED, @@ -176,7 +180,7 @@ class IEC61850Client template Datapoint* m_createDatapoint (const std::string& label, const std::string& objRef, T value, - Quality quality, uint64_t timestampMs); + Quality quality, uint64_t timestampMs, bool hasValue); static int getRootFromCDC (const CDCTYPE cdc); void addQualityDp (Datapoint* cdcDp, Quality quality) const; diff --git a/include/iec61850_client_config.hpp b/include/iec61850_client_config.hpp index ad89b57..25cbb95 100644 --- a/include/iec61850_client_config.hpp +++ b/include/iec61850_client_config.hpp @@ -26,7 +26,9 @@ FRIEND_TEST (ReportingTest, ReportingUpdateQuality); \ FRIEND_TEST (ReportingTest, ReportingGI); \ FRIEND_TEST (ReportingTest, ReportingSetpointCommand); \ + FRIEND_TEST (ReportingTest, ReconfigureDynamicDataset); \ FRIEND_TEST (ReportingTest, ReportingChangeValueMultipleTimes); \ + FRIEND_TEST (ReportingTest, ReportingIndividualAttributes); \ FRIEND_TEST (SpontDataTest, Polling); \ FRIEND_TEST (SpontDataTest, PollingAllCDC); \ FRIEND_TEST (ControlTest, AnalogueCommandDirectNormal); \ @@ -115,6 +117,12 @@ struct DataExchangeDefinition std::string label; std::string id; MmsVariableSpecification* spec; + bool hasIntValue = true; + union{ + int intVal; + float floatVal; + } lastValue; + bool valueSet = false; }; struct ReportSubscription diff --git a/src/iec61850.cpp b/src/iec61850.cpp index bffc1e7..9d9af9d 100644 --- a/src/iec61850.cpp +++ b/src/iec61850.cpp @@ -41,23 +41,6 @@ void IEC61850::start () { Iec61850Utility::log_info ("Starting iec61850"); - // LCOV_EXCL_START - switch (m_config->LogLevel ()) - { - case 1: - Logger::getLogger ()->setMinLevel ("debug"); - break; - case 2: - Logger::getLogger ()->setMinLevel ("info"); - break; - case 3: - Logger::getLogger ()->setMinLevel ("warning"); - break; - default: - Logger::getLogger ()->setMinLevel ("error"); - break; - } - // LCOV_EXCL_STOP m_client = new IEC61850Client (this, m_config); diff --git a/src/iec61850_client.cpp b/src/iec61850_client.cpp index 262de35..6909738 100644 --- a/src/iec61850_client.cpp +++ b/src/iec61850_client.cpp @@ -50,20 +50,6 @@ isCommandCdcType (CDCTYPE type) return type >= SPC && type < SPG; } -static uint64_t -getMonotonicTimeInMs () -{ - uint64_t timeVal = 0; - - struct timespec ts; - - if (clock_gettime (CLOCK_MONOTONIC, &ts) == 0) - { - timeVal = ((uint64_t)ts.tv_sec * 1000LL) + (ts.tv_nsec / 1000000); - } - - return timeVal; -} static long getValueInt (Datapoint* dp) @@ -221,19 +207,20 @@ const std::map cdcToStrMap = { { SPS, "SpsTyp" }, { DPS, "DpsTyp" }, { BSC, "BscTyp" }, { MV, "MvTyp" }, { SPC, "SpcTyp" }, { DPC, "DpcTyp" }, { APC, "ApcTyp" }, { INC, "IncTyp" }, { INS, "InsTyp" }, - { ENS, "EnsTyp" }, { SPG, "SpgTyp" }, { ASG, "AsgType" }, + { ENS, "EnsTyp" }, { SPG, "SpgTyp" }, { ASG, "AsgTyp" }, { ING, "IngTyp" } }; const std::map rootMap - = { { SPS, GTIS }, { DPS, GTIS }, { BSC, GTIC }, { INS, GTIS }, - { ENS, GTIS }, { MV, GTIM }, { SPC, GTIC }, { DPC, GTIC }, - { APC, GTIC }, { INC, GTIC } }; + = { { SPS, GTIS }, { DPS, GTIS }, { BSC, GTIS }, { INS, GTIS }, + { ENS, GTIS }, { MV, GTIM }, { SPC, GTIS }, { DPC, GTIS }, + { APC, GTIM }, { INC, GTIS }, { ASG, GTIM }, { SPG, GTIS }, + { ING, GTIS} }; const std::map rootToStrMap = { { GTIM, "GTIM" }, { GTIS, "GTIS" }, { GTIC, "GTIC" } }; IEC61850Client::IEC61850Client (IEC61850* iec61850, IEC61850ClientConfig* iec61850_client_config) - : m_config (iec61850_client_config), m_iec61850 (iec61850) + : m_config (iec61850_client_config), m_iec61850 (iec61850), firstTimeConnect(true) { } @@ -255,6 +242,11 @@ IEC61850Client::stop () delete m_monitoringThread; m_monitoringThread = nullptr; } + + if(lastEntryId){ + MmsValue_delete(lastEntryId); + lastEntryId = nullptr; + } } int @@ -809,12 +801,25 @@ IEC61850Client::m_handleMonitoringData ( Quality quality = extractQuality (mmsvalue, def->spec, attribute); uint64_t ts; - + if (!mmsVal) ts = extractTimestamp (mmsvalue, def->spec, attribute); else ts = timestamp; + if(attribute == "q"){ + if(!def->valueSet){ + Iec61850Utility::log_debug("Value for %s not yet set, sending only quality", objRef.c_str()); + datapoints.push_back (m_createDatapoint (label, objRef, 0, quality, timestamp, false)); + cleanUpMmsValue (mmsVal, mmsvalue); + return; + } + else{ + datapoints.push_back (m_createDatapoint (label, objRef, def->hasIntValue ? def->lastValue.intVal : def->lastValue.floatVal, quality, timestamp, true)); + cleanUpMmsValue (mmsVal, mmsvalue); + return; + } + } if (!processDatapoint (type, datapoints, label, objRef, mmsvalue, def->spec, quality, ts, attribute)) { @@ -834,7 +839,7 @@ IEC61850Client::extractQuality (MmsValue* mmsvalue, = MmsValue_getSubElement (mmsvalue, varSpec, (char*)"q"); return (!qualityMms && attribute != "q") ? QUALITY_VALIDITY_GOOD - : Quality_fromMmsValue (qualityMms); + : Quality_fromMmsValue ( attribute == "q" ? mmsvalue : qualityMms); } uint64_t @@ -855,7 +860,7 @@ IEC61850Client::processDatapoint ( const std::string& label, const std::string& objRef, MmsValue* mmsvalue, MmsVariableSpecification* varSpec, Quality quality, uint64_t timestamp, const std::string& attribute) -{ +{ switch (type) { case SPC: @@ -916,8 +921,12 @@ IEC61850Client::processBooleanType ( } } bool value = MmsValue_getBoolean (element); + auto def = m_config->getExchangeDefinitionByObjRef (objRef); + def->lastValue.intVal = (long)value; + def->valueSet = true; + datapoints.push_back ( - m_createDatapoint (label, objRef, (long)value, quality, timestamp)); + m_createDatapoint (label, objRef, (long)value, quality, timestamp, true)); return true; } @@ -963,8 +972,12 @@ IEC61850Client::processBSCType (std::vector& datapoints, long value = MmsValue_toInt32 (posVal); bool transIndVal = MmsValue_getBoolean (transInd); long combinedValue = (value << 1) | (long)transIndVal; + auto def = m_config->getExchangeDefinitionByObjRef (objRef); + def->lastValue.intVal = (long)combinedValue; + def->valueSet = true; + datapoints.push_back ( - m_createDatapoint (label, objRef, combinedValue, quality, timestamp)); + m_createDatapoint (label, objRef, combinedValue, quality, timestamp, true)); return true; } @@ -991,6 +1004,7 @@ IEC61850Client::processAnalogType ( } } + auto def = m_config->getExchangeDefinitionByObjRef (objRef); varSpec = MmsVariableSpecification_getChildSpecificationByName ( varSpec, elementName, nullptr); MmsValue* f = MmsValue_getSubElement (element, varSpec, (char*)"f"); @@ -998,8 +1012,11 @@ IEC61850Client::processAnalogType ( if (f) { double value = MmsValue_toFloat (f); + def->lastValue.floatVal = value; + def->valueSet = true; + datapoints.push_back ( - m_createDatapoint (label, objRef, value, quality, timestamp)); + m_createDatapoint (label, objRef, value, quality, timestamp, true)); return true; } @@ -1007,8 +1024,12 @@ IEC61850Client::processAnalogType ( if (i) { long value = MmsValue_toInt32 (i); + def->hasIntValue = true; + def->lastValue.intVal = value; + def->valueSet = true; + datapoints.push_back ( - m_createDatapoint (label, objRef, value, quality, timestamp)); + m_createDatapoint (label, objRef, value, quality, timestamp, true)); return true; } @@ -1039,8 +1060,13 @@ IEC61850Client::processIntegerType ( } } long value = MmsValue_toInt32 (element); + auto def = m_config->getExchangeDefinitionByObjRef (objRef); + + def->lastValue.intVal = (long) value; + def->valueSet = true; + datapoints.push_back ( - m_createDatapoint (label, objRef, value, quality, timestamp)); + m_createDatapoint (label, objRef, value, quality, timestamp,true)); return true; } @@ -1048,7 +1074,7 @@ template Datapoint* IEC61850Client::m_createDatapoint (const std::string& label, const std::string& objRef, T value, - Quality quality, uint64_t timestamp) + Quality quality, uint64_t timestamp, bool hasValue) { auto def = m_config->getExchangeDefinitionByLabel (label); @@ -1061,7 +1087,9 @@ IEC61850Client::m_createDatapoint (const std::string& label, addElementWithValue (rootDp, "Identifier", (std::string)def->label); Datapoint* cdcDp = addElement (rootDp, cdcToStrMap.at (def->cdcType)); - addValueDp (cdcDp, def->cdcType, value); + if(hasValue){ + addValueDp (cdcDp, def->cdcType, value); + } addQualityDp (cdcDp, quality); addTimestampDp (cdcDp, timestamp); diff --git a/src/iec61850_client_config.cpp b/src/iec61850_client_config.cpp index d417135..e72be2c 100644 --- a/src/iec61850_client_config.cpp +++ b/src/iec61850_client_config.cpp @@ -34,12 +34,11 @@ using namespace rapidjson; static const std::unordered_map trgOptions - = { { "data_changed", TRG_OPT_DATA_CHANGED }, - { "quality_changed", TRG_OPT_QUALITY_CHANGED }, - { "data_update", TRG_OPT_DATA_UPDATE }, - { "integrity", TRG_OPT_INTEGRITY }, - { "gi", TRG_OPT_GI }, - { "transient", TRG_OPT_TRANSIENT } }; + = { { "dchg", TRG_OPT_DATA_CHANGED }, + { "qchg", TRG_OPT_QUALITY_CHANGED }, + { "dupd", TRG_OPT_DATA_UPDATE }, + { "period", TRG_OPT_INTEGRITY }, + { "gi", TRG_OPT_GI } }; static const std::unordered_map cdcMap = { { "SpsTyp", SPS }, { "DpsTyp", DPS }, { "BscTyp", BSC }, @@ -283,8 +282,11 @@ IEC61850ClientConfig::importProtocolConfig (const std::string& protocolConfig) const std::shared_ptr def = getExchangeDefinitionByObjRef (extractedObjRef); - if (def) + if (def){ m_polledDatapoints.erase (extractedObjRef); + Iec61850Utility::log_debug ( + "%s won't be polled", extractedObjRef.c_str()); + } } } } @@ -321,6 +323,7 @@ IEC61850ClientConfig::importProtocolConfig (const std::string& protocolConfig) } else { + Iec61850Utility::log_error("Report subscription has no RCB ref , skipping"); continue; } @@ -331,7 +334,8 @@ IEC61850ClientConfig::importProtocolConfig (const std::string& protocolConfig) } else { - report->datasetRef = ""; + Iec61850Utility::log_error("Report subscription %s has no Dataset ref , skipping", report->rcbRef.c_str()); + continue; } if (reportVal.HasMember (JSON_TRGOPS) @@ -377,7 +381,7 @@ IEC61850ClientConfig::importProtocolConfig (const std::string& protocolConfig) } else { - Iec61850Utility::log_error ( + Iec61850Utility::log_warn ( "Report %s has no gi value, defaulting to disabled", report->rcbRef.c_str ()); report->gi = false; @@ -585,59 +589,62 @@ IEC61850ClientConfig::parseOsiTSelector (std::string& inputOsiSelector, } OsiSelectorSize -IEC61850ClientConfig::parseOsiSelector (std::string& inputOsiSelector, - uint8_t* selectorValue, - const uint8_t selectorSize) +IEC61850ClientConfig::parseOsiSelector(std::string& inputOsiSelector, uint8_t* selectorValue, const uint8_t selectorSize) { - char* tokenContext = nullptr; - const char* nextToken - = strtok_r (&inputOsiSelector[0], " ,.-", &tokenContext); - uint8_t count = 0; + uint8_t i = 0; - while (nullptr != nextToken) + if (inputOsiSelector.find(',') == std::string::npos) { - if (count >= selectorSize) + std::regex hexPattern("^([0-9a-fA-F]{2})+$"); + if(inputOsiSelector.substr(0, 2) == "0x") inputOsiSelector.erase(0, 2); + if(!std::regex_match(inputOsiSelector, hexPattern)) { - throw ConfigurationException ( - "bad format for 'OSI Selector' (too many bytes)"); + throw ConfigurationException("bad format for 'OSI Selector' (Expected hexadecimal string)"); } - int base = 10; - - if (0 == strncmp (nextToken, "0x", 2)) + if(inputOsiSelector.size()/2 > selectorSize) { - base = 16; + throw ConfigurationException("bad format for 'OSI Selector' (too many bytes)"); } - unsigned long ul = 0; + std::istringstream hex_chars_stream(inputOsiSelector); + uint8_t count = 0; - try + char hex_chars[2]; + + while (hex_chars_stream >> hex_chars[0] >> hex_chars[1]) { - ul = std::stoul (nextToken, nullptr, base); - } - catch (std::invalid_argument&) - { - throw ConfigurationException ( - "bad format for 'OSI Selector' (not a byte)"); - } - catch (std::out_of_range&) - { - throw ConfigurationException ( - "bad format for 'OSI Selector (exceed an int)'"); + char* endptr; + selectorValue[count] = static_cast(strtol(hex_chars, &endptr, 16)); + if (*endptr != 0) + { + throw ConfigurationException("bad format for 'OSI Selector' (not a byte)"); + } + count++; } + return count; + } + else + { + std::replace(inputOsiSelector.begin(), inputOsiSelector.end(), ',', ' '); + std::istringstream ss(inputOsiSelector); + std::string token; - if (ul > 255) + while(std::getline(ss, token, ' ') && i < selectorSize) { - throw ConfigurationException ( - "bad format for 'OSI Selector' (exceed a byte)"); - } + if(token.substr(0, 2) == "0x") token.erase(0, 2); + + std::regex hexPattern("^([0-9a-fA-F]{2})+$"); + if(!std::regex_match(token, hexPattern)) + { + throw ConfigurationException("bad format for 'OSI Selector' (Expected hexadecimal string)"); + } - selectorValue[count] = static_cast (ul); - count++; - nextToken = strtok_r (nullptr, " ,.-", &tokenContext); + selectorValue[i] = static_cast(std::stoul(token, nullptr, 16)); + i++; + } + return i; } - - return count; } void @@ -775,6 +782,10 @@ IEC61850ClientConfig::importExchangeConfig (const std::string& exchangeConfig) def->label = label; def->id = pivot_id; + if(def->cdcType == MV || def->cdcType == APC || def->cdcType == ASG){ + def->hasIntValue = false; + } + m_exchangeDefinitions.insert ({ label, def }); m_exchangeDefinitionsPivotId.insert ({ pivot_id, def }); m_exchangeDefinitionsObjRef.insert ({ objRef, def }); diff --git a/src/iec61850_client_connection.cpp b/src/iec61850_client_connection.cpp index c7cd286..192d864 100644 --- a/src/iec61850_client_connection.cpp +++ b/src/iec61850_client_connection.cpp @@ -299,9 +299,27 @@ IEC61850ClientConnection::reportCallbackFunction (void* parameter, MmsValue const* dataSetValues = ClientReport_getDataSetValues (report); - Iec61850Utility::log_debug ("received report for %s with rptId %s\n", + char buf[1024]; + if(ClientReport_getEntryId(report) != nullptr){ + if(con->m_client->lastEntryId !=nullptr){ + MmsValue_delete(con->m_client->lastEntryId); + } + con->m_client->lastEntryId = MmsValue_clone(ClientReport_getEntryId(report)); + if(con->m_client->lastEntryId!=nullptr){ + MmsValue_printToBuffer(con->m_client->lastEntryId,buf,1024); + } + } + + Iec61850Utility::log_debug ("received report for %s with rptId %s entryId %s", ClientReport_getRcbReference (report), - ClientReport_getRptId (report)); + ClientReport_getRptId (report), + con->m_client->lastEntryId ? buf : "NULL"); + + if(ClientReport_hasBufOvfl(report) && ClientReport_getBufOvfl(report)){ + Iec61850Utility::log_warn("Buffer overflow bit set for report with rptId %s and entryId %s", + ClientReport_getRptId (report), + con->m_client->lastEntryId ? buf : "NULL"); + } uint64_t unixTime = 0; @@ -342,16 +360,44 @@ IEC61850ClientConnection::reportCallbackFunction (void* parameter, static int configureRcb (const std::shared_ptr& rs, - ClientReportControlBlock rcb) + ClientReportControlBlock rcb, bool firstTimeConnect, MmsValue* lastEntryId) { uint32_t parametersMask = 0; bool isBuffered = ClientReportControlBlock_isBuffered (rcb); if (isBuffered) - parametersMask |= RCB_ELEMENT_RESV_TMS; + { + parametersMask |= RCB_ELEMENT_OPT_FLDS; + ClientReportControlBlock_setOptFlds(rcb,RPT_OPT_REASON_FOR_INCLUSION | RPT_OPT_ENTRY_ID | RPT_OPT_TIME_STAMP| RPT_OPT_DATA_SET); + + if (ClientReportControlBlock_hasResvTms(rcb)) + { + parametersMask |= RCB_ELEMENT_RESV_TMS; + ClientReportControlBlock_setResvTms (rcb, 1000); + } + + if (firstTimeConnect) + { + parametersMask |= RCB_ELEMENT_PURGE_BUF; + ClientReportControlBlock_setPurgeBuf (rcb, true); + } + else{ + char buf[1024]; + if(lastEntryId!=nullptr){ + MmsValue_printToBuffer(lastEntryId,buf,1024); + } + Iec61850Utility::log_debug("Reconnecting, send Entry ID %s for RCB %s", lastEntryId != nullptr ? buf : "NULL", rs->rcbRef.c_str()); + ClientReportControlBlock_setEntryId(rcb,lastEntryId); + + parametersMask |= RCB_ELEMENT_ENTRY_ID; + } + } else + { + ClientReportControlBlock_setResv (rcb, true); parametersMask |= RCB_ELEMENT_RESV; + } if (rs->trgops != -1) { @@ -368,11 +414,6 @@ configureRcb (const std::shared_ptr& rs, parametersMask |= RCB_ELEMENT_INTG_PD; ClientReportControlBlock_setIntgPd (rcb, rs->intgpd); } - if (rs->gi) - { - parametersMask |= RCB_ELEMENT_GI; - ClientReportControlBlock_setGI (rcb, rs->gi); - } if (!rs->datasetRef.empty ()) { @@ -384,6 +425,12 @@ configureRcb (const std::shared_ptr& rs, rcb, modifiedDataSetRef.c_str ()); } + if (rs->gi) + { + parametersMask |= RCB_ELEMENT_GI; + ClientReportControlBlock_setGI (rcb, rs->gi); + } + ClientReportControlBlock_setRptEna (rcb, true); parametersMask |= RCB_ELEMENT_RPT_ENA; @@ -413,7 +460,7 @@ IEC61850ClientConnection::m_configRcb () if (error != IED_ERROR_OK) { Iec61850Utility::log_error ( - "Reading data set directory failed!\n"); + "Reading data set directory failed! %s", rs->datasetRef.c_str()); continue; } @@ -431,11 +478,12 @@ IEC61850ClientConnection::m_configRcb () if (error != IED_ERROR_OK) { - Iec61850Utility::log_error ("GetRCBValues service error!\n"); + m_client->logIedClientError(error,"GetRCBValues " + rs->rcbRef); continue; } - uint32_t parametersMask = configureRcb (rs, rcb); + uint32_t parametersMask + = configureRcb (rs, rcb, m_client->firstTimeConnect,m_client->lastEntryId); auto connDataSetPair = new std::pair ( @@ -448,8 +496,9 @@ IEC61850ClientConnection::m_configRcb () ClientReportControlBlock_getRptId (rcb), reportCallbackFunction, static_cast (connDataSetPair)); + IedConnection_setRCBValues (m_connection, &error, rcb, parametersMask, - true); + true); if (clientDataSet) ClientDataSet_destroy (clientDataSet); @@ -494,7 +543,8 @@ IEC61850ClientConnection::m_initialiseControlObjects () continue; IedClientError err; MmsValue* temp = IedConnection_readObject ( - m_connection, &err, def->objRef.c_str (), IEC61850_FC_ST); + m_connection, &err, (def->objRef+".ctlModel").c_str(), IEC61850_FC_ST); + ControlModel model = (ControlModel) MmsValue_toInt32(temp); if (err != IED_ERROR_OK) { m_client->logIedClientError (err, "Initialise control object"); @@ -502,8 +552,15 @@ IEC61850ClientConnection::m_initialiseControlObjects () } MmsValue_delete (temp); auto co = new ControlObjectStruct; - co->client - = ControlObjectClient_create (def->objRef.c_str (), m_connection); + co->client = ControlObjectClient_create (def->objRef.c_str (), m_connection); + if(!co->client){ + if(model != CONTROL_MODEL_STATUS_ONLY){ + Iec61850Utility::log_warn ("Failed to create Control ObjectClient %s , %s ", + entry.first.c_str (), def->objRef.c_str ()); + } + delete co; + continue; + } co->mode = ControlObjectClient_getControlModel (co->client); co->state = CONTROL_IDLE; co->label = entry.first; @@ -575,6 +632,74 @@ IEC61850ClientConnection::cleanUp () m_connDataSetDirectoryPairs.clear (); } + for(const auto &dataset: m_config->getDatasets()){ + if(dataset.second->dynamic){ + for(const auto &rcb : m_config->getReportSubscriptions()){ + if(rcb.second->datasetRef == dataset.second->datasetRef){ + IedClientError error = IED_ERROR_OK; + if(m_connection && IedConnection_getState(m_connection) == IED_STATE_CONNECTED){ + + ClientReportControlBlock block = IedConnection_getRCBValues (m_connection, &error, + rcb.second->rcbRef.c_str (), nullptr); + + if(!block){ + Iec61850Utility::log_debug("RCB %s not found, continue", rcb.second->rcbRef.c_str()); + m_client->logIedClientError(error, "Get RCB in clean up"); + continue; + } + + ClientReportControlBlock_setRptEna(block,false); + ClientReportControlBlock_setDataSetReference(block, ""); + + IedConnection_setRCBValues(m_connection,&error,block, RCB_ELEMENT_RPT_ENA | RCB_ELEMENT_DATSET, true); + + if(error!=IED_ERROR_OK){ + m_client->logIedClientError(error,"Remove dynamic dataset " + dataset.second->datasetRef + " from RCB " + rcb.second->rcbRef); + } + else{ + Iec61850Utility::log_debug("Update RCB %s", rcb.second->rcbRef.c_str()); + } + ClientReportControlBlock_destroy(block); + + std::string modifiedDatasetRef = dataset.second->datasetRef; + + if(!IedConnection_deleteDataSet(m_connection,&error,modifiedDatasetRef.c_str())){ + m_client->logIedClientError(error, "Delete dynamic dataset " + modifiedDatasetRef); + } + } + } + } + + } + } + + for(const auto &rcb : m_config->getReportSubscriptions()){ + IedClientError error = IED_ERROR_OK; + if(m_connection && IedConnection_getState(m_connection) == IED_STATE_CONNECTED){ + + ClientReportControlBlock block = IedConnection_getRCBValues (m_connection, &error, + rcb.second->rcbRef.c_str (), nullptr); + + if(!block){ + Iec61850Utility::log_debug("RCB %s not found, continue", rcb.second->rcbRef.c_str()); + m_client->logIedClientError(error, "Get RCB in clean up"); + continue; + } + + ClientReportControlBlock_setRptEna(block,false); + + IedConnection_setRCBValues(m_connection,&error,block, RCB_ELEMENT_RPT_ENA, true); + + if(error!=IED_ERROR_OK){ + m_client->logIedClientError(error,"Disable RCB " + rcb.second->rcbRef); + } + else{ + Iec61850Utility::log_debug("Disabled RCB %s", rcb.second->rcbRef.c_str()); + } + ClientReportControlBlock_destroy(block); + } + } + if (!m_controlObjects.empty ()) { for (auto& co : m_controlObjects) @@ -599,6 +724,7 @@ IEC61850ClientConnection::cleanUp () m_controlObjects.clear (); } + if (!m_connControlPairs.empty ()) { for (auto& cc : m_connControlPairs) @@ -1048,6 +1174,7 @@ IEC61850ClientConnection::_conThread () m_connectionState = CON_STATE_CONNECTED; m_connecting = false; m_connected = true; + m_client->firstTimeConnect = false; } } else if (getMonotonicTimeInMs () diff --git a/src/plugin.cpp b/src/plugin.cpp index 0de0ee7..2cb1807 100644 --- a/src/plugin.cpp +++ b/src/plugin.cpp @@ -198,7 +198,7 @@ extern "C" if (config.itemExists ("protocol_stack") && config.itemExists ("exchanged_data") - && config.itemExists ("tls")) + && config.itemExists ("tls_conf")) iec61850->setJsonConfig (config.getValue ("protocol_stack"), config.getValue ("exchanged_data"), config.getValue ("tls_conf")); diff --git a/tests/test_iec61850_client_control.cpp b/tests/test_iec61850_client_control.cpp index d213267..0e02bda 100644 --- a/tests/test_iec61850_client_control.cpp +++ b/tests/test_iec61850_client_control.cpp @@ -1070,9 +1070,6 @@ TEST_F(ControlTest, WriteOperations) { iec61850->setJsonConfig(protocol_config, exchanged_data_3 , tls_config); IedModel* model = ConfigFileParser_createModelFromConfigFileEx("../tests/data/iec61850fledgetest.cfg"); - - ASSERT_TRUE(model != NULL); - IedServer server = IedServer_create(model); IedServer_start(server,10002); diff --git a/tests/test_iec61850_config.cpp b/tests/test_iec61850_config.cpp index 832ff6b..bbfce7a 100644 --- a/tests/test_iec61850_config.cpp +++ b/tests/test_iec61850_config.cpp @@ -132,10 +132,10 @@ static string osi_protocol_config = QUOTE({ "remote_ap_title":"1,2,1200,15,3", "remote_ae_qualifier":1, "local_psel":"0x12,0x34,0x56,0x78", - "local_ssel":"0,1,2,3,4", + "local_ssel":"0x04,0x01,0x02,0x03,0x04", "local_tsel":"0x00,0x01,0x02", "remote_psel":"0x87,0x65,0x43,0x21", - "remote_ssel":"0,1", + "remote_ssel":"0x00,0x01", "remote_tsel":"0x00,0x01" } } @@ -293,8 +293,8 @@ static string wrong_protocol_config_13 = QUOTE({ "rcb_ref" : "simpleIOGenericIO/LLN0.RP.EventsIndexed", "dataset_ref" : "simpleIOGenericIO/LLN0.Events2", "trgops" : [ - "data_changed", - "quality_changed", + "dchg", + "qchg", "gi" ], "gi" : false @@ -325,8 +325,8 @@ static string wrong_protocol_config_14 = QUOTE({ "rcb_ref" : "simpleIOGenericIO/LLN0.RP.EventsIndexed", "dataset_ref" : "simpleIOGenericIO/LLN0.Events2", "trgops" : [ - "data_changed", - "quality_changed", + "dchg", + "qchg", "gi" ], "gi" : false @@ -356,8 +356,8 @@ static string wrong_protocol_config_15 = QUOTE({ { "rcb_ref" : "simpleIOGenericIO/LLN0.RP.EventsIndexed", "trgops" : [ - "data_changed", - "quality_changed", + "dchg", + "qchg", "gi" ], "gi" : false @@ -448,8 +448,8 @@ static string wrong_protocol_config_17 = QUOTE({ "rcb_ref" : "simpleIOGenericIO/LLN0.RP.EventsRCB", "dataset_ref" : "simpleIOGenericIO/LLN0.Mags", "trgops" : [ - "data_changed", - "quality_changed" + "dchg", + "qchg" ], "buftm": 1, "intgpd": 2 @@ -458,8 +458,8 @@ static string wrong_protocol_config_17 = QUOTE({ "rcb_ref" : "simpleIOGenericIO/LLN0.RP.EventsIndexed", "dataset_ref" : "simpleIOGenericIO/LLN0.Events2", "trgops" : [ - "data_changed", - "quality_changed" + "dchg", + "qchg" ], "buftm": 1, "intgpd": 2 @@ -1105,4 +1105,126 @@ TEST_F(ConfigTest, ProtocolConfigBuftmIntgpd) { config->importProtocolConfig(wrong_protocol_config_17); ASSERT_TRUE(config->m_protocolConfigComplete); +} + +TEST_F(ConfigTest, TestOSISelector) { + IEC61850ClientConfig* config = new IEC61850ClientConfig(); + + std::string testStr = "0x00,0x01,0x02,0x03"; + uint8_t selectorValue[10]; + + ASSERT_NO_THROW({ + uint8_t parsedBytes = config->parseOsiSelector(testStr, selectorValue, sizeof(selectorValue)); + ASSERT_EQ(parsedBytes, 4); + for(int i = 0; i < parsedBytes; i++) { + ASSERT_EQ(selectorValue[i], i); + } + }); + + testStr = "0x03"; + + ASSERT_NO_THROW({ + uint8_t parsedBytes = config->parseOsiSelector(testStr, selectorValue, sizeof(selectorValue)); + ASSERT_EQ(parsedBytes, 1); + ASSERT_EQ(selectorValue[0], 3); + }); + + testStr = "0x05,0x02"; + + ASSERT_NO_THROW({ + uint8_t parsedBytes = config->parseOsiSelector(testStr, selectorValue, sizeof(selectorValue)); + ASSERT_EQ(parsedBytes, 2); + ASSERT_EQ(selectorValue[0], 5); + ASSERT_EQ(selectorValue[1], 2); + }); + + testStr = "f143125c"; + + ASSERT_NO_THROW({ + uint8_t parsedBytes = config->parseOsiSelector(testStr, selectorValue, sizeof(selectorValue)); + ASSERT_EQ(parsedBytes, 4); + ASSERT_EQ(selectorValue[0],241); + ASSERT_EQ(selectorValue[1],67); + ASSERT_EQ(selectorValue[2],18); + ASSERT_EQ(selectorValue[3],92); + }); + + testStr = "00000001"; + + ASSERT_NO_THROW({ + uint8_t parsedBytes = config->parseOsiSelector(testStr, selectorValue, sizeof(selectorValue)); + ASSERT_EQ(parsedBytes, 4); + ASSERT_EQ(selectorValue[0],0); + ASSERT_EQ(selectorValue[1],0); + ASSERT_EQ(selectorValue[2],0); + ASSERT_EQ(selectorValue[3],1); + }); + + testStr = "123"; + ASSERT_THROW(config->parseOsiSelector(testStr, selectorValue, sizeof(selectorValue)), ConfigurationException); + + testStr = "0x00,0x01,0x02,0x0Z"; + ASSERT_THROW(config->parseOsiSelector(testStr, selectorValue, sizeof(selectorValue)), ConfigurationException); + + testStr = "0a0b0c0d"; + + ASSERT_NO_THROW({ + uint8_t parsedBytes = config->parseOsiSelector(testStr, selectorValue, sizeof(selectorValue)); + ASSERT_EQ(parsedBytes, 4); + ASSERT_EQ(selectorValue[0], 10); + ASSERT_EQ(selectorValue[1], 11); + ASSERT_EQ(selectorValue[2], 12); + ASSERT_EQ(selectorValue[3], 13); + }); + + testStr = "ff"; + + ASSERT_NO_THROW({ + uint8_t parsedBytes = config->parseOsiSelector(testStr, selectorValue, sizeof(selectorValue)); + ASSERT_EQ(parsedBytes, 1); + ASSERT_EQ(selectorValue[0], 255); + }); + + testStr = "AaBfC112"; + + ASSERT_NO_THROW({ + uint8_t parsedBytes = config->parseOsiSelector(testStr, selectorValue, sizeof(selectorValue)); + ASSERT_EQ(parsedBytes, 4); + ASSERT_EQ(selectorValue[0], 170); + ASSERT_EQ(selectorValue[1], 191); + ASSERT_EQ(selectorValue[2], 193); + ASSERT_EQ(selectorValue[3], 18); + }); + + testStr = "01A609C605CC"; + + ASSERT_THROW({ + config->parseOsiSelector(testStr, selectorValue, 4 ); + }, ConfigurationException); + + testStr = "123G56"; + + ASSERT_THROW({ + config->parseOsiSelector(testStr, selectorValue, sizeof(selectorValue)); + }, ConfigurationException); + +} + +TEST_F(ConfigTest, TestCommaSeparatedBytes) { + IEC61850ClientConfig* config = new IEC61850ClientConfig(); + + std::string testStr = "0x00,0x01,0x02,0x03"; + uint8_t selectorValue[4]; + + ASSERT_NO_THROW({ + uint8_t parsedBytes = config->parseOsiSelector(testStr, selectorValue, sizeof(selectorValue)); + ASSERT_EQ(parsedBytes, 4); + for(int i = 0; i < parsedBytes; i++) { + ASSERT_EQ(selectorValue[i], i); + } + }); + + + testStr = "0x00,0x01,0xG2,0x03"; + ASSERT_THROW(config->parseOsiSelector(testStr, selectorValue, sizeof(selectorValue)), ConfigurationException); } \ No newline at end of file diff --git a/tests/test_iec61850_reporting.cpp b/tests/test_iec61850_reporting.cpp index 906989c..f272b2e 100644 --- a/tests/test_iec61850_reporting.cpp +++ b/tests/test_iec61850_reporting.cpp @@ -51,13 +51,13 @@ static string protocol_config = QUOTE ({ { "rcb_ref" : "simpleIOGenericIO/LLN0.RP.EventsRCB01", "dataset_ref" : "simpleIOGenericIO/LLN0.Mags", - "trgops" : [ "data_changed", "quality_changed", "gi" ], + "trgops" : [ "dchg", "qchg", "gi" ], "gi" : false }, { "rcb_ref" : "simpleIOGenericIO/LLN0.RP.EventsIndexed01", "dataset_ref" : "simpleIOGenericIO/LLN0.Events2", - "trgops" : [ "data_changed", "quality_changed", "gi" ], + "trgops" : [ "dchg", "qchg", "gi" ], "gi" : false } ] @@ -84,8 +84,8 @@ static string protocol_config_2 = QUOTE ({ "simpleIOGenericIO/GGIO1.AnIn2[MX]", "simpleIOGenericIO/GGIO1.AnIn3[MX]", "simpleIOGenericIO/GGIO1.AnIn4[MX]", - "simpleIOGenericIO/GGIO1.SPCSO1[ST]", - "simpleIOGenericIO/GGIO1.SPCSO2[ST]", + "simpleIOGenericIO/GGIO1.SPCSO1.stVal[ST]", + "simpleIOGenericIO/GGIO1.SPCSO2.q[ST]", "simpleIOGenericIO/GGIO1.SPCSO3[ST]", "simpleIOGenericIO/GGIO1.SPCSO4[ST]" ], @@ -106,13 +106,13 @@ static string protocol_config_2 = QUOTE ({ { "rcb_ref" : "simpleIOGenericIO/LLN0.RP.EventsRCB01", "dataset_ref" : "simpleIOGenericIO/LLN0.Mags", - "trgops" : [ "data_changed", "quality_changed", "gi" ], + "trgops" : [ "dchg", "qchg", "gi" ], "gi" : true }, { "rcb_ref" : "simpleIOGenericIO/LLN0.RP.EventsIndexed01", "dataset_ref" : "simpleIOGenericIO/LLN0.Events2", - "trgops" : [ "data_changed", "quality_changed", "gi" ], + "trgops" : [ "dchg", "qchg", "gi" ], "gi" : true } ] @@ -120,6 +120,75 @@ static string protocol_config_2 = QUOTE ({ } }); +// PLUGIN DEFAULT PROTOCOL STACK CONF +static string protocol_config_3 = QUOTE ({ + "protocol_stack" : { + "name" : "iec61850client", + "version" : "0.0.1", + "transport_layer" : { + "ied_name" : "IED1", + "connections" : [ { "ip_addr" : "127.0.0.1", "port" : 10002 } ] + }, + "application_layer" : { + "polling_interval" : 0, + "datasets" : [ + { + "dataset_ref" : "simpleIOGenericIO/LLN0.Mags", + "entries" : [ + "simpleIOGenericIO/GGIO1.AnIn1[MX]", + "simpleIOGenericIO/GGIO1.AnIn2[MX]", + "simpleIOGenericIO/GGIO1.AnIn3[MX]", + "simpleIOGenericIO/GGIO1.AnIn4[MX]", + "simpleIOGenericIO/GGIO1.SPCSO1[ST]" + ], + "dynamic" : true + } + ], + "report_subscriptions" : [ + { + "rcb_ref" : "simpleIOGenericIO/LLN0.RP.EventsRCB01", + "dataset_ref" : "simpleIOGenericIO/LLN0.Mags", + "trgops" : [ "dchg", "qchg", "gi" ], + "gi" : false + } + ] + } + } +}); + +static string protocol_config_4 = QUOTE ({ + "protocol_stack" : { + "name" : "iec61850client", + "version" : "0.0.1", + "transport_layer" : { + "ied_name" : "IED1", + "connections" : [ { "ip_addr" : "127.0.0.1", "port" : 10002 } ], + "tls" : false + }, + "application_layer" : { + "polling_interval" : 10, + "datasets" : [ + { + "dataset_ref" : "simpleIOGenericIO/LLN0.Mags", + "entries" : [ + "simpleIOGenericIO/GGIO1.SPCSO1.stVal[ST]", + "simpleIOGenericIO/GGIO1.SPCSO1.q[ST]" + ], + "dynamic" : true + } + ], + "report_subscriptions" : [ + { + "rcb_ref" : "simpleIOGenericIO/LLN0.RP.EventsRCB01", + "dataset_ref" : "simpleIOGenericIO/LLN0.Mags", + "trgops" : [ "dchg", "qchg"], + "gi" : false + } + ] + } + } +}); + // PLUGIN DEFAULT EXCHANGED DATA CONF static string exchanged_data = QUOTE ({ @@ -201,6 +270,22 @@ static string exchanged_data = QUOTE ({ } }); +static string exchanged_data_2 = QUOTE ({ + "exchanged_data" : { + "datapoints" : [ + { + "pivot_id" : "TS1", + "label" : "TS1", + "protocols" : [ { + "name" : "iec61850", + "objref" : "simpleIOGenericIO/GGIO1.SPCSO1", + "cdc" : "SpcTyp" + } ] + } + ] + } +}); + // PLUGIN DEFAULT TLS CONF static string tls_config = QUOTE ({ "tls_conf" : { @@ -247,14 +332,6 @@ class ReportingTest : public testing::Test void TearDown () override { - iec61850->stop (); - delete iec61850; - - for (auto reading : storedReadings) - { - delete reading; - } - storedReadings.clear (); } static bool @@ -518,9 +595,18 @@ TEST_F (ReportingTest, ReportingWithStaticDataset) double expectedMagVal = 1.2; verifyDatapoint (mag, "f", &expectedMagVal); - IedServer_stop (server); - IedServer_destroy (server); - IedModel_destroy (model); + iec61850->stop (); + delete iec61850; + + for (auto reading : storedReadings) + { + delete reading; + } + storedReadings.clear (); + + IedServer_stop (server); + IedServer_destroy (server); + IedModel_destroy (model); } TEST_F (ReportingTest, ReportingWithDynamicDataset) @@ -596,6 +682,15 @@ TEST_F (ReportingTest, ReportingWithDynamicDataset) verifyDatapoint (qDp, "Validity", &expectedValidity); + iec61850->stop (); + delete iec61850; + + for (auto reading : storedReadings) + { + delete reading; + } + storedReadings.clear (); + IedServer_stop (server); IedServer_destroy (server); IedModel_destroy (model); @@ -652,8 +747,8 @@ TEST_F (ReportingTest, ReportingGI) ASSERT_FALSE (storedReadings.empty ()); ASSERT_EQ (storedReadings.size (), 12); Datapoint* commandResponse = storedReadings[0]->getReadingData ()[0]; - verifyDatapoint (commandResponse, "GTIC"); - Datapoint* gtim = getChild (*commandResponse, "GTIC"); + verifyDatapoint (commandResponse, "GTIS"); + Datapoint* gtim = getChild (*commandResponse, "GTIS"); verifyDatapoint (gtim, "SpcTyp"); Datapoint* SPC = getChild (*gtim, "SpcTyp"); @@ -661,6 +756,15 @@ TEST_F (ReportingTest, ReportingGI) int expectedStVal = false; verifyDatapoint (SPC, "stVal", &expectedStVal); + iec61850->stop (); + delete iec61850; + + for (auto reading : storedReadings) + { + delete reading; + } + storedReadings.clear (); + IedServer_stop (server); IedServer_destroy (server); IedModel_destroy (model); @@ -725,7 +829,7 @@ TEST_F (ReportingTest, ReportingSetpointCommand) delete params[0]; delete[] params; - timeout = std::chrono::seconds (3); + timeout = std::chrono::seconds (5); start = std::chrono::high_resolution_clock::now (); while (ingestCallbackCalled != 2) { @@ -743,8 +847,8 @@ TEST_F (ReportingTest, ReportingSetpointCommand) ASSERT_FALSE (storedReadings.empty ()); ASSERT_EQ (storedReadings.size (), 2); Datapoint* commandResponse = storedReadings[1]->getReadingData ()[0]; - verifyDatapoint (commandResponse, "GTIC"); - Datapoint* gtim = getChild (*commandResponse, "GTIC"); + verifyDatapoint (commandResponse, "GTIS"); + Datapoint* gtim = getChild (*commandResponse, "GTIS"); verifyDatapoint (gtim, "SpcTyp"); Datapoint* SPC = getChild (*gtim, "SpcTyp"); @@ -753,6 +857,16 @@ TEST_F (ReportingTest, ReportingSetpointCommand) verifyDatapoint (SPC, "stVal", &expectedStVal); delete pair; + + iec61850->stop (); + delete iec61850; + + for (auto reading : storedReadings) + { + delete reading; + } + storedReadings.clear (); + IedServer_stop (server); IedServer_destroy (server); IedModel_destroy (model); @@ -840,6 +954,15 @@ TEST_F (ReportingTest, ReportingUpdateQuality) verifyDatapoint (qDp, "Source"); verifyDatapoint (qDp, "operatorBlocked"); + iec61850->stop (); + delete iec61850; + + for (auto reading : storedReadings) + { + delete reading; + } + storedReadings.clear (); + IedServer_stop (server); IedServer_destroy (server); IedModel_destroy (model); @@ -967,7 +1090,308 @@ TEST_F (ReportingTest, ReportingChangeValueMultipleTimes) expectedMagVal = 1.5; verifyDatapoint (mag, "f", &expectedMagVal); + iec61850->stop (); + delete iec61850; + + for (auto reading : storedReadings) + { + delete reading; + } + storedReadings.clear (); + + IedServer_stop (server); + IedServer_destroy (server); + IedModel_destroy (model); +} + +TEST_F (ReportingTest, ReconfigureDynamicDataset) +{ + iec61850->setJsonConfig (protocol_config, exchanged_data, tls_config); + + IedModel* model = ConfigFileParser_createModelFromConfigFileEx ( + "../tests/data/simpleIO_direct_control.cfg"); + + IedServer server = IedServer_create (model); + + IedServer_start (server, 10002); + iec61850->start (); + + Thread_sleep (1000); + + auto start = std::chrono::high_resolution_clock::now (); + auto timeout = std::chrono::seconds (5); + while (!iec61850->m_client->m_active_connection + || !iec61850->m_client->m_active_connection->m_connection + || IedConnection_getState ( + iec61850->m_client->m_active_connection->m_connection) + != IED_STATE_CONNECTED) + { + auto now = std::chrono::high_resolution_clock::now (); + if (now - start > timeout) + { + IedServer_stop (server); + IedServer_destroy (server); + IedModel_destroy (model); + FAIL () << "Connection not established within timeout"; + } + Thread_sleep (10); + } + + Quality q = 0; + Quality_setValidity (&q, QUALITY_VALIDITY_INVALID); + + IedServer_updateQuality ( + server, + (DataAttribute*)IedModel_getModelNodeByObjectReference ( + model, "simpleIOGenericIO/GGIO1.AnIn1.q"), + q); + + timeout = std::chrono::seconds (3); + start = std::chrono::high_resolution_clock::now (); + while (ingestCallbackCalled != 1) + { + auto now = std::chrono::high_resolution_clock::now (); + if (now - start > timeout) + { + IedServer_stop (server); + IedServer_destroy (server); + IedModel_destroy (model); + FAIL () << "Callback not called within timeout"; + } + Thread_sleep (10); + } + + ASSERT_FALSE (storedReadings.empty ()); + ASSERT_EQ (storedReadings.size (), 1); + Datapoint* commandResponse = storedReadings[0]->getReadingData ()[0]; + verifyDatapoint (commandResponse, "GTIM"); + Datapoint* gtim = getChild (*commandResponse, "GTIM"); + + verifyDatapoint (gtim, "MvTyp"); + Datapoint* MV = getChild (*gtim, "MvTyp"); + + verifyDatapoint (MV, "q"); + Datapoint* qDp = getChild (*MV, "q"); + + std::string expectedValidity = "invalid"; + + verifyDatapoint (qDp, "Validity", &expectedValidity); + + iec61850->stop (); + + iec61850->setJsonConfig (protocol_config_3, exchanged_data, tls_config); + + iec61850->start(); + + Thread_sleep (1000); + + start = std::chrono::high_resolution_clock::now (); + timeout = std::chrono::seconds (5); + while (!iec61850->m_client->m_active_connection + || !iec61850->m_client->m_active_connection->m_connection + || IedConnection_getState ( + iec61850->m_client->m_active_connection->m_connection) + != IED_STATE_CONNECTED) + { + auto now = std::chrono::high_resolution_clock::now (); + if (now - start > timeout) + { + IedServer_stop (server); + IedServer_destroy (server); + IedModel_destroy (model); + FAIL () << "Connection not established within timeout"; + } + Thread_sleep (10); + } + + q = 0; + Quality_setValidity (&q, QUALITY_VALIDITY_INVALID); + + IedServer_updateQuality ( + server, + (DataAttribute*)IedModel_getModelNodeByObjectReference ( + model, "simpleIOGenericIO/GGIO1.SPCSO1.q"), + q); + + timeout = std::chrono::seconds (3); + start = std::chrono::high_resolution_clock::now (); + while (ingestCallbackCalled != 2) + { + auto now = std::chrono::high_resolution_clock::now (); + if (now - start > timeout) + { + IedServer_stop (server); + IedServer_destroy (server); + IedModel_destroy (model); + FAIL () << "Callback not called within timeout"; + } + Thread_sleep (10); + } + + ASSERT_EQ (storedReadings.size (), 2); + + iec61850->stop (); + + delete iec61850; + + for (auto reading : storedReadings) + { + delete reading; + } + storedReadings.clear (); + IedServer_stop (server); IedServer_destroy (server); IedModel_destroy (model); +} + +TEST_F (ReportingTest, ReportingIndividualAttributes) +{ + iec61850->setJsonConfig (protocol_config_4, exchanged_data_2, tls_config); + + IedModel* model = ConfigFileParser_createModelFromConfigFileEx ( + "../tests/data/simpleIO_direct_control.cfg"); + + IedServer server = IedServer_create (model); + + IedServer_start (server, 10002); + iec61850->start (); + + Thread_sleep (1000); + + auto start = std::chrono::high_resolution_clock::now (); + auto timeout = std::chrono::seconds (5); + while (!iec61850->m_client->m_active_connection + || !iec61850->m_client->m_active_connection->m_connection + || IedConnection_getState ( + iec61850->m_client->m_active_connection->m_connection) + != IED_STATE_CONNECTED) + { + auto now = std::chrono::high_resolution_clock::now (); + if (now - start > timeout) + { + IedServer_stop (server); + IedServer_destroy (server); + IedModel_destroy (model); + FAIL () << "Connection not established within timeout"; + } + Thread_sleep (10); + } + + Quality q = 0; + Quality_setValidity (&q, QUALITY_VALIDITY_INVALID); + + IedServer_updateQuality ( + server, + (DataAttribute*)IedModel_getModelNodeByObjectReference ( + model, "simpleIOGenericIO/GGIO1.SPCSO1.q"), + q); + + timeout = std::chrono::seconds (3); + start = std::chrono::high_resolution_clock::now (); + while (ingestCallbackCalled != 1) + { + auto now = std::chrono::high_resolution_clock::now (); + if (now - start > timeout) + { + IedServer_stop (server); + IedServer_destroy (server); + IedModel_destroy (model); + FAIL () << "Callback not called within timeout"; + } + Thread_sleep (10); + } + + ASSERT_FALSE (storedReadings.empty ()); + ASSERT_EQ (storedReadings.size (), 1); + Datapoint* commandResponse = storedReadings[0]->getReadingData ()[0]; + verifyDatapoint (commandResponse, "GTIS"); + Datapoint* gtic = getChild (*commandResponse, "GTIS"); + + verifyDatapoint (gtic, "SpcTyp"); + Datapoint* Spc = getChild (*gtic, "SpcTyp"); + + verifyDatapoint (Spc, "q"); + ASSERT_EQ(getChild(*Spc,"stVal"),nullptr); + + IedServer_updateBooleanAttributeValue ( + server, + (DataAttribute*)IedModel_getModelNodeByObjectReference ( + model, "simpleIOGenericIO/GGIO1.SPCSO1.stVal"), + true); + + timeout = std::chrono::seconds (3); + start = std::chrono::high_resolution_clock::now (); + while (ingestCallbackCalled != 2) + { + auto now = std::chrono::high_resolution_clock::now (); + if (now - start > timeout) + { + IedServer_stop (server); + IedServer_destroy (server); + IedModel_destroy (model); + FAIL () << "Callback not called within timeout"; + } + Thread_sleep (10); + } + + ASSERT_FALSE (storedReadings.empty ()); + ASSERT_EQ (storedReadings.size (), 2); + commandResponse = storedReadings[1]->getReadingData ()[0]; + verifyDatapoint (commandResponse, "GTIS"); + gtic = getChild (*commandResponse, "GTIS"); + + verifyDatapoint (gtic, "SpcTyp"); + Spc = getChild (*gtic, "SpcTyp"); + + int expectedIntVal = 1; + verifyDatapoint (Spc, "stVal",&expectedIntVal); + + q = 0; + Quality_setValidity (&q, QUALITY_VALIDITY_GOOD); + + IedServer_updateQuality ( + server, + (DataAttribute*)IedModel_getModelNodeByObjectReference ( + model, "simpleIOGenericIO/GGIO1.SPCSO1.q"), + q); + + timeout = std::chrono::seconds (3); + start = std::chrono::high_resolution_clock::now (); + while (ingestCallbackCalled != 3) + { + auto now = std::chrono::high_resolution_clock::now (); + if (now - start > timeout) + { + IedServer_stop (server); + IedServer_destroy (server); + IedModel_destroy (model); + FAIL () << "Callback not called within timeout"; + } + Thread_sleep (10); + } + + ASSERT_FALSE (storedReadings.empty ()); + ASSERT_EQ (storedReadings.size (), 3); + commandResponse = storedReadings[2]->getReadingData ()[0]; + verifyDatapoint (commandResponse, "GTIS"); + gtic = getChild (*commandResponse, "GTIS"); + + verifyDatapoint (gtic, "SpcTyp"); + Spc = getChild (*gtic, "SpcTyp"); + + verifyDatapoint (Spc, "stVal",&expectedIntVal); + + iec61850->stop (); + delete iec61850; + + for (auto reading : storedReadings) + { + delete reading; + } + storedReadings.clear (); + + IedServer_stop (server); + IedServer_destroy (server); + IedModel_destroy (model); } \ No newline at end of file