├── .deepsource.toml ├── README.md ├── SECURITY.md ├── dnp3generator ├── .gitignore ├── CfgJsonParser.cpp ├── CfgJsonParser.h ├── CidrCalculator.cpp ├── CidrCalculator.h ├── ConfigSpec.txt ├── DataPoint.cpp ├── DataPoint.h ├── LICENSE ├── Makefile ├── MappingOutstation.cpp ├── MappingOutstation.h ├── MappingSoeHandler.cpp ├── MappingSoeHandler.h ├── MasterStation.cpp ├── MasterStation.h ├── Node.cpp ├── Node.h ├── OutStation.cpp ├── OutStation.h ├── README ├── Station.cpp ├── Station.h ├── StringUtilities.cpp ├── StringUtilities.h ├── UtilityScripts │ ├── convertAllLineEndInDir.py │ ├── convertLineEnding.py │ ├── convertMappingToJson.py │ └── merge_relay_files.py ├── common.mk ├── debian │ ├── changelog │ ├── compat │ ├── control │ ├── copyright │ ├── dnp3generator.install │ ├── docs │ ├── init.d │ ├── postinst │ ├── rules │ └── source │ │ └── format ├── dnp3app.cpp ├── dnp3app.h ├── dummy │ ├── Makefile │ ├── dummy.c │ ├── dummy.h │ └── main.c ├── extras │ ├── ExampleConfigs │ │ ├── BufferChange │ │ │ ├── Config.json │ │ │ └── README │ │ ├── LuaConfigs │ │ │ ├── BasicSample1.lua │ │ │ ├── BasicSample2.lua │ │ │ ├── M1_Example_SendCommand.lua │ │ │ ├── M1_Example_Smart_Reading.lua │ │ │ ├── MS1_Multiplexer_Example_InjectValues.lua │ │ │ └── RTDS.lua │ │ ├── MappingOutstation │ │ │ ├── Mapping Outstation Config.json │ │ │ └── README │ │ ├── Master │ │ │ ├── MasterConfig.json │ │ │ └── README │ │ ├── MasterOutstation │ │ │ ├── Config.json │ │ │ └── README │ │ ├── Outstation │ │ │ ├── OutstationConfig.json │ │ │ └── README │ │ └── SimulatedPair │ │ │ ├── Config.json │ │ │ └── README │ └── dockerenv │ │ ├── dockerfile32 │ │ └── dockerfile64 └── tests │ ├── Makefile │ ├── MasterTest.cpp │ └── RTDSTest │ ├── Configitest.json │ ├── DNP3PointListTest.txt │ ├── RTDSTEST.bash │ ├── RTDSitest.lua │ ├── S1_Steady_State.csv │ ├── S2_Overcurrent_Delay_Fault4.csv │ ├── S3_Overcurrent_Instant_Fault6.csv │ ├── S4_Overvoltage_Tripping.csv │ ├── S5_Overvoltage_Warning.csv │ ├── S6_Undervoltage_Warning.csv │ └── S7_Undervoltage_Tripping.csv └── modbusgenerator ├── .gitignore ├── LICENSE ├── README.md ├── __init__.py ├── cfg_json_parser.py ├── cidr_calculator.py ├── config_spec.txt ├── configs-scripts ├── Config.json ├── S1.lua ├── S2.lua ├── S3.lua ├── master_vm.json └── outstation_vm.json ├── extras └── dockerfile ├── master_station.py ├── modbus_app.py ├── mymodbusclient.py ├── node.py ├── outstation.py ├── readwritelock.py ├── requirements.txt ├── test_parser.py └── threadsafe_datastore.py /.deepsource.toml: -------------------------------------------------------------------------------- 1 | version = 1 2 | 3 | [[analyzers]] 4 | name = "python" 5 | enabled = true 6 | runtime_version = "3.x.x" 7 | 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ics-trafficgen 2 | ICS protocol traffic generators 3 | 4 | Developed with various funding sources to aid in ICS research 5 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | | Version | Supported | 6 | | ------- | ------------------ | 7 | | all | :white_check_mark: | 8 | 9 | ## Reporting a Vulnerability 10 | 11 | If a vulnerability is discovered, please email iti-development@illinois.edu 12 | -------------------------------------------------------------------------------- /dnp3generator/.gitignore: -------------------------------------------------------------------------------- 1 | *.o 2 | objs/*.o 3 | dnp3Generator 4 | tests/dnp3GenTest 5 | .vscode/ 6 | /*.lua 7 | /*.json 8 | /*.gdb_history 9 | /*.swp 10 | 11 | -------------------------------------------------------------------------------- /dnp3generator/CfgJsonParser.cpp: -------------------------------------------------------------------------------- 1 | 2 | #include "CfgJsonParser.h" 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | #include 11 | 12 | #define EVENT_BUFFER_SIZE 100 13 | 14 | using boost::property_tree::ptree; 15 | using boost::optional; 16 | using boost::property_tree::read_json; 17 | using boost::property_tree::write_json; 18 | 19 | void CfgJsonParser::readAllDnp3Addresses(const ptree &propTree){ 20 | BOOST_FOREACH(const boost::property_tree::ptree::value_type &v, propTree.get_child("Nodes")) { 21 | boost::optional dnp3Addr = v.second.get_optional("DNP3 Address.Master"); 22 | if (dnp3Addr){ 23 | localDnp3Addrs.push_back(*dnp3Addr); 24 | } 25 | dnp3Addr = v.second.get_optional("DNP3 Address.Outstation"); 26 | if (dnp3Addr){ 27 | localDnp3Addrs.push_back(*dnp3Addr); 28 | } 29 | } 30 | } 31 | 32 | CfgJsonParser::CfgJsonParser(const std::string &configFile){ 33 | ptree propTree; 34 | read_json(configFile, propTree); 35 | int vnictrack = 0; 36 | boost::optional virtInterfaceProp = propTree.get_optional("Virtual Interface"); 37 | if (virtInterfaceProp){ 38 | this->virtualNetworkInterfaceCard = *virtInterfaceProp; 39 | } 40 | boost::optional cidrProp = propTree.get_optional("CIDR Notation"); 41 | if (cidrProp){ 42 | this->cidrGenerator = std::unique_ptr(new CidrCalculator(*cidrProp)); 43 | } 44 | this->readAllDnp3Addresses(propTree); 45 | BOOST_FOREACH(const ptree::value_type &nodeTree, propTree.get_child("Nodes")) { 46 | std::unique_ptr master(nullptr); 47 | std::unique_ptr outstation(nullptr); 48 | boost::optional masterName = nodeTree.second.get_optional("Name.Master"); 49 | boost::optional outStnName = nodeTree.second.get_optional("Name.Outstation"); 50 | if (masterName){ 51 | master = std::unique_ptr(new Node(*masterName, "Master")); 52 | } 53 | if (outStnName){ 54 | outstation = std::unique_ptr(new Node(*outStnName, "Outstation")); 55 | } 56 | readAssignIpAddresses(nodeTree, master.get(), outstation.get()); 57 | readAssignDNP3Addresses(nodeTree, master.get(), outstation.get()); 58 | readIpPorts(nodeTree, master.get(), outstation.get()); 59 | readUnsolicited(nodeTree, master.get(), outstation.get()); 60 | readSleepTimes(nodeTree, master.get(), outstation.get()); 61 | readLuaFileNames(nodeTree, master.get(), outstation.get()); 62 | readLuaFileKeySwitch(nodeTree, master.get(), outstation.get()); 63 | 64 | if (masterName) { 65 | readPollingSpecs(nodeTree, master.get()); 66 | readBoundOutstations(nodeTree, master.get()); 67 | //printf("######Master IP ADdr:%s, remote IP:%s\n", master->local_IPAddress.c_str(), master->remote_IPAddress.c_str()); 68 | nodes.push_back(std::move(master)); 69 | } 70 | 71 | if (outstation) { 72 | //printf("Reading in details of outstation %s\n", outstation->name.c_str()); 73 | readDataGenerationSpecs(nodeTree, outstation.get()); 74 | readMappedDataGenerationSpecs(nodeTree, outstation.get()); 75 | readEventBufferSpecs(nodeTree, outstation.get()); 76 | this->nodes.push_back(std::move(outstation)); 77 | } 78 | } 79 | } 80 | 81 | void CfgJsonParser::readBoundOutstations(const ptree::value_type & nodeTree, Node* const master){ 82 | optional outStnList = nodeTree.second.get_child_optional("Bound Outstations"); 83 | if (outStnList){ 84 | BOOST_FOREACH(const ptree::value_type& pointTree, nodeTree.second.get_child("Bound Outstations")) { 85 | master->boundOutstations.push_back(pointTree.second.get_value()); 86 | } 87 | } 88 | } 89 | 90 | 91 | void CfgJsonParser::readUnsolicited(const ptree::value_type & nodeTree, Node* const master, Node* const outstation){ 92 | boost::optional allowUnsolicited = nodeTree.second.get_optional("Allow Unsolicited"); 93 | if (allowUnsolicited && *allowUnsolicited==true) { 94 | if (master != nullptr) 95 | master->allowUnsolicited = *allowUnsolicited; 96 | 97 | if (outstation != nullptr) 98 | outstation->allowUnsolicited = *allowUnsolicited; 99 | } 100 | } 101 | 102 | void CfgJsonParser::readSleepTimes(const ptree::value_type & nodeTree, Node* const master, Node* const outstation){ 103 | boost::optional sleepMaster = nodeTree.second.get_optional("Sleep Duration.Master"); 104 | boost::optional sleepOutStn = nodeTree.second.get_optional("Sleep Duration.Outstation"); 105 | if (sleepMaster && master != nullptr) 106 | master->msToSleep = static_cast(*sleepMaster *1000*1000); 107 | 108 | if (sleepOutStn && outstation != nullptr) 109 | outstation->msToSleep = static_cast(*sleepOutStn *1000*1000); 110 | } 111 | 112 | void CfgJsonParser::readLuaFileNames(const ptree::value_type &nodeTree, Node* const master, Node* const outstation){ 113 | optional masterLua = nodeTree.second.get_child_optional("Lua File.Master"); 114 | optional outStnLuaList = nodeTree.second.get_child_optional("Lua File.Outstation"); 115 | if (masterLua && master != nullptr){ 116 | master->luaFileNames.clear(); 117 | BOOST_FOREACH(const ptree::value_type& luaTree, nodeTree.second.get_child("Lua File.Master")) { 118 | master->luaFileNames.push_back(luaTree.second.get_value()); 119 | } 120 | } 121 | if(outStnLuaList && outstation != nullptr){ 122 | outstation->luaFileNames.clear(); //clear out any defaults 123 | BOOST_FOREACH(const ptree::value_type& luaTree, nodeTree.second.get_child("Lua File.Outstation")) { 124 | outstation->luaFileNames.push_back(luaTree.second.get_value()); 125 | } 126 | } 127 | } 128 | 129 | void CfgJsonParser::readLuaFileKeySwitch(const ptree::value_type &nodeTree, Node* const master, Node* const outstation){ 130 | boost::optional masterLuaKey = nodeTree.second.get_optional("Lua Switch Trigger.Master"); 131 | boost::optional outStnLuaKey = nodeTree.second.get_optional("Lua Switch Trigger.Outstation"); 132 | if (masterLuaKey && master != nullptr){ 133 | master->luaKeySwitch = *masterLuaKey; 134 | } 135 | if(outStnLuaKey && outstation != nullptr){ 136 | outstation->luaKeySwitch = *outStnLuaKey; 137 | } 138 | } 139 | 140 | void CfgJsonParser::readAssignIpAddresses(const ptree::value_type &nodeTree, Node* const master, Node* const outstation){ 141 | //is Master IP Address specified in the json config file 142 | std::string outAddr; 143 | boost::optional ipAddr = nodeTree.second.get_optional("IP Address.Master"); 144 | 145 | if (master != nullptr) { 146 | if (ipAddr){ 147 | master->local_IPAddress = *ipAddr; 148 | } else { 149 | if (cidrGenerator){ 150 | allocateIpAddress(master, cidrGenerator->GetNextIpAddress()); //if not in config, assign one. 151 | } else{ 152 | std::cout<<"Did not find CIDR notation in the config file to allocate IP address\n"; 153 | } 154 | } 155 | } 156 | 157 | //Outstation IP Address assigned in the json config file 158 | ipAddr = nodeTree.second.get_optional("IP Address.Outstation"); 159 | if (ipAddr) { 160 | if (master != nullptr) 161 | master->remote_IPAddress = *ipAddr; 162 | if (outstation != nullptr) 163 | outstation->local_IPAddress = *ipAddr; 164 | } else { 165 | if (cidrGenerator && outstation != nullptr) { 166 | allocateIpAddress(outstation, cidrGenerator->GetNextIpAddress()); //if not in config assign one. 167 | } else { 168 | std::cout<<"Did not find CIDR notation in the config file to allocate IP address\n"; 169 | } 170 | } 171 | 172 | if (master != nullptr && outstation != nullptr) 173 | master->remote_IPAddress = outstation->local_IPAddress; //master needs to know outstation IP address that it is connected to. 174 | } 175 | 176 | void CfgJsonParser::readIpPorts(const ptree::value_type & nodeTree, Node* const master, Node* const outstation) { 177 | 178 | boost::optional ipPort = nodeTree.second.get_optional("IP Port.Master"); 179 | if (ipPort && master != nullptr){ 180 | master->port = *ipPort; 181 | } 182 | ipPort = nodeTree.second.get_optional("IP Port.Outstation"); 183 | if (ipPort && outstation != nullptr) { 184 | outstation->port = *ipPort; 185 | } 186 | } 187 | 188 | void CfgJsonParser::allocateIpAddress(Node* const node, const std::string& ipAddress) { 189 | static int vnictrack = 0; 190 | node->local_IPAddress = ipAddress; 191 | node->vnic = virtualNetworkInterfaceCard + ":" + std::to_string(vnictrack); 192 | ++vnictrack; 193 | node->Allocate(); 194 | printf("Node allocated at IP ADDress:%s, vnic:%s\n", node->local_IPAddress.c_str(), node->vnic.c_str()); 195 | } 196 | 197 | void CfgJsonParser::readDataGenerationSpecs(const ptree::value_type & nodeTree, Node* const station) { 198 | optional dataTree = nodeTree.second.get_child_optional( "Data" ); 199 | auto pushPtToStn = [&](DataPoint &dpt){ 200 | if(station->dbSize[dpt.pointType] < dpt.index+1){ 201 | station->dbSize[dpt.pointType] = dpt.index+1; 202 | } 203 | station->dataPoints.push_back(dpt); 204 | }; 205 | if (dataTree){ 206 | BOOST_FOREACH(const ptree::value_type &pointTree, nodeTree.second.get_child("Data")) { 207 | DataPoint dp = DataPoint(); 208 | readDataPoint(pointTree, dp); 209 | boost::optional index = pointTree.second.get_optional("Index"); 210 | boost::optional indexList = pointTree.second.get_optional("Index List"); 211 | if (index){ 212 | dp.index = *index; 213 | pushPtToStn(dp); 214 | } 215 | if (indexList){ 216 | std::string indexes = *indexList; 217 | std::regex re("([0-9]+)-*([0-9]+)*"); 218 | for(std::sregex_iterator reg_i = std::sregex_iterator(indexes.begin(), indexes.end(), re); reg_i != std::sregex_iterator(); ++reg_i) 219 | { 220 | std::smatch mtch = *reg_i; 221 | int startIndex = std::stoi(mtch[1]); 222 | int endIndex = startIndex; 223 | try{ //mtch.size() is not useful to see if we got second match? 224 | endIndex = std::stoi(mtch[2]); 225 | } catch(...){} 226 | 227 | for(int i = startIndex; i <= endIndex; i++){ 228 | DataPoint dpI = DataPoint(dp); 229 | dpI.index = i; 230 | pushPtToStn(dpI); 231 | } 232 | } 233 | } 234 | } 235 | for(auto const& pval: station->dbSize){ 236 | if(pval.second > 0){ 237 | station->evtBufferSize[pval.first.c_str()] = EVENT_BUFFER_SIZE; 238 | } 239 | } 240 | } 241 | } 242 | 243 | void CfgJsonParser::readDataPoint(const ptree::value_type &pointTree, DataPoint &dp){ 244 | dp.pointType = pointTree.second.get("Type"); 245 | dp.eventClass = pointTree.second.get("Event Class"); 246 | dp.sVariation = pointTree.second.get("sVariation"); 247 | dp.eVariation = pointTree.second.get("eVariation"); 248 | boost::optional deadband = pointTree.second.get_optional("Deadband"); 249 | if(deadband){ 250 | dp.deadband = *deadband; 251 | } 252 | } 253 | 254 | void CfgJsonParser::readEventBufferSpecs(const ptree::value_type &nodeTree, Node* const station) 255 | { 256 | optional bufferTree = nodeTree.second.get_child_optional( "Event Data" ); 257 | if (bufferTree){ 258 | BOOST_FOREACH(const ptree::value_type& pTree, nodeTree.second.get_child("Event Data")) { 259 | station->evtBufferSize[pTree.second.get("Type")] = pTree.second.get("Size"); 260 | } 261 | } 262 | 263 | } 264 | 265 | void CfgJsonParser::readMappedDataGenerationSpecs(const ptree::value_type &nodeTree, Node* const station){ 266 | optional sourceTree = nodeTree.second.get_child_optional("Data Sources"); 267 | if (sourceTree){ 268 | BOOST_FOREACH(const ptree::value_type &sTree, nodeTree.second.get_child("Data Sources")) { 269 | auto source = sTree.second.get("Source"); 270 | optional dataTree = sTree.second.get_child_optional("Mapped Data"); 271 | if(dataTree){ 272 | BOOST_FOREACH(const ptree::value_type &pointTree, sTree.second.get_child("Mapped Data")) { 273 | MappedDataPoint mdp = MappedDataPoint(); 274 | DataPoint dp = DataPoint(); 275 | readDataPoint(pointTree, dp); 276 | dp.index = pointTree.second.get("Index"); 277 | if(station->dbSize[dp.pointType] < dp.index+1){ 278 | station->dbSize[dp.pointType] = dp.index+1; 279 | } 280 | station->dataPoints.push_back(dp); 281 | 282 | mdp.input_index = pointTree.second.get("InputIndex"); 283 | mdp.index = dp.index; 284 | mdp.pointType = dp.pointType; 285 | station->dataSources[source].push_back(mdp); 286 | } 287 | for(auto const& pval: station->dbSize){ 288 | if(pval.second > 0){ 289 | station->evtBufferSize[pval.first.c_str()] = EVENT_BUFFER_SIZE; 290 | } 291 | } 292 | } 293 | } 294 | } 295 | } 296 | 297 | 298 | void CfgJsonParser::readPollingSpecs (const ptree::value_type &nodeTree, Node* const station){ 299 | optional pollTree = nodeTree.second.get_child_optional( "Poll Interval" ); 300 | if (pollTree){ 301 | BOOST_FOREACH(const ptree::value_type &pTree, nodeTree.second.get_child("Poll Interval")) { 302 | PollPoint ppt; 303 | ppt.eventClass = pTree.second.get("Event Class"); 304 | ppt.frequency = pTree.second.get("Frequency"); 305 | station->pollPoints.push_back(ppt); 306 | } 307 | } 308 | } 309 | 310 | 311 | void CfgJsonParser::readAssignDNP3Addresses(const ptree::value_type & nodeTree, Node* const master, Node* const outstation) { 312 | 313 | boost::optional masterCfgDnp3Addr = nodeTree.second.get_optional("DNP3 Address.Master"); 314 | boost::optional outCfgDnp3Addr = nodeTree.second.get_optional("DNP3 Address.Outstation"); 315 | 316 | int mAddr; 317 | if (masterCfgDnp3Addr){ 318 | mAddr = *masterCfgDnp3Addr; 319 | } else { 320 | mAddr = getNextDNP3Address(); 321 | } 322 | 323 | int oAddr; 324 | if (outCfgDnp3Addr){ 325 | oAddr = *outCfgDnp3Addr; 326 | } else { 327 | oAddr = getNextDNP3Address(); 328 | } 329 | 330 | if (master != nullptr) { 331 | master->localDNP3Addr = mAddr; 332 | master->remoteDNP3Addr = oAddr; 333 | } 334 | 335 | if (outstation != nullptr) { 336 | outstation->localDNP3Addr = oAddr; 337 | outstation->remoteDNP3Addr = mAddr; 338 | } 339 | } 340 | 341 | int CfgJsonParser::getNextDNP3Address(){ 342 | int allocatableDnp3Addr; 343 | if (localDnp3Addrs.empty()) { 344 | allocatableDnp3Addr = 90; 345 | } else { 346 | std::vector::iterator largestAllocatedDnp3Addr = std::max_element(std::begin(localDnp3Addrs), std::end(localDnp3Addrs)); 347 | allocatableDnp3Addr = *largestAllocatedDnp3Addr + 1; 348 | } 349 | localDnp3Addrs.push_back(allocatableDnp3Addr); 350 | return allocatableDnp3Addr; 351 | } 352 | 353 | std::vector>& CfgJsonParser::GetConfiguredNodes() { 354 | return this->nodes; 355 | } 356 | -------------------------------------------------------------------------------- /dnp3generator/CfgJsonParser.h: -------------------------------------------------------------------------------- 1 | #ifndef ITI_CFGJSONPARSER_H 2 | #define ITI_CFGJSONPARSER_H 3 | 4 | #include "Node.h" 5 | #include "CidrCalculator.h" 6 | 7 | #include 8 | #include 9 | #include 10 | 11 | using boost::property_tree::ptree; 12 | 13 | class CfgJsonParser 14 | { 15 | public: 16 | CfgJsonParser(std::string const& filename); 17 | std::vector>& GetConfiguredNodes(); 18 | 19 | private: 20 | std::vector> nodes; //create a vector to keep track of Node objects 21 | 22 | std::string virtualNetworkInterfaceCard; 23 | std::unique_ptr cidrGenerator; 24 | std::vector localDnp3Addrs; 25 | void readDataGenerationSpecs(const ptree::value_type &, Node* const); 26 | void readEventBufferSpecs(const ptree::value_type &, Node* const); 27 | 28 | void readMappedDataGenerationSpecs(const ptree::value_type &, Node* const); 29 | void readDataPoint(const ptree::value_type &, DataPoint &); 30 | 31 | void readPollingSpecs (const ptree::value_type &, Node* const); 32 | void readBoundOutstations (const ptree::value_type &, Node* const); 33 | 34 | void readAllDnp3Addresses(const ptree &propTree); 35 | void readAssignDNP3Addresses(const ptree::value_type &, Node* const, Node* const); 36 | int getNextDNP3Address(); 37 | 38 | void readUnsolicited (const ptree::value_type &, Node* const, Node* const); 39 | void readSleepTimes (const ptree::value_type &, Node* const, Node* const); 40 | void readLuaFileNames (const ptree::value_type &, Node* const, Node* const); 41 | void readLuaFileKeySwitch (const ptree::value_type &, Node* const, Node* const); 42 | 43 | void readIpPorts (const ptree::value_type &, Node* const, Node* const); 44 | void readAssignIpAddresses (const ptree::value_type &, Node* const, Node* const); 45 | void allocateIpAddress(Node* const, const std::string&); 46 | }; 47 | 48 | #endif 49 | -------------------------------------------------------------------------------- /dnp3generator/CidrCalculator.cpp: -------------------------------------------------------------------------------- 1 | 2 | #include "CidrCalculator.h" 3 | #include "StringUtilities.h" 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | CidrCalculator::CidrCalculator(const std::string& cidr) : cidrNotation(cidr), netMask{0,0,0,0}, networkAddr{0,0,0,0} { 11 | std::list ipAddrs; 12 | std::vector parts = split(this->cidrNotation, '/'); 13 | if (parts.size() != 2){ 14 | //throw std::runtime_error("Network address not in CIDR notation"); 15 | } 16 | std::string addrStr = parts.front(); 17 | int cidrI; 18 | std::stringstream(parts.back()) >> cidrI; 19 | 20 | std::vector addr; 21 | for (std::string value: split(addrStr, '.')){ 22 | int dec; 23 | std::stringstream(value) >> dec; 24 | addr.push_back(dec); 25 | } 26 | /* 27 | printf("addr: "); 28 | for(int i = 0; i<4; i++){ 29 | printf("%d.", addr[i]); 30 | } 31 | printf("\n"); 32 | printf("mask: "); 33 | for(int i = 0; i<4; i++){ 34 | printf("%d.", this->netMask[i]); 35 | } 36 | printf("\n"); 37 | */ 38 | std::vector cidrRange(cidrI); 39 | std::iota(cidrRange.begin(), cidrRange.end(), 0); 40 | for (int i: cidrRange){ 41 | this->netMask[i/8] = this->netMask[i/8] + (1 <<(7-i%8)); 42 | } 43 | /* 44 | printf("mask: "); 45 | for(int i = 0; i<4; i++){ 46 | printf("%d.", this->netMask[i]); 47 | } 48 | printf("\n"); 49 | */ 50 | for (int i=0; i<4; i++){ 51 | this->networkAddr[i] = addr[i] & this->netMask[i]; 52 | } 53 | 54 | /* 55 | printf("netaddr: "); 56 | for(int i = 0; i<4; i++){ 57 | printf("%d.", this->networkAddr[i]); 58 | } 59 | printf("\n"); 60 | */ 61 | }; 62 | 63 | std::string CidrCalculator::GetNextIpAddress(){ 64 | //if(this->networkAddr[0] == 0 || this->networkAddr[0] == 255) 65 | //throw std::runtime_error("Check network address") 66 | //return "Check network address"; 67 | 68 | if (this->networkAddr[3] < 254){ 69 | this->networkAddr[3] += 1; 70 | } else { 71 | this->networkAddr[3] = 1; 72 | if (this->networkAddr[2] < 254){ 73 | this->networkAddr[2] += 1; 74 | } else { 75 | this->networkAddr[2] = 0; 76 | if (this->networkAddr[1] < 254){ 77 | this->networkAddr[1] += 1; 78 | } else { 79 | this->networkAddr[1] = 0; 80 | if (this->networkAddr[0] < 254){ 81 | this->networkAddr[0] += 1; 82 | } else { 83 | //throw std::runtime_error("Check network address"); 84 | } 85 | } 86 | } 87 | } 88 | std::stringstream ipAddress; 89 | ipAddress << this->networkAddr[0] << "." << this->networkAddr[1] << "." << this->networkAddr[2] << "." << this->networkAddr[3]; 90 | return ipAddress.str(); 91 | } 92 | -------------------------------------------------------------------------------- /dnp3generator/CidrCalculator.h: -------------------------------------------------------------------------------- 1 | #ifndef ITI_CIDRCALCULATOR_H 2 | #define ITI_CIDRCALCULATOR_H 3 | 4 | #include 5 | 6 | class CidrCalculator{ 7 | public: 8 | CidrCalculator(const std::string& cidr); 9 | std::string GetNextIpAddress(); 10 | 11 | private: 12 | std::string cidrNotation; 13 | int netMask[4]; 14 | int networkAddr[4]; 15 | }; 16 | 17 | #endif 18 | -------------------------------------------------------------------------------- /dnp3generator/ConfigSpec.txt: -------------------------------------------------------------------------------- 1 | 2 | The Config file is in json format. The default config file is Config.json. You can run the application with your json file using the -c flag. 3 | The top level keys in the JSON file are 4 | 5 | "Virtual Interface" 6 | "CIDR Notation" 7 | "Nodes" 8 | 9 | The "Virtual Interface" and "CIDR Notation" keys are useful for development, testing and running a simulated DNP3 flow on a single VM. 10 | 11 | Nodes is a list of nodes (master outstation pair). The top level keys of a node are 12 | "Name" 13 | ""IP Address" 14 | "DNP3 Address" 15 | "IP Port" 16 | "Allow Unsolicited" 17 | "Data" 18 | "Poll Interval" 19 | 20 | For the key "Name" if you want to create a simulated Master station, add the value as shown below 21 | "Name":{"Master":"MasterStationName"} This is used for a simulated Master that is going to talk to a real outstation device. 22 | If you want to simulate an Outstation, use the key value 23 | "Name":{"Outstation":"SimulatedOutstationName"} 24 | If you want to simulate both a Master and an outstation, Use 25 | "Name":{"Master":"MasterName", "Outstation":"OutstationName"} 26 | In this case you can use the top level keys "Virtual Interface", and "CIDR Notation" to allow the software to pick the 27 | virtual network interface card and IP addresses automatically. 28 | 29 | Use "IP Address" as shown below to specify the IP addresses of the master and the outstation. 30 | If this is for a simulated Master talking to a simulated/real outstation, both keys are necessary. 31 | You can use just the Outstation key if you are simulating an outstation. 32 | "IP Address": {"Master":"192.168.3.1", "Outstation":"192.168.8.2"} 33 | 34 | The "DNP3 Address" is used to specify the DNP3 address as set on the physical device as 35 | "DNP3 Address": {"Master":5, "Outstation":3} 36 | If you are simulating both master and outstation(in a VM), you can partially specify or skip specifying DNP3 addresses altogether, 37 | the software will allocate unique addresses and connect them. 38 | Otherwise both DNP3 addresses should be specified to ensure proper connection. 39 | 40 | The "IP Port" defaults to port 20000 for the master and outstation, use the format as shown below if you want to change one or both ports 41 | "IP Port":{"Master":20001, "Outstation":19204} 42 | 43 | The "Allow Unsolicited" sets the Allow Unsolicited flag on the station being configured. A value of true means the outstation will send data 44 | to the master as it becomes available, instead of holding on to the data until the master polls for it. 45 | 46 | The "Data" key is useful when you are creating a simulated Outstation. It is used to specify the type and size of the data being generated. 47 | "Data": 48 | [ 49 | {"Type":"Analog Input", "Event Class":2, "sVariation":5, "eVariation":1, "Index":3} 50 | ] 51 | The currently allowed types are "Analog Input", "Binary Input", and "Counter". 52 | Additional types can be added as needed. The "Event Class", "sVariation", "eVariation" are DNP3 specific, look up DNP3 documentation. 53 | The "Index" specifies the index of the point created. The data is updated through a Lua script(Lua version 5.2). 54 | - The program calls the Lua script for an outstation once every millisecond. 55 | - The Lua script MUST have the same name as the outstation. i.e. it must be of the form OutstationName.lua 56 | - The lua function MUST be named generate_data. 57 | - The data returned by the script MUST be a nested table with the table named "data", the inner tables are named "Analog Input", "Binary Input", "Counter". 58 | The script can return an empty table (no values are updated), or just one or more of the inner tables. However all values must be returned. i.e. Any mismatch 59 | between the "Index" specified in the config file and the number of values returned by the script may lead to undefined results.The "data" table may also contain 60 | a variable named "Timestamp". This can be used to return a delay time. This is currently not used. 61 | 62 | The "Poll Interval" key specifies the rate at which the Master will poll the outstation. 63 | "Poll Interval": 64 | [ 65 | {"Event Class":2, "Frequency":15} 66 | ] 67 | Here we are setting up a poll of all Class 2 events at a frequency of 15 seconds. An event class of "0123" will start a Integrity poll at the frequency specified. 68 | -------------------------------------------------------------------------------- /dnp3generator/DataPoint.cpp: -------------------------------------------------------------------------------- 1 | #include "DataPoint.h" 2 | 3 | DataPoint::DataPoint():sVariation(1), eVariation(1), eventClass(2), index(0), pointType(""), deadband(0){} 4 | -------------------------------------------------------------------------------- /dnp3generator/DataPoint.h: -------------------------------------------------------------------------------- 1 | #ifndef ITI_DATAPOINT_H 2 | #define ITI_DATAPOINT_H 3 | 4 | #include 5 | 6 | struct DataPoint 7 | { 8 | int index; 9 | int sVariation; 10 | int eVariation; 11 | int eventClass; 12 | std::string pointType; 13 | float deadband; 14 | 15 | DataPoint(); 16 | }; 17 | 18 | struct MappedDataPoint 19 | { 20 | int index; 21 | int input_index; 22 | std::string pointType; 23 | }; 24 | 25 | #endif 26 | -------------------------------------------------------------------------------- /dnp3generator/LICENSE: -------------------------------------------------------------------------------- 1 | Automatak Code 2 | Licensed under the terms of the Apache 2.0 License. 3 | 4 | Copyright (c) 2010, 2011 Green Energy Corp 5 | Copyright (c) 2013 - 2015 Automatak LLC 6 | Copyright (c) 2010 - 2015 various contributors 7 | 8 | Modifications 9 | University of Illinois/NCSA Open Source License 10 | Copyright (c) 2015-2017 Information Trust Institute 11 | All rights reserved. 12 | 13 | Developed by: 14 | 15 | Information Trust Institute 16 | University of Illinois Urbana-Champaign 17 | http://www.iti.illinois.edu 18 | 19 | Permission is hereby granted, free of charge, to any person obtaining a copy of 20 | this software and associated documentation files (the "Software"), to deal with 21 | the Software without restriction, including without limitation the rights to 22 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 23 | of the Software, and to permit persons to whom the Software is furnished to do 24 | so, subject to the following conditions: 25 | 26 | Redistributions of source code must retain the above copyright notice, this list 27 | of conditions and the following disclaimers. Redistributions in binary form must 28 | reproduce the above copyright notice, this list of conditions and the following 29 | disclaimers in the documentation and/or other materials provided with the 30 | distribution. 31 | 32 | Neither the names of Information Trust Institute, University of Illinois, nor 33 | the names of its contributors may be used to endorse or promote products derived 34 | from this Software without specific prior written permission. 35 | 36 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 37 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 38 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE CONTRIBUTORS 39 | OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 40 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 41 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS WITH THE SOFTWARE. 42 | -------------------------------------------------------------------------------- /dnp3generator/Makefile: -------------------------------------------------------------------------------- 1 | TARGET = dnp3Generator 2 | OBJDIR := objs 3 | OBJS = $(addprefix $(OBJDIR)/,dnp3app.o $(CODE_OBJS)) 4 | 5 | include common.mk 6 | 7 | #Build for OrionLX 8 | orion: $(OBJS) 9 | $(CC) $(orionargs) $(CFLAGS) -o $(TARGET)-orion $(OBJS) $(LIBS32) 10 | 11 | #Build for i386 12 | test32: $(OBJS) 13 | $(CXX) $(CXXFLAGS) -o $(TARGET)-test32 $(OBJS) $(LIBS32) 14 | 15 | #Build for ARM-RPI 16 | rpiarm: $(OBJS) 17 | $(CC) $(CFLAGS) -o $(TARGET) $(OBJS) $(ARMLIBS) 18 | 19 | $(OBJS): $(OBJDIR)/%.o: %.cpp %.h 20 | $(CXX) $(CXXFLAGS) -c $< -o $@ 21 | 22 | #$(filter-out %OutStation.o,$(OBJS)): $(OBJDIR)/%.o: %.cpp %.h 23 | # $(CXX) $(CXXFLAGS) $(DEBUG) -c $< -o $@ 24 | -------------------------------------------------------------------------------- /dnp3generator/MappingOutstation.h: -------------------------------------------------------------------------------- 1 | #ifndef ITI_MAPPINGOUTSTATION_H 2 | #define ITI_MAPPINGOUTSTATION_H 3 | 4 | #include "OutStation.h" 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | #include 11 | 12 | struct dnp3Point { 13 | double value; 14 | std::time_t timestamp; 15 | }; 16 | 17 | class MappingOutstation : public OutStation 18 | { 19 | public: 20 | MappingOutstation(std::unique_ptr, std::atomic&); 21 | void initialize(); 22 | 23 | void run() override; 24 | 25 | std::shared_ptr outstationInstance; //TODO Can be localized later 26 | std::mutex mutex; 27 | std::map> analogValues; //these are input arrays 28 | std::map> binaryValues; 29 | std::map> counterValues; 30 | std::map> analogFlag; 31 | std::map> binaryFlag; 32 | std::map> counterFlag; 33 | 34 | std::vector analogOutValues; 35 | std::vector binaryOutValues; 36 | std::vector counterOutValues; 37 | 38 | private: 39 | void LuaInvokeAnalogScript(); 40 | void LuaInvokeBinaryScript(); 41 | void LuaInvokeCounterScript(); 42 | 43 | void ConfigureInputMultiplexingArrays(); 44 | void passThruDataChanges(); 45 | void injectDataChanges(); 46 | }; 47 | #endif 48 | -------------------------------------------------------------------------------- /dnp3generator/MappingSoeHandler.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "MappingSoeHandler.h" 4 | 5 | namespace 6 | { 7 | template 8 | inline std::string ValueToString(const T& meas) 9 | { 10 | std::ostringstream oss; 11 | oss << meas.value; 12 | return oss.str(); 13 | } 14 | 15 | inline std::string ValueToString(const opendnp3::DoubleBitBinary& meas) 16 | { 17 | return opendnp3::DoubleBitToString(meas.value); 18 | } 19 | 20 | template 21 | void Print(const opendnp3::HeaderInfo& info, const T& value, uint16_t index) 22 | { 23 | std::cout << "[" << index << "] : " << 24 | ValueToString(value) << " : " << 25 | static_cast(value.flags.value) << " : " << 26 | value.time << std::endl; 27 | } 28 | 29 | template 30 | void PrintAll(const opendnp3::HeaderInfo& info, const opendnp3::ICollection>& values) 31 | { 32 | auto print = [&](const opendnp3::Indexed& pair) 33 | { 34 | Print(info, pair.value, pair.index); 35 | }; 36 | values.ForeachItem(print); 37 | } 38 | 39 | template 40 | void PrintBinary(const opendnp3::HeaderInfo& info, const opendnp3::ICollection>& values); 41 | 42 | template 43 | void PrintCounter(const opendnp3::HeaderInfo& info, const opendnp3::ICollection>& values); 44 | 45 | template 46 | void PrintB(const opendnp3::HeaderInfo& info, const T& value, uint16_t index, asiodnp3::UpdateBuilder *); 47 | 48 | template 49 | void PrintC(const opendnp3::HeaderInfo& info, const T& value, uint16_t index, asiodnp3::UpdateBuilder *); 50 | 51 | std::string GetTimeString(opendnp3::TimestampMode tsmode) 52 | { 53 | switch (tsmode) 54 | { 55 | case(opendnp3::TimestampMode::SYNCHRONIZED) : 56 | return "synchronized"; 57 | 58 | case(opendnp3::TimestampMode::UNSYNCHRONIZED) : 59 | return "unsynchronized"; 60 | 61 | default: 62 | return "no timestamp"; 63 | } 64 | 65 | return ""; 66 | } 67 | 68 | } // end of anonymous namespace 69 | 70 | /************************************** Binary callback ********************************************************/ 71 | void MappingSoeHandler::Process(const opendnp3::HeaderInfo& info, const opendnp3::ICollection>& values) 72 | { 73 | for(std::vector>::iterator oIter = DestinationList.begin(); oIter != DestinationList.end(); ++oIter){ 74 | auto dest = *oIter; 75 | auto updateBinaryArray = [&](const opendnp3::Indexed& pair) 76 | { 77 | /* 78 | std::cout << "Binary Indexes gotten from Outstation are--"<<"[Index:" << pair.index << "], Value: " << 79 | ValueToString(pair.value) << ", Flag value: " << 80 | static_cast(pair.value.flags.value) << ", Timestamp: " << 81 | pair.value.time << std::endl; 82 | */ 83 | if(dest->binaryValues.count(SrcName) > 0 && dest->binaryValues[SrcName].size() > pair.index){ 84 | dnp3Point d_pt = dest->binaryValues[SrcName].at(pair.index); 85 | if (d_pt.value == pair.value.value && pair.value.time == 0){ 86 | return; 87 | } 88 | else { 89 | d_pt.value = pair.value.value; 90 | d_pt.timestamp = pair.value.time; 91 | dest->binaryValues[SrcName].at(pair.index) = d_pt; 92 | } 93 | } 94 | }; 95 | 96 | std::lock_guard lock(dest->mutex); 97 | values.ForeachItem(updateBinaryArray); 98 | dest->binaryFlag[SrcName]=true; 99 | } 100 | } 101 | 102 | /************************************** Analog callback ********************************************************/ 103 | void MappingSoeHandler::Process(const opendnp3::HeaderInfo& info, const opendnp3::ICollection>& values) 104 | { 105 | for(auto dest : DestinationList) { 106 | 107 | auto updateAnalogArray = [&](const opendnp3::Indexed& pair) 108 | { 109 | /* 110 | std::cout <<"Analog inputs Callback are--"<< "[" << pair.index << "] : " << 111 | ValueToString(pair.value) << " : " << 112 | static_cast(pair.value.flags.value) << " : " << 113 | pair.value.time << std::endl; 114 | */ 115 | if(dest->analogValues.count(SrcName) > 0 && dest->analogValues[SrcName].size() > pair.index){ 116 | dnp3Point d_pt = dest->analogValues[SrcName].at(pair.index); 117 | if (d_pt.value == pair.value.value && pair.value.time == 0){ 118 | return; 119 | } 120 | else { 121 | d_pt.value = pair.value.value; 122 | d_pt.timestamp = pair.value.time; 123 | dest->analogValues[SrcName].at(pair.index) = d_pt; 124 | } 125 | } 126 | }; 127 | 128 | std::lock_guard lock(dest->mutex); 129 | values.ForeachItem(updateAnalogArray); 130 | dest->analogFlag[SrcName]=true; 131 | } 132 | } 133 | 134 | /************************************** Counter callback ********************************************************/ 135 | void MappingSoeHandler::Process(const opendnp3::HeaderInfo& info, const opendnp3::ICollection>& values) 136 | { 137 | for(auto dest : DestinationList) { 138 | 139 | auto updateCounterArray = [&](const opendnp3::Indexed& pair) 140 | { 141 | /* 142 | std::cout << "Counter gotten from Outstation are--"<<"[" << index << "] : " << 143 | ValueToString(value) << " : " << 144 | static_cast(value.flags.value) << " : " << 145 | value.time << std::endl; 146 | */ 147 | if(dest->counterValues.count(SrcName) > 0 && dest->counterValues[SrcName].size() > pair.index) { 148 | dnp3Point d_pt = dest->counterValues[SrcName].at(pair.index); 149 | if (d_pt.value == pair.value.value && pair.value.time == 0) { 150 | return; 151 | } 152 | else { 153 | d_pt.value = pair.value.value; 154 | d_pt.timestamp = pair.value.time; 155 | dest->counterValues[SrcName].at(pair.index) = d_pt; 156 | } 157 | } 158 | }; 159 | 160 | std::lock_guard lock(dest->mutex); 161 | values.ForeachItem(updateCounterArray); 162 | dest->counterFlag[SrcName]=true; 163 | } 164 | } 165 | 166 | /************************************** Other types callback default copied from PrintingSOEHandler ********************************************************/ 167 | void MappingSoeHandler::Process(const opendnp3::HeaderInfo& info, const opendnp3::ICollection>& values) 168 | { 169 | return PrintAll(info, values); 170 | } 171 | 172 | void MappingSoeHandler::Process(const opendnp3::HeaderInfo& info, const opendnp3::ICollection>& values) 173 | { 174 | return PrintAll(info, values); 175 | } 176 | 177 | void MappingSoeHandler::Process(const opendnp3::HeaderInfo& info, const opendnp3::ICollection>& values) 178 | { 179 | return PrintAll(info, values); 180 | } 181 | 182 | void MappingSoeHandler::Process(const opendnp3::HeaderInfo& info, const opendnp3::ICollection>& values) 183 | { 184 | return PrintAll(info, values); 185 | } 186 | 187 | void MappingSoeHandler::Process(const opendnp3::HeaderInfo& info, const opendnp3::ICollection>& values) 188 | { 189 | auto print = [](const opendnp3::Indexed& pair) 190 | { 191 | std::cout << "OctetString " << " [" << pair.index << "] : Size : " << pair.value.ToRSlice().Size() << std::endl; 192 | }; 193 | 194 | values.ForeachItem(print); 195 | } 196 | 197 | void MappingSoeHandler::Process(const opendnp3::HeaderInfo& info, const opendnp3::ICollection>& values) 198 | { 199 | auto print = [](const opendnp3::Indexed& pair) 200 | { 201 | std::cout << "TimeAndInterval: " << 202 | "[" << pair.index << "] : " << 203 | pair.value.time << " : " << 204 | pair.value.interval << " : " << 205 | IntervalUnitsToString(pair.value.GetUnitsEnum()) << std::endl; 206 | }; 207 | 208 | values.ForeachItem(print); 209 | } 210 | 211 | void MappingSoeHandler::Process(const opendnp3::HeaderInfo& info, const opendnp3::ICollection>& values) 212 | { 213 | auto print = [](const opendnp3::Indexed& pair) 214 | { 215 | std::cout << "BinaryCommandEvent: " << 216 | "[" << pair.index << "] : " << 217 | pair.value.time << " : " << 218 | pair.value.value << " : " << 219 | CommandStatusToString(pair.value.status) << std::endl; 220 | }; 221 | 222 | values.ForeachItem(print); 223 | } 224 | 225 | void MappingSoeHandler::Process(const opendnp3::HeaderInfo& info, const opendnp3::ICollection>& values) 226 | { 227 | auto print = [](const opendnp3::Indexed& pair) 228 | { 229 | std::cout << "AnalogCommandEvent: " << 230 | "[" << pair.index << "] : " << 231 | pair.value.time << " : " << 232 | pair.value.value << " : " << 233 | CommandStatusToString(pair.value.status) << std::endl; 234 | }; 235 | 236 | values.ForeachItem(print); 237 | } 238 | 239 | void MappingSoeHandler::Process(const opendnp3::HeaderInfo& info, const opendnp3::ICollection>& values) 240 | { 241 | auto print = [](const opendnp3::Indexed& pair) 242 | { 243 | std::cout << "SecurityStat: " << 244 | "[" << pair.index << "] : " << 245 | pair.value.time << " : " << 246 | pair.value.value.count << " : " << 247 | static_cast(pair.value.quality) << " : " << 248 | pair.value.value.assocId << std::endl; 249 | }; 250 | 251 | values.ForeachItem(print); 252 | } 253 | 254 | void MappingSoeHandler::Process(const opendnp3::HeaderInfo& info, const opendnp3::ICollection& values) 255 | { 256 | auto print = [](const opendnp3::DNPTime & value) 257 | { 258 | std::cout << "DNPTime: " << value.value << std::endl; 259 | }; 260 | 261 | values.ForeachItem(print); 262 | } 263 | -------------------------------------------------------------------------------- /dnp3generator/MappingSoeHandler.h: -------------------------------------------------------------------------------- 1 | #ifndef ITI_MappingSoeHandler_H 2 | #define ITI_MappingSoeHandler_H 3 | 4 | #include "MappingOutstation.h" 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | 17 | /** 18 | * MappingSoeHandler singleton that prints to the console. 19 | */ 20 | 21 | class MappingSoeHandler final : public opendnp3::ISOEHandler 22 | { 23 | 24 | public: 25 | 26 | MappingSoeHandler() 27 | {} 28 | std::string SrcName; 29 | std::vector> DestinationList; 30 | 31 | virtual void Process(const opendnp3::HeaderInfo& info, const opendnp3::ICollection>& values) override; 32 | virtual void Process(const opendnp3::HeaderInfo& info, const opendnp3::ICollection>& values) override; 33 | virtual void Process(const opendnp3::HeaderInfo& info, const opendnp3::ICollection>& values) override; 34 | virtual void Process(const opendnp3::HeaderInfo& info, const opendnp3::ICollection>& values) override; 35 | virtual void Process(const opendnp3::HeaderInfo& info, const opendnp3::ICollection>& values) override; 36 | virtual void Process(const opendnp3::HeaderInfo& info, const opendnp3::ICollection>& values) override; 37 | virtual void Process(const opendnp3::HeaderInfo& info, const opendnp3::ICollection>& values) override; 38 | virtual void Process(const opendnp3::HeaderInfo& info, const opendnp3::ICollection>& values) override; 39 | virtual void Process(const opendnp3::HeaderInfo& info, const opendnp3::ICollection>& values) override; 40 | virtual void Process(const opendnp3::HeaderInfo& info, const opendnp3::ICollection>& values) override; 41 | virtual void Process(const opendnp3::HeaderInfo& info, const opendnp3::ICollection>& values) override; 42 | virtual void Process(const opendnp3::HeaderInfo& info, const opendnp3::ICollection>& values) override; 43 | virtual void Process(const opendnp3::HeaderInfo& info, const opendnp3::ICollection& values) override; 44 | 45 | protected: 46 | 47 | void Start() final {} 48 | void End() final {} 49 | }; 50 | 51 | #endif 52 | -------------------------------------------------------------------------------- /dnp3generator/MasterStation.cpp: -------------------------------------------------------------------------------- 1 | 2 | #include "MappingSoeHandler.h" 3 | #include "MasterStation.h" 4 | #include "dnp3app.h" 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | 20 | extern ThreadSafeUserInput luaSwitchObj; 21 | 22 | MasterStation::MasterStation(std::unique_ptr node, std::atomic& quitFlag) 23 | : Station(std::move(node), quitFlag) { 24 | } 25 | 26 | void MasterStation::initialize() { 27 | customDataCallback = nullptr; 28 | if(!node->boundOutstations.empty()){ 29 | std::shared_ptr ss= std::make_shared(); 30 | ss->SrcName = node->name; 31 | customDataCallback = ss; 32 | } 33 | } 34 | 35 | void MasterStation::run() 36 | { 37 | // Specify what log levels to use. NORMAL is warning and above 38 | // You can add all the comms logging by uncommenting below 39 | const uint32_t FILTERS = opendnp3::levels::NORMAL | opendnp3::levels::ALL_APP_COMMS; 40 | 41 | // Connect via a TCPClient socket to a outstation 42 | auto pChannel = manager->AddTCPClient((node->name + " tcpclient").c_str(), FILTERS, asiopal::ChannelRetry::Default(), node->remote_IPAddress, 43 | node->local_IPAddress, node->port, asiodnp3::PrintingChannelListener::Create()); 44 | // The master config object for a master. The default are 45 | // useable, but understanding the options are important. 46 | asiodnp3::MasterStackConfig stackConfig; 47 | 48 | // you can override application layer settings for the master here 49 | // in this example, we've change the application layer timeout to 2 seconds 50 | stackConfig.master.responseTimeout = openpal::TimeDuration::Seconds(2); 51 | stackConfig.master.disableUnsolOnStartup = !node->allowUnsolicited; 52 | 53 | 54 | // You can override the default link layer settings here 55 | // in this example we've changed the default link layer addressing 56 | stackConfig.link.LocalAddr = node->localDNP3Addr; 57 | stackConfig.link.RemoteAddr = node->remoteDNP3Addr; 58 | 59 | // Create a new master on a previously declared port, with a 60 | // name, log level, command acceptor, and config info. This 61 | // returns a thread-safe interface used for sending commands. 62 | std::shared_ptr dataCallback(nullptr); 63 | if(customDataCallback) 64 | dataCallback = customDataCallback; 65 | else 66 | dataCallback = asiodnp3::PrintingSOEHandler::Create(); 67 | auto master = pChannel->AddMaster( 68 | node->name.c_str(), // id for logging 69 | dataCallback, // callback for data processing 70 | asiodnp3::DefaultMasterApplication::Create(), // master application instance 71 | stackConfig // stack configuration 72 | ); 73 | 74 | // std::vector::iterator pp; 75 | for(auto& pp : node->pollPoints) { 76 | if(pp.eventClass == "0123"){ 77 | auto pscan = master->AddClassScan(opendnp3::ClassField::AllClasses(), openpal::TimeDuration::Seconds(pp.frequency)); 78 | } 79 | else if(pointClassVarMap.count(pp.eventClass) == 1){ 80 | auto pscan = master->AddClassScan(opendnp3::ClassField(pointClassVarMap[pp.eventClass]), openpal::TimeDuration::Seconds(pp.frequency)); 81 | } 82 | } 83 | 84 | // Enable the master. This will start communications. 85 | master->Enable(); 86 | if (!node->allowUnsolicited){ 87 | master->PerformFunction("disable unsol", opendnp3::FunctionCode::DISABLE_UNSOLICITED, 88 | { opendnp3::Header::AllObjects(60, 2), opendnp3::Header::AllObjects(60, 3), opendnp3::Header::AllObjects(60, 4) } 89 | ); 90 | } 91 | 92 | /* initialize Lua */ 93 | L = luaL_newstate(); 94 | /* Load Lua base libraries */ 95 | luaL_openlibs(L); 96 | /* load the script */ 97 | bool lua_script_exists = node->luaFileNames.size()>0? FileExists(node->luaFileNames[0].c_str()) : false; 98 | if (lua_script_exists) 99 | luaL_dofile(L, node->luaFileNames[0].c_str()); 100 | while(!this->quitFlag) 101 | { 102 | /* call the lua getdata function */ 103 | if (lua_script_exists){ 104 | while (luaSwitchObj.readCount() > localLuaFlag){ 105 | localLuaFlag+=1; 106 | if (!node->luaKeySwitch.empty() && luaSwitchObj.readInputStr(localLuaFlag) == node->luaKeySwitch){ 107 | auto luaFileToLoad = GetNextLuaFile(); 108 | printf("Master:%s -- Changing lua file to %s\n", node->name.c_str(), luaFileToLoad.c_str()); 109 | luaL_dofile(L, luaFileToLoad.c_str()); 110 | } 111 | } 112 | LuaInvokeScript(master); 113 | } 114 | usleep(node->msToSleep); //sleep for 1 second 115 | } 116 | /* cleanup Lua */ 117 | lua_close(L); 118 | } 119 | 120 | void MasterStation::UpdateDestinations(){ 121 | if (customDataCallback == nullptr) 122 | return; 123 | customDataCallback->DestinationList = destStations; 124 | } 125 | 126 | void MasterStation::LuaInvokeScript(std::shared_ptr master) 127 | { 128 | lua_getglobal(L, "operate_outstation"); 129 | 130 | /* call the lua function generate_data, with 0 parameters, return 1 result (a table) */ 131 | lua_call(L, 0, 1); 132 | 133 | //check if we got any tables back 134 | lua_getglobal(L, "data"); 135 | lua_pushnil(L); 136 | if (lua_next(L, -2) == 0){ 137 | return; 138 | } 139 | 140 | std::list cmdList; 141 | 142 | //empty stack 143 | lua_getglobal(L, "data"); 144 | //-1=>table 145 | lua_pushnil(L); //first key 146 | //-1 => nil, -2 =>table 147 | while(lua_next(L, -2)){ //lua_next pops top of stack(key), and pushes key-value pair at key 148 | //-1 => value, -2 => key, -3 => table 149 | lua_pushvalue(L, -2); 150 | // -1=> key, -2 =>value, -3=>key, -4=>table 151 | //now to unravel the inner table 152 | if(lua_istable(L, -2)){ 153 | lua_pushvalue(L, -2); 154 | //-1 =>inner table, -2 =>key, -3=>value(innertable), -4=>key, -5=>table 155 | lua_pushnil(L); 156 | //-1=>nil, -2=>inner table, -3=>key, -4=>value(innertable), -5=>key, -6 =>table 157 | cmdStruct *cmd = new cmdStruct(); 158 | while(lua_next(L,-2)){ 159 | //-1 =>value -2 =>key, -3=>inner table, -4 =>key, -5 =>value(innertable), -6=>key, -7=>table 160 | lua_pushvalue(L, -2); 161 | //-1=>key, -2=>value, -3=>key, -4 =>innertable, -5=>key, -6=>value(innertable), -7=>key, -8=>table 162 | int i = lua_tointeger(L, -1) -1; //lua indexes start at 1 163 | switch(i){ 164 | case 0: 165 | cmd->fType = lua_tostring(L, -2); 166 | break; 167 | case 1: 168 | cmd->fName = lua_tostring(L, -2); 169 | break; 170 | case 2: 171 | cmd->index = lua_tonumber(L, -2); 172 | break; 173 | case 3: 174 | cmd->value = lua_tonumber(L, -2); 175 | } 176 | lua_pop(L,2); 177 | //-1=>key, -2=>innertable, -3=>key, -4=>value(innertable), -5=>key, -6=>table 178 | } 179 | cmdList.push_back(cmd); 180 | //lua_next pops one from stack at the end 181 | //-1=>innertable, -2=>key, -3=>value(innertable), -4=>key, -5=>table 182 | lua_pop(L,1); 183 | //-1=>key, -2=>value(innertable), -3=>key, -4=>table 184 | } 185 | // pop value + copy of key, leaving original key 186 | lua_pop(L, 2); 187 | //-1=>key, -2 =>table 188 | } 189 | //-1=>table 190 | lua_pop(L,1); 191 | //stack empty again 192 | this->FireOffMasterCommand(master, cmdList); 193 | } 194 | 195 | void MasterStation::FireOffMasterCommand(std::shared_ptr master, std::list cmdList){ 196 | auto callback = [](const opendnp3::ICommandTaskResult& result) -> void 197 | { 198 | std::cout << "Summary: " << opendnp3::TaskCompletionToString(result.summary) << std::endl; 199 | auto print = [](const opendnp3::CommandPointResult& res) 200 | { 201 | std::cout 202 | << "Header: " << res.headerIndex 203 | << " Index: " << res.index 204 | << " State: " << opendnp3::CommandPointStateToString(res.state) 205 | << " Status: " << opendnp3::CommandStatusToString(res.status); 206 | }; 207 | result.ForeachItem(print); 208 | }; 209 | 210 | opendnp3::CommandSet sboCommands; 211 | opendnp3::CommandSet doCommands; 212 | opendnp3::CommandSet *cmdSet; 213 | bool sboFuncCodes = false; 214 | bool doFuncCodes = false; 215 | for (auto& c : cmdList) { 216 | 217 | std::string funcName = c->fName; 218 | std::string funcType = c->fType; 219 | int index = c->index; 220 | double value = c->value; 221 | 222 | if(funcType == "SBO"){ 223 | sboFuncCodes = true; 224 | cmdSet = &sboCommands; 225 | } 226 | else if (funcType == "DO"){ 227 | doFuncCodes = true; 228 | cmdSet = &doCommands; 229 | } 230 | else if(funcType == "Scan"){//scans cannot be accumulated, have to be fired off as they are parsed. 231 | std::regex re("Group([0-9]+)Var([0-9])"); 232 | std::smatch re_match; 233 | printf("IN SCAN, funcName:%s, funcType:%s!!!!!!!!!!!!!!!!!!!!Matched regex:%d\n", funcName.c_str(), funcType.c_str(), std::regex_match(funcName, re)); 234 | if(std::regex_match(funcName, re_match, re) && re_match.size()==3){ 235 | int groupId = std::stoi(re_match[1].str()); 236 | int varId = std::stoi(re_match[2].str()); 237 | printf("Smart Reading %s, for groupID:%d, var:%d, index %d, to index %d\n", funcType.c_str(), groupId, varId, index, (int)std::round(value)); 238 | master->ScanRange(opendnp3::GroupVariationID(groupId, varId), index, (int)std::round(value)); 239 | } 240 | continue; 241 | } 242 | else{ //we don't recognize what the user is asking us to do 243 | printf("funcType:%s not recognized. Contact ITI for further information.\n", funcType); 244 | continue; 245 | } 246 | printf("funcName %s, func type:%s, index %d, value %f\n", funcName.c_str(), funcType.c_str(), index, value); 247 | if(funcName == "CROB"){ //CROB 248 | auto& header = cmdSet->StartHeader(); 249 | if(value == 1){ 250 | header.Add(opendnp3::ControlRelayOutputBlock(opendnp3::ControlCode::PULSE_ON), index); 251 | } 252 | else if (value == 2){ 253 | header.Add(opendnp3::ControlRelayOutputBlock(opendnp3::ControlCode::PULSE_OFF), index); 254 | } 255 | else if(value == 3){ 256 | header.Add(opendnp3::ControlRelayOutputBlock(opendnp3::ControlCode::LATCH_ON), index); 257 | } 258 | else if (value == 4){ 259 | header.Add(opendnp3::ControlRelayOutputBlock(opendnp3::ControlCode::LATCH_OFF), index); 260 | } 261 | } 262 | else if (funcName == "AnalogOutputInt16"){ //AnalogOutputInt16 263 | auto& header = cmdSet->StartHeader(); 264 | header.Add(opendnp3::AnalogOutputInt16(value), index); 265 | } 266 | else if (funcName == "AnalogOutputInt32"){ //AnalogOutputInt32 267 | auto& header = cmdSet->StartHeader(); 268 | header.Add(opendnp3::AnalogOutputInt32(value), index); 269 | } 270 | else if (funcName == "AnalogOutputFloat32"){ //SBO AnalogOutputFloat32 271 | auto& header = cmdSet->StartHeader(); 272 | header.Add(opendnp3::AnalogOutputFloat32(value), index); 273 | } 274 | else if (funcName == "AnalogOutputDouble64"){ //SBO AnalogOutputDouble64 275 | auto& header = cmdSet->StartHeader(); 276 | header.Add(opendnp3::AnalogOutputDouble64(value), index); 277 | } 278 | } 279 | if (sboFuncCodes){ 280 | master->SelectAndOperate(std::move(sboCommands), callback); 281 | } 282 | 283 | if(doFuncCodes){ 284 | master->DirectOperate(std::move(doCommands), callback); 285 | } 286 | 287 | } 288 | -------------------------------------------------------------------------------- /dnp3generator/MasterStation.h: -------------------------------------------------------------------------------- 1 | #ifndef ITI_MASTER_H 2 | #define ITI_MASTER_H 3 | 4 | #include "Station.h" 5 | #include "MappingOutstation.h" 6 | #include "Node.h" 7 | #include "MappingSoeHandler.h" 8 | 9 | #include 10 | #include 11 | 12 | struct cmdStruct{ 13 | std::string fName; 14 | std::string fType; 15 | int index; 16 | double value; 17 | }; 18 | 19 | class MasterStation : public Station 20 | { 21 | public: 22 | MasterStation(std::unique_ptr, std::atomic&); 23 | 24 | void run() override; 25 | void UpdateDestinations(); 26 | void initialize(); 27 | 28 | std::vector> destStations; 29 | 30 | private: 31 | void LuaInvokeScript(std::shared_ptr); 32 | void FireOffMasterCommand(std::shared_ptr, std::list); 33 | std::shared_ptr customDataCallback; 34 | }; 35 | #endif 36 | -------------------------------------------------------------------------------- /dnp3generator/Node.cpp: -------------------------------------------------------------------------------- 1 | #include "Node.h" 2 | #include 3 | 4 | void Node::Allocate() 5 | { 6 | std::string subcmd="ifconfig "+ this->vnic+ " " + this->local_IPAddress; 7 | std::cout << subcmd << "##########\n"; 8 | int shell = system(subcmd.c_str()); 9 | printf("SHELL RESPONSE:%d\n", shell); 10 | } 11 | 12 | // new way of allocating network interface alias 13 | void Node::Allocate(const std::string& nic) 14 | { 15 | //subcmd="ifconfig "+ this->vnic+ " " + this->local_IPAddress; 16 | std::string subcmd = "ip addr add " + this->local_IPAddress + " dev " + nic + " " + " label " + this->vnic; 17 | // std::cout << subcmd << "##########\n"; 18 | int shell = system(subcmd.c_str()); 19 | // printf("SHELL RESPONSE:%d\n", shell); 20 | } 21 | 22 | Node::Node() : port(20000), allowUnsolicited(false) 23 | { 24 | this->dbSize["Binary Input"] = 0; 25 | this->dbSize["Double Binary Input"] = 0; 26 | this->dbSize["Analog Input"] = 0; 27 | this->dbSize["Counter"] = 0; 28 | this->dbSize["Frozen Counter"] = 0; 29 | this->dbSize["Binary Output"] = 0; 30 | this->dbSize["Analog Output"] = 0; 31 | this->dbSize["Time Interval"] = 0; 32 | 33 | this->evtBufferSize["Binary Input"] = 0; 34 | this->evtBufferSize["Double Binary Input"] = 0; 35 | this->evtBufferSize["Analog Input"] = 0; 36 | this->evtBufferSize["Counter"] = 0; 37 | this->evtBufferSize["Frozen Counter"] = 0; 38 | this->evtBufferSize["Binary Output"] = 0; 39 | this->evtBufferSize["Analog Output"] = 0; 40 | } 41 | Node::Node(std::string name, std::string role): Node() 42 | { 43 | this->name = name; 44 | this->luaFileNames.push_back(name + ".lua"); //default lua file name is its name.lua. May be overridden subsequently in the config file 45 | this->role = role; 46 | msToSleep = role=="Master"? 1000*1000 : 1000 ; //default sleep-Master 1 sec, outstation 1 ms 47 | } 48 | 49 | Node::~Node() { 50 | if (vnic == "") 51 | return; 52 | 53 | std::string subcmd="ifconfig "+ this->vnic+ " down" ; 54 | std::cout << "*****************NODE " << this->name<< " IS KILLED, IP address:"<local_IPAddress<<", vnic going down:"<vnic< 7 | #include 8 | #include 9 | #include 10 | 11 | struct PollPoint { 12 | int frequency; 13 | std::string eventClass; 14 | }; 15 | 16 | class Node 17 | { 18 | public: 19 | Node(); 20 | Node(std::string name, std::string role); 21 | ~Node(); 22 | 23 | void Allocate(); 24 | void Allocate(const std::string& nic); 25 | 26 | std::string name; 27 | std::string role; //master or outstation 28 | int msToSleep; 29 | std::vector luaFileNames; 30 | std::string luaKeySwitch; 31 | 32 | int port; //generally 20000 33 | std::string vnic; //nic location if we need to create ip address in CIDR range 34 | std::string local_IPAddress; //ipv4 address 35 | std::string remote_IPAddress; 36 | 37 | uint16_t remoteDNP3Addr; 38 | uint16_t localDNP3Addr; 39 | 40 | std::vector dataPoints; 41 | std::map> dataSources; 42 | std::map dbSize; 43 | std::map evtBufferSize; 44 | 45 | bool allowUnsolicited; 46 | std::vector pollPoints; 47 | std::vector boundOutstations; 48 | 49 | }; 50 | 51 | #endif 52 | -------------------------------------------------------------------------------- /dnp3generator/OutStation.cpp: -------------------------------------------------------------------------------- 1 | 2 | #include "Node.h" 3 | #include "OutStation.h" 4 | #include "dnp3app.h" 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | #include 21 | #include 22 | #include 23 | #include 24 | 25 | extern ThreadSafeUserInput luaSwitchObj; 26 | void OutStation::LuaInvokeScript(std::shared_ptr outstation) 27 | { 28 | //stack empty 29 | lua_getglobal(L, "generate_data"); 30 | /* call the lua function generate_data, with 0 parameters, return 1 result (a table) */ 31 | lua_call(L, 0, 1); 32 | //-1 =>return from fn call 33 | //check if we got any tables back 34 | lua_getglobal(L, "data"); 35 | //-1 =>"data", -2=> return from calling lua function 36 | lua_pushnil(L); 37 | //-1 =>nil, -2=>"data", -3 =>fn call return val 38 | if (lua_next(L, -2) == 0){ 39 | lua_pop(L, lua_gettop(L)); //clean up stack 40 | return; 41 | } 42 | //-1=>value, -2=>key, -3 => "data", -4 =>return val from fn_Call 43 | lua_pop(L, 2); 44 | //-1=>"data", -2=>ret value 45 | 46 | asiodnp3::UpdateBuilder builder; 47 | /* 48 | //get the timestamp from data 49 | lua_pushstring(L, "Timestamp"); 50 | //-1 => "Timestamp", -2 =>data, -3=>ret_value 51 | lua_gettable(L, -2); 52 | //-1=>data["Timestamp"], -2=>"Timestamp", -3=>data, -4=>ret value 53 | float timestamp = lua_tonumber(L, -1); 54 | printf("***********Got time stamp %f\n", timestamp); 55 | lua_pop(L, 2); 56 | //-1=>data, -2=>ret value 57 | */ 58 | auto duration = std::chrono::system_clock::now().time_since_epoch(); 59 | auto timestamp = std::chrono::duration_cast(duration).count();//OpenDNP3 expects in ms 60 | //Get Analog Input table 61 | lua_pushstring(L, "Analog Input"); 62 | //-1=> "Analog Input", -2=>"data", -3 =>ret value 63 | lua_gettable(L, -2); //gets table at -2, with key at top of stack, so gets data["Analaog Input"] 64 | //-1=>data["AnalogInput"], -2=>"data", -3=>ret val 65 | if(lua_istable(L, -1) == 1){ 66 | //printf("In Analog input table"); 67 | lua_pushnil(L); 68 | //-1=>nil, -2=>data["Analog Input"], -3=>"data", -4=>ret val 69 | while (lua_next(L, -2) != 0) { 70 | //-1=>value, -2=>key, -3=>data["Analog Input"], -4=>"data", -5=>ret val 71 | /* uses 'key' (at index -2) and 'value' (at index -1) */ 72 | int index = lua_tointeger(L, -2)-1;//lua indexes start at 1 73 | double dvalue = lua_tonumber(L, -1); 74 | //printf("Got Analog Input Table %d - %f\n", index, dvalue); 75 | //if db is not set up for this data type, break 76 | if (dpTypeIndexMap.count("Analog Input") == 0){ 77 | lua_pop(L, 2); //if we are going to break out of while lua_next loop, restore lua stack to where it should be 78 | break; 79 | } 80 | else if (dpTypeIndexMap["Analog Input"].count(index) == 0){ //if index is not one we want to look at, continue 81 | //printf("Discarding analog value for point index:%d\n", index); 82 | lua_pop(L, 1); //if we are going to jump to lua_next loop, restore lua stack to where it should be 83 | continue; 84 | } 85 | builder.Update(opendnp3::Analog(dvalue, 86 | static_cast(opendnp3::AnalogQuality::ONLINE), 87 | opendnp3::DNPTime(timestamp)), index); 88 | 89 | /* removes 'value'; keeps 'key' for next iteration */ 90 | lua_pop(L, 1); 91 | //-1=>key, -2=>data["Analaog Input"], -3=>"data", -4=>ret val 92 | } 93 | } 94 | //-1=>data["AnalogInput"], -2=>"data", -3=>ret val 95 | lua_pop(L, 1); 96 | //-1=>"data", -2=>ret val 97 | 98 | //REady to get "Counter" table 99 | lua_pushstring(L, "Counter"); 100 | //-1=>"Counter", -2=>"data", -3=>ret val 101 | lua_gettable(L, -2); 102 | if(lua_istable(L, -1) == 1){ 103 | lua_pushnil(L); 104 | //printf("***********Retrieving counter tabel\n"); 105 | while (lua_next(L, -2) != 0) { 106 | // uses 'key' (at index -2) and 'value' (at index -1) 107 | int index = lua_tointeger(L, -2)-1; //lua indexes start at 1 108 | uint32_t cvalue = lua_tonumber(L, -1); 109 | if (dpTypeIndexMap.count("Counter") == 0){ 110 | lua_pop(L, 2); 111 | break; 112 | } 113 | else if (dpTypeIndexMap["Counter"].count(index) == 0){ 114 | lua_pop(L, 1); //if we are going to jump to lua_next loop, restore lua stack to where it should be 115 | continue; 116 | } 117 | //printf("Counter %d - %d\n", index, cvalue); 118 | builder.Update(opendnp3::Counter(cvalue, 119 | static_cast(opendnp3::CounterQuality::ONLINE), 120 | opendnp3::DNPTime(timestamp)), index); 121 | 122 | // removes 'value'; keeps 'key' for next iteration 123 | lua_pop(L, 1); 124 | //-1=>key, -2=>data["Counter"], -3=>"data", -4=>ret val 125 | } 126 | } 127 | //-1=>data["Counter"], -2=>"data", -3=>ret val 128 | lua_pop(L, 1); 129 | //-1=>"data", -2=>ret val 130 | 131 | //Ready for Binary Input table 132 | lua_pushstring(L, "Binary Input"); 133 | lua_gettable(L, -2); 134 | if(lua_istable(L, -1) == 1){ 135 | lua_pushnil(L); 136 | //printf("***********Retrieveing Binary Inputs table\n"); 137 | while (lua_next(L, -2) != 0) { 138 | // uses 'key' (at index -2) and 'value' (at index -1) 139 | int index = lua_tointeger(L, -2)-1; //lua indexes start at 1 140 | bool bvalue = lua_tointeger(L, -1); 141 | //printf("Binary Input %d - %d\n", index, bvalue); 142 | if (dpTypeIndexMap.count("Binary Input") == 0){ 143 | lua_pop(L, 2); 144 | break; 145 | } 146 | else if (dpTypeIndexMap["Binary Input"].count(index) == 0){ 147 | //printf("Discarding binary value for point index:%d\n", index); 148 | lua_pop(L, 1); //if we are going to jump to lua_next loop, restore lua stack to where it should be 149 | continue; 150 | } 151 | builder.Update(opendnp3::Binary(bvalue, 152 | static_cast(opendnp3::BinaryQuality::ONLINE), 153 | opendnp3::DNPTime(timestamp)), index); 154 | 155 | // removes 'value'; keeps 'key' for next iteration 156 | lua_pop(L, 1); 157 | } 158 | } 159 | //pop whatever is left on stack we are getting out of here. Empty the stack. 160 | lua_pop(L, lua_gettop(L)); 161 | //stack empty 162 | outstation->Apply(builder.Build()); 163 | } 164 | 165 | OutStation::OutStation(std::unique_ptr node, std::atomic& quitFlag) 166 | : Station(std::move(node), quitFlag) 167 | { 168 | } 169 | 170 | void OutStation::run() 171 | { 172 | const uint32_t FILTERS = opendnp3::levels::NORMAL | opendnp3::levels::ALL_COMMS ; 173 | 174 | // Create a TCP server (listener) 175 | auto channel = manager->AddTCPServer((node->name + " server").c_str(), 176 | FILTERS, asiopal::ChannelRetry::Default(), 177 | node->local_IPAddress, node->port, 178 | asiodnp3::PrintingChannelListener::Create()); 179 | 180 | // The main object for a outstation. The defaults are useable, 181 | // but understanding the options are important. 182 | asiodnp3::OutstationStackConfig stackConfig(opendnp3::DatabaseSizes( 183 | node->dbSize["Binary Input"], 184 | node->dbSize["Double Binary Input"], 185 | node->dbSize["Analog Input"], 186 | node->dbSize["Counter"], 187 | node->dbSize["Frozen Counter"], 188 | node->dbSize["Binary Output"], 189 | node->dbSize["Analog Output"], 190 | node->dbSize["Time Interval"] 191 | )); 192 | // Specify the size of the event buffers. Max size is currently 250 events 193 | stackConfig.outstation.eventBufferConfig = opendnp3::EventBufferConfig( 194 | node->evtBufferSize["Binary Input"], 195 | node->evtBufferSize["Double Binary Input"], 196 | node->evtBufferSize["Analog Input"], 197 | node->evtBufferSize["Counter"], 198 | node->evtBufferSize["Frozen Counter"], 199 | node->evtBufferSize["Binary Output"], 200 | node->evtBufferSize["Analog Output"], 201 | 10 //maxSecurityStatisticEvents 202 | ); 203 | 204 | // you can override an default outstation parameters here 205 | // in this example, we've enabled the oustation to use unsolicted reporting 206 | // if the master enables it 207 | stackConfig.outstation.params.allowUnsolicited = node->allowUnsolicited; 208 | 209 | // You can override the default link layer settings here 210 | // in this example we've changed the default link layer addressing 211 | stackConfig.link.LocalAddr = node->localDNP3Addr; 212 | stackConfig.link.RemoteAddr = node->remoteDNP3Addr; 213 | stackConfig.link.KeepAliveTimeout = openpal::TimeDuration::Max(); 214 | 215 | // You can optionally change the default reporting variations or class assignment prior to enabling the outstation 216 | ConfigureDatabase(stackConfig.dbConfig); 217 | 218 | // Create a new outstation with a log level, command handler, and 219 | // config info this returns a thread-safe interface used for 220 | // updating the outstation's database. 221 | auto outstation = channel->AddOutstation(node->name.c_str(), opendnp3::SuccessCommandHandler::Create(), 222 | opendnp3::DefaultOutstationApplication::Create(), stackConfig); 223 | 224 | // Enable the outstation and start communications 225 | outstation->Enable(); 226 | 227 | /* initialize Lua */ 228 | L = luaL_newstate(); 229 | /* Load Lua base libraries */ 230 | luaL_openlibs(L); 231 | /* load the script */ 232 | bool lua_script_exists = node->luaFileNames.size() >0 ? true : false;//FileExists(node->luaFileName.c_str()); 233 | luaL_dofile(L, node->luaFileNames[0].c_str()); 234 | 235 | while(!this->quitFlag) 236 | { 237 | /* call the lua getdata function */ 238 | if (lua_script_exists){ 239 | while (luaSwitchObj.readCount() > localLuaFlag){ 240 | localLuaFlag+=1; 241 | if (!node->luaKeySwitch.empty() && luaSwitchObj.readInputStr(localLuaFlag) == node->luaKeySwitch){ 242 | auto luaFileToLoad = GetNextLuaFile(); 243 | printf("Outstation:%s -- Changing lua file to %s\n", node->name.c_str(), luaFileToLoad.c_str()); 244 | luaL_dofile(L, luaFileToLoad.c_str()); 245 | } 246 | } 247 | LuaInvokeScript(outstation); 248 | } 249 | usleep(node->msToSleep); 250 | } 251 | /* cleanup Lua */ 252 | lua_close(L); 253 | } 254 | -------------------------------------------------------------------------------- /dnp3generator/OutStation.h: -------------------------------------------------------------------------------- 1 | #ifndef ITI_OUTSTATION_H 2 | #define ITI_OUTSTATION_H 3 | 4 | #include "DataPoint.h" 5 | #include "Station.h" 6 | #include 7 | #include 8 | 9 | class OutStation : public Station 10 | { 11 | public: 12 | OutStation(std::unique_ptr, std::atomic&); 13 | void run() override; 14 | 15 | protected: 16 | void LuaInvokeScript(std::shared_ptr); 17 | }; 18 | #endif 19 | -------------------------------------------------------------------------------- /dnp3generator/README: -------------------------------------------------------------------------------- 1 | ### 2 | ###Running DNP3 from Automatak 3 | ### 4 | ### 5 | --------------------------------------------------------------------------------------- 6 | To install the open dnp3 libraries, asiodnp3, asiopal, opendnp3, openpal 7 | 8 | sudo apt-get install libboost-all-dev liblua5.2-dev 9 | git clone --recursive https://github.com/automatak/dnp3.git 10 | git checkout 1a82d7b1d745412bd343f59033eec820f1c46201 . 11 | 12 | cmake ../dnp3 13 | make -j 14 | sudo make install 15 | --------------------------------------------------------------------------------------- 16 | If you want to run the demo's supplied with the libraries 17 | Go to dnp3_master and dnp3_slave and do: 18 | make 19 | 20 | for dnp3_slave, the source files changed for database.h (prepended an I to the library) 21 | You will have to do: 22 | sudo ln -s IDatabase.h /usr/local/include/opendnp3/outstation/Database.h 23 | sudo ldconfigsudo ldconfig 24 | 25 | The files are pretty much all based on the examples provided by automatak 26 | --------------------------------------------------------------------------------------- 27 | To create the ITI dnp3Generator executable for Ubuntu 64 bit, ver 16.04 LTS 28 | cd dnp3Generator 29 | make clean 30 | make 31 | sudo ./dnp3Generator 32 | 33 | For the orion LX box which is Ubuntu 32 bit, use 34 | make sorion 35 | -------------------------------------------------------------------------------- /dnp3generator/Station.cpp: -------------------------------------------------------------------------------- 1 | #include"Station.h" 2 | #include 3 | #include 4 | 5 | /***************** Analog Input variations ***************************/ 6 | std::map sAnalogInputVarMap = { 7 | {1, opendnp3::StaticAnalogVariation::Group30Var1}, 8 | {2, opendnp3::StaticAnalogVariation::Group30Var2}, 9 | {3, opendnp3::StaticAnalogVariation::Group30Var3}, 10 | {4, opendnp3::StaticAnalogVariation::Group30Var4}, 11 | {5, opendnp3::StaticAnalogVariation::Group30Var5}, 12 | {6, opendnp3::StaticAnalogVariation::Group30Var6} 13 | }; 14 | std::map eAnalogInputVarMap = { 15 | {1, opendnp3::EventAnalogVariation::Group32Var1}, 16 | {2, opendnp3::EventAnalogVariation::Group32Var2}, 17 | {3, opendnp3::EventAnalogVariation::Group32Var3}, 18 | {4, opendnp3::EventAnalogVariation::Group32Var4}, 19 | {5, opendnp3::EventAnalogVariation::Group32Var5}, 20 | {6, opendnp3::EventAnalogVariation::Group32Var6}, 21 | {7, opendnp3::EventAnalogVariation::Group32Var7}, 22 | {8, opendnp3::EventAnalogVariation::Group32Var8} 23 | }; 24 | /***************** Counter variations ***************************/ 25 | std::map sCounterVarMap = { 26 | {1, opendnp3::StaticCounterVariation::Group20Var1}, 27 | {2, opendnp3::StaticCounterVariation::Group20Var2}, 28 | {5, opendnp3::StaticCounterVariation::Group20Var5}, 29 | {6, opendnp3::StaticCounterVariation::Group20Var6} 30 | }; 31 | std::map eCounterVarMap = { 32 | {1, opendnp3::EventCounterVariation::Group22Var1}, 33 | {2, opendnp3::EventCounterVariation::Group22Var2}, 34 | {5, opendnp3::EventCounterVariation::Group22Var5}, 35 | {6, opendnp3::EventCounterVariation::Group22Var6} 36 | }; 37 | /***************** Frozen Counter variations ***************************/ 38 | std::map sFrozenCounterVarMap = { 39 | {1, opendnp3::StaticFrozenCounterVariation::Group21Var1}, 40 | {2, opendnp3::StaticFrozenCounterVariation::Group21Var2}, 41 | {5, opendnp3::StaticFrozenCounterVariation::Group21Var5}, 42 | {6, opendnp3::StaticFrozenCounterVariation::Group21Var6}, 43 | {9, opendnp3::StaticFrozenCounterVariation::Group21Var9}, 44 | {10, opendnp3::StaticFrozenCounterVariation::Group21Var10} 45 | }; 46 | std::map eFrozenCounterVarMap = { 47 | {1, opendnp3::EventFrozenCounterVariation::Group23Var1}, 48 | {2, opendnp3::EventFrozenCounterVariation::Group23Var2}, 49 | {5, opendnp3::EventFrozenCounterVariation::Group23Var5}, 50 | {6, opendnp3::EventFrozenCounterVariation::Group23Var6} 51 | }; 52 | /***************** Binary Input variations ***************************/ 53 | std::map sBinaryVarMap = { 54 | {1, opendnp3::StaticBinaryVariation::Group1Var1}, 55 | {2, opendnp3::StaticBinaryVariation::Group1Var2} 56 | }; 57 | std::map eBinaryVarMap = { 58 | {1, opendnp3::EventBinaryVariation::Group2Var1}, 59 | {2, opendnp3::EventBinaryVariation::Group2Var2}, 60 | {3, opendnp3::EventBinaryVariation::Group2Var3} 61 | }; 62 | /***************** Double Binary Input variations ********************/ 63 | std::map sDoubleBinaryVarMap = { 64 | {2, opendnp3::StaticDoubleBinaryVariation::Group3Var2} 65 | }; 66 | std::map eDoubleBinaryVarMap = { 67 | {1, opendnp3::EventDoubleBinaryVariation::Group4Var1}, 68 | {2, opendnp3::EventDoubleBinaryVariation::Group4Var2}, 69 | {3, opendnp3::EventDoubleBinaryVariation::Group4Var3} 70 | }; 71 | /***************** Event Class Map ********************/ 72 | std::map pointClassVarMap = { 73 | {"0", opendnp3::PointClass::Class0}, 74 | {"1", opendnp3::PointClass::Class1}, 75 | {"2", opendnp3::PointClass::Class2}, 76 | {"3", opendnp3::PointClass::Class3}, 77 | }; 78 | 79 | Station::Station(std::unique_ptr node, std::atomic &quitFlag): 80 | quitFlag(quitFlag), node(std::move(node)), currentLuaFileIndex(0), localLuaFlag(0) 81 | {} 82 | 83 | Station::~Station() 84 | { 85 | std::cout<<"************Destructor called Station with node "<name<luaFileNames.size()-1){ 96 | currentLuaFileIndex += 1; 97 | } 98 | else{ 99 | currentLuaFileIndex=0; 100 | } 101 | return node->luaFileNames[currentLuaFileIndex]; 102 | } 103 | 104 | void Station::ConfigureDatabase(asiodnp3::DatabaseConfig &view) 105 | { 106 | // example of configuring analog index 0 for Class2 with floating point variations by default 107 | // view.analogs[0].variation = opendnp3::StaticAnalogVariation::Group30Var5; 108 | // view.analogs[0].metadata.clazz = opendnp3::PointClass::Class2; 109 | // view.analogs[0].metadata.variation = opendnp3::EventAnalogVariation::Group32Var7; 110 | 111 | for(auto dp : node->dataPoints) { 112 | //dpTypeSizeMap[dp.pointType] = node->dbSize[dp.pointType];//OUR CHECK FOR LUA RETURNED VALUES 113 | if (dp.pointType == "Analog Input") { 114 | try{ 115 | sAnalogInputVarMap.at(dp.sVariation); 116 | } catch (const std::out_of_range& ) { 117 | dp.sVariation = 5; 118 | } 119 | try{ 120 | eAnalogInputVarMap.at(dp.eVariation); 121 | } catch (const std::out_of_range& ) { 122 | dp.eVariation = 7; 123 | } 124 | view.analog[dp.index].svariation = sAnalogInputVarMap[dp.sVariation]; 125 | view.analog[dp.index].clazz = pointClassVarMap[std::to_string(dp.eventClass)]; 126 | view.analog[dp.index].evariation = eAnalogInputVarMap[dp.eVariation]; 127 | dpTypeIndexMap[dp.pointType].insert(dp.index); 128 | if(dp.deadband>0){ 129 | view.analog[dp.index].deadband = dp.deadband; 130 | } 131 | } 132 | else if (dp.pointType == "Binary Input"){ 133 | try{ 134 | sBinaryVarMap.at(dp.sVariation); 135 | } catch (const std::out_of_range& ) { 136 | dp.sVariation = 1; 137 | } 138 | try{ 139 | eBinaryVarMap.at(dp.eVariation); 140 | } catch (const std::out_of_range& ) { 141 | dp.eVariation = 1; 142 | } 143 | view.binary[dp.index].svariation = sBinaryVarMap[dp.sVariation]; 144 | view.binary[dp.index].clazz = pointClassVarMap[std::to_string(dp.eventClass)]; 145 | view.binary[dp.index].evariation = eBinaryVarMap[dp.eVariation]; 146 | dpTypeIndexMap[dp.pointType].insert(dp.index); 147 | } 148 | else if (dp.pointType == "Double Binary Input"){ 149 | try{ 150 | sDoubleBinaryVarMap.at(dp.sVariation); 151 | } catch (const std::out_of_range& ) { 152 | dp.sVariation = 2; 153 | } 154 | try{ 155 | eDoubleBinaryVarMap.at(dp.eVariation); 156 | } catch (const std::out_of_range& ) { 157 | dp.eVariation = 1; 158 | } 159 | view.doubleBinary[dp.index].svariation = sDoubleBinaryVarMap[dp.sVariation]; 160 | view.doubleBinary[dp.index].clazz = pointClassVarMap[std::to_string(dp.eventClass)]; 161 | view.doubleBinary[dp.index].evariation = eDoubleBinaryVarMap[dp.eVariation]; 162 | dpTypeIndexMap[dp.pointType].insert(dp.index); 163 | } 164 | else if (dp.pointType == "Counter"){ 165 | try{ 166 | sCounterVarMap.at(dp.sVariation); 167 | } catch (const std::out_of_range& ) { 168 | dp.sVariation = 5; 169 | } 170 | try{ 171 | eCounterVarMap.at(dp.eVariation); 172 | } catch (const std::out_of_range& ) { 173 | dp.eVariation = 5; 174 | } 175 | view.counter[dp.index].svariation = sCounterVarMap[dp.sVariation]; 176 | view.counter[dp.index].clazz = pointClassVarMap[std::to_string(dp.eventClass)]; 177 | view.counter[dp.index].evariation = eCounterVarMap[dp.eVariation]; 178 | dpTypeIndexMap[dp.pointType].insert(dp.index); 179 | } 180 | else if (dp.pointType == "Frozen Counter"){ 181 | try{ 182 | sFrozenCounterVarMap.at(dp.sVariation); 183 | } catch (const std::out_of_range& ) { 184 | dp.sVariation = 5; 185 | } 186 | try{ 187 | eFrozenCounterVarMap.at(dp.eVariation); 188 | } catch (const std::out_of_range& ) { 189 | dp.eVariation = 5; 190 | } 191 | view.frozenCounter[dp.index].svariation = sFrozenCounterVarMap[dp.sVariation]; 192 | view.frozenCounter[dp.index].clazz = pointClassVarMap[std::to_string(dp.eventClass)]; 193 | view.frozenCounter[dp.index].evariation = eFrozenCounterVarMap[dp.eVariation]; 194 | dpTypeIndexMap[dp.pointType].insert(dp.index); 195 | } 196 | else if (dp.pointType == "Binary Output"){ 197 | } 198 | else if (dp.pointType == "Analog Output"){ 199 | } 200 | } 201 | /* 202 | printf("Elements of Analog Input map are \n"); 203 | for( std::set::iterator it=dpTypeIndexMap["Analog Input"].begin(); it != dpTypeIndexMap["Analog Input"].end(); ++it){ 204 | std::cout << *it << ", "; 205 | } 206 | std::cout << std::endl; 207 | printf("Elements of Binary Input map are \n"); 208 | for( std::set::iterator it=dpTypeIndexMap["Binary Input"].begin(); it != dpTypeIndexMap["Binary Input"].end(); ++it){ 209 | std::cout << *it << ", "; 210 | } 211 | std::cout << std::endl; 212 | */ 213 | } 214 | -------------------------------------------------------------------------------- /dnp3generator/Station.h: -------------------------------------------------------------------------------- 1 | #ifndef ITI_STATION_H 2 | #define ITI_STATION_H 3 | 4 | #include "Node.h" 5 | 6 | #include 7 | #include 8 | #include 9 | 10 | #include 11 | #include 12 | 13 | extern "C" { 14 | #include 15 | #include 16 | #include 17 | } 18 | /***************** Analog Input variations ***************************/ 19 | extern std::map sAnalogInputVarMap; 20 | extern std::map eAnalogInputVarMap; 21 | /***************** Counter variations ***************************/ 22 | extern std::map sCounterVarMap ; 23 | extern std::map eCounterVarMap; 24 | /***************** Frozen Counter variations ***************************/ 25 | extern std::map sFrozenCounterVarMap; 26 | extern std::map eFrozenCounterVarMap; 27 | /***************** Binary Input variations ***************************/ 28 | extern std::map sBinaryVarMap ; 29 | extern std::map eBinaryVarMap; 30 | /***************** Double Binary Input variations ********************/ 31 | extern std::map sDoubleBinaryVarMap; 32 | extern std::map eDoubleBinaryVarMap; 33 | 34 | extern std::map pointClassVarMap; 35 | 36 | class Station 37 | { 38 | public: 39 | Station(std::unique_ptr, std::atomic &); 40 | virtual ~Station(); 41 | 42 | virtual void run() = 0; 43 | void ConfigureDatabase(asiodnp3::DatabaseConfig&); 44 | bool FileExists(const char *fileName); 45 | std::string GetNextLuaFile(); 46 | 47 | std::unique_ptr node; 48 | asiodnp3::DNP3Manager* manager; 49 | std::map> dpTypeIndexMap; 50 | 51 | protected: 52 | std::atomic &quitFlag; 53 | lua_State *L; 54 | int localLuaFlag; //dont need our in thread flag to be atomic 55 | int currentLuaFileIndex; 56 | }; 57 | 58 | #endif 59 | -------------------------------------------------------------------------------- /dnp3generator/StringUtilities.cpp: -------------------------------------------------------------------------------- 1 | #include "StringUtilities.h" 2 | 3 | #include 4 | 5 | std::string trim(const std::string& source, char const* delims) 6 | { 7 | std::string::size_type pos = source.find_first_not_of(delims); 8 | if (pos == std::string::npos) 9 | pos = 0; 10 | 11 | std::string::size_type count = source.find_last_not_of(delims); 12 | if (count != std::string::npos) 13 | count = count - pos; 14 | 15 | return source.substr(pos, count); 16 | } 17 | 18 | //string splitting function 19 | std::vector split(const std::string& str, char delim) 20 | { 21 | std::vector tokens; 22 | std::stringstream ss(str); 23 | std::string tok; 24 | while(getline(ss, tok, delim)) 25 | { 26 | tokens.push_back(tok); 27 | } 28 | return tokens; 29 | } 30 | -------------------------------------------------------------------------------- /dnp3generator/StringUtilities.h: -------------------------------------------------------------------------------- 1 | #ifndef ITI_STRINGUTILITIES_H 2 | #define ITI_STRINGUTILITIES_H 3 | 4 | #include 5 | #include 6 | 7 | std::string trim(std::string const& source, char const* delims = " \t\r\n"); 8 | std::vector split(const std::string& str, char delim); 9 | 10 | #endif 11 | -------------------------------------------------------------------------------- /dnp3generator/UtilityScripts/convertAllLineEndInDir.py: -------------------------------------------------------------------------------- 1 | import os 2 | import string 3 | import argparse 4 | 5 | for filename in os.listdir('.'): 6 | if "TD" not in filename: 7 | continue 8 | fileread = open(filename, 'r') 9 | print(filename) 10 | out_name = os.path.splitext(filename)[0]+'convert.csv' 11 | print (out_name) 12 | with open(out_name, 'w') as fileout: 13 | for line in fileread: 14 | new_line = string.replace(line, '\r','') 15 | fileout.write(new_line) 16 | -------------------------------------------------------------------------------- /dnp3generator/UtilityScripts/convertLineEnding.py: -------------------------------------------------------------------------------- 1 | import os 2 | import string 3 | import argparse 4 | 5 | parser = argparse.ArgumentParser(description='Convert line ending to line ending recognized by lua.') 6 | parser.add_argument('-i', '--file', default = "Fault 4_24s_1ms.csv", help='Input csv File') 7 | parser.add_argument('-w', '--out', default = "DNP3Data.csv", help='Output csv File') 8 | args = parser.parse_args() 9 | 10 | if not os.path.isfile(args.file): 11 | print("Invalid path for input File") 12 | exit() 13 | if args.file == args.out: 14 | print("Use a different name for the output file. It cannot be the same name as the input file") 15 | exit() 16 | with open(args.file, 'r') as fileread: 17 | with open(args.out, 'w') as fileout: 18 | for line in fileread: 19 | new_line = string.replace(line, '\r','') 20 | fileout.write(new_line) 21 | -------------------------------------------------------------------------------- /dnp3generator/UtilityScripts/convertMappingToJson.py: -------------------------------------------------------------------------------- 1 | import os 2 | import string 3 | import argparse 4 | import re 5 | 6 | parser = argparse.ArgumentParser(description='Convert mapping file to json for config file.') 7 | parser.add_argument('-i', '--file', default = "DNP point list_0808.txt", help='Input mapping File') 8 | parser.add_argument('-w', '--out', default = "dataField.json", help='Output json snippet File') 9 | args = parser.parse_args() 10 | 11 | pattern = re.compile("(AI|BI|CI)\s*:\s*(\d+)\s*(\w+)\s*([0-9]*[.]?[0-9]+)") 12 | typedict = {'AI':'Analog Input', 'BI':'Binary Input', 'CI':'Counter'} 13 | evtVarDict = {'AI': '"Event Class":2, "sVariation":5, "eVariation":7', 14 | 'BI': '"Event Class":1, "sVariation":1, "eVariation":1'} 15 | if not os.path.isfile(args.file): 16 | print("Invalid path for input File") 17 | exit() 18 | 19 | jsonStr = '"Data":\n[\n' 20 | with open(args.file, 'r') as fileread: 21 | for line in fileread: 22 | line = line.strip() 23 | if line.startswith("#"): 24 | continue 25 | match = re.match(pattern, line) 26 | if match: 27 | ftype = match.group(1) 28 | index = match.group(2) 29 | fname = match.group(3) 30 | deadband = match.group(4) 31 | fline = '\t{"Type":"' + typedict[ftype] + '", ' + evtVarDict[ftype] + ', "Index":' + index 32 | if ftype == 'AI': 33 | fline += ', "Deadband":' + deadband 34 | fline += '},' 35 | jsonStr += fline + "\n" 36 | jsonStr = jsonStr[:-2] #last line, we want to remove the trailing "," 37 | jsonStr += '\n]' 38 | #print (jsonStr) 39 | with open(args.out, 'w') as fileout: 40 | fileout.write(jsonStr) 41 | -------------------------------------------------------------------------------- /dnp3generator/UtilityScripts/merge_relay_files.py: -------------------------------------------------------------------------------- 1 | import pandas as pd 2 | import argparse 3 | ''' 4 | This script combines the Steady State csv files for the 4 relays into one steady state file that can be used at the Orion. 5 | It expects an input argument that indicates the base index of the steady state file. Typical values would be 1,2,....6 or 11,12,...16 6 | It writes the SSReplayX.csv file as output. The line endings are not affected by this script. 7 | ''' 8 | parser = argparse.ArgumentParser(description='Combine csv files for 4 relays into one for orion') 9 | parser.add_argument('-i', '--ss_index', default = 1, help='Index of Steady State file') 10 | args = parser.parse_args() 11 | 12 | relay_files = ["R90SS", "R91SS", "R92SS", "R93SS"] 13 | 14 | all_data = pd.DataFrame() 15 | for f in relay_files: 16 | csv_file = f+args.ss_index+".csv" 17 | print(csv_file) 18 | df = pd.read_csv(csv_file) 19 | if not all_data.empty: 20 | del df['Time'] 21 | all_data = pd.concat([all_data, df],axis=1) 22 | 23 | all_data.to_csv('SSReplay'+args.ss_index+'.csv', index=False) 24 | -------------------------------------------------------------------------------- /dnp3generator/common.mk: -------------------------------------------------------------------------------- 1 | 2 | # The C compiler and flags 3 | CC=gcc 4 | CFLAGS=-pthread -g # -wall 5 | 6 | # The C++ compiler and flags 7 | CXX=g++ 8 | CXXFLAGS=-pthread -std=c++11 # -wall 9 | 10 | DEBUG_FLAGS= -DDEBUG -g -O0 11 | RELEASE_FLAGS= -O3 12 | 13 | SCFLAGS=-std=c++11 -fprofile-arcs -ftest-coverage -g # -wall 14 | LFLAGS=-lgcov --coverage 15 | SGCCFLAGS= -static-libgcc -static-libstdc++ 16 | 17 | CODE_OBJS = Station.o MasterStation.o OutStation.o MappingOutstation.o Node.o CidrCalculator.o StringUtilities.o DataPoint.o CfgJsonParser.o MappingSoeHandler.o 18 | 19 | DNP3_LIBS = -lasiodnp3 -lasiopal -lopendnp3 -lopenpal -lstdc++ 20 | LUA_LIBS = -llua5.2 21 | BOOST_LIBS = -lboost_system -lboost_thread 22 | 23 | #x86_64 Configuration 24 | LIBS = $(DNP3_LIBS) $(LUA_LIBS) $(BOOST_LIBS) 25 | 26 | #x86_32 Configuration 27 | LIBS32 = $(DNP3_LIBS) $(subst x86_64,i386,$(LUA_LIBS) $(BOOST_LIBS)) 28 | 29 | #x86_32 Configuration--Static 30 | SLIBS32 = $(LIBS32:%.so=%.a) -ldl 31 | 32 | #RPI-ARM Configuration 33 | ARMLIBS = $(DNP3_LIBS) $(subst x86_64-linux-gnu,arm-linux-gnueabihf,$(LUA_LIBS) $(BOOST_LIBS)) 34 | 35 | #OrionLX Static Configuration 36 | sorionargs = -Wl,--whole-archive -lpthread -Wl,--no-whole-archive 37 | 38 | #OrionLX Static/Dynamic Configuration 39 | sdorionargs = -Wl,--dynamic-linker=/usr/local/dnp3gen/ld-dnp3gen.so $(sorionargs) -Wl,-Bdynamic /usr/local/lib/libdummy.so 40 | 41 | all: $(TARGET) 42 | 43 | #Static Build for OrionLX 44 | sorion: $(OBJS) 45 | $(CXX) $(sorionargs) $(SCFLAGS) $(LFLAGS) -static $(SGCCFLAGS) -o $(TARGET)-orionstatic $(OBJS) $(SLIBS32) 46 | 47 | #Static Build for RPI/ARM 48 | srpiarm: $(OBJS) 49 | $(CXX) $(sorionargs) $(SCFLAGS) $(LFLAGS) -static $(SGCCFLAGS) -o $(TARGET)-armstatic $(OBJS) $(SLIBS32) 50 | 51 | #Static/Dynamic Build for OrionLX with dummy lib for injection 52 | sdorion: $(OBJS) 53 | $(CXX) $(sdorionargs) $(SCFLAGS) $(LFLAGS) $(SGCCFLAGS) -o $(TARGET)-orionsd $(OBJS) $(SLIBS32) 54 | patchelf --set-rpath /usr/local/dnp3gen $(TARGET)-orionsd 55 | cd dummy && $(MAKE) dummy 56 | 57 | 58 | $(TARGET): $(OBJS) 59 | $(CXX) $(CXXFLAGS) $(DEBUG) -o $(TARGET) $(OBJS) $(LIBS) 60 | 61 | .PHONY: clean all 62 | clean: 63 | rm -f $(OBJS) $(TARGET) $(TARGET)-orion $(TARGET)-test32 $(TARGET)-orionstatic $(TARGET)-orionsd $(TARGET)-armstatic 64 | cd dummy && $(MAKE) clean 65 | 66 | #$(filter %OutStation.o,$(OBJS)): $(OBJDIR)/%.o: %.cpp %.h 67 | # $(CXX) $(CXXFLAGS) $(DEBUG) -c $< -o $@ -llua5.2 68 | 69 | #$(OBJS): $(OBJDIR)/%.o: %.cpp %.h 70 | # $(CXX) $(CXXFLAGS) $(DEBUG_FLAGS) -c $< -o $@ 71 | 72 | $(OBJS): | $(OBJDIR) 73 | 74 | $(OBJDIR): 75 | mkdir $(OBJDIR) 76 | -------------------------------------------------------------------------------- /dnp3generator/debian/changelog: -------------------------------------------------------------------------------- 1 | dnp3generator (1.0-1-1) stable; urgency=low 2 | 3 | * Initial Packaging 4 | 5 | -- Steve Granda Mon, 11 Dec 2017 16:17:12 -0600 6 | -------------------------------------------------------------------------------- /dnp3generator/debian/compat: -------------------------------------------------------------------------------- 1 | 9 2 | -------------------------------------------------------------------------------- /dnp3generator/debian/control: -------------------------------------------------------------------------------- 1 | Source: dnp3generator 2 | Section: misc 3 | Priority: extra 4 | Maintainer: Steve Granda 5 | Build-Depends: debhelper (>= 9.0.0) 6 | Standards-Version: 3.9.3 7 | Homepage: http://iti.illinois.edu 8 | 9 | Package: dnp3generator 10 | Architecture: any 11 | Description: DNP3 Traffic Generator 12 | Traffic Generator based on Automatak OpenDNP3 13 | -------------------------------------------------------------------------------- /dnp3generator/debian/copyright: -------------------------------------------------------------------------------- 1 | Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ 2 | Upstream-Name: dnp3Generator 3 | Source: 4 | 5 | Copyright 2017 University of Illinois 6 | License: NCSA Open Source License 7 | 8 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal with the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 9 | . 10 | Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimers. 11 | . 12 | Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimers in the documentation and/or other materials provided with the distribution. 13 | . 14 | Neither the names of Information Trust Institute, nor the names of its contributors may be used to endorse or promote products derived from this Software without specific prior written permission. 15 | . 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE CONTRIBUTORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS WITH THE SOFTWARE. 17 | -------------------------------------------------------------------------------- /dnp3generator/debian/dnp3generator.install: -------------------------------------------------------------------------------- 1 | dnp3Generator /usr/bin 2 | extras/ExampleConfigs/SimulatedPair/Config.json /etc/dnp3 3 | extras/ExampleConfigs/LuaConfigs/BasicSample1.lua /etc/dnp3 4 | #extras/ /etc/dnp3 5 | -------------------------------------------------------------------------------- /dnp3generator/debian/docs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ITI/ics-trafficgen/af2445f21817285382852fd33e7178785f8e63d9/dnp3generator/debian/docs -------------------------------------------------------------------------------- /dnp3generator/debian/init.d: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | ### BEGIN INIT INFO 3 | # Provides: dnp3Generator 4 | # Required-Start: $network $local_fs 5 | # Required-Stop: $local_fs 6 | # Default-Start: 2 3 4 5 7 | # Default-Stop: 0 1 6 8 | # X-Interactive: false 9 | # Short-Description: DNP3Generator Service 10 | # Description: Start/stop Dnp3Generator 11 | ### END INIT INFO 12 | DESC="Dnp3 Traffic Generator" 13 | NAME=dnp3Generator 14 | PIPELOC="/var/run/dnp3pipe" 15 | DNP3PIPE="-p $PIPELOC" 16 | DAEMON=/usr/bin/dnp3Generator 17 | . /lib/lsb/init-functions 18 | 19 | do_start() 20 | { 21 | start-stop-daemon --start --chdir /etc/dnp3/ --background --quiet --name $NAME --exec $DAEMON -- $DNP3PIPE 22 | echo "Starting Dnp3Generator"; 23 | } 24 | 25 | do_stop() 26 | { 27 | start-stop-daemon --stop --quiet --signal QUIT --name $NAME 28 | echo "Stopping Dnp3Generator"; 29 | rm -rf $PIPELOC 30 | } 31 | 32 | 33 | case "$1" in 34 | start) 35 | do_start 36 | ;; 37 | stop) 38 | do_stop 39 | ;; 40 | restart) 41 | do_stop 42 | do_start 43 | ;; 44 | force-reload) 45 | do_stop 46 | do_start 47 | ;; 48 | esac 49 | 50 | exit 0 51 | -------------------------------------------------------------------------------- /dnp3generator/debian/postinst: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # postinst script for dnp3generator 3 | # 4 | # see: dh_installdeb(1) 5 | 6 | set -e 7 | 8 | # summary of how this script can be called: 9 | # * `configure' 10 | # * `abort-upgrade' 11 | # * `abort-remove' `in-favour' 12 | # 13 | # * `abort-remove' 14 | # * `abort-deconfigure' `in-favour' 15 | # `removing' 16 | # 17 | # for details, see http://www.debian.org/doc/debian-policy/ or 18 | # the debian-policy package 19 | 20 | 21 | case "$1" in 22 | configure) 23 | sed -i -e 's/ens38/lo/g' /etc/dnp3/Config.json 24 | ;; 25 | 26 | abort-upgrade|abort-remove|abort-deconfigure) 27 | ;; 28 | 29 | *) 30 | echo "postinst called with unknown argument \`$1'" >&2 31 | exit 1 32 | ;; 33 | esac 34 | 35 | # dh_installdeb will replace this with shell code automatically 36 | # generated by other debhelper scripts. 37 | 38 | #DEBHELPER# 39 | 40 | exit 0 41 | -------------------------------------------------------------------------------- /dnp3generator/debian/rules: -------------------------------------------------------------------------------- 1 | #!/usr/bin/make -f 2 | # -*- makefile -*- 3 | # Sample debian/rules that uses debhelper. 4 | # This file was originally written by Joey Hess and Craig Small. 5 | # As a special exception, when this file is copied by dh-make into a 6 | # dh-make output file, you may use that output file without restriction. 7 | # This special exception was added by Craig Small in version 0.37 of dh-make. 8 | 9 | # Uncomment this to turn on verbose mode. 10 | #export DH_VERBOSE=1 11 | 12 | %: 13 | dh $@ 14 | override_dh_auto_build: 15 | dh_auto_build -- $(MAKE) sorion; mv dnp3Generator-orionstatic dnp3Generator 16 | -------------------------------------------------------------------------------- /dnp3generator/debian/source/format: -------------------------------------------------------------------------------- 1 | 3.0 (quilt) 2 | -------------------------------------------------------------------------------- /dnp3generator/dnp3app.cpp: -------------------------------------------------------------------------------- 1 | 2 | 3 | #include "dnp3app.h" 4 | #include "MasterStation.h" 5 | #include "OutStation.h" 6 | #include "MappingOutstation.h" 7 | #include "Node.h" 8 | #include "CfgJsonParser.h" 9 | 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | 21 | #include 22 | #include 23 | 24 | void signal_h(int signalType) 25 | { 26 | printf("Caught Signal, Exiting %d\n", signalType); 27 | exit(EXIT_SUCCESS); 28 | } 29 | 30 | ThreadSafeUserInput luaSwitchObj; 31 | int main(int argc, char *argv[]) 32 | { 33 | //Handle Ctrl-C event 34 | struct sigaction sigIntHandler; 35 | sigIntHandler.sa_handler = signal_h; 36 | sigemptyset(&sigIntHandler.sa_mask); 37 | sigIntHandler.sa_flags = 0; 38 | sigaction(SIGINT, &sigIntHandler, NULL); 39 | 40 | //Check we can run shell commands 41 | if (system(NULL)) 42 | puts("OK"); 43 | else 44 | exit(EXIT_FAILURE); 45 | 46 | //Parse command line options 47 | std::string cfgFileName = "Config.json"; 48 | std::string pipeName = "dnp3pipe"; 49 | int iflag; 50 | while ((iflag = getopt(argc, argv, "c:p:")) != -1) 51 | { 52 | switch (iflag) 53 | { 54 | case 'c': 55 | cfgFileName = optarg; 56 | break; 57 | case 'p': 58 | pipeName = optarg; 59 | break; 60 | default: 61 | printf("Unknown command line option. Use -c ConfigFile to create your stations.\n"); 62 | exit(EXIT_FAILURE); 63 | } 64 | } 65 | /* Create the pipe for user input, abort the application if it already exists */ 66 | struct stat buffer; 67 | if (stat (pipeName.c_str(), &buffer) == 0){ 68 | printf("Pipe %s already exists. Please delete it and restart the application.\n", pipeName.c_str()); 69 | return 0; 70 | } 71 | std::string subcmd = "mkfifo " + pipeName; 72 | int success = system(subcmd.c_str()); 73 | if(success != 0){ 74 | printf("Was not able to create the specified pipe. Aborting application.\n"); 75 | return 0; 76 | } 77 | 78 | CfgJsonParser cfgReader = CfgJsonParser(cfgFileName); 79 | std::vector>& nodes = cfgReader.GetConfiguredNodes(); 80 | 81 | std::atomic quitFlag(false); 82 | std::set luaTriggers; 83 | std::vector threads; 84 | std::vector> srcMasterStations; 85 | std::vector> mappedOutStations; 86 | 87 | unsigned int nrSupportedConcurrentThreads = std::max(std::thread::hardware_concurrency(), (unsigned int)1); 88 | asiodnp3::DNP3Manager manager(nrSupportedConcurrentThreads, asiodnp3::ConsoleLogger::Create()); 89 | for(auto& vect : nodes) 90 | { 91 | printf("\n>>>>Name:%s IPADDRESS:%s NIC:%s Remote IP Address:%s localDNP3Addr:%i RemoteDNP3Address:%i ROLE:%s\n", vect->name.c_str(), vect->local_IPAddress.c_str(), \ 92 | vect->vnic.c_str(), vect->remote_IPAddress.c_str(), vect->localDNP3Addr, vect->remoteDNP3Addr, vect->role.c_str()); 93 | 94 | if(!vect->luaKeySwitch.empty()){ 95 | luaTriggers.insert(vect->luaKeySwitch); 96 | } 97 | if(vect->role == "Master") 98 | { 99 | std::shared_ptr p_master = std::make_shared(std::move(vect), quitFlag); 100 | p_master->initialize(); 101 | p_master->manager = &manager; 102 | if(!p_master->node->boundOutstations.empty()){ 103 | srcMasterStations.push_back(p_master); 104 | } 105 | threads.push_back(std::thread(&MasterStation::run, p_master)); 106 | } 107 | else if(vect->role == "Outstation") 108 | { 109 | std::shared_ptr p_outstation(nullptr); 110 | if (!vect->dataSources.empty()){ 111 | std::shared_ptr mapped_station= std::make_shared(std::move(vect), quitFlag); 112 | mapped_station->initialize(); 113 | mappedOutStations.push_back(mapped_station); 114 | p_outstation = mapped_station; 115 | } 116 | else 117 | p_outstation = std::make_shared(std::move(vect), quitFlag); 118 | p_outstation->manager = &manager; 119 | 120 | threads.push_back(std::thread(&OutStation::run, p_outstation)); 121 | } 122 | } 123 | //connect the mapped master and bound outstations 124 | 125 | for(auto masterStation : srcMasterStations) { 126 | for(auto boundOutStnName : masterStation->node->boundOutstations) { 127 | for(auto outStation : mappedOutStations) { 128 | if(outStation->node->name == boundOutStnName) { 129 | masterStation->destStations.push_back(outStation); 130 | std::cout<<"Adding station " << outStation->node->name << " to destStations for master " << masterStation->node->name <<"\n"; 131 | } 132 | } 133 | } 134 | masterStation->UpdateDestinations();//TODO TEMP NEED TO MAKE SURE IT IS TIMELY 135 | } 136 | 137 | std::ifstream pipe; 138 | std::string usr_input; 139 | std::cout << "Enter a command" << std::endl; 140 | std::cout << "x - exits program" << std::endl; 141 | 142 | std::chrono::milliseconds waitTime_sec(500); 143 | std::string exitStr("x"); 144 | while (true) 145 | { 146 | if(!pipe.is_open()){ 147 | pipe.open(pipeName.c_str(), std::ifstream::in); 148 | if(!pipe){ 149 | printf("Error opening pipe! Will not be able to process user input.\n"); 150 | } 151 | } 152 | std::getline(pipe, usr_input); //wait here for input from user on the pipe 153 | if (usr_input == exitStr){ // C++ destructor on DNP3Manager cleans everything up for you 154 | quitFlag=true; 155 | for(auto& t : threads) 156 | t.join(); 157 | return 0; 158 | } 159 | else if (luaTriggers.count(usr_input) > 0){ //found a switching character that some outstation is interested in 160 | luaSwitchObj.unconditionalWriter(usr_input); 161 | pipe.close(); 162 | } 163 | else{ //unrecognized string entered by user 164 | pipe.close(); 165 | std::this_thread::sleep_for(waitTime_sec); 166 | } 167 | } 168 | return 0; 169 | } 170 | -------------------------------------------------------------------------------- /dnp3generator/dnp3app.h: -------------------------------------------------------------------------------- 1 | #ifndef ITI_DNP3APP_H 2 | #define ITI_DNP3APP_H 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | struct ThreadSafeUserInput { 9 | boost::shared_mutex _accessMutex; 10 | int readCount() 11 | { 12 | boost::shared_lock lock(_accessMutex); 13 | // do work here, without anyone having exclusive access 14 | return currentCount; 15 | } 16 | std::string readInputStr(int keyCount) { 17 | boost::shared_lock lock(_accessMutex); 18 | // do work here, without anyone having exclusive access 19 | if (keyPresses.count(keyCount) != 0) 20 | return keyPresses[keyCount]; 21 | return '\0'; 22 | } 23 | void unconditionalWriter(std::string input_str) 24 | { 25 | boost::unique_lock lock(_accessMutex); 26 | // exclusive access 27 | //limit size of cached keys to 10 28 | if(keyPresses.size() > 10 && keyPresses.count(currentCount+1-10) != 0){ 29 | keyPresses.erase(currentCount+1-10); //erase the lowest index 30 | } 31 | currentCount++; 32 | keyPresses[currentCount]=input_str; 33 | } 34 | private: 35 | int currentCount = 0; 36 | std::unordered_map keyPresses; 37 | }; 38 | 39 | #endif 40 | -------------------------------------------------------------------------------- /dnp3generator/dummy/Makefile: -------------------------------------------------------------------------------- 1 | dummy: 2 | gcc -c -Wall -Werror -nodefaultlibs -fpic dummy.c 3 | gcc -shared -fPIC -o libdummy.so dummy.o 4 | 5 | clean: 6 | rm -rf libdummy.so 7 | rm -rf dummy.o 8 | -------------------------------------------------------------------------------- /dnp3generator/dummy/dummy.c: -------------------------------------------------------------------------------- 1 | void dummy(void) 2 | { 3 | return; 4 | } 5 | -------------------------------------------------------------------------------- /dnp3generator/dummy/dummy.h: -------------------------------------------------------------------------------- 1 | #ifndef dummy_h__ 2 | #define dummy_h__ 3 | 4 | extern void dummy(void); 5 | 6 | #endif // dummy_h__ 7 | -------------------------------------------------------------------------------- /dnp3generator/dummy/main.c: -------------------------------------------------------------------------------- 1 | #include "dummy.h" 2 | void main(void) 3 | { 4 | dummy(); 5 | return 0; 6 | } 7 | -------------------------------------------------------------------------------- /dnp3generator/extras/ExampleConfigs/BufferChange/Config.json: -------------------------------------------------------------------------------- 1 | { 2 | "Virtual Interface": "ens38", 3 | "CIDR Notation":"172.16.136.1/24", 4 | "Nodes": 5 | [ 6 | { 7 | "Name":{"Master":"MasterStationOne", "Outstation":"OutStationOne"}, 8 | "DNP3 Address":{"Master":95, "Outstation":20}, 9 | "Lua File":{"Outstation":["BasicSample1.lua"]}, 10 | "Allow Unsolicited": false, 11 | "Data": 12 | [ 13 | {"Type":"Analog Input", "Event Class":2, "sVariation":5, "eVariation":7, "Index":0, "Deadband":"1"}, 14 | {"Type":"Analog Input", "Event Class":2, "sVariation":5, "eVariation":7, "Index":1, "Deadband":"1"}, 15 | {"Type":"Analog Input", "Event Class":2, "sVariation":5, "eVariation":7, "Index":2, "Deadband":"1"}, 16 | {"Type":"Analog Input", "Event Class":2, "sVariation":5, "eVariation":7, "Index":3, "Deadband":"1"}, 17 | {"Type":"Analog Input", "Event Class":2, "sVariation":5, "eVariation":7, "Index":4, "Deadband":"1"}, 18 | {"Type":"Analog Input", "Event Class":2, "sVariation":5, "eVariation":7, "Index":5, "Deadband":"1"}, 19 | {"Type":"Analog Input", "Event Class":2, "sVariation":5, "eVariation":7, "Index":6, "Deadband":"1"}, 20 | {"Type":"Analog Input", "Event Class":2, "sVariation":5, "eVariation":7, "Index":7, "Deadband":"1"}, 21 | {"Type":"Analog Input", "Event Class":2, "sVariation":5, "eVariation":7, "Index":8, "Deadband":"1"}, 22 | {"Type":"Analog Input", "Event Class":2, "sVariation":5, "eVariation":7, "Index":9, "Deadband":"1"}, 23 | {"Type":"Binary Input", "Event Class":1, "sVariation":1, "eVariation":1, "Index":0}, 24 | {"Type":"Binary Input", "Event Class":1, "sVariation":1, "eVariation":1, "Index":1}, 25 | {"Type":"Binary Input", "Event Class":1, "sVariation":1, "eVariation":1, "Index":2}, 26 | {"Type":"Binary Input", "Event Class":1, "sVariation":1, "eVariation":1, "Index":3}, 27 | {"Type":"Binary Input", "Event Class":1, "sVariation":1, "eVariation":1, "Index":4}, 28 | {"Type":"Binary Input", "Event Class":1, "sVariation":1, "eVariation":1, "Index":5}, 29 | {"Type":"Binary Input", "Event Class":1, "sVariation":1, "eVariation":1, "Index":6}, 30 | {"Type":"Binary Input", "Event Class":1, "sVariation":1, "eVariation":1, "Index":7}, 31 | {"Type":"Binary Input", "Event Class":1, "sVariation":1, "eVariation":1, "Index":8}, 32 | {"Type":"Binary Input", "Event Class":1, "sVariation":1, "eVariation":1, "Index":9} 33 | ], 34 | "Event Data": 35 | [ 36 | {"Type":"Analog Input", "Size":100}, 37 | {"Type":"Binary Input", "Size":200}, 38 | {"Type":"Double Binary Input", "Size":300}, 39 | {"Type":"Counter", "Size":400}, 40 | {"Type":"Frozen Counter", "Size":500}, 41 | {"Type":"Binary Counter", "Size":600}, 42 | {"Type":"Analog Output", "Size":700} 43 | ], 44 | "Poll Interval": 45 | [ 46 | {"Event Class":"0123", "Frequency":3} 47 | ] 48 | } 49 | 50 | ] 51 | 52 | } 53 | -------------------------------------------------------------------------------- /dnp3generator/extras/ExampleConfigs/BufferChange/README: -------------------------------------------------------------------------------- 1 | ------------------------------------------------------------------------------- 2 | -Buffer Change Example 3 | ------------------------------------------------------------------------------- 4 | 5 | In this example configuration we take a simple Master/Outstation pair and 6 | modify the event buffer. 7 | 8 | To run this example, compile dnp3Generator, and execute with "Config.json". The specified lua file 9 | is be located in ../LuaConfigs. make sure the path to the lua file is correct in the Config file. 10 | 11 | This example uses: 12 | "Lua File":{"Outstation":["BasicSample1.lua"]} 13 | The DNP3Generator will look for BasicSample1.lua (copy located in ../LuaConfigs) to generate values for OutStationOne 14 | 15 | To further expand this, a master station can also have a lua file 16 | The JSON would look similar to: 17 | "Lua File":{"Outstation":["BasicSample1.lua"], "Master":["MasterStationExample.lua"]} 18 | 19 | If needed multiple lua files can be switched for each Outstation and/or Master Instance using specified key triggers. 20 | The JSON to specify Multiple Lua Files looks like: 21 | "Lua File":{"Outstation":["BasicSample1.lua", "AnotherLuaFileExample.lua"]} 22 | 23 | Then a trigger would be specified in the JSON via: 24 | "Lua Switch Trigger":{"Outstation":"letter or string to switch Outstation lua file","Master":"letter or string to switch Master lua file"} 25 | 26 | The paired Outstation and Master can specify lua files for both 27 | "Lua File":{"Master":["M1_Example_Smart_Reading.lua", "Another_Sample_File.lua"], "Outstation":["ExampleFile1.lua","ExampleFile2.lua"]}, 28 | "Lua Switch Trigger":{"Master":"l","Outstation":"j"}, 29 | 30 | The switch trigger key can also be a string instead of a character. 31 | -------------------------------------------------------------------------------- /dnp3generator/extras/ExampleConfigs/LuaConfigs/BasicSample1.lua: -------------------------------------------------------------------------------- 1 | ntime = os.time() +15 2 | 3 | function generate_data() 4 | data = {} 5 | if os.time() > ntime then 6 | data["Analog Input"]={} 7 | data["Counter"]={} 8 | data["Binary Input"]={} 9 | for i =1,10 do 10 | data["Analog Input"][i]=i 11 | data["Binary Input"][i]=math.floor(math.random()*2) 12 | data["Counter"][i]=math.floor(i*math.random()) 13 | end 14 | ntime=ntime+15 15 | end 16 | return data 17 | end 18 | -------------------------------------------------------------------------------- /dnp3generator/extras/ExampleConfigs/LuaConfigs/BasicSample2.lua: -------------------------------------------------------------------------------- 1 | ntime = os.time() +10 2 | 3 | function generate_data() 4 | data = {} 5 | if os.time() > ntime then 6 | data["Analog Input"]={} 7 | data["Counter"]={} 8 | data["Binary Input"]={} 9 | for i =1,5 do 10 | data["Analog Input"][i]=i*2*math.random() 11 | data["Binary Input"][i]=math.floor(math.random()*2) 12 | data["Counter"][i]=math.floor(i*2*math.random()) 13 | end 14 | ntime=ntime+10 15 | end 16 | return data 17 | end 18 | -------------------------------------------------------------------------------- /dnp3generator/extras/ExampleConfigs/LuaConfigs/M1_Example_SendCommand.lua: -------------------------------------------------------------------------------- 1 | ntime = os.time() +15 2 | 3 | function operate_outstation() 4 | data = {} 5 | if os.time() > ntime then 6 | funcType = "DO" 7 | funcName = "CROB" 8 | index= math.floor(math.random()*10) 9 | --print("Printing to index") 10 | --print(index) 11 | value=math.floor(2*math.random()) 12 | --print(value) 13 | table.insert(data, {funcType, funcName, index, value}) 14 | 15 | funcName = "AnalogOutputFloat32" 16 | funcType = "SBO" 17 | index= math.floor(math.random()*10) 18 | --print("Printing to index") 19 | --print(index) 20 | value=15*math.random() 21 | --print(value) 22 | table.insert(data, {funcType, funcName, index, value}) 23 | ntime=ntime+15 24 | --print(data) 25 | --comment out lines below to disable printing of table 26 | for k,v in pairs(data) do 27 | for k1,v1 in pairs(v) do 28 | print(k, k1, v1) 29 | end 30 | end 31 | --end print table 32 | end 33 | return data 34 | end 35 | -------------------------------------------------------------------------------- /dnp3generator/extras/ExampleConfigs/LuaConfigs/M1_Example_Smart_Reading.lua: -------------------------------------------------------------------------------- 1 | ntime = os.time() +15 2 | 3 | function operate_outstation() 4 | data = {} 5 | if os.time() > ntime then 6 | ntime=ntime+15 7 | funcType = "Scan" 8 | funcName = "Group30Var5" 9 | start_index= 5 10 | --print("Printing to index") 11 | --print(start_index) 12 | end_index=9 13 | --print(end_index) 14 | table.insert(data, {funcType, funcName, start_index, end_index}) 15 | 16 | --comment out lines below to disable printing of table 17 | -- for k,v in pairs(data) do 18 | -- for k1,v1 in pairs(v) do 19 | -- print(k, k1, v1) 20 | -- end 21 | -- end 22 | --end print table 23 | end 24 | return data 25 | end 26 | -------------------------------------------------------------------------------- /dnp3generator/extras/ExampleConfigs/LuaConfigs/MS1_Multiplexer_Example_InjectValues.lua: -------------------------------------------------------------------------------- 1 | 2 | 3 | function modify_analog(table) 4 | --print("IN LUA SCRIPT FOR MODIFYING ANALOG VALUES!!!!!!!!!") 5 | -- return does not like local modifier to analog_data. Who knew?? 6 | analog_data = {} 7 | for k,v in pairs(table) do 8 | print(k,v) 9 | analog_data[k] = v+10 10 | end 11 | return analog_data 12 | end 13 | 14 | function modify_binary(table) 15 | --print("IN LUA SCRIPT FOR MODIFYING BINARY VALUES!!!!!!!!!") 16 | -- return does not like local modifier to binary_data. Who knew?? 17 | binary_data = {} 18 | for k,v in pairs(table) do 19 | binary_data[k] = v 20 | end 21 | return binary_data 22 | end 23 | 24 | function modify_counter(table) 25 | --print("IN LUA SCRIPT FOR MODIFYING COUNTER VALUES!!!!!!!!!") 26 | -- return does not like local modifier to counter_data. Who knew?? 27 | counter_data = {} 28 | for k,v in pairs(table) do 29 | counter_data[k] = v+10 30 | end 31 | return counter_data 32 | end 33 | -------------------------------------------------------------------------------- /dnp3generator/extras/ExampleConfigs/LuaConfigs/RTDS.lua: -------------------------------------------------------------------------------- 1 | mappingTable = {} 2 | colNameTable = {} 3 | dataFilePos = "set" 4 | dataLine = "" 5 | 6 | steadyFile = "S1_Steady_State.csv" 7 | tocFile = "S2_Overcurrent_Delay_Fault4.csv" 8 | instFile = "S3_Overcurrent_Instant_Fault6.csv" 9 | s4ovFile = "S4_Overvoltage_Tripping.csv" 10 | s5ovFile = "S5_Overvoltage_Warning.csv" 11 | s6uvFile = "S6_Undervoltage_Warning.csv" 12 | s7uvFile = "S7_Undervoltage_Tripping.csv" 13 | 14 | finalFile = "Nonexistent_File.csv" 15 | 16 | mappingFileName = "DNP point list_0808.txt" 17 | dataFileName = steadyFile 18 | fileIndex = 0 19 | 20 | function string.starts(String,Start) 21 | return string.sub(String,1,string.len(Start))==Start 22 | end 23 | 24 | function CreateMappingTable() 25 | io.input(mappingFileName) 26 | for line in io.lines() do 27 | if string.starts(line, "#") ~= true then 28 | sep = ":" 29 | for dType, ptIndex, colName, deadband in string.gmatch(line, "(%a+)%s*:%s*(%d+)%s*([%w_]+)%s*(%g+)") do 30 | --print(colName, dType, ptIndex+1) 31 | mappingTable[colName] = {dType, ptIndex+1} --lua is index 1 based. So index 0 will cause problems. 32 | end 33 | end 34 | end 35 | end 36 | 37 | function ReadHeaderFromDataFile() 38 | local fHandle = io.open(dataFileName) 39 | local nCol = 1 40 | local sep = "," 41 | for headerLine in fHandle:lines() do 42 | --print(headerLine) 43 | for d in string.gmatch(headerLine,"([^"..sep.."]+)") do 44 | colNameTable[nCol] = d 45 | nCol = nCol + 1 46 | end 47 | dataFilePos = fHandle:seek() 48 | --print(dataFilePos) 49 | fHandle:close() 50 | return 51 | end 52 | end 53 | 54 | function ReadLineFromDataFile() 55 | local fHandle = io.input(dataFileName) 56 | fHandle:seek("set", dataFilePos) 57 | for dline in fHandle:lines() do 58 | dataFilePos = fHandle:seek() 59 | fHandle.close() 60 | return dline 61 | end 62 | end 63 | 64 | function SingleChangeDataFile() 65 | dataFileName = finalFile 66 | ReadHeaderFromDataFile() 67 | dataLine = ReadLineFromDataFile() 68 | return true 69 | end 70 | 71 | function ChangeDataFile() 72 | fileIndex = fileIndex +1 73 | if fileIndex < 2 then 74 | dataFileName = steadyFile 75 | elseif fileIndex == 2 then 76 | dataFileName = tocFile 77 | 78 | elseif fileIndex > 2 and fileIndex < 5 then 79 | dataFileName = steadyFile 80 | elseif fileIndex == 5 then 81 | dataFileName = instFile 82 | 83 | elseif fileIndex > 5 and fileIndex < 8 then 84 | dataFileName = steadyFile 85 | elseif fileIndex == 8 then 86 | dataFileName = s6uvFile 87 | 88 | elseif fileIndex > 8 and fileIndex < 11 then 89 | dataFileName = steadyFile 90 | elseif fileIndex == 11 then 91 | dataFileName = s7uvFile 92 | 93 | elseif fileIndex > 11 and fileIndex < 14 then 94 | dataFileName = steadyFile 95 | elseif fileIndex == 14 then 96 | dataFileName = s4ovFile 97 | 98 | elseif fileIndex > 14 and fileIndex < 17 then 99 | dataFileName = steadyFile 100 | elseif fileIndex == 17 then 101 | dataFileName = s5ovFile 102 | 103 | elseif fileIndex > 17 and fileIndex < 19 then 104 | dataFileName = steadyFile 105 | elseif fileIndex == 19 then 106 | dataFileName = finalFile 107 | else 108 | return false 109 | end 110 | --print("LUA: DATAFILE IS NOW", dataFileName) 111 | ReadHeaderFromDataFile() 112 | dataLine = ReadLineFromDataFile() 113 | return true 114 | end 115 | 116 | function generate_data() 117 | data = {} 118 | data["Analog Input"]={} 119 | data["Counter"]={} 120 | data["Binary Input"]={} 121 | dataLine = ReadLineFromDataFile() 122 | local sep = "," 123 | local nCol=1 124 | if dataLine == nil then 125 | if ChangeDataFile() == false then 126 | return data 127 | end 128 | end 129 | 130 | for d in string.gmatch(dataLine,"([^"..sep.."]+)") do 131 | colName = colNameTable[nCol] 132 | if nCol == 1 then 133 | data["Timestamp"] = d 134 | end 135 | if mappingTable[colName] then 136 | type = mappingTable[colName][1] 137 | index = mappingTable[colName][2] 138 | --print(type, index, d) 139 | if type == "AI" then 140 | data["Analog Input"][index] = d 141 | elseif type == "BI" then 142 | data["Binary Input"][index] = d 143 | elseif type == "CI" then 144 | data["Counter"][index] = d 145 | end 146 | end 147 | nCol = nCol + 1 148 | end 149 | return data 150 | end 151 | 152 | CreateMappingTable() 153 | ReadHeaderFromDataFile() 154 | -------------------------------------------------------------------------------- /dnp3generator/extras/ExampleConfigs/MappingOutstation/Mapping Outstation Config.json: -------------------------------------------------------------------------------- 1 | { 2 | "Nodes": 3 | [ 4 | {"Name":{"Master":"MM"}, 5 | "IP Address":{"Outstation":"172.16.136.132", "Master":"172.16.136.129"}, 6 | "DNP3 Address":{"Master":85, "Outstation":80}, 7 | "Allow Unsolicited": false, 8 | "Poll Interval": 9 | [ 10 | {"Event Class":"0123", "Frequency":30} 11 | ] 12 | }, 13 | { 14 | "Name":{"Outstation": "OutStationOne"}, 15 | "DNP3 Address":{"Outstation":80, "Master":85}, 16 | "IP Address":{"Outstation":"172.16.136.132"}, 17 | "Lua File":{"Master":["MS1_Multiplexer_Example_InjectValues.lua"]}, 18 | "Allow Unsolicited": false, 19 | "Data Sources": 20 | [ 21 | {"Source": "MasterofS1", 22 | "Mapped Data": 23 | [ 24 | {"Type":"Analog Input", "Event Class":2, "sVariation":5, "eVariation":7, "InputIndex":0, "Index":0, "Deadband":"1"}, 25 | {"Type":"Analog Input", "Event Class":2, "sVariation":5, "eVariation":7, "InputIndex":1, "Index":1, "Deadband":"1"}, 26 | {"Type":"Analog Input", "Event Class":2, "sVariation":5, "eVariation":7, "InputIndex":2, "Index":2, "Deadband":"1"}, 27 | {"Type":"Analog Input", "Event Class":2, "sVariation":5, "eVariation":7, "InputIndex":3, "Index":3, "Deadband":"1"}, 28 | {"Type":"Analog Input", "Event Class":2, "sVariation":5, "eVariation":7, "InputIndex":4, "Index":4, "Deadband":"1"}, 29 | {"Type":"Analog Input", "Event Class":2, "sVariation":5, "eVariation":7, "InputIndex":5, "Index":5, "Deadband":"1"}, 30 | {"Type":"Analog Input", "Event Class":2, "sVariation":5, "eVariation":7, "InputIndex":9, "Index":6, "Deadband":"1"}, 31 | {"Type":"Analog Input", "Event Class":2, "sVariation":5, "eVariation":7, "InputIndex":8, "Index":7, "Deadband":"1"}, 32 | {"Type":"Analog Input", "Event Class":2, "sVariation":5, "eVariation":7, "InputIndex":7, "Index":8, "Deadband":"1"}, 33 | {"Type":"Analog Input", "Event Class":2, "sVariation":5, "eVariation":7, "InputIndex":6, "Index":9, "Deadband":"1"}, 34 | {"Type":"Binary Input", "Event Class":1, "sVariation":1, "eVariation":1, "InputIndex":0, "Index":0}, 35 | {"Type":"Binary Input", "Event Class":1, "sVariation":1, "eVariation":1, "InputIndex":1, "Index":1}, 36 | {"Type":"Binary Input", "Event Class":1, "sVariation":1, "eVariation":1, "InputIndex":2, "Index":2}, 37 | {"Type":"Binary Input", "Event Class":1, "sVariation":1, "eVariation":1, "InputIndex":3, "Index":3}, 38 | {"Type":"Binary Input", "Event Class":1, "sVariation":1, "eVariation":1, "InputIndex":4, "Index":4}, 39 | {"Type":"Binary Input", "Event Class":1, "sVariation":1, "eVariation":1, "InputIndex":5, "Index":5}, 40 | {"Type":"Binary Input", "Event Class":1, "sVariation":1, "eVariation":1, "InputIndex":6, "Index":6}, 41 | {"Type":"Binary Input", "Event Class":1, "sVariation":1, "eVariation":1, "InputIndex":7, "Index":7}, 42 | {"Type":"Binary Input", "Event Class":1, "sVariation":1, "eVariation":1, "InputIndex":8, "Index":8}, 43 | {"Type":"Binary Input", "Event Class":1, "sVariation":1, "eVariation":1, "InputIndex":9, "Index":9} 44 | ] 45 | } 46 | ] 47 | }, 48 | { 49 | "Name":{"Master":"MasterofS1"}, 50 | "DNP3 Address":{"Master":90, "Outstation":95}, 51 | "IP Address":{"Master":"172.16.136.131", "Outstation":"172.16.136.130"}, 52 | "Bound Outstations" :["OutStationOne", "Example"], 53 | "Poll Interval": 54 | [ 55 | {"Event Class":"0123", "Frequency":20} 56 | ] 57 | }, 58 | { 59 | "Name":{"Outstation": "OutStationTwo"}, 60 | "DNP3 Address":{"Outstation":95, "Master":90}, 61 | "IP Address":{"Outstation":"172.16.136.130"}, 62 | "Lua Switch Trigger":{"Outstation":"k"}, 63 | "Lua File":{"Outstation":["BasicSample1.lua", "BasicSample2.lua"]}, 64 | "Allow Unsolicited": false, 65 | "Data": 66 | [ 67 | {"Type":"Analog Input", "Event Class":2, "sVariation":5, "eVariation":7, "Index":0, "Deadband":"1"}, 68 | {"Type":"Analog Input", "Event Class":2, "sVariation":5, "eVariation":7, "Index":1, "Deadband":"1"}, 69 | {"Type":"Analog Input", "Event Class":2, "sVariation":5, "eVariation":7, "Index":2, "Deadband":"1"}, 70 | {"Type":"Analog Input", "Event Class":2, "sVariation":5, "eVariation":7, "Index":3, "Deadband":"1"}, 71 | {"Type":"Analog Input", "Event Class":2, "sVariation":5, "eVariation":7, "Index":4, "Deadband":"1"}, 72 | {"Type":"Analog Input", "Event Class":2, "sVariation":5, "eVariation":7, "Index":5, "Deadband":"1"}, 73 | {"Type":"Analog Input", "Event Class":2, "sVariation":5, "eVariation":7, "Index":6, "Deadband":"1"}, 74 | {"Type":"Analog Input", "Event Class":2, "sVariation":5, "eVariation":7, "Index":7, "Deadband":"1"}, 75 | {"Type":"Analog Input", "Event Class":2, "sVariation":5, "eVariation":7, "Index":8, "Deadband":"1"}, 76 | {"Type":"Analog Input", "Event Class":2, "sVariation":5, "eVariation":7, "Index":9, "Deadband":"1"}, 77 | {"Type":"Binary Input", "Event Class":1, "sVariation":1, "eVariation":1, "Index":0}, 78 | {"Type":"Binary Input", "Event Class":1, "sVariation":1, "eVariation":1, "Index":1}, 79 | {"Type":"Binary Input", "Event Class":1, "sVariation":1, "eVariation":1, "Index":2}, 80 | {"Type":"Binary Input", "Event Class":1, "sVariation":1, "eVariation":1, "Index":3}, 81 | {"Type":"Binary Input", "Event Class":1, "sVariation":1, "eVariation":1, "Index":4}, 82 | {"Type":"Binary Input", "Event Class":1, "sVariation":1, "eVariation":1, "Index":5}, 83 | {"Type":"Binary Input", "Event Class":1, "sVariation":1, "eVariation":1, "Index":6}, 84 | {"Type":"Binary Input", "Event Class":1, "sVariation":1, "eVariation":1, "Index":7}, 85 | {"Type":"Binary Input", "Event Class":1, "sVariation":1, "eVariation":1, "Index":8}, 86 | {"Type":"Binary Input", "Event Class":1, "sVariation":1, "eVariation":1, "Index":9} 87 | ] 88 | } 89 | 90 | ] 91 | } 92 | -------------------------------------------------------------------------------- /dnp3generator/extras/ExampleConfigs/MappingOutstation/README: -------------------------------------------------------------------------------- 1 | ------------------------------------------------------------------------------- 2 | -Mapping Outstation Example 3 | ------------------------------------------------------------------------------- 4 | 5 | This example creates a mapped master/outstation configuration acting as a 6 | multiplexer to aggregate values into a single station. 7 | 8 | Example: 9 | 10 | Aggregated Values 11 | ------------------ 12 | |1:a | 13 | |2:b | 14 | |3:c | 15 | |4:d | 16 | |5:e | 17 | |6:f | 18 | ------------------ 19 | | | 20 | | | 21 | 22 | Station A Station B 23 | ------ ------ 24 | 1:a 4:d 25 | 2:b 5:e 26 | 3:c 6:f 27 | 28 | 29 | To run this example, compile dnp3Generator, and execute with "Config.json" and specified lua file 30 | which should be located in ../LuaConfigs 31 | 32 | Here OutStationTwo has two Lua files defined as: 33 | "Lua File":{"Outstation":["BasicSample1.lua", "BasicSample2.lua"]}, 34 | Which are switched using the trigger key "k" 35 | "Lua Switch Trigger":{"Outstation":"k"}, 36 | 37 | 38 | Aggregation and Mapping is performed by OutStationOne 39 | 40 | The "Data Source" refers to which outstation (Station A or Station B) we are currently referencing for the mapping. 41 | 42 | The"InputIndex" is defining the external index we are referencing and "Index" refers to how we are to aggregate it as seen above: 43 | Example: 44 | {"Type":"Analog Input", "Event Class":2, "sVariation":5, "eVariation":7, "InputIndex":0, "Index":0, "Deadband":"1"}, 45 | {"Type":"Binary Input", "Event Class":1, "sVariation":1, "eVariation":1, "InputIndex":0, "Index":0}, 46 | 47 | We further manipulate the values given to us by InputIndex using a lua script as defined by: 48 | "Lua File":{"Master":["MS1_Multiplexer_Example_InjectValues.lua"]}, 49 | 50 | Finally MasterStation MM Polls these aggregated values from OutStationOne every 30 seconds 51 | -------------------------------------------------------------------------------- /dnp3generator/extras/ExampleConfigs/Master/MasterConfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "Nodes": 3 | [ 4 | {"Name":{"Master":"ControlHost"}, 5 | "IP Address":{"Outstation":"192.168.1.4","Master":"192.168.1.90"}, 6 | "DNP3 Address":{"Master":7, "Outstation":6}, 7 | "IP Port":{"Master":20000}, 8 | "Allow Unsolicited": false, 9 | "Poll Interval": 10 | [ 11 | {"Event Class":1, "Frequency":5}, 12 | {"Event Class":2, "Frequency":7}, 13 | {"Event Class":0, "Frequency":20} 14 | ] 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /dnp3generator/extras/ExampleConfigs/Master/README: -------------------------------------------------------------------------------- 1 | ------------------------------------------------------------------------------- 2 | -Master Example 3 | ------------------------------------------------------------------------------- 4 | 5 | In this example configuration we create a simple Master station to query 6 | a real outstation device. 7 | -------------------------------------------------------------------------------- /dnp3generator/extras/ExampleConfigs/MasterOutstation/Config.json: -------------------------------------------------------------------------------- 1 | { 2 | "Nodes": 3 | [ 4 | { 5 | "Name":{"Master":"MasterStationOne"}, 6 | "DNP3 Address":{"Master":90, "Outstation":95}, 7 | "IP Address":{"Master":"172.16.136.131", "Outstation":"172.16.136.130"}, 8 | "Lua File":{"Master":["M1_Example_Smart_Reading.lua"]}, 9 | "Poll Interval": 10 | [ 11 | {"Event Class":"0123", "Frequency":20} 12 | ] 13 | }, 14 | { 15 | "Name":{"Outstation": "OutStationOne"}, 16 | "DNP3 Address":{"Outstation":95, "Master":90}, 17 | "IP Address":{"Outstation":"172.16.136.130"}, 18 | "Lua Switch Trigger":{"Outstation":"k"}, 19 | "Lua File":{"Outstation":["BasicSample1.lua", "BasicSample2.lua"]}, 20 | "Allow Unsolicited": false, 21 | "Data": 22 | [ 23 | {"Type":"Analog Input", "Event Class":2, "sVariation":5, "eVariation":7, "Index":0, "Deadband":1}, 24 | {"Type":"Analog Input", "Event Class":2, "sVariation":5, "eVariation":7, "Index":1, "Deadband":0.5}, 25 | {"Type":"Analog Input", "Event Class":2, "sVariation":5, "eVariation":7, "Index":2, "Deadband":0.75}, 26 | {"Type":"Analog Input", "Event Class":2, "sVariation":5, "eVariation":7, "Index":3, "Deadband":1}, 27 | {"Type":"Analog Input", "Event Class":2, "sVariation":5, "eVariation":7, "Index":4, "Deadband":1}, 28 | {"Type":"Analog Input", "Event Class":2, "sVariation":5, "eVariation":7, "Index":5, "Deadband":1}, 29 | {"Type":"Analog Input", "Event Class":2, "sVariation":5, "eVariation":7, "Index":6, "Deadband":1}, 30 | {"Type":"Analog Input", "Event Class":2, "sVariation":5, "eVariation":7, "Index":7, "Deadband":1}, 31 | {"Type":"Analog Input", "Event Class":2, "sVariation":5, "eVariation":7, "Index":8, "Deadband":1}, 32 | {"Type":"Analog Input", "Event Class":2, "sVariation":5, "eVariation":7, "Index":9, "Deadband":1}, 33 | {"Type":"Binary Input", "Event Class":1, "sVariation":1, "eVariation":1, "Index":0}, 34 | {"Type":"Binary Input", "Event Class":1, "sVariation":1, "eVariation":1, "Index":1}, 35 | {"Type":"Binary Input", "Event Class":1, "sVariation":1, "eVariation":1, "Index":2}, 36 | {"Type":"Binary Input", "Event Class":1, "sVariation":1, "eVariation":1, "Index":3}, 37 | {"Type":"Binary Input", "Event Class":1, "sVariation":1, "eVariation":1, "Index":4}, 38 | {"Type":"Binary Input", "Event Class":1, "sVariation":1, "eVariation":1, "Index":5}, 39 | {"Type":"Binary Input", "Event Class":1, "sVariation":1, "eVariation":1, "Index":6}, 40 | {"Type":"Binary Input", "Event Class":1, "sVariation":1, "eVariation":1, "Index":7}, 41 | {"Type":"Binary Input", "Event Class":1, "sVariation":1, "eVariation":1, "Index":8}, 42 | {"Type":"Binary Input", "Event Class":1, "sVariation":1, "eVariation":1, "Index":9} 43 | ] 44 | } 45 | 46 | ] 47 | } 48 | -------------------------------------------------------------------------------- /dnp3generator/extras/ExampleConfigs/MasterOutstation/README: -------------------------------------------------------------------------------- 1 | ------------------------------------------------------------------------------- 2 | -Master/Outstation Example 3 | ------------------------------------------------------------------------------- 4 | 5 | In this example we create a paired dnp3 Master and Outstation. 6 | The OutStation and Master are seperate Entities here compared to the BufferChange Example 7 | which are automatically generated. 8 | 9 | To run this example, compile dnp3Generator, and execute with "Config.json" and specified lua file(s) 10 | which should be located in ../LuaConfigs 11 | 12 | 13 | ---------------------------- 14 | Outstation Lua Configuration 15 | ---------------------------- 16 | The Outstation in this example has two lua files associated with it as an exercise: 17 | "Lua File":{"Outstation":["BasicSample1.lua", "BasicSample2.lua"]} 18 | The LUA files are located in ../LuaConfigs. 19 | 20 | We switch between the Lua Files using a designated trigger value defined 21 | in Config.json as "k" 22 | 23 | "Lua Switch Trigger":{"Outstation":"k"}, 24 | 25 | Once the dnp3Generator is started using this configuration, hitting "k" followed 26 | by return will show a confirmation the lua scripts have changed. 27 | 28 | ---------------------------- 29 | Master Station Lua Configuration (optional) 30 | ---------------------------- 31 | The Master Station in this example has a single lua file associated with it: 32 | "Lua File":{"Master":["M1_Example_Smart_Reading.lua"]}, 33 | 34 | The LUA file is located in ../LuaConfigs. 35 | A master station lua script is not required. 36 | 37 | Similar to the Outstation Example, we can also define multiple Lua Files for 38 | the Master Station to follow as well as define a trigger in the JSON file for switching 39 | Example: 40 | "Lua File":{"Master":["M1_Example_Smart_Reading.lua", "Another_Sample_File.lua"]}, 41 | "Lua Switch Trigger":{"Master":"l"}, 42 | 43 | Paired Outstation and Masters (such as the autogenerated example in BufferChange) can follow 44 | a similar option using 45 | 46 | "Lua File":{"Master":["M1_Example_Smart_Reading.lua", "Another_Sample_File.lua"], "Outstation":["ExampleFile1.lua","ExampleFile2.lua"]}, 47 | "Lua Switch Trigger":{"Master":"l","Outstation":"j"}, 48 | 49 | The switch trigger key can also be a string instead of a character. 50 | -------------------------------------------------------------------------------- /dnp3generator/extras/ExampleConfigs/Outstation/OutstationConfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "Nodes": 3 | [ 4 | { 5 | "Name":{"Outstation": "Outstation"}, 6 | "DNP3 Address":{"Master":7, "Outstation":6}, 7 | "IP Address":{"Outstation":"192.168.1.4"}, 8 | "Lua File":{"Outstation":["BasicSample1.lua"]}, 9 | "Allow Unsolicited": false, 10 | "Data": 11 | [ 12 | {"Type":"Binary Input", "Event Class":1, "sVariation":1, "eVariation":1, "Index List":"0-9"}, 13 | {"Type":"Analog Input", "Event Class":2, "sVariation":5, "eVariation":7, "Index List":"0-9"} 14 | ] 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /dnp3generator/extras/ExampleConfigs/Outstation/README: -------------------------------------------------------------------------------- 1 | ------------------------------------------------------------------------------- 2 | -Outstation Example 3 | ------------------------------------------------------------------------------- 4 | 5 | In this example configuration we create an outstation and tie it with a lua 6 | file to generate values. 7 | 8 | To run this example, compile dnp3Generator, and execute with "Config.json" and specified lua file 9 | which should be located in ../LuaConfigs 10 | -------------------------------------------------------------------------------- /dnp3generator/extras/ExampleConfigs/SimulatedPair/Config.json: -------------------------------------------------------------------------------- 1 | { 2 | "Virtual Interface": "ens38", 3 | "CIDR Notation":"127.0.0.1/24", 4 | "Nodes": 5 | [ 6 | { 7 | "Name":{"Master":"Master", "Outstation":"OutStation"}, 8 | "DNP3 Address":{"Master":95, "Outstation":20}, 9 | "Lua File":{"Outstation":["BasicSample1.lua"]}, 10 | "Allow Unsolicited": false, 11 | "Data": 12 | [ 13 | {"Type":"Analog Input", "Event Class":2, "sVariation":5, "eVariation":7, "Index List":"0-9", "Deadband":"1"}, 14 | {"Type":"Binary Input", "Event Class":1, "sVariation":1, "eVariation":1, "Index List":"0-9"} 15 | ], 16 | "Poll Interval": 17 | [ 18 | {"Event Class":"0123", "Frequency":3} 19 | ] 20 | } 21 | 22 | ] 23 | 24 | } 25 | -------------------------------------------------------------------------------- /dnp3generator/extras/ExampleConfigs/SimulatedPair/README: -------------------------------------------------------------------------------- 1 | ------------------------------------------------------------------------------- 2 | -Simulated Pair Example 3 | ------------------------------------------------------------------------------- 4 | 5 | In this example configuration we simulate a simple Master/Outstation. 6 | 7 | To run this example, compile dnp3Generator, and execute with "Config.json". The specified lua file 8 | is be located in ../LuaConfigs. make sure the path to the lua file is correct in the Config file. 9 | 10 | This example uses: 11 | "Lua File":{"Outstation":["BasicSample1.lua"]} 12 | The DNP3Generator will look for BasicSample1.lua (copy located in ../LuaConfigs) to generate values for OutStationOne 13 | 14 | To further expand this, a master station can also have a lua file 15 | The JSON would look similar to: 16 | "Lua File":{"Outstation":["BasicSample1.lua"], "Master":["MasterStationExample.lua"]} 17 | 18 | If needed multiple lua files can be switched for each Outstation and/or Master Instance using specified key triggers. 19 | The JSON to specify Multiple Lua Files looks like: 20 | "Lua File":{"Outstation":["BasicSample1.lua", "AnotherLuaFileExample.lua"]} 21 | 22 | Then a trigger would be specified in the JSON via: 23 | "Lua Switch Trigger":{"Outstation":"letter or string to switch Outstation lua file","Master":"letter or string to switch Master lua file"} 24 | 25 | The paired Outstation and Master can specify lua files for both 26 | "Lua File":{"Master":["M1_Example_Smart_Reading.lua", "Another_Sample_File.lua"], "Outstation":["ExampleFile1.lua","ExampleFile2.lua"]}, 27 | "Lua Switch Trigger":{"Master":"l","Outstation":"j"}, 28 | 29 | The switch trigger key can also be a string instead of a character. 30 | -------------------------------------------------------------------------------- /dnp3generator/extras/dockerenv/dockerfile32: -------------------------------------------------------------------------------- 1 | FROM ioft/i386-ubuntu:xenial 2 | ARG DEBIAN_FRONTEND=noninteractive 3 | RUN apt-get update 4 | RUN apt-get install -y apt-utils 5 | RUN apt-get install -y sudo 6 | RUN apt-get install -y net-tools 7 | RUN apt-get install -y git 8 | RUN apt-get install -y g++ 9 | RUN apt-get install -y cmake 10 | RUN apt-get install -y libboost-all-dev 11 | RUN apt-get install -y liblua5.2-dev 12 | RUN apt-get install -y tshark 13 | RUN apt-get install -y tcpdump 14 | RUN git clone --recursive https://github.com/automatak/dnp3.git 15 | WORKDIR /dnp3 16 | RUN git checkout 1a82d7b1d745412bd343f59033eec820f1c46201 . 17 | RUN cmake ../dnp3 18 | RUN sed -i 's/STATICLIBS:BOOL=OFF/STATICLIBS:BOOL=ON/' CMakeCache.txt 19 | RUN make 20 | RUN make install 21 | ARG DEBIAN_FRONTEND=teletype 22 | -------------------------------------------------------------------------------- /dnp3generator/extras/dockerenv/dockerfile64: -------------------------------------------------------------------------------- 1 | FROM ubuntu:xenial 2 | ARG DEBIAN_FRONTEND=noninteractive 3 | RUN apt-get update 4 | RUN apt-get install -y apt-utils 5 | RUN apt-get install -y sudo 6 | RUN apt-get install -y net-tools 7 | RUN apt-get install -y git 8 | RUN apt-get install -y g++ 9 | RUN apt-get install -y cmake 10 | RUN apt-get install -y libboost-all-dev 11 | RUN apt-get install -y liblua5.2-dev 12 | RUN apt-get install -y tshark 13 | RUN apt-get install -y tcpdump 14 | RUN git clone --recursive https://github.com/automatak/dnp3.git 15 | WORKDIR /dnp3 16 | RUN git checkout 1a82d7b1d745412bd343f59033eec820f1c46201 . 17 | RUN cmake ../dnp3 18 | RUN make 19 | RUN make install 20 | RUN ldconfig 21 | ARG DEBIAN_FRONTEND=teletype 22 | -------------------------------------------------------------------------------- /dnp3generator/tests/Makefile: -------------------------------------------------------------------------------- 1 | TARGET = dnp3GenTest 2 | OBJDIR := ../objs 3 | 4 | # ***IMPORTANT*** TEST FILES MUST HAVE SUFFIX *Test.cpp 5 | OBJS = $(addprefix $(OBJDIR)/,MasterTest.o $(CODE_OBJS)) 6 | 7 | include ../common.mk 8 | 9 | vpath % .. 10 | $(filter %Test.o,$(OBJS)): $(OBJDIR)/%Test.o: %Test.cpp 11 | $(CXX) $(CXXFLAGS) $(DEBUG_FLAGS) -c $< -o $@ 12 | 13 | $(filter-out %Test.o,$(OBJS)): $(OBJDIR)/%.o: %.cpp %.h 14 | $(CXX) $(CXXFLAGS) $(DEBUG_FLAGS) -c $< -o $@ 15 | -------------------------------------------------------------------------------- /dnp3generator/tests/MasterTest.cpp: -------------------------------------------------------------------------------- 1 | #define BOOST_TEST_MODULE boost_test_cidr 2 | #include 3 | #include "../CidrCalculator.h" 4 | //#include "MasterTest.h" 5 | BOOST_AUTO_TEST_CASE( test_cidr ) 6 | { 7 | //CIDR Range should go from 192.12.0.0 to 192.12.255.255 8 | //Although we only want safe address ranges -- exclude ending in .0 and 9 | //.255 or having .255 in any octet. 10 | std::string ipaddress; 11 | CidrCalculator cidr("192.12.0.0/16"); 12 | for(int BoundCheck = 1; BoundCheck < 65536; BoundCheck++) 13 | { 14 | ipaddress = cidr.GetNextIpAddress(); 15 | if(BoundCheck == 1) 16 | { 17 | BOOST_CHECK (ipaddress == "192.12.0.1"); 18 | } 19 | BOOST_CHECK (ipaddress != "192.12.0.0"); 20 | BOOST_CHECK (ipaddress != "192.12.0.255"); 21 | BOOST_CHECK (ipaddress != "192.12.255.0"); 22 | BOOST_CHECK (ipaddress != "192.12.254.0"); 23 | BOOST_CHECK (ipaddress != "192.12.255.255"); 24 | BOOST_CHECK (ipaddress != "192.13.0.0"); 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /dnp3generator/tests/RTDSTest/Configitest.json: -------------------------------------------------------------------------------- 1 | { 2 | "Virtual Interface": "eth1", 3 | "CIDR Notation":"192.168.90.1/24", 4 | "Nodes": 5 | [ 6 | {"Name":{"Master":"M1", "Outstation": "RTDSitest"}, 7 | "DNP3 Address":{"Master":95}, 8 | "Allow Unsolicited": false, 9 | "Data": 10 | [ 11 | {"Type":"Analog Input", "Event Class":2, "sVariation":5, "eVariation":7, "Index":0, "Deadband":"0.001"}, 12 | {"Type":"Analog Input", "Event Class":2, "sVariation":5, "eVariation":7, "Index":1, "Deadband":"0.001"}, 13 | {"Type":"Analog Input", "Event Class":2, "sVariation":5, "eVariation":7, "Index":2, "Deadband":"0.001"}, 14 | {"Type":"Analog Input", "Event Class":2, "sVariation":5, "eVariation":7, "Index":3, "Deadband":"0.001"}, 15 | {"Type":"Analog Input", "Event Class":2, "sVariation":5, "eVariation":7, "Index":4, "Deadband":"0.001"}, 16 | {"Type":"Analog Input", "Event Class":2, "sVariation":5, "eVariation":7, "Index":5, "Deadband":"0.001"}, 17 | {"Type":"Analog Input", "Event Class":2, "sVariation":5, "eVariation":7, "Index":6, "Deadband":"0.001"}, 18 | {"Type":"Analog Input", "Event Class":2, "sVariation":5, "eVariation":7, "Index":7, "Deadband":"0.001"}, 19 | {"Type":"Analog Input", "Event Class":2, "sVariation":5, "eVariation":7, "Index":8, "Deadband":"10000000"}, 20 | {"Type":"Analog Input", "Event Class":2, "sVariation":5, "eVariation":7, "Index":9, "Deadband":"10000000"}, 21 | {"Type":"Analog Input", "Event Class":2, "sVariation":5, "eVariation":7, "Index":10, "Deadband":"10000000"}, 22 | {"Type":"Analog Input", "Event Class":2, "sVariation":5, "eVariation":7, "Index":11, "Deadband":"10000000"}, 23 | {"Type":"Analog Input", "Event Class":2, "sVariation":5, "eVariation":7, "Index":12, "Deadband":"10000000"}, 24 | {"Type":"Analog Input", "Event Class":2, "sVariation":5, "eVariation":7, "Index":13, "Deadband":"10000000"}, 25 | {"Type":"Analog Input", "Event Class":2, "sVariation":5, "eVariation":7, "Index":14, "Deadband":"10000000"}, 26 | {"Type":"Analog Input", "Event Class":2, "sVariation":5, "eVariation":7, "Index":15, "Deadband":"10000000"}, 27 | {"Type":"Analog Input", "Event Class":2, "sVariation":5, "eVariation":7, "Index":16, "Deadband":"10000000"}, 28 | {"Type":"Analog Input", "Event Class":2, "sVariation":5, "eVariation":7, "Index":17, "Deadband":"10000000"}, 29 | {"Type":"Analog Input", "Event Class":2, "sVariation":5, "eVariation":7, "Index":18, "Deadband":"10000000"}, 30 | {"Type":"Analog Input", "Event Class":2, "sVariation":5, "eVariation":7, "Index":19, "Deadband":"10000000"}, 31 | {"Type":"Analog Input", "Event Class":2, "sVariation":5, "eVariation":7, "Index":20, "Deadband":"3.985"}, 32 | {"Type":"Analog Input", "Event Class":2, "sVariation":5, "eVariation":7, "Index":21, "Deadband":"0.75"}, 33 | {"Type":"Analog Input", "Event Class":2, "sVariation":5, "eVariation":7, "Index":22, "Deadband":"0.5"}, 34 | {"Type":"Analog Input", "Event Class":2, "sVariation":5, "eVariation":7, "Index":23, "Deadband":"0.5"}, 35 | {"Type":"Analog Input", "Event Class":2, "sVariation":5, "eVariation":7, "Index":24, "Deadband":"0.001"}, 36 | {"Type":"Analog Input", "Event Class":2, "sVariation":5, "eVariation":7, "Index":25, "Deadband":"0.001"}, 37 | {"Type":"Analog Input", "Event Class":2, "sVariation":5, "eVariation":7, "Index":26, "Deadband":"0.001"}, 38 | {"Type":"Analog Input", "Event Class":2, "sVariation":5, "eVariation":7, "Index":27, "Deadband":"0.001"}, 39 | {"Type":"Analog Input", "Event Class":2, "sVariation":5, "eVariation":7, "Index":28, "Deadband":"0.001"}, 40 | {"Type":"Analog Input", "Event Class":2, "sVariation":5, "eVariation":7, "Index":29, "Deadband":"0.001"}, 41 | {"Type":"Analog Input", "Event Class":2, "sVariation":5, "eVariation":7, "Index":30, "Deadband":"0.001"}, 42 | {"Type":"Analog Input", "Event Class":2, "sVariation":5, "eVariation":7, "Index":31, "Deadband":"0.001"}, 43 | {"Type":"Analog Input", "Event Class":2, "sVariation":5, "eVariation":7, "Index":32, "Deadband":"0.001"}, 44 | {"Type":"Analog Input", "Event Class":2, "sVariation":5, "eVariation":7, "Index":33, "Deadband":"0.001"}, 45 | {"Type":"Analog Input", "Event Class":2, "sVariation":5, "eVariation":7, "Index":34, "Deadband":"10000000"}, 46 | {"Type":"Analog Input", "Event Class":2, "sVariation":5, "eVariation":7, "Index":35, "Deadband":"10000000"}, 47 | {"Type":"Analog Input", "Event Class":2, "sVariation":5, "eVariation":7, "Index":36, "Deadband":"10000000"}, 48 | {"Type":"Analog Input", "Event Class":2, "sVariation":5, "eVariation":7, "Index":37, "Deadband":"10000000"}, 49 | {"Type":"Analog Input", "Event Class":2, "sVariation":5, "eVariation":7, "Index":38, "Deadband":"10000000"}, 50 | {"Type":"Analog Input", "Event Class":2, "sVariation":5, "eVariation":7, "Index":39, "Deadband":"10000000"}, 51 | {"Type":"Analog Input", "Event Class":2, "sVariation":5, "eVariation":7, "Index":40, "Deadband":"10000000"}, 52 | {"Type":"Analog Input", "Event Class":2, "sVariation":5, "eVariation":7, "Index":41, "Deadband":"10000000"}, 53 | {"Type":"Analog Input", "Event Class":2, "sVariation":5, "eVariation":7, "Index":42, "Deadband":"10000000"}, 54 | {"Type":"Analog Input", "Event Class":2, "sVariation":5, "eVariation":7, "Index":43, "Deadband":"10000000"}, 55 | {"Type":"Analog Input", "Event Class":2, "sVariation":5, "eVariation":7, "Index":44, "Deadband":"10000000"}, 56 | {"Type":"Analog Input", "Event Class":2, "sVariation":5, "eVariation":7, "Index":45, "Deadband":"10000000"}, 57 | {"Type":"Analog Input", "Event Class":2, "sVariation":5, "eVariation":7, "Index":46, "Deadband":"0.75"}, 58 | {"Type":"Analog Input", "Event Class":2, "sVariation":5, "eVariation":7, "Index":47, "Deadband":"0.75"}, 59 | {"Type":"Analog Input", "Event Class":2, "sVariation":5, "eVariation":7, "Index":48, "Deadband":"0.5"}, 60 | {"Type":"Analog Input", "Event Class":2, "sVariation":5, "eVariation":7, "Index":49, "Deadband":"0.5"}, 61 | {"Type":"Analog Input", "Event Class":2, "sVariation":5, "eVariation":7, "Index":50, "Deadband":"0.001"}, 62 | {"Type":"Analog Input", "Event Class":2, "sVariation":5, "eVariation":7, "Index":51, "Deadband":"0.001"}, 63 | {"Type":"Binary Input", "Event Class":1, "sVariation":1, "eVariation":1, "Index List":"0-25"} 64 | ], 65 | "Poll Interval": 66 | [ 67 | {"Event Class":"0123", "Frequency":5}, 68 | {"Event Class":2, "Frequency":2}, 69 | {"Event Class":1, "Frequency":1} 70 | 71 | ] 72 | } 73 | ] 74 | } 75 | -------------------------------------------------------------------------------- /dnp3generator/tests/RTDSTest/DNP3PointListTest.txt: -------------------------------------------------------------------------------- 1 | # 1-Rely90; 2-Relay91; 3-Relay 92; 4-Relay 93 2 | # Unit:Current-kA, Voltage-kV, P-MW, Q-MVar, S-MVA 3 | # BI: Binary Input Objects 1,2 in DNP Specification (Outputs from RTDS) 4 | BI: 0 Br1 0 5 | BI: 1 Br2 0 6 | BI: 2 Br3 0 7 | BI: 3 Br4 0 8 | BI: 4 BrINST1 0 9 | BI: 5 BrINST2 0 10 | BI: 6 BrINST3 0 11 | BI: 7 BrINST4 0 12 | BI: 8 CB12 0 13 | BI: 9 CB34 0 14 | BI: 10 CB45 0 15 | BI: 11 CB47 0 16 | BI: 12 OV_Warn92 0 17 | BI: 13 OV_Warn93 0 18 | BI: 14 Pickup_hv92 0 19 | BI: 15 Pickup_hv93 0 20 | BI: 16 Pickup_lv92 0 21 | BI: 17 Pickup_lv93 0 22 | BI: 18 Start1 0 23 | BI: 19 Start2 0 24 | BI: 20 Start3 0 25 | BI: 21 Start4 0 26 | BI: 22 Trip_volt92 0 27 | BI: 23 Trip_volt93 0 28 | BI: 24 UV_Warn92 0 29 | BI: 25 UV_Warn93 0 30 | ################################################################# 31 | # AI: Analog Input Objects 301,302 in DNP Specification (Outputs from RTDS) 32 | AI: 0 FaultFreq1 0.001 33 | AI: 1 FaultFreq2 0.001 34 | AI: 2 FaultFreqGrad1 5 35 | AI: 3 FaultFreqGrad2 5 36 | AI: 4 FaultIa1 0.001 37 | AI: 5 FaultIa2 0.001 38 | AI: 6 FaultVa1 0.001 39 | AI: 7 FaultVa2 0.001 40 | AI: 8 MVIa1 10000000 41 | AI: 9 MVIa2 10000000 42 | AI: 10 MVIaAmplitude1 10000000 43 | AI: 11 MVIaAmplitude2 10000000 44 | AI: 12 MVPF1 10000000 45 | AI: 13 MVPF2 10000000 46 | AI: 14 MVPa1 10000000 47 | AI: 15 MVPa2 10000000 48 | AI: 16 MVQa1 10000000 49 | AI: 17 MVQa2 10000000 50 | AI: 18 MVSa1 10000000 51 | AI: 19 MVSa2 10000000 52 | AI: 20 MVVa1 3.985 53 | AI: 21 MVVa2 0.75 54 | AI: 22 MVf1 0.5 55 | AI: 23 MVf2 0.5 56 | AI: 24 MaxFaultIa1 0.001 57 | AI: 25 MaxFaultIa2 0.001 58 | AI: 26 FaultFreq3 0.001 59 | AI: 27 FaultFreq4 0.001 60 | AI: 28 FaultFreqGrad3 5 61 | AI: 29 FaultFreqGrad4 5 62 | AI: 30 FaultIa3 0.001 63 | AI: 31 FaultIa4 0.001 64 | AI: 32 FaultVa3 0.001 65 | AI: 33 FaultVa4 0.001 66 | AI: 34 MVIa3 10000000 67 | AI: 35 MVIa4 10000000 68 | AI: 36 MVIaAmplitude3 10000000 69 | AI: 37 MVIaAmplitude4 10000000 70 | AI: 38 MVPF3 10000000 71 | AI: 39 MVPF4 10000000 72 | AI: 40 MVPa3 10000000 73 | AI: 41 MVPa4 10000000 74 | AI: 42 MVQa3 10000000 75 | AI: 43 MVQa4 10000000 76 | AI: 44 MVSa3 10000000 77 | AI: 45 MVSa4 10000000 78 | AI: 46 MVVa3 0.75 79 | AI: 47 MVVa4 0.75 80 | AI: 48 MVf3 0.5 81 | AI: 49 MVf4 0.5 82 | AI: 50 MaxFaultIa3 0.001 83 | AI: 51 MaxFaultIa4 0.001 84 | -------------------------------------------------------------------------------- /dnp3generator/tests/RTDSTest/RTDSTEST.bash: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ##Trap SIGINT and exit 4 | ctrlc() 5 | { 6 | exit $? 7 | } 8 | 9 | trap ctrlc SIGINT 10 | netset() 11 | { 12 | ip link add eth1 type dummy 13 | ip addr add 192.168.90.1/24 dev eth1 14 | ip link set eth1 up 15 | } 16 | 17 | ##Run DNP3 Generator and Capture Traffic for different scenarios 18 | pkttest() 19 | { 20 | echo "Generating Traffic" 21 | tcpdump -B 4096 -n -i lo -w steadyfile.pcap& 22 | sed -i '16s/.*/dataFileName = steadyFile/' RTDSitest.lua 23 | ./dnp3Generator -c Configitest.json > /dev/null 2>&1 24 | pkill tcpdump 25 | rm ./dnp3pipe 26 | 27 | 28 | tcpdump -B 4096 -n -i lo -w tocfile.pcap& 29 | sed -i '16s/.*/dataFileName = tocFile/' RTDSitest.lua 30 | ./dnp3Generator -c Configitest.json > /dev/null 2>&1 31 | pkill tcpdump 32 | rm ./dnp3pipe 33 | 34 | tcpdump -B 4096 -n -i lo -w instfile.pcap& 35 | sed -i '16s/.*/dataFileName = instFile/' RTDSitest.lua 36 | ./dnp3Generator -c Configitest.json > /dev/null 2>&1 37 | pkill tcpdump 38 | rm ./dnp3pipe 39 | 40 | tcpdump -B 4096 -n -i lo -w s4ovfile.pcap& 41 | sed -i '16s/.*/dataFileName = s4ovFile/' RTDSitest.lua 42 | ./dnp3Generator -c Configitest.json > /dev/null 2>&1 43 | pkill tcpdump 44 | rm ./dnp3pipe 45 | 46 | tcpdump -B 4096 -n -i lo -w s5ovfile.pcap& 47 | sed -i '16s/.*/dataFileName = s5ovFile/' RTDSitest.lua 48 | ./dnp3Generator -c Configitest.json > /dev/null 2>&1 49 | pkill tcpdump 50 | rm ./dnp3pipe 51 | 52 | tcpdump -B 4096 -n -i lo -w s6uvfile.pcap& 53 | sed -i '16s/.*/dataFileName = s6uvFile/' RTDSitest.lua 54 | ./dnp3Generator -c Configitest.json > /dev/null 2>&1 55 | pkill tcpdump 56 | rm ./dnp3pipe 57 | 58 | tcpdump -B 4096 -n -i lo -w s7uvfile.pcap& 59 | sed -i '16s/.*/dataFileName = s7uvFile/' RTDSitest.lua 60 | ./dnp3Generator -c Configitest.json > /dev/null 2>&1 61 | pkill tcpdump 62 | rm ./dnp3pipe 63 | 64 | return $? 65 | } 66 | 67 | ##Analyze pcap output with tshark 68 | pktcheck() 69 | { 70 | echo "ANALYZING PCAPS" 71 | sleep 5 72 | date > TEST_RESULTS.txt 73 | echo "########################################">> TEST_RESULTS.txt 74 | echo "Changes to Index 16" >> TEST_RESULTS.txt 75 | echo "########################################">> TEST_RESULTS.txt 76 | echo "----------------------------------------" >> TEST_RESULTS.txt 77 | steadycnt=$(tshark -r steadyfile.pcap "dnp3.al.point_index == 16" | tee >(wc -l) >> TEST_RESULTS.txt) 78 | echo "Total Events in Steady File: $steadycnt" >> TEST_RESULTS.txt 79 | echo "----------------------------------------" >> TEST_RESULTS.txt 80 | toccnt=$(tshark -r tocfile.pcap "dnp3.al.point_index == 16" | tee >(wc -l) >> TEST_RESULTS.txt) 81 | echo "Total Events in TOC File: $toccnt" >> TEST_RESULTS.txt 82 | echo "----------------------------------------" >> TEST_RESULTS.txt 83 | instcnt=$(tshark -r instfile.pcap "dnp3.al.point_index == 16" | tee >(wc -l) >> TEST_RESULTS.txt) 84 | echo "Total Events in Inst File: $instcnt" >> TEST_RESULTS.txt 85 | echo "----------------------------------------" >> TEST_RESULTS.txt 86 | s4ovcnt=$(tshark -r s4ovfile.pcap "dnp3.al.point_index == 16" | tee >(wc -l) >> TEST_RESULTS.txt) 87 | echo "Total Events in S4ov File: $s4ovcnt" >> TEST_RESULTS.txt 88 | echo "----------------------------------------" >> TEST_RESULTS.txt 89 | s5ovcnt=$(tshark -r s5ovfile.pcap "dnp3.al.point_index == 16" | tee >(wc -l) >> TEST_RESULTS.txt) 90 | echo "Total Events in S5ov File: $s5ovcnt" >> TEST_RESULTS.txt 91 | echo "----------------------------------------" >> TEST_RESULTS.txt 92 | s6uvcnt=$(tshark -r s6uvfile.pcap "dnp3.al.point_index == 16" | tee >(wc -l) >> TEST_RESULTS.txt) 93 | echo "Total Events in S6uv File: $s6uvcnt" >> TEST_RESULTS.txt 94 | echo "----------------------------------------" >> TEST_RESULTS.txt 95 | s7uvcnt=$(tshark -r s7uvfile.pcap "dnp3.al.point_index == 16" | tee >(wc -l) >> TEST_RESULTS.txt) 96 | echo "Total Events in S7uv File: $s7uvcnt" >> TEST_RESULTS.txt 97 | echo "----------------------------------------" >> TEST_RESULTS.txt 98 | echo "----------------------------------------" >> TEST_RESULTS.txt 99 | totalcnt=$(($steadycnt+$toccnt+$instcnt+$s4ovcnt+$s5ovcnt+$s6uvcnt+$s7uvcnt)) 100 | echo "Total Events: $totalcnt" >> TEST_RESULTS.txt 101 | echo "----------------------------------------" >> TEST_RESULTS.txt 102 | 103 | 104 | ##cat TEST_RESULTS.txt 105 | 106 | if [ $totalcnt -gt 250 ] 107 | then 108 | echo "Total Event Changes for Index 16 do not match. $totalcnt/232" 109 | exit 1 110 | fi 111 | 112 | 113 | echo "########################################">> TEST_RESULTS.txt 114 | echo "Total Buffer OverFlows" >> TEST_RESULTS.txt 115 | echo "########################################">> TEST_RESULTS.txt 116 | echo "----Steady State---------------------------------------------------" >> TEST_RESULTS.txt 117 | tshark -r steadyfile.pcap "dnp3.al.iin.ebo == 1" >> TEST_RESULTS.txt 118 | echo "----Over Current Fault 4-------------------------------------------" >> TEST_RESULTS.txt 119 | tshark -r tocfile.pcap "dnp3.al.iin.ebo == 1" >> TEST_RESULTS.txt 120 | echo "----Overcurrent Instant Fault 6------------------------------------" >> TEST_RESULTS.txt 121 | tshark -r instfile.pcap "dnp3.al.iin.ebo == 1" >> TEST_RESULTS.txt 122 | echo "----Overvoltage Tripping-------------------------------------------" >> TEST_RESULTS.txt 123 | tshark -r s4ovfile.pcap "dnp3.al.iin.ebo == 1" >> TEST_RESULTS.txt 124 | echo "----Overvoltage Warning--------------------------------------------" >> TEST_RESULTS.txt 125 | tshark -r s5ovfile.pcap "dnp3.al.iin.ebo == 1" >> TEST_RESULTS.txt 126 | echo "----Undervoltage Warning-------------------------------------------" >> TEST_RESULTS.txt 127 | tshark -r s6uvfile.pcap "dnp3.al.iin.ebo == 1" >> TEST_RESULTS.txt 128 | echo "----Undervoltage Tripping------------------------------------------" >> TEST_RESULTS.txt 129 | tshark -r s7uvfile.pcap "dnp3.al.iin.ebo == 1" >> TEST_RESULTS.txt 130 | echo "----------------------------------------" >> TEST_RESULTS.txt 131 | 132 | 133 | echo "########################################">> TEST_RESULTS.txt 134 | echo "Malformed Packets" >> TEST_RESULTS.txt 135 | echo "########################################">> TEST_RESULTS.txt 136 | echo "----Steady State---------------------------------------------------" >> TEST_RESULTS.txt 137 | tshark -r steadyfile.pcap "_ws.malformed" >> TEST_RESULTS.txt 138 | echo "----Over Current Fault 4-------------------------------------------" >> TEST_RESULTS.txt 139 | tshark -r tocfile.pcap "_ws.malformed" >> TEST_RESULTS.txt 140 | echo "----Overcurrent Instant Fault 6------------------------------------" >> TEST_RESULTS.txt 141 | tshark -r instfile.pcap "_ws.malformed" >> TEST_RESULTS.txt 142 | echo "----Overvoltage Tripping-------------------------------------------" >> TEST_RESULTS.txt 143 | tshark -r s4ovfile.pcap "_ws.malformed" >> TEST_RESULTS.txt 144 | echo "----Overvoltage Warning--------------------------------------------" >> TEST_RESULTS.txt 145 | tshark -r s5ovfile.pcap "_ws.malformed" >> TEST_RESULTS.txt 146 | echo "----Undervoltage Warning-------------------------------------------" >> TEST_RESULTS.txt 147 | tshark -r s6uvfile.pcap "_ws.malformed" >> TEST_RESULTS.txt 148 | echo "----Undervoltage Tripping------------------------------------------" >> TEST_RESULTS.txt 149 | tshark -r s7uvfile.pcap "_ws.malformed" >> TEST_RESULTS.txt 150 | echo "----------------------------------------" >> TEST_RESULTS.txt 151 | echo "EOF">> TEST_RESULTS.txt 152 | 153 | cat TEST_RESULTS.txt 154 | return $? 155 | } 156 | netset 157 | pkttest 158 | pktcheck 159 | -------------------------------------------------------------------------------- /dnp3generator/tests/RTDSTest/RTDSitest.lua: -------------------------------------------------------------------------------- 1 | mappingTable = {} 2 | colNameTable = {} 3 | dataFilePos = "set" 4 | dataLine = "" 5 | 6 | steadyFile = "S1_Steady_State.csv" 7 | tocFile = "S2_Overcurrent_Delay_Fault4.csv" 8 | instFile = "S3_Overcurrent_Instant_Fault6.csv" 9 | s4ovFile = "S4_Overvoltage_Tripping.csv" 10 | s5ovFile = "S5_Overvoltage_Warning.csv" 11 | s6uvFile = "S6_Undervoltage_Warning.csv" 12 | s7uvFile = "S7_Undervoltage_Tripping.csv" 13 | finalFile = "Non_Existing.csv" 14 | 15 | mappingFileName = "DNP3PointListTest.txt" 16 | dataFileName = s7uvFile 17 | fileIndex = 0 18 | 19 | function string.starts(String,Start) 20 | return string.sub(String,1,string.len(Start))==Start 21 | end 22 | 23 | function CreateMappingTable() 24 | io.input(mappingFileName) 25 | for line in io.lines() do 26 | if string.starts(line, "#") ~= true then 27 | sep = ":" 28 | for dType, ptIndex, colName, deadband in string.gmatch(line, "(%a+)%s*:%s*(%d+)%s*([%w_]+)%s*(%g+)") do 29 | --print(colName, dType, ptIndex+1) 30 | mappingTable[colName] = {dType, ptIndex+1} --lua is index 1 based. So index 0 will cause problems. 31 | end 32 | end 33 | end 34 | end 35 | 36 | function ReadHeaderFromDataFile() 37 | local fHandle = io.open(dataFileName) 38 | local nCol = 1 39 | local sep = "," 40 | for headerLine in fHandle:lines() do 41 | --print(headerLine) 42 | for d in string.gmatch(headerLine,"([^"..sep.."]+)") do 43 | colNameTable[nCol] = d 44 | nCol = nCol + 1 45 | end 46 | dataFilePos = fHandle:seek() 47 | --print(dataFilePos) 48 | fHandle:close() 49 | return 50 | end 51 | end 52 | 53 | function ReadLineFromDataFile() 54 | local fHandle = io.input(dataFileName) 55 | fHandle:seek("set", dataFilePos) 56 | for dline in fHandle:lines() do 57 | dataFilePos = fHandle:seek() 58 | fHandle.close() 59 | return dline 60 | end 61 | end 62 | 63 | function SingleChangeDataFile() 64 | fileIndex = fileIndex +1 65 | if fileIndex < 2 then 66 | dataFileName = finalFile 67 | else 68 | return false 69 | end 70 | ReadHeaderFromDataFile() 71 | dataFileName = steadyFile 72 | return true 73 | end 74 | 75 | function generate_data() 76 | data = {} 77 | data["Analog Input"]={} 78 | data["Counter"]={} 79 | data["Binary Input"]={} 80 | dataLine = ReadLineFromDataFile() 81 | local sep = "," 82 | local nCol=1 83 | print(dataFileName) 84 | if dataLine == nil then 85 | if SingleChangeDataFile() == false then 86 | return data 87 | end 88 | end 89 | 90 | for d in string.gmatch(dataLine,"([^"..sep.."]+)") do 91 | colName = colNameTable[nCol] 92 | if nCol == 1 then 93 | data["Timestamp"] = d 94 | end 95 | if mappingTable[colName] then 96 | type = mappingTable[colName][1] 97 | index = mappingTable[colName][2] 98 | --print(type, index, d) 99 | if type == "AI" then 100 | data["Analog Input"][index] = d 101 | elseif type == "BI" then 102 | data["Binary Input"][index] = d 103 | elseif type == "CI" then 104 | data["Counter"][index] = d 105 | end 106 | end 107 | nCol = nCol + 1 108 | end 109 | return data 110 | end 111 | 112 | CreateMappingTable() 113 | ReadHeaderFromDataFile() 114 | -------------------------------------------------------------------------------- /modbusgenerator/.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__/* 2 | pymodbus/* 3 | dist/* 4 | build/* 5 | *.tar.gz 6 | *.spec 7 | -------------------------------------------------------------------------------- /modbusgenerator/LICENSE: -------------------------------------------------------------------------------- 1 | University of Illinois/NCSA Open Source License 2 | Copyright (c) 2015-2017 Information Trust Institute 3 | All rights reserved. 4 | 5 | Developed by: 6 | 7 | Information Trust Institute 8 | University of Illinois Urbana-Champaign 9 | http://www.iti.illinois.edu 10 | 11 | Permission is hereby granted, free of charge, to any person obtaining a copy of 12 | this software and associated documentation files (the "Software"), to deal with 13 | the Software without restriction, including without limitation the rights to 14 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 15 | of the Software, and to permit persons to whom the Software is furnished to do 16 | so, subject to the following conditions: 17 | 18 | Redistributions of source code must retain the above copyright notice, this list 19 | of conditions and the following disclaimers. Redistributions in binary form must 20 | reproduce the above copyright notice, this list of conditions and the following 21 | disclaimers in the documentation and/or other materials provided with the 22 | distribution. 23 | 24 | Neither the names of Information Trust Institute, University of Illinois, nor 25 | the names of its contributors may be used to endorse or promote products derived 26 | from this Software without specific prior written permission. 27 | 28 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 29 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 30 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE CONTRIBUTORS 31 | OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 32 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 33 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS WITH THE SOFTWARE. 34 | -------------------------------------------------------------------------------- /modbusgenerator/README.md: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # General Setup 3 | ############################################################################### 4 | # Install python3 and python dev files 5 | sudo apt-get install python3 python3-dev 6 | 7 | # Install pip3 and python3-virtualenv 8 | sudo apt-get install python3-pip python3-virtualenv 9 | 10 | # Create Virtual Environment 11 | virtualenv -p /usr/bin/python3 venv 12 | 13 | # Activate Virtual Environment 14 | source ./venv/bin/activate 15 | 16 | # Install package requirements 17 | pip install -r requirements.txt --user 18 | 19 | # Download, Checkout, and Install Pymodbus 20 | git clone https://github.com/bashwork/pymodbus 21 | git checkout --track origin/python3 22 | python setup.py install 23 | 24 | # Run Program 25 | python modbusgen.py 26 | 27 | # Exit Virtual Environment 28 | deactivate 29 | -------------------------------------------------------------------------------- /modbusgenerator/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ITI/ics-trafficgen/af2445f21817285382852fd33e7178785f8e63d9/modbusgenerator/__init__.py -------------------------------------------------------------------------------- /modbusgenerator/cfg_json_parser.py: -------------------------------------------------------------------------------- 1 | import json 2 | import netifaces 3 | import logging 4 | from cidr_calculator import cidr_calculator 5 | from node import node 6 | 7 | class json_parser: 8 | def __init__(self, filename, test=False): 9 | self.logger = logging.getLogger('json_parser') 10 | if test: 11 | self.json_data = json.loads(filename) 12 | else: 13 | with open(filename) as json_file: 14 | self.json_data = json.load(json_file) 15 | self.cidr = self.json_data.get("CIDR Notation") and cidr_calculator(self.json_data.get("CIDR Notation")) 16 | if self.json_data.get("Virtual Interface") and self.json_data["Virtual Interface"] not in netifaces.interfaces(): 17 | self.logger.error("Network interface %s not specified or invalid", self.json_data.get("Virtual Interface")) 18 | exit() 19 | 20 | self.process_nodes() 21 | 22 | def process_nodes(self): 23 | master_nodes = [] # list of unique master instances that will be created by the app 24 | outstation_nodes = [] # list of unique outstation instances that will be created by the app 25 | outstation_cidr_addrs = {} #outstation name (key) to cidr generated ip addresses(value). Not a complete list of outstations! 26 | outstation_set = set() #all unique outstation names connected to master nodes. Will be used to get details from cfg file 27 | ipaddr_outstation_map={} #ipaddress(key) to list of outstation names(value). Used so that we gather those outstations cfg info into one outstation instance for the app. 28 | 29 | #first we look at all the Master nodes and the outstations they are connected to. 30 | for connected_nodes in self.json_data["Master Nodes"]: 31 | try: 32 | master_name = connected_nodes["Name"] #must have name, else drop this item 33 | master_ip = connected_nodes.get("IP Address", self.cidr.get_addr()) 34 | except: 35 | continue 36 | master_node = node(master_name, role = "Master", ipv4addr=master_ip) 37 | if not connected_nodes.get("IP Address"): 38 | master_node.allocate(self.json_data["Virtual Interface"]) 39 | outstations = connected_nodes.get("Outstations", ()) 40 | #Some checks- make sure all outstations this master is connected to are defined in configuration 41 | if any(name not in self.json_data for name in outstations): 42 | self.logger.error("Atleast one of the outstations is unspecified in the config file:%s" %outstations) 43 | exit() 44 | #Some more checks- if any ip address is specified for one outstation, it must be specified the same for them all 45 | # at this point, we dont have the ability to manually assign them to the same ip address. It is left for the user. 46 | # we do assign the same cidr address for outstations with unspecified addresses. 47 | if any([self.json_data[name].get("IP Address") for name in outstations]): # if any IP address is specified in the configuration for the outstations 48 | if all([self.json_data[name].get("IP Address") == self.json_data[outstations[0]].get("IP Address") for name in outstations]): #then all have to be the same 49 | master_node.outstation_addr = self.json_data[outstations[0]].get("IP Address") 50 | else: #else need to specify configuration differently. 51 | self.logger.error("Cannot have a master %s connect to different IP addresses. Check your configuration." %master_name) 52 | exit() 53 | else: #no ip address specified, see if we can assign a vnic address without conflict 54 | assigned_ots = outstation_set & set(outstations) #intersection set of already processed outstations and current list of outstations 55 | already_assigned_addresses = set(outstation_cidr_addrs[name] for name in assigned_ots) #set of outstations, already assigned vnic addresses 56 | if len(already_assigned_addresses) > 1: #different vnics assigned, need to exit 57 | self.logger.error("Too many unique vnics assigned, check configuration %s" %already_assigned_addresses) 58 | exit() 59 | elif len(already_assigned_addresses) == 1: #get the assigned vnic 60 | cidr_addr = outstation_cidr_addrs[assigned_ots.pop()] 61 | else: 62 | cidr_addr = self.cidr.get_addr() #get a new vnic 63 | 64 | outstation_cidr_addrs.update(dict.fromkeys(outstations,cidr_addr)) 65 | master_node.outstation_addr=cidr_addr 66 | #read any other configuration details that master needs to know about 67 | master_node.poll_dict = connected_nodes.get("Poll Interval",{}) and {p["Data type"]:p["Frequency"] for p in connected_nodes.get("Poll Interval")} 68 | master_node.simulate = connected_nodes.get("Simulate", True) 69 | master_node.outstation_info = self.get_outstation_datastore(outstations) 70 | master_node.port = self.get_outstation_port(outstations) 71 | 72 | outstation_set.update(outstations) 73 | self.logger.debug("Adding %s to list of nodes" %master_name) 74 | master_nodes.append(master_node) 75 | 76 | #for all the outstation nodes that the masters are connected to, collect them by ip address 77 | for name in outstation_set: 78 | ipaddr = self.json_data[name].get("IP Address") or outstation_cidr_addrs[name] 79 | ipaddr_outstation_map[ipaddr] = [name] if ipaddr not in ipaddr_outstation_map else ipaddr_outstation_map[ipaddr]+[name] 80 | 81 | # We dont assume that outstations that "a" master are connected to are the only ones at that ip address 82 | # Thats why we run the most of the same checks as on outstations connected to a master 83 | #now we can create an outstation node for each ip address 84 | for ipaddr in ipaddr_outstation_map: 85 | outstations = ipaddr_outstation_map[ipaddr] 86 | n = node(outstations, "Outstation", ipv4addr=ipaddr) 87 | n.port = self.get_outstation_port(outstations) 88 | n.simulate = self.get_outstation_sim(outstations) 89 | n.datastore = self.get_outstation_datastore(outstations) 90 | n.name_id_map = self.get_outstation_unitids(outstations) 91 | if not any([self.json_data[name].get("IP Address") for name in outstations]): 92 | n.allocate(self.json_data["Virtual Interface"]) 93 | self.logger.debug("outstations %s has ip address:%s, port:%d"%(outstations, ipaddr, n.port)) 94 | self.logger.debug("Adding %s to list of nodes" %outstations) 95 | outstation_nodes.append(n) 96 | self.masters = master_nodes 97 | self.outstations = outstation_nodes 98 | 99 | def get_outstation_datastore(self, names): 100 | return {self.json_data[name].get("Unit ID", 0):self.json_data[name].get("Data Count", 10) for name in names} 101 | 102 | def get_outstation_unitids(self, names): 103 | name_unitid_map = {name:self.json_data[name].get("Unit ID", 0) for name in names} 104 | unit_ids = name_unitid_map.values() 105 | if len(unit_ids) != len(set(unit_ids)): 106 | self.logger.error("Have duplicated unit ids:%s in the outstations:%s" %(unit_ids, names)) 107 | exit() 108 | if int(max(unit_ids)) > 247: #maximum slave address http://www.modbus.org/docs/Modbus_over_serial_line_V1_02.pdf page 7 109 | self.logger.error("Cannot have unit id greater than 247. We have unit id:%d" %max(unit_ids)) 110 | exit() 111 | return name_unitid_map 112 | 113 | def get_outstation_port(self, names): 114 | ports = [self.json_data[name].get("Port") for name in names] 115 | if len(set(ports)) > 1: 116 | self.logger.error("Outstations %s have different ports in cfg file:%s" %(names, ports)) 117 | exit() 118 | return len(ports)==1 and ports[0] or 502 119 | 120 | def get_outstation_sim(self, names): 121 | simulate = [self.json_data[name].get("Simulate", True) for name in names] 122 | if len(set(simulate)) > 1: 123 | self.logger.error("Master cannot have a combination of real and simulated outstations %s at one IP address." %names) 124 | exit() 125 | return simulate[0] 126 | -------------------------------------------------------------------------------- /modbusgenerator/cidr_calculator.py: -------------------------------------------------------------------------------- 1 | import netaddr 2 | import logging 3 | 4 | class cidr_calculator: 5 | def __init__(self, cidr_notation): 6 | self.addr_generator = netaddr.IPNetwork(cidr_notation).iter_hosts() 7 | self.logger = logging.getLogger('CIDR Calculator') 8 | 9 | def get_addr(self): 10 | try: 11 | return str(next(self.addr_generator)) 12 | except StopIteration: 13 | self.logger.exception("Not able to generate any more addresses. Change your CIDR subnet mask") 14 | exit() 15 | -------------------------------------------------------------------------------- /modbusgenerator/config_spec.txt: -------------------------------------------------------------------------------- 1 | The Config file is in json format. The default config file is Config.json. You can run the application with your json file using the -c flag. 2 | The top level keys in the JSON file are 3 | 4 | "Virtual Interface" 5 | "CIDR Notation" 6 | "Master Nodes" 7 | "Outstation1" 8 | "Outstation2" 9 | ... 10 | 11 | The "Virtual Interface" and "CIDR Notation" keys are useful for development, testing and running a simulated modbus flow on a single VM. 12 | 13 | The "Master Nodes" is a list of nodes that represent masters. 14 | Each master node contains the keys 15 | "Name" 16 | "Outstations": A list of names of outstations that this master is connected to. 17 | "Poll Interval" 18 | 19 | "Poll Interval" contains a list of data types and the frequency (in seconds) by which the master will poll ALL the outstations connected to it. 20 | The "Data Type" can have values 21 | "Coils" 22 | "Discretes Input" 23 | "Input Registers" 24 | "Holding Registers" 25 | 26 | "Frequency": The number of seconds after which to poll all the outstations. 27 | 28 | The "Outstation1" node contains the keys 29 | "Data Count" 30 | "Unit ID" 31 | "Simulate" 32 | "IP Address" 33 | "Port" 34 | 35 | "Data Count" is the only required key. This allows us to set up the correctly sized data blocks. Currently we set up the same sizes blocks 36 | for coils, registers, holding registers, and discrete inputs. 37 | "IP Address" is used to specify the IP address at which this outstation is located. 38 | "Unit ID" is required if multiple outstations are located at the same IP address. 39 | "Simulate" has a default value of True. Set it to False if we are connecting to a real device. 40 | "Port" has a default value of 502. Specify it if you have a non default port. 41 | 42 | The program calls the Lua (version 5.2) script associated with an outstation(Outstation1.lua) once every second. The function MUST be called "generate_data". 43 | The expected output of this function is a nested table called data. The nested tables have the keys "Coils", "Discretes Input", "Input Registers", and "Holding Registers". 44 | The tables can be empty if you don't want any new values generated for that data type. The size of the subtable has to match the data size specified in "Data Count". 45 | -------------------------------------------------------------------------------- /modbusgenerator/configs-scripts/Config.json: -------------------------------------------------------------------------------- 1 | { 2 | "Virtual Interface": "ens38", 3 | "CIDR Notation":"192.168.81.1/25", 4 | "Master Nodes": 5 | [ 6 | {"Name":"M1", 7 | "Outstations":["S1", "S2"], 8 | "Simulate":false, 9 | "Poll Interval": 10 | [ 11 | {"Data type":"Discretes Input", "Frequency":5}, 12 | {"Data type":"Coils", "Frequency":8}, 13 | {"Data type":"Input Registers", "Frequency":2}, 14 | {"Data type":"Holding Registers", "Frequency":10} 15 | ] 16 | }, 17 | {"Name":"M2", 18 | "Outstations":["S3"] 19 | } 20 | ], 21 | "S1":{"Data Count":7, "Unit ID":1}, 22 | "S2":{"Data Count":5, "Unit ID":2}, 23 | "S3":{} 24 | } 25 | -------------------------------------------------------------------------------- /modbusgenerator/configs-scripts/S1.lua: -------------------------------------------------------------------------------- 1 | ntime = os.time() +5 2 | function generate_data() 3 | data = {} 4 | if os.time() > ntime then 5 | data = {"Discretes Input", "Coils", "Input Registers", "Holding Registers"} 6 | data["Discretes Input"]={} 7 | data["Coils"]={} 8 | data["Input Registers"]={} 9 | data["Holding Registers"]={} 10 | for i =1,7 do 11 | data["Discretes Input"][i]=i*math.random()*1.4 12 | data["Coils"][i]=math.floor(2*math.random()) 13 | data["Input Registers"][i]=math.floor(i*math.random()) 14 | data["Holding Registers"][i]=i*math.random()*2.034 15 | end 16 | ntime=ntime+5 17 | end 18 | return data 19 | end 20 | -------------------------------------------------------------------------------- /modbusgenerator/configs-scripts/S2.lua: -------------------------------------------------------------------------------- 1 | ntime = os.time() +15 2 | function generate_data() 3 | data = {} 4 | if os.time() > ntime then 5 | data = {"Discretes Input", "Coils", "Input Registers", "Holding Registers"} 6 | data["Discretes Input"]={} 7 | data["Coils"]={} 8 | data["Input Registers"]={} 9 | data["Holding Registers"]={} 10 | for i =1,5 do 11 | data["Discretes Input"][i]=i*math.random()*2.2 12 | data["Coils"][i]=math.floor(2*math.random()) 13 | data["Input Registers"][i]=math.floor(i*math.random()) 14 | data["Holding Registers"][i]=i*math.random()*1.32 15 | end 16 | ntime=ntime+15 17 | end 18 | return data 19 | end 20 | -------------------------------------------------------------------------------- /modbusgenerator/configs-scripts/S3.lua: -------------------------------------------------------------------------------- 1 | ntime = os.time() +10 2 | function generate_data() 3 | data = {} 4 | if os.time() > ntime then 5 | data = {"Discretes Input", "Coils", "Input Registers", "Holding Registers"} 6 | data["Discretes Input"]={} 7 | data["Coils"]={} 8 | data["Input Registers"]={} 9 | data["Holding Registers"]={} 10 | for i =1,10 do 11 | data["Discretes Input"][i]=i*math.random()*4.32 12 | data["Coils"][i]=math.floor(2*math.random()) 13 | data["Input Registers"][i]=math.floor(i*math.random()) 14 | data["Holding Registers"][i]=i*math.random()*1.66 15 | end 16 | ntime=ntime+10 17 | end 18 | return data 19 | end 20 | -------------------------------------------------------------------------------- /modbusgenerator/configs-scripts/master_vm.json: -------------------------------------------------------------------------------- 1 | { 2 | "Virtual Interface": "ens38", 3 | "CIDR Notation":"192.168.81.1/25", 4 | "Master Nodes": 5 | [ 6 | {"Name":"M1", 7 | "Outstations":["S1", "S2"], 8 | "Poll Interval": 9 | [ 10 | {"Data type":"Discretes Input", "Frequency":5}, 11 | {"Data type":"Coils", "Frequency":8}, 12 | {"Data type":"Input Registers", "Frequency":2}, 13 | {"Data type":"Holding Registers", "Frequency":10} 14 | ] 15 | } 16 | ], 17 | "S1":{"Data Count":7, "Unit ID":1, "Simulate":false, "IP Address":"172.16.136.1"}, 18 | "S2":{"Data Count":5, "Unit ID":2, "Simulate":false, "IP Address":"172.16.136.1"} 19 | } 20 | -------------------------------------------------------------------------------- /modbusgenerator/configs-scripts/outstation_vm.json: -------------------------------------------------------------------------------- 1 | { 2 | "Virtual Interface": "ens33", 3 | "CIDR Notation":"172.16.136.1/24", 4 | "Master Nodes": 5 | [ 6 | {"Name":"M1", 7 | "Outstations":["S1", "S2"], 8 | "Simulate":false, 9 | "Poll Interval": 10 | [ 11 | {"Data type":"Discretes Input", "Frequency":5}, 12 | {"Data type":"Coils", "Frequency":8}, 13 | {"Data type":"Input Registers", "Frequency":2}, 14 | {"Data type":"Holding Registers", "Frequency":10} 15 | ] 16 | } 17 | ], 18 | "S1":{"Data Count":7, "Unit ID":1}, 19 | "S2":{"Data Count":5, "Unit ID":2} 20 | } 21 | -------------------------------------------------------------------------------- /modbusgenerator/extras/dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:xenial 2 | RUN apt-get update 3 | RUN apt-get install -y python3 python3-dev python3-pip git net-tools 4 | RUN git clone https://github.com/bashwork/pymodbus 5 | WORKDIR /pymodbus 6 | RUN git checkout --track origin/python3 7 | RUN python3 setup.py install 8 | RUN pip3 install attrs Automat constantly incremental ipaddress netaddr netifaces pycrypto pyserial six Twisted zope.interface --user 9 | -------------------------------------------------------------------------------- /modbusgenerator/master_station.py: -------------------------------------------------------------------------------- 1 | import threading 2 | import random 3 | import time 4 | import logging 5 | import copy 6 | 7 | from node import node 8 | from mymodbusclient import MyModbusClient 9 | 10 | start_address = 0x00 11 | 12 | class master_station(threading.Thread): 13 | def __init__(self, node, thread_stop): 14 | threading.Thread.__init__(self) 15 | self.masterstop = threading.Event() 16 | self.node = node 17 | self.masterstop = thread_stop 18 | self.logger = logging.getLogger('Master(ModbusTcpClient)') 19 | #self.logger.setLevel(logging.DEBUG) 20 | 21 | def run(self): 22 | self.logger.debug("and we're off!") 23 | self.logger.info("Master %s at ip address %s connected to %s on port %s" %(self.node.name, self.node.ipaddr, self.node.outstation_addr, self.node.port)) 24 | master = MyModbusClient(self.node.outstation_addr, self.node.port, src_ipaddr = self.node.ipaddr) 25 | master.connect() 26 | outstation_unitids = list(self.node.outstation_info.keys()) 27 | sleep_dict = copy.deepcopy(self.node.poll_dict) 28 | while(not self.masterstop.is_set()): 29 | if not sleep_dict: 30 | time.sleep(1) #avoid hogging cpu if no polling specified, TODO- break instead?? 31 | continue 32 | sleep_time = min(sleep_dict.values()) 33 | self.logger.debug("Before, dict is {}, sleeping for {} secs".format(sleep_dict, sleep_time)) 34 | time.sleep(sleep_time) 35 | pollfuncs = [key for key, value in sleep_dict.items() if value == sleep_time] 36 | self.logger.debug("Polling outstations for {}".format(pollfuncs)) 37 | self.poll_outstations(master, pollfuncs, outstation_unitids) 38 | #update sleep_dict now. 39 | for k,v in sleep_dict.items(): 40 | sleep_dict[k] = v - sleep_time if v-sleep_time > 0 else self.node.poll_dict[k] 41 | self.logger.debug("After modification, sleep_dict is {}".format(sleep_dict)) 42 | self.logger.info("stopping master {}".format(self.node.name)) 43 | if hasattr(self.node, 'vnic'): 44 | node.deallocate(self.node.vnic) 45 | master.close() 46 | 47 | def poll_outstations(self, master, pollfuncs, outstation_unitids): 48 | for unitid in outstation_unitids: 49 | data_count = self.node.outstation_info[unitid] 50 | self.logger.debug("Got id:" +str(unitid) + " with datacount:"+str(data_count)) 51 | for fn in pollfuncs: 52 | output = "" 53 | if fn == "Coils": 54 | try: 55 | output = master.read_coils(start_address, data_count, unit=unitid) 56 | self.logger.info("%s read coils to server %s:%s" %(self.node.name, unitid, output)) 57 | except Exception as ex: 58 | self.logger.exception("Cannot read coils:%s", ex) 59 | elif fn == "Discretes Input": 60 | try: 61 | output = master.read_discrete_inputs(start_address, data_count, unit=unitid) 62 | self.logger.info("%s read discrete inputs to server %s:%s" %(self.node.name, unitid, output)) 63 | except Exception as ex: 64 | self.logger.exception("Cannot read discrete inputs:%s", ex) 65 | elif fn == "Holding Registers": 66 | try: 67 | output = master.read_holding_registers(start_address, data_count, unit=unitid) 68 | self.logger.info("%s read holding registers to server %s:%s" %(self.node.name, unitid, output)) 69 | except Exception as ex: 70 | self.logger.exception("Cannot read holding registers:%s", ex) 71 | elif fn == "Input Registers": 72 | try: 73 | output = master.read_input_registers(start_address, data_count, unit=unitid) 74 | self.logger.info("%s read input registers to server %s:%s"%(self.node.name, unitid, output)) 75 | except Exception as ex: 76 | self.logger.exception("Cannot read input registers:%s", ex) 77 | self.logger.info("Output:%s" %output) 78 | -------------------------------------------------------------------------------- /modbusgenerator/modbus_app.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import time 4 | import argparse 5 | import os 6 | import signal 7 | import threading 8 | 9 | from master_station import master_station 10 | from outstation import outstation 11 | from cfg_json_parser import json_parser 12 | import logging 13 | import logging.handlers as Handlers 14 | 15 | thread_stop = threading.Event() 16 | 17 | def signal_handler(signal, frame): 18 | print('Cleaning up...') 19 | #TODO: Signal threads to end 20 | thread_stop.set() 21 | print ('Waiting for threads to finish') 22 | time.sleep(7) 23 | os._exit(0) #Hack because serve_forever callback is broken 24 | 25 | def main(): 26 | parser = argparse.ArgumentParser(description='Generate ModBus Traffic.') 27 | parser.add_argument('-c', '--config', default = "config.json", help='Configuration File') 28 | args = parser.parse_args() 29 | 30 | if not os.path.isfile(args.config): 31 | print("Invalid Configuration File") 32 | exit() 33 | 34 | logging.basicConfig() 35 | log = logging.getLogger() 36 | log.setLevel(logging.INFO) 37 | 38 | parser = json_parser(args.config) 39 | 40 | for node in parser.outstations: 41 | if node.simulate: 42 | o = outstation(node, thread_stop) 43 | o.start() 44 | 45 | for node in parser.masters: 46 | if node.simulate: 47 | m = master_station(node, thread_stop) 48 | m.start() 49 | 50 | signal.signal(signal.SIGINT, signal_handler) 51 | signal.pause() 52 | 53 | if __name__ == '__main__': 54 | main() 55 | -------------------------------------------------------------------------------- /modbusgenerator/mymodbusclient.py: -------------------------------------------------------------------------------- 1 | import socket 2 | 3 | from pymodbus.constants import Defaults 4 | from pymodbus.transaction import ModbusSocketFramer, ModbusBinaryFramer 5 | from pymodbus.transaction import ModbusAsciiFramer, ModbusRtuFramer 6 | 7 | from pymodbus.client.sync import ModbusTcpClient 8 | #---------------------------------------------------------------------------# 9 | # Logging 10 | #---------------------------------------------------------------------------# 11 | import logging 12 | _logger = logging.getLogger(__name__) 13 | 14 | class MyModbusClient(ModbusTcpClient): 15 | def __init__(self, host='127.0.0.1', port=Defaults.Port, framer=ModbusSocketFramer, src_ipaddr=None): 16 | super().__init__(host, port, framer) 17 | self.src_ipaddr=src_ipaddr 18 | 19 | def connect(self): 20 | ''' Connect to the modbus tcp server 21 | 22 | :returns: True if connection succeeded, False otherwise 23 | ''' 24 | if self.socket: return True 25 | try: 26 | self.socket = socket.create_connection((self.host, self.port), Defaults.Timeout, (self.src_ipaddr, 46100)) 27 | except socket.error as msg: 28 | _logger.error('Connection to (%s, %s) failed: %s' % \ 29 | (self.host, self.port, msg)) 30 | self.close() 31 | return self.socket != None 32 | -------------------------------------------------------------------------------- /modbusgenerator/node.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | import logging 3 | 4 | class node: 5 | vnictrack=0 6 | def __init__ (self, name, role, **kwargs): 7 | self.name = name 8 | self.role = role 9 | self.ipaddr = kwargs.get("ipv4addr", None) 10 | self.logger = logging.getLogger('node') 11 | 12 | def allocate(self, vnic): 13 | self.vnic = vnic + ":" + str(node.vnictrack) 14 | self.logger.debug("Going to allocate for %s, got vnic:%s, ip address:%s" %(self.name, self.vnic, self.ipaddr)) 15 | subprocess.call(["ifconfig", self.vnic, self.ipaddr]) 16 | node.vnictrack +=1 17 | 18 | def deallocate(interface): 19 | interface and subprocess.call(["ifconfig", interface, "down"]) 20 | -------------------------------------------------------------------------------- /modbusgenerator/outstation.py: -------------------------------------------------------------------------------- 1 | import threading 2 | import time 3 | import socketserver 4 | import logging 5 | 6 | import lupa 7 | 8 | from pymodbus.server.sync import ModbusSocketFramer, ModbusTcpServer 9 | from pymodbus.device import ModbusDeviceIdentification 10 | from pymodbus.datastore import ModbusSequentialDataBlock 11 | from pymodbus.datastore import ModbusSlaveContext, ModbusServerContext 12 | 13 | from node import node 14 | from threadsafe_datastore import ThreadSafeDataBlock 15 | 16 | start_address = 0x01 17 | 18 | class outstation(threading.Thread): 19 | def __init__(self, node, thread_stop): 20 | threading.Thread.__init__(self) 21 | #Data Store for the server instance. Only set by connecting clients 22 | store={} 23 | for unit_id, data_count in node.datastore.items(): 24 | if len(node.datastore) == 1: 25 | store = ModbusSlaveContext( 26 | di = ThreadSafeDataBlock(ModbusSequentialDataBlock(start_address, [0x00]*data_count)), 27 | co = ThreadSafeDataBlock(ModbusSequentialDataBlock(start_address, [0x00]*data_count)), 28 | hr = ThreadSafeDataBlock(ModbusSequentialDataBlock(start_address, [0x00]*data_count)), 29 | ir = ThreadSafeDataBlock(ModbusSequentialDataBlock(start_address, [0x00]*data_count))) 30 | else: 31 | store[unit_id] = ModbusSlaveContext( 32 | di = ThreadSafeDataBlock(ModbusSequentialDataBlock(start_address, [0x00]*data_count)), 33 | co = ThreadSafeDataBlock(ModbusSequentialDataBlock(start_address, [0x00]*data_count)), 34 | hr = ThreadSafeDataBlock(ModbusSequentialDataBlock(start_address, [0x00]*data_count)), 35 | ir = ThreadSafeDataBlock(ModbusSequentialDataBlock(start_address, [0x00]*data_count))) 36 | 37 | self.context = ModbusServerContext(slaves=store, single= len(node.datastore)==1 and True or False) 38 | self.outstation_stop = thread_stop 39 | 40 | self.identity = ModbusDeviceIdentification() 41 | self.identity.VendorName = 'ITI' 42 | self.identity.ProductCode = 'PM' 43 | self.identity.VendorUrl = 'code.iti.illinois.edu' 44 | self.identity.ProductName = 'Server Instance' 45 | self.identity.ModelName = 'ITI Test' 46 | self.identity.MajorMinorRevision = '1.0' 47 | self.node = node 48 | self.logger = logging.getLogger('Outstation(ModbusTcpServer)') 49 | #self.logger.setLevel(logging.DEBUG) 50 | 51 | def run(self): 52 | print("Starting ModBus Server: {}:{}".format(self.node.ipaddr, self.node.port)) 53 | framer = ModbusSocketFramer 54 | #TODO-REMOVE ALLOW_ADDRESS_REUSE IN FINAL CODE. 55 | socketserver.TCPServer.allow_reuse_address = True 56 | self.server = ModbusTcpServer(self.context, framer, self.identity, address=(self.node.ipaddr, self.node.port)) 57 | for name in self.node.name: 58 | t = threading.Thread(target=ping_outstation, args=(self,name)) 59 | t.start() 60 | self.server.serve_forever() 61 | 62 | def ping_outstation(outstation, outstn_name): 63 | L = lupa.LuaRuntime() 64 | lua_script = open(outstn_name + ".lua").read() 65 | L_func = L.execute(lua_script) 66 | g = L.globals() 67 | #print(g.generate_data) 68 | lua_pymod_dtype_map = {'Discretes Input':'d', 'Coils':'c', 'Input Registers':'i', 'Holding Registers':'h'} 69 | 70 | while not outstation.outstation_stop.is_set(): 71 | time.sleep(1) 72 | unit_id = outstation.node.name_id_map[outstn_name] 73 | try: 74 | slave = outstation.context[unit_id] 75 | except ParameterException as err: 76 | outstation.logger.debug("context.py returned:%s" %err) 77 | continue 78 | data_count = outstation.node.datastore[unit_id] 79 | 80 | data = g.generate_data() 81 | if not data: 82 | continue 83 | for d_type in lua_pymod_dtype_map.keys(): 84 | if d_type not in data or not data[d_type]: 85 | continue 86 | pymodbus_d_type = lua_pymod_dtype_map[d_type] 87 | values = list(data[d_type].values()) 88 | if len(values) == 0 or len(values) != data_count: 89 | continue 90 | dtype_datablock = slave.store.get(pymodbus_d_type) 91 | outstation.logger.debug("Updating Outstation:" + outstn_name + ", datacount:"+ str(data_count) + " dtype:"+ d_type + ", new values : " + str(values)) 92 | dtype_datablock.setValues(start_address, values) 93 | outstation.logger.info("stopping server %s" %outstn_name) 94 | outstation.server.shutdown() 95 | outstation.server.server_close() 96 | if hasattr(outstation.node, 'vnic'): 97 | node.deallocate(outstation.node.vnic) 98 | -------------------------------------------------------------------------------- /modbusgenerator/readwritelock.py: -------------------------------------------------------------------------------- 1 | import threading 2 | from contextlib import contextmanager 3 | 4 | class ReadWriteLock(object): 5 | ''' This reader writer lock gurantees write order, but not 6 | read order and is generally biased towards allowing writes 7 | if they are available to prevent starvation. 8 | 9 | TODO: 10 | 11 | * allow user to choose between read/write/random biasing 12 | - currently write biased 13 | - read biased allow N readers in queue 14 | - random is 50/50 choice of next 15 | ''' 16 | 17 | def __init__(self): 18 | ''' Initializes a new instance of the ReadWriteLock 19 | ''' 20 | self.queue = [] # the current writer queue 21 | self.lock = threading.Lock() # the underlying condition lock 22 | self.read_condition = threading.Condition(self.lock) # the single reader condition 23 | self.readers = 0 # the number of current readers 24 | self.writer = False # is there a current writer 25 | 26 | def __is_pending_writer(self): 27 | return (self.writer # if there is a current writer 28 | or (self.queue # or if there is a waiting writer 29 | and (self.queue[0] != self.read_condition))) # or if the queue head is not a reader 30 | 31 | def acquire_reader(self): 32 | ''' Notifies the lock that a new reader is requesting 33 | the underlying resource. 34 | ''' 35 | with self.lock: 36 | if self.__is_pending_writer(): # if there are existing writers waiting 37 | if self.read_condition not in self.queue: # do not pollute the queue with readers 38 | self.queue.append(self.read_condition) # add the readers in line for the queue 39 | while self.__is_pending_writer(): # until the current writer is finished 40 | self.read_condition.wait(1) # wait on our condition 41 | if self.queue and self.read_condition == self.queue[0]: # if the read condition is at the queue head 42 | self.queue.pop(0) # then go ahead and remove it 43 | self.readers += 1 # update the current number of readers 44 | 45 | def acquire_writer(self): 46 | ''' Notifies the lock that a new writer is requesting 47 | the underlying resource. 48 | ''' 49 | with self.lock: 50 | if self.writer or self.readers: # if we need to wait on a writer or readers 51 | condition = threading.Condition(self.lock) # create a condition just for this writer 52 | self.queue.append(condition) # and put it on the waiting queue 53 | while self.writer or self.readers: # until the write lock is free 54 | condition.wait(1) # wait on our condition 55 | self.queue.pop(0) # remove our condition after our condition is met 56 | self.writer = True # stop other writers from operating 57 | 58 | def release_reader(self): 59 | ''' Notifies the lock that an existing reader is 60 | finished with the underlying resource. 61 | ''' 62 | with self.lock: 63 | self.readers = max(0, self.readers - 1) # readers should never go below 0 64 | if not self.readers and self.queue: # if there are no active readers 65 | self.queue[0].notify_all() # then notify any waiting writers 66 | 67 | def release_writer(self): 68 | ''' Notifies the lock that an existing writer is 69 | finished with the underlying resource. 70 | ''' 71 | with self.lock: 72 | self.writer = False # give up current writing handle 73 | if self.queue: # if someone is waiting in the queue 74 | self.queue[0].notify_all() # wake them up first 75 | else: self.read_condition.notify_all() # otherwise wake up all possible readers 76 | 77 | @contextmanager 78 | def get_reader_lock(self): 79 | ''' Wrap some code with a reader lock using the 80 | python context manager protocol:: 81 | 82 | with rwlock.get_reader_lock(): 83 | do_read_operation() 84 | ''' 85 | try: 86 | self.acquire_reader() 87 | yield self 88 | finally: self.release_reader() 89 | 90 | @contextmanager 91 | def get_writer_lock(self): 92 | ''' Wrap some code with a writer lock using the 93 | python context manager protocol:: 94 | 95 | with rwlock.get_writer_lock(): 96 | do_read_operation() 97 | ''' 98 | try: 99 | self.acquire_writer() 100 | yield self 101 | finally: self.release_writer() 102 | -------------------------------------------------------------------------------- /modbusgenerator/requirements.txt: -------------------------------------------------------------------------------- 1 | attrs==16.3.0 2 | Automat==0.5.0 3 | constantly==15.1.0 4 | incremental==16.10.1 5 | ipaddress==1.0.18 6 | netaddr==0.7.19 7 | netifaces==0.10.5 8 | pycrypto==2.6.1 9 | pyserial==3.2.1 10 | six==1.10.0 11 | Twisted==17.1.0 12 | zope.interface==4.3.3 13 | -------------------------------------------------------------------------------- /modbusgenerator/test_parser.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from master_station import master_station 3 | from outstation import outstation 4 | from cfg_json_parser import json_parser 5 | 6 | ''' 7 | Example Test Counting the total nodes allocated by the parser 8 | ''' 9 | test_cfg = ''' 10 | { 11 | "Master Nodes": 12 | [ 13 | {"Name":"M1", 14 | "Outstations":["S1", "S2"] 15 | }, 16 | {"Name":"M2", 17 | "Outstations":["S1"] 18 | } 19 | ], 20 | "S1":{"IP Address":"192.13.13.12", "Data Count":17, "Unit ID":1}, 21 | "S2":{"IP Address":"192.13.13.12", "Data Count":15, "Unit ID":2}, 22 | "S3":{} 23 | } 24 | ''' 25 | class ParserTest(unittest.TestCase): 26 | def runTest(self): 27 | parser = json_parser(test_cfg, test=True) 28 | 29 | self.assertEqual(len(parser.masters), (2), 'Incorrect Master Nodes') 30 | self.assertEqual(len(parser.masters)+len(parser.outstations), (3), 'Incorrect Number of Nodes') 31 | 32 | if __name__== '__main__': 33 | unittest.main() 34 | -------------------------------------------------------------------------------- /modbusgenerator/threadsafe_datastore.py: -------------------------------------------------------------------------------- 1 | from pymodbus.datastore.store import BaseModbusDataBlock 2 | from readwritelock import ReadWriteLock 3 | 4 | class ThreadSafeDataBlock(BaseModbusDataBlock): 5 | ''' This is a simple decorator for a data block. This allows 6 | a user to inject an existing data block which can then be 7 | safely operated on from multiple cocurrent threads. 8 | 9 | It should be noted that the choice was made to lock around the 10 | datablock instead of the manager as there is less source of 11 | contention (writes can occur to slave 0x01 while reads can 12 | occur to slave 0x02). 13 | ''' 14 | 15 | def __init__(self, block): 16 | ''' Initialize a new thread safe decorator 17 | 18 | :param block: The block to decorate 19 | ''' 20 | self.rwlock = ReadWriteLock() 21 | self.block = block 22 | 23 | def validate(self, address, count=1): 24 | ''' Checks to see if the request is in range 25 | 26 | :param address: The starting address 27 | :param count: The number of values to test for 28 | :returns: True if the request in within range, False otherwise 29 | ''' 30 | with self.rwlock.get_reader_lock(): 31 | return self.block.validate(address, count) 32 | 33 | def getValues(self, address, count=1): 34 | ''' Returns the requested values of the datastore 35 | 36 | :param address: The starting address 37 | :param count: The number of values to retrieve 38 | :returns: The requested values from a:a+c 39 | ''' 40 | with self.rwlock.get_reader_lock(): 41 | return self.block.getValues(address, count) 42 | 43 | def setValues(self, address, values): 44 | ''' Sets the requested values of the datastore 45 | 46 | :param address: The starting address 47 | :param values: The new values to be set 48 | ''' 49 | with self.rwlock.get_writer_lock(): 50 | return self.block.setValues(address, values) 51 | --------------------------------------------------------------------------------