diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 01ee300b..7a15c6fb 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -14,28 +14,39 @@ jobs: strategy: fail-fast: false matrix: - env: - - ICI_JOB_NAME: gcc - ROS_DISTRO: jazzy - UPSTREAM_WORKSPACE: 'github:ros-industrial/vda5050_interfaces#throwaway/remove-serde' - TARGET_CMAKE_ARGS: '-DENABLE_ROS2=ON' - - ICI_JOB_NAME: clang - ROS_DISTRO: jazzy - UPSTREAM_WORKSPACE: 'github:ros-industrial/vda5050_interfaces#throwaway/remove-serde' - ADDITIONAL_DEBS: clang lld - CC: "clang" - CXX: "clang++" - TARGET_CMAKE_ARGS: '-DENABLE_ROS2=ON' + distro: [humble, jazzy] + compiler: [gcc, clang] env: ISOLATION: "shell" runs-on: ubuntu-latest container: - image: ros:jazzy-ros-core + image: ros:${{ matrix.distro }}-ros-core steps: - uses: actions/checkout@v4 - name: Install dependencies run: | apt-get update -qq -y - apt-get install libpaho-mqtt-dev libpaho-mqttpp-dev libfmt-dev -qq -y - - uses: 'ros-industrial/industrial_ci@master' - env: ${{ matrix.env }} + apt-get install libpaho-mqtt-dev libpaho-mqttpp-dev libfmt-dev mosquitto -qq -y + - name: Start MQTT broker + run: | + mosquitto -d + sleep 2 # Wait for broker to start + + # GCC + - if: ${{ matrix.compiler == 'gcc' }} + uses: 'ros-industrial/industrial_ci@master' + env: + ROS_DISTRO: ${{ matrix.distro }} + UPSTREAM_WORKSPACE: 'github:sauk2/vda5050_interfaces#throwaway/temp' + TARGET_CMAKE_ARGS: '-DENABLE_ROS2=ON' + + # Clang + - if: ${{ matrix.compiler == 'clang' }} + uses: 'ros-industrial/industrial_ci@master' + env: + ROS_DISTRO: ${{ matrix.distro }} + UPSTREAM_WORKSPACE: 'github:sauk2/vda5050_interfaces#throwaway/temp' + ADDITIONAL_DEBS: clang lld + CC: "clang" + CXX: "clang++" + TARGET_CMAKE_ARGS: '-DENABLE_ROS2=ON' diff --git a/cmake/vda5050_lint_common.cmake b/cmake/vda5050_lint_common.cmake new file mode 100644 index 00000000..34a5bfe3 --- /dev/null +++ b/cmake/vda5050_lint_common.cmake @@ -0,0 +1,60 @@ +# Copyright (C) 2026 ROS-Industrial Consortium Asia Pacific +# Advanced Remanufacturing and Technology Centre +# A*STAR Research Entities (Co. Registration No. 199702110H) +# +# 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. + +# +# Run common linters for VDA5050 +# + +function(vda5050_lint_common) + find_package(ament_cmake_cppcheck REQUIRED) + ament_cppcheck() + + find_package(ament_cmake_cpplint REQUIRED) + set(cpplint_filters + "-whitespace/newline" + ) + ament_cpplint( + FILTERS "${cpplint_filters}" + ) + + # Only run ament_clang_format for 16.0.0 and higher + find_program(CLANG_FORMAT_EXECUTABLE NAMES clang-format) + if(CLANG_FORMAT_EXECUTABLE) + execute_process( + COMMAND ${CLANG_FORMAT_EXECUTABLE} --version + OUTPUT_VARIABLE CLANG_FORMAT_VERSION_OUTPUT + OUTPUT_STRIP_TRAILING_WHITESPACE + ) + + # Parse the version number from the output + # The output typically looks something like: " clang-format version 17.0.6-" + string(REGEX REPLACE ".*version \([0-9.]\+\).*" "\\1" CLANG_FORMAT_VERSION "${CLANG_FORMAT_VERSION_OUTPUT}") + + message(STATUS "clang-format version: ${CLANG_FORMAT_VERSION}") + if(CLANG_FORMAT_VERSION VERSION_GREATER_EQUAL "16.0.0") + find_package(ament_cmake_clang_format REQUIRED) + ament_clang_format( + CONFIG_FILE "${CMAKE_CURRENT_SOURCE_DIR}/../.clang-format" + ) + endif() + endif() + + # Disable copyright check for older ament_copyright versions + find_package(ament_cmake_copyright 0.13.3 QUIET) + if(ament_cmake_copyright_FOUND) + ament_copyright() + endif() +endfunction() diff --git a/vda5050_core/CMakeLists.txt b/vda5050_core/CMakeLists.txt index 2981a28c..329d8550 100644 --- a/vda5050_core/CMakeLists.txt +++ b/vda5050_core/CMakeLists.txt @@ -1,6 +1,10 @@ cmake_minimum_required(VERSION 3.8) project(vda5050_core) +if(NOT CMAKE_CXX_STANDARD) + set(CMAKE_CXX_STANDARD 17) +endif() + if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang") add_compile_options(-Wall -Wextra -Wpedantic) endif() @@ -34,7 +38,9 @@ target_include_directories(logger $ ) -add_library(client src/vda5050_core/state_manager/state_manager.cpp) +add_library(client + src/vda5050_core/state_manager/state_manager.cpp + src/vda5050_core/client/order/order_graph_validator.cpp) target_link_libraries(client vda5050_types::vda5050_types) target_include_directories(client @@ -79,33 +85,18 @@ install( ) ament_export_targets(export_vda5050_core HAS_LIBRARY_TARGET) -ament_export_dependencies(PahoMqttCpp fmt) +ament_export_dependencies(PahoMqttCpp fmt vda5050_types) if(BUILD_TESTING) - find_package(ament_cmake_cppcheck REQUIRED) - ament_cppcheck() - - find_package(ament_cmake_cpplint REQUIRED) - set(cpplint_filters - "-whitespace/newline" - ) - ament_cpplint( - FILTERS "${cpplint_filters}" - ) - - find_package(ament_cmake_clang_format REQUIRED) - ament_clang_format( - CONFIG_FILE "${CMAKE_CURRENT_SOURCE_DIR}/../.clang-format" - ) - - find_package(ament_cmake_copyright REQUIRED) - ament_copyright() + include(../cmake/vda5050_lint_common.cmake) + vda5050_lint_common() find_package(ament_cmake_gmock REQUIRED) ament_add_gmock(test_mqtt_client test/unit/mqtt_client/test_mqtt_client_interface.cpp test/unit/logger/test_logger.cpp test/unit/logger/test_default_logger.cpp + test/unit/order_graph_validator/test_order_graph_validator.cpp test/unit/state_manager/test_state_manager.cpp test/integration/mqtt_client/test_paho_mqtt_client.cpp ) diff --git a/vda5050_core/include/vda5050_core/client/order/order_graph_validator.hpp b/vda5050_core/include/vda5050_core/client/order/order_graph_validator.hpp new file mode 100644 index 00000000..8a517f1a --- /dev/null +++ b/vda5050_core/include/vda5050_core/client/order/order_graph_validator.hpp @@ -0,0 +1,63 @@ +/** + * Copyright (C) 2025 ROS-Industrial Consortium Asia Pacific + * Advanced Remanufacturing and Technology Centre + * A*STAR Research Entities (Co. Registration No. 199702110H) + * + * 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. + */ + +#ifndef VDA5050_CORE__CLIENT__ORDER__ORDER_GRAPH_VALIDATOR_HPP_ +#define VDA5050_CORE__CLIENT__ORDER__ORDER_GRAPH_VALIDATOR_HPP_ + +#include + +#include "vda5050_core/client/order/validation_result.hpp" +#include "vda5050_types/order.hpp" + +namespace vda5050_core { +namespace order { + +/// \brief Utility class with functions to perform validity checks on the graph +/// contained in a VDA5050 Order message +class OrderGraphValidator +{ +public: + OrderGraphValidator() = delete; + + /// \brief Checks that the nodes and edges in a VDA5050 Order form a valid + /// graph according to the VDA5050 specification sheet. + /// + /// \param order The order to be checked. + /// \return ValidationResult containing if the order being checked is valid, + /// and any errors if it is not. + /// + /// \return True if nodes and edges create a valid graph, false otherwise + static ValidationResult is_valid_graph( + const vda5050_types::Order& order) noexcept(false); + + /// \brief Checks if order update is valid for order stitching + /// + /// \param base_order The base order. + /// + /// \param next_order the update order. + /// + /// \return True new order is valid for stitching, false otherwise + static ValidationResult is_valid_order_update( + const vda5050_types::Order& base_order, + const vda5050_types::Order& next_order) noexcept(false); +}; + +} // namespace order +} // namespace vda5050_core + +#endif // VDA5050_CORE__CLIENT__ORDER__ORDER_GRAPH_VALIDATOR_HPP_ diff --git a/vda5050_core/include/vda5050_core/client/order/validation_result.hpp b/vda5050_core/include/vda5050_core/client/order/validation_result.hpp new file mode 100644 index 00000000..8701da0c --- /dev/null +++ b/vda5050_core/include/vda5050_core/client/order/validation_result.hpp @@ -0,0 +1,48 @@ +/** + * Copyright (C) 2025 ROS-Industrial Consortium Asia Pacific + * Advanced Remanufacturing and Technology Centre + * A*STAR Research Entities (Co. Registration No. 199702110H) + * + * 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. + */ + +#ifndef VDA5050_CORE__CLIENT__ORDER__VALIDATION_RESULT_HPP_ +#define VDA5050_CORE__CLIENT__ORDER__VALIDATION_RESULT_HPP_ + +#include + +#include "vda5050_types/error.hpp" + +namespace vda5050_core { +namespace order { + +/// \brief Struct that details the validity of an order +struct ValidationResult +{ + /// \brief A vector of error(s) that resulted in an invalid order. Empty if + /// order is valid. + std::vector errors; + + /// \brief Allows use in boolean contexts + /// + /// \return True if the order is valid, false otherwise. + explicit operator bool() const + { + return errors.empty(); + } +}; + +} // namespace order +} // namespace vda5050_core + +#endif // VDA5050_CORE__CLIENT__ORDER__VALIDATION_RESULT_HPP_ diff --git a/vda5050_core/include/vda5050_core/mqtt_client/mqtt_client_interface.hpp b/vda5050_core/include/vda5050_core/mqtt_client/mqtt_client_interface.hpp index 13747501..121c6249 100644 --- a/vda5050_core/include/vda5050_core/mqtt_client/mqtt_client_interface.hpp +++ b/vda5050_core/include/vda5050_core/mqtt_client/mqtt_client_interface.hpp @@ -39,13 +39,17 @@ class MqttClientInterface /// \brief Disconnect from the the MQTT broker virtual void disconnect() = 0; + /// \brief Check MQTT connection + virtual bool connected() = 0; + /// \brief Publish a message to the MQTT broker /// /// \param topic Topic for publish /// \param message Raw message string /// \param qos Quality of service setting for the publish virtual void publish( - const std::string& topic, const std::string& message, int qos) = 0; + const std::string& topic, const std::string& message, int qos, + bool retain = false) = 0; using MessageHandler = std::function; @@ -62,6 +66,14 @@ class MqttClientInterface /// /// \param topic Topic to unsubscribe from virtual void unsubscribe(const std::string& topic) = 0; + + /// \brief Set a will message for when the client disconnects abruptly + /// + /// \param topic Topic to publish will message + /// \param message Raw message string + /// \param Quality of service setting for the publish + virtual void set_will( + const std::string& topic, const std::string& message, int qos) = 0; }; /// \brief Create a default MQTT client interface diff --git a/vda5050_core/include/vda5050_core/mqtt_client/paho_mqtt_client.hpp b/vda5050_core/include/vda5050_core/mqtt_client/paho_mqtt_client.hpp index d61ac714..49c0cc47 100644 --- a/vda5050_core/include/vda5050_core/mqtt_client/paho_mqtt_client.hpp +++ b/vda5050_core/include/vda5050_core/mqtt_client/paho_mqtt_client.hpp @@ -23,6 +23,7 @@ #include #include #include +#include #include #include @@ -104,9 +105,13 @@ class PahoMqttClient : public MqttClientInterface // Documentation inherited from MqttClientInterface void disconnect() override; + // Documentation inherited from MqttClientInterface + bool connected() override; + // Documentation inherited from MqttClientInterface void publish( - const std::string& topic, const std::string& message, int qos) override; + const std::string& topic, const std::string& message, int qos, + bool retain = false) override; // Documentation inherited from MqttClientInterface void subscribe( @@ -115,6 +120,15 @@ class PahoMqttClient : public MqttClientInterface // Documentation inherited from MqttClientInterface void unsubscribe(const std::string& topic) override; + // Documentation inherited from MqttClientInterface + void set_will( + const std::string& topic, const std::string& message, int qos) override; + + /// \brief Get a mutable reference to Paho configuration options + /// + /// \return Mutable reference to Paho configuration options + mqtt::connect_options& connect_options(); + friend class MqttCallback; private: @@ -139,6 +153,9 @@ class PahoMqttClient : public MqttClientInterface /// \brief Mutex protecting list of message handlers std::mutex handler_mutex_; + + /// \brief MQTT connection options + mqtt::connect_options conn_options_; }; } // namespace mqtt_client diff --git a/vda5050_core/src/vda5050_core/client/order/order_graph_validator.cpp b/vda5050_core/src/vda5050_core/client/order/order_graph_validator.cpp new file mode 100644 index 00000000..a8da9293 --- /dev/null +++ b/vda5050_core/src/vda5050_core/client/order/order_graph_validator.cpp @@ -0,0 +1,285 @@ +/** + * Copyright (C) 2025 ROS-Industrial Consortium Asia Pacific + * Advanced Remanufacturing and Technology Centre + * A*STAR Research Entities (Co. Registration No. 199702110H) + * + * 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 "vda5050_core/client/order/order_graph_validator.hpp" +#include "vda5050_core/logger/logger.hpp" + +namespace vda5050_core { +namespace order { + +//============================================================================= +ValidationResult OrderGraphValidator::is_valid_graph( + const vda5050_types::Order& order) noexcept(false) +{ + ValidationResult res; + const std::string order_id = order.order_id; + const std::string update_id = std::to_string(order.order_update_id); + + auto add_error = + [&]( + const std::string& msg, + const std::vector& refs = {}) { + std::vector all_refs = { + {"order.order_id", order_id}, {"order.order_update_id", update_id}}; + all_refs.insert(all_refs.end(), refs.begin(), refs.end()); + VDA5050_ERROR("Order Validation Error: {}", msg); + res.errors.push_back(vda5050_types::Error{ + "Order Validation Error", std::move(all_refs), msg, + vda5050_types::ErrorLevel::WARNING}); + }; + + // check if there are exusting nodes, stop if empty + if (order.nodes.empty()) + { + add_error("order does not have any nodes!"); + return res; + } + + // check if number of order is n, and number of edge is n-1 + if (order.nodes.size() != order.edges.size() + 1) + { + add_error( + "Invalid graph. Found " + std::to_string(order.nodes.size()) + + " nodes and " + std::to_string(order.edges.size()) + + " edges. Expected exactly " + std::to_string(order.nodes.size() - 1) + + " edges."); + return res; + } + + // check if order_update_id is 0, (means first update of its order) + // reject if first node sequence id is > 0 + if (order.order_update_id == 0 && order.nodes.front().sequence_id != 0) + { + add_error( + "Initial order (update_id=0) must start with sequence id 0, but found " + + std::to_string(order.nodes.front().sequence_id), + {{"node.node_id", order.nodes.front().node_id}, + {"node.sequence_id", std::to_string(order.nodes.front().sequence_id)}}); + return res; + } + + bool horizon_reached = false; + std::optional last_base_seq; + + // iterate through nodes and edges + for (size_t i(0u); i < order.nodes.size(); ++i) + { + const auto& curr_node = order.nodes[i]; + + // check if node sequence id is even + if (curr_node.sequence_id & 1u) + { + add_error( + "Order Node sequence contains an odd sequence id", + {{"node.sequence_id", std::to_string(curr_node.sequence_id)}}); + } + + // check if base/horizon separation + if (curr_node.released) + { + if (horizon_reached) + add_error( + "Order contains a base sequence id after a horizon sequence id"); + last_base_seq = curr_node.sequence_id; + } + else + { + horizon_reached = true; + } + + // validate edges + // check an edge if curr_node is not last + if (i < order.edges.size()) + { + const auto& edge = order.edges[i]; + + // continuity check: edge sequence_id must be node sequence_id + 1 + if (edge.sequence_id != curr_node.sequence_id + 1) + { + add_error( + "Missing sequence id or unsorted data. Expected edge " + + std::to_string(curr_node.sequence_id + 1) + " but found " + + std::to_string(edge.sequence_id)); + } + + // check if edge is odd + if (!(edge.sequence_id & 1u)) + { + add_error( + "Order Edge sequence contains an even sequence id", + {{"edge.sequence_id", std::to_string(edge.sequence_id)}}); + } + + // check if start node id of edge is the current node_id + if (edge.start_node_id != curr_node.node_id) + { + add_error( + "Edge start_node_id does not match the preceding node ID", + {{"edge.edge_id", edge.edge_id}, + {"node.node_id", curr_node.node_id}}); + } + + // check if end node id of edge is the next node_id + const auto& next_node = order.nodes[i + 1]; + + if (edge.end_node_id != next_node.node_id) + { + add_error( + "Edge end_node_id does not match the following node ID", + {{"edge.edge_id", edge.edge_id}, + {"next_node.node_id", next_node.node_id}}); + } + + // next node sequence_id must be Edge sequence_id + 1 + if (next_node.sequence_id != edge.sequence_id + 1) + { + add_error( + "Missing sequence id or unsorted data. Expected node " + + std::to_string(edge.sequence_id + 1) + " but found " + + std::to_string(next_node.sequence_id)); + } + + // edge base/horizon check, cannot have base (released node) after + // horizon (unreleased node) + if (edge.released) + { + if (horizon_reached) + add_error( + "Order contains a base sequence id after a horizon sequence id"); + last_base_seq = edge.sequence_id; + } + else + { + horizon_reached = true; + } + } + } + + // If the last thing released was edge, order is invalid. + if (last_base_seq && (*last_base_seq & 1u)) + { + add_error( + "The base (released graph) ends with an edge; it must end with a node."); + } + + return res; +} + +//============================================================================= +ValidationResult OrderGraphValidator::is_valid_order_update( + const vda5050_types::Order& base_order, + const vda5050_types::Order& next_order) noexcept(false) +{ + ValidationResult res; + + const std::string base_order_id = base_order.order_id; + const std::string base_order_update_id = + std::to_string(base_order.order_update_id); + + const std::string next_order_id = next_order.order_id; + const std::string next_order_update_id = + std::to_string(next_order.order_update_id); + + auto add_error = + [&]( + const std::string& msg, + const std::vector& refs = {}) { + std::vector all_refs = { + {"base_order.order_id", base_order_id}, + {"next_order.id", next_order_id}}; + all_refs.insert(all_refs.end(), refs.begin(), refs.end()); + VDA5050_ERROR("Order Validation Error: {}", msg); + res.errors.push_back(vda5050_types::Error{ + "Order Validation Error", std::move(all_refs), msg, + vda5050_types::ErrorLevel::WARNING}); + }; + // check validity of next order + ValidationResult next_res = OrderGraphValidator::is_valid_graph(next_order); + if (!next_res.errors.empty()) + { + // Append errors from the sub-validation to the result + res.errors.insert( + res.errors.end(), next_res.errors.begin(), next_res.errors.end()); + add_error("The incoming update order itself is invalid."); + return res; + } + // check validitiy of current order + ValidationResult base_res = OrderGraphValidator::is_valid_graph(base_order); + if (!base_res.errors.empty()) + { + add_error("The internal base order is invalid state. Cannot append."); + return res; + } + + // check if order id matches + if (base_order.order_id != next_order.order_id) + { + add_error( + "Order IDs do not match (" + base_order.order_id + " vs " + + next_order.order_id + ")"); + return res; + } + + // order update id must be increasing + if (next_order.order_update_id < base_order.order_update_id) + { + add_error( + "Update ID must be greater than base. Base: " + + std::to_string(base_order.order_update_id) + + ", Next: " + std::to_string(next_order.order_update_id)); + return res; + } + + // The first node of the order update corresponds + // to the last shared base node of the previous order message. + // @ VDA 5050 Version 2.1.0, January 2025 (Page 16) + + // find the last released node of base order + auto it = std::find_if( + base_order.nodes.rbegin(), base_order.nodes.rend(), + [](const auto& node) { return node.released; }); + + // check if there's a released node + // maybe put to is_valid_graph() + if (it == base_order.nodes.rend()) + { + add_error("Base order has no released nodes to stitch onto."); + return res; + } + + const auto& last_released_node = *it; + const auto& next_first_node = next_order.nodes.front(); + + // nnode must match (node_id and sequence_id) + if (last_released_node != next_first_node) + { + add_error( + "Graph Discontinuity: Base last released node is '" + + last_released_node.node_id + "' but Update starts at node '" + + next_first_node.node_id + "'"); + } + + return res; +} +//============================================================================= + +} // namespace order +} // namespace vda5050_core diff --git a/vda5050_core/src/vda5050_core/mqtt_client/paho_mqtt_client.cpp b/vda5050_core/src/vda5050_core/mqtt_client/paho_mqtt_client.cpp index 63843261..dba105a6 100644 --- a/vda5050_core/src/vda5050_core/mqtt_client/paho_mqtt_client.cpp +++ b/vda5050_core/src/vda5050_core/mqtt_client/paho_mqtt_client.cpp @@ -109,15 +109,7 @@ void PahoMqttClient::connect() try { - mqtt::connect_options conn_options; - conn_options.set_mqtt_version(4); - conn_options.set_clean_session(false); - conn_options.set_user_name(""); - conn_options.set_password(""); - conn_options.set_automatic_reconnect(true); - conn_options.set_automatic_reconnect(2, 32); - - client_->connect(conn_options, nullptr, action_listener_)->wait(); + client_->connect(conn_options_, nullptr, action_listener_)->wait(); } catch (const mqtt::exception& e) { @@ -144,9 +136,15 @@ void PahoMqttClient::disconnect() } } +//============================================================================= +bool PahoMqttClient::connected() +{ + return client_->is_connected(); +} + //============================================================================= void PahoMqttClient::publish( - const std::string& topic, const std::string& message, int qos) + const std::string& topic, const std::string& message, int qos, bool retain) { try { @@ -154,6 +152,7 @@ void PahoMqttClient::publish( msg->set_topic(topic); msg->set_payload(message); msg->set_qos(qos); + msg->set_retained(retain); client_->publish(msg)->wait(); } @@ -194,6 +193,25 @@ void PahoMqttClient::unsubscribe(const std::string& topic) } } +//============================================================================= +void PahoMqttClient::set_will( + const std::string& topic, const std::string& message, int qos) +{ + mqtt::will_options will; + will.set_topic(topic); + will.set_retained(true); + will.set_qos(qos); + will.set_payload(message); + + conn_options_.set_will(will); +} + +//============================================================================= +mqtt::connect_options& PahoMqttClient::connect_options() +{ + return conn_options_; +} + //============================================================================= PahoMqttClient::PahoMqttClient( const std::string& broker_address, const std::string& client_id) @@ -202,6 +220,13 @@ PahoMqttClient::PahoMqttClient( callback_(MqttCallback(*this)) { client_->set_callback(callback_); + + conn_options_.set_mqtt_version(4); + conn_options_.set_clean_session(false); + conn_options_.set_user_name(""); + conn_options_.set_password(""); + conn_options_.set_automatic_reconnect(true); + conn_options_.set_automatic_reconnect(2, 32); } } // namespace mqtt_client diff --git a/vda5050_core/test/integration/mqtt_client/test_paho_mqtt_client.cpp b/vda5050_core/test/integration/mqtt_client/test_paho_mqtt_client.cpp index 33c66d5c..2dcebcb4 100644 --- a/vda5050_core/test/integration/mqtt_client/test_paho_mqtt_client.cpp +++ b/vda5050_core/test/integration/mqtt_client/test_paho_mqtt_client.cpp @@ -18,27 +18,30 @@ #include +#include #include #include #include "vda5050_core/mqtt_client/mqtt_client_interface.hpp" +#include "vda5050_core/mqtt_client/paho_mqtt_client.hpp" TEST(PahoMqttClientTest, PublishSubscribe) { - std::string broker = "tcp://test.mosquitto.org:1883"; + std::string broker = "tcp://localhost:1883"; std::string topic = "/test/integration"; std::string payload = "hello"; int qos = 0; - std::atomic_bool received = false; + std::atomic_int message_count{0}; auto listener = - vda5050_core::mqtt_client::create_default_client(broker, "listener"); + vda5050_core::mqtt_client::PahoMqttClient::make(broker, "listener"); ASSERT_NO_THROW(listener->connect()); + ASSERT_TRUE(listener->connected()); ASSERT_NO_THROW(listener->subscribe( topic, [&](const std::string& topic_, const std::string& payload_) { - received = true; + message_count++; ASSERT_EQ(topic, topic_); ASSERT_EQ(payload, payload_); }, @@ -51,7 +54,7 @@ TEST(PahoMqttClientTest, PublishSubscribe) std::this_thread::sleep_for(std::chrono::milliseconds(1000)); - ASSERT_TRUE(received); + ASSERT_EQ(message_count, 1); ASSERT_NO_THROW(talker->disconnect()); ASSERT_NO_THROW(listener->disconnect()); @@ -59,7 +62,7 @@ TEST(PahoMqttClientTest, PublishSubscribe) TEST(PahoMqttClientTest, UnsubscribeStopsMessages) { - std::string broker = "tcp://test.mosquitto.org:1883"; + std::string broker = "tcp://localhost:1883"; std::string topic = "/test/integration/unsubscribe"; std::string payload = "hello"; int qos = 0; @@ -67,7 +70,7 @@ TEST(PahoMqttClientTest, UnsubscribeStopsMessages) std::atomic_int message_count{0}; auto listener = - vda5050_core::mqtt_client::create_default_client(broker, "unsub_listener"); + vda5050_core::mqtt_client::PahoMqttClient::make(broker, "unsub_listener"); ASSERT_NO_THROW(listener->connect()); ASSERT_NO_THROW(listener->subscribe( topic, @@ -99,3 +102,22 @@ TEST(PahoMqttClientTest, UnsubscribeStopsMessages) ASSERT_NO_THROW(talker->disconnect()); ASSERT_NO_THROW(listener->disconnect()); } + +TEST(PahoMqttClient, LastWill) +{ + std::string broker = "tcp://localhost:1883"; + std::string topic = "/test/integration/unsubscribe"; + std::string payload = "hello"; + int qos = 0; + + auto client = + vda5050_core::mqtt_client::PahoMqttClient::make(broker, "last_will_client"); + ASSERT_NO_THROW(client->set_will(topic, payload, qos)); + ASSERT_NO_THROW(client->connect()); + ASSERT_TRUE(client->connected()); + + ASSERT_EQ(client->connect_options().get_will_topic(), topic); + ASSERT_EQ(client->connect_options().get_will_message()->to_string(), payload); + + ASSERT_NO_THROW(client->disconnect()); +} diff --git a/vda5050_core/test/unit/mqtt_client/test_mqtt_client_interface.cpp b/vda5050_core/test/unit/mqtt_client/test_mqtt_client_interface.cpp index 6b84754b..a45ed77b 100644 --- a/vda5050_core/test/unit/mqtt_client/test_mqtt_client_interface.cpp +++ b/vda5050_core/test/unit/mqtt_client/test_mqtt_client_interface.cpp @@ -25,14 +25,18 @@ class MockMqttClient : public vda5050_core::mqtt_client::MqttClientInterface public: MOCK_METHOD(void, connect, (), (override)); MOCK_METHOD(void, disconnect, (), (override)); + MOCK_METHOD(bool, connected, (), (override)); MOCK_METHOD( - void, publish, (const std::string&, const std::string&, int), (override)); + void, publish, (const std::string&, const std::string&, int, bool), + (override)); MOCK_METHOD( void, subscribe, (const std::string&, std::function, int), (override)); MOCK_METHOD(void, unsubscribe, (const std::string&), (override)); + MOCK_METHOD( + void, set_will, (const std::string&, const std::string&, int), (override)); }; TEST(MqttClientInterfaceTest, ConnectCall) @@ -49,11 +53,18 @@ TEST(MqttClientInterfaceTest, DisconnectCall) mock.disconnect(); } +TEST(MqttClientInterfaceTest, CheckConnection) +{ + MockMqttClient mock; + EXPECT_CALL(mock, connected()).Times(1); + mock.connected(); +} + TEST(MqttClientInterfaceTest, PublishMessage) { MockMqttClient mock; - EXPECT_CALL(mock, publish("topic", "{payload: 'data'}", 0)).Times(1); - mock.publish("topic", "{payload: 'data'}", 0); + EXPECT_CALL(mock, publish("topic", "{payload: 'data'}", 0, false)).Times(1); + mock.publish("topic", "{payload: 'data'}", 0, false); } TEST(MqttClientInterfaceTest, SubscribeTopic) @@ -70,3 +81,10 @@ TEST(MqttClientInterfaceTest, UnsubscribeTopic) EXPECT_CALL(mock, unsubscribe("topic")).Times(1); mock.unsubscribe("topic"); } + +TEST(MqttClientInterfaceTest, SetWill) +{ + MockMqttClient mock; + EXPECT_CALL(mock, set_will("topic", "{payload: 'data'}", 1)).Times(1); + mock.set_will("topic", "{payload: 'data'}", 1); +} diff --git a/vda5050_core/test/unit/order_graph_validator/test_order_graph_validator.cpp b/vda5050_core/test/unit/order_graph_validator/test_order_graph_validator.cpp new file mode 100644 index 00000000..f84953ae --- /dev/null +++ b/vda5050_core/test/unit/order_graph_validator/test_order_graph_validator.cpp @@ -0,0 +1,527 @@ +/** + * Copyright (C) 2025 ROS-Industrial Consortium Asia Pacific + * Advanced Remanufacturing and Technology Centre + * A*STAR Research Entities (Co. Registration No. 199702110H) + * + * 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 "vda5050_core/client/order/order_graph_validator.hpp" +#include "vda5050_core/client/order/validation_result.hpp" + +class OrderGraphValidatorTest : public testing::Test +{ +protected: + vda5050_types::Node n0_{"node0", 0, true, {}, std::nullopt, std::nullopt}; + vda5050_types::Edge e1_{"edge1", 1, + "node0", "node2", + true, {}, + std::nullopt, std::nullopt, + std::nullopt, std::nullopt, + std::nullopt, std::nullopt, + std::nullopt, std::nullopt, + std::nullopt, std::nullopt, + std::nullopt}; + vda5050_types::Node n2_{"node2", 2, true, {}, std::nullopt, std::nullopt}; + vda5050_types::Edge e3_{"edge3", 3, + "node2", "node4", + true, {}, + std::nullopt, std::nullopt, + std::nullopt, std::nullopt, + std::nullopt, std::nullopt, + std::nullopt, std::nullopt, + std::nullopt, std::nullopt, + std::nullopt}; + vda5050_types::Node n4_{"node4", 4, true, {}, std::nullopt, std::nullopt}; + + vda5050_types::Order order_{}; + std::vector nodes; + std::vector edges; +}; + +//============================================================================= +/// \brief Tests that graph validator returns true on a valid graph +TEST_F(OrderGraphValidatorTest, ValidGraphTest) +{ + nodes.push_back(n0_); + edges.push_back(e1_); + nodes.push_back(n2_); + edges.push_back(e3_); + nodes.push_back(n4_); + + order_.order_id = "ValidGraphTest"; + order_.order_update_id = 0; + order_.edges = edges; + order_.nodes = nodes; + + auto res = vda5050_core::order::OrderGraphValidator::is_valid_graph(order_); + EXPECT_TRUE(res); +} + +//============================================================================= +// /// \brief Tests that graph validator returns false when nodes and edges are +// not in traversal order +TEST_F(OrderGraphValidatorTest, NotInTraversalOrderTest) +{ + nodes.push_back(n0_); + edges.push_back(e3_); + nodes.push_back(n2_); + edges.push_back(e1_); + nodes.push_back(n4_); + + order_.order_id = "NotInTraversalOrderTest"; + order_.order_update_id = 0; + order_.edges = edges; + order_.nodes = nodes; + + auto res = vda5050_core::order::OrderGraphValidator::is_valid_graph(order_); + EXPECT_FALSE(res); +} + +//============================================================================= +/// \brief Tests that graph validator returns false if there are more nodes +/// than edges +TEST_F(OrderGraphValidatorTest, MoreNodesThanEdgesTest) +{ + nodes.push_back(n0_); + edges.push_back(e1_); + nodes.push_back(n2_); + edges.push_back(e3_); + nodes.push_back(n4_); + + vda5050_types::Node n6{"node6", 6, true, {}, std::nullopt, std::nullopt}; + nodes.push_back(n6); + + order_.order_id = "MoreNodesThanEdgesTest"; + order_.order_update_id = 0; + order_.nodes = nodes; + order_.edges = edges; + + auto res = vda5050_core::order::OrderGraphValidator::is_valid_graph(order_); + EXPECT_FALSE(res); +} + +//============================================================================= +/// \brief Tests that validation fails if there are more edges +/// than nodes +TEST_F(OrderGraphValidatorTest, MoreEdgesThanNodesTest) +{ + nodes.push_back(n0_); + edges.push_back(e1_); + nodes.push_back(n2_); + edges.push_back(e3_); + + vda5050_types::Edge e5{"edge5", 5, + "node4", "node6", + true, {}, + std::nullopt, std::nullopt, + std::nullopt, std::nullopt, + std::nullopt, std::nullopt, + std::nullopt, std::nullopt, + std::nullopt, std::nullopt, + std::nullopt}; + edges.push_back(e5); + + order_.order_id = "MoreEdgesThanNodesTest"; + order_.order_update_id = 0; + order_.nodes = nodes; + order_.edges = edges; + + auto res = vda5050_core::order::OrderGraphValidator::is_valid_graph(order_); + EXPECT_FALSE(res); +} + +//============================================================================= +/// \brief Tests that an edge in the right sequenceId order causes validation +/// to fail if its startNodeId and endNodeId do not match the nodeIds of +/// its neighbouring nodes +TEST_F(OrderGraphValidatorTest, ValidEdgesTest) +{ + nodes.push_back(n0_); + + e1_.start_node_id = "foo"; + e1_.end_node_id = "bar"; + edges.push_back(e1_); + + nodes.push_back(n2_); + + order_.order_id = "ValidEdgesTest"; + order_.order_update_id = 0; + order_.nodes = nodes; + order_.edges = edges; + + auto res = vda5050_core::order::OrderGraphValidator::is_valid_graph(order_); + EXPECT_FALSE(res); +} + +//============================================================================= +/// \brief Tests that an order with odd node sequenceIds and even edge +/// sequenceIds causes validation to fail +TEST_F(OrderGraphValidatorTest, NodeWithOddSequenceIdTest) +{ + vda5050_types::Node odd_node1{"oddNode1", 1, true, {}, + std::nullopt, std::nullopt}; + nodes.push_back(odd_node1); + vda5050_types::Node odd_node2{"oddNode2", 3, true, {}, + std::nullopt, std::nullopt}; + nodes.push_back(odd_node2); + + vda5050_types::Edge even_edge{"evenEdge", 2, + "node4", "node6", + true, {}, + std::nullopt, std::nullopt, + std::nullopt, std::nullopt, + std::nullopt, std::nullopt, + std::nullopt, std::nullopt, + std::nullopt, std::nullopt, + std::nullopt}; + edges.push_back(even_edge); + + order_.order_id = "NodeWithOddSequenceIdTest"; + order_.order_update_id = 0; + order_.nodes = nodes; + order_.edges = edges; + + auto res = vda5050_core::order::OrderGraphValidator::is_valid_graph(order_); + EXPECT_FALSE(res); +} + +//============================================================================= +/// \brief Tests that no two nodes share the same sequenceId +TEST_F(OrderGraphValidatorTest, DuplicateNodeSequenceIdTest) +{ + n2_.sequence_id = 0; + + nodes.push_back(n0_); + nodes.push_back(n2_); + + edges.push_back(e1_); + + order_.order_id = "DuplicateNodeSequenceIdTest"; + order_.order_update_id = 0; + order_.nodes = nodes; + order_.edges = edges; + + auto res = vda5050_core::order::OrderGraphValidator::is_valid_graph(order_); + EXPECT_FALSE(res); +} + +//============================================================================= +/// \brief Tests that there is only one base in the order +TEST_F(OrderGraphValidatorTest, MultipleBaseTest) +{ + /// n0_, e1_, n2_, and n4_ all released. Create a gap by setting e3_ to + /// unreleased. + e3_.released = false; + + nodes.push_back(n0_); + nodes.push_back(n2_); + nodes.push_back(n4_); + + edges.push_back(e1_); + edges.push_back(e3_); + + order_.order_id = "MultipleBaseTest"; + order_.order_update_id = 0; + order_.nodes = nodes; + order_.edges = edges; + + auto res = vda5050_core::order::OrderGraphValidator::is_valid_graph(order_); + EXPECT_FALSE(res); +} + +//============================================================================= +/// \brief Tests that there is only one base in the order +TEST_F(OrderGraphValidatorTest, ValidOrderUpdate) +{ + // Base order: node0 -> node2 + nodes = {n0_, n2_}; + edges = {e1_}; + + order_.order_id = "OrderA"; + order_.order_update_id = 0; + order_.nodes = nodes; + order_.edges = edges; + + auto base_order = order_; + + // Next order continues from node2 -> node4 + nodes = {n2_, n4_}; + edges = {e3_}; + + order_.order_update_id = 1; + order_.nodes = nodes; + order_.edges = edges; + + auto next_order = order_; + + auto res = vda5050_core::order::OrderGraphValidator::is_valid_order_update( + base_order, next_order); + + EXPECT_TRUE(res); +} + +//============================================================================= +/// \brief test if base order is invalid +TEST_F(OrderGraphValidatorTest, InvalidBaseOrder) +{ + // Invalid base order: missing edge + nodes = {n0_, n2_}; + edges = {}; + + order_.order_id = "OrderA"; + order_.order_update_id = 0; + order_.nodes = nodes; + order_.edges = edges; + + auto base_order = order_; + + // Valid next order + nodes = {n2_, n4_}; + edges = {e3_}; + + order_.order_update_id = 1; + order_.nodes = nodes; + order_.edges = edges; + + auto next_order = order_; + + auto res = vda5050_core::order::OrderGraphValidator::is_valid_order_update( + base_order, next_order); + + EXPECT_FALSE(res); +} + +//============================================================================= +/// \brief test if next order is invalid +TEST_F(OrderGraphValidatorTest, InvalidNextOrder) +{ + // Valid base order + nodes = {n0_, n2_}; + edges = {e1_}; + + order_.order_id = "OrderA"; + order_.order_update_id = 0; + order_.nodes = nodes; + order_.edges = edges; + + auto base_order = order_; + + // Invalid next order: wrong edge count + nodes = {n2_, n4_}; + edges = {}; + + order_.order_update_id = 1; + order_.nodes = nodes; + order_.edges = edges; + + auto next_order = order_; + + auto res = vda5050_core::order::OrderGraphValidator::is_valid_order_update( + base_order, next_order); + + EXPECT_FALSE(res); +} + +//============================================================================= +/// \brief test for order id mismatch between base order and next order +TEST_F(OrderGraphValidatorTest, OrderIdMismatch) +{ + // Base order + nodes = {n0_, n2_}; + edges = {e1_}; + + order_.order_id = "OrderA"; + order_.order_update_id = 0; + order_.nodes = nodes; + order_.edges = edges; + + auto base_order = order_; + + // Next order with different order_id + nodes = {n2_, n4_}; + edges = {e3_}; + + order_.order_id = "OrderB"; // mismatch + order_.order_update_id = 1; + order_.nodes = nodes; + order_.edges = edges; + + auto next_order = order_; + + auto res = vda5050_core::order::OrderGraphValidator::is_valid_order_update( + base_order, next_order); + + EXPECT_FALSE(res); +} + +//============================================================================= +/// \brief test if order update id of next order is greater than base order +TEST_F(OrderGraphValidatorTest, OrderUpdateIdRegression) +{ + // Base order update_id = 2 + nodes = {n0_, n2_}; + edges = {e1_}; + + order_.order_id = "OrderA"; + order_.order_update_id = 2; + order_.nodes = nodes; + order_.edges = edges; + + auto base_order = order_; + + // Next order update_id = 1 (invalid) + nodes = {n2_, n4_}; + edges = {e3_}; + + order_.order_update_id = 1; + order_.nodes = nodes; + order_.edges = edges; + + auto next_order = order_; + + auto res = vda5050_core::order::OrderGraphValidator::is_valid_order_update( + base_order, next_order); + + EXPECT_FALSE(res); +} + +//============================================================================= +/// \brief test if base order and next order is valid for stitching +TEST_F(OrderGraphValidatorTest, ValidOrderStitching) +{ + // Base Order: Node0(Rel) -> Edge1(Rel) -> Node2(Rel) + // Next Order: Node2(Rel) -> Edge3(Rel) -> Node4(Rel) + + // Path: [Node0] -> [Edge1] -> [Node2] (All Released) + order_.order_id = "StitchTest"; + order_.order_update_id = 0; + order_.nodes = {n0_, n2_}; + order_.edges = {e1_}; + + // Path: [Node2] -> [Edge3] -> [Node4] + vda5050_types::Order next_order; + next_order.order_id = "StitchTest"; + next_order.order_update_id = 1; + next_order.nodes = {n2_, n4_}; // Starts at n2_ + next_order.edges = {e3_}; + + auto res = vda5050_core::order::OrderGraphValidator::is_valid_order_update( + order_, next_order); + EXPECT_TRUE(res); +} + +//============================================================================= +/// \brief test for invalid stitch +TEST_F(OrderGraphValidatorTest, InvalidOrderStitching) +{ + // CASE 1: Gap in Graph + // Base Order: Node0(Rel) -> Edge1(Rel) -> Node2(Rel) + // Next Order: Node4(Rel) -> ... + order_.order_id = "StitchFailNode"; + order_.order_update_id = 0; + order_.nodes = {n0_, n2_}; + order_.edges = {e1_}; + + // Starts at Node4 (The robot is at Node2, cannot jump to Node4) + vda5050_types::Order next_order; + next_order.order_id = "StitchFailNode"; + next_order.order_update_id = 1; + next_order.nodes = {n4_}; // Error: Should start at n2_ + next_order.edges = {e3_}; + + auto res = vda5050_core::order::OrderGraphValidator::is_valid_order_update( + order_, next_order); + EXPECT_FALSE(res); + + // CASE 2: Backtracking / Discontinuity + // Base Order: Node0(Rel) -> Edge1(Rel) -> Node2(Rel) + // Next Order: Node0(Rel) -> ... + order_.order_id = "StitchFailNode"; + order_.order_update_id = 0; + order_.nodes = {n0_, n2_}; + order_.edges = {e1_}; + + // Starts at Node4 (The robot is at Node2, next order cant jump to Node0) + next_order.order_id = "StitchFailNode"; + next_order.order_update_id = 1; + next_order.nodes = {n0_}; // Error: Should start at n2_ + next_order.edges = {e3_}; + + res = vda5050_core::order::OrderGraphValidator::is_valid_order_update( + order_, next_order); + EXPECT_FALSE(res); +} + +//============================================================================= +/// \brief test for sequence mismatch +TEST_F(OrderGraphValidatorTest, SequenceMismatch) +{ + // Base Order: Node0(Rel) -> Edge1(Rel) -> Node2(Rel, Seq=2) + // Next Order: Node2(Rel, Seq=0) -> Edge3 -> ... + // Ends at Node2 (Sequence ID 2) + + order_.order_id = "StitchFailSeq"; + order_.order_update_id = 0; + order_.nodes = {n0_, n2_}; + order_.edges = {e1_}; + + // Starts at Node2, BUT with Sequence ID 0 + vda5050_types::Node n2_wrong_seq = n2_; + n2_wrong_seq.sequence_id = 0; + + vda5050_types::Order next_order; + next_order.order_id = "StitchFailSeq"; + next_order.order_update_id = 1; + next_order.nodes = {n2_wrong_seq, n4_}; + next_order.edges = {e3_}; + + auto res = vda5050_core::order::OrderGraphValidator::is_valid_order_update( + order_, next_order); + + EXPECT_FALSE(res); +} + +//============================================================================= +/// \brief test for the "Last Released Node" logic. +TEST_F(OrderGraphValidatorTest, StitchingReplacesHorizon) +{ + // Base Order: Node0(Rel) -> Edge1(Rel) -> Node2(Rel) -> Edge3(Unreleased) -> Node4(Unreleased) + // Next Order: Node2(Rel) -> Edge3(Rel) -> Node4(Rel) + + vda5050_types::Edge e3_horizon = e3_; + e3_horizon.released = false; + vda5050_types::Node n4_horizon = n4_; + n4_horizon.released = false; + + order_.order_id = "StitchingReplacesHorizon"; + order_.order_update_id = 0; + order_.nodes = {n0_, n2_, n4_horizon}; + order_.edges = {e1_, e3_horizon}; + + vda5050_types::Order next_order; + next_order.order_id = "StitchingReplacesHorizon"; + next_order.order_update_id = 1; + next_order.nodes = { + n2_, n4_}; // Stitching at n2 (Last released), IGNORING n4_horizon + next_order.edges = {e3_}; + + auto res = vda5050_core::order::OrderGraphValidator::is_valid_order_update( + order_, next_order); + EXPECT_TRUE(res); +} diff --git a/vda5050_json_utils/CMakeLists.txt b/vda5050_json_utils/CMakeLists.txt index 8ca994ec..f0d9e346 100644 --- a/vda5050_json_utils/CMakeLists.txt +++ b/vda5050_json_utils/CMakeLists.txt @@ -1,6 +1,9 @@ cmake_minimum_required(VERSION 3.20) project(vda5050_json_utils) +if(NOT CMAKE_CXX_STANDARD) + set(CMAKE_CXX_STANDARD 17) +endif() if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang") add_compile_options(-Wall -Wextra -Wpedantic) endif() @@ -8,7 +11,6 @@ endif() find_package(ament_cmake REQUIRED) find_package(vda5050_types REQUIRED) find_package(nlohmann_json REQUIRED) -find_package(vda5050_msgs REQUIRED) include(GNUInstallDirs) @@ -16,7 +18,7 @@ option(ENABLE_ROS2 "Enable ROS 2 message support in JSON utilities" OFF) if(ENABLE_ROS2) message(STATUS "Building vda5050_json_utils with ROS 2 support") - find_package(vda5050_msgs REQUIRED) + find_package(vda5050_interfaces REQUIRED) else() message(STATUS "Building vda5050_json_utils without ROS 2 support") endif() @@ -38,12 +40,12 @@ target_include_directories(vda5050_json_utils if(ENABLE_ROS2) target_link_libraries(vda5050_json_utils INTERFACE - ${vda5050_msgs_LIBRARIES} + ${vda5050_interfaces_LIBRARIES} ) target_include_directories(vda5050_json_utils INTERFACE - ${vda5050_msgs_INCLUDE_DIRS} + ${vda5050_interfaces_INCLUDE_DIRS} ) target_compile_definitions(vda5050_json_utils INTERFACE ENABLE_ROS2) @@ -64,34 +66,15 @@ ament_export_targets(export_vda5050_json_utils HAS_LIBRARY_TARGET) ament_export_dependencies( nlohmann_json vda5050_types - vda5050_msgs ) if(ENABLE_ROS2) - ament_export_dependencies(vda5050_msgs) + ament_export_dependencies(vda5050_interfaces) endif() if(BUILD_TESTING) - find_package(ament_cmake_cppcheck REQUIRED) - ament_cppcheck() - - find_package(ament_cmake_cpplint REQUIRED) - - set(cpplint_filters - "-whitespace/newline" - ) - - ament_cpplint( - FILTERS "${cpplint_filters}" - ) - - find_package(ament_cmake_clang_format REQUIRED) - ament_clang_format( - CONFIG_FILE "${CMAKE_CURRENT_SOURCE_DIR}/../.clang-format" - ) - - find_package(ament_cmake_copyright REQUIRED) - ament_copyright() + include(../cmake/vda5050_lint_common.cmake) + vda5050_lint_common() find_package(ament_cmake_gtest REQUIRED) diff --git a/vda5050_json_utils/include/vda5050_json_utils/serialization.hpp b/vda5050_json_utils/include/vda5050_json_utils/serialization.hpp index 5ec11011..b010000a 100644 --- a/vda5050_json_utils/include/vda5050_json_utils/serialization.hpp +++ b/vda5050_json_utils/include/vda5050_json_utils/serialization.hpp @@ -19,7 +19,6 @@ #ifndef VDA5050_JSON_UTILS__SERIALIZATION_HPP_ #define VDA5050_JSON_UTILS__SERIALIZATION_HPP_ -#include #include #include @@ -37,9 +36,11 @@ #include #include #include +#include #include #include #include +#include #include #include #include @@ -49,27 +50,31 @@ #include #include #include +#include #ifdef ENABLE_ROS2 -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include #endif // ENABLE_ROS2 #include "traits.hpp" @@ -1835,11 +1840,41 @@ inline void from_json(const nlohmann::json& j, Order& msg) vda5050_types::order_detail::from_json(j, msg); } +inline void to_json(nlohmann::json& /*j*/, const InstantActions& /*msg*/) +{ + // TODO(sauk): Add missing serialization +} + +inline void from_json(const nlohmann::json& /*j*/, InstantActions& /*msg*/) +{ + // TODO(sauk): Add missing deserialization +} + +inline void to_json(nlohmann::json& /*j*/, const Factsheet& /*msg*/) +{ + // TODO(sauk): Add missing serialization +} + +inline void from_json(const nlohmann::json& /*j*/, Factsheet& /*msg*/) +{ + // TODO(sauk): Add missing deserialization +} + +inline void to_json(nlohmann::json& /*j*/, const Visualization& /*msg*/) +{ + // TODO(sauk): Add missing serialization +} + +inline void from_json(const nlohmann::json& /*j*/, Visualization& /*msg*/) +{ + // TODO(sauk): Add missing deserialization +} + } // namespace vda5050_types //============================================================================= #ifdef ENABLE_ROS2 -namespace vda5050_msgs { +namespace vda5050_interfaces { namespace msg { @@ -2093,9 +2128,39 @@ inline void from_json(const nlohmann::json& j, Order& msg) vda5050_types::order_detail::from_json(j, msg); } +inline void to_json(nlohmann::json& /*j*/, const InstantActions& /*msg*/) +{ + // TODO(sauk): Add missing serialization +} + +inline void from_json(const nlohmann::json& /*j*/, InstantActions& /*msg*/) +{ + // TODO(sauk): Add missing deserialization +} + +inline void to_json(nlohmann::json& /*j*/, const Factsheet& /*msg*/) +{ + // TODO(sauk): Add missing serialization +} + +inline void from_json(const nlohmann::json& /*j*/, Factsheet& /*msg*/) +{ + // TODO(sauk): Add missing deserialization +} + +inline void to_json(nlohmann::json& /*j*/, const Visualization& /*msg*/) +{ + // TODO(sauk): Add missing serialization +} + +inline void from_json(const nlohmann::json& /*j*/, Visualization& /*msg*/) +{ + // TODO(sauk): Add missing deserialization +} + } // namespace msg -} // namespace vda5050_msgs +} // namespace vda5050_interfaces #endif // ENABLE_ROS2 #endif // VDA5050_JSON_UTILS__SERIALIZATION_HPP_ diff --git a/vda5050_json_utils/include/vda5050_json_utils/traits.hpp b/vda5050_json_utils/include/vda5050_json_utils/traits.hpp index fd2ec244..ae7740bd 100644 --- a/vda5050_json_utils/include/vda5050_json_utils/traits.hpp +++ b/vda5050_json_utils/include/vda5050_json_utils/traits.hpp @@ -42,14 +42,14 @@ #ifdef ENABLE_ROS2 #include -#include -#include -#include -#include -#include -#include -#include -#include +#include +#include +#include +#include +#include +#include +#include +#include #endif // ENABLE_ROS2 namespace vda5050_json_utils { @@ -290,7 +290,7 @@ struct connection_state_traits { static std::string to_string(const std::string& state) { - using vda5050_msgs::msg::Connection; + using vda5050_interfaces::msg::Connection; if ( state == Connection::ONLINE || state == Connection::OFFLINE || @@ -303,7 +303,7 @@ struct connection_state_traits static std::string from_string(const std::string& state) { - using vda5050_msgs::msg::Connection; + using vda5050_interfaces::msg::Connection; if ( state == Connection::ONLINE || state == Connection::OFFLINE || @@ -365,7 +365,7 @@ struct operating_mode_traits { static std::string to_string(const std::string& mode) { - using vda5050_msgs::msg::State; + using vda5050_interfaces::msg::State; if ( mode == State::OPERATING_MODE_AUTOMATIC || @@ -381,7 +381,7 @@ struct operating_mode_traits static std::string from_string(const std::string& mode) { - using vda5050_msgs::msg::State; + using vda5050_interfaces::msg::State; if ( mode == State::OPERATING_MODE_AUTOMATIC || @@ -449,7 +449,7 @@ struct action_status_traits { static std::string to_string(const std::string& status) { - using vda5050_msgs::msg::ActionState; + using vda5050_interfaces::msg::ActionState; if ( status == ActionState::ACTION_STATUS_WAITING || @@ -466,7 +466,7 @@ struct action_status_traits static std::string from_string(const std::string& status) { - using vda5050_msgs::msg::ActionState; + using vda5050_interfaces::msg::ActionState; if ( status == ActionState::ACTION_STATUS_WAITING || @@ -523,7 +523,7 @@ struct error_level_traits { static std::string to_string(const std::string& level) { - using vda5050_msgs::msg::Error; + using vda5050_interfaces::msg::Error; if ( level == Error::ERROR_LEVEL_WARNING || level == Error::ERROR_LEVEL_FATAL) @@ -535,7 +535,7 @@ struct error_level_traits static std::string from_string(const std::string& level) { - using vda5050_msgs::msg::Error; + using vda5050_interfaces::msg::Error; if ( level == Error::ERROR_LEVEL_WARNING || level == Error::ERROR_LEVEL_FATAL) @@ -593,7 +593,7 @@ struct e_stop_traits { static std::string to_string(const std::string& type) { - using vda5050_msgs::msg::SafetyState; + using vda5050_interfaces::msg::SafetyState; if ( type == SafetyState::E_STOP_AUTOACK || @@ -607,7 +607,7 @@ struct e_stop_traits static std::string from_string(const std::string& type) { - using vda5050_msgs::msg::SafetyState; + using vda5050_interfaces::msg::SafetyState; if ( type == SafetyState::E_STOP_AUTOACK || @@ -661,7 +661,7 @@ struct info_level_traits { static std::string to_string(const std::string& level) { - using vda5050_msgs::msg::Info; + using vda5050_interfaces::msg::Info; if (level == Info::INFO_LEVEL_INFO || level == Info::INFO_LEVEL_DEBUG) { @@ -672,7 +672,7 @@ struct info_level_traits static std::string from_string(const std::string& level) { - using vda5050_msgs::msg::Info; + using vda5050_interfaces::msg::Info; if (level == Info::INFO_LEVEL_INFO || level == Info::INFO_LEVEL_DEBUG) { @@ -726,7 +726,7 @@ struct blocking_type_traits { static std::string to_string(const std::string& type) { - using vda5050_msgs::msg::Action; + using vda5050_interfaces::msg::Action; if ( type == Action::BLOCKING_TYPE_NONE || @@ -739,7 +739,7 @@ struct blocking_type_traits static std::string from_string(const std::string& type) { - using vda5050_msgs::msg::Action; + using vda5050_interfaces::msg::Action; if ( type == Action::BLOCKING_TYPE_NONE || @@ -792,7 +792,7 @@ struct orientation_type_traits { static std::string to_string(const std::string& type) { - using vda5050_msgs::msg::Edge; + using vda5050_interfaces::msg::Edge; if ( type == Edge::ORIENTATION_TYPE_TANGENTIAL || @@ -805,7 +805,7 @@ struct orientation_type_traits static std::string from_string(const std::string& type) { - using vda5050_msgs::msg::Edge; + using vda5050_interfaces::msg::Edge; if ( type == Edge::ORIENTATION_TYPE_TANGENTIAL || diff --git a/vda5050_json_utils/package.xml b/vda5050_json_utils/package.xml index 524e58e6..fc0f5baf 100644 --- a/vda5050_json_utils/package.xml +++ b/vda5050_json_utils/package.xml @@ -10,7 +10,8 @@ ament_cmake vda5050_types - vda5050_msgs + vda5050_interfaces + nlohmann-json-dev ament_cmake_cppcheck ament_cmake_cpplint diff --git a/vda5050_json_utils/test/generator/generator_ros2.hpp b/vda5050_json_utils/test/generator/generator_ros2.hpp index 016b5522..36048b88 100644 --- a/vda5050_json_utils/test/generator/generator_ros2.hpp +++ b/vda5050_json_utils/test/generator/generator_ros2.hpp @@ -24,57 +24,57 @@ #include #include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -using vda5050_msgs::msg::Action; -using vda5050_msgs::msg::ActionParameter; -using vda5050_msgs::msg::ActionState; -using vda5050_msgs::msg::AGVPosition; -using vda5050_msgs::msg::BatteryState; -using vda5050_msgs::msg::BoundingBoxReference; -using vda5050_msgs::msg::Connection; -using vda5050_msgs::msg::ControlPoint; -using vda5050_msgs::msg::Edge; -using vda5050_msgs::msg::EdgeState; -using vda5050_msgs::msg::Error; -using vda5050_msgs::msg::ErrorReference; -using vda5050_msgs::msg::Header; -using vda5050_msgs::msg::Info; -using vda5050_msgs::msg::InfoReference; -using vda5050_msgs::msg::Load; -using vda5050_msgs::msg::LoadDimensions; -using vda5050_msgs::msg::Node; -using vda5050_msgs::msg::NodePosition; -using vda5050_msgs::msg::NodeState; -using vda5050_msgs::msg::Order; -using vda5050_msgs::msg::SafetyState; -using vda5050_msgs::msg::State; -using vda5050_msgs::msg::Trajectory; -using vda5050_msgs::msg::Velocity; +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using vda5050_interfaces::msg::Action; +using vda5050_interfaces::msg::ActionParameter; +using vda5050_interfaces::msg::ActionState; +using vda5050_interfaces::msg::AGVPosition; +using vda5050_interfaces::msg::BatteryState; +using vda5050_interfaces::msg::BoundingBoxReference; +using vda5050_interfaces::msg::Connection; +using vda5050_interfaces::msg::ControlPoint; +using vda5050_interfaces::msg::Edge; +using vda5050_interfaces::msg::EdgeState; +using vda5050_interfaces::msg::Error; +using vda5050_interfaces::msg::ErrorReference; +using vda5050_interfaces::msg::Header; +using vda5050_interfaces::msg::Info; +using vda5050_interfaces::msg::InfoReference; +using vda5050_interfaces::msg::Load; +using vda5050_interfaces::msg::LoadDimensions; +using vda5050_interfaces::msg::Node; +using vda5050_interfaces::msg::NodePosition; +using vda5050_interfaces::msg::NodeState; +using vda5050_interfaces::msg::Order; +using vda5050_interfaces::msg::SafetyState; +using vda5050_interfaces::msg::State; +using vda5050_interfaces::msg::Trajectory; +using vda5050_interfaces::msg::Velocity; /// \brief Utility class to generate random instances of VDA 5050 message types class RandomDataGeneratorROS2 diff --git a/vda5050_json_utils/test/test_json_utils_ros2.cpp b/vda5050_json_utils/test/test_json_utils_ros2.cpp index a4d4d59e..6226052c 100644 --- a/vda5050_json_utils/test/test_json_utils_ros2.cpp +++ b/vda5050_json_utils/test/test_json_utils_ros2.cpp @@ -20,61 +20,61 @@ #include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include #include "vda5050_json_utils/serialization.hpp" #include "generator/generator_ros2.hpp" -using vda5050_msgs::msg::Action; -using vda5050_msgs::msg::ActionParameter; -using vda5050_msgs::msg::ActionState; -using vda5050_msgs::msg::AGVPosition; -using vda5050_msgs::msg::BatteryState; -using vda5050_msgs::msg::BoundingBoxReference; -using vda5050_msgs::msg::Connection; -using vda5050_msgs::msg::ControlPoint; -using vda5050_msgs::msg::Edge; -using vda5050_msgs::msg::EdgeState; -using vda5050_msgs::msg::Error; -using vda5050_msgs::msg::ErrorReference; -using vda5050_msgs::msg::Header; -using vda5050_msgs::msg::Info; -using vda5050_msgs::msg::InfoReference; -using vda5050_msgs::msg::Load; -using vda5050_msgs::msg::LoadDimensions; -using vda5050_msgs::msg::Node; -using vda5050_msgs::msg::NodePosition; -using vda5050_msgs::msg::NodeState; -using vda5050_msgs::msg::Order; -using vda5050_msgs::msg::SafetyState; -using vda5050_msgs::msg::State; -using vda5050_msgs::msg::Trajectory; -using vda5050_msgs::msg::Velocity; +using vda5050_interfaces::msg::Action; +using vda5050_interfaces::msg::ActionParameter; +using vda5050_interfaces::msg::ActionState; +using vda5050_interfaces::msg::AGVPosition; +using vda5050_interfaces::msg::BatteryState; +using vda5050_interfaces::msg::BoundingBoxReference; +using vda5050_interfaces::msg::Connection; +using vda5050_interfaces::msg::ControlPoint; +using vda5050_interfaces::msg::Edge; +using vda5050_interfaces::msg::EdgeState; +using vda5050_interfaces::msg::Error; +using vda5050_interfaces::msg::ErrorReference; +using vda5050_interfaces::msg::Header; +using vda5050_interfaces::msg::Info; +using vda5050_interfaces::msg::InfoReference; +using vda5050_interfaces::msg::Load; +using vda5050_interfaces::msg::LoadDimensions; +using vda5050_interfaces::msg::Node; +using vda5050_interfaces::msg::NodePosition; +using vda5050_interfaces::msg::NodeState; +using vda5050_interfaces::msg::Order; +using vda5050_interfaces::msg::SafetyState; +using vda5050_interfaces::msg::State; +using vda5050_interfaces::msg::Trajectory; +using vda5050_interfaces::msg::Velocity; // List of types to be tested for serialization round-trip using SerializableTypesROS2 = ::testing::Types< diff --git a/vda5050_types/CMakeLists.txt b/vda5050_types/CMakeLists.txt index c1f1f015..c89e7485 100644 --- a/vda5050_types/CMakeLists.txt +++ b/vda5050_types/CMakeLists.txt @@ -1,6 +1,9 @@ cmake_minimum_required(VERSION 3.20) project(vda5050_types) +if(NOT CMAKE_CXX_STANDARD) + set(CMAKE_CXX_STANDARD 17) +endif() if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang") add_compile_options(-Wall -Wextra -Wpedantic) endif() @@ -31,19 +34,8 @@ ament_export_include_directories(include) ament_export_targets(export_vda5050_types HAS_LIBRARY_TARGET) if(BUILD_TESTING) - find_package(ament_cmake_cppcheck REQUIRED) - ament_cppcheck() - - find_package(ament_cmake_cpplint REQUIRED) - ament_cpplint() - - find_package(ament_cmake_clang_format REQUIRED) - ament_clang_format( - CONFIG_FILE "${CMAKE_CURRENT_SOURCE_DIR}/../.clang-format" - ) - - find_package(ament_cmake_copyright REQUIRED) - ament_copyright() + include(../cmake/vda5050_lint_common.cmake) + vda5050_lint_common() endif() ament_package()