From c470656a8173cbdb78c0dca4235e57bc5c9b7842 Mon Sep 17 00:00:00 2001 From: Cody Washington <115746975+Jelatinone@users.noreply.github.com> Date: Thu, 23 Apr 2026 17:09:31 -0500 Subject: [PATCH 1/4] Squashed 'src/tools/point_cloud_msg_wrapper/' content from commit 0a62aff git-subtree-dir: src/tools/point_cloud_msg_wrapper git-subtree-split: 0a62affd736fc39744467ff3a68d7e1f6c3d1bb5 --- .gitlab-ci.yml | 80 +++ LICENSE | 201 +++++++ README.md | 24 + point_cloud_msg_wrapper/CMakeLists.txt | 44 ++ .../design/point_cloud_msg_wrapper-design.md | 86 +++ .../default_field_generators.hpp | 36 ++ .../field_generators.hpp | 157 ++++++ .../field_properties.hpp | 135 +++++ .../point_cloud_msg_wrapper.hpp | 507 ++++++++++++++++++ .../point_cloud_msg_wrapper/type_traits.hpp | 85 +++ .../visibility_control.hpp | 40 ++ point_cloud_msg_wrapper/package.xml | 24 + .../test/test_field_generators.cpp | 53 ++ .../test/test_point_cloud_msg_wrapper.cpp | 364 +++++++++++++ 14 files changed, 1836 insertions(+) create mode 100644 .gitlab-ci.yml create mode 100644 LICENSE create mode 100644 README.md create mode 100644 point_cloud_msg_wrapper/CMakeLists.txt create mode 100644 point_cloud_msg_wrapper/design/point_cloud_msg_wrapper-design.md create mode 100644 point_cloud_msg_wrapper/include/point_cloud_msg_wrapper/default_field_generators.hpp create mode 100644 point_cloud_msg_wrapper/include/point_cloud_msg_wrapper/field_generators.hpp create mode 100644 point_cloud_msg_wrapper/include/point_cloud_msg_wrapper/field_properties.hpp create mode 100644 point_cloud_msg_wrapper/include/point_cloud_msg_wrapper/point_cloud_msg_wrapper.hpp create mode 100644 point_cloud_msg_wrapper/include/point_cloud_msg_wrapper/type_traits.hpp create mode 100644 point_cloud_msg_wrapper/include/point_cloud_msg_wrapper/visibility_control.hpp create mode 100644 point_cloud_msg_wrapper/package.xml create mode 100644 point_cloud_msg_wrapper/test/test_field_generators.cpp create mode 100644 point_cloud_msg_wrapper/test/test_point_cloud_msg_wrapper.cpp diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000..513c85a --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,80 @@ +stages: + - build + - test + - deploy + +build: + stage: build + image: osrf/ros:foxy-desktop + script: + - apt-get update + - rosdep update + - rosdep install --from point_cloud_msg_wrapper --ignore-src -y + - mkdir -p /opt/point_cloud_msg_wrapper + - ' + colcon build + --install-base /opt/point_cloud_msg_wrapper + --merge-install + --cmake-args + -DCMAKE_BUILD_TYPE=Release + --base-path point_cloud_msg_wrapper + ' + - cp LICENSE /opt/point_cloud_msg_wrapper + - tar cfz opt.tar.gz /opt/point_cloud_msg_wrapper + artifacts: + expire_in: 1 day + when: always + paths: + - log + - build + - opt.tar.gz + +test: + stage: test + image: osrf/ros:foxy-desktop + dependencies: + - build + script: + - tar xf opt.tar.gz -C / + - ' + colcon test + --install-base /opt/point_cloud_msg_wrapper + --merge-install + --base-path point_cloud_msg_wrapper + ' + - 'colcon test-result --verbose' + artifacts: + expire_in: 1 day + when: always + paths: + - log + +volume: + stage: deploy + image: docker + services: + - docker:dind + dependencies: + - build + before_script: + - '' + script: + - 'echo -e "*\n!opt.tar.gz" > .dockerignore' + - | + cat >Dockerfile < modifier{cloud, "frame_id"}; +for (const auto& point : {{42, 42}, {23, 42}}) { + modifier.push_back(point); +} +``` + +For more details please see [design doc](point_cloud_msg_wrapper/design/point_cloud_msg_wrapper-design.md). + + +## Acknowledgments + +Many thanks go to @xmfcx who has pointed out an idea to use an existing iterator instead of writing +our own as well as for his reviews of the initial version of this code. This has significantly +increased the readability and made the code less error prone. Thanks! diff --git a/point_cloud_msg_wrapper/CMakeLists.txt b/point_cloud_msg_wrapper/CMakeLists.txt new file mode 100644 index 0000000..fd2f63d --- /dev/null +++ b/point_cloud_msg_wrapper/CMakeLists.txt @@ -0,0 +1,44 @@ +# Copyright 2021 Apex.AI, Inc. +# All rights reserved. +cmake_minimum_required(VERSION 3.5) +project(point_cloud_msg_wrapper) + +# Default to C++14 +if(NOT CMAKE_CXX_STANDARD) + set(CMAKE_CXX_STANDARD 14) +endif() + +find_package(ament_cmake_auto REQUIRED) +ament_auto_find_build_dependencies() + +add_library(${PROJECT_NAME} INTERFACE) +target_include_directories(${PROJECT_NAME} INTERFACE include/) + +if(BUILD_TESTING) + # run common linters + find_package(ament_lint_auto) + ament_lint_auto_find_test_dependencies() + ament_add_gtest(${PROJECT_NAME}_test + test/test_field_generators.cpp + test/test_point_cloud_msg_wrapper.cpp + ) + target_compile_options(${PROJECT_NAME}_test PRIVATE + -Wall + -Wextra + -Wpedantic + -Wall + -Wconversion + -Wsign-conversion + -Werror + -Wcast-align + -Wunused + -Wsign-conversion + -Wdouble-promotion) + + ament_target_dependencies(${PROJECT_NAME}_test sensor_msgs geometry_msgs) + target_include_directories(${PROJECT_NAME}_test PRIVATE include) + target_include_directories(${PROJECT_NAME}_test SYSTEM PRIVATE ${GTEST_INCLUDE_DIRS}) + +endif() + +ament_auto_package() diff --git a/point_cloud_msg_wrapper/design/point_cloud_msg_wrapper-design.md b/point_cloud_msg_wrapper/design/point_cloud_msg_wrapper-design.md new file mode 100644 index 0000000..cdd092f --- /dev/null +++ b/point_cloud_msg_wrapper/design/point_cloud_msg_wrapper-design.md @@ -0,0 +1,86 @@ +Point Cloud Message Wrapper (#point-cloud-msg-wrapper) +=========== + +# Purpose / Use cases + +`PointCloudMsgWrapper` is a class that aims to be used instead of `PointCloud2Iterator` and +`PointCloud2Modifier`. The core idea is that it wraps a point cloud message and allows treating it +essentially as a vector of points. To create a wrapper the user needs to specify the type of points +they want to use and a message that the wrapper wraps. If the fields stored in the point cloud +message do not match the fields or methods of the point struct a runtime error is thrown. An array +of PointField objects can be generated from a given point type automatically through the mechanism +of field generators defined in file `field_generators.hpp` and passed as a tuple into the +`PointCloudMsgWrapper`. If a point has `float x`, `float y`, and `uint8_t intensity` members (or +functions that return non-const references to such fields, e.g. `float& x()`, `float& y()` and +`uint8_t& intensity()`), these generators must be generated (these particular ones _are_ generated +by default, see `default_field_generators.hpp`): + +```c++ +LIDAR_UTILS__DEFINE_FIELD_GENERATOR_FOR_MEMBER(x); +LIDAR_UTILS__DEFINE_FIELD_GENERATOR_FOR_MEMBER(y); +LIDAR_UTILS__DEFINE_FIELD_GENERATOR_FOR_MEMBER(intensity); +``` + +These create classes `field_x_generator`, `field_y_generator`, `field_intensity_generator` that are +then passed as a tuple as a template parameter into the `PointCloudMsgWrapper`. + +Other field generators can be generated if needed by the user, but we strive to provide a sane +default value. It is expected that the user provides all needed generators that can be created with +the macros above into the wrapper class upon construction. If the user fails to provide the +appropriate generators an error will be thrown when wrapping an existing point cloud message as an +expected field from a point cloud message would not be matched by the generated ones. See the +default ones in the variable `detail::DefaultFieldGenerators` in `point_cloud_msg_wrapper.hpp` file. + +For convenience, there are two typedefs: `PointCloud2View` and `PointCloud2Modifier`. + +- `PointCloudView{msg}` wraps a constant point cloud and allows read-only access. + Modification through this view is impossible. +- `PointCloud2Modifier{msg}` wraps a mutable message and allows read-write access to the + underlying data. +- `PointCloud2Modifier{msg, new_frame_id}` initializes an empty mutable message. This + constructor is to be used solely to initialize a new message and will throw if the point cloud + message is already initialized (i.e., has non-zero number of fields). + +## Assumptions / Known limits + +### Points used with this class _must_ have an equality operator + +If a point has no equality operator, the user must define a free standing operator for it. Otherwise +a `static_assert` will be hit. + +### For non-common fields, the generators for these must be created by the user + +This package provides a set of generators for common field names, see `default_field_generators.hpp` +file. If other fields are required a custom generator must be created to work with these. We provide +a macro for these purposes: `LIDAR_UTILS__DEFINE_FIELD_GENERATOR_FOR_MEMBER`, also see +`ProcessPointWithCustomField` unit test for an example. + +### Field offsets must match _exactly_ + +To offer safety, fields of the point struct/class must match those stored in the cloud message +_exactly_. That is, if an offset or a type is not strictly matching between the point and the field +stored in the message the wrapper cannot be created for such a combination and an error will be +thrown. If such a situation is encountered, a custom data structure can be created in order to +accomodate for these changes, see `CustomAlignedPoint` struct in the unit tests of this package for +an example. + +### Working with points that have virtual methods + +We allow point type to have any additional functions as long as it has either: +- public members with names matching the wanted fields, e.g. `float x;` +- public methods that return a reference to the needed field, e.g. `float& x() {return m_x;}`. + +That means that some methods can be virtual. In general it is not an issue to work with such types +with `PointCloudMsgWrapper` but one must be careful. It is _forbidden_ to call virtual methods when +reading/writing to a wrapper. The reason is that we memory-map the points into the message, +_including_ the vtable pointer. It is not guaranteed that the same vtable is present when reading +from such a point, so calling any of these methods is undefined behavior. As long as these are not +called, the system behavior is defined. + +## Inputs / Outputs / API + +See the API documentation. + +# Security considerations + +TBD by a security specialist. diff --git a/point_cloud_msg_wrapper/include/point_cloud_msg_wrapper/default_field_generators.hpp b/point_cloud_msg_wrapper/include/point_cloud_msg_wrapper/default_field_generators.hpp new file mode 100644 index 0000000..f0275a2 --- /dev/null +++ b/point_cloud_msg_wrapper/include/point_cloud_msg_wrapper/default_field_generators.hpp @@ -0,0 +1,36 @@ +// Copyright 2021 Apex.AI, Inc. +// +// 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. + +/// \copyright Copyright 2021 Apex.AI, Inc. +/// All rights reserved. + +#ifndef POINT_CLOUD_MSG_WRAPPER__DEFAULT_FIELD_GENERATORS_HPP_ +#define POINT_CLOUD_MSG_WRAPPER__DEFAULT_FIELD_GENERATORS_HPP_ + +#include + +namespace point_cloud_msg_wrapper +{ + +LIDAR_UTILS__DEFINE_FIELD_GENERATOR_FOR_MEMBER(x); +LIDAR_UTILS__DEFINE_FIELD_GENERATOR_FOR_MEMBER(y); +LIDAR_UTILS__DEFINE_FIELD_GENERATOR_FOR_MEMBER(z); +LIDAR_UTILS__DEFINE_FIELD_GENERATOR_FOR_MEMBER(id); +LIDAR_UTILS__DEFINE_FIELD_GENERATOR_FOR_MEMBER(ring); +LIDAR_UTILS__DEFINE_FIELD_GENERATOR_FOR_MEMBER(intensity); +LIDAR_UTILS__DEFINE_FIELD_GENERATOR_FOR_MEMBER(timestamp); + +} // namespace point_cloud_msg_wrapper + +#endif // POINT_CLOUD_MSG_WRAPPER__DEFAULT_FIELD_GENERATORS_HPP_ diff --git a/point_cloud_msg_wrapper/include/point_cloud_msg_wrapper/field_generators.hpp b/point_cloud_msg_wrapper/include/point_cloud_msg_wrapper/field_generators.hpp new file mode 100644 index 0000000..6b35b5f --- /dev/null +++ b/point_cloud_msg_wrapper/include/point_cloud_msg_wrapper/field_generators.hpp @@ -0,0 +1,157 @@ +// Copyright 2021 Apex.AI, Inc. +// +// 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. + +/// \copyright Copyright 2021 Apex.AI, Inc. +/// All rights reserved. + +#ifndef POINT_CLOUD_MSG_WRAPPER__FIELD_GENERATORS_HPP_ +#define POINT_CLOUD_MSG_WRAPPER__FIELD_GENERATORS_HPP_ + +#include +#include + +namespace point_cloud_msg_wrapper +{ + +namespace detail +{ +template +inline std::size_t offset_of(ReturnT ClassT::* member) +{ + ClassT object{}; + return std::size_t(&(object.*member)) - std::size_t(&object); +} + +template +std::size_t offset_of_return_ref(ReturnT (ClassT::* member_func)(void)) +{ + ClassT object; + const auto & result_ref = (object.*member_func)(); + return std::size_t(&result_ref) - std::size_t(&object); +} + +template +void push_back_field( + sensor_msgs::msg::PointCloud2::_fields_type & fields, + const sensor_msgs::msg::PointField::_name_type & name, + MemberT ClassT::* member) +{ + fields.emplace_back(); + fields.back() + .set__name(name) + .set__offset(static_cast(offset_of(member))) + .set__datatype(get_field_datatype()) + .set__count(get_field_count()); +} + +template +void push_back_field( + sensor_msgs::msg::PointCloud2::_fields_type & fields, + const sensor_msgs::msg::PointField::_name_type & name, + ReturnT (ClassT::* member_func)(void)) +{ + using MemberT = std::decay_t; + fields.emplace_back(); + fields.back() + .set__name(name) + .set__offset(static_cast(offset_of_return_ref(member_func))) + .set__datatype(get_field_datatype()) + .set__count(get_field_count()); +} + +/// End of recursion for applying all field adders to a given point type. +template +constexpr std::enable_if_t < (N >= std::tuple_size{}) > +apply_field_adder_to_generate_field_if_needed(sensor_msgs::msg::PointCloud2::_fields_type &) { +} + +/// Standard case to recursively apply all field adders for a given point type. +template +constexpr std::enable_if_t < (N < std::tuple_size{}) > +apply_field_adder_to_generate_field_if_needed(sensor_msgs::msg::PointCloud2::_fields_type & fields) +{ + using CurrentFieldAdder = std::tuple_element_t; + CurrentFieldAdder::template push_back_field_if_needed(fields); + apply_field_adder_to_generate_field_if_needed(fields); +} + +} // namespace detail + + +/// A macro that defines a struct that allows to query for the MEMBER member in any given struct. +/// The intended usecase for this struct is (example for MEMBER 'x'): +/// has_member_x::value +#define IMPL__LIDAR_UTILS__DEFINE_MEMBER_CHECKER(MEMBER) \ + template \ + struct has_member_ ## MEMBER : std::false_type {}; \ + template \ + struct has_member_ ## MEMBER< \ + T, typename std::enable_if< \ + !std::is_same().MEMBER), void>::value, \ + bool>::type>: std::true_type {} + +#define IMPL__LIDAR_UTILS__DEFINE_MEMBER_FUNCTION_CHECKER(FUNCTION_NAME) \ + template \ + struct has_function_ ## FUNCTION_NAME ## _returning_non_const_ref : std::false_type {}; \ + template \ + struct has_function_ ## FUNCTION_NAME ## _returning_non_const_ref< \ + T, std::enable_if_t< \ + std::is_reference().FUNCTION_NAME())>::value && \ + !std::is_const().FUNCTION_NAME())>>::value>>: std::true_type {} + +/// A Macro that defines a generator struct for a given field name. +/// +/// The generator structs take a vector of PointFields and push_back a new +/// PointField into it if the underlying class has the specified field. The +/// existence of the field is checked by the has_member_FIELD::value. +#define LIDAR_UTILS__DEFINE_FIELD_GENERATOR_FOR_MEMBER(FIELD) \ + struct field_ ## FIELD ## _generator { \ + IMPL__LIDAR_UTILS__DEFINE_MEMBER_CHECKER(FIELD); \ + IMPL__LIDAR_UTILS__DEFINE_MEMBER_FUNCTION_CHECKER(FIELD); \ + template \ + static inline std::enable_if_t< \ + !has_member_ ## FIELD::value && \ + !has_function_ ## FIELD ## _returning_non_const_ref::value> \ + push_back_field_if_needed(sensor_msgs::msg::PointCloud2::_fields_type &) { \ + } \ + template \ + static inline std::enable_if_t< \ + has_member_ ## FIELD::value || \ + has_function_ ## FIELD ## _returning_non_const_ref::value> \ + push_back_field_if_needed(sensor_msgs::msg::PointCloud2::_fields_type & fields) { \ + point_cloud_msg_wrapper::detail::push_back_field(fields, #FIELD, &PointT::FIELD); \ + } \ + } + +/// A help function to start iterating over all field adders and return the appropritate fields +/// generated by those. +/// +/// @tparam PointT Type of point +/// @tparam FieldAdders A tuple of field adder types, generated by +/// LIDAR_UTILS__DEFINE_FIELD_GENERATOR_FOR_MEMBER +/// +/// @return All generated PointField structs. +/// +template +inline sensor_msgs::msg::PointCloud2::_fields_type generate_fields_from_point() +{ + sensor_msgs::msg::PointCloud2::_fields_type fields; + detail::apply_field_adder_to_generate_field_if_needed(fields); + return fields; +} + +} // namespace point_cloud_msg_wrapper + +#endif // POINT_CLOUD_MSG_WRAPPER__FIELD_GENERATORS_HPP_ diff --git a/point_cloud_msg_wrapper/include/point_cloud_msg_wrapper/field_properties.hpp b/point_cloud_msg_wrapper/include/point_cloud_msg_wrapper/field_properties.hpp new file mode 100644 index 0000000..c85709c --- /dev/null +++ b/point_cloud_msg_wrapper/include/point_cloud_msg_wrapper/field_properties.hpp @@ -0,0 +1,135 @@ +// Copyright 2021 Apex.AI, Inc. +// +// 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. + +/// \copyright Copyright 2021 Apex.AI, Inc. +/// All rights reserved. + +#ifndef POINT_CLOUD_MSG_WRAPPER__FIELD_PROPERTIES_HPP_ +#define POINT_CLOUD_MSG_WRAPPER__FIELD_PROPERTIES_HPP_ + +#include + +#include + +namespace point_cloud_msg_wrapper +{ + +inline std::uint32_t sizeof_field(const std::uint8_t datatype) +{ + switch (datatype) { + case sensor_msgs::msg::PointField::INT8: + case sensor_msgs::msg::PointField::UINT8: + return 2U; + case sensor_msgs::msg::PointField::INT16: + case sensor_msgs::msg::PointField::UINT16: + return 2U; + case sensor_msgs::msg::PointField::INT32: + case sensor_msgs::msg::PointField::UINT32: + case sensor_msgs::msg::PointField::FLOAT32: + return 4U; + case sensor_msgs::msg::PointField::FLOAT64: + return 8U; + } + throw std::runtime_error("Unexpected datatype provided"); +} + +template +inline constexpr std::uint32_t get_field_count() +{ + static_assert(sizeof(T) == -1, "Only specializations of this function are expected"); + return 0U; +} + +template +inline constexpr std::uint8_t get_field_datatype() +{ + static_assert(sizeof(T) == -1, "Only specializations of this function are expected"); + return 0U; +} + +template<> +inline constexpr std::uint32_t get_field_count() {return 1U;} +template<> +inline constexpr std::uint32_t get_field_count() {return 1U;} +template<> +inline constexpr std::uint32_t get_field_count() {return 1U;} +template<> +inline constexpr std::uint32_t get_field_count() {return 1U;} +template<> +inline constexpr std::uint32_t get_field_count() {return 1U;} +template<> +inline constexpr std::uint32_t get_field_count() {return 1U;} +template<> +inline constexpr std::uint32_t get_field_count() {return 1U;} +template<> +inline constexpr std::uint32_t get_field_count() {return 1U;} +template<> +inline constexpr std::uint32_t get_field_count() {return 2U;} +template<> +inline constexpr std::uint32_t get_field_count() {return 2U;} + +template<> +inline constexpr std::uint8_t get_field_datatype() +{ + return sensor_msgs::msg::PointField::FLOAT32; +} +template<> +inline constexpr std::uint8_t get_field_datatype() +{ + return sensor_msgs::msg::PointField::FLOAT64; +} +template<> +inline constexpr std::uint8_t get_field_datatype() +{ + return sensor_msgs::msg::PointField::INT8; +} +template<> +inline constexpr std::uint8_t get_field_datatype() +{ + return sensor_msgs::msg::PointField::UINT8; +} +template<> +inline constexpr std::uint8_t get_field_datatype() +{ + return sensor_msgs::msg::PointField::INT16; +} +template<> +inline constexpr std::uint8_t get_field_datatype() +{ + return sensor_msgs::msg::PointField::UINT16; +} +template<> +inline constexpr std::uint8_t get_field_datatype() +{ + return sensor_msgs::msg::PointField::INT32; +} +template<> +inline constexpr std::uint8_t get_field_datatype() +{ + return sensor_msgs::msg::PointField::UINT32; +} +template<> +inline constexpr std::uint8_t get_field_datatype() +{ + return sensor_msgs::msg::PointField::INT32; +} +template<> +inline constexpr std::uint8_t get_field_datatype() +{ + return sensor_msgs::msg::PointField::UINT32; +} + +} // namespace point_cloud_msg_wrapper + +#endif // POINT_CLOUD_MSG_WRAPPER__FIELD_PROPERTIES_HPP_ diff --git a/point_cloud_msg_wrapper/include/point_cloud_msg_wrapper/point_cloud_msg_wrapper.hpp b/point_cloud_msg_wrapper/include/point_cloud_msg_wrapper/point_cloud_msg_wrapper.hpp new file mode 100644 index 0000000..22d1767 --- /dev/null +++ b/point_cloud_msg_wrapper/include/point_cloud_msg_wrapper/point_cloud_msg_wrapper.hpp @@ -0,0 +1,507 @@ +// Copyright 2021 Apex.AI, Inc. +// +// 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. + +/// \copyright Copyright 2021 Apex.AI, Inc. +/// All rights reserved. + + +#ifndef POINT_CLOUD_MSG_WRAPPER__POINT_CLOUD_MSG_WRAPPER_HPP_ +#define POINT_CLOUD_MSG_WRAPPER__POINT_CLOUD_MSG_WRAPPER_HPP_ + +#include +#include +#include +#include + +#include + +#include +#include +#include +#include + +/// +/// @brief A help macro to simplify conditional compilation based on mutability. +/// +/// This macro just wraps the SFINAE paradigm and enables compilation only if the +/// QUERY_TYPE is mutable. This macro is replaced by RETURN_TYPE in this case. If +/// QUERY_TYPE is const, the code following this macro invocation will not be compiled. +/// +/// @param QUERY_TYPE The query type to be checked for mutability. +/// @param RETURN_TYPE The return type generated if compilation succeeds. +/// +#define COMPILE_IF_MUTABLE(QUERY_TYPE, RETURN_TYPE) \ + template \ + std::enable_if_t::value, RETURN_TYPE> \ + +namespace point_cloud_msg_wrapper +{ + +namespace detail +{ +using DefaultFieldGenerators = std::tuple< + field_x_generator, + field_y_generator, + field_z_generator, + field_id_generator, + field_ring_generator, + field_intensity_generator, + field_timestamp_generator>; +} // namespace detail + +/// +/// @brief This class implements a point cloud message wrapper. Unless otherwise required, use +/// the typedefs of this class: PointCloud2View and PointCloud2Modifier. Only use this +/// class directly if those typedefs do not provide enough flexibility. +/// +/// @details This class is designed to simplify working with point cloud messages. The idea is +/// that it wraps a point cloud message reference allowing for simple access and +/// modification. Upon creation, the wrapper checks if the fields of the message +/// correspond to the fields generated from the point type provided by the user. If this +/// check has passed, then the fields are identical (including the offsets of the +/// members) and it is safe to reinterpret the point cloud message as an array of PointT +/// points. Note that due to these checks it is relatively expensive (the complexity of +/// the checks is around O(n^2) where n is the number of field generators) to create +/// this wrapper, thus it should be avoided in very tight scopes. This operation is not +/// slow, but is significantly slower than a single element access. It is better to +/// create the wrapper once per message in a function scope and batch all the read/write +/// operations afterwards. +/// +/// For convenience, there are two typedefs: PointCloud2View and PointCloud2Modifier. +/// - PointCloudView{msg} wraps a constant point cloud and allows read-only +/// access. Modification through this view is impossible. +/// - PointCloud2Modifier{msg} wraps a mutable message and allows read-write +/// access to the underlying data. +/// - PointCloud2Modifier{msg, new_frame_id} initializes an empty mutable +/// message. This constructor is to be used solely to initialize a new message and +/// will throw if the point cloud message is already initialized (i.e., has non-zero +/// number of fields). +/// +/// @warning This class wraps a raw reference, so the user is responsible to use it in such a way +/// that the underlying point cloud message is not deleted before the wrapper. +/// +/// @tparam PointT Type of point to use for message reading/writing. +/// @tparam PointCloudMsgT Type of point cloud message. +/// @tparam FieldGenerators A tuple of all field generators that allow generating Field structs +/// from members of a PointT struct. See +/// LIDAR_UTILS__DEFINE_FIELD_GENERATOR_FOR_MEMBER for more details. +/// The class provides a sane default value here, but a custom tuple +/// can be generated by the user by using +/// LIDAR_UTILS__DEFINE_FIELD_GENERATOR_FOR_MEMBER if needed. +/// @tparam kIsMutable Define if the point cloud message is mutable. Used to conditionally +/// compile certain functions. +/// @tparam AllocatorT The message allocator type. This allocator type must be rebindable +/// to the PointT allocator through the use of +/// `std::allocator_traits::rebind_alloc`. +/// +template< + typename PointT, + template class PointCloudMsgT, + typename FieldGenerators, + const bool kIsMutable, + typename AllocatorT> +class PointCloudMsgWrapper +{ + /// Depending on the provided kIsMutable boolean use the const or a mutable point cloud msg type. + using CloudMsgT = + std::conditional_t, const PointCloudMsgT>; + + static_assert( + std::is_default_constructible::value, + "\n\nThe point type must satisfy is_default_constructible trait.\n\n"); + + static_assert( + detail::is_specialization::value, + "\n\nFieldGenerators must be an std::tuple.\n\n"); + + static_assert( + std::is_same, sensor_msgs::msg::PointCloud2_>::value, + "\n\nThis class is designed to work with PointCloud2 messages only for now.\n\n"); + + /// Type of the fields entry + using FieldNameT = typename sensor_msgs::msg::PointField_::_name_type; + /// Type of the data entry + using DataVectorT = typename sensor_msgs::msg::PointCloud2_::_data_type; + + /// Get the type of the allocator to use with points. + using PointAllocatorT = typename std::allocator_traits::template rebind_alloc; + + /// Derive a point vector type (usually std::vector). Needed in case a different (e.g. + /// bounded vector) type used instead and use it for the Point type with the msg Allocator. + using PointVectorType = + typename detail::derived_point_vector::template type; + + /// Size of the point. + using kSizeofPoint = + std::integral_constant(sizeof(PointT))>; + +public: + using value_type = PointT; + using iterator = typename PointVectorType::iterator; + using const_iterator = typename PointVectorType::const_iterator; + using reverse_iterator = typename PointVectorType::reverse_iterator; + using const_reverse_iterator = typename PointVectorType::const_reverse_iterator; + + /// + /// @brief Constructor that wraps a point cloud message. Checks if the message fields + /// correspond to the ones generated from the PointT type and throws in case of a + /// mismatch. + /// + /// @param cloud_ref A reference to the wrapped cloud. + /// + explicit PointCloudMsgWrapper(CloudMsgT & cloud_ref) + : m_cloud_ref{cloud_ref} + { + if (m_cloud_ref.fields.empty()) { + throw std::runtime_error( + "Trying to wrap a message with no fields. Reset this message with" + "'create_wrapper_from_empty_msg' function instead!"); + } + std::string error_msg{}; + if (!can_be_created_from(cloud_ref, &error_msg)) { + throw std::runtime_error(error_msg); + } + } + + /// + /// A constructor that initializes the message and sets a new frame_id. Note that this only + /// compiles for a mutable cloud. It also throws if the message is already initialized. + /// + /// @param cloud_ref A reference to the wrapped cloud + /// @param[in] frame_id A frame id to be set to the message. + /// + explicit PointCloudMsgWrapper(CloudMsgT & cloud_ref, const FieldNameT & frame_id) + : m_cloud_ref{cloud_ref} + { + static_assert( + !std::is_const::value, + "\n\nWe must be able to modify this point cloud.\n\n"); + static_assert( + detail::has_operator_equals::value, + "\n\nTo guarantee that all struct members are present in the fields of the message, " + "the point struct needs an equality operator defined.\n\n"); + static_assert( + sizeof(PointT) < static_cast(UINT32_MAX), + "\n\nOnly points with sizeof that fits in uint32_t are supported\n\n"); + + if (!m_cloud_ref.fields.empty()) { + throw std::runtime_error( + "Trying to reset a non-empty point cloud message." + " Use reset_msg with the correct wrapper instead."); + } + const auto generated_fields = generate_fields_from_point(); + if (!check_that_generated_fields_cover_all_point_members(generated_fields)) { + throw std::runtime_error( + "Generated fields don't match the members of the point struct. " + "Make sure you have correct generators tuple passed into this class."); + } + reset_msg(frame_id, generated_fields); + } + + + /// + /// @brief Determines ability to be created from a given cloud. + /// + /// @param[in] cloud_msg The cloud message + /// @param error_msg Optional error message + /// + /// @return True if able to be created for the provided cloud, False otherwise. + /// + static bool can_be_created_from(const CloudMsgT & cloud_msg, std::string * error_msg = nullptr) + { + const auto find_missing_field = []( + const auto & query_fields, + const auto & source_fields) -> const FieldNameT * { + for (const auto & query_field : query_fields) { + // Note that we use find on a vector here. This is intended. The number of fields is + // usually very limited, so the O(n^2) complexity is ok here. This operation also only + // takes place roughly once per message. The subsequent read/write operations are then + // appropriately fast. + const auto corresponding_field_iter = std::find_if( + source_fields.begin(), source_fields.end(), [&query_field](const auto & field) { + const bool equal{ + (field.name == query_field.name) && (field.count == query_field.count) && + (field.datatype == query_field.datatype) && (field.offset == query_field.offset)}; + return equal; + }); + const auto found_corresponding_field = (corresponding_field_iter != source_fields.end()); + if (!found_corresponding_field) {return &query_field.name;} + } + return nullptr; + }; + + const auto struct_fields = generate_fields_from_point(); + const auto missing_field_in_cloud = find_missing_field(struct_fields, cloud_msg.fields); + if (missing_field_in_cloud) { + if (error_msg) { + *error_msg = "Point struct has a field that the cloud does not! Field: " + + *missing_field_in_cloud; + } + return false; + } + const auto missing_field_in_struct = find_missing_field(cloud_msg.fields, struct_fields); + if (missing_field_in_struct) { + if (error_msg) { + *error_msg = "Cloud has a field that the point struct does not! Field: " + + *missing_field_in_struct; + } + return false; + } + if (cloud_msg.point_step != kSizeofPoint::value) { + if (error_msg) { + *error_msg = + "Point cloud was created with a point of different sizeof. " + "Are the point members in the same order? Cloud point step = " + std::to_string( + cloud_msg.point_step) + " while sizeof(PointT) = " + std::to_string(sizeof(PointT)); + } + return false; + } + if (sizeof(PointT) * cloud_msg.width != cloud_msg.data.size()) { + if (error_msg) { + *error_msg = + "Point cloud data size " + std::to_string(cloud_msg.data.size()) + + " does not match the data size derived from the point size of count, which is " + + std::to_string(sizeof(PointT) * cloud_msg.width); + } + return false; + } + return true; + } + + /// @brief Push a new point into the message. + COMPILE_IF_MUTABLE(CloudMsgT, void) push_back(const PointT & point) + { + PointT point_copy{point}; + extend_data_by(sizeof(PointT)); + m_cloud_ref.row_step += kSizeofPoint::value; + m_cloud_ref.width++; + this->operator[](m_cloud_ref.width - 1U) = point_copy; + } + + /// @brief Push a new point into the message. + COMPILE_IF_MUTABLE(CloudMsgT, void) push_back(PointT && point) + { + extend_data_by(sizeof(PointT)); + m_cloud_ref.row_step += kSizeofPoint::value; + m_cloud_ref.width++; + this->operator[](m_cloud_ref.width - 1U) = std::move(point); + } + + /// Get the number of points in the message. + std::size_t size() const noexcept {return m_cloud_ref.width;} + + /// Check if the point cloud message stores no points. + bool empty() const noexcept {return m_cloud_ref.width == 0U;} + + /// Get the first point. + COMPILE_IF_MUTABLE(CloudMsgT, PointT &) front() noexcept {return *begin();} + /// Get the first point. + const PointT & front() const noexcept {return *begin();} + /// Get the last point. + COMPILE_IF_MUTABLE(CloudMsgT, PointT &) back() noexcept {return *rbegin();} + /// Get the last point. + const PointT & back() const noexcept {return *rbegin();} + + /// Get a point reference at the specified index. + /// @throws std::runtime_error if the index is out of bounds. + const PointT & at(const std::size_t index) const + { + if (index >= size()) { + throw std::out_of_range( + "Index is out of bounds, " + + std::to_string(index) + " >= " + std::to_string(size())); + } + return reinterpret_cast( + *(m_cloud_ref.data.data() + index * sizeof(PointT))); + } + /// Get a point reference at the specified index. Only compiled if message is not const. + /// @throws std::runtime_error if the index is out of bounds. + COMPILE_IF_MUTABLE(CloudMsgT, PointT &) at(const std::size_t index) + { + if (index >= size()) { + throw std::out_of_range( + "Index is out of bounds, " + + std::to_string(index) + " >= " + std::to_string(size())); + } + return reinterpret_cast(*(m_cloud_ref.data.data() + index * sizeof(PointT))); + } + + /// Get a point reference at the specified index. + const PointT & operator[](const std::size_t index) const noexcept + { + return *reinterpret_cast( + m_cloud_ref.data.data() + index * sizeof(PointT)); + } + /// Get a point reference as a specified index. Only compiled if message type is not const. + COMPILE_IF_MUTABLE(CloudMsgT, PointT &) operator[](const std::size_t index) noexcept + { + return *reinterpret_cast(m_cloud_ref.data.data() + index * sizeof(PointT)); + } + + /// @brief Reset the message fields to match the members of the PointT struct. The point + /// cloud message is ready for modification after this operation. + COMPILE_IF_MUTABLE(CloudMsgT, void) reset_msg(const FieldNameT & frame_id) + { + reset_msg(frame_id, generate_fields_from_point()); + } + + /// @brief Clear the message. + COMPILE_IF_MUTABLE(CloudMsgT, void) clear() { + reset_msg(m_cloud_ref.header.frame_id); + } + + /// @brief Allocate memory to hold a specified number of points. + COMPILE_IF_MUTABLE(CloudMsgT, void) reserve(const size_t expected_number_of_points) { + m_cloud_ref.data.reserve(sizeof(PointT) * expected_number_of_points); + } + + /// @brief Resize the container to hold a given number of points. + COMPILE_IF_MUTABLE(CloudMsgT, void) resize(const std::uint32_t new_number_of_points) { + m_cloud_ref.width = new_number_of_points; + m_cloud_ref.row_step = m_cloud_ref.width * kSizeofPoint::value; + m_cloud_ref.data.resize(m_cloud_ref.row_step); + } + + /// An iterator to the beginning of data. Only compiled if point cloud message type is not const. + COMPILE_IF_MUTABLE(CloudMsgT, iterator) begin() noexcept { + return iterator{reinterpret_cast(&m_cloud_ref.data[0U])}; + } + /// An iterator to the end of data. Only compiled if point cloud message type is not const. + COMPILE_IF_MUTABLE(CloudMsgT, iterator) end() noexcept { + return iterator{reinterpret_cast(&m_cloud_ref.data[m_cloud_ref.data.size()])}; + } + /// A reverse iterator staring position. Only compiled if point cloud message type is not const. + COMPILE_IF_MUTABLE(CloudMsgT, reverse_iterator) rbegin() noexcept { + return std::make_reverse_iterator(end()); + } + /// A reverse iterator end position. Only compiled if point cloud message type is not const. + COMPILE_IF_MUTABLE(CloudMsgT, reverse_iterator) rend() noexcept { + return std::make_reverse_iterator(begin()); + } + /// A constant iterator to the beginning of data. + const_iterator begin() const noexcept {return cbegin();} + /// A constant iterator to the end of data. + const_iterator end() const noexcept {return cend();} + /// A constant reverse iterator to the beginning of the reversed data. + const_reverse_iterator rbegin() const noexcept {return crbegin();} + /// A constant reverse iterator to the end of reversed data. + const_reverse_iterator rend() const noexcept {return crend();} + /// A constant iterator to the beginning of data. + const_iterator cbegin() const noexcept + { + return const_iterator{reinterpret_cast(&m_cloud_ref.data[0U])}; + } + /// A constant iterator to the end of data. + const_iterator cend() const noexcept + { + return const_iterator{ + reinterpret_cast(&m_cloud_ref.data[m_cloud_ref.data.size()])}; + } + /// A constant reverse iterator to the reverse beginning of data. + const_reverse_iterator crbegin() const noexcept {return std::make_reverse_iterator(cend());} + /// A constant reverse iterator to the reverse end of data. + const_reverse_iterator crend() const noexcept {return std::make_reverse_iterator(cbegin());} + +private: + /// Allocate additional memory in the end of data field of the message. + COMPILE_IF_MUTABLE(CloudMsgT, void) extend_data_by(const std::size_t bytes_to_allocate) + { + const auto new_size = m_cloud_ref.data.size() + bytes_to_allocate; + m_cloud_ref.data.resize(new_size); + } + + /// @brief Reset the message fields to match the members of the PointT struct. The point + /// cloud message is ready for modification after this operation. + COMPILE_IF_MUTABLE(CloudMsgT, void) reset_msg( + const FieldNameT & frame_id, + const sensor_msgs::msg::PointCloud2::_fields_type & generated_fields) + { + // TODO(igor): All these settings can also be specified through some input struct if needed in + // the future. This is omitted for now. + m_cloud_ref.fields = generated_fields; + m_cloud_ref.height = 1; + m_cloud_ref.is_bigendian = false; + m_cloud_ref.is_dense = false; + m_cloud_ref.header.frame_id = frame_id; + m_cloud_ref.point_step = kSizeofPoint::value; + m_cloud_ref.data.clear(); + m_cloud_ref.width = 0U; + m_cloud_ref.row_step = 0U; + } + + bool check_that_generated_fields_cover_all_point_members( + const sensor_msgs::msg::PointCloud2::_fields_type & generated_fields) + { + std::uint32_t sum_of_all_fields_sizes{}; + for (const auto & field : generated_fields) { + sum_of_all_fields_sizes += field.count * sizeof_field(field.datatype); + } + if (sum_of_all_fields_sizes == kSizeofPoint::value) { + // The generated fields cover all members of the struct without gaps. + return true; + } + // The idea is the following: + // 1. Allocate storage as big as a point. + // 2. Fill the storage with zeros. + // 3. Iterate through all fields and set the appropriate memory to ones. + // 4. Flip all bits - now, if all fields are present, only the padding is non-zero. + // 5. Reinterpret this memory as a point. + // 6. Compare this point to a value initialized one. + // If these points are equal - we have all fields. Otherwise, we are missing a field. + alignas(PointT) std::uint8_t buffer[sizeof(PointT)]; + std::fill(buffer, buffer + sizeof(PointT), 0U); + for (const auto & field : generated_fields) { + const auto field_first_byte = field.offset; + const auto field_last_byte = + field.offset + field.count * static_cast(sizeof_field(field.datatype)); + std::fill( + &buffer[field_first_byte], + &buffer[field_last_byte], + std::numeric_limits::max()); + } + for (auto i = 0U; i < sizeof(PointT); ++i) { + buffer[i] = buffer[i] > 0 ? 0 : std::numeric_limits::max(); // Flip all bits. + } + const PointT * const point_from_flipped_memory{reinterpret_cast(buffer)}; + if (PointT{} == *point_from_flipped_memory) { + return true; + } + return false; + } + + /// A reference to the cloud message. + CloudMsgT & m_cloud_ref; +}; + +/// A typedef for the PointCloudMsgWrapper to represent a view that wraps a const cloud message. +template< + typename PointT, + typename FieldGeneratorsT = detail::DefaultFieldGenerators, + typename AllocatorT = std::allocator> +using PointCloud2View = PointCloudMsgWrapper< + PointT, sensor_msgs::msg::PointCloud2_, FieldGeneratorsT, false, AllocatorT>; + +/// A typedef for the PointCloudMsgWrapper to represent a view that wraps a mutable cloud message. +template< + typename PointT, + typename FieldGeneratorsT = detail::DefaultFieldGenerators, + typename AllocatorT = std::allocator> +using PointCloud2Modifier = PointCloudMsgWrapper< + PointT, sensor_msgs::msg::PointCloud2_, FieldGeneratorsT, true, AllocatorT>; + +} // namespace point_cloud_msg_wrapper + +#undef COMPILE_IF_MUTABLE + +#endif // POINT_CLOUD_MSG_WRAPPER__POINT_CLOUD_MSG_WRAPPER_HPP_ diff --git a/point_cloud_msg_wrapper/include/point_cloud_msg_wrapper/type_traits.hpp b/point_cloud_msg_wrapper/include/point_cloud_msg_wrapper/type_traits.hpp new file mode 100644 index 0000000..ba13225 --- /dev/null +++ b/point_cloud_msg_wrapper/include/point_cloud_msg_wrapper/type_traits.hpp @@ -0,0 +1,85 @@ +// Copyright 2021 Apex.AI, Inc. +// +// 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. + +/// \copyright Copyright 2021 Apex.AI, Inc. +/// All rights reserved. + +#ifndef POINT_CLOUD_MSG_WRAPPER__TYPE_TRAITS_HPP_ +#define POINT_CLOUD_MSG_WRAPPER__TYPE_TRAITS_HPP_ + +#include +#include + +namespace point_cloud_msg_wrapper +{ +namespace detail +{ + +// TODO(igor): this is a general trait, should it live elsewhere? +template class Ref> +struct is_specialization : std::false_type {}; + +template class Ref, typename ... Args> +struct is_specialization, Ref>: std::true_type {}; + +// Taken from https://stackoverflow.com/a/35207812/1763680 +template +struct has_operator_equals_impl +{ + template + static auto test(U *)->decltype(std::declval() == std::declval()); + template + static auto test(...)->std::false_type; + + using type = typename std::is_same(0))>::type; +}; + +template +struct has_operator_equals : has_operator_equals_impl::type {}; + +/// Detect the template type used under the hood. +/// Provided std::vector type will be std::vector. +template +struct derived_point_vector; + +/// An overload for a container like an std::vector>. +template class ContainerT, class PointT> +struct derived_point_vector, PointT> +{ + template + using type = ContainerT; +}; + +/// An overload for containers like a bounded_vector>. +template< + class ValueT, + std::size_t UPPER_BOUND, + class AllocatorT, + template class ContainerT, + class PointT> +struct derived_point_vector, PointT> +{ + using kPointCapacity = std::integral_constant< + std::uint32_t, ContainerT::capacity() / + static_cast(sizeof(PointT))>; + + template + using type = + ContainerT; +}; + +} // namespace detail +} // namespace point_cloud_msg_wrapper + +#endif // POINT_CLOUD_MSG_WRAPPER__TYPE_TRAITS_HPP_ diff --git a/point_cloud_msg_wrapper/include/point_cloud_msg_wrapper/visibility_control.hpp b/point_cloud_msg_wrapper/include/point_cloud_msg_wrapper/visibility_control.hpp new file mode 100644 index 0000000..2b43d17 --- /dev/null +++ b/point_cloud_msg_wrapper/include/point_cloud_msg_wrapper/visibility_control.hpp @@ -0,0 +1,40 @@ +// Copyright 2021 Apex.AI, Inc. +// +// 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. + +/// \copyright Copyright 2021 Apex.AI, Inc. +/// All rights reserved. + + +#ifndef POINT_CLOUD_MSG_WRAPPER__VISIBILITY_CONTROL_HPP_ +#define POINT_CLOUD_MSG_WRAPPER__VISIBILITY_CONTROL_HPP_ + +#if defined(_MSC_VER) && defined(_WIN64) + #if defined(POINT_CLOUD_MSG_WRAPPER_BUILDING_DLL) || defined(POINT_CLOUD_MSG_WRAPPER_EXPORTS) + #define POINT_CLOUD_MSG_WRAPPER_PUBLIC __declspec(dllexport) + #define POINT_CLOUD_MSG_WRAPPER_LOCAL + #else + #define POINT_CLOUD_MSG_WRAPPER_PUBLIC __declspec(dllimport) + #define POINT_CLOUD_MSG_WRAPPER_LOCAL + #endif +#elif defined(__GNUC__) && defined(__linux__) + #define POINT_CLOUD_MSG_WRAPPER_PUBLIC __attribute__((visibility("default"))) + #define POINT_CLOUD_MSG_WRAPPER_LOCAL __attribute__((visibility("hidden"))) +#elif defined(__GNUC__) && defined(__APPLE__) + #define POINT_CLOUD_MSG_WRAPPER_PUBLIC __attribute__((visibility("default"))) + #define POINT_CLOUD_MSG_WRAPPER_LOCAL __attribute__((visibility("hidden"))) +#else // !(defined(__GNUC__) && defined(__APPLE__)) + #error "Unsupported Build Configuration" +#endif // _MSC_VER + +#endif // POINT_CLOUD_MSG_WRAPPER__VISIBILITY_CONTROL_HPP_ diff --git a/point_cloud_msg_wrapper/package.xml b/point_cloud_msg_wrapper/package.xml new file mode 100644 index 0000000..29713bd --- /dev/null +++ b/point_cloud_msg_wrapper/package.xml @@ -0,0 +1,24 @@ + + + + point_cloud_msg_wrapper + 1.0.7 + A point cloud message wrapper that allows for simple and safe PointCloud2 msg usage + Apex.AI, Inc. + Apache 2.0 + Igor Bogoslavskyi + M. Fatih Cırıt + + ament_cmake_auto + + ament_cmake + + sensor_msgs + + ament_cmake_gtest + ament_lint_auto + ament_lint_common + geometry_msgs + + ament_cmake + diff --git a/point_cloud_msg_wrapper/test/test_field_generators.cpp b/point_cloud_msg_wrapper/test/test_field_generators.cpp new file mode 100644 index 0000000..da4ee3b --- /dev/null +++ b/point_cloud_msg_wrapper/test/test_field_generators.cpp @@ -0,0 +1,53 @@ +// Copyright 2021 Apex.AI, Inc. +// +// 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. + +/// \copyright Copyright 2021 Apex.AI, Inc. +/// All rights reserved. + +#include +#include + +#include + +#include +#include + +namespace +{ +struct PointXI +{ + std::int64_t id; + float x; +}; + +} // namespace + +TEST(FieldGeneratorsTest, init) { + sensor_msgs::msg::PointCloud2::_fields_type fields; + point_cloud_msg_wrapper::field_y_generator::push_back_field_if_needed(fields); + ASSERT_TRUE(fields.empty()); + point_cloud_msg_wrapper::field_x_generator::push_back_field_if_needed(fields); + ASSERT_EQ(1UL, fields.size()); + EXPECT_EQ("x", fields.back().name); + EXPECT_EQ(sensor_msgs::msg::PointField::FLOAT32, fields.back().datatype); + EXPECT_EQ(1U, fields.back().count); + EXPECT_EQ(8U, fields.back().offset); + + point_cloud_msg_wrapper::field_id_generator::push_back_field_if_needed(fields); + ASSERT_EQ(2UL, fields.size()); + EXPECT_EQ("id", fields.back().name); + EXPECT_EQ(sensor_msgs::msg::PointField::INT32, fields.back().datatype); + EXPECT_EQ(2U, fields.back().count); + EXPECT_EQ(0U, fields.back().offset); +} diff --git a/point_cloud_msg_wrapper/test/test_point_cloud_msg_wrapper.cpp b/point_cloud_msg_wrapper/test/test_point_cloud_msg_wrapper.cpp new file mode 100644 index 0000000..6e3c7e4 --- /dev/null +++ b/point_cloud_msg_wrapper/test/test_point_cloud_msg_wrapper.cpp @@ -0,0 +1,364 @@ +// Copyright 2021 Apex.AI, Inc. +// +// 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. + +/// \copyright Copyright 2021 Apex.AI, Inc. +/// All rights reserved. + +#include +#include +#include +#include + +#include + +#include +#include +#include +#include +#include + +namespace +{ +struct PointX; +struct PointXYZI; +struct PointXYWithVirtualDestructor; +class ClassPointXY; +using GeometryPointXYZ = geometry_msgs::msg::Point32; + +template::value> * = nullptr> +constexpr bool nearly_equal( + const T & a, + const T & b, + const T & epsilon = std::numeric_limits::epsilon()) noexcept +{ + return std::fabs(a - b) <= + (epsilon * std::max(std::fabs(a), std::fabs(b))); +} + + +struct PointWithCustomField +{ + float x; + double non_standard_test_field; + std::int32_t y; + friend bool operator==(const PointWithCustomField & p1, const PointWithCustomField & p2) noexcept + { + return + ::nearly_equal(p1.x, p2.x) && + ::nearly_equal(p1.non_standard_test_field, p2.non_standard_test_field) && + (p1.y == p2.y); + } +}; + +struct CustomAlignedPoint +{ + float x; + float y; + float z; + alignas(double) std::uint8_t intensity; + double timestamp; + friend bool operator==(const CustomAlignedPoint & p1, const CustomAlignedPoint & p2) noexcept + { + return + ::nearly_equal(p1.x, p2.x) && + ::nearly_equal(p1.y, p2.y) && + ::nearly_equal(p1.z, p2.z) && + ::nearly_equal(p1.timestamp, p2.timestamp) && + (p1.intensity == p2.intensity); + } +}; + +struct PointNotPresentInAllPointTypes {std::int8_t x;}; + +template +PointT create_point() {return PointT{};} +template<> +PointX create_point(); +template<> +PointXYZI create_point(); +template<> +ClassPointXY create_point(); +template<> +PointXYWithVirtualDestructor create_point(); +template<> +GeometryPointXYZ create_point(); +template<> +CustomAlignedPoint create_point(); + +LIDAR_UTILS__DEFINE_FIELD_GENERATOR_FOR_MEMBER(non_standard_test_field); + +} // namespace + +template +class PointCloudMsgWrapperTest : public testing::Test +{ +public: + using PointT = T; +}; + +using AllPointTypes = ::testing::Types< + PointX, + PointXYZI, + ClassPointXY, + PointXYWithVirtualDestructor, + CustomAlignedPoint, + GeometryPointXYZ>; +// cppcheck-suppress syntaxError - trailing comma is the only way to remove the compiler warning. +TYPED_TEST_CASE(PointCloudMsgWrapperTest, AllPointTypes, ); + +using point_cloud_msg_wrapper::PointCloud2View; +using point_cloud_msg_wrapper::PointCloud2Modifier; + +/// @test Test that for any of the different types of points we can read and write them into msg. +TYPED_TEST(PointCloudMsgWrapperTest, ReadingAndWritingGenericPoints) +{ + using Point = typename TestFixture::PointT; + sensor_msgs::msg::PointCloud2 msg; + + // Cannot initialize a wrapper without resetting an empty message. + EXPECT_THROW(PointCloud2View{msg}, std::runtime_error); + EXPECT_THROW(PointCloud2Modifier{msg}, std::runtime_error); + + PointCloud2Modifier cloud_wrapper{msg, "some_frame_id"}; + EXPECT_EQ(msg.header.frame_id, "some_frame_id"); + + const auto point = create_point(); + + ASSERT_FALSE(msg.fields.empty()); + ASSERT_FALSE(msg.fields.front().name.empty()); + ASSERT_NO_THROW(cloud_wrapper.push_back(point)); + + ASSERT_THROW(PointCloud2View{msg}, std::runtime_error); + ASSERT_THROW(PointCloud2Modifier{msg}, std::runtime_error); + + ASSERT_TRUE(PointCloud2Modifier::can_be_created_from(msg)); + const auto initialized_wrapper = PointCloud2Modifier{msg}; + ASSERT_EQ(initialized_wrapper.size(), 1U); + EXPECT_EQ(initialized_wrapper.at(0U), point); + EXPECT_EQ(initialized_wrapper[0U], point); + EXPECT_NO_THROW(cloud_wrapper.push_back(initialized_wrapper[0U])); + EXPECT_EQ(initialized_wrapper.size(), 2U); + + const auto & const_cloud = msg; + const PointCloud2View const_wrapper{const_cloud}; + ASSERT_EQ(const_wrapper.size(), 2U); + EXPECT_EQ(const_wrapper.at(0U), point); + EXPECT_EQ(const_wrapper[0U], point); + EXPECT_EQ(const_wrapper.at(1U), point); + EXPECT_EQ(const_wrapper[1U], point); + + // Test that we can iterate over the message. + std::array points; + std::size_t index{}; + for (const auto & p : cloud_wrapper) { + points[index] = p; + index++; + } + ASSERT_EQ(points.size(), cloud_wrapper.size()); + for (auto i = 0U; i < points.size(); ++i) { + EXPECT_EQ(points[i], cloud_wrapper[i]); + } + EXPECT_EQ(points.front(), cloud_wrapper.front()); + EXPECT_EQ(points.back(), cloud_wrapper.back()); + + cloud_wrapper.resize(3U); + EXPECT_EQ(cloud_wrapper.size(), 3U); + // Check that the untouched points stayed the same. + for (auto i = 0U; i < points.size(); ++i) { + EXPECT_EQ(points[i], cloud_wrapper[i]); + } + + // Change values. + for (auto & p : cloud_wrapper) { + p = Point{}; + } + for (const auto & p : cloud_wrapper) { + EXPECT_EQ(Point{}, p); + } + + cloud_wrapper.clear(); + EXPECT_TRUE(cloud_wrapper.empty()); + + // Cannot reinitialize an already initialized message. + EXPECT_THROW(PointCloud2Modifier(msg, "some_new_frame_id"), std::runtime_error); + cloud_wrapper.reset_msg("some_new_frame_id"); + EXPECT_EQ(msg.header.frame_id, "some_new_frame_id"); + EXPECT_TRUE(cloud_wrapper.empty()); +} + +/// @test Check that using using iterator including std::back_inserter is possible with the wrapper. +TYPED_TEST(PointCloudMsgWrapperTest, Iterators) { + using Point = typename TestFixture::PointT; + sensor_msgs::msg::PointCloud2 msg; + PointCloud2Modifier cloud_wrapper{msg, "some_frame_id"}; + std::array points{{create_point(), Point{}}}; + // Fill the message from an array. + std::transform( + points.cbegin(), points.cend(), std::back_inserter(cloud_wrapper), + [](const Point & point) {return point;}); + ASSERT_EQ(points.size(), cloud_wrapper.size()); + for (auto i = 0U; i < points.size(); ++i) { + EXPECT_EQ(points[i], cloud_wrapper[i]); + } + // Change existing values. + for (auto & point : cloud_wrapper) { + point = Point{}; + } + const auto & const_cloud = msg; + const PointCloud2View const_wrapper{const_cloud}; + for (const auto & point : const_wrapper) { + EXPECT_EQ(Point{}, point); + } + + // Check the reverse iteration. + for (auto riter = const_wrapper.rbegin(); riter != const_wrapper.rend(); ++riter) { + EXPECT_EQ(Point{}, *riter); + } +} + +/// @test Check initialization with a complicated type with an additional custom field. +TEST(PointCloudMsgWrapperTest, ProcessPointWithCustomField) { + sensor_msgs::msg::PointCloud2 msg; + // There is no matching field generator for non_standard_test_field present. + EXPECT_THROW(PointCloud2Modifier(msg, "some_frame_id"), std::runtime_error); + EXPECT_THROW(PointCloud2View{msg}, std::runtime_error); + using Generators = std::tuple< + point_cloud_msg_wrapper::field_x_generator, + point_cloud_msg_wrapper::field_y_generator, + field_non_standard_test_field_generator>; + using CustomCloudModifier = PointCloud2Modifier; + // Cannot initialize message without a new frame id provided. + EXPECT_THROW(CustomCloudModifier{msg}, std::runtime_error); + + CustomCloudModifier cloud_wrapper{msg, "some_frame_id"}; + ASSERT_EQ(msg.fields.size(), 3U); + // Note that the order of fields is defined by the order of generators in the tuple, NOT by the + // order of members in the struct. However, the order of fields plays no role in point + // representation within the message. + EXPECT_EQ(msg.fields[0].name, "x"); + EXPECT_EQ(msg.fields[1].name, "y"); + EXPECT_EQ(msg.fields[2].name, "non_standard_test_field"); + EXPECT_NO_THROW(cloud_wrapper.push_back({42.0F, 42.0, 23})); + + const CustomCloudModifier initialized_wrapper{msg}; + ASSERT_FALSE(initialized_wrapper.empty()); + ASSERT_EQ(initialized_wrapper.size(), 1U); + EXPECT_FLOAT_EQ(initialized_wrapper.at(0U).x, 42.0F); + EXPECT_DOUBLE_EQ(initialized_wrapper.at(0U).non_standard_test_field, 42.0); + EXPECT_EQ(initialized_wrapper.at(0U).y, 23); + EXPECT_THROW(initialized_wrapper.at(1U), std::out_of_range); + EXPECT_FLOAT_EQ(initialized_wrapper[0U].x, 42.0F); + EXPECT_DOUBLE_EQ(initialized_wrapper[0U].non_standard_test_field, 42.0); + EXPECT_EQ(initialized_wrapper[0U].y, 23); + EXPECT_NO_THROW(cloud_wrapper.push_back(initialized_wrapper[0U])); + ASSERT_EQ(initialized_wrapper.size(), 2U); +} + +/// @test Check that a macro we use for readability is not leaking outside of the header file. +TEST(PointCloudMsgWrapperTest, CompilationMacroIsUnset) { + #ifdef COMPILE_IF_MUTABLE + FAIL() << "Compilation macro should not be available outside of point_cloud_msg_wrapper.hpp file"; + #endif +} + + +namespace +{ + +struct PointX +{ + float x; + friend bool operator==(const PointX & p1, const PointX & p2) noexcept + { + return ::nearly_equal(p1.x, p2.x); + } +}; + +class ClassPointXY +{ +public: + ClassPointXY() = default; + explicit ClassPointXY(float x, float y) + : x_{x}, y_{y} {} + + float & x() {return x_;} + const float & x() const {return x_;} + float & y() {return y_;} + const float & y() const {return y_;} + + friend bool operator==(const ClassPointXY & p1, const ClassPointXY & p2) noexcept + { + return ::nearly_equal(p1.x_, p2.x_) && + ::nearly_equal(p1.y_, p2.y_); + } + +private: + float x_; + float y_; +}; + +struct PointXYZI +{ + float x; + float y; + float z; + std::int64_t id; + friend bool operator==(const PointXYZI & p1, const PointXYZI & p2) noexcept + { + return ::nearly_equal(p1.x, p2.x) && + ::nearly_equal(p1.y, p2.y) && + ::nearly_equal(p1.z, p2.z) && + (p1.id == p2.id); + } +}; + +struct PointXYWithVirtualDestructor +{ + PointXYWithVirtualDestructor() = default; + PointXYWithVirtualDestructor(float x_, float y_) + : x{x_}, y{y_} {} + + virtual ~PointXYWithVirtualDestructor() {} + + friend bool operator==( + const PointXYWithVirtualDestructor & p1, + const PointXYWithVirtualDestructor & p2) noexcept + { + return ::nearly_equal(p1.x, p2.x) && + ::nearly_equal(p1.y, p2.y); + } + + float x{}; + float y{}; +}; + +template<> +PointX create_point() {return PointX{42.0F};} +template<> +PointXYZI create_point() {return PointXYZI{42.0F, 23.0F, 13.0F, 42LL};} +template<> +ClassPointXY create_point() {return ClassPointXY{42.0F, 23.0F};} +template<> +PointXYWithVirtualDestructor create_point() {return PointXYWithVirtualDestructor{42.0F, 23.0F};} +template<> +GeometryPointXYZ create_point() +{ + GeometryPointXYZ point; + point.set__x(42.0F).set__y(23.0F).set__x(13.0F); + return point; +} +template<> +CustomAlignedPoint create_point() {return CustomAlignedPoint{42.0F, 23.0F, 4242.0F, 23, 2323.0};} +} // namespace From f6b60e49357d17c9dedfa6ac5a0a0cbe90a4f03b Mon Sep 17 00:00:00 2001 From: Cody Washington <115746975+Jelatinone@users.noreply.github.com> Date: Thu, 23 Apr 2026 17:11:18 -0500 Subject: [PATCH 2/4] Flatten point_cloud_msg_wrapper subtree structure --- .../point_cloud_msg_wrapper/.gitlab-ci.yml | 80 ------- .../CMakeLists.txt | 0 src/tools/point_cloud_msg_wrapper/LICENSE | 201 ------------------ src/tools/point_cloud_msg_wrapper/README.md | 24 --- .../design/point_cloud_msg_wrapper-design.md | 0 .../default_field_generators.hpp | 0 .../field_generators.hpp | 0 .../field_properties.hpp | 0 .../point_cloud_msg_wrapper.hpp | 0 .../point_cloud_msg_wrapper/type_traits.hpp | 0 .../visibility_control.hpp | 0 .../{point_cloud_msg_wrapper => }/package.xml | 0 .../test/test_field_generators.cpp | 0 .../test/test_point_cloud_msg_wrapper.cpp | 0 14 files changed, 305 deletions(-) delete mode 100644 src/tools/point_cloud_msg_wrapper/.gitlab-ci.yml rename src/tools/point_cloud_msg_wrapper/{point_cloud_msg_wrapper => }/CMakeLists.txt (100%) delete mode 100644 src/tools/point_cloud_msg_wrapper/LICENSE delete mode 100644 src/tools/point_cloud_msg_wrapper/README.md rename src/tools/point_cloud_msg_wrapper/{point_cloud_msg_wrapper => }/design/point_cloud_msg_wrapper-design.md (100%) rename src/tools/point_cloud_msg_wrapper/{point_cloud_msg_wrapper => }/include/point_cloud_msg_wrapper/default_field_generators.hpp (100%) rename src/tools/point_cloud_msg_wrapper/{point_cloud_msg_wrapper => }/include/point_cloud_msg_wrapper/field_generators.hpp (100%) rename src/tools/point_cloud_msg_wrapper/{point_cloud_msg_wrapper => }/include/point_cloud_msg_wrapper/field_properties.hpp (100%) rename src/tools/point_cloud_msg_wrapper/{point_cloud_msg_wrapper => }/include/point_cloud_msg_wrapper/point_cloud_msg_wrapper.hpp (100%) rename src/tools/point_cloud_msg_wrapper/{point_cloud_msg_wrapper => }/include/point_cloud_msg_wrapper/type_traits.hpp (100%) rename src/tools/point_cloud_msg_wrapper/{point_cloud_msg_wrapper => }/include/point_cloud_msg_wrapper/visibility_control.hpp (100%) rename src/tools/point_cloud_msg_wrapper/{point_cloud_msg_wrapper => }/package.xml (100%) rename src/tools/point_cloud_msg_wrapper/{point_cloud_msg_wrapper => }/test/test_field_generators.cpp (100%) rename src/tools/point_cloud_msg_wrapper/{point_cloud_msg_wrapper => }/test/test_point_cloud_msg_wrapper.cpp (100%) diff --git a/src/tools/point_cloud_msg_wrapper/.gitlab-ci.yml b/src/tools/point_cloud_msg_wrapper/.gitlab-ci.yml deleted file mode 100644 index 513c85a..0000000 --- a/src/tools/point_cloud_msg_wrapper/.gitlab-ci.yml +++ /dev/null @@ -1,80 +0,0 @@ -stages: - - build - - test - - deploy - -build: - stage: build - image: osrf/ros:foxy-desktop - script: - - apt-get update - - rosdep update - - rosdep install --from point_cloud_msg_wrapper --ignore-src -y - - mkdir -p /opt/point_cloud_msg_wrapper - - ' - colcon build - --install-base /opt/point_cloud_msg_wrapper - --merge-install - --cmake-args - -DCMAKE_BUILD_TYPE=Release - --base-path point_cloud_msg_wrapper - ' - - cp LICENSE /opt/point_cloud_msg_wrapper - - tar cfz opt.tar.gz /opt/point_cloud_msg_wrapper - artifacts: - expire_in: 1 day - when: always - paths: - - log - - build - - opt.tar.gz - -test: - stage: test - image: osrf/ros:foxy-desktop - dependencies: - - build - script: - - tar xf opt.tar.gz -C / - - ' - colcon test - --install-base /opt/point_cloud_msg_wrapper - --merge-install - --base-path point_cloud_msg_wrapper - ' - - 'colcon test-result --verbose' - artifacts: - expire_in: 1 day - when: always - paths: - - log - -volume: - stage: deploy - image: docker - services: - - docker:dind - dependencies: - - build - before_script: - - '' - script: - - 'echo -e "*\n!opt.tar.gz" > .dockerignore' - - | - cat >Dockerfile < modifier{cloud, "frame_id"}; -for (const auto& point : {{42, 42}, {23, 42}}) { - modifier.push_back(point); -} -``` - -For more details please see [design doc](point_cloud_msg_wrapper/design/point_cloud_msg_wrapper-design.md). - - -## Acknowledgments - -Many thanks go to @xmfcx who has pointed out an idea to use an existing iterator instead of writing -our own as well as for his reviews of the initial version of this code. This has significantly -increased the readability and made the code less error prone. Thanks! diff --git a/src/tools/point_cloud_msg_wrapper/point_cloud_msg_wrapper/design/point_cloud_msg_wrapper-design.md b/src/tools/point_cloud_msg_wrapper/design/point_cloud_msg_wrapper-design.md similarity index 100% rename from src/tools/point_cloud_msg_wrapper/point_cloud_msg_wrapper/design/point_cloud_msg_wrapper-design.md rename to src/tools/point_cloud_msg_wrapper/design/point_cloud_msg_wrapper-design.md diff --git a/src/tools/point_cloud_msg_wrapper/point_cloud_msg_wrapper/include/point_cloud_msg_wrapper/default_field_generators.hpp b/src/tools/point_cloud_msg_wrapper/include/point_cloud_msg_wrapper/default_field_generators.hpp similarity index 100% rename from src/tools/point_cloud_msg_wrapper/point_cloud_msg_wrapper/include/point_cloud_msg_wrapper/default_field_generators.hpp rename to src/tools/point_cloud_msg_wrapper/include/point_cloud_msg_wrapper/default_field_generators.hpp diff --git a/src/tools/point_cloud_msg_wrapper/point_cloud_msg_wrapper/include/point_cloud_msg_wrapper/field_generators.hpp b/src/tools/point_cloud_msg_wrapper/include/point_cloud_msg_wrapper/field_generators.hpp similarity index 100% rename from src/tools/point_cloud_msg_wrapper/point_cloud_msg_wrapper/include/point_cloud_msg_wrapper/field_generators.hpp rename to src/tools/point_cloud_msg_wrapper/include/point_cloud_msg_wrapper/field_generators.hpp diff --git a/src/tools/point_cloud_msg_wrapper/point_cloud_msg_wrapper/include/point_cloud_msg_wrapper/field_properties.hpp b/src/tools/point_cloud_msg_wrapper/include/point_cloud_msg_wrapper/field_properties.hpp similarity index 100% rename from src/tools/point_cloud_msg_wrapper/point_cloud_msg_wrapper/include/point_cloud_msg_wrapper/field_properties.hpp rename to src/tools/point_cloud_msg_wrapper/include/point_cloud_msg_wrapper/field_properties.hpp diff --git a/src/tools/point_cloud_msg_wrapper/point_cloud_msg_wrapper/include/point_cloud_msg_wrapper/point_cloud_msg_wrapper.hpp b/src/tools/point_cloud_msg_wrapper/include/point_cloud_msg_wrapper/point_cloud_msg_wrapper.hpp similarity index 100% rename from src/tools/point_cloud_msg_wrapper/point_cloud_msg_wrapper/include/point_cloud_msg_wrapper/point_cloud_msg_wrapper.hpp rename to src/tools/point_cloud_msg_wrapper/include/point_cloud_msg_wrapper/point_cloud_msg_wrapper.hpp diff --git a/src/tools/point_cloud_msg_wrapper/point_cloud_msg_wrapper/include/point_cloud_msg_wrapper/type_traits.hpp b/src/tools/point_cloud_msg_wrapper/include/point_cloud_msg_wrapper/type_traits.hpp similarity index 100% rename from src/tools/point_cloud_msg_wrapper/point_cloud_msg_wrapper/include/point_cloud_msg_wrapper/type_traits.hpp rename to src/tools/point_cloud_msg_wrapper/include/point_cloud_msg_wrapper/type_traits.hpp diff --git a/src/tools/point_cloud_msg_wrapper/point_cloud_msg_wrapper/include/point_cloud_msg_wrapper/visibility_control.hpp b/src/tools/point_cloud_msg_wrapper/include/point_cloud_msg_wrapper/visibility_control.hpp similarity index 100% rename from src/tools/point_cloud_msg_wrapper/point_cloud_msg_wrapper/include/point_cloud_msg_wrapper/visibility_control.hpp rename to src/tools/point_cloud_msg_wrapper/include/point_cloud_msg_wrapper/visibility_control.hpp diff --git a/src/tools/point_cloud_msg_wrapper/point_cloud_msg_wrapper/package.xml b/src/tools/point_cloud_msg_wrapper/package.xml similarity index 100% rename from src/tools/point_cloud_msg_wrapper/point_cloud_msg_wrapper/package.xml rename to src/tools/point_cloud_msg_wrapper/package.xml diff --git a/src/tools/point_cloud_msg_wrapper/point_cloud_msg_wrapper/test/test_field_generators.cpp b/src/tools/point_cloud_msg_wrapper/test/test_field_generators.cpp similarity index 100% rename from src/tools/point_cloud_msg_wrapper/point_cloud_msg_wrapper/test/test_field_generators.cpp rename to src/tools/point_cloud_msg_wrapper/test/test_field_generators.cpp diff --git a/src/tools/point_cloud_msg_wrapper/point_cloud_msg_wrapper/test/test_point_cloud_msg_wrapper.cpp b/src/tools/point_cloud_msg_wrapper/test/test_point_cloud_msg_wrapper.cpp similarity index 100% rename from src/tools/point_cloud_msg_wrapper/point_cloud_msg_wrapper/test/test_point_cloud_msg_wrapper.cpp rename to src/tools/point_cloud_msg_wrapper/test/test_point_cloud_msg_wrapper.cpp From e20578cdc845b71d55b70c9372e262884fc3e152 Mon Sep 17 00:00:00 2001 From: Cody Washington <115746975+Jelatinone@users.noreply.github.com> Date: Thu, 23 Apr 2026 17:12:56 -0500 Subject: [PATCH 3/4] Squashed 'src/drivers/smartmicro_ros2_radars/' content from commit 772288b git-subtree-dir: src/drivers/smartmicro_ros2_radars git-subtree-split: 772288b155fcf3b6ed29b23993818367cacde493 --- .github/workflows/dockerbuild.yml | 35 + .gitignore | 16 + CHANGELOG.md | 135 + CONTRIBUTING.md | 47 + Dockerfile | 35 + LICENSE | 176 + Readme.md | 328 ++ docker-compose.yml | 101 + simulator/COLCON_IGNORE | 0 simulator/config_umrr11/com_lib_config.json | 16 + simulator/config_umrr11/hw_inventory.json | 12 + simulator/config_umrr11/routing_table.json | 14 + simulator/config_umrr96/com_lib_config.json | 16 + simulator/config_umrr96/hw_inventory.json | 12 + simulator/config_umrr96/routing_table.json | 14 + simulator/config_umrr9d/com_lib_config.json | 16 + simulator/config_umrr9d/hw_inventory.json | 12 + simulator/config_umrr9d/routing_table.json | 14 + simulator/config_umrr9f/com_lib_config.json | 16 + simulator/config_umrr9f/hw_inventory.json | 12 + simulator/config_umrr9f/routing_table.json | 14 + simulator/simulation/CMakeLists.txt | 33 + simulator/simulation/build_simulator.sh | 12 + simulator/simulation/src/simulator.cpp | 149 + simulator/targetlist_port_v2_1_0.bin | Bin 0 -> 984 bytes simulator/targetlist_port_v3_0_0.bin | Bin 0 -> 7096 bytes simulator/targetlist_port_v4_0_0.bin | Bin 0 -> 11696 bytes simulator/targetlist_port_v4_1_0.bin | Bin 0 -> 104 bytes smart_extract.sh | 53 + smart_rviz_plugin/CMakeLists.txt | 107 + .../config/images/can_sender.png | Bin 0 -> 23029 bytes .../config/images/smart_rviz_plugin.png | Bin 0 -> 411990 bytes .../config/rviz/smart_plugin.rviz | 212 + smart_rviz_plugin/custom_can_sender.py | 95 + .../smart_rviz_plugin/smart_download.hpp | 89 + .../smart_rviz_plugin/smart_recorder.hpp | 196 + .../smart_rviz_plugin/smart_services.hpp | 160 + .../smart_rviz_plugin/smart_status.hpp | 127 + smart_rviz_plugin/package.xml | 38 + smart_rviz_plugin/plugins_description.xml | 26 + smart_rviz_plugin/src/smart_download.cpp | 141 + smart_rviz_plugin/src/smart_recorder.cpp | 571 +++ smart_rviz_plugin/src/smart_services.cpp | 664 +++ smart_rviz_plugin/src/smart_status.cpp | 236 + umrr_ros2_driver/CMakeLists.txt | 182 + umrr_ros2_driver/cmake/config_path.hpp.in | 33 + .../cmake/smart_access_config.json.in | 16 + umrr_ros2_driver/config/hw_inventory.json | 9 + umrr_ros2_driver/config/routing_table.json | 9 + .../config/rviz/smart_plugin.rviz | 212 + .../smartmicro_radar_node.hpp | 1120 +++++ .../umrr_ros2_driver/update_service.hpp | 33 + .../umrr_ros2_driver/visibility_control.hpp | 37 + umrr_ros2_driver/launch/radar.launch.py | 43 + umrr_ros2_driver/package.xml | 28 + .../param/example/radar.adapter.example.yaml | 34 + .../param/example/radar.sensor.example.yaml | 122 + .../param/radar.params.template.yaml | 136 + umrr_ros2_driver/smartmicro/Readme.md | 5 + .../smartmicro/include/DataServicesIface.h | 337 ++ .../smartmicro/include/InstructionBuffer.h | 191 + umrr_ros2_driver/smartmicro/include/Types.h | 574 +++ .../src/smartmicro_radar_node.cpp | 4298 +++++++++++++++++ umrr_ros2_driver/src/update_service.cpp | 95 + .../test/radar_node_test.launch.py | 172 + umrr_ros2_msgs/CMakeLists.txt | 45 + umrr_ros2_msgs/msg/CanObjectHeader.msg | 9 + umrr_ros2_msgs/msg/CanTargetHeader.msg | 7 + umrr_ros2_msgs/msg/PortObjectHeader.msg | 12 + umrr_ros2_msgs/msg/PortTargetHeader.msg | 17 + umrr_ros2_msgs/package.xml | 19 + umrr_ros2_msgs/srv/FirmwareDownload.srv | 4 + umrr_ros2_msgs/srv/GetMode.srv | 6 + umrr_ros2_msgs/srv/GetStatus.srv | 6 + umrr_ros2_msgs/srv/SendCommand.srv | 6 + umrr_ros2_msgs/srv/SetIp.srv | 4 + umrr_ros2_msgs/srv/SetMode.srv | 7 + 77 files changed, 11778 insertions(+) create mode 100644 .github/workflows/dockerbuild.yml create mode 100644 .gitignore create mode 100644 CHANGELOG.md create mode 100644 CONTRIBUTING.md create mode 100644 Dockerfile create mode 100644 LICENSE create mode 100644 Readme.md create mode 100644 docker-compose.yml create mode 100644 simulator/COLCON_IGNORE create mode 100644 simulator/config_umrr11/com_lib_config.json create mode 100644 simulator/config_umrr11/hw_inventory.json create mode 100644 simulator/config_umrr11/routing_table.json create mode 100644 simulator/config_umrr96/com_lib_config.json create mode 100644 simulator/config_umrr96/hw_inventory.json create mode 100644 simulator/config_umrr96/routing_table.json create mode 100644 simulator/config_umrr9d/com_lib_config.json create mode 100644 simulator/config_umrr9d/hw_inventory.json create mode 100644 simulator/config_umrr9d/routing_table.json create mode 100644 simulator/config_umrr9f/com_lib_config.json create mode 100644 simulator/config_umrr9f/hw_inventory.json create mode 100644 simulator/config_umrr9f/routing_table.json create mode 100644 simulator/simulation/CMakeLists.txt create mode 100755 simulator/simulation/build_simulator.sh create mode 100644 simulator/simulation/src/simulator.cpp create mode 100644 simulator/targetlist_port_v2_1_0.bin create mode 100644 simulator/targetlist_port_v3_0_0.bin create mode 100644 simulator/targetlist_port_v4_0_0.bin create mode 100644 simulator/targetlist_port_v4_1_0.bin create mode 100755 smart_extract.sh create mode 100644 smart_rviz_plugin/CMakeLists.txt create mode 100644 smart_rviz_plugin/config/images/can_sender.png create mode 100644 smart_rviz_plugin/config/images/smart_rviz_plugin.png create mode 100644 smart_rviz_plugin/config/rviz/smart_plugin.rviz create mode 100644 smart_rviz_plugin/custom_can_sender.py create mode 100644 smart_rviz_plugin/include/smart_rviz_plugin/smart_download.hpp create mode 100644 smart_rviz_plugin/include/smart_rviz_plugin/smart_recorder.hpp create mode 100644 smart_rviz_plugin/include/smart_rviz_plugin/smart_services.hpp create mode 100644 smart_rviz_plugin/include/smart_rviz_plugin/smart_status.hpp create mode 100644 smart_rviz_plugin/package.xml create mode 100644 smart_rviz_plugin/plugins_description.xml create mode 100644 smart_rviz_plugin/src/smart_download.cpp create mode 100644 smart_rviz_plugin/src/smart_recorder.cpp create mode 100644 smart_rviz_plugin/src/smart_services.cpp create mode 100644 smart_rviz_plugin/src/smart_status.cpp create mode 100644 umrr_ros2_driver/CMakeLists.txt create mode 100644 umrr_ros2_driver/cmake/config_path.hpp.in create mode 100644 umrr_ros2_driver/cmake/smart_access_config.json.in create mode 100644 umrr_ros2_driver/config/hw_inventory.json create mode 100644 umrr_ros2_driver/config/routing_table.json create mode 100644 umrr_ros2_driver/config/rviz/smart_plugin.rviz create mode 100644 umrr_ros2_driver/include/umrr_ros2_driver/smartmicro_radar_node.hpp create mode 100644 umrr_ros2_driver/include/umrr_ros2_driver/update_service.hpp create mode 100644 umrr_ros2_driver/include/umrr_ros2_driver/visibility_control.hpp create mode 100644 umrr_ros2_driver/launch/radar.launch.py create mode 100644 umrr_ros2_driver/package.xml create mode 100644 umrr_ros2_driver/param/example/radar.adapter.example.yaml create mode 100644 umrr_ros2_driver/param/example/radar.sensor.example.yaml create mode 100644 umrr_ros2_driver/param/radar.params.template.yaml create mode 100644 umrr_ros2_driver/smartmicro/Readme.md create mode 100644 umrr_ros2_driver/smartmicro/include/DataServicesIface.h create mode 100644 umrr_ros2_driver/smartmicro/include/InstructionBuffer.h create mode 100644 umrr_ros2_driver/smartmicro/include/Types.h create mode 100644 umrr_ros2_driver/src/smartmicro_radar_node.cpp create mode 100644 umrr_ros2_driver/src/update_service.cpp create mode 100644 umrr_ros2_driver/test/radar_node_test.launch.py create mode 100644 umrr_ros2_msgs/CMakeLists.txt create mode 100644 umrr_ros2_msgs/msg/CanObjectHeader.msg create mode 100644 umrr_ros2_msgs/msg/CanTargetHeader.msg create mode 100644 umrr_ros2_msgs/msg/PortObjectHeader.msg create mode 100644 umrr_ros2_msgs/msg/PortTargetHeader.msg create mode 100644 umrr_ros2_msgs/package.xml create mode 100644 umrr_ros2_msgs/srv/FirmwareDownload.srv create mode 100644 umrr_ros2_msgs/srv/GetMode.srv create mode 100644 umrr_ros2_msgs/srv/GetStatus.srv create mode 100644 umrr_ros2_msgs/srv/SendCommand.srv create mode 100644 umrr_ros2_msgs/srv/SetIp.srv create mode 100644 umrr_ros2_msgs/srv/SetMode.srv diff --git a/.github/workflows/dockerbuild.yml b/.github/workflows/dockerbuild.yml new file mode 100644 index 0000000..1fb3e19 --- /dev/null +++ b/.github/workflows/dockerbuild.yml @@ -0,0 +1,35 @@ +name: Build and test + +on: + pull_request: + push: + branches: + - master + +jobs: + build-and-test: + runs-on: ubuntu-latest + strategy: + matrix: + ros_distro: [foxy, humble, jazzy] + steps: + - name: Checkout + uses: actions/checkout@v2 + + - name: Get the Smart Access binaries + run: echo "yes" | ./smart_extract.sh + + - name: Build docker container + run: docker build --build-arg ROS_DISTRO=${{ matrix.ros_distro }} -t umrr-ros:${{ matrix.ros_distro }} . + + - name: Build the driver + run: docker run --rm -v $(pwd):/code umrr-ros:${{ matrix.ros_distro }} colcon build --packages-skip smart_rviz_plugin + + - name: Run tests + run: ROS_DISTRO=${{ matrix.ros_distro }} docker compose up + + - name: Collect test results + run: docker run --rm -v`pwd`:/code umrr-ros:${{ matrix.ros_distro }} colcon test-result --all --verbose + + - name: Shut down containers + run: docker compose down diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..69310c6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,16 @@ +build +install +log +*.pyc +umrr_ros2_driver/__pycache__ +simulator/simulation/build_dir +simulator/simulation/out +umrr_ros2_driver/include/umrr_ros2_driver/sensor_params.hpp +umrr_ros2_driver/include/umrr_ros2_driver/sensor_commands.hpp +umrr_ros2_driver/smartmicro/* +!umrr_ros2_driver/smartmicro/include +umrr_ros2_driver/smartmicro/include/* +!umrr_ros2_driver/smartmicro/include/DataServicesIface.h +!umrr_ros2_driver/smartmicro/include/InstructionBuffer.h +!umrr_ros2_driver/smartmicro/include/Types.h +!umrr_ros2_driver/smartmicro/Readme.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..dc97002 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,135 @@ +# Change Log +All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/). + +## v1.0.0 - 2021-11-01 + +### Initial Release +- **smartmicro_ros2_radars**: First official versioned release. +- **ROS 2 Node**: Wrapped around `Smart Access C++ API v3.3.15`. +- **Functionality**: Interfaces with smartmicro automotive radars and publishes incoming data as point cloud. + +## v2.0.0 - 2022-05-05 + +### Major Update +- **Multi-User Interface**: Supports and publishes data from UMRR96 and UMRR11. +- **Testing Enhancements**: Implemented a new test approach simulating sensors and interfacing with the node. +- **New Sensor Firmware Requirement For**: + - UMRR11-T132: V5.1.4 + - UMRR96-T153: V5.2.4 + +# v2.1.0 - 2022-06-02 + +### Minor Update +- **ROS 2 Services**: Added services to communicate with the sensor. +- **Mode Changes**: Supports mode changes for UMRR96 and UMRR11. +- **IP Configuration**: Allows configuring sensor IP addresses via ROS 2 services. +- **Extended Testing**: Includes ROS 2 services check. +- **New Sensor Firmware Requirement For**: + - UMRR11-T132: V5.1.4 + - UMRR96-T153: V5.2.4 + +## v3.0.0 - 2022-09-23 + +### Major Update +- **New Sensor Support**: Added support for smartmicro sensor DRVEGRD 169. +- **Functionality**: Mode changes and configuration for DRVEGRD 169, publishes radar targets as point cloud data. +- **Callback Changes**: Data stream callbacks now require a clientID. + +## v3.1.0 - 2022-10-19 + +### Minor Update +- **New Sensor Support**: Added support for smartmicro sensor DRVEGRD 152. +- **Functionality**: Mode changes and configuration for DRVEGRD 152, publishes radar targets as point cloud data. +- **Callback Changes**: Data stream callbacks now require a clientID. + +## v3.2.0 - 2022-11-11 + +### Minor Update +- **Point Cloud Enhancement**: Introduced signal-to-noise field. +- **Bug Fixes**: Fixed the max number of sensors that could be connected simultaneously. + +## v3.2.1 - 2022-12-16 + +### Patch Update +- **Bug Fixes**: + - Fixed offset causing anomalies in point clouds from DRVEGRD 152 sensor. + - Fixed timestamp calculation bug causing RViz to crash. +- **Updates**: Updated simulator source files. + +## v4.0.0 - 2023-02-06 + +### Major Update +- **New UI**: Added user-interface for DRVEGRD 169. +- **New Modes**: Introduced new modes for DRVEGRD 169. +- **New Service**: Added ROS 2 service to save configurations. +- **Model Update**: 'UMRR9F' radar parameter model split into 'UMRR9F_V1_1_1' and 'UMRR9F_V2_0_0'. + +## v4.1.0 - 2023-08-21 + +### Minor Update +- **New UI**: Added user-interfaces for DRVEGRD 171 and DRVEGRD 152. +- **Parameter List**: Complete list of all parameters and commands for all sensors accessible via ROS 2 services. + +## v5.0.0 - 2023-09-22 + +### Major Update +- **CAN Communication**: Enabled for all provided sensor types and interfaces. +- **Parameter Expansion**: Extended parameters to include settings for connected adapters along with sensor parameters. + +## v6.0.0 - 2023-12-20 + +### Major Update +- **Firmware Download**: Added features for downloading sensor firmware onto the sensors. +- **RViz Plugins**: + - Log target list data, record, and save it. + - Send instructions to the sensors. +- **Python GUI**: Added a GUI to send custom CAN messages. +- **Parameter Templates**: Merged radar parameter templates into one file. +- **Point Cloud Enhancement**: Added polar coordinates. +- **New UI**: Added user-interface for sensor A4 T171. + +## v6.1.0 - 2024-01-26 + +### Minor Update +- **New UIs**: Added user interfaces for DRVEGRD 169 and DRVEGRD 152. +- **Bug Fixes**: + - Fixed issues with the smart record plugin. + - Added azimuth and elevation angles in degrees. + +## v7.0.0 - 2024-06-07 + +### New Features +- **User Interface for DRVEGRD 169**: Introduced a new UI with integrated object tracking capabilities. +- **RViz Plugins**: Added plugins to RViz for decoding targets, objects, and header information. +- **Enhanced Command Configurator**: Improved the command configurator for better usability and functionality. +- **New ROS 2 Parameter - `pub_type`**: Added the `pub_type` parameter to manage the desired publication type. +- **New message definitions**: + - CanObjectHeader.msg includes object status for sensors connected over CAN. + - CanTargetHeader.msg includes target status for sensors connected over CAN. + - PortObjectHeader.msg includes object status for sensors connected over ethernet. + - PortTargetHeader.msg includes target status for sensors connected over ethernet. + +## v7.1.0 - 2025-02-12 + +### New Features +- **User Interface for DRVEGRD 169 MSE v1.1.0**: Introduced a new UI with integrated object tracking capabilities. +- **User Interface for DRVEGRD 171 MSE v1.0.0**: Introduced a new UI with integrated object tracking capabilities. +- **New User Interfaces**: Added user interfaces for DRVEGRD 152 v1.5.0 and DRVEGRD 171 v1.4.0. +- **RViz Plugins**: Extended service configurator with DRVEGRD 171 MSE instruction set. +- **Adopted Testing**: Includes check for new DRVEGRD 171 v1.5.0. +- **Updated External libraries**: Updated external libraries compatible with the new smart access release. + +## v8.0.0 - 2026-03-03 + +### New Features +- **User Interface for DRVEGRD 166 v1.0.0**: Introduced a new UI. +- **User Interface for DRVEGRD 166 v2.0.0**: Introduced a new UI. +- **New User Interfaces**: Added user interfaces for DRVEGRD 169 v3.0.0, DRVEGRD 169 MSE v1.3.0 and DRVEGRD 171 MSE v2.1.0. +- **Multi-Distro Support**: Added full compatibility for ROS 2 Foxy, Humble, and Jazzy via updated Docker configurations and CI/CD pipelines. +- **Batch Instruction Interface**: Introduced a new extensible service structure allowing multiple parameters of varying types (float, uint8/16/32) to be sent in a single request. +- **Asynchronous Firmware Updates**: Refactored the download protocol to use a thread-safe, non-blocking architecture, preventing RViz UI freezes during sensor updates. +- **Expanded Service Set**: Integrated new GetMode and GetStatus services into the smartmicro node logic. +- **Enhanced RViz Plugins**: Updated the Service Configurator and Download plugins with improved layouts, section-based parameters, and read/write capabilities. + +### Breaking Changes +- **Message Structure Refactor**: Replaced single-field param and value fields with array-based params, values, and value_types to support batch processing. Existing integrations must be updated to the new array format. \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..c7960a4 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,47 @@ +# Contributions + +Contributions to the `smartmicro_ros2_radars` are welcomed and highly +appreciated! :tada::+1: + + +## Contribution Guidelines + +First and foremost to get an overview of the project, please read the +[Readme](Readme.md). + +### Gitflow Workflow + +This document provides certain guidelines and rules that should be +followed while contributing to `smartmicro_ros2_radars` and its +packages. The project follows the Gitflow guidelines and so it is +expected from the contributor to firstly read the Gitflow guide and +adhere to it. + +If not familiar with Gitflow Workflow, please refer to the following +link: +- [Gitflow Workflow](https://www.atlassian.com/git/tutorials/comparing-workflows/gitflow-workflow) + +### Bugs and Features + +We use GitHub issues to report bugs and make feature requests. For +each bug/feature a new issue should be created first. It is advised to +give each issue a label. Once creating a pull request which stems from +an issue, the pull request must be linked with the respective issue. +The issue should be closed once the pull request has been successfully +merged. Also, it is advisable to name the pull request as such that it +includes the issue number. + +In case of suggestions or enhancements, simply create an issue. +:smile_cat: + +### Commits + +We would like to have clear and understandable commits, please refer to +the following link for certain good commit practices: +- [Commit practices](https://chris.beams.io/posts/git-commit/) + +### Coding Style + +It is advised to maintain a consistency in the coding style. Refer to +the following link for ROS2 coding guidelines: +- [ROS2 code style](https://docs.ros.org/en/foxy/Contributing/Code-Style-Language-Versions.html) diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..21f6c49 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,35 @@ +# ROS 2 distro (default = foxy) +ARG ROS_DISTRO=foxy +FROM ros:${ROS_DISTRO} + +# Fix repo keys ONLY for Foxy (snapshots issue). +RUN if [ "$ROS_DISTRO" = "foxy" ]; then \ + rm -f /etc/apt/sources.list.d/ros2-snapshots.list || true && \ + apt-get update && apt-get install -y curl gnupg2 lsb-release && \ + curl -sSL https://raw.githubusercontent.com/ros/rosdistro/master/ros.key \ + -o /usr/share/keyrings/ros-archive-keyring.gpg && \ + echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/ros-archive-keyring.gpg] \ + http://packages.ros.org/ros2/ubuntu $(. /etc/os-release && echo $UBUNTU_CODENAME) main" \ + | tee /etc/apt/sources.list.d/ros2.list > /dev/null ; \ + fi + +# Common dependencies +RUN apt-get update && apt-get install -y \ + iputils-ping \ + python3 \ + python3-pip \ + wget \ + git \ + ca-certificates \ + && update-ca-certificates \ + && rm -rf /var/lib/apt/lists/* + +# ROS-specific dependencies +RUN apt-get update && apt-get install -y \ + ros-${ROS_DISTRO}-point-cloud-msg-wrapper \ + ros-${ROS_DISTRO}-rviz2 \ + ros-${ROS_DISTRO}-rviz-common \ + ros-${ROS_DISTRO}-rviz-default-plugins \ + ros-${ROS_DISTRO}-rviz-rendering + +WORKDIR /code diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..2bb9ad2 --- /dev/null +++ b/LICENSE @@ -0,0 +1,176 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS \ No newline at end of file diff --git a/Readme.md b/Readme.md new file mode 100644 index 0000000..7dbeab7 --- /dev/null +++ b/Readme.md @@ -0,0 +1,328 @@ +# ROS2 smartmicro radar driver + +[![Build and test](https://github.com/smartmicro/smartmicro_ros2_radars/actions/workflows/dockerbuild.yml/badge.svg)](https://github.com/smartmicro/smartmicro_ros2_radars/actions/workflows/dockerbuild.yml) + +## Purpose / Use cases +There is a need for a node that will interface with a smartmicro radar driver and publish the data +acquired by the sensor through the ROS2 pipeline. This package implements such a node. + +## Get the Smart Access release +```bash +./smart_extract.sh +``` + +## How to launch this node +``` +ros2 launch umrr_ros2_driver radar.launch.py +``` + +## How to launch the rviz with recorder plugin +From a separate terminal and after sourcing workspace +``` +rviz2 -d smartmicro_ros2_radars/umrr_ros2_driver/config/rviz/smart_plugin.rviz +``` + +![Recorder](smart_rviz_plugin/config/images/smart_rviz_plugin.png "Rviz Outlook") + +## How to start the custom can message sender +From smart_rviz_plugin folder +``` +python custom_can_sender.py +``` + +![Sender](smart_rviz_plugin/config/images/can_sender.png "Custom CAN Sender") + +## Prerequisites + +### Supported ROS distributions: +- ROS2 foxy + +### UMRR radars and Smart Access API version +A [smartmicro](https://www.smartmicro.com/automotive-radar) UMRR96, UMRR11, DRVEGRD 171, DRVEGRD 152, DRVEGRD 169, DRVEGRD 169 MSE or DRVEGRD 171 MSE radar is +required to run this node. This code is bundled with a version of Smart Access API. Please make +sure the version used to publish the data is compatible with this version: + +- Date of release: `March 03, 2026` +- Smart Access Automotive version: `v3.11.0` + +For each sensor user interface there is a corressponding sensor firmware. The following list all the possible combinations. + +| **User Interface Version** | **Sensor Firmware Version** | +|--------------------------------------------------|-------------------------------------| +| UMRR96 Type 153 AUTOMOTIVE v1.2.1 | UMRR96 Type 153: V5.2.4 | +| UMRR96 Type 153 AUTOMOTIVE v1.2.2 | UMRR96 Type 153: V5.2.4 | +| UMRR11 Type 132 AUTOMOTIVE v1.1.1 | UMRR11 Type 132: V5.1.4 | +| UMRR11 Type 132 AUTOMOTIVE v1.1.2 | UMRR11 Type 132: V5.1.4 | +| UMRR9F Type 169 AUTOMOTIVE v1.1.1 | UMRR9F Type 169: V1.3.0 | +| UMRR9F Type 169 AUTOMOTIVE v2.0.0 | UMRR9F Type 169: V2.0.1 | +| UMRR9F Type 169 AUTOMOTIVE v2.1.1 | UMRR9F Type 169: V2.0.1 | +| UMRR9F Type 169 AUTOMOTIVE v2.2.0 | UMRR9F Type 169: V2.2.0 | +| UMRR9F Type 169 AUTOMOTIVE v2.2.1 | UMRR9F Type 169: V2.2.0 | +| UMRR9F Type 169 AUTOMOTIVE v2.4.1 | UMRR9F Type 169: V2.4.0 | +| UMRR9D Type 152 AUTOMOTIVE v1.0.2 | UMRR9D Type 152: V2.1.0 | +| UMRR9D Type 152 AUTOMOTIVE v1.0.3 | UMRR9D Type 152: V2.5.0 | +| UMRR9D Type 152 AUTOMOTIVE v1.2.2 | UMRR9D Type 152: V2.5.0 | +| UMRR9D Type 152 AUTOMOTIVE v1.4.1 | UMRR9D Type 152: V2.7.0 | +| UMRR9D Type 152 AUTOMOTIVE v1.5.0 | UMRR9D Type 152: V3.3.0 | +| UMRR9D Type 152 AUTOMOTIVE v1.5.0 | UMRR9D Type 152: V3.6.0 | +| UMRRA4 Type 171 AUTOMOTIVE v1.0.0 | UMRRA4 Type 171: V1.0.0 | +| UMRRA4 Type 171 AUTOMOTIVE v1.0.1 | UMRRA4 Type 171: V1.0.0 | +| UMRRA4 Type 171 AUTOMOTIVE v1.2.1 | UMRRA4 Type 171: V1.2.1 | +| UMRRA4 Type 171 AUTOMOTIVE v1.4.0 | UMRRA4 Type 171: V2.0.0 | +| UMRRA4 Type 171 AUTOMOTIVE v1.4.0 | UMRRA4 Type 171: V2.3.0 | +| UMRR11 Type 132 MSE v1.1.1 | UMRR11 Type 132-MSE: V6.1.2 | +| UMRR9F Type 169 MSE v1.0.0 | UMRR9F Type 169-MSE: V1.1.0 | +| UMRR9F Type 169 MSE v1.1.0 | UMRR9F Type 169-MSE: V1.3.0 | +| UMRR9F Type 169 MSE v1.3.0 | UMRR9F Type 169-MSE: V1.5.0 | +| UMRRA4 Type 171 MSE v1.0.0 | UMRR9F Type 171-MSE: V1.0.0 | +| UMRRA4 Type 171 MSE v1.3.0 | UMRR9F Type 171-MSE: V2.1.0 | +| UMRRA1 Type 166 AUTOMOTIVE v1.0.0 | UMRRA1 Type 166: V1.0.0 | +| UMRRA1 Type 166 AUTOMOTIVE v2.0.0 | UMRRA1 Type 166: V1.0.0 | + +### Point cloud message wrapper library +To add targets to the point cloud in a safe and quick fashion a +[`point_cloud_msg_wrapper`](https://gitlab.com/ApexAI/point_cloud_msg_wrapper) library is used within +this project's node. This project can be installed either through `rosdep` or manually by executing: +``` +sudo apt install ros-foxy-point-cloud-msg-wrapper +``` + +To use the GUI provided, it is required to install the following package: +``` +pip install python-can +``` + +## Inputs / Outputs / Configuration + +### The inputs: +The inputs are coming as network packages generated in either of the following two ways: +- Through directly interfacing with the sensor +- Through a provided pcap file +- Through using the sensor simulators + +These inputs are processed through the Smart Access C++ API and trigger a callback. Every time this +callback is triggered a new point cloud message is created and published. + +### The outputs: +The driver publishes `sensor_msgs::msg::PointCloud2` messages with the radar targets on the topic +`umrr/targets` which can be remapped through the parameters. + +### Interface Configuration: +For setting up a sensor with ethernet or can, the interfaces of the should be set properly prior to configuring the node. +The sensor is equipped with three physical layers (RS485, CAN and ethernet) however the driver uses only ethernet and can: +- ethernet: to set-up an ethernet interface the following command could be used `ifconfig my_interface_name 192.168.11.17 netmask 255.255.255.0`. +The command above uses the default source ip address used by the sensors. +- can: if using LAWICEL to set-up a can interface the following commands could be used `slcand -o -s6 -t hw -S 3000000 /dev/ttyUSBx`and than `ip link set up my_interface_name`. +This uses the default baudrate of _500000_. When using Peak CAN the interfaces are recognized by linux and is only needed to set the baudrate. + +### Node Configuration: +The node is configured through the parameters. Here is a short recap of the most important parts. +For more details, see the [`radar.sensor.example.yaml`](umrr_ros2_driver/param/radar.sensor.example.yaml) and +[`radar.adapter.example.yaml`](umrr_ros2_driver/param/radar.adapter.example.yaml) files. + +To set up the ***sensors***, configure the following parameters: + +- **`link_type`**: Specifies the type of hardware connection. + +- **`model`**: Defines the model of the sensor being used. + - **CAN Models**: + 'umrra4_can_mse_v1_0_0', 'umrra4_can_mse_v2_1_0', 'umrr9f_can_mse_v1_1_0', 'umrr9f_can_mse_v1_0_0', 'umrr96_can_v1_2_2', + 'umrr11_can_v1_1_2', 'umrr9f_can_v2_1_1', 'umrr9f_can_v2_2_1', 'umrr9f_can_v2_4_1', 'umrr9f_can_v3_0_0', 'umrr9d_can_v1_0_3', + 'umrr9d_can_v1_2_2', 'umrr9d_can_v1_4_1', 'umrr9d_can_v1_5_0', 'umrra4_can_v1_0_1', 'umrra4_can_v1_2_1', 'umrra4_can_v1_4_0' + - **Port Models**: + 'umrra1_v2_0_0', 'umrra1_v1_0_0', 'umrra4_mse_v1_0_0', 'umrra4_mse_v2_1_0', 'umrr9f_mse_v1_3_0', 'umrr9f_mse_v1_1_0', + 'umrr9f_mse_v1_0_0', 'umrr96_v1_2_2', 'umrr11_v1_1_2', 'umrr9f_v2_1_1', 'umrr9f_v2_2_1', 'umrr9f_v2_4_1','umrr9f_v3_0_0', + 'umrr9d_v1_0_3', 'umrr9d_v1_2_2', 'umrr9d_v1_4_1', 'umrr9d_v1_5_0', 'umrra4_v1_0_1', 'umrra4_v1_2_1', 'umrra4_v1_4_0' + +- **`dev_id`**: Adapter ID to which the sensor is connected. + ***Note:*** The adapter and sensor must have the same `dev_id`. + +- **`id`**: The client ID of the sensor/source. + ***Must be a unique, non-zero integer.*** + +- **`ip`**: The ***unique*** IP address of the sensor or source acting as a sensor. + ***Required only for sensors using Ethernet.*** + +- **`port`**: Port used to receive packets. + ***Required only for sensors using Ethernet and default is:*** `55555`. + +- **`frame_id`**: Name of the frame in which messages will be published. + +- **`history_size`**: Size of the history buffer for the message publisher. + +- **`inst_type`**: Instruction serialization type. + ***Relevant for Ethernet sensors.***. + ***Should be set to:*** `port_based`. + +- **`data_type`**: Data serialization type. + ***Relevant for Ethernet sensors.***. + ***Should be set to:*** `port_based`. + +- **`uifname`**: User interface name of the sensor (refer to the [`user_interfaces`](umrr_ros2_driver/smartmicro/user_interfaces/)). + - **`uifmajorv`**: Major version of the sensor user interface. + - **`uifminorv`**: Minor version of the sensor user interface. + - **`uifpatchv`**: Patch version of the sensor user interface. + +To set up the ***adapters***, configure the following parameters: + +- **`master_inst_serial_type`**: Instruction serilization type of the master. + ***Note:*** When using a hybrid of `can` and `port` use `can_based`. + +- **`master_data_serial_type`**: Data serilization type of the master. + ***Note:*** When using a hybrid of `can` and `port` use `can_based`. + +- **`hw_type`**: Specifies the type of the hardware connection. + +- **`hw_dev_id`**: Adapter id of the hardware. + ***Note:*** The adapter and sensor must have the same `dev_id`. + +- **`hw_iface_name`**: Name of the used network interface. + +- **`baudrate`**: Baudrate of the sensor connected with CAN. + ***Required only for sensors using CAN and default is:*** _500000_. + +- **`port`**: Port used to receive packets. + ***Required only for sensors using Ethernet and default is:*** _55555_. + +## Mode of operations of the sensors +The smartmicro radars come equipped with numerous features and modes of operation. Using the ros2 services provided one +may access these modes and send commands to the sensor. A list of available sensor operations is given in the [`user_interfaces`](umrr_ros2_driver/smartmicro/user_interfaces/). + +A ros2 `SetMode` service should be called to implement these mode changes. These are the inputs to a ros2 `SetMode` service call: +- `params`: name/names of the mode instructions (specific to the sensor). +- `values`: the mode of operation (specific to sensor where the modes are same). +- `sensor_id`: the id of the sensor to which the service call should be sent. +- `value_types`: the data types for the params `0: float32, 1: uint32, 2: uint16, 3: uint8`. +- `section_name`: the name of the section in the instruction file. + +For instance, changing the `Index of center frequency (center_frequency_idx)` of a UMRR-A4 sensor to `(1)` mode would require the following call: +`ros2 service call /smart_radar/set_radar_mode umrr_ros2_msgs/srv/SetMode "{section_name: auto_interface_0dim, sensor_id: 100, params: ['center_frequency_idx'], values: ['1'], value_types: [3]}"` + +A ros2 'GetMode' service can be called to get the actual sensor modes. The inputs for this call are: +- `params`: name/names of the mode instructions (specific to the sensor). +- `sensor_id`: the id of the sensor to which the service call should be sent. +- `param_types`: the data types for the params `0: float32, 1: uint32, 2: uint16, 3: uint8`. +- `section_name`: the name of the section in the instruction file. + +For instance, getting the `Index of center frequency (center_frequency_idx)` of a UMRR-A4 sensor to `(1)` mode would require the following call: +`ros2 service call /smart_radar/get_radar_mode umrr_ros2_msgs/srv/GetMode "{section_name: auto_interface_0dim, sensor_id: 100, params: ['center_frequency_idx'], param_types: [3]}"` + +Similarly, a ros2 `SendCommand` service could be used to send commands to the sensors. There are three inputs for sending a command: +- `command`: name of the command (specific to the sensor interface) +- `value`: the value of the command +- `sensor_id`: the id of the sensor to which the service call should be sent. +- `section_name`: the name of the section in the instruction file. + +The call for such a service would be as follows: +`ros2 service call /smart_radar/send_command umrr_ros2_msgs/srv/SendCommand "{section_name: auto_interface_command, command: "comp_eeprom_ctrl_default_param_sec", value: 2, sensor_id: 100}"` + +Apart from commands and modes we can also access the status of the sensor using a ros2 `GetStatus` service. It has three inputs: +- `statuses`: name/names of the status (specific to the sensor). +- `sensor_id`: the id of the sensor to which the service call should be sent. +- `status_types`: the data types for the status `0: uint32, 1: uint16`. +- `section_name`: the name of the section in the instruction file. + +The call would be like follows: +`ros2 service call /smart_radar/get_radar_status umrr_ros2_msgs/srv/GetStatus "{section_name: auto_interface, sensor_id: 100, statuses: ["sw_version_major", "sw_version_minor"], status_types: [1, 1]}"` + +## Configuration of the sensors +In order to use multiple sensors (maximum of up to eight sensors) with the node the sensors should be configured separately. +The IP addresses of the sensors could be assigned using: +- The smartmicro tool `DriveRecorder`. +- Using the `Smart Access C++ API` +- Using `Sensor Services` provided by the node + +Each sensor has to be assigned a unique IP address! + +To use the ros2 `SetIp`service we require two inputs: +- `value_ip`: the value of the ip address in decimal. For instance to set the IP to `192.168.11.64` its corresponding +value in decimal `3232238400` should be used. +- `sensor_id`: the sensor whose ip address is to be changed. + +The call for such a service would be as follows: +`ros2 service call /smart_radar/set_ip_address umrr_ros2_msgs/srv/SetIp "{value_ip: 3232238400, sensor_id: 100}"` + +Note: For successful execution of this call it is important that the sensor is restarted, the ip address in the +[`radar.template.yaml`](umrr_ros2_driver/param/radar.template.yaml) is updated and the driver is build again. + +## Firmware download +All the smartmicro radar sensors have independent firmware which are updated every now and than. To keep the sensor updated a firmware download +needs to be performed. + +A ros2 `FirmwareDownload` service should be called to implement these mode changes. There are two inputs to a ros2 service call: +- `file_path`: the path where the firmware is located +- `sensor_id`: the id of the sensor to which the service call should be sent. + +The call for such a service would be as follows: +`ros2 service call /smart_radar/firmware_download umrr_ros2_msgs/srv/FirmwareDownload "{sensor_id: 100, file_path: '/path/to/firmware/file'}"` + +Note: The download could be performed only for one sensor at a time! +Important: The download requires that the transfer length of the interface is set to minimum 4k! + +## Sensor Service Responses +The sensor services respond with certain value codes. The following is a lookup table for the possible responses: + +**Value** | **Description** +--- | --- +0 | No instruction Response +1 | Instruction Response was processed successfully +2 | General error +6 | Invalid protection +7 | Value out of minimal bounds +8 | Value out of maximal bounds + +## RVIZ plugins and custom CAN sender +Custom plugins for rviz has been provided. This plugin provides logging of the target list, object list and their respective headers. +It provides a command configurator plugin through which commands, status and mode reqeust could be send. It also provides a plugin for initiating a firmware download. +A config file is available which adds this plugin to the rviz. Along with logging the data the plugin also gives the possibility to record +the target/object list data, convert it into a csv format and save it. + +Separately, a python GUI is also provided with which it is possible to send custom CAN messages. + +## Development +The dockerfile can be used to build and test the ros driver. + +### Prerequisites + +- Docker version >= 20.10.14 +- Docker compose version >= 1.29.2 + +## Building and Testing +Accept the agreement and get the smartaccess release +```bash +./smart_extract.sh +``` + +Building docker container +```bash +docker build . -t umrr-ros:latest +``` + +Building the driver with the docker container +```bash +docker run --rm -v`pwd`:/code umrr-ros colcon build --packages-skip smart_rviz_plugin +``` + +Running the unit and integration tests via the docker compose +```bash +docker-compose up +``` + +Getting the test coverage via the docker container +```bash +docker run --rm -v`pwd`:/code umrr-ros colcon test-result --all --verbose +``` + +Stop and remove docker containers and networks +```bash +docker-compose down +``` +## ARMv8 Support +The Smart Access release which will be downloaded using the script also offers platform support for armv8. In order to build the driver on an armv8 machine, the [`CMakeLists.txt`](umrr_ros2_driver/CMakeLists.txt) should be adopted. +Instead of using the default `lib-linux-x86_64_gcc_9` the user should plugin the `lib-linux-armv8-gcc_9` for armv8. + +## Contribution +This project is a joint effort between [smartmicro](https://www.smartmicro.com/) and [Apex.AI](https://www.apex.ai/). The initial version of the code was developed by Igor Bogoslavskyi of Apex.AI (@niosus) and was thereafter adapted and extended by smartmicro. + +## License +Licensed under the [Apache 2.0 License](LICENSE). diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..e139d33 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,101 @@ +# Copyright (c) 2021, s.m.s, smart microwave sensors GmbH, Brunswick, Germany +# +# 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. + +version: "3" +services: + build_simulator: + image: umrr-ros:${ROS_DISTRO:-foxy} + volumes: + - .:/code + entrypoint: ["/code/simulator/simulation/build_simulator.sh"] + sensor_0: + depends_on: + build_simulator: + condition: service_completed_successfully + image: umrr-ros:${ROS_DISTRO:-foxy} + volumes: + - .:/code + environment: + - SMART_ACCESS_CFG_FILE_PATH=/code/simulator/config_umrr11/com_lib_config.json + - LD_LIBRARY_PATH=/code/umrr_ros2_driver/smartmicro/lib-linux-x86_64-gcc_9 + entrypoint: ["/code/simulator/simulation/out/bin/simulator"] + command: ["4", "1", "1", "A"] + networks: + device_network: + ipv4_address: 172.22.10.101 + sensor_1: + depends_on: + build_simulator: + condition: service_completed_successfully + image: umrr-ros:${ROS_DISTRO:-foxy} + volumes: + - .:/code + environment: + - SMART_ACCESS_CFG_FILE_PATH=/code/simulator/config_umrr96/com_lib_config.json + - LD_LIBRARY_PATH=/code/umrr_ros2_driver/smartmicro/lib-linux-x86_64-gcc_9 + entrypoint: ["/code/simulator/simulation/out/bin/simulator"] + command: ["5", "1", "2", "A"] + networks: + device_network: + ipv4_address: 172.22.10.102 + sensor_2: + depends_on: + build_simulator: + condition: service_completed_successfully + image: umrr-ros:${ROS_DISTRO:-foxy} + volumes: + - .:/code + environment: + - SMART_ACCESS_CFG_FILE_PATH=/code/simulator/config_umrr9f/com_lib_config.json + - LD_LIBRARY_PATH=/code/umrr_ros2_driver/smartmicro/lib-linux-x86_64-gcc_9 + entrypoint: ["/code/simulator/simulation/out/bin/simulator"] + command: ["9", "2", "1", "C"] + networks: + device_network: + ipv4_address: 172.22.10.103 + sensor_3: + depends_on: + build_simulator: + condition: service_completed_successfully + image: umrr-ros:${ROS_DISTRO:-foxy} + volumes: + - .:/code + environment: + - SMART_ACCESS_CFG_FILE_PATH=/code/simulator/config_umrr9d/com_lib_config.json + - LD_LIBRARY_PATH=/code/umrr_ros2_driver/smartmicro/lib-linux-x86_64-gcc_9 + entrypoint: ["/code/simulator/simulation/out/bin/simulator"] + command: ["11", "1", "5", "D"] + networks: + device_network: + ipv4_address: 172.22.10.104 + ros_node: + image: umrr-ros:${ROS_DISTRO:-foxy} + depends_on: + - "sensor_0" + - "sensor_1" + - "sensor_2" + - "sensor_3" + volumes: + - .:/code + command: ["colcon", "test", "--packages-above", "umrr_ros2_driver"] + networks: + device_network: + ipv4_address: 172.22.10.100 + +networks: + device_network: + ipam: + driver: default + config: + - subnet: 172.22.10.0/24 diff --git a/simulator/COLCON_IGNORE b/simulator/COLCON_IGNORE new file mode 100644 index 0000000..e69de29 diff --git a/simulator/config_umrr11/com_lib_config.json b/simulator/config_umrr11/com_lib_config.json new file mode 100644 index 0000000..b86f707 --- /dev/null +++ b/simulator/config_umrr11/com_lib_config.json @@ -0,0 +1,16 @@ +{ + "name": "Com Lib Config", + "version": "1.0.0", + "client_id": 100, + "role": "slave", + "shared_lib_path": "/code/umrr_ros2_driver/smartmicro/lib-linux-x86_64-gcc_9", + "config_path": "/code/simulator/config_umrr11", + "user_interface_name": "umrr11_t132_automotive", + "user_interface_major_v": 1, + "user_interface_minor_v": 1, + "user_interface_patch_v": 1, + "download_path": "", + "instruction_serialization_type": "port_based", + "data_serialization_type": "port_based", + "alive": false +} diff --git a/simulator/config_umrr11/hw_inventory.json b/simulator/config_umrr11/hw_inventory.json new file mode 100644 index 0000000..94f370a --- /dev/null +++ b/simulator/config_umrr11/hw_inventory.json @@ -0,0 +1,12 @@ +{ + "hwItems": [ + { + "type": "eth", + "dev_id": 1, + "iface_name": "eth0", + "port": 55555 + } + ], + "name": "HW inventory List", + "version": "1.1.0" +} diff --git a/simulator/config_umrr11/routing_table.json b/simulator/config_umrr11/routing_table.json new file mode 100644 index 0000000..1eaffd3 --- /dev/null +++ b/simulator/config_umrr11/routing_table.json @@ -0,0 +1,14 @@ +{ + "clients": [ + { + "client_id": 1, + "data_serialization_type": "port_based", + "instruction_serialization_type": "port_based", + "ip": "172.22.10.100", + "link_type": "eth", + "port": 55555 + } + ], + "name": "Client Routing Table", + "version": "1.0.0" +} diff --git a/simulator/config_umrr96/com_lib_config.json b/simulator/config_umrr96/com_lib_config.json new file mode 100644 index 0000000..447e4b0 --- /dev/null +++ b/simulator/config_umrr96/com_lib_config.json @@ -0,0 +1,16 @@ +{ + "name": "Com Lib Config", + "version": "1.0.0", + "client_id": 200, + "role": "slave", + "shared_lib_path": "/code/umrr_ros2_driver/smartmicro/lib-linux-x86_64-gcc_9", + "config_path": "/code/simulator/config_umrr96", + "user_interface_name": "umrr96_t153_automotive", + "user_interface_major_v": 1, + "user_interface_minor_v": 2, + "user_interface_patch_v": 1, + "download_path": "", + "instruction_serialization_type": "port_based", + "data_serialization_type": "port_based", + "alive": false +} diff --git a/simulator/config_umrr96/hw_inventory.json b/simulator/config_umrr96/hw_inventory.json new file mode 100644 index 0000000..94f370a --- /dev/null +++ b/simulator/config_umrr96/hw_inventory.json @@ -0,0 +1,12 @@ +{ + "hwItems": [ + { + "type": "eth", + "dev_id": 1, + "iface_name": "eth0", + "port": 55555 + } + ], + "name": "HW inventory List", + "version": "1.1.0" +} diff --git a/simulator/config_umrr96/routing_table.json b/simulator/config_umrr96/routing_table.json new file mode 100644 index 0000000..1eaffd3 --- /dev/null +++ b/simulator/config_umrr96/routing_table.json @@ -0,0 +1,14 @@ +{ + "clients": [ + { + "client_id": 1, + "data_serialization_type": "port_based", + "instruction_serialization_type": "port_based", + "ip": "172.22.10.100", + "link_type": "eth", + "port": 55555 + } + ], + "name": "Client Routing Table", + "version": "1.0.0" +} diff --git a/simulator/config_umrr9d/com_lib_config.json b/simulator/config_umrr9d/com_lib_config.json new file mode 100644 index 0000000..5541739 --- /dev/null +++ b/simulator/config_umrr9d/com_lib_config.json @@ -0,0 +1,16 @@ +{ + "name": "Com Lib Config", + "version": "1.0.0", + "client_id": 400, + "role": "slave", + "shared_lib_path": "/code/umrr_ros2_driver/smartmicro/lib-linux-x86_64-gcc_9", + "config_path": "/code/simulator/config_umrr9d", + "user_interface_name": "umrr9d_t152_automotive", + "user_interface_major_v": 1, + "user_interface_minor_v": 5, + "user_interface_patch_v": 0, + "download_path": "", + "instruction_serialization_type": "port_based", + "data_serialization_type": "port_based", + "alive": false +} diff --git a/simulator/config_umrr9d/hw_inventory.json b/simulator/config_umrr9d/hw_inventory.json new file mode 100644 index 0000000..94f370a --- /dev/null +++ b/simulator/config_umrr9d/hw_inventory.json @@ -0,0 +1,12 @@ +{ + "hwItems": [ + { + "type": "eth", + "dev_id": 1, + "iface_name": "eth0", + "port": 55555 + } + ], + "name": "HW inventory List", + "version": "1.1.0" +} diff --git a/simulator/config_umrr9d/routing_table.json b/simulator/config_umrr9d/routing_table.json new file mode 100644 index 0000000..1eaffd3 --- /dev/null +++ b/simulator/config_umrr9d/routing_table.json @@ -0,0 +1,14 @@ +{ + "clients": [ + { + "client_id": 1, + "data_serialization_type": "port_based", + "instruction_serialization_type": "port_based", + "ip": "172.22.10.100", + "link_type": "eth", + "port": 55555 + } + ], + "name": "Client Routing Table", + "version": "1.0.0" +} diff --git a/simulator/config_umrr9f/com_lib_config.json b/simulator/config_umrr9f/com_lib_config.json new file mode 100644 index 0000000..5a444bb --- /dev/null +++ b/simulator/config_umrr9f/com_lib_config.json @@ -0,0 +1,16 @@ +{ + "name": "Com Lib Config", + "version": "1.0.0", + "client_id": 300, + "role": "slave", + "shared_lib_path": "/code/umrr_ros2_driver/smartmicro/lib-linux-x86_64-gcc_9", + "config_path": "/code/simulator/config_umrr9f", + "user_interface_name": "umrr9f_t169_automotive", + "user_interface_major_v": 2, + "user_interface_minor_v": 1, + "user_interface_patch_v": 1, + "download_path": "", + "instruction_serialization_type": "port_based", + "data_serialization_type": "port_based", + "alive": false +} diff --git a/simulator/config_umrr9f/hw_inventory.json b/simulator/config_umrr9f/hw_inventory.json new file mode 100644 index 0000000..94f370a --- /dev/null +++ b/simulator/config_umrr9f/hw_inventory.json @@ -0,0 +1,12 @@ +{ + "hwItems": [ + { + "type": "eth", + "dev_id": 1, + "iface_name": "eth0", + "port": 55555 + } + ], + "name": "HW inventory List", + "version": "1.1.0" +} diff --git a/simulator/config_umrr9f/routing_table.json b/simulator/config_umrr9f/routing_table.json new file mode 100644 index 0000000..1eaffd3 --- /dev/null +++ b/simulator/config_umrr9f/routing_table.json @@ -0,0 +1,14 @@ +{ + "clients": [ + { + "client_id": 1, + "data_serialization_type": "port_based", + "instruction_serialization_type": "port_based", + "ip": "172.22.10.100", + "link_type": "eth", + "port": 55555 + } + ], + "name": "Client Routing Table", + "version": "1.0.0" +} diff --git a/simulator/simulation/CMakeLists.txt b/simulator/simulation/CMakeLists.txt new file mode 100644 index 0000000..a9f111b --- /dev/null +++ b/simulator/simulation/CMakeLists.txt @@ -0,0 +1,33 @@ +project(SensorSimulator) + +cmake_minimum_required(VERSION 3.13) + +set(CMAKE_SYSTEM_NAME Linux) +set(CMAKE_C_COMPILER gcc) +set(CMAKE_CXX_COMPILER g++) +set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -m64 -fPIC") +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11 -pthread -m64 -fPIC") +set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -m64 -fPIC") + +link_directories(${CMAKE_CURRENT_SOURCE_DIR}/${SMART_ACCESS_LIB_FOLDER}) + +add_executable(simulator + src/simulator.cpp) + +target_include_directories(simulator PUBLIC + ../../umrr_ros2_driver/smartmicro/include + umrr_ros2_driver/smartmicro/include/umrr11_t132_automotive_v1_1_2 + umrr_ros2_driver/smartmicro/include/umrr96_t153_automotive_v1_2_2 + umrr_ros2_driver/smartmicro/include/umrr9f_t169_automotive_v2_1_1 + umrr_ros2_driver/smartmicro/include/umrr9d_t152_automotive_v1_5_0) + +target_link_libraries(simulator + com_lib + osal + umrr11_t132_automotivev1.1.2_user_interface + umrr96_t153_automotivev1.2.2_user_interface + umrr9f_t169_automotivev2.1.1_user_interface + umrr9d_t152_automotivev1.5.0_user_interface + dl) + +install(TARGETS simulator DESTINATION bin) diff --git a/simulator/simulation/build_simulator.sh b/simulator/simulation/build_simulator.sh new file mode 100755 index 0000000..b6b8729 --- /dev/null +++ b/simulator/simulation/build_simulator.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env bash +work_dir=$(pwd) +set -e + +cd /code/simulator/simulation + +mkdir -p build_dir +cmake -G "Unix Makefiles" -S. -Bbuild_dir \ +-DCMAKE_INSTALL_PREFIX=out \ +-DSMART_ACCESS_LIB_FOLDER=../../umrr_ros2_driver/smartmicro/lib-linux-x86_64-gcc_9 +make -C build_dir +make -C build_dir install diff --git a/simulator/simulation/src/simulator.cpp b/simulator/simulation/src/simulator.cpp new file mode 100644 index 0000000..1e7d654 --- /dev/null +++ b/simulator/simulation/src/simulator.cpp @@ -0,0 +1,149 @@ +// Copyright (c) 2021, s.m.s, smart microwave sensors GmbH, Brunswick, Germany. +// +// 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 +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +using namespace com::common; +using namespace com::master; +using namespace com::types; + +namespace +{ +uint64_t identifier; +uint64_t majorVersion; +uint64_t minorVersion; +std::string port; +std::string portFile; +} // namespace + +std::shared_ptr dataServices = + com::common::DataServicesIface::Get(); + +void slave_callback(ClientId clientId, PortId, BufferDescriptor buffer) +{ + InstructionBuffer * receive = reinterpret_cast(buffer.GetBufferPtr()); + int sizeIncomingBuf = buffer.GetSize(); + uint32_t instnumber = receive->GetNumOfInstructions(); + auto instructions = receive->GetInstructions(); + + for (auto instruction : instructions) { + if (instruction->GetSectionId() == 2010 && instruction->GetId() == 2) { + std::cout << "UMRR96 mode frequency_sweep set!" << std::endl; + instruction->SetResponse(COM_INSTR_PORT_SUCCESS); + } else if (instruction->GetSectionId() == 2010 && instruction->GetId() == 5) { + std::cout << "UMRR9F mode range_toggle_mode get!" << std::endl; + instruction->SetResponse(COM_INSTR_PORT_SUCCESS); + } else if (instruction->GetSectionId() == 2010 && instruction->GetId() == 4) { + std::cout << "UMRR11 mode angular_separation set!" << std::endl; + instruction->SetResponse(COM_INSTR_PORT_SUCCESS); + } else if (instruction->GetSectionId() == 2012 && instruction->GetId() == 3) { + std::cout << "Software major version read!" << std::endl; + instruction->SetResponse(COM_INSTR_PORT_SUCCESS); + } else if (instruction->GetSectionId() == 2012 && instruction->GetId() == 4) { + std::cout << "Software minorr version read!" << std::endl; + instruction->SetResponse(COM_INSTR_PORT_SUCCESS); + } else { + std::cout << "Unknown instruction received from ROS driver!" << std::endl; + } + } + + dataServices->SetInstructionBuffer(clientId, *receive, nullptr); +} + +void stream_port(std::string portFile) +{ + dataServices->RegisterInstRecvCallback(slave_callback); + + ClientId masterId = 1; + PortId portTargetListId = 66; + std::ifstream ifs(portFile, std::ifstream::binary | std::ios::binary); + std::filebuf * pbuf = ifs.rdbuf(); + int size = pbuf->pubseekoff(0, ifs.end, ifs.in); + pbuf->pubseekpos(0, ifs.in); + char * filebuffer = new (std::nothrow) char[size]; + + if (filebuffer == nullptr) { + std::cout << "error assigning memory!" << std::endl; + } + + pbuf->sgetn(filebuffer, size); + BufferDescriptor bufferdesc((uint8_t *)filebuffer, size); + std::this_thread::sleep_for(std::chrono::seconds(1)); + + if (ERROR_CODE_OK != dataServices->StreamDataPort(masterId, portTargetListId, bufferdesc)) { + return; + } + + std::cout << "sensor is transmitting data! " << std::endl; + ifs.close(); + delete[] filebuffer; +} + +int main(int argc, char * argv[]) +{ + if (argc != 5) { + std::cout << "Specifiy User Interface for the sensor" << std::endl; + return 1; + } + + identifier = strtoll(argv[1], nullptr, 10); + majorVersion = strtoll(argv[2], nullptr, 10); + minorVersion = strtoll(argv[3], nullptr, 10); + port = argv[4]; + + if (!dataServices->Init()) { + throw std::runtime_error("Data services have not been initialized!"); + } + + auto Start = std::chrono::steady_clock::now(); + + while (1) { + if (port == "A") { + std::string portFile = "/code/simulator/targetlist_port_v2_1_0.bin"; + stream_port(portFile); + } else if (port == "B") { + std::string portFile = "/code/simulator/targetlist_port_v3_0_0.bin"; + stream_port(portFile); + } else if (port == "C") { + std::string portFile = "/code/simulator/targetlist_port_v4_0_0.bin"; + stream_port(portFile); + } else if (port == "D") { + std::string portFile = "/code/simulator/targetlist_port_v4_1_0.bin"; + stream_port(portFile); + } else { + std::cout << "Invalid input!" << std::endl; + } + if (std::chrono::steady_clock::now() - Start > std::chrono::seconds(15)) { + break; + } + } + return 0; +} diff --git a/simulator/targetlist_port_v2_1_0.bin b/simulator/targetlist_port_v2_1_0.bin new file mode 100644 index 0000000000000000000000000000000000000000..a4fc28492a68e8c1d9255f71a0f0e716682545ff GIT binary patch literal 984 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 C@h8my literal 0 HcmV?d00001 diff --git a/smart_extract.sh b/smart_extract.sh new file mode 100755 index 0000000..3f19c6a --- /dev/null +++ b/smart_extract.sh @@ -0,0 +1,53 @@ +#!/bin/bash +set -e + +smart_pack=SmartAccessAutomotive_3_11_0.tgz +URL_smartbinaries=https://www.smartmicro.com/fileadmin/media/Downloads/Automotive_Radar/Software/${smart_pack} + +cat << EOF + +The following clause is explicit for the Smart Access release. + +********************************************************************************* + +This software is licensed under the Apache 2.0 License + +Copyright (c) 2021, s.m.s, smart microwave sensors GmbH, Brunswick, Germany + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software +without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit +persons to whom the Software is furnished to do so, subject to the following conditions: + +The Software is provided "as is", without warranty of any kind, express or implied, including but not limited to the warranties of merchantability, fitness for a particular +purpose and noninfringement. In no event shall the authors or copyright holders be liable for any claim, damages or other liability, whether in an action of contract, tort or +otherwise, arising from, out of or in connection with the software or the use or other dealings in the Software. + +********************************************************************************* +EOF +echo +echo -n "Do you accept the agreement you just read? (yes/no)" +echo "" +read REPLY +echo "" +case "$REPLY" in + yes) + echo "You have accepted the agreement." + ;; + *) + echo "Agreement not accepted." + exit +esac +echo + +function getSmartaccessBinaries { + wget -c $URL_smartbinaries + echo "extracting smart access" + tar xfz $smart_pack --strip-components=1 -C umrr_ros2_driver/smartmicro/ +} + +function cleanup { + rm -rf $smart_pack +} + +getSmartaccessBinaries +cleanup diff --git a/smart_rviz_plugin/CMakeLists.txt b/smart_rviz_plugin/CMakeLists.txt new file mode 100644 index 0000000..50e5cf8 --- /dev/null +++ b/smart_rviz_plugin/CMakeLists.txt @@ -0,0 +1,107 @@ +cmake_minimum_required(VERSION 3.11) + +project(smart_rviz_plugin) + +# Default to C++17 +if(NOT CMAKE_CXX_STANDARD) + set(CMAKE_CXX_STANDARD 17) + set(CMAKE_CXX_STANDARD_REQUIRED ON) +endif() + +if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang") + add_compile_options(-Wall -Wextra -Wpedantic -Wnon-virtual-dtor -Woverloaded-virtual) +endif() + +add_definitions(-D_BUILD_DIR_PATH="${CMAKE_CURRENT_BINARY_DIR}") +add_definitions(-D_SRC_DIR_PATH="${CMAKE_CURRENT_SOURCE_DIR}") + +find_package(ament_cmake_auto REQUIRED) +find_package(umrr_ros2_msgs REQUIRED) +find_package(rviz_common REQUIRED) +find_package(rviz_default_plugins REQUIRED) +find_package(rviz_rendering REQUIRED) +find_package(Qt5 REQUIRED COMPONENTS Widgets Test) +find_package(pluginlib REQUIRED) +find_package(rclcpp REQUIRED) +find_package(OpenCV REQUIRED) +find_package(cv_bridge REQUIRED) + +set(CMAKE_AUTOMOC ON) + +set(smart_rviz_plugin_SRCS + src/smart_recorder.cpp + src/smart_services.cpp + src/smart_download.cpp + src/smart_status.cpp +) + +set(smart_rviz_plugin_HDRS + include/smart_rviz_plugin/smart_recorder.hpp + include/smart_rviz_plugin/smart_services.hpp + include/smart_rviz_plugin/smart_download.hpp + include/smart_rviz_plugin/smart_status.hpp +) + +add_library(smart_rviz_plugin SHARED + ${smart_rviz_plugin_SRCS} + ${smart_rviz_plugin_HDRS} +) + +set(dependencies + umrr_ros2_msgs + pluginlib + Qt5 + rclcpp + rviz_common + rviz_default_plugins + rviz_ogre_vendor + rviz_rendering + std_msgs + OpenCV + cv_bridge +) + +ament_target_dependencies(smart_rviz_plugin + ${dependencies} +) + +target_include_directories(smart_rviz_plugin PUBLIC + $ + $ + ${Qt5Widgets_INCLUDE_DIRS} + ${OpenCV_INCLUDE_DIRS} +) + + +target_link_libraries(smart_rviz_plugin + rviz_common::rviz_common +) + + +target_compile_definitions(smart_rviz_plugin PRIVATE "RVIZ_DEFAULT_PLUGINS_BUILDING_LIBRARY") + +pluginlib_export_plugin_description_file(rviz_common plugins_description.xml) + +install( + TARGETS smart_rviz_plugin + EXPORT smart_rviz_plugin + ARCHIVE DESTINATION lib + LIBRARY DESTINATION lib + RUNTIME DESTINATION bin +) + +install( + DIRECTORY include/ + DESTINATION include/ +) + +ament_export_targets(smart_rviz_plugin HAS_LIBRARY_TARGET) +ament_export_dependencies( + rclcpp + rviz_common + rviz_ogre_vendor + sensor_msgs + umrr_ros2_msgs +) + +ament_package() diff --git a/smart_rviz_plugin/config/images/can_sender.png b/smart_rviz_plugin/config/images/can_sender.png new file mode 100644 index 0000000000000000000000000000000000000000..c202ae8c0d67b3f1cd54010d63f06628cae32f46 GIT binary patch literal 23029 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 JzCZW#-vE0F0Gt2- literal 0 HcmV?d00001 diff --git a/smart_rviz_plugin/config/images/smart_rviz_plugin.png b/smart_rviz_plugin/config/images/smart_rviz_plugin.png new file mode 100644 index 0000000000000000000000000000000000000000..fb19657dc5a96557094f784a5ed6b63230c2ebb3 GIT binary patch literal 411990 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≷}QrQB@+cJ9{2&NILc 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�Q4V_rMdgu^hd=JHC3^4Y`!3#7tg|Yj;+UHk~<;W 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ŮCwRd&&`fIH=r# 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⪼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 + Value: true + - Class: rviz_default_plugins/Axes + Enabled: true + Length: 1 + Name: Axes + Radius: 0.10000000149011612 + Reference Frame: + Value: true + - Alpha: 1 + Autocompute Intensity Bounds: true + Autocompute Value Bounds: + Max Value: 10 + Min Value: -10 + Value: true + Axis: Z + Channel Name: power + Class: rviz_default_plugins/PointCloud2 + Color: 255; 255; 255 + Color Transformer: Intensity + Decay Time: 0 + Enabled: true + Invert Rainbow: false + Max Color: 255; 255; 255 + Max Intensity: 88.64950561523438 + Min Color: 0; 0; 0 + Min Intensity: 56.63246154785156 + Name: Port_Tgts_S0 + Position Transformer: XYZ + Selectable: true + Size (Pixels): 10 + Size (m): 0.009999999776482582 + Style: Points + Topic: + Depth: 5 + Durability Policy: Volatile + History Policy: Keep Last + Reliability Policy: Reliable + Value: /smart_radar/port_targets_0 + Use Fixed Frame: true + Use rainbow: true + Value: true + - Alpha: 1 + Autocompute Intensity Bounds: true + Autocompute Value Bounds: + Max Value: 10 + Min Value: -10 + Value: true + Axis: Z + Channel Name: intensity + Class: rviz_default_plugins/PointCloud2 + Color: 138; 226; 52 + Color Transformer: FlatColor + Decay Time: 0 + Enabled: true + Invert Rainbow: false + Max Color: 255; 255; 255 + Max Intensity: 4096 + Min Color: 0; 0; 0 + Min Intensity: 0 + Name: Can_Tgts_S1 + Position Transformer: XYZ + Selectable: true + Size (Pixels): 10 + Size (m): 0.009999999776482582 + Style: Points + Topic: + Depth: 5 + Durability Policy: Volatile + History Policy: Keep Last + Reliability Policy: Reliable + Value: /smart_radar/can_targets_1 + Use Fixed Frame: true + Use rainbow: true + Value: true + Enabled: true + Global Options: + Background Color: 48; 48; 48 + Fixed Frame: umrr + Frame Rate: 30 + Name: root + Tools: + - Class: rviz_default_plugins/Interact + Hide Inactive Objects: true + - Class: rviz_default_plugins/MoveCamera + - Class: rviz_default_plugins/Select + - Class: rviz_default_plugins/FocusCamera + - Class: rviz_default_plugins/Measure + Line color: 128; 128; 0 + - Class: rviz_default_plugins/SetInitialPose + Topic: + Depth: 5 + Durability Policy: Volatile + History Policy: Keep Last + Reliability Policy: Reliable + Value: /initialpose + - Class: rviz_default_plugins/SetGoal + Topic: + Depth: 5 + Durability Policy: Volatile + History Policy: Keep Last + Reliability Policy: Reliable + Value: /goal_pose + - Class: rviz_default_plugins/PublishPoint + Single click: true + Topic: + Depth: 5 + Durability Policy: Volatile + History Policy: Keep Last + Reliability Policy: Reliable + Value: /clicked_point + Transformation: + Current: + Class: rviz_default_plugins/TF + Value: true + Views: + Current: + Class: rviz_default_plugins/Orbit + Distance: 38.8543701171875 + Enable Stereo Rendering: + Stereo Eye Separation: 0.05999999865889549 + Stereo Focal Distance: 1 + Swap Stereo Eyes: false + Value: false + Focal Point: + X: 10.84825325012207 + Y: 0.366975337266922 + Z: 0.01084582507610321 + Focal Shape Fixed Size: true + Focal Shape Size: 0.05000000074505806 + Invert Z Axis: false + Name: Current View + Near Clip Distance: 0.009999999776482582 + Pitch: 1.5697963237762451 + Target Frame: + Value: Orbit (rviz) + Yaw: 3.1353957653045654 + Saved: ~ +Window Geometry: + Displays: + collapsed: false + Height: 1757 + Hide Left Dock: false + Hide Right Dock: false + QMainWindow State: 000000ff00000000fd0000000400000000000007ce00000677fc020000000afb0000001200530065006c0065006300740069006f006e00000001e10000009b0000006a00fffffffb0000001e0054006f006f006c002000500072006f007000650072007400690065007302000001ed000001df00000185000000a3fb000000120056006900650077007300200054006f006f02000001df000002110000018500000122fb000000200054006f006f006c002000500072006f0070006500720074006900650073003203000002880000011d000002210000017afb000000100044006900730070006c00610079007300000000450000016d000000e800fffffffb0000002000730065006c0065006300740069006f006e00200062007500660066006500720200000138000000aa0000023a00000294fb00000014005700690064006500530074006500720065006f02000000e6000000d2000003ee0000030bfb0000000c004b0069006e0065006300740200000186000001060000030c00000261fb0000001c0053006d0061007200740020005200650063006f007200640065007201000000450000025e0000010e00fffffffb000000340053006d00610072007400200043006f006d006d0061006e006400200043006f006e0066006900670075007200610074006f007201000002aa000004120000026000ffffff000000010000015300000677fc0200000006fb000000180053006d00610072007400200053007400610074007500730100000045000004c40000009f00fffffffb0000002e0053006d0061007200740020004600690072006d007700610072006500200044006f0077006e006c006f006100640100000510000001ac0000014b00fffffffb000000180053006d006100720074002000530074006100740075007300000000450000053c0000009f00fffffffb0000001e0054006f006f006c002000500072006f00700065007200740069006500730100000041000000780000000000000000fb0000000a00560069006500770073000000004500000320000000be00fffffffb0000001200530065006c0065006300740069006f006e010000025a000000b200000000000000000000000200000490000000a9fc0100000001fb0000000a00560069006500770073030000004e00000080000002e10000019700000003000004420000003efc0100000002fb0000000800540069006d00650100000000000004420000000000000000fb0000000800540069006d00650100000000000004500000000000000000000003200000067700000004000000040000000800000008fc0000000100000002000000010000000a0054006f006f006c00730100000000ffffffff0000000000000000 + Selection: + collapsed: false + Smart Command Configurator: + collapsed: false + Smart Firmware Download: + collapsed: false + Smart Recorder: + collapsed: false + Smart Status: + collapsed: false + Tool Properties: + collapsed: false + Views: + collapsed: false + Width: 3151 + X: 374 + Y: 214 diff --git a/smart_rviz_plugin/custom_can_sender.py b/smart_rviz_plugin/custom_can_sender.py new file mode 100644 index 0000000..f179c82 --- /dev/null +++ b/smart_rviz_plugin/custom_can_sender.py @@ -0,0 +1,95 @@ +import can +import tkinter as tk +from tkinter import Entry, Label, Button, Text, Checkbutton + +class CANMessageSender: + def __init__(self, master): + self.master = master + master.title("Custom CAN Message Sender") + + self.label_interface = Label(master, text="Interface:") + self.label_id = Label(master, text="CAN ID:") + self.label_payload = Label(master, text="Payload:") + self.entry_interface = Entry(master) + self.entry_id = Entry(master, validate='key', validatecommand=(master.register(self.validate_hex), '%P')) + self.entry_payload = [Entry(master, width=3, validate='key', validatecommand=(master.register(self.validate_hex), '%P')) for _ in range(8)] + + self.label_interface.grid(row=0, column=0, padx=10, pady=10) + self.label_id.grid(row=0, column=2, padx=2, pady=2) + self.label_payload.grid(row=0, column=4, padx=10, pady=10) + self.entry_interface.grid(row=0, column=1, padx=10, pady=10) + self.entry_id.grid(row=0, column=3, padx=2, pady=2) + for i, entry in enumerate(self.entry_payload): + entry.grid(row=0, column=5+i, padx=2, pady=10) + + self.text_output = Text(master, height=10, width=60) + self.text_output.grid(row=1, columnspan=12, padx=10, pady=10) + + self.send_button = Button(master, text="Send Message", command=self.send_message) + self.send_button.grid(row=2, columnspan=12, pady=10) + + self.loop_button = Checkbutton(master, text="Loop", command=self.toggle_loop) + self.loop_button.grid(row=2, column=1, pady=10) + + self.loop_task_id = None + self.loop_enabled = False + + + def send_message(self): + try: + can_interface = self.entry_interface.get() + can_id_str = self.entry_id.get() + can_id = int(can_id_str, 16) + payload = [int(entry.get(), 16) for entry in self.entry_payload] + message_str = f"{can_interface} {can_id:X} {' '.join(map(lambda x: f'{x:02X}', payload))}" + send_custom_message_from_string(message_str) + + self.text_output.insert(tk.END, message_str + "\n") + self.text_output.yview(tk.END) + except ValueError as e: + print(f"Error: {e}") + + def toggle_loop(self): + self.loop_enabled = not self.loop_enabled + + if self.loop_enabled: + self.start_loop() + elif self.loop_task_id: + self.master.after_cancel(self.loop_task_id) + self.loop_task_id = None + + def start_loop(self): + if self.loop_task_id: + self.master.after_cancel(self.loop_task_id) + + if self.loop_enabled: + self.loop_task_id = self.master.after(1000, self.send_loop_message) + + def send_loop_message(self): + self.send_message() + if self.loop_enabled: + self.loop_task_id = self.master.after(1000, self.send_loop_message) + + def validate_hex(self, value): + return all(c in '0123456789ABCDEFabcdef' for c in value) + +def send_custom_message_from_string(message_str): + parts = message_str.split() + if len(parts) < 3: + raise ValueError("Invalid message format") + + can_interface = parts[0] + can_id = int(parts[1], 16) + data = [int(byte, 16) for byte in parts[2:]] + + bus = can.interface.Bus(channel=can_interface, bustype='socketcan') + try: + message = can.Message(arbitration_id=can_id, is_extended_id=False, data=data) + bus.send(message) + finally: + bus.shutdown() + +if __name__ == "__main__": + root = tk.Tk() + app = CANMessageSender(root) + root.mainloop() diff --git a/smart_rviz_plugin/include/smart_rviz_plugin/smart_download.hpp b/smart_rviz_plugin/include/smart_rviz_plugin/smart_download.hpp new file mode 100644 index 0000000..b9dcfdf --- /dev/null +++ b/smart_rviz_plugin/include/smart_rviz_plugin/smart_download.hpp @@ -0,0 +1,89 @@ +#ifndef SMART_RVIZ_PLUGIN__SMART_DOWNLOAD_HPP_ +#define SMART_RVIZ_PLUGIN__SMART_DOWNLOAD_HPP_ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "umrr_ros2_msgs/srv/firmware_download.hpp" + +namespace smart_rviz_plugin +{ +/// +/// @brief The class for the firmware download onto the sensor. +/// +/// This class provides a graphical user interface (GUI) panel for downloading +/// firmware to a sensor within the RViz environment. It extends the rviz_common::Panel +/// class and includes functionalities for browsing firmware files, initiating the +/// download process, and displaying responses from the download service. +/// +class SmartDownloadService : public rviz_common::Panel +{ + Q_OBJECT + +public: + explicit SmartDownloadService(QWidget * parent = nullptr); + ~SmartDownloadService() override; + +private slots: + /// + /// @brief Slot function to handle firmware download action. + /// + /// This function is triggered when the download button is pressed. It sends + /// a request to the firmware download service with the specified file path + /// and sensor ID. + /// + void download_firmware(); + + /// + /// @brief Slot function to handle file browsing action. + /// + /// This function is triggered when the browse button is pressed. It opens a + /// file dialog to allow the user to select a firmware file, and sets the file + /// path input field with the selected file. + /// + void browse_file(); + +private: + /// + /// @brief Initializes the ROS2 clients. + /// + /// This function initializes the ROS2 node and + /// client for the firmware download service + /// + void initialize_ros(); + + /// + /// @brief Initializes the panel's components. + /// + /// This function sets up the GUI elements and connects the signals and + /// slots. + /// + void setup_ui(); + + rclcpp::Node::SharedPtr download_node_; + rclcpp::Client::SharedPtr download_client_; + std::shared_ptr executor_; + std::thread ros_thread_; + + QLineEdit * file_path_input_; + QLineEdit * sensor_id_input_; + QPushButton * start_download_button_; + QPushButton * browse_button_; + QTextEdit * response_text_edit_; +}; + +} // namespace smart_rviz_plugin + +#endif // SMART_RVIZ_PLUGIN__SMART_DOWNLOAD_HPP_ diff --git a/smart_rviz_plugin/include/smart_rviz_plugin/smart_recorder.hpp b/smart_rviz_plugin/include/smart_rviz_plugin/smart_recorder.hpp new file mode 100644 index 0000000..3b019fd --- /dev/null +++ b/smart_rviz_plugin/include/smart_rviz_plugin/smart_recorder.hpp @@ -0,0 +1,196 @@ +#ifndef SMART_RVIZ_PLUGIN__SMART_RECORDER_HPP_ +#define SMART_RVIZ_PLUGIN__SMART_RECORDER_HPP_ + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "std_msgs/msg/string.hpp" + +namespace smart_rviz_plugin +{ +struct TargetData +{ + float range; + float power; + float azimuth_deg; + float elevation_deg; + float rcs; + float noise; + float snr; + float radial_speed; + float azimuth_angle; + float elevation_angle; + uint32_t timestamp_sec; + uint32_t timestamp_nanosec; +}; + +struct ObjectData +{ + float x_pos; + float y_pos; + float z_pos; + float speed_abs; + float heading; + float length; + float quality; + float acceleration; + uint16_t object_id; + uint32_t timestamp_sec; + uint32_t timestamp_nanosec; +}; + +/// +/// @brief The class for the target and object recorder. +/// +/// This class provides a graphical user interface (GUI) panel for viewing +/// the sensor data within the RViz environment. It extends the rviz_common::Panel +/// class and includes functionalities for selecting, recording +/// and saving the sensor data as csv format. +/// +class SmartRadarRecorder : public rviz_common::Panel +{ + Q_OBJECT + +public: + /// + /// @brief Constructor for the SmartRadarRecorder class. + /// + /// @param parent The parent widget. Defaults to nullptr. + /// + SmartRadarRecorder(QWidget * parent = nullptr); + +private slots: + /// + /// @brief Slot function to start recording the sensor data being viewed. + /// + void start_recording(); + + /// + /// @brief Slot function to stop recording the sensor data. + /// + void stop_recording(); + + /// + /// @brief Slot function to save the recorded sensor data as csv fomrat. + /// + void save_data(); + + /// + /// @brief Slot function to check the data is being published. + /// + void check_data(); + + /// + /// @brief Slot function to update the table based on the topic selected. + /// + void update_table(); + +private: + /// + /// @brief Initializes the panel's components and ROS2 client. + /// + /// This function sets up the GUI elements, initializes the ROS2 node and + /// client for the recorder, and connects the signals and + /// slots. + /// + void initialize(); + + /// + /// @brief Subscribe to the port targets topics publisheb by the smartmicro_radar_node. + /// + void port_target_callback( + const sensor_msgs::msg::PointCloud2::SharedPtr msg, const std::string topic_name); + + /// + /// @brief Subscribe to the can targets topics publisheb by the smartmicro_radar_node. + /// + void can_target_callback( + const sensor_msgs::msg::PointCloud2::SharedPtr msg, const std::string topic_name); + + /// + /// @brief Subscribe to the port objects topics publisheb by the smartmicro_radar_node. + /// + void port_object_callback( + const sensor_msgs::msg::PointCloud2::SharedPtr msg, const std::string topic_name); + + /// + /// @brief Subscribe to the can objects topics publisheb by the smartmicro_radar_node. + /// + void can_object_callback( + const sensor_msgs::msg::PointCloud2::SharedPtr msg, const std::string topic_name); + + /// + /// @brief Subscribe to the raw image topic. + /// + void image_callback(const sensor_msgs::msg::CompressedImage::SharedPtr msg); + + /// + /// @brief Function to handle the data recording for target topics. + /// + void update_target_recorded_data( + float range, float power, float azimuth_deg, float elevation_deg, float rcs, float noise, + float snr, float radial_speed, float azimuth_angle, float elevation_angle, + uint32_t timestamp_sec, uint32_t timestamp_nanosec); + + /// + /// @brief Function to handle the data recording for objects topics. + /// + void update_object_recorded_data( + float x_pos, float y_pos, float z_pos, float speed_abs, float heading, float length, + float quality, float acceleration, uint16_t object_id, uint32_t timestamp_sec, + uint32_t timestamp_nanosec); + + QTableWidget * table_data_; + QTableWidget * table_data_2_; + QTableWidget * table_timestamps_; + QWidget * widget_1; + QWidget * widget_2; + QSplitter * splitter_; + QSplitter * horiz_splitter_; + QComboBox * topic_dropdown_; + QComboBox * topic_dropdown_2_; + QVBoxLayout * gui_layout_; + QVBoxLayout * vert_layout_1; + QVBoxLayout * vert_layout_2; + QPushButton * start_button_; + QPushButton * stop_button_; + QPushButton * save_button_; + QString selectedTopic; + QTimer * timer_; + rclcpp::Node::SharedPtr node_; + std::unordered_map::SharedPtr> + subscribers_{}; + std::unordered_map::SharedPtr> + object_subscribers_{}; + + rclcpp::Subscription::SharedPtr subscription_; + rclcpp::Publisher::SharedPtr publisher_; + + std::vector target_recorded_data; + std::vector object_recorded_data; + std::string selected_topic_; + std::string selected_topic_2_; + bool recording_active_{false}; +}; + +} // namespace smart_rviz_plugin + +#endif // SMART_RVIZ_PLUGIN__SMART_RECORDER_HPP_ diff --git a/smart_rviz_plugin/include/smart_rviz_plugin/smart_services.hpp b/smart_rviz_plugin/include/smart_rviz_plugin/smart_services.hpp new file mode 100644 index 0000000..0c484ee --- /dev/null +++ b/smart_rviz_plugin/include/smart_rviz_plugin/smart_services.hpp @@ -0,0 +1,160 @@ +#ifndef SMART_RVIZ_PLUGIN__SMART_SERVICES_HPP_ +#define SMART_RVIZ_PLUGIN__SMART_SERVICES_HPP_ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "umrr_ros2_msgs/srv/send_command.hpp" +#include "umrr_ros2_msgs/srv/set_mode.hpp" +#include "umrr_ros2_msgs/srv/get_status.hpp" +#include "umrr_ros2_msgs/srv/get_mode.hpp" + +namespace smart_rviz_plugin +{ +/// +/// @brief The class for the sending instructions to the sensors. +/// +/// This class provides a graphical user interface (GUI) panel for sending +/// instructions to the sensor within the RViz environment. It extends the +/// rviz_common::Panel class and includes functionalities for viewing, recording +/// sensor data and sending instructions to the sensor. +/// +class SmartRadarService : public rviz_common::Panel +{ + Q_OBJECT + +public: + /// @brief Constructor for the SmartRadarService class. + /// @param parent The parent widget. Defaults to nullptr. + explicit SmartRadarService(QWidget * parent = nullptr); + +private slots: + /// @brief Slot function to send a parameter instruction to the sensor. + void on_send_param(); + + /// @brief Slot function to send a command instruction to the sensor. + void on_send_command(); + + /// @brief Slot function to send a status instruction to the sensor. + void on_get_status(); + + /// @brief Slot function to display instruction set of selected sensor model. + /// @param index The index of the selected file in the combo box. + void on_file_selected(int index); + + /// @brief Slot function to handle param selection from the table. + void on_param_selection(); + + /// @brief Slot function to handle command selection from the table. + void on_command_selection(); + + /// @brief Slot function to handle status selection from the table. + void on_status_selection(); + +private: + /// @brief Initializes the panel's components. + void initialize(); + + /// @brief Initializes the ROS2 client. + void setup_ros_clients(); + + /// @brief Create widgets for the plugin. + void create_widgets(); + + /// @brief Create layout. + void setup_layout(); + + /// @brief Set up the connections based on activity. + void setup_connections(); + + /// @brief Read parameters from user interface json file. + void read_param_json_data(); + + /// @brief Read commands from user interface json file. + void read_command_json_data(); + + /// @brief Read statuses from user interface json file. + void read_status_json_data(); + + /// @brief Populates the table with the available user interface files. + void populate_file_menu(); + + // File paths + const QString current_directory = QDir::currentPath(); + QString param_json_file_path; + QString command_json_file_path; + QString status_json_file_path; + + // Layout components + QVBoxLayout * main_layout{nullptr}; + QTabWidget * tab_widget{nullptr}; + QWidget * param_tab{nullptr}; + QWidget * command_tab{nullptr}; + QWidget * status_tab{nullptr}; + + // Parameter input fields + QLineEdit * param_name_line_edit{nullptr}; + QLineEdit * param_value_line_edit{nullptr}; + QLineEdit * param_sensor_id{nullptr}; + QLineEdit * param_section_name{nullptr}; + + // Command input fields + QLineEdit * command_name_line_edit{nullptr}; + //QLineEdit * command_comment_line_edit{nullptr}; + QLineEdit * command_sensor_id{nullptr}; + QLineEdit * command_value_line_edit{nullptr}; + QLineEdit * command_section_name{nullptr}; + + // Status input fields + QLineEdit * status_name_line_edit{nullptr}; + QLineEdit * status_sensor_id{nullptr}; + QLineEdit * status_section_name{nullptr}; + + // Buttons + QPushButton * send_param_button{nullptr}; + QPushButton * send_command_button{nullptr}; + QPushButton * send_status_button{nullptr}; + + // Tables and combo boxes + QTableWidget * param_table_widget{nullptr}; + QTableWidget * command_table_widget{nullptr}; + QTableWidget * status_table_widget{nullptr}; + QTextEdit * response_text_edit{nullptr}; + QComboBox * file_selector_combo_box{nullptr}; + QComboBox * param_value_type{nullptr}; + QComboBox * status_value_type{nullptr}; + QComboBox * param_action_combo{nullptr}; + + // Forms + QFormLayout * param_form{nullptr}; + QFormLayout * command_form{nullptr}; + QFormLayout * status_form{nullptr}; + + // ROS2 components + rclcpp::Node::SharedPtr client_node; + rclcpp::Client::SharedPtr mode_client; + rclcpp::Client::SharedPtr command_client; + rclcpp::Client::SharedPtr status_client; + rclcpp::Client::SharedPtr get_param_client; + + // Constants + static constexpr auto SERVICE_AVAILABILITY_TIMEOUT = std::chrono::seconds(2); +}; +} // namespace smart_rviz_plugin + +#endif // SMART_RVIZ_PLUGIN__SMART_SERVICES_HPP_ diff --git a/smart_rviz_plugin/include/smart_rviz_plugin/smart_status.hpp b/smart_rviz_plugin/include/smart_rviz_plugin/smart_status.hpp new file mode 100644 index 0000000..4ab2e1b --- /dev/null +++ b/smart_rviz_plugin/include/smart_rviz_plugin/smart_status.hpp @@ -0,0 +1,127 @@ +#ifndef SMART_RVIZ_PLUGIN__SMART_STATUS_HPP_ +#define SMART_RVIZ_PLUGIN__SMART_STATUS_HPP_ + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "std_msgs/msg/string.hpp" +#include "umrr_ros2_msgs/msg/can_object_header.hpp" +#include "umrr_ros2_msgs/msg/can_target_header.hpp" +#include "umrr_ros2_msgs/msg/port_object_header.hpp" +#include "umrr_ros2_msgs/msg/port_target_header.hpp" + +namespace smart_rviz_plugin +{ +/// +/// @brief The class for the target and object headers. +/// +/// This class provides a graphical user interface (GUI) panel for viewing +/// the sensor header data within the RViz environment. It extends the rviz_common::Panel +/// class and includes functionalities for selecting, viewing +/// the sensor header data. +/// +class SmartRadarStatus : public rviz_common::Panel +{ + Q_OBJECT + +public: + /// + /// @brief Constructor for the SmartRadarStatus class. + /// + /// @param parent The parent widget. Defaults to nullptr. + /// + SmartRadarStatus(QWidget * parent = nullptr); + +private slots: + /// + /// @brief Slot function to update the table based on the topic selected. + /// + void update_table(); + + /// + /// @brief Slot function to check the data is being published. + /// + void check_data(); + +private: + /// + /// @brief Initializes the panel's components and ROS2 client. + /// + /// This function sets up the GUI elements, initializes the ROS2 node and + /// client for the status, and connects the signals and + /// slots. + /// + void initialize(); + + /// + /// @brief Subscribe to the port target headers publisheb by the smartmicro_radar_node. + /// + void port_targetheader_callback( + const umrr_ros2_msgs::msg::PortTargetHeader::SharedPtr msg, const std::string topic_name); + + /// + /// @brief Subscribe to the can target headers publisheb by the smartmicro_radar_node. + /// + void can_targetheader_callback( + const umrr_ros2_msgs::msg::CanTargetHeader::SharedPtr msg, const std::string topic_name); + + /// + /// @brief Subscribe to the port object headers publisheb by the smartmicro_radar_node. + /// + void port_objectheader_callback( + const umrr_ros2_msgs::msg::PortObjectHeader::SharedPtr msg, const std::string topic_name); + + /// + /// @brief Subscribe to the can object headers publisheb by the smartmicro_radar_node. + /// + void can_objectheader_callback( + const umrr_ros2_msgs::msg::CanObjectHeader::SharedPtr msg, const std::string topic_name); + + QTableWidget * table_data_; + QTableWidget * table_data_2_; + QTableWidget * table_timestamps_; + QSplitter * splitter_; + QComboBox * topic_dropdown_; + QVBoxLayout * gui_layout_; + QPushButton * start_button_; + QPushButton * stop_button_; + QPushButton * save_button_; + QTimer * timer_; + rclcpp::Node::SharedPtr node_; + std::string selected_topic_; + std::unordered_map< + std::string, rclcpp::Subscription::SharedPtr> + port_header_target_subscribers_{}; + std::unordered_map< + std::string, rclcpp::Subscription::SharedPtr> + can_header_target_subscribers_{}; + std::unordered_map< + std::string, rclcpp::Subscription::SharedPtr> + port_header_object_subscribers_{}; + std::unordered_map< + std::string, rclcpp::Subscription::SharedPtr> + can_header_object_subscribers_{}; +}; + +} // namespace smart_rviz_plugin + +#endif // SMART_RVIZ_PLUGIN__SMART_RECORDER_HPP_ diff --git a/smart_rviz_plugin/package.xml b/smart_rviz_plugin/package.xml new file mode 100644 index 0000000..c4266b8 --- /dev/null +++ b/smart_rviz_plugin/package.xml @@ -0,0 +1,38 @@ + + + + smart_rviz_plugin + 3.0.0 + TargetList Recorder + shahrukh + Apache 2.0 License + + ament_cmake + + qtbase5-dev + + pluginlib + rclcpp + umrr_ros2_msgs + resource_retriever + rviz_common + rviz_default_plugins + rviz_ogre_vendor + rviz_rendering + std_msgs + visualization_msgs + + libqt5-core + libqt5-gui + xacro + rviz + libqt5-opengl + libqt5-widgets + + ament_lint_common + ament_lint_auto + + + ament_cmake + + diff --git a/smart_rviz_plugin/plugins_description.xml b/smart_rviz_plugin/plugins_description.xml new file mode 100644 index 0000000..d7a4f89 --- /dev/null +++ b/smart_rviz_plugin/plugins_description.xml @@ -0,0 +1,26 @@ + + + The Smart TargetList Recorder. + + + The Smart Command Controller. + + + The Smart Download. + + + The Smart Status. + + diff --git a/smart_rviz_plugin/src/smart_download.cpp b/smart_rviz_plugin/src/smart_download.cpp new file mode 100644 index 0000000..9104faa --- /dev/null +++ b/smart_rviz_plugin/src/smart_download.cpp @@ -0,0 +1,141 @@ +#include "smart_rviz_plugin/smart_download.hpp" +#include + +namespace smart_rviz_plugin +{ + +SmartDownloadService::SmartDownloadService(QWidget * parent) +: rviz_common::Panel(parent) +{ + initialize_ros(); + setup_ui(); +} + +SmartDownloadService::~SmartDownloadService() +{ + if (executor_) { + executor_->cancel(); + } + if (ros_thread_.joinable()) { + ros_thread_.join(); + } + rclcpp::shutdown(); +} + +void SmartDownloadService::initialize_ros() +{ + if (!rclcpp::ok()) { + rclcpp::init(0, nullptr); + } + + download_node_ = rclcpp::Node::make_shared("smart_download_gui"); + download_client_ = download_node_->create_client( + "smart_radar/firmware_download"); + + executor_ = std::make_shared(); + executor_->add_node(download_node_); + + ros_thread_ = std::thread([this]() { executor_->spin(); }); + + RCLCPP_INFO(download_node_->get_logger(), "SmartDownloadService node initialized."); +} + +void SmartDownloadService::setup_ui() +{ + file_path_input_ = new QLineEdit(this); + sensor_id_input_ = new QLineEdit(this); + start_download_button_ = new QPushButton("Start Download", this); + browse_button_ = new QPushButton("Browse", this); + response_text_edit_ = new QTextEdit(this); + response_text_edit_->setReadOnly(true); + response_text_edit_->setFixedHeight(120); + + // Compact file path layout + auto * file_layout = new QHBoxLayout; + file_layout->addWidget(file_path_input_); + file_layout->addWidget(browse_button_); + + // Main layout + auto * layout = new QVBoxLayout(this); + layout->addWidget(new QLabel("Firmware File Path:")); + layout->addLayout(file_layout); + layout->addWidget(new QLabel("Sensor ID:")); + layout->addWidget(sensor_id_input_); + layout->addWidget(start_download_button_); + layout->addWidget(response_text_edit_); + setLayout(layout); + + // Connect signals and slots + connect(start_download_button_, &QPushButton::clicked, this, &SmartDownloadService::download_firmware); + connect(browse_button_, &QPushButton::clicked, this, &SmartDownloadService::browse_file); +} + +void SmartDownloadService::download_firmware() +{ + if (!download_client_) { + RCLCPP_ERROR(download_node_->get_logger(), "Download client not created."); + return; + } + + QString file_path = file_path_input_->text().trimmed(); + QString sensor_id_str = sensor_id_input_->text().trimmed(); + + if (file_path.isEmpty()) { + QMessageBox::warning(this, "Warning", "Please select a firmware file.", QMessageBox::Ok); + return; + } + if (sensor_id_str.isEmpty() || !sensor_id_str.toInt()) { + QMessageBox::warning(this, "Warning", "Please enter a valid numeric sensor ID.", QMessageBox::Ok); + return; + } + + int sensor_id = sensor_id_str.toInt(); + start_download_button_->setEnabled(false); + start_download_button_->setText("Downloading..."); + + auto request = std::make_shared(); + request->file_path = file_path.toStdString(); + request->sensor_id = sensor_id; + + if (!download_client_->wait_for_service(std::chrono::seconds(2))) { + QMessageBox::critical(this, "Error", "Firmware download service not available."); + start_download_button_->setEnabled(true); + start_download_button_->setText("Start Download"); + return; + } + + auto future = download_client_->async_send_request(request); + + // Async callback, do non block rviz + std::thread([this, future]() mutable { + try { + auto result = future.get(); + QString response_msg = QString::fromStdString(result->res); + QMetaObject::invokeMethod(this, [this, response_msg]() { + response_text_edit_->append("Response: " + response_msg); + start_download_button_->setEnabled(true); + start_download_button_->setText("Start Download"); + }); + RCLCPP_INFO(download_node_->get_logger(), "Firmware download succeeded: %s", result->res.c_str()); + } catch (const std::exception & e) { + QMetaObject::invokeMethod(this, [this, e]() { + response_text_edit_->append(QString("Error: %1").arg(e.what())); + start_download_button_->setEnabled(true); + start_download_button_->setText("Start Download"); + }); + RCLCPP_ERROR(download_node_->get_logger(), "Firmware download failed: %s", e.what()); + } + }).detach(); +} + +void SmartDownloadService::browse_file() +{ + QString file_path = QFileDialog::getOpenFileName(this, tr("Select Firmware File"), "", tr("All Files (*)")); + if (!file_path.isEmpty()) { + file_path_input_->setText(file_path); + } +} + +} // namespace smart_rviz_plugin + +PLUGINLIB_EXPORT_CLASS(smart_rviz_plugin::SmartDownloadService, rviz_common::Panel) diff --git a/smart_rviz_plugin/src/smart_recorder.cpp b/smart_rviz_plugin/src/smart_recorder.cpp new file mode 100644 index 0000000..2b8acbf --- /dev/null +++ b/smart_rviz_plugin/src/smart_recorder.cpp @@ -0,0 +1,571 @@ +#include "smart_rviz_plugin/smart_recorder.hpp" + +#include +#include +#include + +const double radToDeg = 180.0 / M_PI; +namespace smart_rviz_plugin +{ +SmartRadarRecorder::SmartRadarRecorder(QWidget * parent) : rviz_common::Panel(parent) +{ + initialize(); +} + +void SmartRadarRecorder::initialize() +{ + node_ = rclcpp::Node::make_shared("smart_radar_gui_node"); + + subscription_ = node_->create_subscription( + "/ip_camera_front_right/image_raw/compressed", 10, + [this](const sensor_msgs::msg::CompressedImage::SharedPtr msg) { image_callback(msg); }); + + publisher_ = + node_->create_publisher("/ip_camera_front_right/image_raw", 10); + + // Reorder setup + gui_layout_ = new QVBoxLayout(); + topic_dropdown_ = new QComboBox(); + topic_dropdown_->addItem("Select a Topic"); + + // Retrieve available topics + auto topic_names_and_types = node_->get_topic_names_and_types(); + + // Create subscribers for selected topics + for (const auto & topic : topic_names_and_types) { + if (topic.first.find("port_targets") != std::string::npos) { + subscribers_[topic.first] = node_->create_subscription( + topic.first, 10, [this, topic](const sensor_msgs::msg::PointCloud2::SharedPtr msg) { + port_target_callback(msg, topic.first); + }); + + topic_dropdown_->addItem(QString::fromStdString(topic.first)); + } else if (topic.first.find("can_targets") != std::string::npos) { + subscribers_[topic.first] = node_->create_subscription( + topic.first, 10, [this, topic](const sensor_msgs::msg::PointCloud2::SharedPtr msg) { + can_target_callback(msg, topic.first); + }); + + topic_dropdown_->addItem(QString::fromStdString(topic.first)); + } else if (topic.first.find("port_objects") != std::string::npos) { + object_subscribers_[topic.first] = node_->create_subscription( + topic.first, 10, [this, topic](const sensor_msgs::msg::PointCloud2::SharedPtr msg) { + port_object_callback(msg, topic.first); + }); + + topic_dropdown_->addItem(QString::fromStdString(topic.first)); + } else if (topic.first.find("can_objects") != std::string::npos) { + object_subscribers_[topic.first] = node_->create_subscription( + topic.first, 10, [this, topic](const sensor_msgs::msg::PointCloud2::SharedPtr msg) { + can_object_callback(msg, topic.first); + }); + + topic_dropdown_->addItem(QString::fromStdString(topic.first)); + } + } + + connect(topic_dropdown_, SIGNAL(currentIndexChanged(int)), this, SLOT(update_table())); + + // Table one layout + table_data_ = new QTableWidget(); + table_data_->setColumnCount(13); + table_data_->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); + table_data_->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch); + + table_timestamps_ = new QTableWidget(); + table_timestamps_->setColumnCount(2); + table_timestamps_->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch); + table_timestamps_->setHorizontalHeaderLabels({"TsSec", "TsNanoSec"}); + + splitter_ = new QSplitter(Qt::Vertical); + splitter_->addWidget(topic_dropdown_); + splitter_->addWidget(table_data_); + + horiz_splitter_ = new QSplitter(Qt::Horizontal); + horiz_splitter_->addWidget(splitter_); + horiz_splitter_->addWidget(table_timestamps_); + horiz_splitter_->setSizes(QList({400, 100})); + + gui_layout_->addWidget(horiz_splitter_); + + timer_ = new QTimer(); + connect(timer_, SIGNAL(timeout()), this, SLOT(check_data())); + timer_->start(20); + + start_button_ = new QPushButton("Record"); + connect(start_button_, SIGNAL(clicked()), this, SLOT(start_recording())); + + stop_button_ = new QPushButton("Stop Recording"); + connect(stop_button_, SIGNAL(clicked()), this, SLOT(stop_recording())); + + save_button_ = new QPushButton("Save Data as CSV"); + connect(save_button_, SIGNAL(clicked()), this, SLOT(save_data())); + + gui_layout_->addWidget(start_button_); + gui_layout_->addWidget(stop_button_); + gui_layout_->addWidget(save_button_); + + setLayout(gui_layout_); + + RCLCPP_INFO(rclcpp::get_logger("rclcpp"), "Recorder Plugin Created!"); +} + +void SmartRadarRecorder::image_callback(const sensor_msgs::msg::CompressedImage::SharedPtr msg) +{ + cv_bridge::CvImagePtr cv_ptr; + try { + cv_ptr = cv_bridge::toCvCopy(msg, sensor_msgs::image_encodings::BGR8); + } catch (cv_bridge::Exception & e) { + RCLCPP_ERROR(rclcpp::get_logger("rclcpp"), "cv_bridge exception: %s", e.what()); + return; + } + + sensor_msgs::msg::Image ros_image; + ros_image.header = msg->header; + ros_image.height = cv_ptr->image.rows; + ros_image.width = cv_ptr->image.cols; + ros_image.encoding = sensor_msgs::image_encodings::BGR8; + ros_image.is_bigendian = false; + ros_image.step = sizeof(unsigned char) * cv_ptr->image.cols * cv_ptr->image.channels(); + ros_image.data.assign( + cv_ptr->image.data, cv_ptr->image.data + cv_ptr->image.total() * cv_ptr->image.elemSize()); + + publisher_->publish(ros_image); +} + +void SmartRadarRecorder::update_target_recorded_data( + float range, float power, float azimuth_deg, float elevation_deg, float rcs, float noise, + float snr, float radial_speed, float azimuth_angle, float elevation_angle, uint32_t timestamp_sec, + uint32_t timestamp_nanosec) +{ + TargetData data; + data.range = range; + data.power = power; + data.azimuth_deg = azimuth_deg; + data.elevation_deg = elevation_deg; + data.rcs = rcs; + data.noise = noise; + data.snr = snr; + data.radial_speed = radial_speed; + data.azimuth_angle = azimuth_angle; + data.elevation_angle = elevation_angle; + data.timestamp_sec = timestamp_sec; + data.timestamp_nanosec = timestamp_nanosec; + + target_recorded_data.push_back(data); +} + +void SmartRadarRecorder::update_object_recorded_data( + float x_pos, float y_pos, float z_pos, float speed_abs, float heading, float length, + float quality, float acceleration, uint16_t object_id, uint32_t timestamp_sec, + uint32_t timestamp_nanosec) +{ + ObjectData data; + data.x_pos = x_pos; + data.y_pos = y_pos; + data.z_pos = z_pos; + data.speed_abs = speed_abs; + data.heading = heading; + data.length = length; + data.quality = quality; + data.object_id = object_id; + data.acceleration = acceleration; + data.timestamp_sec = timestamp_sec; + data.timestamp_nanosec = timestamp_nanosec; + + object_recorded_data.push_back(data); +} + +void SmartRadarRecorder::port_target_callback( + const sensor_msgs::msg::PointCloud2::SharedPtr msg, const std::string topic_name) +{ + if (!selected_topic_.empty() && topic_name == selected_topic_) { + auto timestamp_sec = msg->header.stamp.sec; + auto timestamp_nanosec = msg->header.stamp.nanosec; + + // Create iterators for the pc2 fields + sensor_msgs::PointCloud2ConstIterator iter_x(*msg, "x"); + sensor_msgs::PointCloud2ConstIterator iter_y(*msg, "y"); + sensor_msgs::PointCloud2ConstIterator iter_z(*msg, "z"); + sensor_msgs::PointCloud2ConstIterator iter_radial_speed(*msg, "radial_speed"); + sensor_msgs::PointCloud2ConstIterator iter_power(*msg, "power"); + sensor_msgs::PointCloud2ConstIterator iter_rcs(*msg, "rcs"); + sensor_msgs::PointCloud2ConstIterator iter_noise(*msg, "noise"); + sensor_msgs::PointCloud2ConstIterator iter_snr(*msg, "snr"); + sensor_msgs::PointCloud2ConstIterator iter_azimuth_angle(*msg, "azimuth_angle"); + sensor_msgs::PointCloud2ConstIterator iter_elevation_angle(*msg, "elevation_angle"); + sensor_msgs::PointCloud2ConstIterator iter_range(*msg, "range"); + + table_data_->setRowCount(0); + + for (size_t i = 0; i < msg->height * msg->width; ++i, ++iter_x, ++iter_y, ++iter_z, + ++iter_radial_speed, ++iter_power, ++iter_rcs, ++iter_noise, ++iter_snr, + ++iter_azimuth_angle, ++iter_elevation_angle, ++iter_range) { + double azimuth_deg = *iter_azimuth_angle * radToDeg; + double elevation_deg = *iter_elevation_angle * radToDeg; + + // Update the recorded data + if (recording_active_) { + update_target_recorded_data( + *iter_range, *iter_power, azimuth_deg, elevation_deg, *iter_rcs, *iter_noise, *iter_snr, + *iter_radial_speed, *iter_azimuth_angle, *iter_elevation_angle, timestamp_sec, + timestamp_nanosec); + } + + // Add items to the table + int row_index = table_data_->rowCount(); + table_data_->insertRow(row_index); + table_data_->setItem(row_index, 0, new QTableWidgetItem(QString::number(*iter_x, 'f', 2))); + table_data_->setItem(row_index, 1, new QTableWidgetItem(QString::number(*iter_y, 'f', 2))); + table_data_->setItem(row_index, 2, new QTableWidgetItem(QString::number(*iter_z, 'f', 2))); + table_data_->setItem( + row_index, 3, new QTableWidgetItem(QString::number(*iter_radial_speed, 'f', 2))); + table_data_->setItem( + row_index, 4, new QTableWidgetItem(QString::number(*iter_power, 'f', 2))); + table_data_->setItem(row_index, 5, new QTableWidgetItem(QString::number(*iter_rcs, 'f', 2))); + table_data_->setItem( + row_index, 6, new QTableWidgetItem(QString::number(*iter_noise, 'f', 2))); + table_data_->setItem(row_index, 7, new QTableWidgetItem(QString::number(*iter_snr, 'f', 2))); + table_data_->setItem( + row_index, 8, new QTableWidgetItem(QString::number(azimuth_deg, 'f', 2))); + table_data_->setItem( + row_index, 9, new QTableWidgetItem(QString::number(elevation_deg, 'f', 2))); + table_data_->setItem( + row_index, 10, new QTableWidgetItem(QString::number(*iter_range, 'f', 2))); + table_data_->setItem( + row_index, 11, new QTableWidgetItem(QString::number(*iter_azimuth_angle, 'f', 2))); + table_data_->setItem( + row_index, 12, new QTableWidgetItem(QString::number(*iter_elevation_angle, 'f', 2))); + } + + // Update the timestamp table + table_timestamps_->setRowCount(0); + table_timestamps_->insertRow(0); + table_timestamps_->setItem(0, 0, new QTableWidgetItem(QString::number(timestamp_sec))); + table_timestamps_->setItem(0, 1, new QTableWidgetItem(QString::number(timestamp_nanosec))); + } +} + +void SmartRadarRecorder::can_target_callback( + const sensor_msgs::msg::PointCloud2::SharedPtr msg, const std::string topic_name) +{ + if (!selected_topic_.empty() && topic_name == selected_topic_) { + auto timestamp_sec = msg->header.stamp.sec; + auto timestamp_nanosec = msg->header.stamp.nanosec; + + // Create iterators for the pc2 fields + sensor_msgs::PointCloud2ConstIterator iter_x(*msg, "x"); + sensor_msgs::PointCloud2ConstIterator iter_y(*msg, "y"); + sensor_msgs::PointCloud2ConstIterator iter_z(*msg, "z"); + sensor_msgs::PointCloud2ConstIterator iter_radial_speed(*msg, "radial_speed"); + sensor_msgs::PointCloud2ConstIterator iter_power(*msg, "power"); + sensor_msgs::PointCloud2ConstIterator iter_rcs(*msg, "rcs"); + sensor_msgs::PointCloud2ConstIterator iter_noise(*msg, "noise"); + sensor_msgs::PointCloud2ConstIterator iter_snr(*msg, "snr"); + sensor_msgs::PointCloud2ConstIterator iter_azimuth_angle(*msg, "azimuth_angle"); + sensor_msgs::PointCloud2ConstIterator iter_elevation_angle(*msg, "elevation_angle"); + sensor_msgs::PointCloud2ConstIterator iter_range(*msg, "range"); + + table_data_->setRowCount(0); + + for (size_t i = 0; i < msg->height * msg->width; ++i, ++iter_x, ++iter_y, ++iter_z, + ++iter_radial_speed, ++iter_power, ++iter_rcs, ++iter_noise, ++iter_snr, + ++iter_azimuth_angle, ++iter_elevation_angle, ++iter_range) { + double azimuth_deg = *iter_azimuth_angle * radToDeg; + double elevation_deg = *iter_elevation_angle * radToDeg; + + // Update the recorded data + if (recording_active_) { + update_target_recorded_data( + *iter_range, *iter_power, azimuth_deg, elevation_deg, *iter_rcs, *iter_noise, *iter_snr, + *iter_radial_speed, *iter_azimuth_angle, *iter_elevation_angle, timestamp_sec, + timestamp_nanosec); + } + + // Add items to the table + int row_index = table_data_->rowCount(); + table_data_->insertRow(row_index); + table_data_->setItem(row_index, 0, new QTableWidgetItem(QString::number(*iter_x, 'f', 2))); + table_data_->setItem(row_index, 1, new QTableWidgetItem(QString::number(*iter_y, 'f', 2))); + table_data_->setItem(row_index, 2, new QTableWidgetItem(QString::number(*iter_z, 'f', 2))); + table_data_->setItem( + row_index, 3, new QTableWidgetItem(QString::number(*iter_radial_speed, 'f', 2))); + table_data_->setItem( + row_index, 4, new QTableWidgetItem(QString::number(*iter_power, 'f', 2))); + table_data_->setItem(row_index, 5, new QTableWidgetItem(QString::number(*iter_rcs, 'f', 2))); + table_data_->setItem( + row_index, 6, new QTableWidgetItem(QString::number(*iter_noise, 'f', 2))); + table_data_->setItem(row_index, 7, new QTableWidgetItem(QString::number(*iter_snr, 'f', 2))); + table_data_->setItem( + row_index, 8, new QTableWidgetItem(QString::number(azimuth_deg, 'f', 2))); + table_data_->setItem( + row_index, 9, new QTableWidgetItem(QString::number(elevation_deg, 'f', 2))); + table_data_->setItem( + row_index, 10, new QTableWidgetItem(QString::number(*iter_range, 'f', 2))); + table_data_->setItem( + row_index, 11, new QTableWidgetItem(QString::number(*iter_azimuth_angle, 'f', 2))); + table_data_->setItem( + row_index, 12, new QTableWidgetItem(QString::number(*iter_elevation_angle, 'f', 2))); + } + + // Update the timestamp table + table_timestamps_->setRowCount(0); + table_timestamps_->insertRow(0); + table_timestamps_->setItem(0, 0, new QTableWidgetItem(QString::number(timestamp_sec))); + table_timestamps_->setItem(0, 1, new QTableWidgetItem(QString::number(timestamp_nanosec))); + // } + } +} + +void SmartRadarRecorder::port_object_callback( + const sensor_msgs::msg::PointCloud2::SharedPtr msg, const std::string topic_name) +{ + if (!selected_topic_.empty() && topic_name == selected_topic_) { + auto timestamp_sec = msg->header.stamp.sec; + auto timestamp_nanosec = msg->header.stamp.nanosec; + + // Create iterators for the pc2 fields + sensor_msgs::PointCloud2ConstIterator iter_x(*msg, "x"); + sensor_msgs::PointCloud2ConstIterator iter_y(*msg, "y"); + sensor_msgs::PointCloud2ConstIterator iter_z(*msg, "z"); + sensor_msgs::PointCloud2ConstIterator iter_speed_absolute(*msg, "speed_absolute"); + sensor_msgs::PointCloud2ConstIterator iter_heading(*msg, "heading"); + sensor_msgs::PointCloud2ConstIterator iter_length(*msg, "length"); + sensor_msgs::PointCloud2ConstIterator iter_mileage(*msg, "mileage"); + sensor_msgs::PointCloud2ConstIterator iter_quality(*msg, "quality"); + sensor_msgs::PointCloud2ConstIterator iter_acceleration(*msg, "acceleration"); + sensor_msgs::PointCloud2ConstIterator iter_object_id(*msg, "object_id"); + sensor_msgs::PointCloud2ConstIterator iter_idle_cycles(*msg, "idle_cycles"); + sensor_msgs::PointCloud2ConstIterator iter_spline_idx(*msg, "spline_idx"); + sensor_msgs::PointCloud2ConstIterator iter_object_class(*msg, "object_class"); + sensor_msgs::PointCloud2ConstIterator iter_status(*msg, "status"); + + table_data_->setRowCount(0); + size_t num_points = msg->height * msg->width; + + for (size_t i = num_points; i > 0; --i, ++iter_x, ++iter_y, ++iter_z, ++iter_speed_absolute, + ++iter_heading, ++iter_length, ++iter_mileage, ++iter_quality, ++iter_acceleration, + ++iter_object_id, ++iter_idle_cycles, ++iter_spline_idx, ++iter_object_class, + ++iter_status) { + double heading_deg = *iter_heading * radToDeg; + + // Update the recorded data + if (recording_active_) { + update_object_recorded_data( + *iter_x, *iter_y, *iter_z, *iter_speed_absolute, *iter_heading, *iter_length, + *iter_quality, *iter_acceleration, *iter_object_id, timestamp_sec, timestamp_nanosec); + } + + // Add items to the table + int row_index = table_data_->rowCount(); + table_data_->insertRow(row_index); + table_data_->setItem(row_index, 0, new QTableWidgetItem(QString::number(*iter_x, 'f', 2))); + table_data_->setItem(row_index, 1, new QTableWidgetItem(QString::number(*iter_y, 'f', 2))); + table_data_->setItem(row_index, 2, new QTableWidgetItem(QString::number(*iter_z, 'f', 2))); + table_data_->setItem( + row_index, 3, new QTableWidgetItem(QString::number(*iter_speed_absolute, 'f', 2))); + table_data_->setItem( + row_index, 4, new QTableWidgetItem(QString::number(heading_deg, 'f', 2))); + table_data_->setItem( + row_index, 5, new QTableWidgetItem(QString::number(*iter_length, 'f', 2))); + table_data_->setItem( + row_index, 6, new QTableWidgetItem(QString::number(*iter_quality, 'f', 2))); + table_data_->setItem( + row_index, 7, new QTableWidgetItem(QString::number(*iter_acceleration, 'f', 2))); + table_data_->setItem(row_index, 8, new QTableWidgetItem(QString::number(*iter_object_id))); + } + + // Update the timestamp table + table_timestamps_->setRowCount(0); + table_timestamps_->insertRow(0); + table_timestamps_->setItem(0, 0, new QTableWidgetItem(QString::number(timestamp_sec))); + table_timestamps_->setItem(0, 1, new QTableWidgetItem(QString::number(timestamp_nanosec))); + } +} + +void SmartRadarRecorder::can_object_callback( + const sensor_msgs::msg::PointCloud2::SharedPtr msg, const std::string topic_name) +{ + if (!selected_topic_.empty() && topic_name == selected_topic_) { + auto timestamp_sec = msg->header.stamp.sec; + auto timestamp_nanosec = msg->header.stamp.nanosec; + + // Create iterators for the pc2 fields + sensor_msgs::PointCloud2ConstIterator iter_x(*msg, "x"); + sensor_msgs::PointCloud2ConstIterator iter_y(*msg, "y"); + sensor_msgs::PointCloud2ConstIterator iter_z(*msg, "z"); + sensor_msgs::PointCloud2ConstIterator iter_speed_abs(*msg, "speed_absolute"); + sensor_msgs::PointCloud2ConstIterator iter_heading(*msg, "heading"); + sensor_msgs::PointCloud2ConstIterator iter_length(*msg, "length"); + sensor_msgs::PointCloud2ConstIterator iter_quality(*msg, "quality"); + sensor_msgs::PointCloud2ConstIterator iter_acceleration(*msg, "acceleration"); + sensor_msgs::PointCloud2ConstIterator iter_object_id(*msg, "object_id"); + sensor_msgs::PointCloud2ConstIterator iter_status(*msg, "status"); + + table_data_->setRowCount(0); + + size_t num_points = msg->height * msg->width; + + for (size_t i = num_points; i > 0; --i, ++iter_x, ++iter_y, ++iter_z, ++iter_speed_abs, + ++iter_heading, ++iter_length, ++iter_quality, ++iter_acceleration, + ++iter_object_id) { + // Update the recorded data + if (recording_active_) { + update_object_recorded_data( + *iter_x, *iter_y, *iter_z, *iter_speed_abs, *iter_heading, *iter_length, *iter_quality, + *iter_acceleration, *iter_object_id, timestamp_sec, timestamp_nanosec); + } + + int row_index = table_data_->rowCount(); + table_data_->insertRow(row_index); + table_data_->setItem(row_index, 0, new QTableWidgetItem(QString::number(*iter_x, 'f', 2))); + table_data_->setItem(row_index, 1, new QTableWidgetItem(QString::number(*iter_y, 'f', 2))); + table_data_->setItem(row_index, 2, new QTableWidgetItem(QString::number(*iter_z, 'f', 2))); + table_data_->setItem( + row_index, 3, new QTableWidgetItem(QString::number(*iter_speed_abs, 'f', 2))); + table_data_->setItem( + row_index, 4, new QTableWidgetItem(QString::number(*iter_heading, 'f', 2))); + table_data_->setItem( + row_index, 5, new QTableWidgetItem(QString::number(*iter_length, 'f', 2))); + table_data_->setItem( + row_index, 6, new QTableWidgetItem(QString::number(*iter_quality, 'f', 2))); + table_data_->setItem( + row_index, 7, new QTableWidgetItem(QString::number(*iter_acceleration, 'f', 2))); + table_data_->setItem( + row_index, 8, new QTableWidgetItem(QString::number(*iter_object_id, 'f', 2))); + } + + // Update the timestamp table + table_timestamps_->setRowCount(0); + table_timestamps_->insertRow(0); + table_timestamps_->setItem(0, 0, new QTableWidgetItem(QString::number(timestamp_sec))); + table_timestamps_->setItem(0, 1, new QTableWidgetItem(QString::number(timestamp_nanosec))); + } +} + +void SmartRadarRecorder::update_table() +{ + table_data_->setRowCount(0); + selected_topic_ = topic_dropdown_->currentText().toStdString(); + if (selected_topic_.find("port_targets") != std::string::npos) { + table_data_->setHorizontalHeaderLabels( + {"X_pos [m]", "Y_pos [m]", "Z_pos [m]", "RadialSpeed [m/s]", "Power [dB]", "RCS [m^2]", + "Noise [dB]", "SNR [dB]", "AzimuthAngle [Deg]", "ElevationAngle [Deg]", "Range [m]", + "AzimuthAngle [rad]", "ElevationAngle [rad]"}); + } else if (selected_topic_.find("can_targets") != std::string::npos) { + table_data_->setRowCount(0); + table_data_->setHorizontalHeaderLabels( + {"X_pos [m]", "Y_pos [m]", "Z_pos [m]", "RadialSpeed [m/s]", "Power [dB]", "RCS [dB]", + "Noise [dB]", "SNR [dB]", "AzimuthAngle [Deg]", "ElevationAngle [Deg]", "Range [m]", + "AzimuthAngle [rad]", "ElevationAngle [rad]"}); + } else if (selected_topic_.find("can_objects") != std::string::npos) { + table_data_->setRowCount(0); + table_data_->setHorizontalHeaderLabels( + {"X_pos [m]", "Y_pos [m]", "Z_pos [m]", "AbsoluteSpeed [m/s]", "Heading [Deg]", + "ObjectLength [m]", "Quality", "Acceleration [m/s^2]", "Object_ID", "", "", "", ""}); + } else if (selected_topic_.find("port_objects") != std::string::npos) { + table_data_->setRowCount(0); + table_data_->setHorizontalHeaderLabels( + {"PosX [m]", "PosY [m]", "PosZ [m]", "AbsoluteSpeed [m/s]", "Heading [Deg]", + "ObjectLength [m]", "Quality", "Acceleration [m/s^2]", "ObjectId", "", "", "", ""}); + } +} + +void SmartRadarRecorder::start_recording() +{ + qDebug() << "Recording started!"; + recording_active_ = true; + start_button_->setText("Recording..."); +} + +void SmartRadarRecorder::stop_recording() +{ + qDebug() << "Recording stopped!"; + recording_active_ = false; + start_button_->setText("Record"); +} + +void SmartRadarRecorder::save_data() +{ + qDebug() << "Saving data to CSV!"; + if (!target_recorded_data.empty() || !object_recorded_data.empty()) { + QFileDialog file_dialog; + QString file_path = + file_dialog.getSaveFileName(this, "Save Data", "", "CSV Files (*.csv);;All Files (*)"); + + if (!file_path.isEmpty()) { + QFile csvfile(file_path); + if (csvfile.open(QIODevice::WriteOnly | QIODevice::Text)) { + QTextStream csv_writer(&csvfile); + csv_writer + << "Type, Range [m], Power [dB], AzimuthAngle [Deg], ElevationAngle [Deg], RCS [dB], " + "Noise [dB], SNR [dB], " + "RadialSpeed [m/s], ElevationAngle [rad], AzimuthAngle [rad], " + "TimestampSec, TimestampNanoSec\n"; + + for (const auto & data_row : target_recorded_data) { + QStringList data_str_list; + data_str_list << "Target"; + data_str_list << QString::number(data_row.range, 'f', 2); + data_str_list << QString::number(data_row.power, 'f', 2); + data_str_list << QString::number(data_row.azimuth_angle * 180.0 / M_PI, 'f', 2); + data_str_list << QString::number(data_row.elevation_angle * 180.0 / M_PI, 'f', 2); + data_str_list << QString::number(data_row.rcs, 'f', 2); + data_str_list << QString::number(data_row.noise, 'f', 2); + data_str_list << QString::number(data_row.snr, 'f', 2); + data_str_list << QString::number(data_row.radial_speed, 'f', 2); + data_str_list << QString::number(data_row.azimuth_angle, 'f', 2); + data_str_list << QString::number(data_row.elevation_angle, 'f', 2); + data_str_list << QString::number(data_row.timestamp_sec); + data_str_list << QString::number(data_row.timestamp_nanosec); + + csv_writer << data_str_list.join(", ") << "\n"; + } + + csv_writer << "Type, PosX [m], PosY [m], PosZ [m], AbsoluteSpeed [m/s], Heading [Deg], " + "ObjectLength [m], Quality, Acceleration [m/s^2], ObjectId, " + "TimestampSec, TimestampNanoSec\n"; + // Write object data + for (const auto & object : object_recorded_data) { + QStringList data_str_list; + data_str_list << "Object"; + data_str_list << QString::number(object.x_pos, 'f', 2); + data_str_list << QString::number(object.y_pos, 'f', 2); + data_str_list << QString::number(object.z_pos, 'f', 2); + data_str_list << QString::number(object.speed_abs, 'f', 2); + data_str_list << QString::number(object.heading * 180.0 / M_PI, 'f', 2); + data_str_list << QString::number(object.length, 'f', 2); + data_str_list << QString::number(object.quality, 'f', 2); + data_str_list << QString::number(object.acceleration, 'f', 2); + data_str_list << QString::number(object.object_id); + data_str_list << QString::number(object.timestamp_sec); + data_str_list << QString::number(object.timestamp_nanosec); + + csv_writer << data_str_list.join(", ") << "\n"; + } + + csvfile.close(); + } else { + qDebug() << "Error: Could not open the file for writing."; + } + } + } else { + qDebug() << "No recorded data to save."; + } + // Clear recorded_data after saving + target_recorded_data.clear(); + object_recorded_data.clear(); +} + +void SmartRadarRecorder::check_data() +{ + if (rclcpp::ok()) // Check if ROS2 is still running + { + rclcpp::spin_some(node_); + } +} + +} // namespace smart_rviz_plugin + +#include +PLUGINLIB_EXPORT_CLASS(smart_rviz_plugin::SmartRadarRecorder, rviz_common::Panel) \ No newline at end of file diff --git a/smart_rviz_plugin/src/smart_services.cpp b/smart_rviz_plugin/src/smart_services.cpp new file mode 100644 index 0000000..05522f4 --- /dev/null +++ b/smart_rviz_plugin/src/smart_services.cpp @@ -0,0 +1,664 @@ +#include "smart_rviz_plugin/smart_services.hpp" + +#include +#include +#include + +namespace smart_rviz_plugin +{ +SmartRadarService::SmartRadarService(QWidget * parent) : rviz_common::Panel(parent) +{ + initialize(); +} + +void SmartRadarService::initialize() +{ + // Initialize ROS 2 + if (!rclcpp::ok()) { + rclcpp::init(0, nullptr); + } + + setup_ros_clients(); + create_widgets(); + setup_layout(); + setup_connections(); +} + +void SmartRadarService::setup_ros_clients() +{ + client_node = rclcpp::Node::make_shared("smart_service_gui"); + mode_client = client_node->create_client("smart_radar/set_radar_mode"); + command_client = client_node->create_client("smart_radar/send_command"); + status_client = client_node->create_client("/smart_radar/get_radar_status"); + get_param_client = client_node->create_client("smart_radar/get_radar_mode"); + + RCLCPP_INFO(rclcpp::get_logger("rclcpp"), "Client node created!"); +} + +void SmartRadarService::create_widgets() +{ + // Parameter tab widget + param_name_line_edit = new QLineEdit(this); + param_name_line_edit->setPlaceholderText("Enter parameter name"); + + param_value_line_edit = new QLineEdit(this); + param_value_line_edit->setPlaceholderText("Enter value"); + + param_sensor_id = new QLineEdit(this); + param_sensor_id->setPlaceholderText("Enter sensor ID"); + + param_section_name = new QLineEdit(this); + param_section_name->setPlaceholderText("Enter param section name"); + + param_action_combo = new QComboBox(this); + param_action_combo->addItem("Write Parameter"); + param_action_combo->addItem("Read Parameter"); + + param_value_type = new QComboBox(this); + param_value_type->addItem("float32, (0)"); + param_value_type->addItem("uint32, (1)"); + param_value_type->addItem("uint16, (2)"); + param_value_type->addItem("uint8, (3)"); + + send_param_button = new QPushButton("Send Parameter", this); + param_table_widget = new QTableWidget(this); + param_table_widget->setSelectionBehavior(QAbstractItemView::SelectRows); + param_table_widget->setSelectionMode(QAbstractItemView::SingleSelection); + + // Command tab widgets + command_name_line_edit = new QLineEdit(this); + command_name_line_edit->setPlaceholderText("Enter command name"); + + command_value_line_edit = new QLineEdit(this); + command_value_line_edit->setPlaceholderText("Enter command value"); + + command_sensor_id = new QLineEdit(this); + command_sensor_id->setPlaceholderText("Enter sensor ID"); + + command_section_name = new QLineEdit(this); + command_section_name->setPlaceholderText("Enter command sectionn name"); + + send_command_button = new QPushButton("Send Command", this); + command_table_widget = new QTableWidget(this); + command_table_widget->setSelectionBehavior(QAbstractItemView::SelectRows); + command_table_widget->setSelectionMode(QAbstractItemView::SingleSelection); + + // Status tab widget + status_name_line_edit = new QLineEdit(this); + status_name_line_edit->setPlaceholderText("Enter status name"); + + status_sensor_id = new QLineEdit(this); + status_sensor_id->setPlaceholderText("Enter sensor ID"); + + status_section_name = new QLineEdit(this); + status_section_name->setPlaceholderText("Enter status section name"); + + status_value_type = new QComboBox(this); + status_value_type->addItem("uint32, (0)"); + status_value_type->addItem("uint16, (1)"); + + send_status_button = new QPushButton("Get Status", this); + status_table_widget = new QTableWidget(this); + status_table_widget->setSelectionBehavior(QAbstractItemView::SelectRows); + status_table_widget->setSelectionMode(QAbstractItemView::SingleSelection); + + // Create tabs + tab_widget = new QTabWidget(this); + param_tab = new QWidget(tab_widget); + command_tab = new QWidget(tab_widget); + status_tab = new QWidget(tab_widget); + + // Add dropdown menu for file selection + file_selector_combo_box = new QComboBox(this); + populate_file_menu(); + + response_text_edit = new QTextEdit(this); + response_text_edit->setReadOnly(true); + response_text_edit->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); + response_text_edit->setFixedHeight(100); + +} + +void SmartRadarService::setup_layout() +{ + // Parameter tab layout + QVBoxLayout * param_layout = new QVBoxLayout(param_tab); + param_layout->setSpacing(10); + param_layout->setContentsMargins(10, 10, 10, 10); + + QFormLayout * param_form = new QFormLayout(); + param_form->addRow("Action:", param_action_combo); + param_form->addRow("Parameter Name:", param_name_line_edit); + param_form->addRow("Section Name:", param_section_name); + param_form->addRow("Value Type:", param_value_type); + param_form->addRow("Value:", param_value_line_edit); + param_form->addRow("Sensor ID:", param_sensor_id); + + param_layout->addWidget(param_table_widget); + param_layout->addLayout(param_form); + param_layout->addWidget(send_param_button); + + // Command tab widget + QVBoxLayout * command_layout = new QVBoxLayout(command_tab); + command_layout->setSpacing(10); + command_layout->setContentsMargins(10, 10, 10, 10); + + QFormLayout * command_form = new QFormLayout(); + command_form->addRow("Command Name:", command_name_line_edit); + command_form->addRow("Section Name:", command_section_name); + command_form->addRow("Value:", command_value_line_edit); + command_form->addRow("Sensor ID:", command_sensor_id); + + command_layout->addWidget(command_table_widget); + command_layout->addLayout(command_form); + command_layout->addWidget(send_command_button); + + // Status tab layout + QVBoxLayout * status_layout = new QVBoxLayout(status_tab); + status_layout->setSpacing(10); + status_layout->setContentsMargins(10, 10, 10, 10); + + QFormLayout * status_form = new QFormLayout(); + status_form->addRow("Status Name:", status_name_line_edit); + status_form->addRow("Section Name:", status_section_name); + status_form->addRow("Value Type:", status_value_type); + status_form->addRow("Sensor ID:", status_sensor_id); + + status_layout->addWidget(status_table_widget); + status_layout->addLayout(status_form); + status_layout->addWidget(send_status_button); + + // Add tabs to tab widget + tab_widget->addTab(param_tab, "Parameter"); + tab_widget->addTab(command_tab, "Command"); + tab_widget->addTab(status_tab, "Status"); + + // Main layout + QVBoxLayout * main_layout = new QVBoxLayout(this); + main_layout->setSpacing(10); + main_layout->setContentsMargins(10, 10, 10, 10); + + main_layout->addWidget(file_selector_combo_box); + main_layout->addWidget(tab_widget); + main_layout->addWidget(response_text_edit); +} + +void SmartRadarService::setup_connections() +{ + // Connect dropdown menu signal to slot + connect(file_selector_combo_box, QOverload::of(&QComboBox::activated), this, &SmartRadarService::on_file_selected); + connect(send_param_button, &QPushButton::clicked, this, &SmartRadarService::on_send_param); + connect(send_command_button, &QPushButton::clicked, this, &SmartRadarService::on_send_command); + connect(send_status_button, &QPushButton::clicked, this, &SmartRadarService::on_get_status); + connect(param_table_widget, &QTableWidget::itemSelectionChanged, this, &SmartRadarService::on_param_selection); + connect(command_table_widget, &QTableWidget::itemSelectionChanged, this, &SmartRadarService::on_command_selection); + connect(status_table_widget, &QTableWidget::itemSelectionChanged, this, &SmartRadarService::on_status_selection); + connect(param_action_combo, QOverload::of(&QComboBox::currentIndexChanged), + [this](int index) { + // Disable value input when "Read Parameter" + param_value_line_edit->setEnabled(index == 0); + }); +} + +void SmartRadarService::populate_file_menu() +{ + // Add file names to the dropdown menu + file_selector_combo_box->clear(); + file_selector_combo_box->addItem("Choose Sensor Type"); + file_selector_combo_box->addItem("UMRR9F MSE"); + file_selector_combo_box->addItem("UMRR9F"); + file_selector_combo_box->addItem("UMRR9D"); + file_selector_combo_box->addItem("UMRRA4"); + file_selector_combo_box->addItem("UMRRA4 MSE"); + file_selector_combo_box->addItem("UMRRA1"); +} + +void SmartRadarService::on_param_selection() +{ + QList selected = param_table_widget->selectedItems(); + if (!selected.isEmpty()) { + int row = selected[0]->row(); + param_name_line_edit->setText(param_table_widget->item(row, 1)->text()); + param_section_name->setText(param_table_widget->item(row, 0)->text()); + } +} + +void SmartRadarService::on_command_selection() +{ + QList selected = command_table_widget->selectedItems(); + if (!selected.isEmpty()) { + int row = selected[0]->row(); + command_name_line_edit->setText(command_table_widget->item(row, 1)->text()); + command_section_name->setText(command_table_widget->item(row, 0)->text()); + } +} + +void SmartRadarService::on_status_selection() +{ + QList selected = status_table_widget->selectedItems(); + if (!selected.isEmpty()) { + int row = selected[0]->row(); + status_name_line_edit->setText(status_table_widget->item(row, 1)->text()); + status_section_name->setText(status_table_widget->item(row, 0)->text()); + } +} + +void SmartRadarService::on_file_selected(int index) +{ + // Update file paths based on selected file + if (index == 0) { + // Clear param tab + param_table_widget->setRowCount(0); + param_name_line_edit->clear(); + param_value_line_edit->clear(); + param_sensor_id->clear(); + param_section_name->clear(); + + // Clear command tab + command_table_widget->setRowCount(0); + command_name_line_edit->clear(); + command_value_line_edit->clear(); + command_sensor_id->clear(); + command_section_name->clear(); + + // Clear status tab + status_table_widget->setRowCount(0); + status_name_line_edit->clear(); + status_sensor_id->clear(); + status_section_name->clear(); + + return; + } + + QString base_path = current_directory + + "/src/smartmicro_ros2_radars/umrr_ros2_driver/smartmicro/user_interfaces/"; + + switch (index) { + case 1: + param_json_file_path = base_path + + "UserInterfaceUmrr9f_t169_mseV1.0.0/instructions/params/auto_interface_0dim.param"; + command_json_file_path = base_path + + "UserInterfaceUmrr9f_t169_mseV1.0.0/instructions/command/auto_interface.command"; + status_json_file_path = base_path + + "UserInterfaceUmrr9f_t169_mseV1.0.0/instructions/status/auto_interface.status"; + break; + case 2: + param_json_file_path = base_path + + "UserInterfaceUmrr9f_t169_automotiveV2.4.1/instructions/params/auto_interface_0dim.param"; + command_json_file_path = base_path + + "UserInterfaceUmrr9f_t169_automotiveV2.4.1/instructions/command/auto_interface.command"; + status_json_file_path = base_path + + "UserInterfaceUmrr9f_t169_automotiveV2.4.1/instructions/status/auto_interface.status"; + break; + case 3: + param_json_file_path = base_path + + "UserInterfaceUmrr9d_t152_automotiveV1.4.1/instructions/params/auto_interface_0dim.param"; + command_json_file_path = base_path + + "UserInterfaceUmrr9d_t152_automotiveV1.4.1/instructions/command/auto_interface.command"; + status_json_file_path = base_path + + "UserInterfaceUmrr9d_t152_automotiveV1.4.1/instructions/status/auto_interface.status"; + break; + case 4: + param_json_file_path = base_path + + "UserInterfaceUmrra4_automotiveV1.2.1/instructions/params/auto_interface_0dim.param"; + command_json_file_path = base_path + + "UserInterfaceUmrra4_automotiveV1.2.1/instructions/command/auto_interface.command"; + status_json_file_path = base_path + + "UserInterfaceUmrra4_automotiveV1.2.1/instructions/status/auto_interface.status"; + break; + case 5: + param_json_file_path = base_path + + "UserInterfaceUmrra4_mseV1.0.0/instructions/params/auto_interface_0dim.param"; + command_json_file_path = base_path + + "UserInterfaceUmrra4_mseV1.0.0/instructions/command/auto_interface.command"; + status_json_file_path = base_path + + "UserInterfaceUmrra4_mseV1.0.0/instructions/status/auto_interface.status"; + break; + case 6: + param_json_file_path = base_path + + "user_interface_umrra1_t166_b_automotive_v2_0_0/instructions/params/auto_interface_rrm.param"; + command_json_file_path = base_path + + "user_interface_umrra1_t166_b_automotive_v2_0_0/instructions/command/auto_interface_rrm.command"; + status_json_file_path = base_path + + "user_interface_umrra1_t166_b_automotive_v2_0_0/instructions/status/auto_interface_rrm.status"; + break; + } + + read_param_json_data(); + read_command_json_data(); + read_status_json_data(); +} + +void SmartRadarService::read_command_json_data() +{ + QFile command_json_file(command_json_file_path); + if (!command_json_file.open(QIODevice::ReadOnly)) { + RCLCPP_INFO(rclcpp::get_logger("rclcpp"), "Parse Failed!"); + return; + } + + QByteArray json_data = command_json_file.readAll(); + QJsonDocument doc(QJsonDocument::fromJson(json_data)); + QJsonObject json_object = doc.object(); + + // Extract gloabl section name + QString global_section_name = json_object["name"].toString(); + + // Extract the "commands" array + QJsonArray commands_array = json_object["commands"].toArray(); + + // Set up table headers + QStringList command_header_labels = {"Section", "Name", "Argument", "Comment"}; + command_table_widget->setColumnCount(3); + command_table_widget->setHorizontalHeaderLabels(command_header_labels); + + // Populate the table with command data + command_table_widget->setRowCount(commands_array.size()); + for (int i = 0; i < commands_array.size(); ++i) { + QJsonObject command_object = commands_array[i].toObject(); + QString name = command_object["name"].toString(); + QString argument = command_object["argument"].toString(); + QString comment = command_object["comment"].toString(); + + QTableWidgetItem * section_item = new QTableWidgetItem(global_section_name); + QTableWidgetItem * name_item = new QTableWidgetItem(name); + QTableWidgetItem * argument_item = new QTableWidgetItem(argument); + QTableWidgetItem * comment_item = new QTableWidgetItem(comment); + + command_table_widget->setItem(i, 0, section_item); + command_table_widget->setItem(i, 1, name_item); + command_table_widget->setItem(i, 2, argument_item); + command_table_widget->setItem(i, 3, comment_item); + } +} + +void SmartRadarService::read_param_json_data() +{ + QFile param_json_file(param_json_file_path); + if (!param_json_file.open(QIODevice::ReadOnly)) { + RCLCPP_INFO(rclcpp::get_logger("rclcpp"), "Parse Failed!"); + return; + } + + QByteArray json_data = param_json_file.readAll(); + QJsonDocument doc(QJsonDocument::fromJson(json_data)); + QJsonObject json_object = doc.object(); + + // Extract gloabl section name + QString global_section_name = json_object["name"].toString(); + + // Extract the "parameters" array + QJsonArray params_array = json_object["parameters"].toArray(); + + // Set up table headers + QStringList param_header_labels = {"Section", "Name", "Comment", "Type"}; + param_table_widget->setColumnCount(3); + param_table_widget->setHorizontalHeaderLabels(param_header_labels); + + // Populate the table with parameter data + param_table_widget->setRowCount(params_array.size()); + for (int i = 0; i < params_array.size(); ++i) { + QJsonObject param_object = params_array[i].toObject(); + QString name = param_object["name"].toString(); + QString comment = param_object["comment"].toString(); + QString type = param_object["type"].toString(); + + QTableWidgetItem * section_item = new QTableWidgetItem(global_section_name); + QTableWidgetItem * name_item = new QTableWidgetItem(name); + QTableWidgetItem * comment_item = new QTableWidgetItem(comment); + QTableWidgetItem * type_item = new QTableWidgetItem(type); + + param_table_widget->setItem(i, 0, section_item); + param_table_widget->setItem(i, 1, name_item); + param_table_widget->setItem(i, 2, comment_item); + param_table_widget->setItem(i, 3, type_item); + } +} + +void SmartRadarService::read_status_json_data() +{ + QFile status_json_file(status_json_file_path); + if (!status_json_file.open(QIODevice::ReadOnly)) { + RCLCPP_INFO(rclcpp::get_logger("rclcpp"), "Parse Failed!"); + return; + } + + QByteArray json_data = status_json_file.readAll(); + QJsonDocument doc(QJsonDocument::fromJson(json_data)); + QJsonObject json_object = doc.object(); + + // Extract gloabl section name + QString global_section_name = json_object["name"].toString(); + + // Extract the "status" array + QJsonArray status_array = json_object["status"].toArray(); + + // Set up table headers + QStringList status_header_labels = {"Section", "Name", "Comment", "Type"}; + status_table_widget->setColumnCount(3); + status_table_widget->setHorizontalHeaderLabels(status_header_labels); + + // Populate the table with status data + status_table_widget->setRowCount(status_array.size()); + for (int i = 0; i < status_array.size(); ++i) { + QJsonObject status_object = status_array[i].toObject(); + QString name = status_object["name"].toString(); + QString comment = status_object["comment"].toString(); + QString type = status_object["type"].toString(); + + QTableWidgetItem * section_item = new QTableWidgetItem(global_section_name); + QTableWidgetItem * name_item = new QTableWidgetItem(name); + QTableWidgetItem * comment_item = new QTableWidgetItem(comment); + QTableWidgetItem * type_item = new QTableWidgetItem(type); + + status_table_widget->setItem(i, 0, section_item); + status_table_widget->setItem(i, 1, name_item); + status_table_widget->setItem(i, 2, comment_item); + status_table_widget->setItem(i, 3, type_item); + } +} + +void SmartRadarService::on_send_param() +{ + // Validate common inputs + if (param_name_line_edit->text().isEmpty() || param_sensor_id->text().isEmpty()) { + response_text_edit->append("Error: Parameter name and sensor ID fields must be filled!"); + return; + } + + if (param_action_combo->currentIndex() == 0) { + // Writing param + if (param_value_line_edit->text().isEmpty()) { + response_text_edit->append("Error: Value must be provided for write!"); + return; + } + if (!mode_client->wait_for_service(SERVICE_AVAILABILITY_TIMEOUT)) { + response_text_edit->append("SetMode service not available!"); + return; + } + auto request = std::make_shared(); + request->section_name = param_section_name->text().toStdString(); + request->params.push_back(param_name_line_edit->text().toStdString()); + request->sensor_id = std::stoi(param_sensor_id->text().toStdString()); + request->value_types.push_back(param_value_type->currentIndex()); + request->values.push_back(param_value_line_edit->text().toStdString()); + auto result = mode_client->async_send_request(request); + auto status = rclcpp::spin_until_future_complete(client_node, result); + switch (status) { + case rclcpp::FutureReturnCode::SUCCESS: + { + auto response = result.get(); + QString response_msg = QString::fromStdString(response->res); + response_text_edit->append(response_msg); + } + break; + + case rclcpp::FutureReturnCode::TIMEOUT: + RCLCPP_ERROR(rclcpp::get_logger("rclcpp"), "Service timed out!"); + response_text_edit->append("Service timed out!"); + break; + + case rclcpp::FutureReturnCode::INTERRUPTED: + RCLCPP_ERROR(rclcpp::get_logger("rclcpp"), "Service call was interrupted!"); + response_text_edit->append("Service call was interrupted!"); + break; + + default: + RCLCPP_ERROR(rclcpp::get_logger("rclcpp"), "Service call failed due to unknown reason!"); + response_text_edit->append("Service call failed due to unknown reason!"); + break; + } + } + else { + // Reading param + if (!get_param_client->wait_for_service(SERVICE_AVAILABILITY_TIMEOUT)) { + response_text_edit->append("GetMode service not available!"); + return; + } + auto request = std::make_shared(); + request->section_name = param_section_name->text().toStdString(); + request->params.push_back(param_name_line_edit->text().toStdString()); + request->sensor_id = std::stoi(param_sensor_id->text().toStdString()); + request->param_types.push_back(param_value_type->currentIndex()); + auto result = get_param_client->async_send_request(request); + auto status = rclcpp::spin_until_future_complete(client_node, result); + switch (status) { + case rclcpp::FutureReturnCode::SUCCESS: + { + auto response = result.get(); + QString response_msg = QString::fromStdString(response->res); + response_text_edit->append(response_msg); + } + break; + + case rclcpp::FutureReturnCode::TIMEOUT: + RCLCPP_ERROR(rclcpp::get_logger("rclcpp"), "Service timed out!"); + response_text_edit->append("Service timed out!"); + break; + + case rclcpp::FutureReturnCode::INTERRUPTED: + RCLCPP_ERROR(rclcpp::get_logger("rclcpp"), "Service call was interrupted!"); + response_text_edit->append("Service call was interrupted!"); + break; + + default: + RCLCPP_ERROR(rclcpp::get_logger("rclcpp"), "Service call failed due to unknown reason!"); + response_text_edit->append("Service call failed due to unknown reason!"); + break; + } + } +} + +void SmartRadarService::on_send_command() +{ + // Validate inputs + if (command_name_line_edit->text().isEmpty() || command_value_line_edit->text().isEmpty() || command_sensor_id->text().isEmpty()) { + response_text_edit->append("Error: All command fields must be filled. Add dummy value of command if not specified!"); + return; + } + + if (!command_client) { + RCLCPP_ERROR(rclcpp::get_logger("rclcpp"), "Failed to create command client"); + return; + } + + if(!command_client->wait_for_service(SERVICE_AVAILABILITY_TIMEOUT)) { + RCLCPP_ERROR(rclcpp::get_logger("rclcpp"), "Service not available!"); + response_text_edit->append("Service not available! Is the radar node running?"); + return; + } + + auto request = std::make_shared(); + request->section_name = command_section_name->text().toStdString(); + request->command = command_name_line_edit->text().toStdString(); + request->value = std::stoi(command_value_line_edit->text().toUtf8().constData()); + request->sensor_id = std::stoi(command_sensor_id->text().toUtf8().constData()); + + auto result = command_client->async_send_request(request); + auto status = rclcpp::spin_until_future_complete(client_node, result); + + switch (status) { + case rclcpp::FutureReturnCode::SUCCESS: + { + auto response = result.get(); + QString response_msg = QString::fromStdString(response->res); + response_text_edit->append(response_msg); + } + break; + + case rclcpp::FutureReturnCode::TIMEOUT: + RCLCPP_ERROR(rclcpp::get_logger("rclcpp"), "Service timed out!"); + response_text_edit->append("Service timed out!"); + break; + + case rclcpp::FutureReturnCode::INTERRUPTED: + RCLCPP_ERROR(rclcpp::get_logger("rclcpp"), "Service call was interrupted!"); + response_text_edit->append("Service call was interrupted!"); + break; + + default: + RCLCPP_ERROR(rclcpp::get_logger("rclcpp"), "Service call failed due to unknown reason!"); + response_text_edit->append("Service call failed due to unknown reason!"); + break; + } +} + +void SmartRadarService::on_get_status() +{ + // Validate inputs + if (status_name_line_edit->text().isEmpty() || status_sensor_id->text().isEmpty()) { + response_text_edit->append("Error: Status name and sensor ID must be filled!"); + return; + } + + if (!status_client) { + RCLCPP_ERROR(rclcpp::get_logger("rclcpp"), "Failed to create status_client"); + return; + } + + if(!status_client->wait_for_service(SERVICE_AVAILABILITY_TIMEOUT)) { + RCLCPP_ERROR(rclcpp::get_logger("rclcpp"), "Service not available!"); + response_text_edit->append("Service not available! Is the radar node running?"); + return; + } + + auto request = std::make_shared(); + + request->section_name = status_section_name->text().toStdString(); + request->statuses.push_back(status_name_line_edit->text().toStdString()); + request->sensor_id = std::stoi(status_sensor_id->text().toUtf8().constData()); + request->status_types.push_back(status_value_type->currentIndex()); + + auto result = status_client->async_send_request(request); + auto status = rclcpp::spin_until_future_complete(client_node, result); + + switch (status) { + case rclcpp::FutureReturnCode::SUCCESS: + { + auto response = result.get(); + QString response_msg = QString::fromStdString(response->res); + response_text_edit->append(response_msg); + } + break; + + case rclcpp::FutureReturnCode::TIMEOUT: + RCLCPP_ERROR(rclcpp::get_logger("rclcpp"), "Service timed out!"); + response_text_edit->append("Service timed out!"); + break; + + case rclcpp::FutureReturnCode::INTERRUPTED: + RCLCPP_ERROR(rclcpp::get_logger("rclcpp"), "Service call was interrupted!"); + response_text_edit->append("Service call was interrupted!"); + break; + + default: + RCLCPP_ERROR(rclcpp::get_logger("rclcpp"), "Service call failed due to unknown reason!"); + response_text_edit->append("Service call failed due to unknown reason!"); + break; + } +} + +} // namespace smart_rviz_plugin + +#include +PLUGINLIB_EXPORT_CLASS(smart_rviz_plugin::SmartRadarService, rviz_common::Panel) diff --git a/smart_rviz_plugin/src/smart_status.cpp b/smart_rviz_plugin/src/smart_status.cpp new file mode 100644 index 0000000..73237c0 --- /dev/null +++ b/smart_rviz_plugin/src/smart_status.cpp @@ -0,0 +1,236 @@ +#include "smart_rviz_plugin/smart_status.hpp" + +#include +#include +#include +#include + +namespace smart_rviz_plugin +{ +SmartRadarStatus::SmartRadarStatus(QWidget * parent) : rviz_common::Panel(parent) { initialize(); } + +void SmartRadarStatus::initialize() +{ + node_ = rclcpp::Node::make_shared("smart_radar_gui_node"); + + // Status setup + gui_layout_ = new QVBoxLayout(); + topic_dropdown_ = new QComboBox(); + topic_dropdown_->addItem("Select a Topic"); + + // Retrieve available topics + auto topic_names_and_types = node_->get_topic_names_and_types(); + + // Create subscribers for selected topics + for (const auto & topic : topic_names_and_types) { + if (topic.first.find("port_targetheader") != std::string::npos) { + port_header_target_subscribers_[topic.first] = + node_->create_subscription( + topic.first, 10, + [this, topic](const umrr_ros2_msgs::msg::PortTargetHeader::SharedPtr msg) { + port_targetheader_callback(msg, topic.first); + }); + + topic_dropdown_->addItem(QString::fromStdString(topic.first)); + } else if (topic.first.find("can_targetheader") != std::string::npos) { + can_header_target_subscribers_[topic.first] = + node_->create_subscription( + topic.first, 10, + [this, topic](const umrr_ros2_msgs::msg::CanTargetHeader::SharedPtr msg) { + can_targetheader_callback(msg, topic.first); + }); + + topic_dropdown_->addItem(QString::fromStdString(topic.first)); + } else if (topic.first.find("port_objectheader") != std::string::npos) { + port_header_object_subscribers_[topic.first] = + node_->create_subscription( + topic.first, 10, + [this, topic](const umrr_ros2_msgs::msg::PortObjectHeader::SharedPtr msg) { + port_objectheader_callback(msg, topic.first); + }); + topic_dropdown_->addItem(QString::fromStdString(topic.first)); + } else if (topic.first.find("can_objectheader") != std::string::npos) { + can_header_object_subscribers_[topic.first] = + node_->create_subscription( + topic.first, 10, + [this, topic](const umrr_ros2_msgs::msg::CanObjectHeader::SharedPtr msg) { + can_objectheader_callback(msg, topic.first); + }); + topic_dropdown_->addItem(QString::fromStdString(topic.first)); + } + } + + connect(topic_dropdown_, SIGNAL(currentIndexChanged(int)), this, SLOT(update_table())); + + table_data_ = new QTableWidget(); + table_data_->setRowCount(16); + table_data_->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); + table_data_->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch); + + splitter_ = new QSplitter(Qt::Vertical); + splitter_->addWidget(topic_dropdown_); + splitter_->addWidget(table_data_); + + gui_layout_->addWidget(splitter_); + + timer_ = new QTimer(); + connect(timer_, SIGNAL(timeout()), this, SLOT(check_data())); + timer_->start(20); + + setLayout(gui_layout_); + + RCLCPP_INFO(rclcpp::get_logger("rclcpp"), "Status Plugin Created!"); +} + +void SmartRadarStatus::port_targetheader_callback( + const umrr_ros2_msgs::msg::PortTargetHeader::SharedPtr msg, const std::string topic_name) +{ + if (!selected_topic_.empty() && topic_name == selected_topic_) { + table_data_->setColumnCount(0); + int col_index = 0; + std::uint64_t ntp_timestamp = msg->acquisition_start; + // Extract the first 4 bytes (seconds) - most significant 32 bits + std::uint32_t seconds = static_cast(ntp_timestamp >> 32); + // Extract the last 4 bytes (fraction of a second) - least significant 32 bits + std::uint32_t fraction_sec = static_cast(ntp_timestamp & 0xFFFFFFFF); + table_data_->insertColumn(col_index); + table_data_->setItem( + 0, col_index, new QTableWidgetItem(QString::number(msg->cycle_time, 'f', 3))); + table_data_->setItem( + 1, col_index, new QTableWidgetItem(QString::number(msg->number_of_targets))); + table_data_->setItem( + 2, col_index, new QTableWidgetItem(QString::number(msg->acquisition_tx_ant_idx))); + table_data_->setItem( + 3, col_index, new QTableWidgetItem(QString::number(msg->acquisition_sweep_idx))); + table_data_->setItem( + 4, col_index, new QTableWidgetItem(QString::number(msg->acquisition_cf_idx))); + table_data_->setItem(5, col_index, new QTableWidgetItem(QString::number(seconds))); + table_data_->setItem(6, col_index, new QTableWidgetItem(QString::number(fraction_sec))); + table_data_->setItem(7, col_index, new QTableWidgetItem(QString::number(msg->prf))); + table_data_->setItem( + 8, col_index, new QTableWidgetItem(QString::number(msg->umambiguous_speed, 'f', 2))); + table_data_->setItem(9, col_index, new QTableWidgetItem(QString::number(msg->port_identifier))); + table_data_->setItem(10, col_index, new QTableWidgetItem(QString::number(msg->port_ver_major))); + table_data_->setItem(11, col_index, new QTableWidgetItem(QString::number(msg->port_ver_minor))); + table_data_->setItem(12, col_index, new QTableWidgetItem(QString::number(msg->port_size))); + table_data_->setItem( + 13, col_index, new QTableWidgetItem(QString::number(msg->body_endianness))); + table_data_->setItem(14, col_index, new QTableWidgetItem(QString::number(msg->port_index))); + table_data_->setItem( + 15, col_index, new QTableWidgetItem(QString::number(msg->header_ver_major))); + table_data_->setItem( + 16, col_index, new QTableWidgetItem(QString::number(msg->header_ver_minor))); + } +} + +void SmartRadarStatus::can_targetheader_callback( + const umrr_ros2_msgs::msg::CanTargetHeader::SharedPtr msg, const std::string topic_name) +{ + if (!selected_topic_.empty() && topic_name == selected_topic_) { + table_data_->setColumnCount(0); + int col_index = 0; + table_data_->insertColumn(col_index); + table_data_->setItem( + 0, col_index, new QTableWidgetItem(QString::number(msg->cycle_time, 'f', 3))); + table_data_->setItem( + 1, col_index, new QTableWidgetItem(QString::number(msg->number_of_targets))); + table_data_->setItem(2, col_index, new QTableWidgetItem(QString::number(msg->cycle_count))); + table_data_->setItem( + 3, col_index, new QTableWidgetItem(QString::number(msg->acquisition_setup))); + table_data_->setItem(4, col_index, new QTableWidgetItem(QString::number(msg->time_stamp))); + table_data_->setItem(5, col_index, new QTableWidgetItem(QString::number(msg->acq_ts_fraction))); + } +} + +void SmartRadarStatus::port_objectheader_callback( + const umrr_ros2_msgs::msg::PortObjectHeader::SharedPtr msg, const std::string topic_name) +{ + if (!selected_topic_.empty() && topic_name == selected_topic_) { + float_t cycle_time = msg->cycle_time; + std::uint16_t no_objs = msg->number_of_objects; + std::uint64_t ntp_timestamp = msg->ts_measurement; + std::uint32_t seconds = static_cast(ntp_timestamp >> 32); + std::uint32_t fraction_sec = static_cast(ntp_timestamp & 0xFFFFFFFF); + + table_data_->setColumnCount(0); + int col_index = 0; + table_data_->insertColumn(col_index); + table_data_->setItem(0, col_index, new QTableWidgetItem(QString::number(cycle_time, 'f', 3))); + table_data_->setItem(1, col_index, new QTableWidgetItem(QString::number(no_objs))); + table_data_->setItem(2, col_index, new QTableWidgetItem(QString::number(seconds))); + table_data_->setItem(3, col_index, new QTableWidgetItem(QString::number(fraction_sec))); + table_data_->setItem(4, col_index, new QTableWidgetItem(QString::number(msg->port_identifier))); + table_data_->setItem(5, col_index, new QTableWidgetItem(QString::number(msg->port_ver_major))); + table_data_->setItem(6, col_index, new QTableWidgetItem(QString::number(msg->port_ver_minor))); + table_data_->setItem(7, col_index, new QTableWidgetItem(QString::number(msg->port_size))); + table_data_->setItem(8, col_index, new QTableWidgetItem(QString::number(msg->body_endianness))); + table_data_->setItem(9, col_index, new QTableWidgetItem(QString::number(msg->port_index))); + table_data_->setItem( + 10, col_index, new QTableWidgetItem(QString::number(msg->header_ver_major))); + table_data_->setItem( + 11, col_index, new QTableWidgetItem(QString::number(msg->header_ver_minor))); + } +} + +void SmartRadarStatus::can_objectheader_callback( + const umrr_ros2_msgs::msg::CanObjectHeader::SharedPtr msg, const std::string topic_name) +{ + if (!selected_topic_.empty() && topic_name == selected_topic_) { + table_data_->setColumnCount(0); + int col_index = 0; + table_data_->insertColumn(col_index); + table_data_->setItem( + 0, col_index, new QTableWidgetItem(QString::number(msg->cycle_time, 'f', 3))); + table_data_->setItem( + 1, col_index, new QTableWidgetItem(QString::number(msg->number_of_objects))); + table_data_->setItem(2, col_index, new QTableWidgetItem(QString::number(msg->cycle_count))); + table_data_->setItem(3, col_index, new QTableWidgetItem(QString::number(msg->ego_speed))); + table_data_->setItem( + 4, col_index, new QTableWidgetItem(QString::number(msg->ego_speed_quality))); + table_data_->setItem(5, col_index, new QTableWidgetItem(QString::number(msg->ego_yaw_rate))); + table_data_->setItem( + 6, col_index, new QTableWidgetItem(QString::number(msg->ego_yaw_rate_quality))); + table_data_->setItem(7, col_index, new QTableWidgetItem(QString::number(msg->dyn_source))); + } +} + +void SmartRadarStatus::update_table() +{ + table_data_->setColumnCount(0); + selected_topic_ = topic_dropdown_->currentText().toStdString(); + if (selected_topic_.find("port_targetheader") != std::string::npos) { + table_data_->setVerticalHeaderLabels( + {"CycleDuration [s]", "NumOfTargets", "AcquisitionTxAntIdx", "AcquisitionSweepIdx", + "AcquisitionCfIdx", "AcqTimeStamp [s]", "AcqTimeStampfrac [NTP]", "PRF", "UmambiguousSpeed", + "PortIdentifier", "PortVersionMajor", "PortVersionMinor", "PortSize", "BodyEndianness", + "PortIndex", "HeaderVersionMajor", "HeaderVersionMinor"}); + } else if (selected_topic_.find("can_targetheader") != std::string::npos) { + table_data_->setVerticalHeaderLabels( + {"CycleDuration [s]", "NumOfTargets", "CycleCount", "AcquisitionSetup", "AcqTimeStamp [s]", + "AcqTimeStampfrac [NTP]", "", "", "", "", "", "", "", "", "", "", ""}); + } else if (selected_topic_.find("can_objectheader") != std::string::npos) { + table_data_->setColumnCount(0); + table_data_->setVerticalHeaderLabels( + {"CycleDuration [s]", "NumOfObjects", "CycleCount", "Speed [km/h]", "SpeedQuality", + "YawRate [rad/s]", "YawRateQuality", "DynamicSource", "", "", "", "", "", "", "", "", ""}); + } else if (selected_topic_.find("port_objectheader") != std::string::npos) { + table_data_->setColumnCount(0); + table_data_->setVerticalHeaderLabels( + {"CycleDuration [s]", "NumOfObjects", "AcqTimeStamp [s]", "AcqTimeStampfrac [NTP]", + "PortIdentifier", "PortVersionMajor", "PortVersionMinor", "PortSize", "BodyEndianness", + "PortIndex", "HeaderVersionMajor", "HeaderVersionMinor", "", "", "", "", ""}); + } +} + +void SmartRadarStatus::check_data() +{ + if (rclcpp::ok()) // Check if ROS2 is still running + { + rclcpp::spin_some(node_); + } +} + +} // namespace smart_rviz_plugin + +#include +PLUGINLIB_EXPORT_CLASS(smart_rviz_plugin::SmartRadarStatus, rviz_common::Panel) diff --git a/umrr_ros2_driver/CMakeLists.txt b/umrr_ros2_driver/CMakeLists.txt new file mode 100644 index 0000000..c650596 --- /dev/null +++ b/umrr_ros2_driver/CMakeLists.txt @@ -0,0 +1,182 @@ +# Copyright 2021 Apex.AI, Inc. +# +# 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. +# +# The initial version of the code was developed by Apex.AI and +# was thereafter adapted and extended by smartmicro. + +cmake_minimum_required(VERSION 3.11) + +project(umrr_ros2_driver) +include(FetchContent) + +# Default to C++17 +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 -Wnarrowing -Wpedantic -Wno-unused-parameter -g) +endif() + +find_package(ament_cmake_auto REQUIRED) +find_package(Threads REQUIRED) +find_package(visualization_msgs REQUIRED) + +fetchcontent_declare(json + URL https://github.com/nlohmann/json/releases/download/v3.12.0/include.zip + URL_HASH SHA256=b8cb0ef2dd7f57f18933997c9934bb1fa962594f701cd5a8d3c2c80541559372 +) + +fetchcontent_getproperties(json) +if(NOT json_POPULATED) + fetchcontent_populate(json) + add_library(nlohmann_json INTERFACE) + target_include_directories(nlohmann_json INTERFACE ${json_SOURCE_DIR}/include) + add_library(nlohmann_json::nlohmann_json ALIAS nlohmann_json) +endif() + +ament_auto_find_build_dependencies() + +set(SMARTMICRO_LIB_DIR "lib-linux-x86_64-gcc_9") + +# specify the path to search for the libraries +link_directories(${CMAKE_CURRENT_SOURCE_DIR}/smartmicro/${SMARTMICRO_LIB_DIR}) + +install(FILES + "${PROJECT_SOURCE_DIR}/smartmicro/${SMARTMICRO_LIB_DIR}/libsmart_access.so" + "${PROJECT_SOURCE_DIR}/smartmicro/${SMARTMICRO_LIB_DIR}/libosal.so" + "${PROJECT_SOURCE_DIR}/smartmicro/${SMARTMICRO_LIB_DIR}/libcom_lib.so" + "${PROJECT_SOURCE_DIR}/smartmicro/${SMARTMICRO_LIB_DIR}/libbasev1.0.2_user_interface.so" + "${PROJECT_SOURCE_DIR}/smartmicro/${SMARTMICRO_LIB_DIR}/libumrr11_t132_automotivev1.1.2_user_interface.so" + "${PROJECT_SOURCE_DIR}/smartmicro/${SMARTMICRO_LIB_DIR}/libumrr96_t153_automotivev1.2.2_user_interface.so" + "${PROJECT_SOURCE_DIR}/smartmicro/${SMARTMICRO_LIB_DIR}/libumrr9f_t169_automotivev1.1.1_user_interface.so" + "${PROJECT_SOURCE_DIR}/smartmicro/${SMARTMICRO_LIB_DIR}/libumrr9f_t169_automotivev2.0.0_user_interface.so" + "${PROJECT_SOURCE_DIR}/smartmicro/${SMARTMICRO_LIB_DIR}/libumrr9f_t169_automotivev2.1.1_user_interface.so" + "${PROJECT_SOURCE_DIR}/smartmicro/${SMARTMICRO_LIB_DIR}/libumrr9f_t169_automotivev2.2.1_user_interface.so" + "${PROJECT_SOURCE_DIR}/smartmicro/${SMARTMICRO_LIB_DIR}/libumrr9f_t169_automotivev2.4.1_user_interface.so" + "${PROJECT_SOURCE_DIR}/smartmicro/${SMARTMICRO_LIB_DIR}/libumrr9f_t169_automotivev3.0.0_user_interface.so" + "${PROJECT_SOURCE_DIR}/smartmicro/${SMARTMICRO_LIB_DIR}/libumrr9d_t152_automotivev1.0.3_user_interface.so" + "${PROJECT_SOURCE_DIR}/smartmicro/${SMARTMICRO_LIB_DIR}/libumrr9d_t152_automotivev1.2.2_user_interface.so" + "${PROJECT_SOURCE_DIR}/smartmicro/${SMARTMICRO_LIB_DIR}/libumrr9d_t152_automotivev1.4.1_user_interface.so" + "${PROJECT_SOURCE_DIR}/smartmicro/${SMARTMICRO_LIB_DIR}/libumrr9d_t152_automotivev1.5.0_user_interface.so" + "${PROJECT_SOURCE_DIR}/smartmicro/${SMARTMICRO_LIB_DIR}/libumrra4_automotivev1.0.1_user_interface.so" + "${PROJECT_SOURCE_DIR}/smartmicro/${SMARTMICRO_LIB_DIR}/libumrra4_automotivev1.2.1_user_interface.so" + "${PROJECT_SOURCE_DIR}/smartmicro/${SMARTMICRO_LIB_DIR}/libumrra4_automotivev1.4.0_user_interface.so" + "${PROJECT_SOURCE_DIR}/smartmicro/${SMARTMICRO_LIB_DIR}/libumrr9f_t169_msev1.0.0_user_interface.so" + "${PROJECT_SOURCE_DIR}/smartmicro/${SMARTMICRO_LIB_DIR}/libumrr9f_t169_msev1.1.0_user_interface.so" + "${PROJECT_SOURCE_DIR}/smartmicro/${SMARTMICRO_LIB_DIR}/libumrr9f_t169_msev1.3.0_user_interface.so" + "${PROJECT_SOURCE_DIR}/smartmicro/${SMARTMICRO_LIB_DIR}/libumrra4_msev1.0.0_user_interface.so" + "${PROJECT_SOURCE_DIR}/smartmicro/${SMARTMICRO_LIB_DIR}/libumrra4_msev2.1.0_user_interface.so" + "${PROJECT_SOURCE_DIR}/smartmicro/${SMARTMICRO_LIB_DIR}/libumrra1_t166_b_automotivev1.0.0_user_interface.so" + "${PROJECT_SOURCE_DIR}/smartmicro/${SMARTMICRO_LIB_DIR}/libumrra1_t166_b_automotivev2.0.0_user_interface.so" +DESTINATION lib) + +set(LIB_PATH "${CMAKE_INSTALL_PREFIX}/lib") +set(CONFIG_FOLDER_PATH "${CMAKE_INSTALL_PREFIX}/share/${PROJECT_NAME}/config") +set(CONFIG_FILE_PATH "${CONFIG_FOLDER_PATH}/smart_access_config.json") +set(HW_INVENTORY_FILE_PATH "${CONFIG_FOLDER_PATH}/hw_inventory.json") +set(ROUTING_TABLE_FILE_PATH "${CONFIG_FOLDER_PATH}/routing_table.json") +configure_file( + "${PROJECT_SOURCE_DIR}/cmake/smart_access_config.json.in" "${CONFIG_FILE_PATH}" @ONLY) +configure_file( + "${PROJECT_SOURCE_DIR}/cmake/config_path.hpp.in" + "${CMAKE_INSTALL_PREFIX}/include/${PROJECT_NAME}/config_path.hpp" @ONLY) + +ament_auto_add_library(smartmicro_radar_node SHARED + "src/smartmicro_radar_node.cpp" + "src/update_service.cpp" + ) + +target_include_directories(smartmicro_radar_node PUBLIC ${CMAKE_INSTALL_PREFIX}/include/ +smartmicro/include/ +smartmicro/include/umrr11_t132_automotive_v1_1_2 +smartmicro/include/umrr96_t153_automotive_v1_2_2 +smartmicro/include/umrr9f_t169_automotive_v1_1_1 +smartmicro/include/umrr9f_t169_automotive_v2_0_0 +smartmicro/include/umrr9f_t169_automotive_v2_1_1 +smartmicro/include/umrr9f_t169_automotive_v2_2_1 +smartmicro/include/umrr9f_t169_automotive_v2_4_1 +smartmicro/include/umrr9f_t169_automotive_v3_0_0 +smartmicro/include/umrr9d_t152_automotive_v1_0_3 +smartmicro/include/umrr9d_t152_automotive_v1_2_2 +smartmicro/include/umrr9d_t152_automotive_v1_4_1 +smartmicro/include/umrr9d_t152_automotive_v1_5_0 +smartmicro/include/umrra4_automotive_v1_0_1 +smartmicro/include/umrra4_automotive_v1_2_1 +smartmicro/include/umrra4_automotive_v1_4_0 +smartmicro/include/umrr9f_t169_mse_v1_0_0 +smartmicro/include/umrr9f_t169_mse_v1_1_0 +smartmicro/include/umrr9f_t169_mse_v1_3_0 +smartmicro/include/umrra4_mse_v1_0_0 +smartmicro/include/umrra4_mse_v2_1_0 +smartmicro/include/umrra1_t166_b_automotive_v1_0_0 +smartmicro/include/umrra1_t166_b_automotive_v2_0_0) + +# link smart_access_lib-linux-x86_64-gcc_9 to the node +target_link_libraries(smartmicro_radar_node + smart_access + nlohmann_json::nlohmann_json + com_lib + osal + umrr11_t132_automotivev1.1.2_user_interface + umrr96_t153_automotivev1.2.2_user_interface + umrr9f_t169_automotivev1.1.1_user_interface + umrr9f_t169_automotivev2.0.0_user_interface + umrr9f_t169_automotivev2.1.1_user_interface + umrr9f_t169_automotivev2.2.1_user_interface + umrr9f_t169_automotivev2.4.1_user_interface + umrr9f_t169_automotivev3.0.0_user_interface + umrr9d_t152_automotivev1.0.3_user_interface + umrr9d_t152_automotivev1.2.2_user_interface + umrr9d_t152_automotivev1.4.1_user_interface + umrr9d_t152_automotivev1.5.0_user_interface + umrra4_automotivev1.0.1_user_interface + umrra4_automotivev1.2.1_user_interface + umrra4_automotivev1.4.0_user_interface + umrr9f_t169_msev1.0.0_user_interface + umrr9f_t169_msev1.1.0_user_interface + umrr9f_t169_msev1.3.0_user_interface + umrra4_msev1.0.0_user_interface + umrra4_msev2.1.0_user_interface + umrra1_t166_b_automotivev1.0.0_user_interface + umrra1_t166_b_automotivev2.0.0_user_interface) + +rclcpp_components_register_node(smartmicro_radar_node + PLUGIN "smartmicro::drivers::radar::SmartmicroRadarNode" + EXECUTABLE smartmicro_radar_node_exe +) + +if(BUILD_TESTING) + find_package(launch_testing_ament_cmake) + add_launch_test(test/radar_node_test.launch.py TIMEOUT "100") + + # Not applying ros2 linters on external sources + list(APPEND AMENT_LINT_AUTO_EXCLUDE + ament_cmake_copyright + ament_cmake_lint_cmake + ament_cmake_uncrustify + ament_cmake_cpplint + ament_cmake_flake8 + ament_cmake_cppcheck + ament_cmake_pep257 + ) + + find_package(ament_lint_auto REQUIRED) + ament_lint_auto_find_test_dependencies() + ament_copyright("src/smartmicro_radar_node.cpp") + ament_cppcheck("src/smartmicro_radar_node.cpp") + +endif() + +ament_auto_package(INSTALL_TO_SHARE config launch param) diff --git a/umrr_ros2_driver/cmake/config_path.hpp.in b/umrr_ros2_driver/cmake/config_path.hpp.in new file mode 100644 index 0000000..3c94fdc --- /dev/null +++ b/umrr_ros2_driver/cmake/config_path.hpp.in @@ -0,0 +1,33 @@ +// Copyright 2021 Apex.AI, Inc. +// +// 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 UMRR_ROS2_DRIVER__CMAKE_CONFIG_PATH_H_ +#define UMRR_ROS2_DRIVER__CMAKE_CONFIG_PATH_H_ + +namespace smartmicro +{ +namespace drivers +{ +namespace radar +{ + +constexpr auto kConfigFilePath = "@CONFIG_FILE_PATH@"; +constexpr auto kHwInventoryFilePath = "@HW_INVENTORY_FILE_PATH@"; +constexpr auto kRoutingTableFilePath = "@ROUTING_TABLE_FILE_PATH@"; + +} // namespace radar +} // namespace drivers +} // namespace smartmicro + +#endif // UMRR_ROS2_DRIVER__CMAKE_CONFIG_PATH_H_ diff --git a/umrr_ros2_driver/cmake/smart_access_config.json.in b/umrr_ros2_driver/cmake/smart_access_config.json.in new file mode 100644 index 0000000..644827d --- /dev/null +++ b/umrr_ros2_driver/cmake/smart_access_config.json.in @@ -0,0 +1,16 @@ +{ + "name": "Smart Access Lib Config", + "version" : "1.0.0", + "client_id": 1, + "role": "master", + "shared_lib_path": "@LIB_PATH@", + "config_path": "@CONFIG_FOLDER_PATH@", + "user_interface_name": "base", + "user_interface_major_v": 1, + "user_interface_minor_v": 0, + "user_interface_patch_v": 2, + "download_path": "", + "instruction_serialization_type": "can_based", + "data_serialization_type": "can_based", + "alive": false +} diff --git a/umrr_ros2_driver/config/hw_inventory.json b/umrr_ros2_driver/config/hw_inventory.json new file mode 100644 index 0000000..48d7e7a --- /dev/null +++ b/umrr_ros2_driver/config/hw_inventory.json @@ -0,0 +1,9 @@ +{ + "hwItems": [ + { + + } + ], + "name": "HW Inventory List", + "version": "1.1.0" +} diff --git a/umrr_ros2_driver/config/routing_table.json b/umrr_ros2_driver/config/routing_table.json new file mode 100644 index 0000000..bc6cee3 --- /dev/null +++ b/umrr_ros2_driver/config/routing_table.json @@ -0,0 +1,9 @@ +{ + "clients": [ + { + "can_network_id": 0 + } + ], + "name": "Client Routing Table", + "version": "1.0.0" +} diff --git a/umrr_ros2_driver/config/rviz/smart_plugin.rviz b/umrr_ros2_driver/config/rviz/smart_plugin.rviz new file mode 100644 index 0000000..d5e90ac --- /dev/null +++ b/umrr_ros2_driver/config/rviz/smart_plugin.rviz @@ -0,0 +1,212 @@ +Panels: + - Class: rviz_common/Displays + Help Height: 0 + Name: Displays + Property Tree Widget: + Expanded: ~ + Splitter Ratio: 0.5 + Tree Height: 295 + - Class: rviz_common/Selection + Name: Selection + - Class: rviz_common/Tool Properties + Expanded: + - /2D Goal Pose1 + - /Publish Point1 + Name: Tool Properties + Splitter Ratio: 0.5886790156364441 + - Class: rviz_common/Views + Expanded: + - /Current View1 + Name: Views + Splitter Ratio: 0.5 + - Class: smart_rviz_plugin/Smart Recorder + Name: Smart Recorder + - Class: smart_rviz_plugin/Smart Status + Name: Smart Status + - Class: smart_rviz_plugin/Smart Command Configurator + Name: Smart Command Configurator + - Class: smart_rviz_plugin/Smart Status + Name: Smart Status + - Class: smart_rviz_plugin/Smart Firmware Download + Name: Smart Firmware Download +Visualization Manager: + Class: "" + Displays: + - Alpha: 0.5 + Cell Size: 1 + Class: rviz_default_plugins/Grid + Color: 160; 160; 164 + Enabled: true + Line Style: + Line Width: 0.029999999329447746 + Value: Lines + Name: Grid + Normal Cell Count: 0 + Offset: + X: 0 + Y: 0 + Z: 0 + Plane: XY + Plane Cell Count: 10 + Reference Frame: + Value: true + - Class: rviz_default_plugins/Axes + Enabled: true + Length: 1 + Name: Axes + Radius: 0.10000000149011612 + Reference Frame: + Value: true + - Alpha: 1 + Autocompute Intensity Bounds: true + Autocompute Value Bounds: + Max Value: 10 + Min Value: -10 + Value: true + Axis: Z + Channel Name: power + Class: rviz_default_plugins/PointCloud2 + Color: 255; 255; 255 + Color Transformer: Intensity + Decay Time: 0 + Enabled: true + Invert Rainbow: false + Max Color: 255; 255; 255 + Max Intensity: 88.64950561523438 + Min Color: 0; 0; 0 + Min Intensity: 56.63246154785156 + Name: Port_Tgts_S0 + Position Transformer: XYZ + Selectable: true + Size (Pixels): 10 + Size (m): 0.009999999776482582 + Style: Points + Topic: + Depth: 5 + Durability Policy: Volatile + History Policy: Keep Last + Reliability Policy: Reliable + Value: /smart_radar/port_targets_0 + Use Fixed Frame: true + Use rainbow: true + Value: true + - Alpha: 1 + Autocompute Intensity Bounds: true + Autocompute Value Bounds: + Max Value: 10 + Min Value: -10 + Value: true + Axis: Z + Channel Name: intensity + Class: rviz_default_plugins/PointCloud2 + Color: 138; 226; 52 + Color Transformer: FlatColor + Decay Time: 0 + Enabled: true + Invert Rainbow: false + Max Color: 255; 255; 255 + Max Intensity: 4096 + Min Color: 0; 0; 0 + Min Intensity: 0 + Name: Can_Tgts_S1 + Position Transformer: XYZ + Selectable: true + Size (Pixels): 10 + Size (m): 0.009999999776482582 + Style: Points + Topic: + Depth: 5 + Durability Policy: Volatile + History Policy: Keep Last + Reliability Policy: Reliable + Value: /smart_radar/can_targets_1 + Use Fixed Frame: true + Use rainbow: true + Value: true + Enabled: true + Global Options: + Background Color: 48; 48; 48 + Fixed Frame: umrr + Frame Rate: 30 + Name: root + Tools: + - Class: rviz_default_plugins/Interact + Hide Inactive Objects: true + - Class: rviz_default_plugins/MoveCamera + - Class: rviz_default_plugins/Select + - Class: rviz_default_plugins/FocusCamera + - Class: rviz_default_plugins/Measure + Line color: 128; 128; 0 + - Class: rviz_default_plugins/SetInitialPose + Topic: + Depth: 5 + Durability Policy: Volatile + History Policy: Keep Last + Reliability Policy: Reliable + Value: /initialpose + - Class: rviz_default_plugins/SetGoal + Topic: + Depth: 5 + Durability Policy: Volatile + History Policy: Keep Last + Reliability Policy: Reliable + Value: /goal_pose + - Class: rviz_default_plugins/PublishPoint + Single click: true + Topic: + Depth: 5 + Durability Policy: Volatile + History Policy: Keep Last + Reliability Policy: Reliable + Value: /clicked_point + Transformation: + Current: + Class: rviz_default_plugins/TF + Value: true + Views: + Current: + Class: rviz_default_plugins/Orbit + Distance: 38.8543701171875 + Enable Stereo Rendering: + Stereo Eye Separation: 0.05999999865889549 + Stereo Focal Distance: 1 + Swap Stereo Eyes: false + Value: false + Focal Point: + X: 10.84825325012207 + Y: 0.366975337266922 + Z: 0.01084582507610321 + Focal Shape Fixed Size: true + Focal Shape Size: 0.05000000074505806 + Invert Z Axis: false + Name: Current View + Near Clip Distance: 0.009999999776482582 + Pitch: 1.5697963237762451 + Target Frame: + Value: Orbit (rviz) + Yaw: 3.1353957653045654 + Saved: ~ +Window Geometry: + Displays: + collapsed: false + Height: 1757 + Hide Left Dock: false + Hide Right Dock: false + QMainWindow State: 000000ff00000000fd0000000400000000000007ce00000677fc020000000afb0000001200530065006c0065006300740069006f006e00000001e10000009b0000006a00fffffffb0000001e0054006f006f006c002000500072006f007000650072007400690065007302000001ed000001df00000185000000a3fb000000120056006900650077007300200054006f006f02000001df000002110000018500000122fb000000200054006f006f006c002000500072006f0070006500720074006900650073003203000002880000011d000002210000017afb000000100044006900730070006c00610079007300000000450000016d000000e800fffffffb0000002000730065006c0065006300740069006f006e00200062007500660066006500720200000138000000aa0000023a00000294fb00000014005700690064006500530074006500720065006f02000000e6000000d2000003ee0000030bfb0000000c004b0069006e0065006300740200000186000001060000030c00000261fb0000001c0053006d0061007200740020005200650063006f007200640065007201000000450000025e0000010e00fffffffb000000340053006d00610072007400200043006f006d006d0061006e006400200043006f006e0066006900670075007200610074006f007201000002aa000004120000026000ffffff000000010000015300000677fc0200000006fb000000180053006d00610072007400200053007400610074007500730100000045000004c40000009f00fffffffb0000002e0053006d0061007200740020004600690072006d007700610072006500200044006f0077006e006c006f006100640100000510000001ac0000014b00fffffffb000000180053006d006100720074002000530074006100740075007300000000450000053c0000009f00fffffffb0000001e0054006f006f006c002000500072006f00700065007200740069006500730100000041000000780000000000000000fb0000000a00560069006500770073000000004500000320000000be00fffffffb0000001200530065006c0065006300740069006f006e010000025a000000b200000000000000000000000200000490000000a9fc0100000001fb0000000a00560069006500770073030000004e00000080000002e10000019700000003000004420000003efc0100000002fb0000000800540069006d00650100000000000004420000000000000000fb0000000800540069006d00650100000000000004500000000000000000000003200000067700000004000000040000000800000008fc0000000100000002000000010000000a0054006f006f006c00730100000000ffffffff0000000000000000 + Selection: + collapsed: false + Smart Command Configurator: + collapsed: false + Smart Firmware Download: + collapsed: false + Smart Recorder: + collapsed: false + Smart Status: + collapsed: false + Tool Properties: + collapsed: false + Views: + collapsed: false + Width: 3151 + X: 374 + Y: 214 diff --git a/umrr_ros2_driver/include/umrr_ros2_driver/smartmicro_radar_node.hpp b/umrr_ros2_driver/include/umrr_ros2_driver/smartmicro_radar_node.hpp new file mode 100644 index 0000000..99eac67 --- /dev/null +++ b/umrr_ros2_driver/include/umrr_ros2_driver/smartmicro_radar_node.hpp @@ -0,0 +1,1120 @@ +// Copyright 2021 Apex.AI, Inc. +// +// 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. +// The initial version of the code was developed by Apex.AI and +// was thereafter adapted and extended by smartmicro. + +#ifndef UMRR_ROS2_DRIVER__SMARTMICRO_RADAR_NODE_HPP_ +#define UMRR_ROS2_DRIVER__SMARTMICRO_RADAR_NODE_HPP_ + +#include +#include +#include +#include +#include "umrr_ros2_driver/update_service.hpp" + +#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 "umrr_ros2_msgs/msg/can_object_header.hpp" +#include "umrr_ros2_msgs/msg/can_target_header.hpp" +#include "umrr_ros2_msgs/msg/port_object_header.hpp" +#include "umrr_ros2_msgs/msg/port_target_header.hpp" +#include "umrr_ros2_msgs/srv/firmware_download.hpp" +#include "umrr_ros2_msgs/srv/send_command.hpp" +#include "umrr_ros2_msgs/srv/set_ip.hpp" +#include "umrr_ros2_msgs/srv/set_mode.hpp" +#include "umrr_ros2_msgs/srv/get_status.hpp" +#include "umrr_ros2_msgs/srv/get_mode.hpp" + +namespace smartmicro +{ +namespace drivers +{ +namespace radar +{ +namespace detail +{ +constexpr auto kMaxSensorCount = 10UL; +constexpr auto kMaxHwCount = 6UL; + +struct SensorConfig +{ + std::uint32_t id{}; + std::uint32_t dev_id{}; + std::string frame_id{}; + std::uint32_t history_size{}; + std::string ip{}; + std::string link_type{}; + std::string model{}; + std::uint32_t port{}; + std::string inst_type{}; + std::string data_type{}; + std::string uifname{}; + std::uint32_t uifmajorv{}; + std::uint32_t uifminorv{}; + std::uint32_t uifpatchv{}; + std::string pub_type{}; +}; + +struct HWConfig +{ + std::uint32_t hw_dev_id{}; + std::string hw_iface_name{}; + std::string hw_type{}; + std::uint32_t baudrate{}; + std::uint32_t port{}; +}; +} // namespace detail + +/// +/// @brief The class for the Smartmicro radar node. +/// +class UMRR_ROS2_DRIVER_PUBLIC SmartmicroRadarNode : public ::rclcpp::Node +{ +public: + /// + /// ROS 2 parameter constructor. + /// + /// @param[in] node_options Node options for this node. + /// + explicit SmartmicroRadarNode(const rclcpp::NodeOptions & node_options); + +protected: + /// + /// @brief A timer to handle the services. + /// + void my_timer_callback() { timer->cancel(); } + + /// + /// @brief A callback to initate shutdown. + /// + void on_shutdown_callback(); + +private: + /// + /// @brief A callback that is called when a new object list port for + /// umrra4_v1_0_0 T171 MSE arrives. + /// + /// @param[in] sensor_idx The sensor id for respective published topic. + /// @param[in] objectlist_port_umrra4_mse_v1_0_0 The object list port + /// @param[in] client_id The client_id of the sensor + /// + void objectlist_callback_umrra4_mse_v1_0_0( + const std::uint32_t sensor_idx, + const std::shared_ptr & + objectlist_port_umrra4_mse_v1_0_0, + const com::types::ClientId client_id); + + /// + /// @brief A callback that is called when a new target list port for + /// umrra4_v1_0_0 T171 MSE arrives. + /// + /// @param[in] sensor_idx The sensor id for respective published topic. + /// @param[in] targetlist_port_umrra4_mse_v1_0_0 The target list port + /// @param[in] client_id The client_id of the sensor + /// + void targetlist_callback_umrra4_mse_v1_0_0( + const std::uint32_t sensor_idx, + const std::shared_ptr & + targetlist_port_umrra4_mse_v1_0_0, + const com::types::ClientId client_id); + + + /// + /// @brief A callback that is called when a new object list port for + /// umrra4_v2_1_0 T171 MSE arrives. + /// + /// @param[in] sensor_idx The sensor id for respective published topic. + /// @param[in] objectlist_port_umrra4_mse_v2_1_0 The object list port + /// @param[in] client_id The client_id of the sensor + /// + void objectlist_callback_umrra4_mse_v2_1_0( + const std::uint32_t sensor_idx, + const std::shared_ptr & + objectlist_port_umrra4_mse_v2_1_0, + const com::types::ClientId client_id); + + /// + /// @brief A callback that is called when a new target list port for + /// umrra4_v2_1_0 T171 MSE arrives. + /// + /// @param[in] sensor_idx The sensor id for respective published topic. + /// @param[in] targetlist_port_umrra4_mse_v2_1_0 The target list port + /// @param[in] client_id The client_id of the sensor + /// + void targetlist_callback_umrra4_mse_v2_1_0( + const std::uint32_t sensor_idx, + const std::shared_ptr & + targetlist_port_umrra4_mse_v2_1_0, + const com::types::ClientId client_id); + + + + /// + /// @brief A callback that is called when a new object list port for + /// umrr9f_v1_0_0 T169 MSE arrives. + /// + /// @param[in] sensor_idx The sensor id for respective published topic. + /// @param[in] objectlist_port_umrr9f_mse_v1_0_0 The object list port + /// @param[in] client_id The client_id of the sensor + /// + void objectlist_callback_umrr9f_mse_v1_0_0( + const std::uint32_t sensor_idx, + const std::shared_ptr & + objectlist_port_umrr9f_mse_v1_0_0, + const com::types::ClientId client_id); + + /// + /// @brief A callback that is called when a new target list port for + /// umrr9f_v1_0_0 T169 MSE arrives. + /// + /// @param[in] sensor_idx The sensor id for respective published topic. + /// @param[in] targetlist_port_umrr9f_mse_v1_0_0 The target list port + /// @param[in] client_id The client_id of the sensor + /// + void targetlist_callback_umrr9f_mse_v1_0_0( + const std::uint32_t sensor_idx, + const std::shared_ptr & + targetlist_port_umrr9f_mse_v1_0_0, + const com::types::ClientId client_id); + + /// + /// @brief A callback that is called when a new object list port for + /// umrr9f_v1_1_0 T169 MSE arrives. + /// + /// @param[in] sensor_idx The sensor id for respective published topic. + /// @param[in] objectlist_port_umrr9f_mse_v1_1_0 The object list port + /// @param[in] client_id The client_id of the sensor + /// + void objectlist_callback_umrr9f_mse_v1_1_0( + const std::uint32_t sensor_idx, + const std::shared_ptr & + objectlist_port_umrr9f_mse_v1_1_0, + const com::types::ClientId client_id); + + /// + /// @brief A callback that is called when a new target list port for + /// umrr9f_v1_1_0 T169 MSE arrives. + /// + /// @param[in] sensor_idx The sensor id for respective published topic. + /// @param[in] targetlist_port_umrr9f_mse_v1_1_0 The target list port + /// @param[in] client_id The client_id of the sensor + /// + void targetlist_callback_umrr9f_mse_v1_1_0( + const std::uint32_t sensor_idx, + const std::shared_ptr & + targetlist_port_umrr9f_mse_v1_1_0, + const com::types::ClientId client_id); + + /// + /// @brief A callback that is called when a new object list port for + /// umrr9f_v1_3_0 T169 MSE arrives. + /// + /// @param[in] sensor_idx The sensor id for respective published topic. + /// @param[in] objectlist_port_umrr9f_mse_v1_3_0 The object list port + /// @param[in] client_id The client_id of the sensor + /// + void objectlist_callback_umrr9f_mse_v1_3_0( + const std::uint32_t sensor_idx, + const std::shared_ptr & + objectlist_port_umrr9f_mse_v1_3_0, + const com::types::ClientId client_id); + + /// + /// @brief A callback that is called when a new target list port for + /// umrr9f_v1_3_0 T169 MSE arrives. + /// + /// @param[in] sensor_idx The sensor id for respective published topic. + /// @param[in] targetlist_port_umrr9f_mse_v1_3_0 The target list port + /// @param[in] client_id The client_id of the sensor + /// + void targetlist_callback_umrr9f_mse_v1_3_0( + const std::uint32_t sensor_idx, + const std::shared_ptr & + targetlist_port_umrr9f_mse_v1_3_0, + const com::types::ClientId client_id); + + /// + /// @brief A callback that is called when a new target list port for + /// umrr11 T132 arrives. + /// + /// @param[in] sensor_idx The sensor id for respective published topic. + /// @param[in] targetlist_port_umrr11 The target list port + /// @param[in] client_id The client_id of the sensor + /// + void targetlist_callback_umrr11( + const std::uint32_t sensor_idx, + const std::shared_ptr< + com::master::umrr11_t132_automotive_v1_1_2::comtargetlist::ComTargetList> & + targetlist_port_umrr11, + const com::types::ClientId client_id); + + /// + /// @brief A callback that is called when a new target list port for + /// umrr96 T153 arrives. + /// + /// @param[in] sensor_idx The sensor id for respective published topic. + /// @param[in] targetlist_port_umrr96 The target list port + /// @param[in] client_id The client_id of the sensor + /// + void targetlist_callback_umrr96( + const std::uint32_t sensor_idx, + const std::shared_ptr< + com::master::umrr96_t153_automotive_v1_2_2::comtargetlist::ComTargetList> & + targetlist_port_umrr96, + const com::types::ClientId client_id); + + /// + /// @brief A callback that is called when a new target list port for + /// umrr9f_v1_1_1 T169 arrives. + /// + /// @param[in] sensor_idx The sensor id for respective published topic. + /// @param[in] targetlist_port_umrr9f_v1_1_1 The target list port + /// @param[in] client_id The client_id of the sensor + /// + void targetlist_callback_umrr9f_v1_1_1( + const std::uint32_t sensor_idx, + const std::shared_ptr< + com::master::umrr9f_t169_automotive_v1_1_1::comtargetlistport::ComTargetListPort> & + targetlist_port_umrr9f_v1_1_1, + const com::types::ClientId client_id); + + /// + /// @brief A callback that is called when a new target list port for + /// umrr9f_v2_0_0 T169 arrives. + /// + /// @param[in] sensor_idx The sensor id for respective published topic. + /// @param[in] targetlist_port_umrr9f_v2_0_0 The target list port + /// @param[in] client_id The client_id of the sensor + /// + void targetlist_callback_umrr9f_v2_0_0( + const std::uint32_t sensor_idx, + const std::shared_ptr< + com::master::umrr9f_t169_automotive_v2_0_0::comtargetlistport::ComTargetListPort> & + targetlist_port_umrr9f_v2_0_0, + const com::types::ClientId client_id); + + /// + /// @brief A callback that is called when a new target list port for + /// umrr9f_v2_1_1 T169 arrives. + /// + /// @param[in] sensor_idx The sensor id for respective published topic. + /// @param[in] targetlist_port_umrr9f_v2_1_1 The target list port + /// @param[in] client_id The client_id of the sensor + /// + void targetlist_callback_umrr9f_v2_1_1( + const std::uint32_t sensor_idx, + const std::shared_ptr< + com::master::umrr9f_t169_automotive_v2_1_1::comtargetlist::ComTargetList> & + targetlist_port_umrr9f_v2_1_1, + const com::types::ClientId client_id); + + /// + /// @brief A callback that is called when a new target list port for + /// umrr9f_v2_2_1 T169 arrives. + /// + /// @param[in] sensor_idx The sensor id for respective published topic. + /// @param[in] targetlist_port_umrr9f_v2_2_1 The target list port + /// @param[in] client_id The client_id of the sensor + /// + void targetlist_callback_umrr9f_v2_2_1( + const std::uint32_t sensor_idx, + const std::shared_ptr< + com::master::umrr9f_t169_automotive_v2_2_1::comtargetlist::ComTargetList> & + targetlist_port_umrr9f_v2_2_1, + const com::types::ClientId client_id); + + /// + /// @brief A callback that is called when a new target list port for + /// umrr9f_v2_4_1 T169 arrives. + /// + /// @param[in] sensor_idx The sensor id for respective published topic. + /// @param[in] targetlist_port_umrr9f_v2_4_1 The target list port + /// @param[in] client_id The client_id of the sensor + /// + void targetlist_callback_umrr9f_v2_4_1( + const std::uint32_t sensor_idx, + const std::shared_ptr< + com::master::umrr9f_t169_automotive_v2_4_1::comtargetlist::ComTargetList> & + targetlist_port_umrr9f_v2_4_1, + const com::types::ClientId client_id); + + /// + /// @brief A callback that is called when a new target list port for + /// umrr9f_v3_0_0 T169 arrives. + /// + /// @param[in] sensor_idx The sensor id for respective published topic. + /// @param[in] targetlist_port_umrr9f_v3_0_0 The target list port + /// @param[in] client_id The client_id of the sensor + /// + void targetlist_callback_umrr9f_v3_0_0( + const std::uint32_t sensor_idx, + const std::shared_ptr< + com::master::umrr9f_t169_automotive_v3_0_0::comtargetlist::ComTargetList> & + targetlist_port_umrr9f_v3_0_0, + const com::types::ClientId client_id); + + /// + /// @brief A callback that is called when a new target list port for + /// umrr9d_v1_0_3 T152 arrives. + /// + /// @param[in] sensor_idx The sensor id for respective published topic. + /// @param[in] targetlist_port_umrr9d_v1_0_3 The target list port + /// @param[in] client_id The client_id of the sensor + /// + void targetlist_callback_umrr9d_v1_0_3( + const std::uint32_t sensor_idx, + const std::shared_ptr< + com::master::umrr9d_t152_automotive_v1_0_3::comtargetlist::ComTargetList> & + targetlist_port_umrr9d_v1_0_3, + const com::types::ClientId client_id); + + /// + /// @brief A callback that is called when a new target list port for + /// umrr9d_v1_2_2 T152 arrives. + /// + /// @param[in] sensor_idx The sensor id for respective published topic. + /// @param[in] targetlist_port_umrr9d_v1_2_2 The target list port + /// @param[in] client_id The client_id of the sensor + /// + void targetlist_callback_umrr9d_v1_2_2( + const std::uint32_t sensor_idx, + const std::shared_ptr< + com::master::umrr9d_t152_automotive_v1_2_2::comtargetlist::ComTargetList> & + targetlist_port_umrr9d_v1_2_2, + const com::types::ClientId client_id); + + /// + /// @brief A callback that is called when a new target list port for + /// umrr9d_v1_4_1 T152 arrives. + /// @param[in] sensor_idx The sensor id for the respected published topic. + /// @param[in] targetlist_port_umrr9d_v1_4_1 The target list port + /// @param[in] client_id The client_id of the sensor + /// + void targetlist_callback_umrr9d_v1_4_1( + const std::uint32_t sensor_idx, + const std::shared_ptr< + com::master::umrr9d_t152_automotive_v1_4_1::comtargetlist::ComTargetList> & + targetlist_port_umrr9d_v1_4_1, + const com::types::ClientId client_id); + + /// + /// @brief A callback that is called when a new target list port for + /// umrr9d_v1_5_0 T152 arrives. + /// @param[in] sensor_idx The sensor id for the respected published topic. + /// @param[in] targetlist_port_umrr9d_v1_5_0 The target list port + /// @param[in] client_id The client_id of the sensor + /// + void targetlist_callback_umrr9d_v1_5_0( + const std::uint32_t sensor_idx, + const std::shared_ptr< + com::master::umrr9d_t152_automotive_v1_5_0::comtargetlist::ComTargetList> & + targetlist_port_umrr9d_v1_5_0, + const com::types::ClientId client_id); + + /// + /// @brief A callback that is called when a new target list port for + /// umrra4_v1_0_1 T171 arrives. + /// @param[in] sensor_idx The sensor id for the respected published topic. + /// @param[in] targetlist_port_umrra4_v1_0_1 The target list port + /// @param[in] client_id The client_id of the sensor + /// + void targetlist_callback_umrra4_v1_0_1( + const std::uint32_t sensor_idx, + const std::shared_ptr & + targetlist_port_umrra4_v1_0_1, + const com::types::ClientId client_id); + + /// + /// @brief A callback that is called when a new target list port for + /// umrra4_v1_2_1 T171 arrives. + /// @param[in] sensor_idx The sensor id for the respected published topic. + /// @param[in] targetlist_port_umrra4_v1_2_1 The target list port + /// @param[in] client_id The client_id of the sensor + /// + void targetlist_callback_umrra4_v1_2_1( + const std::uint32_t sensor_idx, + const std::shared_ptr & + targetlist_port_umrra4_v1_2_1, + const com::types::ClientId client_id); + + /// + /// @brief A callback that is called when a new target list port for + /// umrra4_v1_4_0 T171 arrives. + /// @param[in] sensor_idx The sensor id for the respected published topic. + /// @param[in] targetlist_port_umrra4_v1_4_0 The target list port + /// @param[in] client_id The client_id of the sensor + /// + void targetlist_callback_umrra4_v1_4_0( + const std::uint32_t sensor_idx, + const std::shared_ptr & + targetlist_port_umrra4_v1_4_0, + const com::types::ClientId client_id); + + /// + /// @brief A callback that is called when a new target list port for + /// umrra1_v1_0_0 T166 arrives. + /// @param[in] sensor_idx The sensor id for the respected published topic. + /// @param[in] targetlist_port_umrra1_v1_0_0 The target list port + /// @param[in] client_id The client_id of the sensor + /// + void targetlist_callback_umrra1_v1_0_0( + const std::uint32_t sensor_idx, + const std::shared_ptr & + targetlist_port_umrra1_v1_0_0, + const com::types::ClientId client_id); + + /// + /// @brief A callback that is called when a new target list port for + /// umrra1_v2_0_0 T166 arrives. + /// @param[in] sensor_idx The sensor id for the respected published topic. + /// @param[in] targetlist_port_umrra1_v2_0_0 The target list port + /// @param[in] client_id The client_id of the sensor + /// + void targetlist_callback_umrra1_v2_0_0( + const std::uint32_t sensor_idx, + const std::shared_ptr & + targetlist_port_umrra1_v2_0_0, + const com::types::ClientId client_id); + + /// + /// @brief A callback that is called when a new CAN object list for + /// umrra4_v1_0_0 T171 MSE arrives. + /// + /// @param[in] sensor_idx The sensor id for respective published topic. + /// @param[in] objectlist_can_umrra4_mse_v1_0_0 The object list port + /// @param[in] client_id The client_id of the sensor + /// + /// + void CAN_objectlist_callback_umrra4_mse_v1_0_0( + const std::uint32_t sensor_idx, + const std::shared_ptr & + objectlist_can_umrra4_mse_v1_0_0, + const com::types::ClientId client_id); + + /// + /// @brief A callback that is called when a new CAN target list for + /// umrra4_mse_v1_0_0 T171 MSE arrives. + /// @param[in] sensor_idx The sensor id for respective published topic. + /// @param[in] targetlist_can_umrra4_mse_v1_0_0 The target list port + /// @param[in] client_id The client_id of the sensor. + /// + /// + void CAN_targetlist_callback_umrra4_mse_v1_0_0( + const std::uint32_t sensor_idx, + const std::shared_ptr & + targetlist_can_umrra4_mse_v1_0_0, + const com::types::ClientId client_id); + + + /// + /// @brief A callback that is called when a new CAN object list for + /// umrra4_v2_1_0 T171 MSE arrives. + /// + /// @param[in] sensor_idx The sensor id for respective published topic. + /// @param[in] objectlist_can_umrra4_mse_v2_1_0 The object list port + /// @param[in] client_id The client_id of the sensor + /// + /// + void CAN_objectlist_callback_umrra4_mse_v2_1_0( + const std::uint32_t sensor_idx, + const std::shared_ptr & + objectlist_can_umrra4_mse_v2_1_0, + const com::types::ClientId client_id); + + /// + /// @brief A callback that is called when a new CAN target list for + /// umrra4_mse_v2_1_0 T171 MSE arrives. + /// @param[in] sensor_idx The sensor id for respective published topic. + /// @param[in] targetlist_can_umrra4_mse_v2_1_0 The target list port + /// @param[in] client_id The client_id of the sensor. + /// + /// + void CAN_targetlist_callback_umrra4_mse_v2_1_0( + const std::uint32_t sensor_idx, + const std::shared_ptr & + targetlist_can_umrra4_mse_v2_1_0, + const com::types::ClientId client_id); + + + /// + /// @brief A callback that is called when a new CAN object list for + /// umrr9f_v1_0_0 T169 MSE arrives. + /// + /// @param[in] sensor_idx The sensor id for respective published topic. + /// @param[in] objectlist_can_umrr9f_mse_v1_0_0 The object list port + /// @param[in] client_id The client_id of the sensor + /// + /// + void CAN_objectlist_callback_umrr9f_mse_v1_0_0( + const std::uint32_t sensor_idx, + const std::shared_ptr< + com::master::umrr9f_t169_mse_v1_0_0::comobjectbaselist::ComObjectBaseList> & + objectlist_can_umrr9f_mse_v1_0_0, + const com::types::ClientId client_id); + + /// + /// @brief A callback that is called when a new CAN target list for + /// umrr9f_mse_v1_0_0 T169 MSE arrives. + /// @param[in] sensor_idx The sensor id for respective published topic. + /// @param[in] targetlist_can_umrr9f_mse_v1_0_0 The target list port + /// @param[in] client_id The client_id of the sensor. + /// + /// + void CAN_targetlist_callback_umrr9f_mse_v1_0_0( + const std::uint32_t sensor_idx, + const std::shared_ptr< + com::master::umrr9f_t169_mse_v1_0_0::comtargetbaselist::ComTargetBaseList> & + targetlist_can_umrr9f_mse_v1_0_0, + const com::types::ClientId client_id); + + /// + /// @brief A callback that is called when a new CAN object list for + /// umrr9f_v1_1_0 T169 MSE arrives. + /// + /// @param[in] sensor_idx The sensor id for respective published topic. + /// @param[in] objectlist_can_umrr9f_mse_v1_1_0 The object list port + /// @param[in] client_id The client_id of the sensor + /// + /// + void CAN_objectlist_callback_umrr9f_mse_v1_1_0( + const std::uint32_t sensor_idx, + const std::shared_ptr< + com::master::umrr9f_t169_mse_v1_1_0::comobjectbaselist::ComObjectBaseList> & + objectlist_can_umrr9f_mse_v1_1_0, + const com::types::ClientId client_id); + + /// + /// @brief A callback that is called when a new CAN target list for + /// umrr9f_mse_v1_1_0 T169 MSE arrives. + /// @param[in] sensor_idx The sensor id for respective published topic. + /// @param[in] targetlist_can_umrr9f_mse_v1_1_0 The target list port + /// @param[in] client_id The client_id of the sensor. + /// + /// + void CAN_targetlist_callback_umrr9f_mse_v1_1_0( + const std::uint32_t sensor_idx, + const std::shared_ptr< + com::master::umrr9f_t169_mse_v1_1_0::comtargetbaselist::ComTargetBaseList> & + targetlist_can_umrr9f_mse_v1_1_0, + const com::types::ClientId client_id); + + /// + /// @brief A callback that is called when a new CAN object list for + /// umrr9f_v1_3_0 T169 MSE arrives. + /// + /// @param[in] sensor_idx The sensor id for respective published topic. + /// @param[in] objectlist_can_umrr9f_mse_v1_3_0 The object list port + /// @param[in] client_id The client_id of the sensor + /// + /// + void CAN_objectlist_callback_umrr9f_mse_v1_3_0( + const std::uint32_t sensor_idx, + const std::shared_ptr< + com::master::umrr9f_t169_mse_v1_3_0::comobjectbaselist::ComObjectBaseList> & + objectlist_can_umrr9f_mse_v1_3_0, + const com::types::ClientId client_id); + + /// + /// @brief A callback that is called when a new CAN target list for + /// umrr9f_mse_v1_3_0 T169 MSE arrives. + /// @param[in] sensor_idx The sensor id for respective published topic. + /// @param[in] targetlist_can_umrr9f_mse_v1_3_0 The target list port + /// @param[in] client_id The client_id of the sensor. + /// + /// + void CAN_targetlist_callback_umrr9f_mse_v1_3_0( + const std::uint32_t sensor_idx, + const std::shared_ptr< + com::master::umrr9f_t169_mse_v1_3_0::comtargetbaselist::ComTargetBaseList> & + targetlist_can_umrr9f_mse_v1_3_0, + const com::types::ClientId client_id); + + /// + /// @brief A callback that is called when a new CAN target list for + /// umrr96_v1_2_2 T153 arrives. + /// + /// @param[in] sensor_idx The sensor id for respective published topic. + /// @param[in] targetlist_can_umrr96 The target list port + /// @param[in] client_id The client_id of the sensor + /// + void CAN_targetlist_callback_umrr96( + const std::uint32_t sensor_idx, + const std::shared_ptr< + com::master::umrr96_t153_automotive_v1_2_2::comtargetbaselist::ComTargetBaseList> & + targetlist_can_umrr96, + const com::types::ClientId client_id); + + /// @brief A callback that is called when a new CAN target list for + /// umrr11_v1_1_2 T132 arrives. + /// + /// @param[in] sensor_idx The sensor id for respective published topic. + /// @param[in] targetlist_can_umrr11 The target list port + /// @param[in] client_id The client_id of the sensor + /// + void CAN_targetlist_callback_umrr11( + const std::uint32_t sensor_idx, + const std::shared_ptr< + com::master::umrr11_t132_automotive_v1_1_2::comtargetbaselist::ComTargetBaseList> & + targetlist_can_umrr11, + const com::types::ClientId client_id); + + /// + /// @brief A callback that is called when a new CAN target list for + /// umrr9f_v2_1_1 T169 arrives. + /// + /// @param[in] sensor_idx The sensor id for respective published topic. + /// @param[in] targetlist_can_umrr9f_v2_1_1 The target list port + /// @param[in] client_id The client_id of the sensor + /// + void CAN_targetlist_callback_umrr9f_v2_1_1( + const std::uint32_t sensor_idx, + const std::shared_ptr< + com::master::umrr9f_t169_automotive_v2_1_1::comtargetbaselist::ComTargetBaseList> & + targetlist_can_umrr9f_v2_1_1, + const com::types::ClientId client_id); + + /// + /// @brief A callback that is called when a new CAN target list for + /// umrr9f_v2_2_1 T169 arrives. + /// + /// @param[in] sensor_idx The sensor id for respective published topic. + /// @param[in] targetlist_can_umrr9f_v2_2_1 The target list port + /// @param[in] client_id The client_id of the sensor + /// + void CAN_targetlist_callback_umrr9f_v2_2_1( + const std::uint32_t sensor_idx, + const std::shared_ptr< + com::master::umrr9f_t169_automotive_v2_2_1::comtargetbaselist::ComTargetBaseList> & + targetlist_can_umrr9f_v2_2_1, + const com::types::ClientId client_id); + + /// + /// @brief A callback that is called when a new CAN target list for + /// umrr9f_v2_4_1 T169 arrives. + /// + /// @param[in] sensor_idx The sensor id for respective published topic. + /// @param[in] targetlist_can_umrr9f_v2_4_1 The target list port + /// @param[in] client_id The client_id of the sensor + /// + void CAN_targetlist_callback_umrr9f_v2_4_1( + const std::uint32_t sensor_idx, + const std::shared_ptr< + com::master::umrr9f_t169_automotive_v2_4_1::comtargetbaselist::ComTargetBaseList> & + targetlist_can_umrr9f_v2_4_1, + const com::types::ClientId client_id); + + + /// + /// @brief A callback that is called when a new CAN target list for + /// umrr9f_v3_0_0 T169 arrives. + /// + /// @param[in] sensor_idx The sensor id for respective published topic. + /// @param[in] targetlist_can_umrr9f_v3_0_0 The target list port + /// @param[in] client_id The client_id of the sensor + /// + void CAN_targetlist_callback_umrr9f_v3_0_0( + const std::uint32_t sensor_idx, + const std::shared_ptr< + com::master::umrr9f_t169_automotive_v3_0_0::comtargetbaselist::ComTargetBaseList> & + targetlist_can_umrr9f_v3_0_0, + const com::types::ClientId client_id); + + + /// + /// @brief A callback that is called when a new CAN target list for + /// umrr9d_v1_0_3 T152 arrives. + /// + /// @param[in] sensor_idx The sensor id for respective published topic. + /// @param[in] targetlist_can_umrr9d_v1_0_3 The target list port + /// @param[in] client_id The client_id of the sensor + /// + void CAN_targetlist_callback_umrr9d_v1_0_3( + const std::uint32_t sensor_idx, + const std::shared_ptr< + com::master::umrr9d_t152_automotive_v1_0_3::comtargetbaselist::ComTargetBaseList> & + targetlist_can_umrr9d_v1_0_3, + const com::types::ClientId client_id); + + /// + /// @brief A callback that is called when a new CAN target list for + /// umrr9d_v1_2_2 T152 arrives. + /// + /// @param[in] sensor_idx The sensor id for respective published topic. + /// @param[in] targetlist_can_umrr9d_v1_2_2 The target list port + /// @param[in] client_id The client_id of the sensor + /// + void CAN_targetlist_callback_umrr9d_v1_2_2( + const std::uint32_t sensor_idx, + const std::shared_ptr< + com::master::umrr9d_t152_automotive_v1_2_2::comtargetbaselist::ComTargetBaseList> & + targetlist_can_umrr9d_v1_2_2, + const com::types::ClientId client_id); + + /// + /// @brief A callback that is called when a new CAN target list for + /// umrr9d_v1_4_1 T152 arrives. + /// + /// @param[in] sensor_idx The sensor id for respective published topic. + /// @param[in] targetlist_can_umrr9d_v1_4_1 The target list port + /// @param[in] client_id The client_id of the sensor + /// + void CAN_targetlist_callback_umrr9d_v1_4_1( + const std::uint32_t sensor_idx, + const std::shared_ptr< + com::master::umrr9d_t152_automotive_v1_4_1::comtargetbaselist::ComTargetBaseList> & + targetlist_can_umrr9d_v1_4_1, + const com::types::ClientId client_id); + + /// + /// @brief A callback that is called when a new CAN target list for + /// umrr9d_v1_5_0 T152 arrives. + /// + /// @param[in] sensor_idx The sensor id for respective published topic. + /// @param[in] targetlist_can_umrr9d_v1_5_0 The target list port + /// @param[in] client_id The client_id of the sensor + /// + void CAN_targetlist_callback_umrr9d_v1_5_0( + const std::uint32_t sensor_idx, + const std::shared_ptr< + com::master::umrr9d_t152_automotive_v1_5_0::comtargetbaselist::ComTargetBaseList> & + targetlist_can_umrr9d_v1_5_0, + const com::types::ClientId client_id); + + /// + /// @brief A callback that is called when a new CAN target list for + /// umrra4_v1_0_1 T171 arrives. + /// + /// @param[in] sensor_idx The sensor id for respective published topic. + /// @param[in] targetlist_can_umrra4_v1_0_1 The target list port + /// @param[in] client_id The client_id of the sensor + /// + void CAN_targetlist_callback_umrra4_v1_0_1( + const std::uint32_t sensor_idx, + const std::shared_ptr< + com::master::umrra4_automotive_v1_0_1::comtargetbaselist::ComTargetBaseList> & + targetlist_can_umrra4_v1_0_1, + const com::types::ClientId client_id); + + /// + /// @brief A callback that is called when a new CAN target list for + /// umrra4_v1_2_1 T171 arrives. + /// + /// @param[in] sensor_idx The sensor id for respective published topic. + /// @param[in] targetlist_can_umrra4_v1_2_1 The target list port + /// @param[in] client_id The client_id of the sensor + /// + void CAN_targetlist_callback_umrra4_v1_2_1( + const std::uint32_t sensor_idx, + const std::shared_ptr< + com::master::umrra4_automotive_v1_2_1::comtargetbaselist::ComTargetBaseList> & + targetlist_can_umrra4_v1_2_1, + const com::types::ClientId client_id); + + /// + /// @brief A callback that is called when a new CAN target list for + /// umrra4_v1_4_0 T171 arrives. + /// + /// @param[in] sensor_idx The sensor id for respective published topic. + /// @param[in] targetlist_can_umrra4_v1_4_0 The target list port + /// @param[in] client_id The client_id of the sensor + /// + void CAN_targetlist_callback_umrra4_v1_4_0( + const std::uint32_t sensor_idx, + const std::shared_ptr< + com::master::umrra4_automotive_v1_4_0::comtargetbaselist::ComTargetBaseList> & + targetlist_can_umrra4_v1_4_0, + const com::types::ClientId client_id); + + /// + /// @brief Read parameters and update the json config files required by + /// Smart Access C++ API. + /// + void update_config_files_from_params(); + + /// + /// @brief Creates publishers for sensors using ports. + /// + /// @param[in] sensor The sensor configuration. + /// @param[in] sensor_idx The sensor index. + /// + void port_publishers(const detail::SensorConfig & sensor, size_t sensor_idx); + + /// + /// @brief Creates publishers for sensors using CAN. + /// + /// @param[in] sensor The sensor configuration. + /// @param[in] sensor_idx The sensor index. + /// + void can_publishers(const detail::SensorConfig & sensor, size_t sensor_idx); + + /// + /// @brief Callback for getting the parameter response. + /// + /// @param[in] client_id The client identifier. + /// @param[in] response The response batch. + /// @param[in] instruction_name The instruction name. + /// + void mode_response( + const com::types::ClientId client_id, + const std::shared_ptr & response, + const std::vector & instruction_names, + const std::string & section_name); + + /// + /// @brief Callback for getting the command response. + /// + /// @param[in] client_id The client identifier. + /// @param[in] response The response batch. + /// @param[in] command_name The command name. + /// + void command_response( + const com::types::ClientId client_id, + const std::shared_ptr & response, const std::string command_name, + const std::string & section_name); + + /// + /// @brief Callback for changing IP address. + /// + /// @param[in] client_id The client identifier. + /// @param[in] response The response batch. + /// + void sensor_response_ip( + const com::types::ClientId client_id, + const std::shared_ptr & response); + + /// + /// @brief Sends instructions to the sensor. + /// + /// @param[in] request The request. + /// @param[out] response The response. + /// + void set_radar_mode( + const std::shared_ptr request, + std::shared_ptr response); + + /// + /// @brief Configures the sensor IP address. + /// + /// @param[in] request The request. + /// @param[out] response The response. + /// + void ip_address( + const std::shared_ptr request, + std::shared_ptr response); + + /// + /// @brief Sends command to the sensor. + /// + /// @param[in] request The request. + /// @param[out] response The response. + /// + void radar_command( + const std::shared_ptr request, + std::shared_ptr response); + + /// + /// @brief Service for firmware download. + /// + /// @param[in] request The request. + /// @param[out] result The result. + /// + void firmware_download( + const std::shared_ptr request, + std::shared_ptr result); + + /// + /// @brief Converts a timestamp from microseconds to seconds and nanoseconds. + /// + /// @param timestamp The input timestamp in microseconds as a `std::chrono::microseconds`. + /// @return A `std::pair` where: + /// - `first` is the number of seconds (`int32_t`). + /// - `second` is the number of nanoseconds (`uint32_t`). + /// + inline std::pair convert_timestamp(std::chrono::microseconds timestamp) + { + const auto sec = std::chrono::duration_cast(timestamp); + const auto nanosec = std::chrono::duration_cast(timestamp - sec); + return {sec.count(), nanosec.count()}; + } + + /// + /// @brief Initializes all the smart access and ros2 services. + /// + void initialize_services(); + + /// + /// @brief Check and set up publsihers w.r.t defined radar parameters. + /// + void setup_publishers(); + + /// + /// @brief Service to get sensor status. + /// + /// @param[in] request The request. + /// @param[out] response The response. + /// + void get_radar_status( + const std::shared_ptr request, + std::shared_ptr response); + + /// + /// @brief Service to get sensor modes. + /// + /// @param[in] request The request. + /// @param[out] response The response. + /// + void get_radar_mode( + const std::shared_ptr request, + std::shared_ptr response); + + /// + /// @brief Callback for getting the status response. + /// + /// @param[in] client_id The client identifier. + /// @param[in] response The response batch. + /// @param[in] statuses The status name of the sensor. + /// @param[in] section_name The section name of the interface. + /// + void status_response( + const com::types::ClientId client_id, + const std::shared_ptr & response, + const std::vector & statuses, + const std::string & section_name); + + /// + /// @brief Callback for getting the reading param response. + /// + /// @param[in] client_id The client identifier. + /// @param[in] response The response batch. + /// @param[in] params The param name of the sensor. + /// + void param_response( + const com::types::ClientId client_id, + const std::shared_ptr & response, + const std::vector & params, + const std::string & section_name); + + rclcpp::Service::SharedPtr mode_srv_; + rclcpp::Service::SharedPtr ip_addr_srv_; + rclcpp::Service::SharedPtr command_srv_; + rclcpp::Service::SharedPtr download_srv_; + rclcpp::Service::SharedPtr status_srv_; + rclcpp::Service::SharedPtr read_mode_srv_; + + std::array m_sensors{}; + std::array m_adapters{}; + + std::array::SharedPtr, detail::kMaxSensorCount> + m_publishers_obj{}; + + std::array< + rclcpp::Publisher::SharedPtr, detail::kMaxSensorCount> + m_publishers_port_obj_header{}; + + std::array< + rclcpp::Publisher::SharedPtr, detail::kMaxSensorCount> + m_publishers_can_obj_header{}; + + std::array::SharedPtr, detail::kMaxSensorCount> + m_publishers{}; + + std::array< + rclcpp::Publisher::SharedPtr, detail::kMaxSensorCount> + m_publishers_port_target_header{}; + + std::array< + rclcpp::Publisher::SharedPtr, detail::kMaxSensorCount> + m_publishers_can_target_header{}; + + std::size_t m_number_of_sensors{}; + std::size_t m_number_of_adapters{}; + rclcpp::TimerBase::SharedPtr timer; + com::types::ClientId client_id; + std::uint64_t response_type{}; + std::shared_ptr update_service; +}; + +bool check_signal = false; +std::string update_image{}; +std::shared_ptr m_services{}; +std::shared_ptr data_umrra4_v1_0_1{}; +std::shared_ptr data_umrra4_v1_2_1{}; +std::shared_ptr data_umrra4_v1_4_0{}; +std::shared_ptr data_umrr11{}; +std::shared_ptr data_umrr96{}; +std::shared_ptr + data_umrr9f_v1_1_1{}; +std::shared_ptr + data_umrr9f_v2_0_0{}; +std::shared_ptr + data_umrr9f_v2_1_1{}; +std::shared_ptr + data_umrr9f_v2_2_1{}; +std::shared_ptr + data_umrr9f_v2_4_1{}; +std::shared_ptr + data_umrr9f_v3_0_0{}; +std::shared_ptr + data_umrr9d_v1_0_3{}; +std::shared_ptr + data_umrr9d_v1_2_2{}; +std::shared_ptr + data_umrr9d_v1_4_1{}; +std::shared_ptr + data_umrr9d_v1_5_0{}; +std::shared_ptr + data_umrr9f_mse_v1_0_0{}; +std::shared_ptr + data_umrr9f_mse_v1_1_0{}; +std::shared_ptr + data_umrr9f_mse_v1_3_0{}; +std::shared_ptr data_umrra4_mse_v1_0_0{}; +std::shared_ptr data_umrra4_mse_v2_1_0{}; +std::shared_ptr + data_umrra1_v1_0_0{}; +std::shared_ptr + data_umrra1_v2_0_0{}; + +} // namespace radar +} // namespace drivers +} // namespace smartmicro + +#endif // UMRR_ROS2_DRIVER__SMARTMICRO_RADAR_NODE_HPP_ diff --git a/umrr_ros2_driver/include/umrr_ros2_driver/update_service.hpp b/umrr_ros2_driver/include/umrr_ros2_driver/update_service.hpp new file mode 100644 index 0000000..abd9ecc --- /dev/null +++ b/umrr_ros2_driver/include/umrr_ros2_driver/update_service.hpp @@ -0,0 +1,33 @@ +#ifndef UPDATE_SERVICE_HPP +#define UPDATE_SERVICE_HPP + +#include +#include +#include + +#include + +#include +#include +#include +#include +#include +#include + +class UpdateService { + public: + UpdateService(); + ~UpdateService() = default; + + void StartSoftwareUpdate(com::types::ClientId client_id, std::string &update_image); + + private: + void UpdateCallback(com::types::SWUpdateInfo &info); + void HandleResult(); + + com::types::SWUpdateInfo updateInfo_; + std::mutex mutex_; + std::condition_variable cv_; +}; + +#endif // UPDATE_SERVICE_HPP diff --git a/umrr_ros2_driver/include/umrr_ros2_driver/visibility_control.hpp b/umrr_ros2_driver/include/umrr_ros2_driver/visibility_control.hpp new file mode 100644 index 0000000..75a5e49 --- /dev/null +++ b/umrr_ros2_driver/include/umrr_ros2_driver/visibility_control.hpp @@ -0,0 +1,37 @@ +// Copyright 2021 Apex.AI, Inc. +// +// 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 UMRR_ROS2_DRIVER__VISIBILITY_CONTROL_HPP_ +#define UMRR_ROS2_DRIVER__VISIBILITY_CONTROL_HPP_ + +//////////////////////////////////////////////////////////////////////////////// +#if defined(__WIN32) + #if defined(UMRR_ROS2_DRIVER_BUILDING_DLL) || defined(UMRR_ROS2_DRIVER_EXPORTS) + #define UMRR_ROS2_DRIVER_PUBLIC __declspec(dllexport) + #define UMRR_ROS2_DRIVER_LOCAL + #else // defined(UMRR_ROS2_DRIVER_BUILDING_DLL) || defined(UMRR_ROS2_DRIVER_EXPORTS) + #define UMRR_ROS2_DRIVER_PUBLIC __declspec(dllimport) + #define UMRR_ROS2_DRIVER_LOCAL + #endif // defined(UMRR_ROS2_DRIVER_BUILDING_DLL) || defined(UMRR_ROS2_DRIVER_EXPORTS) +#elif defined(__linux__) + #define UMRR_ROS2_DRIVER_PUBLIC __attribute__((visibility("default"))) + #define UMRR_ROS2_DRIVER_LOCAL __attribute__((visibility("hidden"))) +#elif defined(__APPLE__) + #define UMRR_ROS2_DRIVER_PUBLIC __attribute__((visibility("default"))) + #define UMRR_ROS2_DRIVER_LOCAL __attribute__((visibility("hidden"))) +#else + #error "Unsupported Build Configuration" +#endif + +#endif // UMRR_ROS2_DRIVER__VISIBILITY_CONTROL_HPP_ diff --git a/umrr_ros2_driver/launch/radar.launch.py b/umrr_ros2_driver/launch/radar.launch.py new file mode 100644 index 0000000..fb0bd5e --- /dev/null +++ b/umrr_ros2_driver/launch/radar.launch.py @@ -0,0 +1,43 @@ +# Copyright (c) 2021, s.m.s, smart microwave sensors GmbH, Brunswick, Germany +# +# 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 os +import launch_ros + +from ament_index_python import get_package_share_directory +from launch_ros.actions import Node + +from launch import LaunchDescription +from launch.actions import DeclareLaunchArgument +from launch.substitutions import LaunchConfiguration + +PACKAGE_NAME = 'umrr_ros2_driver' + + +def generate_launch_description(): + """Generate the launch description.""" + + radar__params = os.path.join( + get_package_share_directory(PACKAGE_NAME), 'param/radar.params.template.yaml') + radar_node = Node( + package=PACKAGE_NAME, + executable='smartmicro_radar_node_exe', + name='smart_radar', + parameters=[radar__params] + ) + return LaunchDescription([ + radar_node + ]) + + diff --git a/umrr_ros2_driver/package.xml b/umrr_ros2_driver/package.xml new file mode 100644 index 0000000..a32d764 --- /dev/null +++ b/umrr_ros2_driver/package.xml @@ -0,0 +1,28 @@ + + + + umrr_ros2_driver + 8.0.0 + A node to publish data from a smartmicro radar. + s.m.s, smart microwave sensors GmbH. + Apache 2.0 License + + ament_cmake_auto + + rclcpp + rclpy + rclcpp_components + sensor_msgs + point_cloud_msg_wrapper + visualization_msgs + umrr_ros2_msgs + + ament_lint_auto + ros_testing + ament_lint_common + ament_cmake_gtest + + + ament_cmake + + diff --git a/umrr_ros2_driver/param/example/radar.adapter.example.yaml b/umrr_ros2_driver/param/example/radar.adapter.example.yaml new file mode 100644 index 0000000..c517a94 --- /dev/null +++ b/umrr_ros2_driver/param/example/radar.adapter.example.yaml @@ -0,0 +1,34 @@ +--- +/**: + ros__parameters: + # This sets the iface_name in the hw_inventory.json + # sudo slcand -o -s6 -t hw -S 3000000 /dev/ttyUSBx + # sudo ip link set up slcan0 + # set buffer length using /sbin/ip link set slcan0 txqueuelen 4096 + # For peak CAN adapters, only required to set baudrate + # sudo ip link set can0 up type can bitrate 500000 + # One sensor per interface for can, for ethernet we can use one interface/dev for multiple sensors + master_inst_serial_type: "can_based" + master_data_serial_type: "can_based" + adapters: + adapter_0: + hw_type: "can" + hw_dev_id: 2 + hw_iface_name: "slcan0" + baudrate: 500000 + adapter_1: + hw_type: "eth" + hw_dev_id: 3 + hw_iface_name: "eth0" + port: 55555 + adapter_2: + hw_type: "can" + hw_dev_id: 4 + hw_iface_name: "slcan1" + baudrate: 500000 + adapter_3: + hw_type: "can" + hw_dev_id: 5 + hw_iface_name: "can0" + baudrate: 500000 + diff --git a/umrr_ros2_driver/param/example/radar.sensor.example.yaml b/umrr_ros2_driver/param/example/radar.sensor.example.yaml new file mode 100644 index 0000000..7a38608 --- /dev/null +++ b/umrr_ros2_driver/param/example/radar.sensor.example.yaml @@ -0,0 +1,122 @@ +--- +/**: + ros__parameters: +# An array of sensors to subscribe to. + sensors: + # As many as 10 sensors all named as "sensor_" in increasing order of numbers, + # e.g., sensor_0, sensor_1, etc. The list must start with sensor_0. + sensor_0: + # The model of the connected sensor. Available models umrr11, umrr9d, umrr96, + # umrr9f_v1_1_1, umrr9f_v2_0_0 + link_type: "can" + model: "umrr9d_can" + # The client_id of the sensor/source, must be a unique integer. + id: 500 + # Adapter id to which sensor is connected + dev_id: 2 + # The interface name of the sensor + uifname: "umrr9d_t152_automotive" + # The major version of the interface + uifmajorv: 1 + # The minor version of the interface + uifminorv: 0 + # The pathc version of the interface + uifpatchv: 3 + # The frame_id to be set to the published messages. + frame_id: "umrr" + # Specify the history size. + history_size: 10 + sensor_1: + # The model of the connected sensor. Available models umrr11, umrr9d, umrr96, + # umrr9f_v1_1_1, umrr9f_v2_0_0 + link_type: "can" + model: "umrr9f_can_v2_1_1" + # The client_id of the sensor/source, must be a unique integer. + id: 600 + # Adapter id to which sensor is connected + dev_id: 4 + # The interface name of the sensor + uifname: "umrr9f_t169_automotive" + # The major version of the interface + uifmajorv: 2 + # The minor version of the interface + uifminorv: 1 + # The pathc version of the interface + uifpatchv: 1 + # The frame_id to be set to the published messages. + frame_id: "umrr" + # Specify the history size. + history_size: 10 + sensor_2: + link_type: "eth" + # The model of the connected sensor. + model: "umrr9f_v2_1_1" + # Adapter id to which sensor is connected + dev_id: 3 + # The client_id of the sensor/source, must be a unique integer. + id: 700 + # The ip address of the sensor or of the source acting as a sensor. + ip: "192.168.11.11" + # The port to be used. + port: 55555 + # The frame_id to be set to the published messages. + frame_id: "umrr" + # Specify the history size. + history_size: 10 + inst_type: "port_based" + data_type: "port_based" + # The interface name of the sensor + uifname: "umrr9f_t169_automotive" + # The major version of the interface + uifmajorv: 2 + # The minor version of the interface + uifminorv: 1 + # The pathc version of the interface + uifpatchv: 1 + sensor_3: + link_type: "eth" + # The model of the connected sensor. + model: "umrr11" + # Adapter id to which sensor is connected + dev_id: 3 + # The client_id of the sensor/source, must be a unique integer. + id: 800 + # The ip address of the sensor or of the source acting as a sensor. + ip: "172.22.10.101" + # The port to be used. + port: 55555 + # The frame_id to be set to the published messages. + frame_id: "umrr" + # Specify the history size. + history_size: 10 + inst_type: "port_based" + data_type: "port_based" + # The interface name of the sensor + uifname: "umrr11_t132_automotive" + # The major version of the interface + uifmajorv: 1 + # The minor version of the interface + uifminorv: 1 + # The pathc version of the interface + uifpatchv: 2 + sensor_4: + # The model of the connected sensor. Available models umrr11, umrr9d, umrr96, + # umrr9f_v1_1_1, umrr9f_v2_0_0 + link_type: "can" + model: "umrr9d_can" + # The client_id of the sensor/source, must be a unique integer. + id: 900 + # Adapter id to which sensor is connected + dev_id: 5 + # The interface name of the sensor + uifname: "umrr9d_t152_automotive" + # The major version of the interface + uifmajorv: 1 + # The minor version of the interface + uifminorv: 0 + # The pathc version of the interface + uifpatchv: 3 + # The frame_id to be set to the published messages. + frame_id: "umrr" + # Specify the history size. + history_size: 10 diff --git a/umrr_ros2_driver/param/radar.params.template.yaml b/umrr_ros2_driver/param/radar.params.template.yaml new file mode 100644 index 0000000..97bf6df --- /dev/null +++ b/umrr_ros2_driver/param/radar.params.template.yaml @@ -0,0 +1,136 @@ +/smart_radar: + ros__parameters: + adapters: + adapter_0: + hw_type: "eth" + hw_dev_id: 4 + hw_iface_name: "eth0" + port: 55555 + master_data_serial_type: port_based + master_inst_serial_type: port_based + sensors: + # As many as 10 sensors all named as "sensor_" in increasing order of numbers, + # e.g., sensor_0, sensor_1, etc. The list must start with sensor_0. + # + # Sensor `model` names if using link type: + # can: 'umrra4_can_mse_v1_0_0', 'umrra4_can_mse_v2_1_0', 'umrr9f_can_mse_v1_1_0', 'umrr9f_can_mse_v1_0_0', 'umrr96_can_v1_2_2', 'umrr11_can_v1_1_2', 'umrr9f_can_v2_1_1', 'umrr9f_can_v2_2_1', + # 'umrr9f_can_v2_4_1', 'umrr9f_can_v3_0_0', 'umrr9d_can_v1_0_3', 'umrr9d_can_v1_2_2', 'umrr9d_can_v1_4_1', 'umrr9d_can_v1_5_0', 'umrra4_can_v1_0_1', 'umrra4_can_v1_2_1', 'umrra4_can_v1_4_0' + # + # port: 'umrra1_v2_0_0', 'umrra1_v1_0_0', 'umrra4_mse_v1_0_0', 'umrra4_mse_v2_1_0', 'umrr9f_mse_v1_3_0', 'umrr9f_mse_v1_1_0', 'umrr9f_mse_v1_0_0', 'umrr96_v1_2_2', 'umrr11_v1_1_2', 'umrr9f_v2_1_1', 'umrr9f_v2_2_1', 'umrr9f_v2_4_1', + # 'umrr9f_v3_0_0', 'umrr9d_v1_0_3', 'umrr9d_v1_2_2', 'umrr9d_v1_4_1', 'umrr9d_v1_5_0', 'umrra4_v1_0_1', 'umrra4_v1_2_1', 'umrra4_v1_4_0' + sensor_0: + # The link of the sensor either 'can' or 'eth' + link_type: "eth" + # The publisher type of the sensor + pub_type: "target" + # The model of the connected sensor. + model: "umrr11_v1_1_2" + # Adapter id to which sensor is connected + dev_id: 4 + # The client_id of the sensor/source, must be a unique integer. + id: 100 + # The frame_id to be set to the published messages. + frame_id: "umrr" + # Specify the history size. + history_size: 10 + # The ip address of the sensor or of the source acting as a sensor. + ip: "192.168.11.11" + inst_type: "port_based" + data_type: "port_based" + # The port to be used. + port: 55555 + # The interface name of the sensor + uifname: "umrr11_t132_automotive" + # The major version of the interface + uifmajorv: 1 + # The minor version of the interface + uifminorv: 1 + # The pathc version of the interface + uifpatchv: 2 + sensor_1: + # The link of the sensor either 'can' or 'eth' + link_type: "eth" + # The publisher type of the sensor + pub_type: "target" + # The model of the connected sensor. + model: "umrr96_v1_2_2" + # Adapter id to which sensor is connected + dev_id: 4 + # The client_id of the sensor/source, must be a unique integer. + id: 200 + # The ip address of the sensor or of the source acting as a sensor. + ip: "172.22.10.102" + # The port to be used. + port: 55555 + # The frame_id to be set to the published messages. + frame_id: "umrr" + # Specify the history size. + history_size: 10 + inst_type: "port_based" + data_type: "port_based" + # The interface name of the sensor + uifname: "umrr96_t153_automotive" + # The major version of the interface + uifmajorv: 1 + # The minor version of the interface + uifminorv: 2 + # The pathc version of the interface + uifpatchv: 2 + sensor_2: + # The link of the sensor either 'can' or 'eth' + link_type: "eth" + # The publisher type of the sensor + pub_type: "target" + # The model of the connected sensor. + model: "umrr9f_v2_1_1" + # Adapter id to which sensor is connected + dev_id: 4 + # The client_id of the sensor/source, must be a unique integer. + id: 300 + # The ip address of the sensor or of the source acting as a sensor. + ip: "172.22.10.103" + # The port to be used. + port: 55555 + # The frame_id to be set to the published messages. + frame_id: "umrr" + # Specify the history size. + history_size: 10 + inst_type: "port_based" + data_type: "port_based" + # The interface name of the sensor + uifname: "umrr9f_t169_automotive" + # The major version of the interface + uifmajorv: 2 + # The minor version of the interface + uifminorv: 1 + # The pathc version of the interface + uifpatchv: 1 + sensor_3: + # The link of the sensor either 'can' or 'eth' + link_type: "eth" + # The publisher type of the sensor + pub_type: "target" + # The model of the connected sensor. + model: "umrr9d_v1_5_0" + # Adapter id to which sensor is connected + dev_id: 4 + # The client_id of the sensor/source, must be a unique integer. + id: 400 + # The ip address of the sensor or of the source acting as a sensor. + ip: "172.22.10.104" + # The port to be used. + port: 55555 + # The frame_id to be set to the published messages. + frame_id: "umrr" + # Specify the history size. + history_size: 10 + inst_type: "port_based" + data_type: "port_based" + # The interface name of the sensor + uifname: "umrr9d_t152_automotive" + # The major version of the interface + uifmajorv: 1 + # The minor version of the interface + uifminorv: 5 + # The pathc version of the interface + uifpatchv: 0 diff --git a/umrr_ros2_driver/smartmicro/Readme.md b/umrr_ros2_driver/smartmicro/Readme.md new file mode 100644 index 0000000..991748b --- /dev/null +++ b/umrr_ros2_driver/smartmicro/Readme.md @@ -0,0 +1,5 @@ +## Purpose / Use cases +This directory will include the Smart Access release once it has been download using the `smart_extract.sh` script! + +## License +Licensed under the [Apache 2.0 License](LICENSE). diff --git a/umrr_ros2_driver/smartmicro/include/DataServicesIface.h b/umrr_ros2_driver/smartmicro/include/DataServicesIface.h new file mode 100644 index 0000000..36e22bc --- /dev/null +++ b/umrr_ros2_driver/smartmicro/include/DataServicesIface.h @@ -0,0 +1,337 @@ +/************************************************************************ + * + * File: DataServicesIface.h + * + * Copyright + * + * s.m.s. smart microwave sensors GmbH + * In den Waashainen 1 + * 38108 Braunschweig - Germany + * + * Register Court: Amtsgericht Braunschweig / Register Number: HRB 4525 + * VAT ID: DE 193324798 / Tax Number: 13/207/02357 + ************************************************************************/ + +#ifndef COM_COMMON_DATA_SERVICES_IF_H +#define COM_COMMON_DATA_SERVICES_IF_H + +#include +#include +#include +#include + +#include + +using namespace com::types; + +namespace com { +namespace common { + +typedef std::function + DataReceiverCallback; +typedef std::function + ResponseCallback; +typedef std::function + LogReceiverCallback; + +typedef std::function UpdateCallback; +typedef std::function GetLocalTimeCallback; +typedef std::function SetTimeOffsetCallback; +typedef std::function OnUpdateInfoCallback; +typedef std::function + ConnectedInfoCallback; + +class EXPORT_COM_LIB DataServicesIface : public com::VersionedIface { + +public: + DataServicesIface() {} + virtual ~DataServicesIface() {} + + /* + * Function: Get() + * Arguments: none + * Return: std::shared_ptr + * Description: Returns a singletone instance of a this class, since this is + * only an abstract class, the instantiation should be done by a deriving + * class. + */ + static std::shared_ptr Get(); + + /* + * Function: SetInstructionBuffer + * Arguments: IN ClientId clientId - destination client id. + * IN const InstructionBuffer& instBuffer - instruction port + * object. IN ResponseCallback callback - callback function which should be + * called upon a response. + * + * Return: ErrorCode + * Description: This function sends an instruction port to the input client. + * It is used to send requests and responses. In the case of a response, no + * callback is required. + */ + virtual ErrorCode SetInstructionBuffer(IN ClientId clientId, + IN InstructionBuffer &instBuffer, + IN ResponseCallback callback) = 0; + /* + * Function: RegisterDataRecvCallback + * this function will pass all deserialzied data ports to centrally registered + * callback function + * Arguments: IN DataReceiverCallback callback - callback function which + * should be called upon a reception. Return: ErrorCode Description: Registers + * a receiver callback function , which should be called , when a new buffer + * arrives. + */ + virtual ErrorCode + RegisterDataRecvCallback(IN DataReceiverCallback callback) = 0; + + /* + * Function: RegisterDataRecvCallback + * Arguments: IN ClientId clientId - client id of the sending client + * IN PortId portId - port id of the required port + * IN DataReceiverCallback callback - callback function which + * should be called upon a reception. Return: ErrorCode Description: Registers + * a receiver callback function , which should be called , when a new buffer + * arrives. + */ + virtual ErrorCode + RegisterDataRecvCallback(IN ClientId clientId, IN PortId portId, + IN DataReceiverCallback callback) = 0; + + /* + * Function: RegisterComHubDataRecvCallback + * this function will pass all data ports from comhub device to registered + * callback function + * Arguments: IN DataReceiverCallback callback - callback function which + * should be called upon a reception. Return: ErrorCode Description: Registers + * a receiver callback function , which should be called , when a new buffer + * arrives. + */ + virtual ErrorCode + RegisterComHubDataRecvCallback(IN DataReceiverCallback callback) = 0; + + /* + * Function: RegisterInstRecvCallback + * Arguments: IN DataReceiverCallback callback - callback function will be + called upon a reception of new instruction port. + * Return: ErrorCode + * Description: Registers a receiver callback function, which should be + called, when new instructions arrive. + */ + virtual ErrorCode + RegisterInstRecvCallback(IN DataReceiverCallback callback) = 0; + + /* + * Function: StreamDataPort + * Arguments: IN ClientId clientId - destination client id. + * IN PortId portId - id of the streamed port. + * IN BufferDescriptor& buffer - data buffer + * Return: ErrorCode + * Description: Streams a data port to input client id + */ + virtual ErrorCode StreamDataPort(IN ClientId clientId, IN PortId portId, + IN BufferDescriptor &buffer) = 0; + + /* + * Function: StreamInternalDataPort + * Arguments: ClientId clientId - destination client id. + * PortId portId - id of the streamed port. + * BufferDescriptor& buffer - data buffer + * Return: ErrorCode + * Description: Streams a internal data port to input client id. + * This will return with an error code if the port is + * content of client user interface + */ + virtual ErrorCode StreamInternalDataPort(ClientId clientId, PortId portId, + BufferDescriptor &buffer) = 0; + + /* + * Function: Init + * Arguments: none + * Return: bool + * Description: Initializes the data services layer. + */ + virtual bool Init() = 0; + + /* + * Function: RegisterUpdCallback + * Arguments: UpdateCallback + * Return: ErrorCode + * Description: Register an update callback, will be called upon + * receiving a new download package + */ + virtual ErrorCode RegisterUpdCallback(IN UpdateCallback callback) = 0; + + /* + * Function: RegisterConnectedClientCallback + * Arguments: IN ConnectedInfoCallback callback + * Callback-Function which shall be called to inform about + * change in 'connected client list' + * Return: ErrorCode + * Description: Registers the ConnectedClientCallback + */ + virtual ErrorCode + RegisterConnectedClientCallback(IN ConnectedInfoCallback callback) = 0; + + /* + * Function: RegisterGetLocalTimeCallback + * Arguments: GetLocalTimeCallback + * Return: ErrorCode + * Description: Register a callback to receive the local time for the time + * sync feature. Valid for time sync master and slave + */ + virtual ErrorCode + RegisterGetTimeCallback(IN GetLocalTimeCallback callback) = 0; + + /* + * Function: RegisterSetTimeOffsetCallback + * Arguments: SetTimeOffsetCallback + * Return: ErrorCode + * Description: Register a callback to set adjust the local time with the + * offset Valid for time sync slave. + */ + virtual ErrorCode + RegisterSetTimeOffsetCallback(IN SetTimeOffsetCallback callback) = 0; + + /* + * Function: RegisterLogMessageCallBack + * Arguments: LogReceiverCallback + * Return: ErrorCode + * Description: Register a callback to receive log messages from + * BareMetal sensor. + */ + virtual ErrorCode + RegisterLogMessageCallBack(IN LogReceiverCallback callback) = 0; + + /* + * Function: AddTimeSyncSlave + * Arguments: uint8_t slaveDevId slave device id + * Return: ErrorCode + * Description: adds a time sync slave to be synchronized + * Valid for time sync master + */ + virtual ErrorCode AddTimeSyncSlave(IN uint8_t slaveDevId) = 0; + + /* + * Function: RemoveTimeSyncSlave + * Arguments: uint8_t slaveDevId slave device id + * Return: ErrorCode + * Description: removes a time sync slave. + * Valid for time sync master + */ + virtual ErrorCode RemoveTimeSyncSlave(IN uint8_t slaveDevId) = 0; + /* + * Function: SetTimeSyncSlaveId + * Arguments: uint8_t slaveDevId slave device id + * Return: ErrorCode + * Description: set time sync slave id. + * Valid for time sync slave + */ + virtual ErrorCode SetTimeSyncSlaveId(IN uint8_t slaveDevId) = 0; + /* + * Function: ActivateTimeSync + * Arguments: bool activate - true activates , false diactivates + * Return: ErrorCode + * Description: activates/diactivates the time sync protocol + * Valid for time sync slave and master + */ + virtual ErrorCode ActivateTimeSync(IN bool activate) = 0; + + /* + * Function: Configure Time Sync Slave. an only be configured if the service + * is not running. + * Arguments: uint8_t Number of required Iteration to synchronize timestamp. + * Return: ErrorCode + * Description: Valid only for time sync slave and sets the required number of + * iteration. + */ + virtual ErrorCode ConfigureTimeSyncSlave(uint8_t requireIteration) = 0; + + /* + * Function: Configure Time Sync Master. Can only be configured if the service + * is not running. + * Arguments: uint8_t Cooldown time between two time sync + * intervals. uint8_t Maximum number of iteration which support time sync + * master service. It is not possible to synchronize + * sensors that require a higher iteration number than the + * maximum number of iterations. + * Return: ErrorCode Description: Valid only for + * time sync master and sets cooldown time between two time sync intervals. + */ + virtual ErrorCode ConfigureTimeSyncMaster(uint8_t cooldownTime, + uint8_t maxNumberOfIteration) = 0; + + /* + * Function: StartSWUpdate + * Arguments: IN std::map updateList + * IN ClientId clientID + * IN OnUpdateInfoCallback callback + * Return : ErrorCode + * Description : Function for trigger a new Software-Update + * in Data-Service-Layer + */ + virtual ErrorCode + StartSWUpdate(IN std::map &updateList, + IN ClientId clientID, IN OnUpdateInfoCallback callback) = 0; + /* + * Function: AbortSWUpdate + * Arguments: None + * Return: void + * Description: Function for aborting a currently running Update-Service + */ + virtual void AbortSWUpdate() = 0; + + /* + * Function: GetConnectedClients + * Arguments: OUT std::map>& + * clients Return: bool Description: Returns a map of connected client + * descriptions. + */ + virtual bool GetConnectedClients( + OUT std::map> &clients) = 0; + + /* + * Function: GetConnectedClients + * Arguments: OUT std::map>& + * OUT std::map& + * clients Return: bool Description: Returns a map of connected client + * descriptions. + */ + virtual bool GetConnectedClients( + OUT std::map> &clients, + OUT std::map &client_ips) = 0; + + /* + * Function: SetClientUserInterfaceInfo + * Arguments: IN ClientId clientId client id + IN const std::string& userIfName user interface name + * IN uint32_t majorVer user interface major version + * IN uint32_t minorVer user interface minor version + * IN uint32_t patchVer user interface patch version + * Return: bool + * Description: set user interface information of a certain client + */ + virtual bool SetClientUserInterfaceInfo(IN ClientId clientId, + IN const std::string &userIfName, + IN uint32_t majorVer, + IN uint32_t minorVer, + IN uint32_t patchVer) = 0; + + /* + * Function: GetConfig + * Arguments: OUT ComLibConfig + * Return: bool + * Description: Get data from ComLibConfig file + */ + virtual bool GetConfig(OUT ComLibConfig &config) = 0; + /* + * Function: GetTimeSyncMasterTimeOutInfo + * Return: const TimeOutMap as reference + * Description: Gets the time-out details from TimeSync master + */ + virtual TimeOutMap GetTimeSyncMasterTimeOutInfo() = 0; +}; + +} // namespace common +} // namespace com + +#endif // COM_COMMON_DATA_SERVICES_IF_H diff --git a/umrr_ros2_driver/smartmicro/include/InstructionBuffer.h b/umrr_ros2_driver/smartmicro/include/InstructionBuffer.h new file mode 100644 index 0000000..ccbfb0a --- /dev/null +++ b/umrr_ros2_driver/smartmicro/include/InstructionBuffer.h @@ -0,0 +1,191 @@ +/************************************************************************ + * Copyright + * + * s.m.s. smart microwave sensors GmbH + * In den Waashainen 1 + * 38108 Braunschweig - Germany + * + * Register Court: Amtsgericht Braunschweig / Register Number: HRB 4525 + * VAT ID: DE 193324798 / Tax Number: 13/207/02357 + ************************************************************************/ + +#ifndef COM_COMMON_INSTRUCTION_BUFFER_H +#define COM_COMMON_INSTRUCTION_BUFFER_H + +#include + +#include + +using namespace com::types; + +namespace com { +namespace common { + +/** @} */ + +#define MAX_DIM_ELEMENT \ + 2U /**< @brief Max. numbers of available dimension elements */ +#define INSTR_HANDLER_CMD_DATA_TYPE 6U + +enum RequestType { + COM_INSTR_PORT_INVALID = 0U, /**< @brief Instruction Requests is invalid */ + COM_INSTR_PORT_PARAM_SET_PARAMETER = + 1U, /**< @brief Instruction Requests for setting a parameter */ + COM_INSTR_PORT_PARAM_GET_PARAMETER = + 2U, /**< @brief Instruction Requests for reading a parameter */ + COM_INSTR_PORT_STATUS_GET_STATUS = + 3U, /**< @brief Instruction Requests for reading a status */ + COM_INSTR_PORT_CMD = 4U, /**< @brief Instruction Requests for command */ + COM_INSTR_PORT_EXPORT = + 5U, /**< @brief Instruction Requests for export param/status */ + COM_INSTR_PORT_RESERVED_0 = 248U, /**< @brief Reserved 0 */ + COM_INSTR_PORT_RESERVED_1 = 249U, /**< @brief Reserved 1 */ + COM_INSTR_PORT_RESERVED_2 = 250U, /**< @brief Reserved 2 */ + COM_INSTR_PORT_RESERVED_3 = 251U, /**< @brief Reserved 3 */ + COM_INSTR_PORT_RESERVED_4 = 252U, /**< @brief Reserved 4 */ + COM_INSTR_PORT_RESERVED_5 = 253U, /**< @brief Reserved 5 */ + COM_INSTR_PORT_RESERVED_6 = 254U, /**< @brief Reserved 6 */ + COM_INSTR_PORT_RESERVED_7 = 255U, /**< @brief Reserved 7 */ +}; + +enum ResposeType { + COM_INSTR_PORT_NO_RESPONSE = 0U, /**< @brief No instruction Response */ + COM_INSTR_PORT_SUCCESS = + 1U, /**< @brief Instruction Response was processed successfully */ + COM_INSTR_PORT_ERROR = 2U, /**< @brief General error */ + COM_INSTR_PORT_ERROR_REQUEST = 3U, /**< @brief Invalid request */ + COM_INSTR_PORT_ERROR_SECTION = 4U, /**< @brief Invalid section */ + COM_INSTR_PORT_ERROR_ID = 5U, /**< @brief Invalid id */ + COM_INSTR_PORT_ERROR_PROT = 6U, /**< @brief Invalid protection */ + COM_INSTR_PORT_ERROR_MIN = 7U, /**< @brief Value out of minimal bounds */ + COM_INSTR_PORT_ERROR_MAX = 8U, /**< @brief Value out of maximal bounds */ + COM_INSTR_PORT_ERROR_NAN = 9U, /**< @brief Value is not a number */ + COM_INSTR_PORT_ERROR_TYPE = + 10U, /**< @brief Type of Instruction is not valid */ + COM_INSTR_PORT_ERROR_DIM = 11U, /**< @brief Dim of Instruction is not valid */ + COM_INSTR_PORT_ERROR_ELEMENT = + 12U, /**< @brief Element of Instruction is not valid */ + COM_INSTR_PORT_ERROR_SIGNATURE = + 13U, /**< @brief Signature of Instruction is not valid */ + COM_INSTR_PORT_ERROR_ACCESS_LVL = + 14U, /**< @brief Access level is not valid */ + COM_INSTR_PORT_ERROR_INTERNAL = + 15U, /**< @brief Internal error shall not be sent */ +}; + +class Instruction { + public: + Instruction(uint32_t sectionId, uint32_t id) + : _request(0), + _response(0), + _sectionId(sectionId), + _id(id), + _dataType(0), + _dimCount(0), + _signature(0), + _value(0) {} + + ~Instruction() {} + + inline uint8_t GetRequest() const { return _request; } + + inline void SetRequest(IN uint8_t request) { _request = request; } + + inline uint8_t GetResponse() const { return _response; } + + inline void SetResponse(IN uint8_t response) { _response = response; } + + inline uint16_t GetSectionId() const { return _sectionId; } + + inline void SetSectionId(IN uint16_t sectionId) { _sectionId = sectionId; } + + inline uint16_t GetId() const { return _id; } + + inline void SetId(IN uint16_t id) { _id = id; } + + inline uint8_t GetDataType() const { return _dataType; } + + inline void SetDataType(IN uint8_t dataType) { _dataType = dataType; } + + inline uint8_t GetDimCount() const { return _dimCount; } + + inline bool SetDimCount(IN uint8_t dimCount) { + if (dimCount <= MAX_DIM_ELEMENT) { + _dimCount = dimCount; + return true; + } + return false; + } + + inline uint16_t GetDimElement(IN uint8_t index) const { + return (index < _dimCount ? _dimElement[index] : 0); + } + + inline bool SetDimElement(IN uint8_t index, IN uint16_t element) { + if (index < _dimCount) { + _dimElement[index] = element; + return true; + } + return false; + } + + inline uint32_t GetSignature() const { return _signature; } + + inline void SetSignature(IN uint32_t signature) { _signature = signature; } + + inline uint64_t GetValue() const { return _value; } + + inline void SetValue(IN uint64_t value) { _value = value; } + + private: + uint8_t _request; /**< @brief Instruction request command */ + uint8_t _response; /**< @brief Instruction response */ + uint16_t _sectionId; /**< @brief Section number. */ + uint16_t _id; /**< @brief Each instruction has a unique ID */ + uint8_t _dataType; /**< @brief The data type. */ + uint8_t _dimCount; /**< @brief Number of dimensions. + Value 0 for non-array structures. */ + uint16_t _dimElement[MAX_DIM_ELEMENT]; /**< @brief Status element indices + addressing. */ + uint32_t _signature; /**< @brief Instruction signature. */ + uint64_t _value; /**< @brief Instruction value. */ +}; + +class InstructionBuffer { + public: + InstructionBuffer() : _seqCount(0) {} + + InstructionBuffer(uint32_t seqCount) : _seqCount(seqCount) {} + + ~InstructionBuffer() {} + + inline void AddInstruction(IN std::shared_ptr inst) { + _instructions.push_back(inst); + } + + inline std::vector>& GetInstructions() { + return _instructions; + } + + inline size_t GetNumOfInstructions() const { return _instructions.size(); } + + inline void SetSeqCount(IN SequenceNumber seqCount) { _seqCount = seqCount; } + + inline SequenceNumber GetSeqCount() const { return _seqCount; } + + inline void Reset() { _instructions.clear(); } + + void operator=(const InstructionBuffer& rhs) { + _instructions = rhs._instructions; + _seqCount = rhs._seqCount; + } + + private: + std::vector> _instructions; + uint32_t _seqCount; +}; + +} // namespace common +} // namespace com + +#endif // COM_COMMON_INSTRUCTION_BUFFER_H diff --git a/umrr_ros2_driver/smartmicro/include/Types.h b/umrr_ros2_driver/smartmicro/include/Types.h new file mode 100644 index 0000000..e8f7c70 --- /dev/null +++ b/umrr_ros2_driver/smartmicro/include/Types.h @@ -0,0 +1,574 @@ +/************************************************************************ + * Copyright + * + * s.m.s. smart microwave sensors GmbH + * In den Waashainen 1 + * 38108 Braunschweig - Germany + * + * Register Court: Amtsgericht Braunschweig / Register Number: HRB 4525 + * VAT ID: DE 193324798 / Tax Number: 13/207/02357 + ************************************************************************/ + +#ifndef COM_TYPES_H +#define COM_TYPES_H + +#include +#include +#include + +#include +#include +#include +#include + +namespace com { + +namespace types { + +const uint8_t MAX_NUM_OF_INST = 255; + +#ifdef _WIN32 +#define USER_IF_LIB_PREFIX "" +#define USER_IF_LIB_EXT "_user_interface.dll" +#else +#define USER_IF_LIB_PREFIX "lib" +#define USER_IF_LIB_EXT "_user_interface.so" +#endif +#define CAN_MAX_DATA_BYTES 8 + +enum SerializationType { // NOLINT(*enum-size) + SERIALIZATION_TYPE_CAN_SPEC = 0, + SERIALIZATION_TYPE_PORT_BASED, + SERIALIZATION_TYPE_UNKNOWN +}; + +struct CanFormat { + uint16_t u16_identifier; + uint8_t u8_dlc; + uint8_t au8_data[CAN_MAX_DATA_BYTES]; // NOLINT(*arrays) +}; + +enum LinkType { // NOLINT(*enum-size) + LINK_TYPE_UDP = 0, + LINK_TYPE_UDP_DISCOVERY, + LINK_TYPE_CAN, + LINK_TYPE_CAN_DISCOVERY, + LINK_TYPE_RS485, + LIMK_TYPE_UNKNOWN, + LINK_TYPE_UNKNOWN = LIMK_TYPE_UNKNOWN +}; + +enum ProtocolType { // NOLINT(*enum-size) + PROTOCOL_TYPE_UNKNOWN = 0, + PROTOCOL_TYPE_SMS_CAN_BASE_DATA_V1, + PROTOCOL_TYPE_ATXMEGA_SDLC, + PROTOCOL_TYPE_STG, + PROTOCOL_TYPE_SMS_CAN_BASE_DATA_V2, + PROTOCOL_TYPE_DEBUG, + PROTOCOL_TYPE_LOG_MSG, + PROTOCOL_TYPE_ALIVE, + PROTOCOL_TYPE_PORT, + PROTOCOL_TYPE_INTERVIEW, + PROTOCOL_TYPE_DOWNLOAD, + PROTOCOL_TYPE_TIME_SYNC, + PROTOCOL_TYPE_DATA_STREAM, + PROTOCOL_TYPE_INSTRUCTION +}; + +enum LibraryRole { // NOLINT(*enum-size) + LIBRARY_ROLE_MASTER, + LIBRARY_ROLE_SLAVE, + LIBRARY_ROLE_UNKNOWN +}; + +enum TimeSyncRole { // NOLINT(*enum-size) + TIME_SYNC_ROLE_MASTER, + TIME_SYNC_ROLE_SLAVE, + TIME_SYNC_ROLE_UNKNOWN +}; + +using LinkId = uint16_t; +using SequenceNumber = uint32_t; +using CanNetId = uint8_t; +using CanId = uint16_t; +using UdtId = uint16_t; +using PhyDeviceId = uint8_t; + +struct TimeOutDetails { + uint16_t timeOutCount; + uint64_t latestOffsetValue; +}; +using TimeOutDeatils = TimeOutDetails; // TODO(RKO): Remove in next release +using TimeOutMap = std::map; +const PortId INSTRUCTION_PORT_ID = 46; + +class SharedLibDescriptor { + public: + // NOLINTNEXTLINE(*explicit*) + SharedLibDescriptor(const std::string& libName); + SharedLibDescriptor(const SharedLibDescriptor&) = delete; + SharedLibDescriptor(SharedLibDescriptor&&) = default; + SharedLibDescriptor& operator=(const SharedLibDescriptor&) = delete; + SharedLibDescriptor& operator=(SharedLibDescriptor&&) = default; + virtual ~SharedLibDescriptor(); + + bool Link(); + + void* GetHandle() { return _handle; } + + protected: + // NOLINTBEGIN(*non-private-member-variables-in-classes) + std::string _libName; + void* _handle{nullptr}; + // NOLINTEND(*non-private-member-variables-in-classes) + + private: + static std::mutex mutex_; +}; + +class ReceiverKey { + public: + ReceiverKey(ClientId clientId, PortId portId); + ReceiverKey() = default; + ReceiverKey(const ReceiverKey&) = default; + ReceiverKey(ReceiverKey&&) = delete; + ReceiverKey& operator=(const ReceiverKey&) = default; + ReceiverKey& operator=(ReceiverKey&&) = delete; + ~ReceiverKey() = default; + + ClientId GetClientId() const; + void SetClientId(const ClientId& clientId); + + PortId GetPortId() const; + void SetPortId(const PortId& portId); + + private: + ClientId _clientId{0}; + PortId _portId{0}; +}; + +inline bool operator<(const ReceiverKey& lhs, const ReceiverKey& rhs) { + return (lhs.GetClientId() < rhs.GetClientId()) || + ((lhs.GetClientId() == rhs.GetClientId()) && + (lhs.GetPortId() < rhs.GetPortId())); +} + +class ResponseKey { + public: + ResponseKey(ClientId clientId, SequenceNumber seqNum); + ResponseKey() = default; + ResponseKey(const ResponseKey&) = default; + ResponseKey(ResponseKey&&) = delete; + ResponseKey& operator=(const ResponseKey&) = default; + ResponseKey& operator=(ResponseKey&&) = delete; + ~ResponseKey() = default; + + ClientId GetClientId() const; + void SetClientId(IN const ClientId& clientId); + + SequenceNumber GetSequenceNumber() const; + void SetSequenceNumber(IN const SequenceNumber& seqNum); + + private: + ClientId _clientId{0}; + SequenceNumber _seqNum{0}; +}; + +inline bool operator<(IN const ResponseKey& lhs, IN const ResponseKey& rhs) { + return (lhs.GetClientId() < rhs.GetClientId()) || + ((lhs.GetClientId() == rhs.GetClientId()) && + (lhs.GetSequenceNumber() < rhs.GetSequenceNumber())); +} + +class EthNetDescriptor { + public: + EthNetDescriptor(const std::string& ip, uint32_t port, EthTransportType type); + EthNetDescriptor() = default; + EthNetDescriptor(const EthNetDescriptor&) = default; + EthNetDescriptor(EthNetDescriptor&&) = delete; + EthNetDescriptor& operator=(const EthNetDescriptor&) = default; + EthNetDescriptor& operator=(EthNetDescriptor&&) = delete; + ~EthNetDescriptor() = default; + + void GetIp(OUT std::string& ip) const; + void SetIp(IN const std::string& ip); + + uint32_t GetPort() const; + void SetPort(IN uint32_t port); + + EthTransportType GetType() const; + void SetType(IN EthTransportType type); + + private: + std::string _ip; + uint32_t _port{0}; + EthTransportType _type{ETH_TRANSPORT_TYPE_TCP}; +}; + +enum ClientPhyType { // NOLINT(*enum-size) + CLIENT_PHY_CAN = 0, + CLIENT_PHY_ETH, + CLIENT_PHY_RS485, + CLIENT_PHY_UNKNOWN +}; + +class ClientDescriptor { + public: + ClientDescriptor(ClientId clientId, ClientPhyType phyType, + SerializationType instSerialType, + SerializationType dataSerialType, uint8_t phyDevId); + ClientDescriptor(const ClientDescriptor&) = default; + ClientDescriptor(ClientDescriptor&&) = delete; + ClientDescriptor& operator=(const ClientDescriptor&) = default; + ClientDescriptor& operator=(ClientDescriptor&&) = delete; + virtual ~ClientDescriptor() = default; + + ClientPhyType GetPhyType() const; + void SetPhyType(IN ClientPhyType phyType); + + ClientId GetId() const; + void SetId(IN ClientId clientId); + + PhyDeviceId GetPhyDevId() const; + void SetPhyDevId(IN PhyDeviceId phyDevId); + + SerializationType GetInstSerialType() const; + void SetInstSerialType(IN SerializationType type); + + SerializationType GetDataSerialType() const; + void SetDataSerialType(IN SerializationType type); + + std::string GetUserIfName() const; + void SetUserIfName(IN const std::string& userIfName); + + uint32_t GetUserIfMajorVer() const; + void SetUserIfMajorVer(IN uint32_t majorVer); + + uint32_t GetUserIfMinorVer() const; + void SetUserIfMinorVer(IN uint32_t minorVer); + + uint32_t GetUserIfPatchVer() const; + void SetUserIfPatchVer(IN uint32_t patchVer); + + private: + ClientId _clientId{0}; + ClientPhyType _phyType{CLIENT_PHY_UNKNOWN}; + SerializationType _instSerialType{SERIALIZATION_TYPE_UNKNOWN}; + SerializationType _dataSerialType{SERIALIZATION_TYPE_UNKNOWN}; + PhyDeviceId _phyDevId{0}; + std::string _userIfName; + uint32_t _userIfMajorVer{0}; + uint32_t _userIfMinorVer{0}; + uint32_t _userIfPatchVer{0}; +}; + +class CanClientDescriptor : public ClientDescriptor { + public: + CanClientDescriptor(ClientId clientId, CanNetId canNetId, + PhyDeviceId phyDevId); + CanClientDescriptor(const CanClientDescriptor&) = default; + CanClientDescriptor(CanClientDescriptor&&) = delete; + CanClientDescriptor& operator=(const CanClientDescriptor&) = default; + CanClientDescriptor& operator=(CanClientDescriptor&&) = delete; + ~CanClientDescriptor() override = default; + + CanNetId GetCanNetId() const; + void SetCanNetId(IN CanNetId canNetId); + + private: + CanNetId _canNetId{0}; +}; + +class Rs485ClientDescriptor : public ClientDescriptor { + public: + Rs485ClientDescriptor(ClientId clientId, SerializationType instSerialType, + SerializationType dataSerialType, PhyDeviceId phyDevId); + Rs485ClientDescriptor(const Rs485ClientDescriptor&) = default; + Rs485ClientDescriptor(Rs485ClientDescriptor&&) = delete; + Rs485ClientDescriptor& operator=(const Rs485ClientDescriptor&) = default; + Rs485ClientDescriptor& operator=(Rs485ClientDescriptor&&) = delete; + ~Rs485ClientDescriptor() override = default; +}; + +class EthClientDescriptor : public ClientDescriptor { + public: + EthClientDescriptor(ClientId clientId, SerializationType instSerialType, + SerializationType dataSerialType); + EthClientDescriptor(const EthClientDescriptor&) = default; + EthClientDescriptor(EthClientDescriptor&&) = delete; + EthClientDescriptor& operator=(const EthClientDescriptor&) = default; + EthClientDescriptor& operator=(EthClientDescriptor&&) = delete; + ~EthClientDescriptor() override = default; +}; + +class SerializerConfig { + public: + SerializerConfig(ClientId clientId, SerializationType serialType); + SerializerConfig(const SerializerConfig&) = default; + SerializerConfig(SerializerConfig&&) = delete; + SerializerConfig& operator=(const SerializerConfig&) = default; + SerializerConfig& operator=(SerializerConfig&&) = delete; + virtual ~SerializerConfig() = default; + + ClientId GetClientId() const; + SerializationType GetType() const; + + private: + ClientId _clientId{0}; + SerializationType _serialType{SERIALIZATION_TYPE_UNKNOWN}; +}; + +class CanSerializerConfig : public SerializerConfig { + public: + CanSerializerConfig(ClientId clientId, LibraryRole role, CanNetId canNetId); + CanSerializerConfig(const CanSerializerConfig&) = default; + CanSerializerConfig(CanSerializerConfig&&) = delete; + CanSerializerConfig& operator=(const CanSerializerConfig&) = default; + CanSerializerConfig& operator=(CanSerializerConfig&&) = delete; + ~CanSerializerConfig() override = default; + + CanNetId GetCanNetId() const; + LibraryRole GetLibraryRole() const; + + private: + CanNetId _canNetId{0}; + LibraryRole _role{LIBRARY_ROLE_UNKNOWN}; +}; + +class PortBasedSerializerConfig : public SerializerConfig { + public: + PortBasedSerializerConfig(ClientId clientId, LibraryRole role); + PortBasedSerializerConfig(const PortBasedSerializerConfig&) = default; + PortBasedSerializerConfig(PortBasedSerializerConfig&&) = delete; + PortBasedSerializerConfig& operator=(const PortBasedSerializerConfig&) = + default; + PortBasedSerializerConfig& operator=(PortBasedSerializerConfig&&) = delete; + ~PortBasedSerializerConfig() override = default; + + LibraryRole GetLibraryRole() const; + + private: + LibraryRole _role{LIBRARY_ROLE_UNKNOWN}; +}; + +class InterfaceConfig { + public: + InterfaceConfig(LinkType linkType); // NOLINT(*explicit*) + InterfaceConfig(const InterfaceConfig&) = default; + InterfaceConfig(InterfaceConfig&&) = delete; + InterfaceConfig& operator=(const InterfaceConfig&) = default; + InterfaceConfig& operator=(InterfaceConfig&&) = delete; + virtual ~InterfaceConfig() = default; + + LinkType GetLinkType() const; + + private: + LinkType _linkType{LINK_TYPE_UNKNOWN}; +}; + +class CanInterfaceConfig : public InterfaceConfig { + public: + CanInterfaceConfig(CanNetId canNetId, CanDevId canDevId); + CanInterfaceConfig(const CanInterfaceConfig&) = default; + CanInterfaceConfig(CanInterfaceConfig&&) = delete; + CanInterfaceConfig& operator=(const CanInterfaceConfig&) = default; + CanInterfaceConfig& operator=(CanInterfaceConfig&&) = delete; + ~CanInterfaceConfig() override = default; + + CanNetId GetNetId() const; + CanDevId GetDevId() const; + + std::string GetUserIfName() const; + void SetUserIfName(IN const std::string& userIfName); + + uint32_t GetUserIfMajorVer() const; + void SetUserIfMajorVer(IN uint32_t majorVer); + + uint32_t GetUserIfMinorVer() const; + void SetUserIfMinorVer(IN uint32_t minorVer); + + uint32_t GetUserIfPatchVer() const; + void SetUserIfPatchVer(IN uint32_t patchVer); + + private: + CanNetId _canNetId{0}; + CanDevId _canDevId{0}; + std::string _userIfName; + uint32_t _userIfMajorVer{0}; + uint32_t _userIfMinorVer{0}; + uint32_t _userIfPatchVer{0}; +}; + +class Rs485InterfaceConfig : public InterfaceConfig { + public: + Rs485InterfaceConfig(SerialDevId devId); // NOLINT(*explicit*) + Rs485InterfaceConfig(const Rs485InterfaceConfig&) = default; + Rs485InterfaceConfig(Rs485InterfaceConfig&&) = delete; + Rs485InterfaceConfig& operator=(const Rs485InterfaceConfig&) = default; + Rs485InterfaceConfig& operator=(Rs485InterfaceConfig&&) = delete; + ~Rs485InterfaceConfig() override = default; + + SerialDevId GetDevId() const; + + private: + SerialDevId _devId{0}; +}; + +class IpConfig : public InterfaceConfig { + public: + IpConfig(const std::string& ip, uint16_t port, LinkType type); + IpConfig(const IpConfig&) = default; + IpConfig(IpConfig&&) = delete; + IpConfig& operator=(const IpConfig&) = default; + IpConfig& operator=(IpConfig&&) = delete; + ~IpConfig() override = default; + + std::string GetIp() const; + uint16_t GetPort() const; + + private: + std::string _ip; + uint16_t _port{0}; +}; + +class UdpConfig : public IpConfig { + public: + UdpConfig(const std::string& ip, uint16_t port); + UdpConfig(const UdpConfig&) = default; + UdpConfig(UdpConfig&&) = delete; + UdpConfig& operator=(const UdpConfig&) = default; + UdpConfig& operator=(UdpConfig&&) = delete; + ~UdpConfig() override = default; +}; + +class MulticastConfig : public IpConfig { + public: + MulticastConfig(const std::string& ip, uint16_t port); + MulticastConfig(const MulticastConfig&) = default; + MulticastConfig(MulticastConfig&&) = delete; + MulticastConfig& operator=(const MulticastConfig&) = default; + MulticastConfig& operator=(MulticastConfig&&) = delete; + ~MulticastConfig() override = default; +}; + +class ComLibConfig { + public: + ComLibConfig() = default; + ComLibConfig(uint16_t majorVer, uint16_t minorVer, uint16_t patchVer); + ComLibConfig(const ComLibConfig&) = default; + ComLibConfig(ComLibConfig&&) = delete; + ComLibConfig& operator=(const ComLibConfig&) = default; + ComLibConfig& operator=(ComLibConfig&&) = delete; + ~ComLibConfig() = default; + + LibraryRole GetRole() const; + ClientId GetClientId() const; + const std::string& GetConfigPath() const; + const std::string& GetSerialLibPath() const; + const std::string& GetUserInterfaceName() const; + uint16_t GetUserInterfaceMajorVersion() const; + uint16_t GetUserInterfaceMinorVersion() const; + uint16_t GetUserInterfacePatchVersion() const; + const std::string& GetDownloadPath() const; + SerializationType GetInstSerializationType() const; + SerializationType GetDataSerializationType() const; + ClientPhyType GetTimeSyncHwIfaceType() const; + uint8_t GetTimeSyncHwDeviceId() const; + bool IsTimeSyncSupported() const; + bool IsAliveSupported() const; + TimeSyncRole GetTimeSyncRole() const; + uint16_t GetMajorVersion() const; + uint16_t GetMinorVersion() const; + uint16_t GetPatchVersion() const; + + void SetRole(IN LibraryRole role); + void SetClientId(IN ClientId clientId); + void SetConfigPath(IN const std::string& path); + void SetSerialLibPath(IN const std::string& path); + void SetUserInterfaceName(IN const std::string& name); + void SetDownloadPath(IN const std::string& path); + void SetDataSerializationType(IN SerializationType type); + void SetInstSerializationType(IN SerializationType type); + void SetTimeSyncHwIfaceType(IN ClientPhyType hwIfType); + void SetTimeSyncHwDeviceId(IN uint8_t deviceId); + void SetTimeSyncSupport(IN bool isSupported); + void SetTimeSyncRole(IN TimeSyncRole role); + void SetAliveSupport(IN bool support); + void SetUserInterfaceMajorVersion(uint16_t majorVer); + void SetUserInterfaceMinorVersion(uint16_t minorVer); + void SetUserInterfacePatchVersion(uint16_t patchVer); + void SetComLibConfigMajorVersion(uint16_t majorVer); + void SetComLibConfigMinorVersion(uint16_t minorVer); + void SetComLibConfigPatchVersion(uint16_t patchVer); + + private: + uint16_t _majorVer{0}; + uint16_t _minorVer{0}; + uint16_t _patchVer{0}; + std::string _serialLibPath; + std::string _configFilesPath; + ClientId _clientId{0}; + LibraryRole _role{LIBRARY_ROLE_UNKNOWN}; + std::string _userIfName; + std::string _downloadPath; + SerializationType _instSerialization{SERIALIZATION_TYPE_UNKNOWN}; + SerializationType _dataSerialization{SERIALIZATION_TYPE_UNKNOWN}; + ClientPhyType _timeSyncHwIfaceType{CLIENT_PHY_UNKNOWN}; + uint8_t _timeSyncHwDevId{}; + bool _isTimeSyncSupported{false}; + TimeSyncRole _timeSyncRole{TIME_SYNC_ROLE_UNKNOWN}; + bool _isAliveSupported{false}; + uint16_t _userIfMajorVer{0}; + uint16_t _userIfMinorVer{0}; + uint16_t _userIfPatchVer{0}; +}; + +class SWUpdateConfigData { + public: + void SetData(IN bool pathFlag, IN std::string pathStr, IN bool dataFlag, + IN int32_t dataSize, IN uint8_t dataEncrypt, + IN uint8_t* dataPtr); + + void cleanData(); + + bool GetPathAvailableFlag() const; + std::string GetSegmentFilePath() const; + bool GetDataPtrValidFlag() const; + uint8_t GetSegmentEncryption() const; + int32_t GetSegmentBufferSize() const; + uint8_t* GetSegmentDataPtr(); + + private: + bool _pathAvailableFlag{false}; + std::string _segmentPath; + bool _dataPtrValidFlag{false}; + int32_t _segmentBufferSize{0}; + uint8_t _segmentEncryption{0}; + uint8_t* _segmentDataPtr{nullptr}; +}; + +class InternalTransportHeader { + public: + com::types::ProtocolType GetProtocolType() const; + void SetProtocolType(IN com::types::ProtocolType protoType); + + bool HasSrcClientId() const; + ClientId GetSrcClientId() const; + void SetSrcClientId(IN ClientId clientId); + + bool HasDstClientId() const; + ClientId GetDstClientId() const; + void SetDstClientId(IN ClientId clientId); + + private: + com::types::ProtocolType _protoType{PROTOCOL_TYPE_UNKNOWN}; + ClientId _srcClientId{0}; + bool _srcClientIdValid{false}; + ClientId _dstClientId{0}; + bool _dstClientIdValid{false}; +}; + +} // namespace types +} // namespace com + +#endif // COM_TYPES_H diff --git a/umrr_ros2_driver/src/smartmicro_radar_node.cpp b/umrr_ros2_driver/src/smartmicro_radar_node.cpp new file mode 100644 index 0000000..41b3e4a --- /dev/null +++ b/umrr_ros2_driver/src/smartmicro_radar_node.cpp @@ -0,0 +1,4298 @@ +// Copyright 2021 Apex.AI, Inc. +// +// 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. +// The initial version of the code was developed by Apex.AI and +// was thereafter adapted and extended by smartmicro. + +#include "umrr_ros2_driver/smartmicro_radar_node.hpp" + +#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 +#include +#include +#include +#include +#include +#include + +#include "umrr_ros2_driver/config_path.hpp" + +using com::common::Instruction; +using com::master::CmdRequest; +using com::master::CommunicationServicesIface; +using com::master::GetParamRequest; +using com::master::GetStatusRequest; +using com::master::InstructionBatch; +using com::master::InstructionServiceIface; +using com::master::Response; +using com::master::ResponseBatch; +using com::master::SetParamRequest; +using point_cloud_msg_wrapper::PointCloud2Modifier; +using std::literals::string_view_literals::operator""sv; + +namespace +{ +constexpr auto kMseType = "mse"sv; +constexpr auto kTargetType = "target"sv; +constexpr auto kEthLink = "eth"sv; +constexpr auto kCanLink = "can"sv; + +constexpr auto kDefaultClientId = 0; +constexpr auto kDefaultInterfaceName = "lo"; +constexpr auto kDefaultIp = "127.0.0.1"; +constexpr auto kDefaultPort = 55555; +constexpr auto kDefaultHistorySize = 10; +constexpr auto kDefaultFrameId = "umrr"; +constexpr auto kDefaultSensorType = "umrr11"; +constexpr auto kDefaultInstType = "port_based"; +constexpr auto kDefaultDataType = "port_based"; + +constexpr auto kDefaultHwDevId = 1; +constexpr auto kDefaultHwDevIface = "slcan"; +constexpr auto kDefaultHwLinkType = "can"; + +constexpr auto kHwDevLinkTag = "type"; +constexpr auto kClientLinkTag = "link_type"; +constexpr auto kHwDevIdTag = "dev_id"; +constexpr auto kHwDevIfaceNameTag = "iface_name"; + +constexpr auto kHwDevPortTag = "hw_port"; + +constexpr auto kClientIdTag = "client_id"; +constexpr auto kPortTag = "port"; +constexpr auto kBaudRateTag = "baudrate"; +constexpr auto kIpTag = "ip"; + +constexpr auto kInstSerialTypeTag = "master_inst_serial_type"; +constexpr auto kDataSerialTypeTag = "master_data_serial_type"; + +constexpr auto kInstSerialTypeJsonTag = "instruction_serialization_type"; +constexpr auto kDataSerialTypeJsonTag = "data_serialization_type"; + +constexpr auto kClientsJsonTag = "clients"; +constexpr auto kHwItemsJsonTag = "hwItems"; +constexpr auto kUINameTag = "user_interface_name"; +constexpr auto kUIMajorVTag = "user_interface_major_v"; +constexpr auto kUIMinorVTag = "user_interface_minor_v"; +constexpr auto kUIPatchVTag = "user_interface_patch_v"; + +constexpr bool float_eq(const float a, const float b) noexcept +{ + const auto maximum = std::max(std::fabs(a), std::fabs(b)); + return std::fabs(a - b) <= maximum * std::numeric_limits::epsilon(); +} +struct RadarPoint +{ + float x{}; + float y{}; + float z{}; + float radial_speed{}; + float power{}; + float rcs{}; + float noise{}; + float snr{}; + float azimuth_angle{}; + float elevation_angle{}; + float range{}; + constexpr friend bool operator==(const RadarPoint & p1, const RadarPoint & p2) noexcept + { + return float_eq(p1.x, p2.x) && float_eq(p1.y, p2.y) && float_eq(p1.z, p2.z) && + float_eq(p1.radial_speed, p2.radial_speed) && float_eq(p1.power, p2.power) && + float_eq(p1.rcs, p2.rcs) && float_eq(p1.noise, p2.noise) && float_eq(p1.snr, p2.snr) && + float_eq(p1.azimuth_angle, p2.azimuth_angle) && + float_eq(p1.elevation_angle, p2.elevation_angle) && float_eq(p1.range, p2.range); + } +}; + +LIDAR_UTILS__DEFINE_FIELD_GENERATOR_FOR_MEMBER(radial_speed); +LIDAR_UTILS__DEFINE_FIELD_GENERATOR_FOR_MEMBER(power); +LIDAR_UTILS__DEFINE_FIELD_GENERATOR_FOR_MEMBER(rcs); +LIDAR_UTILS__DEFINE_FIELD_GENERATOR_FOR_MEMBER(noise); +LIDAR_UTILS__DEFINE_FIELD_GENERATOR_FOR_MEMBER(snr); +LIDAR_UTILS__DEFINE_FIELD_GENERATOR_FOR_MEMBER(azimuth_angle); +LIDAR_UTILS__DEFINE_FIELD_GENERATOR_FOR_MEMBER(elevation_angle); +LIDAR_UTILS__DEFINE_FIELD_GENERATOR_FOR_MEMBER(range); +using Generators = std::tuple< + point_cloud_msg_wrapper::field_x_generator, point_cloud_msg_wrapper::field_y_generator, + point_cloud_msg_wrapper::field_z_generator, field_radial_speed_generator, field_power_generator, + field_rcs_generator, field_noise_generator, field_snr_generator, field_azimuth_angle_generator, + field_elevation_angle_generator, field_range_generator>; +using RadarCloudModifier = PointCloud2Modifier; + +struct ObjectPoint +{ + float x{}; + float y{}; + float z{}; + float speed_absolute{}; + float heading{}; + float length{}; + float mileage{}; + float quality{}; + float acceleration{}; + int16_t object_id{}; + uint16_t idle_cycles{}; + uint16_t spline_idx{}; + uint8_t object_class{}; + uint16_t status{}; + + constexpr friend bool operator==(const ObjectPoint & p1, const ObjectPoint & p2) noexcept + { + return float_eq(p1.x, p2.x) && float_eq(p1.y, p2.y) && float_eq(p1.z, p2.z) && + float_eq(p1.speed_absolute, p2.speed_absolute) && float_eq(p1.heading, p2.heading) && + float_eq(p1.length, p2.length) && float_eq(p1.mileage, p2.mileage) && + float_eq(p1.quality, p2.quality) && float_eq(p1.acceleration, p2.acceleration); + } +}; + +LIDAR_UTILS__DEFINE_FIELD_GENERATOR_FOR_MEMBER(speed_absolute); +LIDAR_UTILS__DEFINE_FIELD_GENERATOR_FOR_MEMBER(heading); +LIDAR_UTILS__DEFINE_FIELD_GENERATOR_FOR_MEMBER(length); +LIDAR_UTILS__DEFINE_FIELD_GENERATOR_FOR_MEMBER(mileage); +LIDAR_UTILS__DEFINE_FIELD_GENERATOR_FOR_MEMBER(quality); +LIDAR_UTILS__DEFINE_FIELD_GENERATOR_FOR_MEMBER(acceleration); +LIDAR_UTILS__DEFINE_FIELD_GENERATOR_FOR_MEMBER(object_id); +LIDAR_UTILS__DEFINE_FIELD_GENERATOR_FOR_MEMBER(idle_cycles); +LIDAR_UTILS__DEFINE_FIELD_GENERATOR_FOR_MEMBER(spline_idx); +LIDAR_UTILS__DEFINE_FIELD_GENERATOR_FOR_MEMBER(object_class); +LIDAR_UTILS__DEFINE_FIELD_GENERATOR_FOR_MEMBER(status); +using GeneratorsObjectPoint = std::tuple< + point_cloud_msg_wrapper::field_x_generator, point_cloud_msg_wrapper::field_y_generator, + point_cloud_msg_wrapper::field_z_generator, field_speed_absolute_generator, + field_heading_generator, field_length_generator, field_mileage_generator, field_quality_generator, + field_acceleration_generator, field_object_id_generator, field_idle_cycles_generator, + field_spline_idx_generator, field_object_class_generator, field_status_generator>; + +using ObjectPointCloudModifier = PointCloud2Modifier; +} // namespace + +namespace smartmicro +{ +namespace drivers +{ +namespace radar +{ +SmartmicroRadarNode::SmartmicroRadarNode(const rclcpp::NodeOptions & node_options) +: rclcpp::Node{"smartmicro_radar_node", node_options} +{ + update_config_files_from_params(); + update_service = std::make_shared(); + + const auto override = false; + setenv("SMART_ACCESS_CFG_FILE_PATH", kConfigFilePath, override); + + initialize_services(); + setup_publishers(); + + rclcpp::on_shutdown(std::bind(&SmartmicroRadarNode::on_shutdown_callback, this)); +} + +void SmartmicroRadarNode::initialize_services() +{ + // Getting the communication services + m_services = CommunicationServicesIface::Get(); + if (!m_services->Init()) { + throw std::runtime_error("Communication Service initialization failed"); + } + + // Getting the data stream service + data_umrra4_v1_0_1 = com::master::umrra4_automotive_v1_0_1::DataStreamServiceIface::Get(); + data_umrra4_v1_2_1 = com::master::umrra4_automotive_v1_2_1::DataStreamServiceIface::Get(); + data_umrra4_v1_4_0 = com::master::umrra4_automotive_v1_4_0::DataStreamServiceIface::Get(); + data_umrr11 = com::master::umrr11_t132_automotive_v1_1_2::DataStreamServiceIface::Get(); + data_umrr96 = com::master::umrr96_t153_automotive_v1_2_2::DataStreamServiceIface::Get(); + data_umrr9f_v1_1_1 = com::master::umrr9f_t169_automotive_v1_1_1::DataStreamServiceIface::Get(); + data_umrr9f_v2_0_0 = com::master::umrr9f_t169_automotive_v2_0_0::DataStreamServiceIface::Get(); + data_umrr9f_v2_1_1 = com::master::umrr9f_t169_automotive_v2_1_1::DataStreamServiceIface::Get(); + data_umrr9f_v2_2_1 = com::master::umrr9f_t169_automotive_v2_2_1::DataStreamServiceIface::Get(); + data_umrr9f_v2_4_1 = com::master::umrr9f_t169_automotive_v2_4_1::DataStreamServiceIface::Get(); + data_umrr9f_v3_0_0 = com::master::umrr9f_t169_automotive_v3_0_0::DataStreamServiceIface::Get(); + data_umrr9d_v1_0_3 = com::master::umrr9d_t152_automotive_v1_0_3::DataStreamServiceIface::Get(); + data_umrr9d_v1_2_2 = com::master::umrr9d_t152_automotive_v1_2_2::DataStreamServiceIface::Get(); + data_umrr9d_v1_4_1 = com::master::umrr9d_t152_automotive_v1_4_1::DataStreamServiceIface::Get(); + data_umrr9d_v1_5_0 = com::master::umrr9d_t152_automotive_v1_5_0::DataStreamServiceIface::Get(); + data_umrr9f_mse_v1_0_0 = com::master::umrr9f_t169_mse_v1_0_0::DataStreamServiceIface::Get(); + data_umrr9f_mse_v1_1_0 = com::master::umrr9f_t169_mse_v1_1_0::DataStreamServiceIface::Get(); + data_umrr9f_mse_v1_3_0 = com::master::umrr9f_t169_mse_v1_3_0::DataStreamServiceIface::Get(); + data_umrra4_mse_v1_0_0 = com::master::umrra4_mse_v1_0_0::DataStreamServiceIface::Get(); + data_umrra4_mse_v2_1_0 = com::master::umrra4_mse_v2_1_0::DataStreamServiceIface::Get(); + data_umrra1_v1_0_0 = com::master::umrra1_t166_b_automotive_v1_0_0::DataStreamServiceIface::Get(); + data_umrra1_v2_0_0 = com::master::umrra1_t166_b_automotive_v2_0_0::DataStreamServiceIface::Get(); + + // Wait for initailization + std::this_thread::sleep_for(std::chrono::seconds(2)); + RCLCPP_INFO(this->get_logger(), "Data stream services have been received!"); + + // create a ros2 service to change the radar parameters + mode_srv_ = create_service( + "smart_radar/set_radar_mode", + std::bind( + &SmartmicroRadarNode::set_radar_mode, this, std::placeholders::_1, std::placeholders::_2)); + + // create a ros2 service to change the IP address + ip_addr_srv_ = create_service( + "smart_radar/set_ip_address", + std::bind( + &SmartmicroRadarNode::ip_address, this, std::placeholders::_1, std::placeholders::_2)); + + // create a ros2 service to send command to radar + command_srv_ = create_service( + "smart_radar/send_command", + std::bind( + &SmartmicroRadarNode::radar_command, this, std::placeholders::_1, std::placeholders::_2)); + + // create a ros2 service to perform firmware download + download_srv_ = create_service( + "smart_radar/firmware_download", + std::bind( + &SmartmicroRadarNode::firmware_download, this, std::placeholders::_1, std::placeholders::_2)); + + // create a ros2 service to read the radar status + status_srv_ = create_service( + "smart_radar/get_radar_status", + std::bind( + &SmartmicroRadarNode::get_radar_status, this, std::placeholders::_1, std::placeholders::_2)); + + // create a ros2 service to read the radar modes + read_mode_srv_ = create_service( + "smart_radar/get_radar_mode", + std::bind( + &SmartmicroRadarNode::get_radar_mode, this, std::placeholders::_1, std::placeholders::_2)); + + RCLCPP_INFO(this->get_logger(), "Radar services are ready."); +} + +void SmartmicroRadarNode::setup_publishers() +{ + const auto validate_sensor = [](const auto & sensor) { + std::string_view pub_type{sensor.pub_type}; + std::string_view model{sensor.model}; + + const bool is_mse = (pub_type == kMseType); + const bool has_mse = (model.find(kMseType) != std::string_view::npos); + + if (is_mse == !has_mse) { + throw std::runtime_error( + std::string("Model name ") + (is_mse ? "must" : "must not") + + " contain 'mse' when pub_type is '" + std::string(pub_type) + "'"); + } + }; + + for (size_t i = 0; i < m_number_of_sensors; ++i) { + const auto & sensor = m_sensors[i]; + + validate_sensor(sensor); + + std::string_view link_type{sensor.link_type}; + if (link_type == kEthLink) { + port_publishers(sensor, i); + } else if (link_type == kCanLink) { + can_publishers(sensor, i); + } else { + RCLCPP_WARN(get_logger(), "Unknown link type for sensor %zu", i); + } + } +} + +void SmartmicroRadarNode::port_publishers(const detail::SensorConfig & sensor, size_t sensor_idx) +{ + std::string_view pub_type{m_sensors[sensor_idx].pub_type}; + + try { + if (pub_type == kMseType) { + m_publishers_obj[sensor_idx] = create_publisher( + "smart_radar/port_objects_" + std::to_string(sensor_idx), sensor.history_size); + m_publishers_port_obj_header[sensor_idx] = + create_publisher( + "smart_radar/port_objectheader_" + std::to_string(sensor_idx), sensor.history_size); + m_publishers[sensor_idx] = create_publisher( + "smart_radar/port_targets_" + std::to_string(sensor_idx), sensor.history_size); + m_publishers_port_target_header[sensor_idx] = + create_publisher( + "smart_radar/port_targetheader_" + std::to_string(sensor_idx), sensor.history_size); + + } else if (pub_type == kTargetType) { + m_publishers[sensor_idx] = create_publisher( + "smart_radar/port_targets_" + std::to_string(sensor_idx), sensor.history_size); + m_publishers_port_target_header[sensor_idx] = + create_publisher( + "smart_radar/port_targetheader_" + std::to_string(sensor_idx), sensor.history_size); + } else { + RCLCPP_ERROR(get_logger(), "Unknown publish type: %s", sensor.pub_type.c_str()); + throw std::invalid_argument("Unknown publish type"); + } + + RCLCPP_INFO(get_logger(), "Successfully created PORT publishers for sensor %zu", sensor_idx); + } catch (const std::exception & e) { + RCLCPP_ERROR( + get_logger(), "Failed to create publishers for sensor %zu: %s", sensor_idx, e.what()); + throw; + } + + if (sensor.model == "umrra4_mse_v2_1_0") { + if ( + com::types::ERROR_CODE_OK != + data_umrra4_mse_v2_1_0->RegisterComObjectListReceiveCallback( + sensor.id, std::bind( + &SmartmicroRadarNode::objectlist_callback_umrra4_mse_v2_1_0, this, sensor_idx, + std::placeholders::_1, std::placeholders::_2))) { + RCLCPP_INFO( + this->get_logger(), "Failed to register objectlist callback for sensor umrra4_mse_v2_1_0"); + } + if ( + com::types::ERROR_CODE_OK != + data_umrra4_mse_v2_1_0->RegisterComTargetListReceiveCallback( + sensor.id, std::bind( + &SmartmicroRadarNode::targetlist_callback_umrra4_mse_v2_1_0, this, sensor_idx, + std::placeholders::_1, std::placeholders::_2))) { + RCLCPP_INFO( + this->get_logger(), "Failed to register targetlist callback for sensor umrra4_mse_v2_1_0"); + } + } + if (sensor.model == "umrra4_mse_v1_0_0") { + if ( + com::types::ERROR_CODE_OK != + data_umrra4_mse_v1_0_0->RegisterComObjectListReceiveCallback( + sensor.id, std::bind( + &SmartmicroRadarNode::objectlist_callback_umrra4_mse_v1_0_0, this, sensor_idx, + std::placeholders::_1, std::placeholders::_2))) { + RCLCPP_INFO( + this->get_logger(), "Failed to register objectlist callback for sensor umrra4_mse_v1_0_0"); + } + if ( + com::types::ERROR_CODE_OK != + data_umrra4_mse_v1_0_0->RegisterComTargetListReceiveCallback( + sensor.id, std::bind( + &SmartmicroRadarNode::targetlist_callback_umrra4_mse_v1_0_0, this, sensor_idx, + std::placeholders::_1, std::placeholders::_2))) { + RCLCPP_INFO( + this->get_logger(), "Failed to register targetlist callback for sensor umrra4_mse_v1_0_0"); + } + } + if (sensor.model == "umrr9f_mse_v1_3_0") { + if ( + com::types::ERROR_CODE_OK != + data_umrr9f_mse_v1_3_0->RegisterComObjectListReceiveCallback( + sensor.id, std::bind( + &SmartmicroRadarNode::objectlist_callback_umrr9f_mse_v1_3_0, this, sensor_idx, + std::placeholders::_1, std::placeholders::_2))) { + RCLCPP_INFO( + this->get_logger(), "Failed to register objectlist callback for sensor umrr9f_mse_v1_3_0"); + } + if ( + com::types::ERROR_CODE_OK != + data_umrr9f_mse_v1_3_0->RegisterComTargetListReceiveCallback( + sensor.id, std::bind( + &SmartmicroRadarNode::targetlist_callback_umrr9f_mse_v1_3_0, this, sensor_idx, + std::placeholders::_1, std::placeholders::_2))) { + RCLCPP_INFO( + this->get_logger(), "Failed to register targetlist callback for sensor umrr9f_mse_v1_3_0"); + } + } + if (sensor.model == "umrr9f_mse_v1_1_0") { + if ( + com::types::ERROR_CODE_OK != + data_umrr9f_mse_v1_1_0->RegisterComObjectListReceiveCallback( + sensor.id, std::bind( + &SmartmicroRadarNode::objectlist_callback_umrr9f_mse_v1_1_0, this, sensor_idx, + std::placeholders::_1, std::placeholders::_2))) { + RCLCPP_INFO( + this->get_logger(), "Failed to register objectlist callback for sensor umrr9f_mse_v1_1_0"); + } + if ( + com::types::ERROR_CODE_OK != + data_umrr9f_mse_v1_1_0->RegisterComTargetListReceiveCallback( + sensor.id, std::bind( + &SmartmicroRadarNode::targetlist_callback_umrr9f_mse_v1_1_0, this, sensor_idx, + std::placeholders::_1, std::placeholders::_2))) { + RCLCPP_INFO( + this->get_logger(), "Failed to register targetlist callback for sensor umrr9f_mse_v1_1_0"); + } + } + if (sensor.model == "umrr9f_mse_v1_0_0") { + if ( + com::types::ERROR_CODE_OK != + data_umrr9f_mse_v1_0_0->RegisterComObjectListReceiveCallback( + sensor.id, std::bind( + &SmartmicroRadarNode::objectlist_callback_umrr9f_mse_v1_0_0, this, sensor_idx, + std::placeholders::_1, std::placeholders::_2))) { + RCLCPP_INFO( + this->get_logger(), "Failed to register objectlist callback for sensor umrr9f_mse_v1_0_0"); + } + if ( + com::types::ERROR_CODE_OK != + data_umrr9f_mse_v1_0_0->RegisterComTargetListReceiveCallback( + sensor.id, std::bind( + &SmartmicroRadarNode::targetlist_callback_umrr9f_mse_v1_0_0, this, sensor_idx, + std::placeholders::_1, std::placeholders::_2))) { + RCLCPP_INFO( + this->get_logger(), "Failed to register targetlist callback for sensor umrr9f_mse_v1_0_0"); + } + } + if ( + sensor.model == "umrr96_v1_2_2" && + com::types::ERROR_CODE_OK != + data_umrr96->RegisterComTargetListReceiveCallback( + sensor.id, std::bind( + &SmartmicroRadarNode::targetlist_callback_umrr96, this, sensor_idx, + std::placeholders::_1, std::placeholders::_2))) { + RCLCPP_INFO( + this->get_logger(), "Failed to register targetlist callback for sensor umrr96_v1_2_2"); + } + if ( + sensor.model == "umrr11_v1_1_2" && + com::types::ERROR_CODE_OK != + data_umrr11->RegisterComTargetListReceiveCallback( + sensor.id, std::bind( + &SmartmicroRadarNode::targetlist_callback_umrr11, this, sensor_idx, + std::placeholders::_1, std::placeholders::_2))) { + RCLCPP_INFO( + this->get_logger(), "Failed to register targetlist callback for sensor umrr11_v1_1_2"); + } + if ( + sensor.model == "umrr9f_v1_1_1" && + com::types::ERROR_CODE_OK != + data_umrr9f_v1_1_1->RegisterComTargetListPortReceiveCallback( + sensor.id, std::bind( + &SmartmicroRadarNode::targetlist_callback_umrr9f_v1_1_1, this, sensor_idx, + std::placeholders::_1, std::placeholders::_2))) { + RCLCPP_INFO( + this->get_logger(), "Failed to register targetlist callback for sensor umrr9f_v1_1_1"); + } + if ( + sensor.model == "umrr9f_v2_0_0" && + com::types::ERROR_CODE_OK != + data_umrr9f_v2_0_0->RegisterComTargetListPortReceiveCallback( + sensor.id, std::bind( + &SmartmicroRadarNode::targetlist_callback_umrr9f_v2_0_0, this, sensor_idx, + std::placeholders::_1, std::placeholders::_2))) { + RCLCPP_INFO( + this->get_logger(), "Failed to register targetlist callback for sensor umrr9f_v2_0_0"); + } + if ( + sensor.model == "umrr9f_v2_1_1" && + com::types::ERROR_CODE_OK != + data_umrr9f_v2_1_1->RegisterComTargetListReceiveCallback( + sensor.id, std::bind( + &SmartmicroRadarNode::targetlist_callback_umrr9f_v2_1_1, this, sensor_idx, + std::placeholders::_1, std::placeholders::_2))) { + RCLCPP_INFO( + this->get_logger(), "Failed to register targetlist callback for sensor umrr9f_v2_1_1"); + } + if ( + sensor.model == "umrr9f_v2_2_1" && + com::types::ERROR_CODE_OK != + data_umrr9f_v2_2_1->RegisterComTargetListReceiveCallback( + sensor.id, std::bind( + &SmartmicroRadarNode::targetlist_callback_umrr9f_v2_2_1, this, sensor_idx, + std::placeholders::_1, std::placeholders::_2))) { + RCLCPP_INFO( + this->get_logger(), "Failed to register targetlist callback for sensor umrr9f_v2_2_1"); + } + if ( + sensor.model == "umrr9f_v2_4_1" && + com::types::ERROR_CODE_OK != + data_umrr9f_v2_4_1->RegisterComTargetListReceiveCallback( + sensor.id, std::bind( + &SmartmicroRadarNode::targetlist_callback_umrr9f_v2_4_1, this, sensor_idx, + std::placeholders::_1, std::placeholders::_2))) { + RCLCPP_INFO( + this->get_logger(), "Failed to register targetlist callback for sensor umrr9f_v2_4_1"); + } + if ( + sensor.model == "umrr9f_v3_0_0" && + com::types::ERROR_CODE_OK != + data_umrr9f_v3_0_0->RegisterComTargetListReceiveCallback( + sensor.id, std::bind( + &SmartmicroRadarNode::targetlist_callback_umrr9f_v3_0_0, this, sensor_idx, + std::placeholders::_1, std::placeholders::_2))) { + RCLCPP_INFO( + this->get_logger(), "Failed to register targetlist callback for sensor umrr9f_v3_0_0"); + } + if ( + sensor.model == "umrr9d_v1_0_3" && + com::types::ERROR_CODE_OK != + data_umrr9d_v1_0_3->RegisterComTargetListReceiveCallback( + sensor.id, std::bind( + &SmartmicroRadarNode::targetlist_callback_umrr9d_v1_0_3, this, sensor_idx, + std::placeholders::_1, std::placeholders::_2))) { + RCLCPP_INFO( + this->get_logger(), "Failed to register targetlist callback for sensor umrr9d_v1_0_3"); + } + if ( + sensor.model == "umrr9d_v1_2_2" && + com::types::ERROR_CODE_OK != + data_umrr9d_v1_2_2->RegisterComTargetListReceiveCallback( + sensor.id, std::bind( + &SmartmicroRadarNode::targetlist_callback_umrr9d_v1_2_2, this, sensor_idx, + std::placeholders::_1, std::placeholders::_2))) { + RCLCPP_INFO( + this->get_logger(), "Failed to register targetlist callback for sensor umrr9d_v1_2_2"); + } + if ( + sensor.model == "umrr9d_v1_4_1" && + com::types::ERROR_CODE_OK != + data_umrr9d_v1_4_1->RegisterComTargetListReceiveCallback( + sensor.id, std::bind( + &SmartmicroRadarNode::targetlist_callback_umrr9d_v1_4_1, this, sensor_idx, + std::placeholders::_1, std::placeholders::_2))) { + RCLCPP_INFO( + this->get_logger(), "Failed to register targetlist callback for sensor umrr9d_v1_4_1"); + } + if ( + sensor.model == "umrr9d_v1_5_0" && + com::types::ERROR_CODE_OK != + data_umrr9d_v1_5_0->RegisterComTargetListReceiveCallback( + sensor.id, std::bind( + &SmartmicroRadarNode::targetlist_callback_umrr9d_v1_5_0, this, sensor_idx, + std::placeholders::_1, std::placeholders::_2))) { + RCLCPP_INFO( + this->get_logger(), "Failed to register targetlist callback for sensor umrr9d_v1_5_0"); + } + if ( + sensor.model == "umrra4_v1_0_1" && + com::types::ERROR_CODE_OK != + data_umrra4_v1_0_1->RegisterComTargetListReceiveCallback( + sensor.id, std::bind( + &SmartmicroRadarNode::targetlist_callback_umrra4_v1_0_1, this, sensor_idx, + std::placeholders::_1, std::placeholders::_2))) { + RCLCPP_INFO( + this->get_logger(), "Failed to register targetlist callback for sensor umrra4_v1_0_1"); + } + if ( + sensor.model == "umrra4_v1_2_1" && + com::types::ERROR_CODE_OK != + data_umrra4_v1_2_1->RegisterComTargetListReceiveCallback( + sensor.id, std::bind( + &SmartmicroRadarNode::targetlist_callback_umrra4_v1_2_1, this, sensor_idx, + std::placeholders::_1, std::placeholders::_2))) { + RCLCPP_INFO( + this->get_logger(), "Failed to register targetlist callback for sensor umrra4_v1_2_1"); + } + if ( + sensor.model == "umrra4_v1_4_0" && + com::types::ERROR_CODE_OK != + data_umrra4_v1_4_0->RegisterComTargetListReceiveCallback( + sensor.id, std::bind( + &SmartmicroRadarNode::targetlist_callback_umrra4_v1_4_0, this, sensor_idx, + std::placeholders::_1, std::placeholders::_2))) { + RCLCPP_INFO( + this->get_logger(), "Failed to register targetlist callback for sensor umrra4_v1_4_0"); + } + if ( + sensor.model == "umrra1_v1_0_0" && + com::types::ERROR_CODE_OK != + data_umrra1_v1_0_0->RegisterComTargetListReceiveCallback( + sensor.id, std::bind( + &SmartmicroRadarNode::targetlist_callback_umrra1_v1_0_0, this, sensor_idx, + std::placeholders::_1, std::placeholders::_2))) { + RCLCPP_INFO( + this->get_logger(), "Failed to register targetlist callback for sensor umrra1_v1_0_0"); + } + if ( + sensor.model == "umrra1_v2_0_0" && + com::types::ERROR_CODE_OK != + data_umrra1_v2_0_0->RegisterComTargetListReceiveCallback( + sensor.id, std::bind( + &SmartmicroRadarNode::targetlist_callback_umrra1_v2_0_0, this, sensor_idx, + std::placeholders::_1, std::placeholders::_2))) { + RCLCPP_INFO( + this->get_logger(), "Failed to register targetlist callback for sensor umrra1_v2_0_0"); + } +} + +void SmartmicroRadarNode::can_publishers(const detail::SensorConfig & sensor, size_t sensor_idx) +{ + std::string_view pub_type{m_sensors[sensor_idx].pub_type}; + + try { + if (pub_type == kMseType) { + m_publishers_obj[sensor_idx] = create_publisher( + "smart_radar/can_objects_" + std::to_string(sensor_idx), sensor.history_size); + m_publishers_can_obj_header[sensor_idx] = + create_publisher( + "smart_radar/can_objectheader_" + std::to_string(sensor_idx), sensor.history_size); + + m_publishers[sensor_idx] = create_publisher( + "smart_radar/can_targets_" + std::to_string(sensor_idx), sensor.history_size); + m_publishers_can_target_header[sensor_idx] = + create_publisher( + "smart_radar/can_targetheader_" + std::to_string(sensor_idx), sensor.history_size); + } else if (pub_type == kTargetType) { + m_publishers[sensor_idx] = create_publisher( + "smart_radar/can_targets_" + std::to_string(sensor_idx), sensor.history_size); + m_publishers_can_target_header[sensor_idx] = + create_publisher( + "smart_radar/can_targetheader_" + std::to_string(sensor_idx), sensor.history_size); + } else { + RCLCPP_INFO(this->get_logger(), "Unkwon publish type!"); + } + + RCLCPP_INFO(get_logger(), "Successfully created CAN publishers for sensor %zu", sensor_idx); + } catch (const std::exception & e) { + RCLCPP_ERROR( + get_logger(), "Failed to create publishers for sensor %zu: %s", sensor_idx, e.what()); + throw; + } + + if (sensor.model == "umrra4_can_mse_v2_1_0") { + if ( + com::types::ERROR_CODE_OK != + data_umrra4_mse_v2_1_0->RegisterComObjectBaseListReceiveCallback( + sensor.id, std::bind( + &SmartmicroRadarNode::CAN_objectlist_callback_umrra4_mse_v2_1_0, this, + sensor_idx, std::placeholders::_1, std::placeholders::_2))) { + RCLCPP_INFO( + this->get_logger(), + "Failed to register objectlist callback for sensor umrra4_can_mse_v2_1_0"); + } + if ( + com::types::ERROR_CODE_OK != + data_umrra4_mse_v2_1_0->RegisterComTargetBaseListReceiveCallback( + sensor.id, std::bind( + &SmartmicroRadarNode::CAN_targetlist_callback_umrra4_mse_v2_1_0, this, + sensor_idx, std::placeholders::_1, std::placeholders::_2))) { + RCLCPP_INFO( + this->get_logger(), + "Failed to register targetlist callback for sensor umrra4_can_mse_v2_1_0"); + } + } + if (sensor.model == "umrra4_can_mse_v1_0_0") { + if ( + com::types::ERROR_CODE_OK != + data_umrra4_mse_v1_0_0->RegisterComObjectBaseListReceiveCallback( + sensor.id, std::bind( + &SmartmicroRadarNode::CAN_objectlist_callback_umrra4_mse_v1_0_0, this, + sensor_idx, std::placeholders::_1, std::placeholders::_2))) { + RCLCPP_INFO( + this->get_logger(), + "Failed to register objectlist callback for sensor umrra4_can_mse_v1_0_0"); + } + if ( + com::types::ERROR_CODE_OK != + data_umrra4_mse_v1_0_0->RegisterComTargetBaseListReceiveCallback( + sensor.id, std::bind( + &SmartmicroRadarNode::CAN_targetlist_callback_umrra4_mse_v1_0_0, this, + sensor_idx, std::placeholders::_1, std::placeholders::_2))) { + RCLCPP_INFO( + this->get_logger(), + "Failed to register targetlist callback for sensor umrra4_can_mse_v1_0_0"); + } + } + if (sensor.model == "umrr9f_can_mse_v1_3_0") { + if ( + com::types::ERROR_CODE_OK != + data_umrr9f_mse_v1_3_0->RegisterComObjectBaseListReceiveCallback( + sensor.id, std::bind( + &SmartmicroRadarNode::CAN_objectlist_callback_umrr9f_mse_v1_3_0, this, + sensor_idx, std::placeholders::_1, std::placeholders::_2))) { + RCLCPP_INFO( + this->get_logger(), + "Failed to register objectlist callback for sensor umrr9f_can_mse_v1_3_0"); + } + if ( + com::types::ERROR_CODE_OK != + data_umrr9f_mse_v1_3_0->RegisterComTargetBaseListReceiveCallback( + sensor.id, std::bind( + &SmartmicroRadarNode::CAN_targetlist_callback_umrr9f_mse_v1_3_0, this, + sensor_idx, std::placeholders::_1, std::placeholders::_2))) { + RCLCPP_INFO( + this->get_logger(), + "Failed to register targetlist callback for sensor umrr9f_can_mse_v1_3_0"); + } + } + if (sensor.model == "umrr9f_can_mse_v1_1_0") { + if ( + com::types::ERROR_CODE_OK != + data_umrr9f_mse_v1_1_0->RegisterComObjectBaseListReceiveCallback( + sensor.id, std::bind( + &SmartmicroRadarNode::CAN_objectlist_callback_umrr9f_mse_v1_1_0, this, + sensor_idx, std::placeholders::_1, std::placeholders::_2))) { + RCLCPP_INFO( + this->get_logger(), + "Failed to register objectlist callback for sensor umrr9f_can_mse_v1_1_0"); + } + if ( + com::types::ERROR_CODE_OK != + data_umrr9f_mse_v1_1_0->RegisterComTargetBaseListReceiveCallback( + sensor.id, std::bind( + &SmartmicroRadarNode::CAN_targetlist_callback_umrr9f_mse_v1_1_0, this, + sensor_idx, std::placeholders::_1, std::placeholders::_2))) { + RCLCPP_INFO( + this->get_logger(), + "Failed to register targetlist callback for sensor umrr9f_can_mse_v1_1_0"); + } + } + if (sensor.model == "umrr9f_can_mse_v1_0_0") { + if ( + com::types::ERROR_CODE_OK != + data_umrr9f_mse_v1_0_0->RegisterComObjectBaseListReceiveCallback( + sensor.id, std::bind( + &SmartmicroRadarNode::CAN_objectlist_callback_umrr9f_mse_v1_0_0, this, + sensor_idx, std::placeholders::_1, std::placeholders::_2))) { + RCLCPP_INFO( + this->get_logger(), + "Failed to register objectlist callback for sensor umrr9f_can_mse_v1_0_0"); + } + if ( + com::types::ERROR_CODE_OK != + data_umrr9f_mse_v1_0_0->RegisterComTargetBaseListReceiveCallback( + sensor.id, std::bind( + &SmartmicroRadarNode::CAN_targetlist_callback_umrr9f_mse_v1_0_0, this, + sensor_idx, std::placeholders::_1, std::placeholders::_2))) { + RCLCPP_INFO( + this->get_logger(), + "Failed to register targetlist callback for sensor umrr9f_can_mse_v1_0_0"); + } + } + if ( + sensor.model == "umrr96_can_v1_2_2" && + com::types::ERROR_CODE_OK != + data_umrr96->RegisterComTargetBaseListReceiveCallback( + sensor.id, std::bind( + &SmartmicroRadarNode::CAN_targetlist_callback_umrr96, this, sensor_idx, + std::placeholders::_1, std::placeholders::_2))) { + RCLCPP_INFO( + this->get_logger(), "Failed to register CAN targetlist for sensor umrr96_can_v1_2_2"); + } + if ( + sensor.model == "umrr11_can_v1_1_2" && + com::types::ERROR_CODE_OK != + data_umrr11->RegisterComTargetBaseListReceiveCallback( + sensor.id, std::bind( + &SmartmicroRadarNode::CAN_targetlist_callback_umrr11, this, sensor_idx, + std::placeholders::_1, std::placeholders::_2))) { + RCLCPP_INFO( + this->get_logger(), "Failed to register CAN targetlist for sensor umrr11_can_v1_1_2"); + } + if ( + sensor.model == "umrr9f_can_v2_1_1" && + com::types::ERROR_CODE_OK != + data_umrr9f_v2_1_1->RegisterComTargetBaseListReceiveCallback( + sensor.id, std::bind( + &SmartmicroRadarNode::CAN_targetlist_callback_umrr9f_v2_1_1, this, sensor_idx, + std::placeholders::_1, std::placeholders::_2))) { + RCLCPP_INFO( + this->get_logger(), "Failed to register CAN targetlist for sensor umrr9f_can_v2_1_1"); + } + if ( + sensor.model == "umrr9f_can_v2_2_1" && + com::types::ERROR_CODE_OK != + data_umrr9f_v2_2_1->RegisterComTargetBaseListReceiveCallback( + sensor.id, std::bind( + &SmartmicroRadarNode::CAN_targetlist_callback_umrr9f_v2_2_1, this, sensor_idx, + std::placeholders::_1, std::placeholders::_2))) { + RCLCPP_INFO( + this->get_logger(), "Failed to register CAN targetlist for sensor umrr9f_can_v2_2_1"); + } + if ( + sensor.model == "umrr9f_can_v2_4_1" && + com::types::ERROR_CODE_OK != + data_umrr9f_v2_4_1->RegisterComTargetBaseListReceiveCallback( + sensor.id, std::bind( + &SmartmicroRadarNode::CAN_targetlist_callback_umrr9f_v2_4_1, this, sensor_idx, + std::placeholders::_1, std::placeholders::_2))) { + RCLCPP_INFO( + this->get_logger(), "Failed to register CAN targetlist for sensor umrr9f_can_v2_4_1"); + } + if ( + sensor.model == "umrr9f_can_v3_0_0" && + com::types::ERROR_CODE_OK != + data_umrr9f_v3_0_0->RegisterComTargetBaseListReceiveCallback( + sensor.id, std::bind( + &SmartmicroRadarNode::CAN_targetlist_callback_umrr9f_v3_0_0, this, sensor_idx, + std::placeholders::_1, std::placeholders::_2))) { + RCLCPP_INFO( + this->get_logger(), "Failed to register CAN targetlist for sensor umrr9f_can_v3_0_0"); + } + if ( + sensor.model == "umrr9d_can_v1_0_3" && + com::types::ERROR_CODE_OK != + data_umrr9d_v1_0_3->RegisterComTargetBaseListReceiveCallback( + sensor.id, std::bind( + &SmartmicroRadarNode::CAN_targetlist_callback_umrr9d_v1_0_3, this, sensor_idx, + std::placeholders::_1, std::placeholders::_2))) { + RCLCPP_INFO( + this->get_logger(), "Failed to register CAN targetlist for sensor umrr9d_can_v1_0_3"); + } + if ( + sensor.model == "umrr9d_can_v1_2_2" && + com::types::ERROR_CODE_OK != + data_umrr9d_v1_2_2->RegisterComTargetBaseListReceiveCallback( + sensor.id, std::bind( + &SmartmicroRadarNode::CAN_targetlist_callback_umrr9d_v1_2_2, this, sensor_idx, + std::placeholders::_1, std::placeholders::_2))) { + RCLCPP_INFO( + this->get_logger(), "Failed to register CAN targetlist for sensor umrr9d_can_v1_2_2"); + } + if ( + sensor.model == "umrr9d_can_v1_4_1" && + com::types::ERROR_CODE_OK != + data_umrr9d_v1_4_1->RegisterComTargetBaseListReceiveCallback( + sensor.id, std::bind( + &SmartmicroRadarNode::CAN_targetlist_callback_umrr9d_v1_4_1, this, sensor_idx, + std::placeholders::_1, std::placeholders::_2))) { + RCLCPP_INFO( + this->get_logger(), "Failed to register CAN targetlist for sensor umrr9d_can_v1_4_1"); + } + if ( + sensor.model == "umrr9d_can_v1_5_0" && + com::types::ERROR_CODE_OK != + data_umrr9d_v1_5_0->RegisterComTargetBaseListReceiveCallback( + sensor.id, std::bind( + &SmartmicroRadarNode::CAN_targetlist_callback_umrr9d_v1_5_0, this, sensor_idx, + std::placeholders::_1, std::placeholders::_2))) { + RCLCPP_INFO( + this->get_logger(), "Failed to register CAN targetlist for sensor umrr9d_can_v1_5_0"); + } + if ( + sensor.model == "umrra4_can_v1_0_1" && + com::types::ERROR_CODE_OK != + data_umrra4_v1_0_1->RegisterComTargetBaseListReceiveCallback( + sensor.id, std::bind( + &SmartmicroRadarNode::CAN_targetlist_callback_umrra4_v1_0_1, this, sensor_idx, + std::placeholders::_1, std::placeholders::_2))) { + RCLCPP_INFO( + this->get_logger(), "Failed to register CAN targetlist for sensor umrra4_can_v1_0_1"); + } + if ( + sensor.model == "umrra4_can_v1_2_1" && + com::types::ERROR_CODE_OK != + data_umrra4_v1_2_1->RegisterComTargetBaseListReceiveCallback( + sensor.id, std::bind( + &SmartmicroRadarNode::CAN_targetlist_callback_umrra4_v1_2_1, this, sensor_idx, + std::placeholders::_1, std::placeholders::_2))) { + RCLCPP_INFO( + this->get_logger(), "Failed to register CAN targetlist for sensor umrra4_can_v1_2_1"); + } + if ( + sensor.model == "umrra4_can_v1_4_0" && + com::types::ERROR_CODE_OK != + data_umrra4_v1_4_0->RegisterComTargetBaseListReceiveCallback( + sensor.id, std::bind( + &SmartmicroRadarNode::CAN_targetlist_callback_umrra4_v1_4_0, this, sensor_idx, + std::placeholders::_1, std::placeholders::_2))) { + RCLCPP_INFO( + this->get_logger(), "Failed to register CAN targetlist for sensor umrra4_can_v1_4_0"); + } +} + +void SmartmicroRadarNode::on_shutdown_callback() +{ + RCLCPP_INFO(rclcpp::get_logger("rclcpp"), "Shutdown called!"); + check_signal = true; + rclcpp::Rate sleepRate(std::chrono::milliseconds(100)); + sleepRate.sleep(); + m_services.reset(); +} + +void SmartmicroRadarNode::firmware_download( + const std::shared_ptr request, + std::shared_ptr result) +{ + bool check_flag_id = false; + client_id = request->sensor_id; + update_image = request->file_path; + + for (auto & sensor : m_sensors) { + if (client_id == sensor.id) { + check_flag_id = true; + break; + } + } + if (!check_flag_id) { + result->res = "The sensor ID value entered is invalid! "; + return; + } + + update_service->StartSoftwareUpdate(client_id, update_image); + result->res = "Service ended, check the console output for status! "; +} + +void SmartmicroRadarNode::set_radar_mode( + const std::shared_ptr request, + std::shared_ptr result) +{ + // Validate sensor ID + bool check_flag_id = false; + client_id = request->sensor_id; + for (auto & sensor : m_sensors) { + if (client_id == sensor.id) { + check_flag_id = true; + break; + } + } + if (!check_flag_id) { + result->res = "Error: Sensor ID is invalid! "; + return; + } + + auto section_name = request->section_name; + if ( + section_name != "auto_interface_0dim" && section_name != "auto_interface_rrm" && + section_name != "Parameter") { + result->res = + "Error: Invalid section name specified! Must be 'auto_interface_0dim', " + "'auto_interface_rrm', or 'Parameter'."; + return; + } + + // Check arrays have same length + if ( + request->params.size() != request->values.size() || + request->params.size() != request->value_types.size()) { + result->res = "Error: param, values and value_types arrays must have same length"; + return; + } + + std::shared_ptr inst{m_services->GetInstructionService()}; + if (!inst) { + result->res = "Error: Failed to get instruction service"; + return; + } + + timer = this->create_wall_timer( + std::chrono::seconds(2), std::bind(&SmartmicroRadarNode::my_timer_callback, this)); + + std::shared_ptr batch; + if (!inst->AllocateInstructionBatch(client_id, batch)) { + result->res = "Error: Failed to allocate instruction! "; + return; + } + + for (size_t i = 0; i < request->params.size(); i++) { + const auto & param = request->params[i]; + const auto & value = request->values[i]; + const auto & value_type = request->value_types[i]; + bool request_added = false; + try { + switch (value_type) { + case 0: { + float float_value = std::stof(value); + auto radar_mode_float = + std::make_shared>(section_name, param, float_value); + request_added = batch->AddRequest(radar_mode_float); + break; + } + case 1: { + if (value.find('.') != std::string::npos) { + result->res = "Error: uint32 value cannot contain decimal points"; + return; + } + uint32_t u32_value = static_cast(std::stoul(value)); + auto radar_mode_u32 = + std::make_shared>(section_name, param, u32_value); + request_added = batch->AddRequest(radar_mode_u32); + break; + } + case 2: { + if (value.find('.') != std::string::npos) { + result->res = "Error: uint16 value cannot contain decimal points"; + return; + } + uint64_t temp = std::stoul(value); + if (temp > 65535) { + result->res = "Error: uint16 value must be between 0 and 65535"; + return; + } + uint16_t u16_value = static_cast(temp); + auto radar_mode_u16 = + std::make_shared>(section_name, param, u16_value); + request_added = batch->AddRequest(radar_mode_u16); + break; + } + case 3: { + if (value.find('.') != std::string::npos) { + result->res = "Error: uint8 value cannot contain decimal points"; + return; + } + uint64_t temp = std::stoul(value); + if (temp > 255) { + result->res = "Error: uint8 value must be between 0 and 255"; + return; + } + uint8_t u8_value = static_cast(temp); + auto radar_mode_u8 = + std::make_shared>(section_name, param, u8_value); + request_added = batch->AddRequest(radar_mode_u8); + break; + } + default: + result->res = + "Error: Invalid value_type specified. Must be 0 (f32), 1 (u32), 2 (u16), 3 (u8)"; + return; + } + } catch (const std::invalid_argument & e) { + result->res = "Error: Failed to convert value string, invalid format"; + return; + } catch (const std::out_of_range & e) { + result->res = "Error: Value is out of range for the specified type"; + return; + } + + if (!request_added) { + result->res = "Error: Failed to add instruction '" + param + "'! "; + return; + } + } + + if ( + com::types::ERROR_CODE_OK != + inst->SendInstructionBatch( + batch, std::bind( + &SmartmicroRadarNode::mode_response, this, client_id, std::placeholders::_2, + request->params, section_name))) { + result->res = "Error: Check params are valid for this sensor and values within range!"; + return; + } + result->res = "Success: Request sent successfully. Check main terminal for sensor response!"; + RCLCPP_INFO(this->get_logger(), "Service call result: %s", result->res.c_str()); +} + +void SmartmicroRadarNode::ip_address( + const std::shared_ptr request, + std::shared_ptr result) +{ + std::shared_ptr inst{m_services->GetInstructionService()}; + timer = this->create_wall_timer( + std::chrono::seconds(2), std::bind(&SmartmicroRadarNode::my_timer_callback, this)); + bool check_flag = false; + client_id = request->sensor_id; + for (auto & sensor : m_sensors) { + if (client_id == sensor.id) { + check_flag = true; + break; + } + } + if (!check_flag) { + result->res_ip = "Sensor ID entered is not listed in the param file! "; + return; + } + + std::shared_ptr batch; + if (!inst->AllocateInstructionBatch(client_id, batch)) { + result->res_ip = "Failed to allocate instruction! "; + return; + } + + std::shared_ptr> ip_address = + std::make_shared>( + "auto_interface_0dim", "ip_source_address", request->value_ip); + + std::shared_ptr cmd = + std::make_shared("auto_interface_command", "comp_eeprom_ctrl_save_param_sec", 2010); + + if (!batch->AddRequest(ip_address)) { + result->res_ip = "Failed to add instruction! "; + return; + } + if (!batch->AddRequest(cmd)) { + result->res_ip = "Failed to add instruction! "; + return; + } + // send instruction batch to the device + if ( + com::types::ERROR_CODE_OK != + inst->SendInstructionBatch( + batch, std::bind( + &SmartmicroRadarNode::sensor_response_ip, this, client_id, std::placeholders::_2))) { + result->res_ip = "Service not conducted"; + return; + } else { + RCLCPP_INFO( + this->get_logger(), + "Radar must be restarted and the parameters in the param file " + "must be updated !!."); + result->res_ip = + "Success: IP change executed successfully. Radar must be restarted " + "and the parameters in the param file must be updated"; + } +} + +void SmartmicroRadarNode::radar_command( + const std::shared_ptr request, + std::shared_ptr result) +{ + std::string command_name{}; + bool check_flag_id = false; + + command_name = request->command; + client_id = request->sensor_id; + + for (auto & sensor : m_sensors) { + if (client_id == sensor.id) { + check_flag_id = true; + break; + } + } + if (!check_flag_id) { + result->res = "The sensor ID value entered is invalid! "; + return; + } + + auto section_name = request->section_name; + if ( + section_name != "auto_interface_command" && section_name != "auto_interface_rrm_command" && + section_name != "Command") { + result->res = + "Error: Invalid section name specified! Must be 'auto_interface_command', " + "'auto_interface_rrm_command', or 'Command'."; + return; + } + + std::shared_ptr inst{m_services->GetInstructionService()}; + timer = this->create_wall_timer( + std::chrono::seconds(2), std::bind(&SmartmicroRadarNode::my_timer_callback, this)); + + std::shared_ptr batch; + + if (!inst->AllocateInstructionBatch(client_id, batch)) { + result->res = "Failed to allocate instruction! "; + return; + } + + std::shared_ptr radar_command = + std::make_shared(section_name, request->command, request->value); + + if (!batch->AddRequest(radar_command)) { + result->res = "Failed to add instruction! "; + return; + } + + if ( + com::types::ERROR_CODE_OK != inst->SendInstructionBatch( + batch, std::bind( + &SmartmicroRadarNode::command_response, this, client_id, + std::placeholders::_2, command_name, section_name))) { + result->res = "Error in sending command to the sensor!"; + return; + } + result->res = "Success: Request sent successfully."; +} + +void SmartmicroRadarNode::get_radar_status( + const std::shared_ptr request, + std::shared_ptr result) +{ + // Validate sensor ID + bool check_flag_id = false; + client_id = request->sensor_id; + for (auto & sensor : m_sensors) { + if (client_id == sensor.id) { + check_flag_id = true; + break; + } + } + if (!check_flag_id) { + result->res = "Error: Sensor ID is invalid! "; + return; + } + + auto section_name = request->section_name; + if ( + section_name != "auto_interface" && section_name != "auto_interface_rrm" && + section_name != "Status") { + result->res = + "Error: Invalid section name specified! Must be 'auto_interface', 'auto_interface_rrm', or " + "'Status'."; + return; + } + + // Check arrays have same length + if (request->statuses.size() != request->status_types.size()) { + result->res = "Error: status and status_types arrays must have same length"; + return; + } + + std::shared_ptr inst{m_services->GetInstructionService()}; + if (!inst) { + result->res = "Error: Failed to get instruction service"; + return; + } + + timer = this->create_wall_timer( + std::chrono::seconds(2), std::bind(&SmartmicroRadarNode::my_timer_callback, this)); + + std::shared_ptr batch; + if (!inst->AllocateInstructionBatch(client_id, batch)) { + result->res = "Error: Failed to allocate instruction! "; + return; + } + + for (size_t i = 0; i < request->statuses.size(); i++) { + const auto & status = request->statuses[i]; + const auto & status_type = request->status_types[i]; + bool request_added = false; + + switch (status_type) { + case 0: { + auto radar_status_u32 = std::make_shared>(section_name, status); + request_added = batch->AddRequest(radar_status_u32); + break; + } + case 1: { + auto radar_status_u16 = std::make_shared>(section_name, status); + request_added = batch->AddRequest(radar_status_u16); + break; + } + default: + result->res = "Error: Invalid value_type specified. Must be 0 (u32) or 1 (u8)"; + return; + } + + if (!request_added) { + result->res = "Error: Failed to add instruction '" + status + "' ! "; + return; + } + } + + if ( + com::types::ERROR_CODE_OK != + inst->SendInstructionBatch( + batch, std::bind( + &SmartmicroRadarNode::status_response, this, client_id, std::placeholders::_2, + request->statuses, section_name))) { + result->res = "Error: Check status are valid for this sensor!"; + return; + } + result->res = "Success: Request sent successfully. Check main terminal for sensor response!"; +} + +void SmartmicroRadarNode::get_radar_mode( + const std::shared_ptr request, + std::shared_ptr result) +{ + // Validate sensor ID + bool check_flag_id = false; + client_id = request->sensor_id; + for (auto & sensor : m_sensors) { + if (client_id == sensor.id) { + check_flag_id = true; + break; + } + } + if (!check_flag_id) { + result->res = "Error: Sensor ID is invalid! "; + return; + } + + auto section_name = request->section_name; + if ( + section_name != "auto_interface_0dim" && section_name != "auto_interface_rrm" && + section_name != "Parameter") { + result->res = + "Error: Invalid section name specified! Must be 'auto_interface_0dim', 'auto_interface_rrm', " + "or 'Parameter'."; + return; + } + + // Check arrays have same length + if (request->params.size() != request->param_types.size()) { + result->res = "Error: param and value_types arrays must have same length"; + return; + } + + std::shared_ptr inst{m_services->GetInstructionService()}; + if (!inst) { + result->res = "Error: Failed to get instruction service"; + return; + } + + timer = this->create_wall_timer( + std::chrono::seconds(2), std::bind(&SmartmicroRadarNode::my_timer_callback, this)); + + std::shared_ptr batch; + if (!inst->AllocateInstructionBatch(client_id, batch)) { + result->res = "Error: Failed to allocate instruction! "; + return; + } + + for (size_t i = 0; i < request->params.size(); i++) { + const auto & param = request->params[i]; + const auto & param_type = request->param_types[i]; + bool request_added = false; + + switch (param_type) { + case 0: { + auto radar_param_float = std::make_shared>(section_name, param); + request_added = batch->AddRequest(radar_param_float); + break; + } + case 1: { + auto radar_param_u32 = std::make_shared>(section_name, param); + request_added = batch->AddRequest(radar_param_u32); + break; + } + case 2: { + auto radar_param_u16 = std::make_shared>(section_name, param); + request_added = batch->AddRequest(radar_param_u16); + break; + } + case 3: { + auto radar_param_u8 = std::make_shared>(section_name, param); + request_added = batch->AddRequest(radar_param_u8); + break; + } + default: + result->res = + "Error: Invalid value_type specified. Must be 0(u32), 1(u16), 2(u8) or 3(float)"; + return; + } + + if (!request_added) { + result->res = "Error: Failed to add instruction '" + param + "' Check param types match! "; + return; + } + } + + if ( + com::types::ERROR_CODE_OK != + inst->SendInstructionBatch( + batch, std::bind( + &SmartmicroRadarNode::param_response, this, client_id, std::placeholders::_2, + request->params, section_name))) { + result->res = "Error: Check params are valid for this sensor!"; + return; + } + result->res = "Success: Request sent successfully. Check main terminal for sensor response!"; +} + +void SmartmicroRadarNode::mode_response( + const com::types::ClientId client_id, + const std::shared_ptr & response, + const std::vector & instruction_names, const std::string & section_name) +{ + for (const auto & instruction_name : instruction_names) { + std::vector>> resp_u8; + std::vector>> resp_u32; + std::vector>> resp_f; + bool response_found = false; + + if (response->GetResponse(section_name, instruction_name.c_str(), resp_u8)) { + response_found = true; + for (auto & resp : resp_u8) { + RCLCPP_INFO( + this->get_logger(), + "Response details:\n" + " Instruction: %s\n" + " Response type: %u\n" + " Value: %u\n", + resp->GetInstructionName().c_str(), resp->GetResponseType(), resp->GetValue()); + } + } + + if (response->GetResponse(section_name, instruction_name.c_str(), resp_u32)) { + response_found = true; + for (auto & resp : resp_u32) { + RCLCPP_INFO( + this->get_logger(), + "Response details:\n" + " Instruction: %s\n" + " Response type: %u\n" + " Value: %u\n", + resp->GetInstructionName().c_str(), resp->GetResponseType(), resp->GetValue()); + } + } + + if (response->GetResponse(section_name, instruction_name.c_str(), resp_f)) { + response_found = true; + for (auto & resp : resp_f) { + RCLCPP_INFO( + this->get_logger(), + "Response details:\n" + " Instruction: %s\n" + " Response type: %u\n" + " Value: %f\n", + resp->GetInstructionName().c_str(), resp->GetResponseType(), resp->GetValue()); + } + } + + if (!response_found) { + RCLCPP_WARN(this->get_logger(), "No response received!"); + } + } +} + +void SmartmicroRadarNode::sensor_response_ip( + const com::types::ClientId client_id, + const std::shared_ptr & response) +{ + std::vector>> resp_ip; + if (response->GetResponse("auto_interface_0dim", "ip_source_address", resp_ip)) { + for (auto & resp : resp_ip) { + RCLCPP_INFO( + this->get_logger(), + "Response details:\n" + " Instruction: %s\n" + " Response type: %u\n" + " Value: %u\n", + resp->GetInstructionName().c_str(), resp->GetResponseType(), resp->GetValue()); + } + } +} + +void SmartmicroRadarNode::command_response( + const com::types::ClientId client_id, + const std::shared_ptr & response, const std::string command_name, + const std::string & section_name) +{ + std::vector>> command_resp; + if (response->GetResponse(section_name, command_name.c_str(), command_resp)) { + for (auto & resp : command_resp) { + RCLCPP_INFO( + this->get_logger(), + "Response details:\n" + " Instruction: %s\n" + " Response type: %u\n" + " Value: %u\n", + resp->GetInstructionName().c_str(), resp->GetResponseType(), resp->GetValue()); + } + } +} + +void SmartmicroRadarNode::status_response( + const com::types::ClientId client_id, + const std::shared_ptr & response, + const std::vector & statuses, const std::string & section_name) +{ + for (const auto & instruction_name : statuses) { + std::vector>> resp_u16; + std::vector>> resp_u32; + bool response_found = false; + + if (response->GetResponse(section_name, instruction_name.c_str(), resp_u16)) { + response_found = true; + for (auto & resp : resp_u16) { + RCLCPP_INFO( + this->get_logger(), + "Response details:\n" + " Instruction: %s\n" + " Response type: %u\n" + " Value: %u\n", + resp->GetInstructionName().c_str(), resp->GetResponseType(), resp->GetValue()); + } + } + + if (response->GetResponse(section_name, instruction_name.c_str(), resp_u32)) { + response_found = true; + for (auto & resp : resp_u32) { + RCLCPP_INFO( + this->get_logger(), + "Response details:\n" + " Instruction: %s\n" + " Response type: %u\n" + " Value: %u\n", + resp->GetInstructionName().c_str(), resp->GetResponseType(), resp->GetValue()); + } + } + + if (!response_found) { + RCLCPP_WARN(this->get_logger(), "No response received!"); + } + } +} + +void SmartmicroRadarNode::param_response( + const com::types::ClientId client_id, + const std::shared_ptr & response, + const std::vector & statuses, const std::string & section_name) +{ + for (const auto & instruction_name : statuses) { + std::vector>> resp_u16; + std::vector>> resp_u32; + std::vector>> resp_u8; + std::vector>> resp_f; + bool response_found = false; + + if (response->GetResponse(section_name, instruction_name.c_str(), resp_u16)) { + response_found = true; + for (auto & resp : resp_u16) { + RCLCPP_INFO( + this->get_logger(), + "Response details:\n" + " Instruction: %s\n" + " Response type: %u\n" + " Value: %u\n", + resp->GetInstructionName().c_str(), resp->GetResponseType(), resp->GetValue()); + } + } + + if (response->GetResponse(section_name, instruction_name.c_str(), resp_u32)) { + response_found = true; + for (auto & resp : resp_u32) { + RCLCPP_INFO( + this->get_logger(), + "Response details:\n" + " Instruction: %s\n" + " Response type: %u\n" + " Value: %u\n", + resp->GetInstructionName().c_str(), resp->GetResponseType(), resp->GetValue()); + } + } + + if (response->GetResponse(section_name, instruction_name.c_str(), resp_u8)) { + response_found = true; + for (auto & resp : resp_u8) { + RCLCPP_INFO( + this->get_logger(), + "Response details:\n" + " Instruction: %s\n" + " Response type: %u\n" + " Value: %u\n", + resp->GetInstructionName().c_str(), resp->GetResponseType(), resp->GetValue()); + } + } + + if (response->GetResponse(section_name, instruction_name.c_str(), resp_f)) { + response_found = true; + for (auto & resp : resp_f) { + RCLCPP_INFO( + this->get_logger(), + "Response details:\n" + " Instruction: %s\n" + " Response type: %u\n" + " Value: %f\n", + resp->GetInstructionName().c_str(), resp->GetResponseType(), resp->GetValue()); + } + } + + if (!response_found) { + RCLCPP_WARN(this->get_logger(), "No response received!"); + } + } +} + +void SmartmicroRadarNode::objectlist_callback_umrra4_mse_v2_1_0( + const std::uint32_t sensor_idx, + const std::shared_ptr & + objectlist_port_umrra4_mse_v2_1_0, + const com::types::ClientId client_id) +{ + std::cout << "Objectlist for umrra4_mse_v2_1_0" << std::endl; + if (!check_signal) { + std::shared_ptr port_header; + port_header = objectlist_port_umrra4_mse_v2_1_0->GetPortHeader(); + std::shared_ptr object_header; + object_header = objectlist_port_umrra4_mse_v2_1_0->GetObjectListHeader(); + sensor_msgs::msg::PointCloud2 msg; + umrr_ros2_msgs::msg::PortObjectHeader header; + ObjectPointCloudModifier modifier{msg, m_sensors[sensor_idx].frame_id}; + const auto [sec, nanosec] = + convert_timestamp(std::chrono::microseconds{port_header->GetTimestamp()}); + msg.header.stamp.sec = sec; + msg.header.stamp.nanosec = nanosec; + header.frame_id = m_sensors[sensor_idx].frame_id; + header.port_identifier = port_header->GetPortIdentifier(); + header.port_ver_major = port_header->GetPortVersionMajor(); + header.port_ver_minor = port_header->GetPortVersionMinor(); + header.port_size = port_header->GetPortSize(); + header.body_endianness = port_header->GetBodyEndianness(); + header.port_index = port_header->GetPortIndex(); + header.header_ver_major = port_header->GetHeaderVersionMajor(); + header.header_ver_minor = port_header->GetHeaderVersionMinor(); + header.cycle_time = object_header->GetCycleTime(); + header.number_of_objects = object_header->GetNumberOfObjects(); + header.ts_measurement = object_header->GetTimestampOfMeasurement(); + for (const auto & object : objectlist_port_umrra4_mse_v2_1_0->GetObjectList()) { + const auto x_pos = object->GetPosX(); + const auto y_pos = object->GetPosY(); + const auto z_pos = object->GetPosZ(); + const auto speed_abs = object->GetSpeedAbs(); + const auto heading = object->GetHeading(); + const auto length = object->GetLength(); + const auto mileage = object->GetMileage(); + const auto quality = object->GetQuality(); + const auto acceleration = object->GetAcceleration(); + const auto object_id = object->GetObjectId(); + const auto idle_cycles = object->GetIdleCycles(); + const auto status = object->GetStatus(); + + modifier.push_back( + {x_pos, y_pos, z_pos, speed_abs, heading, length, mileage, quality, acceleration, object_id, + idle_cycles, status}); + } + + m_publishers_obj[sensor_idx]->publish(msg); + m_publishers_port_obj_header[sensor_idx]->publish(header); + } +} + +void SmartmicroRadarNode::targetlist_callback_umrra4_mse_v2_1_0( + const std::uint32_t sensor_idx, + const std::shared_ptr & + targetlist_port_umrra4_mse_v2_1_0, + const com::types::ClientId client_id) +{ + std::cout << "Port Targetlist for umrra4_mse_v2_1_0" << std::endl; + if (!check_signal) { + std::shared_ptr port_header; + port_header = targetlist_port_umrra4_mse_v2_1_0->GetPortHeader(); + std::shared_ptr target_header; + target_header = targetlist_port_umrra4_mse_v2_1_0->GetTargetListHeader(); + sensor_msgs::msg::PointCloud2 msg; + umrr_ros2_msgs::msg::PortTargetHeader header; + RadarCloudModifier modifier{msg, m_sensors[sensor_idx].frame_id}; + const auto [sec, nanosec] = + convert_timestamp(std::chrono::microseconds{port_header->GetTimestamp()}); + msg.header.stamp.sec = sec; + msg.header.stamp.nanosec = nanosec; + header.frame_id = m_sensors[sensor_idx].frame_id; + header.port_identifier = port_header->GetPortIdentifier(); + header.port_ver_major = port_header->GetPortVersionMajor(); + header.port_ver_minor = port_header->GetPortVersionMinor(); + header.port_size = port_header->GetPortSize(); + header.body_endianness = port_header->GetBodyEndianness(); + header.port_index = port_header->GetPortIndex(); + header.header_ver_major = port_header->GetHeaderVersionMajor(); + header.header_ver_minor = port_header->GetHeaderVersionMinor(); + header.cycle_time = target_header->GetCycleTime(); + header.number_of_targets = target_header->GetNumberOfTargets(); + header.acquisition_tx_ant_idx = target_header->GetAcquisitionTxAntIdx(); + header.acquisition_sweep_idx = target_header->GetAcquisitionSweepIdx(); + header.acquisition_cf_idx = target_header->GetAcquisitionCfIdx(); + header.prf = target_header->GetPrf(); + header.umambiguous_speed = target_header->GetUmambiguousSpeed(); + header.acquisition_start = target_header->GetAcquisitionStart(); + for (const auto & target : targetlist_port_umrra4_mse_v2_1_0->GetTargetList()) { + const auto range = target->GetRange(); + const auto elevation_angle = target->GetElevationAngle(); + const auto range_2d = range * std::cos(elevation_angle); + const auto azimuth_angle = target->GetAzimuthAngle(); + const auto snr = target->GetPower() - target->GetNoise(); + modifier.push_back( + {range_2d * std::cos(azimuth_angle), range_2d * std::sin(azimuth_angle), + range * std::sin(elevation_angle), target->GetSpeedRadial(), target->GetPower(), + target->GetRcs(), target->GetNoise(), snr, azimuth_angle, elevation_angle, range}); + } + + m_publishers[sensor_idx]->publish(msg); + m_publishers_port_target_header[sensor_idx]->publish(header); + } +} + +void SmartmicroRadarNode::objectlist_callback_umrra4_mse_v1_0_0( + const std::uint32_t sensor_idx, + const std::shared_ptr & + objectlist_port_umrra4_mse_v1_0_0, + const com::types::ClientId client_id) +{ + std::cout << "Objectlist for umrra4_mse_v1_0_0" << std::endl; + if (!check_signal) { + std::shared_ptr port_header; + port_header = objectlist_port_umrra4_mse_v1_0_0->GetPortHeader(); + std::shared_ptr object_header; + object_header = objectlist_port_umrra4_mse_v1_0_0->GetObjectListHeader(); + sensor_msgs::msg::PointCloud2 msg; + umrr_ros2_msgs::msg::PortObjectHeader header; + ObjectPointCloudModifier modifier{msg, m_sensors[sensor_idx].frame_id}; + const auto [sec, nanosec] = + convert_timestamp(std::chrono::microseconds{port_header->GetTimestamp()}); + msg.header.stamp.sec = sec; + msg.header.stamp.nanosec = nanosec; + header.frame_id = m_sensors[sensor_idx].frame_id; + header.port_identifier = port_header->GetPortIdentifier(); + header.port_ver_major = port_header->GetPortVersionMajor(); + header.port_ver_minor = port_header->GetPortVersionMinor(); + header.port_size = port_header->GetPortSize(); + header.body_endianness = port_header->GetBodyEndianness(); + header.port_index = port_header->GetPortIndex(); + header.header_ver_major = port_header->GetHeaderVersionMajor(); + header.header_ver_minor = port_header->GetHeaderVersionMinor(); + header.cycle_time = object_header->GetCycleTime(); + header.number_of_objects = object_header->GetNumberOfObjects(); + header.ts_measurement = object_header->GetTimestampOfMeasurement(); + for (const auto & object : objectlist_port_umrra4_mse_v1_0_0->GetObjectList()) { + const auto x_pos = object->GetPosX(); + const auto y_pos = object->GetPosY(); + const auto z_pos = object->GetPosZ(); + const auto speed_abs = object->GetSpeedAbs(); + const auto heading = object->GetHeading(); + const auto length = object->GetLength(); + const auto mileage = object->GetMileage(); + const auto quality = object->GetQuality(); + const auto acceleration = object->GetAcceleration(); + const auto object_id = object->GetObjectId(); + const auto idle_cycles = object->GetIdleCycles(); + const auto status = object->GetStatus(); + + modifier.push_back( + {x_pos, y_pos, z_pos, speed_abs, heading, length, mileage, quality, acceleration, object_id, + idle_cycles, status}); + } + + m_publishers_obj[sensor_idx]->publish(msg); + m_publishers_port_obj_header[sensor_idx]->publish(header); + } +} + +void SmartmicroRadarNode::targetlist_callback_umrra4_mse_v1_0_0( + const std::uint32_t sensor_idx, + const std::shared_ptr & + targetlist_port_umrra4_mse_v1_0_0, + const com::types::ClientId client_id) +{ + std::cout << "Port Targetlist for umrra4_mse_v1_0_0" << std::endl; + if (!check_signal) { + std::shared_ptr port_header; + port_header = targetlist_port_umrra4_mse_v1_0_0->GetPortHeader(); + std::shared_ptr target_header; + target_header = targetlist_port_umrra4_mse_v1_0_0->GetTargetListHeader(); + sensor_msgs::msg::PointCloud2 msg; + umrr_ros2_msgs::msg::PortTargetHeader header; + RadarCloudModifier modifier{msg, m_sensors[sensor_idx].frame_id}; + const auto [sec, nanosec] = + convert_timestamp(std::chrono::microseconds{port_header->GetTimestamp()}); + msg.header.stamp.sec = sec; + msg.header.stamp.nanosec = nanosec; + header.frame_id = m_sensors[sensor_idx].frame_id; + header.port_identifier = port_header->GetPortIdentifier(); + header.port_ver_major = port_header->GetPortVersionMajor(); + header.port_ver_minor = port_header->GetPortVersionMinor(); + header.port_size = port_header->GetPortSize(); + header.body_endianness = port_header->GetBodyEndianness(); + header.port_index = port_header->GetPortIndex(); + header.header_ver_major = port_header->GetHeaderVersionMajor(); + header.header_ver_minor = port_header->GetHeaderVersionMinor(); + header.cycle_time = target_header->GetCycleTime(); + header.number_of_targets = target_header->GetNumberOfTargets(); + header.acquisition_tx_ant_idx = target_header->GetAcquisitionTxAntIdx(); + header.acquisition_sweep_idx = target_header->GetAcquisitionSweepIdx(); + header.acquisition_cf_idx = target_header->GetAcquisitionCfIdx(); + header.prf = target_header->GetPrf(); + header.umambiguous_speed = target_header->GetUmambiguousSpeed(); + header.acquisition_start = target_header->GetAcquisitionStart(); + for (const auto & target : targetlist_port_umrra4_mse_v1_0_0->GetTargetList()) { + const auto range = target->GetRange(); + const auto elevation_angle = target->GetElevationAngle(); + const auto range_2d = range * std::cos(elevation_angle); + const auto azimuth_angle = target->GetAzimuthAngle(); + const auto snr = target->GetPower() - target->GetNoise(); + modifier.push_back( + {range_2d * std::cos(azimuth_angle), range_2d * std::sin(azimuth_angle), + range * std::sin(elevation_angle), target->GetSpeedRadial(), target->GetPower(), + target->GetRcs(), target->GetNoise(), snr, azimuth_angle, elevation_angle, range}); + } + + m_publishers[sensor_idx]->publish(msg); + m_publishers_port_target_header[sensor_idx]->publish(header); + } +} + +void SmartmicroRadarNode::objectlist_callback_umrr9f_mse_v1_3_0( + const std::uint32_t sensor_idx, + const std::shared_ptr & + objectlist_port_umrr9f_mse_v1_3_0, + const com::types::ClientId client_id) +{ + std::cout << "Objectlist for umrr9f_mse_v1_3_0" << std::endl; + if (!check_signal) { + std::shared_ptr port_header; + port_header = objectlist_port_umrr9f_mse_v1_3_0->GetPortHeader(); + std::shared_ptr + object_header; + object_header = objectlist_port_umrr9f_mse_v1_3_0->GetObjectListHeader(); + sensor_msgs::msg::PointCloud2 msg; + umrr_ros2_msgs::msg::PortObjectHeader header; + ObjectPointCloudModifier modifier{msg, m_sensors[sensor_idx].frame_id}; + const auto [sec, nanosec] = + convert_timestamp(std::chrono::microseconds{port_header->GetTimestamp()}); + msg.header.stamp.sec = sec; + msg.header.stamp.nanosec = nanosec; + header.frame_id = m_sensors[sensor_idx].frame_id; + header.port_identifier = port_header->GetPortIdentifier(); + header.port_ver_major = port_header->GetPortVersionMajor(); + header.port_ver_minor = port_header->GetPortVersionMinor(); + header.port_size = port_header->GetPortSize(); + header.body_endianness = port_header->GetBodyEndianness(); + header.port_index = port_header->GetPortIndex(); + header.header_ver_major = port_header->GetHeaderVersionMajor(); + header.header_ver_minor = port_header->GetHeaderVersionMinor(); + header.cycle_time = object_header->GetCycleTime(); + header.number_of_objects = object_header->GetNumberOfObjects(); + header.ts_measurement = object_header->GetTimestampOfMeasurement(); + for (const auto & object : objectlist_port_umrr9f_mse_v1_3_0->GetObjectList()) { + const auto x_pos = object->GetPosX(); + const auto y_pos = object->GetPosY(); + const auto z_pos = object->GetPosZ(); + const auto speed_abs = object->GetSpeedAbs(); + const auto heading = object->GetHeading(); + const auto length = object->GetLength(); + const auto mileage = object->GetMileage(); + const auto quality = object->GetQuality(); + const auto acceleration = object->GetAcceleration(); + const auto object_id = object->GetObjectId(); + const auto idle_cycles = object->GetIdleCycles(); + const auto status = object->GetStatus(); + + modifier.push_back( + {x_pos, y_pos, z_pos, speed_abs, heading, length, mileage, quality, acceleration, object_id, + idle_cycles, status}); + } + + m_publishers_obj[sensor_idx]->publish(msg); + m_publishers_port_obj_header[sensor_idx]->publish(header); + } +} + +void SmartmicroRadarNode::targetlist_callback_umrr9f_mse_v1_3_0( + const std::uint32_t sensor_idx, + const std::shared_ptr & + targetlist_port_umrr9f_mse_v1_3_0, + const com::types::ClientId client_id) +{ + std::cout << "Port Targetlist for umrr9f_mse_v1_3_0" << std::endl; + if (!check_signal) { + std::shared_ptr port_header; + port_header = targetlist_port_umrr9f_mse_v1_3_0->GetPortHeader(); + std::shared_ptr + target_header; + target_header = targetlist_port_umrr9f_mse_v1_3_0->GetTargetListHeader(); + sensor_msgs::msg::PointCloud2 msg; + umrr_ros2_msgs::msg::PortTargetHeader header; + RadarCloudModifier modifier{msg, m_sensors[sensor_idx].frame_id}; + const auto [sec, nanosec] = + convert_timestamp(std::chrono::microseconds{port_header->GetTimestamp()}); + msg.header.stamp.sec = sec; + msg.header.stamp.nanosec = nanosec; + header.frame_id = m_sensors[sensor_idx].frame_id; + header.port_identifier = port_header->GetPortIdentifier(); + header.port_ver_major = port_header->GetPortVersionMajor(); + header.port_ver_minor = port_header->GetPortVersionMinor(); + header.port_size = port_header->GetPortSize(); + header.body_endianness = port_header->GetBodyEndianness(); + header.port_index = port_header->GetPortIndex(); + header.header_ver_major = port_header->GetHeaderVersionMajor(); + header.header_ver_minor = port_header->GetHeaderVersionMinor(); + header.cycle_time = target_header->GetCycleTime(); + header.number_of_targets = target_header->GetNumberOfTargets(); + header.acquisition_tx_ant_idx = target_header->GetAcquisitionTxAntIdx(); + header.acquisition_sweep_idx = target_header->GetAcquisitionSweepIdx(); + header.acquisition_cf_idx = target_header->GetAcquisitionCfIdx(); + header.prf = target_header->GetPrf(); + header.umambiguous_speed = target_header->GetUmambiguousSpeed(); + header.acquisition_start = target_header->GetAcquisitionStart(); + for (const auto & target : targetlist_port_umrr9f_mse_v1_3_0->GetTargetList()) { + const auto range = target->GetRange(); + const auto elevation_angle = target->GetElevationAngle(); + const auto range_2d = range * std::cos(elevation_angle); + const auto azimuth_angle = target->GetAzimuthAngle(); + const auto snr = target->GetPower() - target->GetNoise(); + modifier.push_back( + {range_2d * std::cos(azimuth_angle), range_2d * std::sin(azimuth_angle), + range * std::sin(elevation_angle), target->GetSpeedRadial(), target->GetPower(), + target->GetRcs(), target->GetNoise(), snr, azimuth_angle, elevation_angle, range}); + } + + m_publishers[sensor_idx]->publish(msg); + m_publishers_port_target_header[sensor_idx]->publish(header); + } +} + +void SmartmicroRadarNode::objectlist_callback_umrr9f_mse_v1_1_0( + const std::uint32_t sensor_idx, + const std::shared_ptr & + objectlist_port_umrr9f_mse_v1_1_0, + const com::types::ClientId client_id) +{ + std::cout << "Objectlist for umrr9f_mse_v1_1_0" << std::endl; + if (!check_signal) { + std::shared_ptr port_header; + port_header = objectlist_port_umrr9f_mse_v1_1_0->GetPortHeader(); + std::shared_ptr + object_header; + object_header = objectlist_port_umrr9f_mse_v1_1_0->GetObjectListHeader(); + sensor_msgs::msg::PointCloud2 msg; + umrr_ros2_msgs::msg::PortObjectHeader header; + ObjectPointCloudModifier modifier{msg, m_sensors[sensor_idx].frame_id}; + const auto [sec, nanosec] = + convert_timestamp(std::chrono::microseconds{port_header->GetTimestamp()}); + msg.header.stamp.sec = sec; + msg.header.stamp.nanosec = nanosec; + header.frame_id = m_sensors[sensor_idx].frame_id; + header.port_identifier = port_header->GetPortIdentifier(); + header.port_ver_major = port_header->GetPortVersionMajor(); + header.port_ver_minor = port_header->GetPortVersionMinor(); + header.port_size = port_header->GetPortSize(); + header.body_endianness = port_header->GetBodyEndianness(); + header.port_index = port_header->GetPortIndex(); + header.header_ver_major = port_header->GetHeaderVersionMajor(); + header.header_ver_minor = port_header->GetHeaderVersionMinor(); + header.cycle_time = object_header->GetCycleTime(); + header.number_of_objects = object_header->GetNumberOfObjects(); + header.ts_measurement = object_header->GetTimestampOfMeasurement(); + for (const auto & object : objectlist_port_umrr9f_mse_v1_1_0->GetObjectList()) { + const auto x_pos = object->GetPosX(); + const auto y_pos = object->GetPosY(); + const auto z_pos = object->GetPosZ(); + const auto speed_abs = object->GetSpeedAbs(); + const auto heading = object->GetHeading(); + const auto length = object->GetLength(); + const auto mileage = object->GetMileage(); + const auto quality = object->GetQuality(); + const auto acceleration = object->GetAcceleration(); + const auto object_id = object->GetObjectId(); + const auto idle_cycles = object->GetIdleCycles(); + const auto status = object->GetStatus(); + + modifier.push_back( + {x_pos, y_pos, z_pos, speed_abs, heading, length, mileage, quality, acceleration, object_id, + idle_cycles, status}); + } + + m_publishers_obj[sensor_idx]->publish(msg); + m_publishers_port_obj_header[sensor_idx]->publish(header); + } +} + +void SmartmicroRadarNode::targetlist_callback_umrr9f_mse_v1_1_0( + const std::uint32_t sensor_idx, + const std::shared_ptr & + targetlist_port_umrr9f_mse_v1_1_0, + const com::types::ClientId client_id) +{ + std::cout << "Port Targetlist for umrr9f_mse_v1_1_0" << std::endl; + if (!check_signal) { + std::shared_ptr port_header; + port_header = targetlist_port_umrr9f_mse_v1_1_0->GetPortHeader(); + std::shared_ptr + target_header; + target_header = targetlist_port_umrr9f_mse_v1_1_0->GetTargetListHeader(); + sensor_msgs::msg::PointCloud2 msg; + umrr_ros2_msgs::msg::PortTargetHeader header; + RadarCloudModifier modifier{msg, m_sensors[sensor_idx].frame_id}; + const auto [sec, nanosec] = + convert_timestamp(std::chrono::microseconds{port_header->GetTimestamp()}); + msg.header.stamp.sec = sec; + msg.header.stamp.nanosec = nanosec; + header.frame_id = m_sensors[sensor_idx].frame_id; + header.port_identifier = port_header->GetPortIdentifier(); + header.port_ver_major = port_header->GetPortVersionMajor(); + header.port_ver_minor = port_header->GetPortVersionMinor(); + header.port_size = port_header->GetPortSize(); + header.body_endianness = port_header->GetBodyEndianness(); + header.port_index = port_header->GetPortIndex(); + header.header_ver_major = port_header->GetHeaderVersionMajor(); + header.header_ver_minor = port_header->GetHeaderVersionMinor(); + header.cycle_time = target_header->GetCycleTime(); + header.number_of_targets = target_header->GetNumberOfTargets(); + header.acquisition_tx_ant_idx = target_header->GetAcquisitionTxAntIdx(); + header.acquisition_sweep_idx = target_header->GetAcquisitionSweepIdx(); + header.acquisition_cf_idx = target_header->GetAcquisitionCfIdx(); + header.prf = target_header->GetPrf(); + header.umambiguous_speed = target_header->GetUmambiguousSpeed(); + header.acquisition_start = target_header->GetAcquisitionStart(); + for (const auto & target : targetlist_port_umrr9f_mse_v1_1_0->GetTargetList()) { + const auto range = target->GetRange(); + const auto elevation_angle = target->GetElevationAngle(); + const auto range_2d = range * std::cos(elevation_angle); + const auto azimuth_angle = target->GetAzimuthAngle(); + const auto snr = target->GetPower() - target->GetNoise(); + modifier.push_back( + {range_2d * std::cos(azimuth_angle), range_2d * std::sin(azimuth_angle), + range * std::sin(elevation_angle), target->GetSpeedRadial(), target->GetPower(), + target->GetRcs(), target->GetNoise(), snr, azimuth_angle, elevation_angle, range}); + } + + m_publishers[sensor_idx]->publish(msg); + m_publishers_port_target_header[sensor_idx]->publish(header); + } +} + +void SmartmicroRadarNode::objectlist_callback_umrr9f_mse_v1_0_0( + const std::uint32_t sensor_idx, + const std::shared_ptr & + objectlist_port_umrr9f_mse_v1_0_0, + const com::types::ClientId client_id) +{ + std::cout << "Objectlist for umrr9f_mse_v1_0_0" << std::endl; + if (!check_signal) { + std::shared_ptr port_header; + port_header = objectlist_port_umrr9f_mse_v1_0_0->GetPortHeader(); + std::shared_ptr + object_header; + object_header = objectlist_port_umrr9f_mse_v1_0_0->GetObjectListHeader(); + sensor_msgs::msg::PointCloud2 msg; + umrr_ros2_msgs::msg::PortObjectHeader header; + ObjectPointCloudModifier modifier{msg, m_sensors[sensor_idx].frame_id}; + const auto [sec, nanosec] = + convert_timestamp(std::chrono::microseconds{port_header->GetTimestamp()}); + msg.header.stamp.sec = sec; + msg.header.stamp.nanosec = nanosec; + header.frame_id = m_sensors[sensor_idx].frame_id; + header.port_identifier = port_header->GetPortIdentifier(); + header.port_ver_major = port_header->GetPortVersionMajor(); + header.port_ver_minor = port_header->GetPortVersionMinor(); + header.port_size = port_header->GetPortSize(); + header.body_endianness = port_header->GetBodyEndianness(); + header.port_index = port_header->GetPortIndex(); + header.header_ver_major = port_header->GetHeaderVersionMajor(); + header.header_ver_minor = port_header->GetHeaderVersionMinor(); + header.cycle_time = object_header->GetCycleTime(); + header.number_of_objects = object_header->GetNumberOfObjects(); + header.ts_measurement = object_header->GetTimestampOfMeasurement(); + for (const auto & object : objectlist_port_umrr9f_mse_v1_0_0->GetObjectList()) { + const auto x_pos = object->GetPosX(); + const auto y_pos = object->GetPosY(); + const auto z_pos = object->GetPosZ(); + const auto speed_abs = object->GetSpeedAbs(); + const auto heading = object->GetHeading(); + const auto length = object->GetLength(); + const auto mileage = object->GetMileage(); + const auto quality = object->GetQuality(); + const auto acceleration = object->GetAcceleration(); + const auto object_id = object->GetObjectId(); + const auto idle_cycles = object->GetIdleCycles(); + const auto status = object->GetStatus(); + + modifier.push_back( + {x_pos, y_pos, z_pos, speed_abs, heading, length, mileage, quality, acceleration, object_id, + idle_cycles, status}); + } + + m_publishers_obj[sensor_idx]->publish(msg); + m_publishers_port_obj_header[sensor_idx]->publish(header); + } +} + +void SmartmicroRadarNode::targetlist_callback_umrr9f_mse_v1_0_0( + const std::uint32_t sensor_idx, + const std::shared_ptr & + targetlist_port_umrr9f_mse_v1_0_0, + const com::types::ClientId client_id) +{ + std::cout << "Port Targetlist for umrr9f_mse_v1_0_0" << std::endl; + if (!check_signal) { + std::shared_ptr port_header; + port_header = targetlist_port_umrr9f_mse_v1_0_0->GetPortHeader(); + std::shared_ptr + target_header; + target_header = targetlist_port_umrr9f_mse_v1_0_0->GetTargetListHeader(); + sensor_msgs::msg::PointCloud2 msg; + umrr_ros2_msgs::msg::PortTargetHeader header; + RadarCloudModifier modifier{msg, m_sensors[sensor_idx].frame_id}; + const auto [sec, nanosec] = + convert_timestamp(std::chrono::microseconds{port_header->GetTimestamp()}); + msg.header.stamp.sec = sec; + msg.header.stamp.nanosec = nanosec; + header.frame_id = m_sensors[sensor_idx].frame_id; + header.port_identifier = port_header->GetPortIdentifier(); + header.port_ver_major = port_header->GetPortVersionMajor(); + header.port_ver_minor = port_header->GetPortVersionMinor(); + header.port_size = port_header->GetPortSize(); + header.body_endianness = port_header->GetBodyEndianness(); + header.port_index = port_header->GetPortIndex(); + header.header_ver_major = port_header->GetHeaderVersionMajor(); + header.header_ver_minor = port_header->GetHeaderVersionMinor(); + header.cycle_time = target_header->GetCycleTime(); + header.number_of_targets = target_header->GetNumberOfTargets(); + header.acquisition_tx_ant_idx = target_header->GetAcquisitionTxAntIdx(); + header.acquisition_sweep_idx = target_header->GetAcquisitionSweepIdx(); + header.acquisition_cf_idx = target_header->GetAcquisitionCfIdx(); + header.prf = target_header->GetPrf(); + header.umambiguous_speed = target_header->GetUmambiguousSpeed(); + header.acquisition_start = target_header->GetAcquisitionStart(); + for (const auto & target : targetlist_port_umrr9f_mse_v1_0_0->GetTargetList()) { + const auto range = target->GetRange(); + const auto elevation_angle = target->GetElevationAngle(); + const auto range_2d = range * std::cos(elevation_angle); + const auto azimuth_angle = target->GetAzimuthAngle(); + const auto snr = target->GetPower() - target->GetNoise(); + modifier.push_back( + {range_2d * std::cos(azimuth_angle), range_2d * std::sin(azimuth_angle), + range * std::sin(elevation_angle), target->GetSpeedRadial(), target->GetPower(), + target->GetRcs(), target->GetNoise(), snr, azimuth_angle, elevation_angle, range}); + } + + m_publishers[sensor_idx]->publish(msg); + m_publishers_port_target_header[sensor_idx]->publish(header); + } +} + +void SmartmicroRadarNode::targetlist_callback_umrr96( + const std::uint32_t sensor_idx, + const std::shared_ptr & + targetlist_port_umrr96, + const com::types::ClientId client_id) +{ + std::cout << "Targetlist for umrr96_v1_2_2" << std::endl; + if (!check_signal) { + std::shared_ptr + port_header; + port_header = targetlist_port_umrr96->GetPortHeader(); + std::shared_ptr + target_header; + target_header = targetlist_port_umrr96->GetTargetListHeader(); + sensor_msgs::msg::PointCloud2 msg; + umrr_ros2_msgs::msg::PortTargetHeader header; + RadarCloudModifier modifier{msg, m_sensors[sensor_idx].frame_id}; + const auto [sec, nanosec] = + convert_timestamp(std::chrono::microseconds{port_header->GetTimestamp()}); + msg.header.stamp.sec = sec; + msg.header.stamp.nanosec = nanosec; + header.frame_id = m_sensors[sensor_idx].frame_id; + + header.port_identifier = port_header->GetPortIdentifier(); + header.port_ver_major = port_header->GetPortVersionMajor(); + header.port_ver_minor = port_header->GetPortVersionMinor(); + header.port_size = port_header->GetPortSize(); + header.body_endianness = port_header->GetBodyEndianness(); + header.port_index = port_header->GetPortIndex(); + header.header_ver_major = port_header->GetHeaderVersionMajor(); + header.header_ver_minor = port_header->GetHeaderVersionMinor(); + + header.cycle_time = target_header->GetCycleTime(); + header.number_of_targets = target_header->GetNumberOfTargets(); + for (const auto & target : targetlist_port_umrr96->GetTargetList()) { + const auto range = target->GetRange(); + const auto elevation_angle = target->GetElevationAngle(); + const auto range_2d = range * std::cos(elevation_angle); + const auto azimuth_angle = target->GetAzimuthAngle(); + const auto snr = target->GetPower() - target->GetNoise(); + modifier.push_back( + {range_2d * std::cos(azimuth_angle), range_2d * std::sin(azimuth_angle), + range * std::sin(elevation_angle), target->GetSpeedRadial(), target->GetPower(), + target->GetRCS(), target->GetNoise(), snr, azimuth_angle, elevation_angle, range}); + } + + m_publishers[sensor_idx]->publish(msg); + m_publishers_port_target_header[sensor_idx]->publish(header); + } +} + +void SmartmicroRadarNode::targetlist_callback_umrr11( + const std::uint32_t sensor_idx, + const std::shared_ptr & + targetlist_port_umrr11, + const com::types::ClientId client_id) +{ + std::cout << "Targetlist for umrr11_v1_1_2" << std::endl; + if (!check_signal) { + std::shared_ptr + port_header; + port_header = targetlist_port_umrr11->GetPortHeader(); + std::shared_ptr + target_header; + target_header = targetlist_port_umrr11->GetTargetListHeader(); + sensor_msgs::msg::PointCloud2 msg; + umrr_ros2_msgs::msg::PortTargetHeader header; + RadarCloudModifier modifier{msg, m_sensors[sensor_idx].frame_id}; + const auto [sec, nanosec] = + convert_timestamp(std::chrono::microseconds{port_header->GetTimestamp()}); + msg.header.stamp.sec = sec; + msg.header.stamp.nanosec = nanosec; + header.frame_id = m_sensors[sensor_idx].frame_id; + + header.port_identifier = port_header->GetPortIdentifier(); + header.port_ver_major = port_header->GetPortVersionMajor(); + header.port_ver_minor = port_header->GetPortVersionMinor(); + header.port_size = port_header->GetPortSize(); + header.body_endianness = port_header->GetBodyEndianness(); + header.port_index = port_header->GetPortIndex(); + header.header_ver_major = port_header->GetHeaderVersionMajor(); + header.header_ver_minor = port_header->GetHeaderVersionMinor(); + + header.cycle_time = target_header->GetCycleTime(); + header.number_of_targets = target_header->GetNumberOfTargets(); + for (const auto & target : targetlist_port_umrr11->GetTargetList()) { + const auto range = target->GetRange(); + const auto elevation_angle = target->GetElevationAngle(); + const auto range_2d = range * std::cos(elevation_angle); + const auto azimuth_angle = target->GetAzimuthAngle(); + const auto snr = target->GetPower() - target->GetNoise(); + modifier.push_back( + {range_2d * std::cos(azimuth_angle), range_2d * std::sin(azimuth_angle), + range * std::sin(elevation_angle), target->GetSpeedRadial(), target->GetPower(), + target->GetRCS(), target->GetNoise(), snr, azimuth_angle, elevation_angle, range}); + } + + m_publishers[sensor_idx]->publish(msg); + m_publishers_port_target_header[sensor_idx]->publish(header); + } +} + +void SmartmicroRadarNode::targetlist_callback_umrr9f_v1_1_1( + const std::uint32_t sensor_idx, + const std::shared_ptr< + com::master::umrr9f_t169_automotive_v1_1_1::comtargetlistport::ComTargetListPort> & + targetlist_port_umrr9f_v1_1_1, + const com::types::ClientId client_id) +{ + std::cout << "Targetlist for umrr9f_v1_1_1" << std::endl; + if (!check_signal) { + std::shared_ptr< + com::master::umrr9f_t169_automotive_v1_1_1::comtargetlistport::GenericPortHeader> + port_header; + port_header = targetlist_port_umrr9f_v1_1_1->GetGenericPortHeader(); + std::shared_ptr + target_header; + target_header = targetlist_port_umrr9f_v1_1_1->GetStaticPortHeader(); + sensor_msgs::msg::PointCloud2 msg; + umrr_ros2_msgs::msg::PortTargetHeader header; + RadarCloudModifier modifier{msg, m_sensors[sensor_idx].frame_id}; + const auto [sec, nanosec] = + convert_timestamp(std::chrono::microseconds{port_header->GetTimestamp()}); + msg.header.stamp.sec = sec; + msg.header.stamp.nanosec = nanosec; + header.frame_id = m_sensors[sensor_idx].frame_id; + + header.port_identifier = port_header->GetPortId(); + header.port_ver_major = port_header->GetPortVersionMajor(); + header.port_ver_minor = port_header->GetPortVersionMinor(); + header.port_size = port_header->GetPortSize(); + header.body_endianness = port_header->GetBodyEndianness(); + header.port_index = port_header->GetPortIndex(); + header.header_ver_major = port_header->GetHeaderVersionMajor(); + header.header_ver_minor = port_header->GetHeaderVersionMinor(); + + header.cycle_time = target_header->GetCycleTime(); + header.number_of_targets = target_header->GetNumberOfTargets(); + for (const auto & target : targetlist_port_umrr9f_v1_1_1->GetTargetList()) { + const auto range = target->GetRange(); + const auto elevation_angle = target->GetElevationAngle(); + const auto range_2d = range * std::cos(elevation_angle); + const auto azimuth_angle = target->GetAzimuthAngle(); + const auto snr = target->GetPower() - target->GetTgtNoise(); + modifier.push_back( + {range_2d * std::cos(azimuth_angle), range_2d * std::sin(azimuth_angle), + range * std::sin(elevation_angle), target->GetSpeedRadial(), target->GetPower(), + target->GetRCS(), target->GetTgtNoise(), snr, azimuth_angle, elevation_angle, range}); + } + + m_publishers[sensor_idx]->publish(msg); + m_publishers_port_target_header[sensor_idx]->publish(header); + } +} + +void SmartmicroRadarNode::targetlist_callback_umrr9f_v2_0_0( + const std::uint32_t sensor_idx, + const std::shared_ptr< + com::master::umrr9f_t169_automotive_v2_0_0::comtargetlistport::ComTargetListPort> & + targetlist_port_umrr9f_v2_0_0, + const com::types::ClientId client_id) +{ + std::cout << "Targetlist callback is being called for umrr9f_v2_0_0" << std::endl; + if (!check_signal) { + std::shared_ptr< + com::master::umrr9f_t169_automotive_v2_0_0::comtargetlistport::GenericPortHeader> + port_header; + port_header = targetlist_port_umrr9f_v2_0_0->GetGenericPortHeader(); + std::shared_ptr + target_header; + target_header = targetlist_port_umrr9f_v2_0_0->GetStaticPortHeader(); + sensor_msgs::msg::PointCloud2 msg; + umrr_ros2_msgs::msg::PortTargetHeader header; + RadarCloudModifier modifier{msg, m_sensors[sensor_idx].frame_id}; + const auto [sec, nanosec] = + convert_timestamp(std::chrono::microseconds{port_header->GetTimestamp()}); + msg.header.stamp.sec = sec; + msg.header.stamp.nanosec = nanosec; + header.frame_id = m_sensors[sensor_idx].frame_id; + + header.port_identifier = port_header->GetPortId(); + header.port_ver_major = port_header->GetPortVersionMajor(); + header.port_ver_minor = port_header->GetPortVersionMinor(); + header.port_size = port_header->GetPortSize(); + header.body_endianness = port_header->GetBodyEndianness(); + header.port_index = port_header->GetPortIndex(); + header.header_ver_major = port_header->GetHeaderVersionMajor(); + header.header_ver_minor = port_header->GetHeaderVersionMinor(); + + header.cycle_time = target_header->GetCycleTime(); + header.number_of_targets = target_header->GetNumberOfTargets(); + header.acquisition_tx_ant_idx = target_header->GetAcquisitionTxAnt(); + header.acquisition_sweep_idx = target_header->GetAcquisitionSweep(); + header.acquisition_cf_idx = target_header->GetAcquisitionTx(); + header.acquisition_start = target_header->GetAcquisitionStart(); + for (const auto & target : targetlist_port_umrr9f_v2_0_0->GetTargetList()) { + const auto range = target->GetRange(); + const auto elevation_angle = target->GetElevationAngle(); + const auto range_2d = range * std::cos(elevation_angle); + const auto azimuth_angle = target->GetAzimuthAngle(); + const auto snr = target->GetPower() - target->GetTgtNoise(); + modifier.push_back( + {range_2d * std::cos(azimuth_angle), range_2d * std::sin(azimuth_angle), + range * std::sin(elevation_angle), target->GetSpeedRadial(), target->GetPower(), + target->GetRCS(), target->GetTgtNoise(), snr, azimuth_angle, elevation_angle, range}); + } + + m_publishers[sensor_idx]->publish(msg); + m_publishers_port_target_header[sensor_idx]->publish(header); + } +} + +void SmartmicroRadarNode::targetlist_callback_umrr9f_v2_1_1( + const std::uint32_t sensor_idx, + const std::shared_ptr & + targetlist_port_umrr9f_v2_1_1, + const com::types::ClientId client_id) +{ + std::cout << "Targetlist for umrr9f_v2_1_1" << std::endl; + if (!check_signal) { + std::shared_ptr + port_header; + port_header = targetlist_port_umrr9f_v2_1_1->GetPortHeader(); + std::shared_ptr + target_header; + target_header = targetlist_port_umrr9f_v2_1_1->GetTargetListHeader(); + umrr_ros2_msgs::msg::PortTargetHeader header; + sensor_msgs::msg::PointCloud2 msg; + RadarCloudModifier modifier{msg, m_sensors[sensor_idx].frame_id}; + const auto [sec, nanosec] = + convert_timestamp(std::chrono::microseconds{port_header->GetTimestamp()}); + msg.header.stamp.sec = sec; + msg.header.stamp.nanosec = nanosec; + header.frame_id = m_sensors[sensor_idx].frame_id; + + header.port_identifier = port_header->GetPortIdentifier(); + header.port_ver_major = port_header->GetPortVersionMajor(); + header.port_ver_minor = port_header->GetPortVersionMinor(); + header.port_size = port_header->GetPortSize(); + header.body_endianness = port_header->GetBodyEndianness(); + header.port_index = port_header->GetPortIndex(); + header.header_ver_major = port_header->GetHeaderVersionMajor(); + header.header_ver_minor = port_header->GetHeaderVersionMinor(); + + header.cycle_time = target_header->GetCycleTime(); + header.number_of_targets = target_header->GetNumberOfTargets(); + header.acquisition_tx_ant_idx = target_header->GetAcquisitionTxAntIdx(); + header.acquisition_sweep_idx = target_header->GetAcquisitionSweepIdx(); + header.acquisition_cf_idx = target_header->GetAcquisitionCfIdx(); + header.acquisition_start = target_header->GetAcquisitionStart(); + for (const auto & target : targetlist_port_umrr9f_v2_1_1->GetTargetList()) { + const auto range = target->GetRange(); + const auto elevation_angle = target->GetElevationAngle(); + const auto range_2d = range * std::cos(elevation_angle); + const auto azimuth_angle = target->GetAzimuthAngle(); + const auto snr = target->GetPower() - target->GetNoise(); + modifier.push_back( + {range_2d * std::cos(azimuth_angle), range_2d * std::sin(azimuth_angle), + range * std::sin(elevation_angle), target->GetSpeedRadial(), target->GetPower(), + target->GetRcs(), target->GetNoise(), snr, azimuth_angle, elevation_angle, range}); + } + + m_publishers[sensor_idx]->publish(msg); + m_publishers_port_target_header[sensor_idx]->publish(header); + } +} + +void SmartmicroRadarNode::targetlist_callback_umrr9f_v2_2_1( + const std::uint32_t sensor_idx, + const std::shared_ptr & + targetlist_port_umrr9f_v2_2_1, + const com::types::ClientId client_id) +{ + std::cout << "Targetlist for umrr9f v2_2_1" << std::endl; + if (!check_signal) { + std::shared_ptr + port_header; + port_header = targetlist_port_umrr9f_v2_2_1->GetPortHeader(); + std::shared_ptr + target_header; + target_header = targetlist_port_umrr9f_v2_2_1->GetTargetListHeader(); + sensor_msgs::msg::PointCloud2 msg; + umrr_ros2_msgs::msg::PortTargetHeader header; + RadarCloudModifier modifier{msg, m_sensors[sensor_idx].frame_id}; + const auto [sec, nanosec] = + convert_timestamp(std::chrono::microseconds{port_header->GetTimestamp()}); + msg.header.stamp.sec = sec; + msg.header.stamp.nanosec = nanosec; + header.frame_id = m_sensors[sensor_idx].frame_id; + + header.port_identifier = port_header->GetPortIdentifier(); + header.port_ver_major = port_header->GetPortVersionMajor(); + header.port_ver_minor = port_header->GetPortVersionMinor(); + header.port_size = port_header->GetPortSize(); + header.body_endianness = port_header->GetBodyEndianness(); + header.port_index = port_header->GetPortIndex(); + header.header_ver_major = port_header->GetHeaderVersionMajor(); + header.header_ver_minor = port_header->GetHeaderVersionMinor(); + + header.cycle_time = target_header->GetCycleTime(); + header.number_of_targets = target_header->GetNumberOfTargets(); + header.acquisition_tx_ant_idx = target_header->GetAcquisitionTxAntIdx(); + header.acquisition_sweep_idx = target_header->GetAcquisitionSweepIdx(); + header.acquisition_cf_idx = target_header->GetAcquisitionCfIdx(); + header.prf = target_header->GetPrf(); + header.umambiguous_speed = target_header->GetUmambiguousSpeed(); + header.acquisition_start = target_header->GetAcquisitionStart(); + for (const auto & target : targetlist_port_umrr9f_v2_2_1->GetTargetList()) { + const auto range = target->GetRange(); + const auto elevation_angle = target->GetElevationAngle(); + const auto range_2d = range * std::cos(elevation_angle); + const auto azimuth_angle = target->GetAzimuthAngle(); + const auto snr = target->GetPower() - target->GetNoise(); + modifier.push_back( + {range_2d * std::cos(azimuth_angle), range_2d * std::sin(azimuth_angle), + range * std::sin(elevation_angle), target->GetSpeedRadial(), target->GetPower(), + target->GetRcs(), target->GetNoise(), snr, azimuth_angle, elevation_angle, range}); + } + + m_publishers[sensor_idx]->publish(msg); + m_publishers_port_target_header[sensor_idx]->publish(header); + } +} + +void SmartmicroRadarNode::targetlist_callback_umrr9f_v2_4_1( + const std::uint32_t sensor_idx, + const std::shared_ptr & + targetlist_port_umrr9f_v2_4_1, + const com::types::ClientId client_id) +{ + std::cout << "Targetlist for umrr9f v2_4_1" << std::endl; + if (!check_signal) { + std::shared_ptr + port_header; + port_header = targetlist_port_umrr9f_v2_4_1->GetPortHeader(); + std::shared_ptr + target_header; + target_header = targetlist_port_umrr9f_v2_4_1->GetTargetListHeader(); + sensor_msgs::msg::PointCloud2 msg; + umrr_ros2_msgs::msg::PortTargetHeader header; + RadarCloudModifier modifier{msg, m_sensors[sensor_idx].frame_id}; + const auto [sec, nanosec] = + convert_timestamp(std::chrono::microseconds{port_header->GetTimestamp()}); + msg.header.stamp.sec = sec; + msg.header.stamp.nanosec = nanosec; + header.frame_id = m_sensors[sensor_idx].frame_id; + + header.port_identifier = port_header->GetPortIdentifier(); + header.port_ver_major = port_header->GetPortVersionMajor(); + header.port_ver_minor = port_header->GetPortVersionMinor(); + header.port_size = port_header->GetPortSize(); + header.body_endianness = port_header->GetBodyEndianness(); + header.port_index = port_header->GetPortIndex(); + header.header_ver_major = port_header->GetHeaderVersionMajor(); + header.header_ver_minor = port_header->GetHeaderVersionMinor(); + + header.cycle_time = target_header->GetCycleTime(); + header.number_of_targets = target_header->GetNumberOfTargets(); + header.acquisition_tx_ant_idx = target_header->GetAcquisitionTxAntIdx(); + header.acquisition_sweep_idx = target_header->GetAcquisitionSweepIdx(); + header.acquisition_cf_idx = target_header->GetAcquisitionCfIdx(); + header.prf = target_header->GetPrf(); + header.umambiguous_speed = target_header->GetUmambiguousSpeed(); + header.acquisition_start = target_header->GetAcquisitionStart(); + for (const auto & target : targetlist_port_umrr9f_v2_4_1->GetTargetList()) { + const auto range = target->GetRange(); + const auto elevation_angle = target->GetElevationAngle(); + const auto range_2d = range * std::cos(elevation_angle); + const auto azimuth_angle = target->GetAzimuthAngle(); + const auto snr = target->GetPower() - target->GetNoise(); + modifier.push_back( + {range_2d * std::cos(azimuth_angle), range_2d * std::sin(azimuth_angle), + range * std::sin(elevation_angle), target->GetSpeedRadial(), target->GetPower(), + target->GetRcs(), target->GetNoise(), snr, azimuth_angle, elevation_angle, range}); + } + + m_publishers[sensor_idx]->publish(msg); + m_publishers_port_target_header[sensor_idx]->publish(header); + } +} + +void SmartmicroRadarNode::targetlist_callback_umrr9f_v3_0_0( + const std::uint32_t sensor_idx, + const std::shared_ptr & + targetlist_port_umrr9f_v3_0_0, + const com::types::ClientId client_id) +{ + std::cout << "Targetlist for umrr9f v3_0_0" << std::endl; + if (!check_signal) { + std::shared_ptr + port_header; + port_header = targetlist_port_umrr9f_v3_0_0->GetPortHeader(); + std::shared_ptr + target_header; + target_header = targetlist_port_umrr9f_v3_0_0->GetTargetListHeader(); + sensor_msgs::msg::PointCloud2 msg; + umrr_ros2_msgs::msg::PortTargetHeader header; + RadarCloudModifier modifier{msg, m_sensors[sensor_idx].frame_id}; + const auto [sec, nanosec] = + convert_timestamp(std::chrono::microseconds{port_header->GetTimestamp()}); + msg.header.stamp.sec = sec; + msg.header.stamp.nanosec = nanosec; + header.frame_id = m_sensors[sensor_idx].frame_id; + + header.port_identifier = port_header->GetPortIdentifier(); + header.port_ver_major = port_header->GetPortVersionMajor(); + header.port_ver_minor = port_header->GetPortVersionMinor(); + header.port_size = port_header->GetPortSize(); + header.body_endianness = port_header->GetBodyEndianness(); + header.port_index = port_header->GetPortIndex(); + header.header_ver_major = port_header->GetHeaderVersionMajor(); + header.header_ver_minor = port_header->GetHeaderVersionMinor(); + + header.cycle_time = target_header->GetCycleTime(); + header.number_of_targets = target_header->GetNumberOfTargets(); + header.acquisition_tx_ant_idx = target_header->GetAcquisitionTxAntIdx(); + header.acquisition_sweep_idx = target_header->GetAcquisitionSweepIdx(); + header.acquisition_cf_idx = target_header->GetAcquisitionCfIdx(); + header.prf = target_header->GetPrf(); + header.umambiguous_speed = target_header->GetUmambiguousSpeed(); + header.acquisition_start = target_header->GetAcquisitionStart(); + for (const auto & target : targetlist_port_umrr9f_v3_0_0->GetTargetList()) { + const auto range = target->GetRange(); + const auto elevation_angle = target->GetElevationAngle(); + const auto range_2d = range * std::cos(elevation_angle); + const auto azimuth_angle = target->GetAzimuthAngle(); + const auto snr = target->GetPower() - target->GetNoise(); + modifier.push_back( + {range_2d * std::cos(azimuth_angle), range_2d * std::sin(azimuth_angle), + range * std::sin(elevation_angle), target->GetSpeedRadial(), target->GetPower(), + target->GetRcs(), target->GetNoise(), snr, azimuth_angle, elevation_angle, range}); + } + + m_publishers[sensor_idx]->publish(msg); + m_publishers_port_target_header[sensor_idx]->publish(header); + } +} + +void SmartmicroRadarNode::targetlist_callback_umrr9d_v1_0_3( + const std::uint32_t sensor_idx, + const std::shared_ptr & + targetlist_port_umrr9d_v1_0_3, + const com::types::ClientId client_id) +{ + std::cout << "Targetlist for umrr9d_v1_0_3" << std::endl; + if (!check_signal) { + std::shared_ptr + port_header; + port_header = targetlist_port_umrr9d_v1_0_3->GetPortHeader(); + std::shared_ptr + target_header; + target_header = targetlist_port_umrr9d_v1_0_3->GetTargetListHeader(); + sensor_msgs::msg::PointCloud2 msg; + umrr_ros2_msgs::msg::PortTargetHeader header; + RadarCloudModifier modifier{msg, m_sensors[sensor_idx].frame_id}; + const auto [sec, nanosec] = + convert_timestamp(std::chrono::microseconds{port_header->GetTimestamp()}); + msg.header.stamp.sec = sec; + msg.header.stamp.nanosec = nanosec; + header.frame_id = m_sensors[sensor_idx].frame_id; + + header.port_identifier = port_header->GetPortIdentifier(); + header.port_ver_major = port_header->GetPortVersionMajor(); + header.port_ver_minor = port_header->GetPortVersionMinor(); + header.port_size = port_header->GetPortSize(); + header.body_endianness = port_header->GetBodyEndianness(); + header.port_index = port_header->GetPortIndex(); + header.header_ver_major = port_header->GetHeaderVersionMajor(); + header.header_ver_minor = port_header->GetHeaderVersionMinor(); + + header.cycle_time = target_header->GetCycleTime(); + header.number_of_targets = target_header->GetNumberOfTargets(); + header.acquisition_tx_ant_idx = target_header->GetAcquisitionTxAntIdx(); + header.acquisition_sweep_idx = target_header->GetAcquisitionSweepIdx(); + header.acquisition_cf_idx = target_header->GetAcquisitionCfIdx(); + header.acquisition_start = target_header->GetAcquisitionStart(); + for (const auto & target : targetlist_port_umrr9d_v1_0_3->GetTargetList()) { + const auto range = target->GetRange(); + const auto elevation_angle = target->GetElevationAngle(); + const auto range_2d = range * std::cos(elevation_angle); + const auto azimuth_angle = target->GetAzimuthAngle(); + const auto snr = target->GetPower() - target->GetNoise(); + modifier.push_back( + {range_2d * std::cos(azimuth_angle), range_2d * std::sin(azimuth_angle), + range * std::sin(elevation_angle), target->GetSpeedRadial(), target->GetPower(), + target->GetRcs(), target->GetNoise(), snr, azimuth_angle, elevation_angle, range}); + } + + m_publishers[sensor_idx]->publish(msg); + m_publishers_port_target_header[sensor_idx]->publish(header); + } +} + +void SmartmicroRadarNode::targetlist_callback_umrr9d_v1_2_2( + const std::uint32_t sensor_idx, + const std::shared_ptr & + targetlist_port_umrr9d_v1_2_2, + const com::types::ClientId client_id) +{ + std::cout << "Targetlist for umrr9d_v1_2_2" << std::endl; + if (!check_signal) { + std::shared_ptr + port_header; + port_header = targetlist_port_umrr9d_v1_2_2->GetPortHeader(); + std::shared_ptr + target_header; + target_header = targetlist_port_umrr9d_v1_2_2->GetTargetListHeader(); + sensor_msgs::msg::PointCloud2 msg; + umrr_ros2_msgs::msg::PortTargetHeader header; + RadarCloudModifier modifier{msg, m_sensors[sensor_idx].frame_id}; + const auto [sec, nanosec] = + convert_timestamp(std::chrono::microseconds{port_header->GetTimestamp()}); + msg.header.stamp.sec = sec; + msg.header.stamp.nanosec = nanosec; + header.frame_id = m_sensors[sensor_idx].frame_id; + + header.port_identifier = port_header->GetPortIdentifier(); + header.port_ver_major = port_header->GetPortVersionMajor(); + header.port_ver_minor = port_header->GetPortVersionMinor(); + header.port_size = port_header->GetPortSize(); + header.body_endianness = port_header->GetBodyEndianness(); + header.port_index = port_header->GetPortIndex(); + header.header_ver_major = port_header->GetHeaderVersionMajor(); + header.header_ver_minor = port_header->GetHeaderVersionMinor(); + + header.cycle_time = target_header->GetCycleTime(); + header.number_of_targets = target_header->GetNumberOfTargets(); + header.acquisition_tx_ant_idx = target_header->GetAcquisitionTxAntIdx(); + header.acquisition_sweep_idx = target_header->GetAcquisitionSweepIdx(); + header.acquisition_cf_idx = target_header->GetAcquisitionCfIdx(); + header.prf = target_header->GetPrf(); + header.umambiguous_speed = target_header->GetUmambiguousSpeed(); + header.acquisition_start = target_header->GetAcquisitionStart(); + for (const auto & target : targetlist_port_umrr9d_v1_2_2->GetTargetList()) { + const auto range = target->GetRange(); + const auto elevation_angle = target->GetElevationAngle(); + const auto range_2d = range * std::cos(elevation_angle); + const auto azimuth_angle = target->GetAzimuthAngle(); + const auto snr = target->GetPower() - target->GetNoise(); + modifier.push_back( + {range_2d * std::cos(azimuth_angle), range_2d * std::sin(azimuth_angle), + range * std::sin(elevation_angle), target->GetSpeedRadial(), target->GetPower(), + target->GetRcs(), target->GetNoise(), snr, azimuth_angle, elevation_angle, range}); + } + + m_publishers[sensor_idx]->publish(msg); + m_publishers_port_target_header[sensor_idx]->publish(header); + } +} + +void SmartmicroRadarNode::targetlist_callback_umrr9d_v1_4_1( + const std::uint32_t sensor_idx, + const std::shared_ptr & + targetlist_port_umrr9d_v1_4_1, + const com::types::ClientId client_id) +{ + std::cout << "Targetlist for umrr9d_v1_4_1" << std::endl; + if (!check_signal) { + std::shared_ptr + port_header; + port_header = targetlist_port_umrr9d_v1_4_1->GetPortHeader(); + std::shared_ptr + target_header; + target_header = targetlist_port_umrr9d_v1_4_1->GetTargetListHeader(); + sensor_msgs::msg::PointCloud2 msg; + umrr_ros2_msgs::msg::PortTargetHeader header; + RadarCloudModifier modifier{msg, m_sensors[sensor_idx].frame_id}; + const auto [sec, nanosec] = + convert_timestamp(std::chrono::microseconds{port_header->GetTimestamp()}); + msg.header.stamp.sec = sec; + msg.header.stamp.nanosec = nanosec; + header.frame_id = m_sensors[sensor_idx].frame_id; + + header.port_identifier = port_header->GetPortIdentifier(); + header.port_ver_major = port_header->GetPortVersionMajor(); + header.port_ver_minor = port_header->GetPortVersionMinor(); + header.port_size = port_header->GetPortSize(); + header.body_endianness = port_header->GetBodyEndianness(); + header.port_index = port_header->GetPortIndex(); + header.header_ver_major = port_header->GetHeaderVersionMajor(); + header.header_ver_minor = port_header->GetHeaderVersionMinor(); + + header.cycle_time = target_header->GetCycleTime(); + header.number_of_targets = target_header->GetNumberOfTargets(); + header.acquisition_tx_ant_idx = target_header->GetAcquisitionTxAntIdx(); + header.acquisition_sweep_idx = target_header->GetAcquisitionSweepIdx(); + header.acquisition_cf_idx = target_header->GetAcquisitionCfIdx(); + header.prf = target_header->GetPrf(); + header.umambiguous_speed = target_header->GetUmambiguousSpeed(); + header.acquisition_start = target_header->GetAcquisitionStart(); + for (const auto & target : targetlist_port_umrr9d_v1_4_1->GetTargetList()) { + const auto range = target->GetRange(); + const auto elevation_angle = target->GetElevationAngle(); + const auto range_2d = range * std::cos(elevation_angle); + const auto azimuth_angle = target->GetAzimuthAngle(); + const auto snr = target->GetPower() - target->GetNoise(); + modifier.push_back( + {range_2d * std::cos(azimuth_angle), range_2d * std::sin(azimuth_angle), + range * std::sin(elevation_angle), target->GetSpeedRadial(), target->GetPower(), + target->GetRcs(), target->GetNoise(), snr, azimuth_angle, elevation_angle, range}); + } + + m_publishers[sensor_idx]->publish(msg); + m_publishers_port_target_header[sensor_idx]->publish(header); + } +} + +void SmartmicroRadarNode::targetlist_callback_umrr9d_v1_5_0( + const std::uint32_t sensor_idx, + const std::shared_ptr & + targetlist_port_umrr9d_v1_5_0, + const com::types::ClientId client_id) +{ + std::cout << "Targetlist for umrr9d_v1_5_0" << std::endl; + if (!check_signal) { + std::shared_ptr + port_header; + port_header = targetlist_port_umrr9d_v1_5_0->GetPortHeader(); + std::shared_ptr + target_header; + target_header = targetlist_port_umrr9d_v1_5_0->GetTargetListHeader(); + sensor_msgs::msg::PointCloud2 msg; + umrr_ros2_msgs::msg::PortTargetHeader header; + RadarCloudModifier modifier{msg, m_sensors[sensor_idx].frame_id}; + const auto [sec, nanosec] = + convert_timestamp(std::chrono::microseconds{port_header->GetTimestamp()}); + msg.header.stamp.sec = sec; + msg.header.stamp.nanosec = nanosec; + header.frame_id = m_sensors[sensor_idx].frame_id; + + header.port_identifier = port_header->GetPortIdentifier(); + header.port_ver_major = port_header->GetPortVersionMajor(); + header.port_ver_minor = port_header->GetPortVersionMinor(); + header.port_size = port_header->GetPortSize(); + header.body_endianness = port_header->GetBodyEndianness(); + header.port_index = port_header->GetPortIndex(); + header.header_ver_major = port_header->GetHeaderVersionMajor(); + header.header_ver_minor = port_header->GetHeaderVersionMinor(); + + header.cycle_time = target_header->GetCycleTime(); + header.number_of_targets = target_header->GetNumberOfTargets(); + header.acquisition_tx_ant_idx = target_header->GetAcquisitionTxAntIdx(); + header.acquisition_sweep_idx = target_header->GetAcquisitionSweepIdx(); + header.acquisition_cf_idx = target_header->GetAcquisitionCfIdx(); + header.prf = target_header->GetPrf(); + header.umambiguous_speed = target_header->GetUmambiguousSpeed(); + header.acquisition_start = target_header->GetAcquisitionStart(); + for (const auto & target : targetlist_port_umrr9d_v1_5_0->GetTargetList()) { + const auto range = target->GetRange(); + const auto elevation_angle = target->GetElevationAngle(); + const auto range_2d = range * std::cos(elevation_angle); + const auto azimuth_angle = target->GetAzimuthAngle(); + const auto snr = target->GetPower() - target->GetNoise(); + modifier.push_back( + {range_2d * std::cos(azimuth_angle), range_2d * std::sin(azimuth_angle), + range * std::sin(elevation_angle), target->GetSpeedRadial(), target->GetPower(), + target->GetRcs(), target->GetNoise(), snr, azimuth_angle, elevation_angle, range}); + } + + m_publishers[sensor_idx]->publish(msg); + m_publishers_port_target_header[sensor_idx]->publish(header); + } +} + +void SmartmicroRadarNode::targetlist_callback_umrra4_v1_0_1( + const std::uint32_t sensor_idx, + const std::shared_ptr & + targetlist_port_umrra4_v1_0_1, + const com::types::ClientId client_id) +{ + std::cout << "Targetlist for umrra4_v1_0_1" << std::endl; + if (!check_signal) { + std::shared_ptr port_header; + port_header = targetlist_port_umrra4_v1_0_1->GetPortHeader(); + std::shared_ptr + target_header; + target_header = targetlist_port_umrra4_v1_0_1->GetTargetListHeader(); + sensor_msgs::msg::PointCloud2 msg; + umrr_ros2_msgs::msg::PortTargetHeader header; + RadarCloudModifier modifier{msg, m_sensors[sensor_idx].frame_id}; + const auto [sec, nanosec] = + convert_timestamp(std::chrono::microseconds{port_header->GetTimestamp()}); + msg.header.stamp.sec = sec; + msg.header.stamp.nanosec = nanosec; + header.frame_id = m_sensors[sensor_idx].frame_id; + + header.port_identifier = port_header->GetPortIdentifier(); + header.port_ver_major = port_header->GetPortVersionMajor(); + header.port_ver_minor = port_header->GetPortVersionMinor(); + header.port_size = port_header->GetPortSize(); + header.body_endianness = port_header->GetBodyEndianness(); + header.port_index = port_header->GetPortIndex(); + header.header_ver_major = port_header->GetHeaderVersionMajor(); + header.header_ver_minor = port_header->GetHeaderVersionMinor(); + + header.cycle_time = target_header->GetCycleTime(); + header.number_of_targets = target_header->GetNumberOfTargets(); + header.acquisition_tx_ant_idx = target_header->GetAcquisitionTxAntIdx(); + header.acquisition_sweep_idx = target_header->GetAcquisitionSweepIdx(); + header.acquisition_cf_idx = target_header->GetAcquisitionCfIdx(); + header.prf = target_header->GetPrf(); + header.umambiguous_speed = target_header->GetUmambiguousSpeed(); + header.acquisition_start = target_header->GetAcquisitionStart(); + for (const auto & target : targetlist_port_umrra4_v1_0_1->GetTargetList()) { + const auto range = target->GetRange(); + const auto elevation_angle = target->GetElevationAngle(); + const auto range_2d = range * std::cos(elevation_angle); + const auto azimuth_angle = target->GetAzimuthAngle(); + const auto snr = target->GetPower() - target->GetNoise(); + modifier.push_back( + {range_2d * std::cos(azimuth_angle), range_2d * std::sin(azimuth_angle), + range * std::sin(elevation_angle), target->GetSpeedRadial(), target->GetPower(), + target->GetRcs(), target->GetNoise(), snr, azimuth_angle, elevation_angle, range}); + } + + m_publishers[sensor_idx]->publish(msg); + m_publishers_port_target_header[sensor_idx]->publish(header); + } +} + +void SmartmicroRadarNode::targetlist_callback_umrra4_v1_2_1( + const std::uint32_t sensor_idx, + const std::shared_ptr & + targetlist_port_umrra4_v1_2_1, + const com::types::ClientId client_id) +{ + std::cout << "Targetlist for umrra4_v1_2_1" << std::endl; + if (!check_signal) { + std::shared_ptr port_header; + port_header = targetlist_port_umrra4_v1_2_1->GetPortHeader(); + std::shared_ptr + target_header; + target_header = targetlist_port_umrra4_v1_2_1->GetTargetListHeader(); + sensor_msgs::msg::PointCloud2 msg; + umrr_ros2_msgs::msg::PortTargetHeader header; + RadarCloudModifier modifier{msg, m_sensors[sensor_idx].frame_id}; + const auto [sec, nanosec] = + convert_timestamp(std::chrono::microseconds{port_header->GetTimestamp()}); + msg.header.stamp.sec = sec; + msg.header.stamp.nanosec = nanosec; + header.port_identifier = port_header->GetPortIdentifier(); + header.port_ver_major = port_header->GetPortVersionMajor(); + header.port_ver_minor = port_header->GetPortVersionMinor(); + header.port_size = port_header->GetPortSize(); + header.body_endianness = port_header->GetBodyEndianness(); + header.port_index = port_header->GetPortIndex(); + header.header_ver_major = port_header->GetHeaderVersionMajor(); + header.header_ver_minor = port_header->GetHeaderVersionMinor(); + + header.frame_id = m_sensors[sensor_idx].frame_id; + header.cycle_time = target_header->GetCycleTime(); + header.number_of_targets = target_header->GetNumberOfTargets(); + header.acquisition_tx_ant_idx = target_header->GetAcquisitionTxAntIdx(); + header.acquisition_sweep_idx = target_header->GetAcquisitionSweepIdx(); + header.acquisition_cf_idx = target_header->GetAcquisitionCfIdx(); + header.prf = target_header->GetPrf(); + header.umambiguous_speed = target_header->GetUmambiguousSpeed(); + header.acquisition_start = target_header->GetAcquisitionStart(); + for (const auto & target : targetlist_port_umrra4_v1_2_1->GetTargetList()) { + const auto range = target->GetRange(); + const auto elevation_angle = target->GetElevationAngle(); + const auto range_2d = range * std::cos(elevation_angle); + const auto azimuth_angle = target->GetAzimuthAngle(); + const auto snr = target->GetPower() - target->GetNoise(); + modifier.push_back( + {range_2d * std::cos(azimuth_angle), range_2d * std::sin(azimuth_angle), + range * std::sin(elevation_angle), target->GetSpeedRadial(), target->GetPower(), + target->GetRcs(), target->GetNoise(), snr, azimuth_angle, elevation_angle, range}); + } + + m_publishers[sensor_idx]->publish(msg); + m_publishers_port_target_header[sensor_idx]->publish(header); + } +} + +void SmartmicroRadarNode::targetlist_callback_umrra4_v1_4_0( + const std::uint32_t sensor_idx, + const std::shared_ptr & + targetlist_port_umrra4_v1_4_0, + const com::types::ClientId client_id) +{ + std::cout << "Targetlist for umrra4_v1_4_0" << std::endl; + if (!check_signal) { + std::shared_ptr port_header; + port_header = targetlist_port_umrra4_v1_4_0->GetPortHeader(); + std::shared_ptr + target_header; + target_header = targetlist_port_umrra4_v1_4_0->GetTargetListHeader(); + sensor_msgs::msg::PointCloud2 msg; + umrr_ros2_msgs::msg::PortTargetHeader header; + RadarCloudModifier modifier{msg, m_sensors[sensor_idx].frame_id}; + const auto [sec, nanosec] = + convert_timestamp(std::chrono::microseconds{port_header->GetTimestamp()}); + msg.header.stamp.sec = sec; + msg.header.stamp.nanosec = nanosec; + header.port_identifier = port_header->GetPortIdentifier(); + header.port_ver_major = port_header->GetPortVersionMajor(); + header.port_ver_minor = port_header->GetPortVersionMinor(); + header.port_size = port_header->GetPortSize(); + header.body_endianness = port_header->GetBodyEndianness(); + header.port_index = port_header->GetPortIndex(); + header.header_ver_major = port_header->GetHeaderVersionMajor(); + header.header_ver_minor = port_header->GetHeaderVersionMinor(); + + header.frame_id = m_sensors[sensor_idx].frame_id; + header.cycle_time = target_header->GetCycleTime(); + header.number_of_targets = target_header->GetNumberOfTargets(); + header.acquisition_tx_ant_idx = target_header->GetAcquisitionTxAntIdx(); + header.acquisition_sweep_idx = target_header->GetAcquisitionSweepIdx(); + header.acquisition_cf_idx = target_header->GetAcquisitionCfIdx(); + header.prf = target_header->GetPrf(); + header.umambiguous_speed = target_header->GetUmambiguousSpeed(); + header.acquisition_start = target_header->GetAcquisitionStart(); + for (const auto & target : targetlist_port_umrra4_v1_4_0->GetTargetList()) { + const auto range = target->GetRange(); + const auto elevation_angle = target->GetElevationAngle(); + const auto range_2d = range * std::cos(elevation_angle); + const auto azimuth_angle = target->GetAzimuthAngle(); + const auto snr = target->GetPower() - target->GetNoise(); + modifier.push_back( + {range_2d * std::cos(azimuth_angle), range_2d * std::sin(azimuth_angle), + range * std::sin(elevation_angle), target->GetSpeedRadial(), target->GetPower(), + target->GetRcs(), target->GetNoise(), snr, azimuth_angle, elevation_angle, range}); + } + + m_publishers[sensor_idx]->publish(msg); + m_publishers_port_target_header[sensor_idx]->publish(header); + } +} + +void SmartmicroRadarNode::targetlist_callback_umrra1_v1_0_0( + const std::uint32_t sensor_idx, + const std::shared_ptr< + com::master::umrra1_t166_b_automotive_v1_0_0::comtargetlist::ComTargetList> & + targetlist_port_umrra1_v1_0_0, + const com::types::ClientId client_id) +{ + std::cout << "Targetlist for umrra1_v1_0_0" << std::endl; + if (!check_signal) { + std::shared_ptr + port_header; + port_header = targetlist_port_umrra1_v1_0_0->GetPortHeader(); + std::shared_ptr + target_header; + target_header = targetlist_port_umrra1_v1_0_0->GetTargetListHeader(); + sensor_msgs::msg::PointCloud2 msg; + umrr_ros2_msgs::msg::PortTargetHeader header; + RadarCloudModifier modifier{msg, m_sensors[sensor_idx].frame_id}; + const auto [sec, nanosec] = + convert_timestamp(std::chrono::microseconds{port_header->GetTimestamp()}); + msg.header.stamp.sec = sec; + msg.header.stamp.nanosec = nanosec; + header.port_identifier = port_header->GetPortIdentifier(); + header.port_ver_major = port_header->GetPortVersionMajor(); + header.port_ver_minor = port_header->GetPortVersionMinor(); + header.port_size = port_header->GetPortSize(); + header.body_endianness = port_header->GetBodyEndianness(); + header.port_index = port_header->GetPortIndex(); + header.header_ver_major = port_header->GetHeaderVersionMajor(); + header.header_ver_minor = port_header->GetHeaderVersionMinor(); + + header.frame_id = m_sensors[sensor_idx].frame_id; + header.cycle_time = target_header->GetCycleTime(); + header.number_of_targets = target_header->GetNumberOfTargets(); + header.acquisition_tx_ant_idx = target_header->GetAcquisitionTxAntIdx(); + header.acquisition_sweep_idx = target_header->GetAcquisitionSweepIdx(); + header.acquisition_cf_idx = target_header->GetAcquisitionCfIdx(); + header.prf = target_header->GetPrf(); + header.umambiguous_speed = target_header->GetUmambiguousSpeed(); + header.acquisition_start = target_header->GetAcquisitionStart(); + for (const auto & target : targetlist_port_umrra1_v1_0_0->GetTargetList()) { + const auto range = target->GetRange(); + const auto elevation_angle = target->GetElevationAngle(); + const auto range_2d = range * std::cos(elevation_angle); + const auto azimuth_angle = target->GetAzimuthAngle(); + const auto snr = target->GetPower() - target->GetNoise(); + modifier.push_back( + {range_2d * std::cos(azimuth_angle), range_2d * std::sin(azimuth_angle), + range * std::sin(elevation_angle), target->GetSpeedRadial(), target->GetPower(), + target->GetRcs(), target->GetNoise(), snr, azimuth_angle, elevation_angle, range}); + } + + m_publishers[sensor_idx]->publish(msg); + m_publishers_port_target_header[sensor_idx]->publish(header); + } +} + +void SmartmicroRadarNode::targetlist_callback_umrra1_v2_0_0( + const std::uint32_t sensor_idx, + const std::shared_ptr< + com::master::umrra1_t166_b_automotive_v2_0_0::comtargetlist::ComTargetList> & + targetlist_port_umrra1_v2_0_0, + const com::types::ClientId client_id) +{ + std::cout << "Targetlist for umrra1_v2_0_0" << std::endl; + if (!check_signal) { + std::shared_ptr + port_header; + port_header = targetlist_port_umrra1_v2_0_0->GetPortHeader(); + std::shared_ptr + target_header; + target_header = targetlist_port_umrra1_v2_0_0->GetTargetListHeader(); + sensor_msgs::msg::PointCloud2 msg; + umrr_ros2_msgs::msg::PortTargetHeader header; + RadarCloudModifier modifier{msg, m_sensors[sensor_idx].frame_id}; + const auto [sec, nanosec] = + convert_timestamp(std::chrono::microseconds{port_header->GetTimestamp()}); + msg.header.stamp.sec = sec; + msg.header.stamp.nanosec = nanosec; + header.port_identifier = port_header->GetPortIdentifier(); + header.port_ver_major = port_header->GetPortVersionMajor(); + header.port_ver_minor = port_header->GetPortVersionMinor(); + header.port_size = port_header->GetPortSize(); + header.body_endianness = port_header->GetBodyEndianness(); + header.port_index = port_header->GetPortIndex(); + header.header_ver_major = port_header->GetHeaderVersionMajor(); + header.header_ver_minor = port_header->GetHeaderVersionMinor(); + + header.frame_id = m_sensors[sensor_idx].frame_id; + header.cycle_time = target_header->GetCycleTime(); + header.number_of_targets = target_header->GetNumberOfTargets(); + header.acquisition_tx_ant_idx = target_header->GetAcquisitionTxAntIdx(); + header.acquisition_sweep_idx = target_header->GetAcquisitionSweepIdx(); + header.acquisition_cf_idx = target_header->GetAcquisitionCfIdx(); + header.prf = target_header->GetPrf(); + header.umambiguous_speed = target_header->GetUmambiguousSpeed(); + header.acquisition_start = target_header->GetAcquisitionStart(); + for (const auto & target : targetlist_port_umrra1_v2_0_0->GetTargetList()) { + const auto range = target->GetRange(); + const auto elevation_angle = target->GetElevationAngle(); + const auto range_2d = range * std::cos(elevation_angle); + const auto azimuth_angle = target->GetAzimuthAngle(); + const auto snr = target->GetPower() - target->GetNoise(); + modifier.push_back( + {range_2d * std::cos(azimuth_angle), range_2d * std::sin(azimuth_angle), + range * std::sin(elevation_angle), target->GetSpeedRadial(), target->GetPower(), + target->GetRcs(), target->GetNoise(), snr, azimuth_angle, elevation_angle, range}); + } + + m_publishers[sensor_idx]->publish(msg); + m_publishers_port_target_header[sensor_idx]->publish(header); + } +} + +void SmartmicroRadarNode::CAN_objectlist_callback_umrra4_mse_v2_1_0( + const std::uint32_t sensor_idx, + const std::shared_ptr & + objectlist_can_umrra4_mse_v2_1_0, + const com::types::ClientId client_id) +{ + std::cout << "Objectlist for umrra4_mse_can_v2_1_0" << std::endl; + if (!check_signal) { + std::shared_ptr port_header; + port_header = objectlist_can_umrra4_mse_v2_1_0->GetPortHeader(); + std::shared_ptr + object_header; + object_header = objectlist_can_umrra4_mse_v2_1_0->GetComObjectBaseListHeader(); + sensor_msgs::msg::PointCloud2 msg; + umrr_ros2_msgs::msg::CanObjectHeader header; + ObjectPointCloudModifier modifier{msg, m_sensors[sensor_idx].frame_id}; + const auto [sec, nanosec] = + convert_timestamp(std::chrono::microseconds{port_header->GetTimestamp()}); + msg.header.stamp.sec = sec; + msg.header.stamp.nanosec = nanosec; + header.frame_id = m_sensors[sensor_idx].frame_id; + header.cycle_time = object_header->GetCycleDuration(); + header.cycle_count = object_header->GetCycleCount(); + header.number_of_objects = object_header->GetNoOfObjects(); + header.ego_speed = object_header->GetSpeed(); + header.ego_speed_quality = object_header->GetSpeedQuality(); + header.ego_yaw_rate = object_header->GetYawRate(); + header.ego_yaw_rate_quality = object_header->GetYawRateQuality(); + header.dyn_source = object_header->GetDynamicSource(); + for (const auto & object : objectlist_can_umrra4_mse_v2_1_0->GetObjectList()) { + const auto x_pos = object->GetXPoint1(); + const auto y_pos = object->GetYPoint1(); + const auto z_pos = object->GetZPoint1(); + const auto speed_abs = object->GetSpeedAbs(); + const auto heading = object->GetHeadingDeg(); + const auto length = object->GetObjectLen(); + const auto quality = object->GetQuality(); + const auto acceleration = object->GetAcceleration(); + const auto object_id = static_cast(object->GetObjectId()); + modifier.push_back( + {x_pos, y_pos, z_pos, speed_abs, heading, length, quality, acceleration, object_id}); + } + + m_publishers_obj[sensor_idx]->publish(msg); + m_publishers_can_obj_header[sensor_idx]->publish(header); + } +} + +void SmartmicroRadarNode::CAN_targetlist_callback_umrra4_mse_v2_1_0( + const std::uint32_t sensor_idx, + const std::shared_ptr & + targetlist_can_umrra4_mse_v2_1_0, + const com::types::ClientId client_id) +{ + std::cout << "Targetlist for umrra4_mse_can_v2_1_0" << std::endl; + if (!check_signal) { + std::shared_ptr port_header; + port_header = targetlist_can_umrra4_mse_v2_1_0->GetPortHeader(); + std::shared_ptr + target_header; + target_header = targetlist_can_umrra4_mse_v2_1_0->GetTargetListHeader(); + sensor_msgs::msg::PointCloud2 msg; + umrr_ros2_msgs::msg::CanTargetHeader header; + RadarCloudModifier modifier{msg, m_sensors[sensor_idx].frame_id}; + const auto [sec, nanosec] = + convert_timestamp(std::chrono::microseconds{port_header->GetTimestamp()}); + msg.header.stamp.sec = sec; + msg.header.stamp.nanosec = nanosec; + header.frame_id = m_sensors[sensor_idx].frame_id; + header.cycle_time = target_header->GetCycleDuration(); + header.number_of_targets = target_header->GetNumberOfTargets(); + header.acquisition_setup = target_header->GetAcquisitionSetup(); + header.cycle_count = target_header->GetCycleCount(); + header.time_stamp = target_header->GetTimeStamp(); + header.acq_ts_fraction = target_header->GetAcqTimeStampFraction(); + for (const auto & target : targetlist_can_umrra4_mse_v2_1_0->GetTargetList()) { + const auto range = target->GetRange(); + const auto elevation_angle = target->GetElevationAngle(); + const auto range_2d = range * std::cos(elevation_angle); + const auto azimuth_angle = target->GetAzimuthAngle(); + const auto snr = target->GetSignalLevel() - target->GetNoise(); + modifier.push_back( + {range_2d * std::cos(azimuth_angle), range_2d * std::sin(azimuth_angle), + range * std::sin(elevation_angle), target->GetSpeedRadial(), target->GetSignalLevel(), + target->GetRCS(), target->GetNoise(), snr, azimuth_angle, elevation_angle, range}); + } + + m_publishers[sensor_idx]->publish(msg); + m_publishers_can_target_header[sensor_idx]->publish(header); + } +} + +void SmartmicroRadarNode::CAN_objectlist_callback_umrra4_mse_v1_0_0( + const std::uint32_t sensor_idx, + const std::shared_ptr & + objectlist_can_umrra4_mse_v1_0_0, + const com::types::ClientId client_id) +{ + std::cout << "Objectlist for umrra4_mse_can_v1_0_0" << std::endl; + if (!check_signal) { + std::shared_ptr port_header; + port_header = objectlist_can_umrra4_mse_v1_0_0->GetPortHeader(); + std::shared_ptr + object_header; + object_header = objectlist_can_umrra4_mse_v1_0_0->GetComObjectBaseListHeader(); + sensor_msgs::msg::PointCloud2 msg; + umrr_ros2_msgs::msg::CanObjectHeader header; + ObjectPointCloudModifier modifier{msg, m_sensors[sensor_idx].frame_id}; + const auto [sec, nanosec] = + convert_timestamp(std::chrono::microseconds{port_header->GetTimestamp()}); + msg.header.stamp.sec = sec; + msg.header.stamp.nanosec = nanosec; + header.frame_id = m_sensors[sensor_idx].frame_id; + header.cycle_time = object_header->GetCycleDuration(); + header.cycle_count = object_header->GetCycleCount(); + header.number_of_objects = object_header->GetNoOfObjects(); + header.ego_speed = object_header->GetSpeed(); + header.ego_speed_quality = object_header->GetSpeedQuality(); + header.ego_yaw_rate = object_header->GetYawRate(); + header.ego_yaw_rate_quality = object_header->GetYawRateQuality(); + header.dyn_source = object_header->GetDynamicSource(); + for (const auto & object : objectlist_can_umrra4_mse_v1_0_0->GetObjectList()) { + const auto x_pos = object->GetXPoint1(); + const auto y_pos = object->GetYPoint1(); + const auto z_pos = object->GetZPoint1(); + const auto speed_abs = object->GetSpeedAbs(); + const auto heading = object->GetHeadingDeg(); + const auto length = object->GetObjectLen(); + const auto quality = object->GetQuality(); + const auto acceleration = object->GetAcceleration(); + const auto object_id = static_cast(object->GetObjectId()); + modifier.push_back( + {x_pos, y_pos, z_pos, speed_abs, heading, length, quality, acceleration, object_id}); + } + + m_publishers_obj[sensor_idx]->publish(msg); + m_publishers_can_obj_header[sensor_idx]->publish(header); + } +} + +void SmartmicroRadarNode::CAN_targetlist_callback_umrra4_mse_v1_0_0( + const std::uint32_t sensor_idx, + const std::shared_ptr & + targetlist_can_umrra4_mse_v1_0_0, + const com::types::ClientId client_id) +{ + std::cout << "Targetlist for umrra4_mse_can_v1_0_0" << std::endl; + if (!check_signal) { + std::shared_ptr port_header; + port_header = targetlist_can_umrra4_mse_v1_0_0->GetPortHeader(); + std::shared_ptr + target_header; + target_header = targetlist_can_umrra4_mse_v1_0_0->GetTargetListHeader(); + sensor_msgs::msg::PointCloud2 msg; + umrr_ros2_msgs::msg::CanTargetHeader header; + RadarCloudModifier modifier{msg, m_sensors[sensor_idx].frame_id}; + const auto [sec, nanosec] = + convert_timestamp(std::chrono::microseconds{port_header->GetTimestamp()}); + msg.header.stamp.sec = sec; + msg.header.stamp.nanosec = nanosec; + header.frame_id = m_sensors[sensor_idx].frame_id; + header.cycle_time = target_header->GetCycleDuration(); + header.number_of_targets = target_header->GetNumberOfTargets(); + header.acquisition_setup = target_header->GetAcquisitionSetup(); + header.cycle_count = target_header->GetCycleCount(); + header.time_stamp = target_header->GetTimeStamp(); + header.acq_ts_fraction = target_header->GetAcqTimeStampFraction(); + for (const auto & target : targetlist_can_umrra4_mse_v1_0_0->GetTargetList()) { + const auto range = target->GetRange(); + const auto elevation_angle = target->GetElevationAngle(); + const auto range_2d = range * std::cos(elevation_angle); + const auto azimuth_angle = target->GetAzimuthAngle(); + const auto snr = target->GetSignalLevel() - target->GetNoise(); + modifier.push_back( + {range_2d * std::cos(azimuth_angle), range_2d * std::sin(azimuth_angle), + range * std::sin(elevation_angle), target->GetSpeedRadial(), target->GetSignalLevel(), + target->GetRCS(), target->GetNoise(), snr, azimuth_angle, elevation_angle, range}); + } + + m_publishers[sensor_idx]->publish(msg); + m_publishers_can_target_header[sensor_idx]->publish(header); + } +} + +void SmartmicroRadarNode::CAN_objectlist_callback_umrr9f_mse_v1_0_0( + const std::uint32_t sensor_idx, + const std::shared_ptr & + objectlist_can_umrr9f_mse_v1_0_0, + const com::types::ClientId client_id) +{ + std::cout << "Objectlist for umrr9f_mse_can_v1_0_0" << std::endl; + if (!check_signal) { + std::shared_ptr port_header; + port_header = objectlist_can_umrr9f_mse_v1_0_0->GetPortHeader(); + std::shared_ptr + object_header; + object_header = objectlist_can_umrr9f_mse_v1_0_0->GetComObjectBaseListHeader(); + sensor_msgs::msg::PointCloud2 msg; + umrr_ros2_msgs::msg::CanObjectHeader header; + ObjectPointCloudModifier modifier{msg, m_sensors[sensor_idx].frame_id}; + const auto [sec, nanosec] = + convert_timestamp(std::chrono::microseconds{port_header->GetTimestamp()}); + msg.header.stamp.sec = sec; + msg.header.stamp.nanosec = nanosec; + header.frame_id = m_sensors[sensor_idx].frame_id; + header.cycle_time = object_header->GetCycleDuration(); + header.cycle_count = object_header->GetCycleCount(); + header.number_of_objects = object_header->GetNoOfObjects(); + header.ego_speed = object_header->GetSpeed(); + header.ego_speed_quality = object_header->GetSpeedQuality(); + header.ego_yaw_rate = object_header->GetYawRate(); + header.ego_yaw_rate_quality = object_header->GetYawRateQuality(); + header.dyn_source = object_header->GetDynamicSource(); + for (const auto & object : objectlist_can_umrr9f_mse_v1_0_0->GetObjectList()) { + const auto x_pos = object->GetXPoint1(); + const auto y_pos = object->GetYPoint1(); + const auto z_pos = object->GetZPoint1(); + const auto speed_abs = object->GetSpeedAbs(); + const auto heading = object->GetHeadingDeg(); + const auto length = object->GetObjectLen(); + const auto quality = object->GetQuality(); + const auto acceleration = object->GetAcceleration(); + const auto object_id = static_cast(object->GetObjectId()); + modifier.push_back( + {x_pos, y_pos, z_pos, speed_abs, heading, length, quality, acceleration, object_id}); + } + + m_publishers_obj[sensor_idx]->publish(msg); + m_publishers_can_obj_header[sensor_idx]->publish(header); + } +} + +void SmartmicroRadarNode::CAN_targetlist_callback_umrr9f_mse_v1_0_0( + const std::uint32_t sensor_idx, + const std::shared_ptr & + targetlist_can_umrr9f_mse_v1_0_0, + const com::types::ClientId client_id) +{ + std::cout << "Targetlist for umrr9f_mse_can_v1_0_0" << std::endl; + if (!check_signal) { + std::shared_ptr port_header; + port_header = targetlist_can_umrr9f_mse_v1_0_0->GetPortHeader(); + std::shared_ptr + target_header; + target_header = targetlist_can_umrr9f_mse_v1_0_0->GetTargetListHeader(); + sensor_msgs::msg::PointCloud2 msg; + umrr_ros2_msgs::msg::CanTargetHeader header; + RadarCloudModifier modifier{msg, m_sensors[sensor_idx].frame_id}; + const auto [sec, nanosec] = + convert_timestamp(std::chrono::microseconds{port_header->GetTimestamp()}); + msg.header.stamp.sec = sec; + msg.header.stamp.nanosec = nanosec; + header.frame_id = m_sensors[sensor_idx].frame_id; + header.cycle_time = target_header->GetCycleDuration(); + header.number_of_targets = target_header->GetNumberOfTargets(); + header.acquisition_setup = target_header->GetAcquisitionSetup(); + header.cycle_count = target_header->GetCycleCount(); + header.time_stamp = target_header->GetTimeStamp(); + header.acq_ts_fraction = target_header->GetAcqTimeStampFraction(); + for (const auto & target : targetlist_can_umrr9f_mse_v1_0_0->GetTargetList()) { + const auto range = target->GetRange(); + const auto elevation_angle = target->GetElevationAngle(); + const auto range_2d = range * std::cos(elevation_angle); + const auto azimuth_angle = target->GetAzimuthAngle(); + const auto snr = target->GetSignalLevel() - target->GetNoise(); + modifier.push_back( + {range_2d * std::cos(azimuth_angle), range_2d * std::sin(azimuth_angle), + range * std::sin(elevation_angle), target->GetSpeedRadial(), target->GetSignalLevel(), + target->GetRCS(), target->GetNoise(), snr, azimuth_angle, elevation_angle, range}); + } + + m_publishers[sensor_idx]->publish(msg); + m_publishers_can_target_header[sensor_idx]->publish(header); + } +} + +void SmartmicroRadarNode::CAN_objectlist_callback_umrr9f_mse_v1_1_0( + const std::uint32_t sensor_idx, + const std::shared_ptr & + objectlist_can_umrr9f_mse_v1_1_0, + const com::types::ClientId client_id) +{ + std::cout << "Objectlist for umrr9f_mse_can_v1_1_0" << std::endl; + if (!check_signal) { + std::shared_ptr port_header; + port_header = objectlist_can_umrr9f_mse_v1_1_0->GetPortHeader(); + std::shared_ptr + object_header; + object_header = objectlist_can_umrr9f_mse_v1_1_0->GetComObjectBaseListHeader(); + sensor_msgs::msg::PointCloud2 msg; + umrr_ros2_msgs::msg::CanObjectHeader header; + ObjectPointCloudModifier modifier{msg, m_sensors[sensor_idx].frame_id}; + const auto [sec, nanosec] = + convert_timestamp(std::chrono::microseconds{port_header->GetTimestamp()}); + msg.header.stamp.sec = sec; + msg.header.stamp.nanosec = nanosec; + header.frame_id = m_sensors[sensor_idx].frame_id; + header.cycle_time = object_header->GetCycleDuration(); + header.cycle_count = object_header->GetCycleCount(); + header.number_of_objects = object_header->GetNoOfObjects(); + header.ego_speed = object_header->GetSpeed(); + header.ego_speed_quality = object_header->GetSpeedQuality(); + header.ego_yaw_rate = object_header->GetYawRate(); + header.ego_yaw_rate_quality = object_header->GetYawRateQuality(); + header.dyn_source = object_header->GetDynamicSource(); + for (const auto & object : objectlist_can_umrr9f_mse_v1_1_0->GetObjectList()) { + const auto x_pos = object->GetXPoint1(); + const auto y_pos = object->GetYPoint1(); + const auto z_pos = object->GetZPoint1(); + const auto speed_abs = object->GetSpeedAbs(); + const auto heading = object->GetHeadingDeg(); + const auto length = object->GetObjectLen(); + const auto quality = object->GetQuality(); + const auto acceleration = object->GetAcceleration(); + const auto object_id = static_cast(object->GetObjectId()); + modifier.push_back( + {x_pos, y_pos, z_pos, speed_abs, heading, length, quality, acceleration, object_id}); + } + + m_publishers_obj[sensor_idx]->publish(msg); + m_publishers_can_obj_header[sensor_idx]->publish(header); + } +} + +void SmartmicroRadarNode::CAN_targetlist_callback_umrr9f_mse_v1_1_0( + const std::uint32_t sensor_idx, + const std::shared_ptr & + targetlist_can_umrr9f_mse_v1_1_0, + const com::types::ClientId client_id) +{ + std::cout << "Targetlist for umrr9f_mse_can_v1_1_0" << std::endl; + if (!check_signal) { + std::shared_ptr port_header; + port_header = targetlist_can_umrr9f_mse_v1_1_0->GetPortHeader(); + std::shared_ptr + target_header; + target_header = targetlist_can_umrr9f_mse_v1_1_0->GetTargetListHeader(); + sensor_msgs::msg::PointCloud2 msg; + umrr_ros2_msgs::msg::CanTargetHeader header; + RadarCloudModifier modifier{msg, m_sensors[sensor_idx].frame_id}; + const auto [sec, nanosec] = + convert_timestamp(std::chrono::microseconds{port_header->GetTimestamp()}); + msg.header.stamp.sec = sec; + msg.header.stamp.nanosec = nanosec; + header.frame_id = m_sensors[sensor_idx].frame_id; + header.cycle_time = target_header->GetCycleDuration(); + header.number_of_targets = target_header->GetNumberOfTargets(); + header.acquisition_setup = target_header->GetAcquisitionSetup(); + header.cycle_count = target_header->GetCycleCount(); + header.time_stamp = target_header->GetTimeStamp(); + header.acq_ts_fraction = target_header->GetAcqTimeStampFraction(); + for (const auto & target : targetlist_can_umrr9f_mse_v1_1_0->GetTargetList()) { + const auto range = target->GetRange(); + const auto elevation_angle = target->GetElevationAngle(); + const auto range_2d = range * std::cos(elevation_angle); + const auto azimuth_angle = target->GetAzimuthAngle(); + const auto snr = target->GetSignalLevel() - target->GetNoise(); + modifier.push_back( + {range_2d * std::cos(azimuth_angle), range_2d * std::sin(azimuth_angle), + range * std::sin(elevation_angle), target->GetSpeedRadial(), target->GetSignalLevel(), + target->GetRCS(), target->GetNoise(), snr, azimuth_angle, elevation_angle, range}); + } + + m_publishers[sensor_idx]->publish(msg); + m_publishers_can_target_header[sensor_idx]->publish(header); + } +} + +void SmartmicroRadarNode::CAN_objectlist_callback_umrr9f_mse_v1_3_0( + const std::uint32_t sensor_idx, + const std::shared_ptr & + objectlist_can_umrr9f_mse_v1_3_0, + const com::types::ClientId client_id) +{ + std::cout << "Objectlist for umrr9f_mse_can_v1_3_0" << std::endl; + if (!check_signal) { + std::shared_ptr port_header; + port_header = objectlist_can_umrr9f_mse_v1_3_0->GetPortHeader(); + std::shared_ptr + object_header; + object_header = objectlist_can_umrr9f_mse_v1_3_0->GetComObjectBaseListHeader(); + sensor_msgs::msg::PointCloud2 msg; + umrr_ros2_msgs::msg::CanObjectHeader header; + ObjectPointCloudModifier modifier{msg, m_sensors[sensor_idx].frame_id}; + const auto [sec, nanosec] = + convert_timestamp(std::chrono::microseconds{port_header->GetTimestamp()}); + msg.header.stamp.sec = sec; + msg.header.stamp.nanosec = nanosec; + header.frame_id = m_sensors[sensor_idx].frame_id; + header.cycle_time = object_header->GetCycleDuration(); + header.cycle_count = object_header->GetCycleCount(); + header.number_of_objects = object_header->GetNoOfObjects(); + header.ego_speed = object_header->GetSpeed(); + header.ego_speed_quality = object_header->GetSpeedQuality(); + header.ego_yaw_rate = object_header->GetYawRate(); + header.ego_yaw_rate_quality = object_header->GetYawRateQuality(); + header.dyn_source = object_header->GetDynamicSource(); + for (const auto & object : objectlist_can_umrr9f_mse_v1_3_0->GetObjectList()) { + const auto x_pos = object->GetXPoint1(); + const auto y_pos = object->GetYPoint1(); + const auto z_pos = object->GetZPoint1(); + const auto speed_abs = object->GetSpeedAbs(); + const auto heading = object->GetHeadingDeg(); + const auto length = object->GetObjectLen(); + const auto quality = object->GetQuality(); + const auto acceleration = object->GetAcceleration(); + const auto object_id = static_cast(object->GetObjectId()); + modifier.push_back( + {x_pos, y_pos, z_pos, speed_abs, heading, length, quality, acceleration, object_id}); + } + + m_publishers_obj[sensor_idx]->publish(msg); + m_publishers_can_obj_header[sensor_idx]->publish(header); + } +} + +void SmartmicroRadarNode::CAN_targetlist_callback_umrr9f_mse_v1_3_0( + const std::uint32_t sensor_idx, + const std::shared_ptr & + targetlist_can_umrr9f_mse_v1_3_0, + const com::types::ClientId client_id) +{ + std::cout << "Targetlist for umrr9f_mse_can_v1_3_0" << std::endl; + if (!check_signal) { + std::shared_ptr port_header; + port_header = targetlist_can_umrr9f_mse_v1_3_0->GetPortHeader(); + std::shared_ptr + target_header; + target_header = targetlist_can_umrr9f_mse_v1_3_0->GetTargetListHeader(); + sensor_msgs::msg::PointCloud2 msg; + umrr_ros2_msgs::msg::CanTargetHeader header; + RadarCloudModifier modifier{msg, m_sensors[sensor_idx].frame_id}; + const auto [sec, nanosec] = + convert_timestamp(std::chrono::microseconds{port_header->GetTimestamp()}); + msg.header.stamp.sec = sec; + msg.header.stamp.nanosec = nanosec; + header.frame_id = m_sensors[sensor_idx].frame_id; + header.cycle_time = target_header->GetCycleDuration(); + header.number_of_targets = target_header->GetNumberOfTargets(); + header.acquisition_setup = target_header->GetAcquisitionSetup(); + header.cycle_count = target_header->GetCycleCount(); + header.time_stamp = target_header->GetTimeStamp(); + header.acq_ts_fraction = target_header->GetAcqTimeStampFraction(); + for (const auto & target : targetlist_can_umrr9f_mse_v1_3_0->GetTargetList()) { + const auto range = target->GetRange(); + const auto elevation_angle = target->GetElevationAngle(); + const auto range_2d = range * std::cos(elevation_angle); + const auto azimuth_angle = target->GetAzimuthAngle(); + const auto snr = target->GetSignalLevel() - target->GetNoise(); + modifier.push_back( + {range_2d * std::cos(azimuth_angle), range_2d * std::sin(azimuth_angle), + range * std::sin(elevation_angle), target->GetSpeedRadial(), target->GetSignalLevel(), + target->GetRCS(), target->GetNoise(), snr, azimuth_angle, elevation_angle, range}); + } + + m_publishers[sensor_idx]->publish(msg); + m_publishers_can_target_header[sensor_idx]->publish(header); + } +} + +void SmartmicroRadarNode::CAN_targetlist_callback_umrr96( + const std::uint32_t sensor_idx, + const std::shared_ptr< + com::master::umrr96_t153_automotive_v1_2_2::comtargetbaselist::ComTargetBaseList> & + targetlist_can_umrr96, + const com::types::ClientId client_id) +{ + std::cout << "CAN Targetlist for umrr96_v1_2_2" << std::endl; + if (!check_signal) { + std::shared_ptr + port_header; + port_header = targetlist_can_umrr96->GetPortHeader(); + std::shared_ptr + target_header; + target_header = targetlist_can_umrr96->GetTargetListHeader(); + sensor_msgs::msg::PointCloud2 msg; + umrr_ros2_msgs::msg::CanTargetHeader header; + RadarCloudModifier modifier{msg, m_sensors[sensor_idx].frame_id}; + const auto [sec, nanosec] = + convert_timestamp(std::chrono::microseconds{port_header->GetTimestamp()}); + msg.header.stamp.sec = sec; + msg.header.stamp.nanosec = nanosec; + header.frame_id = m_sensors[sensor_idx].frame_id; + header.acq_ts_fraction = target_header->GetAcqTimeStampFraction(); + header.time_stamp = target_header->GetTimeStamp(); + header.cycle_time = target_header->GetCycleDuration(); + header.number_of_targets = target_header->GetNumberOfTargets(); + header.acquisition_setup = target_header->GetAcquisitionSetup(); + header.cycle_count = target_header->GetCycleCount(); + for (const auto & target : targetlist_can_umrr96->GetTargetList()) { + const auto range = target->GetRange(); + const auto elevation_angle = target->GetElevationAngle(); + const auto range_2d = range * std::cos(elevation_angle); + const auto azimuth_angle = target->GetAzimuthAngle(); + const auto snr = target->GetSignalLevel() - target->GetNoise(); + modifier.push_back( + {range_2d * std::cos(azimuth_angle), range_2d * std::sin(azimuth_angle), + range * std::sin(elevation_angle), target->GetSpeedRadial(), target->GetSignalLevel(), + target->GetRCS(), target->GetNoise(), snr, azimuth_angle, elevation_angle, range}); + } + + m_publishers[sensor_idx]->publish(msg); + m_publishers_can_target_header[sensor_idx]->publish(header); + } +} + +void SmartmicroRadarNode::CAN_targetlist_callback_umrr11( + const std::uint32_t sensor_idx, + const std::shared_ptr< + com::master::umrr11_t132_automotive_v1_1_2::comtargetbaselist::ComTargetBaseList> & + targetlist_can_umrr11, + const com::types::ClientId client_id) +{ + std::cout << "CAN Targetlist for umrr11_v1_1_2" << std::endl; + if (!check_signal) { + std::shared_ptr + port_header; + port_header = targetlist_can_umrr11->GetPortHeader(); + std::shared_ptr + target_header; + target_header = targetlist_can_umrr11->GetTargetListHeader(); + sensor_msgs::msg::PointCloud2 msg; + umrr_ros2_msgs::msg::CanTargetHeader header; + RadarCloudModifier modifier{msg, m_sensors[sensor_idx].frame_id}; + const auto [sec, nanosec] = + convert_timestamp(std::chrono::microseconds{port_header->GetTimestamp()}); + msg.header.stamp.sec = sec; + msg.header.stamp.nanosec = nanosec; + header.frame_id = m_sensors[sensor_idx].frame_id; + header.acq_ts_fraction = target_header->GetAcqTimeStampFraction(); + header.time_stamp = target_header->GetTimeStamp(); + header.cycle_time = target_header->GetCycleDuration(); + header.number_of_targets = target_header->GetNumberOfTargets(); + header.acquisition_setup = target_header->GetAcquisitionSetup(); + header.cycle_count = target_header->GetCycleCount(); + for (const auto & target : targetlist_can_umrr11->GetTargetList()) { + const auto range = target->GetRange(); + const auto elevation_angle = target->GetElevationAngle(); + const auto range_2d = range * std::cos(elevation_angle); + const auto azimuth_angle = target->GetAzimuthAngle(); + const auto snr = target->GetSignalLevel() - target->GetNoise(); + modifier.push_back( + {range_2d * std::cos(azimuth_angle), range_2d * std::sin(azimuth_angle), + range * std::sin(elevation_angle), target->GetSpeedRadial(), target->GetSignalLevel(), + target->GetRCS(), target->GetNoise(), snr, azimuth_angle, elevation_angle, range}); + } + + m_publishers[sensor_idx]->publish(msg); + m_publishers_can_target_header[sensor_idx]->publish(header); + } +} + +void SmartmicroRadarNode::CAN_targetlist_callback_umrr9d_v1_0_3( + const std::uint32_t sensor_idx, + const std::shared_ptr< + com::master::umrr9d_t152_automotive_v1_0_3::comtargetbaselist::ComTargetBaseList> & + targetlist_can_umrr9d_v1_0_3, + const com::types::ClientId client_id) +{ + std::cout << "Targetlist for umrr9d_v1_0_3" << std::endl; + if (!check_signal) { + std::shared_ptr + port_header; + port_header = targetlist_can_umrr9d_v1_0_3->GetPortHeader(); + std::shared_ptr + target_header; + target_header = targetlist_can_umrr9d_v1_0_3->GetTargetListHeader(); + sensor_msgs::msg::PointCloud2 msg; + umrr_ros2_msgs::msg::CanTargetHeader header; + RadarCloudModifier modifier{msg, m_sensors[sensor_idx].frame_id}; + const auto [sec, nanosec] = + convert_timestamp(std::chrono::microseconds{port_header->GetTimestamp()}); + msg.header.stamp.sec = sec; + msg.header.stamp.nanosec = nanosec; + header.frame_id = m_sensors[sensor_idx].frame_id; + header.acq_ts_fraction = target_header->GetAcqTimeStampFraction(); + header.time_stamp = target_header->GetTimeStamp(); + header.cycle_time = target_header->GetCycleDuration(); + header.number_of_targets = target_header->GetNumberOfTargets(); + header.acquisition_setup = target_header->GetAcquisitionSetup(); + header.cycle_count = target_header->GetCycleCount(); + for (const auto & target : targetlist_can_umrr9d_v1_0_3->GetTargetList()) { + const auto range = target->GetRange(); + const auto elevation_angle = target->GetElevationAngle(); + const auto range_2d = range * std::cos(elevation_angle); + const auto azimuth_angle = target->GetAzimuthAngle(); + const auto snr = target->GetSignalLevel() - target->GetNoise(); + modifier.push_back( + {range_2d * std::cos(azimuth_angle), range_2d * std::sin(azimuth_angle), + range * std::sin(elevation_angle), target->GetSpeedRadial(), target->GetSignalLevel(), + target->GetRCS(), target->GetNoise(), snr, azimuth_angle, elevation_angle, range}); + } + + m_publishers[sensor_idx]->publish(msg); + m_publishers_can_target_header[sensor_idx]->publish(header); + } +} + +void SmartmicroRadarNode::CAN_targetlist_callback_umrr9d_v1_2_2( + const std::uint32_t sensor_idx, + const std::shared_ptr< + com::master::umrr9d_t152_automotive_v1_2_2::comtargetbaselist::ComTargetBaseList> & + targetlist_can_umrr9d_v1_2_2, + const com::types::ClientId client_id) +{ + std::cout << "Targetlist for umrr9d_v1_2_2" << std::endl; + if (!check_signal) { + std::shared_ptr + port_header; + port_header = targetlist_can_umrr9d_v1_2_2->GetPortHeader(); + std::shared_ptr + target_header; + target_header = targetlist_can_umrr9d_v1_2_2->GetTargetListHeader(); + sensor_msgs::msg::PointCloud2 msg; + umrr_ros2_msgs::msg::CanTargetHeader header; + RadarCloudModifier modifier{msg, m_sensors[sensor_idx].frame_id}; + const auto [sec, nanosec] = + convert_timestamp(std::chrono::microseconds{port_header->GetTimestamp()}); + msg.header.stamp.sec = sec; + msg.header.stamp.nanosec = nanosec; + header.frame_id = m_sensors[sensor_idx].frame_id; + header.acq_ts_fraction = target_header->GetAcqTimeStampFraction(); + header.time_stamp = target_header->GetTimeStamp(); + header.cycle_time = target_header->GetCycleDuration(); + header.number_of_targets = target_header->GetNumberOfTargets(); + header.acquisition_setup = target_header->GetAcquisitionSetup(); + header.cycle_count = target_header->GetCycleCount(); + for (const auto & target : targetlist_can_umrr9d_v1_2_2->GetTargetList()) { + const auto range = target->GetRange(); + const auto elevation_angle = target->GetElevationAngle(); + const auto range_2d = range * std::cos(elevation_angle); + const auto azimuth_angle = target->GetAzimuthAngle(); + const auto snr = target->GetSignalLevel() - target->GetNoise(); + modifier.push_back( + {range_2d * std::cos(azimuth_angle), range_2d * std::sin(azimuth_angle), + range * std::sin(elevation_angle), target->GetSpeedRadial(), target->GetSignalLevel(), + target->GetRCS(), target->GetNoise(), snr, azimuth_angle, elevation_angle, range}); + } + + m_publishers[sensor_idx]->publish(msg); + m_publishers_can_target_header[sensor_idx]->publish(header); + } +} + +void SmartmicroRadarNode::CAN_targetlist_callback_umrr9d_v1_4_1( + const std::uint32_t sensor_idx, + const std::shared_ptr< + com::master::umrr9d_t152_automotive_v1_4_1::comtargetbaselist::ComTargetBaseList> & + targetlist_can_umrr9d_v1_4_1, + const com::types::ClientId client_id) +{ + std::cout << "Targetlist for umrr9d_v1_4_1" << std::endl; + if (!check_signal) { + std::shared_ptr + port_header; + port_header = targetlist_can_umrr9d_v1_4_1->GetPortHeader(); + std::shared_ptr + target_header; + target_header = targetlist_can_umrr9d_v1_4_1->GetTargetListHeader(); + sensor_msgs::msg::PointCloud2 msg; + umrr_ros2_msgs::msg::CanTargetHeader header; + RadarCloudModifier modifier{msg, m_sensors[sensor_idx].frame_id}; + const auto [sec, nanosec] = + convert_timestamp(std::chrono::microseconds{port_header->GetTimestamp()}); + msg.header.stamp.sec = sec; + msg.header.stamp.nanosec = nanosec; + header.frame_id = m_sensors[sensor_idx].frame_id; + header.acq_ts_fraction = target_header->GetAcqTimeStampFraction(); + header.time_stamp = target_header->GetTimeStamp(); + header.cycle_time = target_header->GetCycleDuration(); + header.number_of_targets = target_header->GetNumberOfTargets(); + header.acquisition_setup = target_header->GetAcquisitionSetup(); + header.cycle_count = target_header->GetCycleCount(); + for (const auto & target : targetlist_can_umrr9d_v1_4_1->GetTargetList()) { + const auto range = target->GetRange(); + const auto elevation_angle = target->GetElevationAngle(); + const auto range_2d = range * std::cos(elevation_angle); + const auto azimuth_angle = target->GetAzimuthAngle(); + const auto snr = target->GetSignalLevel() - target->GetNoise(); + modifier.push_back( + {range_2d * std::cos(azimuth_angle), range_2d * std::sin(azimuth_angle), + range * std::sin(elevation_angle), target->GetSpeedRadial(), target->GetSignalLevel(), + target->GetRCS(), target->GetNoise(), snr, azimuth_angle, elevation_angle, range}); + } + + m_publishers[sensor_idx]->publish(msg); + m_publishers_can_target_header[sensor_idx]->publish(header); + } +} + +void SmartmicroRadarNode::CAN_targetlist_callback_umrr9d_v1_5_0( + const std::uint32_t sensor_idx, + const std::shared_ptr< + com::master::umrr9d_t152_automotive_v1_5_0::comtargetbaselist::ComTargetBaseList> & + targetlist_can_umrr9d_v1_5_0, + const com::types::ClientId client_id) +{ + std::cout << "Targetlist for umrr9d_v1_5_0" << std::endl; + if (!check_signal) { + std::shared_ptr + port_header; + port_header = targetlist_can_umrr9d_v1_5_0->GetPortHeader(); + std::shared_ptr + target_header; + target_header = targetlist_can_umrr9d_v1_5_0->GetTargetListHeader(); + sensor_msgs::msg::PointCloud2 msg; + umrr_ros2_msgs::msg::CanTargetHeader header; + RadarCloudModifier modifier{msg, m_sensors[sensor_idx].frame_id}; + const auto [sec, nanosec] = + convert_timestamp(std::chrono::microseconds{port_header->GetTimestamp()}); + msg.header.stamp.sec = sec; + msg.header.stamp.nanosec = nanosec; + header.frame_id = m_sensors[sensor_idx].frame_id; + header.acq_ts_fraction = target_header->GetAcqTimeStampFraction(); + header.time_stamp = target_header->GetTimeStamp(); + header.cycle_time = target_header->GetCycleDuration(); + header.number_of_targets = target_header->GetNumberOfTargets(); + header.acquisition_setup = target_header->GetAcquisitionSetup(); + header.cycle_count = target_header->GetCycleCount(); + for (const auto & target : targetlist_can_umrr9d_v1_5_0->GetTargetList()) { + const auto range = target->GetRange(); + const auto elevation_angle = target->GetElevationAngle(); + const auto range_2d = range * std::cos(elevation_angle); + const auto azimuth_angle = target->GetAzimuthAngle(); + const auto snr = target->GetSignalLevel() - target->GetNoise(); + modifier.push_back( + {range_2d * std::cos(azimuth_angle), range_2d * std::sin(azimuth_angle), + range * std::sin(elevation_angle), target->GetSpeedRadial(), target->GetSignalLevel(), + target->GetRCS(), target->GetNoise(), snr, azimuth_angle, elevation_angle, range}); + } + + m_publishers[sensor_idx]->publish(msg); + m_publishers_can_target_header[sensor_idx]->publish(header); + } +} + +void SmartmicroRadarNode::CAN_targetlist_callback_umrr9f_v2_1_1( + const std::uint32_t sensor_idx, + const std::shared_ptr< + com::master::umrr9f_t169_automotive_v2_1_1::comtargetbaselist::ComTargetBaseList> & + targetlist_can_umrr9f_v2_1_1, + const com::types::ClientId client_id) +{ + std::cout << "Targetlist for umrr9f_v2_1_1" << std::endl; + if (!check_signal) { + std::shared_ptr + port_header; + port_header = targetlist_can_umrr9f_v2_1_1->GetPortHeader(); + std::shared_ptr + target_header; + target_header = targetlist_can_umrr9f_v2_1_1->GetTargetListHeader(); + sensor_msgs::msg::PointCloud2 msg; + umrr_ros2_msgs::msg::CanTargetHeader header; + RadarCloudModifier modifier{msg, m_sensors[sensor_idx].frame_id}; + const auto [sec, nanosec] = + convert_timestamp(std::chrono::microseconds{port_header->GetTimestamp()}); + msg.header.stamp.sec = sec; + msg.header.stamp.nanosec = nanosec; + header.frame_id = m_sensors[sensor_idx].frame_id; + header.acq_ts_fraction = target_header->GetAcqTimeStampFraction(); + header.time_stamp = target_header->GetTimeStamp(); + header.cycle_time = target_header->GetCycleDuration(); + header.number_of_targets = target_header->GetNumberOfTargets(); + header.acquisition_setup = target_header->GetAcquisitionSetup(); + header.cycle_count = target_header->GetCycleCount(); + for (const auto & target : targetlist_can_umrr9f_v2_1_1->GetTargetList()) { + const auto range = target->GetRange(); + const auto elevation_angle = target->GetElevationAngle(); + const auto range_2d = range * std::cos(elevation_angle); + const auto azimuth_angle = target->GetAzimuthAngle(); + const auto snr = target->GetSignalLevel() - target->GetNoise(); + modifier.push_back( + {range_2d * std::cos(azimuth_angle), range_2d * std::sin(azimuth_angle), + range * std::sin(elevation_angle), target->GetSpeedRadial(), target->GetSignalLevel(), + target->GetRCS(), target->GetNoise(), snr, azimuth_angle, elevation_angle, range}); + } + + m_publishers[sensor_idx]->publish(msg); + m_publishers_can_target_header[sensor_idx]->publish(header); + } +} + +void SmartmicroRadarNode::CAN_targetlist_callback_umrr9f_v2_2_1( + const std::uint32_t sensor_idx, + const std::shared_ptr< + com::master::umrr9f_t169_automotive_v2_2_1::comtargetbaselist::ComTargetBaseList> & + targetlist_can_umrr9f_v2_2_1, + const com::types::ClientId client_id) +{ + std::cout << "Targetlist for umrr9f_v2_2_1" << std::endl; + if (!check_signal) { + std::shared_ptr + port_header; + port_header = targetlist_can_umrr9f_v2_2_1->GetPortHeader(); + std::shared_ptr + target_header; + target_header = targetlist_can_umrr9f_v2_2_1->GetTargetListHeader(); + sensor_msgs::msg::PointCloud2 msg; + umrr_ros2_msgs::msg::CanTargetHeader header; + RadarCloudModifier modifier{msg, m_sensors[sensor_idx].frame_id}; + const auto [sec, nanosec] = + convert_timestamp(std::chrono::microseconds{port_header->GetTimestamp()}); + msg.header.stamp.sec = sec; + msg.header.stamp.nanosec = nanosec; + header.frame_id = m_sensors[sensor_idx].frame_id; + header.acq_ts_fraction = target_header->GetAcqTimeStampFraction(); + header.time_stamp = target_header->GetTimeStamp(); + header.cycle_time = target_header->GetCycleDuration(); + header.number_of_targets = target_header->GetNumberOfTargets(); + header.acquisition_setup = target_header->GetAcquisitionSetup(); + header.cycle_count = target_header->GetCycleCount(); + for (const auto & target : targetlist_can_umrr9f_v2_2_1->GetTargetList()) { + const auto range = target->GetRange(); + const auto elevation_angle = target->GetElevationAngle(); + const auto range_2d = range * std::cos(elevation_angle); + const auto azimuth_angle = target->GetAzimuthAngle(); + const auto snr = target->GetSignalLevel() - target->GetNoise(); + modifier.push_back( + {range_2d * std::cos(azimuth_angle), range_2d * std::sin(azimuth_angle), + range * std::sin(elevation_angle), target->GetSpeedRadial(), target->GetSignalLevel(), + target->GetRCS(), target->GetNoise(), snr, azimuth_angle, elevation_angle, range}); + } + + m_publishers[sensor_idx]->publish(msg); + m_publishers_can_target_header[sensor_idx]->publish(header); + } +} + +void SmartmicroRadarNode::CAN_targetlist_callback_umrr9f_v2_4_1( + const std::uint32_t sensor_idx, + const std::shared_ptr< + com::master::umrr9f_t169_automotive_v2_4_1::comtargetbaselist::ComTargetBaseList> & + targetlist_can_umrr9f_v2_4_1, + const com::types::ClientId client_id) +{ + std::cout << "Targetlist for umrr9f_v2_4_1" << std::endl; + if (!check_signal) { + std::shared_ptr + port_header; + port_header = targetlist_can_umrr9f_v2_4_1->GetPortHeader(); + std::shared_ptr + target_header; + target_header = targetlist_can_umrr9f_v2_4_1->GetTargetListHeader(); + sensor_msgs::msg::PointCloud2 msg; + umrr_ros2_msgs::msg::CanTargetHeader header; + RadarCloudModifier modifier{msg, m_sensors[sensor_idx].frame_id}; + const auto [sec, nanosec] = + convert_timestamp(std::chrono::microseconds{port_header->GetTimestamp()}); + msg.header.stamp.sec = sec; + msg.header.stamp.nanosec = nanosec; + header.frame_id = m_sensors[sensor_idx].frame_id; + header.acq_ts_fraction = target_header->GetAcqTimeStampFraction(); + header.time_stamp = target_header->GetTimeStamp(); + header.cycle_time = target_header->GetCycleDuration(); + header.number_of_targets = target_header->GetNumberOfTargets(); + header.acquisition_setup = target_header->GetAcquisitionSetup(); + header.cycle_count = target_header->GetCycleCount(); + for (const auto & target : targetlist_can_umrr9f_v2_4_1->GetTargetList()) { + const auto range = target->GetRange(); + const auto elevation_angle = target->GetElevationAngle(); + const auto range_2d = range * std::cos(elevation_angle); + const auto azimuth_angle = target->GetAzimuthAngle(); + const auto snr = target->GetSignalLevel() - target->GetNoise(); + modifier.push_back( + {range_2d * std::cos(azimuth_angle), range_2d * std::sin(azimuth_angle), + range * std::sin(elevation_angle), target->GetSpeedRadial(), target->GetSignalLevel(), + target->GetRCS(), target->GetNoise(), snr, azimuth_angle, elevation_angle, range}); + } + + m_publishers[sensor_idx]->publish(msg); + m_publishers_can_target_header[sensor_idx]->publish(header); + } +} + +void SmartmicroRadarNode::CAN_targetlist_callback_umrr9f_v3_0_0( + const std::uint32_t sensor_idx, + const std::shared_ptr< + com::master::umrr9f_t169_automotive_v3_0_0::comtargetbaselist::ComTargetBaseList> & + targetlist_can_umrr9f_v3_0_0, + const com::types::ClientId client_id) +{ + std::cout << "Targetlist for umrr9f_v3_0_0" << std::endl; + if (!check_signal) { + std::shared_ptr + port_header; + port_header = targetlist_can_umrr9f_v3_0_0->GetPortHeader(); + std::shared_ptr + target_header; + target_header = targetlist_can_umrr9f_v3_0_0->GetTargetListHeader(); + sensor_msgs::msg::PointCloud2 msg; + umrr_ros2_msgs::msg::CanTargetHeader header; + RadarCloudModifier modifier{msg, m_sensors[sensor_idx].frame_id}; + const auto [sec, nanosec] = + convert_timestamp(std::chrono::microseconds{port_header->GetTimestamp()}); + msg.header.stamp.sec = sec; + msg.header.stamp.nanosec = nanosec; + header.frame_id = m_sensors[sensor_idx].frame_id; + header.acq_ts_fraction = target_header->GetAcqTimeStampFraction(); + header.time_stamp = target_header->GetTimeStamp(); + header.cycle_time = target_header->GetCycleDuration(); + header.number_of_targets = target_header->GetNumberOfTargets(); + header.acquisition_setup = target_header->GetAcquisitionSetup(); + header.cycle_count = target_header->GetCycleCount(); + for (const auto & target : targetlist_can_umrr9f_v3_0_0->GetTargetList()) { + const auto range = target->GetRange(); + const auto elevation_angle = target->GetElevationAngle(); + const auto range_2d = range * std::cos(elevation_angle); + const auto azimuth_angle = target->GetAzimuthAngle(); + const auto snr = target->GetSignalLevel() - target->GetNoise(); + modifier.push_back( + {range_2d * std::cos(azimuth_angle), range_2d * std::sin(azimuth_angle), + range * std::sin(elevation_angle), target->GetSpeedRadial(), target->GetSignalLevel(), + target->GetRCS(), target->GetNoise(), snr, azimuth_angle, elevation_angle, range}); + } + + m_publishers[sensor_idx]->publish(msg); + m_publishers_can_target_header[sensor_idx]->publish(header); + } +} + +void SmartmicroRadarNode::CAN_targetlist_callback_umrra4_v1_0_1( + const std::uint32_t sensor_idx, + const std::shared_ptr< + com::master::umrra4_automotive_v1_0_1::comtargetbaselist::ComTargetBaseList> & + targetlist_can_umrra4_v1_0_1, + const com::types::ClientId client_id) +{ + std::cout << "CAN Targetlist for umrra4_v1_0_1" << std::endl; + if (!check_signal) { + std::shared_ptr + port_header; + port_header = targetlist_can_umrra4_v1_0_1->GetPortHeader(); + std::shared_ptr + target_header; + target_header = targetlist_can_umrra4_v1_0_1->GetTargetListHeader(); + sensor_msgs::msg::PointCloud2 msg; + umrr_ros2_msgs::msg::CanTargetHeader header; + RadarCloudModifier modifier{msg, m_sensors[sensor_idx].frame_id}; + const auto [sec, nanosec] = + convert_timestamp(std::chrono::microseconds{port_header->GetTimestamp()}); + msg.header.stamp.sec = sec; + msg.header.stamp.nanosec = nanosec; + header.frame_id = m_sensors[sensor_idx].frame_id; + header.acq_ts_fraction = target_header->GetAcqTimeStampFraction(); + header.time_stamp = target_header->GetTimeStamp(); + header.cycle_time = target_header->GetCycleDuration(); + header.number_of_targets = target_header->GetNumberOfTargets(); + header.acquisition_setup = target_header->GetAcquisitionSetup(); + header.cycle_count = target_header->GetCycleCount(); + for (const auto & target : targetlist_can_umrra4_v1_0_1->GetTargetList()) { + const auto range = target->GetRange(); + const auto elevation_angle = target->GetElevationAngle(); + const auto range_2d = range * std::cos(elevation_angle); + const auto azimuth_angle = target->GetAzimuthAngle(); + const auto snr = target->GetSignalLevel() - target->GetNoise(); + modifier.push_back( + {range_2d * std::cos(azimuth_angle), range_2d * std::sin(azimuth_angle), + range * std::sin(elevation_angle), target->GetSpeedRadial(), target->GetSignalLevel(), + target->GetRCS(), target->GetNoise(), snr, azimuth_angle, elevation_angle, range}); + } + + m_publishers[sensor_idx]->publish(msg); + m_publishers_can_target_header[sensor_idx]->publish(header); + } +} + +void SmartmicroRadarNode::CAN_targetlist_callback_umrra4_v1_2_1( + const std::uint32_t sensor_idx, + const std::shared_ptr< + com::master::umrra4_automotive_v1_2_1::comtargetbaselist::ComTargetBaseList> & + targetlist_can_umrra4_v1_2_1, + const com::types::ClientId client_id) +{ + std::cout << "CAN Targetlist for umrra4_v1_2_1" << std::endl; + if (!check_signal) { + std::shared_ptr + port_header; + port_header = targetlist_can_umrra4_v1_2_1->GetPortHeader(); + std::shared_ptr + target_header; + target_header = targetlist_can_umrra4_v1_2_1->GetTargetListHeader(); + sensor_msgs::msg::PointCloud2 msg; + umrr_ros2_msgs::msg::CanTargetHeader header; + RadarCloudModifier modifier{msg, m_sensors[sensor_idx].frame_id}; + const auto [sec, nanosec] = + convert_timestamp(std::chrono::microseconds{port_header->GetTimestamp()}); + msg.header.stamp.sec = sec; + msg.header.stamp.nanosec = nanosec; + header.frame_id = m_sensors[sensor_idx].frame_id; + header.cycle_time = target_header->GetCycleDuration(); + header.number_of_targets = target_header->GetNumberOfTargets(); + header.acquisition_setup = target_header->GetAcquisitionSetup(); + header.cycle_count = target_header->GetCycleCount(); + header.time_stamp = target_header->GetTimeStamp(); + header.acq_ts_fraction = target_header->GetAcqTimeStampFraction(); + for (const auto & target : targetlist_can_umrra4_v1_2_1->GetTargetList()) { + const auto range = target->GetRange(); + const auto elevation_angle = target->GetElevationAngle(); + const auto range_2d = range * std::cos(elevation_angle); + const auto azimuth_angle = target->GetAzimuthAngle(); + const auto snr = target->GetSignalLevel() - target->GetNoise(); + modifier.push_back( + {range_2d * std::cos(azimuth_angle), range_2d * std::sin(azimuth_angle), + range * std::sin(elevation_angle), target->GetSpeedRadial(), target->GetSignalLevel(), + target->GetRCS(), target->GetNoise(), snr, azimuth_angle, elevation_angle, range}); + } + + m_publishers[sensor_idx]->publish(msg); + m_publishers_can_target_header[sensor_idx]->publish(header); + } +} + +void SmartmicroRadarNode::CAN_targetlist_callback_umrra4_v1_4_0( + const std::uint32_t sensor_idx, + const std::shared_ptr< + com::master::umrra4_automotive_v1_4_0::comtargetbaselist::ComTargetBaseList> & + targetlist_can_umrra4_v1_4_0, + const com::types::ClientId client_id) +{ + std::cout << "CAN Targetlist for umrra4_v1_4_0" << std::endl; + if (!check_signal) { + std::shared_ptr + port_header; + port_header = targetlist_can_umrra4_v1_4_0->GetPortHeader(); + std::shared_ptr + target_header; + target_header = targetlist_can_umrra4_v1_4_0->GetTargetListHeader(); + sensor_msgs::msg::PointCloud2 msg; + umrr_ros2_msgs::msg::CanTargetHeader header; + RadarCloudModifier modifier{msg, m_sensors[sensor_idx].frame_id}; + const auto [sec, nanosec] = + convert_timestamp(std::chrono::microseconds{port_header->GetTimestamp()}); + msg.header.stamp.sec = sec; + msg.header.stamp.nanosec = nanosec; + header.frame_id = m_sensors[sensor_idx].frame_id; + header.cycle_time = target_header->GetCycleDuration(); + header.number_of_targets = target_header->GetNumberOfTargets(); + header.acquisition_setup = target_header->GetAcquisitionSetup(); + header.cycle_count = target_header->GetCycleCount(); + header.time_stamp = target_header->GetTimeStamp(); + header.acq_ts_fraction = target_header->GetAcqTimeStampFraction(); + for (const auto & target : targetlist_can_umrra4_v1_4_0->GetTargetList()) { + const auto range = target->GetRange(); + const auto elevation_angle = target->GetElevationAngle(); + const auto range_2d = range * std::cos(elevation_angle); + const auto azimuth_angle = target->GetAzimuthAngle(); + const auto snr = target->GetSignalLevel() - target->GetNoise(); + modifier.push_back( + {range_2d * std::cos(azimuth_angle), range_2d * std::sin(azimuth_angle), + range * std::sin(elevation_angle), target->GetSpeedRadial(), target->GetSignalLevel(), + target->GetRCS(), target->GetNoise(), snr, azimuth_angle, elevation_angle, range}); + } + + m_publishers[sensor_idx]->publish(msg); + m_publishers_can_target_header[sensor_idx]->publish(header); + } +} + +void SmartmicroRadarNode::update_config_files_from_params() +{ + const auto master_inst_serial_type = declare_parameter(kInstSerialTypeTag, std::string{}); + const auto master_data_serial_type = declare_parameter(kDataSerialTypeTag, std::string{}); + + auto read_adapter_params_if_possible = [&](const std::uint32_t index) { + auto & current_adapter = m_adapters[index]; + const auto prefix_2 = "adapters.adapter_" + std::to_string(index); + current_adapter.hw_dev_id = this->declare_parameter(prefix_2 + ".hw_dev_id", kDefaultHwDevId); + if (current_adapter.hw_dev_id == kDefaultHwDevId) { + // The id was not set, so the adapter with this index was not defined. + // Stop here. + return false; + } + current_adapter.hw_iface_name = + this->declare_parameter(prefix_2 + ".hw_iface_name", kDefaultHwDevIface); + current_adapter.hw_type = this->declare_parameter(prefix_2 + ".hw_type", kDefaultHwLinkType); + current_adapter.baudrate = this->declare_parameter(prefix_2 + ".baudrate", 500000); + current_adapter.port = this->declare_parameter(prefix_2 + ".port", kDefaultPort); + + return true; + }; + + auto read_sensor_params_if_possible = [&](const std::uint32_t index) { + auto & sensor = m_sensors[index]; + const auto prefix_3 = "sensors.sensor_" + std::to_string(index); + sensor.dev_id = this->declare_parameter(prefix_3 + ".dev_id", kDefaultHwDevId); + sensor.uifname = this->declare_parameter(prefix_3 + ".uifname", ""); + sensor.uifmajorv = this->declare_parameter(prefix_3 + ".uifmajorv", 0); + sensor.uifminorv = this->declare_parameter(prefix_3 + ".uifminorv", 0); + sensor.uifpatchv = this->declare_parameter(prefix_3 + ".uifpatchv", 0); + sensor.model = this->declare_parameter(prefix_3 + ".model", kDefaultSensorType); + sensor.id = this->declare_parameter(prefix_3 + ".id", kDefaultClientId); + if (sensor.id == kDefaultClientId) { + // The id was not set, so the sensor with this index was not defined. Stop + // here. + return false; + } + sensor.ip = this->declare_parameter(prefix_3 + ".ip", ""); + sensor.port = this->declare_parameter(prefix_3 + ".port", 0); + sensor.frame_id = this->declare_parameter(prefix_3 + ".frame_id", kDefaultFrameId); + sensor.history_size = this->declare_parameter(prefix_3 + ".history_size", kDefaultHistorySize); + sensor.inst_type = this->declare_parameter(prefix_3 + ".inst_type", ""); + sensor.data_type = this->declare_parameter(prefix_3 + ".data_type", ""); + sensor.link_type = this->declare_parameter(prefix_3 + ".link_type", kDefaultHwLinkType); + sensor.pub_type = this->declare_parameter(prefix_3 + ".pub_type", ""); + return true; + }; + + for (auto j = 0UL; j < m_adapters.size(); ++j) { + if (!read_adapter_params_if_possible(j)) { + m_number_of_adapters = j; + break; + } + } + + if (!m_number_of_adapters) { + throw std::runtime_error("At least one adapter must be configured."); + } + + for (auto i = 0UL; i < m_sensors.size(); ++i) { + if (!read_sensor_params_if_possible(i)) { + m_number_of_sensors = i; + break; + } + } + if (!m_number_of_sensors) { + throw std::runtime_error("At least one sensor must be configured."); + } + + auto config = nlohmann::json::parse(std::ifstream{kConfigFilePath}); + config[kDataSerialTypeJsonTag] = master_data_serial_type; + config[kInstSerialTypeJsonTag] = master_inst_serial_type; + std::ofstream{kConfigFilePath, std::ios::trunc} << config; + + auto hw_inventory = nlohmann::json::parse(std::ifstream{kHwInventoryFilePath}); + auto & hw_items = hw_inventory[kHwItemsJsonTag]; + if (hw_items.empty()) { + throw std::runtime_error("There are no 'hwItems' defined in the hw_inventory.json file."); + } + auto hw_item = hw_items.front(); + hw_items.clear(); + for (auto j = 0UL; j < m_number_of_adapters; ++j) { + const auto & adapter = m_adapters[j]; + hw_item[kPortTag] = adapter.port; + hw_item[kHwDevLinkTag] = adapter.hw_type; + hw_item[kHwDevIdTag] = adapter.hw_dev_id; + hw_item[kHwDevIfaceNameTag] = adapter.hw_iface_name; + hw_item[kBaudRateTag] = adapter.baudrate; + hw_items.push_back(hw_item); + } + std::ofstream{kHwInventoryFilePath, std::ios::trunc} << hw_inventory; + + auto routing_table = nlohmann::json::parse(std::ifstream{kRoutingTableFilePath}); + auto & clients = routing_table[kClientsJsonTag]; + if (clients.empty()) { + throw std::runtime_error("There are no 'clients' defined in the routing_table.json file."); + } + auto client = clients.front(); // Make a copy of the first client. + clients.clear(); + for (auto i = 0UL; i < m_number_of_sensors; ++i) { + const auto & sensor = m_sensors[i]; + client[kClientLinkTag] = sensor.link_type; + client[kClientIdTag] = sensor.id; + client[kHwDevIdTag] = sensor.dev_id; + client[kPortTag] = sensor.port; + client[kIpTag] = sensor.ip; + client[kInstSerialTypeJsonTag] = sensor.inst_type; + client[kDataSerialTypeJsonTag] = sensor.data_type; + client[kUINameTag] = sensor.uifname; + client[kUIMajorVTag] = sensor.uifmajorv; + client[kUIMinorVTag] = sensor.uifminorv; + client[kUIPatchVTag] = sensor.uifpatchv; + clients.push_back(client); + } + + std::ofstream{kRoutingTableFilePath, std::ios::trunc} << std::setw(4) << routing_table; +} + +} // namespace radar +} // namespace drivers +} // namespace smartmicro + +RCLCPP_COMPONENTS_REGISTER_NODE(smartmicro::drivers::radar::SmartmicroRadarNode) diff --git a/umrr_ros2_driver/src/update_service.cpp b/umrr_ros2_driver/src/update_service.cpp new file mode 100644 index 0000000..67aaca2 --- /dev/null +++ b/umrr_ros2_driver/src/update_service.cpp @@ -0,0 +1,95 @@ +#include "umrr_ros2_driver/update_service.hpp" + +#include +#include + +using namespace com::types; +using namespace com::master; + +UpdateService::UpdateService() {} + +void UpdateService::StartSoftwareUpdate(ClientId client_id, std::string & update_image) +{ + std::ifstream fileStream(update_image, std::ios::binary | std::ios::ate); + if (!fileStream.is_open()) { + RCLCPP_ERROR( + rclcpp::get_logger("FirmwareUpdater"), "Couldn't open file: %s", update_image.c_str()); + return; + } + + const uint64_t totalSize = fileStream.tellg(); + + auto comServicesPtr = CommunicationServicesIface::Get(); + auto updateService = comServicesPtr->GetUpdateService(); + + { + std::lock_guard lock(mutex_); + updateInfo_.SetUpdateStatus(RUNNING); + updateInfo_.SetCurrentDownloadedBytes(0); + } // mutex(unlocked) + + RCLCPP_INFO( + rclcpp::get_logger("UpdateService"), "Starting firmware download of %lu bytes...", totalSize); + + if (updateService->SoftwareUpdate(update_image, client_id, [this](SWUpdateInfo & info) { + this->UpdateCallback(info); + }) != ERROR_CODE_OK) { + RCLCPP_ERROR(rclcpp::get_logger("FirmwareUpdater"), "Start of software download failed"); + return; + } + + { + std::unique_lock lock( + mutex_); + cv_.wait( + lock, + [this, + totalSize] { + return updateInfo_.GetUpdateStatus() != RUNNING || + updateInfo_.GetCurrentDownloadedBytes() >= totalSize; + }); + } + + HandleResult(); +} + +void UpdateService::UpdateCallback(SWUpdateInfo & info) +{ + { + std::lock_guard lock(mutex_); + updateInfo_ = info; + RCLCPP_INFO( + rclcpp::get_logger("FirmwareUpdater"), "Downloaded %lu bytes...", + info.GetCurrentDownloadedBytes()); + } + cv_.notify_one(); +} + +void UpdateService::HandleResult() +{ + std::lock_guard lock(mutex_); + switch (updateInfo_.GetUpdateStatus()) { + case READY_SUCCESS: + RCLCPP_INFO( + rclcpp::get_logger("FirmwareUpdater"), "Firmware download completed successfully."); + break; + case STOPPED_BY_MASTER: + RCLCPP_WARN(rclcpp::get_logger("FirmwareUpdater"), "Download stopped by master."); + break; + case STOPPED_BY_SLAVE: + RCLCPP_ERROR(rclcpp::get_logger("FirmwareUpdater"), "Download stopped by slave."); + break; + case STOPPED_BY_ERROR_TIMEOUT: + RCLCPP_ERROR(rclcpp::get_logger("FirmwareUpdater"), "Download failed: timeout."); + break; + case STOPPED_BY_ERROR_BLOCK_REPEAT: + RCLCPP_ERROR(rclcpp::get_logger("FirmwareUpdater"), "Download failed: block repeat error."); + break; + case STOPPED_BY_ERROR_IMAGE_INVALID: + RCLCPP_ERROR(rclcpp::get_logger("FirmwareUpdater"), "Download failed: invalid image."); + break; + default: + RCLCPP_ERROR(rclcpp::get_logger("FirmwareUpdater"), "Download failed: unknown error."); + break; + } +} diff --git a/umrr_ros2_driver/test/radar_node_test.launch.py b/umrr_ros2_driver/test/radar_node_test.launch.py new file mode 100644 index 0000000..bb4279c --- /dev/null +++ b/umrr_ros2_driver/test/radar_node_test.launch.py @@ -0,0 +1,172 @@ +# Copyright (c) 2021, s.m.s, smart microwave sensors GmbH, Brunswick, Germany +# +# 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 os +import time +import unittest + +import launch +import launch_testing +import launch_testing.actions +from launch.actions import ExecuteProcess + +import pytest +import rclpy +import sensor_msgs.msg as sensor_msgs +from ament_index_python import get_package_share_directory +from launch_ros.actions import Node + +PACKAGE_NAME = 'umrr_ros2_driver' + + +@pytest.mark.launch_test +def generate_test_description(): + + radar__params = os.path.join( + get_package_share_directory(PACKAGE_NAME), 'param/radar.params.template.yaml') + radar_node = Node( + package=PACKAGE_NAME, + executable='smartmicro_radar_node_exe', + name='smart_radar', + parameters=[radar__params] + ) + + set_frequency_sweep_service = ExecuteProcess( + cmd = [ + 'ros2', 'service', 'call', + '/smart_radar/set_radar_mode', + 'umrr_ros2_msgs/srv/SetMode', + '{section_name: auto_interface_0dim, sensor_id: 200, params: ["frequency_sweep_idx"], values: ["1"], value_types: [3]}' + ], + output='screen', + ) + + set_angular_separation_service = ExecuteProcess( + cmd=[ + 'ros2', 'service', 'call', + '/smart_radar/set_radar_mode', + 'umrr_ros2_msgs/srv/SetMode', + '{section_name: auto_interface_0dim, sensor_id: 100, params: ["angular_separation"], values: ["1"], value_types: [3]}' + ], + output='screen' + ) + + + get_range_toggle_mode_service = ExecuteProcess( + cmd = [ + 'ros2', 'service', 'call', + '/smart_radar/get_radar_mode', + 'umrr_ros2_msgs/srv/GetMode', + '{section_name: auto_interface_0dim, sensor_id: 300, params: ["range_toggle_mode"], param_types: [3]}' + ], + output='screen', + ) + + get_software_version_service = ExecuteProcess( + cmd = [ + 'ros2', 'service', 'call', + '/smart_radar/get_radar_status', + 'umrr_ros2_msgs/srv/GetStatus', + '{section_name: auto_interface, sensor_id: 400, statuses: ["sw_version_major", "sw_version_minor"], status_types: [1, 1]}' + ], + output='screen', + ) + + return ( + launch.LaunchDescription([ + radar_node, + set_frequency_sweep_service, + set_angular_separation_service, + get_range_toggle_mode_service, + get_software_version_service, + launch_testing.actions.ReadyToTest(), + ]), + { + 'radar_node': radar_node, + } + ) + + +class TestSmartNode(unittest.TestCase): + + @classmethod + def setUpClass(cls): + # Initialize the ROS context for the test node + rclpy.init() + + @classmethod + def tearDownClass(cls): + # Shutdown the ROS context + rclpy.shutdown() + + def setUp(self): + # Create a ROS node for tests + self.test_node = rclpy.create_node('test_node') + + def tearDown(self): + self.test_node.destroy_node() + + def test_smart_node_publishes(self): + # Expect the smartnode to publish strings on 'smart_radar/port_targets_' + data_rx_s1 = [] + data_rx_s2 = [] + data_rx_s3 = [] + + def data_rx_s1_callback(msg): + data_rx_s1.append(msg) + + def data_rx_s2_callback(msg): + data_rx_s2.append(msg) + + def data_rx_s3_callback(msg): + data_rx_s3.append(msg) + + sub_s1 = self.test_node.create_subscription( + sensor_msgs.PointCloud2, + 'smart_radar/port_targets_3', + data_rx_s1_callback, + 10 + ) + sub_s2 = self.test_node.create_subscription( + sensor_msgs.PointCloud2, + 'smart_radar/port_targets_1', + data_rx_s2_callback, + 10 + ) + sub_s3 = self.test_node.create_subscription( + sensor_msgs.PointCloud2, + 'smart_radar/port_targets_2', + data_rx_s3_callback, + 10 + ) + try: + # Wait until the publisher publishes + end_time = time.time() + 20 + + while time.time() < end_time: + rclpy.spin_once(self.test_node, timeout_sec=0.1) + if len(data_rx_s1) > 1: + print(f"Data from S1 received at {time.time()}") + if len(data_rx_s2) > 1: + print(f"Data from S2 received at {time.time()}") + if len(data_rx_s3) > 1: + print(f"Data from S3 received at {time.time()}") + self.assertGreater(len(data_rx_s1), 1) + self.assertGreater(len(data_rx_s2), 1) + self.assertGreater(len(data_rx_s3), 1) + + finally: + self.test_node.destroy_subscription(sub_s1) + self.test_node.destroy_subscription(sub_s2) + self.test_node.destroy_subscription(sub_s3) diff --git a/umrr_ros2_msgs/CMakeLists.txt b/umrr_ros2_msgs/CMakeLists.txt new file mode 100644 index 0000000..a21888d --- /dev/null +++ b/umrr_ros2_msgs/CMakeLists.txt @@ -0,0 +1,45 @@ +cmake_minimum_required(VERSION 3.11) +project(umrr_ros2_msgs) + +# Default to C++17 +if(NOT CMAKE_CXX_STANDARD) + set(CMAKE_CXX_STANDARD 17) + set(CMAKE_CXX_STANDARD_REQUIRED ON) +endif() + +if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang") + add_compile_options(-Wall -Wextra -Wpedantic -Wnon-virtual-dtor -Woverloaded-virtual) +endif() + +# find dependencies +find_package(ament_cmake REQUIRED) +find_package(rosidl_default_generators REQUIRED) +find_package(std_msgs REQUIRED) +find_package(geometry_msgs REQUIRED) +find_package(sensor_msgs REQUIRED) + +rosidl_generate_interfaces(${PROJECT_NAME} + "srv/SetMode.srv" + "srv/SetIp.srv" + "srv/SendCommand.srv" + "srv/FirmwareDownload.srv" + "srv/GetStatus.srv" + "srv/GetMode.srv" + "msg/CanTargetHeader.msg" + "msg/PortTargetHeader.msg" + "msg/PortObjectHeader.msg" + "msg/CanObjectHeader.msg" + DEPENDENCIES + "geometry_msgs" + "sensor_msgs" + "std_msgs" +) + +ament_export_dependencies(rosidl_default_runtime) + +if(BUILD_TESTING) + find_package(ament_lint_auto REQUIRED) + ament_lint_auto_find_test_dependencies() +endif() + +ament_package() diff --git a/umrr_ros2_msgs/msg/CanObjectHeader.msg b/umrr_ros2_msgs/msg/CanObjectHeader.msg new file mode 100644 index 0000000..f32938e --- /dev/null +++ b/umrr_ros2_msgs/msg/CanObjectHeader.msg @@ -0,0 +1,9 @@ +string frame_id +float32 cycle_time +uint16 number_of_objects +float32 ego_speed +float32 ego_speed_quality +float32 ego_yaw_rate +float32 ego_yaw_rate_quality +uint32 cycle_count +uint8 dyn_source diff --git a/umrr_ros2_msgs/msg/CanTargetHeader.msg b/umrr_ros2_msgs/msg/CanTargetHeader.msg new file mode 100644 index 0000000..0d519e5 --- /dev/null +++ b/umrr_ros2_msgs/msg/CanTargetHeader.msg @@ -0,0 +1,7 @@ +string frame_id +float32 cycle_time +uint8 number_of_targets +uint32 cycle_count +uint8 acquisition_setup +uint32 time_stamp +float32 acq_ts_fraction diff --git a/umrr_ros2_msgs/msg/PortObjectHeader.msg b/umrr_ros2_msgs/msg/PortObjectHeader.msg new file mode 100644 index 0000000..6b71d49 --- /dev/null +++ b/umrr_ros2_msgs/msg/PortObjectHeader.msg @@ -0,0 +1,12 @@ +string frame_id +uint32 port_identifier +int16 port_ver_major +int16 port_ver_minor +uint32 port_size +uint8 body_endianness +uint8 port_index +uint8 header_ver_major +uint8 header_ver_minor +float32 cycle_time +uint16 number_of_objects +uint64 ts_measurement diff --git a/umrr_ros2_msgs/msg/PortTargetHeader.msg b/umrr_ros2_msgs/msg/PortTargetHeader.msg new file mode 100644 index 0000000..f82cdbf --- /dev/null +++ b/umrr_ros2_msgs/msg/PortTargetHeader.msg @@ -0,0 +1,17 @@ +string frame_id +uint32 port_identifier +int16 port_ver_major +int16 port_ver_minor +uint32 port_size +uint8 body_endianness +uint8 port_index +uint8 header_ver_major +uint8 header_ver_minor +float32 cycle_time +uint16 number_of_targets +uint8 acquisition_tx_ant_idx +uint8 acquisition_sweep_idx +uint8 acquisition_cf_idx +uint8 prf +float32 umambiguous_speed +uint64 acquisition_start diff --git a/umrr_ros2_msgs/package.xml b/umrr_ros2_msgs/package.xml new file mode 100644 index 0000000..2b6bf47 --- /dev/null +++ b/umrr_ros2_msgs/package.xml @@ -0,0 +1,19 @@ + + + + umrr_ros2_msgs + 3.0.0 + A package for smartmicro radar messages + shah + Apache 2.0 + + ament_cmake + + rosidl_default_generators + + rosidl_interface_packages + + + ament_cmake + + diff --git a/umrr_ros2_msgs/srv/FirmwareDownload.srv b/umrr_ros2_msgs/srv/FirmwareDownload.srv new file mode 100644 index 0000000..4557eff --- /dev/null +++ b/umrr_ros2_msgs/srv/FirmwareDownload.srv @@ -0,0 +1,4 @@ +uint32 sensor_id +string file_path +--- +string res diff --git a/umrr_ros2_msgs/srv/GetMode.srv b/umrr_ros2_msgs/srv/GetMode.srv new file mode 100644 index 0000000..bf0cd88 --- /dev/null +++ b/umrr_ros2_msgs/srv/GetMode.srv @@ -0,0 +1,6 @@ +string section_name # name of the interface section +uint32 sensor_id # id of the sensor to send request +string[] params # list of parameters or parameter to read +uint8[] param_types # 0: float32, 1: uint32, 2: uint16, 3: uint8 +--- +string res # response from the sensor diff --git a/umrr_ros2_msgs/srv/GetStatus.srv b/umrr_ros2_msgs/srv/GetStatus.srv new file mode 100644 index 0000000..5d495b3 --- /dev/null +++ b/umrr_ros2_msgs/srv/GetStatus.srv @@ -0,0 +1,6 @@ +string section_name # name of the interface section +uint32 sensor_id # id of the sensor to send request +string[] statuses # list of status or status to send +uint8[] status_types # 0: uint32, 1: uint16 +--- +string res # response from the sensor diff --git a/umrr_ros2_msgs/srv/SendCommand.srv b/umrr_ros2_msgs/srv/SendCommand.srv new file mode 100644 index 0000000..adb43cc --- /dev/null +++ b/umrr_ros2_msgs/srv/SendCommand.srv @@ -0,0 +1,6 @@ +string section_name # name of the interface section +string command # name of command to send +float32 value # value to send +uint32 sensor_id # id of the sensor to send request +--- +string res diff --git a/umrr_ros2_msgs/srv/SetIp.srv b/umrr_ros2_msgs/srv/SetIp.srv new file mode 100644 index 0000000..d21c0c2 --- /dev/null +++ b/umrr_ros2_msgs/srv/SetIp.srv @@ -0,0 +1,4 @@ +uint32 value_ip +uint32 sensor_id +--- +string res_ip diff --git a/umrr_ros2_msgs/srv/SetMode.srv b/umrr_ros2_msgs/srv/SetMode.srv new file mode 100644 index 0000000..9f7549c --- /dev/null +++ b/umrr_ros2_msgs/srv/SetMode.srv @@ -0,0 +1,7 @@ +string section_name # name of the interface section +uint32 sensor_id # id of the sensor to send request +string[] params # list of params or param to send +string[] values # list of values or value to send +uint8[] value_types # 0: float32, 1: uint32, 2: uint16, 3: uint8 +--- +string res # response from the sensor From 4fdbc5f7453d3c55dafba0b769ea86610f6543f4 Mon Sep 17 00:00:00 2001 From: Cody Washington <115746975+Jelatinone@users.noreply.github.com> Date: Thu, 23 Apr 2026 17:20:37 -0500 Subject: [PATCH 4/4] Add Parameter Config, Launch Config; Add to Hail Bopp Launch --- data/radar/radar.parm.yaml | 27 +++++++++++++++++++++++++++ launch.hailbopp_full.py | 1 + launch_node_definitions.py | 7 +++++++ 3 files changed, 35 insertions(+) create mode 100644 data/radar/radar.parm.yaml diff --git a/data/radar/radar.parm.yaml b/data/radar/radar.parm.yaml new file mode 100644 index 0000000..ac99859 --- /dev/null +++ b/data/radar/radar.parm.yaml @@ -0,0 +1,27 @@ +/smart_radar: + ros__parameters: + master_inst_serial_type: "port_based" + master_data_serial_type: "port_based" + adapters: + adapter_0: + hw_type: "eth" + hw_dev_id: 2 + hw_iface_name: "enp3s0" + port: 55555 + sensors: + sensor_0: + link_type: "eth" + pub_type: "target" + model: "umrr9f_v2_4_1" + dev_id: 2 + id: 100 + ip: "192.168.11.11" + port: 55555 + frame_id: "umrr" + history_size: 10 + inst_type: "port_based" + data_type: "port_based" + uifname: "umrr9f_t169_automotive" + uifmajorv: 2 + uifminorv: 4 + uifpatchv: 1 \ No newline at end of file diff --git a/launch.hailbopp_full.py b/launch.hailbopp_full.py index ca961f3..58f031b 100644 --- a/launch.hailbopp_full.py +++ b/launch.hailbopp_full.py @@ -30,6 +30,7 @@ def generate_launch_description(): joystick_microsoft, # #camera, gnss, + radar, # Old lidar (left and right): # lidar_driver_left, # lidar_pointcloud_left, diff --git a/launch_node_definitions.py b/launch_node_definitions.py index cb985fa..27cfdeb 100644 --- a/launch_node_definitions.py +++ b/launch_node_definitions.py @@ -29,6 +29,13 @@ executable='clock_node' ) +radar = Node( + package="umrr_ros2_driver", + executable="smartmicro_radar_node_exe", + name="smart_radar", + parameters=['/vehicle_interface/data/radar/radar.param.yaml'], +) + # I don't think we use this... maybe the old gps? # but we should keep it for broader vehicle interface... gps_node = Node(