diff --git a/.editorconfig b/.editorconfig index b5ea25092..1a24ac2f8 100644 --- a/.editorconfig +++ b/.editorconfig @@ -29,3 +29,9 @@ indent_size = 4 [*.{nix,json}] indent_style = space indent_size = 2 + +[*.patch] +charset = unset +end_of_line = unset +insert_final_newline = unset +trim_trailing_whitespace = unset diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 4bfba7eb1..c7baf4508 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -8,8 +8,9 @@ repos: rev: v5.0.0 hooks: - id: trailing-whitespace + exclude: \.patch$ - id: end-of-file-fixer - exclude: \.svg$ + exclude: \.(svg|patch)$ - id: check-ast - id: check-xml - id: check-yaml diff --git a/CMakeLists.txt b/CMakeLists.txt index ba316a1b7..ced886b62 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -75,6 +75,7 @@ find_package(Etherlab) find_package(Lua) find_package(LibDataChannel) find_package(re) +find_package(OpenDSSC) # Check for tools find_program(PASTE NAMES paste) @@ -203,6 +204,7 @@ cmake_dependent_option(WITH_NODE_ULDAQ "Build with uldaq node-type" cmake_dependent_option(WITH_NODE_WEBRTC "Build with webrtc node-type" "${WITH_DEFAULTS}" "WITH_WEB; LibDataChannel_FOUND" OFF) cmake_dependent_option(WITH_NODE_WEBSOCKET "Build with websocket node-type" "${WITH_DEFAULTS}" "WITH_WEB" OFF) cmake_dependent_option(WITH_NODE_ZEROMQ "Build with zeromq node-type" "${WITH_DEFAULTS}" "LIBZMQ_FOUND; NOT WITHOUT_GPL" OFF) +cmake_dependent_option(WITH_NODE_OPENDSS "Build with opendss node-type" "${WITH_DEFAULTS}" "OpenDSSC_FOUND" OFF) # set a default for the build type if("${CMAKE_BUILD_TYPE}" STREQUAL "") @@ -296,6 +298,7 @@ add_feature_info(NODE_MODBUS WITH_NODE_MODBUS "Build with add_feature_info(NODE_MQTT WITH_NODE_MQTT "Build with mqtt node-type") add_feature_info(NODE_NANOMSG WITH_NODE_NANOMSG "Build with nanomsg node-type") add_feature_info(NODE_NGSI WITH_NODE_NGSI "Build with ngsi node-type") +add_feature_info(NODE_OPENDSS WITH_NODE_OPENDSS "Build with opendss node-type") add_feature_info(NODE_REDIS WITH_NODE_REDIS "Build with redis node-type") add_feature_info(NODE_RTP WITH_NODE_RTP "Build with rtp node-type") add_feature_info(NODE_SHMEM WITH_NODE_SHMEM "Build with shmem node-type") diff --git a/REUSE.toml b/REUSE.toml index 863a452a9..b669f4c15 100644 --- a/REUSE.toml +++ b/REUSE.toml @@ -1,10 +1,19 @@ +# SPDX-FileCopyrightText: 2025 OPAL-RT Germany GmbH +# SPDX-License-Identifier: Apache-2.0 + version = 1 SPDX-PackageName = "VILLASnode" SPDX-PackageSupplier = "Steffen Vogel " SPDX-PackageDownloadLocation = "https://fein-aachen.org/en/projects/villas-node/" [[annotations]] -path = ["**.prefs", "**.vi", "**.opal", "**.dft", "**.sib", "**.json", "**.ipynb", "**_pb2.py", "doc/pictures/**", "doc/favicon.png", "clients/opal-rt/rtlab-asyncip/models/send_receive/eonerc_logo.png", "clients/rtds/**/**.txt", "clients/opal-rt/hypersim-ucm/**.ecf", "etc/labs/lab3.pcap", "packaging/live-iso/files/etc/**", "flake.lock", "tests/valgrind.supp", "packaging/archlinux/villas-node.install"] +path = ["**.prefs", "**.vi", "**.opal", "**.dft", "**.sib", "**.json", "**.ipynb", "**_pb2.py", "doc/pictures/**", "doc/favicon.png", "clients/opal-rt/rtlab-asyncip/models/send_receive/eonerc_logo.png", "clients/rtds/**/**.txt", "clients/opal-rt/hypersim-ucm/**.ecf", "etc/labs/lab3.pcap", "packaging/live-iso/files/etc/**", "tests/valgrind.supp", "packaging/archlinux/villas-node.install"] precedence = "aggregate" SPDX-FileCopyrightText = "2018-2023, Institute for Automation of Complex Power Systems, RWTH Aachen University" SPDX-License-Identifier = "Apache-2.0" + +[[annotations]] +path = ["flake.lock", "**.patch"] +precedence = "aggregate" +SPDX-FileCopyrightText = "2025 OPAL-RT Germany GmbH" +SPDX-License-Identifier = "Apache-2.0" diff --git a/cmake/FindOpenDSSC.cmake b/cmake/FindOpenDSSC.cmake new file mode 100644 index 000000000..f15e43d69 --- /dev/null +++ b/cmake/FindOpenDSSC.cmake @@ -0,0 +1,29 @@ +# CMakeLists.txt. +# +# Author: Jitpanu Maneeratpongsuk +# SPDX-FileCopyrightText: 2025 Institute for Automation of Complex Power Systems, RWTH Aachen University +# SPDX-License-Identifier: Apache-2.0 + +find_path(OPENDSSC_INCLUDE_DIR + NAMES OpenDSSCDLL.h + PATH_SUFFIXES OpenDSSC +) + +find_library(OPENDSSC_LIBRARY_DIR + NAMES OpenDSSC + PATH_SUFFIXES openDSSC/bin +) + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(OpenDSSC DEFAULT_MSG OPENDSSC_LIBRARY_DIR OPENDSSC_INCLUDE_DIR) + +mark_as_advanced(OPENDSSC_INCLUDE_DIR OPENDSS_LIBRARY_DIR) + +if(OpenDSSC_FOUND) + add_library(OpenDSSC SHARED IMPORTED) + set_target_properties(OpenDSSC PROPERTIES + IMPORTED_LOCATION "${OPENDSSC_LIBRARY_DIR}" + INTERFACE_INCLUDE_DIRECTORIES "${OPENDSSC_INCLUDE_DIR}" + INTERFACE_COMPILE_OPTIONS "-D_D2C_SYSFILE_H_LONG_IS_INT64" + ) +endif() diff --git a/doc/openapi/components/schemas/config/global.yaml b/doc/openapi/components/schemas/config/global.yaml index 96673a5b8..3f37130f7 100644 --- a/doc/openapi/components/schemas/config/global.yaml +++ b/doc/openapi/components/schemas/config/global.yaml @@ -68,3 +68,12 @@ properties: If the setting is not provided, a UUID will be generated by hashing the active VILLASnode configuration. This ensures that restarting the VILLASnode instance with the identical configuration will yield always the same UUID. + + seed: + type: integer + default: 0 + title: Random number generator seed + description: | + The seed for the random number generator used by the VILLASnode instance. + + If the setting is not provided, a random seed of 0 will be used. diff --git a/doc/openapi/components/schemas/config/nodes/_opendss.yaml b/doc/openapi/components/schemas/config/nodes/_opendss.yaml new file mode 100644 index 000000000..5706315b3 --- /dev/null +++ b/doc/openapi/components/schemas/config/nodes/_opendss.yaml @@ -0,0 +1,7 @@ +# yaml-language-server: $schema=http://json-schema.org/draft-07/schema +# SPDX-FileCopyrightText: 2025 Institute for Automation of Complex Power Systems, RWTH Aachen University +# SPDX-License-Identifier: Apache-2.0 +--- +allOf: +- $ref: ../node_obj.yaml +- $ref: opendss.yaml diff --git a/doc/openapi/components/schemas/config/nodes/opendss.yaml b/doc/openapi/components/schemas/config/nodes/opendss.yaml new file mode 100644 index 000000000..aa1525b96 --- /dev/null +++ b/doc/openapi/components/schemas/config/nodes/opendss.yaml @@ -0,0 +1,49 @@ +# yaml-language-server: $schema=http://json-schema.org/draft-07/schema +# SPDX-FileCopyrightText: 2025 Institute for Automation of Complex Power Systems, RWTH Aachen University +# SPDX-License-Identifier: Apache-2.0 +--- +allOf: +- type: object + properties: + + path: + file_path: string + format: uri + description: | + Specifies the URI to a OpenDSS file. + + in: + type: array + item: + type: object + properties: + name: + type: string + description: | + Name of the element. + type: + type: string + enum: + - load + - generator + - isource + description: | + Type of the element. + data: + type: string + description: | + Data to be input. Possible option are depent on element type. + + - load: kV, kW, kVA, Pf + - generator: kV, kW, kVA, Pf + - isource: Amps, AngleDeg, f + + out: + description: | + Name of the monitor to be read. + type: array + items: + type: string + +- $ref: ../node_signals.yaml +- $ref: ../node.yaml diff --git a/etc/examples/nodes/opendss.conf b/etc/examples/nodes/opendss.conf new file mode 100644 index 000000000..48b3c4d7e --- /dev/null +++ b/etc/examples/nodes/opendss.conf @@ -0,0 +1,74 @@ +# SPDX-FileCopyrightText: 2025 Institute for Automation of Complex Power Systems, RWTH Aachen University +# SPDX-License-Identifier: Apache-2.0 + +nodes = { + opendss_node = { + type = "opendss" + + # Path to OpenDSS file + file_path = "OpenDSS_file/sample.DSS" + + # Example input configuration. Input data will be used to set Active power and power factor + # of load element name load1 and Active power of the generator element name gen1. + # Element name are deleared in OpenDSS file. + # Available input type and data: + # - load: kV, kW, kVA, Pf + # - generator: kV, kW, kVA, Pf + # - isource: Amps, AngleDeg, f + in = { + list = ( + {name = "load1", type = "load", data = ("kW", "Pf")}, + {name = "gen1", type = "generator", data = ("kW")} + ) + } + + # Example output configuration. Output data will be read from monitor name load1_power and load1_v. + # Monitor name are declared in OpenDSS file. + out = { + list = ["load1_power", "load1_v"] + } + } + + udp_node = { + type = "socket" + + layer = "udp" + + format = "villas.human" + + in = { + address = "*:12000" + } + out = { + address = "127.0.0.1:12001" + } + } + + file_node1 = { + type = "file" + + uri = "load.dat" + + in = { + epoch_mode = "direct" + epoch = 10 + rate = 2 + buffer_size = 0 + } + } +} + +paths = ( + { + # Get input for file node type + in = "file_node1" + out = "opendss_node" + hooks = ( { type = "print" } ) + }, + { + # Output to udp node type + in = "opendss_node" + out = "udp_node" + hooks = ( { type = "print" } ) + } +) diff --git a/flake.nix b/flake.nix index be8d2ecae..ba75bed90 100644 --- a/flake.nix +++ b/flake.nix @@ -70,6 +70,8 @@ contents = [ villas-node ]; config.ENTRYPOINT = "/bin/villas"; }; + + opendssc = pkgs.callPackage (nixDir + "/opendssc.nix") { }; }; in { diff --git a/include/villas/hooks/decimate.hpp b/include/villas/hooks/decimate.hpp index c9e3767a1..d5c046c52 100644 --- a/include/villas/hooks/decimate.hpp +++ b/include/villas/hooks/decimate.hpp @@ -20,7 +20,8 @@ class DecimateHook : public LimitHook { unsigned counter; public: - using LimitHook::LimitHook; + DecimateHook(Path *p, Node *n, int fl, int prio, bool en = true) + : LimitHook(p, n, fl, prio, en), ratio(1), renumber(false), counter(0) {} virtual void setRate(double rate, double maxRate = -1) { assert(maxRate > 0); diff --git a/include/villas/nodes/opendss.hpp b/include/villas/nodes/opendss.hpp new file mode 100644 index 000000000..52ea87ffd --- /dev/null +++ b/include/villas/nodes/opendss.hpp @@ -0,0 +1,77 @@ +/* OpenDSS node-type for electric power distribution system simulator OpenDSS. + * + * Author: Jitpanu Maneeratpongsuk + * SPDX-FileCopyrightText: 2025 Institute for Automation of Complex Power Systems, RWTH Aachen University + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include + +#include +#include +#include +#include +#include + +namespace villas { +namespace node { + +// Forward declarations +struct Sample; + +class OpenDSS : public Node { +protected: + enum ElementType { generator, load, monitor, isource }; + + struct Element { + std::string + name; // Name of the element must be that same as declared in OpenDSS file + ElementType type; // Type of the element + std::vector + mode; // Mode is set according to the function mode in OpenDSS + }; + + bool writingTurn; + const char *path; + timespec ts; + + std::vector dataIn; // Vector of elements to be written. + std::vector monitorNames; + std::unordered_set loads, generators, monitors, + isource_set; // Set of corresponding element type. + + pthread_mutex_t mutex; + pthread_cond_t cv; + + virtual int _read(struct Sample *smps[], unsigned cnt); + + virtual int _write(struct Sample *smps[], unsigned cnt); + + void parseData(json_t *json, bool in); + + void getElementName(ElementType type, std::unordered_set *set); + + int extractMonitorData(struct Sample *const *smps); + + std::string dssPutCommand(const std::string &cmd) const; + +public: + OpenDSS(const uuid_t &id = {}, const std::string &name = ""); + + virtual ~OpenDSS(); + + virtual int prepare(); + + virtual int parse(json_t *json); + + virtual int check(); + + virtual int start(); + + virtual int stop(); +}; + +} // namespace node +} // namespace villas diff --git a/include/villas/super_node.hpp b/include/villas/super_node.hpp index ca73c1486..f4836fdfd 100644 --- a/include/villas/super_node.hpp +++ b/include/villas/super_node.hpp @@ -56,14 +56,15 @@ class SuperNode { Web web; #endif - int priority; // Process priority (lower is better) - int affinity; // Process affinity of the server and all created threads + unsigned seed; // Random seed for random number generation. + int priority; // Process priority (lower is better). + int affinity; // Process affinity of the server and all created threads. int hugepages; // Number of hugepages to reserve. double statsRate; // Rate at which we display the periodic stats. - struct Task task; // Task for periodic stats output + struct Task task; // Task for periodic stats output. - uuid_t uuid; // A globally unique identifier of the instance + uuid_t uuid; // A globally unique identifier of the instance. struct timespec started; // The time at which the instance has been started. diff --git a/lib/nodes/CMakeLists.txt b/lib/nodes/CMakeLists.txt index 1a9185257..01cc0e7ef 100644 --- a/lib/nodes/CMakeLists.txt +++ b/lib/nodes/CMakeLists.txt @@ -148,8 +148,14 @@ endif() if(WITH_NODE_EXAMPLE) list(APPEND NODE_SRC example.cpp) -# Add your library dependencies for the new node-type here -# list(APPEND LIBRARIES PkgConfig::EXAMPLELIB) + # Add your library dependencies for the new node-type here + # list(APPEND LIBRARIES PkgConfig::EXAMPLELIB) +endif() + +# Enable OpenDSS node type +if(WITH_NODE_OPENDSS) + list(APPEND NODE_SRC opendss.cpp) + list(APPEND LIBRARIES OpenDSSC) endif() # Enable Ethercat support diff --git a/lib/nodes/opendss.cpp b/lib/nodes/opendss.cpp new file mode 100644 index 000000000..a87540796 --- /dev/null +++ b/lib/nodes/opendss.cpp @@ -0,0 +1,364 @@ +/* OpenDSS node-type for electric power distribution system simulator OpenDSS. + * + * Author: Jitpanu Maneeratpongsuk + * SPDX-FileCopyrightText: 2025 Institute for Automation of Complex Power Systems, RWTH Aachen University + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include + +#include +#include +#include +#include +#include +#include + +using namespace villas; +using namespace villas::node; +using namespace villas::utils; + +OpenDSS::OpenDSS(const uuid_t &id, const std::string &name) + : Node(id, name), writingTurn(false), path("") { + int ret = pthread_mutex_init(&mutex, nullptr); + if (ret) + throw RuntimeError("failed to initialize mutex"); + + ret = pthread_cond_init(&cv, nullptr); + if (ret) + throw RuntimeError("failed to initialize mutex"); +} + +OpenDSS::~OpenDSS() { + int ret __attribute__((unused)); + + ret = pthread_mutex_destroy(&mutex); + ret = pthread_cond_destroy(&cv); +} + +std::string OpenDSS::dssPutCommand(const std::string &cmd) const { + static std::string cmdCommand = cmd; + + return DSSPut_Command(cmdCommand.data()); +} + +void OpenDSS::parseData(json_t *json, bool in) { + size_t i; + json_t *json_data; + json_error_t err; + + json_array_foreach(json, i, json_data) { + if (in) { + const char *name; + const char *type; + json_t *a_mode = nullptr; + Element ele; + + int ret = json_unpack_ex(json_data, &err, 0, "{ s: s, s: s, s: o }", + "name", &name, "type", &type, "data", &a_mode); + if (ret) + throw ConfigError(json, err, "node-config-node-opendss"); + + if (!json_is_array(a_mode) && a_mode) + throw ConfigError( + a_mode, "node-config-opendssIn", + "datatype must be configured as a list of name objects"); + + ele.name = name; + + if (!strcmp(type, "generator")) + ele.type = ElementType::generator; + else if (!strcmp(type, "load")) + ele.type = ElementType::load; + else if (!strcmp(type, "isource")) + ele.type = ElementType::isource; + else + throw SystemError("Invalid element type '{}'", type); + + size_t n; + json_t *json_mode; + json_array_foreach(a_mode, n, json_mode) { + const char *mode = json_string_value(json_mode); + // Assign mode according to the OpenDSS function mode. + switch (ele.type) { + case ElementType::generator: + if (!strcmp(mode, "kV")) + ele.mode.push_back(1); + else if (!strcmp(mode, "kW")) + ele.mode.push_back(3); + else if (!strcmp(mode, "kVar")) + ele.mode.push_back(5); + else if (!strcmp(mode, "Pf")) + ele.mode.push_back(7); + else + throw SystemError("Invalid data type: {}", mode); + break; + case ElementType::load: + if (!strcmp(mode, "kW")) + ele.mode.push_back(1); + else if (!strcmp(mode, "kV")) + ele.mode.push_back(3); + else if (!strcmp(mode, "kVar")) + ele.mode.push_back(5); + else if (!strcmp(mode, "Pf")) + ele.mode.push_back(7); + else + throw SystemError("Invalid data type: {}", mode); + break; + case ElementType::isource: + if (!strcmp(mode, "Amps")) + ele.mode.push_back(1); + else if (!strcmp(mode, "AngleDeg")) + ele.mode.push_back(3); + else if (!strcmp(mode, "Frequency")) + ele.mode.push_back(5); + else + throw SystemError("Invalid data type: {}", mode); + break; + default: + throw SystemError("Invalid element type"); + break; + } + } + dataIn.push_back(ele); + } else { + monitorNames.push_back(json_string_value(json_data)); + } + } +} + +int OpenDSS::parse(json_t *json) { + + int ret = Node::parse(json); + if (ret) + return ret; + + json_error_t err; + json_t *json_dataIn = nullptr; + json_t *json_dataOut = nullptr; + + ret = json_unpack_ex(json, &err, 0, "{ s?: s, s: {s: o}, s: {s: o} }", + "file_path", &path, "in", "list", &json_dataIn, "out", + "list", &json_dataOut); + + if (ret) + throw ConfigError(json, err, "node-config-node-opendss"); + + if (!json_is_array(json_dataIn) && json_dataIn) + throw ConfigError(json_dataIn, "node-config-opendssIn", + "DataIn must be configured as a list of name objects"); + + if (!json_is_array(json_dataOut) && json_dataOut) + throw ConfigError(json_dataIn, "node-config-opendssIn", + "DataOut must be configured as a list of name objects"); + + parseData(json_dataIn, true); + parseData(json_dataOut, false); + + return 0; +} + +int OpenDSS::check() { return Node::check(); } + +void OpenDSS::getElementName(ElementType type, + std::unordered_set *set) { + // Get all of the element name for each type and use it to check if the name in the config file is vaild. + uintptr_t myPtr; + int myType; + int mySize = 0; + std::string name; + + switch (type) { + case ElementType::generator: + GeneratorsV(0, &myPtr, &myType, &mySize); + break; + case ElementType::load: + DSSLoadsV(0, &myPtr, &myType, &mySize); + break; + case ElementType::monitor: + MonitorsV(0, &myPtr, &myType, &mySize); + break; + case ElementType::isource: + IsourceV(0, &myPtr, &myType, &mySize); + break; + } + + for (int i = 0; i < mySize; i++) { + if (*(char *)myPtr != '\0') { + name += *(char *)myPtr; + } else { + set->insert(name); + name = ""; + } + myPtr++; + } +} + +int OpenDSS::prepare() { + // Start OpenDSS. + int ret = DSSI(3, 0); + if (!ret) { + throw SystemError("Failed to start OpenDSS"); + } + + // Hide OpenDSS terminal output. + DSSI(8, 0); + + // Compile OpenDSS file. + dssPutCommand(fmt::format("compile \"{}\"", path)); + + getElementName(ElementType::load, &loads); + getElementName(ElementType::generator, &generators); + getElementName(ElementType::monitor, &monitors); + getElementName(ElementType::isource, &isource_set); + + // Check if element name is valid + for (Element ele : dataIn) { + switch (ele.type) { + case ElementType::generator: + if (generators.find(ele.name) == generators.end()) { + throw SystemError("Invalid generator name '{}'", ele.name); + } + break; + case ElementType::load: + if (loads.find(ele.name) == loads.end()) { + throw SystemError("Invalid load name '{}'", ele.name); + } + break; + case ElementType::isource: + if (isource_set.find(ele.name) == isource_set.end()) { + throw SystemError("Invalid isource name '{}'", ele.name); + } + break; + default: + throw SystemError("Invalid input type '{}'", ele.name); + } + } + + for (auto m_name : monitorNames) { + if (monitors.find(m_name) == monitors.end()) { + throw SystemError("Invalid monitor name '{}'", m_name); + } + } + + return Node::prepare(); +} + +int OpenDSS::start() { + // Start with writing. + writingTurn = true; + + return Node::start(); +} + +int OpenDSS::extractMonitorData(struct Sample *const *smps) { + // Get the data from the OpenDSS monitor. + uintptr_t myPtr; + int myType; + int mySize; + int data_count = 0; + + for (auto &Name : monitorNames) { + MonitorsS(2, Name.data()); + MonitorsV(1, &myPtr, &myType, &mySize); + + int channel = MonitorsI(17, 0); + if (data_count == 0) { + channel += 2; + } + + float *data_ptr = reinterpret_cast(myPtr); + data_ptr += (mySize / 4) - channel; + for (int i = 0; i < channel; i++) { + smps[0]->data[data_count + i].f = *data_ptr; + data_ptr++; + } + data_count += channel; + } + + return data_count; +} + +int OpenDSS::_read(struct Sample *smps[], unsigned cnt) { + // Wait until writing is done. + pthread_mutex_lock(&mutex); + while (writingTurn) { + pthread_cond_wait(&cv, &mutex); + } + + smps[0]->ts.origin = ts; + smps[0]->flags = (int)SampleFlags::HAS_DATA | + (int)SampleFlags::HAS_TS_ORIGIN | + (int)SampleFlags::HAS_SEQUENCE; + smps[0]->length = 0; + + // Solve OpenDSS file. + SolutionI(0, 0); + + smps[0]->length = extractMonitorData(smps); + smps[0]->sequence = smps[0]->data[0].f; + + writingTurn = true; + pthread_cond_signal(&cv); + pthread_mutex_unlock(&mutex); + + return 1; +} + +int OpenDSS::_write(struct Sample *smps[], unsigned cnt) { + // Wait until reading is done. + pthread_mutex_lock(&mutex); + while (!writingTurn) { + pthread_cond_wait(&cv, &mutex); + } + + ts = smps[0]->ts.origin; + + int i = 0; + for (auto &ele : dataIn) { + double (*func)(int, double); + switch (ele.type) { + case ElementType::generator: + func = GeneratorsF; + GeneratorsS(1, ele.name.data()); + break; + case ElementType::load: + func = DSSLoadsF; + DSSLoadsS(1, ele.name.data()); + break; + case ElementType::isource: + func = IsourceF; + IsourceS(1, ele.name.data()); + break; + default: + throw SystemError("Invalid element type"); + } + + for (auto &m : ele.mode) { + func(m, smps[0]->data[i].f); + i++; + } + } + + writingTurn = false; + pthread_cond_signal(&cv); + pthread_mutex_unlock(&mutex); + + return cnt; +} + +int OpenDSS::stop() { + dssPutCommand("CloseDI"); // Close OpenDSS. + + return Node::stop(); +} + +// Register node. +static char n[] = "opendss"; +static char d[] = "Interface to OpenDSS, EPRI's Distribution System Simulator"; +static NodePlugin + p; diff --git a/lib/super_node.cpp b/lib/super_node.cpp index bfdd49a41..3f91f2588 100644 --- a/lib/super_node.cpp +++ b/lib/super_node.cpp @@ -40,8 +40,8 @@ SuperNode::SuperNode() web(), #endif #endif - priority(0), affinity(0), hugepages(DEFAULT_NR_HUGEPAGES), statsRate(1.0), - task(), started(time_now()) { + seed(0), priority(0), affinity(0), hugepages(DEFAULT_NR_HUGEPAGES), + statsRate(1.0), task(), started(time_now()) { int ret; char hname[128]; @@ -82,14 +82,14 @@ void SuperNode::parse(json_t *root) { int stop = -1; - ret = - json_unpack_ex(root, &err, 0, - "{ s?: F, s?: o, s?: o, s?: o, s?: o, s?: i, s?: i, s?: " - "i, s?: b, s?: s }", - "stats", &statsRate, "http", &json_http, "logging", - &json_logging, "nodes", &json_nodes, "paths", &json_paths, - "hugepages", &hugepages, "affinity", &affinity, "priority", - &priority, "idle_stop", &stop, "uuid", &uuid_str); + ret = json_unpack_ex(root, &err, 0, + "{ s?: F, s?: o, s?: o, s?: o, s?: o, s?: i, s?: i, s?: " + "i, s?: b, s?: s, s?: i }", + "stats", &statsRate, "http", &json_http, "logging", + &json_logging, "nodes", &json_nodes, "paths", + &json_paths, "hugepages", &hugepages, "affinity", + &affinity, "priority", &priority, "idle_stop", &stop, + "uuid", &uuid_str, "seed", &seed); if (ret) throw ConfigError(root, err, "node-config", "Unpacking top-level config failed"); @@ -328,6 +328,8 @@ void SuperNode::prepare() { void SuperNode::start() { assert(state == State::PREPARED); + srand(seed); + #ifdef WITH_API api.start(); #endif diff --git a/packaging/deps.sh b/packaging/deps.sh index 653817607..d8f86f2a8 100644 --- a/packaging/deps.sh +++ b/packaging/deps.sh @@ -93,6 +93,8 @@ export PKG_CONFIG_PATH # GIT_OPTS+=" -c http.sslVerify=false" # fi +SOURCE_DIR=$(realpath $(dirname "${BASH_SOURCE[0]}")) + # Build in a temporary directory TMPDIR=$(mktemp -d) @@ -482,6 +484,27 @@ if ! pkg-config "libmodbus >= 3.1.0" && \ popd fi +if ! find /usr/{local/,}{lib,bin} -name "libOpenDSSC.so" | grep -q . && + should_build "opendss" "For opendss node-type"; then + git svn clone -r 4020:4020 https://svn.code.sf.net/p/electricdss/code/trunk/VersionC OpenDSS-C + mkdir -p OpenDSS-C/build + pushd OpenDSS-C + for i in ${SOURCE_DIR}/patches/*-opendssc-*.patch; do patch --strip=1 --binary < "$i"; done + popd + pushd OpenDSS-C/build + if command -v g++-14 2>&1 >/dev/null; then + # OpenDSS rev 4020 is not compatible with GCC 15 + OPENDSS_CMAKE_OPTS="-DCMAKE_C_COMPILER=gcc-14 -DCMAKE_CXX_COMPILER=g++-14" + else + OPENDSS_CMAKE_OPTS="" + fi + cmake -DMyOutputType=DLL \ + ${OPENDSS_CMAKE_OPTS} \ + ${CMAKE_OPTS} .. + make ${MAKE_OPTS} install + popd +fi + popd >/dev/null rm -rf ${TMPDIR} diff --git a/packaging/docker/Dockerfile.debian b/packaging/docker/Dockerfile.debian index 52dedf9a0..5dfe8bf69 100644 --- a/packaging/docker/Dockerfile.debian +++ b/packaging/docker/Dockerfile.debian @@ -20,7 +20,7 @@ RUN apt-get update && \ gcc g++ \ pkg-config cmake make \ autoconf automake autogen libtool \ - texinfo git curl tar wget diffutils \ + texinfo git git-svn curl tar wget diffutils \ flex bison \ protobuf-compiler protobuf-c-compiler \ clang-format clangd @@ -53,9 +53,10 @@ RUN apt-get update && \ libnice-dev \ libmodbus-dev -# or install unpackaged dependencies from source -ADD packaging/deps.sh / -RUN bash deps.sh +# Install unpackaged dependencies from source +ADD packaging/patches /deps/patches +ADD packaging/deps.sh /deps +RUN bash /deps/deps.sh # Expose ports for HTTP and WebSocket frontend EXPOSE 80 diff --git a/packaging/docker/Dockerfile.debian-multiarch b/packaging/docker/Dockerfile.debian-multiarch index 478dfa990..a24e7c492 100644 --- a/packaging/docker/Dockerfile.debian-multiarch +++ b/packaging/docker/Dockerfile.debian-multiarch @@ -86,8 +86,9 @@ ENV RANLIB=${TRIPLET}-ranlib RUN mkdir ${PREFIX} # Install unpackaged dependencies from source -ADD packaging/deps.sh / -RUN bash deps.sh +ADD packaging/patches /deps/patches +ADD packaging/deps.sh /deps +RUN bash /deps/deps.sh # Expose ports for HTTP and WebSocket frontend EXPOSE 80 diff --git a/packaging/docker/Dockerfile.fedora b/packaging/docker/Dockerfile.fedora index c68ff0bd0..69b9e3803 100644 --- a/packaging/docker/Dockerfile.fedora +++ b/packaging/docker/Dockerfile.fedora @@ -17,7 +17,7 @@ RUN dnf -y install \ gcc-14 g++-14 \ pkgconfig cmake make \ autoconf automake autogen libtool \ - texinfo git awk git-svn curl tar \ + texinfo git awk git-svn curl tar patchutils \ flex bison \ protobuf-compiler protobuf-c-compiler \ clang-tools-extra @@ -75,8 +75,10 @@ ENV CXX=g++-14 # Add local library directory to linker paths RUN echo /usr/local/lib >> /etc/ld.so.conf -ADD packaging/deps.sh / -RUN bash deps.sh +# Install unpackaged dependencies from source +ADD packaging/patches /deps/patches +ADD packaging/deps.sh /deps +RUN bash /deps/deps.sh RUN ldconfig # Workaround for libnl3's search path for netem distributions diff --git a/packaging/docker/Dockerfile.rocky b/packaging/docker/Dockerfile.rocky index 508b74e46..eebd8f448 100644 --- a/packaging/docker/Dockerfile.rocky +++ b/packaging/docker/Dockerfile.rocky @@ -23,7 +23,7 @@ RUN dnf --allowerasing -y install \ pkgconfig cmake make \ autoconf automake libtool \ flex bison \ - texinfo git curl tar \ + texinfo git git-svn curl tar patchutils \ protobuf-compiler protobuf-c-compiler \ clang-tools-extra @@ -50,9 +50,10 @@ RUN dnf -y install \ libnice-devel \ libmodbus-devel -# or install unpackaged dependencies from source -ADD packaging/deps.sh / -RUN bash deps.sh +# Install unpackaged dependencies from source +ADD packaging/patches /deps/patches +ADD packaging/deps.sh /deps +RUN bash /deps/deps.sh # Workaround for libnl3's search path for netem distributions RUN ln -s /usr/lib64/tc /usr/lib/tc diff --git a/packaging/docker/Dockerfile.ubuntu b/packaging/docker/Dockerfile.ubuntu index 542c12cad..65009bc35 100644 --- a/packaging/docker/Dockerfile.ubuntu +++ b/packaging/docker/Dockerfile.ubuntu @@ -21,7 +21,7 @@ RUN apt-get update && \ gcc g++ \ pkg-config cmake make \ autoconf automake autogen libtool \ - texinfo git curl tar wget diffutils \ + texinfo git git-svn curl tar wget diffutils \ flex bison \ protobuf-compiler protobuf-c-compiler \ clang-format clangd \ @@ -61,9 +61,10 @@ RUN apt-get update && \ libglib2.0-dev \ libcriterion-dev -# or install unpackaged dependencies from source -ADD packaging/deps.sh / -RUN bash deps.sh +# Install unpackaged dependencies from source +ADD packaging/patches /deps/patches +ADD packaging/deps.sh /deps +RUN bash /deps/deps.sh # Expose ports for HTTP and WebSocket frontend EXPOSE 80 diff --git a/packaging/nix/opendssc.nix b/packaging/nix/opendssc.nix new file mode 100644 index 000000000..a93abf624 --- /dev/null +++ b/packaging/nix/opendssc.nix @@ -0,0 +1,46 @@ +# SPDX-FileCopyrightText: 2025 OPAL-RT Germany GmbH +# SPDX-License-Identifier: Apache-2.0 +{ + lib, + stdenv, + cmake, + fetchsvn, + libuuid, +}: +stdenv.mkDerivation { + pname = "opendssc"; + version = "10.1.0.1"; + + src = fetchsvn { + url = "https://svn.code.sf.net/p/electricdss/code/trunk/VersionC"; + rev = 4020; + hash = "sha256-9mCnjNZEQNsc75NbuUU0QHKf9bX9HsMsYKXuOJVQ+VE="; + }; + + nativeBuildInputs = [ cmake ]; + + buildInputs = [ libuuid ]; + + cmakeFlags = [ "-DMyOutputType=DLL" ]; + + enableParallelBuilding = true; + + patchFlags = [ "--binary" "--strip=1" ]; # OpenDSS is using CRLF line endings + patches = [ + ../patches/0001-opendssc-cmakelists.patch + ../patches/0002-opendssc-set-data-path-chdir.patch + ]; + + postInstall = '' + mv $out/openDSSC/bin/*.so $out/lib/ + rm -rf $out/openDSSC + ''; + + meta = { + description = "EPRI Distribution System Simulator"; + homepage = "https://sourceforge.net/projects/electricdss/"; + license = lib.licenses.bsd3; + maintainers = with lib.maintainers; [ stv0g ]; + platforms = lib.platforms.unix; + }; +} diff --git a/packaging/nix/villas.nix b/packaging/nix/villas.nix index c98d33461..134918258 100644 --- a/packaging/nix/villas.nix +++ b/packaging/nix/villas.nix @@ -27,6 +27,7 @@ withNodeModbus ? withAllNodes, withNodeMqtt ? withAllNodes, withNodeNanomsg ? withAllNodes, + withNodeOpenDSS ? withAllNodes, withNodeRedis ? withAllNodes, withNodeRtp ? withAllNodes, withNodeSocket ? withAllNodes, @@ -65,6 +66,7 @@ lua, mosquitto, nanomsg, + opendssc, openssl, pkgsBuildBuild, protobuf, @@ -85,6 +87,7 @@ stdenv.mkDerivation { "out" "dev" ]; + enableParallelBuilding = true; separateDebugInfo = true; cmakeFlags = [ ] @@ -146,6 +149,7 @@ stdenv.mkDerivation { ++ lib.optionals withNodeModbus [ libmodbus ] ++ lib.optionals withNodeMqtt [ mosquitto ] ++ lib.optionals withNodeNanomsg [ nanomsg ] + ++ lib.optionals withNodeNanomsg [ opendssc ] ++ lib.optionals withNodeRedis [ redis-plus-plus ] ++ lib.optionals withNodeRtp [ libre ] ++ lib.optionals withNodeSocket [ libnl ] diff --git a/packaging/patches/.gitattributes b/packaging/patches/.gitattributes new file mode 100644 index 000000000..6c0609d0e --- /dev/null +++ b/packaging/patches/.gitattributes @@ -0,0 +1,4 @@ +# SPDX-FileCopyrightText: 2025 OPAL-RT Germany GmbH +# SPDX-License-Identifier: Apache-2.0 + +*.patch binary diff --git a/packaging/patches/0001-opendssc-cmakelists.patch b/packaging/patches/0001-opendssc-cmakelists.patch new file mode 100644 index 000000000..85421f3c8 --- /dev/null +++ b/packaging/patches/0001-opendssc-cmakelists.patch @@ -0,0 +1,15 @@ +diff --git a/CMakeLists.txt b/CMakeLists.txt +index acb9b35..86fe53f 100644 +--- a/CMakeLists.txt ++++ b/CMakeLists.txt +@@ -414,3 +414,10 @@ foreach(_tgt ${OPENDSSC_TARGETS}) + # > + # ) + endforeach() ++ ++# Install header files ++file(GLOB_RECURSE HEADERS "*.h") ++install(FILES ${HEADERS} DESTINATION include/OpenDSSC) ++ ++# Install libklusolve_all shared library ++install(TARGETS klusolve_all) diff --git a/packaging/patches/0002-opendssc-set-data-path-chdir.patch b/packaging/patches/0002-opendssc-set-data-path-chdir.patch new file mode 100644 index 000000000..ffbd35d0f --- /dev/null +++ b/packaging/patches/0002-opendssc-set-data-path-chdir.patch @@ -0,0 +1,12 @@ +diff --git a/Common/DSSGlobals.cpp b/Common/DSSGlobals.cpp +index 38b9188..4d85e92 100644 +--- a/Common/DSSGlobals.cpp ++++ b/Common/DSSGlobals.cpp +@@ -1885,6 +1885,7 @@ namespace DSSGlobals + + StartupDirectory = GetCurrentDir() + DIRSEP_STR; + SetDataPath( GetDefaultDataDirectory() + DIRSEP_STR + ProgramName + DIRSEP_STR ); ++ ChDir(StartupDirectory); // Revert ChDir within SetDataPath + //DSS_Registry = TIniRegSave.Create( DataDirectory[ActiveActor] + "opendsscmd.ini" ); + AuxParser[ActiveActor] = new TParser(); + diff --git a/tests/integration/node-opendss.sh b/tests/integration/node-opendss.sh new file mode 100755 index 000000000..3a818b639 --- /dev/null +++ b/tests/integration/node-opendss.sh @@ -0,0 +1,123 @@ +#!/usr/bin/env bash +# +# Integration OpenDSS test using VILLASnode. +# +# Author: Jitpanu Maneeratpongsuk +# SPDX-FileCopyrightText: 2025 Institute for Automation of Complex Power Systems, RWTH Aachen University +# SPDX-License-Identifier: Apache-2.0 + +set -e + +DIR=$(mktemp -d) +pushd ${DIR} + +function finish { + popd + rm -rf ${DIR} +} +trap finish EXIT + +cat > load.dat < expect.dat < Test.DSS << EOF +Clear + +Set DefaultBaseFrequency=60 + +new object=circuit.Test basekV=12.47 phases=3 + +new transformer.t1 xhl=6 +~ wdg=1 bus=n2 conn=wye kV=12.47 kVA=6000 %r=0.5 +~ wdg=2 bus=n3 conn=wye kV=4.16 kVA=6000 %r=0.5 + +new line.line1 bus1=sourcebus bus2=n2 length=1 units=km +new line.line2 bus1=n3 bus2=n4 length=1 units=km + +new load.load1 bus1=n4 phases=3 kV=4.16 kW=5400 pf=0.9 model=1 + +new monitor.t1_p element=Transformer.t1 terminal=1 ppolar=no mode=65 + +set voltagebases=[12.47, 4.16] +calcvoltagebases + +set mode=daily +set number=1 +EOF + +cat > config.json << EOF +{ + "nodes": { + "node1": { + "type": "opendss", + "file_path" : "Test.DSS", + "in": { + "list": [ + {"name": "load1", "type": "load", "data": ["kW"]} + ] + }, + "out": { + "list": ["t1_p"] + } + }, + "node2": { + "type": "file", + "uri": "load.dat", + "in": { + "epoch_mode": "original", + "epoch": 10, + "rate": 4, + "buffer": 0 + } + } + }, + "paths": [ + { + "in": "node2", + "out": "node1" + }, + { + "in": "node1", + "hooks": [ + {"type": "print", "output": "output.dat"} + ] + } + ] +} +EOF + +VILLAS_LOG_PREFIX="[node] " \ +villas node config.json & + +# Wait for node to complete init +sleep 2 + +kill %% +wait %% + +# Send / Receive data to node +VILLAS_LOG_PREFIX="[compare] " \ +villas compare expect.dat output.dat