diff --git a/include/iec61850.hpp b/include/iec61850.hpp index 63d7896..ef41b2b 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, diff --git a/include/iec61850_client_config.hpp b/include/iec61850_client_config.hpp index ad89b57..71dbbc3 100644 --- a/include/iec61850_client_config.hpp +++ b/include/iec61850_client_config.hpp @@ -26,6 +26,7 @@ FRIEND_TEST (ReportingTest, ReportingUpdateQuality); \ FRIEND_TEST (ReportingTest, ReportingGI); \ FRIEND_TEST (ReportingTest, ReportingSetpointCommand); \ + FRIEND_TEST (ReportingTest, ReconfigureDynamicDataset); \ FRIEND_TEST (ReportingTest, ReportingChangeValueMultipleTimes); \ FRIEND_TEST (SpontDataTest, Polling); \ FRIEND_TEST (SpontDataTest, PollingAllCDC); \ 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..9a96f4c 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) @@ -233,7 +219,7 @@ const std::map rootToStrMap 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 +241,11 @@ IEC61850Client::stop () delete m_monitoringThread; m_monitoringThread = nullptr; } + + if(lastEntryId){ + MmsValue_delete(lastEntryId); + lastEntryId = nullptr; + } } int diff --git a/src/iec61850_client_config.cpp b/src/iec61850_client_config.cpp index d417135..cf7dc2f 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 }, @@ -585,59 +584,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 diff --git a/src/iec61850_client_connection.cpp b/src/iec61850_client_connection.cpp index 12b50ad..0711637 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,19 +360,39 @@ 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) { + if (isBuffered) + { + 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); parametersMask |= RCB_ELEMENT_RESV_TMS; - ClientReportControlBlock_setResvTms(rcb, 10); + 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 { + else + { + ClientReportControlBlock_setResv (rcb, true); parametersMask |= RCB_ELEMENT_RESV; - ClientReportControlBlock_setResv(rcb, true); } if (rs->trgops != -1) @@ -372,11 +410,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 ()) { @@ -388,6 +421,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; @@ -417,7 +456,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; } @@ -439,7 +478,8 @@ IEC61850ClientConnection::m_configRcb () continue; } - uint32_t parametersMask = configureRcb (rs, rcb); + uint32_t parametersMask + = configureRcb (rs, rcb, m_client->firstTimeConnect,m_client->lastEntryId); auto connDataSetPair = new std::pair ( @@ -452,8 +492,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); @@ -498,7 +539,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"); @@ -506,8 +548,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; @@ -579,6 +628,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) @@ -603,6 +720,7 @@ IEC61850ClientConnection::cleanUp () m_controlObjects.clear (); } + if (!m_connControlPairs.empty ()) { for (auto& cc : m_connControlPairs) @@ -1052,6 +1170,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..6150dcf 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 } ] @@ -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,42 @@ 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 + } + ] + } + } +}); + // PLUGIN DEFAULT EXCHANGED DATA CONF static string exchanged_data = QUOTE ({ @@ -247,14 +283,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 +546,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 +633,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); @@ -661,6 +707,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 +780,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) { @@ -753,6 +808,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 +905,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,6 +1041,156 @@ 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);