diff --git a/.github/docker/ddsenabler/Dockerfile b/.github/docker/ddsenabler/Dockerfile index c085e8a0..582e2f35 100644 --- a/.github/docker/ddsenabler/Dockerfile +++ b/.github/docker/ddsenabler/Dockerfile @@ -55,12 +55,12 @@ RUN echo "fastcdr branch: <${fastcdr_branch}>" && \ RUN mkdir src && \ wget https://raw.githubusercontent.com/eProsima/DDS-Enabler/${ddsenabler_branch}/ddsenabler.repos && \ vcs import src < ddsenabler.repos && \ - cd src/Fast-CDR && git checkout ${fastcdr_branch} && cd ../.. && \ - cd src/Fast-DDS && git checkout ${fastdds_branch} && cd ../.. && \ - cd src/Dev-Utils && git checkout ${devutils_branch} && cd ../.. && \ - cd src/DDS-Pipe && git checkout ${ddspipe_branch} && cd ../.. && \ - cd src/DDS-Enabler && git checkout ${ddsenabler_branch} && cd ../.. && \ - cp -r src/Fast-DDS/examples/cpp/configuration src/. && \ + cd src/fastcdr && git checkout ${fastcdr_branch} && cd ../.. && \ + cd src/fastdds && git checkout ${fastdds_branch} && cd ../.. && \ + cd src/dev-utils && git checkout ${devutils_branch} && cd ../.. && \ + cd src/ddspipe && git checkout ${ddspipe_branch} && cd ../.. && \ + cd src/ddsenabler && git checkout ${ddsenabler_branch} && cd ../.. && \ + cp -r src/fastdds/examples/cpp/configuration src/. && \ colcon build --packages-skip ddsenabler ddsenabler_docs --event-handlers console_direct+ --cmake-args -DLOG_INFO=ON -DSECURITY=ON && \ colcon build --packages-select ddsenabler --event-handlers console_direct+ --cmake-args -DLOG_INFO=ON -DCOMPILE_EXAMPLES=ON diff --git a/README.md b/README.md index 34318da7..b94b34bc 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -[![DDS Enabler](resources/images/github_banner_ddsenabler.png)](https://eprosima.com/middleware/tools/DDS-Enabler) +[![DDS Enabler](resources/images/github_banner_ddsenabler.png)](https://dds-enabler.readthedocs.io/en/latest/02-formalia/titlepage.html)
@@ -60,7 +60,6 @@ Find more about us at [eProsima’s webpage](https://eprosima.com/). Explore the full user and developer guide hosted on ReadTheDocs: -- [Introduction](https://dds-enabler.readthedocs.io/en/latest/rst/02-formalia/titlepage.html) -- [Project Overview](https://dds-enabler.readthedocs.io/en/latest/rst/getting_started/project_overview.html) -- [User Manual](https://dds-enabler.readthedocs.io/en/latest/rst/user_manual/context_broker_interface.html) -- [API Reference](https://dds-enabler.readthedocs.io/en/latest/rst/ddsenabler/api_reference/api_reference.html) +- [Introduction](https://dds-enabler.readthedocs.io/en/latest/02-formalia/titlepage.html) +- [Installation Guide](https://dds-enabler.readthedocs.io/en/latest/ddsenabler/installation/linux.html) +- [API Reference](https://dds-enabler.readthedocs.io/en/latest/ddsenabler/api_reference/api_reference.html) diff --git a/ddsenabler.repos b/ddsenabler.repos index 89f512f4..7d14338f 100644 --- a/ddsenabler.repos +++ b/ddsenabler.repos @@ -3,23 +3,23 @@ repositories: type: git url: https://github.com/eProsima/foonathan_memory_vendor.git version: v1.3.1 - Fast-CDR: + fastcdr: type: git url: https://github.com/eProsima/Fast-CDR.git version: master - Fast-DDS: + fastdds: type: git url: https://github.com/eProsima/Fast-DDS.git version: master - Dev-Utils: + dev-utils: type: git url: https://github.com/eProsima/dev-utils.git version: main - DDS-Pipe: + ddspipe: type: git url: https://github.com/eProsima/DDS-Pipe.git version: main - DDS-Enabler: + ddsenabler: type: git url: https://github.com/eProsima/DDS-Enabler.git version: main diff --git a/ddsenabler/examples/CMakeLists.txt b/ddsenabler/examples/CMakeLists.txt index fc89d7fe..8d8fa754 100644 --- a/ddsenabler/examples/CMakeLists.txt +++ b/ddsenabler/examples/CMakeLists.txt @@ -12,38 +12,15 @@ # See the License for the specific language governing permissions and # limitations under the License. -cmake_minimum_required(VERSION 3.20) -project(ddsenabler_example LANGUAGES CXX) - -add_executable(ddsenabler_example main.cpp) - -target_link_libraries(ddsenabler_example PRIVATE ddsenabler) - -# Install rule -install(TARGETS ddsenabler_example - RUNTIME DESTINATION bin +# Install persistence directory +install( + DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/persistence + DESTINATION examples ) -# Copy the json files over to the build directory -file(GLOB_RECURSE JSON_FILES ${CMAKE_CURRENT_SOURCE_DIR}/*.json) -foreach(JSON_FILE_COMPLETE_PATH ${JSON_FILES}) - get_filename_component(JSON_FILE ${JSON_FILE_COMPLETE_PATH} NAME_WE) - configure_file( - ${JSON_FILE_COMPLETE_PATH} - ${CMAKE_CURRENT_BINARY_DIR}/${JSON_FILE}.json - COPYONLY) - install(FILES ${JSON_FILE_COMPLETE_PATH} - DESTINATION ${DATA_INSTALL_DIR}/ddsenabler/examples/ddsenabler/${BIN_INSTALL_DIR}) -endforeach() +# Add subdirectories for examples +add_subdirectory(utils) +add_subdirectory(publish) +add_subdirectory(service) +add_subdirectory(action) -# Copy the yaml files over to the build directory -file(GLOB_RECURSE YAML_FILES ${CMAKE_CURRENT_SOURCE_DIR}/*.yml) -foreach(YAML_FILE_COMPLETE_PATH ${YAML_FILES}) - get_filename_component(YAML_FILE ${YAML_FILE_COMPLETE_PATH} NAME_WE) - configure_file( - ${YAML_FILE_COMPLETE_PATH} - ${CMAKE_CURRENT_BINARY_DIR}/${YAML_FILE}.yml - COPYONLY) - install(FILES ${YAML_FILE_COMPLETE_PATH} - DESTINATION ${DATA_INSTALL_DIR}/ddsenabler/examples/ddsenabler/${BIN_INSTALL_DIR}) -endforeach() diff --git a/ddsenabler/examples/action/CLIParser.hpp b/ddsenabler/examples/action/CLIParser.hpp new file mode 100644 index 00000000..387c7350 --- /dev/null +++ b/ddsenabler/examples/action/CLIParser.hpp @@ -0,0 +1,339 @@ +// Copyright 2025 Proyectos y Sistemas de Mantenimiento SL (eProsima). +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include +#include +#include +#include + +#include + +#pragma once + +class CLIParser +{ +public: + + CLIParser() = delete; + + struct example_config + { + std::string config_file_path = ""; + std::string action_name = "fibonacci/_action/"; + bool announce_server = true; + uint32_t timeout = 30; + std::string persistence_path = ""; + std::string goals_path = ""; + uint32_t expected_requests = 0; + uint32_t request_initial_wait = 0; + bool cancel_requests = false; + }; + + /** + * @brief Print usage help message and exit with the given return code + * + * @param return_code return code to exit with + * + * @warning This method finishes the execution of the program with the input return code + */ + static void print_help( + uint8_t return_code) + { + std::cout << "Usage: ddsenabler_example_action [options]" << + std::endl; + std::cout << "" << + std::endl; + std::cout << "--config Path to the configuration file" << + std::endl; + std::cout << " (optional, if not provided default config is used)" << + std::endl; + std::cout << "--action-name Name of the action to be registered" << + std::endl; + std::cout << " (Default: 'Fibonacci/_action/')" << + std::endl; + std::cout << "--timeout Time (seconds) to wait before stopping the" << + std::endl; + std::cout << " program if expectations are not met" << + std::endl; + std::cout << " (Default: 30)" << + std::endl; + std::cout << "--persistence-path Path to the persistence directory" << + std::endl; + std::cout << " (No default value as it is required)" << + std::endl; + std::cout << "\n-------------------------------------SERVER OPTIONS------------------------------------\n" << + std::endl; + std::cout << "--expected-requests Number of requests expected to be received" << + std::endl; + std::cout << " (Default: 0, meaning infinite)" << + std::endl; + std::cout << "\n-------------------------------------CLIENT OPTIONS------------------------------------\n" << + std::endl; + std::cout << "--request-initial-wait Time (seconds) to wait before starting" << + std::endl; + std::cout << " requests publication since server matching" << + std::endl; + std::cout << " (Default: 0)" << + std::endl; + std::cout << "--cancel-requests Whether to cancel requests after sending them" << + std::endl; + std::cout << " (Default: false)" << + std::endl; + std::cout << "--goals-path Directory containing goal JSON files" << + std::endl; + std::cout << " (No default value as it is required for client mode)" << + std::endl; + std::exit(return_code); + } + + /** + * @brief Parse the command line options and return the configuration_config object + * + * @param argc number of arguments + * @param argv array of arguments + * @return configuration_config object with the parsed options + * + * @warning This method finishes the execution of the program if the input arguments are invalid + */ + static example_config parse_cli_options( + int argc, + char* argv[]) + { + example_config config; + + if (argc < 2) + { + std::cerr << "Mode is required as the first argument: 'client' or 'server'" << std::endl; + print_help(EXIT_FAILURE); + } + + bool client_flag = false; + bool server_flag = false; + + // First positional argument must be the mode + std::string mode = argv[1]; + if (mode == "-h" || mode == "--help") + { + print_help(EXIT_SUCCESS); + } + else if (mode == "client") + { + client_flag = true; + config.announce_server = false; + } + else if (mode == "server") + { + server_flag = true; + config.announce_server = true; + } + else + { + std::cerr << "Invalid mode '" << mode << "'. First argument must be 'client' or 'server'" << std::endl; + print_help(EXIT_FAILURE); + } + + // Parse optional arguments starting from index 2 + for (int i = 2; i < argc; ++i) + { + std::string arg = argv[i]; + + if (arg == "-h" || arg == "--help") + { + print_help(EXIT_SUCCESS); + } + else if (arg == "--config") + { + if (++i < argc) + { + config.config_file_path = argv[i]; + if (!std::filesystem::exists(config.config_file_path)) + { + std::cerr << "Invalid configuration file path: " << config.config_file_path << std::endl; + print_help(EXIT_FAILURE); + } + } + else + { + std::cerr << "Failed to parse --config argument" << std::endl; + print_help(EXIT_FAILURE); + } + } + else if (arg == "--action-name") + { + if (++i < argc) + { + config.action_name = argv[i]; + } + else + { + std::cerr << "Failed to parse --action-name argument" << std::endl; + print_help(EXIT_FAILURE); + } + } + else if (arg == "--timeout") + { + if (++i < argc) + { + try + { + config.timeout = static_cast(std::stoi(argv[i])); + } + catch (const std::exception& e) + { + std::cerr << "Invalid --timeout argument " << argv[i] << ": " << e.what() << std::endl; + print_help(EXIT_FAILURE); + } + } + else + { + std::cerr << "Failed to parse --timeout argument" << std::endl; + print_help(EXIT_FAILURE); + } + } + else if (arg == "--persistence-path") + { + if (++i < argc) + { + config.persistence_path = argv[i]; + } + else + { + std::cerr << "Failed to parse --persistence-path argument" << std::endl; + print_help(EXIT_FAILURE); + } + } + else if (arg == "--expected-requests") + { + if (++i < argc) + { + try + { + config.expected_requests = static_cast(std::stoi(argv[i])); + } + catch (const std::exception& e) + { + std::cerr << "Invalid --expected-requests argument " << argv[i] << ": " << e.what() << + std::endl; + print_help(EXIT_FAILURE); + } + } + else + { + std::cerr << "Failed to parse --expected-requests argument" << std::endl; + print_help(EXIT_FAILURE); + } + } + else if (arg == "--request-initial-wait") + { + if (++i < argc) + { + try + { + config.request_initial_wait = static_cast(std::stoi(argv[i])); + } + catch (const std::exception& e) + { + std::cerr << "Invalid --request-initial-wait argument " << argv[i] << ": " << e.what() << + std::endl; + print_help(EXIT_FAILURE); + } + } + else + { + std::cerr << "Failed to parse --request-initial-wait argument" << std::endl; + print_help(EXIT_FAILURE); + } + } + else if (arg == "--cancel-requests") + { + if (++i < argc) + { + config.cancel_requests = (std::string(argv[i]) == "true") || + (std::string(argv[i]) == "True") || + (std::string(argv[i]) == "1"); + } + else + { + std::cerr << "Failed to parse --cancel-requests argument" << std::endl; + print_help(EXIT_FAILURE); + } + } + else if (arg == "--goals-path") + { + if (++i < argc) + { + config.goals_path = argv[i]; + if (!std::filesystem::exists(config.goals_path) || + !std::filesystem::is_directory(config.goals_path)) + { + std::cerr << "Invalid --goals-path directory: " << config.goals_path << std::endl; + print_help(EXIT_FAILURE); + } + } + else + { + std::cerr << "Failed to parse --goals-path argument" << std::endl; + print_help(EXIT_FAILURE); + } + } + else + { + std::cerr << "Failed to parse unknown argument: " << arg << std::endl; + print_help(EXIT_FAILURE); + } + } + + // Sanity check (should be set from the positional mode) + if (!client_flag && !server_flag) + { + std::cerr << "Either 'client' or 'server' must be specified as the first argument" << std::endl; + print_help(EXIT_FAILURE); + } + + if (client_flag && config.goals_path.empty()) + { + std::cerr << "--goals-path is required in client mode" << std::endl; + print_help(EXIT_FAILURE); + } + + return config; + } + + /** + * @brief Parse the signal number into the signal name + * + * @param signum signal number + * @return std::string signal name + */ + static std::string parse_signal( + const int& signum) + { + switch (signum) + { + case SIGINT: + return "SIGINT"; + case SIGTERM: + return "SIGTERM"; +#ifndef _WIN32 + case SIGQUIT: + return "SIGQUIT"; + case SIGHUP: + return "SIGHUP"; +#endif // _WIN32 + default: + return "UNKNOWN SIGNAL"; + } + } + +}; diff --git a/ddsenabler/examples/action/CMakeLists.txt b/ddsenabler/examples/action/CMakeLists.txt new file mode 100644 index 00000000..97e1d660 --- /dev/null +++ b/ddsenabler/examples/action/CMakeLists.txt @@ -0,0 +1,32 @@ +# Copyright 2025 Proyectos y Sistemas de Mantenimiento SL (eProsima). +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +cmake_minimum_required(VERSION 3.20) +project(ddsenabler_example_action LANGUAGES CXX) + +add_executable(ddsenabler_example_action main.cpp) + +target_include_directories(ddsenabler_example_action PRIVATE ${PROJECT_SOURCE_DIR}/../utils) +target_link_libraries(ddsenabler_example_action PRIVATE ddsenabler utils) + +# Install rule +install(TARGETS ddsenabler_example_action + RUNTIME DESTINATION examples/action +) + +# Install goals directory +install( + DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/goals + DESTINATION examples/action +) diff --git a/ddsenabler/examples/action/Readme.md b/ddsenabler/examples/action/Readme.md new file mode 100644 index 00000000..9e7fa8b3 --- /dev/null +++ b/ddsenabler/examples/action/Readme.md @@ -0,0 +1,31 @@ +# Action Example Readme + +This example demonstrates the usage of the DDS Enabler for implementing an action. The example includes a client and a server setup for the desired action. + +By default, the example is prepared for the `Fibonacci` action, providing a standard reply with a constant result of the Fibonacci sequence of order 5 for any request. + +Users are encouraged to implement their own functional server logic as needed in the `server_specific_logic` function. The persistence/goals directory must be modified to suit the specific action type and its expected goal requests. + +## Example Commands + +### CLIENT +```bash +./install/ddsenabler/examples/action/ddsenabler_example_action client \ + --persistence-path ./install/ddsenabler/examples/persistence/ \ + --goals-path ./install/ddsenabler/examples/action/goals/ \ + --request-initial-wait 3 + --cancel-requests false + --config $PATH_TO_CONFIG_FILE + --action-name $ACTION_NAME + --timeout N +``` + +### SERVER +```bash +./install/ddsenabler/examples/action/ddsenabler_example_action server \ + --persistence-path ./install/ddsenabler/examples/persistence/ + --config $PATH_TO_CONFIG_FILE + --expected-requests N + --action-name $ACTION_NAME + --timeout N +``` diff --git a/ddsenabler/examples/action/goals/1 b/ddsenabler/examples/action/goals/1 new file mode 100644 index 00000000..12762e95 --- /dev/null +++ b/ddsenabler/examples/action/goals/1 @@ -0,0 +1,3 @@ +{ + "order": 5 +} diff --git a/ddsenabler/examples/action/goals/2 b/ddsenabler/examples/action/goals/2 new file mode 100644 index 00000000..b733ce25 --- /dev/null +++ b/ddsenabler/examples/action/goals/2 @@ -0,0 +1,3 @@ +{ + "order": 6 +} diff --git a/ddsenabler/examples/action/goals/3 b/ddsenabler/examples/action/goals/3 new file mode 100644 index 00000000..327d3eb8 --- /dev/null +++ b/ddsenabler/examples/action/goals/3 @@ -0,0 +1,3 @@ +{ + "order": 7 +} diff --git a/ddsenabler/examples/action/main.cpp b/ddsenabler/examples/action/main.cpp new file mode 100644 index 00000000..1b571a28 --- /dev/null +++ b/ddsenabler/examples/action/main.cpp @@ -0,0 +1,614 @@ +// Copyright 2025 Proyectos y Sistemas de Mantenimiento SL (eProsima). +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/** + * @file main.cpp + * + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "Utils.hpp" + +#include "ddsenabler/dds_enabler_runner.hpp" +#include "ddsenabler/DDSEnabler.hpp" + +#include "CLIParser.hpp" + +CLIParser::example_config config; +bool action_discovered_ = false; +uint32_t received_results_ = 0; +std::vector> received_requests_; +std::mutex app_mutex_; +std::condition_variable app_cv_; +bool stop_app_ = false; + +const std::string REQUESTS_SUBDIR = "goals"; +const std::string TYPES_SUBDIR = "types"; +const std::string ACTION_SUBDIR = "actions"; + +void signal_handler( + int signum) +{ + std::cout << "Signal " << CLIParser::parse_signal(signum) << " received, stopping..." << std::endl; + { + std::lock_guard lock(app_mutex_); + stop_app_ = true; + } + app_cv_.notify_all(); +} + +// Static log callback +void test_log_callback( + const char* file_name, + int line_no, + const char* func_name, + int category, + const char* msg) +{ + // NOTE: Default stdout logs can be disabled via configuration ("logging": {"stdout": false}) to avoid duplicated traces + + std::stringstream ss; + ss << file_name << ":" << line_no << " (" << func_name << "): " << msg << std::endl; + if (category == eprosima::utils::Log::Kind::Warning) + { + std::cerr << "[WARNING] " << ss.str(); + } + else if (category == eprosima::utils::Log::Kind::Error) + { + std::cerr << "[ERROR] " << ss.str(); + } +} + +// Static type notification callback +static void test_type_notification_callback( + const char* type_name, + const char* serialized_type, + const unsigned char* serialized_type_internal, + uint32_t serialized_type_internal_size, + const char* data_placeholder) +{ + std::lock_guard lock(app_mutex_); + std::cout << "Type callback received: " << type_name << std::endl; + if (!config.persistence_path.empty() && + !utils::save_type_to_file((std::filesystem::path(config.persistence_path) / TYPES_SUBDIR).string(), + type_name, + serialized_type_internal, serialized_type_internal_size)) + { + std::cerr << "Failed to save type: " << type_name << std::endl; + } +} + +// Static type query callback +static bool test_type_query_callback( + const char* type_name, + std::unique_ptr& serialized_type_internal, + uint32_t& serialized_type_internal_size) +{ + if (config.persistence_path.empty()) + { + std::cerr << "Persistence path is not set, cannot query type: " << type_name << std::endl; + return false; + } + + // Load the type from file + if (!utils::load_type_from_file((std::filesystem::path(config.persistence_path) / TYPES_SUBDIR).string(), type_name, + serialized_type_internal, serialized_type_internal_size)) + { + std::cerr << "Failed to load type: " << type_name << std::endl; + return false; + } + return true; +} + +// Static data notification callback +static void test_data_notification_callback( + const char* topic_name, + const char* json, + int64_t publish_time) +{ +} + +// Static topic notification callback +static void test_topic_notification_callback( + const char* topic_name, + const eprosima::ddsenabler::participants::TopicInfo& topic_info) +{ +} + +// Static type query callback +static bool test_topic_query_callback( + const char* topic_name, + eprosima::ddsenabler::participants::TopicInfo& topic_info) +{ + return false; +} + +// Static action notification callback +static void test_action_notification_callback( + const char* action_name, + const eprosima::ddsenabler::participants::ActionInfo& action_info) +{ + std::lock_guard lock(app_mutex_); + if (config.action_name == std::string(action_name)) + { + std::cout << "Action callback received: " << action_name << std::endl; + + std::string action_file = (std::filesystem::path( + config.persistence_path) / + ACTION_SUBDIR + ).string(); + if (!config.persistence_path.empty()) + { + utils::save_action_to_file( + action_file, + action_name, + action_info); + } + action_discovered_ = true; + app_cv_.notify_all(); + } + else + { + std::cout << "Ignoring action callback for: " << action_name << std::endl; + } +} + +// Static action query callback +static bool test_action_query_callback( + const char* action_name, + eprosima::ddsenabler::participants::ActionInfo& action_info) +{ + std::lock_guard lock(app_mutex_); + if (config.action_name == std::string(action_name)) + { + std::cout << "Action query callback received: " << action_name << std::endl; + + if (!config.persistence_path.empty()) + { + std::string action_file = (std::filesystem::path( + config.persistence_path) / + ACTION_SUBDIR + ).string(); + if (!utils::load_action_from_file( + action_file, + action_name, + action_info)) + { + std::cerr << "Failed to load action: " << action_name << std::endl; + return false; + } + return true; + } + } + else + { + std::cout << "Ignoring action query callback for: " << action_name << std::endl; + } + return false; +} + +// Static action goal request notification callback +static bool test_action_goal_request_notification_callback( + const char* action_name, + const char* json, + const eprosima::ddsenabler::participants::UUID& goal_id, + int64_t publish_time) +{ + std::lock_guard lock(app_mutex_); + if (config.action_name == std::string(action_name)) + { + std::cout << "Action goal request callback received: " << action_name << std::endl; + received_requests_.emplace_back(goal_id, json); + app_cv_.notify_all(); + return true; + } + return false; +} + +// Static action result notification callback +static void test_action_result_notification_callback( + const char* action_name, + const char* json, + const eprosima::ddsenabler::participants::UUID& goal_id, + int64_t publish_time) +{ + std::lock_guard lock(app_mutex_); + std::cout << "Action result callback received: " << action_name << std::endl; + received_results_++; + app_cv_.notify_all(); +} + +// Static action feedback notification callback +static void test_action_feedback_notification_callback( + const char* action_name, + const char* json, + const eprosima::ddsenabler::participants::UUID& goal_id, + int64_t publish_time) +{ + std::lock_guard lock(app_mutex_); + std::cout << "Action feedback callback received for action: " << action_name << std::endl; +} + +// Static action status notification callback +static void test_action_status_notification_callback( + const char* action_name, + const eprosima::ddsenabler::participants::UUID& goal_id, + eprosima::ddsenabler::participants::StatusCode statusCode, + const char* statusMessage, + int64_t publish_time) +{ + std::lock_guard lock(app_mutex_); + std::cout << "Action status callback received: " << statusMessage << std::endl; +} + +// Static action cancel request notification callback +static void test_action_cancel_request_notification_callback( + const char* action_name, + const eprosima::ddsenabler::participants::UUID& goal_id, + int64_t timestamp, + uint64_t request_id, + int64_t publish_time) +{ + // NOT IMPLEMENTED +} + +bool wait_for_action_discovery( + uint32_t timeout, + std::mutex& app_mutex, + std::condition_variable& app_cv) +{ + std::unique_lock lock(app_mutex); + if (!app_cv.wait_for(lock, std::chrono::seconds(timeout), + []() + { + return stop_app_ || action_discovered_; + }) || stop_app_) + { + if (!stop_app_) + { + std::cerr << "Timeout waiting for service discovery." << std::endl; + } + return false; + } + return true; +} + +bool wait_for_action_request( + uint32_t timeout, + std::mutex& app_mutex, + std::condition_variable& app_cv, + eprosima::ddsenabler::participants::UUID& request_id, + std::string& goal_json) +{ + std::unique_lock lock(app_mutex); + if (!app_cv.wait_for(lock, std::chrono::seconds(timeout), + []() + { + return stop_app_ || !received_requests_.empty(); + }) || stop_app_) + { + if (!stop_app_) + { + std::cerr << "Timeout waiting for service request." << std::endl; + } + return false; + } + + request_id = received_requests_.back().first; + goal_json = received_requests_.back().second; + received_requests_.pop_back(); + return true; +} + +bool wait_for_action_result( + uint32_t timeout, + std::mutex& app_mutex, + std::condition_variable& app_cv, + uint32_t sent_requests) +{ + std::unique_lock lock(app_mutex); + if (!app_cv.wait_for(lock, std::chrono::seconds(timeout), + [&sent_requests]() + { + return stop_app_ || received_results_ >= sent_requests; + }) || stop_app_) + { + if (!stop_app_) + { + std::cerr << "Timeout waiting for action result." << std::endl; + } + return false; + } + return true; +} + +bool client_routine( + std::shared_ptr enabler, + const std::string& action_name, + const std::string& goal_path, + uint32_t timeout, + uint32_t request_initial_wait, + bool cancel_requests) +{ + // Wait for service to be discovered + if (!wait_for_action_discovery(timeout, app_mutex_, app_cv_)) + { + return false; + } + + // Wait a bit before starting to publish so types and topics can be discovered + std::this_thread::sleep_for(std::chrono::seconds(request_initial_wait)); + + // Get collection of files to publish, sorted in increasing order by their name (assumed to be numeric) + std::vector> sample_files; + utils::get_sorted_files(goal_path, sample_files); + uint32_t sent_requests = 0; + for (const auto& [path, number] : sample_files) + { + eprosima::ddsenabler::participants::UUID request_id; + std::ifstream file(path, std::ios::binary); + if (file) + { + std::string file_content((std::istreambuf_iterator(file)), std::istreambuf_iterator()); + if (enabler->send_action_goal(action_name, file_content, request_id)) + { + std::cout << "Published content from file: " << path.filename() << " in service: " + << action_name << std::endl; + sent_requests++; + } + else + { + std::cerr << "Failed to publish content from file: " << path.filename() << " in service: " + << action_name << std::endl; + return false; + } + } + else + { + std::cerr << "Failed to open file: " << path << std::endl; + } + if (cancel_requests) + { + std::this_thread::sleep_for(std::chrono::milliseconds(500)); // Simulate processing time + // Cancel the request after sending it + if (!enabler->cancel_action_goal(action_name, request_id)) + { + std::cerr << "Failed to send action cancel request." << std::endl; + return false; + } + std::cout << "Sent cancel request for action: " << action_name << std::endl; + } + + // Wait publish period or until stop signal is received + else if (!wait_for_action_result(timeout, app_mutex_, app_cv_, sent_requests)) + { + return false; + } + } + std::this_thread::sleep_for(std::chrono::seconds(1)); // Wait for any last feedback/result/cancel notifications + return true; +} + +bool server_specific_logic( + std::shared_ptr enabler, + const std::string& action_name, + uint64_t fibonacci_number, + const eprosima::ddsenabler::participants::UUID& request_id) +{ + std::vector fibonacci_sequence = {0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610}; + std::string json = "{\"sequence\": ["; + std::string feedback_json = "{\"partial_sequence\": ["; + for (size_t i = 0; i < fibonacci_number; ++i) + { + { + std::lock_guard lock(app_mutex_); + if (stop_app_) + { + return false; + } + } + + json += std::to_string(fibonacci_sequence[i]); + feedback_json += std::to_string(fibonacci_sequence[i]); + + std::string feedback_tmp = feedback_json; + feedback_tmp += "]}"; + if (!enabler->send_action_feedback( + action_name.c_str(), + feedback_tmp.c_str(), + request_id)) + { + std::cerr << "Failed to send action feedback" << std::endl; + return false; + } + std::this_thread::sleep_for(std::chrono::milliseconds(500)); + if (i != fibonacci_number - 1) + { + json += ", "; + feedback_json += ", "; + } + } + json += "]}"; + + if (!enabler->send_action_result( + action_name.c_str(), + request_id, + eprosima::ddsenabler::participants::StatusCode::SUCCEEDED, + json.c_str())) + { + std::cerr << "Failed to send action result" << std::endl; + return false; + } + + return true; +} + +bool server_routine( + std::shared_ptr enabler, + const std::string& action_name, + uint32_t expected_requests, + uint32_t timeout) +{ + // Announce action + if (!enabler->announce_action(action_name)) + { + std::cerr << "Failed to announce action: " << action_name << std::endl; + return false; + } + + std::cout << "Action announced: " << action_name << std::endl; + bool stop_app = false; + { + std::lock_guard lock(app_mutex_); + stop_app = stop_app_; + } + while (!stop_app) + { + eprosima::ddsenabler::participants::UUID request_id; + std::string goal_json; + if (!wait_for_action_request(timeout, app_mutex_, app_cv_, request_id, goal_json)) + { + return false; + } + + uint64_t fibonacci_number = 5; // Default Fibonacci number, can be parsed from json if needed + std::cout << "Received request for action: " << action_name << std::endl; + std::this_thread::sleep_for(std::chrono::milliseconds(500)); // Simulate processing time + + // Send Feedback & Result + if (!server_specific_logic( + enabler, + action_name, + fibonacci_number, + request_id)) + { + std::cerr << "Failed to process action: " << action_name << std::endl; + return false; + } + + // Check if we have received the expected number of requests (or run indefinitely if expected_requests is 0) + { + std::lock_guard lock(app_mutex_); + if (expected_requests != 0 && ++received_results_ >= expected_requests) + { + break; + } + stop_app = stop_app_; + } + } + + // Revoke service + if (!enabler->revoke_action(action_name)) + { + std::cerr << "Failed to revoke service: " << action_name << std::endl; + return false; + } + + return true; +} + +int main( + int argc, + char** argv) +{ + signal(SIGINT, signal_handler); + signal(SIGTERM, signal_handler); +#ifndef _WIN32 + signal(SIGQUIT, signal_handler); + signal(SIGHUP, signal_handler); +#endif // _WIN32 + + using namespace eprosima::ddsenabler; + + eprosima::utils::Log::ReportFilenames(true); + + // Parse CLI options + config = CLIParser::parse_cli_options(argc, argv); + + // Initialize persistence if required + std::vector subdirs = {TYPES_SUBDIR, ACTION_SUBDIR}; + utils::init_persistence(config.persistence_path, subdirs); + + // Set up callbacks + CallbackSet callbacks{ + test_log_callback, + { + test_type_notification_callback, + test_topic_notification_callback, + test_data_notification_callback, + test_type_query_callback, + test_topic_query_callback + }, + { + // NOTE: Service callbacks are not used in this example, but can be added if needed + }, + { + test_action_notification_callback, + test_action_goal_request_notification_callback, + test_action_feedback_notification_callback, + test_action_cancel_request_notification_callback, + test_action_result_notification_callback, + test_action_status_notification_callback, + test_action_query_callback + } + }; + + std::shared_ptr enabler; + bool enabler_created = false; + if (config.config_file_path.empty()) + { + enabler_created = create_dds_enabler(yaml::EnablerConfiguration(""), callbacks, enabler); + } + else + { + enabler_created = create_dds_enabler(config.config_file_path.c_str(), callbacks, enabler); + } + if (!enabler_created) + { + std::cerr << "Failed to create DDSEnabler instance." << std::endl; + return EXIT_FAILURE; + } + + bool ret = false; + // Service logic based on announce_server flag + if (config.announce_server) + { + + ret = server_routine(enabler, config.action_name, config.expected_requests, config.timeout); + } + else + { + if (config.goals_path.empty()) + { + std::cerr << "Request path is not set." << std::endl; + return EXIT_FAILURE; + } + + ret = client_routine(enabler, config.action_name, config.goals_path, config.timeout, + config.request_initial_wait, + config.cancel_requests); + } + + return ret ? EXIT_SUCCESS : EXIT_FAILURE; +} diff --git a/ddsenabler/examples/config.yml b/ddsenabler/examples/config.yml deleted file mode 100644 index 5cc4050d..00000000 --- a/ddsenabler/examples/config.yml +++ /dev/null @@ -1,26 +0,0 @@ -dds: - domain: 0 - - allowlist: - - name: "topic_name" - - name: "HelloWorldTopic" - type: "HelloWorld" - - blocklist: - - name: "topic_to_block" - type: "type_to_block" - - topics: - - name: "temperature/*" - type: "temperature/types/*" - qos: - max-rx-rate: 15 - downsampling: 2 - - ignore-participant-flags: filter_same_process - transport: shm - whitelist-interfaces: - - "127.0.0.1" - -ddsenabler: - initial-publish-wait: 500 diff --git a/ddsenabler/examples/persistence/actions/fibonacci__action_ b/ddsenabler/examples/persistence/actions/fibonacci__action_ new file mode 100644 index 00000000..22db6867 --- /dev/null +++ b/ddsenabler/examples/persistence/actions/fibonacci__action_ @@ -0,0 +1,19 @@ +{ + "action_name": "fibonacci/_action/", + "cancel_reply_action_serialized_qos": "reliability: true\ndurability: false\nownership: false\nkeyed: false", + "cancel_reply_action_type": "action_msgs::srv::dds_::CancelGoal_Response_", + "cancel_request_action_serialized_qos": "reliability: true\ndurability: false\nownership: false\nkeyed: false", + "cancel_request_action_type": "action_msgs::srv::dds_::CancelGoal_Request_", + "feedback_action_serialized_qos": "reliability: true\ndurability: false\nownership: false\nkeyed: false", + "feedback_action_type": "action_tutorials_interfaces::action::dds_::Fibonacci_FeedbackMessage_", + "goal_reply_action_serialized_qos": "reliability: true\ndurability: false\nownership: false\nkeyed: false", + "goal_reply_action_type": "action_tutorials_interfaces::action::dds_::Fibonacci_SendGoal_Response_", + "goal_request_action_serialized_qos": "reliability: true\ndurability: false\nownership: false\nkeyed: false", + "goal_request_action_type": "action_tutorials_interfaces::action::dds_::Fibonacci_SendGoal_Request_", + "result_reply_action_serialized_qos": "reliability: true\ndurability: false\nownership: false\nkeyed: false", + "result_reply_action_type": "action_tutorials_interfaces::action::dds_::Fibonacci_GetResult_Response_", + "result_request_action_serialized_qos": "reliability: true\ndurability: false\nownership: false\nkeyed: false", + "result_request_action_type": "action_tutorials_interfaces::action::dds_::Fibonacci_GetResult_Request_", + "status_action_serialized_qos": "reliability: true\ndurability: true\nownership: false\nkeyed: false", + "status_action_type": "action_msgs::msg::dds_::GoalStatusArray_" +} \ No newline at end of file diff --git a/ddsenabler/examples/persistence/services/add_two_ints b/ddsenabler/examples/persistence/services/add_two_ints new file mode 100644 index 00000000..5af99fa0 --- /dev/null +++ b/ddsenabler/examples/persistence/services/add_two_ints @@ -0,0 +1,7 @@ +{ + "reply_serialized_qos": "reliability: true\ndurability: false\nownership: false\nkeyed: false", + "reply_type_name": "example_interfaces::srv::dds_::AddTwoInts_Response_", + "request_serialized_qos": "reliability: true\ndurability: false\nownership: false\nkeyed: false", + "request_type_name": "example_interfaces::srv::dds_::AddTwoInts_Request_", + "service_name": "add_two_ints" +} \ No newline at end of file diff --git a/ddsenabler/examples/persistence/topics/rt_chatter.bin b/ddsenabler/examples/persistence/topics/rt_chatter.bin new file mode 100644 index 00000000..87325663 Binary files /dev/null and b/ddsenabler/examples/persistence/topics/rt_chatter.bin differ diff --git a/ddsenabler/examples/persistence/types/action_msgs__msg__dds___GoalStatusArray_.bin b/ddsenabler/examples/persistence/types/action_msgs__msg__dds___GoalStatusArray_.bin new file mode 100644 index 00000000..9b2666b4 Binary files /dev/null and b/ddsenabler/examples/persistence/types/action_msgs__msg__dds___GoalStatusArray_.bin differ diff --git a/ddsenabler/examples/persistence/types/action_msgs__srv__dds___CancelGoal_Request_.bin b/ddsenabler/examples/persistence/types/action_msgs__srv__dds___CancelGoal_Request_.bin new file mode 100644 index 00000000..3312fb7d Binary files /dev/null and b/ddsenabler/examples/persistence/types/action_msgs__srv__dds___CancelGoal_Request_.bin differ diff --git a/ddsenabler/examples/persistence/types/action_msgs__srv__dds___CancelGoal_Response_.bin b/ddsenabler/examples/persistence/types/action_msgs__srv__dds___CancelGoal_Response_.bin new file mode 100644 index 00000000..9b9e0b42 Binary files /dev/null and b/ddsenabler/examples/persistence/types/action_msgs__srv__dds___CancelGoal_Response_.bin differ diff --git a/ddsenabler/examples/persistence/types/action_tutorials_interfaces__action__dds___Fibonacci_FeedbackMessage_.bin b/ddsenabler/examples/persistence/types/action_tutorials_interfaces__action__dds___Fibonacci_FeedbackMessage_.bin new file mode 100644 index 00000000..fac6aee9 Binary files /dev/null and b/ddsenabler/examples/persistence/types/action_tutorials_interfaces__action__dds___Fibonacci_FeedbackMessage_.bin differ diff --git a/ddsenabler/examples/persistence/types/action_tutorials_interfaces__action__dds___Fibonacci_GetResult_Request_.bin b/ddsenabler/examples/persistence/types/action_tutorials_interfaces__action__dds___Fibonacci_GetResult_Request_.bin new file mode 100644 index 00000000..423f3111 Binary files /dev/null and b/ddsenabler/examples/persistence/types/action_tutorials_interfaces__action__dds___Fibonacci_GetResult_Request_.bin differ diff --git a/ddsenabler/examples/persistence/types/action_tutorials_interfaces__action__dds___Fibonacci_GetResult_Response_.bin b/ddsenabler/examples/persistence/types/action_tutorials_interfaces__action__dds___Fibonacci_GetResult_Response_.bin new file mode 100644 index 00000000..62adda76 Binary files /dev/null and b/ddsenabler/examples/persistence/types/action_tutorials_interfaces__action__dds___Fibonacci_GetResult_Response_.bin differ diff --git a/ddsenabler/examples/persistence/types/action_tutorials_interfaces__action__dds___Fibonacci_SendGoal_Request_.bin b/ddsenabler/examples/persistence/types/action_tutorials_interfaces__action__dds___Fibonacci_SendGoal_Request_.bin new file mode 100644 index 00000000..4bffcfdd Binary files /dev/null and b/ddsenabler/examples/persistence/types/action_tutorials_interfaces__action__dds___Fibonacci_SendGoal_Request_.bin differ diff --git a/ddsenabler/examples/persistence/types/action_tutorials_interfaces__action__dds___Fibonacci_SendGoal_Response_.bin b/ddsenabler/examples/persistence/types/action_tutorials_interfaces__action__dds___Fibonacci_SendGoal_Response_.bin new file mode 100644 index 00000000..63bb3f25 Binary files /dev/null and b/ddsenabler/examples/persistence/types/action_tutorials_interfaces__action__dds___Fibonacci_SendGoal_Response_.bin differ diff --git a/ddsenabler/examples/persistence/types/example_interfaces__srv__dds___AddTwoInts_Request_.bin b/ddsenabler/examples/persistence/types/example_interfaces__srv__dds___AddTwoInts_Request_.bin new file mode 100644 index 00000000..4b4e5e7d Binary files /dev/null and b/ddsenabler/examples/persistence/types/example_interfaces__srv__dds___AddTwoInts_Request_.bin differ diff --git a/ddsenabler/examples/persistence/types/example_interfaces__srv__dds___AddTwoInts_Response_.bin b/ddsenabler/examples/persistence/types/example_interfaces__srv__dds___AddTwoInts_Response_.bin new file mode 100644 index 00000000..6f5f40ef Binary files /dev/null and b/ddsenabler/examples/persistence/types/example_interfaces__srv__dds___AddTwoInts_Response_.bin differ diff --git a/ddsenabler/examples/persistence/types/rcl_interfaces__msg__dds___Log_.bin b/ddsenabler/examples/persistence/types/rcl_interfaces__msg__dds___Log_.bin new file mode 100644 index 00000000..6e56d56a Binary files /dev/null and b/ddsenabler/examples/persistence/types/rcl_interfaces__msg__dds___Log_.bin differ diff --git a/ddsenabler/examples/persistence/types/rcl_interfaces__msg__dds___ParameterEvent_.bin b/ddsenabler/examples/persistence/types/rcl_interfaces__msg__dds___ParameterEvent_.bin new file mode 100644 index 00000000..98649ae7 Binary files /dev/null and b/ddsenabler/examples/persistence/types/rcl_interfaces__msg__dds___ParameterEvent_.bin differ diff --git a/ddsenabler/examples/persistence/types/rcl_interfaces__srv__dds___DescribeParameters_Request_.bin b/ddsenabler/examples/persistence/types/rcl_interfaces__srv__dds___DescribeParameters_Request_.bin new file mode 100644 index 00000000..d33a48d9 Binary files /dev/null and b/ddsenabler/examples/persistence/types/rcl_interfaces__srv__dds___DescribeParameters_Request_.bin differ diff --git a/ddsenabler/examples/persistence/types/rcl_interfaces__srv__dds___DescribeParameters_Response_.bin b/ddsenabler/examples/persistence/types/rcl_interfaces__srv__dds___DescribeParameters_Response_.bin new file mode 100644 index 00000000..f7a41219 Binary files /dev/null and b/ddsenabler/examples/persistence/types/rcl_interfaces__srv__dds___DescribeParameters_Response_.bin differ diff --git a/ddsenabler/examples/persistence/types/rcl_interfaces__srv__dds___GetParameterTypes_Request_.bin b/ddsenabler/examples/persistence/types/rcl_interfaces__srv__dds___GetParameterTypes_Request_.bin new file mode 100644 index 00000000..f1cf3f19 Binary files /dev/null and b/ddsenabler/examples/persistence/types/rcl_interfaces__srv__dds___GetParameterTypes_Request_.bin differ diff --git a/ddsenabler/examples/persistence/types/rcl_interfaces__srv__dds___GetParameterTypes_Response_.bin b/ddsenabler/examples/persistence/types/rcl_interfaces__srv__dds___GetParameterTypes_Response_.bin new file mode 100644 index 00000000..55815c1f Binary files /dev/null and b/ddsenabler/examples/persistence/types/rcl_interfaces__srv__dds___GetParameterTypes_Response_.bin differ diff --git a/ddsenabler/examples/persistence/types/rcl_interfaces__srv__dds___GetParameters_Request_.bin b/ddsenabler/examples/persistence/types/rcl_interfaces__srv__dds___GetParameters_Request_.bin new file mode 100644 index 00000000..fe10b8fb Binary files /dev/null and b/ddsenabler/examples/persistence/types/rcl_interfaces__srv__dds___GetParameters_Request_.bin differ diff --git a/ddsenabler/examples/persistence/types/rcl_interfaces__srv__dds___GetParameters_Response_.bin b/ddsenabler/examples/persistence/types/rcl_interfaces__srv__dds___GetParameters_Response_.bin new file mode 100644 index 00000000..ac082bff Binary files /dev/null and b/ddsenabler/examples/persistence/types/rcl_interfaces__srv__dds___GetParameters_Response_.bin differ diff --git a/ddsenabler/examples/persistence/types/rcl_interfaces__srv__dds___ListParameters_Request_.bin b/ddsenabler/examples/persistence/types/rcl_interfaces__srv__dds___ListParameters_Request_.bin new file mode 100644 index 00000000..9b9387d7 Binary files /dev/null and b/ddsenabler/examples/persistence/types/rcl_interfaces__srv__dds___ListParameters_Request_.bin differ diff --git a/ddsenabler/examples/persistence/types/rcl_interfaces__srv__dds___ListParameters_Response_.bin b/ddsenabler/examples/persistence/types/rcl_interfaces__srv__dds___ListParameters_Response_.bin new file mode 100644 index 00000000..a1212fb4 Binary files /dev/null and b/ddsenabler/examples/persistence/types/rcl_interfaces__srv__dds___ListParameters_Response_.bin differ diff --git a/ddsenabler/examples/persistence/types/rcl_interfaces__srv__dds___SetParametersAtomically_Request_.bin b/ddsenabler/examples/persistence/types/rcl_interfaces__srv__dds___SetParametersAtomically_Request_.bin new file mode 100644 index 00000000..c19eeb6a Binary files /dev/null and b/ddsenabler/examples/persistence/types/rcl_interfaces__srv__dds___SetParametersAtomically_Request_.bin differ diff --git a/ddsenabler/examples/persistence/types/rcl_interfaces__srv__dds___SetParametersAtomically_Response_.bin b/ddsenabler/examples/persistence/types/rcl_interfaces__srv__dds___SetParametersAtomically_Response_.bin new file mode 100644 index 00000000..1043f036 Binary files /dev/null and b/ddsenabler/examples/persistence/types/rcl_interfaces__srv__dds___SetParametersAtomically_Response_.bin differ diff --git a/ddsenabler/examples/persistence/types/rcl_interfaces__srv__dds___SetParameters_Request_.bin b/ddsenabler/examples/persistence/types/rcl_interfaces__srv__dds___SetParameters_Request_.bin new file mode 100644 index 00000000..da5208da Binary files /dev/null and b/ddsenabler/examples/persistence/types/rcl_interfaces__srv__dds___SetParameters_Request_.bin differ diff --git a/ddsenabler/examples/persistence/types/rcl_interfaces__srv__dds___SetParameters_Response_.bin b/ddsenabler/examples/persistence/types/rcl_interfaces__srv__dds___SetParameters_Response_.bin new file mode 100644 index 00000000..7b8a8ec8 Binary files /dev/null and b/ddsenabler/examples/persistence/types/rcl_interfaces__srv__dds___SetParameters_Response_.bin differ diff --git a/ddsenabler/examples/persistence/types/rmw_dds_common__msg__dds___ParticipantEntitiesInfo_.bin b/ddsenabler/examples/persistence/types/rmw_dds_common__msg__dds___ParticipantEntitiesInfo_.bin new file mode 100644 index 00000000..c3236d96 Binary files /dev/null and b/ddsenabler/examples/persistence/types/rmw_dds_common__msg__dds___ParticipantEntitiesInfo_.bin differ diff --git a/ddsenabler/examples/persistence/types/std_msgs__msg__dds___String_.bin b/ddsenabler/examples/persistence/types/std_msgs__msg__dds___String_.bin new file mode 100644 index 00000000..991d0cdb Binary files /dev/null and b/ddsenabler/examples/persistence/types/std_msgs__msg__dds___String_.bin differ diff --git a/ddsenabler/examples/persistence/types/type_description_interfaces__srv__dds___GetTypeDescription_Request_.bin b/ddsenabler/examples/persistence/types/type_description_interfaces__srv__dds___GetTypeDescription_Request_.bin new file mode 100644 index 00000000..ba4d778e Binary files /dev/null and b/ddsenabler/examples/persistence/types/type_description_interfaces__srv__dds___GetTypeDescription_Request_.bin differ diff --git a/ddsenabler/examples/persistence/types/type_description_interfaces__srv__dds___GetTypeDescription_Response_.bin b/ddsenabler/examples/persistence/types/type_description_interfaces__srv__dds___GetTypeDescription_Response_.bin new file mode 100644 index 00000000..f68b4410 Binary files /dev/null and b/ddsenabler/examples/persistence/types/type_description_interfaces__srv__dds___GetTypeDescription_Response_.bin differ diff --git a/ddsenabler/examples/CLIParser.hpp b/ddsenabler/examples/publish/CLIParser.hpp similarity index 97% rename from ddsenabler/examples/CLIParser.hpp rename to ddsenabler/examples/publish/CLIParser.hpp index a9469f4f..8dfae01d 100644 --- a/ddsenabler/examples/CLIParser.hpp +++ b/ddsenabler/examples/publish/CLIParser.hpp @@ -51,7 +51,7 @@ class CLIParser static void print_help( uint8_t return_code) { - std::cout << "Usage: ddsenabler_example [options]" << std::endl; + std::cout << "Usage: ddsenabler_example_publish [options]" << std::endl; std::cout << "" << std::endl; std::cout << "--config Path to the configuration file" << std::endl; std::cout << " (Default: '')" << std::endl; @@ -302,12 +302,6 @@ class CLIParser } } - if (config.config_file_path.empty()) - { - std::cerr << "Configuration file path is required" << std::endl; - print_help(EXIT_FAILURE); - } - // Check that if publish path is set, publish topic is also set if (!config.publish_path.empty() && config.publish_topic.empty()) { diff --git a/ddsenabler/examples/publish/CMakeLists.txt b/ddsenabler/examples/publish/CMakeLists.txt new file mode 100644 index 00000000..3839f418 --- /dev/null +++ b/ddsenabler/examples/publish/CMakeLists.txt @@ -0,0 +1,32 @@ +# Copyright 2025 Proyectos y Sistemas de Mantenimiento SL (eProsima). +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +cmake_minimum_required(VERSION 3.20) +project(ddsenabler_example_publish LANGUAGES CXX) + +add_executable(ddsenabler_example_publish main.cpp) + +target_include_directories(ddsenabler_example_publish PRIVATE ${PROJECT_SOURCE_DIR}/../utils) +target_link_libraries(ddsenabler_example_publish PRIVATE ddsenabler utils) + +# Install rule +install(TARGETS ddsenabler_example_publish + RUNTIME DESTINATION examples/publish +) + +# Install samples directory +install( + DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/samples + DESTINATION examples/publish +) diff --git a/ddsenabler/examples/publish/Readme.md b/ddsenabler/examples/publish/Readme.md new file mode 100644 index 00000000..91e21945 --- /dev/null +++ b/ddsenabler/examples/publish/Readme.md @@ -0,0 +1,30 @@ +# Publish Example Readme + +This example demonstrates the usage of the DDS Enabler for implementing a publish-subscribe pattern. The example includes a publisher and a subscriber setup for the desired topic. + +The following example commands illustrate how to run the publisher and subscriber to match with the `talker` and `listener` examples provided in the ROS2 demos. + +## Example Commands + +### PUBLISHER +```bash +./install/ddsenabler/examples/publish/ddsenabler_example_publish \ + --persistence-path ./install/ddsenabler/examples/persistence/ \ + --publish-path ./install/ddsenabler/examples/publish/samples \ + --publish-period 200 \ + --publish-initial-wait 1000 \ + --publish-topic rt/chatter + --timeout 5 + --config $PATH_TO_CONFIG_FILE +``` + +### SUBSCRIBER +```bash +./install/ddsenabler/examples/publish/ddsenabler_example_publish \ + --persistence-path ./install/ddsenabler/examples/persistence/ \ + --expected-types 1 \ + --expected-topics 1 \ + --expected-data 5 + --timeout 5 + --config $PATH_TO_CONFIG_FILE +``` diff --git a/ddsenabler/examples/main.cpp b/ddsenabler/examples/publish/main.cpp similarity index 77% rename from ddsenabler/examples/main.cpp rename to ddsenabler/examples/publish/main.cpp index 9976c3f6..e144dd75 100644 --- a/ddsenabler/examples/main.cpp +++ b/ddsenabler/examples/publish/main.cpp @@ -28,11 +28,12 @@ #include #include +#include + #include "ddsenabler/dds_enabler_runner.hpp" #include "ddsenabler/DDSEnabler.hpp" #include "CLIParser.hpp" -#include "utils.hpp" CLIParser::example_config config; uint32_t received_types_ = 0; @@ -87,7 +88,8 @@ static void test_type_notification_callback( std::cout << "Type callback received: " << type_name << ", Total types: " << received_types_ << std::endl << serialized_type << std::endl << std::endl; if (!config.persistence_path.empty() && - !save_type_to_file((std::filesystem::path(config.persistence_path) / TYPES_SUBDIR).string(), type_name, + !utils::save_type_to_file((std::filesystem::path(config.persistence_path) / TYPES_SUBDIR).string(), + type_name, serialized_type_internal, serialized_type_internal_size)) { std::cerr << "Failed to save type: " << type_name << std::endl; @@ -112,7 +114,7 @@ static bool test_type_query_callback( } // Load the type from file - if (!load_type_from_file((std::filesystem::path(config.persistence_path) / TYPES_SUBDIR).string(), type_name, + if (!utils::load_type_from_file((std::filesystem::path(config.persistence_path) / TYPES_SUBDIR).string(), type_name, serialized_type_internal, serialized_type_internal_size)) { std::cerr << "Failed to load type: " << type_name << std::endl; @@ -124,19 +126,19 @@ static bool test_type_query_callback( // Static topic notification callback static void test_topic_notification_callback( const char* topic_name, - const char* type_name, - const char* serialized_qos) + const eprosima::ddsenabler::participants::TopicInfo& topic_info) { bool notify = false; { std::lock_guard lock(app_mutex_); notify = ++received_topics_ >= config.expected_topics; - std::cout << "Topic callback received: " << topic_name << " of type " << type_name << ", Total topics: " << - received_topics_ << std::endl << serialized_qos << std::endl << std::endl; + std::cout << "Topic callback received: " << topic_name << " of type " << topic_info.type_name << + ", Total topics: " << + received_topics_ << std::endl << topic_info.serialized_qos << std::endl << std::endl; if (!config.persistence_path.empty() && - !save_topic_to_file((std::filesystem::path(config.persistence_path) / TOPICS_SUBDIR).string(), + !utils::save_topic_to_file((std::filesystem::path(config.persistence_path) / TOPICS_SUBDIR).string(), topic_name, - type_name, serialized_qos)) + topic_info)) { std::cerr << "Failed to save topic: " << topic_name << std::endl; } @@ -150,8 +152,7 @@ static void test_topic_notification_callback( // Static type query callback static bool test_topic_query_callback( const char* topic_name, - std::string& type_name, - std::string& serialized_qos) + eprosima::ddsenabler::participants::TopicInfo& topic_info) { if (config.persistence_path.empty()) { @@ -160,9 +161,9 @@ static bool test_topic_query_callback( } // Load the topic from file - if (!load_topic_from_file((std::filesystem::path(config.persistence_path) / TOPICS_SUBDIR).string(), topic_name, - type_name, - serialized_qos)) + if (!utils::load_topic_from_file((std::filesystem::path(config.persistence_path) / TOPICS_SUBDIR).string(), + topic_name, + topic_info)) { std::cerr << "Failed to load topic: " << topic_name << std::endl; return false; @@ -183,7 +184,7 @@ static void test_data_notification_callback( std::cout << "Data callback received: " << topic_name << ", Total data: " << received_data_ << std::endl << json << std::endl << std::endl; if (!config.persistence_path.empty() && - !save_data_to_file((std::filesystem::path(config.persistence_path) / SAMPLES_SUBDIR).string(), + !utils::save_data_to_file((std::filesystem::path(config.persistence_path) / SAMPLES_SUBDIR).string(), topic_name, json, publish_time)) { @@ -215,57 +216,6 @@ bool expected_received( return validate_received(config); } -void init_persistence( - const std::string& persistence_path) -{ - auto ensure_directory_exists = [](const std::filesystem::path& path) - { - if (!std::filesystem::exists(path) && !std::filesystem::create_directories(path)) - { - std::cerr << "Failed to create directory: " << path << std::endl; - } - }; - - if (!persistence_path.empty()) - { - ensure_directory_exists(persistence_path); - std::vector subdirs = {SAMPLES_SUBDIR, TYPES_SUBDIR, TOPICS_SUBDIR}; - for (const auto& sub : subdirs) - { - ensure_directory_exists(std::filesystem::path(persistence_path) / sub); - } - } -} - -void get_sorted_files( - const std::string& directory, - std::vector>& files) -{ - for (const auto& entry : std::filesystem::directory_iterator(directory)) - { - if (entry.is_regular_file()) - { - std::string filename = entry.path().filename().string(); - try - { - // assumes name is just a number - files.emplace_back(entry.path(), static_cast(std::stoll(filename))); - } - catch (const std::invalid_argument& e) - { - std::cerr << "Skipping non-numeric file: " << filename << std::endl; - } - } - } - - // Sort files by numeric value - std::sort(files.begin(), files.end(), - [](const auto& a, const auto& b) - { - return a.second < b.second; - }); -} - void publish_routine( std::shared_ptr enabler, const std::string& publish_path, @@ -281,7 +231,7 @@ void publish_routine( // Get collection of files to publish, sorted in increasing order by their name (assumed to be numeric) std::vector> sample_files; - get_sorted_files(publish_path, sample_files); + utils::get_sorted_files(publish_path, sample_files); for (const auto& [path, number] : sample_files) { @@ -341,7 +291,8 @@ int main( config = CLIParser::parse_cli_options(argc, argv); - init_persistence(config.persistence_path); + std::vector subdirs = {SAMPLES_SUBDIR, TYPES_SUBDIR, TOPICS_SUBDIR}; + utils::init_persistence(config.persistence_path, subdirs); CallbackSet callbacks{ test_log_callback, @@ -355,9 +306,18 @@ int main( }; std::shared_ptr enabler; - if (!create_dds_enabler(config.config_file_path.c_str(), callbacks, enabler)) + bool enabler_created = false; + if (config.config_file_path.empty()) + { + enabler_created = create_dds_enabler(yaml::EnablerConfiguration(""), callbacks, enabler); + } + else + { + enabler_created = create_dds_enabler(config.config_file_path.c_str(), callbacks, enabler); + } + if (!enabler_created) { - std::cerr << "Failed to create DDS Enabler with the provided configuration." << std::endl; + std::cerr << "Failed to create DDSEnabler instance." << std::endl; return EXIT_FAILURE; } diff --git a/ddsenabler/examples/publish/samples/1 b/ddsenabler/examples/publish/samples/1 new file mode 100644 index 00000000..2f813e6c --- /dev/null +++ b/ddsenabler/examples/publish/samples/1 @@ -0,0 +1,3 @@ +{ + "data": "Hello World: 1" +} diff --git a/ddsenabler/examples/publish/samples/2 b/ddsenabler/examples/publish/samples/2 new file mode 100644 index 00000000..35641d7f --- /dev/null +++ b/ddsenabler/examples/publish/samples/2 @@ -0,0 +1,3 @@ +{ + "data": "Hello World: 2" +} diff --git a/ddsenabler/examples/publish/samples/3 b/ddsenabler/examples/publish/samples/3 new file mode 100644 index 00000000..cae03a51 --- /dev/null +++ b/ddsenabler/examples/publish/samples/3 @@ -0,0 +1,3 @@ +{ + "data": "Hello World: 3" +} diff --git a/ddsenabler/examples/publish/samples/4 b/ddsenabler/examples/publish/samples/4 new file mode 100644 index 00000000..112b310d --- /dev/null +++ b/ddsenabler/examples/publish/samples/4 @@ -0,0 +1,3 @@ +{ + "data": "Hello World: 4" +} diff --git a/ddsenabler/examples/publish/samples/5 b/ddsenabler/examples/publish/samples/5 new file mode 100644 index 00000000..5c53f169 --- /dev/null +++ b/ddsenabler/examples/publish/samples/5 @@ -0,0 +1,3 @@ +{ + "data": "Hello World: 5" +} diff --git a/ddsenabler/examples/service/CLIParser.hpp b/ddsenabler/examples/service/CLIParser.hpp new file mode 100644 index 00000000..f874daa5 --- /dev/null +++ b/ddsenabler/examples/service/CLIParser.hpp @@ -0,0 +1,326 @@ +// Copyright 2025 Proyectos y Sistemas de Mantenimiento SL (eProsima). +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include +#include +#include +#include + +#include + +#pragma once + +class CLIParser +{ +public: + + CLIParser() = delete; + + struct example_config + { + std::string config_file_path = ""; + std::string service_name = "add_two_ints"; + bool announce_server = true; + uint32_t timeout = 30; + std::string persistence_path = ""; + std::string requests_path = ""; + uint32_t expected_requests = 0; + uint32_t request_initial_wait = 0; + }; + + /** + * @brief Print usage help message and exit with the given return code + * + * @param return_code return code to exit with + * + * @warning This method finishes the execution of the program with the input return code + */ + static void print_help( + uint8_t return_code) + { + std::cout << "Usage: ddsenabler_example_service [options]" << + std::endl; + std::cout << "" << + std::endl; + std::cout << "--config Path to the configuration file" << + std::endl; + std::cout << " (optional, if not provided default config is used)" << + std::endl; + std::cout << "--service-name Name of the service to be registered" << + std::endl; + std::cout << " (Default: 'add_two_ints')" << + std::endl; + std::cout << "--timeout Time (seconds) to wait before stopping the" << + std::endl; + std::cout << " program if expectations are not met" << + std::endl; + std::cout << " (Default: 30)" << + std::endl; + std::cout << "--persistence-path Path to the persistence directory" << + std::endl; + std::cout << " (No default value as it is required)" << + std::endl; + std::cout << "\n-------------------------------------SERVER OPTIONS------------------------------------\n" << + std::endl; + std::cout << "--expected-requests Number of requests expected to be received" << + std::endl; + std::cout << " (Default: 0, meaning infinite)" << + std::endl; + std::cout << "\n-------------------------------------CLIENT OPTIONS------------------------------------\n" << + std::endl; + std::cout << "--request-initial-wait Time (seconds) to wait before starting" << + std::endl; + std::cout << " requests publication since server matching" << + std::endl; + std::cout << " (Default: 0)" << + std::endl; + std::cout << "--requests-path Directory containing request JSON files" << + std::endl; + std::cout << " (No default value as it is required for client mode)" << + std::endl; + std::exit(return_code); + } + + /** + * @brief Parse the command line options and return the configuration_config object + * + * @param argc number of arguments + * @param argv array of arguments + * @return configuration_config object with the parsed options + * + * @warning This method finishes the execution of the program if the input arguments are invalid + */ + static example_config parse_cli_options( + int argc, + char* argv[]) + { + example_config config; + + if (argc < 2) + { + std::cerr << "Mode is required as the first argument: 'client' or 'server'" << std::endl; + print_help(EXIT_FAILURE); + } + + bool client_flag = false; + bool server_flag = false; + + // First positional argument must be the mode + std::string mode = argv[1]; + if (mode == "-h" || mode == "--help") + { + print_help(EXIT_SUCCESS); + } + else if (mode == "client") + { + client_flag = true; + config.announce_server = false; + } + else if (mode == "server") + { + server_flag = true; + config.announce_server = true; + } + else + { + std::cerr << "Invalid mode '" << mode << "'. First argument must be 'client' or 'server'" << std::endl; + print_help(EXIT_FAILURE); + } + + // Parse optional arguments starting from index 2 + for (int i = 2; i < argc; ++i) + { + std::string arg = argv[i]; + + if (arg == "-h" || arg == "--help") + { + print_help(EXIT_SUCCESS); + } + else if (arg == "--config") + { + if (++i < argc) + { + config.config_file_path = argv[i]; + if (!std::filesystem::exists(config.config_file_path)) + { + std::cerr << "Invalid configuration file path: " << config.config_file_path << std::endl; + print_help(EXIT_FAILURE); + } + } + else + { + std::cerr << "Failed to parse --config argument" << std::endl; + print_help(EXIT_FAILURE); + } + } + else if (arg == "--service-name") + { + if (++i < argc) + { + config.service_name = argv[i]; + } + else + { + std::cerr << "Failed to parse --service-name argument" << std::endl; + print_help(EXIT_FAILURE); + } + } + else if (arg == "--timeout") + { + if (++i < argc) + { + try + { + config.timeout = static_cast(std::stoi(argv[i])); + } + catch (const std::exception& e) + { + std::cerr << "Invalid --timeout argument " << argv[i] << ": " << e.what() << std::endl; + print_help(EXIT_FAILURE); + } + } + else + { + std::cerr << "Failed to parse --timeout argument" << std::endl; + print_help(EXIT_FAILURE); + } + } + else if (arg == "--persistence-path") + { + if (++i < argc) + { + config.persistence_path = argv[i]; + } + else + { + std::cerr << "Failed to parse --persistence-path argument" << std::endl; + print_help(EXIT_FAILURE); + } + } + else if (arg == "--expected-requests") + { + if (++i < argc) + { + try + { + config.expected_requests = static_cast(std::stoi(argv[i])); + } + catch (const std::exception& e) + { + std::cerr << "Invalid --expected-requests argument " << argv[i] << ": " << e.what() << + std::endl; + print_help(EXIT_FAILURE); + } + } + else + { + std::cerr << "Failed to parse --expected-requests argument" << std::endl; + print_help(EXIT_FAILURE); + } + } + else if (arg == "--request-initial-wait") + { + if (++i < argc) + { + try + { + config.request_initial_wait = static_cast(std::stoi(argv[i])); + } + catch (const std::exception& e) + { + std::cerr << "Invalid --request-initial-wait argument " << argv[i] << ": " << e.what() << + std::endl; + print_help(EXIT_FAILURE); + } + } + else + { + std::cerr << "Failed to parse --request-initial-wait argument" << std::endl; + print_help(EXIT_FAILURE); + } + } + else if (arg == "--requests-path") + { + if (++i < argc) + { + config.requests_path = argv[i]; + if (!std::filesystem::exists(config.requests_path) || + !std::filesystem::is_directory(config.requests_path)) + { + std::cerr << "Invalid --requests-path directory: " << config.requests_path << std::endl; + print_help(EXIT_FAILURE); + } + } + else + { + std::cerr << "Failed to parse --requests-path argument" << std::endl; + print_help(EXIT_FAILURE); + } + } + else + { + std::cerr << "Failed to parse unknown argument: " << arg << std::endl; + print_help(EXIT_FAILURE); + } + } + + // Sanity check (should be set from the positional mode) + if (!client_flag && !server_flag) + { + std::cerr << "Either 'client' or 'server' must be specified as the first argument" << std::endl; + print_help(EXIT_FAILURE); + } + + if (client_flag && config.requests_path.empty()) + { + std::cerr << "--requests-path is required in client mode" << std::endl; + print_help(EXIT_FAILURE); + } + + if (config.persistence_path.empty()) + { + std::cerr << "Warning: persistence path is not set, persistence features will be disabled" << + std::endl; + } + + return config; + } + + /** + * @brief Parse the signal number into the signal name + * + * @param signum signal number + * @return std::string signal name + */ + static std::string parse_signal( + const int& signum) + { + switch (signum) + { + case SIGINT: + return "SIGINT"; + case SIGTERM: + return "SIGTERM"; +#ifndef _WIN32 + case SIGQUIT: + return "SIGQUIT"; + case SIGHUP: + return "SIGHUP"; +#endif // _WIN32 + default: + return "UNKNOWN SIGNAL"; + } + } + +}; diff --git a/ddsenabler/examples/service/CMakeLists.txt b/ddsenabler/examples/service/CMakeLists.txt new file mode 100644 index 00000000..f3c0a259 --- /dev/null +++ b/ddsenabler/examples/service/CMakeLists.txt @@ -0,0 +1,32 @@ +# Copyright 2025 Proyectos y Sistemas de Mantenimiento SL (eProsima). +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +cmake_minimum_required(VERSION 3.20) +project(ddsenabler_example_service LANGUAGES CXX) + +add_executable(ddsenabler_example_service main.cpp) + +target_include_directories(ddsenabler_example_service PRIVATE ${PROJECT_SOURCE_DIR}/../utils) +target_link_libraries(ddsenabler_example_service PRIVATE ddsenabler utils) + +# Install rule +install(TARGETS ddsenabler_example_service + RUNTIME DESTINATION examples/service +) + +# Install requests directory +install( + DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/requests + DESTINATION examples/service +) diff --git a/ddsenabler/examples/service/Readme.md b/ddsenabler/examples/service/Readme.md new file mode 100644 index 00000000..4805c1d9 --- /dev/null +++ b/ddsenabler/examples/service/Readme.md @@ -0,0 +1,30 @@ +# Service Example Readme + +This example demonstrates the usage of the DDS Enabler for implementing a service. The example includes a client and a server setup for the desired service. + +By default, the example is prepared for the `add_two_ints` service, providing a standard reply with a constant result of 3 for any request. + +Users are encouraged to implement their own functional server logic as needed in the `server_specific_logic` function. The persistence/requests directory must be modified to suit the specific service type and its expected requests. + +## Example Commands + +### CLIENT +```bash +./install/ddsenabler/examples/service/ddsenabler_example_service client \ + --persistence-path ./install/ddsenabler/examples/persistence/ \ + --requests-path ./install/ddsenabler/examples/service/requests \ + --request-initial-wait 3 + --config $PATH_TO_CONFIG_FILE + --service-name $SERVICE_NAME + --timeout N +``` + +### SERVER +```bash +./install/ddsenabler/examples/service/ddsenabler_example_service server \ + --persistence-path ./install/ddsenabler/examples/persistence/ + --expected-requests N + --config $PATH_TO_CONFIG_FILE + --service-name $SERVICE_NAME + --timeout N +``` diff --git a/ddsenabler/examples/service/main.cpp b/ddsenabler/examples/service/main.cpp new file mode 100644 index 00000000..03864ae3 --- /dev/null +++ b/ddsenabler/examples/service/main.cpp @@ -0,0 +1,547 @@ +// Copyright 2025 Proyectos y Sistemas de Mantenimiento SL (eProsima). +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/** + * @file main.cpp + * + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "ddsenabler/dds_enabler_runner.hpp" +#include "ddsenabler/DDSEnabler.hpp" + +#include "CLIParser.hpp" + +CLIParser::example_config config; +bool service_discovered_ = false; +uint32_t received_replies_ = 0; +uint32_t sent_replies_ = 0; +std::vector> received_requests_; +std::mutex app_mutex_; +std::condition_variable app_cv_; +bool stop_app_ = false; + +const std::string REQUESTS_SUBDIR = "requests"; +const std::string TYPES_SUBDIR = "types"; +const std::string SERVICES_SUBDIR = "services"; + +void signal_handler( + int signum) +{ + std::cout << "Signal " << CLIParser::parse_signal(signum) << " received, stopping..." << std::endl; + { + std::lock_guard lock(app_mutex_); + stop_app_ = true; + } + app_cv_.notify_all(); +} + +// Static log callback +void test_log_callback( + const char* file_name, + int line_no, + const char* func_name, + int category, + const char* msg) +{ + // NOTE: Default stdout logs can be disabled via configuration ("logging": {"stdout": false}) to avoid duplicated traces + + std::stringstream ss; + ss << file_name << ":" << line_no << " (" << func_name << "): " << msg << std::endl; + if (category == eprosima::utils::Log::Kind::Warning) + { + std::cerr << "[WARNING] " << ss.str(); + } + else if (category == eprosima::utils::Log::Kind::Error) + { + std::cerr << "[ERROR] " << ss.str(); + } +} + +// Static type notification callback +static void test_type_notification_callback( + const char* type_name, + const char* serialized_type, + const unsigned char* serialized_type_internal, + uint32_t serialized_type_internal_size, + const char* data_placeholder) +{ + std::lock_guard lock(app_mutex_); + std::cout << "Type callback received: " << type_name << std::endl; + if (!config.persistence_path.empty() && + !utils::save_type_to_file((std::filesystem::path(config.persistence_path) / TYPES_SUBDIR).string(), + type_name, + serialized_type_internal, serialized_type_internal_size)) + { + std::cerr << "Failed to save type: " << type_name << std::endl; + } +} + +// Static type query callback +static bool test_type_query_callback( + const char* type_name, + std::unique_ptr& serialized_type_internal, + uint32_t& serialized_type_internal_size) +{ + if (config.persistence_path.empty()) + { + std::cerr << "Persistence path is not set, cannot query type: " << type_name << std::endl; + return false; + } + + // Load the type from file + if (!utils::load_type_from_file((std::filesystem::path(config.persistence_path) / TYPES_SUBDIR).string(), type_name, + serialized_type_internal, serialized_type_internal_size)) + { + std::cerr << "Failed to load type: " << type_name << std::endl; + return false; + } + return true; +} + +// Static data notification callback +static void test_data_notification_callback( + const char* topic_name, + const char* json, + int64_t publish_time) +{ +} + +// Static topic notification callback +static void test_topic_notification_callback( + const char* topic_name, + const eprosima::ddsenabler::participants::TopicInfo& topic_info) +{ +} + +// Static type query callback +static bool test_topic_query_callback( + const char* topic_name, + eprosima::ddsenabler::participants::TopicInfo& topic_info) +{ + return false; +} + +// Static service notification callback +static void test_service_notification_callback( + const char* service_name, + const eprosima::ddsenabler::participants::ServiceInfo& service_info) +{ + std::lock_guard lock(app_mutex_); + if (config.service_name == std::string(service_name)) + { + std::cout << "Service callback received: " << service_name << std::endl; + + std::string service_file = (std::filesystem::path( + config.persistence_path) / SERVICES_SUBDIR + ).string(); + if (!config.persistence_path.empty()) + { + utils::save_service_to_file( + service_file, + service_name, + service_info); + } + service_discovered_ = true; + app_cv_.notify_all(); + } + else + { + std::cout << "Ignoring service callback for: " << service_name << std::endl; + } +} + +// Static service query callback +static bool test_service_query_callback( + const char* service_name, + eprosima::ddsenabler::participants::ServiceInfo& service_info) +{ + std::lock_guard lock(app_mutex_); + + if (config.persistence_path.empty()) + { + std::cerr << "Persistence path is not set, cannot query service: " << service_name << std::endl; + return false; + } + + if (config.service_name == std::string(service_name)) + { + std::cout << "Service type request callback received: " << service_name << std::endl; + + std::string service_file = (std::filesystem::path( + config.persistence_path) / + SERVICES_SUBDIR + ).string(); + if (utils::load_service_from_file( + service_file, + service_name, + service_info)) + { + app_cv_.notify_all(); + return true; + } + std::cout << "ERROR: fail to load service from file: " << service_file << std::endl; + return false; + } + else + { + std::cout << "Ignoring service type request callback for: " << service_name << std::endl; + return false; + } +} + +// Static service reply notification callback +static void test_service_reply_notification_callback( + const char* service_name, + const char* json, + uint64_t request_id, + int64_t publish_time) +{ + std::lock_guard lock(app_mutex_); + received_replies_++; + std::cout << "Reply callback received with id: " << request_id << " for service: " << service_name << std::endl; + std::cout << "Reply: " << json << std::endl; + + app_cv_.notify_all(); +} + +// Static service request notification callback +static void test_service_request_notification_callback( + const char* service_name, + const char* json, + uint64_t request_id, + int64_t publish_time) +{ + std::lock_guard lock(app_mutex_); + + std::cout << "Request callback received with id: " << request_id << " for service: " << service_name << std::endl; + received_requests_.emplace_back(request_id, json); + app_cv_.notify_all(); +} + +bool wait_for_service_discovery( + uint32_t timeout, + std::mutex& app_mutex, + std::condition_variable& app_cv) +{ + std::unique_lock lock(app_mutex); + if (!app_cv.wait_for(lock, std::chrono::seconds(timeout), + []() + { + return stop_app_ || service_discovered_; + }) || stop_app_) + { + if (!stop_app_) + { + std::cerr << "Timeout waiting for service discovery." << std::endl; + } + return false; + } + return true; +} + +bool wait_for_service_request( + uint32_t timeout, + std::mutex& app_mutex, + std::condition_variable& app_cv, + uint64_t& request_id, + std::string& request) +{ + std::unique_lock lock(app_mutex); + if (!app_cv.wait_for(lock, std::chrono::seconds(timeout), + []() + { + return stop_app_ || !received_requests_.empty(); + }) || stop_app_) + { + if (!stop_app_) + { + std::cerr << "Timeout waiting for service request." << std::endl; + } + return false; + } + + // Pop the last request from the received requests vector + request_id = received_requests_.back().first; + request = received_requests_.back().second; + received_requests_.pop_back(); + return true; +} + +bool wait_for_service_reply( + uint32_t timeout, + std::mutex& app_mutex, + std::condition_variable& app_cv, + uint32_t sent_requests) +{ + std::unique_lock lock(app_mutex); + if (!app_cv.wait_for(lock, std::chrono::seconds(timeout), + [&sent_requests]() + { + return stop_app_ || received_replies_ >= sent_requests; + }) || stop_app_) + { + if (!stop_app_) + { + std::cerr << "Timeout waiting for service reply." << std::endl; + } + return false; + } + return true; +} + +bool client_routine( + std::shared_ptr enabler, + const std::string& service_name, + const std::string& requests_path, + uint32_t timeout, + uint32_t request_initial_wait) +{ + // Wait for service to be discovered + if (!wait_for_service_discovery(timeout, app_mutex_, app_cv_)) + { + return false; + } + + // Wait a bit before starting to publish so types and topics can be discovered + std::this_thread::sleep_for(std::chrono::seconds(request_initial_wait)); + + // Get collection of files to publish, sorted in increasing order by their name (assumed to be numeric) + std::vector> sample_files; + utils::get_sorted_files(requests_path, sample_files); + uint32_t sent_requests = 0; + for (const auto& [path, number] : sample_files) + { + { + std::lock_guard lock(app_mutex_); + if (stop_app_) + { + return false; + } + } + + std::ifstream file(path, std::ios::binary); + if (file) + { + std::string file_content((std::istreambuf_iterator(file)), std::istreambuf_iterator()); + uint64_t request_id = 0; + if (enabler->send_service_request(service_name, file_content, request_id)) + { + std::cout << "Published content from file: " << path.filename() << " in service: " + << service_name << " with request ID: " << request_id << std::endl; + sent_requests++; + } + else + { + std::cerr << "Failed to publish content from file: " << path.filename() << " in service: " + << service_name << std::endl; + } + } + else + { + std::cerr << "Failed to open file: " << path << std::endl; + return false; + } + + // Wait publish period or until stop signal is received + if (!wait_for_service_reply(timeout, app_mutex_, app_cv_, sent_requests)) + { + return false; + } + } + return true; +} + +bool server_specific_logic( + std::shared_ptr enabler, + const std::string& service_name, + const std::string& request, + uint64_t request_id) +{ + std::this_thread::sleep_for(std::chrono::milliseconds(200)); // Simulate processing time + + // Add_two_ints service logic + int a = 1; // Default values in case of parsing failure + int b = 2; // Default values in case of parsing failure + try + { + auto request_json = nlohmann::json::parse(request); + a = request_json["rq/add_two_intsRequest"]["data"]["0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0"]["a"].get(); + b = request_json["rq/add_two_intsRequest"]["data"]["0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0"]["b"].get(); + } + catch (const std::exception& e) + { + std::cerr << "Failed to parse request JSON: " << e.what() << " Using default values a=1, b=2." << std::endl; + } + nlohmann::json response_json; + response_json["sum"] = a + b; + std::string json = response_json.dump(); + + if (!enabler->send_service_reply(service_name, json, request_id)) + { + std::cerr << "Failed to send service reply for request ID: " << request_id << std::endl; + return false; + } + return true; +} + +bool server_routine( + std::shared_ptr enabler, + const std::string& service_name, + uint32_t expected_requests, + uint32_t timeout) +{ + // Announce service + if (!enabler->announce_service(service_name)) + { + std::cerr << "Failed to announce service: " << service_name << std::endl; + return false; + } + + std::cout << "Service announced: " << service_name << std::endl; + + bool stop_app = false; + { + std::lock_guard lock(app_mutex_); + stop_app = stop_app_; + } + + while (!stop_app) + { + uint64_t request_id = 0; + std::string request; + if (!wait_for_service_request(timeout, app_mutex_, app_cv_, request_id, request)) + { + return false; + } + + std::cout << "Received request for service: " << service_name << " with request ID: " << request_id << + std::endl; + + // Example response + server_specific_logic( + enabler, + service_name, + request, + request_id); + + // Check if we have received the expected number of requests (or run indefinitely if expected_requests is 0) + { + std::lock_guard lock(app_mutex_); + if (expected_requests != 0 && ++sent_replies_ >= expected_requests) + { + // Wait to ensure the last sent reply reaches destination + std::this_thread::sleep_for(std::chrono::milliseconds(1000)); + return true; + } + stop_app = stop_app_; + } + } + + // Revoke service + if (!enabler->revoke_service(service_name)) + { + std::cerr << "Failed to revoke service: " << service_name << std::endl; + return false; + } + return true; +} + +int main( + int argc, + char** argv) +{ + signal(SIGINT, signal_handler); + signal(SIGTERM, signal_handler); +#ifndef _WIN32 + signal(SIGQUIT, signal_handler); + signal(SIGHUP, signal_handler); +#endif // _WIN32 + + using namespace eprosima::ddsenabler; + + eprosima::utils::Log::ReportFilenames(true); + + // Parse CLI options + config = CLIParser::parse_cli_options(argc, argv); + + // Initialize persistence if required + std::vector subdirs = {TYPES_SUBDIR, SERVICES_SUBDIR}; + utils::init_persistence(config.persistence_path, subdirs); + + // Set up callbacks + CallbackSet callbacks{ + test_log_callback, + { + test_type_notification_callback, + test_topic_notification_callback, + test_data_notification_callback, + test_type_query_callback, + test_topic_query_callback + }, + { + test_service_notification_callback, + test_service_request_notification_callback, + test_service_reply_notification_callback, + test_service_query_callback + } + }; + + std::shared_ptr enabler; + bool enabler_created = false; + if (config.config_file_path.empty()) + { + enabler_created = create_dds_enabler(yaml::EnablerConfiguration(""), callbacks, enabler); + } + else + { + enabler_created = create_dds_enabler(config.config_file_path.c_str(), callbacks, enabler); + } + if (!enabler_created) + { + std::cerr << "Failed to create DDSEnabler instance." << std::endl; + return EXIT_FAILURE; + } + + bool ret = false; + // Service logic based on announce_server flag + if (config.announce_server) + { + ret = server_routine(enabler, config.service_name, config.expected_requests, config.timeout); + } + else + { + if (config.requests_path.empty()) + { + std::cerr << "Request path is not set." << std::endl; + return EXIT_FAILURE; + } + ret = client_routine(enabler, config.service_name, config.requests_path, config.timeout, + config.request_initial_wait); + } + + return ret ? EXIT_SUCCESS : EXIT_FAILURE; +} diff --git a/ddsenabler/examples/service/requests/1 b/ddsenabler/examples/service/requests/1 new file mode 100644 index 00000000..fa79c13a --- /dev/null +++ b/ddsenabler/examples/service/requests/1 @@ -0,0 +1,4 @@ +{ + "a": 1, + "b": 2 +} diff --git a/ddsenabler/examples/service/requests/2 b/ddsenabler/examples/service/requests/2 new file mode 100644 index 00000000..4f64570c --- /dev/null +++ b/ddsenabler/examples/service/requests/2 @@ -0,0 +1,4 @@ +{ + "a": 2, + "b": 3 +} diff --git a/ddsenabler/examples/service/requests/3 b/ddsenabler/examples/service/requests/3 new file mode 100644 index 00000000..c5250e29 --- /dev/null +++ b/ddsenabler/examples/service/requests/3 @@ -0,0 +1,4 @@ +{ + "a": 3, + "b": 4 +} diff --git a/ddsenabler/examples/utils/CMakeLists.txt b/ddsenabler/examples/utils/CMakeLists.txt new file mode 100644 index 00000000..937e9877 --- /dev/null +++ b/ddsenabler/examples/utils/CMakeLists.txt @@ -0,0 +1,28 @@ +# Copyright 2025 Proyectos y Sistemas de Mantenimiento SL (eProsima). +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +cmake_minimum_required(VERSION 3.20) +project(ddsenabler_example_utils LANGUAGES CXX) + +find_package(ddsenabler_participants) + +add_library(utils STATIC + Utils.cpp +) + +target_include_directories(utils PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR} +) + +target_link_libraries(utils PUBLIC ddsenabler_participants) diff --git a/ddsenabler/examples/utils.hpp b/ddsenabler/examples/utils/Utils.cpp similarity index 50% rename from ddsenabler/examples/utils.hpp rename to ddsenabler/examples/utils/Utils.cpp index e49c73ed..acf3daf7 100644 --- a/ddsenabler/examples/utils.hpp +++ b/ddsenabler/examples/utils/Utils.cpp @@ -12,15 +12,18 @@ // See the License for the specific language governing permissions and // limitations under the License. -#include -#include -#include +/** + * @file Utils.cpp + */ -#include +#include "Utils.hpp" -#pragma once +#include +#include +#include +#include -// TODO: move these methods somewhere they can be reused +namespace utils { std::string safe_file_name( const std::string& name) @@ -147,8 +150,7 @@ bool load_type_from_file( bool save_topic_to_file( const std::string& directory, const char* topic_name, - const char* type_name, - const char* serialized_qos) + const TopicInfo& topic_info) { // Check if directory exists if (!std::filesystem::exists(directory)) @@ -179,14 +181,14 @@ bool save_topic_to_file( } // Write type name - uint32_t len_type_name = static_cast(std::strlen(type_name)); + uint32_t len_type_name = static_cast(topic_info.type_name.size()); ofs.write(reinterpret_cast(&len_type_name), sizeof(len_type_name)); - ofs.write(type_name, len_type_name); + ofs.write(topic_info.type_name.data(), len_type_name); // Write serialized QoS - uint32_t len_serialized_qos = static_cast(std::strlen(serialized_qos)); + uint32_t len_serialized_qos = static_cast(topic_info.serialized_qos.size()); ofs.write(reinterpret_cast(&len_serialized_qos), sizeof(len_serialized_qos)); - ofs.write(serialized_qos, len_serialized_qos); + ofs.write(topic_info.serialized_qos.data(), len_serialized_qos); if (!ofs.good()) { @@ -201,8 +203,7 @@ bool save_topic_to_file( bool load_topic_from_file( const std::string& directory, const char* topic_name, - std::string& type_name, - std::string& serialized_qos) + TopicInfo& topic_info) { // Check if directory exists if (!std::filesystem::exists(directory)) @@ -268,8 +269,8 @@ bool load_topic_from_file( return false; } - type_name.assign(type_name_buffer.begin(), type_name_buffer.end()); - serialized_qos.assign(serialized_qos_buffer.begin(), serialized_qos_buffer.end()); + topic_info.type_name.assign(type_name_buffer.begin(), type_name_buffer.end()); + topic_info.serialized_qos.assign(serialized_qos_buffer.begin(), serialized_qos_buffer.end()); return true; } @@ -362,3 +363,256 @@ bool save_data_to_file( ofs.close(); return true; } + +void save_service_to_file( + const std::string& directory, + const char* service_name, + const ServiceInfo& service_info) +{ + // Check if directory exists + if (!std::filesystem::exists(directory)) + { + std::cerr << "Directory does not exist: " << directory << std::endl; + return; + } + + // Remove problematic characters + std::string safe_service_name = safe_file_name(service_name); + + // Construct full file path + auto file_path = std::filesystem::path(directory) / safe_service_name; + + // Check if already exists, do nothing if it does + if (std::filesystem::exists(file_path)) + { + std::cout << "File already exists: " << file_path << std::endl; + return; + } + + nlohmann::json j; + j["service_name"] = service_name; + j["request_type_name"] = service_info.request.type_name; + j["reply_type_name"] = service_info.reply.type_name; + j["request_serialized_qos"] = service_info.request.serialized_qos; + j["reply_serialized_qos"] = service_info.reply.serialized_qos; + + std::ofstream ofs(file_path); + if (ofs.is_open()) + { + ofs << j.dump(4); + } +} + +bool load_service_from_file( + const std::string& directory, + const char* service_name, + ServiceInfo& service_info) +{ + // Check if directory exists + if (!std::filesystem::exists(directory)) + { + std::cerr << "Directory does not exist: " << directory << std::endl; + return false; + } + + // Remove problematic characters + std::string safe_service_name = safe_file_name(service_name); + + // Construct full file path + auto file_path = std::filesystem::path(directory) / safe_service_name; + + // Check if already exists, do nothing if it does not + if (!std::filesystem::exists(file_path)) + { + std::cout << "File does not exist: " << file_path << std::endl; + return false; + } + + std::ifstream ifs(file_path); + if (!ifs.is_open()) + { + return false; + } + + nlohmann::json j; + ifs >> j; + + std::string file_service_name = j["service_name"].get(); + if (file_service_name != std::string(service_name)) + { + return false; // Service name does not match + } + + service_info.request.type_name = j["request_type_name"].get(); + service_info.reply.type_name = j["reply_type_name"].get(); + service_info.request.serialized_qos = j["request_serialized_qos"].get(); + service_info.reply.serialized_qos = j["reply_serialized_qos"].get(); + ifs.close(); + + return true; +} + +void save_action_to_file( + const std::string& directory, + const char* action_name, + const ActionInfo& action_info) +{ + // Check if directory exists + if (!std::filesystem::exists(directory)) + { + std::cerr << "Directory does not exist: " << directory << std::endl; + return; + } + + // Remove problematic characters + std::string safe_action_name = safe_file_name(action_name); + + // Construct full file path + auto file_path = std::filesystem::path(directory) / safe_action_name; + + // Check if already exists, do nothing if it does + if (std::filesystem::exists(file_path)) + { + std::cout << "File already exists: " << file_path << std::endl; + return; + } + + nlohmann::json j; + j["action_name"] = action_name; + j["goal_request_action_type"] = action_info.goal.request.type_name; + j["goal_reply_action_type"] = action_info.goal.reply.type_name; + j["cancel_request_action_type"] = action_info.cancel.request.type_name; + j["cancel_reply_action_type"] = action_info.cancel.reply.type_name; + j["result_request_action_type"] = action_info.result.request.type_name; + j["result_reply_action_type"] = action_info.result.reply.type_name; + j["feedback_action_type"] = action_info.feedback.type_name; + j["status_action_type"] = action_info.status.type_name; + j["goal_request_action_serialized_qos"] = action_info.goal.request.serialized_qos; + j["goal_reply_action_serialized_qos"] = action_info.goal.reply.serialized_qos; + j["cancel_request_action_serialized_qos"] = action_info.cancel.request.serialized_qos; + j["cancel_reply_action_serialized_qos"] = action_info.cancel.reply.serialized_qos; + j["result_request_action_serialized_qos"] = action_info.result.request.serialized_qos; + j["result_reply_action_serialized_qos"] = action_info.result.reply.serialized_qos; + j["feedback_action_serialized_qos"] = action_info.feedback.serialized_qos; + j["status_action_serialized_qos"] = action_info.status.serialized_qos; + + std::ofstream ofs(file_path); + if (ofs.is_open()) + { + ofs << j.dump(4); + ofs.close(); + } +} + +bool load_action_from_file( + const std::string& directory, + const char* action_name, + ActionInfo& action_info) +{ + // Check if directory exists + if (!std::filesystem::exists(directory)) + { + std::cerr << "Directory does not exist: " << directory << std::endl; + return false; + } + + // Remove problematic characters + std::string safe_action_name = safe_file_name(action_name); + + // Construct full file path + auto file_path = std::filesystem::path(directory) / safe_action_name; + + // Check if already exists, do nothing if it does not + if (!std::filesystem::exists(file_path)) + { + std::cout << "File does not exist: " << file_path << std::endl; + return false; + } + + std::ifstream ifs(file_path); + if (!ifs.is_open()) + { + return false; + } + + nlohmann::json j; + ifs >> j; + + std::string file_action_name = j["action_name"].get(); + if (file_action_name != std::string(action_name)) + { + return false; // Action name does not match + } + action_info.goal.request.type_name = j["goal_request_action_type"].get(); + action_info.goal.reply.type_name = j["goal_reply_action_type"].get(); + action_info.cancel.request.type_name = j["cancel_request_action_type"].get(); + action_info.cancel.reply.type_name = j["cancel_reply_action_type"].get(); + action_info.result.request.type_name = j["result_request_action_type"].get(); + action_info.result.reply.type_name = j["result_reply_action_type"].get(); + action_info.feedback.type_name = j["feedback_action_type"].get(); + action_info.status.type_name = j["status_action_type"].get(); + action_info.goal.request.serialized_qos = j["goal_request_action_serialized_qos"].get(); + action_info.goal.reply.serialized_qos = j["goal_reply_action_serialized_qos"].get(); + action_info.cancel.request.serialized_qos = j["cancel_request_action_serialized_qos"].get(); + action_info.cancel.reply.serialized_qos = j["cancel_reply_action_serialized_qos"].get(); + action_info.result.request.serialized_qos = j["result_request_action_serialized_qos"].get(); + action_info.result.reply.serialized_qos = j["result_reply_action_serialized_qos"].get(); + action_info.feedback.serialized_qos = j["feedback_action_serialized_qos"].get(); + action_info.status.serialized_qos = j["status_action_serialized_qos"].get(); + ifs.close(); + + return true; +} + +void init_persistence( + const std::string& persistence_path, + std::vector subdirs) +{ + auto ensure_directory_exists = [](const std::filesystem::path& path) + { + if (!std::filesystem::exists(path) && !std::filesystem::create_directories(path)) + { + std::cerr << "Failed to create directory: " << path << std::endl; + } + }; + + if (!persistence_path.empty()) + { + ensure_directory_exists(persistence_path); + for (const auto& sub : subdirs) + { + ensure_directory_exists(std::filesystem::path(persistence_path) / sub); + } + } +} + +void get_sorted_files( + const std::string& directory, + std::vector>& files) +{ + for (const auto& entry : std::filesystem::directory_iterator(directory)) + { + if (entry.is_regular_file()) + { + std::string filename = entry.path().filename().string(); + try + { + // assumes name is just a number + files.emplace_back(entry.path(), static_cast(std::stoll(filename))); + } + catch (const std::invalid_argument& e) + { + std::cerr << "Skipping non-numeric file: " << filename << std::endl; + } + } + } + + // Sort files by numeric value + std::sort(files.begin(), files.end(), + [](const auto& a, const auto& b) + { + return a.second < b.second; + }); +} + +} // namespace utils diff --git a/ddsenabler/examples/utils/Utils.hpp b/ddsenabler/examples/utils/Utils.hpp new file mode 100644 index 00000000..f8a0bed4 --- /dev/null +++ b/ddsenabler/examples/utils/Utils.hpp @@ -0,0 +1,89 @@ +// Copyright 2025 Proyectos y Sistemas de Mantenimiento SL (eProsima). +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include + +#include +#include +#include +#include + +#pragma once + +namespace utils { + +using eprosima::ddsenabler::participants::TopicInfo; +using eprosima::ddsenabler::participants::ServiceInfo; +using eprosima::ddsenabler::participants::ActionInfo; + +std::string safe_file_name( + const std::string& name); + +bool save_type_to_file( + const std::string& directory, + const char* type_name, + const unsigned char*& serialized_type_internal, + uint32_t serialized_type_internal_size); + +bool load_type_from_file( + const std::string& directory, + const char* type_name, + std::unique_ptr& serialized_type_internal, + uint32_t& serialized_type_internal_size); + +bool save_topic_to_file( + const std::string& directory, + const char* topic_name, + const TopicInfo& topic_info); + +bool load_topic_from_file( + const std::string& directory, + const char* topic_name, + TopicInfo& topic_info); + +bool save_data_to_file( + const std::string& directory, + const std::string& topic_name, + const std::string& json, + int64_t publish_time); + +void save_service_to_file( + const std::string& directory, + const char* service_name, + const ServiceInfo& service_info); + +bool load_service_from_file( + const std::string& directory, + const char* service_name, + ServiceInfo& service_info); + +void save_action_to_file( + const std::string& directory, + const char* action_name, + const ActionInfo& action_info); + +bool load_action_from_file( + const std::string& directory, + const char* action_name, + ActionInfo& action_info); + +void init_persistence( + const std::string& persistence_path, + std::vector subdirs); + +void get_sorted_files( + const std::string& directory, + std::vector>& files); + +} // namespace utils diff --git a/ddsenabler/include/ddsenabler/CallbackSet.hpp b/ddsenabler/include/ddsenabler/CallbackSet.hpp index ef1e3e12..1ede155d 100644 --- a/ddsenabler/include/ddsenabler/CallbackSet.hpp +++ b/ddsenabler/include/ddsenabler/CallbackSet.hpp @@ -44,6 +44,45 @@ struct DdsCallbacks participants::DdsTopicQuery topic_query{nullptr}; }; +struct ServiceCallbacks +{ + //! Callback for notifying the discovery of DDS services + participants::ServiceNotification service_notification{nullptr}; + + //! Callback for notifying the reception of service requests + participants::ServiceRequestNotification service_request_notification{nullptr}; + + //! Callback for notifying the reception of service replies + participants::ServiceReplyNotification service_reply_notification{nullptr}; + + //! Callback for requesting information of a DDS service + participants::ServiceQuery service_query{nullptr}; +}; + +struct ActionCallbacks +{ + //! Callback for notifying the discovery of DDS actions + participants::ActionNotification action_notification{nullptr}; + + //! Callback for notifying the reception of action goal requests + participants::ActionGoalRequestNotification action_goal_request_notification{nullptr}; + + //! Callback for notifying the reception of action feedback + participants::ActionFeedbackNotification action_feedback_notification{nullptr}; + + //! Callback for notifying the reception of action cancel requests + participants::ActionCancelRequestNotification action_cancel_request_notification{nullptr}; + + //! Callback for notifying the reception of action results + participants::ActionResultNotification action_result_notification{nullptr}; + + //! Callback for notifying the reception of action status notifications + participants::ActionStatusNotification action_status_notification{nullptr}; + + //! Callback for requesting information of a DDS action + participants::ActionQuery action_query{nullptr}; +}; + /** * @brief Struct that encapsulates all the callbacks used by the DDS Enabler. */ @@ -54,6 +93,10 @@ struct CallbackSet //! DDS related callbacks DdsCallbacks dds; + //! Service related callbacks + ServiceCallbacks service; + //! Action related callbacks + ActionCallbacks action; }; } /* namespace ddsenabler */ diff --git a/ddsenabler/include/ddsenabler/DDSEnabler.hpp b/ddsenabler/include/ddsenabler/DDSEnabler.hpp index d53b12dd..9bf0d875 100644 --- a/ddsenabler/include/ddsenabler/DDSEnabler.hpp +++ b/ddsenabler/include/ddsenabler/DDSEnabler.hpp @@ -38,6 +38,7 @@ #include #include #include +#include #include @@ -96,6 +97,7 @@ class DDSEnabler * * @param topic_name: The name of the topic to publish to. * @param json: The JSON message to publish. + * * @return \c true if the message was published successfully, \c false otherwise. */ DDSENABLER_DllAPI @@ -103,8 +105,323 @@ class DDSEnabler const std::string& topic_name, const std::string& json); + /*****************************************/ + /* SERVICE */ + /*****************************************/ + + /** + * @brief Creates a server for the given service. + * + * This function announces a service by setting up a server for it. + * It returns a boolean indicating whether the operation was successful. + * Failure may occur if there is an issue requesting the data types to user's app + * for the corresponding request and reply topics. + * + * @param service_name The name of the service to be announced. + * @param Protocol The RPC protocol to be used (overloaded function, without this parameter uses ROS2 by default). + * + * @return true if the service was successfully announced, false otherwise. + */ + DDSENABLER_DllAPI + bool announce_service( + const std::string& service_name, + participants::Protocol Protocol); + + DDSENABLER_DllAPI + bool announce_service( + const std::string& service_name); + + /** + * @brief Stops the server for the given service. + * + * This function revokes the server associated with the specified service. + * It returns a boolean indicating whether the operation was successful. + * The operation will fail if the service was not previously announced. + * + * @param service_name The name of the service to be stopped. + * @return true if the service was successfully stopped, false otherwise. + */ + DDSENABLER_DllAPI + bool revoke_service( + const std::string& service_name); + + /** + * @brief Sends a request to a specified service. + * + * Sends a request containing the given JSON data to the specified service. + * If successful, the request identifier is stored in the provided reference + * and will match the one received in the reply callback. + * + * The function fails if there is no server for the service or if the request is malformed. + * The request identifier is incremented sequentially across all services. + * + * @param service_name The target service name. + * @param json The JSON-formatted request data. + * @param request_id Reference to store the unique request identifier. + * @param Protocol The RPC protocol to be used (overloaded function, without this parameter uses ROS2 by default). + * + * @return true if the request was successfully sent, false otherwise. + */ + DDSENABLER_DllAPI + bool send_service_request( + const std::string& service_name, + const std::string& json, + uint64_t& request_id, + participants::Protocol Protocol); + + DDSENABLER_DllAPI + bool send_service_request( + const std::string& service_name, + const std::string& json, + uint64_t& request_id); + + /** + * @brief Sends a reply to the given service. + * + * This function sends a reply to the specified service with the provided JSON data. + * It returns a boolean indicating whether the operation was successful. + * Failure may occur if the service was not previously created or if the request ID does not match any request. + * + * @param service_name The name of the service to send the reply to. + * @param json The JSON data to be sent with the reply. + * @param request_id The unique identifier of the request to which this reply corresponds. + * + * @return true if the reply was successfully sent, false otherwise. + * + * @note The request_id must coincide with the one received in the request. + */ + DDSENABLER_DllAPI + bool send_service_reply( + const std::string& service_name, + const std::string& json, + const uint64_t request_id); + + /*****************************************/ + /* ACTION SERVER */ + /*****************************************/ + + /** + * @brief Creates server for the given action. + * + * This function announces an action by setting up a server for it. + * It returns a boolean indicating whether the operation was successful. + * Failure may occur if there is an issue requesting the data types to user's app. + * + * @param action_name The name of the action to be announced. + * @param Protocol The RPC protocol to be used (overloaded function, without this parameter uses ROS2 by default). + * + * @return true if the action was successfully announced, false otherwise. + */ + DDSENABLER_DllAPI + bool announce_action( + const std::string& action_name, + participants::Protocol Protocol); + + DDSENABLER_DllAPI + bool announce_action( + const std::string& action_name); + + /** + * @brief Stops the server for the given action. + * + * This function revokes the server associated with the specified action. + * It returns a boolean indicating whether the operation was successful. + * Failure may occur if the action was not previously announced. + * + * @param action_name The name of the action to be stopped. + * + * @return true if the action was successfully stopped, false otherwise. + */ + DDSENABLER_DllAPI + bool revoke_action( + const std::string& action_name); + + /** + * @brief Send feedback for the specified goal_id action. + * + * This function sends feedback for the specified action goal identified by the given goal ID. + * It returns a boolean indicating whether the operation was successful. + * + * @param action_name The name of the action for which the feedback is being sent. + * @param json The JSON data to be sent as feedback. + * @param goal_id The unique identifier of the action goal for which the feedback is being sent. + * + * @return true if the feedback was successfully sent, false otherwise. + */ + DDSENABLER_DllAPI + bool send_action_feedback( + const char* action_name, + const char* json, + const participants::UUID& goal_id); + + /** + * @brief Send result for the specified goal_id action. + * + * This function sends the result for the specified action goal identified by the given goal ID. + * It returns a boolean indicating whether the operation was successful. + * + * @param action_name The name of the action for which the result is being sent. + * @param goal_id The unique identifier of the action goal for which the result is being sent. + * @param status_code The status code representing the result of the action. + * @param json The JSON data to be sent as the result. + * + * @return true if the result was successfully sent, false otherwise. + */ + DDSENABLER_DllAPI + bool send_action_result( + const char* action_name, + const participants::UUID& goal_id, + const participants::StatusCode& status_code, + const char* json); + + /** + * @brief Sends a cancel goal reply for the specified action. + * + * This function sends a reply for a cancel goal request for the specified action. + * It returns a boolean indicating whether the operation was successful. + * + * @param action_name The name of the action for which the cancel goal reply is being sent. + * @param goal_ids The unique identifiers of the action goals for which the cancel goal reply is being sent. + * @param cancel_code The cancel code representing the result of the cancel goal request. + * @param request_id The unique identifier of the cancel goal request. + * + * @return true if the cancel goal reply was successfully sent, false otherwise. + */ + DDSENABLER_DllAPI + bool send_action_cancel_goal_reply( + const char* action_name, + const std::vector& goal_ids, + const participants::CancelCode& cancel_code, + const uint64_t request_id); + + /** + * @brief Publishes an update for the status of an action. + * + * This function publishes a msg update for the status of an action. + * It returns a boolean indicating whether the operation was successful. + * Failure may occur if the action was not previously announced or if the goal ID is not currently active. + * + * @param action_name The name of the action for which the status is being updated. + * @param goal_id The unique identifier of the action goal for which the status is being updated. + * @param status_code The status code representing the current state of the action. + * + * @return true if the status update was successfully sent, false otherwise. + * + */ + DDSENABLER_DllAPI + bool update_action_status( + const std::string& action_name, + const participants::UUID& goal_id, + const participants::StatusCode& status_code); + + /*****************************************/ + /* ACTION CLIENT */ + /*****************************************/ + + /** + * @brief Sends an action goal to the specified action. + * + * This function sends an action goal to the specified action with the provided JSON data. + * It returns a boolean indicating whether the operation was successful. + * Failure may occur if the action server was not previously created, if the action types are unknown + * or if the action goal is malformed. + * + * The goal_id is a unique identifier for the action goal and is generated by the function. + * + * @param action_name The name of the action to send the goal to. + * @param json The JSON data to be sent with the action goal. + * @param goal_id Reference to store the unique identifier of the action goal. + * @param Protocol The RPC protocol to be used (overloaded function, without this parameter uses ROS2 by default). + * + * @return true if the action goal was successfully sent, false otherwise. + */ + DDSENABLER_DllAPI + bool send_action_goal( + const std::string& action_name, + const std::string& json, + participants::UUID& goal_id, + participants::Protocol Protocol); + + DDSENABLER_DllAPI + bool send_action_goal( + const std::string& action_name, + const std::string& json, + participants::UUID& goal_id); + + /** + * @brief Cancels an action goal for the specified action. + * + * This function sends a request to an action server to cancel: + * If the goal ID is empty and timestamp is zero, cancel all goals + * If the goal ID is empty and timestamp is not zero, cancel all goals accepted at or before the timestamp + * If the goal ID is not empty and timestamp is zero, cancel the goal with the given ID regardless of the time it was accepted + * If the goal ID is not empty and timestamp is not zero, cancel the goal with the given ID and all goals accepted at or before the timestamp + * The success of the operation only indicates that the cancel request was sent, not that it was processed. + * The actual cancellation of the goals would be notified via a status update of each individual action. + * + * @param action_name The name of the action for which the goal is to be canceled. + * @param goal_id The unique identifier of the action goal to be canceled or empty to cancel all goals. + * @param timestamp The acceptance timestamp of the goal to be canceled. + * + * @return true if the cancel action request was successfully sent, false otherwise. + */ + DDSENABLER_DllAPI + bool cancel_action_goal( + const std::string& action_name, + const participants::UUID& goal_id, + const int64_t timestamp = 0); + protected: + /** + * @brief Sends a request to get the result of an action. + * + * This function sends a request to get the result of an action identified by the given action name and goal ID. + * It returns a boolean indicating whether the operation was successful. + * + * It is invoked via lambda from the writer_ when the send_goal_reply is received and positive. + * + * @param action_name The name of the action for which the result is being requested. + * @param goal_id The unique identifier of the action goal for which the result is being requested. + * + * @return true if the request to get the result was successfully sent, false otherwise. + */ + bool send_action_get_result_request( + const std::string& action_name, + const participants::UUID& goal_id); + + /** + * @brief Sends a reply to an action goal request. + * + * This function sends a reply to an action goal request, indicating whether the goal was accepted or not. + * + * It is invoked via lambda from the writer_ when request is notified. + * + * @param action_name The name of the action for which the goal reply is being sent. + * @param goal_id The unique identifier of the action goal for which the reply is being sent. + * @param accepted A boolean indicating whether the goal was accepted (true) or rejected (false). + */ + void send_action_send_goal_reply( + const std::string& action_name, + const uint64_t goal_id, + bool accepted); + + /** + * @brief Actually sends the result reply for an action. + * + * This function is only called when all the conditions are met to send the result reply. + * + * @param action_name The name of the action for which the result reply is being sent. + * @param goal_id The unique identifier of the action goal for which the result reply is being sent. + * @param reply_json The JSON data to be sent as the result reply. + * @param request_id The unique identifier of the request to which this reply corresponds. + */ + bool send_action_get_result_reply( + const std::string& action_name, + const participants::UUID& goal_id, + const std::string& reply_json, + const uint64_t request_id); + /** * Load the Enabler's internal topics into a configuration object. * @@ -131,15 +448,15 @@ class DDSEnabler //! Payload Pool std::shared_ptr payload_pool_; - //! Thread Pool - std::shared_ptr thread_pool_; - //! Discovery Database std::shared_ptr discovery_database_; //! Participants Database std::shared_ptr participants_database_; + //! Thread Pool + std::shared_ptr thread_pool_; + //! Handler std::shared_ptr handler_; diff --git a/ddsenabler/src/cpp/DDSEnabler.cpp b/ddsenabler/src/cpp/DDSEnabler.cpp index b97edfa3..3a5577d1 100644 --- a/ddsenabler/src/cpp/DDSEnabler.cpp +++ b/ddsenabler/src/cpp/DDSEnabler.cpp @@ -12,12 +12,14 @@ // See the License for the specific language governing permissions and // limitations under the License. +#include + #include #include #include -#include "ddsenabler/DDSEnabler.hpp" +#include namespace eprosima { namespace ddsenabler { @@ -61,6 +63,30 @@ DDSEnabler::DDSEnabler( handler_config, payload_pool_); + handler_->set_send_action_get_result_request_callback( + [this](const std::string& action_name, const UUID& action_id) + { + if (this->send_action_get_result_request(action_name, action_id)) + { + return true; + } + this->cancel_action_goal(action_name, action_id, 0); + return false; + }); + + handler_->set_send_action_send_goal_reply_callback( + [this](const std::string& action_name, const uint64_t goal_id, bool accepted) + { + return this->send_action_send_goal_reply(action_name, goal_id, accepted); + }); + + handler_->set_send_action_get_result_reply_callback( + [this](const std::string& action_name, const UUID& goal_id, const std::string& reply_json, + const uint64_t request_id) + { + return this->send_action_get_result_reply(action_name, goal_id, reply_json, request_id); + }); + // Create Enabler Participant enabler_participant_ = std::make_shared( configuration_.enabler_configuration, @@ -209,6 +235,55 @@ void DDSEnabler::set_internal_callbacks_( { enabler_participant_->set_topic_query_callback(callbacks.dds.topic_query); } + if (callbacks.service.service_notification) + { + handler_->set_service_notification_callback(callbacks.service.service_notification); + } + if (callbacks.service.service_request_notification) + { + handler_->set_service_request_notification_callback(callbacks.service.service_request_notification); + } + if (callbacks.service.service_reply_notification) + { + handler_->set_service_reply_notification_callback(callbacks.service.service_reply_notification); + } + if (callbacks.service.service_query) + { + enabler_participant_->set_service_query_callback(callbacks.service.service_query); + } + if (callbacks.action.action_notification) + { + handler_->set_action_notification_callback(callbacks.action.action_notification); + } + if (callbacks.action.action_goal_request_notification) + { + handler_->set_action_goal_request_notification_callback( + callbacks.action.action_goal_request_notification); + } + if (callbacks.action.action_feedback_notification) + { + handler_->set_action_feedback_notification_callback( + callbacks.action.action_feedback_notification); + } + if (callbacks.action.action_cancel_request_notification) + { + handler_->set_action_cancel_request_notification_callback( + callbacks.action.action_cancel_request_notification); + } + if (callbacks.action.action_result_notification) + { + handler_->set_action_result_notification_callback( + callbacks.action.action_result_notification); + } + if (callbacks.action.action_status_notification) + { + handler_->set_action_status_notification_callback( + callbacks.action.action_status_notification); + } + if (callbacks.action.action_query) + { + enabler_participant_->set_action_query_callback(callbacks.action.action_query); + } } bool DDSEnabler::publish( @@ -218,5 +293,196 @@ bool DDSEnabler::publish( return enabler_participant_->publish(topic_name, json); } +bool DDSEnabler::send_service_request( + const std::string& service_name, + const std::string& json, + uint64_t& request_id) +{ + return send_service_request( + service_name, + json, + request_id, + participants::Protocol::ROS2); +} + +bool DDSEnabler::send_service_request( + const std::string& service_name, + const std::string& json, + uint64_t& request_id, + participants::Protocol Protocol) +{ + return enabler_participant_->send_service_request( + service_name, + json, + request_id, + Protocol); +} + +bool DDSEnabler::announce_service( + const std::string& service_name, + participants::Protocol Protocol) +{ + return enabler_participant_->announce_service(service_name, Protocol); +} + +bool DDSEnabler::announce_service( + const std::string& service_name) +{ + return announce_service(service_name, participants::Protocol::ROS2); +} + +bool DDSEnabler::revoke_service( + const std::string& service_name) +{ + return enabler_participant_->revoke_service(service_name); +} + +bool DDSEnabler::send_service_reply( + const std::string& service_name, + const std::string& json, + const uint64_t request_id) +{ + return enabler_participant_->send_service_reply( + service_name, + json, + request_id); +} + +bool DDSEnabler::send_action_goal( + const std::string& action_name, + const std::string& json, + UUID& action_id) +{ + return send_action_goal( + action_name, + json, + action_id, + participants::Protocol::ROS2); +} + +bool DDSEnabler::send_action_goal( + const std::string& action_name, + const std::string& json, + UUID& action_id, + participants::Protocol Protocol) +{ + return enabler_participant_->send_action_goal( + action_name, + json, + action_id, + Protocol); +} + +bool DDSEnabler::send_action_get_result_request( + const std::string& action_name, + const UUID& action_id) +{ + return enabler_participant_->send_action_get_result_request( + action_name, + action_id); +} + +bool DDSEnabler::cancel_action_goal( + const std::string& action_name, + const participants::UUID& goal_id, + const int64_t timestamp) +{ + return enabler_participant_->cancel_action_goal( + action_name, + goal_id, + timestamp); +} + +bool DDSEnabler::announce_action( + const std::string& action_name) +{ + return announce_action(action_name, participants::Protocol::ROS2); +} + +bool DDSEnabler::announce_action( + const std::string& action_name, + participants::Protocol Protocol) +{ + return enabler_participant_->announce_action(action_name, Protocol); +} + +bool DDSEnabler::revoke_action( + const std::string& action_name) +{ + return enabler_participant_->revoke_action(action_name); +} + +void DDSEnabler::send_action_send_goal_reply( + const std::string& action_name, + const uint64_t goal_id, + bool accepted) +{ + enabler_participant_->send_action_send_goal_reply( + action_name, + goal_id, + accepted); +} + +bool DDSEnabler::send_action_cancel_goal_reply( + const char* action_name, + const std::vector& goal_ids, + const participants::CancelCode& cancel_code, + const uint64_t request_id) +{ + return enabler_participant_->send_action_cancel_goal_reply( + action_name, + goal_ids, + cancel_code, + request_id); +} + +bool DDSEnabler::send_action_result( + const char* action_name, + const participants::UUID& goal_id, + const participants::StatusCode& status_code, + const char* json) +{ + return enabler_participant_->send_action_result( + action_name, + goal_id, + status_code, + json); +} + +bool DDSEnabler::send_action_get_result_reply( + const std::string& action_name, + const participants::UUID& goal_id, + const std::string& reply_json, + const uint64_t request_id) +{ + return enabler_participant_->send_action_get_result_reply( + action_name, + goal_id, + reply_json, + request_id); +} + +bool DDSEnabler::send_action_feedback( + const char* action_name, + const char* json, + const participants::UUID& goal_id) +{ + return enabler_participant_->send_action_feedback( + action_name, + json, + goal_id); +} + +bool DDSEnabler::update_action_status( + const std::string& action_name, + const participants::UUID& goal_id, + const participants::StatusCode& status_code) +{ + return enabler_participant_->update_action_status( + action_name, + goal_id, + status_code); +} + } /* namespace ddsenabler */ } /* namespace eprosima */ diff --git a/ddsenabler/test/DDSEnablerTester.hpp b/ddsenabler/test/DDSEnablerTester.hpp index a3c65e43..50cc9cdf 100644 --- a/ddsenabler/test/DDSEnablerTester.hpp +++ b/ddsenabler/test/DDSEnablerTester.hpp @@ -13,6 +13,9 @@ // limitations under the License. #include +#include +#include + #include #include @@ -26,6 +29,8 @@ #include "ddsenabler/dds_enabler_runner.hpp" +#include + using namespace eprosima::ddspipe; using namespace eprosima::ddsenabler; using namespace eprosima::ddsenabler::participants; @@ -47,6 +52,8 @@ static int write_delay_ms_ = 20; static int wait_for_ack_ns_ = 1000000000; static int wait_after_publication_ms_ = 200; +static const std::filesystem::path input_file_path = std::filesystem::current_path() / "test_files"; + class DDSEnablerTester : public ::testing::Test { public: @@ -90,6 +97,21 @@ class DDSEnablerTester : public ::testing::Test test_data_notification_callback, test_type_query_callback, test_topic_query_callback + }, + { + test_service_notification_callback, + test_service_request_notification_callback, + test_service_reply_notification_callback, + test_service_query_callback + }, + { + test_action_notification_callback, + test_action_goal_request_notification_callback, + test_action_feedback_notification_callback, + test_action_cancel_request_notification_callback, + test_action_result_notification_callback, + test_action_status_notification_callback, + test_action_query_callback } }; @@ -130,6 +152,21 @@ class DDSEnablerTester : public ::testing::Test test_data_notification_callback, test_type_query_callback, test_topic_query_callback + }, + { + test_service_notification_callback, + test_service_request_notification_callback, + test_service_reply_notification_callback, + test_service_query_callback + }, + { + test_action_notification_callback, + test_action_goal_request_notification_callback, + test_action_feedback_notification_callback, + test_action_cancel_request_notification_callback, + test_action_result_notification_callback, + test_action_status_notification_callback, + test_action_query_callback } }; @@ -299,8 +336,7 @@ class DDSEnablerTester : public ::testing::Test // eprosima::ddsenabler::participants::DdsTopicNotification topic_notification static void test_topic_notification_callback( const char* topic_name, - const char* type_name, - const char* serialized_qos) + const eprosima::ddsenabler::participants::TopicInfo& topic_info) { if (current_test_instance_) { @@ -315,8 +351,7 @@ class DDSEnablerTester : public ::testing::Test // eprosima::ddsenabler::participants::DdsTopicQuery topic_query; static bool test_topic_query_callback( const char* topic_name, - std::string& type_name, - std::string& serialized_qos) + eprosima::ddsenabler::participants::TopicInfo& topic_info) { return false; } @@ -327,6 +362,16 @@ class DDSEnablerTester : public ::testing::Test std::unique_ptr& serialized_type_internal, uint32_t& serialized_type_internal_size) { + std::string persistence_dir = input_file_path.string(); + if (utils::load_type_from_file( + persistence_dir, + type_name, + serialized_type_internal, + serialized_type_internal_size)) + { + return true; + } + std::cout << "ERROR: fail to load type from directory: " << persistence_dir << std::endl; return false; } @@ -382,6 +427,119 @@ class DDSEnablerTester : public ::testing::Test } } + // SERVICES + static void test_service_notification_callback( + const char* service_name, + const eprosima::ddsenabler::participants::ServiceInfo& service_info) + { + } + + static bool test_service_query_callback( + const char* service_name, + eprosima::ddsenabler::participants::ServiceInfo& service_info) + { + std::string persistence_dir = input_file_path.string(); + if (utils::load_service_from_file( + persistence_dir, + service_name, + service_info)) + { + return true; + } + std::cout << "ERROR: fail to load service from directory: " << persistence_dir << std::endl; + return false; + } + + static void test_service_reply_notification_callback( + const char* service_name, + const char* json, + uint64_t request_id, + int64_t publish_time) + { + } + + static void test_service_request_notification_callback( + const char* service_name, + const char* json, + uint64_t request_id, + int64_t publish_time) + { + } + + // ACTIONS + + // Static action notification callback + static void test_action_notification_callback( + const char* action_name, + const eprosima::ddsenabler::participants::ActionInfo& action_info) + { + } + + // Static action query callback + static bool test_action_query_callback( + const char* action_name, + eprosima::ddsenabler::participants::ActionInfo& action_info) + { + std::string persistence_dir = input_file_path.string(); + if (!utils::load_action_from_file( + persistence_dir, + action_name, + action_info)) + { + std::cerr << "Failed to load action: " << action_name << std::endl; + return false; + } + return true; + } + + // Static action goal request notification callback + static bool test_action_goal_request_notification_callback( + const char* action_name, + const char* json, + const eprosima::ddsenabler::participants::UUID& goal_id, + int64_t publish_time) + { + return false; + } + + // Static action result notification callback + static void test_action_result_notification_callback( + const char* action_name, + const char* json, + const eprosima::ddsenabler::participants::UUID& goal_id, + int64_t publish_time) + { + } + + // Static action feedback notification callback + static void test_action_feedback_notification_callback( + const char* action_name, + const char* json, + const eprosima::ddsenabler::participants::UUID& goal_id, + int64_t publish_time) + { + } + + // Static action status notification callback + static void test_action_status_notification_callback( + const char* action_name, + const eprosima::ddsenabler::participants::UUID& goal_id, + eprosima::ddsenabler::participants::StatusCode statusCode, + const char* statusMessage, + int64_t publish_time) + { + } + + // Static action cancel request notification callback + static void test_action_cancel_request_notification_callback( + const char* action_name, + const eprosima::ddsenabler::participants::UUID& goal_id, + int64_t timestamp, + uint64_t request_id, + int64_t publish_time) + { + } + // Pointer to the current test instance (for use in the static callback) static DDSEnablerTester* current_test_instance_; @@ -394,7 +552,13 @@ class DDSEnablerTester : public ::testing::Test std::mutex type_received_mutex_; std::mutex topic_received_mutex_; std::mutex data_received_mutex_; + std::mutex rpc_mutex_; }; + + + + + } // namespace ddsenablertester diff --git a/ddsenabler/test/ddsEnablerTests/CMakeLists.txt b/ddsenabler/test/ddsEnablerTests/CMakeLists.txt index 5a0d230c..31e09e60 100644 --- a/ddsenabler/test/ddsEnablerTests/CMakeLists.txt +++ b/ddsenabler/test/ddsEnablerTests/CMakeLists.txt @@ -40,6 +40,10 @@ set(TEST_LIST send_history_bigger_than_writer send_history_smaller_than_writer send_history_multiple_types + service_client + service_server + action_client + action_server ) set(TEST_NEEDED_SOURCES @@ -48,6 +52,7 @@ set(TEST_NEEDED_SOURCES set(TEST_EXTRA_HEADERS ${PROJECT_SOURCE_DIR}/test ${PROJECT_SOURCE_DIR}/test/ddsEnablerTests/types + ${PROJECT_SOURCE_DIR}/examples/utils ) add_blackbox_executable( @@ -59,6 +64,29 @@ add_blackbox_executable( "${TEST_LIBRARY_SOURCES}" ) +target_sources(blackbox_DDSEnablerTest PRIVATE ${PROJECT_SOURCE_DIR}/examples/utils/Utils.cpp) +target_include_directories(blackbox_DDSEnablerTest PRIVATE ${PROJECT_SOURCE_DIR}/examples/utils) + +# Copy test resources next to the test binary so runtime can use a relative path +add_custom_command(TARGET blackbox_DDSEnablerTest POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_directory + ${PROJECT_SOURCE_DIR}/test/ddsEnablerTests/test_files + $/test_files +) + +# Also copy test resources to the test target binary directory (configuration-agnostic) +# Rationale: On single-config generators (Linux), CTest runs from the target dir, which matches +# $. On multi-config generators (Windows/MSVC), CTest's working directory is usually +# the CMake current binary dir (e.g., .../ddsEnablerTests) while the executable lives under a +# configuration subfolder (e.g., .../ddsEnablerTests/Debug). The tests locate resources via +# std::filesystem::current_path()/"test_files"; thus, we ensure the resources are available from +# the CTest working dir as well. +add_custom_command(TARGET blackbox_DDSEnablerTest POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_directory + ${PROJECT_SOURCE_DIR}/test/ddsEnablerTests/test_files + ${CMAKE_CURRENT_BINARY_DIR}/test_files +) + ################################################################################## diff --git a/ddsenabler/test/ddsEnablerTests/DdsEnablerTest.cpp b/ddsenabler/test/ddsEnablerTests/DdsEnablerTest.cpp index 85305197..c44f02a3 100644 --- a/ddsenabler/test/ddsEnablerTests/DdsEnablerTest.cpp +++ b/ddsenabler/test/ddsEnablerTests/DdsEnablerTest.cpp @@ -385,10 +385,76 @@ TEST_F(DDSEnablerTest, send_history_multiple_types) ASSERT_EQ(get_received_data(), types * history_depth); } +// SERVICES + +TEST_F(DDSEnablerTest, service_client) +{ + auto enabler = create_ddsenabler(); + ASSERT_TRUE(enabler != nullptr); + + std::this_thread::sleep_for(std::chrono::seconds(2)); + + std::string json = "{\"a\": 1, \"b\": 2}"; + std::string service_name = "add_two_ints"; + uint64_t request_id = 0; + ASSERT_FALSE(enabler->send_service_request(service_name, json, request_id)); +} + + +TEST_F(DDSEnablerTest, service_server) +{ + auto enabler = create_ddsenabler(); + ASSERT_TRUE(enabler != nullptr); + + std::string service_name = "add_two_ints"; + + ASSERT_FALSE(enabler->revoke_service(service_name)); + ASSERT_TRUE(enabler->announce_service(service_name)); + ASSERT_FALSE(enabler->announce_service(service_name)); + + std::this_thread::sleep_for(std::chrono::seconds(2)); + + ASSERT_TRUE(enabler->revoke_service(service_name)); + ASSERT_FALSE(enabler->revoke_service(service_name)); +} + +// ACTIONS + +TEST_F(DDSEnablerTest, action_client) +{ + auto enabler = create_ddsenabler(); + ASSERT_TRUE(enabler != nullptr); + + std::this_thread::sleep_for(std::chrono::seconds(2)); + + std::string json = "{\"order\": 20}"; + std::string action_name = "fibonacci/_action/"; + UUID action_id; + + ASSERT_FALSE(enabler->send_action_goal(action_name, json, action_id)); +} + +TEST_F(DDSEnablerTest, action_server) +{ + auto enabler = create_ddsenabler(); + ASSERT_TRUE(enabler != nullptr); + + std::string action_name = "fibonacci/_action/"; + + ASSERT_FALSE(enabler->revoke_action(action_name)); + ASSERT_TRUE(enabler->announce_action(action_name)); + ASSERT_FALSE(enabler->announce_action(action_name)); + + std::this_thread::sleep_for(std::chrono::seconds(2)); + + ASSERT_TRUE(enabler->revoke_action(action_name)); + ASSERT_FALSE(enabler->revoke_action(action_name)); +} + int main( int argc, char** argv) { ::testing::InitGoogleTest(&argc, argv); return RUN_ALL_TESTS(); -} \ No newline at end of file +} diff --git a/ddsenabler/test/ddsEnablerTests/ReloadConfig.cpp b/ddsenabler/test/ddsEnablerTests/ReloadConfig.cpp index e41581d9..1b6afb2e 100644 --- a/ddsenabler/test/ddsEnablerTests/ReloadConfig.cpp +++ b/ddsenabler/test/ddsEnablerTests/ReloadConfig.cpp @@ -16,6 +16,7 @@ #include #include "dds_enabler_runner.hpp" +#include "ddsenabler_participants/Callbacks.hpp" #include #include @@ -47,16 +48,14 @@ void test_type_notification_callback( // eprosima::ddsenabler::participants::DdsTopicNotification topic_notification; void test_topic_notification_callback( const char* topic_name, - const char* type_name, - const char* serialized_qos) + const eprosima::ddsenabler::participants::TopicInfo& topic_info) { } // eprosima::ddsenabler::participants::DdsTopicQuery topic_query; bool test_topic_query_callback( const char* topic_name, - std::string& type_name, - std::string& serialized_qos) + eprosima::ddsenabler::participants::TopicInfo& topic_info) { return false; } diff --git a/ddsenabler/test/ddsEnablerTests/test_files/action_msgs__msg__dds___GoalStatusArray_.bin b/ddsenabler/test/ddsEnablerTests/test_files/action_msgs__msg__dds___GoalStatusArray_.bin new file mode 100644 index 00000000..9b2666b4 Binary files /dev/null and b/ddsenabler/test/ddsEnablerTests/test_files/action_msgs__msg__dds___GoalStatusArray_.bin differ diff --git a/ddsenabler/test/ddsEnablerTests/test_files/action_msgs__srv__dds___CancelGoal_Request_.bin b/ddsenabler/test/ddsEnablerTests/test_files/action_msgs__srv__dds___CancelGoal_Request_.bin new file mode 100644 index 00000000..3312fb7d Binary files /dev/null and b/ddsenabler/test/ddsEnablerTests/test_files/action_msgs__srv__dds___CancelGoal_Request_.bin differ diff --git a/ddsenabler/test/ddsEnablerTests/test_files/action_msgs__srv__dds___CancelGoal_Response_.bin b/ddsenabler/test/ddsEnablerTests/test_files/action_msgs__srv__dds___CancelGoal_Response_.bin new file mode 100644 index 00000000..9b9e0b42 Binary files /dev/null and b/ddsenabler/test/ddsEnablerTests/test_files/action_msgs__srv__dds___CancelGoal_Response_.bin differ diff --git a/ddsenabler/test/ddsEnablerTests/test_files/action_tutorials_interfaces__action__dds___Fibonacci_FeedbackMessage_.bin b/ddsenabler/test/ddsEnablerTests/test_files/action_tutorials_interfaces__action__dds___Fibonacci_FeedbackMessage_.bin new file mode 100644 index 00000000..fac6aee9 Binary files /dev/null and b/ddsenabler/test/ddsEnablerTests/test_files/action_tutorials_interfaces__action__dds___Fibonacci_FeedbackMessage_.bin differ diff --git a/ddsenabler/test/ddsEnablerTests/test_files/action_tutorials_interfaces__action__dds___Fibonacci_GetResult_Request_.bin b/ddsenabler/test/ddsEnablerTests/test_files/action_tutorials_interfaces__action__dds___Fibonacci_GetResult_Request_.bin new file mode 100644 index 00000000..423f3111 Binary files /dev/null and b/ddsenabler/test/ddsEnablerTests/test_files/action_tutorials_interfaces__action__dds___Fibonacci_GetResult_Request_.bin differ diff --git a/ddsenabler/test/ddsEnablerTests/test_files/action_tutorials_interfaces__action__dds___Fibonacci_GetResult_Response_.bin b/ddsenabler/test/ddsEnablerTests/test_files/action_tutorials_interfaces__action__dds___Fibonacci_GetResult_Response_.bin new file mode 100644 index 00000000..62adda76 Binary files /dev/null and b/ddsenabler/test/ddsEnablerTests/test_files/action_tutorials_interfaces__action__dds___Fibonacci_GetResult_Response_.bin differ diff --git a/ddsenabler/test/ddsEnablerTests/test_files/action_tutorials_interfaces__action__dds___Fibonacci_SendGoal_Request_.bin b/ddsenabler/test/ddsEnablerTests/test_files/action_tutorials_interfaces__action__dds___Fibonacci_SendGoal_Request_.bin new file mode 100644 index 00000000..4bffcfdd Binary files /dev/null and b/ddsenabler/test/ddsEnablerTests/test_files/action_tutorials_interfaces__action__dds___Fibonacci_SendGoal_Request_.bin differ diff --git a/ddsenabler/test/ddsEnablerTests/test_files/action_tutorials_interfaces__action__dds___Fibonacci_SendGoal_Response_.bin b/ddsenabler/test/ddsEnablerTests/test_files/action_tutorials_interfaces__action__dds___Fibonacci_SendGoal_Response_.bin new file mode 100644 index 00000000..63bb3f25 Binary files /dev/null and b/ddsenabler/test/ddsEnablerTests/test_files/action_tutorials_interfaces__action__dds___Fibonacci_SendGoal_Response_.bin differ diff --git a/ddsenabler/test/ddsEnablerTests/test_files/add_two_ints b/ddsenabler/test/ddsEnablerTests/test_files/add_two_ints new file mode 100644 index 00000000..5af99fa0 --- /dev/null +++ b/ddsenabler/test/ddsEnablerTests/test_files/add_two_ints @@ -0,0 +1,7 @@ +{ + "reply_serialized_qos": "reliability: true\ndurability: false\nownership: false\nkeyed: false", + "reply_type_name": "example_interfaces::srv::dds_::AddTwoInts_Response_", + "request_serialized_qos": "reliability: true\ndurability: false\nownership: false\nkeyed: false", + "request_type_name": "example_interfaces::srv::dds_::AddTwoInts_Request_", + "service_name": "add_two_ints" +} \ No newline at end of file diff --git a/ddsenabler/test/ddsEnablerTests/test_files/example_interfaces__srv__dds___AddTwoInts_Request_.bin b/ddsenabler/test/ddsEnablerTests/test_files/example_interfaces__srv__dds___AddTwoInts_Request_.bin new file mode 100644 index 00000000..4b4e5e7d Binary files /dev/null and b/ddsenabler/test/ddsEnablerTests/test_files/example_interfaces__srv__dds___AddTwoInts_Request_.bin differ diff --git a/ddsenabler/test/ddsEnablerTests/test_files/example_interfaces__srv__dds___AddTwoInts_Response_.bin b/ddsenabler/test/ddsEnablerTests/test_files/example_interfaces__srv__dds___AddTwoInts_Response_.bin new file mode 100644 index 00000000..6f5f40ef Binary files /dev/null and b/ddsenabler/test/ddsEnablerTests/test_files/example_interfaces__srv__dds___AddTwoInts_Response_.bin differ diff --git a/ddsenabler/test/ddsEnablerTests/test_files/fibonacci__action_ b/ddsenabler/test/ddsEnablerTests/test_files/fibonacci__action_ new file mode 100644 index 00000000..22db6867 --- /dev/null +++ b/ddsenabler/test/ddsEnablerTests/test_files/fibonacci__action_ @@ -0,0 +1,19 @@ +{ + "action_name": "fibonacci/_action/", + "cancel_reply_action_serialized_qos": "reliability: true\ndurability: false\nownership: false\nkeyed: false", + "cancel_reply_action_type": "action_msgs::srv::dds_::CancelGoal_Response_", + "cancel_request_action_serialized_qos": "reliability: true\ndurability: false\nownership: false\nkeyed: false", + "cancel_request_action_type": "action_msgs::srv::dds_::CancelGoal_Request_", + "feedback_action_serialized_qos": "reliability: true\ndurability: false\nownership: false\nkeyed: false", + "feedback_action_type": "action_tutorials_interfaces::action::dds_::Fibonacci_FeedbackMessage_", + "goal_reply_action_serialized_qos": "reliability: true\ndurability: false\nownership: false\nkeyed: false", + "goal_reply_action_type": "action_tutorials_interfaces::action::dds_::Fibonacci_SendGoal_Response_", + "goal_request_action_serialized_qos": "reliability: true\ndurability: false\nownership: false\nkeyed: false", + "goal_request_action_type": "action_tutorials_interfaces::action::dds_::Fibonacci_SendGoal_Request_", + "result_reply_action_serialized_qos": "reliability: true\ndurability: false\nownership: false\nkeyed: false", + "result_reply_action_type": "action_tutorials_interfaces::action::dds_::Fibonacci_GetResult_Response_", + "result_request_action_serialized_qos": "reliability: true\ndurability: false\nownership: false\nkeyed: false", + "result_request_action_type": "action_tutorials_interfaces::action::dds_::Fibonacci_GetResult_Request_", + "status_action_serialized_qos": "reliability: true\ndurability: true\nownership: false\nkeyed: false", + "status_action_type": "action_msgs::msg::dds_::GoalStatusArray_" +} \ No newline at end of file diff --git a/ddsenabler/test/ddsEnablerTests/test_files/rcl_interfaces__msg__dds___Log_.bin b/ddsenabler/test/ddsEnablerTests/test_files/rcl_interfaces__msg__dds___Log_.bin new file mode 100644 index 00000000..6e56d56a Binary files /dev/null and b/ddsenabler/test/ddsEnablerTests/test_files/rcl_interfaces__msg__dds___Log_.bin differ diff --git a/ddsenabler/test/ddsEnablerTests/test_files/rcl_interfaces__msg__dds___ParameterEvent_.bin b/ddsenabler/test/ddsEnablerTests/test_files/rcl_interfaces__msg__dds___ParameterEvent_.bin new file mode 100644 index 00000000..98649ae7 Binary files /dev/null and b/ddsenabler/test/ddsEnablerTests/test_files/rcl_interfaces__msg__dds___ParameterEvent_.bin differ diff --git a/ddsenabler/test/ddsEnablerTests/test_files/rcl_interfaces__srv__dds___DescribeParameters_Request_.bin b/ddsenabler/test/ddsEnablerTests/test_files/rcl_interfaces__srv__dds___DescribeParameters_Request_.bin new file mode 100644 index 00000000..d33a48d9 Binary files /dev/null and b/ddsenabler/test/ddsEnablerTests/test_files/rcl_interfaces__srv__dds___DescribeParameters_Request_.bin differ diff --git a/ddsenabler/test/ddsEnablerTests/test_files/rcl_interfaces__srv__dds___DescribeParameters_Response_.bin b/ddsenabler/test/ddsEnablerTests/test_files/rcl_interfaces__srv__dds___DescribeParameters_Response_.bin new file mode 100644 index 00000000..f7a41219 Binary files /dev/null and b/ddsenabler/test/ddsEnablerTests/test_files/rcl_interfaces__srv__dds___DescribeParameters_Response_.bin differ diff --git a/ddsenabler/test/ddsEnablerTests/test_files/rcl_interfaces__srv__dds___GetParameterTypes_Request_.bin b/ddsenabler/test/ddsEnablerTests/test_files/rcl_interfaces__srv__dds___GetParameterTypes_Request_.bin new file mode 100644 index 00000000..f1cf3f19 Binary files /dev/null and b/ddsenabler/test/ddsEnablerTests/test_files/rcl_interfaces__srv__dds___GetParameterTypes_Request_.bin differ diff --git a/ddsenabler/test/ddsEnablerTests/test_files/rcl_interfaces__srv__dds___GetParameterTypes_Response_.bin b/ddsenabler/test/ddsEnablerTests/test_files/rcl_interfaces__srv__dds___GetParameterTypes_Response_.bin new file mode 100644 index 00000000..55815c1f Binary files /dev/null and b/ddsenabler/test/ddsEnablerTests/test_files/rcl_interfaces__srv__dds___GetParameterTypes_Response_.bin differ diff --git a/ddsenabler/test/ddsEnablerTests/test_files/rcl_interfaces__srv__dds___GetParameters_Request_.bin b/ddsenabler/test/ddsEnablerTests/test_files/rcl_interfaces__srv__dds___GetParameters_Request_.bin new file mode 100644 index 00000000..fe10b8fb Binary files /dev/null and b/ddsenabler/test/ddsEnablerTests/test_files/rcl_interfaces__srv__dds___GetParameters_Request_.bin differ diff --git a/ddsenabler/test/ddsEnablerTests/test_files/rcl_interfaces__srv__dds___GetParameters_Response_.bin b/ddsenabler/test/ddsEnablerTests/test_files/rcl_interfaces__srv__dds___GetParameters_Response_.bin new file mode 100644 index 00000000..ac082bff Binary files /dev/null and b/ddsenabler/test/ddsEnablerTests/test_files/rcl_interfaces__srv__dds___GetParameters_Response_.bin differ diff --git a/ddsenabler/test/ddsEnablerTests/test_files/rcl_interfaces__srv__dds___ListParameters_Request_.bin b/ddsenabler/test/ddsEnablerTests/test_files/rcl_interfaces__srv__dds___ListParameters_Request_.bin new file mode 100644 index 00000000..9b9387d7 Binary files /dev/null and b/ddsenabler/test/ddsEnablerTests/test_files/rcl_interfaces__srv__dds___ListParameters_Request_.bin differ diff --git a/ddsenabler/test/ddsEnablerTests/test_files/rcl_interfaces__srv__dds___ListParameters_Response_.bin b/ddsenabler/test/ddsEnablerTests/test_files/rcl_interfaces__srv__dds___ListParameters_Response_.bin new file mode 100644 index 00000000..a1212fb4 Binary files /dev/null and b/ddsenabler/test/ddsEnablerTests/test_files/rcl_interfaces__srv__dds___ListParameters_Response_.bin differ diff --git a/ddsenabler/test/ddsEnablerTests/test_files/rcl_interfaces__srv__dds___SetParametersAtomically_Request_.bin b/ddsenabler/test/ddsEnablerTests/test_files/rcl_interfaces__srv__dds___SetParametersAtomically_Request_.bin new file mode 100644 index 00000000..c19eeb6a Binary files /dev/null and b/ddsenabler/test/ddsEnablerTests/test_files/rcl_interfaces__srv__dds___SetParametersAtomically_Request_.bin differ diff --git a/ddsenabler/test/ddsEnablerTests/test_files/rcl_interfaces__srv__dds___SetParametersAtomically_Response_.bin b/ddsenabler/test/ddsEnablerTests/test_files/rcl_interfaces__srv__dds___SetParametersAtomically_Response_.bin new file mode 100644 index 00000000..1043f036 Binary files /dev/null and b/ddsenabler/test/ddsEnablerTests/test_files/rcl_interfaces__srv__dds___SetParametersAtomically_Response_.bin differ diff --git a/ddsenabler/test/ddsEnablerTests/test_files/rcl_interfaces__srv__dds___SetParameters_Request_.bin b/ddsenabler/test/ddsEnablerTests/test_files/rcl_interfaces__srv__dds___SetParameters_Request_.bin new file mode 100644 index 00000000..da5208da Binary files /dev/null and b/ddsenabler/test/ddsEnablerTests/test_files/rcl_interfaces__srv__dds___SetParameters_Request_.bin differ diff --git a/ddsenabler/test/ddsEnablerTests/test_files/rcl_interfaces__srv__dds___SetParameters_Response_.bin b/ddsenabler/test/ddsEnablerTests/test_files/rcl_interfaces__srv__dds___SetParameters_Response_.bin new file mode 100644 index 00000000..7b8a8ec8 Binary files /dev/null and b/ddsenabler/test/ddsEnablerTests/test_files/rcl_interfaces__srv__dds___SetParameters_Response_.bin differ diff --git a/ddsenabler/test/ddsEnablerTests/test_files/rmw_dds_common__msg__dds___ParticipantEntitiesInfo_.bin b/ddsenabler/test/ddsEnablerTests/test_files/rmw_dds_common__msg__dds___ParticipantEntitiesInfo_.bin new file mode 100644 index 00000000..c3236d96 Binary files /dev/null and b/ddsenabler/test/ddsEnablerTests/test_files/rmw_dds_common__msg__dds___ParticipantEntitiesInfo_.bin differ diff --git a/ddsenabler/test/ddsEnablerTests/test_files/type_description_interfaces__srv__dds___GetTypeDescription_Request_.bin b/ddsenabler/test/ddsEnablerTests/test_files/type_description_interfaces__srv__dds___GetTypeDescription_Request_.bin new file mode 100644 index 00000000..ba4d778e Binary files /dev/null and b/ddsenabler/test/ddsEnablerTests/test_files/type_description_interfaces__srv__dds___GetTypeDescription_Request_.bin differ diff --git a/ddsenabler/test/ddsEnablerTests/test_files/type_description_interfaces__srv__dds___GetTypeDescription_Response_.bin b/ddsenabler/test/ddsEnablerTests/test_files/type_description_interfaces__srv__dds___GetTypeDescription_Response_.bin new file mode 100644 index 00000000..f68b4410 Binary files /dev/null and b/ddsenabler/test/ddsEnablerTests/test_files/type_description_interfaces__srv__dds___GetTypeDescription_Response_.bin differ diff --git a/ddsenabler/test/ddsEnablerTypedTests/CMakeLists.txt b/ddsenabler/test/ddsEnablerTypedTests/CMakeLists.txt index 0de57b01..c272ae70 100644 --- a/ddsenabler/test/ddsEnablerTypedTests/CMakeLists.txt +++ b/ddsenabler/test/ddsEnablerTypedTests/CMakeLists.txt @@ -686,6 +686,7 @@ set(TEST_NEEDED_SOURCES set(TEST_EXTRA_HEADERS ${PROJECT_SOURCE_DIR}/test ${PROJECT_SOURCE_DIR}/test/ddsEnablerTypedTests/types + ${PROJECT_SOURCE_DIR}/examples/utils ) add_blackbox_executable( @@ -697,3 +698,6 @@ add_blackbox_executable( "${TEST_LIBRARY_SOURCES}" ) +target_sources(blackbox_DDSEnablerTypedTest PRIVATE ${PROJECT_SOURCE_DIR}/examples/utils/Utils.cpp) +target_include_directories(blackbox_DDSEnablerTypedTest PRIVATE ${PROJECT_SOURCE_DIR}/examples/utils) + diff --git a/ddsenabler/test/ddsEnablerTypedTests/DdsEnablerTypedTest.cpp b/ddsenabler/test/ddsEnablerTypedTests/DdsEnablerTypedTest.cpp index 01aa630a..040780c2 100644 --- a/ddsenabler/test/ddsEnablerTypedTests/DdsEnablerTypedTest.cpp +++ b/ddsenabler/test/ddsEnablerTypedTests/DdsEnablerTypedTest.cpp @@ -46,6 +46,9 @@ class DDSEnablerTypedTest : public ddsenablertester::DDSEnablerTester a_type.type_sup_.reset(new Type()); \ \ ASSERT_TRUE(create_publisher(a_type)); \ +\ + /* Wait some time for discovery */ \ + std::this_thread::sleep_for(std::chrono::milliseconds(wait_after_publication_ms)); \ \ ASSERT_EQ(get_received_types(), 1); \ ASSERT_EQ(get_received_topics(), 1); \ diff --git a/ddsenabler_docs/docs/ddsenabler/api_reference/api_reference.rst b/ddsenabler_docs/docs/ddsenabler/api_reference/api_reference.rst index 1f991434..271ecaea 100644 --- a/ddsenabler_docs/docs/ddsenabler/api_reference/api_reference.rst +++ b/ddsenabler_docs/docs/ddsenabler/api_reference/api_reference.rst @@ -19,4 +19,6 @@ The following pages document the classes, functions, and types exposed by *DDS E /ddsenabler/api_reference/dds_enabler/dds_enabler /ddsenabler/api_reference/callback_set/callback_set /ddsenabler/api_reference/dds_callbacks/dds_callbacks + /ddsenabler/api_reference/dds_callbacks/service_callbacks + /ddsenabler/api_reference/dds_callbacks/action_callbacks /ddsenabler/api_reference/dds_callbacks/callbacks diff --git a/ddsenabler_docs/docs/ddsenabler/api_reference/dds_callbacks/action_callbacks.rst b/ddsenabler_docs/docs/ddsenabler/api_reference/dds_callbacks/action_callbacks.rst new file mode 100644 index 00000000..967fc5c8 --- /dev/null +++ b/ddsenabler_docs/docs/ddsenabler/api_reference/dds_callbacks/action_callbacks.rst @@ -0,0 +1,12 @@ + +.. _api_pim_action_callbacks: + +.. rst-class:: api-ref + +Action Callbacks +---------------- + +.. doxygenstruct:: eprosima::ddsenabler::ActionCallbacks + :project: DDSEnabler + :members: + diff --git a/ddsenabler_docs/docs/ddsenabler/api_reference/dds_callbacks/service_callbacks.rst b/ddsenabler_docs/docs/ddsenabler/api_reference/dds_callbacks/service_callbacks.rst new file mode 100644 index 00000000..59ecd67f --- /dev/null +++ b/ddsenabler_docs/docs/ddsenabler/api_reference/dds_callbacks/service_callbacks.rst @@ -0,0 +1,12 @@ + +.. _api_pim_service_callbacks: + +.. rst-class:: api-ref + +Service Callbacks +----------------- + +.. doxygenstruct:: eprosima::ddsenabler::ServiceCallbacks + :project: DDSEnabler + :members: + diff --git a/ddsenabler_docs/docs/ddsenabler/installation/linux.rst b/ddsenabler_docs/docs/ddsenabler/installation/linux.rst index d8b7c2cd..ebb7c377 100644 --- a/ddsenabler_docs/docs/ddsenabler/installation/linux.rst +++ b/ddsenabler_docs/docs/ddsenabler/installation/linux.rst @@ -272,6 +272,10 @@ Run an example In this section, we will run a publish example mimicking the behavior of the classic *Hello World* ROS 2 talker in a specific domain. For simplicity, we will use the *eProsima DDS Enabler* example application with the already provided configuration file and sample data from a test case in the *dds_enabler_test* package. +.. note:: + + To run a service or action example, please refer to the corresponding readme in the examples folder. + To run this *eProsima DDS Enabler* example, source the installation path and execute the executable file that has been installed in :code:`/dds_enabler_tool/bin/dds_enabler`: .. code-block:: bash @@ -286,7 +290,7 @@ To run this *eProsima DDS Enabler* example, source the installation path and exe cd source install/setup.bash export TEST_PATH=$PWD/src/DDS-Enabler/ddsenabler_test/compose/test_cases/publish/discovered_type - ./build/ddsenabler/examples/ddsenabler_example --config $TEST_PATH/config.yml --timeout 10 --expected-types 1 --expected-topics 1 --publish-path $TEST_PATH/samples --publish-topic rt/chatter --publish-period 200 --publish-initial-wait 5000 + ./install/ddsenabler/examples/publish/ddsenabler_example_publish --config $TEST_PATH/config.yml --timeout 10 --expected-types 1 --expected-topics 1 --publish-path $TEST_PATH/samples --publish-topic rt/chatter --publish-period 200 --publish-initial-wait 5000 .. important:: diff --git a/ddsenabler_participants/include/ddsenabler_participants/Callbacks.hpp b/ddsenabler_participants/include/ddsenabler_participants/Callbacks.hpp index ed71b6a0..99347e17 100644 --- a/ddsenabler_participants/include/ddsenabler_participants/Callbacks.hpp +++ b/ddsenabler_participants/include/ddsenabler_participants/Callbacks.hpp @@ -22,10 +22,38 @@ #include #include +#include + namespace eprosima { namespace ddsenabler { namespace participants { + +/*********************/ +/* DDS */ +/*********************/ + +/** + * @brief Struct that contains information about a DDS topic. + * + * This struct is used to encapsulate the type name and serialized QoS of a DDS topic. + */ +struct TopicInfo +{ + TopicInfo() = default; + + TopicInfo ( + const std::string& _type_name, + const std::string& _serialized_qos) + : type_name(_type_name) + , serialized_qos(_serialized_qos) + { + } + + std::string type_name; + std::string serialized_qos; +}; + /** * DdsLogFunc - callback executed when consuming log messages * @@ -62,13 +90,11 @@ typedef void (* DdsTypeNotification)( * DdsTopicNotification - callback for notifying the reception of DDS topics * * @param [in] topic_name Name of the received topic - * @param [in] type_name Name of the type associated with the topic - * @param [in] serialized_qos Serialized Quality of Service (QoS) of the topic + * @param [in] topic_info Information about the topic, including type name and serialized QoS */ typedef void (* DdsTopicNotification)( const char* topic_name, - const char* type_name, - const char* serialized_qos); + const TopicInfo& topic_info); /** * DdsDataNotification - callback for notifying the reception of DDS data @@ -99,14 +125,247 @@ typedef bool (* DdsTypeQuery)( * DdsTopicQuery - callback for requesting information (type and QoS) of a DDS topic * * @param [in] topic_name Name of the topic to query - * @param [out] type_name Name of the type associated with the topic - * @param [out] serialized_qos Serialized Quality of Service (QoS) of the topic + * @param [out] topic_info Information about the topic, including type name and serialized QoS * @return \c true if the topic was found and the information was retrieved successfully, \c false otherwise */ typedef bool (* DdsTopicQuery)( const char* topic_name, - std::string& type_name, - std::string& serialized_qos); + TopicInfo& topic_info); + + +/**********************/ +/* SERVICES */ +/**********************/ + +/** + * @brief Struct that contains information about a DDS service. + * + * This struct is used to encapsulate the request and reply topics of a DDS service. + */ +struct ServiceInfo +{ + ServiceInfo() = default; + + ServiceInfo( + const TopicInfo& _request, + const TopicInfo& _reply) + : request(_request) + , reply(_reply) + { + } + + TopicInfo request; + TopicInfo reply; +}; + +/** + * @brief Callback for notification of service discovery and its request and reply types. + * + * This callback is used to notify the discovery of a service and its associated request and reply types. + * + * @param [in] service_name The name of the service that was discovered. + * @param [in] service_info Information about the service, including request and reply types and their serialized QoS. + */ +typedef void (* ServiceNotification)( + const char* service_name, + const ServiceInfo& service_info); + +/** + * @brief Callback for reception of service request data. + * + * This callback is used to notify the reception of a request for a specific service. + * + * @param [in] service_name The name of the service for which the request was received. + * @param [in] json The JSON data received in the request. + * @param [in] request_id The unique identifier of the request. + * @param [in] publish_time The time at which the request was published. + * + * @note The request_id is unique for each request and must be later used to identify the reply. + */ +typedef void (* ServiceRequestNotification)( + const char* service_name, + const char* json, + uint64_t request_id, + int64_t publish_time); + +/** + * @brief Callback for reception of RPC reply data. + * + * This callback is used to notify the reception of a reply for a specific service. + * + * @param [in] service_name The name of the service for which the reply was received. + * @param [in] json The JSON data received in the reply. + * @param [in] request_id The unique identifier of the request for which this is a reply. + * @param [in] publish_time The time at which the reply was published. + */ +typedef void (* ServiceReplyNotification)( + const char* service_name, + const char* json, + uint64_t request_id, + int64_t publish_time); + +/** + * @brief Callback requesting the information of a given service's request and reply. + * + * This callback is used to request the information for a service's request and reply. + * + * @param [in] service_name The name of the service for which the information is requested. + * @param [out] service_info Information about the service, including request and reply types and their serialized QoS. + * @return \c true if the service was found and the information was retrieved successfully, \c false otherwise. + */ +typedef bool (* ServiceQuery)( + const char* service_name, + ServiceInfo& service_info); + + +/**********************/ +/* ACTIONS */ +/**********************/ + +/** + * @brief Struct that contains information about a DDS action. + * + * This struct is used to encapsulate the goal, result and cancel services as well as the feedback and status topics of a DDS action. + */ +struct ActionInfo +{ + ActionInfo() = default; + + ActionInfo( + const ServiceInfo& _goal, + const ServiceInfo& _result, + const ServiceInfo& _cancel, + const TopicInfo& _feedback, + const TopicInfo& _status) + : goal(_goal) + , result(_result) + , cancel(_cancel) + , feedback(_feedback) + , status(_status) + { + } + + ServiceInfo goal; + ServiceInfo result; + ServiceInfo cancel; + TopicInfo feedback; + TopicInfo status; +}; + +/** + * @brief Callback for notification of action discovery and its associated types. + * + * This callback is used to notify the discovery of an action and its associated types. + * + * @param [in] action_name The name of the action that was discovered. + * @param [in] action_info Information about the action, including goal, result, cancel, feedback, and status types and their serialized QoS. + */ +typedef void (* ActionNotification)( + const char* action_name, + const ActionInfo& action_info); + +/** + * @brief Callback for notification of an action goal request. + * + * This callback is used to notify the request of an action goal. + * + * @param [in] action_name The name of the action for which the goal is being requested. + * @param [in] json The JSON data representing the goal request. + * @param [in] goal_id The unique identifier of the goal associated with the action. + * @param [in] publish_time The time at which the goal request was published. + * @return \c true if the goal request has been accepted, \c false otherwise. + */ +typedef bool (* ActionGoalRequestNotification)( + const char* action_name, + const char* json, + const UUID& goal_id, + int64_t publish_time); + +/** + * @brief Callback for notification of an action cancel request. + * + * This callback is used to notify the request to cancel an action goal: + * + * If the goal ID is empty and timestamp is zero, cancel all goals + * If the goal ID is empty and timestamp is not zero, cancel all goals accepted at or before the timestamp + * If the goal ID is not empty and timestamp is zero, cancel the goal with the given ID regardless of the time it was accepted + * If the goal ID is not empty and timestamp is not zero, cancel the goal with the given ID and all goals accepted at or before the timestamp + * + * @param [in] action_name The name of the action for which the cancel request is being made. + * @param [in] goal_id The unique identifier of the goal associated with the action. + * @param [in] timestamp The timestamp used for discriminating which goals to cancel. + * @param [in] request_id The identifier of the cancel request used for dicriminating which goals to cancel. + * @param [in] publish_time The time at which the cancel request was published. + */ +typedef void (* ActionCancelRequestNotification)( + const char* action_name, + const UUID& goal_id, + int64_t timestamp, + uint64_t request_id, + int64_t publish_time); + +/** + * @brief Callback for notification of action feedback. + * + * This callback is used to notify the feedback of an action. + * + * @param [in] action_name The name of the action for which the feedback is being notified. + * @param [in] json The JSON data representing the feedback of the action. + * @param [in] goal_id The unique identifier of the goal associated with the action. + * @param [in] publish_time The time at which the feedback was published. + */ +typedef void (* ActionFeedbackNotification)( + const char* action_name, + const char* json, + const UUID& goal_id, + int64_t publish_time); + +/** + * @brief Callback for notification of an update for an action status. + * + * This callback is used to notify the update of the status of an action. + * + * @param [in] action_name The name of the action for which the status is being notified. + * @param [in] goal_id The unique identifier of the goal associated with the action. + * @param [in] status_code The status code representing the current state of the action. + * @param [in] status_message A message providing additional information about the status. + * @param [in] publish_time The time at which the status was published. + */ +typedef void (* ActionStatusNotification)( + const char* action_name, + const UUID& goal_id, + StatusCode status_code, + const char* status_message, + int64_t publish_time); + +/** + * @brief Callback for notification of action result. + * + * This callback is used to notify the result of an action in case of success. + * + * @param [in] action_name The name of the action for which the result is being notified. + * @param [in] json The JSON data representing the result of the action. + * @param [in] goal_id The unique identifier of the goal associated with the action. + * @param [in] publish_time The time at which the result was published. + */ +typedef void (* ActionResultNotification)( + const char* action_name, + const char* json, + const UUID& goal_id, + int64_t publish_time); + +/** + * @brief Callback for requesting action's information. + * + * This callback is used to request information about a specific action. + * + * @param [in] action_name The name of the action for which the information is being requested. + * @param [out] action_info Information about the action, including goal, result, cancel, feedback, and status types and their serialized QoS. + * @return \c true if the action was found and the information was retrieved successfully, \c false otherwise. + */ +typedef bool (* ActionQuery)( + const char* action_name, + ActionInfo& action_info); } /* namespace participants */ } /* namespace ddsenabler */ diff --git a/ddsenabler_participants/include/ddsenabler_participants/Constants.hpp b/ddsenabler_participants/include/ddsenabler_participants/Constants.hpp index acbf84ac..df74515f 100644 --- a/ddsenabler_participants/include/ddsenabler_participants/Constants.hpp +++ b/ddsenabler_participants/include/ddsenabler_participants/Constants.hpp @@ -28,6 +28,28 @@ constexpr const char* QOS_SERIALIZATION_DURABILITY("durability"); constexpr const char* QOS_SERIALIZATION_OWNERSHIP("ownership"); constexpr const char* QOS_SERIALIZATION_KEYED("keyed"); +// Topic mangling +constexpr const char* ROS_TOPIC_PREFIX("rt/"); +constexpr const char* FASTDDS_TOPIC_PREFIX(""); + +// Service mangling +constexpr const char* ROS_REQUEST_PREFIX("rq/"); +constexpr const char* ROS_REQUEST_SUFFIX("Request"); +constexpr const char* ROS_REPLY_PREFIX("rr/"); +constexpr const char* ROS_REPLY_SUFFIX("Reply"); + +constexpr const char* DDS_REQUEST_PREFIX(""); +constexpr const char* DDS_REQUEST_SUFFIX("_Request"); +constexpr const char* DDS_REPLY_PREFIX(""); +constexpr const char* DDS_REPLY_SUFFIX("_Reply"); + +// Action mangling +constexpr const char* ACTION_GOAL_SUFFIX("send_goal"); +constexpr const char* ACTION_RESULT_SUFFIX("get_result"); +constexpr const char* ACTION_CANCEL_SUFFIX("cancel_goal"); +constexpr const char* ACTION_FEEDBACK_SUFFIX("feedback"); +constexpr const char* ACTION_STATUS_SUFFIX("status"); + } /* namespace participants */ } /* namespace ddsenabler */ } /* namespace eprosima */ diff --git a/ddsenabler_participants/include/ddsenabler_participants/EnablerParticipant.hpp b/ddsenabler_participants/include/ddsenabler_participants/EnablerParticipant.hpp index 2e67b8a7..51282e2f 100644 --- a/ddsenabler_participants/include/ddsenabler_participants/EnablerParticipant.hpp +++ b/ddsenabler_participants/include/ddsenabler_participants/EnablerParticipant.hpp @@ -28,6 +28,10 @@ #include #include #include +#include +#include +#include + namespace eprosima { namespace ddsenabler { @@ -44,6 +48,12 @@ class EnablerParticipant : public ddspipe::participants::SchemaParticipant std::shared_ptr discovery_database, std::shared_ptr schema_handler); + DDSENABLER_PARTICIPANTS_DllAPI + bool is_rtps_kind() const noexcept override + { + return true; // Temporal workaround until Pipe refactor + } + DDSENABLER_PARTICIPANTS_DllAPI std::shared_ptr create_reader( const ddspipe::core::ITopic& topic) override; @@ -53,6 +63,12 @@ class EnablerParticipant : public ddspipe::participants::SchemaParticipant const std::string& topic_name, const std::string& json); + DDSENABLER_PARTICIPANTS_DllAPI + bool publish_rpc( + const std::string& topic_name, + const std::string& json, + const uint64_t request_id); + DDSENABLER_PARTICIPANTS_DllAPI void set_topic_query_callback( participants::DdsTopicQuery callback) @@ -60,22 +76,188 @@ class EnablerParticipant : public ddspipe::participants::SchemaParticipant topic_query_callback_ = callback; } + DDSENABLER_PARTICIPANTS_DllAPI + void set_service_query_callback( + participants::ServiceQuery callback) + { + service_query_callback_ = callback; + } + + DDSENABLER_PARTICIPANTS_DllAPI + void set_action_query_callback( + participants::ActionQuery callback) + { + action_query_callback_ = callback; + } + + DDSENABLER_PARTICIPANTS_DllAPI + bool announce_service( + const std::string& service_name, + Protocol Protocol); + + DDSENABLER_PARTICIPANTS_DllAPI + bool revoke_service( + const std::string& service_name); + + DDSENABLER_PARTICIPANTS_DllAPI + bool send_service_request( + const std::string& service_name, + const std::string& json, + uint64_t& request_id, + participants::Protocol Protocol); + + DDSENABLER_PARTICIPANTS_DllAPI + bool send_service_reply( + const std::string& service_name, + const std::string& json, + const uint64_t request_id); + + DDSENABLER_PARTICIPANTS_DllAPI + Protocol get_service_protocol( + const std::string& service_name); + + DDSENABLER_PARTICIPANTS_DllAPI + bool announce_action( + const std::string& action_name, + Protocol Protocol); + + DDSENABLER_PARTICIPANTS_DllAPI + bool revoke_action( + const std::string& action_name); + + DDSENABLER_PARTICIPANTS_DllAPI + bool send_action_goal( + const std::string& action_name, + const std::string& json, + UUID& action_id, + participants::Protocol Protocol); + + DDSENABLER_PARTICIPANTS_DllAPI + bool cancel_action_goal( + const std::string& action_name, + const UUID& goal_id, + const int64_t timestamp); + + DDSENABLER_PARTICIPANTS_DllAPI + bool send_action_get_result_request( + const std::string& action_name, + const UUID& action_id); + + DDSENABLER_PARTICIPANTS_DllAPI + void send_action_send_goal_reply( + const std::string& action_name, + const uint64_t goal_id, + bool accepted); + + DDSENABLER_PARTICIPANTS_DllAPI + bool send_action_cancel_goal_reply( + const char* action_name, + const std::vector& goal_ids, + const CancelCode& cancel_code, + const uint64_t request_id); + + DDSENABLER_PARTICIPANTS_DllAPI + bool send_action_result( + const char* action_name, + const UUID& goal_id, + const StatusCode& status_code, + const char* json); + + DDSENABLER_PARTICIPANTS_DllAPI + bool send_action_get_result_reply( + const std::string& action_name, + const UUID& goal_id, + const std::string& reply_json, + const uint64_t request_id); + + DDSENABLER_PARTICIPANTS_DllAPI + bool send_action_feedback( + const char* action_name, + const char* json, + const UUID& goal_id); + + DDSENABLER_PARTICIPANTS_DllAPI + bool update_action_status( + const std::string& action_name, + const UUID& goal_id, + const StatusCode& status_code); + protected: - std::shared_ptr lookup_reader_nts_( + bool query_topic_nts_( + const std::string& topic_name, + ddspipe::core::types::DdsTopic& topic); + + bool query_service_nts_( + std::shared_ptr service, + Protocol Protocol); + + bool query_action_nts_( + ActionDiscovered& action, + Protocol Protocol, + std::unique_lock& lck); + + bool create_topic_writer_nts_( + const ddspipe::core::types::DdsTopic& topic, + std::shared_ptr& reader, + std::unique_lock& lck); + + bool create_topic_writer_nts_( + const ddspipe::core::types::DdsTopic& topic, + std::shared_ptr& reader, + ddspipe::core::types::Endpoint& request_edp, + std::unique_lock& lck); + + + bool create_service_request_writer_nts_( + std::shared_ptr service, + std::unique_lock& lck); + + bool fill_topic_struct_nts_( + const std::string& topic_name, + const TopicInfo& topic_info, + ddspipe::core::types::DdsTopic& topic); + + bool fill_service_type_nts_( + const ServiceInfo& service_info, + std::shared_ptr service, + Protocol Protocol); + + std::shared_ptr lookup_reader_nts_( const std::string& topic_name, std::string& type_name) const; - std::shared_ptr lookup_reader_nts_( + std::shared_ptr lookup_reader_nts_( const std::string& topic_name) const; + bool service_discovered_nts_( + const std::shared_ptr rpc_info, + const ddspipe::core::types::DdsTopic& topic); + + bool action_discovered_nts_( + const std::shared_ptr rpc_info, + const ddspipe::core::types::DdsTopic& topic); + + bool revoke_service_nts_( + const std::string& service_name); + std::map> readers_; + std::map> services_; + + std::map> actions_; + std::mutex mtx_; std::condition_variable cv_; DdsTopicQuery topic_query_callback_; + + ServiceQuery service_query_callback_; + + ActionQuery action_query_callback_; + + std::shared_ptr handler_; }; } /* namespace participants */ diff --git a/ddsenabler_participants/include/ddsenabler_participants/Handler.hpp b/ddsenabler_participants/include/ddsenabler_participants/Handler.hpp index 99a4e6f4..92fc5324 100644 --- a/ddsenabler_participants/include/ddsenabler_participants/Handler.hpp +++ b/ddsenabler_participants/include/ddsenabler_participants/Handler.hpp @@ -21,11 +21,13 @@ #include #include #include -#include +#include +#include #include #include #include +#include #include #include @@ -35,6 +37,8 @@ #include #include #include +#include +#include #include namespace std { @@ -52,12 +56,30 @@ struct hash }; +template<> +struct hash +{ + std::size_t operator ()( + const eprosima::ddsenabler::participants::UUID& uuid) const noexcept + { + std::size_t hash = 0; + for (uint8_t byte : uuid) + { + hash ^= std::hash{}(byte) + 0x9e3779b9 + (hash << 6) + (hash >> 2); + } + return hash; + } + +}; + } // std namespace eprosima { namespace ddsenabler { namespace participants { +class Writer; + /** * Class that manages the interaction between \c EnablerParticipant and the user's app. * Payloads are efficiently passed from DDS Pipe to the user's app without copying data (only references). @@ -108,6 +130,24 @@ class Handler : public ddspipe::participants::ISchemaHandler void add_topic( const ddspipe::core::types::DdsTopic& topic); + /** + * @brief Add a service, associated to the given \c service. + * + * @param [in] service RPC topic associated to this service. + */ + DDSENABLER_PARTICIPANTS_DllAPI + void add_service( + const ddspipe::core::types::RpcTopic& service); + + /** + * @brief Add an action, associated to the given \c action. + * + * @param [in] action RPC action associated to this action. + */ + DDSENABLER_PARTICIPANTS_DllAPI + void add_action( + const RpcAction& action); + /** * @brief Add a data sample, associated to the given \c topic. * @@ -145,6 +185,86 @@ class Handler : public ddspipe::participants::ISchemaHandler const std::string& json, ddspipe::core::types::Payload& payload); + /** + * @brief Store an action request (goal, cancel or result) with its associated UUID and request ID. + * This info will later be used to associate the reply id with the UUID of the action. + * + * @note: An ActionRequestInfo can only be created if ActionType is GOAL, otherwise it will return false. + * @note: When creating the ActionRequestInfo the current time will be stored as the goal accepted stamp. + * + * @param [in] action_name Name of the action. + * @param [in] action_id UUID of the action. + * @param [in] request_id Request ID of the action request. + * @param [in] action_type Type of the action (GOAL, RESULT, CANCEL). + * @param [in] Protocol Protocol of the action, only used to create the ActionRequestInfo if action_type is GOAL. + * @return \c true if the action request was successfully stored, \c false otherwise. + */ + DDSENABLER_PARTICIPANTS_DllAPI + bool store_action_request( + const std::string& action_name, + const UUID& action_id, + const uint64_t request_id, + const ActionType action_type, + const Protocol Protocol = Protocol::ROS2); + + /** + * @brief Send the reply containing the result of an action or store it for a later reply. + * If the result request was previously received the reply will be sent, otherwise the result will be stored. + * + * @param [in] action_name Name of the action. + * @param [in] action_id UUID of the action. + * @param [in] result Result of the action. + * @return \c true if the result was successfully stored, \c false otherwise. + */ + DDSENABLER_PARTICIPANTS_DllAPI + bool handle_action_result( + const std::string& action_name, + const UUID& action_id, + const std::string& result); + + /** + * @brief Erase an action UUID from the internal map that tracks which actions are active. + * + * An action is considered active if it has a goal request and has not received its result and a final status (aborted, rejected, succeeded). + * The action will be forced to be erased if \c force_erase is set to \c true , allowing to erase actions without complying with the previous condition. + * + * @note: The status updates are not a strict requirement in the ROS2 actions specification, so there could be servers not sending them. + * + * @param [in] action_id UUID of the action to be erased. + * @param [in] force_erase If \c true, the action will be erased regardless of its status. + */ + DDSENABLER_PARTICIPANTS_DllAPI + void erase_action_UUID( + const UUID& action_id, + ActionEraseReason erase_reason); + + /** + * @brief Check if an action represented by its UUID is active. + * An action is considered active if it has a goal request and has not received its result and a final status (aborted, rejected, succeeded). + * + * @param [in] action_name Name of the action. + * @param [in] action_id UUID of the action to be checked. + * @param [out] goal_accepted_stamp Optional pointer to get the timestamp when the goal was accepted. + * @return \c true if the action is active, \c false otherwise. + */ + DDSENABLER_PARTICIPANTS_DllAPI + bool is_UUID_active( + const std::string& action_name, + const UUID& action_id, + std::chrono::system_clock::time_point* goal_accepted_stamp = nullptr); + + /** + * @brief Get the RPC protocol of an action represented by its UUID. + * + * @param [in] action_name Name of the action. + * @param [in] action_id UUID of the action to be checked. + * @return The RPC protocol of the action, or Protocol::PROTOCOL_UNKNOWN if the action is not found. + */ + DDSENABLER_PARTICIPANTS_DllAPI + Protocol get_action_protocol( + const std::string& action_name, + const UUID& action_id); + /** * @brief Set the data notification callback. * @@ -152,10 +272,7 @@ class Handler : public ddspipe::participants::ISchemaHandler */ DDSENABLER_PARTICIPANTS_DllAPI void set_data_notification_callback( - participants::DdsDataNotification callback) - { - writer_->set_data_notification_callback(callback); - } + participants::DdsDataNotification callback); /** * @brief Set the topic notification callback. @@ -164,10 +281,7 @@ class Handler : public ddspipe::participants::ISchemaHandler */ DDSENABLER_PARTICIPANTS_DllAPI void set_topic_notification_callback( - participants::DdsTopicNotification callback) - { - writer_->set_topic_notification_callback(callback); - } + participants::DdsTopicNotification callback); /** * @brief Set the type notification callback. @@ -176,10 +290,7 @@ class Handler : public ddspipe::participants::ISchemaHandler */ DDSENABLER_PARTICIPANTS_DllAPI void set_type_notification_callback( - participants::DdsTypeNotification callback) - { - writer_->set_type_notification_callback(callback); - } + participants::DdsTypeNotification callback); /** * @brief Set the type query callback. @@ -188,13 +299,149 @@ class Handler : public ddspipe::participants::ISchemaHandler */ DDSENABLER_PARTICIPANTS_DllAPI void set_type_query_callback( - participants::DdsTypeQuery callback) - { - type_query_callback_ = callback; - } + participants::DdsTypeQuery callback); + /** + * @brief Set the service notification callback. + * + * @param [in] callback Callback to be set. + */ + DDSENABLER_PARTICIPANTS_DllAPI + void set_service_notification_callback( + participants::ServiceNotification callback); + + /** + * @brief Set the service reply notification callback. + * + * @param [in] callback Callback to be set. + */ + DDSENABLER_PARTICIPANTS_DllAPI + void set_service_reply_notification_callback( + participants::ServiceReplyNotification callback); + + /** + * @brief Set the service request notification callback. + * + * @param [in] callback Callback to be set. + */ + DDSENABLER_PARTICIPANTS_DllAPI + void set_service_request_notification_callback( + participants::ServiceRequestNotification callback); + + /** + * @brief Set the action notification callback. + * + * @param [in] callback Callback to be set. + */ + DDSENABLER_PARTICIPANTS_DllAPI + void set_action_notification_callback( + participants::ActionNotification callback); + + /** + * @brief Set the action result notification callback. + * + * @param [in] callback Callback to be set. + */ + DDSENABLER_PARTICIPANTS_DllAPI + void set_action_result_notification_callback( + participants::ActionResultNotification callback); + + /** + * @brief Set the action feedback notification callback. + * + * @param [in] callback Callback to be set. + */ + DDSENABLER_PARTICIPANTS_DllAPI + void set_action_feedback_notification_callback( + participants::ActionFeedbackNotification callback); + + /** + * @brief Set the action status notification callback. + * + * @param [in] callback Callback to be set. + */ + DDSENABLER_PARTICIPANTS_DllAPI + void set_action_status_notification_callback( + participants::ActionStatusNotification callback); + + /** + * @brief Set the action get result request callback. + * + * @param [in] callback Callback to be set. + */ + DDSENABLER_PARTICIPANTS_DllAPI + void set_send_action_get_result_request_callback( + std::function callback); + + /** + * @brief Set the action goal request notification callback. + * + * @param [in] callback Callback to be set. + */ + DDSENABLER_PARTICIPANTS_DllAPI + void set_action_goal_request_notification_callback( + participants::ActionGoalRequestNotification callback); + + /** + * @brief Set the action cancel request notification callback. + * + * @param [in] callback Callback to be set. + */ + DDSENABLER_PARTICIPANTS_DllAPI + void set_action_cancel_request_notification_callback( + participants::ActionCancelRequestNotification callback); + + /** + * @brief Set the action send goal reply callback. + * + * @param [in] callback Callback to be set. + */ + DDSENABLER_PARTICIPANTS_DllAPI + void set_send_action_send_goal_reply_callback( + std::function callback); + + /** + * @brief Set the action get result reply callback. + * + * @param [in] callback Callback to be set. + */ + DDSENABLER_PARTICIPANTS_DllAPI + void set_send_action_get_result_reply_callback( + std::function callback); + + /** + * @brief Get a new request ID (incremented by one) for creating a service or action request. + * + * @return A new request ID. + */ + DDSENABLER_PARTICIPANTS_DllAPI + uint64_t get_new_request_id(); protected: + /** + * @brief Pop an action request UUID from the internal map that tracks which actions are active. + * + * @param [in] request_id Request ID of the action request. + * @param [in] action_type Type of the action (GOAL, RESULT, CANCEL). + * @param [out] action_id UUID of the action associated to the request. + * @return \c true if the action request was successfully popped, \c false otherwise. + */ + bool get_action_request_UUID( + const uint64_t request_id, + const ActionType action_type, + UUID& action_id); + + /** + * @brief Get the result (if it has been previouly stored) of an action associated to the given \c action_id. + * + * @param [in] action_id UUID of the action. + * @param [out] result Result of the action. + * @return \c true if the result was found, \c false otherwise. + */ + bool get_action_result( + const UUID& action_id, + std::string& result); + /** * @brief Add a schema, associated to the given \c dyn_type and \c type_id. * @@ -238,6 +485,21 @@ class Handler : public ddspipe::participants::ISchemaHandler void write_topic_nts_( const ddspipe::core::types::DdsTopic& topic); + /** + * @brief Write the service to user's app. + * + * @param [in] service RPC topic associated to this service. + */ + void write_service_nts_( + const ddspipe::core::types::RpcTopic& service); + + /** + * @brief Write the action to user's app. + * + * @param [in] action RPC action associated to this action. + */ + void write_action_nts_( + const RpcAction& action); /** * @brief Write to user's app. * @@ -248,6 +510,107 @@ class Handler : public ddspipe::participants::ISchemaHandler const Message& msg, const fastdds::dds::DynamicType::_ref_type& dyn_type); + /** + * @brief Write the service reply to user's app. + * + * @param [in] msg Message containing the service reply. + * @param [in] dyn_type DynamicType containing the type information required. + * @param [in] request_id Request ID of the service reply. + */ + void write_service_reply_nts_( + const Message& msg, + const fastdds::dds::DynamicType::_ref_type& dyn_type, + const uint64_t request_id, + const std::string& service_name); + + /** + * @brief Write the service request to user's app. + * + * @param [in] msg Message containing the service request. + * @param [in] dyn_type DynamicType containing the type information required. + * @param [in] request_id Request ID of the service request. + */ + void write_service_request_nts_( + const Message& msg, + const fastdds::dds::DynamicType::_ref_type& dyn_type, + const uint64_t request_id, + const std::string& service_name); + + /** + * @brief Write the action result to user's app. + * + * @param [in] msg Message containing the action goal request. + * @param [in] dyn_type DynamicType containing the type information required. + * @param [in] request_id Request ID of the action goal request. + */ + void write_action_result_nts_( + const Message& msg, + const fastdds::dds::DynamicType::_ref_type& dyn_type, + const UUID& action_id, + const std::string& action_name); + + /** + * @brief Write the action feedback to user's app. + * + * @param [in] msg Message containing the action feedback. + * @param [in] dyn_type DynamicType containing the type information required. + */ + void write_action_feedback_nts_( + const Message& msg, + const fastdds::dds::DynamicType::_ref_type& dyn_type, + const std::string& action_name); + + /** + * @brief Write the action goal reply to user's app. + * + * @param [in] msg Message containing the action goal reply. + * @param [in] dyn_type DynamicType containing the type information required. + * @param [in] action_id UUID of the action. + */ + void write_action_goal_reply_nts_( + const Message& msg, + const fastdds::dds::DynamicType::_ref_type& dyn_type, + const UUID& action_id, + const std::string& action_name); + + /** + * @brief Write the action cancel reply to user's app. + * + * @param [in] msg Message containing the action cancel reply. + * @param [in] dyn_type DynamicType containing the type information required. + * @param [in] request_id Request ID of the action cancel reply. + */ + void write_action_cancel_reply_nts_( + const Message& msg, + const fastdds::dds::DynamicType::_ref_type& dyn_type, + const uint64_t request_id, + const std::string& action_name); + + /** + * @brief Write the action status to user's app. + * + * @param [in] msg Message containing the action status. + * @param [in] dyn_type DynamicType containing the type information required. + */ + void write_action_status_nts_( + const Message& msg, + const fastdds::dds::DynamicType::_ref_type& dyn_type, + const std::string& action_name); + + /** + * @brief Write the action (goal or cancel) request to user's app. + * + * @param [in] msg Message containing the action request. + * @param [in] dyn_type DynamicType containing the type information required. + * @param [in] request_id Request ID of the action request. + */ + void write_action_request_nts_( + const Message& msg, + const fastdds::dds::DynamicType::_ref_type& dyn_type, + const uint64_t request_id, + const std::string& action_name, + const ActionType action_type); + /** * @brief Register a type using the given serialized type data. * @@ -282,10 +645,20 @@ class Handler : public ddspipe::participants::ISchemaHandler unsigned int unique_sequence_number_{0}; //! Mutex synchronizing access to object's data structures - std::mutex mtx_; + std::recursive_mutex mtx_; //! Callback to request types from the user DdsTypeQuery type_query_callback_; + + //! Identifier for the received and sent requests + uint64_t requests_id_{0}; + + //! Map of all ActionRequestInfos to their UUIDs + std::unordered_map action_request_id_to_uuid_; + + //! Lambda to send action get result reply + std::function send_action_get_result_reply_callback_; }; } /* namespace participants */ diff --git a/ddsenabler_participants/include/ddsenabler_participants/InternalRpcReader.hpp b/ddsenabler_participants/include/ddsenabler_participants/InternalRpcReader.hpp new file mode 100644 index 00000000..ed0be3d9 --- /dev/null +++ b/ddsenabler_participants/include/ddsenabler_participants/InternalRpcReader.hpp @@ -0,0 +1,104 @@ +// Copyright 2025 Proyectos y Sistemas de Mantenimiento SL (eProsima). +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/** + * @file InternalRpcReader.hpp + */ + +#pragma once + +#include +#include + +#include + +#include +#include + +#include +#include + +namespace eprosima { +namespace ddsenabler { +namespace participants { + +/** + * Reader implementation that allows to introduce custom data to DDS Pipe. + */ +class InternalRpcReader : public ddspipe::participants::InternalReader +{ +public: + + DDSENABLER_PARTICIPANTS_DllAPI + InternalRpcReader( + const ddspipe::core::types::ParticipantId& participant_id, + const ddspipe::core::types::DdsTopic& topic) + : topic_(topic) + , ddspipe::participants::InternalReader(participant_id) + { + + guid_ = ddspipe::core::types::Guid::new_unique_guid(); + } + + ~InternalRpcReader() = default; + + DDSENABLER_PARTICIPANTS_DllAPI + ddspipe::core::types::Guid guid() const override + { + return guid_; + } + + DDSENABLER_PARTICIPANTS_DllAPI + fastdds::RecursiveTimedMutex& get_rtps_mutex() const override + { + return rtps_mutex_; + } + + DDSENABLER_PARTICIPANTS_DllAPI + uint64_t get_unread_count() const override + { + if (unread_count_ == 0) + { + return 0; + } + + return unread_count_--; + } + + DDSENABLER_PARTICIPANTS_DllAPI + void simulate_data_reception( + std::unique_ptr&& data) noexcept + { + std::lock_guard lock(rtps_mutex_); + unread_count_++; + ddspipe::participants::InternalReader::simulate_data_reception(std::move(data)); + } + + DDSENABLER_PARTICIPANTS_DllAPI + ddspipe::core::types::DdsTopic topic() const override + { + return topic_; + } + +private: + + ddspipe::core::types::Guid guid_; + mutable fastdds::RecursiveTimedMutex rtps_mutex_; + mutable uint64_t unread_count_ = 0; + ddspipe::core::types::DdsTopic topic_; +}; + +} /* namespace participants */ +} /* namespace ddsenabler */ +} /* namespace eprosima */ diff --git a/ddsenabler_participants/include/ddsenabler_participants/Writer.hpp b/ddsenabler_participants/include/ddsenabler_participants/Writer.hpp index 81d097a5..8f14eac7 100644 --- a/ddsenabler_participants/include/ddsenabler_participants/Writer.hpp +++ b/ddsenabler_participants/include/ddsenabler_participants/Writer.hpp @@ -20,14 +20,19 @@ #include +#include + #include #include #include #include +#include #include #include +#include +#include namespace eprosima { namespace ddsenabler { @@ -70,6 +75,69 @@ class Writer topic_notification_callback_ = callback; } + DDSENABLER_PARTICIPANTS_DllAPI + void set_service_notification_callback( + ServiceNotification callback) + { + service_notification_callback_ = callback; + } + + DDSENABLER_PARTICIPANTS_DllAPI + void set_service_reply_notification_callback( + ServiceReplyNotification callback) + { + service_reply_notification_callback_ = callback; + } + + DDSENABLER_PARTICIPANTS_DllAPI + void set_service_request_notification_callback( + ServiceRequestNotification callback) + { + service_request_notification_callback_ = callback; + } + + DDSENABLER_PARTICIPANTS_DllAPI + void set_action_notification_callback( + ActionNotification callback) + { + action_notification_callback_ = callback; + } + + DDSENABLER_PARTICIPANTS_DllAPI + void set_action_result_notification_callback( + ActionResultNotification callback) + { + action_result_notification_callback_ = callback; + } + + DDSENABLER_PARTICIPANTS_DllAPI + void set_action_feedback_notification_callback( + ActionFeedbackNotification callback) + { + action_feedback_notification_callback_ = callback; + } + + DDSENABLER_PARTICIPANTS_DllAPI + void set_action_status_notification_callback( + ActionStatusNotification callback) + { + action_status_notification_callback_ = callback; + } + + DDSENABLER_PARTICIPANTS_DllAPI + void set_action_goal_request_notification_callback( + ActionGoalRequestNotification callback) + { + action_goal_request_notification_callback_ = callback; + } + + DDSENABLER_PARTICIPANTS_DllAPI + void set_action_cancel_request_notification_callback( + ActionCancelRequestNotification callback) + { + action_cancel_request_notification_callback_ = callback; + } + /** * @brief Writes the schema of a DynamicType to user's app. * @@ -101,6 +169,102 @@ class Writer const Message& msg, const fastdds::dds::DynamicType::_ref_type& dyn_type); + DDSENABLER_PARTICIPANTS_DllAPI + void write_service_notification( + const ddspipe::core::types::RpcTopic& service); + + DDSENABLER_PARTICIPANTS_DllAPI + void write_service_reply_notification( + const Message& msg, + const fastdds::dds::DynamicType::_ref_type& dyn_type, + const uint64_t request_id, + const std::string& service_name); + + DDSENABLER_PARTICIPANTS_DllAPI + void write_service_request_notification( + const Message& msg, + const fastdds::dds::DynamicType::_ref_type& dyn_type, + const uint64_t request_id, + const std::string& service_name); + + DDSENABLER_PARTICIPANTS_DllAPI + void write_action_notification( + const RpcAction& action); + + DDSENABLER_PARTICIPANTS_DllAPI + void write_action_result_notification( + const Message& msg, + const fastdds::dds::DynamicType::_ref_type& dyn_type, + const UUID& action_id, + const std::string& action_name); + + DDSENABLER_PARTICIPANTS_DllAPI + void write_action_feedback_notification( + const Message& msg, + const fastdds::dds::DynamicType::_ref_type& dyn_type, + const std::string& action_name); + + DDSENABLER_PARTICIPANTS_DllAPI + void write_action_goal_reply_notification( + const Message& msg, + const fastdds::dds::DynamicType::_ref_type& dyn_type, + const UUID& action_id, + const std::string& action_name); + + DDSENABLER_PARTICIPANTS_DllAPI + void write_action_cancel_reply_notification( + const Message& msg, + const fastdds::dds::DynamicType::_ref_type& dyn_type, + const uint64_t request_id, + const std::string& action_name); + + DDSENABLER_PARTICIPANTS_DllAPI + void write_action_status_notification( + const Message& msg, + const fastdds::dds::DynamicType::_ref_type& dyn_type, + const std::string& action_name); + + DDSENABLER_PARTICIPANTS_DllAPI + void write_action_request_notification( + const Message& msg, + const fastdds::dds::DynamicType::_ref_type& dyn_type, + const uint64_t request_id, + const std::string& action_name, + const ActionType action_type); + + DDSENABLER_PARTICIPANTS_DllAPI + void set_is_UUID_active_callback( + std::function callback) + { + is_UUID_active_callback_ = callback; + } + + DDSENABLER_PARTICIPANTS_DllAPI + void set_erase_action_UUID_callback( + std::function callback) + { + erase_action_UUID_callback_ = callback; + } + + DDSENABLER_PARTICIPANTS_DllAPI + void set_send_action_get_result_request_callback( + std::function callback) + { + send_action_get_result_request_callback_ = callback; + } + + DDSENABLER_PARTICIPANTS_DllAPI + void set_send_action_send_goal_reply_callback( + std::function callback) + { + send_action_send_goal_reply_callback_ = callback; + } + + bool uuid_from_request_json( + const Message& msg, + const fastdds::dds::DynamicType::_ref_type& dyn_type, + UUID& uuid); + protected: /** @@ -123,13 +287,35 @@ class Writer fastdds::dds::DynamicPubSubType get_pubsub_type_( const fastdds::dds::DynamicType::_ref_type& dyn_type) noexcept; + bool prepare_json_data_( + const Message& msg, + const fastdds::dds::DynamicType::_ref_type& dyn_type, + nlohmann::json& json_output); + // Callbacks to notify the user's app DdsDataNotification data_notification_callback_; DdsTypeNotification type_notification_callback_; DdsTopicNotification topic_notification_callback_; + ServiceNotification service_notification_callback_; + ServiceReplyNotification service_reply_notification_callback_; + ServiceRequestNotification service_request_notification_callback_; + ActionNotification action_notification_callback_; + ActionResultNotification action_result_notification_callback_; + ActionFeedbackNotification action_feedback_notification_callback_; + ActionStatusNotification action_status_notification_callback_; + ActionGoalRequestNotification action_goal_request_notification_callback_; + ActionCancelRequestNotification action_cancel_request_notification_callback_; + + // Map to store the pubsub types associated to dynamic types so they can be reused std::map dynamic_pubsub_types_; + + std::function is_UUID_active_callback_; + std::function erase_action_UUID_callback_; + std::function send_action_get_result_request_callback_; + std::function send_action_send_goal_reply_callback_; + }; } /* namespace participants */ diff --git a/ddsenabler_participants/include/ddsenabler_participants/rpc/RpcStructs.hpp b/ddsenabler_participants/include/ddsenabler_participants/rpc/RpcStructs.hpp new file mode 100644 index 00000000..c98f7e1f --- /dev/null +++ b/ddsenabler_participants/include/ddsenabler_participants/rpc/RpcStructs.hpp @@ -0,0 +1,185 @@ +// Copyright 2025 Proyectos y Sistemas de Mantenimiento SL (eProsima). +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/** + * @file RpcStructs.hpp + */ + +#pragma once + + +#include +#include + +#include + +#include + +namespace eprosima { +namespace ddsenabler { +namespace participants { + +enum class ActionEraseReason +{ + FINAL_STATUS, // Erase triggered by receiving final status (e.g., succeeded, aborted, canceled) + RESULT, // Erase triggered by receiving result + FORCED // Force erase regardless of status/result +}; + +struct RpcAction +{ + RpcAction() = default; + RpcAction( + const std::string& action_name, + const ddspipe::core::types::RpcTopic& goal, + const ddspipe::core::types::RpcTopic& result, + const ddspipe::core::types::RpcTopic& cancel, + const ddspipe::core::types::DdsTopic& feedback, + const ddspipe::core::types::DdsTopic& status); + + std::string action_name; + ddspipe::core::types::RpcTopic goal; + ddspipe::core::types::RpcTopic result; + ddspipe::core::types::RpcTopic cancel; + ddspipe::core::types::DdsTopic feedback; + ddspipe::core::types::DdsTopic status; +}; + +struct RpcInfo +{ + RpcInfo( + const std::string& dds_topic_name); + + void detect_protocol(); + + void extract_rpc_info(); + + std::string topic_name; + std::string service_name; + std::string action_name; + Protocol protocol; + RpcType rpc_type; + ServiceType service_type; + ActionType action_type; +}; + +struct ActionRequestInfo +{ + ActionRequestInfo() = default; + + ActionRequestInfo( + const std::string& _action_name, + ActionType action_type, + uint64_t request_id, + Protocol protocol); + + void set_request( + uint64_t request_id, + ActionType action_type); + + uint64_t get_request( + ActionType action_type) const; + + bool set_result( + const std::string&& str); + + bool erase( + ActionEraseReason erase_reason); + + Protocol get_protocol() const; + + std::string action_name; + Protocol protocol; + uint64_t goal_request_id = 0; + uint64_t result_request_id = 0; + std::chrono::system_clock::time_point goal_accepted_stamp; + std::string result; + bool result_received = false; // Indicates if the result has been received + bool final_status_received = false; // Indicates if the final status has been received +}; + +struct ServiceDiscovered +{ + + ServiceDiscovered( + const std::string& service_name, + Protocol protocol); + + bool add_topic( + const ddspipe::core::types::DdsTopic& topic, + ServiceType service_type); + + bool remove_topic( + ServiceType service_type); + + ddspipe::core::types::RpcTopic get_service(); + + bool get_topic( + ServiceType service_type, + ddspipe::core::types::DdsTopic& topic); + + Protocol get_protocol() const; + + std::string service_name; + Protocol protocol{Protocol::PROTOCOL_UNKNOWN}; + + ddspipe::core::types::DdsTopic topic_request; + bool request_discovered{false}; + + ddspipe::core::types::DdsTopic topic_reply; + bool reply_discovered{false}; + + std::optional rpc_topic; + bool fully_discovered{false}; + + std::optional endpoint_request; + bool enabler_as_server{false}; + bool external_server{false}; +}; + +struct ActionDiscovered +{ + ActionDiscovered( + const std::string& action_name, + Protocol protocol); + + bool check_fully_discovered(); + + bool add_service( + std::shared_ptr service, + ActionType action_type); + + bool add_topic( + const ddspipe::core::types::DdsTopic& topic, + ActionType action_type); + + RpcAction get_action(); + + std::string action_name; + Protocol protocol{Protocol::PROTOCOL_UNKNOWN}; + std::weak_ptr goal; + std::weak_ptr result; + std::weak_ptr cancel; + ddspipe::core::types::DdsTopic feedback; + bool feedback_discovered{false}; + ddspipe::core::types::DdsTopic status; + bool status_discovered{false}; + bool fully_discovered{false}; + bool enabler_as_server{false}; + bool external_server{false}; +}; + +} // namespace participants +} // namespace ddsenabler +} // namespace eprosima diff --git a/ddsenabler_participants/include/ddsenabler_participants/rpc/RpcTypes.hpp b/ddsenabler_participants/include/ddsenabler_participants/rpc/RpcTypes.hpp new file mode 100644 index 00000000..85c5153a --- /dev/null +++ b/ddsenabler_participants/include/ddsenabler_participants/rpc/RpcTypes.hpp @@ -0,0 +1,90 @@ +// Copyright 2025 Proyectos y Sistemas de Mantenimiento SL (eProsima). +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/** + * @file RpcTypes.hpp + */ + +#include +#include +#include + +#include +#include + +#pragma once + +namespace eprosima { +namespace ddsenabler { +namespace participants { + +using UUID = std::array; + +enum class Protocol +{ + ROS2 = 0, + DDS, + PROTOCOL_UNKNOWN +}; + +enum class RpcType +{ + NONE = 0, + SERVICE, + ACTION +}; + +enum class ServiceType +{ + NONE = 0, + REQUEST, + REPLY +}; + +enum class ActionType +{ + NONE = 0, + GOAL, + RESULT, + CANCEL, + FEEDBACK, + STATUS +}; + +enum class StatusCode +{ + UNKNOWN = 0, + ACCEPTED, + EXECUTING, + CANCELING, + SUCCEEDED, + CANCELED, + ABORTED, + REJECTED, + TIMEOUT, + FAILED, + CANCEL_REQUEST_FAILED +}; + +enum class CancelCode +{ + NONE = 0, + REJECTED, + UNKNOWN_GOAL_ID, + GOAL_TERMINATED +}; + +} /* namespace participants */ +} /* namespace ddsenabler */ +} /* namespace eprosima */ diff --git a/ddsenabler_participants/include/ddsenabler_participants/rpc/RpcUtils.hpp b/ddsenabler_participants/include/ddsenabler_participants/rpc/RpcUtils.hpp new file mode 100644 index 00000000..44281312 --- /dev/null +++ b/ddsenabler_participants/include/ddsenabler_participants/rpc/RpcUtils.hpp @@ -0,0 +1,145 @@ +// Copyright 2025 Proyectos y Sistemas de Mantenimiento SL (eProsima). +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/** + * @file RpcUtils.hpp + */ + +#pragma once + +#include + +#include +#include +#include +#include + +#include + +namespace eprosima { +namespace ddsenabler { +namespace participants { +namespace RpcUtils { + +/** + * @brief Generates a UUID. + * + * @return A new UUID. + */ +DDSENABLER_PARTICIPANTS_DllAPI +UUID generate_UUID(); + +// ROS 2 ACTION MSGS +/** + * @brief Creates a JSON string for sending a goal request. + * + * @param goal_json The JSON string representing the goal (without the UUID part). + * @return The JSON string for sending the goal request. + */ +DDSENABLER_PARTICIPANTS_DllAPI +std::string create_goal_request_msg( + const std::string& goal_json, + UUID& goal_id); + +/** + * @brief Creates a JSON string for sending a goal reply. + * + * @param accepted Indicates whether the goal was accepted or not. + * @return A JSON string representing the goal reply. + */ +DDSENABLER_PARTICIPANTS_DllAPI +std::string create_goal_reply_msg( + bool accepted); + +/** + * @brief Creates a cancel message for an action. + * + * @param goal_id The UUID of the goal to cancel. + * @param timestamp The timestamp for canceling logic. + * @return A JSON string representing the cancel message. + */ +DDSENABLER_PARTICIPANTS_DllAPI +std::string create_cancel_request_msg( + const UUID& goal_id, + const int64_t timestamp); + +/** + * @brief Creates a cancel reply message for an action. + * + * @param cancelling_goals A vector of pairs containing the UUIDs of the goals to cancel and their timestamps. + * @param cancel_code The cancel code indicating the result of the cancellation. + * @return A JSON string representing the cancel reply message. + */ +DDSENABLER_PARTICIPANTS_DllAPI +std::string create_cancel_reply_msg( + std::vector> cancelling_goals, + const CancelCode& cancel_code); + +/** + * @brief Creates a result request message for an action. + * + * @param goal_id The UUID of the goal for which the result is being requested. + * @return A JSON string representing the result request message. + */ +DDSENABLER_PARTICIPANTS_DllAPI +std::string create_result_request_msg( + const UUID& goal_id); + +/** + * @brief Creates a result reply message for an action. + * + * @param status_code The status code of the action result. + * @param json The JSON string representing the result. + * @return A JSON string representing the result reply message. + */ +DDSENABLER_PARTICIPANTS_DllAPI +std::string create_result_reply_msg( + const StatusCode& status_code, + const char* json); + +/** + * @brief Creates a feedback message for an action. + * + * @param json The JSON string representing the feedback. + * @param goal_id The UUID of the goal for which the feedback is being sent. + * @return A JSON string representing the feedback message. + */ +DDSENABLER_PARTICIPANTS_DllAPI +std::string create_feedback_msg( + const char* json, + const UUID& goal_id); + +/** + * @brief Creates a status message for an action. + * + * @param goal_id The UUID of the goal. + * @param status_code The status code of the action. + * @param goal_accepted_stamp The timestamp when the goal was accepted. + * @return A JSON string representing the status message. + */ +DDSENABLER_PARTICIPANTS_DllAPI +std::string create_status_msg( + const UUID& goal_id, + const StatusCode& status_code, + std::chrono::system_clock::time_point goal_accepted_stamp); + +} // namespace RpcUtils +} // namespace participants +} // namespace ddsenabler +} // namespace eprosima + +DDSENABLER_PARTICIPANTS_DllAPI +std::ostream& operator <<( + std::ostream& os, + const eprosima::ddsenabler::participants::UUID& uuid); diff --git a/ddsenabler_participants/src/cpp/EnablerParticipant.cpp b/ddsenabler_participants/src/cpp/EnablerParticipant.cpp index a649a6b1..fc8f2930 100644 --- a/ddsenabler_participants/src/cpp/EnablerParticipant.cpp +++ b/ddsenabler_participants/src/cpp/EnablerParticipant.cpp @@ -17,6 +17,7 @@ */ #include +#include #include #include #include @@ -42,6 +43,7 @@ EnablerParticipant::EnablerParticipant( std::shared_ptr schema_handler) : ddspipe::participants::SchemaParticipant(participant_configuration, payload_pool, discovery_database, schema_handler) + , handler_(std::static_pointer_cast(schema_handler_)) { } @@ -56,14 +58,69 @@ std::shared_ptr EnablerParticipant::create_reader( std::shared_ptr reader; { std::lock_guard lck(mtx_); - reader = std::make_shared(id()); auto dds_topic = dynamic_cast(topic); - readers_[dds_topic] = reader; - // Only notify the discovery of topics that do not originate from a topic query callback - if (dds_topic.topic_discoverer() != this->id()) + std::shared_ptr rpc_info = std::make_shared(dds_topic.m_topic_name); + + if (RpcType::NONE != rpc_info->rpc_type) + { + if (ServiceType::NONE != rpc_info->service_type) + { + reader = std::make_shared(id(), dds_topic); + } + else + { + reader = std::make_shared(id()); + } + + // Only notify the discovery of topics that do not originate from a topic query callback + if (dds_topic.topic_discoverer() != this->id()) + { + if (RpcType::SERVICE == rpc_info->rpc_type) + { + if (service_discovered_nts_(rpc_info, dds_topic)) + { + try + { + RpcTopic service = services_.find(rpc_info->service_name)->second->get_service(); + handler_->add_service(service); + } + catch (const std::exception& e) + { + EPROSIMA_LOG_ERROR(DDSENABLER_ENABLER_PARTICIPANT, + "Failed to add service " << rpc_info->service_name << ": " << e.what()); + return std::make_shared(); + } + } + } + else if (RpcType::ACTION == rpc_info->rpc_type) + { + if (action_discovered_nts_(rpc_info, dds_topic)) + { + try + { + auto action = actions_.find(rpc_info->action_name)->second->get_action(); + handler_->add_action(action); + } + catch (const std::exception& e) + { + EPROSIMA_LOG_ERROR(DDSENABLER_ENABLER_PARTICIPANT, + "Failed to add action " << rpc_info->action_name << ": " << e.what()); + return std::make_shared(); + } + } + } + } + } + else { - std::static_pointer_cast(schema_handler_)->add_topic(dds_topic); + reader = std::make_shared(id()); + // Only notify the discovery of topics that do not originate from a topic query callback + if (dds_topic.topic_discoverer() != this->id()) + { + handler_->add_topic(dds_topic); + } } + readers_[dds_topic] = reader; } cv_.notify_all(); return reader; @@ -83,99 +140,1035 @@ bool EnablerParticipant::publish( std::unique_lock lck(mtx_); std::string type_name; - auto reader = lookup_reader_nts_(topic_name, type_name); + auto i_reader = lookup_reader_nts_(topic_name, type_name); - if (nullptr == reader) + if (nullptr == i_reader) { - if (!topic_query_callback_) + DdsTopic topic; + if (!query_topic_nts_(topic_name, topic)) { EPROSIMA_LOG_ERROR(DDSENABLER_ENABLER_PARTICIPANT, - "Failed to publish data in topic " << topic_name << - " : topic is unknown and topic query callback not set."); + "Failed to publish data in topic " << topic_name); return false; } - std::string serialized_qos; - if (!topic_query_callback_(topic_name.c_str(), type_name, serialized_qos)) + if (!create_topic_writer_nts_(topic, i_reader, lck)) { EPROSIMA_LOG_ERROR(DDSENABLER_ENABLER_PARTICIPANT, - "Failed to publish data in topic " << topic_name << " : topic query callback failed."); + "Failed to publish data in topic " << topic_name << " : writer creation failed."); return false; } - // Deserialize QoS if provided by the user (otherwise use default one) - TopicQoS qos; - if (!serialized_qos.empty()) + type_name = topic.type_name; + + // (Optionally) wait for writer created in DDS participant to match with external readers, to avoid losing this + // message when not using transient durability + std::this_thread::sleep_for(std::chrono::milliseconds(std::static_pointer_cast( + configuration_)->initial_publish_wait)); + } + + auto reader = std::dynamic_pointer_cast(i_reader); + + auto data = std::make_unique(); + + Payload payload; + if (!handler_->get_serialized_data(type_name, json, payload)) + { + EPROSIMA_LOG_ERROR(DDSENABLER_ENABLER_PARTICIPANT, + "Failed to publish data in topic " << topic_name << " : data serialization failed."); + return false; + } + + if (!payload_pool_->get_payload(payload, data->payload)) + { + EPROSIMA_LOG_ERROR(DDSENABLER_ENABLER_PARTICIPANT, + "Failed to publish data in topic " << topic_name << " : get_payload failed."); + return false; + } + + reader->simulate_data_reception(std::move(data)); + return true; +} + +bool EnablerParticipant::publish_rpc( + const std::string& topic_name, + const std::string& json, + const uint64_t request_id) +{ + std::unique_lock lck(mtx_); + + std::shared_ptr rpc_info = std::make_shared(topic_name); + + if (ServiceType::NONE == rpc_info->service_type) + { + EPROSIMA_LOG_ERROR(DDSENABLER_ENABLER_PARTICIPANT, + "Failed to publish data in topic " << topic_name << " : not a service topic."); + return false; + } + + auto it = services_.find(rpc_info->service_name); + if (it == services_.end()) + { + // There is no case where none of the service topics are discovered and yet the publish should be done + EPROSIMA_LOG_ERROR(DDSENABLER_ENABLER_PARTICIPANT, + "Failed to publish data in service " << rpc_info->service_name << " : service does not exist."); + return false; + } + + if (!it->second->external_server && rpc_info->service_type == ServiceType::REQUEST) + { + EPROSIMA_LOG_ERROR(DDSENABLER_ENABLER_PARTICIPANT, + "Failed to publish data in service " << rpc_info->service_name << + " : service is only announced on the enabler side."); + return false; + } + + std::string type_name; + auto reader = std::dynamic_pointer_cast(lookup_reader_nts_(topic_name, type_name)); + + if (nullptr == reader) + { + EPROSIMA_LOG_ERROR(DDSENABLER_ENABLER_PARTICIPANT, + "Failed to publish data in service " << rpc_info->service_name << " : service does not exist."); + return false; + } + + DdsTopic topic; + if (!it->second->get_topic(rpc_info->service_type, topic)) + { + EPROSIMA_LOG_ERROR(DDSENABLER_ENABLER_PARTICIPANT, + "Failed to publish data in service " << rpc_info->service_name << " : topic not found."); + return false; + } + + if (type_name != topic.type_name) + { + EPROSIMA_LOG_ERROR(DDSENABLER_ENABLER_PARTICIPANT, + "Failed to publish data in topic " << topic.m_topic_name << " : type name mismatch."); + return false; + } + + auto data = std::make_unique(); + + Payload payload; + if (!handler_->get_serialized_data(type_name, json, payload)) + { + EPROSIMA_LOG_ERROR(DDSENABLER_ENABLER_PARTICIPANT, + "Failed to publish data in topic " << topic.m_topic_name << " : data serialization failed."); + return false; + } + + if (!payload_pool_->get_payload(payload, data->payload)) + { + EPROSIMA_LOG_ERROR(DDSENABLER_ENABLER_PARTICIPANT, + "Failed to publish data in topic " << topic.m_topic_name << " : get_payload failed."); + return false; + } + + data->origin_sequence_number = eprosima::fastdds::rtps::SequenceNumber_t(request_id); + data->sent_sequence_number = eprosima::fastdds::rtps::SequenceNumber_t(request_id); + data->participant_receiver = id(); + + fastdds::rtps::SampleIdentity sample_identity; + sample_identity.sequence_number(fastdds::rtps::SequenceNumber_t(request_id)); + sample_identity.writer_guid(reader->guid()); + data->write_params.get_reference().sample_identity(sample_identity); + data->write_params.get_reference().related_sample_identity(sample_identity); + + reader->simulate_data_reception(std::move(data)); + return true; +} + +bool EnablerParticipant::announce_service( + const std::string& service_name, + Protocol Protocol) +{ + std::unique_lock lck(mtx_); + + auto it = services_.find(service_name); + if (it != services_.end()) + { + if (it->second->enabler_as_server) { - try - { - qos = serialization::deserialize_qos(serialized_qos); - } - catch (const std::exception& e) + EPROSIMA_LOG_ERROR(DDSENABLER_ENABLER_PARTICIPANT, + "Failed to announce service " << service_name << " : service already announced."); + return false; + } + it->second->enabler_as_server = true; + return true; + } + + std::shared_ptr service = std::make_shared(service_name, Protocol); + if (!query_service_nts_(service, Protocol)) + { + return false; + } + + if (create_service_request_writer_nts_(service, lck)) + { + services_.insert_or_assign(service_name, service); + return true; + } + + EPROSIMA_LOG_ERROR(DDSENABLER_ENABLER_PARTICIPANT, + "Failed to announce service " << service_name << " : service writer creation failed."); + return false; +} + +// TODO after revoking the service the client is still matched +bool EnablerParticipant::revoke_service( + const std::string& service_name) +{ + std::unique_lock lck(mtx_); + + return revoke_service_nts_(service_name); +} + +bool EnablerParticipant::send_service_request( + const std::string& service_name, + const std::string& json, + uint64_t& request_id, + Protocol Protocol) +{ + std::string prefix, suffix; + switch (Protocol) + { + case Protocol::ROS2: + prefix = ROS_REQUEST_PREFIX; + suffix = ROS_REQUEST_SUFFIX; + break; + case Protocol::DDS: + prefix = DDS_REQUEST_PREFIX; + suffix = DDS_REQUEST_SUFFIX; + break; + default: + EPROSIMA_LOG_ERROR(DDSENABLER_EXECUTION, + "Failed to send service request to service " << service_name << ": unknown RPC protocol."); + return false; + } + + request_id = handler_->get_new_request_id(); + if (!publish_rpc( + prefix + service_name + suffix, + json, + request_id)) + { + EPROSIMA_LOG_ERROR(DDSENABLER_EXECUTION, + "Failed to send service request to service " << service_name); + return false; + } + + return true; +} + +bool EnablerParticipant::send_service_reply( + const std::string& service_name, + const std::string& json, + const uint64_t request_id) +{ + Protocol Protocol = get_service_protocol(service_name); + std::string prefix, suffix; + switch (Protocol) + { + case Protocol::ROS2: + prefix = ROS_REPLY_PREFIX; + suffix = ROS_REPLY_SUFFIX; + break; + case Protocol::DDS: + prefix = DDS_REPLY_PREFIX; + suffix = DDS_REPLY_SUFFIX; + break; + default: + EPROSIMA_LOG_ERROR(DDSENABLER_EXECUTION, + "Failed to send service reply to unknown service " << service_name); + return false; + } + + return publish_rpc( + prefix + service_name + suffix, + json, + request_id); +} + +Protocol EnablerParticipant::get_service_protocol( + const std::string& service_name) +{ + std::unique_lock lck(mtx_); + auto it = services_.find(service_name); + if (it != services_.end()) + { + return it->second->get_protocol(); + } + + return Protocol::PROTOCOL_UNKNOWN; +} + +bool EnablerParticipant::announce_action( + const std::string& action_name, + Protocol Protocol) +{ + std::unique_lock lck(mtx_); + + if (Protocol != Protocol::ROS2) + { + EPROSIMA_LOG_ERROR(DDSENABLER_ENABLER_PARTICIPANT, + "Failed to announce action " << action_name << " : only ROS2 actions are currently supported."); + return false; + } + + { + auto it = actions_.find(action_name); + if (it != actions_.end()) + { + if (it->second->enabler_as_server) { EPROSIMA_LOG_ERROR(DDSENABLER_ENABLER_PARTICIPANT, - "Failed to deserialize QoS for topic " << topic_name << ": " << e.what()); + "Failed to announce action " << action_name << " : action already announced."); return false; } + // Erase the action, to allow re-announcing it + auto goal = it->second->goal.lock(); + auto result = it->second->result.lock(); + auto cancel = it->second->cancel.lock(); + if (goal && result && cancel) + { + it->second->enabler_as_server = true; + goal->enabler_as_server = true; + result->enabler_as_server = true; + cancel->enabler_as_server = true; + return true; + } + actions_.erase(it); } + } + + std::shared_ptr action = std::make_shared(action_name, Protocol); + if (!query_action_nts_(*action, Protocol, lck)) + { + EPROSIMA_LOG_ERROR(DDSENABLER_ENABLER_PARTICIPANT, + "Failed to announce action " << action_name << " : action type request failed."); + return false; + } + + actions_.insert_or_assign(action_name, action); + return true; +} + +bool EnablerParticipant::revoke_action( + const std::string& action_name) +{ + std::unique_lock lck(mtx_); + + auto it = actions_.find(action_name); + if (it == actions_.end()) + { + EPROSIMA_LOG_ERROR(DDSENABLER_ENABLER_PARTICIPANT, + "Failed to revoke action " << action_name << " : action not found."); + return false; + } + + if (!it->second->enabler_as_server) + { + EPROSIMA_LOG_ERROR(DDSENABLER_ENABLER_PARTICIPANT, + "Failed to revoke action " << action_name << " : action not announced as server."); + return false; + } + + if (it->second->external_server) + { + it->second->enabler_as_server = false; + return true; + } + + auto action = it->second; - fastdds::dds::xtypes::TypeIdentifier type_identifier; - if (!std::static_pointer_cast(schema_handler_)->get_type_identifier(type_name, type_identifier)) + auto goal = action->goal.lock(); + auto result = action->result.lock(); + auto cancel = action->cancel.lock(); + if (!goal || !result || !cancel) + { + EPROSIMA_LOG_ERROR(DDSENABLER_ENABLER_PARTICIPANT, + "Failed to revoke action " << action_name << " : action services not fully discovered."); + return false; + } + if (this->revoke_service_nts_(goal->service_name) && + this->revoke_service_nts_(result->service_name) && + this->revoke_service_nts_(cancel->service_name)) + { + action->enabler_as_server = false; + action->fully_discovered = false; + return true; + } + + EPROSIMA_LOG_ERROR(DDSENABLER_ENABLER_PARTICIPANT, + "Failed to revoke action " << action_name << " : error revoking action services."); + return false; +} + +bool EnablerParticipant::send_action_goal( + const std::string& action_name, + const std::string& json, + UUID& action_id, + Protocol Protocol) +{ + std::string goal_json = RpcUtils::create_goal_request_msg(json, action_id); + std::string goal_request_topic = action_name + ACTION_GOAL_SUFFIX; + uint64_t goal_request_id = 0; + + if (!send_service_request( + goal_request_topic, + goal_json, + goal_request_id, + Protocol)) + { + EPROSIMA_LOG_ERROR(DDSENABLER_EXECUTION, + "Failed to send action goal request to action " << action_name); + return false; + } + + if (!handler_->store_action_request( + action_name, + action_id, + goal_request_id, + ActionType::GOAL, + Protocol)) + { + EPROSIMA_LOG_ERROR(DDSENABLER_EXECUTION, + "Failed to store action goal request to action " << action_name); + handler_->erase_action_UUID(action_id, ActionEraseReason::FORCED); + return false; + } + + return true; +} + +bool EnablerParticipant::cancel_action_goal( + const std::string& action_name, + const UUID& goal_id, + const int64_t timestamp) +{ + if (goal_id != UUID() && + !handler_->is_UUID_active(action_name, goal_id)) + { + EPROSIMA_LOG_ERROR(DDSENABLER_EXECUTION, + "Failed to cancel action goal for action " << action_name + << ": goal id not found."); + return false; + } + + std::string cancel_json = RpcUtils::create_cancel_request_msg(goal_id, timestamp); + + Protocol protocol = handler_->get_action_protocol(action_name, goal_id); + + uint64_t cancel_request_id = 0; + std::string cancel_request_topic = action_name + ACTION_CANCEL_SUFFIX; + + if (send_service_request( + cancel_request_topic, + cancel_json, + cancel_request_id, + protocol)) + { + return true; + } + + EPROSIMA_LOG_ERROR(DDSENABLER_EXECUTION, + "Failed to send action cancel goal to action " << action_name); + return false; +} + +bool EnablerParticipant::send_action_get_result_request( + const std::string& action_name, + const UUID& action_id) +{ + std::string json = RpcUtils::create_result_request_msg(action_id); + + std::string get_result_request_topic = action_name + ACTION_RESULT_SUFFIX; + uint64_t get_result_request_id = 0; + + Protocol protocol = handler_->get_action_protocol(action_name, action_id); + + if (!send_service_request( + get_result_request_topic, + json, + get_result_request_id, + protocol)) + { + EPROSIMA_LOG_ERROR(DDSENABLER_EXECUTION, + "Failed to send action get result request to action " << action_name); + return false; + } + + if (!handler_->store_action_request( + action_name, + action_id, + get_result_request_id, + ActionType::RESULT)) + { + EPROSIMA_LOG_ERROR(DDSENABLER_EXECUTION, + "Failed to store action get result request to action " << action_name + << ": cancelling."); + cancel_action_goal(action_name, action_id, 0); + return false; + } + + return true; +} + +void EnablerParticipant::send_action_send_goal_reply( + const std::string& action_name, + const uint64_t goal_id, + bool accepted) +{ + std::string reply_json = RpcUtils::create_goal_reply_msg(accepted); + + if (!send_service_reply( + action_name + ACTION_GOAL_SUFFIX, + reply_json, + goal_id)) + { + EPROSIMA_LOG_ERROR(DDSENABLER_EXECUTION, + "Failed to send action goal reply to action " << action_name + << ": goal id not found."); + } + return; +} + +bool EnablerParticipant::send_action_cancel_goal_reply( + const char* action_name, + const std::vector& goal_ids, + const CancelCode& cancel_code, + const uint64_t request_id) +{ + std::vector> cancelling_goals; + for (const auto& goal_id : goal_ids) + { + std::chrono::system_clock::time_point timestamp; + if (!handler_->is_UUID_active(action_name, goal_id, ×tamp)) { - EPROSIMA_LOG_ERROR(DDSENABLER_ENABLER_PARTICIPANT, - "Failed to publish data in topic " << topic_name << " : type identifier not found."); - return false; + continue; } - DdsTopic topic; - topic.m_topic_name = topic_name; - topic.type_name = type_name; - topic.topic_qos = qos; - topic.type_identifiers.type_identifier1(type_identifier); - this->discovery_database_->add_endpoint(rtps::CommonParticipant::simulate_endpoint(topic, this->id())); - - // Wait for reader to be created from discovery thread - // NOTE: Set a timeout to avoid a deadlock in case the reader is never created for some reason (e.g. the topic - // is blocked or the underlying DDS Pipe object is disabled/destroyed before the reader is created). - if (!cv_.wait_for(lck, std::chrono::seconds(5), [&] - { - return nullptr != (reader = lookup_reader_nts_(topic_name)); - })) + cancelling_goals.emplace_back(goal_id, timestamp); + } + std::string reply_json = RpcUtils::create_cancel_reply_msg(cancelling_goals, cancel_code); + + if (!send_service_reply( + std::string(action_name) + ACTION_CANCEL_SUFFIX, + reply_json, + request_id)) + { + EPROSIMA_LOG_ERROR(DDSENABLER_EXECUTION, + "Failed to send action cancel reply to action " << action_name + << ": request id not found."); + return false; + } + return true; +} + +bool EnablerParticipant::send_action_result( + const char* action_name, + const UUID& goal_id, + const StatusCode& status_code, + const char* json) +{ + if (!handler_->is_UUID_active(action_name, goal_id)) + { + EPROSIMA_LOG_ERROR(DDSENABLER_EXECUTION, + "Failed to send action result to action " << action_name + << ": goal id not found."); + return false; + } + + std::string reply_json = RpcUtils::create_result_reply_msg(status_code, json); + + return handler_->handle_action_result(action_name, goal_id, reply_json); +} + +bool EnablerParticipant::send_action_get_result_reply( + const std::string& action_name, + const UUID& goal_id, + const std::string& reply_json, + const uint64_t request_id) +{ + std::string result_topic = action_name + ACTION_RESULT_SUFFIX; + + if (send_service_reply( + result_topic, + reply_json, + request_id)) + { + handler_->erase_action_UUID(goal_id, ActionEraseReason::FORCED); + return true; + } + + EPROSIMA_LOG_ERROR(DDSENABLER_EXECUTION, + "Failed to send action get result to action " << action_name); + + return false; +} + +bool EnablerParticipant::send_action_feedback( + const char* action_name, + const char* json, + const UUID& goal_id) +{ + if (!handler_->is_UUID_active(action_name, goal_id)) + { + EPROSIMA_LOG_ERROR(DDSENABLER_EXECUTION, + "Failed to send action feedback to action " << action_name + << ": goal id not found."); + return false; + } + + Protocol protocol = handler_->get_action_protocol(action_name, goal_id); + + std::string prefix; + switch (protocol) + { + case Protocol::ROS2: + prefix = ROS_TOPIC_PREFIX; + break; + case Protocol::DDS: + prefix = FASTDDS_TOPIC_PREFIX; + break; + default: + EPROSIMA_LOG_ERROR(DDSENABLER_EXECUTION, + "Failed to send feedback to action " << action_name + << ": unsupported RPC protocol."); + return false; + } + + std::string feedback_json = RpcUtils::create_feedback_msg(json, goal_id); + std::string feedback_topic = prefix + std::string(action_name) + ACTION_FEEDBACK_SUFFIX; + + return publish(feedback_topic, feedback_json); +} + +bool EnablerParticipant::update_action_status( + const std::string& action_name, + const UUID& goal_id, + const StatusCode& status_code) +{ + std::chrono::system_clock::time_point goal_accepted_stamp; + if (!handler_->is_UUID_active(action_name, goal_id, &goal_accepted_stamp)) + { + EPROSIMA_LOG_ERROR(DDSENABLER_EXECUTION, + "Failed to update action status to action " << action_name + << ": goal id not found."); + return false; + } + + Protocol protocol = handler_->get_action_protocol(action_name, goal_id); + + std::string prefix; + switch (protocol) + { + case Protocol::ROS2: + prefix = ROS_TOPIC_PREFIX; + break; + case Protocol::DDS: + prefix = FASTDDS_TOPIC_PREFIX; + break; + default: + EPROSIMA_LOG_ERROR(DDSENABLER_EXECUTION, + "Failed to send status to action " << action_name + << ": unsupported RPC protocol."); + return false; + } + + std::string status_json = RpcUtils::create_status_msg(goal_id, status_code, goal_accepted_stamp); + std::string status_topic = prefix + action_name + ACTION_STATUS_SUFFIX; + return publish(status_topic, status_json); +} + +bool EnablerParticipant::query_topic_nts_( + const std::string& topic_name, + DdsTopic& topic) +{ + if (!topic_query_callback_) + { + EPROSIMA_LOG_ERROR(DDSENABLER_ENABLER_PARTICIPANT, + "Failed to query data from topic " << topic_name << + " : topic is unknown and topic query callback not set."); + return false; + } + TopicInfo topic_info; + if (!topic_query_callback_(topic_name.c_str(), topic_info)) + { + EPROSIMA_LOG_ERROR(DDSENABLER_ENABLER_PARTICIPANT, + "Failed to query data from topic " << topic_name << " : topic query callback failed."); + return false; + } + + return fill_topic_struct_nts_(topic_name, topic_info, topic); +} + +bool EnablerParticipant::query_service_nts_( + std::shared_ptr service, + Protocol Protocol) +{ + if (!service_query_callback_) + { + EPROSIMA_LOG_ERROR(DDSENABLER_ENABLER_PARTICIPANT, + "Failed to announce service " << service->service_name << + " : service is unknown and service request callback not set."); + return false; + } + + std::string request_type_name; + std::string serialized_request_qos_content; + std::string reply_type_name; + std::string serialized_reply_qos_content; + + + ServiceInfo service_info; + if (!service_query_callback_(service->service_name.c_str(), service_info)) + { + EPROSIMA_LOG_ERROR(DDSENABLER_ENABLER_PARTICIPANT, + "Failed to announce service " << service->service_name << " : service type request failed."); + return false; + } + + return fill_service_type_nts_(service_info, service, Protocol); +} + +bool EnablerParticipant::query_action_nts_( + ActionDiscovered& action, + Protocol Protocol, + std::unique_lock& lck) +{ + if (!action_query_callback_) + { + EPROSIMA_LOG_ERROR(DDSENABLER_ENABLER_PARTICIPANT, + "Failed to announce action " << action.action_name << + " : action is unknown and action request callback not set."); + return false; + } + + ActionInfo action_info; + + if (!action_query_callback_(action.action_name.c_str(), + action_info)) + { + EPROSIMA_LOG_ERROR(DDSENABLER_ENABLER_PARTICIPANT, + "Failed to announce action " << action.action_name << " : action type request failed."); + return false; + } + + std::string goal_service_name = action.action_name + ACTION_GOAL_SUFFIX; + std::string cancel_service_name = action.action_name + ACTION_CANCEL_SUFFIX; + std::string result_service_name = action.action_name + ACTION_RESULT_SUFFIX; + std::vector topics_names = + { + goal_service_name, + cancel_service_name, + result_service_name + }; + for (const auto& topic_name : topics_names) + { + auto it = services_.find(topic_name); + if (it != services_.end()) { + // Erase the service, to allow re-announcing it + services_.erase(it); + } + } + + std::shared_ptr goal_service = std::make_shared(goal_service_name, + Protocol); + if (!fill_service_type_nts_( + action_info.goal, + goal_service, + Protocol)) + { + EPROSIMA_LOG_ERROR(DDSENABLER_ENABLER_PARTICIPANT, + "Failed to announce action " << action.action_name << " : goal service type not found."); + return false; + } + if (!create_service_request_writer_nts_(goal_service, lck)) + { + EPROSIMA_LOG_ERROR(DDSENABLER_ENABLER_PARTICIPANT, + "Failed to announce action " << action.action_name << " : goal service writer creation failed."); + return false; + } + services_.insert_or_assign(goal_service_name, goal_service); + action.goal = goal_service; + + std::shared_ptr cancel_service = std::make_shared(cancel_service_name, + Protocol); + if (!fill_service_type_nts_( + action_info.cancel, + cancel_service, + Protocol)) + { + EPROSIMA_LOG_ERROR(DDSENABLER_ENABLER_PARTICIPANT, + "Failed to announce action " << action.action_name << " : cancel service type not found."); + return false; + } + if (!create_service_request_writer_nts_(cancel_service, lck)) + { + EPROSIMA_LOG_ERROR(DDSENABLER_ENABLER_PARTICIPANT, + "Failed to announce action " << action.action_name << " : cancel service writer creation failed."); + return false; + } + services_.insert_or_assign(cancel_service_name, cancel_service); + action.cancel = cancel_service; + + std::shared_ptr result_service = std::make_shared(result_service_name, + Protocol); + if (!fill_service_type_nts_( + action_info.result, + result_service, + Protocol)) + { + EPROSIMA_LOG_ERROR(DDSENABLER_ENABLER_PARTICIPANT, + "Failed to announce action " << action.action_name << " : result service type not found."); + return false; + } + if (!create_service_request_writer_nts_(result_service, lck)) + { + EPROSIMA_LOG_ERROR(DDSENABLER_ENABLER_PARTICIPANT, + "Failed to announce action " << action.action_name << " : result service writer creation failed."); + return false; + } + services_.insert_or_assign(result_service_name, result_service); + action.result = result_service; + + std::string prefix; + switch (Protocol) + { + case Protocol::ROS2: + prefix = ROS_TOPIC_PREFIX; + break; + case Protocol::DDS: + prefix = FASTDDS_TOPIC_PREFIX; + break; + default: EPROSIMA_LOG_ERROR(DDSENABLER_ENABLER_PARTICIPANT, - "Failed to create internal reader for topic " << topic_name << - " , please verify that the topic is allowed."); + "Failed to announce action " << action.action_name << " : unsupported RPC protocol."); return false; + } + + std::string feedback_topic_name = prefix + action.action_name + ACTION_FEEDBACK_SUFFIX; + DdsTopic feedback_topic; + if (!fill_topic_struct_nts_( + feedback_topic_name, + action_info.feedback, + feedback_topic)) + { + EPROSIMA_LOG_ERROR(DDSENABLER_ENABLER_PARTICIPANT, + "Failed to announce action " << action.action_name << " : feedback topic type not found."); + return false; + } + { + auto feedback_reader = lookup_reader_nts_(feedback_topic_name); + if (!feedback_reader) + { + if (!create_topic_writer_nts_( + feedback_topic, + feedback_reader, + lck)) + { + EPROSIMA_LOG_ERROR(DDSENABLER_ENABLER_PARTICIPANT, + "Failed to announce action " << action.action_name << + " : feedback topic writer creation failed."); + return false; + } } + } + action.feedback = feedback_topic; + action.feedback_discovered = true; - // (Optionally) wait for writer created in DDS participant to match with external readers, to avoid losing this - // message when not using transient durability - std::this_thread::sleep_for(std::chrono::milliseconds(std::static_pointer_cast( - configuration_)->initial_publish_wait)); + std::string status_topic_name = prefix + action.action_name + ACTION_STATUS_SUFFIX; + DdsTopic status_topic; + if (!fill_topic_struct_nts_( + status_topic_name, + action_info.status, + status_topic)) + { + EPROSIMA_LOG_ERROR(DDSENABLER_ENABLER_PARTICIPANT, + "Failed to announce action " << action.action_name << " : status topic type not found."); + return false; + } + { + auto status_reader = lookup_reader_nts_(action.status.m_topic_name); + if (!status_reader) + { + if (!create_topic_writer_nts_( + status_topic, + status_reader, + lck)) + { + EPROSIMA_LOG_ERROR(DDSENABLER_ENABLER_PARTICIPANT, + "Failed to announce action " << action.action_name << + " : status topic writer creation failed."); + return false; + } + } } + action.status = status_topic; + action.status_discovered = true; - auto data = std::make_unique(); + action.fully_discovered = true; + action.enabler_as_server = true; + return true; +} - Payload payload; - if (!std::static_pointer_cast(schema_handler_)->get_serialized_data(type_name, json, payload)) +bool EnablerParticipant::create_topic_writer_nts_( + const DdsTopic& topic, + std::shared_ptr& reader, + std::unique_lock& lck) +{ + ddspipe::core::types::Endpoint request_edp; + return create_topic_writer_nts_(topic, reader, request_edp, lck); +} + +bool EnablerParticipant::create_topic_writer_nts_( + const DdsTopic& topic, + std::shared_ptr& reader, + ddspipe::core::types::Endpoint& request_edp, + std::unique_lock& lck) +{ + request_edp = rtps::CommonParticipant::simulate_endpoint(topic, this->id()); + this->discovery_database_->add_endpoint(request_edp); + + // Wait for reader to be created from discovery thread + // NOTE: Set a timeout to avoid a deadlock in case the reader is never created for some reason (e.g. the topic + // is blocked or the underlying DDS Pipe object is disabled/destroyed before the reader is created). + if (!cv_.wait_for(lck, std::chrono::seconds(5), [&] + { + return nullptr != (reader = lookup_reader_nts_(topic.m_topic_name)); + })) { EPROSIMA_LOG_ERROR(DDSENABLER_ENABLER_PARTICIPANT, - "Failed to publish data in topic " << topic_name << " : data serialization failed."); + "Failed to create internal reader for topic " << topic.m_topic_name << + " , please verify that the topic is allowed."); return false; } - if (!payload_pool_->get_payload(payload, data->payload)) + return true; +} + +bool EnablerParticipant::create_service_request_writer_nts_( + std::shared_ptr service, + std::unique_lock& lck) +{ + auto reader = lookup_reader_nts_(service->topic_request.m_topic_name); + + if (nullptr == reader) { + ddspipe::core::types::Endpoint request_edp; + if (create_topic_writer_nts_( + service->topic_request, + reader, + request_edp, + lck)) + { + service->endpoint_request = request_edp; + return true; + } EPROSIMA_LOG_ERROR(DDSENABLER_ENABLER_PARTICIPANT, - "Failed to publish data in topic " << topic_name << " : get_payload failed."); + "Failed to create service request writer for service " << service->service_name << "."); return false; } - reader->simulate_data_reception(std::move(data)); + EPROSIMA_LOG_ERROR(DDSENABLER_ENABLER_PARTICIPANT, + "Failed to create server as there is already a server running for service " << service->service_name << + "."); + return false; +} + +bool EnablerParticipant::fill_topic_struct_nts_( + const std::string& topic_name, + const TopicInfo& topic_info, + DdsTopic& topic) + +{ + // Deserialize QoS if provided by the user (otherwise use default one) + TopicQoS qos; + if (!topic_info.serialized_qos.empty()) + { + try + { + qos = serialization::deserialize_qos(topic_info.serialized_qos); + } + catch (const std::exception& e) + { + EPROSIMA_LOG_ERROR(DDSENABLER_ENABLER_PARTICIPANT, + "Failed to deserialize QoS for topic " << topic_name << ": " << e.what()); + return false; + } + } + + fastdds::dds::xtypes::TypeIdentifier type_identifier; + if (!handler_->get_type_identifier(topic_info.type_name, type_identifier)) + { + EPROSIMA_LOG_ERROR(DDSENABLER_ENABLER_PARTICIPANT, + "Failed to create topic " << topic_name << " : type identifier not found."); + return false; + } + + topic.m_topic_name = topic_name; + topic.type_name = topic_info.type_name; + topic.topic_qos = qos; + topic.type_identifiers.type_identifier1(type_identifier); + return true; } -std::shared_ptr EnablerParticipant::lookup_reader_nts_( +bool EnablerParticipant::fill_service_type_nts_( + const ServiceInfo& service_info, + std::shared_ptr service, + Protocol Protocol) +{ + std::string rq_prefix, rq_suffix, rp_prefix, rp_suffix; + switch (Protocol) + { + case Protocol::ROS2: + rq_prefix = ROS_REQUEST_PREFIX; + rq_suffix = ROS_REQUEST_SUFFIX; + rp_prefix = ROS_REPLY_PREFIX; + rp_suffix = ROS_REPLY_SUFFIX; + break; + case Protocol::DDS: + rq_prefix = DDS_REQUEST_PREFIX; + rq_suffix = DDS_REQUEST_SUFFIX; + rp_prefix = DDS_REPLY_PREFIX; + rp_suffix = DDS_REPLY_SUFFIX; + break; + default: + EPROSIMA_LOG_ERROR(DDSENABLER_ENABLER_PARTICIPANT, + "Failed to announce service " << service->service_name << ": unknown RPC protocol."); + return false; + } + + DdsTopic topic_request; + std::string topic_request_name = rq_prefix + service->service_name + rq_suffix; + if (!fill_topic_struct_nts_(topic_request_name, service_info.request, topic_request)) + { + return false; + } + service->add_topic(topic_request, ServiceType::REQUEST); + + DdsTopic topic_reply; + std::string topic_reply_name = rp_prefix + service->service_name + rp_suffix; + if (!fill_topic_struct_nts_(topic_reply_name, service_info.reply, topic_reply)) + { + return false; + } + service->add_topic(topic_reply, ServiceType::REPLY); + + if (!service->fully_discovered) + { + return false; + } + + service->enabler_as_server = true; + return true; +} + +std::shared_ptr EnablerParticipant::lookup_reader_nts_( const std::string& topic_name, std::string& type_name) const { @@ -190,13 +1183,99 @@ std::shared_ptr EnablerParticipant::looku return nullptr; } -std::shared_ptr EnablerParticipant::lookup_reader_nts_( +std::shared_ptr EnablerParticipant::lookup_reader_nts_( const std::string& topic_name) const { std::string _; return lookup_reader_nts_(topic_name, _); } +bool EnablerParticipant::service_discovered_nts_( + const std::shared_ptr rpc_info, + const DdsTopic& topic) +{ + auto [it, inserted] = services_.try_emplace(rpc_info->service_name, + std::make_shared(rpc_info->service_name, rpc_info->protocol)); + if (it->second->add_topic(topic, rpc_info->service_type)) + { + it->second->external_server = true; + return true; + } + return false; +} + +bool EnablerParticipant::action_discovered_nts_( + const std::shared_ptr rpc_info, + const DdsTopic& topic) +{ + auto [it, inserted] = actions_.try_emplace(rpc_info->action_name, + std::make_shared(rpc_info->action_name, rpc_info->protocol)); + if (ServiceType::NONE != rpc_info->service_type) + { + if (service_discovered_nts_(rpc_info, topic)) + { + auto service_it = services_.find(rpc_info->service_name); + if (services_.end() == service_it) + { + EPROSIMA_LOG_ERROR(DDSENABLER_ENABLER_PARTICIPANT, + "Service " << rpc_info->service_name << " not found in action " << rpc_info->action_name); + return false; + } + + it->second->add_service(service_it->second, rpc_info->action_type); + } + } + else + { + it->second->add_topic(topic, rpc_info->action_type); + } + + if (it->second->check_fully_discovered()) + { + it->second->external_server = true; + return true; + } + return false; +} + +bool EnablerParticipant::revoke_service_nts_( + const std::string& service_name) +{ + auto it = services_.find(service_name); + if (it == services_.end()) + { + EPROSIMA_LOG_ERROR(DDSENABLER_ENABLER_PARTICIPANT, + "Failed to revoke service " << service_name << " : service not found."); + return false; + } + if (!it->second->enabler_as_server || !it->second->endpoint_request.has_value()) + { + EPROSIMA_LOG_ERROR(DDSENABLER_ENABLER_PARTICIPANT, + "Failed to revoke service " << service_name << " : service not announced as server."); + return false; + } + + if (it->second->external_server) + { + it->second->enabler_as_server = false; + return true; + } + + std::string request_name = it->second->topic_request.m_topic_name; + + this->discovery_database_->erase_endpoint(it->second->endpoint_request.value()); + it->second->endpoint_request.reset(); + it->second->remove_topic(ServiceType::REQUEST); + + auto reader = lookup_reader_nts_(request_name); + if (nullptr != reader) + { + readers_.erase(reader->topic()); + } + + return true; +} + } /* namespace participants */ } /* namespace ddsenabler */ } /* namespace eprosima */ diff --git a/ddsenabler_participants/src/cpp/Handler.cpp b/ddsenabler_participants/src/cpp/Handler.cpp index cf9d144b..468d7e94 100644 --- a/ddsenabler_participants/src/cpp/Handler.cpp +++ b/ddsenabler_participants/src/cpp/Handler.cpp @@ -46,6 +46,20 @@ Handler::Handler( "Creating handler instance."); writer_ = std::make_unique(); + + writer_->set_is_UUID_active_callback( + [this](const std::string& action_name, const UUID& uuid) + { + return this->is_UUID_active(action_name, uuid, nullptr); + } + ); + + writer_->set_erase_action_UUID_callback( + [this](const UUID& uuid, ActionEraseReason reason) + { + return this->erase_action_UUID(uuid, reason); + } + ); } Handler::~Handler() @@ -58,7 +72,7 @@ void Handler::add_schema( const fastdds::dds::DynamicType::_ref_type& dyn_type, const fastdds::dds::xtypes::TypeIdentifier& type_id) { - std::lock_guard lock(mtx_); + std::lock_guard lock(mtx_); add_schema_nts_(dyn_type, type_id); } @@ -66,7 +80,7 @@ void Handler::add_schema( void Handler::add_topic( const DdsTopic& topic) { - std::lock_guard lock(mtx_); + std::lock_guard lock(mtx_); EPROSIMA_LOG_INFO(DDSENABLER_HANDLER, "Adding topic: " << topic << "."); @@ -74,11 +88,33 @@ void Handler::add_topic( write_topic_nts_(topic); } +void Handler::add_service( + const RpcTopic& service) +{ + std::lock_guard lock(mtx_); + + EPROSIMA_LOG_INFO(DDSENABLER_HANDLER, + "Adding service: " << service.service_name() << "."); + + write_service_nts_(service); +} + +void Handler::add_action( + const RpcAction& action) +{ + std::lock_guard lock(mtx_); + + EPROSIMA_LOG_INFO(DDSENABLER_HANDLER, + "Adding action: " << action.action_name << "."); + + write_action_nts_(action); +} + void Handler::add_data( const DdsTopic& topic, RtpsPayloadData& data) { - std::lock_guard lock(mtx_); + std::lock_guard lock(mtx_); EPROSIMA_LOG_INFO(DDSENABLER_HANDLER, "Adding data in topic: " << topic << "."); @@ -120,14 +156,210 @@ void Handler::add_data( throw utils::InconsistencyException(STR_ENTRY << "Received sample with no payload."); } - write_sample_nts_(msg, dyn_type); + std::shared_ptr rpc_info = std::make_shared(topic.m_topic_name); + + switch (rpc_info->rpc_type) + { + case RpcType::NONE: + { + write_sample_nts_(msg, dyn_type); + break; + } + + // SERVICE + case RpcType::SERVICE: + { + if (rpc_info->service_type == ServiceType::REQUEST) + { + requests_id_++; + RpcPayloadData& rpc_data = dynamic_cast(data); + rpc_data.sent_sequence_number = eprosima::fastdds::rtps::SequenceNumber_t(requests_id_); + write_service_request_nts_(msg, dyn_type, requests_id_, rpc_info->service_name); + } + else + { + auto request_id = + dynamic_cast(data).write_params.get_reference(). + related_sample_identity().sequence_number().to64long(); + write_service_reply_nts_(msg, dyn_type, request_id, rpc_info->service_name); + } + break; + } + + // ACTIONS CLIENT + case RpcType::ACTION: + { + switch (rpc_info->service_type) + { + case ServiceType::REPLY: + { + switch (rpc_info->action_type) + { + case ActionType::RESULT: + { + auto action_id = + dynamic_cast(data).write_params.get_reference() + .related_sample_identity().sequence_number().to64long(); + UUID action_id_uuid; + if (get_action_request_UUID(action_id, ActionType::RESULT, action_id_uuid)) + { + write_action_result_nts_(msg, dyn_type, action_id_uuid, rpc_info->action_name); + } + erase_action_UUID(action_id_uuid, ActionEraseReason::RESULT); + break; + } + + case ActionType::GOAL: + { + auto action_id = + dynamic_cast(data).write_params.get_reference() + .related_sample_identity().sequence_number().to64long(); + UUID action_id_uuid; + if (get_action_request_UUID(action_id, ActionType::GOAL, action_id_uuid)) + { + write_action_goal_reply_nts_(msg, dyn_type, action_id_uuid, rpc_info->action_name); + } + break; + } + + case ActionType::CANCEL: + { + auto request_id = + dynamic_cast(data).write_params.get_reference() + .related_sample_identity().sequence_number().to64long(); + write_action_cancel_reply_nts_(msg, dyn_type, request_id, rpc_info->action_name); + break; + } + + default: + { + EPROSIMA_LOG_ERROR(DDSENABLER_HANDLER, + "Unknown action type for topic " << topic.m_topic_name << "."); + break; + } + } + break; + } + + case ServiceType::REQUEST: + { + switch (rpc_info->action_type) + { + case ActionType::GOAL: + case ActionType::CANCEL: + { + requests_id_++; + RpcPayloadData& rpc_data = dynamic_cast(data); + rpc_data.sent_sequence_number = eprosima::fastdds::rtps::SequenceNumber_t(requests_id_); + UUID uuid; + if (!writer_->uuid_from_request_json( + msg, + dyn_type, + uuid)) + { + EPROSIMA_LOG_ERROR(DDSENABLER_HANDLER, + "Failed to extract UUID from send_goal_request JSON."); + return; + } + + if (store_action_request( + rpc_info->action_name, + uuid, + requests_id_, + rpc_info->action_type)) + { + write_action_request_nts_(msg, dyn_type, requests_id_, rpc_info->action_name, + rpc_info->action_type); + } + + break; + } + + case ActionType::RESULT: + { + UUID uuid; + if (!writer_->uuid_from_request_json( + msg, + dyn_type, + uuid)) + { + EPROSIMA_LOG_ERROR(DDSENABLER_HANDLER, + "Failed to extract UUID from get_result_request JSON."); + return; + } + + requests_id_++; + RpcPayloadData& rpc_data = dynamic_cast(data); + rpc_data.sent_sequence_number = eprosima::fastdds::rtps::SequenceNumber_t(requests_id_); + if (!store_action_request( + rpc_info->action_name, + uuid, + requests_id_, + ActionType::RESULT)) + { + EPROSIMA_LOG_ERROR(DDSENABLER_HANDLER, + "Failed to store action request for get_result_request."); + return; + } + + std::string result; + if (get_action_result(uuid, result)) + { + if (send_action_get_result_reply_callback_) + { + send_action_get_result_reply_callback_( + rpc_info->action_name, + uuid, + result, + requests_id_); + } + } + break; + } + + default: + EPROSIMA_LOG_ERROR(DDSENABLER_HANDLER, + "Unknown action type for topic " << topic.m_topic_name << "."); + break; + } + break; + } + + case ServiceType::NONE: + { + switch (rpc_info->action_type) + { + case ActionType::FEEDBACK: + { + write_action_feedback_nts_(msg, dyn_type, rpc_info->action_name); + break; + } + + case ActionType::STATUS: + { + write_action_status_nts_(msg, dyn_type, rpc_info->action_name); + break; + } + + default: + { + EPROSIMA_LOG_ERROR(DDSENABLER_HANDLER, + "Unknown RPC type for topic " << topic.m_topic_name << "."); + break; + } + } + break; + } + } + } + } } bool Handler::get_type_identifier( const std::string& type_name, fastdds::dds::xtypes::TypeIdentifier& type_identifier) { - std::lock_guard lock(mtx_); + std::lock_guard lock(mtx_); auto it = schemas_.find(type_name); if (it != schemas_.end()) @@ -198,7 +430,7 @@ bool Handler::get_serialized_data( const std::string& json, Payload& payload) { - std::lock_guard lock(mtx_); + std::lock_guard lock(mtx_); fastdds::dds::DynamicType::_ref_type dyn_type; auto it = schemas_.find(type_name); @@ -309,6 +541,89 @@ void Handler::write_sample_nts_( writer_->write_data(msg, dyn_type); } +void Handler::write_service_nts_( + const RpcTopic& service) +{ + writer_->write_service_notification(service); +} + +void Handler::write_service_reply_nts_( + const Message& msg, + const fastdds::dds::DynamicType::_ref_type& dyn_type, + const uint64_t request_id, + const std::string& service_name) +{ + writer_->write_service_reply_notification(msg, dyn_type, request_id, service_name); +} + +void Handler::write_service_request_nts_( + const Message& msg, + const fastdds::dds::DynamicType::_ref_type& dyn_type, + const uint64_t request_id, + const std::string& service_name) +{ + writer_->write_service_request_notification(msg, dyn_type, request_id, service_name); +} + +void Handler::write_action_nts_( + const RpcAction& action) +{ + writer_->write_action_notification(action); +} + +void Handler::write_action_result_nts_( + const Message& msg, + const fastdds::dds::DynamicType::_ref_type& dyn_type, + const UUID& action_id, + const std::string& action_name) +{ + writer_->write_action_result_notification(msg, dyn_type, action_id, action_name); +} + +void Handler::write_action_feedback_nts_( + const Message& msg, + const fastdds::dds::DynamicType::_ref_type& dyn_type, + const std::string& action_name) +{ + writer_->write_action_feedback_notification(msg, dyn_type, action_name); +} + +void Handler::write_action_goal_reply_nts_( + const Message& msg, + const fastdds::dds::DynamicType::_ref_type& dyn_type, + const UUID& action_id, + const std::string& action_name) +{ + writer_->write_action_goal_reply_notification(msg, dyn_type, action_id, action_name); +} + +void Handler::write_action_cancel_reply_nts_( + const Message& msg, + const fastdds::dds::DynamicType::_ref_type& dyn_type, + const uint64_t request_id, + const std::string& action_name) +{ + writer_->write_action_cancel_reply_notification(msg, dyn_type, request_id, action_name); +} + +void Handler::write_action_status_nts_( + const Message& msg, + const fastdds::dds::DynamicType::_ref_type& dyn_type, + const std::string& action_name) +{ + writer_->write_action_status_notification(msg, dyn_type, action_name); +} + +void Handler::write_action_request_nts_( + const Message& msg, + const fastdds::dds::DynamicType::_ref_type& dyn_type, + const uint64_t request_id, + const std::string& action_name, + const ActionType action_type) +{ + writer_->write_action_request_notification(msg, dyn_type, request_id, action_name, action_type); +} + bool Handler::register_type_nts_( const std::string& type_name, const unsigned char* serialized_type, @@ -356,7 +671,8 @@ bool Handler::register_type_nts_( if (_type_name != type_name) { EPROSIMA_LOG_ERROR(DDSENABLER_HANDLER, - "Unexpected dynamic types collection format: " << type_name << " expected to be last item, found " << _type_name << + "Unexpected dynamic types collection format: " << type_name << " expected to be last item, found " << + _type_name << " instead."); return false; } @@ -368,6 +684,282 @@ bool Handler::register_type_nts_( return true; } +bool Handler::store_action_request( + const std::string& action_name, + const UUID& action_id, + const uint64_t request_id, + const ActionType action_type, + const Protocol Protocol) +{ + std::lock_guard lock(mtx_); + + auto it = action_request_id_to_uuid_.find(action_id); + if (it != action_request_id_to_uuid_.end()) + { + if (it->second.action_name != action_name) + { + EPROSIMA_LOG_ERROR(DDSENABLER_HANDLER, + "Action name mismatch for action, expected " + << it->second.action_name << ", got " << action_name); + return false; + } + if (ActionType::GOAL == action_type) + { + EPROSIMA_LOG_ERROR(DDSENABLER_HANDLER, + "Cannot store action goal request as action id already exists."); + return false; + } + // If it exists, update the request_id for the given action_type + it->second.set_request(request_id, action_type); + } + else + { + // If it does not exist, create a new entry only if the action type is goal request + if (ActionType::GOAL != action_type) + { + EPROSIMA_LOG_ERROR(DDSENABLER_HANDLER, + "Cannot store action request, action does not exist and request type is not GOAL."); + return false; + } + action_request_id_to_uuid_[action_id] = ActionRequestInfo(action_name, action_type, request_id, Protocol); + } + + return true; +} + +bool Handler::handle_action_result( + const std::string& action_name, + const UUID& action_id, + const std::string& result) +{ + std::lock_guard lock(mtx_); + auto it = action_request_id_to_uuid_.find(action_id); + if (it != action_request_id_to_uuid_.end()) + { + if (it->second.action_name != action_name) + { + EPROSIMA_LOG_ERROR(DDSENABLER_HANDLER, + "Action name mismatch for action, expected " << it->second.action_name + << ", got " << action_name); + return false; + } + if (it->second.result_request_id != 0) + { + return send_action_get_result_reply_callback_( + action_name, + action_id, + result, + it->second.result_request_id); + } + if (it->second.set_result(std::move(result))) + { + return true; + } + else + { + EPROSIMA_LOG_ERROR(DDSENABLER_HANDLER, + "Failed to store action result for action, result already set."); + return false; + } + } + EPROSIMA_LOG_ERROR(DDSENABLER_HANDLER, + "Failed to send action result, goal id not found."); + return false; +} + +void Handler::erase_action_UUID( + const UUID& action_id, + ActionEraseReason erase_reason) +{ + std::lock_guard lock(mtx_); + auto it = action_request_id_to_uuid_.find(action_id); + if (it != action_request_id_to_uuid_.end()) + { + if (it->second.erase(erase_reason)) + { + action_request_id_to_uuid_.erase(it); + } + } +} + +bool Handler::is_UUID_active( + const std::string& action_name, + const UUID& action_id, + std::chrono::system_clock::time_point* goal_accepted_stamp) +{ + std::lock_guard lock(mtx_); + auto it = action_request_id_to_uuid_.find(action_id); + if (it != action_request_id_to_uuid_.end() && action_name == it->second.action_name) + { + if (goal_accepted_stamp) + { + *goal_accepted_stamp = it->second.goal_accepted_stamp; + } + return true; + } + + return false; +} + +Protocol Handler::get_action_protocol( + const std::string& action_name, + const UUID& action_id) +{ + std::lock_guard lock(mtx_); + auto it = action_request_id_to_uuid_.find(action_id); + if (it != action_request_id_to_uuid_.end() && action_name == it->second.action_name) + { + return it->second.protocol; + } + + return Protocol::PROTOCOL_UNKNOWN; +} + +bool Handler::get_action_request_UUID( + const uint64_t request_id, + const ActionType action_type, + UUID& action_id) +{ + std::lock_guard lock(mtx_); + for (auto it = action_request_id_to_uuid_.begin(); it != action_request_id_to_uuid_.end(); ++it) + { + try + { + uint64_t action_request_id = it->second.get_request(action_type); + if (request_id == action_request_id) + { + action_id = it->first; + return true; + } + } + catch (const std::exception& e) + { + EPROSIMA_LOG_ERROR(DDSENABLER_HANDLER, + "Error getting action request ID: " << e.what()); + } + } + return false; +} + +bool Handler::get_action_result( + const UUID& action_id, + std::string& result) +{ + std::lock_guard lock(mtx_); + auto it = action_request_id_to_uuid_.find(action_id); + if (it != action_request_id_to_uuid_.end()) + { + if (!it->second.result.empty()) + { + result = it->second.result; + return true; + } + } + return false; +} + +void Handler::set_data_notification_callback( + participants::DdsDataNotification callback) +{ + writer_->set_data_notification_callback(callback); +} + +void Handler::set_topic_notification_callback( + participants::DdsTopicNotification callback) +{ + writer_->set_topic_notification_callback(callback); +} + +void Handler::set_type_notification_callback( + participants::DdsTypeNotification callback) +{ + writer_->set_type_notification_callback(callback); +} + +void Handler::set_type_query_callback( + participants::DdsTypeQuery callback) +{ + type_query_callback_ = callback; +} + +void Handler::set_service_notification_callback( + participants::ServiceNotification callback) +{ + writer_->set_service_notification_callback(callback); +} + +void Handler::set_service_reply_notification_callback( + participants::ServiceReplyNotification callback) +{ + writer_->set_service_reply_notification_callback(callback); +} + +void Handler::set_service_request_notification_callback( + participants::ServiceRequestNotification callback) +{ + writer_->set_service_request_notification_callback(callback); +} + +void Handler::set_action_notification_callback( + participants::ActionNotification callback) +{ + writer_->set_action_notification_callback(callback); +} + +void Handler::set_action_result_notification_callback( + participants::ActionResultNotification callback) +{ + writer_->set_action_result_notification_callback(callback); +} + +void Handler::set_action_feedback_notification_callback( + participants::ActionFeedbackNotification callback) +{ + writer_->set_action_feedback_notification_callback(callback); +} + +void Handler::set_action_status_notification_callback( + participants::ActionStatusNotification callback) +{ + writer_->set_action_status_notification_callback(callback); +} + +void Handler::set_send_action_get_result_request_callback( + std::function callback) +{ + writer_->set_send_action_get_result_request_callback(callback); +} + +void Handler::set_action_goal_request_notification_callback( + participants::ActionGoalRequestNotification callback) +{ + writer_->set_action_goal_request_notification_callback(callback); +} + +void Handler::set_action_cancel_request_notification_callback( + participants::ActionCancelRequestNotification callback) +{ + writer_->set_action_cancel_request_notification_callback(callback); +} + +void Handler::set_send_action_send_goal_reply_callback( + std::function callback) +{ + writer_->set_send_action_send_goal_reply_callback(callback); +} + +void Handler::set_send_action_get_result_reply_callback( + std::function callback) +{ + send_action_get_result_reply_callback_ = callback; +} + +uint64_t Handler::get_new_request_id() +{ + std::lock_guard lock(mtx_); + return ++requests_id_; +} + } /* namespace participants */ } /* namespace ddsenabler */ } /* namespace eprosima */ diff --git a/ddsenabler_participants/src/cpp/Writer.cpp b/ddsenabler_participants/src/cpp/Writer.cpp index 3ae3706f..c7a35228 100644 --- a/ddsenabler_participants/src/cpp/Writer.cpp +++ b/ddsenabler_participants/src/cpp/Writer.cpp @@ -16,8 +16,6 @@ * @file Writer.cpp */ -#include - #include #include #include @@ -25,10 +23,13 @@ #include #include +#include #include #include +#include + namespace eprosima { namespace ddsenabler { namespace participants { @@ -36,6 +37,25 @@ namespace participants { using namespace eprosima::ddsenabler::participants::serialization; using namespace eprosima::ddspipe::core::types; + +UUID json_to_uuid( + const nlohmann::json& json) +{ + UUID uuid; + if (!json.contains("uuid") || !json["uuid"].is_array() || json["uuid"].size() != sizeof(UUID)) + + { + throw std::invalid_argument("Invalid UUID format in JSON"); + } + + for (size_t i = 0; i < sizeof(UUID); ++i) + { + uuid[i] = json["uuid"][i].get(); + } + + return uuid; +} + void Writer::write_schema( const fastdds::dds::DynamicType::_ref_type& dyn_type, const fastdds::dds::xtypes::TypeIdentifier& type_id) @@ -85,6 +105,44 @@ void Writer::write_schema( return; } + // Filter placeholder based on action/service type suffix + std::string data_placeholder = ss_data_holder.str(); + try + { + using json = nlohmann::json; + auto j = json::parse(data_placeholder); + + if (type_name.find("SendGoal_Request_") != std::string::npos) + { + // only the 'goal' sub-object + data_placeholder = j.at("goal").dump(); + } + else if (type_name.find("GetResult_Response_") != std::string::npos) + { + // only the 'result' sub-object + data_placeholder = j.at("result").dump(); + } + else if (type_name.find("FeedbackMessage_") != std::string::npos) + { + // only the 'feedback' sub-object + data_placeholder = j.at("feedback").dump(); + } + // all other types (SendGoal_Response, CancelGoal_*, GetResult_Request, + // GoalStatusArray_ get an empty placeholder + else if (type_name.find("SendGoal_Response_") != std::string::npos || + type_name.find("CancelGoal_Request_") != std::string::npos || + type_name.find("GetResult_Request_") != std::string::npos || + type_name.find("GoalStatusArray_") != std::string::npos) + { + data_placeholder = "{}"; + } + } + catch (const std::exception& e) + { + EPROSIMA_LOG_ERROR(DDSENABLER_WRITER, + "JSON filter failed for " << type_name << ": " << e.what()); + } + // Notify type reception if (type_notification_callback_) { @@ -93,7 +151,7 @@ void Writer::write_schema( ss_idl.str().c_str(), types_collection_payload->data, types_collection_payload->length, - ss_data_holder.str().c_str() + data_placeholder.c_str() ); } } @@ -110,8 +168,7 @@ void Writer::write_topic( std::string serialized_qos = serialize_qos(topic.topic_qos); topic_notification_callback_( topic.topic_name().c_str(), - topic.type_name.c_str(), - serialized_qos.c_str() + TopicInfo(topic.type_name, serialized_qos) ); } } @@ -119,6 +176,526 @@ void Writer::write_topic( void Writer::write_data( const Message& msg, const fastdds::dds::DynamicType::_ref_type& dyn_type) +{ + nlohmann::json json_data; + if (data_notification_callback_ && prepare_json_data_(msg, dyn_type, json_data)) + { + data_notification_callback_( + msg.topic.topic_name().c_str(), + json_data.dump(4).c_str(), + msg.publish_time.to_ns() + ); + } +} + +void Writer::write_service_notification( + const ddspipe::core::types::RpcTopic& service) +{ + EPROSIMA_LOG_INFO(DDSENABLER_WRITER, + "Writting service: " << service.service_name() << "."); + + if (service_notification_callback_) + { + std::string request_serialized_qos = serialize_qos(service.request_topic().topic_qos); + std::string reply_serialized_qos = serialize_qos(service.reply_topic().topic_qos); + service_notification_callback_( + service.service_name().c_str(), + ServiceInfo( + TopicInfo(service.request_topic().type_name, request_serialized_qos), + TopicInfo(service.reply_topic().type_name, reply_serialized_qos) + ) + ); + } +} + +void Writer::write_service_reply_notification( + const Message& msg, + const fastdds::dds::DynamicType::_ref_type& dyn_type, + const uint64_t request_id, + const std::string& service_name) +{ + nlohmann::json json_data; + if (service_reply_notification_callback_ && prepare_json_data_(msg, dyn_type, json_data)) + { + service_reply_notification_callback_( + service_name.c_str(), + json_data.dump(4).c_str(), + request_id, + msg.publish_time.to_ns() + ); + } +} + +void Writer::write_service_request_notification( + const Message& msg, + const fastdds::dds::DynamicType::_ref_type& dyn_type, + const uint64_t request_id, + const std::string& service_name) +{ + nlohmann::json json_data; + if (service_request_notification_callback_ && prepare_json_data_(msg, dyn_type, json_data)) + { + service_request_notification_callback_( + service_name.c_str(), + json_data.dump(4).c_str(), + request_id, + msg.publish_time.to_ns() + ); + } +} + +void Writer::write_action_notification( + const RpcAction& action) +{ + EPROSIMA_LOG_INFO(DDSENABLER_WRITER, + "Writting action: " << action.action_name << "."); + + if (action_notification_callback_) + { + std::string goal_request_serialized_qos = serialize_qos(action.goal.request_topic().topic_qos); + std::string goal_reply_serialized_qos = serialize_qos(action.goal.reply_topic().topic_qos); + std::string cancel_request_serialized_qos = serialize_qos(action.cancel.request_topic().topic_qos); + std::string cancel_reply_serialized_qos = serialize_qos(action.cancel.reply_topic().topic_qos); + std::string result_request_serialized_qos = serialize_qos(action.result.request_topic().topic_qos); + std::string result_reply_serialized_qos = serialize_qos(action.result.reply_topic().topic_qos); + std::string feedback_serialized_qos = serialize_qos(action.feedback.topic_qos); + std::string status_serialized_qos = serialize_qos(action.status.topic_qos); + + action_notification_callback_( + action.action_name.c_str(), + ActionInfo( + ServiceInfo( + TopicInfo(action.goal.request_topic().type_name, goal_request_serialized_qos), + TopicInfo(action.goal.reply_topic().type_name, goal_reply_serialized_qos) + ), + ServiceInfo( + TopicInfo(action.cancel.request_topic().type_name, cancel_request_serialized_qos), + TopicInfo(action.cancel.reply_topic().type_name, cancel_reply_serialized_qos) + ), + ServiceInfo( + TopicInfo(action.result.request_topic().type_name, result_request_serialized_qos), + TopicInfo(action.result.reply_topic().type_name, result_reply_serialized_qos) + ), + TopicInfo(action.feedback.type_name, feedback_serialized_qos), + TopicInfo(action.status.type_name, status_serialized_qos) + )); + } +} + +void Writer::write_action_result_notification( + const Message& msg, + const fastdds::dds::DynamicType::_ref_type& dyn_type, + const UUID& action_id, + const std::string& action_name) +{ + nlohmann::json json_data; + if (action_result_notification_callback_ && prepare_json_data_(msg, dyn_type, json_data)) + { + action_result_notification_callback_( + action_name.c_str(), + json_data.dump(4).c_str(), + action_id, + msg.publish_time.to_ns() + ); + } +} + +void Writer::write_action_feedback_notification( + const Message& msg, + const fastdds::dds::DynamicType::_ref_type& dyn_type, + const std::string& action_name) +{ + nlohmann::json json_data; + if (action_feedback_notification_callback_ && prepare_json_data_(msg, dyn_type, json_data)) + { + std::stringstream instanceHandle; + instanceHandle << msg.instanceHandle; + UUID uuid; + try + { + uuid = json_to_uuid(json_data[msg.topic.topic_name()]["data"][instanceHandle.str()]["goal_id"]); + } + catch (const nlohmann::json::exception& e) + { + EPROSIMA_LOG_ERROR(DDSENABLER_WRITER, + "Error parsing UUID from JSON: " << e.what()); + return; + } + + action_feedback_notification_callback_( + action_name.c_str(), + json_data[msg.topic.topic_name()]["data"][instanceHandle.str()]["feedback"].dump(4).c_str(), + uuid, + msg.publish_time.to_ns() + ); + } +} + +void Writer::write_action_goal_reply_notification( + const Message& msg, + const fastdds::dds::DynamicType::_ref_type& dyn_type, + const UUID& action_id, + const std::string& action_name) +{ + nlohmann::json json_data; + if (!prepare_json_data_(msg, dyn_type, json_data)) + { + return; + } + + std::string status_message = "Action goal accepted"; + ddsenabler::participants::StatusCode status_code = ddsenabler::participants::StatusCode::ACCEPTED; + std::stringstream instanceHandle; + instanceHandle << msg.instanceHandle; + try + { + if (!json_data[msg.topic.topic_name()]["data"][instanceHandle.str()]["accepted"]) + { + status_message = "Action goal rejected"; + status_code = ddsenabler::participants::StatusCode::REJECTED; + } + } + catch (const nlohmann::json::exception& e) + { + EPROSIMA_LOG_ERROR(DDSENABLER_WRITER, + "Error parsing action goal reply notification: " << e.what()); + status_message = "Action goal reply notification malformed"; + status_code = ddsenabler::participants::StatusCode::UNKNOWN; + } + if (action_status_notification_callback_) + { + action_status_notification_callback_( + action_name.c_str(), + action_id, + status_code, + status_message.c_str(), + msg.publish_time.to_ns() + ); + } + + if (ddsenabler::participants::StatusCode::ACCEPTED == status_code && + send_action_get_result_request_callback_ && + !send_action_get_result_request_callback_( + action_name.c_str(), + action_id)) + { + status_message = "Action goal aborted"; + status_code = ddsenabler::participants::StatusCode::ABORTED; + + if (action_status_notification_callback_) + { + action_status_notification_callback_( + action_name.c_str(), + action_id, + status_code, + status_message.c_str(), + msg.publish_time.to_ns() + ); + } + } +} + +void Writer::write_action_cancel_reply_notification( + const Message& msg, + const fastdds::dds::DynamicType::_ref_type& dyn_type, + const uint64_t request_id, + const std::string& action_name) +{ + ddsenabler::participants::StatusCode status_code = ddsenabler::participants::StatusCode::CANCELED; + + nlohmann::json json_data; + if (!prepare_json_data_(msg, dyn_type, json_data)) + { + return; + } + + std::stringstream instanceHandle; + instanceHandle << msg.instanceHandle; + + std::string status_message; + int return_code; + try + { + return_code = json_data[msg.topic.topic_name()]["data"][instanceHandle.str()]["return_code"]; + } + catch (const nlohmann::json::exception& e) + { + EPROSIMA_LOG_ERROR(DDSENABLER_WRITER, + "Error parsing action cancel reply notification: " << e.what()); + status_code = ddsenabler::participants::StatusCode::UNKNOWN; + return; + } + switch (return_code) + { + case 0: + status_message = "Action canceled successfully"; + status_code = ddsenabler::participants::StatusCode::CANCELED; + break; + case 1: + status_message = "Action cancel request rejected"; + status_code = ddsenabler::participants::StatusCode::REJECTED; + break; + case 2: + status_message = "Action cancel request unknown goal ID"; + status_code = ddsenabler::participants::StatusCode::CANCEL_REQUEST_FAILED; + break; + case 3: + status_message = "Action cancel request rejected as goal was already terminated"; + status_code = ddsenabler::participants::StatusCode::CANCEL_REQUEST_FAILED; + break; + default: + status_message = "Action cancel request unknown code"; + status_code = ddsenabler::participants::StatusCode::UNKNOWN; + break; + } + + try + { + const auto& goals = json_data[msg.topic.topic_name()]["data"][instanceHandle.str()]["goals_canceling"]; + for (const auto& goal : goals) + { + UUID uuid; + try + { + uuid = json_to_uuid(goal["goal_id"]); + } + catch (const nlohmann::json::exception& e) + { + EPROSIMA_LOG_ERROR(DDSENABLER_WRITER, + "Error parsing UUID from JSON: " << e.what()); + return; + } + + if (is_UUID_active_callback_ && !is_UUID_active_callback_(action_name, uuid)) + { + continue; + } + + if (action_status_notification_callback_) + { + action_status_notification_callback_( + action_name.c_str(), + uuid, + status_code, + status_message.c_str(), + msg.publish_time.to_ns() + ); + } + } + } + catch (const nlohmann::json::exception& e) + { + EPROSIMA_LOG_ERROR(DDSENABLER_WRITER, + "Error parsing action cancel reply notification goals: " << e.what()); + } +} + +void Writer::write_action_status_notification( + const Message& msg, + const fastdds::dds::DynamicType::_ref_type& dyn_type, + const std::string& action_name) +{ + nlohmann::json json_data; + if (!prepare_json_data_(msg, dyn_type, json_data)) + { + return; + } + + std::stringstream instanceHandle; + instanceHandle << msg.instanceHandle; + + try + { + const auto& list = json_data[msg.topic.topic_name()]["data"][instanceHandle.str()]["status_list"]; + for (const auto& status : list) + { + UUID uuid; + try + { + uuid = json_to_uuid(status["goal_info"]["goal_id"]); + } + catch (const nlohmann::json::exception& e) + { + EPROSIMA_LOG_ERROR(DDSENABLER_WRITER, + "Error parsing UUID from JSON: " << e.what()); + return; + } + + if (is_UUID_active_callback_ && !is_UUID_active_callback_(action_name, uuid)) + { + continue; + } + + ddsenabler::participants::StatusCode status_code(status["status"]); + std::string status_message; + switch (status_code) + { + case ddsenabler::participants::StatusCode::UNKNOWN: + status_message = "The status has not been properly set"; + break; + + case ddsenabler::participants::StatusCode::ACCEPTED: + status_message = "The goal has been accepted and is awaiting execution"; + break; + + case ddsenabler::participants::StatusCode::EXECUTING: + status_message = "The goal is currently being executed by the action server"; + break; + + case ddsenabler::participants::StatusCode::CANCELING: + status_message = + "The client has requested that the goal be canceled and the action server has accepted the cancel request"; + break; + + case ddsenabler::participants::StatusCode::SUCCEEDED: + if (erase_action_UUID_callback_) + { + erase_action_UUID_callback_(uuid, ActionEraseReason::FINAL_STATUS); + } + status_message = "The goal was achieved successfully by the action server"; + break; + + case ddsenabler::participants::StatusCode::CANCELED: + if (erase_action_UUID_callback_) + { + erase_action_UUID_callback_(uuid, ActionEraseReason::FINAL_STATUS); + } + status_message = "The goal was canceled after an external request from an action client"; + break; + + case ddsenabler::participants::StatusCode::ABORTED: + if (erase_action_UUID_callback_) + { + erase_action_UUID_callback_(uuid, ActionEraseReason::FINAL_STATUS); + } + status_message = "The goal was terminated by the action server without an external request"; + break; + case ddsenabler::participants::StatusCode::REJECTED: + if (erase_action_UUID_callback_) + { + erase_action_UUID_callback_(uuid, ActionEraseReason::FINAL_STATUS); + } + status_message = "The goal was rejected by the action server, it will not be executed"; + break; + default: + status_message = "Unknown status code"; + break; + } + + if (action_status_notification_callback_) + { + action_status_notification_callback_( + action_name.c_str(), + uuid, + status_code, + status_message.c_str(), + msg.publish_time.to_ns() + ); + } + } + } + catch (const nlohmann::json::exception& e) + { + EPROSIMA_LOG_ERROR(DDSENABLER_WRITER, + "Error parsing action status notification: " << e.what()); + return; + } +} + +void Writer::write_action_request_notification( + const Message& msg, + const fastdds::dds::DynamicType::_ref_type& dyn_type, + const uint64_t request_id, + const std::string& action_name, + const ActionType action_type) +{ + nlohmann::json json_data; + if (!prepare_json_data_(msg, dyn_type, json_data)) + { + return; + } + + std::stringstream instanceHandle; + instanceHandle << msg.instanceHandle; + try + { + if (ActionType::GOAL == action_type) + { + bool accepted; + if (action_goal_request_notification_callback_) + { + accepted = action_goal_request_notification_callback_( + action_name.c_str(), + json_data.dump(4).c_str(), + json_to_uuid(json_data[msg.topic.topic_name()]["data"][instanceHandle.str()]["goal_id"]), + msg.publish_time.to_ns() + ); + } + + send_action_send_goal_reply_callback_( + action_name.c_str(), + request_id, + accepted); + + return; + } + if (ActionType::CANCEL == action_type) + { + if (action_cancel_request_notification_callback_) + { + auto sec = json_data[msg.topic.topic_name()]["data"][instanceHandle.str()]["goal_info"]["sec"]; + auto nanosec = json_data[msg.topic.topic_name()]["data"][instanceHandle.str()]["goal_info"]["nanosec"]; + int64_t timestamp = static_cast(sec.get()) * 1000000000 + + static_cast(nanosec.get()); + action_cancel_request_notification_callback_( + action_name.c_str(), + json_to_uuid(json_data[msg.topic.topic_name()]["data"][instanceHandle.str()]["goal_id"]), + timestamp, + request_id, + msg.publish_time.to_ns() + ); + } + return; + } + } + catch (const nlohmann::json::exception& e) + { + EPROSIMA_LOG_ERROR(DDSENABLER_WRITER, + "Error parsing action request notification: " << e.what()); + return; + } +} + +bool Writer::uuid_from_request_json( + const Message& msg, + const fastdds::dds::DynamicType::_ref_type& dyn_type, + UUID& uuid) +{ + nlohmann::json json_data; + if (!prepare_json_data_(msg, dyn_type, json_data)) + { + return false; + } + + std::stringstream instanceHandle; + instanceHandle << msg.instanceHandle; + try + { + uuid = json_to_uuid(json_data[msg.topic.topic_name()]["data"][instanceHandle.str()]["goal_id"]); + } + catch (const nlohmann::json::exception& e) + { + EPROSIMA_LOG_ERROR(DDSENABLER_WRITER, + "Error parsing UUID from request JSON: " << e.what()); + return false; + } + return true; +} + +bool Writer::prepare_json_data_( + const Message& msg, + const fastdds::dds::DynamicType::_ref_type& dyn_type, + nlohmann::json& json_output) { assert(nullptr != dyn_type); @@ -132,7 +709,7 @@ void Writer::write_data( { EPROSIMA_LOG_ERROR(DDSENABLER_WRITER, "Not able to get DynamicData from topic " << msg.topic.topic_name() << "."); - return; + return false; } std::stringstream ss_dyn_data; @@ -142,11 +719,10 @@ void Writer::write_data( { EPROSIMA_LOG_ERROR(DDSENABLER_WRITER, "Not able to serialize data of topic " << msg.topic.topic_name() << " into JSON format."); - return; + return false; } // Fill JSON object with the data - nlohmann::json json_output; { // Set id to be the source guid prefix std::stringstream ss_source_guid_prefix; @@ -168,15 +744,7 @@ void Writer::write_data( json_output[msg.topic.topic_name()]["data"][ss_instanceHandle.str()] = nlohmann::json::parse(ss_dyn_data.str()); } - // Notify data reception - if (data_notification_callback_) - { - data_notification_callback_( - msg.topic.topic_name().c_str(), - json_output.dump(4).c_str(), - msg.publish_time.to_ns() - ); - } + return true; } fastdds::dds::DynamicData::_ref_type Writer::get_dynamic_data_( diff --git a/ddsenabler_participants/src/cpp/rpc/RpcStructs.cpp b/ddsenabler_participants/src/cpp/rpc/RpcStructs.cpp new file mode 100644 index 00000000..9c095214 --- /dev/null +++ b/ddsenabler_participants/src/cpp/rpc/RpcStructs.cpp @@ -0,0 +1,496 @@ +// Copyright 2025 Proyectos y Sistemas de Mantenimiento SL (eProsima). +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/** + * @file RpcStructs.cpp + */ + +#include + +namespace eprosima { +namespace ddsenabler { +namespace participants { + +RpcAction::RpcAction( + const std::string& action_name, + const ddspipe::core::types::RpcTopic& goal, + const ddspipe::core::types::RpcTopic& result, + const ddspipe::core::types::RpcTopic& cancel, + const ddspipe::core::types::DdsTopic& feedback, + const ddspipe::core::types::DdsTopic& status) + : action_name(action_name) + , goal(goal) + , result(result) + , cancel(cancel) + , feedback(feedback) + , status(status) +{ +} + +RpcInfo::RpcInfo( + const std::string& dds_topic_name) + : topic_name(dds_topic_name) + , protocol(Protocol::PROTOCOL_UNKNOWN) + , rpc_type(RpcType::NONE) + , service_type(ServiceType::NONE) + , action_type(ActionType::NONE) +{ + detect_protocol(); + try + { + extract_rpc_info(); + } + catch (const std::exception& e) + { + EPROSIMA_LOG_ERROR(DDSENABLER_RPC_UTILS, + "Error extracting RPC info from topic name " << dds_topic_name << ": " << e.what()); + } +} + +void RpcInfo::detect_protocol() +{ + if (topic_name.rfind(ROS_TOPIC_PREFIX, 0) == 0 || + topic_name.rfind(ROS_REQUEST_PREFIX, 0) == 0 || + topic_name.rfind(ROS_REPLY_PREFIX, 0) == 0) + { + protocol = Protocol::ROS2; + return; + } + // With the current Fast Prefixes being empty strings, this check must be the last one + else if (topic_name.rfind(FASTDDS_TOPIC_PREFIX, 0) == 0 || + topic_name.rfind(DDS_REQUEST_PREFIX, 0) == 0 || + topic_name.rfind(DDS_REPLY_PREFIX, 0) == 0) + { + protocol = Protocol::DDS; + return; + } + protocol = Protocol::PROTOCOL_UNKNOWN; +} + +void RpcInfo::extract_rpc_info() +{ + std::string request_prefix, request_suffix, reply_prefix, reply_suffix, topic_prefix; + switch (protocol) + { + case Protocol::ROS2: + request_prefix = ROS_REQUEST_PREFIX; + request_suffix = ROS_REQUEST_SUFFIX; + reply_prefix = ROS_REPLY_PREFIX; + reply_suffix = ROS_REPLY_SUFFIX; + topic_prefix = ROS_TOPIC_PREFIX; + break; + case Protocol::DDS: + request_prefix = DDS_REQUEST_PREFIX; + request_suffix = DDS_REQUEST_SUFFIX; + reply_prefix = DDS_REPLY_PREFIX; + reply_suffix = DDS_REPLY_SUFFIX; + // As there are no fastdds actions there is no need to check for feedback/status topics with prefix + break; + default: + EPROSIMA_LOG_ERROR(DDSENABLER_RPC_UTILS, + "Invalid protocol"); + throw std::runtime_error("Invalid protocol"); + } + + std::string base = topic_name; + + // Check for request topics + if ((base.rfind(request_prefix, 0) == 0) && + (base.size() >= request_suffix.length()) && + (base.substr(base.size() - request_suffix.length()) == request_suffix)) + { + base = base.substr(request_prefix.length()); + base = base.substr(0, base.size() - (request_suffix.length())); + service_type = ServiceType::REQUEST; + service_name = base; + + if (base.size() >= (std::strlen(ACTION_GOAL_SUFFIX)) && + base.substr(base.size() - (std::strlen(ACTION_GOAL_SUFFIX))) == ACTION_GOAL_SUFFIX) + { + action_name = base.substr(0, base.size() - (std::strlen(ACTION_GOAL_SUFFIX))); + rpc_type = RpcType::ACTION; + action_type = ActionType::GOAL; + return; + } + else if (base.size() >= (std::strlen(ACTION_RESULT_SUFFIX)) && + base.substr(base.size() - (std::strlen(ACTION_RESULT_SUFFIX))) == ACTION_RESULT_SUFFIX) + { + action_name = base.substr(0, base.size() - (std::strlen(ACTION_RESULT_SUFFIX))); + rpc_type = RpcType::ACTION; + action_type = ActionType::RESULT; + return; + } + else if (base.size() >= (std::strlen(ACTION_CANCEL_SUFFIX)) && + base.substr(base.size() - (std::strlen(ACTION_CANCEL_SUFFIX))) == ACTION_CANCEL_SUFFIX) + { + action_name = base.substr(0, base.size() - (std::strlen(ACTION_CANCEL_SUFFIX))); + rpc_type = RpcType::ACTION; + action_type = ActionType::CANCEL; + return; + } + + rpc_type = RpcType::SERVICE; + return; + } + + // Check for reply topics + if ((base.rfind(reply_prefix, 0) == 0) && + (base.size() >= reply_suffix.length()) && + (base.substr(base.size() - reply_suffix.length()) == reply_suffix)) + { + base = base.substr(reply_prefix.length()); + base = base.substr(0, base.size() - (reply_suffix.length())); + service_type = ServiceType::REPLY; + service_name = base; + + if (base.size() >= (std::strlen(ACTION_GOAL_SUFFIX)) && + base.substr(base.size() - (std::strlen(ACTION_GOAL_SUFFIX))) == ACTION_GOAL_SUFFIX) + { + action_name = base.substr(0, base.size() - (std::strlen(ACTION_GOAL_SUFFIX))); + rpc_type = RpcType::ACTION; + action_type = ActionType::GOAL; + return; + } + else if (base.size() >= (std::strlen(ACTION_RESULT_SUFFIX)) && + base.substr(base.size() - (std::strlen(ACTION_RESULT_SUFFIX))) == ACTION_RESULT_SUFFIX) + { + action_name = base.substr(0, base.size() - (std::strlen(ACTION_RESULT_SUFFIX))); + rpc_type = RpcType::ACTION; + action_type = ActionType::RESULT; + return; + } + else if (base.size() >= (std::strlen(ACTION_CANCEL_SUFFIX)) && + base.substr(base.size() - (std::strlen(ACTION_CANCEL_SUFFIX))) == ACTION_CANCEL_SUFFIX) + { + action_name = base.substr(0, base.size() - (std::strlen(ACTION_CANCEL_SUFFIX))); + rpc_type = RpcType::ACTION; + action_type = ActionType::CANCEL; + return; + } + + rpc_type = RpcType::SERVICE; + return; + } + + // Check for action feedback/status topics + if (Protocol::ROS2 != protocol) // Feedback and Status topics only exist for ROS2 actions + { + return; + } + base = base.substr(topic_prefix.length()); + if (base.size() >= (std::strlen(ACTION_FEEDBACK_SUFFIX) + 1) && + base.substr(base.size() - (std::strlen(ACTION_FEEDBACK_SUFFIX) + 1)) == + (std::string("/") + ACTION_FEEDBACK_SUFFIX)) + { + action_name = base.substr(0, base.size() - std::strlen(ACTION_FEEDBACK_SUFFIX)); + rpc_type = RpcType::ACTION; + action_type = ActionType::FEEDBACK; + return; + } + if (base.size() >= (std::strlen(ACTION_STATUS_SUFFIX) + 1) && + base.substr(base.size() - (std::strlen(ACTION_STATUS_SUFFIX) + 1)) == + (std::string("/") + ACTION_STATUS_SUFFIX)) + { + action_name = base.substr(0, base.size() - (std::strlen(ACTION_STATUS_SUFFIX))); + rpc_type = RpcType::ACTION; + action_type = ActionType::STATUS; + return; + } + + return; +} + +ActionRequestInfo::ActionRequestInfo( + const std::string& _action_name, + ActionType action_type, + uint64_t request_id, + Protocol protocol) + : action_name(_action_name) + , goal_accepted_stamp(std::chrono::system_clock::now()) + , protocol(protocol) +{ + set_request(request_id, action_type); +} + +void ActionRequestInfo::set_request( + uint64_t request_id, + ActionType action_type) +{ + switch (action_type) + { + case ActionType::GOAL: + goal_request_id = request_id; + break; + case ActionType::RESULT: + result_request_id = request_id; + break; + default: + break; + } + return; +} + +uint64_t ActionRequestInfo::get_request( + ActionType action_type) const +{ + switch (action_type) + { + case ActionType::GOAL: + return goal_request_id; + case ActionType::RESULT: + return result_request_id; + default: + throw std::runtime_error("Invalid action type for request retrieval"); + } +} + +bool ActionRequestInfo::set_result( + const std::string&& str) +{ + if (str.empty() || !result.empty()) + { + return false; // Cannot set string if already set or empty + } + result = std::move(str); + return true; +} + +bool ActionRequestInfo::erase( + ActionEraseReason erase_reason) +{ + switch (erase_reason) + { + case ActionEraseReason::FINAL_STATUS: + final_status_received = true; + break; + case ActionEraseReason::RESULT: + result_received = true; + break; + case ActionEraseReason::FORCED: + final_status_received = true; + result_received = true; + break; + } + return final_status_received && result_received; +} + +Protocol ActionRequestInfo::get_protocol() const +{ + return protocol; +} + +ServiceDiscovered::ServiceDiscovered( + const std::string& service_name, + Protocol protocol) + : service_name(service_name) + , protocol(protocol) +{ +} + +bool ServiceDiscovered::add_topic( + const ddspipe::core::types::DdsTopic& topic, + ServiceType service_type) +{ + if (service_type == ServiceType::REQUEST) + { + if (request_discovered) + { + return false; + } + topic_request = topic; + request_discovered = true; + } + else + { + if (reply_discovered) + { + return false; + } + topic_reply = topic; + reply_discovered = true; + } + + if (request_discovered && reply_discovered) + { + if (service_name.empty()) + { + return false; + } + fully_discovered = true; + rpc_topic = std::make_optional( + service_name, + topic_request, + topic_reply); + return true; + } + + return false; +} + +bool ServiceDiscovered::remove_topic( + ServiceType service_type) +{ + if (service_type == ServiceType::REQUEST) + { + request_discovered = false; + topic_request = ddspipe::core::types::DdsTopic(); + } + else + { + reply_discovered = false; + topic_reply = ddspipe::core::types::DdsTopic(); + } + + fully_discovered = false; + rpc_topic = std::nullopt; + + return true; +} + +ddspipe::core::types::RpcTopic ServiceDiscovered::get_service() +{ + if (!fully_discovered || rpc_topic == std::nullopt) + { + throw std::runtime_error("Service " + service_name + " not fully discovered"); + } + return rpc_topic.value(); +} + +bool ServiceDiscovered::get_topic( + ServiceType service_type, + ddspipe::core::types::DdsTopic& topic) +{ + if (service_type == ServiceType::REQUEST) + { + if (!request_discovered) + { + return false; + } + topic = topic_request; + return true; + } + if (service_type == ServiceType::REPLY) + { + if (!reply_discovered) + { + return false; + } + topic = topic_reply; + return true; + } + return false; +} + +Protocol ServiceDiscovered::get_protocol() const +{ + return protocol; +} + +ActionDiscovered::ActionDiscovered( + const std::string& action_name, + Protocol protocol) + : action_name(action_name) + , protocol(protocol) +{ +} + +bool ActionDiscovered::check_fully_discovered() +{ + auto g = goal.lock(); + auto r = result.lock(); + auto c = cancel.lock(); + + if (g && r && c && + g->fully_discovered && r->fully_discovered && c->fully_discovered && + feedback_discovered && status_discovered) + { + fully_discovered = true; + return true; + } + fully_discovered = false; + return false; +} + +bool ActionDiscovered::add_service( + std::shared_ptr service, + ActionType action_type) +{ + switch (action_type) + { + case ActionType::GOAL: + goal = service; + break; + case ActionType::RESULT: + result = service; + break; + case ActionType::CANCEL: + cancel = service; + break; + default: + return false; + } + return true; +} + +bool ActionDiscovered::add_topic( + const ddspipe::core::types::DdsTopic& topic, + ActionType action_type) +{ + switch (action_type) + { + case ActionType::FEEDBACK: + feedback = topic; + feedback_discovered = true; + break; + case ActionType::STATUS: + status = topic; + status_discovered = true; + break; + default: + return false; + } + + return true; +} + +RpcAction ActionDiscovered::get_action() +{ + auto g = goal.lock(); + auto r = result.lock(); + auto c = cancel.lock(); + + if (!fully_discovered || !g || !r || !c) + { + throw std::runtime_error("Action not fully discovered or ServiceDiscovered expired"); + } + + try + { + return RpcAction( + action_name, + g->get_service(), + r->get_service(), + c->get_service(), + feedback, + status); + } + catch (const std::exception& e) + { + throw std::runtime_error("Failed to create action " + action_name + ": " + e.what()); + } +} + +} // namespace participants +} // namespace ddsenabler +} // namespace eprosima diff --git a/ddsenabler_participants/src/cpp/rpc/RpcUtils.cpp b/ddsenabler_participants/src/cpp/rpc/RpcUtils.cpp new file mode 100644 index 00000000..51ebab67 --- /dev/null +++ b/ddsenabler_participants/src/cpp/rpc/RpcUtils.cpp @@ -0,0 +1,175 @@ +// Copyright 2025 Proyectos y Sistemas de Mantenimiento SL (eProsima). +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/** + * @file RpcUtils.cpp + */ + +#include + +#include + +#include + +namespace eprosima { +namespace ddsenabler { +namespace participants { +namespace RpcUtils { + +UUID generate_UUID() +{ + UUID uuid; + utils::RandomManager rm; + + for (size_t i = 0; i < sizeof(uuid); ++i) + { + uuid[i] = static_cast(rm.pure_rand()); + } + return uuid; +} + +std::string create_goal_request_msg( + const std::string& goal_json, + UUID& goal_id) +{ + goal_id = generate_UUID(); + + nlohmann::json j; + j["goal_id"]["uuid"] = goal_id; + j["goal"] = nlohmann::json::parse(goal_json); + return j.dump(4); +} + +std::string create_goal_reply_msg( + bool accepted) +{ + auto now = std::chrono::system_clock::now(); + auto duration_since_epoch = now.time_since_epoch(); + auto sec = std::chrono::duration_cast(duration_since_epoch).count(); + auto nanosec = std::chrono::duration_cast(duration_since_epoch).count() % 1'000'000'000; + + nlohmann::json j; + j["accepted"] = accepted; + j["stamp"]["sec"] = static_cast(sec); + j["stamp"]["nanosec"] = static_cast(nanosec); + return j.dump(4); +} + +std::string create_cancel_request_msg( + const UUID& goal_id, + const int64_t timestamp) +{ + nlohmann::json j; + int64_t sec = timestamp / 1'000'000'000; + uint32_t nanosec = timestamp % 1'000'000'000; + j["goal_info"]["stamp"]["sec"] = static_cast(sec); + j["goal_info"]["stamp"]["nanosec"] = static_cast(nanosec); + j["goal_info"]["goal_id"]["uuid"] = goal_id; + return j.dump(4); +} + +std::string create_cancel_reply_msg( + std::vector> cancelling_goals, + const CancelCode& cancel_code) +{ + nlohmann::json j; + j["return_code"] = cancel_code; + j["goals_canceling"] = nlohmann::json::array(); + for (const auto& [goal_id, timestamp] : cancelling_goals) + { + nlohmann::json goal_json; + goal_json["goal_id"]["uuid"] = goal_id; + auto duration_since_epoch = timestamp.time_since_epoch(); + auto sec = std::chrono::duration_cast(duration_since_epoch).count(); + auto nanosec = std::chrono::duration_cast(duration_since_epoch).count() % + 1'000'000'000; + goal_json["stamp"]["sec"] = static_cast(sec); + goal_json["stamp"]["nanosec"] = static_cast(nanosec); + j["goals_canceling"].push_back(goal_json); + } + return j.dump(4); +} + +std::string create_result_request_msg( + const UUID& goal_id) +{ + nlohmann::json j; + j["goal_id"]["uuid"] = goal_id; + return j.dump(4); +} + +std::string create_result_reply_msg( + const StatusCode& status_code, + const char* json) +{ + nlohmann::json j; + j["status"] = status_code; + j["result"] = nlohmann::json::parse(json); + return j.dump(4); +} + +std::string create_status_msg( + const UUID& goal_id, + const StatusCode& status_code, + std::chrono::system_clock::time_point goal_accepted_stamp) +{ + auto duration_since_epoch = goal_accepted_stamp.time_since_epoch(); + auto sec = std::chrono::duration_cast(duration_since_epoch).count(); + auto nanosec = std::chrono::duration_cast(duration_since_epoch).count() % 1'000'000'000; + + nlohmann::json goal_info; + goal_info["goal_id"]["uuid"] = goal_id; + goal_info["stamp"]["sec"] = static_cast(sec); + goal_info["stamp"]["nanosec"] = static_cast(nanosec); + + + nlohmann::json goal_status; + goal_status["goal_info"] = goal_info; + goal_status["status"] = status_code; + + nlohmann::json j; + j["status_list"] = nlohmann::json::array({goal_status}); + + return j.dump(4); +} + +std::string create_feedback_msg( + const char* json, + const UUID& goal_id) +{ + nlohmann::json j; + j["goal_id"]["uuid"] = goal_id; + j["feedback"] = nlohmann::json::parse(json); + return j.dump(4); +} + +std::ostream& operator <<( + std::ostream& os, + const UUID& uuid) +{ + for (size_t i = 0; i < uuid.size(); ++i) + { + if (i != 0) + { + os << "-"; + } + os << std::to_string(uuid[i]); + } + return os; +} + +} // namespace RpcUtils +} // namespace participants +} // namespace ddsenabler +} // namespace eprosima diff --git a/ddsenabler_participants/test/DdsEnablerParticipantsTest.cpp b/ddsenabler_participants/test/DdsEnablerParticipantsTest.cpp index d37ca79e..434c17b9 100644 --- a/ddsenabler_participants/test/DdsEnablerParticipantsTest.cpp +++ b/ddsenabler_participants/test/DdsEnablerParticipantsTest.cpp @@ -127,8 +127,7 @@ class HandlerTest : public participants::Handler // eprosima::ddsenabler::participants::DdsTopicNotification topic_notification; static void test_topic_notification_callback( const char* topic_name, - const char* type_name, - const char* serialized_qos) + const eprosima::ddsenabler::participants::TopicInfo& topic_info) { if (current_test_instance_ == nullptr) { diff --git a/ddsenabler_test/compose/CMakeLists.txt b/ddsenabler_test/compose/CMakeLists.txt index f855833b..35633614 100644 --- a/ddsenabler_test/compose/CMakeLists.txt +++ b/ddsenabler_test/compose/CMakeLists.txt @@ -18,6 +18,15 @@ set(TESTS publish/requested_type reception/topics_json reception/topics_yaml + services/service_server + services/service_server_multiclient + services/service_client + services/service_client_discovered + actions/action_client + actions/action_client_cancel + actions/action_client_discovered + actions/action_server + actions/action_server_multiclient ) file(COPY ${CMAKE_CURRENT_SOURCE_DIR}/test_cases DESTINATION ${CMAKE_CURRENT_BINARY_DIR}) diff --git a/ddsenabler_test/compose/scripts/execute_and_validate_action_client.py b/ddsenabler_test/compose/scripts/execute_and_validate_action_client.py new file mode 100644 index 00000000..f4f654b8 --- /dev/null +++ b/ddsenabler_test/compose/scripts/execute_and_validate_action_client.py @@ -0,0 +1,187 @@ +# Copyright 2025 Proyectos y Sistemas de Mantenimiento SL (eProsima). +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import argparse +import re +from typing import List, Tuple + +import log + +import validation + +DESCRIPTION = """Script to validate Fibonacci action client output""" +USAGE = ('python3 execute_and_validate_action_client.py ' + '[-s ] [-t ] [-e ] [--delay ] [-d]') + + +def parse_options(): + """ + Parse arguments. + + :return: The arguments parsed. + """ + parser = argparse.ArgumentParser( + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + add_help=True, + description=(DESCRIPTION), + usage=(USAGE) + ) + parser.add_argument( + '-s', + '--samples', + type=int, + default=3, + help='Number of goal requests to send/expect.' + ) + parser.add_argument( + '-t', + '--timeout', + type=int, + default=20, + help='Timeout for the action client application.' + ) + parser.add_argument( + '-e', + '--exe', + type=str, + default='/scripts/ros2_nodes/node_main.py', + help='Path to the action client executable (python entrypoint).' + ) + parser.add_argument( + '--delay', + type=float, + default=0, + help='Time to wait before starting execution.' + ) + parser.add_argument( + '-d', + '--debug', + action='store_true', + help='Print test debugging info.' + ) + + return parser.parse_args() + + +def _client_command(args): + """ + Build the command to execute the client. + + :param args: Arguments parsed + :return: Command to execute the client + """ + command = [ + 'python3', args.exe, + '--action', + '--client', + '--samples', str(args.samples) + ] + + return command + + +# ------------------------- Parsing & Validation ------------------------- # + +def _extract_number_sequences_from_line(line: str) -> List[int]: + """Extract a list of ints from a line, supporting formats like: + - Result { 0,1,1,2 } + - Result: [0, 1, 1, 2] + - result -> 0 1 1 2 + The function returns the first reasonable list of ints found. + """ + # Prefer content inside brackets or braces + for opening, closing in [('[', ']'), ('{', '}')]: + if opening in line and closing in line: + content = line.split(opening, 1)[1].split(closing, 1)[0] + nums = re.findall(r'-?\d+', content) + return [int(n) for n in nums] + + # Otherwise, extract from the tail of the line + tail = line.split(':', 1)[-1] + nums = re.findall(r'-?\d+', tail) + return [int(n) for n in nums] + + +def _client_parse_output(stdout: str, stderr: str) -> Tuple[List[List[int]], str]: + """ + Transform the output of the program into a list of Fibonacci sequences. + + We look for lines containing the word 'Result' (case-insensitive) and + extract number sequences from them. Each sequence corresponds to one goal. + """ + lines = stdout.splitlines() + + sequences: List[List[int]] = [] + for line in lines: + if 'result' in line.lower(): + seq = _extract_number_sequences_from_line(line) + if len(seq) >= 2: # minimal Fibonacci + sequences.append(seq) + + return sequences, stderr + + +def _is_fibonacci(seq: List[int]) -> bool: + """Check that a sequence is a valid Fibonacci progression starting at 0, 1.""" + if len(seq) < 2: + return False + if not (seq[0] == 0 and seq[1] == 1): + return False + for i in range(2, len(seq)): + if seq[i] != seq[i - 1] + seq[i - 2]: + return False + return True + + +def _client_validate(stdout_parsed: List[List[int]], stderr_parsed: str): + # First apply the default validator + ret_code = validation.validate_default(stdout_parsed, stderr_parsed) + if ret_code != validation.ReturnCode.SUCCESS: + return ret_code + + # Ensure we have at least one sequence + if len(stdout_parsed) == 0: + log.logger.error('No result sequences were found in client output.') + return validation.ReturnCode.NOT_VALID_MESSAGES + + # Validate each result sequence as Fibonacci + for seq in stdout_parsed: + if not _is_fibonacci(seq): + log.logger.error(f'Non-Fibonacci sequence detected: {seq}') + return validation.ReturnCode.NOT_VALID_MESSAGES + + return ret_code + + +if __name__ == '__main__': + + # Parse arguments + args = parse_options() + + # Set log level + if args.debug: + log.activate_debug() + + command = _client_command(args) + + ret_code = validation.run_and_validate( + command=command, + timeout=args.timeout, + delay=args.delay, + parse_output_function=_client_parse_output, + validate_output_function=_client_validate) + + log.logger.info(f'Action client validator exited with code {ret_code}') + + exit(ret_code.value) diff --git a/ddsenabler_test/compose/scripts/execute_and_validate_action_server.py b/ddsenabler_test/compose/scripts/execute_and_validate_action_server.py new file mode 100644 index 00000000..9d3c1cbe --- /dev/null +++ b/ddsenabler_test/compose/scripts/execute_and_validate_action_server.py @@ -0,0 +1,121 @@ +# Copyright 2025 Proyectos y Sistemas de Mantenimiento SL (eProsima). +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import argparse + +import log + +import validation + +DESCRIPTION = """Script to validate Fibonacci action server output""" +USAGE = ('python3 execute_and_validate_action_server.py ' + '[-s ] [--expect-cancel] [-t ] [-e ] [--delay ] [-d]') + + +def parse_options(): + """ + Parse arguments. + + :return: The arguments parsed. + """ + parser = argparse.ArgumentParser( + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + add_help=True, + description=(DESCRIPTION), + usage=(USAGE) + ) + parser.add_argument( + '-s', + '--samples', + type=int, + default=3, + help='Number of goals the server must process before exiting.' + ) + parser.add_argument( + '--expect-cancel', + action='store_true', + help='Expect goals to be cancelled; count cancellations towards samples.' + ) + parser.add_argument( + '-t', + '--timeout', + type=int, + default=20, + help='Timeout for the action server application.' + ) + parser.add_argument( + '-e', + '--exe', + type=str, + default='/scripts/ros2_nodes/node_main.py', + help='Path to the action server executable (python entrypoint).' + ) + parser.add_argument( + '--delay', + type=float, + default=0, + help='Time to wait before starting execution.' + ) + parser.add_argument( + '-d', + '--debug', + action='store_true', + help='Print test debugging info.' + ) + + return parser.parse_args() + + +def _server_command(args): + """ + Build the command to execute the server. + + :param args: Arguments parsed + :return: Command to execute the server + """ + command = [ + 'python3', args.exe, + '--action', + '--samples', str(args.samples) + ] + if args.expect_cancel: + command.append('--expect-cancel') + + return command + + +if __name__ == '__main__': + + # Parse arguments + args = parse_options() + + # Set log level + if args.debug: + log.activate_debug() + + command = _server_command(args) + + # Check process exits after serving the expected goals and stderr is empty + ret_code = validation.run_and_validate( + command=command, + timeout=args.timeout, + delay=args.delay, + parse_output_function=validation.parse_default, + validate_output_function=validation.validate_default, + timeout_as_error=True + ) + + log.logger.info(f'Action server validator exited with code {ret_code}') + + exit(ret_code.value) diff --git a/ddsenabler_test/compose/scripts/ros2_nodes/AdditionClient.py b/ddsenabler_test/compose/scripts/ros2_nodes/AdditionClient.py new file mode 100644 index 00000000..ddff425d --- /dev/null +++ b/ddsenabler_test/compose/scripts/ros2_nodes/AdditionClient.py @@ -0,0 +1,115 @@ +# Copyright 2022 Proyectos y Sistemas de Mantenimiento SL (eProsima). +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +ROS 2 Addition Service Client used for testing. +""" + +import random + +from example_interfaces.srv import AddTwoInts + +import rclpy +from rclpy.node import Node + +### +# Required for import ../utils +import inspect +import os +import sys + +currentdir = os.path.dirname( + os.path.abspath(inspect.getfile(inspect.currentframe()))) +parentdir = os.path.dirname(currentdir) +sys.path.insert(0, parentdir) +### + +from utils import print_with_timestamp, sleep_random_time # noqa: E402 + + +class AdditionClient(Node): + + def __init__( + self): + + # Call Node constructor + super().__init__('ClientAddition') + + # Create both clients + self.addition_client = self.create_client( + AddTwoInts, 'add_two_ints') + + print_with_timestamp('Client Addition created.') + + def run( + self, + samples: int, + wait: bool): + """ + Loop until number of samples has been replied. + + :param samples: Number of samples to reply. + + :return: True if all samples have been replied, False otherwise. + """ + print_with_timestamp('Client waiting for server.') + while not self.addition_client.wait_for_service(timeout_sec=1.0): + print_with_timestamp('Server not available yet...') + + print_with_timestamp( + f'Running Client Addition for {samples} samples.') + + while samples > 0: + # Sleep a minimum amount of time + if wait: + sleep_random_time(0.1, 0.2) + + # Generate random numbers + a = random.randint(0, 100) + b = random.randint(0, 100) + + # Send request + result = self._send_request_sync(a, b) + + # Log result + print_with_timestamp( + f'Result {{ {a},{b},{result} }}') + + # Decrement samples + samples -= 1 + + return True + + def _send_request_sync( + self, + a: int, + b: int) -> int: + + """Send addition request and wait for result.""" + # Set request + request = AddTwoInts.Request() + request.a = a + request.b = b + + print_with_timestamp( + f'Request {{ {a},{b} }} sent, waiting for server result.') + + # Send request + self.future = self.addition_client.call_async(request) + + # Wait for response + rclpy.spin_until_future_complete(self, self.future) + + # Return result + return self.future.result().sum diff --git a/ddsenabler_test/compose/scripts/ros2_nodes/AdditionServer.py b/ddsenabler_test/compose/scripts/ros2_nodes/AdditionServer.py new file mode 100644 index 00000000..e18f87be --- /dev/null +++ b/ddsenabler_test/compose/scripts/ros2_nodes/AdditionServer.py @@ -0,0 +1,98 @@ +# Copyright 2022 Proyectos y Sistemas de Mantenimiento SL (eProsima). +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +ROS 2 Add two ints Service Server used for testing. +""" + +import time + +import rclpy +from rclpy.node import Node + +from example_interfaces.srv import AddTwoInts + +### +# Required for import ../utils +import inspect +import os +import sys + +currentdir = os.path.dirname( + os.path.abspath(inspect.getfile(inspect.currentframe()))) +parentdir = os.path.dirname(currentdir) +sys.path.insert(0, parentdir) +### + +from utils import print_with_timestamp, sleep_random_time # noqa: E402 + + +class AdditionServer(Node): + + def __init__( + self): + """Construct default Addition Server.""" + super().__init__('AdditionMicroServer') + self.srv = self.create_service( + AddTwoInts, + 'add_two_ints', + self._addition_service_callback) + + self.samples_replied = 0 + self.wait_ = False + + print_with_timestamp('Server Addition created.') + + def _addition_service_callback( + self, + request: AddTwoInts.Request, + response: AddTwoInts.Response): + """Response to the request received.""" + # Calculate this microservice result + response.sum = request.a + request.b + self.samples_replied += 1 + + # Log server result + print_with_timestamp( + f'Request {{ {request.a} + {request.b} = {response.sum} }}') + + # Sleep a minimum amount of time to simulate a long computation + if self.wait_: + sleep_random_time(0.1, 0.2) + + # return response + return response + + def run( + self, + samples: int, + wait: bool): + """ + Loop until number of samples has been replied. + + :param samples: Number of samples to reply. + + :return: True if all samples have been replied, False otherwise. + """ + print_with_timestamp( + f'Running Server Addition for {samples} samples.') + self.wait_ = wait + + while self.samples_replied < samples: + rclpy.spin_once(self) + + # Sleep some time after last reply sent, in case it needs to be resent + time.sleep(2) + + return True diff --git a/ddsenabler_test/compose/scripts/ros2_nodes/FibonacciClient.py b/ddsenabler_test/compose/scripts/ros2_nodes/FibonacciClient.py new file mode 100644 index 00000000..2ce8423d --- /dev/null +++ b/ddsenabler_test/compose/scripts/ros2_nodes/FibonacciClient.py @@ -0,0 +1,84 @@ +# Copyright 2025 Proyectos y Sistemas de Mantenimiento SL (eProsima). +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +ROS 2 Fibonacci Action Client used for testing. +""" + +import rclpy +from rclpy.node import Node +from rclpy.action import ActionClient + +# Prefer action_tutorials_interfaces to match standard tutorial servers +from action_tutorials_interfaces.action import Fibonacci + +# Required for import ../utils +import inspect +import os +import sys + +currentdir = os.path.dirname( + os.path.abspath(inspect.getfile(inspect.currentframe()))) +parentdir = os.path.dirname(currentdir) +sys.path.insert(0, parentdir) + +from utils import print_with_timestamp, sleep_random_time # noqa: E402 + + +class FibonacciClient(Node): + """Fibonacci Action Client node.""" + + def __init__(self): + super().__init__('FibonacciActionClient') + self._client = ActionClient(self, Fibonacci, 'fibonacci') + print_with_timestamp('Fibonacci Action Client created.') + + def run(self, samples: int, wait: bool): + """Send `samples` goals and wait for their results.""" + print_with_timestamp('Client waiting for action server.') + while not self._client.wait_for_server(timeout_sec=1.0): + print_with_timestamp('Action server not available yet...') + + print_with_timestamp(f'Running Fibonacci Action Client for {samples} goals.') + while samples > 0: + # Optionally wait before sending a goal + if wait: + sleep_random_time(0.05, 0.15) + + sequence = self._send_goal_sync(order=10) + + print_with_timestamp( + 'Result { ' + ','.join(map(str, sequence)) + ' }') + + samples -= 1 + + return True + + def _send_goal_sync(self, order: int): + goal_msg = Fibonacci.Goal() + goal_msg.order = int(order) + + send_future = self._client.send_goal_async(goal_msg) + rclpy.spin_until_future_complete(self, send_future) + goal_handle = send_future.result() + + if not goal_handle.accepted: + print_with_timestamp('Goal rejected') + return [] + + get_result_future = goal_handle.get_result_async() + rclpy.spin_until_future_complete(self, get_result_future) + result = get_result_future.result().result + + return list(result.sequence) diff --git a/ddsenabler_test/compose/scripts/ros2_nodes/FibonacciServer.py b/ddsenabler_test/compose/scripts/ros2_nodes/FibonacciServer.py new file mode 100644 index 00000000..49c587f9 --- /dev/null +++ b/ddsenabler_test/compose/scripts/ros2_nodes/FibonacciServer.py @@ -0,0 +1,148 @@ +# Copyright 2025 Proyectos y Sistemas de Mantenimiento SL (eProsima). +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +ROS 2 Fibonacci Action Server used for testing. +""" + +import time + +from rclpy.node import Node +from rclpy.action import ActionServer, CancelResponse, GoalResponse +from rclpy.callback_groups import ReentrantCallbackGroup +from rclpy.executors import MultiThreadedExecutor + +# Prefer action_tutorials_interfaces to match standard tutorial servers +from action_tutorials_interfaces.action import Fibonacci + +import inspect +import os +import sys + +currentdir = os.path.dirname( + os.path.abspath(inspect.getfile(inspect.currentframe()))) +parentdir = os.path.dirname(currentdir) +sys.path.insert(0, parentdir) + +from utils import print_with_timestamp # noqa: E402 + + +class FibonacciServer(Node): + """Fibonacci Action Server node.""" + + def __init__(self): + super().__init__('FibonacciActionServer') + + self._cb_group = ReentrantCallbackGroup() + + self._action_server = ActionServer( + self, + Fibonacci, + 'fibonacci', + self.execute_callback, + goal_callback=self.goal_callback, + cancel_callback=self.cancel_callback, + callback_group=self._cb_group) + + self.goals_executed = 0 + self.goals_canceled = 0 + self.expect_cancel_ = False + + print_with_timestamp('Fibonacci Action Server created.') + + def goal_callback(self, goal_request: Fibonacci.Goal): + if goal_request.order < 0: + return GoalResponse.REJECT + return GoalResponse.ACCEPT + + def cancel_callback(self, goal_handle): + return CancelResponse.ACCEPT + + async def execute_callback(self, goal_handle): + """Execute received goal to compute Fibonacci sequence.""" + order = int(goal_handle.request.order) + + # Build Fibonacci sequence + sequence = [] + if order <= 0: + sequence = [] + elif order == 1: + sequence = [0] + else: + sequence = [0, 1] + feedback_msg = Fibonacci.Feedback() + for i in range(2, order): + # If cancel requested, cancel goal and return partial sequence + if goal_handle.is_cancel_requested: + goal_handle.canceled() + self.goals_canceled += 1 + result = Fibonacci.Result() + result.sequence = sequence + print_with_timestamp( + 'Goal canceled with partial { ' + ','.join(map(str, sequence)) + ' }') + return result + sequence.append(sequence[i - 1] + sequence[i - 2]) + # Publish feedback so clients can track progress + feedback_msg.partial_sequence = sequence.copy() + goal_handle.publish_feedback(feedback_msg) + time.sleep(1) + + if goal_handle.is_cancel_requested: + goal_handle.canceled() + self.goals_canceled += 1 + result = Fibonacci.Result() + result.sequence = sequence + print_with_timestamp( + 'Goal canceled with partial { ' + ','.join(map(str, sequence)) + ' }') + return result + + goal_handle.succeed() + result = Fibonacci.Result() + result.sequence = sequence + + self.goals_executed += 1 + + print_with_timestamp( + 'Result { ' + ','.join(map(str, sequence)) + ' }') + + return result + + def run(self, samples: int, wait: bool, expect_cancel: bool = False): + """Spin until a number of goals have been processed. + + If expect_cancel is True, count canceled goals towards the target. + Otherwise, count successfully executed goals. + """ + self.expect_cancel_ = expect_cancel + print_with_timestamp( + f'Running Fibonacci Action Server, waiting for {samples} ' + + ('canceled' if self.expect_cancel_ else 'executed') + ' goals.') + + executor = MultiThreadedExecutor(num_threads=2) + executor.add_node(self) + + try: + while True: + executor.spin_once(timeout_sec=0.1) + if self.expect_cancel_: + if self.goals_canceled >= samples: + break + else: + if self.goals_executed >= samples: + break + finally: + executor.shutdown() + + time.sleep(0.5) + return True diff --git a/ddsenabler_test/compose/scripts/ros2_nodes/node_main.py b/ddsenabler_test/compose/scripts/ros2_nodes/node_main.py index 10f4b5a8..4faa7249 100644 --- a/ddsenabler_test/compose/scripts/ros2_nodes/node_main.py +++ b/ddsenabler_test/compose/scripts/ros2_nodes/node_main.py @@ -14,13 +14,16 @@ import argparse +# Import service and action nodes lazily to keep optional deps from AdditionClient import AdditionClient from AdditionServer import AdditionServer +from FibonacciClient import FibonacciClient +from FibonacciServer import FibonacciServer import rclpy -DESCRIPTION = """Script to Execute a ROS2 Service Node""" -USAGE = ('python3 node_main.py [-s] [-s 15] [-a]') +DESCRIPTION = """Script to Execute a ROS2 Node (Service or Action)""" +USAGE = ('python3 node_main.py [--action] [--client] [-s 10] [-w]') def parse_options(): @@ -35,13 +38,26 @@ def parse_options(): description=(DESCRIPTION), usage=(USAGE) ) - required_args = parser.add_argument_group('required arguments') + required_args = parser.add_argument_group('role') required_args.add_argument( '-c', '--client', action='store_true', help='Execute Client instead of Server.' ) + parser.add_argument( + '--action', + action='store_true', + help='Run Action node instead of Service node.' + ) + parser.add_argument( + '--expect-cancel', + action='store_true', + help=( + 'In Action server mode, expect goals to be cancelled and count ' + 'cancellations towards samples.' + ) + ) parser.add_argument( '-s', '--samples', @@ -75,13 +91,23 @@ def main(): # Create Server or Client node = None - if args.client: - node = AdditionClient() + if args.action: + if args.client: + node = FibonacciClient() + else: + node = FibonacciServer() else: - node = AdditionServer() + if args.client: + node = AdditionClient() + else: + node = AdditionServer() # Run nodes until finish - result = node.run(args.samples, args.wait) + if args.action and not args.client: + # Action server: pass expect-cancel behavior + result = node.run(args.samples, args.wait, args.expect_cancel) + else: + result = node.run(args.samples, args.wait) if not result: exit(1) diff --git a/ddsenabler_test/compose/test_cases/actions/action_client/compose.yml b/ddsenabler_test/compose/test_cases/actions/action_client/compose.yml new file mode 100644 index 00000000..33d24b88 --- /dev/null +++ b/ddsenabler_test/compose/test_cases/actions/action_client/compose.yml @@ -0,0 +1,34 @@ +# Test description: +# Run ddsenabler example action client against a Fibonacci action server. +# The action client sends 3 consecutive goal requests and expects to receive +# the corresponding feedbacks, status and results. +# Domain 40 is set for ROS 2 nodes. + +services: + ddsenabler: + image: ${DDSENABLER_COMPOSE_TEST_DOCKER_IMAGE} + container_name: ddsenabler + networks: + - std_net + volumes: + - ./config.yml:/config/config.yml + - ../persistence:/persistence + command: ./install/ddsenabler/examples/action/ddsenabler_example_action client --persistence-path /persistence --goals-path /persistence/goals --config /config/config.yml --request-initial-wait 2 + + fibonacci_server: + image: ${DDSENABLER_COMPOSE_TEST_ROS2_DOCKER_IMAGE} + container_name: fibonacci_server + depends_on: + - ddsenabler + networks: + - std_net + environment: + - ROS_DOMAIN_ID=40 + volumes: + - ../../../scripts:/scripts + command: python3 /scripts/execute_and_validate_action_server.py --timeout 20 --samples 3 + +networks: + std_net: + default: + driver: none diff --git a/ddsenabler_test/compose/test_cases/actions/action_client/config.yml b/ddsenabler_test/compose/test_cases/actions/action_client/config.yml new file mode 100644 index 00000000..820ec0f8 --- /dev/null +++ b/ddsenabler_test/compose/test_cases/actions/action_client/config.yml @@ -0,0 +1,2 @@ +dds: + domain: 40 diff --git a/ddsenabler_test/compose/test_cases/actions/action_client_cancel/compose.yml b/ddsenabler_test/compose/test_cases/actions/action_client_cancel/compose.yml new file mode 100644 index 00000000..8379befd --- /dev/null +++ b/ddsenabler_test/compose/test_cases/actions/action_client_cancel/compose.yml @@ -0,0 +1,33 @@ +# Test description: +# Run ddsenabler example action client with the cancel cli option against a Fibonacci action server. +# The action client sends a goal request and then it cancels it. +# All containers use domain 41. + +services: + ddsenabler: + image: ${DDSENABLER_COMPOSE_TEST_DOCKER_IMAGE} + container_name: ddsenabler + networks: + - std_net + volumes: + - ./config.yml:/config/config.yml + - ../persistence:/persistence + command: ./install/ddsenabler/examples/action/ddsenabler_example_action client --persistence-path /persistence --goals-path /persistence/goals --config /config/config.yml --request-initial-wait 2 --cancel-requests true + + fibonacci_server: + image: ${DDSENABLER_COMPOSE_TEST_ROS2_DOCKER_IMAGE} + container_name: fibonacci_server + depends_on: + - ddsenabler + networks: + - std_net + environment: + - ROS_DOMAIN_ID=41 + volumes: + - ../../../scripts:/scripts + command: python3 /scripts/execute_and_validate_action_server.py --timeout 20 --samples 3 --expect-cancel + +networks: + std_net: + default: + driver: none diff --git a/ddsenabler_test/compose/test_cases/actions/action_client_cancel/config.yml b/ddsenabler_test/compose/test_cases/actions/action_client_cancel/config.yml new file mode 100644 index 00000000..203583b5 --- /dev/null +++ b/ddsenabler_test/compose/test_cases/actions/action_client_cancel/config.yml @@ -0,0 +1,2 @@ +dds: + domain: 41 diff --git a/ddsenabler_test/compose/test_cases/actions/action_client_discovered/compose.yml b/ddsenabler_test/compose/test_cases/actions/action_client_discovered/compose.yml new file mode 100644 index 00000000..851c2f60 --- /dev/null +++ b/ddsenabler_test/compose/test_cases/actions/action_client_discovered/compose.yml @@ -0,0 +1,35 @@ +# Test description: +# Run ddsenabler example action client against an already running Fibonacci action server. +# No persistence path is used as the types are discovered at runtime. +# The action client sends 3 consecutive goal requests and expects to receive +# the corresponding feedbacks, status and results. +# Domain 45 is set for ROS 2 nodes. + +services: + fibonacci_server: + image: ${DDSENABLER_COMPOSE_TEST_ROS2_DOCKER_IMAGE} + container_name: fibonacci_server + networks: + - std_net + environment: + - ROS_DOMAIN_ID=45 + volumes: + - ../../../scripts:/scripts + command: python3 /scripts/execute_and_validate_action_server.py --timeout 20 --samples 3 + + ddsenabler: + image: ${DDSENABLER_COMPOSE_TEST_DOCKER_IMAGE} + container_name: ddsenabler + depends_on: + - fibonacci_server + networks: + - std_net + volumes: + - ./config.yml:/config/config.yml + - ../persistence:/persistence + command: ./install/ddsenabler/examples/action/ddsenabler_example_action client --goals-path /persistence/goals --config /config/config.yml --request-initial-wait 2 + +networks: + std_net: + default: + driver: none diff --git a/ddsenabler_test/compose/test_cases/actions/action_client_discovered/config.yml b/ddsenabler_test/compose/test_cases/actions/action_client_discovered/config.yml new file mode 100644 index 00000000..4228342f --- /dev/null +++ b/ddsenabler_test/compose/test_cases/actions/action_client_discovered/config.yml @@ -0,0 +1,2 @@ +dds: + domain: 45 diff --git a/ddsenabler_test/compose/test_cases/actions/action_server/compose.yml b/ddsenabler_test/compose/test_cases/actions/action_server/compose.yml new file mode 100644 index 00000000..f9402a00 --- /dev/null +++ b/ddsenabler_test/compose/test_cases/actions/action_server/compose.yml @@ -0,0 +1,32 @@ +# Test description: +# Run ddsenabler example action server and a Fibonacci client sending goals. +# Domain 42 is used by all containers. + +services: + ddsenabler: + image: ${DDSENABLER_COMPOSE_TEST_DOCKER_IMAGE} + container_name: ddsenabler + networks: + - std_net + volumes: + - ./config.yml:/config/config.yml + - ../persistence:/persistence + command: ./install/ddsenabler/examples/action/ddsenabler_example_action server --persistence-path /persistence --goals-path /persistence/goals --config /config/config.yml --expected-requests 3 + + fibonacci_client: + image: ${DDSENABLER_COMPOSE_TEST_ROS2_DOCKER_IMAGE} + container_name: fibonacci_client_1 + depends_on: + - ddsenabler + networks: + - std_net + environment: + - ROS_DOMAIN_ID=42 + volumes: + - ../../../scripts:/scripts + command: python3 /scripts/execute_and_validate_action_client.py --timeout 20 --samples 3 + +networks: + std_net: + default: + driver: none diff --git a/ddsenabler_test/compose/test_cases/actions/action_server/config.yml b/ddsenabler_test/compose/test_cases/actions/action_server/config.yml new file mode 100644 index 00000000..eff135c7 --- /dev/null +++ b/ddsenabler_test/compose/test_cases/actions/action_server/config.yml @@ -0,0 +1,2 @@ +dds: + domain: 42 diff --git a/ddsenabler_test/compose/test_cases/actions/action_server_multiclient/compose.yml b/ddsenabler_test/compose/test_cases/actions/action_server_multiclient/compose.yml new file mode 100644 index 00000000..621902e6 --- /dev/null +++ b/ddsenabler_test/compose/test_cases/actions/action_server_multiclient/compose.yml @@ -0,0 +1,60 @@ +# Test description: +# Run ddsenabler example action server and 3 Fibonacci clients sending each one 3 goals. +# Domain 43 is used by all containers. + +services: + ddsenabler: + image: ${DDSENABLER_COMPOSE_TEST_DOCKER_IMAGE} + container_name: ddsenabler + networks: + - std_net + volumes: + - ./config.yml:/config/config.yml + - ../persistence:/persistence + command: ./install/ddsenabler/examples/action/ddsenabler_example_action server --persistence-path /persistence --goals-path /persistence/goals --config /config/config.yml --expected-requests 9 + + fibonacci_client_1: + image: ${DDSENABLER_COMPOSE_TEST_ROS2_DOCKER_IMAGE} + container_name: fibonacci_client_1 + depends_on: + - ddsenabler + networks: + - std_net + environment: + - ROS_DOMAIN_ID=43 + volumes: + - ../../../scripts:/scripts + command: python3 /scripts/execute_and_validate_action_client.py --timeout 40 --samples 3 + + fibonacci_client_2: + image: ${DDSENABLER_COMPOSE_TEST_ROS2_DOCKER_IMAGE} + container_name: fibonacci_client_2 + depends_on: + - ddsenabler + networks: + - std_net + environment: + - ROS_DOMAIN_ID=43 + volumes: + - ../../../scripts:/scripts + command: python3 /scripts/execute_and_validate_action_client.py --timeout 40 --samples 3 + + fibonacci_client_3: + image: ${DDSENABLER_COMPOSE_TEST_ROS2_DOCKER_IMAGE} + container_name: fibonacci_client_3 + depends_on: + - ddsenabler + networks: + - std_net + environment: + - ROS_DOMAIN_ID=43 + volumes: + - ../../../scripts:/scripts + command: python3 /scripts/execute_and_validate_action_client.py --timeout 40 --samples 3 + + + +networks: + std_net: + default: + driver: none diff --git a/ddsenabler_test/compose/test_cases/actions/action_server_multiclient/config.yml b/ddsenabler_test/compose/test_cases/actions/action_server_multiclient/config.yml new file mode 100644 index 00000000..09131445 --- /dev/null +++ b/ddsenabler_test/compose/test_cases/actions/action_server_multiclient/config.yml @@ -0,0 +1,2 @@ +dds: + domain: 43 diff --git a/ddsenabler_test/compose/test_cases/actions/persistence/actions/fibonacci__action_ b/ddsenabler_test/compose/test_cases/actions/persistence/actions/fibonacci__action_ new file mode 100644 index 00000000..22db6867 --- /dev/null +++ b/ddsenabler_test/compose/test_cases/actions/persistence/actions/fibonacci__action_ @@ -0,0 +1,19 @@ +{ + "action_name": "fibonacci/_action/", + "cancel_reply_action_serialized_qos": "reliability: true\ndurability: false\nownership: false\nkeyed: false", + "cancel_reply_action_type": "action_msgs::srv::dds_::CancelGoal_Response_", + "cancel_request_action_serialized_qos": "reliability: true\ndurability: false\nownership: false\nkeyed: false", + "cancel_request_action_type": "action_msgs::srv::dds_::CancelGoal_Request_", + "feedback_action_serialized_qos": "reliability: true\ndurability: false\nownership: false\nkeyed: false", + "feedback_action_type": "action_tutorials_interfaces::action::dds_::Fibonacci_FeedbackMessage_", + "goal_reply_action_serialized_qos": "reliability: true\ndurability: false\nownership: false\nkeyed: false", + "goal_reply_action_type": "action_tutorials_interfaces::action::dds_::Fibonacci_SendGoal_Response_", + "goal_request_action_serialized_qos": "reliability: true\ndurability: false\nownership: false\nkeyed: false", + "goal_request_action_type": "action_tutorials_interfaces::action::dds_::Fibonacci_SendGoal_Request_", + "result_reply_action_serialized_qos": "reliability: true\ndurability: false\nownership: false\nkeyed: false", + "result_reply_action_type": "action_tutorials_interfaces::action::dds_::Fibonacci_GetResult_Response_", + "result_request_action_serialized_qos": "reliability: true\ndurability: false\nownership: false\nkeyed: false", + "result_request_action_type": "action_tutorials_interfaces::action::dds_::Fibonacci_GetResult_Request_", + "status_action_serialized_qos": "reliability: true\ndurability: true\nownership: false\nkeyed: false", + "status_action_type": "action_msgs::msg::dds_::GoalStatusArray_" +} \ No newline at end of file diff --git a/ddsenabler_test/compose/test_cases/actions/persistence/goals/1 b/ddsenabler_test/compose/test_cases/actions/persistence/goals/1 new file mode 100644 index 00000000..8402bb25 --- /dev/null +++ b/ddsenabler_test/compose/test_cases/actions/persistence/goals/1 @@ -0,0 +1,3 @@ +{ + "order": 3 +} \ No newline at end of file diff --git a/ddsenabler_test/compose/test_cases/actions/persistence/goals/2 b/ddsenabler_test/compose/test_cases/actions/persistence/goals/2 new file mode 100644 index 00000000..461fa735 --- /dev/null +++ b/ddsenabler_test/compose/test_cases/actions/persistence/goals/2 @@ -0,0 +1,3 @@ +{ + "order": 5 +} \ No newline at end of file diff --git a/ddsenabler_test/compose/test_cases/actions/persistence/goals/3 b/ddsenabler_test/compose/test_cases/actions/persistence/goals/3 new file mode 100644 index 00000000..853b8317 --- /dev/null +++ b/ddsenabler_test/compose/test_cases/actions/persistence/goals/3 @@ -0,0 +1,3 @@ +{ + "order": 7 +} \ No newline at end of file diff --git a/ddsenabler_test/compose/test_cases/actions/persistence/types/action_msgs__msg__dds___GoalStatusArray_.bin b/ddsenabler_test/compose/test_cases/actions/persistence/types/action_msgs__msg__dds___GoalStatusArray_.bin new file mode 100644 index 00000000..9b2666b4 Binary files /dev/null and b/ddsenabler_test/compose/test_cases/actions/persistence/types/action_msgs__msg__dds___GoalStatusArray_.bin differ diff --git a/ddsenabler_test/compose/test_cases/actions/persistence/types/action_msgs__srv__dds___CancelGoal_Request_.bin b/ddsenabler_test/compose/test_cases/actions/persistence/types/action_msgs__srv__dds___CancelGoal_Request_.bin new file mode 100644 index 00000000..3312fb7d Binary files /dev/null and b/ddsenabler_test/compose/test_cases/actions/persistence/types/action_msgs__srv__dds___CancelGoal_Request_.bin differ diff --git a/ddsenabler_test/compose/test_cases/actions/persistence/types/action_msgs__srv__dds___CancelGoal_Response_.bin b/ddsenabler_test/compose/test_cases/actions/persistence/types/action_msgs__srv__dds___CancelGoal_Response_.bin new file mode 100644 index 00000000..9b9e0b42 Binary files /dev/null and b/ddsenabler_test/compose/test_cases/actions/persistence/types/action_msgs__srv__dds___CancelGoal_Response_.bin differ diff --git a/ddsenabler_test/compose/test_cases/actions/persistence/types/action_tutorials_interfaces__action__dds___Fibonacci_FeedbackMessage_.bin b/ddsenabler_test/compose/test_cases/actions/persistence/types/action_tutorials_interfaces__action__dds___Fibonacci_FeedbackMessage_.bin new file mode 100644 index 00000000..fac6aee9 Binary files /dev/null and b/ddsenabler_test/compose/test_cases/actions/persistence/types/action_tutorials_interfaces__action__dds___Fibonacci_FeedbackMessage_.bin differ diff --git a/ddsenabler_test/compose/test_cases/actions/persistence/types/action_tutorials_interfaces__action__dds___Fibonacci_GetResult_Request_.bin b/ddsenabler_test/compose/test_cases/actions/persistence/types/action_tutorials_interfaces__action__dds___Fibonacci_GetResult_Request_.bin new file mode 100644 index 00000000..423f3111 Binary files /dev/null and b/ddsenabler_test/compose/test_cases/actions/persistence/types/action_tutorials_interfaces__action__dds___Fibonacci_GetResult_Request_.bin differ diff --git a/ddsenabler_test/compose/test_cases/actions/persistence/types/action_tutorials_interfaces__action__dds___Fibonacci_GetResult_Response_.bin b/ddsenabler_test/compose/test_cases/actions/persistence/types/action_tutorials_interfaces__action__dds___Fibonacci_GetResult_Response_.bin new file mode 100644 index 00000000..62adda76 Binary files /dev/null and b/ddsenabler_test/compose/test_cases/actions/persistence/types/action_tutorials_interfaces__action__dds___Fibonacci_GetResult_Response_.bin differ diff --git a/ddsenabler_test/compose/test_cases/actions/persistence/types/action_tutorials_interfaces__action__dds___Fibonacci_SendGoal_Request_.bin b/ddsenabler_test/compose/test_cases/actions/persistence/types/action_tutorials_interfaces__action__dds___Fibonacci_SendGoal_Request_.bin new file mode 100644 index 00000000..4bffcfdd Binary files /dev/null and b/ddsenabler_test/compose/test_cases/actions/persistence/types/action_tutorials_interfaces__action__dds___Fibonacci_SendGoal_Request_.bin differ diff --git a/ddsenabler_test/compose/test_cases/actions/persistence/types/action_tutorials_interfaces__action__dds___Fibonacci_SendGoal_Response_.bin b/ddsenabler_test/compose/test_cases/actions/persistence/types/action_tutorials_interfaces__action__dds___Fibonacci_SendGoal_Response_.bin new file mode 100644 index 00000000..63bb3f25 Binary files /dev/null and b/ddsenabler_test/compose/test_cases/actions/persistence/types/action_tutorials_interfaces__action__dds___Fibonacci_SendGoal_Response_.bin differ diff --git a/ddsenabler_test/compose/test_cases/actions/persistence/types/example_interfaces__srv__dds___AddTwoInts_Request_.bin b/ddsenabler_test/compose/test_cases/actions/persistence/types/example_interfaces__srv__dds___AddTwoInts_Request_.bin new file mode 100644 index 00000000..4b4e5e7d Binary files /dev/null and b/ddsenabler_test/compose/test_cases/actions/persistence/types/example_interfaces__srv__dds___AddTwoInts_Request_.bin differ diff --git a/ddsenabler_test/compose/test_cases/actions/persistence/types/example_interfaces__srv__dds___AddTwoInts_Response_.bin b/ddsenabler_test/compose/test_cases/actions/persistence/types/example_interfaces__srv__dds___AddTwoInts_Response_.bin new file mode 100644 index 00000000..6f5f40ef Binary files /dev/null and b/ddsenabler_test/compose/test_cases/actions/persistence/types/example_interfaces__srv__dds___AddTwoInts_Response_.bin differ diff --git a/ddsenabler_test/compose/test_cases/actions/persistence/types/rcl_interfaces__msg__dds___Log_.bin b/ddsenabler_test/compose/test_cases/actions/persistence/types/rcl_interfaces__msg__dds___Log_.bin new file mode 100644 index 00000000..6e56d56a Binary files /dev/null and b/ddsenabler_test/compose/test_cases/actions/persistence/types/rcl_interfaces__msg__dds___Log_.bin differ diff --git a/ddsenabler_test/compose/test_cases/actions/persistence/types/rcl_interfaces__msg__dds___ParameterEvent_.bin b/ddsenabler_test/compose/test_cases/actions/persistence/types/rcl_interfaces__msg__dds___ParameterEvent_.bin new file mode 100644 index 00000000..98649ae7 Binary files /dev/null and b/ddsenabler_test/compose/test_cases/actions/persistence/types/rcl_interfaces__msg__dds___ParameterEvent_.bin differ diff --git a/ddsenabler_test/compose/test_cases/actions/persistence/types/rcl_interfaces__srv__dds___DescribeParameters_Request_.bin b/ddsenabler_test/compose/test_cases/actions/persistence/types/rcl_interfaces__srv__dds___DescribeParameters_Request_.bin new file mode 100644 index 00000000..d33a48d9 Binary files /dev/null and b/ddsenabler_test/compose/test_cases/actions/persistence/types/rcl_interfaces__srv__dds___DescribeParameters_Request_.bin differ diff --git a/ddsenabler_test/compose/test_cases/actions/persistence/types/rcl_interfaces__srv__dds___DescribeParameters_Response_.bin b/ddsenabler_test/compose/test_cases/actions/persistence/types/rcl_interfaces__srv__dds___DescribeParameters_Response_.bin new file mode 100644 index 00000000..f7a41219 Binary files /dev/null and b/ddsenabler_test/compose/test_cases/actions/persistence/types/rcl_interfaces__srv__dds___DescribeParameters_Response_.bin differ diff --git a/ddsenabler_test/compose/test_cases/actions/persistence/types/rcl_interfaces__srv__dds___GetParameterTypes_Request_.bin b/ddsenabler_test/compose/test_cases/actions/persistence/types/rcl_interfaces__srv__dds___GetParameterTypes_Request_.bin new file mode 100644 index 00000000..f1cf3f19 Binary files /dev/null and b/ddsenabler_test/compose/test_cases/actions/persistence/types/rcl_interfaces__srv__dds___GetParameterTypes_Request_.bin differ diff --git a/ddsenabler_test/compose/test_cases/actions/persistence/types/rcl_interfaces__srv__dds___GetParameterTypes_Response_.bin b/ddsenabler_test/compose/test_cases/actions/persistence/types/rcl_interfaces__srv__dds___GetParameterTypes_Response_.bin new file mode 100644 index 00000000..55815c1f Binary files /dev/null and b/ddsenabler_test/compose/test_cases/actions/persistence/types/rcl_interfaces__srv__dds___GetParameterTypes_Response_.bin differ diff --git a/ddsenabler_test/compose/test_cases/actions/persistence/types/rcl_interfaces__srv__dds___GetParameters_Request_.bin b/ddsenabler_test/compose/test_cases/actions/persistence/types/rcl_interfaces__srv__dds___GetParameters_Request_.bin new file mode 100644 index 00000000..fe10b8fb Binary files /dev/null and b/ddsenabler_test/compose/test_cases/actions/persistence/types/rcl_interfaces__srv__dds___GetParameters_Request_.bin differ diff --git a/ddsenabler_test/compose/test_cases/actions/persistence/types/rcl_interfaces__srv__dds___GetParameters_Response_.bin b/ddsenabler_test/compose/test_cases/actions/persistence/types/rcl_interfaces__srv__dds___GetParameters_Response_.bin new file mode 100644 index 00000000..ac082bff Binary files /dev/null and b/ddsenabler_test/compose/test_cases/actions/persistence/types/rcl_interfaces__srv__dds___GetParameters_Response_.bin differ diff --git a/ddsenabler_test/compose/test_cases/actions/persistence/types/rcl_interfaces__srv__dds___ListParameters_Request_.bin b/ddsenabler_test/compose/test_cases/actions/persistence/types/rcl_interfaces__srv__dds___ListParameters_Request_.bin new file mode 100644 index 00000000..9b9387d7 Binary files /dev/null and b/ddsenabler_test/compose/test_cases/actions/persistence/types/rcl_interfaces__srv__dds___ListParameters_Request_.bin differ diff --git a/ddsenabler_test/compose/test_cases/actions/persistence/types/rcl_interfaces__srv__dds___ListParameters_Response_.bin b/ddsenabler_test/compose/test_cases/actions/persistence/types/rcl_interfaces__srv__dds___ListParameters_Response_.bin new file mode 100644 index 00000000..a1212fb4 Binary files /dev/null and b/ddsenabler_test/compose/test_cases/actions/persistence/types/rcl_interfaces__srv__dds___ListParameters_Response_.bin differ diff --git a/ddsenabler_test/compose/test_cases/actions/persistence/types/rcl_interfaces__srv__dds___SetParametersAtomically_Request_.bin b/ddsenabler_test/compose/test_cases/actions/persistence/types/rcl_interfaces__srv__dds___SetParametersAtomically_Request_.bin new file mode 100644 index 00000000..c19eeb6a Binary files /dev/null and b/ddsenabler_test/compose/test_cases/actions/persistence/types/rcl_interfaces__srv__dds___SetParametersAtomically_Request_.bin differ diff --git a/ddsenabler_test/compose/test_cases/actions/persistence/types/rcl_interfaces__srv__dds___SetParametersAtomically_Response_.bin b/ddsenabler_test/compose/test_cases/actions/persistence/types/rcl_interfaces__srv__dds___SetParametersAtomically_Response_.bin new file mode 100644 index 00000000..1043f036 Binary files /dev/null and b/ddsenabler_test/compose/test_cases/actions/persistence/types/rcl_interfaces__srv__dds___SetParametersAtomically_Response_.bin differ diff --git a/ddsenabler_test/compose/test_cases/actions/persistence/types/rcl_interfaces__srv__dds___SetParameters_Request_.bin b/ddsenabler_test/compose/test_cases/actions/persistence/types/rcl_interfaces__srv__dds___SetParameters_Request_.bin new file mode 100644 index 00000000..da5208da Binary files /dev/null and b/ddsenabler_test/compose/test_cases/actions/persistence/types/rcl_interfaces__srv__dds___SetParameters_Request_.bin differ diff --git a/ddsenabler_test/compose/test_cases/actions/persistence/types/rcl_interfaces__srv__dds___SetParameters_Response_.bin b/ddsenabler_test/compose/test_cases/actions/persistence/types/rcl_interfaces__srv__dds___SetParameters_Response_.bin new file mode 100644 index 00000000..7b8a8ec8 Binary files /dev/null and b/ddsenabler_test/compose/test_cases/actions/persistence/types/rcl_interfaces__srv__dds___SetParameters_Response_.bin differ diff --git a/ddsenabler_test/compose/test_cases/actions/persistence/types/rmw_dds_common__msg__dds___ParticipantEntitiesInfo_.bin b/ddsenabler_test/compose/test_cases/actions/persistence/types/rmw_dds_common__msg__dds___ParticipantEntitiesInfo_.bin new file mode 100644 index 00000000..c3236d96 Binary files /dev/null and b/ddsenabler_test/compose/test_cases/actions/persistence/types/rmw_dds_common__msg__dds___ParticipantEntitiesInfo_.bin differ diff --git a/ddsenabler_test/compose/test_cases/actions/persistence/types/type_description_interfaces__srv__dds___GetTypeDescription_Request_.bin b/ddsenabler_test/compose/test_cases/actions/persistence/types/type_description_interfaces__srv__dds___GetTypeDescription_Request_.bin new file mode 100644 index 00000000..ba4d778e Binary files /dev/null and b/ddsenabler_test/compose/test_cases/actions/persistence/types/type_description_interfaces__srv__dds___GetTypeDescription_Request_.bin differ diff --git a/ddsenabler_test/compose/test_cases/actions/persistence/types/type_description_interfaces__srv__dds___GetTypeDescription_Response_.bin b/ddsenabler_test/compose/test_cases/actions/persistence/types/type_description_interfaces__srv__dds___GetTypeDescription_Response_.bin new file mode 100644 index 00000000..f68b4410 Binary files /dev/null and b/ddsenabler_test/compose/test_cases/actions/persistence/types/type_description_interfaces__srv__dds___GetTypeDescription_Response_.bin differ diff --git a/ddsenabler_test/compose/test_cases/publish/discovered_type/compose.yml b/ddsenabler_test/compose/test_cases/publish/discovered_type/compose.yml index 4e6a437f..1c4fc126 100644 --- a/ddsenabler_test/compose/test_cases/publish/discovered_type/compose.yml +++ b/ddsenabler_test/compose/test_cases/publish/discovered_type/compose.yml @@ -21,7 +21,7 @@ services: volumes: - ./config.yml:/config/config.yml - ./samples:/samples - command: ./build/ddsenabler/examples/ddsenabler_example --config /config/config.yml --timeout 5 --expected-types 1 --expected-topics 1 --publish-path /samples --publish-topic rt/chatter --publish-period 200 --publish-initial-wait 2000 + command: ./install/ddsenabler/examples/publish/ddsenabler_example_publish --config /config/config.yml --timeout 5 --expected-types 1 --expected-topics 1 --publish-path /samples --publish-topic rt/chatter --publish-period 200 --publish-initial-wait 2000 listener: image: ${DDSENABLER_COMPOSE_TEST_ROS2_DOCKER_IMAGE} diff --git a/ddsenabler_test/compose/test_cases/publish/requested_type/compose.yml b/ddsenabler_test/compose/test_cases/publish/requested_type/compose.yml index ef716078..a5d2002f 100644 --- a/ddsenabler_test/compose/test_cases/publish/requested_type/compose.yml +++ b/ddsenabler_test/compose/test_cases/publish/requested_type/compose.yml @@ -16,7 +16,7 @@ services: - ./config.yml:/config/config.yml - ./samples:/samples - ./persistence:/persistence - command: ./build/ddsenabler/examples/ddsenabler_example --config /config/config.yml --timeout 5 --publish-path /samples --publish-topic test_topic --publish-period 200 --persistence-path /persistence + command: ./install/ddsenabler/examples/publish/ddsenabler_example_publish --config /config/config.yml --timeout 5 --publish-path /samples --publish-topic test_topic --publish-period 200 --persistence-path /persistence subscriber: image: ${DDSENABLER_COMPOSE_TEST_DOCKER_IMAGE} diff --git a/ddsenabler_test/compose/test_cases/reception/topics_json/compose.yml b/ddsenabler_test/compose/test_cases/reception/topics_json/compose.yml index 4d80171b..ef5d9a7e 100644 --- a/ddsenabler_test/compose/test_cases/reception/topics_json/compose.yml +++ b/ddsenabler_test/compose/test_cases/reception/topics_json/compose.yml @@ -18,7 +18,7 @@ services: - std_net volumes: - ./config.json:/config/config.json - command: ./build/ddsenabler/examples/ddsenabler_example --config /config/config.json --expected-types 1 --expected-topics 1 --expected-data 3 --timeout 6 + command: ./install/ddsenabler/examples/publish/ddsenabler_example_publish --config /config/config.json --expected-types 1 --expected-topics 1 --expected-data 3 --timeout 6 talker: image: ${DDSENABLER_COMPOSE_TEST_ROS2_DOCKER_IMAGE} diff --git a/ddsenabler_test/compose/test_cases/reception/topics_yaml/compose.yml b/ddsenabler_test/compose/test_cases/reception/topics_yaml/compose.yml index fb0739e0..4c527fa3 100644 --- a/ddsenabler_test/compose/test_cases/reception/topics_yaml/compose.yml +++ b/ddsenabler_test/compose/test_cases/reception/topics_yaml/compose.yml @@ -18,7 +18,7 @@ services: - std_net volumes: - ./config.yml:/config/config.yml - command: ./build/ddsenabler/examples/ddsenabler_example --config /config/config.yml --expected-types 1 --expected-topics 1 --expected-data 3 --timeout 6 + command: ./install/ddsenabler/examples/publish/ddsenabler_example_publish --config /config/config.yml --expected-types 1 --expected-topics 1 --expected-data 3 --timeout 6 talker: image: ${DDSENABLER_COMPOSE_TEST_ROS2_DOCKER_IMAGE} diff --git a/ddsenabler_test/compose/test_cases/services/persistence/requests/1 b/ddsenabler_test/compose/test_cases/services/persistence/requests/1 new file mode 100644 index 00000000..0fbddba4 --- /dev/null +++ b/ddsenabler_test/compose/test_cases/services/persistence/requests/1 @@ -0,0 +1,4 @@ +{ + "a": 1, + "b": 2 +} \ No newline at end of file diff --git a/ddsenabler_test/compose/test_cases/services/persistence/requests/2 b/ddsenabler_test/compose/test_cases/services/persistence/requests/2 new file mode 100644 index 00000000..9e5938e4 --- /dev/null +++ b/ddsenabler_test/compose/test_cases/services/persistence/requests/2 @@ -0,0 +1,4 @@ +{ + "a": 2, + "b": 3 +} \ No newline at end of file diff --git a/ddsenabler_test/compose/test_cases/services/persistence/requests/3 b/ddsenabler_test/compose/test_cases/services/persistence/requests/3 new file mode 100644 index 00000000..fe99b20a --- /dev/null +++ b/ddsenabler_test/compose/test_cases/services/persistence/requests/3 @@ -0,0 +1,4 @@ +{ + "a": 3, + "b": 4 +} \ No newline at end of file diff --git a/ddsenabler_test/compose/test_cases/services/persistence/services/add_two_ints b/ddsenabler_test/compose/test_cases/services/persistence/services/add_two_ints new file mode 100644 index 00000000..5af99fa0 --- /dev/null +++ b/ddsenabler_test/compose/test_cases/services/persistence/services/add_two_ints @@ -0,0 +1,7 @@ +{ + "reply_serialized_qos": "reliability: true\ndurability: false\nownership: false\nkeyed: false", + "reply_type_name": "example_interfaces::srv::dds_::AddTwoInts_Response_", + "request_serialized_qos": "reliability: true\ndurability: false\nownership: false\nkeyed: false", + "request_type_name": "example_interfaces::srv::dds_::AddTwoInts_Request_", + "service_name": "add_two_ints" +} \ No newline at end of file diff --git a/ddsenabler_test/compose/test_cases/services/persistence/types/action_msgs__msg__dds___GoalStatusArray_.bin b/ddsenabler_test/compose/test_cases/services/persistence/types/action_msgs__msg__dds___GoalStatusArray_.bin new file mode 100644 index 00000000..9b2666b4 Binary files /dev/null and b/ddsenabler_test/compose/test_cases/services/persistence/types/action_msgs__msg__dds___GoalStatusArray_.bin differ diff --git a/ddsenabler_test/compose/test_cases/services/persistence/types/action_msgs__srv__dds___CancelGoal_Request_.bin b/ddsenabler_test/compose/test_cases/services/persistence/types/action_msgs__srv__dds___CancelGoal_Request_.bin new file mode 100644 index 00000000..3312fb7d Binary files /dev/null and b/ddsenabler_test/compose/test_cases/services/persistence/types/action_msgs__srv__dds___CancelGoal_Request_.bin differ diff --git a/ddsenabler_test/compose/test_cases/services/persistence/types/action_msgs__srv__dds___CancelGoal_Response_.bin b/ddsenabler_test/compose/test_cases/services/persistence/types/action_msgs__srv__dds___CancelGoal_Response_.bin new file mode 100644 index 00000000..9b9e0b42 Binary files /dev/null and b/ddsenabler_test/compose/test_cases/services/persistence/types/action_msgs__srv__dds___CancelGoal_Response_.bin differ diff --git a/ddsenabler_test/compose/test_cases/services/persistence/types/action_tutorials_interfaces__action__dds___Fibonacci_FeedbackMessage_.bin b/ddsenabler_test/compose/test_cases/services/persistence/types/action_tutorials_interfaces__action__dds___Fibonacci_FeedbackMessage_.bin new file mode 100644 index 00000000..fac6aee9 Binary files /dev/null and b/ddsenabler_test/compose/test_cases/services/persistence/types/action_tutorials_interfaces__action__dds___Fibonacci_FeedbackMessage_.bin differ diff --git a/ddsenabler_test/compose/test_cases/services/persistence/types/action_tutorials_interfaces__action__dds___Fibonacci_GetResult_Request_.bin b/ddsenabler_test/compose/test_cases/services/persistence/types/action_tutorials_interfaces__action__dds___Fibonacci_GetResult_Request_.bin new file mode 100644 index 00000000..423f3111 Binary files /dev/null and b/ddsenabler_test/compose/test_cases/services/persistence/types/action_tutorials_interfaces__action__dds___Fibonacci_GetResult_Request_.bin differ diff --git a/ddsenabler_test/compose/test_cases/services/persistence/types/action_tutorials_interfaces__action__dds___Fibonacci_GetResult_Response_.bin b/ddsenabler_test/compose/test_cases/services/persistence/types/action_tutorials_interfaces__action__dds___Fibonacci_GetResult_Response_.bin new file mode 100644 index 00000000..62adda76 Binary files /dev/null and b/ddsenabler_test/compose/test_cases/services/persistence/types/action_tutorials_interfaces__action__dds___Fibonacci_GetResult_Response_.bin differ diff --git a/ddsenabler_test/compose/test_cases/services/persistence/types/action_tutorials_interfaces__action__dds___Fibonacci_SendGoal_Request_.bin b/ddsenabler_test/compose/test_cases/services/persistence/types/action_tutorials_interfaces__action__dds___Fibonacci_SendGoal_Request_.bin new file mode 100644 index 00000000..4bffcfdd Binary files /dev/null and b/ddsenabler_test/compose/test_cases/services/persistence/types/action_tutorials_interfaces__action__dds___Fibonacci_SendGoal_Request_.bin differ diff --git a/ddsenabler_test/compose/test_cases/services/persistence/types/action_tutorials_interfaces__action__dds___Fibonacci_SendGoal_Response_.bin b/ddsenabler_test/compose/test_cases/services/persistence/types/action_tutorials_interfaces__action__dds___Fibonacci_SendGoal_Response_.bin new file mode 100644 index 00000000..63bb3f25 Binary files /dev/null and b/ddsenabler_test/compose/test_cases/services/persistence/types/action_tutorials_interfaces__action__dds___Fibonacci_SendGoal_Response_.bin differ diff --git a/ddsenabler_test/compose/test_cases/services/persistence/types/example_interfaces__srv__dds___AddTwoInts_Request_.bin b/ddsenabler_test/compose/test_cases/services/persistence/types/example_interfaces__srv__dds___AddTwoInts_Request_.bin new file mode 100644 index 00000000..4b4e5e7d Binary files /dev/null and b/ddsenabler_test/compose/test_cases/services/persistence/types/example_interfaces__srv__dds___AddTwoInts_Request_.bin differ diff --git a/ddsenabler_test/compose/test_cases/services/persistence/types/example_interfaces__srv__dds___AddTwoInts_Response_.bin b/ddsenabler_test/compose/test_cases/services/persistence/types/example_interfaces__srv__dds___AddTwoInts_Response_.bin new file mode 100644 index 00000000..6f5f40ef Binary files /dev/null and b/ddsenabler_test/compose/test_cases/services/persistence/types/example_interfaces__srv__dds___AddTwoInts_Response_.bin differ diff --git a/ddsenabler_test/compose/test_cases/services/persistence/types/rcl_interfaces__msg__dds___Log_.bin b/ddsenabler_test/compose/test_cases/services/persistence/types/rcl_interfaces__msg__dds___Log_.bin new file mode 100644 index 00000000..6e56d56a Binary files /dev/null and b/ddsenabler_test/compose/test_cases/services/persistence/types/rcl_interfaces__msg__dds___Log_.bin differ diff --git a/ddsenabler_test/compose/test_cases/services/persistence/types/rcl_interfaces__msg__dds___ParameterEvent_.bin b/ddsenabler_test/compose/test_cases/services/persistence/types/rcl_interfaces__msg__dds___ParameterEvent_.bin new file mode 100644 index 00000000..98649ae7 Binary files /dev/null and b/ddsenabler_test/compose/test_cases/services/persistence/types/rcl_interfaces__msg__dds___ParameterEvent_.bin differ diff --git a/ddsenabler_test/compose/test_cases/services/persistence/types/rcl_interfaces__srv__dds___DescribeParameters_Request_.bin b/ddsenabler_test/compose/test_cases/services/persistence/types/rcl_interfaces__srv__dds___DescribeParameters_Request_.bin new file mode 100644 index 00000000..d33a48d9 Binary files /dev/null and b/ddsenabler_test/compose/test_cases/services/persistence/types/rcl_interfaces__srv__dds___DescribeParameters_Request_.bin differ diff --git a/ddsenabler_test/compose/test_cases/services/persistence/types/rcl_interfaces__srv__dds___DescribeParameters_Response_.bin b/ddsenabler_test/compose/test_cases/services/persistence/types/rcl_interfaces__srv__dds___DescribeParameters_Response_.bin new file mode 100644 index 00000000..f7a41219 Binary files /dev/null and b/ddsenabler_test/compose/test_cases/services/persistence/types/rcl_interfaces__srv__dds___DescribeParameters_Response_.bin differ diff --git a/ddsenabler_test/compose/test_cases/services/persistence/types/rcl_interfaces__srv__dds___GetParameterTypes_Request_.bin b/ddsenabler_test/compose/test_cases/services/persistence/types/rcl_interfaces__srv__dds___GetParameterTypes_Request_.bin new file mode 100644 index 00000000..f1cf3f19 Binary files /dev/null and b/ddsenabler_test/compose/test_cases/services/persistence/types/rcl_interfaces__srv__dds___GetParameterTypes_Request_.bin differ diff --git a/ddsenabler_test/compose/test_cases/services/persistence/types/rcl_interfaces__srv__dds___GetParameterTypes_Response_.bin b/ddsenabler_test/compose/test_cases/services/persistence/types/rcl_interfaces__srv__dds___GetParameterTypes_Response_.bin new file mode 100644 index 00000000..55815c1f Binary files /dev/null and b/ddsenabler_test/compose/test_cases/services/persistence/types/rcl_interfaces__srv__dds___GetParameterTypes_Response_.bin differ diff --git a/ddsenabler_test/compose/test_cases/services/persistence/types/rcl_interfaces__srv__dds___GetParameters_Request_.bin b/ddsenabler_test/compose/test_cases/services/persistence/types/rcl_interfaces__srv__dds___GetParameters_Request_.bin new file mode 100644 index 00000000..fe10b8fb Binary files /dev/null and b/ddsenabler_test/compose/test_cases/services/persistence/types/rcl_interfaces__srv__dds___GetParameters_Request_.bin differ diff --git a/ddsenabler_test/compose/test_cases/services/persistence/types/rcl_interfaces__srv__dds___GetParameters_Response_.bin b/ddsenabler_test/compose/test_cases/services/persistence/types/rcl_interfaces__srv__dds___GetParameters_Response_.bin new file mode 100644 index 00000000..ac082bff Binary files /dev/null and b/ddsenabler_test/compose/test_cases/services/persistence/types/rcl_interfaces__srv__dds___GetParameters_Response_.bin differ diff --git a/ddsenabler_test/compose/test_cases/services/persistence/types/rcl_interfaces__srv__dds___ListParameters_Request_.bin b/ddsenabler_test/compose/test_cases/services/persistence/types/rcl_interfaces__srv__dds___ListParameters_Request_.bin new file mode 100644 index 00000000..9b9387d7 Binary files /dev/null and b/ddsenabler_test/compose/test_cases/services/persistence/types/rcl_interfaces__srv__dds___ListParameters_Request_.bin differ diff --git a/ddsenabler_test/compose/test_cases/services/persistence/types/rcl_interfaces__srv__dds___ListParameters_Response_.bin b/ddsenabler_test/compose/test_cases/services/persistence/types/rcl_interfaces__srv__dds___ListParameters_Response_.bin new file mode 100644 index 00000000..a1212fb4 Binary files /dev/null and b/ddsenabler_test/compose/test_cases/services/persistence/types/rcl_interfaces__srv__dds___ListParameters_Response_.bin differ diff --git a/ddsenabler_test/compose/test_cases/services/persistence/types/rcl_interfaces__srv__dds___SetParametersAtomically_Request_.bin b/ddsenabler_test/compose/test_cases/services/persistence/types/rcl_interfaces__srv__dds___SetParametersAtomically_Request_.bin new file mode 100644 index 00000000..c19eeb6a Binary files /dev/null and b/ddsenabler_test/compose/test_cases/services/persistence/types/rcl_interfaces__srv__dds___SetParametersAtomically_Request_.bin differ diff --git a/ddsenabler_test/compose/test_cases/services/persistence/types/rcl_interfaces__srv__dds___SetParametersAtomically_Response_.bin b/ddsenabler_test/compose/test_cases/services/persistence/types/rcl_interfaces__srv__dds___SetParametersAtomically_Response_.bin new file mode 100644 index 00000000..1043f036 Binary files /dev/null and b/ddsenabler_test/compose/test_cases/services/persistence/types/rcl_interfaces__srv__dds___SetParametersAtomically_Response_.bin differ diff --git a/ddsenabler_test/compose/test_cases/services/persistence/types/rcl_interfaces__srv__dds___SetParameters_Request_.bin b/ddsenabler_test/compose/test_cases/services/persistence/types/rcl_interfaces__srv__dds___SetParameters_Request_.bin new file mode 100644 index 00000000..da5208da Binary files /dev/null and b/ddsenabler_test/compose/test_cases/services/persistence/types/rcl_interfaces__srv__dds___SetParameters_Request_.bin differ diff --git a/ddsenabler_test/compose/test_cases/services/persistence/types/rcl_interfaces__srv__dds___SetParameters_Response_.bin b/ddsenabler_test/compose/test_cases/services/persistence/types/rcl_interfaces__srv__dds___SetParameters_Response_.bin new file mode 100644 index 00000000..7b8a8ec8 Binary files /dev/null and b/ddsenabler_test/compose/test_cases/services/persistence/types/rcl_interfaces__srv__dds___SetParameters_Response_.bin differ diff --git a/ddsenabler_test/compose/test_cases/services/persistence/types/rmw_dds_common__msg__dds___ParticipantEntitiesInfo_.bin b/ddsenabler_test/compose/test_cases/services/persistence/types/rmw_dds_common__msg__dds___ParticipantEntitiesInfo_.bin new file mode 100644 index 00000000..c3236d96 Binary files /dev/null and b/ddsenabler_test/compose/test_cases/services/persistence/types/rmw_dds_common__msg__dds___ParticipantEntitiesInfo_.bin differ diff --git a/ddsenabler_test/compose/test_cases/services/persistence/types/type_description_interfaces__srv__dds___GetTypeDescription_Request_.bin b/ddsenabler_test/compose/test_cases/services/persistence/types/type_description_interfaces__srv__dds___GetTypeDescription_Request_.bin new file mode 100644 index 00000000..ba4d778e Binary files /dev/null and b/ddsenabler_test/compose/test_cases/services/persistence/types/type_description_interfaces__srv__dds___GetTypeDescription_Request_.bin differ diff --git a/ddsenabler_test/compose/test_cases/services/persistence/types/type_description_interfaces__srv__dds___GetTypeDescription_Response_.bin b/ddsenabler_test/compose/test_cases/services/persistence/types/type_description_interfaces__srv__dds___GetTypeDescription_Response_.bin new file mode 100644 index 00000000..f68b4410 Binary files /dev/null and b/ddsenabler_test/compose/test_cases/services/persistence/types/type_description_interfaces__srv__dds___GetTypeDescription_Response_.bin differ diff --git a/ddsenabler_test/compose/test_cases/services/service_client/compose.yml b/ddsenabler_test/compose/test_cases/services/service_client/compose.yml new file mode 100644 index 00000000..5676c48b --- /dev/null +++ b/ddsenabler_test/compose/test_cases/services/service_client/compose.yml @@ -0,0 +1,33 @@ +# Test description: +# Run ddsenabler example service client against an AddTwoInts server. +# 3 requests are sent to the server. +# Domain 33 is used. + +services: + # ENABLER + ddsenabler: + image: ${DDSENABLER_COMPOSE_TEST_DOCKER_IMAGE} + container_name: ddsenabler + networks: + - std_net + volumes: + - ./config.yml:/config/config.yml + - ../persistence:/persistence + command: ./install/ddsenabler/examples/service/ddsenabler_example_service client --persistence-path /persistence --requests-path /persistence/requests --config /config/config.yml --request-initial-wait 2 + add_two_ints_server: + image: ${DDSENABLER_COMPOSE_TEST_ROS2_DOCKER_IMAGE} + container_name: add_two_ints_server + depends_on: + - ddsenabler + networks: + - std_net + environment: + - ROS_DOMAIN_ID=33 + volumes: + - ../../../scripts:/scripts + command: python3 /scripts/execute_and_validate_server.py --timeout 10 --samples 3 + +networks: + std_net: + default: + driver: none diff --git a/ddsenabler_test/compose/test_cases/services/service_client/config.yml b/ddsenabler_test/compose/test_cases/services/service_client/config.yml new file mode 100644 index 00000000..21f38f14 --- /dev/null +++ b/ddsenabler_test/compose/test_cases/services/service_client/config.yml @@ -0,0 +1,2 @@ +dds: + domain: 33 diff --git a/ddsenabler_test/compose/test_cases/services/service_client_discovered/compose.yml b/ddsenabler_test/compose/test_cases/services/service_client_discovered/compose.yml new file mode 100644 index 00000000..ca28c9ae --- /dev/null +++ b/ddsenabler_test/compose/test_cases/services/service_client_discovered/compose.yml @@ -0,0 +1,34 @@ +# Test description: +# Run ddsenabler example service client against an already running AddTwoInts server. +# No persistence path is used as the types are discovered at runtime. +# 3 requests are sent to the server. +# Domain 33 is used. + +services: + add_two_ints_server: + image: ${DDSENABLER_COMPOSE_TEST_ROS2_DOCKER_IMAGE} + container_name: add_two_ints_server + networks: + - std_net + environment: + - ROS_DOMAIN_ID=33 + volumes: + - ../../../scripts:/scripts + command: python3 /scripts/execute_and_validate_server.py --timeout 10 --samples 3 + + ddsenabler: + image: ${DDSENABLER_COMPOSE_TEST_DOCKER_IMAGE} + container_name: ddsenabler + depends_on: + - add_two_ints_server + networks: + - std_net + volumes: + - ./config.yml:/config/config.yml + - ../persistence:/persistence + command: ./install/ddsenabler/examples/service/ddsenabler_example_service client --requests-path /persistence/requests --config /config/config.yml --request-initial-wait 2 + +networks: + std_net: + default: + driver: none diff --git a/ddsenabler_test/compose/test_cases/services/service_client_discovered/config.yml b/ddsenabler_test/compose/test_cases/services/service_client_discovered/config.yml new file mode 100644 index 00000000..21f38f14 --- /dev/null +++ b/ddsenabler_test/compose/test_cases/services/service_client_discovered/config.yml @@ -0,0 +1,2 @@ +dds: + domain: 33 diff --git a/ddsenabler_test/compose/test_cases/services/service_server/compose.yml b/ddsenabler_test/compose/test_cases/services/service_server/compose.yml new file mode 100644 index 00000000..d7f6ec9d --- /dev/null +++ b/ddsenabler_test/compose/test_cases/services/service_server/compose.yml @@ -0,0 +1,32 @@ +# Test description: +# Run ddsenabler example service server and a client sending 3 goals. +# Domain 34 is used by all containers. + +services: + ddsenabler: + image: ${DDSENABLER_COMPOSE_TEST_DOCKER_IMAGE} + container_name: ddsenabler + networks: + - std_net + volumes: + - ./config.yml:/config/config.yml + - ../persistence:/persistence + command: ./install/ddsenabler/examples/service/ddsenabler_example_service server --persistence-path /persistence --requests-path /persistence/requests --config /config/config.yml --expected-requests 3 + + add_two_ints_client: + image: ${DDSENABLER_COMPOSE_TEST_ROS2_DOCKER_IMAGE} + container_name: add_two_ints_client_1 + depends_on: + - ddsenabler + networks: + - std_net + environment: + - ROS_DOMAIN_ID=34 + volumes: + - ../../../scripts:/scripts + command: python3 /scripts/execute_and_validate_client.py --timeout 10 --samples 3 + +networks: + std_net: + default: + driver: none diff --git a/ddsenabler_test/compose/test_cases/services/service_server/config.yml b/ddsenabler_test/compose/test_cases/services/service_server/config.yml new file mode 100644 index 00000000..7c7408ac --- /dev/null +++ b/ddsenabler_test/compose/test_cases/services/service_server/config.yml @@ -0,0 +1,2 @@ +dds: + domain: 34 diff --git a/ddsenabler_test/compose/test_cases/services/service_server_multiclient/compose.yml b/ddsenabler_test/compose/test_cases/services/service_server_multiclient/compose.yml new file mode 100644 index 00000000..bc4ef95c --- /dev/null +++ b/ddsenabler_test/compose/test_cases/services/service_server_multiclient/compose.yml @@ -0,0 +1,58 @@ +# Test description: +# Run ddsenabler example service server and 3 clients sending each one 3 goals. +# Domain 35 is used by all containers. + +services: + ddsenabler: + image: ${DDSENABLER_COMPOSE_TEST_DOCKER_IMAGE} + container_name: ddsenabler + networks: + - std_net + volumes: + - ./config.yml:/config/config.yml + - ../persistence:/persistence + command: ./install/ddsenabler/examples/service/ddsenabler_example_service server --persistence-path /persistence --requests-path /persistence/requests --config /config/config.yml --expected-requests 9 + + add_two_ints_client_1: + image: ${DDSENABLER_COMPOSE_TEST_ROS2_DOCKER_IMAGE} + container_name: add_two_ints_client_1 + depends_on: + - ddsenabler + networks: + - std_net + environment: + - ROS_DOMAIN_ID=35 + volumes: + - ../../../scripts:/scripts + command: python3 /scripts/execute_and_validate_client.py --timeout 20 --samples 3 + + add_two_ints_client_2: + image: ${DDSENABLER_COMPOSE_TEST_ROS2_DOCKER_IMAGE} + container_name: add_two_ints_client_2 + depends_on: + - ddsenabler + networks: + - std_net + environment: + - ROS_DOMAIN_ID=35 + volumes: + - ../../../scripts:/scripts + command: python3 /scripts/execute_and_validate_client.py --timeout 20 --samples 3 + + add_two_ints_client_3: + image: ${DDSENABLER_COMPOSE_TEST_ROS2_DOCKER_IMAGE} + container_name: add_two_ints_client_3 + depends_on: + - ddsenabler + networks: + - std_net + environment: + - ROS_DOMAIN_ID=35 + volumes: + - ../../../scripts:/scripts + command: python3 /scripts/execute_and_validate_client.py --timeout 20 --samples 3 + +networks: + std_net: + default: + driver: none diff --git a/ddsenabler_test/compose/test_cases/services/service_server_multiclient/config.yml b/ddsenabler_test/compose/test_cases/services/service_server_multiclient/config.yml new file mode 100644 index 00000000..ba1235cf --- /dev/null +++ b/ddsenabler_test/compose/test_cases/services/service_server_multiclient/config.yml @@ -0,0 +1,2 @@ +dds: + domain: 35 diff --git a/ddsenabler_yaml/src/cpp/EnablerConfiguration.cpp b/ddsenabler_yaml/src/cpp/EnablerConfiguration.cpp index bbd5d3f9..eb8a6ef4 100644 --- a/ddsenabler_yaml/src/cpp/EnablerConfiguration.cpp +++ b/ddsenabler_yaml/src/cpp/EnablerConfiguration.cpp @@ -204,23 +204,6 @@ void EnablerConfiguration::load_ddsenabler_configuration_( load_dds_configuration_(dds_yml, version); } - // Block ROS 2 services (RPC) topics - // RATIONALE: - // At the time of this writing, services in ROS 2 behave in the following manner: a ROS 2 service - // client awaits to discover a server, and it is then when a request is sent to this (and only this) server, - // from which a response is expected. - // Hence, if these topics are not blocked, the client would wrongly believe DDS-Enabler is a server, thus - // sending a request for which a response will not be received. - WildcardDdsFilterTopic rpc_request_topic, rpc_response_topic; - rpc_request_topic.topic_name.set_value("rq/*"); - rpc_response_topic.topic_name.set_value("rr/*"); - - ddspipe_configuration.blocklist.insert( - utils::Heritable::make_heritable(rpc_request_topic)); - - ddspipe_configuration.blocklist.insert( - utils::Heritable::make_heritable(rpc_response_topic)); - // Enable manually after all callbacks are set to avoid missing notifications ddspipe_configuration.init_enabled = false;