diff --git a/CMakeLists.txt b/CMakeLists.txt index 22c5686cd..60f4f5b1e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -77,6 +77,8 @@ find_package(re) find_package(OpenDSSC) find_package(FileSystem) find_package(Criterion) +find_package(OpalOrchestra) +find_package(LibXml2) # Check for tools find_program(PROTOBUFC_COMPILER NAMES protoc-c) @@ -161,55 +163,55 @@ else() endif() # Build options -cmake_dependent_option(WITHOUT_GPL "Build VILLASnode without any GPL code" OFF "" ON) -cmake_dependent_option(WITH_GHC_FS "Build using ghc::filesystem, a drop in replacement for std::filesystem" ON "STDCXX_FS_NOT_FOUND" OFF) -cmake_dependent_option(WITH_DEFAULTS "Defaults for non required build options" ON "" OFF) - -cmake_dependent_option(WITH_API "Build with remote control API" "${WITH_DEFAULTS}" "" OFF) -cmake_dependent_option(WITH_CLIENTS "Build client applications" "${WITH_DEFAULTS}" "TOPLEVEL_PROJECT" OFF) -cmake_dependent_option(WITH_CONFIG "Build with support for libconfig configuration syntax" "${WITH_DEFAULTS}" "LIBCONFIG_FOUND" OFF) -cmake_dependent_option(WITH_FPGA "Build with support for VILLASfpga" "${WITH_DEFAULTS}" "FOUND_FPGA_SUBMODULES" OFF) -cmake_dependent_option(WITH_GRAPHVIZ "Build with Graphviz" "${WITH_DEFAULTS}" "CGRAPH_FOUND; GVC_FOUND" OFF) -cmake_dependent_option(WITH_HOOKS "Build with support for processing hook plugins" "${WITH_DEFAULTS}" "" OFF) -cmake_dependent_option(WITH_LUA "Build with Lua" "${WITH_DEFAULTS}" "LUA_FOUND" OFF) -cmake_dependent_option(WITH_OPENMP "Build with support for OpenMP for parallel hooks" "${WITH_DEFAULTS}" "OPENMP_FOUND" OFF) -cmake_dependent_option(WITH_PLUGINS "Build plugins" "${WITH_DEFAULTS}" "TOPLEVEL_PROJECT" OFF) -cmake_dependent_option(WITH_SRC "Build executables" "${WITH_DEFAULTS}" "TOPLEVEL_PROJECT" OFF) -cmake_dependent_option(WITH_TESTS "Run tests" "${WITH_DEFAULTS}" "TOPLEVEL_PROJECT" OFF) -cmake_dependent_option(WITH_TOOLS "Build auxilary tools" "${WITH_DEFAULTS}" "TOPLEVEL_PROJECT" OFF) -cmake_dependent_option(WITH_WEB "Build with internal webserver" "${WITH_DEFAULTS}" "LIBWEBSOCKETS_FOUND" OFF) - -cmake_dependent_option(WITH_NODE_AMQP "Build with amqp node-type" "${WITH_DEFAULTS}" "RABBITMQ_C_FOUND" OFF) -cmake_dependent_option(WITH_NODE_CAN "Build with can node-type" "${WITH_DEFAULTS}" "" OFF) -cmake_dependent_option(WITH_NODE_COMEDI "Build with comedi node-type" "${WITH_DEFAULTS}" "COMEDILIB_FOUND" OFF) -cmake_dependent_option(WITH_NODE_ETHERCAT "Build with ethercat node-type" "${WITH_DEFAULTS}" "ETHERLAB_FOUND; NOT WITHOUT_GPL" OFF) -cmake_dependent_option(WITH_NODE_EXAMPLE "Build with example node-type" "${WITH_DEFAULTS}" "" OFF) -cmake_dependent_option(WITH_NODE_EXEC "Build with exec node-type" "${WITH_DEFAULTS}" "" OFF) -cmake_dependent_option(WITH_NODE_FILE "Build with file node-type" "${WITH_DEFAULTS}" "" OFF) -cmake_dependent_option(WITH_NODE_FPGA "Build with fpga node-type" "${WITH_DEFAULTS}" "WITH_FPGA" OFF) -cmake_dependent_option(WITH_NODE_IEC61850 "Build with iec61850 node-types" "${WITH_DEFAULTS}" "LIBIEC61850_FOUND; NOT WITHOUT_GPL" OFF) -cmake_dependent_option(WITH_NODE_IEC60870 "Build with iec60870 node-types" "${WITH_DEFAULTS}" "LIB60870_FOUND; NOT WITHOUT_GPL" OFF) -cmake_dependent_option(WITH_NODE_INFINIBAND "Build with infiniband node-type" "${WITH_DEFAULTS}" "IBVerbs_FOUND; RDMACM_FOUND" OFF) # Infiniband node-type is currenly broken -cmake_dependent_option(WITH_NODE_INFLUXDB "Build with influxdb node-type" "${WITH_DEFAULTS}" "" OFF) -cmake_dependent_option(WITH_NODE_KAFKA "Build with kafka node-type" "${WITH_DEFAULTS}" "RDKAFKA_FOUND" OFF) -cmake_dependent_option(WITH_NODE_LOOPBACK "Build with loopback node-type" "${WITH_DEFAULTS}" "" OFF) -cmake_dependent_option(WITH_NODE_MODBUS "Build with modbus node-type" "${WITH_DEFAULTS}" "MODBUS_FOUND" OFF) -cmake_dependent_option(WITH_NODE_MQTT "Build with mqtt node-type" "${WITH_DEFAULTS}" "MOSQUITTO_FOUND" OFF) -cmake_dependent_option(WITH_NODE_NANOMSG "Build with nanomsg node-type" "${WITH_DEFAULTS}" "NANOMSG_FOUND" OFF) -cmake_dependent_option(WITH_NODE_NGSI "Build with ngsi node-type" "${WITH_DEFAULTS}" "" OFF) -cmake_dependent_option(WITH_NODE_REDIS "Build with redis node-type" "${WITH_DEFAULTS}" "HIREDIS_FOUND; REDISPP_FOUND" OFF) -cmake_dependent_option(WITH_NODE_RTP "Build with rtp node-type" "${WITH_DEFAULTS}" "re_FOUND" OFF) -cmake_dependent_option(WITH_NODE_SHMEM "Build with shmem node-type" "${WITH_DEFAULTS}" "HAS_SEMAPHORE; HAS_MMAN" OFF) -cmake_dependent_option(WITH_NODE_SIGNAL "Build with signal node-type" "${WITH_DEFAULTS}" "" OFF) -cmake_dependent_option(WITH_NODE_SOCKET "Build with socket node-type" "${WITH_DEFAULTS}" "LIBNL3_ROUTE_FOUND" OFF) -cmake_dependent_option(WITH_NODE_STATS "Build with stats node-type" "${WITH_DEFAULTS}" "" OFF) -cmake_dependent_option(WITH_NODE_TEMPER "Build with temper node-type" "${WITH_DEFAULTS}" "LIBUSB_FOUND" OFF) -cmake_dependent_option(WITH_NODE_TEST_RTT "Build with test_rtt node-type" "${WITH_DEFAULTS}" "" OFF) -cmake_dependent_option(WITH_NODE_ULDAQ "Build with uldaq node-type" "${WITH_DEFAULTS}" "LIBULDAQ_FOUND" OFF) -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) +cmake_dependent_option(WITHOUT_GPL "Build VILLASnode without any GPL code" OFF "" ON) +cmake_dependent_option(WITH_GHC_FS "Build using ghc::filesystem, a drop in replacement for std::filesystem" ON "STDCXX_FS_NOT_FOUND" OFF) +cmake_dependent_option(WITH_DEFAULTS "Defaults for non required build options" ON "" OFF) +cmake_dependent_option(WITH_API "Build with remote control API" "${WITH_DEFAULTS}" "" OFF) +cmake_dependent_option(WITH_CLIENTS "Build client applications" "${WITH_DEFAULTS}" "TOPLEVEL_PROJECT" OFF) +cmake_dependent_option(WITH_CONFIG "Build with support for libconfig configuration syntax" "${WITH_DEFAULTS}" "LIBCONFIG_FOUND" OFF) +cmake_dependent_option(WITH_FPGA "Build with support for VILLASfpga" "${WITH_DEFAULTS}" "FOUND_FPGA_SUBMODULES" OFF) +cmake_dependent_option(WITH_GRAPHVIZ "Build with Graphviz" "${WITH_DEFAULTS}" "CGRAPH_FOUND; GVC_FOUND" OFF) +cmake_dependent_option(WITH_HOOKS "Build with support for processing hook plugins" "${WITH_DEFAULTS}" "" OFF) +cmake_dependent_option(WITH_LUA "Build with Lua" "${WITH_DEFAULTS}" "LUA_FOUND" OFF) +cmake_dependent_option(WITH_OPENMP "Build with support for OpenMP for parallel hooks" "${WITH_DEFAULTS}" "OPENMP_FOUND" OFF) +cmake_dependent_option(WITH_PLUGINS "Build plugins" "${WITH_DEFAULTS}" "TOPLEVEL_PROJECT" OFF) +cmake_dependent_option(WITH_SRC "Build executables" "${WITH_DEFAULTS}" "TOPLEVEL_PROJECT" OFF) +cmake_dependent_option(WITH_TESTS "Run tests" "${WITH_DEFAULTS}" "TOPLEVEL_PROJECT" OFF) +cmake_dependent_option(WITH_TOOLS "Build auxilary tools" "${WITH_DEFAULTS}" "TOPLEVEL_PROJECT" OFF) +cmake_dependent_option(WITH_WEB "Build with internal webserver" "${WITH_DEFAULTS}" "LIBWEBSOCKETS_FOUND" OFF) + +cmake_dependent_option(WITH_NODE_AMQP "Build with amqp node-type" "${WITH_DEFAULTS}" "RABBITMQ_C_FOUND" OFF) +cmake_dependent_option(WITH_NODE_CAN "Build with can node-type" "${WITH_DEFAULTS}" "" OFF) +cmake_dependent_option(WITH_NODE_COMEDI "Build with comedi node-type" "${WITH_DEFAULTS}" "COMEDILIB_FOUND" OFF) +cmake_dependent_option(WITH_NODE_ETHERCAT "Build with ethercat node-type" "${WITH_DEFAULTS}" "ETHERLAB_FOUND; NOT WITHOUT_GPL" OFF) +cmake_dependent_option(WITH_NODE_EXAMPLE "Build with example node-type" "${WITH_DEFAULTS}" "" OFF) +cmake_dependent_option(WITH_NODE_EXEC "Build with exec node-type" "${WITH_DEFAULTS}" "" OFF) +cmake_dependent_option(WITH_NODE_FILE "Build with file node-type" "${WITH_DEFAULTS}" "" OFF) +cmake_dependent_option(WITH_NODE_FPGA "Build with fpga node-type" "${WITH_DEFAULTS}" "WITH_FPGA" OFF) +cmake_dependent_option(WITH_NODE_IEC61850 "Build with iec61850 node-types" "${WITH_DEFAULTS}" "LIBIEC61850_FOUND; NOT WITHOUT_GPL" OFF) +cmake_dependent_option(WITH_NODE_IEC60870 "Build with iec60870 node-types" "${WITH_DEFAULTS}" "LIB60870_FOUND; NOT WITHOUT_GPL" OFF) +cmake_dependent_option(WITH_NODE_INFINIBAND "Build with infiniband node-type" "${WITH_DEFAULTS}" "IBVerbs_FOUND; RDMACM_FOUND" OFF) # Infiniband node-type is currenly broken +cmake_dependent_option(WITH_NODE_INFLUXDB "Build with influxdb node-type" "${WITH_DEFAULTS}" "" OFF) +cmake_dependent_option(WITH_NODE_KAFKA "Build with kafka node-type" "${WITH_DEFAULTS}" "RDKAFKA_FOUND" OFF) +cmake_dependent_option(WITH_NODE_LOOPBACK "Build with loopback node-type" "${WITH_DEFAULTS}" "" OFF) +cmake_dependent_option(WITH_NODE_MODBUS "Build with modbus node-type" "${WITH_DEFAULTS}" "MODBUS_FOUND" OFF) +cmake_dependent_option(WITH_NODE_MQTT "Build with mqtt node-type" "${WITH_DEFAULTS}" "MOSQUITTO_FOUND" OFF) +cmake_dependent_option(WITH_NODE_NANOMSG "Build with nanomsg node-type" "${WITH_DEFAULTS}" "NANOMSG_FOUND" OFF) +cmake_dependent_option(WITH_NODE_NGSI "Build with ngsi node-type" "${WITH_DEFAULTS}" "" OFF) +cmake_dependent_option(WITH_NODE_OPAL_ORCHESTRA "Build with the opal-orchestra node-type" "${WITH_DEFAULTS}" "OpalOrchestra_FOUND; LibXml2_FOUND" OFF) +cmake_dependent_option(WITH_NODE_REDIS "Build with redis node-type" "${WITH_DEFAULTS}" "HIREDIS_FOUND; REDISPP_FOUND" OFF) +cmake_dependent_option(WITH_NODE_RTP "Build with rtp node-type" "${WITH_DEFAULTS}" "re_FOUND" OFF) +cmake_dependent_option(WITH_NODE_SHMEM "Build with shmem node-type" "${WITH_DEFAULTS}" "HAS_SEMAPHORE; HAS_MMAN" OFF) +cmake_dependent_option(WITH_NODE_SIGNAL "Build with signal node-type" "${WITH_DEFAULTS}" "" OFF) +cmake_dependent_option(WITH_NODE_SOCKET "Build with socket node-type" "${WITH_DEFAULTS}" "LIBNL3_ROUTE_FOUND" OFF) +cmake_dependent_option(WITH_NODE_STATS "Build with stats node-type" "${WITH_DEFAULTS}" "" OFF) +cmake_dependent_option(WITH_NODE_TEMPER "Build with temper node-type" "${WITH_DEFAULTS}" "LIBUSB_FOUND" OFF) +cmake_dependent_option(WITH_NODE_TEST_RTT "Build with test_rtt node-type" "${WITH_DEFAULTS}" "" OFF) +cmake_dependent_option(WITH_NODE_ULDAQ "Build with uldaq node-type" "${WITH_DEFAULTS}" "LIBULDAQ_FOUND" OFF) +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 "") diff --git a/cmake/FindOpalOrchestra.cmake b/cmake/FindOpalOrchestra.cmake new file mode 100644 index 000000000..72ed54422 --- /dev/null +++ b/cmake/FindOpalOrchestra.cmake @@ -0,0 +1,29 @@ +# CMakeLists.txt. +# +# Author: Steffen Vogel +# SPDX-FileCopyrightText: 2025, OPAL-RT Germany GmbH +# SPDX-License-Identifier: Apache-2.0 + +find_path(OPAL_ORCHESTRA_INCLUDE_DIR + NAMES RTAPI.h + PATHS + /usr/opalrt/common/bin +) + +find_library(OPAL_ORCHESTRA_LIBRARY + NAMES OpalOrchestra + PATHS + /usr/opalrt/common/include +) + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(OpalOrchestra DEFAULT_MSG OPAL_ORCHESTRA_LIBRARY OPAL_ORCHESTRA_INCLUDE_DIR) + +mark_as_advanced(OPAL_ORCHESTRA_INCLUDE_DIR OPAL_ORCHESTRA_LIBRARY) + +set(OPAL_ORCHESTRA_LIBRARIES ${OPAL_ORCHESTRA_LIBRARY}) +set(OPAL_ORCHESTRA_INCLUDE_DIRS ${OPAL_ORCHESTRA_INCLUDE_DIR}) + +add_library(OpalOrchestra INTERFACE) +target_link_libraries(OpalOrchestra INTERFACE ${OPAL_ORCHESTRA_LIBRARY}) +target_include_directories(OpalOrchestra INTERFACE ${OPAL_ORCHESTRA_INCLUDE_DIR}) diff --git a/common/include/villas/graph/directed.hpp b/common/include/villas/graph/directed.hpp index 9b4ea8328..e74c3a9d5 100644 --- a/common/include/villas/graph/directed.hpp +++ b/common/include/villas/graph/directed.hpp @@ -10,11 +10,11 @@ #include #include #include -#include #include #include #include #include +#include #include #include @@ -245,8 +245,8 @@ class DirectedGraph { VertexIdentifier lastVertexId; EdgeIdentifier lastEdgeId; - std::map> vertices; - std::map> edges; + std::unordered_map> vertices; + std::unordered_map> edges; Logger logger; }; diff --git a/common/include/villas/memory.hpp b/common/include/villas/memory.hpp index 922eb516d..8434d6e51 100644 --- a/common/include/villas/memory.hpp +++ b/common/include/villas/memory.hpp @@ -275,7 +275,8 @@ class HostDmaRam { static HostDmaRamAllocator &getAllocator(int num = 0); private: - static std::map> allocators; + static std::unordered_map> + allocators; static std::string getUdmaBufName(int num); diff --git a/common/include/villas/memory_manager.hpp b/common/include/villas/memory_manager.hpp index 71ede716e..fee4fdf14 100644 --- a/common/include/villas/memory_manager.hpp +++ b/common/include/villas/memory_manager.hpp @@ -8,9 +8,9 @@ #pragma once #include -#include #include #include +#include #include #include @@ -214,7 +214,7 @@ class MemoryManager { MemoryGraph memoryGraph; // Cache mapping of names to address space ids for fast lookup - std::map addrSpaceLookup; + std::unordered_map addrSpaceLookup; // Logger for universal access in this class Logger logger; diff --git a/common/include/villas/popen.hpp b/common/include/villas/popen.hpp index 95994f59d..f47881eba 100644 --- a/common/include/villas/popen.hpp +++ b/common/include/villas/popen.hpp @@ -8,10 +8,10 @@ #pragma once #include -#include #include #include #include +#include #include #include @@ -24,7 +24,7 @@ class Popen { public: using arg_list = std::vector; - using env_map = std::map; + using env_map = std::unordered_map; using char_type = char; using stdio_buf = __gnu_cxx::stdio_filebuf; diff --git a/common/lib/log.cpp b/common/lib/log.cpp index 572a5065a..b59f5c715 100644 --- a/common/lib/log.cpp +++ b/common/lib/log.cpp @@ -7,7 +7,7 @@ #include #include -#include +#include #include #include @@ -19,7 +19,7 @@ using namespace villas; -static std::map levelNames = { +static std::unordered_map levelNames = { {spdlog::level::trace, "trc"}, {spdlog::level::debug, "dbg"}, {spdlog::level::info, "info"}, {spdlog::level::warn, "warn"}, {spdlog::level::err, "err"}, {spdlog::level::critical, "crit"}, diff --git a/common/lib/memory.cpp b/common/lib/memory.cpp index 8605b54aa..9750194b3 100644 --- a/common/lib/memory.cpp +++ b/common/lib/memory.cpp @@ -155,7 +155,7 @@ HostRam::HostRamAllocator::HostRamAllocator() }; } -std::map> +std::unordered_map> HostDmaRam::allocators; HostDmaRam::HostDmaRamAllocator::HostDmaRamAllocator(int num) diff --git a/doc/openapi/components/schemas/config/node_obj.yaml b/doc/openapi/components/schemas/config/node_obj.yaml index 6025e4857..3363db42d 100644 --- a/doc/openapi/components/schemas/config/node_obj.yaml +++ b/doc/openapi/components/schemas/config/node_obj.yaml @@ -40,6 +40,7 @@ discriminator: nanomsg: nodes/_nanomsg.yaml ngsi: nodes/_ngsi.yaml opendss: nodes/_opendss.yaml + opal.orchestra: nodes/_opal_orchestra.yaml redis: nodes/_redis.yaml rtp: nodes/_rtp.yaml shmem: nodes/_shmem.yaml diff --git a/doc/openapi/components/schemas/config/nodes/_opal_orchestra.yaml b/doc/openapi/components/schemas/config/nodes/_opal_orchestra.yaml new file mode 100644 index 000000000..b56efd75d --- /dev/null +++ b/doc/openapi/components/schemas/config/nodes/_opal_orchestra.yaml @@ -0,0 +1,7 @@ +# yaml-language-server: $schema=http://json-schema.org/draft-07/schema +# SPDX-FileCopyrightText: 2025 OPAL-RT Germany GmbH +# SPDX-License-Identifier: Apache-2.0 +--- +allOf: +- $ref: ../node_obj.yaml +- $ref: opal_orchestra.yaml diff --git a/doc/openapi/components/schemas/config/nodes/opal_orchestra.yaml b/doc/openapi/components/schemas/config/nodes/opal_orchestra.yaml new file mode 100644 index 000000000..302462e29 --- /dev/null +++ b/doc/openapi/components/schemas/config/nodes/opal_orchestra.yaml @@ -0,0 +1,77 @@ +# yaml-language-server: $schema=http://json-schema.org/draft-07/schema +# SPDX-FileCopyrightText: 2025 OPAL-RT Germany GmbH +# SPDX-License-Identifier: Apache-2.0 +--- +allOf: +- type: object + properties: + domain: + type: string + description: >- + The name of the domain to which the connection is requested. This domain must exist in the DDF read by an RT-LAB subsystem. + + synchronous: + type: boolean + description: >- + Determines whether domain participants exchange simulation data synchronously or asynchronously. + + states: + type: boolean + + connection: + $ref: ./opal_orchestra_connection.yaml + + ddf: + type: string + description: >- + The path to the DDF file that describes the data exchanged in the specified domain. + + connect_timeout: + $ref: ../../duration.yaml + default: 5s + description: >- + The duration after which a failed connection attempt times out. + + flag_delay: + $ref: ../../duration.yaml + default: 0s + description: >- + Forces the local Orchestra communication to be made with flags instead of semaphores when using an external communication process. + Flags are recommended for better performance: they are faster but also more CPU-consuming. + + flag_delay_tool: + $ref: ../../duration.yaml + description: >- + Forces the local Orchestra communication to be made with flags instead of semaphores when using an external communication process. + Flags are recommended for better performance: they are faster but also more CPU-consuming. + + skip_wait_to_go: + type: boolean + default: false + description: >- + Sets the WaitToGo setting of the model. + When true, VILLASnode ignores the WaitToGo during the connection step. + When false, VILLASnode performs the WaitToGo during the connection step. + + ddf_overwrite: + type: boolean + default: false + description: >- + If true, the DDF file provided in the 'dff' setting will be overwriting with settings and signals from the VILLASnode configuration. + + ddf_overwrite_only: + type: boolean + default: false + description: >- + If true, VILLASnode will overwrite the file provided in the 'ddf' setting, and terminate immediately afterwards. + + rate: + type: number + default: 1 + description: >- + In asynchronous mode (see 'synchronous' setting), this rate defines how often per second the data exchange with the Orchestra domain takes place. + + required: + - domain +- $ref: ../node_signals.yaml +- $ref: ../node.yaml diff --git a/doc/openapi/components/schemas/config/nodes/opal_orchestra_connection.yaml b/doc/openapi/components/schemas/config/nodes/opal_orchestra_connection.yaml new file mode 100644 index 000000000..9f3588499 --- /dev/null +++ b/doc/openapi/components/schemas/config/nodes/opal_orchestra_connection.yaml @@ -0,0 +1,18 @@ +# yaml-language-server: $schema=http://json-schema.org/draft-07/schema +# SPDX-FileCopyrightText: 2025 OPAL-RT Germany GmbH +# SPDX-License-Identifier: Apache-2.0 +--- +type: object +description: | + Configuration of the connection to the OPAL-RT Orchestra framework. +required: +- type +properties: + type: + type: string +discriminator: + propertyName: type + mapping: + local: opal_orchestra_connection_local.yaml + remote: opal_orchestra_connection_remote.yaml + dolphin: opal_orchestra_connection_dolphin.yaml diff --git a/doc/openapi/components/schemas/config/nodes/opal_orchestra_connection_dolphin.yaml b/doc/openapi/components/schemas/config/nodes/opal_orchestra_connection_dolphin.yaml new file mode 100644 index 000000000..18582a276 --- /dev/null +++ b/doc/openapi/components/schemas/config/nodes/opal_orchestra_connection_dolphin.yaml @@ -0,0 +1,23 @@ +# yaml-language-server: $schema=http://json-schema.org/draft-07/schema +# SPDX-FileCopyrightText: 2025 OPAL-RT Germany GmbH +# SPDX-License-Identifier: Apache-2.0 +--- +type: object +required: +- node_id_framework +- segment_id +properties: + node_id_framework: + type: integer + min: 4 + max: 4096 + description: >- + Node ID for Dolphin node which hosts the Orchestra framework. + + segment_id: + type: integer + min: 1 + max: 65535 + description: >- + Segment ID used to uniquely identify the framework domain. + Note that another segment ID is automatically calculated outside of this range to identify the client segment. diff --git a/doc/openapi/components/schemas/config/nodes/opal_orchestra_connection_local.yaml b/doc/openapi/components/schemas/config/nodes/opal_orchestra_connection_local.yaml new file mode 100644 index 000000000..824d98c81 --- /dev/null +++ b/doc/openapi/components/schemas/config/nodes/opal_orchestra_connection_local.yaml @@ -0,0 +1,50 @@ +# yaml-language-server: $schema=http://json-schema.org/draft-07/schema +# SPDX-FileCopyrightText: 2025 OPAL-RT Germany GmbH +# SPDX-License-Identifier: Apache-2.0 +--- +type: object +properties: + extcomm: + type: string + default: none + enum: + - udp + - tcp + - none + description: Type of external communication protocol helper which should be started. + + addr_framework: + type: string + min: 0 + max: 65535 + description: >- + The IP address of the target on which the framework is running. + + port_framework: + type: integer + min: 0 + max: 65535 + description: >- + The port on which the framework will be reachable. + + nic_framework: + type: string + description: >- + The network interface that the framework will use to communicate with the client. + + nic_client: + type: string + description: >- + The network interface that the client will use to communicate with the framework. + + core_framework: + type: integer + min: 0 + description: >- + The core on which the tool of the framework is running. The index starts at 0. + + core_client: + type: integer + min: 0 + description: >- + The core on which the tool of the client is running. The index starts at 0. diff --git a/doc/openapi/components/schemas/config/nodes/opal_orchestra_connection_remote.yaml b/doc/openapi/components/schemas/config/nodes/opal_orchestra_connection_remote.yaml new file mode 100644 index 000000000..0a4487a4a --- /dev/null +++ b/doc/openapi/components/schemas/config/nodes/opal_orchestra_connection_remote.yaml @@ -0,0 +1,20 @@ +# yaml-language-server: $schema=http://json-schema.org/draft-07/schema +# SPDX-FileCopyrightText: 2025 OPAL-RT Germany GmbH +# SPDX-License-Identifier: Apache-2.0 +--- +type: object +required: +- card +- pci_index +properties: + card: + type: string + example: VMIPCI5565-64M + description: >- + Type of reflective memory card used for a remote connection. + + pci_index: + type: integer + min: 1 + description: >- + PCI index that corresponds to the communication card used for remote connection. diff --git a/etc/examples/nodes/opal_orchestra.conf b/etc/examples/nodes/opal_orchestra.conf new file mode 100644 index 000000000..2e755f700 --- /dev/null +++ b/etc/examples/nodes/opal_orchestra.conf @@ -0,0 +1,106 @@ +# SPDX-FileCopyrightText: 2025, OPAL-RT Germany GmbH +# SPDX-License-Identifier: Apache-2.0 + +stats = 1 + +nodes = { + domain1 = { + type = "opal.orchestra" + + # Path to the OPAL-RT Orchestra Data Defintion XML file (DDF). + ddf = "orchestra.xml" + + # Enable to overwrite the DDF file. + # This is useful when you want to generate the DDF file from the configuration file + # for importing it into RT-LAB or HYPERSIM. + ddf_overwrite = true + + # Orchestra domain name. + domain = "domain1" + + # Connection timeout. In seconds. + connect_timeout = "2s" + + # Define the delay to wait when using flag synchronisation (XHP). + # This will call the system function usleep and free the CPU. + # In micro-seconds. + flag_delay = "5us" + + # Force the local Orchestra communication to be made with flag instead of semaphore when using an external communication process. + # Define the delay to wait, this will call the system function usleep and free the CPU. + # In micro-seconds. + flag_delay_tool = "5us" + skip_wait_to_go = false + + rate = 500.0; + + # The following parameters are used to generate the Orchestra DDF XML file. + synchronous = false + states = false + multiple_publish_allowed = false + + connection = { + # One of: local, remote or dolphin + type = "local" + + # For 'local' + extcomm = "udp"; + addr_framework = "10.168.13.5"; + port_framework = 10000 + core_framework = 0 + core_client = 0 + nic_framework = "eno2" + nic_client = "eno1" + + # For 'remote' + card = "test" + pci_index = 0 + + # For 'dolphin' + node_id_framework = 0 + segment_id = 0 + } + + in = { + hooks = ( + { type = "stats" }, + { type = "print" } + ) + + signals = ( + { + name = "pub_signal1", + type = "float" + + orchestra_name = "pub_signal_float" + orchestra_type = "float64" + orchestra_index = 0 + }, + { + name = "pub_signal2", + type = "float" + + orchestra_name = "pub_signal_float" + orchestra_type = "float64" + }, + { name = "signal_float", orchestra_name = "some_bus/signal_float", orchestra_type = "float64", orchestra_index = 2 }, + { name = "signal_bool", orchestra_name = "some_bus/signal_bool", orchestra_type = "boolean" }, + { name = "signal_uint8", orchestra_name = "some_bus/some_nested_bus/signal_uint8", orchestra_type = "unsigned int8" }, + { name = "signal_uint8_2", orchestra_type = "unsigned int8" } + ) + } + + out = { + signals = ( + { name="pub_signal_float", init = 1.2, orchestra_name = "sub_signal_float", type = "float" } + ) + } + } +} + +paths = ( + { + in = "domain1" + out = "domain1" + } +) diff --git a/flake.nix b/flake.nix index 62298fa28..22f15e472 100644 --- a/flake.nix +++ b/flake.nix @@ -17,7 +17,11 @@ }; outputs = - { self, nixpkgs, ... }: + { + self, + nixpkgs, + ... + }: let inherit (nixpkgs) lib; @@ -106,6 +110,7 @@ # Third-party dependencies opendssc = pkgs.callPackage (nixDir + "/opendssc.nix") { }; + orchestra = pkgs.callPackage (nixDir + "/orchestra.nix") { }; }; in { diff --git a/fpga/include/villas/fpga/card.hpp b/fpga/include/villas/fpga/card.hpp index 153aa1213..e62f7c47c 100644 --- a/fpga/include/villas/fpga/card.hpp +++ b/fpga/include/villas/fpga/card.hpp @@ -49,7 +49,8 @@ class Card { protected: // Keep a map of already mapped memory blocks - std::map> + std::unordered_map> memoryBlocksMapped; Logger logger; diff --git a/fpga/include/villas/fpga/core.hpp b/fpga/include/villas/fpga/core.hpp index 0210d5fed..16cb40142 100644 --- a/fpga/include/villas/fpga/core.hpp +++ b/fpga/include/villas/fpga/core.hpp @@ -11,8 +11,8 @@ #pragma once #include -#include #include +#include #include #include @@ -194,16 +194,18 @@ class Core { IpIdentifier id; // All interrupts of this IP with their associated interrupt controller - std::map irqs; + std::unordered_map irqs; // Cached translations from the process address space to each memory block - std::map addressTranslations; + std::unordered_map addressTranslations; // Lookup for IP's slave address spaces (= memory blocks) - std::map slaveAddressSpaces; + std::unordered_map + slaveAddressSpaces; // AXI bus master interfaces to access memory somewhere - std::map busMasterInterfaces; + std::unordered_map + busMasterInterfaces; size_t baseaddr = 0; }; diff --git a/fpga/include/villas/fpga/ips/pcie.hpp b/fpga/include/villas/fpga/ips/pcie.hpp index 85960bf8d..b892daf05 100644 --- a/fpga/include/villas/fpga/ips/pcie.hpp +++ b/fpga/include/villas/fpga/ips/pcie.hpp @@ -38,8 +38,8 @@ class AxiPciExpressBridge : public Core { uintptr_t translation; }; - std::map axiToPcieTranslations; - std::map pcieToAxiTranslations; + std::unordered_map axiToPcieTranslations; + std::unordered_map pcieToAxiTranslations; }; class XDmaBridge : public AxiPciExpressBridge { diff --git a/fpga/include/villas/fpga/ips/switch.hpp b/fpga/include/villas/fpga/ips/switch.hpp index c55a4af56..0c90481f4 100644 --- a/fpga/include/villas/fpga/ips/switch.hpp +++ b/fpga/include/villas/fpga/ips/switch.hpp @@ -10,7 +10,7 @@ #pragma once -#include +#include #include @@ -45,7 +45,7 @@ class AxiStreamSwitch : public Node { XAxis_Switch xSwitch; XAxis_Switch_Config xConfig; - std::map portMapping; + std::unordered_map portMapping; }; class AxiStreamSwitchFactory : NodeFactory { diff --git a/fpga/include/villas/fpga/node.hpp b/fpga/include/villas/fpga/node.hpp index 2224cbc34..9a5c87a0c 100644 --- a/fpga/include/villas/fpga/node.hpp +++ b/fpga/include/villas/fpga/node.hpp @@ -10,8 +10,8 @@ #pragma once -#include #include +#include #include #include @@ -76,7 +76,7 @@ class Node : public virtual Core { return *portsMaster.at(name); } - const std::map> & + const std::unordered_map> & getMasterPorts() const { return portsMaster; } @@ -85,7 +85,7 @@ class Node : public virtual Core { return *portsSlave.at(name); } - const std::map> & + const std::unordered_map> & getSlavePorts() const { return portsSlave; } @@ -128,8 +128,8 @@ class Node : public virtual Core { std::pair getLoopbackPorts() const; protected: - std::map> portsMaster; - std::map> portsSlave; + std::unordered_map> portsMaster; + std::unordered_map> portsSlave; static StreamGraph streamGraph; }; diff --git a/fpga/lib/node.cpp b/fpga/lib/node.cpp index 43b57bf25..e0785e23a 100644 --- a/fpga/lib/node.cpp +++ b/fpga/lib/node.cpp @@ -5,8 +5,8 @@ * SPDX-License-Identifier: Apache-2.0 */ -#include #include +#include #include diff --git a/include/villas/api/response.hpp b/include/villas/api/response.hpp index 2fc9e3ec3..1ee690363 100644 --- a/include/villas/api/response.hpp +++ b/include/villas/api/response.hpp @@ -7,7 +7,7 @@ #pragma once -#include +#include #include @@ -53,7 +53,7 @@ class Response { int code; std::string contentType; - std::map headers; + std::unordered_map headers; }; class JsonResponse : public Response { diff --git a/include/villas/nodes/iec61850_goose.hpp b/include/villas/nodes/iec61850_goose.hpp index 783aafd62..22304b038 100644 --- a/include/villas/nodes/iec61850_goose.hpp +++ b/include/villas/nodes/iec61850_goose.hpp @@ -11,11 +11,11 @@ #include #include #include -#include #include #include #include #include +#include #include #include @@ -164,7 +164,7 @@ class GooseNode : public Node { CQueueSignalled queue; Pool pool; - std::map contexts; + std::unordered_map contexts; std::vector mappings; std::string interface_id; std::string local_address; @@ -244,8 +244,9 @@ class GooseNode : public Node { void parseInput(json_t *json); void parseSessionKey(json_t *json); void parseSubscriber(json_t *json, SubscriberConfig &sc); - void parseSubscribers(json_t *json, - std::map &ctx); + void + parseSubscribers(json_t *json, + std::unordered_map &ctx); void parseInputSignals(json_t *json, std::vector &mappings); void parseOutput(json_t *json); diff --git a/include/villas/nodes/opal_orchestra/ddf.hpp b/include/villas/nodes/opal_orchestra/ddf.hpp new file mode 100644 index 000000000..cc41b9888 --- /dev/null +++ b/include/villas/nodes/opal_orchestra/ddf.hpp @@ -0,0 +1,154 @@ +/* OPAL-RT Orchestra Data Definition File (DDF) + * + * Author: Steffen Vogel + * + * SPDX-FileCopyrightText: 2025, OPAL-RT Germany GmbH + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include +#include +#include +#include +#include + +#include + +#include +#include +#include +#include + +namespace villas { +namespace node { +namespace orchestra { + +class Item { +public: + virtual void toXml(xmlNode *parent, bool withDefault) const = 0; +}; + +class DataItem : public Item { +public: + // Config-time members. + std::string name; + SignalType type; + unsigned short length; + double defaultValue; + + explicit DataItem(std::string name) : name(std::move(name)) {} + + static const unsigned int IDENTIFIER_NAME_LENGTH = 64; + + void toXml(xmlNode *parent, bool withDefault) const override; +}; + +class BusItem : public Item { +public: + std::unordered_map> items; + std::string name; + + explicit BusItem(std::string name) : items(), name(std::move(name)) {} + + std::shared_ptr upsertItem(std::string_view path, bool &inserted); + + void toXml(xmlNode *parent, bool withDefault) const override; +}; + +class DataSet { +public: + std::unordered_map> items; + std::string name; + + explicit DataSet(std::string name) : items(), name(std::move(name)) {} + + std::shared_ptr upsertItem(std::string_view path, bool &inserted); + + void toXml(xmlNode *parent, bool withDefault) const; +}; + +class Connection { +public: + virtual void toXml(xmlNode *domain) const = 0; + virtual std::string getDetails() const = 0; + virtual void parse(json_t *json) = 0; + + static std::shared_ptr fromJson(json_t *json); +}; + +class ConnectionLocal : public Connection { +public: + std::optional extcomm; + std::optional addressFramework; + std::optional portFramework; + std::optional nicFramework; + std::optional nicClient; + std::optional coreFramework; + std::optional coreClient; + + std::string getDetails() const override; + + void toXml(xmlNode *domain) const override; + + void parse(json_t *json) override; +}; + +class ConnectionRemote : public Connection { +public: + std::string name; // The reflective memory card name. + int pciIndex; // The index of the card attributed when the PCI bus is scanned. If there is only one card, this index must be 0. + + std::string getDetails() const override; + + void toXml(xmlNode *domain) const override; + + void parse(json_t *json) override; +}; + +class ConnectionDolphin : public Connection { +public: + int nodeIdFramework; + int segmentId; + + std::string getDetails() const override; + + void toXml(xmlNode *domain) const override; + + void parse(json_t *json) override; +}; + +class Domain { +public: + std::string name; + std::shared_ptr connection; + bool synchronous; + bool states; + bool multiplePublishAllowed; + + DataSet + publish; // Signals published by the Orchestra framework to VILLASnode. + DataSet + subscribe; // Signals subscribed by VILLASnode from the Orchestra framework. + + Domain() + : name("villas-node"), connection(new ConnectionLocal{}), + synchronous(false), states(false), multiplePublishAllowed(false), + publish("PUBLISH"), subscribe("SUBSCRIBE") {} + + void toXml(xmlNode *parent) const; +}; + +class DataDefinitionFile { +public: + std::vector domains; + + xmlNode *toXml() const; + + void writeToFile(const std::filesystem::path &filename) const; +}; + +} // namespace orchestra +} // namespace node +} // namespace villas diff --git a/include/villas/nodes/opal_orchestra/error.hpp b/include/villas/nodes/opal_orchestra/error.hpp new file mode 100644 index 000000000..78e593298 --- /dev/null +++ b/include/villas/nodes/opal_orchestra/error.hpp @@ -0,0 +1,37 @@ +/* Helpers for RTAPI error handling. + * + * Author: Steffen Vogel + * + * SPDX-FileCopyrightText: 2025, OPAL-RT Germany GmbH + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include + +extern "C" { +#include +} + +#include + +namespace villas { +namespace node { +namespace orchestra { + +class RTError : public RuntimeError { + +public: + template + RTError(RTAPIReturn_t rc, fmt::format_string fmt, Args &&...args) + : RuntimeError("{}: {}", fmt::format(fmt, std::forward(args)...), + RTGetErrorMessage(rc)), + returnCode(rc) {} + + RTAPIReturn_t returnCode; +}; + +} // namespace orchestra +} // namespace node +} // namespace villas diff --git a/include/villas/nodes/opal_orchestra/locks.hpp b/include/villas/nodes/opal_orchestra/locks.hpp new file mode 100644 index 000000000..bd235acd9 --- /dev/null +++ b/include/villas/nodes/opal_orchestra/locks.hpp @@ -0,0 +1,48 @@ +/* Helpers for RTAPI locking primitives + * + * Author: Steffen Vogel + * + * SPDX-FileCopyrightText: 2025, OPAL-RT Germany GmbH + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include + +#include + +extern "C" { +#include +} + +namespace villas { +namespace node { +namespace orchestra { + +class RTConnectionLockGuard : std::lock_guard { +public: + RTConnectionLockGuard(unsigned int connectionKey); +}; + +template +class RTLockGuard : RTConnectionLockGuard { +public: + RTLockGuard(unsigned int connectionKey) + : RTConnectionLockGuard(connectionKey) { + + auto ret = lock(); + if (ret != RTAPI_SUCCESS) { + throw RTError(ret, "Failed to lock Orchestra"); + } + } + + ~RTLockGuard() { unlock(); } +}; + +using RTPublishLockGuard = RTLockGuard; +using RTSubscribeLockGuard = RTLockGuard; + +} // namespace orchestra +} // namespace node +} // namespace villas diff --git a/include/villas/nodes/opal_orchestra/signal.hpp b/include/villas/nodes/opal_orchestra/signal.hpp new file mode 100644 index 000000000..be5dcab6a --- /dev/null +++ b/include/villas/nodes/opal_orchestra/signal.hpp @@ -0,0 +1,65 @@ +/* Helpers for RTAPI signal type conversion. + * + * Author: Steffen Vogel + * + * SPDX-FileCopyrightText: 2025, OPAL-RT Germany GmbH + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include + +#include + +#include +#include + +namespace villas { +namespace node { +namespace orchestra { + +enum class SignalType { + BOOLEAN, + UINT8, + UINT16, + UINT32, + UINT64, + INT8, + INT16, + INT32, + INT64, + FLOAT32, + FLOAT64, + BUS, +}; + +orchestra::SignalType toOrchestraSignalType(node::SignalType t); + +node::SignalType toNodeSignalType(orchestra::SignalType t); + +orchestra::SignalType signalTypeFromString(const std::string &t); + +std::string signalTypeToString(orchestra::SignalType t); + +node::SignalData toNodeSignalData(const char *orchestraData, + orchestra::SignalType orchestraType, + node::SignalType &villasType); + +void toOrchestraSignalData(char *orchestraData, + orchestra::SignalType orchestraType, + const SignalData &villasData, + node::SignalType villasType); + +} // namespace orchestra +} // namespace node +} // namespace villas + +template <> +struct fmt::formatter + : formatter { + auto format(villas::node::orchestra::SignalType t, format_context &ctx) const + -> format_context::iterator { + return formatter::format(signalTypeToString(t), ctx); + } +}; diff --git a/lib/hooks/lua.cpp b/lib/hooks/lua.cpp index 97f15368e..0dd89a402 100644 --- a/lib/hooks/lua.cpp +++ b/lib/hooks/lua.cpp @@ -6,7 +6,7 @@ */ #include -#include +#include #include extern "C" { @@ -464,7 +464,7 @@ void LuaHook::parse(json_t *json) { void LuaHook::lookupFunctions() { int ret; - std::map funcs = { + std::unordered_map funcs = { {"start", &functions.start}, {"stop", &functions.stop}, {"restart", &functions.restart}, {"prepare", &functions.prepare}, {"periodic", &functions.periodic}, {"process", &functions.process}}; diff --git a/lib/nodes/CMakeLists.txt b/lib/nodes/CMakeLists.txt index 01cc0e7ef..b861d7f03 100644 --- a/lib/nodes/CMakeLists.txt +++ b/lib/nodes/CMakeLists.txt @@ -144,6 +144,12 @@ if(WITH_NODE_CAN) list(APPEND NODE_SRC can.cpp) endif() +# Enable OPAL-RT Orchestra node-type +if(WITH_NODE_OPAL_ORCHESTRA) + list(APPEND NODE_SRC opal_orchestra.cpp opal_orchestra/ddf.cpp opal_orchestra/signal.cpp opal_orchestra/locks.cpp) + list(APPEND LIBRARIES OpalOrchestra LibXml2::LibXml2) +endif() + # Enable Example node type if(WITH_NODE_EXAMPLE) list(APPEND NODE_SRC example.cpp) diff --git a/lib/nodes/iec61850_goose.cpp b/lib/nodes/iec61850_goose.cpp index a2ab62879..8a551892e 100644 --- a/lib/nodes/iec61850_goose.cpp +++ b/lib/nodes/iec61850_goose.cpp @@ -823,7 +823,7 @@ void GooseNode::parseSubscriber(json_t *json, GooseNode::SubscriberConfig &sc) { } void GooseNode::parseSubscribers( - json_t *json, std::map &ctx) { + json_t *json, std::unordered_map &ctx) { char const *key; json_t *json_subscriber; diff --git a/lib/nodes/opal_orchestra.cpp b/lib/nodes/opal_orchestra.cpp new file mode 100644 index 000000000..2a8bcc2b3 --- /dev/null +++ b/lib/nodes/opal_orchestra.cpp @@ -0,0 +1,718 @@ +/* Node type: OPAL-RT Orchestra co-simulation client. + * + * Author: Steffen Vogel + * + * SPDX-FileCopyrightText: 2025, OPAL-RT Germany GmbH + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +extern "C" { +#include +} + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace villas; +using namespace villas::node; +using namespace villas::utils; +using namespace villas::node::orchestra; + +// An OpalOrchestraMapping maps one or more VILLASnode signals to an Orchestra data item. +class OpalOrchestraMapping { +public: + std::shared_ptr item; + std::string path; + std::vector signals; + + // Cached signal indices + // We keep a vector of indices to map the signal index in the signal list. + SignalList::Ptr signalList; // Signal list for which the indices are valid. + std::vector> indices; + + // Run-time members which will be retrieved from Orchestra in prepare(). + unsigned short key; + char *buffer; + unsigned int typeSize; // sizeof() of the signal type. See RTSignalType. + unsigned short length; + + OpalOrchestraMapping(std::shared_ptr item, std::string path) + : item(item), path(std::move(path)), signals(), signalList(), indices() {} + + void addSignal(Signal::Ptr signal, std::optional orchestraIdx) { + if (!orchestraIdx) { + orchestraIdx = signals.size(); + } + + if (*orchestraIdx < signals.size()) { + if (signals[*orchestraIdx]) { + throw RuntimeError("Index {} of Orchestra signal already mapped", + *orchestraIdx); + } + } else { + signals.resize(*orchestraIdx + 1, nullptr); + item->length = signals.size(); + } + + signals[*orchestraIdx] = signal; + } + + void check() { + if (signals.empty()) { + throw RuntimeError("No signal mapped to Orchestra signal '{}'", + item->name); + } + + if (item->name.empty()) { + throw RuntimeError("Signal name cannot be empty"); + } + + if (item->name.find_first_of(" \t\n") != std::string::npos) { + throw RuntimeError("Signal name '{}' contains whitespace", item->name); + } + + if (item->name.find_first_of(",#") != std::string::npos) { + throw RuntimeError("Signal name '{}' contains comma or hash-tag", + item->name); + } + + if (item->name.size() > DataItem::IDENTIFIER_NAME_LENGTH - 1) { + throw RuntimeError("Signal name '{}' is too long", item->name); + } + + auto firstSignal = signals[0]; + + for (auto &signal : signals) { + if (signal->type != firstSignal->type) { + throw RuntimeError("Signal type mismatch: {} vs {} for signal '{}'", + signalTypeToString(signal->type), + signalTypeToString(firstSignal->type), signal->name); + } + + if (signal->init.f != firstSignal->init.f) { + throw RuntimeError("Signal default value mismatch: {} vs {} for signal " + "'{}'", + signal->init.toString(signal->type), + firstSignal->init.toString(firstSignal->type), + signal->name); + } + } + + auto orchestraType = toOrchestraSignalType(firstSignal->type); + if (item->type != orchestraType) { + throw RuntimeError("Signal type mismatch: {} vs {} for signal '{}'", + signalTypeToString(item->type), + signalTypeToString(orchestraType), item->name); + } + } + + void prepare(unsigned int connectionKey) { + auto ret = RTGetInfoForItem(path.c_str(), &typeSize, &length); + if (ret != RTAPI_SUCCESS) { + throw RTError(ret, "Failed to get info for signal '{}'", item->name); + } + + ret = RTGetKeyForItem(path.c_str(), &key); + if (ret != RTAPI_SUCCESS) { + throw RTError(ret, "Failed to get key for signal '{}'", item->name); + } + + ret = RTGetBuffer(connectionKey, key, (void **)&buffer); + if (ret != RTAPI_SUCCESS) { + throw RTError(ret, "Failed to get buffer for signal '{}'", item->name); + } + + auto logger = Log::get("orchestra"); + logger->trace( + "Prepared mapping: path='{}', type={}, typeSize={}, key={}, buffer={}" + "length={}, default={}", + path, orchestra::signalTypeToString(item->type), key, buffer, typeSize, + length, item->defaultValue); + } + + void publish(struct Sample *smp) { + updateIndices(smp->signals); + + auto *orchestraDataPtr = buffer; + for (auto &index : indices) { + if (!index || *index >= smp->length) { + orchestraDataPtr += typeSize; + continue; // Unused index or index out of range. + } + + auto signal = smp->signals->getByIndex(*index); + if (!signal) { + throw RuntimeError("Signal {} not found", index); + } + + toOrchestraSignalData(orchestraDataPtr, item->type, smp->data[*index], + signal->type); + + orchestraDataPtr += typeSize; + } + } + + void subscribe(struct Sample *smp) { + updateIndices(smp->signals); + + auto *orchestraDataPtr = buffer; + for (auto &index : indices) { + if (!index || *index >= smp->capacity) { + continue; // Unused index or index out of range. + } + + for (unsigned i = smp->length; i < *index; i++) { + smp->data[i].i = 0; + } + + auto signal = smp->signals->getByIndex(*index); + if (!signal) { + throw RuntimeError("Signal {} not found", *index); + } + + node::SignalType villasType; + SignalData villasData = + toNodeSignalData(orchestraDataPtr, item->type, villasType); + + smp->data[*index] = villasData.cast(villasType, signal->type); + + if (index >= static_cast(smp->length)) { + smp->length = *index + 1; + } + + orchestraDataPtr += typeSize; + } + } + +protected: + void updateIndices(SignalList::Ptr newSignalList) { + if (signalList == newSignalList) { + return; // Already up to date. + } + + indices.clear(); + + for (const auto &signal : signals) { + if (signal) { + auto idx = newSignalList->getIndexByName(signal->name); + if (idx < 0) { + throw RuntimeError("Signal '{}' not found", signal->name); + } + + indices.push_back(idx); + } else { + indices.emplace_back(); // Unused index + } + } + + signalList = newSignalList; + } +}; + +class OpalOrchestraNode : public Node { + +protected: + Task task; // The task which is used to pace the node in asynchronous mode. + + unsigned int + connectionKey; // A connection key identifies a connection between a specific combo of Orchestra's framework and client. + unsigned int *status; + + Domain domain; // The domain to which the node belongs. + + std::unordered_map, OpalOrchestraMapping> + subscribeMappings; + std::unordered_map, OpalOrchestraMapping> + publishMappings; + + double rate; + std::optional dataDefinitionFilename; + + std::chrono::seconds connectTimeout; + std::optional + flagDelay; // Define a delay to wait, this will call the system function usleep and free the CPU. + std::optional + flagDelayTool; // Force the local Orchestra communication to be made with flag instead of semaphore when using an external communication process. + bool skipWaitToGo; // Skip wait-to-go step during start. + bool dataDefinitionFileOverwrite; // Overwrite the data definition file (DDF). + bool + dataDefinitionFileWriteOnly; // Overwrite the data definition file (DDF) and terminate VILLASnode. + + int _read(struct Sample *smps[], unsigned cnt) override { + if (dataDefinitionFileWriteOnly) { + logger->warn("Stopping node after writing the DDF file"); + setState(State::STOPPING); + return -1; + } + + assert(cnt == 1); + + if (!domain.synchronous) { + task.wait(); + } + + try { + RTSubscribeLockGuard guard(connectionKey); + + auto *smp = smps[0]; + smp->signals = getInputSignals(false); + smp->length = 0; + smp->flags = 0; + + for (auto &[item, mapping] : publishMappings) { + mapping.subscribe(smp); + } + + if (smp->length > 0) { + smp->flags |= (int)SampleFlags::HAS_DATA; + } + + return 1; + } catch (const RTError &e) { + if (e.returnCode == RTAPI_SUBSCRIBE_FAILED_RTLAB_SUBSYSTEM_UNMAPPED) { + logger->warn("Orchestra Framework has been stopped / unmapped"); + setState(State::STOPPING); + } else { + logger->error("Failed to read from Orchestra: {}", e.what()); + } + return -1; + } + } + + int _write(struct Sample *smps[], unsigned cnt) override { + if (dataDefinitionFileWriteOnly) { + logger->warn("Stopping node after writing the DDF file"); + setState(State::STOPPING); + return -1; + } + + assert(cnt == 1); + + try { + RTPublishLockGuard guard(connectionKey); + + auto *smp = smps[0]; + + for (auto &[item, mapping] : subscribeMappings) { + mapping.publish(smp); + } + + return 1; + } catch (const RTError &e) { + if (e.returnCode == RTAPI_PUBLISH_FAILED_RTLAB_SUBSYSTEM_UNMAPPED) { + logger->warn("Orchestra Framework has been stopped / unmapped"); + setState(State::STOPPING); + } else { + logger->error("Failed to write to Orchestra: {}", e.what()); + } + return -1; + } + } + +public: + OpalOrchestraNode(const uuid_t &id = {}, const std::string &name = "", + unsigned int key = 0) + : Node(id, name), task(), connectionKey(key), domain(), + subscribeMappings(), publishMappings(), rate(1), connectTimeout(5), + skipWaitToGo(false), dataDefinitionFileOverwrite(false), + dataDefinitionFileWriteOnly(false) {} + + void parseSignals(json_t *json, SignalList::Ptr signals, DataSet &dataSet, + std::unordered_map, + OpalOrchestraMapping> &mappings) { + if (!json_is_array(json)) { + throw ConfigError(json, "node-config-node-opal-orchestra-signals", + "Signals must be an array"); + } + + size_t i; + json_t *json_signal; + json_error_t err; + + json_array_foreach(json, i, json_signal) { + auto signal = signals->getByIndex(i); + + const char *nme = nullptr; + const char *typ = nullptr; + int oi = -1; + + auto ret = json_unpack_ex(json_signal, &err, 0, "{ s?: s, s?: s, s?: i }", + "orchestra_name", &nme, "orchestra_type", &typ, + "orchestra_index", &oi); + if (ret) { + throw ConfigError(json_signal, err, + "node-config-node-opal-orchestra-signals"); + } + + std::optional orchestraIdx; + + if (oi >= 0) { + orchestraIdx = oi; + } + + auto defaultValue = + signal->init.cast(signal->type, node::SignalType::FLOAT); + + auto orchestraType = typ ? orchestra::signalTypeFromString(typ) + : orchestra::toOrchestraSignalType(signal->type); + + auto orchestraName = nme ? nme : signal->name; + + bool inserted = false; + auto item = dataSet.upsertItem(orchestraName, inserted); + + if (inserted) { + item->type = orchestraType; + item->defaultValue = defaultValue.f; + + mappings.emplace(item, OpalOrchestraMapping(item, orchestraName)); + } + + auto &mapping = mappings.at(item); + mapping.addSignal(signal, orchestraIdx); + } + } + + int parse(json_t *json) override { + int ret = Node::parse(json); + if (ret) + return ret; + + const char *dn = nullptr; + const char *ddf = nullptr; + json_t *json_in_signals = nullptr; + json_t *json_out_signals = nullptr; + json_t *json_connection = nullptr; + json_t *json_connect_timeout = nullptr; + json_t *json_flag_delay = nullptr; + json_t *json_flag_delay_tool = nullptr; + + int sw = -1; + int ow = -1; + int owo = -1; + int sy = -1; + int sts = -1; + + json_error_t err; + ret = json_unpack_ex( + json, &err, 0, + "{ s: s, s?: b, s?: b, s?: o, s?: s, s?: o, s?: o, s?: o, s?: b, s?: " + "b, s?: b, s?: F, s?: { s?: o }, s?: { s?: o } }", + "domain", &dn, "synchronous", &sy, "states", &sts, "connection", + &json_connection, "ddf", &ddf, "connect_timeout", &json_connect_timeout, + "flag_delay", &json_flag_delay, "flag_delay_tool", + &json_flag_delay_tool, "skip_wait_to_go", &sw, "ddf_overwrite", &ow, + "ddf_overwrite_only", &owo, "rate", &rate, "in", "signals", + &json_in_signals, "out", "signals", &json_out_signals); + if (ret) { + throw ConfigError(json, err, "node-config-node-opal-orchestra"); + } + + domain.name = dn; + + if (ddf) { + dataDefinitionFilename = ddf; + } + + if (sy >= 0) { + domain.synchronous = sy > 0; + } + + if (sts >= 0) { + domain.states = sts > 0; + } + + if (sw >= 0) { + skipWaitToGo = sw > 0; + } + + if (ow >= 0) { + dataDefinitionFileOverwrite = ow > 0; + } + + if (owo >= 0) { + dataDefinitionFileWriteOnly = owo > 0; + } + + if (json_connect_timeout) { + connectTimeout = + parse_duration(json_connect_timeout); + } + + if (json_flag_delay) { + *flagDelay = parse_duration(json_flag_delay); + } + + if (json_flag_delay_tool) { + *flagDelayTool = + parse_duration(json_flag_delay_tool); + } + + if (json_connection) { + domain.connection = Connection::fromJson(json_connection); + } + + if (json_in_signals) { + parseSignals(json_in_signals, in.getSignals(false), domain.publish, + publishMappings); + } + + if (json_out_signals) { + parseSignals(json_out_signals, out.getSignals(false), domain.subscribe, + subscribeMappings); + } + + return 0; + } + + void checkDuplicateNames(const DataSet &a, const DataSet &b) { + for (auto &[name, item] : a.items) { + if (b.items.find(name) != b.items.end()) { + throw RuntimeError( + "Orchestra signal '{}' is used for both publish and subscribe", + name); + } + } + } + + int check() override { + if (dataDefinitionFilename) { + if (!std::filesystem::exists(*dataDefinitionFilename) && + !dataDefinitionFileOverwrite) { + throw RuntimeError("OPAL-RT Orchestra Data Definition file (DDF) at " + "'{}' does not exist", + *dataDefinitionFilename); + } + } else { + if (dataDefinitionFileOverwrite) { + throw RuntimeError( + "The option 'ddf_overwrite' requires the option 'ddf' to be set"); + } + } + + checkDuplicateNames(domain.publish, domain.subscribe); + + return Node::check(); + } + + int prepare() override { + // Write DDF. + if (dataDefinitionFilename && + (dataDefinitionFileOverwrite || dataDefinitionFileWriteOnly)) { + + // TODO: Possibly merge Orchestra domains from all nodes into one DDF. + auto ddf = DataDefinitionFile(); + ddf.domains.push_back(domain); + ddf.writeToFile(*dataDefinitionFilename); + + logger->info("Wrote Orchestra Data Definition file (DDF) to '{}'", + *dataDefinitionFilename); + + if (dataDefinitionFileWriteOnly) { + return Node::prepare(); + } + } + + logger->debug("Connecting to Orchestra framework: domain={}, ddf={}, " + "connection_key={}", + domain.name, + dataDefinitionFilename ? *dataDefinitionFilename : "none", + connectionKey); + + RTConnectionLockGuard guard(connectionKey); + + auto ret = + dataDefinitionFilename + ? RTConnectWithFile(dataDefinitionFilename->c_str(), + domain.name.c_str(), connectTimeout.count()) + : RTConnect(domain.name.c_str(), connectTimeout.count()); + if (ret != RTAPI_SUCCESS) { + throw RTError(ret, "Failed to connect to Orchestra framework"); + } + + logger->info("Connected to Orchestra framework: domain={}", domain.name); + + ret = RTGetConnectionStatusPtr(connectionKey, &status); + if (ret != RTAPI_SUCCESS) { + throw RTError(ret, "Failed to get connection status pointer"); + } + + if (flagDelay) { + ret = RTDefineFlagDelay(flagDelay->count()); + if (ret != RTAPI_SUCCESS) { + throw RTError(ret, "Failed to set flag delay"); + } + } + + if (flagDelayTool) { + ret = RTUseFlagWithTool(flagDelayTool->count()); + if (ret != RTAPI_SUCCESS) { + throw RTError(ret, "Failed to set flag with tool"); + } + } + + ret = RTSetSkipWaitToGoAtConnection(skipWaitToGo); + if (ret != RTAPI_SUCCESS) { + throw RTError(ret, "Failed to check ready to go"); + } + + if (std::shared_ptr c = + std::dynamic_pointer_cast(domain.connection)) { + ret = RTSetupRefMemRemoteConnection(c->name.c_str(), c->pciIndex); + if (ret != RTAPI_SUCCESS) { + throw RTError(ret, + "Failed to setup reflective memory remote connection"); + } + } + + for (auto &[item, mapping] : publishMappings) { + mapping.prepare(connectionKey); + } + + for (auto &[item, mapping] : subscribeMappings) { + mapping.prepare(connectionKey); + } + + char *bufferPublish, *bufferSubscribe; + unsigned int bufferPublishSize, bufferSubscribeSize; + ret = RTGetBufferInfo(connectionKey, &bufferPublish, &bufferPublishSize, + &bufferSubscribe, &bufferSubscribeSize); + if (ret != RTAPI_SUCCESS) { + throw RTError(ret, "Failed to get buffer info"); + } + logger->debug("Buffer publish: size={}, ptr={:p}", bufferPublishSize, + bufferPublish); + logger->debug("Buffer subscribe: size={}, ptr={:p}", bufferSubscribeSize, + bufferSubscribe); + + return Node::prepare(); + } + + int start() override { + if (dataDefinitionFileWriteOnly) { + return Node::start(); + } + + RTConnectionLockGuard guard(connectionKey); + + RTWaitReadyToGo(); + + if (!domain.synchronous) { + task.setRate(rate); + } + + return Node::start(); + } + + int stop() override { + int ret = Node::stop(); + if (ret) + return ret; + + RTConnectionLockGuard guard(connectionKey); + + if (!domain.synchronous) { + task.stop(); + } + + auto ret2 = RTDisconnect(); + if (ret != RTAPI_SUCCESS) { + throw RTError(ret2, "Failed to disconnect"); + } + + return 0; + } + + const std::string &getDetails() override { + details = fmt::format("domain={}", domain.name); + + if (dataDefinitionFilename) { + details += fmt::format(", ddf={}", *dataDefinitionFilename); + } + + details += fmt::format(", connect_timeout={}, skip_wait_to_go={}", + connectTimeout, skipWaitToGo); + + if (flagDelay) + details += fmt::format(", flag_delay = {}", *flagDelay); + + if (flagDelayTool) + details += fmt::format(", flag_delay_tool = {}", *flagDelayTool); + + details += fmt::format( + ", #publish_items={}, #subcribe_items={}, " + "connection_key={}, synchronous={}, states={}, " + "multiple_publish_allowed={}, connection={{ {} }}", + domain.publish.items.size(), domain.subscribe.items.size(), + connectionKey, domain.synchronous, domain.states, + domain.multiplePublishAllowed, domain.connection->getDetails()); + + return details; + } + + std::vector getPollFDs() override { + if (!domain.synchronous) { + return {task.getFD()}; + } + + return {}; + } +}; + +class OpalOrchestraNodeFactory : public NodeFactory { + +public: + using NodeFactory::NodeFactory; + + Node *make(const uuid_t &id = {}, const std::string &nme = "") override { + RTConnectionLockGuard guard(0); + + unsigned int connectionKey; + auto ret = RTNewConnectionKey(&connectionKey); + if (ret != RTAPI_SUCCESS) { + throw RTError(ret, "Failed to create new Orchestra node"); + } + + auto *n = new OpalOrchestraNode(id, nme, connectionKey); + + init(n); + + return n; + } + + int getFlags() const override { + return (int)NodeFactory::Flags::SUPPORTS_READ | + (int)NodeFactory::Flags::SUPPORTS_WRITE | + (int)NodeFactory::Flags::SUPPORTS_POLL | + (int)NodeFactory::Flags::HIDDEN; + } + + std::string getName() const override { return "opal.orchestra"; } + + std::string getDescription() const override { + return "OPAL-RT Orchestra client"; + } +}; + +static OpalOrchestraNodeFactory p; diff --git a/lib/nodes/opal_orchestra/ddf.cpp b/lib/nodes/opal_orchestra/ddf.cpp new file mode 100644 index 000000000..0992772ba --- /dev/null +++ b/lib/nodes/opal_orchestra/ddf.cpp @@ -0,0 +1,383 @@ +/* OPAL-RT Orchestra Data Definition File (DDF) + * + * Author: Steffen Vogel + * + * SPDX-FileCopyrightText: 2025, OPAL-RT Germany GmbH + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +extern "C" { +#include +} + +#include +#include +#include +#include +#include + +using namespace villas::node; +using namespace villas::node::orchestra; + +void DataItem::toXml(xmlNode *parent, bool withDefault) const { + xmlNode *item = xmlNewChild(parent, nullptr, BAD_CAST "item", nullptr); + xmlNewProp(item, BAD_CAST "name", BAD_CAST name.c_str()); + xmlNewChild(item, nullptr, BAD_CAST "type", + BAD_CAST signalTypeToString(type).c_str()); + xmlNewChild(item, nullptr, BAD_CAST "length", + BAD_CAST fmt::format("{}", length).c_str()); + + if (withDefault) { + std::string defaultValueStr; + switch (type) { + case orchestra::SignalType::BOOLEAN: + defaultValueStr = defaultValue ? "yes" : "no"; + break; + + default: + defaultValueStr = fmt::format("{}", defaultValue); + break; + } + + xmlNewChild(item, nullptr, BAD_CAST "default", + BAD_CAST defaultValueStr.c_str()); + } +} + +void BusItem::toXml(xmlNode *parent, bool withDefault) const { + xmlNode *node = xmlNewChild(parent, NULL, BAD_CAST "item", NULL); + xmlNewProp(node, BAD_CAST "name", BAD_CAST name.c_str()); + xmlNewChild(node, NULL, BAD_CAST "type", BAD_CAST "bus"); + + for (const auto &p : items) { + auto &item = p.second; + item->toXml(node, withDefault); + } +} + +std::shared_ptr BusItem::upsertItem(std::string_view path, + bool &inserted) { + auto separator = path.find('/'); + auto isSignal = separator == std::string::npos; // No bus, just a signal. + auto name = std::string(path.substr(0, separator)); + + auto it = items.find(name); + if (it == items.end()) { // No item with this name exists. Create a new one. + if (isSignal) { + auto item = std::make_shared(name); + items.emplace(name, item); + inserted = true; + return item; + } else { + auto bus = std::make_shared(name); + items.emplace(name, bus); + + return bus->upsertItem(path.substr(separator + 1), inserted); + } + } else { + if (isSignal) { + auto item = std::dynamic_pointer_cast(it->second); + if (!item) { + throw RuntimeError("Item with name '{}' is not a data item", name); + } + + inserted = false; + return item; + } else { + auto bus = std::dynamic_pointer_cast(it->second); + if (!bus) { + throw RuntimeError("Item with name '{}' is not a bus", name); + } + + return bus->upsertItem(path.substr(separator + 1), inserted); + } + } +} + +std::shared_ptr DataSet::upsertItem(std::string_view path, + bool &inserted) { + auto separator = path.find('/'); + auto name = std::string(path.substr(0, separator)); + auto it = items.find(name); + if (it == items.end()) { // No item with this name exists. Create a new one. + if (separator == std::string_view::npos) { // No bus, just a signal. + auto item = std::make_shared(name); + items.emplace(std::move(name), item); + inserted = true; + return item; + } else { + auto bus = std::make_shared(name); + items.emplace(std::move(name), bus); + return bus->upsertItem(path.substr(separator + 1), inserted); + } + } else { + if (separator == std::string_view::npos) { + auto item = std::dynamic_pointer_cast(it->second); + if (!item) { + throw RuntimeError("Item with name '{}' is not a data item", name); + } + inserted = false; + return item; + } else { + // Item with this name exists. Check if it is a bus. + auto bus = std::dynamic_pointer_cast(it->second); + if (!bus) { + throw RuntimeError("Item with name '{}' is not a bus", name); + } + return bus->upsertItem(path.substr(separator + 1), inserted); + } + } +} + +void DataSet::toXml(xmlNode *parent, bool withDefault) const { + xmlNode *node = xmlNewChild(parent, NULL, BAD_CAST "set", NULL); + xmlNewProp(node, BAD_CAST "name", BAD_CAST name.c_str()); + + for (const auto &p : items) { + auto &item = p.second; + item->toXml(node, withDefault); + } +} + +std::string ConnectionLocal::getDetails() const { + auto details = fmt::format("type=local"); + if (extcomm) { + details += fmt::format(", extcomm={}", *extcomm); + + if (addressFramework) { + details += fmt::format(", addr_framework={}", *addressFramework); + } + if (portFramework) { + details += fmt::format(", port_framework={}", portFramework.value()); + } + if (nicFramework) { + details += fmt::format(", nic_framework={}", *nicFramework); + } + if (nicClient) { + details += fmt::format(", nic_client={}", *nicClient); + } + if (coreFramework) { + details += fmt::format(", core_framework={}", coreFramework.value()); + } + if (coreClient) { + details += fmt::format(", core_client={}", coreClient.value()); + } + } + details += " }"; + + return details; +} + +void ConnectionLocal::toXml(xmlNode *domain) const { + xmlNode *connection = + xmlNewChild(domain, nullptr, BAD_CAST "connection", nullptr); + xmlNewProp(connection, BAD_CAST "type", BAD_CAST "local"); + if (extcomm) { + xmlNewProp(connection, BAD_CAST "extcomm", BAD_CAST extcomm->c_str()); + + if (addressFramework) { + xmlNewProp(connection, BAD_CAST "addrframework", + BAD_CAST addressFramework->c_str()); + } + if (portFramework) { + xmlNewProp(connection, BAD_CAST "portframework", + BAD_CAST fmt::format("{}", portFramework.value()).c_str()); + } + if (nicFramework) { + xmlNewProp(connection, BAD_CAST "nicframework", + BAD_CAST nicFramework->c_str()); + } + if (nicClient) { + xmlNewProp(connection, BAD_CAST "nicclient", BAD_CAST nicClient->c_str()); + } + if (coreFramework) { + xmlNewProp(connection, BAD_CAST "coreframework", + BAD_CAST fmt::format("{}", coreFramework.value()).c_str()); + } + if (coreClient) { + xmlNewProp(connection, BAD_CAST "coreclient", + BAD_CAST fmt::format("{}", coreClient.value()).c_str()); + } + } +} + +void ConnectionLocal::parse(json_t *json) { + int pf = -1; + int cf = -1; + int cc = -1; + const char *ec = nullptr; + const char *af = nullptr; + const char *nf = nullptr; + const char *nc = nullptr; + + json_error_t err; + + auto ret = json_unpack_ex( + json, &err, 0, "{ s?: s, s?: s, s?: i, s?: s, s?: s, s?: i, s?: i }", + "extcomm", &ec, "addr_framework", &af, "port_framework", &pf, + "nic_framework", &nf, "nic_client", &nc, "core_framework", &cf, + "core_client", &cc); + if (ret) { + throw ConfigError(json, err, "node-config-node-opal-orchestra-connection", + "Failed to parse configuration of local connection"); + } + + if (ec) { + extcomm = ec; + } + if (af) { + addressFramework = af; + } + if (pf >= 0) { + portFramework = pf; + } + if (nf) { + nicFramework = nf; + } + if (nc) { + nicClient = nc; + } + if (cf >= 0) { + coreFramework = cf; + } + if (cc >= 0) { + coreClient = cc; + } +} + +std::string ConnectionRemote::getDetails() const { + return fmt::format(", connection={{ type=remote, name={}, pci_index={} }}", + name, pciIndex); +} + +void ConnectionRemote::toXml(xmlNode *domain) const { + xmlNode *connection = + xmlNewChild(domain, nullptr, BAD_CAST "remote", nullptr); + xmlNewProp(connection, BAD_CAST "type", BAD_CAST "local"); + xmlNewProp(connection, BAD_CAST "type", BAD_CAST "remote"); + xmlNewChild(connection, nullptr, BAD_CAST "card", BAD_CAST name.c_str()); + xmlNewChild(connection, nullptr, BAD_CAST "pciindex", + BAD_CAST fmt::format("{}", pciIndex).c_str()); +} + +void ConnectionRemote::parse(json_t *json) { + const char *nme; + + json_error_t err; + + int ret = json_unpack_ex(json, &err, 0, "{ s: s, s: i }", "card", &nme, + "pci_index", &pciIndex); + if (ret) { + throw ConfigError( + json, err, "node-config-node-opal-orchestra-rfm", + "Failed to parse configuration of Reflective Memory Card (RFM)"); + } + + name = nme; +} + +std::string ConnectionDolphin::getDetails() const { + return fmt::format( + ", connection={{ type=dolphin, node_id_framework={}, segment_id={} }}", + nodeIdFramework, segmentId); +} + +void ConnectionDolphin::toXml(xmlNode *domain) const { + xmlNode *connection = + xmlNewChild(domain, nullptr, BAD_CAST "dolphin", nullptr); + xmlNewProp(connection, BAD_CAST "type", BAD_CAST "local"); + xmlNewProp(connection, BAD_CAST "type", BAD_CAST "dolphin"); + xmlNewProp(connection, BAD_CAST "nodeIdFramework", + BAD_CAST fmt::format("{}", nodeIdFramework).c_str()); + xmlNewProp(connection, BAD_CAST "segmentId", + BAD_CAST fmt::format("{}", segmentId).c_str()); +} + +void ConnectionDolphin::parse(json_t *json) { + json_error_t err; + auto ret = + json_unpack_ex(json, &err, 0, "{ s: i, s: i }", "node_id_framework", + &nodeIdFramework, "segment_id", &segmentId); + if (ret) { + throw ConfigError(json, err, "node-config-node-opal-orchestra-connection", + "Failed to parse configuration of dolphin connection"); + } +} + +std::shared_ptr Connection::fromJson(json_t *json) { + const char *type = nullptr; + json_error_t err; + + int ret = json_unpack_ex(json, &err, 0, "{ s: s, }", "type", &type); + if (ret) { + throw ConfigError(json, err, "node-config-node-opal-orchestra-connection", + "Failed to parse connection type"); + } + + std::shared_ptr c; + + if (strcmp(type, "local") == 0) { + c = std::make_shared(); + c->parse(json); + } else if (strcmp(type, "remote") == 0) { + c = std::make_shared(); + c->parse(json); + } else if (strcmp(type, "dolphin") == 0) { + c = std::make_shared(); + c->parse(json); + } else { + throw ConfigError(json, err, "node-config-node-opal-orchestra-connection", + "Unknown type of connection '{}'", type); + } + + return c; +} + +void Domain::toXml(xmlNode *parent) const { + // Create the node with attributes + xmlNode *domain = xmlNewChild(parent, nullptr, BAD_CAST "domain", nullptr); + xmlNewProp(domain, BAD_CAST "name", BAD_CAST name.c_str()); + + // Add child nodes to the domain + xmlNewChild(domain, nullptr, BAD_CAST "synchronous", + BAD_CAST(synchronous ? "yes" : "no")); + xmlNewChild(domain, nullptr, BAD_CAST "multiplePublishAllowed", + BAD_CAST(multiplePublishAllowed ? "yes" : "no")); + xmlNewChild(domain, nullptr, BAD_CAST "states", + BAD_CAST(states ? "yes" : "no")); + + connection->toXml(domain); + + publish.toXml( + domain, + false); // Add the PUBLISH set (input signals from Orchestra clients (VILLASnode's) perspective) + subscribe.toXml( + domain, + true); // Add the SUBSCRIBE set (output signals from Orchestra clients (VILLASnode's) perspective) +} + +xmlNode *DataDefinitionFile::toXml() const { + xmlNode *rootNode = xmlNewNode(nullptr, BAD_CAST "orchestra"); + + for (const auto &domain : domains) { + domain.toXml(rootNode); + } + + return rootNode; +} + +void DataDefinitionFile::writeToFile( + const std::filesystem::path &filename) const { + // Create a new XML document + auto *doc = xmlNewDoc(BAD_CAST "1.0"); + auto *rootNode = toXml(); + + xmlDocSetRootElement(doc, rootNode); + + // Save the document to a file + if (xmlSaveFormatFileEnc(filename.c_str(), doc, "UTF-8", 1) == -1) { + xmlFreeDoc(doc); + throw RuntimeError("Failed to save XML file"); + } +} diff --git a/lib/nodes/opal_orchestra/locks.cpp b/lib/nodes/opal_orchestra/locks.cpp new file mode 100644 index 000000000..a318802f3 --- /dev/null +++ b/lib/nodes/opal_orchestra/locks.cpp @@ -0,0 +1,28 @@ +/* Helpers for RTAPI locking primitives + * + * Author: Steffen Vogel + * + * SPDX-FileCopyrightText: 2025, OPAL-RT Germany GmbH + * SPDX-License-Identifier: Apache-2.0 + */ + +#include + +extern "C" { +#include +} + +#include + +// Global lock for access to OPAL-RT's RTAPI Orchestra API. +// The Orchestra RTAPI is non-thread-safe and non-reentrant. +// Hence, this mutex is needed for mediating access to the API +// by a single thread at a time. +static std::mutex globalLock; + +using namespace villas::node::orchestra; + +RTConnectionLockGuard::RTConnectionLockGuard(unsigned int connectionKey) + : std::lock_guard(globalLock) { + RTSetConnectionKey(connectionKey); +} diff --git a/lib/nodes/opal_orchestra/signal.cpp b/lib/nodes/opal_orchestra/signal.cpp new file mode 100644 index 000000000..48be3e39c --- /dev/null +++ b/lib/nodes/opal_orchestra/signal.cpp @@ -0,0 +1,221 @@ +/* Helpers for RTAPI signal type conversion. + * + * Author: Steffen Vogel + * + * SPDX-FileCopyrightText: 2025, OPAL-RT Germany GmbH + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include + +using namespace villas; +using namespace villas::node; +using namespace villas::node::orchestra; + +orchestra::SignalType +villas::node::orchestra::toOrchestraSignalType(node::SignalType t) { + switch (t) { + case node::SignalType::FLOAT: + return orchestra::SignalType::FLOAT64; + case node::SignalType::INTEGER: + return orchestra::SignalType::INT64; + case node::SignalType::BOOLEAN: + return orchestra::SignalType::BOOLEAN; + default: + throw RuntimeError("Unsupported signal type: {}", signalTypeToString(t)); + } +} + +node::SignalType +villas::node::orchestra::toNodeSignalType(orchestra::SignalType t) { + switch (t) { + case orchestra::SignalType::FLOAT32: + case orchestra::SignalType::FLOAT64: + return node::SignalType::FLOAT; + + case orchestra::SignalType::INT8: + case orchestra::SignalType::UINT8: + case orchestra::SignalType::INT16: + case orchestra::SignalType::UINT16: + case orchestra::SignalType::INT32: + case orchestra::SignalType::UINT32: + case orchestra::SignalType::INT64: + case orchestra::SignalType::UINT64: + return node::SignalType::INTEGER; + + case orchestra::SignalType::BOOLEAN: + return node::SignalType::BOOLEAN; + + default: + throw RuntimeError("Unsupported Orchestra signal type: {}", (int)t); + } +} + +orchestra::SignalType +villas::node::orchestra::signalTypeFromString(const std::string &t) { + if (t == "boolean") { + return orchestra::SignalType::BOOLEAN; + } else if (t == "unsigned int8") { + return orchestra::SignalType::UINT8; + } else if (t == "unsigned int16") { + return orchestra::SignalType::UINT16; + } else if (t == "unsigned int32") { + return orchestra::SignalType::UINT32; + } else if (t == "unsigned int64") { + return orchestra::SignalType::UINT64; + } else if (t == "int8") { + return orchestra::SignalType::INT8; + } else if (t == "int16") { + return orchestra::SignalType::INT16; + } else if (t == "int32") { + return orchestra::SignalType::INT32; + } else if (t == "int64") { + return orchestra::SignalType::INT64; + } else if (t == "float32") { + return orchestra::SignalType::FLOAT32; + } else if (t == "float64") { + return orchestra::SignalType::FLOAT64; + } else if (t == "bus") { + return orchestra::SignalType::BUS; + } else { + throw RuntimeError("Unknown Orchestra signal type: {}", t); + } +} + +std::string +villas::node::orchestra::signalTypeToString(orchestra::SignalType t) { + switch (t) { + case orchestra::SignalType::BOOLEAN: + return "boolean"; + case orchestra::SignalType::UINT8: + return "unsigned int8"; + case orchestra::SignalType::UINT16: + return "unsigned int16"; + case orchestra::SignalType::UINT32: + return "unsigned int32"; + case orchestra::SignalType::UINT64: + return "unsigned int64"; + case orchestra::SignalType::INT8: + return "int8"; + case orchestra::SignalType::INT16: + return "int16"; + case orchestra::SignalType::INT32: + return "int32"; + case orchestra::SignalType::INT64: + return "int64"; + case orchestra::SignalType::FLOAT32: + return "float32"; + case orchestra::SignalType::FLOAT64: + return "float64"; + case orchestra::SignalType::BUS: + return "bus"; + default: + throw RuntimeError("Unknown Orchestra signal type: {}", (int)t); + } +} + +SignalData +villas::node::orchestra::toNodeSignalData(const char *orchestraData, + orchestra::SignalType orchestraType, + node::SignalType &villasType) { + + villasType = toNodeSignalType(orchestraType); + + switch (orchestraType) { + case orchestra::SignalType::BOOLEAN: + return *reinterpret_cast(orchestraData); + + case orchestra::SignalType::UINT8: + return *reinterpret_cast(orchestraData); + + case orchestra::SignalType::UINT16: + return *reinterpret_cast(orchestraData); + + case orchestra::SignalType::UINT32: + return *reinterpret_cast(orchestraData); + + case orchestra::SignalType::UINT64: + return *reinterpret_cast(orchestraData); + + case orchestra::SignalType::INT8: + return *reinterpret_cast(orchestraData); + + case orchestra::SignalType::INT16: + return *reinterpret_cast(orchestraData); + + case orchestra::SignalType::INT32: + return *reinterpret_cast(orchestraData); + + case orchestra::SignalType::INT64: + return *reinterpret_cast(orchestraData); + + case orchestra::SignalType::FLOAT32: + return *reinterpret_cast(orchestraData); + + case orchestra::SignalType::FLOAT64: + return *reinterpret_cast(orchestraData); + + default: + throw RuntimeError("Orchestra signal type {} is not supported", + signalTypeToString(orchestraType)); + }; + + return SignalData(); // Unreachable +} + +void villas::node::orchestra::toOrchestraSignalData( + char *orchestraData, orchestra::SignalType orchestraType, + const SignalData &villasData, node::SignalType villasType) { + auto villasTypeCasted = toNodeSignalType(orchestraType); + auto villasDataCasted = villasData.cast(villasType, villasTypeCasted); + + switch (orchestraType) { + case orchestra::SignalType::BOOLEAN: + *reinterpret_cast(orchestraData) = villasDataCasted; + break; + + case orchestra::SignalType::UINT8: + *reinterpret_cast(orchestraData) = villasDataCasted; + break; + + case orchestra::SignalType::UINT16: + *reinterpret_cast(orchestraData) = villasDataCasted; + break; + + case orchestra::SignalType::UINT32: + *reinterpret_cast(orchestraData) = villasDataCasted; + break; + + case orchestra::SignalType::UINT64: + *reinterpret_cast(orchestraData) = villasDataCasted; + break; + + case orchestra::SignalType::INT8: + *reinterpret_cast(orchestraData) = villasDataCasted; + break; + + case orchestra::SignalType::INT16: + *reinterpret_cast(orchestraData) = villasDataCasted; + break; + + case orchestra::SignalType::INT32: + *reinterpret_cast(orchestraData) = villasDataCasted; + break; + + case orchestra::SignalType::INT64: + *reinterpret_cast(orchestraData) = villasDataCasted; + break; + + case orchestra::SignalType::FLOAT32: + *reinterpret_cast(orchestraData) = villasDataCasted; + break; + + case orchestra::SignalType::FLOAT64: + *reinterpret_cast(orchestraData) = villasDataCasted; + break; + + case orchestra::SignalType::BUS: + throw RuntimeError("Bus signal type not supported"); + } +} diff --git a/lib/path.cpp b/lib/path.cpp index 2657fdb95..5adac5bc0 100644 --- a/lib/path.cpp +++ b/lib/path.cpp @@ -8,7 +8,7 @@ #include #include #include -#include +#include #include #include @@ -166,7 +166,7 @@ void Path::prepare(NodeList &nodes) { this->toString()); // Create path sources - std::map psm; + std::unordered_map psm; unsigned i = 0, j = 0; for (auto me : mappings) { Node *n = me->node; diff --git a/lib/super_node.cpp b/lib/super_node.cpp index 00fd8e8f8..4f374dacf 100644 --- a/lib/super_node.cpp +++ b/lib/super_node.cpp @@ -490,7 +490,7 @@ graph_t *SuperNode::getGraph() { g = agopen((char *)"g", Agdirected, 0); - std::map nodeMap; + std::unordered_map nodeMap; for (auto *n : nodes) { nodeMap[n] = agnode(g, (char *)n->getNameShort().c_str(), 1); diff --git a/packaging/nix/orchestra.nix b/packaging/nix/orchestra.nix new file mode 100644 index 000000000..5d0a839e3 --- /dev/null +++ b/packaging/nix/orchestra.nix @@ -0,0 +1,55 @@ +# SPDX-FileCopyrightText: 2025, OPAL-RT Germany GmbH +# SPDX-License-Identifier: Apache-2.0 + +{ + stdenv, + libredirect, + nettools, + fetchurl, + autoPatchelfHook, + dpkg, + libuuid, + makeWrapper , +}: +let + # src = requireFile { + # name = "componentorchestra_7.6.2_amd64.deb"; + # hash = "sha256-2cQtYkf1InKrDPL5UDQDHaYM7bq21Dw77itfFwuXa54="; + # }; + + src = fetchurl { + url = "https://blob.opal-rt.com/softwares/rt-lab-archives/componentorchestra_7.6.2_amd64.deb?sp=r&st=2024-10-30T06:31:59Z&se=2034-11-30T14:31:59Z&spr=https&sv=2022-11-02&sr=b&sig=cnKY8RxZf8hv91gWLIBG6iBGSVziXkKR3%2BOYIE6MSkI%3D"; + hash = "sha256-2cQtYkf1InKrDPL5UDQDHaYM7bq21Dw77itfFwuXa54="; + }; +in +stdenv.mkDerivation { + pname = "libOpalOrchestra"; + version = "7.6.2"; + inherit src; + + dontUnpack = true; + + nativeBuildInputs = [ + dpkg + autoPatchelfHook + makeWrapper + ]; + + buildInputs = [ + libuuid + stdenv.cc.cc.lib + ]; + + installPhase = '' + ${dpkg}/bin/dpkg-deb -x ${src} . + + mv usr/opalrt/exportedOrchestra $out + mv $out/bin/OrchestraExtCommIPDebian $out/bin/OrchestraExtCommIP + ''; + + preFixup = '' + wrapProgram $out/bin/OrchestraExtCommIP \ + --set LD_PRELOAD "${libredirect}/lib/libredirect.so" \ + --set NIX_REDIRECTS "/sbin/ifconfig=${nettools}/bin/ifconfig" \ + ''; +} diff --git a/packaging/nix/villas.nix b/packaging/nix/villas.nix index d87f7dcad..74492896c 100644 --- a/packaging/nix/villas.nix +++ b/packaging/nix/villas.nix @@ -29,6 +29,7 @@ withNodeMqtt ? withAllNodes, withNodeNanomsg ? withAllNodes, withNodeOpenDSS ? withAllNodes, + withNodeOpalOrchestra ? (withAllNodes && system == "x86_64-linux"), withNodeRedis ? withAllNodes, withNodeRtp ? withAllNodes, withNodeSocket ? withAllNodes, @@ -71,11 +72,13 @@ libusb1, libuuid, libwebsockets, + libxml2, lua, mosquitto, nanomsg, opendssc, openssl, + orchestra, pcre2, pkgsBuildBuild, protobuf, @@ -142,58 +145,60 @@ stdenv.mkDerivation { protobufcBuildBuild ]; - buildInputs = - [ - libwebsockets - openssl - curl - spdlog - bash - ] - ++ lib.optionals withExtraTesting [ - boxfort - criterion - libffi - libgit2 - pcre2 - ] - ++ lib.optionals withExtraGraphviz [ graphviz ] - ++ lib.optionals withHookLua [ lua ] - ++ lib.optionals withNodeAmqp [ rabbitmq-c ] - ++ lib.optionals withNodeComedi [ comedilib ] - ++ lib.optionals withNodeEthercat [ ethercat ] - ++ lib.optionals withNodeIec60870 [ lib60870 ] - ++ lib.optionals withNodeIec61850 [ libiec61850 ] - ++ lib.optionals withNodeKafka [ - rdkafka - cyrus_sasl - ] - ++ lib.optionals withNodeModbus [ libmodbus ] - ++ lib.optionals withNodeMqtt [ mosquitto ] - ++ lib.optionals withNodeNanomsg [ nanomsg ] - ++ lib.optionals withNodeOpenDSS [ opendssc ] - ++ lib.optionals withNodeRedis [ redis-plus-plus ] - ++ lib.optionals withNodeRtp [ libre ] - ++ lib.optionals withNodeSocket [ libnl ] - ++ lib.optionals withNodeTemper [ libusb1 ] - ++ lib.optionals withNodeUldaq [ libuldaq ] - ++ lib.optionals withNodeWebrtc [ libdatachannel ] - ++ lib.optionals withNodeZeromq [ - czmq - libsodium - ]; + buildInputs = [ + libwebsockets + openssl + curl + spdlog + bash + ] + ++ lib.optionals withExtraTesting [ + boxfort + criterion + libffi + libgit2 + pcre2 + ] + ++ lib.optionals withExtraGraphviz [ graphviz ] + ++ lib.optionals withHookLua [ lua ] + ++ lib.optionals withNodeAmqp [ rabbitmq-c ] + ++ lib.optionals withNodeComedi [ comedilib ] + ++ lib.optionals withNodeEthercat [ ethercat ] + ++ lib.optionals withNodeIec60870 [ lib60870 ] + ++ lib.optionals withNodeIec61850 [ libiec61850 ] + ++ lib.optionals withNodeKafka [ + rdkafka + cyrus_sasl + ] + ++ lib.optionals withNodeModbus [ libmodbus ] + ++ lib.optionals withNodeMqtt [ mosquitto ] + ++ lib.optionals withNodeNanomsg [ nanomsg ] + ++ lib.optionals withNodeOpenDSS [ opendssc ] + ++ lib.optionals withNodeOpalOrchestra [ + orchestra + libxml2 + ] + ++ lib.optionals withNodeRedis [ redis-plus-plus ] + ++ lib.optionals withNodeRtp [ libre ] + ++ lib.optionals withNodeSocket [ libnl ] + ++ lib.optionals withNodeTemper [ libusb1 ] + ++ lib.optionals withNodeUldaq [ libuldaq ] + ++ lib.optionals withNodeWebrtc [ libdatachannel ] + ++ lib.optionals withNodeZeromq [ + czmq + libsodium + ]; - propagatedBuildInputs = - [ - libuuid - jansson - ] - ++ lib.optionals withFormatProtobuf [ - protobuf - protobufc - ] - ++ lib.optionals withNodeInfiniband [ rdma-core ] - ++ lib.optionals withExtraConfig [ libconfig ]; + propagatedBuildInputs = [ + libuuid + jansson + ] + ++ lib.optionals withFormatProtobuf [ + protobuf + protobufc + ] + ++ lib.optionals withNodeInfiniband [ rdma-core ] + ++ lib.optionals withExtraConfig [ libconfig ]; meta = { mainProgram = "villas"; diff --git a/src/villas-relay.cpp b/src/villas-relay.cpp index c4f29bed2..a31dde6b4 100644 --- a/src/villas-relay.cpp +++ b/src/villas-relay.cpp @@ -7,9 +7,9 @@ #include #include -#include #include #include +#include #include #include @@ -97,7 +97,7 @@ json_t *RelaySession::toJson() const { "connects", connects); } -std::map RelaySession::sessions; +std::unordered_map RelaySession::sessions; RelayConnection::RelayConnection(Relay *r, lws *w, bool lo) : wsi(w), currentFrame(std::make_shared()), outgoingFrames(), diff --git a/src/villas-relay.hpp b/src/villas-relay.hpp index 1c62f244e..3c6a4053f 100644 --- a/src/villas-relay.hpp +++ b/src/villas-relay.hpp @@ -5,9 +5,9 @@ * SPDX-License-Identifier: Apache-2.0 */ -#include #include #include +#include #include #include @@ -54,11 +54,11 @@ class RelaySession { Identifier identifier; Logger logger; - std::map connections; + std::unordered_map connections; int connects; - static std::map sessions; + static std::unordered_map sessions; public: static RelaySession *get(Relay *r, lws *wsi); diff --git a/tests/integration/test-config.sh b/tests/integration/test-config.sh index fb4e0e79c..32b776cbf 100755 --- a/tests/integration/test-config.sh +++ b/tests/integration/test-config.sh @@ -12,7 +12,7 @@ set -e CONFIGS=$(find ${SRCDIR}/etc/ -name '*.conf' -o -name '*.json') for CONFIG in ${CONFIGS}; do - if [ "$(basename ${CONFIG})" == "opal.conf" ] || + if [ "$(basename ${CONFIG})" == "opal_orchestra.conf" ] || [ "$(basename ${CONFIG})" == "fpga.conf" ] || [ "$(basename ${CONFIG})" == "paths.conf" ] || [ "$(basename ${CONFIG})" == "tricks.json" ] ||