From 04ad503ca38f6b873bdaa0c327071765cae516f6 Mon Sep 17 00:00:00 2001 From: "Eichmann, Christian" Date: Wed, 13 Nov 2024 16:46:32 +0000 Subject: [PATCH] ODS Integration --- .clang-format | 24 +- .gitignore | 29 +- .gitlab-ci.yml | 72 ++ .vscode/settings.json | 85 ++ CHANGELOG.rst | 27 + CMakeLists.txt | 93 +- Dockerfile | 25 +- README.md | 66 +- build_container.sh | 16 +- config/camera_default_parameters.yaml | 15 +- config/examples/o3r_2d.yaml | 7 +- config/examples/o3r_3d.yaml | 8 +- config/examples/two_o3r_heads.yaml | 32 +- config/ods_default_parameters.yaml | 15 + config/param1.yml | 9 - config/params.yaml | 29 - config/params_2cams.yaml | 13 - doc/building.md | 33 +- doc/camera_node/camera_transforms.md | 0 .../figures/O3R_merged_point_cloud.png | Bin doc/{ => camera_node}/figures/rviz_sample.png | Bin doc/camera_node/figures/transforms-1.png | Bin 0 -> 35858 bytes doc/camera_node/figures/transforms-2.png | Bin 0 -> 33020 bytes doc/camera_node/index_camera_node.md | 11 + doc/{ => camera_node}/launch.md | 11 +- doc/camera_node/multi_head.md | 64 + doc/camera_node/parameters.md | 40 + doc/camera_node/topics.md | 26 + doc/deployment.md | 11 +- doc/diagnostic.md | 32 + doc/figures/transforms-1.png | Bin 38061 -> 0 bytes doc/figures/transforms-2.png | Bin 24387 -> 0 bytes doc/multi_head.md | 52 - doc/ods_node/index_ods_node.md | 12 + doc/ods_node/ods_configuration.md | 38 + doc/ods_node/ods_launch.md | 88 ++ doc/ods_node/ods_params.md | 11 + doc/ods_node/ods_topics.md | 16 + doc/ods_node/ods_transforms.md | 14 + doc/parameters.md | 29 - doc/rpc_error_codes.md | 5 - doc/services.md | 226 +--- doc/topics.md | 27 - doc/visualization.md | 9 +- examples/compute_cartesian.py | 133 -- examples/latched_subscriber.py | 47 - include/ifm3d_ros2/buffer_conversions.hpp | 497 ++------ include/ifm3d_ros2/buffer_id_utils.hpp | 237 +--- include/ifm3d_ros2/camera_node.hpp | 190 +-- include/ifm3d_ros2/camera_tf_publisher.hpp | 57 + include/ifm3d_ros2/diag_module.hpp | 93 ++ include/ifm3d_ros2/function_module.hpp | 57 + include/ifm3d_ros2/ods_module.hpp | 78 ++ include/ifm3d_ros2/ods_node.hpp | 206 +++ include/ifm3d_ros2/qos.hpp | 2 +- include/ifm3d_ros2/rgb_module.hpp | 101 ++ include/ifm3d_ros2/services.hpp | 88 ++ include/ifm3d_ros2/tof_module.hpp | 127 ++ include/ifm3d_ros2/visibility_control.h | 51 +- index.md | 8 +- launch/camera.launch.py | 2 +- launch/camera_managed.launch.py | 241 ---- launch/camera_standalone.launch.py | 123 -- .../examples/example_o3r_2d_and_3d.launch.py | 2 +- .../examples/example_two_o3r_heads.launch.py | 2 +- launch/multi_cameras.launch.py | 80 -- launch/ods.launch.py | 148 +++ launch/rviz.launch.py | 41 - msg/Zones.msg | 4 + package.xml | 51 +- scripts/config | 69 - scripts/dump | 71 -- scripts/launch_in_docker.sh | 24 - src/bin/camera_standalone.cpp | 2 +- src/bin/ods_standalone.cpp | 23 + src/lib/buffer_conversions.cpp | 788 ++++++++++++ src/lib/buffer_id_utils.cpp | 250 ++++ src/lib/camera_node.cpp | 1116 +++-------------- src/lib/camera_tf_publisher.cpp | 223 ++++ src/lib/diag_module.cpp | 207 +++ src/lib/function_module.cpp | 22 + src/lib/ods_module.cpp | 211 ++++ src/lib/ods_node.cpp | 434 +++++++ src/lib/rgb_module.cpp | 363 ++++++ src/lib/services.cpp | 292 +++++ src/lib/tof_module.cpp | 448 +++++++ srv/GetDiag.srv | 4 + 87 files changed, 5388 insertions(+), 3145 deletions(-) create mode 100644 .gitlab-ci.yml create mode 100644 .vscode/settings.json create mode 100644 config/ods_default_parameters.yaml delete mode 100644 config/param1.yml delete mode 100644 config/params.yaml delete mode 100644 config/params_2cams.yaml create mode 100644 doc/camera_node/camera_transforms.md rename doc/{ => camera_node}/figures/O3R_merged_point_cloud.png (100%) rename doc/{ => camera_node}/figures/rviz_sample.png (100%) create mode 100644 doc/camera_node/figures/transforms-1.png create mode 100644 doc/camera_node/figures/transforms-2.png create mode 100644 doc/camera_node/index_camera_node.md rename doc/{ => camera_node}/launch.md (85%) create mode 100644 doc/camera_node/multi_head.md create mode 100644 doc/camera_node/parameters.md create mode 100644 doc/camera_node/topics.md create mode 100644 doc/diagnostic.md delete mode 100644 doc/figures/transforms-1.png delete mode 100644 doc/figures/transforms-2.png delete mode 100644 doc/multi_head.md create mode 100644 doc/ods_node/index_ods_node.md create mode 100644 doc/ods_node/ods_configuration.md create mode 100644 doc/ods_node/ods_launch.md create mode 100644 doc/ods_node/ods_params.md create mode 100644 doc/ods_node/ods_topics.md create mode 100644 doc/ods_node/ods_transforms.md delete mode 100644 doc/parameters.md delete mode 100644 doc/rpc_error_codes.md delete mode 100644 doc/topics.md delete mode 100644 examples/compute_cartesian.py delete mode 100644 examples/latched_subscriber.py create mode 100644 include/ifm3d_ros2/camera_tf_publisher.hpp create mode 100644 include/ifm3d_ros2/diag_module.hpp create mode 100644 include/ifm3d_ros2/function_module.hpp create mode 100644 include/ifm3d_ros2/ods_module.hpp create mode 100644 include/ifm3d_ros2/ods_node.hpp create mode 100644 include/ifm3d_ros2/rgb_module.hpp create mode 100644 include/ifm3d_ros2/services.hpp create mode 100644 include/ifm3d_ros2/tof_module.hpp delete mode 100644 launch/camera_managed.launch.py delete mode 100644 launch/camera_standalone.launch.py delete mode 100644 launch/multi_cameras.launch.py create mode 100644 launch/ods.launch.py delete mode 100644 launch/rviz.launch.py create mode 100644 msg/Zones.msg delete mode 100644 scripts/config delete mode 100644 scripts/dump delete mode 100755 scripts/launch_in_docker.sh create mode 100644 src/bin/ods_standalone.cpp create mode 100644 src/lib/buffer_conversions.cpp create mode 100644 src/lib/buffer_id_utils.cpp create mode 100644 src/lib/camera_tf_publisher.cpp create mode 100644 src/lib/diag_module.cpp create mode 100644 src/lib/function_module.cpp create mode 100644 src/lib/ods_module.cpp create mode 100644 src/lib/ods_node.cpp create mode 100644 src/lib/rgb_module.cpp create mode 100644 src/lib/services.cpp create mode 100644 src/lib/tof_module.cpp create mode 100644 srv/GetDiag.srv diff --git a/.clang-format b/.clang-format index 817fd6d..4d2e393 100644 --- a/.clang-format +++ b/.clang-format @@ -50,16 +50,14 @@ SpaceAfterCStyleCast: false BreakBeforeBraces: Custom # Control of individual brace wrapping cases -BraceWrapping: { - AfterClass: 'true' - AfterControlStatement: 'true' - AfterEnum : 'true' - AfterFunction : 'true' - AfterNamespace : 'true' - AfterStruct : 'true' - AfterUnion : 'true' - BeforeCatch : 'true' - BeforeElse : 'true' - IndentBraces : 'false' -} -... +BraceWrapping: + AfterClass: 'true' + AfterControlStatement: 'true' + AfterEnum : 'true' + AfterFunction : 'true' + AfterNamespace : 'true' + AfterStruct : 'true' + AfterUnion : 'true' + BeforeCatch : 'true' + BeforeElse : 'true' + IndentBraces : 'false' \ No newline at end of file diff --git a/.gitignore b/.gitignore index b2f99f6..8868941 100644 --- a/.gitignore +++ b/.gitignore @@ -12,31 +12,6 @@ # Built Visual Studio Code Extensions *.vsix - -# ROS -devel/ -logs/ -build/ -bin/ -lib/ -msg_gen/ -srv_gen/ -msg/*Action.msg -msg/*ActionFeedback.msg -msg/*ActionGoal.msg -msg/*ActionResult.msg -msg/*Feedback.msg -msg/*Goal.msg -msg/*Result.msg -msg/_*.py -build_isolated/ -devel_isolated/ - -# Generated by dynamic reconfigure -*.cfgc -/cfg/cpp/ -/cfg/*.py - # Ignore generated docs *.dox *.wikidoc @@ -63,5 +38,5 @@ qtcreator-* # Emacs .#* -# Catkin custom files -CATKIN_IGNORE \ No newline at end of file +# Builds +build*/ diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000..2b90ff2 --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,72 @@ +# General concept is strongly inspired by the industrial_ci project https://github.com/ros-industrial/industrial_ci +--- +variables: + GIT_STRATEGY: clone + +stages: +- build + +.build_matrix: + parallel: + matrix: + - RUN_NAME: "22.04+1.4.3" # No effect on CI, just for making the gitlab pipeline view easier to read + CI_IMAGE: ros:humble-ros-core-jammy + IFM3D_PACKAGE_PATH: https://github.com/ifm/ifm3d/releases/download/v1.4.3/ifm3d-ubuntu-22.04-amd64-debs_1.4.3.tar + ROS_DISTRO: humble + - RUN_NAME: "22.04+1.5.0" + CI_IMAGE: ros:humble-ros-core-jammy + IFM3D_PACKAGE_PATH: https://github.com/ifm/ifm3d/releases/download/v1.5.0/ifm3d-ubuntu-22.04-amd64-debs_1.5.0.tar + ROS_DISTRO: humble + - RUN_NAME: "22.04+1.5.1" + CI_IMAGE: ros:humble-ros-core-jammy + IFM3D_PACKAGE_PATH: https://github.com/ifm/ifm3d/releases/download/v1.5.1/ifm3d-ubuntu-22.04-amd64-debs_1.5.1.tar + ROS_DISTRO: humble + - RUN_NAME: "22.04+1.5.2" + CI_IMAGE: ros:humble-ros-core-jammy + IFM3D_PACKAGE_PATH: https://github.com/ifm/ifm3d/releases/download/v1.5.2/ifm3d-ubuntu-22.04-amd64-debs_1.5.2.tar + ROS_DISTRO: humble + - RUN_NAME: "22.04+1.5.3" + CI_IMAGE: ros:humble-ros-core-jammy + IFM3D_PACKAGE_PATH: https://github.com/ifm/ifm3d/releases/download/v1.5.3/ifm3d-ubuntu-22.04-amd64-debs_1.5.3.tar + ROS_DISTRO: humble + + - RUN_NAME: "24.04+1.5.3" + CI_IMAGE: ros:jazzy-ros-core-noble + IFM3D_PACKAGE_PATH: https://github.com/ifm/ifm3d/releases/download/v1.5.3/ifm3d-ubuntu-22.04-amd64-debs_1.5.3.tar + ROS_DISTRO: jazzy + + +build: # very short job name to keep the pipeline preview readable on Gitlab + stage: build + image: $CI_IMAGE + parallel: !reference [.build_matrix,parallel] + + before_script: + # Download the released version ofifm3d from github + - apt-get update + - apt-get install -y curl + - curl --location $IFM3D_PACKAGE_PATH | tar x + # Install dependencies + - apt-get install -y libboost-all-dev git libcurl4-openssl-dev libgtest-dev libgoogle-glog-dev + libxmlrpc-c++8-dev libopencv-dev libpcl-dev libproj-dev python3-dev python3-pip build-essential + coreutils findutils cmake locales ninja-build + # Install ifm3d packages + - dpkg -i ./ifm3d_*.deb + # Cleanup + - rm ifm3d_*.deb + + script: + # Install some dependencies, update rosdep + - apt-get update + - apt-get install -y ros-dev-tools + - rosdep init || true # init needed for 22.04 image but throws error on 24.04; error can be ignored + - rosdep update > /dev/null + # Create Workspace and copy sources + - mkdir -p /root/target_ws/src + - cp -r . /root/target_ws/src + - cd /root/target_ws/ + # Install dependencies, build workspace and run tests + - source /opt/ros/$ROS_DISTRO/setup.bash + - rosdep install --from-paths /root/target_ws/src --ignore-src -y + - colcon build --event-handlers desktop_notification- status- terminal_title- --cmake-args -DENABLE_COVERAGE_TESTING=ON -DCMAKE_BUILD_TYPE=Debug -DCMAKE_EXPORT_COMPILE_COMMANDS=ON + - colcon test --event-handlers desktop_notification- status- terminal_title- console_cohesion+ --executor sequential --ctest-args -j1 \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..4a7d3f3 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,85 @@ +{ + "files.associations": { + "cctype": "cpp", + "clocale": "cpp", + "cmath": "cpp", + "csignal": "cpp", + "cstdarg": "cpp", + "cstddef": "cpp", + "cstdio": "cpp", + "cstdlib": "cpp", + "cstring": "cpp", + "ctime": "cpp", + "cwchar": "cpp", + "cwctype": "cpp", + "any": "cpp", + "array": "cpp", + "atomic": "cpp", + "strstream": "cpp", + "bit": "cpp", + "*.tcc": "cpp", + "bitset": "cpp", + "chrono": "cpp", + "codecvt": "cpp", + "compare": "cpp", + "complex": "cpp", + "concepts": "cpp", + "condition_variable": "cpp", + "coroutine": "cpp", + "cstdint": "cpp", + "deque": "cpp", + "forward_list": "cpp", + "list": "cpp", + "map": "cpp", + "set": "cpp", + "string": "cpp", + "unordered_map": "cpp", + "unordered_set": "cpp", + "vector": "cpp", + "exception": "cpp", + "algorithm": "cpp", + "functional": "cpp", + "iterator": "cpp", + "memory": "cpp", + "memory_resource": "cpp", + "numeric": "cpp", + "optional": "cpp", + "random": "cpp", + "ratio": "cpp", + "regex": "cpp", + "source_location": "cpp", + "string_view": "cpp", + "system_error": "cpp", + "tuple": "cpp", + "type_traits": "cpp", + "utility": "cpp", + "fstream": "cpp", + "future": "cpp", + "initializer_list": "cpp", + "iomanip": "cpp", + "iosfwd": "cpp", + "iostream": "cpp", + "istream": "cpp", + "limits": "cpp", + "mutex": "cpp", + "new": "cpp", + "numbers": "cpp", + "ostream": "cpp", + "ranges": "cpp", + "semaphore": "cpp", + "shared_mutex": "cpp", + "span": "cpp", + "sstream": "cpp", + "stdexcept": "cpp", + "stop_token": "cpp", + "streambuf": "cpp", + "thread": "cpp", + "cfenv": "cpp", + "cinttypes": "cpp", + "typeindex": "cpp", + "typeinfo": "cpp", + "valarray": "cpp", + "variant": "cpp", + "*.ipp": "cpp" + } +} \ No newline at end of file diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 9f95838..ca176ea 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,33 @@ ^^^^^^^^^^^^^^^^^^^^^^^^^ Changelog for package ifm3d-ros2 ^^^^^^^^^^^^^^^^^^^^^^^^^ +1.2 +=== + +1.2.0 +----- +* Create an ODS node to publish ODS data: + * The launch file `ods.launch.py` can be used, + * Add two topics, `"~/ods_info"` and `"~/ods_occupancy_map_ros"`, + * An example launch configuration for ODS is provided `ods_default_parameters.yaml` and can be used with the launch file, + * It is expected that ODS is configured before the node is launched. Alternatively, one can use the new `config_file` parameter. +* Remove the `diag_mode` parameter: diagnostic is always polled periodically and published to `"/diagnostic"` +* Add the `GetDiag` service for polling filtered diagnostic data. +* Camera info topic: + * Add a `"~/camera_info"` topic for RGB cameras. + * The `"~/camera_info"` topic for the TOF cameras is published when the `TOF_INFO` buffer is requested, instead of the `INTRINSIC` buffer. +* Add a `config_file` parameter. It should be formatted in JSON and will be used to configure the device when the `CONFIGURE` state is triggered. +* Transforms: + * The `cloud_link` was renamed to `ifm_base_link`, and is used as the reference ifm calibrated coordinate system for all ifm data (RGB, 3D and ODS). + * The transforms between the `cloud_link` (now `ifm_base_link`), and the `mounting_link` and `optical_link` are fixed. + * The `mounting_link` to `optical_link` transform is read when the first `TOF_INFO` or `RGB_INFO` buffer is received, and remains constant. + * The `ifm_base_link` to `mounting_link` transform is published once when the first `TOF_INFO` or `RGB_INFO` buffer is received, and is only re-published subsequently if changed. + * The camera node parameters related to tf publication got reworked, the new parameters are: + * `tf.base_frame_name`: Name for ifm reference frame +| * `tf.mounting_frame_name`: Name for the mounting point frame +| * `tf.optical_frame_name`: Name for the optical frame +| * `tf.publish_base_to_mounting`: Whether the transform from the ifm base link to the camera mounting point should be published +| * `tf.publish_mounting_to_optical`: Whether the transform from the cameras mounting point to the optical center should be published 1.1 === diff --git a/CMakeLists.txt b/CMakeLists.txt index d38cee8..e32dad4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -14,6 +14,8 @@ set(IFM3D_ROS2_DEPS diagnostic_msgs geometry_msgs lifecycle_msgs + nav_msgs + nav2_msgs rclcpp rclcpp_components rclcpp_lifecycle @@ -40,8 +42,10 @@ rosidl_generate_interfaces(${PROJECT_NAME} "msg/InverseIntrinsics.msg" "msg/RGBInfo.msg" "msg/TOFInfo.msg" + "msg/Zones.msg" "srv/Dump.srv" "srv/Config.srv" + "srv/GetDiag.srv" "srv/Softoff.srv" "srv/Softon.srv" DEPENDENCIES builtin_interfaces std_msgs @@ -53,11 +57,38 @@ rosidl_generate_interfaces(${PROJECT_NAME} include_directories(include) -# +# Target for messages included in this package +rosidl_get_typesupport_target(msgs_typesupport_target ${PROJECT_NAME} "rosidl_typesupport_cpp") + + +add_library(ifm3d_ros2_buffer_conversions SHARED + src/lib/buffer_conversions.cpp) +target_link_libraries(ifm3d_ros2_buffer_conversions + "${msgs_typesupport_target}" + ifm3d::framegrabber) +ament_target_dependencies(ifm3d_ros2_buffer_conversions ${IFM3D_ROS2_DEPS}) + +add_library(ifm3d_ros2_buffer_id_utils SHARED + src/lib/buffer_id_utils.cpp) +target_link_libraries(ifm3d_ros2_buffer_id_utils + "${msgs_typesupport_target}" + ifm3d::framegrabber) +ament_target_dependencies(ifm3d_ros2_buffer_id_utils ${IFM3D_ROS2_DEPS}) + # ifm3d camera "component" (.so) state machine ("lifecycle node") # -add_library(ifm3d_ros2_camera_node SHARED src/lib/camera_node.cpp) +add_library(ifm3d_ros2_camera_node SHARED + src/lib/camera_node.cpp + src/lib/diag_module.cpp + src/lib/function_module.cpp + src/lib/rgb_module.cpp + src/lib/camera_tf_publisher.cpp + src/lib/tof_module.cpp + src/lib/services.cpp) target_link_libraries(ifm3d_ros2_camera_node + "${msgs_typesupport_target}" + ifm3d_ros2_buffer_conversions + ifm3d_ros2_buffer_id_utils ifm3d::device ifm3d::framegrabber ) @@ -65,8 +96,26 @@ ament_target_dependencies(ifm3d_ros2_camera_node ${IFM3D_ROS2_DEPS}) rclcpp_components_register_nodes( ifm3d_ros2_camera_node "ifm3d_ros2::CameraNode" ) -rosidl_target_interfaces(ifm3d_ros2_camera_node - ${PROJECT_NAME} "rosidl_typesupport_cpp" + +# +# ifm3d ods node +# +add_library(ifm3d_ros2_ods_node SHARED + src/lib/ods_node.cpp + src/lib/function_module.cpp + src/lib/diag_module.cpp + src/lib/ods_module.cpp + src/lib/services.cpp) +target_link_libraries(ifm3d_ros2_ods_node + "${msgs_typesupport_target}" + ifm3d_ros2_buffer_conversions + ifm3d_ros2_buffer_id_utils + ifm3d::device + ifm3d::framegrabber + ) +ament_target_dependencies(ifm3d_ros2_ods_node ${IFM3D_ROS2_DEPS}) +rclcpp_components_register_nodes( + ifm3d_ros2_ods_node "ifm3d_ros2::OdsNode" ) # @@ -75,36 +124,37 @@ rosidl_target_interfaces(ifm3d_ros2_camera_node # launch_ros). # ament_auto_add_executable(camera_standalone src/bin/camera_standalone.cpp) -target_link_libraries(camera_standalone ifm3d_ros2_camera_node) +target_link_libraries(camera_standalone + "${msgs_typesupport_target}" + ifm3d_ros2_camera_node) ament_target_dependencies(camera_standalone ${IFM3D_ROS2_DEPS}) -rosidl_target_interfaces(camera_standalone - ${PROJECT_NAME} "rosidl_typesupport_cpp" - ) - -find_package(Boost 1.65.0 REQUIRED COMPONENTS system) target_link_libraries(ifm3d_ros2_camera_node ${Boost_LIBRARIES}) + +ament_auto_add_executable(ods_standalone src/bin/ods_standalone.cpp) +target_link_libraries(ods_standalone + "${msgs_typesupport_target}" + ifm3d_ros2_ods_node) +ament_target_dependencies(ods_standalone ${IFM3D_ROS2_DEPS}) +target_link_libraries(ifm3d_ros2_ods_node ${Boost_LIBRARIES}) # TODO needed? + + ############## ## Install ## ############## install( - TARGETS camera_standalone + TARGETS camera_standalone ods_standalone DESTINATION lib/${PROJECT_NAME} ) install( - TARGETS ifm3d_ros2_camera_node + TARGETS ifm3d_ros2_camera_node ifm3d_ros2_ods_node ifm3d_ros2_buffer_conversions ifm3d_ros2_buffer_id_utils ARCHIVE DESTINATION lib LIBRARY DESTINATION lib RUNTIME DESTINATION bin ) -install( - PROGRAMS scripts/dump scripts/config - DESTINATION lib/${PROJECT_NAME}/ - ) - install( DIRECTORY launch DESTINATION share/${PROJECT_NAME}/ @@ -115,15 +165,6 @@ install( DESTINATION share/${PROJECT_NAME}/ ) -install( - DIRECTORY examples - DESTINATION share/${PROJECT_NAME}/ - PATTERN "examples/*.py" - PERMISSIONS OWNER_EXECUTE OWNER_WRITE OWNER_READ - GROUP_EXECUTE GROUP_READ - WORLD_EXECUTE WORLD_READ - ) - install( DIRECTORY config DESTINATION share/${PROJECT_NAME}/ diff --git a/Dockerfile b/Dockerfile index 00443c2..cdc85ee 100644 --- a/Dockerfile +++ b/Dockerfile @@ -26,17 +26,12 @@ RUN apt-get update && apt-get install -y \ coreutils \ git \ jq \ - libcurl4-openssl-dev \ - libboost-all-dev \ - libgoogle-glog-dev \ - libgoogle-glog0v5 \ - libproj-dev \ libssl-dev \ - libxmlrpc-c++8-dev \ + libxmlrpc-c++8v5 \ wget \ && rm -rf /var/lib/apt/lists/* -# Update libcurl4 library for the dependancy of ifm3dAPI 1.4.3 & requires libcurl4 version 16 -RUN apt-get update && apt-get upgrade libcurl4 +# Update libcurl4 library for the dependancy of ifm3dAPI 1.5.3 & requires libcurl4 version 16 +RUN apt-get update && apt-get upgrade -y libcurl4 # Install ifm3d using the deb files RUN mkdir /home/ifm/ifm3d ADD https://github.com/ifm/ifm3d/releases/download/v${IFM3D_VERSION}/ifm3d-ubuntu-${UBUNTU_VERSION}-${ARCH}-debs_${IFM3D_VERSION}.tar /home/ifm/ifm3d @@ -51,10 +46,10 @@ RUN cd /home/ifm/colcon_ws && \ rosdep install --from-path src -y --ignore-src -t build # OPTION 2: clone and build from git repo, i.e. download specific repo branch during build process -# # Clone and build ifm3d-ros2 repo -# RUN mkdir -p /home/ifm/colcon_ws/src && \ -# cd /home/ifm/colcon_ws/src && \ -# git clone ${IFM3D_ROS2_REPO} -b ${IFM3D_ROS2_BRANCH} --single-branch +# Clone and build ifm3d-ros2 repo +#RUN mkdir -p /home/ifm/colcon_ws/src && \ +# cd /home/ifm/colcon_ws/src && \ +# git clone ${IFM3D_ROS2_REPO} -b ${IFM3D_ROS2_BRANCH} --single-branch SHELL ["/bin/bash", "-c"] RUN cd /home/ifm/colcon_ws && \ @@ -75,16 +70,14 @@ WORKDIR /home/ifm ARG DEBIAN_FRONTEND=noninteractive RUN apt-get update \ && apt-get install -y --no-install-recommends \ - libboost-all-dev \ - libgoogle-glog0v5 \ libssl-dev \ libxmlrpc-c++8v5 \ locales \ python3-rosdep \ && rm -rf /var/lib/apt/lists/* -# Update libcurl4 library for the dependancy of ifm3dAPI 1.4.3 & requires libcurl4 version 16 -RUN apt-get update && apt-get upgrade libcurl4 +# Update libcurl4 library for the dependancy of ifm3dAPI 1.5.3 & requires libcurl4 version 16 +RUN apt-get update && apt-get upgrade -y libcurl4 # Install ifm3d RUN cd /home/ifm/ifm3d &&\ diff --git a/README.md b/README.md index b5b65ee..bac2006 100644 --- a/README.md +++ b/README.md @@ -1,49 +1,49 @@ # ifm3d-ros2 overview *This documentation is formatted to be read on [www.ros2.ifm3d.com](https://ros2.ifm3d.com/latest/).* -:::{warning} -The ifm3d-ros2 package has had major changes recently. Please be aware that this might cause problems on your system for building pipelines based on our old build instructions. -::: - - :::{note} -This release is intended to be used with the O3R camera platform ONLY. For other ifm cameras (e.g. O3D3xx and O3X2xx) please see the tagged releases 0.3.0 and 0.7.0 respectively. +This release is intended to be used with the O3R camera platform ONLY. For other ifm cameras (e.g. O3D3xx and O3X1xx) please see the tagged releases 0.3.0 and 0.7.0 respectively. ::: -`ifm3d-ros2` is a wrapper around [ifm3d](https://github.com/ifm/ifm3d) enabling the usage of ifm O3R ToF camera platform from within [ROS 2](https://docs.ros.org/en/jazzy/index.html) software systems. +`ifm3d-ros2` is a wrapper around [ifm3d](https://github.com/ifm/ifm3d) enabling the usage of ifm O3R camera platform from within [ROS 2](https://index.ros.org/doc/ros2/) software systems. -![rviz](doc/figures/O3R_merged_point_cloud.png) +As of version 1.2.0, `ifm3d_ros2` also supports the Obstacle Detection Solution (ODS). Refer to [the ODS documentation](https://ifm3d.com/latest/ODS/index_ods.html) for more details on this application. + +![rviz](doc/camera_node/figures/O3R_merged_point_cloud.png) ## Software Compatibility Matrix -### Release versions +### Releases versions + +| `ifm3d_ros2` version | ifm3d version | O3R firmware version | ROS 2 distribution | Comment | +| -------------------- | -------------- | -------------------- | ------------------ | ---------------------------------- | +| 1.2.0 | 1.4.3 to 1.5.3 | 1.4.30 | Jazzy, Humble | Added support for ODS applications | +| 1.1.0 | 1.2.6 | 1.0.14 | Humble | | +| 1.0.1 | 0.93.0 | 0.14.23 | Foxy | | + +> Note: The version numbers listed above for the ifm3d API or the O3R firmware versions are the ones explicitly tested. Any other version might work but is not officially supported. -| ifm3d_ros2 version | ifm3d version | (O3R) embedded FW versions | ROS 2 distribution | -| ------------------ | ------------- | -------------------------- | ------------------ | -| 1.1.0 | 1.4.3 | 1.0.14, 1.1.x, 1.4.x | Humble | -| 1.0.1 | 0.93.0 | 0.14.23 | Foxy | +### Known Issues + +For a complete list of changes, refer to [the changelog](./CHANGELOG.rst). + +Below is a list of all known issues with the latest released version: +- There is an issue in how the intrinsic parameters are calculated: see [issue 18](https://github.com/ifm/ifm3d-ros2/issues/18). +- The `camera_info` topic does not work for rectification: see [issue 24](https://github.com/ifm/ifm3d-ros2/issues/24). + +### Deprecated ifm3d-ros2 versions -### Deprecated ifm3d-ros2 Versions The following versions are deprecated and no longer supported. -| ifm3d_ros2 version | ifm3d version | ROS 2 distribution | -| ------------------ | ------------- | ------------------ | -| 1.0.1 DEPRECATED | 0.93.0 | Galactic | -| 1.0.0 DEPRECATED | 0.92.0 | Galactic | -| 0.3.0 DEPRECATED | 0.17.0 | Dashing, Eloquent | -| 0.2.0 DEPRECATED | 0.12.0 | Dashing | -| 0.1.1 DEPRECATED | 0.12.0 | Dashing | -| 0.1.0 DEPRECATED | 0.12.0 | Dashing | - -## ToDo - -We are currently working on rounding out the feature set of our ROS2 interface. Our current objectives are to get the feature set to an equivalent -level to that of our ROS1 interface and to tune the ROS2/DDS performance to optimize the usage of our cameras from within ROS2 system (for different DDS implementations). -Thanks for your patience as we continue to ensure our ROS2 interface is feature-rich, robust, and performant. Your feedback is greatly appreciated. - -## Known Issues -+ Installing ifm3d API with it's default runtime libs may result in multiple versions of glog on the system: this results in a compilable but non-functional ROS node. -Please either use the glog version as included in your Ubuntu release (if compatible) or uninstall any incompatible lib version before installing the ifm3d required version. -We are working on providing a fix to the underlying API which removes the GLOG logging from the API. +| `ifm3d_ros2` version | ifm3d version | ROS 2 distribution | +| -------------------- | ------------- | ------------------ | +| 1.0.1 DEPRECATED | 0.93.0 | Galactic | +| 1.0.0 DEPRECATED | 0.92.0 | Galactic | +| 0.3.0 DEPRECATED | 0.17.0 | Dashing, Eloquent | +| 0.2.0 DEPRECATED | 0.12.0 | Dashing | +| 0.1.1 DEPRECATED | 0.12.0 | Dashing | +| 0.1.0 DEPRECATED | 0.12.0 | Dashing | + + ## LICENSE Please see the file called [LICENSE](LICENSE). diff --git a/build_container.sh b/build_container.sh index f8e7acb..45ee236 100755 --- a/build_container.sh +++ b/build_container.sh @@ -6,23 +6,23 @@ set -euo pipefail ############## ARCH="amd64" BASE_IMAGE="ros" -TAG=ifm3d-ros:humble-amd64 +TAG=ifm3d-ros:jazzy-amd64 ############## # For ARM64V8: ############## -# ARCH="arm64" -# BASE_IMAGE="arm64v8/ros" -# TAG=ifm3d-ros:humble-arm64_v8 +#ARCH="arm64" +#BASE_IMAGE="arm64v8/ros" +#TAG=ifm3d-ros:jazzy-arm64_v8 ############## # Arguments common for both architecture ############## -BUILD_IMAGE_TAG="humble" -FINAL_IMAGE_TAG="humble-ros-core" -IFM3D_VERSION="1.2.6" +BUILD_IMAGE_TAG="jazzy" +FINAL_IMAGE_TAG="jazzy-ros-core" +IFM3D_VERSION="1.5.3" IFM3D_ROS2_REPO="https://github.com/ifm/ifm3d-ros2.git" -IFM3D_ROS2_BRANCH="lm_humble_tests" +IFM3D_ROS2_BRANCH="v1.2.0" UBUNTU_VERSION="22.04" docker build -t $TAG \ diff --git a/config/camera_default_parameters.yaml b/config/camera_default_parameters.yaml index db6821d..4d33d17 100644 --- a/config/camera_default_parameters.yaml +++ b/config/camera_default_parameters.yaml @@ -5,7 +5,6 @@ ros__parameters: buffer_id_list: - CONFIDENCE_IMAGE - - DIAGNOSTIC - EXTRINSIC_CALIB - INTRINSIC_CALIB - INVERSE_INTRINSIC_CALIBRATION @@ -18,14 +17,10 @@ ip: 192.168.0.69 pcic_port: 50010 tf: - cloud_link: - frame_name: "camera_cloud_link" - publish_transform: true - mounting_link: - frame_name: "camera_mounting_link" - optical_link: - frame_name: "camera_optical_link" - publish_transform: true - transform: [0.0, 0.0, 0.0, 0.0, 0.0, 0.0] + base_frame_name: "ifm_base_link" + mounting_frame_name: "camera_mounting_link" + optical_frame_name: "camera_optical_link" + publish_base_to_mounting: true + publish_mounting_to_optical: true use_sim_time: false xmlrpc_port: 80 diff --git a/config/examples/o3r_2d.yaml b/config/examples/o3r_2d.yaml index fb1c8fe..d975f2f 100644 --- a/config/examples/o3r_2d.yaml +++ b/config/examples/o3r_2d.yaml @@ -4,7 +4,6 @@ /ifm3d/camera_2d: ros__parameters: buffer_id_list: - - DIAGNOSTIC - JPEG_IMAGE - RGB_INFO frame_latency_thresh: 1.0 @@ -14,5 +13,11 @@ sync_clocks: false timeout_millis: 500 timeout_tolerance_secs: 5.0 + tf: + base_frame_name: "ifm_base_link" + mounting_frame_name: "camera_2d_mounting_link" + optical_frame_name: "camera_2d_optical_link" + publish_base_to_mounting: true + publish_mounting_to_optical: true use_sim_time: false xmlrpc_port: 80 diff --git a/config/examples/o3r_3d.yaml b/config/examples/o3r_3d.yaml index a9a80ec..4e919d7 100644 --- a/config/examples/o3r_3d.yaml +++ b/config/examples/o3r_3d.yaml @@ -5,10 +5,10 @@ ros__parameters: buffer_id_list: - CONFIDENCE_IMAGE - - DIAGNOSTIC - EXTRINSIC_CALIB - NORM_AMPLITUDE_IMAGE - RADIAL_DISTANCE_IMAGE + - TOF_INFO - XYZ frame_latency_thresh: 1.0 ip: 192.168.0.69 @@ -19,3 +19,9 @@ timeout_tolerance_secs: 5.0 use_sim_time: false xmlrpc_port: 80 + tf: + base_frame_name: "ifm_base_link" + mounting_frame_name: "camera_3d_mounting_link" + optical_frame_name: "camera_3d_optical_link" + publish_base_to_mounting: true + publish_mounting_to_optical: true diff --git a/config/examples/two_o3r_heads.yaml b/config/examples/two_o3r_heads.yaml index 3ab7003..1fff14f 100644 --- a/config/examples/two_o3r_heads.yaml +++ b/config/examples/two_o3r_heads.yaml @@ -5,7 +5,6 @@ /ifm3d/left_camera_2d: ros__parameters: buffer_id_list: - - DIAGNOSTIC - JPEG_IMAGE - RGB_INFO frame_latency_thresh: 1.0 @@ -15,6 +14,12 @@ sync_clocks: false timeout_millis: 500 timeout_tolerance_secs: 5.0 + tf: + base_frame_name: "ifm_base_link" + mounting_frame_name: "left_camera_2d_mounting_link" + optical_frame_name: "left_camera_2d_optical_link" + publish_base_to_mounting: true + publish_mounting_to_optical: true use_sim_time: false xmlrpc_port: 80 @@ -22,10 +27,10 @@ ros__parameters: buffer_id_list: - CONFIDENCE_IMAGE - - DIAGNOSTIC - EXTRINSIC_CALIB - NORM_AMPLITUDE_IMAGE - RADIAL_DISTANCE_IMAGE + - TOF_INFO - XYZ frame_latency_thresh: 1.0 ip: 192.168.0.69 @@ -34,13 +39,18 @@ sync_clocks: false timeout_millis: 500 timeout_tolerance_secs: 5.0 + tf: + base_frame_name: "ifm_base_link" + mounting_frame_name: "left_camera_3d_mounting_link" + optical_frame_name: "left_camera_3d_optical_link" + publish_base_to_mounting: true + publish_mounting_to_optical: true use_sim_time: false xmlrpc_port: 80 /ifm3d/right_camera_2d: ros__parameters: buffer_id_list: - - DIAGNOSTIC - JPEG_IMAGE - RGB_INFO frame_latency_thresh: 1.0 @@ -50,6 +60,12 @@ sync_clocks: false timeout_millis: 500 timeout_tolerance_secs: 5.0 + tf: + base_frame_name: "ifm_base_link" + mounting_frame_name: "right_camera_2d_mounting_link" + optical_frame_name: "right_camera_2d_optical_link" + publish_base_to_mounting: true + publish_mounting_to_optical: true use_sim_time: false xmlrpc_port: 80 @@ -57,17 +73,23 @@ ros__parameters: buffer_id_list: - CONFIDENCE_IMAGE - - DIAGNOSTIC - EXTRINSIC_CALIB - NORM_AMPLITUDE_IMAGE - RADIAL_DISTANCE_IMAGE + - TOF_INFO - XYZ frame_latency_thresh: 1.0 ip: 192.168.0.69 password: "" - pcic_port: 50013 # 3D stream of left camera connected to Port 3 + pcic_port: 50013 # 3D stream of right camera connected to Port 3 sync_clocks: false timeout_millis: 500 timeout_tolerance_secs: 5.0 + tf: + base_frame_name: "ifm_base_link" + mounting_frame_name: "right_camera_3d_mounting_link" + optical_frame_name: "right_camera_3d_optical_link" + publish_base_to_mounting: true + publish_mounting_to_optical: true use_sim_time: false xmlrpc_port: 80 diff --git a/config/ods_default_parameters.yaml b/config/ods_default_parameters.yaml new file mode 100644 index 0000000..12563ad --- /dev/null +++ b/config/ods_default_parameters.yaml @@ -0,0 +1,15 @@ +# Example configuration for an ODS application with two cameras. +# Expected to be used with ods.launch.py. + +# By default, this file expects two cameras with 3D ports connected to +# ports 2 and 3, and one ods application, "app0". + +/ifm3d/ods: + ros__parameters: + config_file: "config/examples/o3r_ods.json" + ip: 192.168.0.69 + pcic_port: 51010 + ods: + frame_id: "ifm_base_link" + publish_occupancy_grid: true + publish_costmap: false \ No newline at end of file diff --git a/config/param1.yml b/config/param1.yml deleted file mode 100644 index b87acde..0000000 --- a/config/param1.yml +++ /dev/null @@ -1,9 +0,0 @@ -################################################################################### -# # -# This parameter file is deprecated and will be removed in future distributions. # -# # -################################################################################### - -/ifm3d_ros2/camera1: - ros__parameters: - pcic_port: 50011 diff --git a/config/params.yaml b/config/params.yaml deleted file mode 100644 index 805c51a..0000000 --- a/config/params.yaml +++ /dev/null @@ -1,29 +0,0 @@ -################################################################################### -# # -# This parameter file is deprecated and will be removed in future distributions. # -# # -################################################################################### - -/ifm3d_ros2/camera0: - ros__parameters: - pcic_port: 50010 - -/ifm3d_ros2/camera1: - ros__parameters: - pcic_port: 50011 - -/ifm3d_ros2/camera2: - ros__parameters: - pcic_port: 50012 - -/ifm3d_ros2/camera3: - ros__parameters: - pcic_port: 50013 - -/ifm3d_ros2/camera4: - ros__parameters: - pcic_port: 50014 - -/ifm3d_ros2/camera5: - ros__parameters: - pcic_port: 50015 diff --git a/config/params_2cams.yaml b/config/params_2cams.yaml deleted file mode 100644 index 353a844..0000000 --- a/config/params_2cams.yaml +++ /dev/null @@ -1,13 +0,0 @@ -################################################################################### -# # -# This parameter file is deprecated and will be removed in future distributions. # -# # -################################################################################### - -/ifm3d_ros2/camera0: - ros__parameters: - pcic_port: 50010 - -/ifm3d_ros2/camera1: - ros__parameters: - pcic_port: 50012 diff --git a/doc/building.md b/doc/building.md index 979d5de..c5fba4f 100644 --- a/doc/building.md +++ b/doc/building.md @@ -3,7 +3,8 @@ ## Prerequisites ### Ubuntu and ROS -We suggest building the `ifm3d-ros2` node on top of [Ubuntu 22.04 Jammy Jellyfish](https://releases.ubuntu.com/jammy/) and [ROS Humble](https://docs.ros.org/en/humble/index.html). +We suggest building the `ifm3d-ros2` node on top of [Ubuntu 22.04 Jammy Jellyfish](https://releases.ubuntu.com/jammy/) and [ROS Humble](https://docs.ros.org/en/humble/index.html) or +[Ubuntu 24.04 Noble Numbat](https://releases.ubuntu.com/noble/) and [ROS Jazzy](https://docs.ros.org/en/jazzy/index.html) ### ifm3d C++ API The ROS node `ifm3d_ros2` requires the C++ API ifm3d to be installed locally for your system before compiling and running the ROS node. @@ -11,29 +12,18 @@ Refer to [the compatibility matrix](../README.md) to find out the correct ifm3d Follow these instructions on how to install `ifm3d` (we recommend using the pre-built package): [install ifm3d](https://api.ifm3d.com/stable/content/installation_instructions/install_linux_binary.html). -### Testing prerequisite - -These two packages are only required for testing but not at runtime: -- launch_testing -- launch_testing_ament_cmake - -On debian based systems they may be installed as follows: -``` -$ sudo apt install ros-${ROS_DISTRO}-launch-testing ros-${ROS_DISTRO}-launch-testing-ament-cmake -``` -:::{note} -The tests are currently a work in progress. -::: ## Build and install `ifm3d-ros2` -Building and installing ifm3d-ros2 is accomplished by utilizing the ROS2 [colcon](https://colcon.readthedocs.io/en/released/) tool. +Building and installing `ifm3d-ros2` is accomplished by utilizing the ROS 2 [colcon](https://colcon.readthedocs.io/en/released/) tool. ### Installation directory -First, we need to decide where we want our software to be installed. For purposes of this document, we will assume that we will install our ROS packages at `~/colcon_ws/src`. +First, we need to decide where we want our software to be installed. +For purposes of this document, we will assume that we will install our ROS packages at `~/colcon_ws/`. :::{note} Below we assume `humble`. Adapting to other ROS distributions is left as an exercise for the reader. ::: + ### Colcon workspace Next, we want to create a _colcon workspace_ that we can use to build and install that code from. @@ -42,9 +32,11 @@ $ mkdir -p ~/colcon_ws/src ``` ### Get the `ifm3d-ros2` code from GitHub -Next, we need to get the code from GitHub. Please adapt the commands when not following the suggested directory structure: `~/colcon_ws/src/` + +Next, we need to get the code from GitHub. Please adapt the commands when not following the suggested directory structure. ```bash +$ source /opt/ros/humble/setup.bash $ cd ~/colcon_ws/src $ git clone https://github.com/ifm/ifm3d-ros2.git $ git checkout # Replace the targeted version @@ -61,7 +53,8 @@ Build your workspace: ```bash $ cd ~/colcon_ws/ -$ colcon build --cmake-args -DBUILD_TESTING=OFF +$ rosdep install --from-paths src --ignore-src -r -y +$ colcon build Starting >>> ifm3d_ros2 Finished <<< ifm3d_ros2 [17.6s] @@ -72,9 +65,9 @@ Summary: 1 package finished [17.8s] The tests are not functional at the moment. ::: -To confirm that the node is functional, try [launching it](../doc/launch.md) and inspecting [the published topics](../doc/topics.md). +To confirm that the node is functional, try [launching it](camera_node/launch.md) and inspecting [the published topics](camera_node/topics.md). ## Docker containers We provide a Dockerfile build instruction for building the ROS2 node inside a Docker container. -For reference and for deployment of the ROS node via a Docker software Docker container see the [Dockerfile](../Dockerfile). \ No newline at end of file +For reference and for deployment of the ROS node via a Docker container see the [Dockerfile](../Dockerfile). \ No newline at end of file diff --git a/doc/camera_node/camera_transforms.md b/doc/camera_node/camera_transforms.md new file mode 100644 index 0000000..e69de29 diff --git a/doc/figures/O3R_merged_point_cloud.png b/doc/camera_node/figures/O3R_merged_point_cloud.png similarity index 100% rename from doc/figures/O3R_merged_point_cloud.png rename to doc/camera_node/figures/O3R_merged_point_cloud.png diff --git a/doc/figures/rviz_sample.png b/doc/camera_node/figures/rviz_sample.png similarity index 100% rename from doc/figures/rviz_sample.png rename to doc/camera_node/figures/rviz_sample.png diff --git a/doc/camera_node/figures/transforms-1.png b/doc/camera_node/figures/transforms-1.png new file mode 100644 index 0000000000000000000000000000000000000000..947082fa6565ade1b9ea892107d5a8b0863798f4 GIT binary patch literal 35858 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 n|F5&ONo-y;;VaMZ;GR@Fy>=Bvm0{81#PtjpxLf literal 0 HcmV?d00001 diff --git a/doc/camera_node/index_camera_node.md b/doc/camera_node/index_camera_node.md new file mode 100644 index 0000000..9a9d1b2 --- /dev/null +++ b/doc/camera_node/index_camera_node.md @@ -0,0 +1,11 @@ +# Camera node + +The `ifm3d-ros2` package provides a camera node, which is the main interface for getting RGB or 3D data, and configuring an O3R platform. + +:::{toctree} + :maxdepth: 2 +Launch +Topics +Parameters +Multi-camera applications +::: \ No newline at end of file diff --git a/doc/launch.md b/doc/camera_node/launch.md similarity index 85% rename from doc/launch.md rename to doc/camera_node/launch.md index a49c3fa..3ad7862 100644 --- a/doc/launch.md +++ b/doc/camera_node/launch.md @@ -2,14 +2,17 @@ Launch the camera node (assuming you are in `~/colcon_ws/`): ``` -$ . install/setup.bash +$ source install/setup.bash $ ros2 launch ifm3d_ros2 camera.launch.py ``` -This will launch a `/ifm3d/camera/` node with default arguments, i.e. utilize the defaults yml configuration `camera_default_parameters.yaml`. +This will launch the `/ifm3d/camera/` node with default arguments, using the default YAML configuration file `camera_default_parameters.yaml`. Depending on the specified `pcic_port`, the node will initialize either a 3D camera (if the port corresponds to a 3D camera) or a 2D camera. + +The respective node information should look like this: -The respective node information should look like this: `ros2 node info /ifm3d/camera` ``` +$ ros2 node info /ifm3d/camera + /ifm3d/camera Subscribers: /parameter_events: rcl_interfaces/msg/ParameterEvent @@ -64,6 +67,6 @@ $ ros2 launch ifm3d_ros2 camera.launch.py visualization:=true ``` -![rviz1](figures/O3R_merged_point_cloud.png) +![rviz1](./figures/O3R_merged_point_cloud.png) Congratulations! You can now have complete control over the O3R perception platform from inside ROS2. diff --git a/doc/camera_node/multi_head.md b/doc/camera_node/multi_head.md new file mode 100644 index 0000000..97ddff8 --- /dev/null +++ b/doc/camera_node/multi_head.md @@ -0,0 +1,64 @@ +# Multi head launch file configuration + +It is possible to stream data from multiple cameras or from the two ports of one camera at once. +This can be achieved by setting the camera-specific parameters in the launch configuration files, namely `pcic-port` and `buffer_id_lists`. +We provide examples of default parameters for getting the 2D or 3D data from one camera, or all the data from two full O3R camera (two 2D data streams and two 3D data streams). + +## `o3r_2d.yaml` +This example configuration connects to the 2D (RGB) data stream of one O3R camera head connected to port 0: + +The ports on the VPU should be connected as follows: +* Camera 2D: physical port 0 - corresponds to `pcic_port=50010` + +To launch this example, use the following command: +```bash +ros2 launch ifm3d_ros2 camera.launch.py camera_name:=camera_2d parameter_file_name:=examples/o3r_2d.yaml +``` +> **Note:** Don’t forget to specify the `camera_name` parameter if it differs from the default value, "camera", and the `camera_namespace` if it differs from the default, "ifm3d". + +## `o3r_3d.yaml` +This example configuration connects to the 3D data stream of one O3R camera head connected to port 2: + +The ports on the VPU should be connected as follows: +* Camera 3D: physical port 2 - corresponds to `pcic_port=50012` + +To launch this example, use the following command: +```bash +ros2 launch ifm3d_ros2 camera.launch.py camera_name:=camera_3d parameter_file_name:=examples/o3r_3d.yaml +``` +> **Note:** Don’t forget to specify the `camera_name` parameter if it differs from the default value, "camera", and the `camera_namespace` if it differs from the default, "ifm3d". + +## Launching a 2D and a 3D node simultaneously +This example configuration connects to one 3D data stream **AND** one 2D (RGB) data stream connected to ports 0 and 2: + +- Camera 2D: physical port 0 - corresponds to `pcic_port=50010` +- Camera 3D: physical port 2 - corresponds to `pcic_port=50012` + +To launch this example, use the following command: +```bash +ros2 launch ifm3d_ros2 example_o3r_2d_and_3d.launch.py +``` + + +## `two_o3r_heads.yaml` +This example configuration connects to two 3D data stream **AND** two 2D (RGB) data stream of **two** O3R camera head connected to ports 0 - 3: + +The ports on the VPU should be connected as follows: +Camera head 1: +* Camera 2D: physical port 0 - corresponds to `pcic_port=50010` +* Camera 3D: physical port 2 - corresponds to `pcic_port=50012` + +Camera head 2: +* Camera 2D: physical port 1 - corresponds to `pcic_port=50011` +* Camera 3D: physical port 3 - corresponds to `pcic_port=50013` + +To launch this example, use the following command: +```bash +ros2 launch ifm3d_ros2 example_two_o3r_heads.launch.py parameter_file_name:=two_o3r_heads.yaml +``` + +## Adapting the camera configuration to your needs +The example configurations are provided in the directory `config/examples`. +They can act as inspiration when configuring your own multi-camera setup. + +To adjust the configuration to your own setup, use these configuration files as a base to add or remove cameras, change the port number or edit the list of requested buffers. \ No newline at end of file diff --git a/doc/camera_node/parameters.md b/doc/camera_node/parameters.md new file mode 100644 index 0000000..aa71073 --- /dev/null +++ b/doc/camera_node/parameters.md @@ -0,0 +1,40 @@ +# Parameters + +| Name | Data Type | Default Value | Description | +| ---------------------------------- | ------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ | +| `~/buffer_id_list` | string array | `{"CONFIDENCE_IMAGE", "EXTRINSIC_CALIB", "NORM_AMPLITUDE_IMAGE", "RADIAL_DISTANCE_IMAGE", "TOF_INFO", "XYZ"}` for the TOF cameras and `{"JPEG_IMAGE", "RGB_INFO"}` for the RGB cameras. | List of buffer_id strings denoting the wanted buffers. | +| `~/config_file` | string | `""` | Path to a JSON configuration file to be used when configuring the node. | +| `~/ip` | string | 192.168.0.69 | The ip address of the camera. | +| `~/log_level` | string | warning | ifm3d-ros2 node logging level. | +| `~/pcic_port` | int | 50010 | PCIC port corresponding to the targeted camera port. | +| `~/tf.base_frame_name` | string | `ifm_base_link` | Name for ifm reference frame. | +| `~/tf.mounting_frame_name` | string | `_mounting_link` | Name for the mounting point frame. | +| `~/tf.optical_frame_name` | string | `_optical_link` | Name for the optical frame. | +| `~/tf.publish_base_to_mounting` | bool | true | Whether the transform from the ifm base link to the camera mounting point should be published. | +| `~/tf.publish_mounting_to_optical` | bool | true | Whether the transform from the cameras mounting point to the optical center should be published. | +| `~/xmlrpc_port` | uint | 50010 | TCP port the on-camera xmlrpc server is listening on. | + +## Details on the published transforms + +The `ifm3d-ros2` node can publish two transforms: +* one from the `ifm_base_link` to the `mounting_link`, according to the camera calibration (either via JSON or via the ifm Vision Assistant GUI), +* and one from the `mounting_link` the `optical_link`, according to the manufacturing parameters. + +The publication of these transform can be deactivated via parameter. +The names for all three links can be changed via node parameters. + +To clarify which frame each of these transform refers to, consider the drawings below: + +![Description of the base, mounting and optical frames](./figures/transforms-1.png) + + +The reference of the mounting frame is at the back of the O3R camera head housing (scale drawings can be found on ifm.com in the download section of the specific article). +The reference for the ifm base frame is defined by the extrinsic parameters set in the JSON configuration of the O3R platform (`extrinsicHeadToUser`). +Generally, the ifm base frame is configured to have for origin the center of the robot coordinate system, but this is not required. +When no extrinsic parameter is set, the ifm base frame and the mounting frame are the same. + +![Focused description of the optical and mounting frames](./figures/transforms-2.png) + +The optical frame refers to the reference point of the optical system (lens, chip, etc). +A static transform is published between the mounting link and the optical link, that corresponds to the intrinsic calibration parameters of the camera. +Each set of intrinsic parameters is unique to a specific camera head and set in production. These parameters are not expected to change over time. \ No newline at end of file diff --git a/doc/camera_node/topics.md b/doc/camera_node/topics.md new file mode 100644 index 0000000..f8baa4e --- /dev/null +++ b/doc/camera_node/topics.md @@ -0,0 +1,26 @@ +# Topics + +## Published Topics + +| Name | Data Type | Description | +| ------------------------------- | --------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- | +| `amplitude` | `sensor_msgs/msg/Image` | The normalized amplitude image | +| `cloud` | `sensor_msgs/msg/PointCloud2` | The point cloud data | +| `confidence` | `sensor_msgs/msg/Image` | The confidence image | +| `distance` | `sensor_msgs/msg/Image` | The radial distance image | +| `rgb` | `sensor_msgs/msg/Image` | The RGB 2D image of the 2D imager | +| `extrinsics` | `ifm3d_ros2::msg::Extrinsics` | The extrinsic calibration of the camera (camera to world) | +| `intrinsic_calib` | `ifm3d_ros2::msg::Intrinsics` | The intrinsic calibration of the camera (optical system parameters) | +| `inverse_intrinsic_calibration` | `ifm3d_ros2::msg::InverseIntrinsics` | The inverse intrinsic calibration of the camera | +| `camera_info` | `sensor_msgs::msg::CameraInfo` | The camera info topic containing the distortion model. This topic is published if the `INTRINSIC_CALIB` is part of the buffer list for the 3D cameras, or if the `RGB_INFO` is part of the buffer list for the RGB cameras. | +| `tof_info` | `ifm3d_ros2::msg::TOFInfo` | A topic gathering various information from the tof camera (see [TOFinfo.msg](../msg/TOFInfo.msg)) | +| `rgb_info` | `ifm3d_ros2::msg::RGBInfo` | A topic gathering various information from the rgb camera (see [RGBInfo.msg](../msg/RGBInfo.msg)) | +| `diagnostics` | `diagnostic_msgs::msg::DiagnosticArray` | Diagnostic messages pulled from the device every second | + +:::{note} +All the topics are published with QoS `ifm3d_ros2::LowLatencyQoS`. +::: + +## Subscribed Topics + +None. diff --git a/doc/deployment.md b/doc/deployment.md index b4a7c1f..6c406ed 100644 --- a/doc/deployment.md +++ b/doc/deployment.md @@ -4,11 +4,13 @@ Follow these steps to get our supplied Docker container to run on your system. :::{note} The instructions below apply to ROS2 Humble. Please change the commands to suit your ROS 2 distribution. ::: + ## Build the docker container -We provide a Dockerfile and a build script to help you build a docker container with ifm3d and ifm3d-ros2. To use it, check out the `build_container.sh` script. Open it up and adjust the arguments to suit your target architecture (arm64v8 or amd64) and the targeted ifm3d and ifm3d-ros2 version. Then you can build the container: + +We provide a Dockerfile and a build script to help you build a docker container with `ifm3d` and `ifm3d-ros2`. To get started, check out the [`build_container.sh`](../build_container.sh) script. Open it up and adjust the arguments to suit your target architecture (arm64v8 or amd64) and the targeted `ifm3d` and `ifm3d-ros2` version. Then you can build the container: ```bash -$ ./build_humble.sh +$ ./build_container.sh [+] Building 675.3s (23/23) FINISHED => [internal] load build definition from Dockerfile 0.0s => => transferring dockerfile: 2.92kB 0.0s @@ -46,6 +48,7 @@ To deploy the container on to the VPU, or use the container locally to interact Resources on the OVP8xx are limited and shared between all the running processes. We recommend assigning the Docker process to specific cores so as not to interfere with other applications. Refer to [the resource management documentation on ifm3d.com](https://ifm3d.com/latest/SoftwareInterfaces/Docker/cpu.html). ## Distributed setup + It is possible to run a complete ROS system in a distributed way. In this section we provide instructions to run ifm3d-ros2 in a container deployed on the VPU (primary container), and the visualization locally on a laptop (secondary system). These instructions can be adapted to suit other architectural designs. @@ -55,7 +58,9 @@ These instructions can be adapted to suit other architectural designs. ```bash $ docker save docker.io/library/ifm3d-ros:humble-arm64_v8 | ssh -C oem@192.168.0.69 docker load ``` + - SSH to the VPU and run the container: +- ```bash $ ssh oem@192.168.0.69 #Adapt to the IP address of your VPU o3r-vpu-c0$ docker run -ti --net=host ifm3d-ros:humble-arm64_v8 @@ -82,7 +87,7 @@ root@62b0c2e120bb:/home/ifm/$ ros2 launch ifm3d_ros2 camera.launch.py parameter 1. Source ROS2 on the secondary machine: ```bash -$ source /opt/ros/galactic/setup.bash +$ source /opt/ros/humble/setup.bash ``` - Check that ROS topics are available (on `ROS_DOMAIN_ID=0`): ```bash diff --git a/doc/diagnostic.md b/doc/diagnostic.md new file mode 100644 index 0000000..e3eba9d --- /dev/null +++ b/doc/diagnostic.md @@ -0,0 +1,32 @@ +# Diagnostic + +Both the camera node and the ODS node publish diagnostic information to the `/diagnostic` topic. +The diagnostic message contains an error code and a message, referring directly to an error from ifm3d or from the embedded software. + +Below is an example of a diagnostic message: +```bash +$ ros2 topic echo /diagnostics +header: + stamp: + sec: 1652509745 + nanosec: 818232736 + frame_id: '' +status: +- level: "\x02" + name: '' + message: '' + hardware_id: /ifm3d/diag_module + values: + - key: bootid + value: '"71fd8e7d-385a-4f86-8a88-bb59f5112c73"' + - key: events + value: '[{"description":"Unable to determine velocity","id":105007,"name":"ERROR_ODSAPP_VELOCITY_UNAVAILABLE","source":"/applications/in...' + - key: timestamp + value: '1652509745818232736' + - key: version + value: '{"diagnostics":"0.0.11","euphrates":"1.34.226","firmware":"1.1.41.4507"}' +--- +``` + +For more details on the error codes and potential troubleshooting strategies, refer to the [O3R diagnostic documentation](../../../documentation/SoftwareInterfaces/ifmDiagnostic/index_diagnostic.md). + diff --git a/doc/figures/transforms-1.png b/doc/figures/transforms-1.png deleted file mode 100644 index f3278aba268b895fba79c9e6accd00d113ea29c0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 38061 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

R8W>W;uss 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ÈoSVk& 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 kC3p5;k){7Xf1BGOJ90v1K#)D$8=V{ut0wV!Z diff --git a/doc/figures/transforms-2.png b/doc/figures/transforms-2.png deleted file mode 100644 index 30a5460a4679ab5432497b4dad05eff2f44b047c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 24387 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 oQoz3$!+#(7H + Launch + Topics + Transforms + Parameters +::: \ No newline at end of file diff --git a/doc/ods_node/ods_configuration.md b/doc/ods_node/ods_configuration.md new file mode 100644 index 0000000..ca8ebd5 --- /dev/null +++ b/doc/ods_node/ods_configuration.md @@ -0,0 +1,38 @@ +# Configuring ODS + +:::{note} +ifm's Obstacle Detection Solution (ODS) is supported in `ifm3d-ros2` version 1.2.0 and above. +::: + +ODS is an application that runs on the O3R platform, that provides the ability to detect the position of obstacles within the camera field of view. +For more details on ODS, refer to [the ODS documentation on ifm3d.com](https://ifm3d.com/latest/ODS/index_ods.html). + +## Using the Vision Assistant + +There are multiple ways to create and configure an ODS application. +We typically recommend to use the ifm Vision Assistant, which is ifm's official GUI for interfacing with all the vision products. + +To get started with ODS in the Vision Assistant, refer to [the ODS getting started documentation](https://ifm3d.com/latest/ODS/getting_started.html). +After following these instructions, you will have set up and properly configured an ODS application, and you will be ready to launch the ODS node. + +## Using the `Config` service + +It is also possible to configure an application directly using the `ifm3d-ros2` service `Config`. +We recommend to use this method when re-configuring the application at runtime, for example to change the active ports. + +In this case, you can use the following command. +This will switch the `activePort` parameter of the application `app0` to `"port3"`. +```bash +$ ros2 service call /ifm3d/ods/Config ifm3d_ros2/srv/Config "{json: '{\"applications\":{\"instances\":{\"app0\":{\"configuration\":{\"activePorts\":[\"port3\"]}}}}}'}" +requester: making request: ifm3d_ros2.srv.Config_Request(json='{"applications":{"instances":{"app0":{"configuration":{"activePorts":["port3"]}}}}}') + +response: +ifm3d_ros2.srv.Config_Response(status=0, msg='OK') + +``` + +## Using the `config_file` parameter + +Additionally, it is possible to configure an application (or any other aspect of the system) using a configuration file, provided through the `config_file` parameter. +This configuration file will be used to set the configuration in the `on_configuration` transition of the node. +To re-configure a node using a new configuration file, the `on_configuration` transition has to be triggered again. \ No newline at end of file diff --git a/doc/ods_node/ods_launch.md b/doc/ods_node/ods_launch.md new file mode 100644 index 0000000..4a15a5f --- /dev/null +++ b/doc/ods_node/ods_launch.md @@ -0,0 +1,88 @@ +# Launch the ODS node + +To launch the ODS node, you can use the provided launchfile `ods.launch.py`: +```bash +$ ros2 launch ifm3d_ros2 ods.launch.py +[INFO] [launch]: Default logging verbosity is set to INFO +[INFO] [ods_standalone-1]: process details: cmd='/home/usmasslo/ROS/ROS2/colcon_ws/install/ifm3d_ros2/lib/ifm3d_ros2/ods_standalone --ros-args --log-level info --ros-args -r __node:=ods -r __ns:=/ifm3d --params- +file ~/colcon_ws/install/ifm3d_ros2/share/ifm3d_ros2/config/ods_default_parameters.yaml', cwd='None', custom_env?=True +[INFO] [ods_standalone-1]: process started with pid [486398] +[ods_standalone-1] [INFO] [1728931867.359101479] [ifm3d.ods]: namespace: /ifm3d +[ods_standalone-1] [INFO] [1728931867.359179144] [ifm3d.ods]: node name: ods +[ods_standalone-1] [INFO] [1728931867.359186657] [ifm3d.ods]: middleware: rmw_fastrtps_cpp +[ods_standalone-1] [INFO] [1728931867.359190490] [ifm3d.ods]: Declaring parameters... +[ods_standalone-1] [INFO] [1728931867.359323428] [ifm3d.ods]: After the parameters declaration +[ods_standalone-1] [INFO] [1728931867.359406942] [ifm3d.ods]: node created, waiting for `configure()`... +[ods_standalone-1] [INFO] [1728931867.521751135] [ifm3d.ods]: on_configure(): unconfigured -> configuring +[ods_standalone-1] [INFO] [1728931867.521862570] [ifm3d.ods]: Parsing parameters... [ods_standalone-1] [INFO] [1728931867.521944451] [ifm3d.ods]: ip: 192.168.0.69 +[ods_standalone-1] [INFO] [1728931867.521980451] [ifm3d.ods]: pcic_port: 51010 +[ods_standalone-1] [INFO] [1728931867.521997881] [ifm3d.ods]: xmlrpc_port: 80 +[ods_standalone-1] [INFO] [1728931867.522012697] [ifm3d.ods]: Parameters parsed. +[ods_standalone-1] [INFO] [1728931867.522028686] [ifm3d.ods]: Adding callbacks to handle parameter changes at runtime... +[ods_standalone-1] [INFO] [1728931867.525109429] [ifm3d.ods]: Callbacks set. +[ods_standalone-1] [INFO] [1728931867.525177112] [ifm3d.ods]: Initializing Device +[ods_standalone-1] [INFO] [1728931867.525325450] [ifm3d.ods]: Initializing FrameGrabber for data +[ods_standalone-1] [INFO] [1728931868.040227637] [ifm3d.ods]: Creating OdsModule... +[ods_standalone-1] [INFO] [1728931868.040519950] [ifm3d.ods]: FunctionModule contructor called. +[ods_standalone-1] [INFO] [1728931868.040545387] [ifm3d.ods]: OdsModule contructor called. +[ods_standalone-1] [INFO] [1728931868.040638930] [ifm3d.ods]: OdsModule created. +[ods_standalone-1] [INFO] [1728931868.040644930] [ifm3d.ods]: Creating DiagModule... +[ods_standalone-1] [INFO] [1728931868.040675690] [ifm3d.ods]: FunctionModule contructor called. +[ods_standalone-1] [INFO] [1728931868.040736808] [ifm3d.ods]: hardware_id: /ifm3d/diag_module +[ods_standalone-1] [INFO] [1728931868.040756934] [ifm3d.ods]: DiagModule created. +[ods_standalone-1] [INFO] [1728931868.040803035] [ifm3d.ods]: OdsModule: on_configure called +[ods_standalone-1] [INFO] [1728931868.055527902] [ifm3d.ods]: Parameter ods.frame_id set to 'ods_frame' +[ods_standalone-1] [INFO] [1728931868.055542337] [ifm3d.ods]: DiagModule: on_configure called +[ods_standalone-1] [INFO] [1728931868.057683191] [ifm3d.ods]: Creating BaseServices... +[2] +[ods_standalone-1] [INFO] [1728931868.057733035] [ifm3d.ods]: BaseServices constructor called. +[ods_standalone-1] [INFO] [1728931868.060201097] [ifm3d.ods]: Services created; +[ods_standalone-1] [INFO] [1728931868.060224508] [ifm3d.ods]: BaseServices created. +[ods_standalone-1] [INFO] [1728931868.060237899] [ifm3d.ods]: Configuration complete. +[ods_standalone-1] [INFO] [1728931868.061711296] [ifm3d.ods]: on_activate(): inactive -> activating [89/139] +[ods_standalone-1] [INFO] [1728931868.061807873] [ifm3d.ods]: Starting the Framegrabbers... +[ods_standalone-1] [INFO] [1728931868.187758863] [ifm3d.ods]: Diagnostic monitoring active. +[ods_standalone-1] [INFO] [1728931868.187814195] [ifm3d.ods]: OdsModule: on_activate called +[ods_standalone-1] [INFO] [1728931868.187832369] [ifm3d.ods]: DiagModule: on_activate called +``` + +This will launch the `/ifm3d/ods` node, using the default parameters defined in `config/ods_default_parameters.yaml`. + +This node provides the following interfaces: +```bash +$ ros2 node info /ifm3d/ods +/ifm3d/ods + Subscribers: + /parameter_events: rcl_interfaces/msg/ParameterEvent + Publishers: + /diagnostics: diagnostic_msgs/msg/DiagnosticArray + /ifm3d/ods/ods_info: ifm3d_ros2/msg/Zones + /ifm3d/ods/ods_occupancy_map_ifm: ifm3d_ros2/msg/OccGrid + /ifm3d/ods/ods_occupancy_map_ros: nav_msgs/msg/OccupancyGrid + /ifm3d/ods/transition_event: lifecycle_msgs/msg/TransitionEvent + /parameter_events: rcl_interfaces/msg/ParameterEvent + /rosout: rcl_interfaces/msg/Log + Service Servers: + /ifm3d/ods/Config: ifm3d_ros2/srv/Config + /ifm3d/ods/Dump: ifm3d_ros2/srv/Dump + /ifm3d/ods/GetDiag: ifm3d_ros2/srv/GetDiag + /ifm3d/ods/Softoff: ifm3d_ros2/srv/Softoff + /ifm3d/ods/Softon: ifm3d_ros2/srv/Softon + /ifm3d/ods/change_state: lifecycle_msgs/srv/ChangeState + /ifm3d/ods/describe_parameters: rcl_interfaces/srv/DescribeParameters + /ifm3d/ods/get_available_states: lifecycle_msgs/srv/GetAvailableStates + /ifm3d/ods/get_available_transitions: lifecycle_msgs/srv/GetAvailableTransitions + /ifm3d/ods/get_parameter_types: rcl_interfaces/srv/GetParameterTypes + /ifm3d/ods/get_parameters: rcl_interfaces/srv/GetParameters + /ifm3d/ods/get_state: lifecycle_msgs/srv/GetState + /ifm3d/ods/get_transition_graph: lifecycle_msgs/srv/GetAvailableTransitions + /ifm3d/ods/get_type_description: type_description_interfaces/srv/GetTypeDescription + /ifm3d/ods/list_parameters: rcl_interfaces/srv/ListParameters + /ifm3d/ods/set_parameters: rcl_interfaces/srv/SetParameters + /ifm3d/ods/set_parameters_atomically: rcl_interfaces/srv/SetParametersAtomically + Service Clients: + + Action Servers: + + Action Clients: +``` \ No newline at end of file diff --git a/doc/ods_node/ods_params.md b/doc/ods_node/ods_params.md new file mode 100644 index 0000000..28aa258 --- /dev/null +++ b/doc/ods_node/ods_params.md @@ -0,0 +1,11 @@ +# ODS parameters + +| Name | Data Type | Default Value | Description | +| ------------------------------ | --------- | ---------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `~/config_file` | string | `""` | Path to a JSON configuration file to be used when configuring the node. | +| `~/ip` | String | "192.168.0.69" | The IP address of the OVP8xx platform. | +| `~/ods.frame_id` | String | "ifm_base_link" | The name of the `frame_id` used for the occupancy grid and the zones topics headers. | +| `~/ods.publish_occupancy_grid` | bool | true | Set module to publish `nav2_msgs/OccupancyGrid`. | +| `~/ods.publish_costmap` | bool | false | Set module to publish `nav2_msgs/Costmap`. | +| `~/pcic_port` | Integer | 51010 | The TCP port the PCIC server for the active application is listening to. Can be read out in the JSON configuration at the `"/applications/instances/appX/data/PcicTCPPort"` key, or retrieved using the ifm3d API with `O3R->Port("appX").pcic_port`, where `"appX"` is the active application. | +| `~/xmlrpc_port` | Integer | `ifm3d::DEFAULT_XMLRPC_PORT` | The TCP port the XMLRPC server for the active application is listening to. Typically, the default value can be used. | diff --git a/doc/ods_node/ods_topics.md b/doc/ods_node/ods_topics.md new file mode 100644 index 0000000..366e7a6 --- /dev/null +++ b/doc/ods_node/ods_topics.md @@ -0,0 +1,16 @@ +# ODS topics + +## Published topics +| Name | Data type | Description | +| ---------------------------------- | ---------------------------- | -------------------------------------------------------------------------------------------------------------------------------- | +| `/ifm3d/ods/ods_info` | `ifm3d_ros2/msg/Zones` | Provides the id of the currently active zone set, as well as whether each zone is occupied (`=1`) or not (`=0`). | +| `/ifm3d/ods/ods_occupancy_grid` | `nav_msgs/msg/OccupancyGrid` | The occupancy grid where each cell represent the probability of an obstacle being there. | +| `/ifm3d/ods/ods_occupancy_grid_costmap` | `nav2_msgs/msg/Costmap` | The occupancy grid, formatted as a costmap. Publication of this topic is optional. | +| `/diagnostics` | `diagnostic_msgs/msg/DiagnosticArray` | Check out [the diagnostic documentation](../diagnostic.md) for more details. | + +:::{note} +All the topics are published with QoS `ifm3d_ros2::LowLatencyQoS`. +::: + +## Subscribed topics +None \ No newline at end of file diff --git a/doc/ods_node/ods_transforms.md b/doc/ods_node/ods_transforms.md new file mode 100644 index 0000000..6ccd5cf --- /dev/null +++ b/doc/ods_node/ods_transforms.md @@ -0,0 +1,14 @@ +# ODS transforms + +ODS data is published with reference to the `"ifm_base_link"`, which, in general, corresponds to the robot's `"base_link"`. + +The reference coordinate system for ODS is constrained such that: +- The center of the ODS coordinate system is at floor level, +- The X axis is pointing in the direction of forward movement, +- The Z axis is pointing upwards. + +The cameras used by ODS must be calibrated with reference to the ODS coordinate system, as part of the initial ODS configuration (see [the ODS configuration documentation](./ods_configuration.md)). + +The origin of the ODS coordinate system corresponds to the center of the occupancy grid. + +Publishing the tf from the robot's `"base_link"` to the `"ifm_base_link"` should be done by the user. diff --git a/doc/parameters.md b/doc/parameters.md deleted file mode 100644 index 1d7d08d..0000000 --- a/doc/parameters.md +++ /dev/null @@ -1,29 +0,0 @@ -# Parameters -| Name | Data Type | Default Value | Description | -| ------------------------ | --------- | ------------- | ------------ | -| ~/buffer_id_list | string array | {"CONFIDENCE_IMAGE","EXTRINSIC_CALIB","JPEG_IMAGE" "NORM_AMPLITUDE_IMAGE","RADIAL_DISTANCE_IMAGE","RGB_INFO","XYZ"}| List of buffer_id strings denoting the wanted buffers.| -| ~/ip | string | 192.168.0.69 | The ip address of the camera. | -| ~/log_level| string | warning| ifm3d-ros2 node logging level. | -| ~/tf.cloud_link.frame_name | string | _optical_link | Name for the point cloud frame. | -| ~/tf.cloud_link.publish_transform | bool | true | Whether the transform from the cameras mounting point to the point cloud center should be published. | -| ~/tf.mounting_link.frame_name | string | _mounting_link | Name for the mounting point frame. | -| ~/tf.optical_link.frame_name | string | _optical_link | Name for the point optical frame. | -| ~/tf.optical_link.publish_transform | bool | true | Whether the transform from the cameras mounting point to the point optical center should be published. | -| ~/tf.optical_link.transform | double array | [0, 0, 0, 0, 0, 0] | Static transform from mounting link to optical link, as [x, y, z, rot_x, rot_y, rot_z] | -| ~/buffer_id_list | string array | {"NORM_AMPLITUDE_IMAGE", "CONFIDENCE_IMAGE", JPEG_IMAGE", "RADIAL_DISTANCE_IMAGE", "XYZ", "EXTRINSIC_CALIB", } | List of buffer_id strings denoting the wanted buffers. | -| ~/diag_mode | string: "async" or "periodic" | "async" | Diagnostic mode: asynchronous monitoring ("async") or periodic polling ("periodic"). | -| ~/xmlrpc_port| unint | 50010 | TCP port the on-camera xmlrpc server is listening on | - -## Details on the published transforms - -The ifm3d-ros2 node published three transforms: the `cloud_link`, the `mounting_link` and the `optical_link`. To clarify which frame each of these transform refers to, consider the drawings below: - -![Description of the cloud, mounting and optical frames](figures/transforms-1.png) - - -The reference of the mounting frame is at the back of the O3R camera head housing (scale drawings can be found on ifm.com in the download section of the specific article). The reference for the cloud frame is defined by the extrinsic parameters set in the JSON configuration of the O3R platform (`extrinsicHeadToUser`). Generally, the cloud is configured to have for origin the center of the robot coordinate system. When no extrinsic parameter is set, the origin of the point cloud is the back of the camera head. In this case, the cloud link and the mounting link are the same. - -![Focused description of the optical and mounting frames](figures/transforms-2.png) - -The optical frame refers to the reference point of the optical system (lens, chip, etc). A static transform is published between the mounting link and the optical link, that corresponds to the intrinsic calibration parameters of the camera. Each set of intrinsic parameters is unique to a specific camera head and set in production. These parameters are not expected to change over time. -Note that the intrinsic parameters are split into `ExtrinsicOpticToUser` (rotations and translations of the optical system in the housing) and `Intrinsics` (specific parameters of the lens, chip, etc). For more details on the calibrations, refer to the [calibration documentation](https://ifm3d.com/latest/CalibrationRoutines/index_calibrations.html). diff --git a/doc/rpc_error_codes.md b/doc/rpc_error_codes.md deleted file mode 100644 index 2431315..0000000 --- a/doc/rpc_error_codes.md +++ /dev/null @@ -1,5 +0,0 @@ -# Diagnostic - -The ifm3d-ros2 package periodically publishes diagnostic information. The diagnostic message contains an error code and a message, referring directly to an error from ifm3d or from the embedded software. -For more details on the error codes and potential troubleshooting strategies, refer to the [O3R diagnostic documentation](../../../documentation/SoftwareInterfaces/ifmDiagnostic/index_diagnostic.md). - diff --git a/doc/services.md b/doc/services.md index 9cfb955..c873895 100644 --- a/doc/services.md +++ b/doc/services.md @@ -1,28 +1,25 @@ # Advertised Services + | Name | Service Definition | Description | | ------- | ------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- | -| Dump | [ifm3d/Dump](../srv/Dump.srv) | Dumps the state of the camera parameters to JSON | -| Config | [ifm3d/Config](../srv/Config.srv) | Provides a means to configure the camera and imager settings, declaratively from a JSON encoding of the desired settings. | -| Softon | [ifm3d/Softon](../srv/Softon.srv) | Provides a means to quickly change the camera state from IDLE to RUN. | -| Softoff | [ifm3d/Softoff](../srv/Softoff.srv) | Provides a means to quickly change the camera state from RUN to IDLE. | +| `Dump` | ifm3d_ros2/srv/Dump | Dumps the state of the camera parameters to JSON | +| `Config` | ifm3d_ros2/srv/Config | Provides a means to configure the camera and imager settings, declaratively from a JSON encoding of the desired settings. | +| `GetDiag` | ifm3d_ros2/srv/GetDiag | Get all the diagnostic messages corresponding to a JSON filter.
The filter can be left blank to get all active and dormant error messages: `ros2 service call /ifm3d/camera/GetDiag ifm3d_ros2/srv/GetDiag "{filter: '{}'}"`.| +| `Softon` | ifm3d_ros2/srv/Softon | Provides a means to quickly change the camera state from IDLE to RUN. | +| `Softoff` | ifm3d_ros2/srv/Softoff | Provides a means to quickly change the camera state from RUN to IDLE. | ## Dump and Config -The ifm3d_ros2 package allows the user to configure the O3R camera platform via two separate ways: -1. Use ROS native service calls -2. Use the dump and config service proxies - -### ROS native service calls +The ifm3d_ros2 package allows the user to configure the O3R camera platform using ROS native service calls. -#### Dump +### Dump Calling the native ROS service `/ifm3d_ros2/camera/Dump` for a certain `camera` will return the camera configuration as a JSON string. Please notice the use of backslashes (`\` before each `"`) to escape each upper quotation mark. This is necessary to allow us to keep the JSON syntax native to the underlying API (ifm3d). Call this service via, e.g. for camera: ```bash $ ros2 service call /ifm3d/camera/Dump ifm3d_ros2/srv/Dump -1637355550.468025 [0] ros2: using network interface enp0s31f6 (udp/192.168.0.10) selected arbitrarily from: enp0s31f6, wlp0s20f3, docker0, enx98fc84eebfc8 requester: making request: ifm3d_ros2.srv.Dump_Request() response: @@ -30,218 +27,27 @@ ifm3d_ros2.srv.Dump_Response(status=0, config='{"device":{"clock":{"currentTime" ``` -#### Config +### Config Below you can see an example on how to configure your camera via a ROS service call. The JSON string can be a partial JSON string. It only needs to follow basic JSON syntax. Please wrap the JSON string in a YAML syntax and use the field `"json"`. ```bash $ ros2 service call /ifm3d/camera/Config ifm3d_ros2/srv/Config "{json: '{\"ports\":{\"port0\":{\"mode\":\"standard_range4m\"}}}'}" -1637355711.266033 [0] ros2: using network interface enp0s31f6 (udp/192.168.0.10) selected arbitrarily from: enp0s31f6, wlp0s20f3, docker0, enx98fc84eebfc8 requester: making request: ifm3d_ros2.srv.Config_Request(json='{"ports":{"port2":{"mode":"standard_range4m"}}}') response: ifm3d_ros2.srv.Config_Response(status=0, msg='OK') ``` +## Diagnostic +The `ifm3d_ros2` package provides a service to poll diagnostic data from the device. A filter can be provided to retrieve specific diagnostic data. -### Dump and config service proxies -`ifm3d-ros2` provides access to each camera parameter via the `Dump` and `Config` services exposed by the `camera_node`. - -Additionally, command-line scripts called `dump` and `config` are provided as wrapper interfaces to the native API `ifm3d`. This gives a feel similar to using the underlying C++ API's command-line tool, from the ROS-independent driver except proxying the calls through the ROS network. - -For example, to dump the state of the camera: -(exemplary output from an O3R perception platform with one camera head connected is shown) - -```bash -$ ros2 run ifm3d_ros2 dump -{ - "device": { - "clock": { - "currentTime": 1581193835185485800 - }, - "diagnostic": { - "temperatures": [], - "upTime": 103190000000000 - }, - "info": { - "device": "0301", - "deviceTreeBinaryBlob": "tegra186-quill-p3310-1000-c03-00-base.dtb", - "features": {}, - "name": "", - " partNumber": "M03975", - "productionState": "AA", - "serialNumber": "000201234160", - "vendor": "0001" - }, - "network": { - "authorized_keys": "", - "ipAddressConfig": 0, - "macEth0": "00:04:4B:EA:9F:0E", - "macEth1": "00:02:01:23:41:60", - "networkSpeed": 1000, - "staticIPv4Address": "192.168.0.69", - "staticIPv4Gateway": "192.168.0.201", - "staticIPv4SubNetMask": "255.255.255.0", - "useDHCP": false - }, - "state": { - "errorMessage": "", - "errorNumber": "" - }, - "swVersion": { - "kernel": "4.9.140-l4t-r32.4+gc35f5eb9d1d9", - "l4t": "r32.4.3", - "os": "0.13.13-221", - "schema": "v0.1.0", - "swu": "0.15.12" - } - }, - "ports": { - "port0": { - "acquisition": { - "framerate": 10, - "version": { - "major": 0, - "minor": 0, - "patch": 0 - } - }, - "data": { - "algoDebugConfig": {}, - "availablePCICOutput": [], - "pcicTCPPort": 50010 - }, - "info": { - "device": "2301", - "deviceTreeBinaryBlobOverlay": "001-ov9782.dtbo", - "features": { - "fov": { - "horizontal": 127, - "vertical": 80 - }, - " resolution": { - "height": 800, - "width": 1280 - }, - "type": "2D" - }, - "name": "", - "partNumber": "M03969", - "productionState": "AA", - "sensor": "OV9782", - "sensorID": "OV9782_127x80_noIllu_Csample", - "serialNumber": "000000000382", - "vendor": "0001" - }, - "mode": "experimental_autoexposure2D", - "processing": { - "extrinsicHeadToUser": { - "rotX": 0, - "rotY": 0, - "rotZ": 0, - " transX": 0, - "transY": 0, - "transZ": 0 - }, - "version": { - "major": 0, - "minor": 0, - " patch": 0 - } - }, - "state": "RUN" - }, - "port2": { - "acquisition": { - "exposureLong": 5000, - " exposureShort": 400, - "framerate": 10, - "offset": 0, - "version": { - "major": 0, - " minor": 0, - "patch": 0 - } - }, - "data": { - "algoDebugConfig": {}, - "availablePCICOutput": [], - "pcicTCPPort": 50012 - }, - "info": { - "device": "3101", - "deviceTreeBinaryBlobOverlay": "001-irs2381c.dtbo", - "features": { - "fov": { - "horizontal": 105, - "vertical": 78 - }, - " resolution": { - "height": 172, - "width": 224 - }, - "type": "3D" - }, - "name": "", - "partNumber": "M03969", - "productionState": "AA", - "sensor": "IRS2381C", - "sensorID": "IRS2381C_105x78_4x2W_110x90_C7", - "serialNumber": "000000000382", - "vendor": "0001" - }, - "mode": "standard_range4m", - "processing": { - "diParam": { - "anfFilterSizeDiv2": 2, - "enableDynamicSymmetry": true, - "enableStraylight": true, - "enableTemporalFilter": true, - "excessiveCorrectionThreshAmp": 0.3, - "excessiveCorrectionThreshDist": 0.08, - "maxDistNoise": 0.02, - "maxSymmetry": 0.4, - "medianSizeDiv2": 0, - "minAmplitude": 20, - "minReflectivity": 0, - "mixedPixelFilterMode": 1, - "mixedPixelThresholdRad": 0.15 - }, - "extrinsicHeadToUser": { - "rotX": 1, - "rotY": 0, - "rotZ": 0, - "transX": 100, - "transY": 0, - "transZ": 0 - }, - "version": { - " major": 0, - "minor": 0, - "patch": 0 - } - }, - "state": "RUN" - } - } -} +For example, to retrieve all active diagnostic corresponding to port 0: ``` +$ ros2 service call /ifm3d/camera/GetDiag ifm3d_ros2/srv/GetDiag "{filter: '{\"source\":\"/ports/port0\", \"state\":\"active\"}'}" -Chaining together Linux pipelines works just as it does in `ifm3d`. For example, using a combination of `dump` and `config` to change the framerate -from 5Hz to 10Hz of the single application on this particular camera would look like: +requester: making request: ifm3d_ros2.srv.GetDiag_Request(filter='{"source":"/ports/port0", "state":"active"}') -```bash -$ ros2 run ifm3d_ros2 dump | jq '.ports.port0.mode="standard_range2m"' | ros2 run ifm3d_ros2 config -$ ros2 run ifm3d_ros2 dump | jq .ports.port0.mode -"standard_range2m" -``` - -:::{note} -If you do not have `jq` on your system, it can be installed with: `$ sudo apt install jq`. -::: - -For the `config` command, one difference between our ROS implementation and the `ifm3d` implementation is that we only accept input on `stdin`. So, if you had a file with JSON you wish to configure your camera with, you would simply use the file I/O redirection facilities of your shell (or something like `cat`) to feed the data to `stdin`. For example, in `bash`: - -```bash -$ ros2 run ifm3d_ros2 config < camera.json +response: +ifm3d_ros2.srv.GetDiag_Response(status=0, msg='{"bootid":"3202350a-a620-40a0-9114-5221bb55b1ff","events":[],"timestamp":1651187568817246656,"version": {"diagnostics":"0.0.11","euphrates":"1.34.32","firmware":"1.4.30.4443"}}') ``` \ No newline at end of file diff --git a/doc/topics.md b/doc/topics.md deleted file mode 100644 index 6658c76..0000000 --- a/doc/topics.md +++ /dev/null @@ -1,27 +0,0 @@ -# Topics - -## Published Topics - -| Name | Data Type | Description | -| --------------- | ---------------------------- | ------------------------------------------------------------------- | -| amplitude | sensor_msgs/msg/Image | The normalized amplitude image | -| cloud | sensor_msgs/msg/PointCloud2 | The point cloud data | -| confidence | sensor_msgs/msg/Image | The confidence image | -| distance | sensor_msgs/msg/Image | The radial distance image | -| raw_amplitude | sensor_msgs/msg/Image | The raw amplitude image (currently not available for the O3R) | -| rgb | sensor_msgs/msg/Image | The RGB 2D image of the 2D imager | -| extrinsics | ifm3d_ros2::msg::Extrinsics | The extrinsic calibration of the camera (camera to world) | -| INTRINSIC_CALIB | ifm3d_ros2::msg::Intrinsics | The intrinsic calibration of the camera (optical system parameters) | -| INVERSE_INTRINSIC_CALIBRATION | ifm3d_ros2::msg::InverseIntrinsics | The inverse intrinsic calibration of the camera | -| camera_info | sensor_msgs::msg::CameraInfo | The camera info topic containing the distortion model. This topic is published if the INTRINSIC_CALIB is part of the buffer list | -| TOF_INFO | ifm3d_ros2::msg::TOFInfo | A topic gathering various information from the tof camera (see [TOFinfo.msg](../msg/TOFInfo.msg)) | -| RGB_INFO | ifm3d_ros2::msg::RGBInfo | A topic gathering various information from the rgb camera (see [RGBInfo.msg](../msg/RGBInfo.msg)) | -| diagnostics | diagnostic_msgs::msg::DiagnosticArray | Diagnostic messages pulled from the device every second | - -:::{note} -All the topics are published with QoS `ifm3d_ros2::LowLatencyQoS`. -::: - -## Subscribed Topics - -None. diff --git a/doc/visualization.md b/doc/visualization.md index 6caf292..28d46bb 100644 --- a/doc/visualization.md +++ b/doc/visualization.md @@ -1,4 +1,5 @@ # How to visualize data with RVIZ + The included launch file `camera.launch.py` will publish and remap all topics and services to `/ifm3d/camera`, for example the point cloud topic will be remapped to `/ifm3d/camera/cloud`. Both the namespace (default: `ifm3d`) and the node name (default: `camera`) can be changed via the launch description files. @@ -7,6 +8,7 @@ You have to change the topic subscriptions yourself when using non-default value ```bash ros2 launch ifm3d_ros2 camera.launch.py visualization:=true ``` + ## Viewing the RGB image The RGB image is published on the `/ifm3d/camera*/rgb` topic in a compressed JPEG format. @@ -19,10 +21,3 @@ $ ros2 run image_transport republish compressed raw --ros-args --remap /in/compr ``` In RViz, you can now subscribe to the `/uncompressed_rgb` topic to visualize the RGB image. -## QoS reliability - best effort -When you open RVIZ for the first time and subscribe to the point cloud topic, it will not be displayed as we need to change the default quality of service (QoS) settings per topic. - -To change these settings: -1. subscribe to a topic in RVIZ by adding it (ADD button) -2. Expand the topic settings -3. Select `reliability` and set it to `Best Effort` diff --git a/examples/compute_cartesian.py b/examples/compute_cartesian.py deleted file mode 100644 index 8a6a19b..0000000 --- a/examples/compute_cartesian.py +++ /dev/null @@ -1,133 +0,0 @@ -#!/usr/bin/env python3 - -# -# Example showing how to compute the Cartesian data (point cloud) off-board the -# camera from the Unit Vectors, Extrinsics Calibration, and Distance Image. -# -import sys -import threading -import numpy as np - -import rclpy -from rclpy.node import Node -from rclpy.qos import QoSDurabilityPolicy -from rclpy.qos import QoSProfile -from rclpy.qos import QoSReliabilityPolicy -from sensor_msgs.msg import Image -from ifm3d_ros2.msg import Extrinsics -from cv_bridge import CvBridge - -# -# NOTE: As of the date of writing this script, the ROS2 python implementation -# of message_filters.TimeSchronizer had issues (i.e., did not handle QoS -# properly and a few other things). So, I am manually time syncing the -# extrinsics and the radial distance data. -# - -class CartesianCompute(object): - - def __init__(self, node): - self.node_ = node - - self.topic_uvec_ = "/ifm3d/camera/unit_vectors" - self.topic_extr_ = "/ifm3d/camera/extrinsics" - self.topic_dist_ = "/ifm3d/camera/distance" - - self.bridge_ = CvBridge() - - self.lock_ = threading.Lock() - self.uvec_ = None - self.extr_ = None - - self.sub_uvec_ = self.node_.create_subscription( - Image, - self.topic_uvec_, - self.uvec_cb, - QoSProfile( - depth=1, - reliability=QoSReliabilityPolicy.RELIABLE, - durability=QoSDurabilityPolicy.TRANSIENT_LOCAL - ) - ) - - qos = QoSProfile( - depth=2, - reliability=QoSReliabilityPolicy.BEST_EFFORT, - durability=QoSDurabilityPolicy.VOLATILE - ) - - self.sub_extr_ = self.node_.create_subscription( - Extrinsics, self.topic_extr_, self.extr_cb, qos) - - self.sub_dist_ = self.node_.create_subscription( - Image, self.topic_dist_, self.dist_cb, qos) - - def uvec_cb(self, msg): - with self.lock_: - self.uvec_ = self.bridge_.imgmsg_to_cv2(msg) - - def extr_cb(self, msg): - with self.lock_: - self.extr_ = msg - - def dist_cb(self, msg): - dist = None - tx = None - ty = None - tz = None - - with self.lock_: - if ((self.extr_ is not None) and (self.uvec_ is not None)): - if msg.header.stamp == self.extr_.header.stamp: - dist = self.bridge_.imgmsg_to_cv2(msg) - tx = self.extr_.tx - ty = self.extr_.ty - tz = self.extr_.tz - else: - return - else: - return - - # unit vectors - ex = self.uvec_[:,:,0] - ey = self.uvec_[:,:,1] - ez = self.uvec_[:,:,2] - - # cast distance image to float - rdis_f = dist.astype(np.float32) - if (dist.dtype == np.float32): - # assume dist was in meters, convert to mm - rdis_f *= 1000. - - # compute cartesian - x_ = ex * rdis_f + tx - y_ = ey * rdis_f + ty - z_ = ez * rdis_f + tz - - # convert to camera frame - x = z_ - y = -x_ - z = -y_ - - # print results in meters - print(np.dstack((x,y,z))/1000.) - -def main(): - rclpy.init() - node = Node('cartesian_compute') - - try: - c = CartesianCompute(node) - rclpy.spin(node) - - except KeyboardInterrupt: - pass - - finally: - node.destroy_node() - rclpy.shutdown() - - return 0 - -if __name__ == '__main__': - sys.exit(main()) diff --git a/examples/latched_subscriber.py b/examples/latched_subscriber.py deleted file mode 100644 index ff37ba7..0000000 --- a/examples/latched_subscriber.py +++ /dev/null @@ -1,47 +0,0 @@ -#!/usr/bin/env python3 - -# -# Quick script to test that latching the unit vectors is working. Simply run -# the script once the camera_node is up. This will act as a late -# subscriber. You should see the unit vectors Image message serialized to the -# screen. Press Ctl-C to exit. -# - -import sys - -import rclpy -from rclpy.node import Node -from rclpy.qos import QoSDurabilityPolicy -from rclpy.qos import QoSProfile -from rclpy.qos import QoSReliabilityPolicy -from sensor_msgs.msg import Image - -def main(args=None): - topic = '/ifm3d/camera/unit_vectors' - topic_type = Image - - rclpy.init(args=args) - qos_profile = QoSProfile( - depth=1, - reliability=QoSReliabilityPolicy.RELIABLE, - durability=QoSDurabilityPolicy.TRANSIENT_LOCAL - ) - node = Node('uvec_listener') - sub = node.create_subscription( - topic_type, - topic, - lambda msg: print(msg), - qos_profile) - - try: - rclpy.spin(node) - - except KeyboardInterrupt: - node.destroy_node() - rclpy.shutdown() - - finally: - return 0 - -if __name__ == "__main__": - sys.exit(main()) diff --git a/include/ifm3d_ros2/buffer_conversions.hpp b/include/ifm3d_ros2/buffer_conversions.hpp index fc75bda..c78e7d4 100644 --- a/include/ifm3d_ros2/buffer_conversions.hpp +++ b/include/ifm3d_ros2/buffer_conversions.hpp @@ -1,446 +1,125 @@ // -*- c++ -*- /* * SPDX-License-Identifier: Apache-2.0 - * Copyright (C) 2019 ifm electronic, gmbh + * Copyright (C) 2024 ifm electronic, gmbh */ #ifndef IFM3D_ROS2_BUFFER_CONVERSIONS_HPP_ #define IFM3D_ROS2_BUFFER_CONVERSIONS_HPP_ #include -#include +#include +#include +#include +#include + #include +#include +#include +#include +#include +#include + #include -#include -#include #include #include #include #include +#include +#include +#include +#include + +using TimePointT = std::chrono::time_point; namespace ifm3d_ros2 { -sensor_msgs::msg::Image ifm3d_to_ros_image(ifm3d::Buffer& image, // Need non-const image because image.begin(), - // image.end() don't have const overloads. - const std_msgs::msg::Header& header, const rclcpp::Logger& logger) -{ - static constexpr auto max_pixel_format = static_cast(ifm3d::pixel_format::FORMAT_32F3); - static constexpr auto image_format_info = [] { - auto image_format_info = std::array{}; - - { - using namespace ifm3d; - using namespace sensor_msgs::image_encodings; - image_format_info[static_cast(pixel_format::FORMAT_8U)] = TYPE_8UC1; - image_format_info[static_cast(pixel_format::FORMAT_8S)] = TYPE_8SC1; - image_format_info[static_cast(pixel_format::FORMAT_16U)] = TYPE_16UC1; - image_format_info[static_cast(pixel_format::FORMAT_16S)] = TYPE_16SC1; - image_format_info[static_cast(pixel_format::FORMAT_32U)] = "32UC1"; - image_format_info[static_cast(pixel_format::FORMAT_32S)] = TYPE_32SC1; - image_format_info[static_cast(pixel_format::FORMAT_32F)] = TYPE_32FC1; - image_format_info[static_cast(pixel_format::FORMAT_64U)] = "64UC1"; - image_format_info[static_cast(pixel_format::FORMAT_64F)] = TYPE_64FC1; - image_format_info[static_cast(pixel_format::FORMAT_16U2)] = TYPE_16UC2; - image_format_info[static_cast(pixel_format::FORMAT_32F3)] = TYPE_32FC3; - } - - return image_format_info; - }(); - - const auto format = static_cast(image.dataFormat()); - - sensor_msgs::msg::Image result{}; - result.header = header; - result.height = image.height(); - result.width = image.width(); - result.is_bigendian = 0; - - if (image.begin() == image.end()) - { - return result; - } - - if (format >= max_pixel_format) - { - RCLCPP_ERROR(logger, "Pixel format out of range (%ld >= %ld)", format, max_pixel_format); - return result; - } - - result.encoding = image_format_info.at(format); - result.step = result.width * sensor_msgs::image_encodings::bitDepth(image_format_info.at(format)) / 8; - result.data.insert(result.data.end(), image.ptr<>(0), std::next(image.ptr<>(0), result.step * result.height)); - - if (result.encoding.empty()) - { - RCLCPP_WARN(logger, "Can't handle encoding %ld (32U == %ld, 64U == %ld)", format, - static_cast(ifm3d::pixel_format::FORMAT_32U), - static_cast(ifm3d::pixel_format::FORMAT_64U)); - result.encoding = sensor_msgs::image_encodings::TYPE_8UC1; - } - - return result; -} - +sensor_msgs::msg::Image ifm3d_to_ros_image(ifm3d::Buffer& image, const std_msgs::msg::Header& header, + const rclcpp::Logger& logger); sensor_msgs::msg::Image ifm3d_to_ros_image(ifm3d::Buffer&& image, const std_msgs::msg::Header& header, - const rclcpp::Logger& logger) -{ - return ifm3d_to_ros_image(image, header, logger); -} - -sensor_msgs::msg::CompressedImage ifm3d_to_ros_compressed_image(ifm3d::Buffer& image, // Need non-const image because - // image.begin(), image.end() - // don't have const overloads. + const rclcpp::Logger& logger); +sensor_msgs::msg::CompressedImage ifm3d_to_ros_compressed_image(ifm3d::Buffer& image, const std_msgs::msg::Header& header, - const std::string& format, // "jpeg" or "png" - const rclcpp::Logger& logger) -{ - sensor_msgs::msg::CompressedImage result{}; - result.header = header; - result.format = format; - - if (const auto dataFormat = image.dataFormat(); - dataFormat != ifm3d::pixel_format::FORMAT_8S && dataFormat != ifm3d::pixel_format::FORMAT_8U) - { - RCLCPP_ERROR(logger, "Invalid data format for %s data (%ld)", format.c_str(), static_cast(dataFormat)); - return result; - } - - result.data.insert(result.data.end(), image.ptr<>(0), std::next(image.ptr<>(0), image.width() * image.height())); - return result; -} - + const std::string& format, + const rclcpp::Logger& logger); sensor_msgs::msg::CompressedImage ifm3d_to_ros_compressed_image(ifm3d::Buffer&& image, const std_msgs::msg::Header& header, - const std::string& format, const rclcpp::Logger& logger) -{ - return ifm3d_to_ros_compressed_image(image, header, format, logger); -} - -sensor_msgs::msg::PointCloud2 ifm3d_to_ros_cloud(ifm3d::Buffer& image, // Need non-const image because image.begin(), - // image.end() don't have const overloads. - const std_msgs::msg::Header& header, const rclcpp::Logger& logger) -{ - sensor_msgs::msg::PointCloud2 result{}; - result.header = header; - result.height = image.height(); - result.width = image.width(); - result.is_bigendian = false; - - if (image.begin() == image.end()) - { - return result; - } - - if (image.dataFormat() != ifm3d::pixel_format::FORMAT_32F3 && image.dataFormat() != ifm3d::pixel_format::FORMAT_32F) - { - RCLCPP_ERROR(logger, "Unsupported pixel format %ld for point cloud", static_cast(image.dataFormat())); - return result; - } - - sensor_msgs::msg::PointField x_field{}; - x_field.name = "x"; - x_field.offset = 0; - x_field.datatype = sensor_msgs::msg::PointField::FLOAT32; - x_field.count = 1; - - sensor_msgs::msg::PointField y_field{}; - y_field.name = "y"; - y_field.offset = 4; - y_field.datatype = sensor_msgs::msg::PointField::FLOAT32; - y_field.count = 1; - - sensor_msgs::msg::PointField z_field{}; - z_field.name = "z"; - z_field.offset = 8; - z_field.datatype = sensor_msgs::msg::PointField::FLOAT32; - z_field.count = 1; - - result.fields = { - x_field, - y_field, - z_field, - }; - - result.point_step = result.fields.size() * sizeof(float); - result.row_step = result.point_step * result.width; - result.is_dense = true; - result.data.insert(result.data.end(), image.ptr<>(0), std::next(image.ptr<>(0), result.row_step * result.height)); - - return result; -} - + const std::string& format, + const rclcpp::Logger& logger); +sensor_msgs::msg::PointCloud2 ifm3d_to_ros_cloud(ifm3d::Buffer& image, const std_msgs::msg::Header& header, + const rclcpp::Logger& logger); sensor_msgs::msg::PointCloud2 ifm3d_to_ros_cloud(ifm3d::Buffer&& image, const std_msgs::msg::Header& header, - const rclcpp::Logger& logger) -{ - return ifm3d_to_ros_cloud(image, header, logger); -} - + const rclcpp::Logger& logger); +nav_msgs::msg::MapMetaData ifm3d_to_ros_map_meta_data(ifm3d::Buffer& buffer, const std_msgs::msg::Header& header, + const rclcpp::Logger& logger); +nav_msgs::msg::MapMetaData ifm3d_to_ros_map_meta_data(ifm3d::Buffer&& buffer, const std_msgs::msg::Header& header, + const rclcpp::Logger& logger); +nav_msgs::msg::OccupancyGrid ifm3d_to_ros_occupancy_grid(ifm3d::Buffer& buffer, const std_msgs::msg::Header& header, + const rclcpp::Logger& logger); +nav_msgs::msg::OccupancyGrid ifm3d_to_ros_occupancy_grid(ifm3d::Buffer&& buffer, const std_msgs::msg::Header& header, + const rclcpp::Logger& logger); ifm3d_ros2::msg::Extrinsics ifm3d_to_extrinsics(ifm3d::Buffer& buffer, const std_msgs::msg::Header& header, - const rclcpp::Logger& logger) -{ - ifm3d_ros2::msg::Extrinsics extrinsics_msg; - extrinsics_msg.header = header; - try - { - extrinsics_msg.tx = buffer.at(0); - extrinsics_msg.ty = buffer.at(1); - extrinsics_msg.tz = buffer.at(2); - extrinsics_msg.rot_x = buffer.at(3); - extrinsics_msg.rot_y = buffer.at(4); - extrinsics_msg.rot_z = buffer.at(5); - } - catch (const std::out_of_range& ex) - { - RCLCPP_WARN(logger, "Out-of-range error fetching extrinsics"); - } - return extrinsics_msg; -} - + const rclcpp::Logger& logger); ifm3d_ros2::msg::Extrinsics ifm3d_to_extrinsics(ifm3d::Buffer&& buffer, const std_msgs::msg::Header& header, - const rclcpp::Logger& logger) -{ - return ifm3d_to_extrinsics(buffer, header, logger); -} - -sensor_msgs::msg::CameraInfo ifm3d_to_camera_info(ifm3d::Buffer& buffer, const std_msgs::msg::Header& header, - const uint32_t height, const uint32_t width, - const rclcpp::Logger& logger) -{ - ifm3d::calibration::IntrinsicCalibration intrinsic; - intrinsic.Read(buffer.ptr(0)); - - sensor_msgs::msg::CameraInfo camera_info_msg; - camera_info_msg.header = header; - - try - { - camera_info_msg.height = height; - camera_info_msg.width = width; - camera_info_msg.distortion_model = sensor_msgs::distortion_models::PLUMB_BOB; - - // Read data from buffer - const float fx = intrinsic.model_parameters[0]; - const float fy = intrinsic.model_parameters[1]; - const float mx = intrinsic.model_parameters[2]; - const float my = intrinsic.model_parameters[3]; - const float alpha = intrinsic.model_parameters[4]; - const float k1 = intrinsic.model_parameters[5]; - const float k2 = intrinsic.model_parameters[6]; - const float k3 = intrinsic.model_parameters[7]; - const float k4 = intrinsic.model_parameters[8]; - // next in buffer is k5 for bouguet or theta_max for fisheye model, both not needed here - - const float ix = width - 1; - const float iy = height - 1; - const float cy = (iy + 0.5 - my) / fy; - const float cx = (ix + 0.5 - mx) / fx - alpha * cy; - const float r2 = cx * cx + cy * cy; - const float h = 2 * cx * cy; - const float tx = k3 * h + k4 * (r2 + 2 * cx * cx); - const float ty = k3 * (r2 + 2 * cy * cy) + k4 * h; - - // Distortion parameters - camera_info_msg.d.resize(5); - camera_info_msg.d[0] = k1; - camera_info_msg.d[1] = k2; - camera_info_msg.d[2] = tx; // TODO t1 == tx ? - camera_info_msg.d[3] = ty; // TODO t2 == ty ? - camera_info_msg.d[4] = k3; - - // Intrinsic camera matrix - camera_info_msg.k[0] = fx; - camera_info_msg.k[4] = fy; - camera_info_msg.k[2] = cx; - camera_info_msg.k[5] = cy; - camera_info_msg.k[8] = 1.0; // fixed to 1.0 - - // Projection matrix - camera_info_msg.p[0] = fx; - camera_info_msg.p[5] = fy; - camera_info_msg.p[2] = cx; - camera_info_msg.p[6] = cy; - camera_info_msg.p[10] = 1.0; // fixed to 1.0 - - RCLCPP_DEBUG_ONCE(logger, - "Intrinsics:\nfx=%f \nfy=%f \nmx=%f \nmy=%f \nalpha=%f \nk1=%f \nk2=%f \nk3=%f \nk4=%f " - "\nCalculated:\nix=%f \niy=%f \ncx=%f \ncy=%f \nr2=%f \nh=%f \ntx=%f \nty=%f", - fx, fy, mx, my, alpha, k1, k2, k3, k4, ix, iy, cx, cy, r2, h, tx, ty); - } - catch (const std::out_of_range& ex) - { - RCLCPP_WARN(logger, "Out-of-range error fetching intrinsics"); - } - - return camera_info_msg; -} - -sensor_msgs::msg::CameraInfo ifm3d_to_camera_info(ifm3d::Buffer&& buffer, const std_msgs::msg::Header& header, - const uint32_t height, const uint32_t width, - const rclcpp::Logger& logger) - -{ - return ifm3d_to_camera_info(buffer, header, height, width, logger); -} - + const rclcpp::Logger& logger); +bool ifm3d_rgb_info_to_camera_info(ifm3d::Buffer& buffer, const std_msgs::msg::Header& header, const uint32_t height, + const uint32_t width, const rclcpp::Logger& logger, + sensor_msgs::msg::CameraInfo& camera_info_msg); +bool ifm3d_rgb_info_to_camera_info(ifm3d::Buffer&& buffer, const std_msgs::msg::Header& header, const uint32_t height, + const uint32_t width, const rclcpp::Logger& logger, + sensor_msgs::msg::CameraInfo& camera_info_msg); +bool ifm3d_tof_info_to_camera_info(ifm3d::Buffer& buffer, const std_msgs::msg::Header& header, const uint32_t height, + const uint32_t width, const rclcpp::Logger& logger, + sensor_msgs::msg::CameraInfo& camera_info_msg); +bool ifm3d_tof_info_to_camera_info(ifm3d::Buffer&& buffer, const std_msgs::msg::Header& header, const uint32_t height, + const uint32_t width, const rclcpp::Logger& logger, + sensor_msgs::msg::CameraInfo& camera_info_msg); +sensor_msgs::msg::CameraInfo ifm3d_to_camera_info(ifm3d::calibration::IntrinsicCalibration intrinsic, + const std_msgs::msg::Header& header, const uint32_t height, + const uint32_t width, const rclcpp::Logger& logger); ifm3d_ros2::msg::Intrinsics ifm3d_to_intrinsics(ifm3d::Buffer& buffer, const std_msgs::msg::Header& header, - const rclcpp::Logger& logger) -{ - ifm3d_ros2::msg::Intrinsics intrinsics_msg; - intrinsics_msg.header = header; - - ifm3d::calibration::IntrinsicCalibration intrinsics; - - try - { - intrinsics.Read(buffer.ptr(0)); - intrinsics_msg.model_id = intrinsics.model_id; - intrinsics_msg.model_parameters = intrinsics.model_parameters; - } - catch (...) - { - RCLCPP_ERROR(logger, "Failed to read intrinsics."); - } - - return intrinsics_msg; -} - + const rclcpp::Logger& logger); ifm3d_ros2::msg::Intrinsics ifm3d_to_intrinsics(ifm3d::Buffer&& buffer, const std_msgs::msg::Header& header, - const rclcpp::Logger& logger) - -{ - return ifm3d_to_intrinsics(buffer, header, logger); -} - + const rclcpp::Logger& logger); ifm3d_ros2::msg::InverseIntrinsics ifm3d_to_inverse_intrinsics(ifm3d::Buffer& buffer, const std_msgs::msg::Header& header, - const rclcpp::Logger& logger) -{ - ifm3d_ros2::msg::InverseIntrinsics inverse_intrinsics_msg; - inverse_intrinsics_msg.header = header; - - ifm3d::calibration::InverseIntrinsicCalibration inverse_intrinsics; - - try - { - inverse_intrinsics.Read(buffer.ptr(0)); - inverse_intrinsics_msg.model_id = inverse_intrinsics.model_id; - inverse_intrinsics_msg.model_parameters = inverse_intrinsics.model_parameters; - } - catch (...) - { - RCLCPP_ERROR(logger, "Failed to read inverse intrinsics."); - } - - return inverse_intrinsics_msg; -} - + const rclcpp::Logger& logger); ifm3d_ros2::msg::InverseIntrinsics ifm3d_to_inverse_intrinsics(ifm3d::Buffer&& buffer, const std_msgs::msg::Header& header, - const rclcpp::Logger& logger) - -{ - return ifm3d_to_inverse_intrinsics(buffer, header, logger); -} - + const rclcpp::Logger& logger); ifm3d_ros2::msg::RGBInfo ifm3d_to_rgb_info(ifm3d::Buffer& buffer, const std_msgs::msg::Header& header, - const rclcpp::Logger& logger) -{ - ifm3d_ros2::msg::RGBInfo rgb_info_msg; - rgb_info_msg.header = header; - - try - { - auto rgb_info = ifm3d::RGBInfoV1::Deserialize(buffer); - - rgb_info_msg.version = rgb_info.version; - rgb_info_msg.frame_counter = rgb_info.frame_counter; - rgb_info_msg.timestamp_ns = rgb_info.timestamp_ns; - rgb_info_msg.exposure_time = rgb_info.exposure_time; - - rgb_info_msg.extrinsics.header = header; - rgb_info_msg.extrinsics.tx = rgb_info.extrinsic_optic_to_user.trans_x; - rgb_info_msg.extrinsics.ty = rgb_info.extrinsic_optic_to_user.trans_y; - rgb_info_msg.extrinsics.tz = rgb_info.extrinsic_optic_to_user.trans_z; - rgb_info_msg.extrinsics.rot_x = rgb_info.extrinsic_optic_to_user.rot_x; - rgb_info_msg.extrinsics.rot_y = rgb_info.extrinsic_optic_to_user.rot_y; - rgb_info_msg.extrinsics.rot_z = rgb_info.extrinsic_optic_to_user.rot_z; - - rgb_info_msg.intrinsics.header = header; - rgb_info_msg.intrinsics.model_id = rgb_info.intrinsic_calibration.model_id; - rgb_info_msg.intrinsics.model_parameters = rgb_info.intrinsic_calibration.model_parameters; - - rgb_info_msg.inverse_intrinsics.header = header; - rgb_info_msg.inverse_intrinsics.model_id = rgb_info.inverse_intrinsic_calibration.model_id; - rgb_info_msg.inverse_intrinsics.model_parameters = rgb_info.inverse_intrinsic_calibration.model_parameters; - } - catch (...) - { - RCLCPP_ERROR(logger, "Failed to read rgb info."); - } - - return rgb_info_msg; -} - + const rclcpp::Logger& logger); ifm3d_ros2::msg::RGBInfo ifm3d_to_rgb_info(ifm3d::Buffer&& buffer, const std_msgs::msg::Header& header, - const rclcpp::Logger& logger) - -{ - return ifm3d_to_rgb_info(buffer, header, logger); -} - + const rclcpp::Logger& logger); ifm3d_ros2::msg::TOFInfo ifm3d_to_tof_info(ifm3d::Buffer& buffer, const std_msgs::msg::Header& header, - const rclcpp::Logger& logger) -{ - ifm3d_ros2::msg::TOFInfo tof_info_msg; - tof_info_msg.header = header; - - try - { - auto tof_info = ifm3d::TOFInfoV4::Deserialize(buffer); - tof_info_msg.measurement_block_index = tof_info.measurement_block_index; - tof_info_msg.measurement_range_min = tof_info.measurement_range_min; - tof_info_msg.measurement_range_max = tof_info.measurement_range_max; - tof_info_msg.version = tof_info.version; - tof_info_msg.distance_resolution = tof_info.distance_resolution; - tof_info_msg.amplitude_resolution = tof_info.amplitude_resolution; - tof_info_msg.amp_normalization_factors = tof_info.amp_normalization_factors; - tof_info_msg.exposure_timestamps_ns = tof_info.exposure_timestamps_ns; - tof_info_msg.exposure_times_s = tof_info.exposure_times_s; - tof_info_msg.illu_temperature = tof_info.illu_temperature; - tof_info_msg.mode = std::string(std::begin(tof_info.mode), std::end(tof_info.mode)); - tof_info_msg.imager = std::string(std::begin(tof_info.imager), std::end(tof_info.imager)); - - tof_info_msg.extrinsics.header = header; - tof_info_msg.extrinsics.tx = tof_info.extrinsic_optic_to_user.trans_x; - tof_info_msg.extrinsics.ty = tof_info.extrinsic_optic_to_user.trans_y; - tof_info_msg.extrinsics.tz = tof_info.extrinsic_optic_to_user.trans_z; - tof_info_msg.extrinsics.rot_x = tof_info.extrinsic_optic_to_user.rot_x; - tof_info_msg.extrinsics.rot_y = tof_info.extrinsic_optic_to_user.rot_y; - tof_info_msg.extrinsics.rot_z = tof_info.extrinsic_optic_to_user.rot_z; - - tof_info_msg.intrinsics.header = header; - tof_info_msg.intrinsics.model_id = tof_info.intrinsic_calibration.model_id; - tof_info_msg.intrinsics.model_parameters = tof_info.intrinsic_calibration.model_parameters; - - tof_info_msg.inverse_intrinsics.header = header; - tof_info_msg.inverse_intrinsics.model_id = tof_info.inverse_intrinsic_calibration.model_id; - tof_info_msg.inverse_intrinsics.model_parameters = tof_info.inverse_intrinsic_calibration.model_parameters; - } - catch (...) - { - RCLCPP_ERROR(logger, "Failed to read tof info."); - } - - return tof_info_msg; -} - + const rclcpp::Logger& logger); ifm3d_ros2::msg::TOFInfo ifm3d_to_tof_info(ifm3d::Buffer&& buffer, const std_msgs::msg::Header& header, - const rclcpp::Logger& logger) - -{ - return ifm3d_to_tof_info(buffer, header, logger); -} - + const rclcpp::Logger& logger); +nav2_msgs::msg::Costmap ifm3d_to_ros_costmap(ifm3d::Buffer& buffer, const std_msgs::msg::Header& header, + const rclcpp::Logger& logger); +nav2_msgs::msg::Costmap ifm3d_to_ros_costmap(ifm3d::Buffer&& buffer, const std_msgs::msg::Header& header, + const rclcpp::Logger& logger); +rclcpp::Time ifm3d_to_ros_time(const TimePointT& time_point); +geometry_msgs::msg::TransformStamped trans_rot_to_optical_mount_link(std::vector trans_rot, + std::uint64_t timestamp, + std::string mounting_frame_name, + std::string optical_frame_name); +bool ifm3d_extrinsic_opt_to_user_to_optical_mount_link(ifm3d::calibration::ExtrinsicOpticToUser opt_to_user, + std::uint64_t timestamp, std::string mounting_frame_name, + std::string optical_frame_name, const rclcpp::Logger& logger, + geometry_msgs::msg::TransformStamped& tf); +bool ifm3d_rgb_info_to_optical_mount_link(ifm3d::Buffer& buffer, std::string mounting_frame_name, + std::string optical_frame_name, const rclcpp::Logger& logger, + geometry_msgs::msg::TransformStamped& tf); +bool ifm3d_rgb_info_to_optical_mount_link(ifm3d::Buffer&& buffer, std::string mounting_frame_name, + std::string optical_frame_name, const rclcpp::Logger& logger, + geometry_msgs::msg::TransformStamped& tf); +bool ifm3d_tof_info_to_optical_mount_link(ifm3d::Buffer& buffer, std::string mounting_frame_name, + std::string optical_frame_name, const rclcpp::Logger& logger, + geometry_msgs::msg::TransformStamped& tf); +bool ifm3d_tof_info_to_optical_mount_link(ifm3d::Buffer&& buffer, std::string mounting_frame_name, + std::string optical_frame_name, const rclcpp::Logger& logger, + geometry_msgs::msg::TransformStamped& tf); } // namespace ifm3d_ros2 -#endif \ No newline at end of file +#endif // IFM3D_ROS2_BUFFER_CONVERSIONS_HPP_ diff --git a/include/ifm3d_ros2/buffer_id_utils.hpp b/include/ifm3d_ros2/buffer_id_utils.hpp index c39b63c..6dfae86 100644 --- a/include/ifm3d_ros2/buffer_id_utils.hpp +++ b/include/ifm3d_ros2/buffer_id_utils.hpp @@ -1,3 +1,9 @@ +// -*- c++ -*- +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright (C) 2024 ifm electronic, gmbh + */ + #ifndef IFM3D_ROS2_CONSTANTS_HPP_ #define IFM3D_ROS2_CONSTANTS_HPP_ @@ -5,7 +11,9 @@ #include #include -#include "ifm3d/fg/frame.h" +#include +#include +#include namespace ifm3d_ros2 { @@ -18,6 +26,7 @@ enum data_stream_type { rgb_2d, tof_3d, + ods, }; /** @@ -25,14 +34,16 @@ enum data_stream_type */ enum message_type { - intrinsics, compressed_image, extrinsics, + intrinsics, inverse_intrinsics, + occupancy_grid, pointcloud, raw_image, rgb_info, tof_info, + zones, not_implemented, }; @@ -42,37 +53,7 @@ enum message_type * It is not declared const because the operator[] of std::map would not be available. * String are taken from the ifm3d SDK (modules/pybind11/src/bindings/frame.h) */ -std::map buffer_id_map = { - { "RADIAL_DISTANCE_IMAGE", ifm3d::buffer_id::RADIAL_DISTANCE_IMAGE }, - { "NORM_AMPLITUDE_IMAGE", ifm3d::buffer_id::NORM_AMPLITUDE_IMAGE }, - { "AMPLITUDE_IMAGE", ifm3d::buffer_id::AMPLITUDE_IMAGE }, - { "GRAYSCALE_IMAGE", ifm3d::buffer_id::GRAYSCALE_IMAGE }, - { "RADIAL_DISTANCE_NOISE", ifm3d::buffer_id::RADIAL_DISTANCE_NOISE }, - { "REFLECTIVITY", ifm3d::buffer_id::REFLECTIVITY }, - { "CARTESIAN_X_COMPONENT", ifm3d::buffer_id::CARTESIAN_X_COMPONENT }, - { "CARTESIAN_Y_COMPONENT", ifm3d::buffer_id::CARTESIAN_Y_COMPONENT }, - { "CARTESIAN_Z_COMPONENT", ifm3d::buffer_id::CARTESIAN_Z_COMPONENT }, - { "CARTESIAN_ALL", ifm3d::buffer_id::CARTESIAN_ALL }, - { "UNIT_VECTOR_ALL", ifm3d::buffer_id::UNIT_VECTOR_ALL }, - { "MONOCHROM_2D_12BIT", ifm3d::buffer_id::MONOCHROM_2D_12BIT }, - { "MONOCHROM_2D", ifm3d::buffer_id::MONOCHROM_2D }, - { "JPEG_IMAGE", ifm3d::buffer_id::JPEG_IMAGE }, - { "CONFIDENCE_IMAGE", ifm3d::buffer_id::CONFIDENCE_IMAGE }, - { "DIAGNOSTIC", ifm3d::buffer_id::DIAGNOSTIC }, - { "JSON_DIAGNOSTIC", ifm3d::buffer_id::JSON_DIAGNOSTIC }, - { "EXTRINSIC_CALIB", ifm3d::buffer_id::EXTRINSIC_CALIB }, - { "INTRINSIC_CALIB", ifm3d::buffer_id::INTRINSIC_CALIB }, - { "INVERSE_INTRINSIC_CALIBRATION", ifm3d::buffer_id::INVERSE_INTRINSIC_CALIBRATION }, - { "TOF_INFO", ifm3d::buffer_id::TOF_INFO }, - { "RGB_INFO", ifm3d::buffer_id::RGB_INFO }, - { "JSON_MODEL", ifm3d::buffer_id::JSON_MODEL }, - { "ALGO_DEBUG", ifm3d::buffer_id::ALGO_DEBUG }, - { "O3R_ODS_OCCUPANCY_GRID", ifm3d::buffer_id::O3R_ODS_OCCUPANCY_GRID }, - { "O3R_ODS_INFO", ifm3d::buffer_id::O3R_ODS_INFO }, - { "XYZ", ifm3d::buffer_id::XYZ }, - { "EXPOSURE_TIME", ifm3d::buffer_id::EXPOSURE_TIME }, - { "ILLUMINATION_TEMP", ifm3d::buffer_id::ILLUMINATION_TEMP } -}; +extern std::map buffer_id_map; /** * @brief mapping buffer_ids to data_stream_type where they are available @@ -81,39 +62,7 @@ std::map buffer_id_map = { * as some buffer might be available for different data_stream_type * It is not declared const because equal_range() of std::multimap would not be available. */ -std::multimap data_stream_type_map = { - { ifm3d::buffer_id::RADIAL_DISTANCE_IMAGE, data_stream_type::tof_3d }, - { ifm3d::buffer_id::NORM_AMPLITUDE_IMAGE, data_stream_type::tof_3d }, - { ifm3d::buffer_id::AMPLITUDE_IMAGE, data_stream_type::tof_3d }, - { ifm3d::buffer_id::GRAYSCALE_IMAGE, data_stream_type::tof_3d }, - { ifm3d::buffer_id::RADIAL_DISTANCE_NOISE, data_stream_type::tof_3d }, - { ifm3d::buffer_id::REFLECTIVITY, data_stream_type::tof_3d }, - { ifm3d::buffer_id::CARTESIAN_X_COMPONENT, data_stream_type::tof_3d }, - { ifm3d::buffer_id::CARTESIAN_Y_COMPONENT, data_stream_type::tof_3d }, - { ifm3d::buffer_id::CARTESIAN_Z_COMPONENT, data_stream_type::tof_3d }, - { ifm3d::buffer_id::CARTESIAN_ALL, data_stream_type::tof_3d }, - { ifm3d::buffer_id::UNIT_VECTOR_ALL, data_stream_type::tof_3d }, - { ifm3d::buffer_id::MONOCHROM_2D_12BIT, data_stream_type::rgb_2d }, - { ifm3d::buffer_id::MONOCHROM_2D, data_stream_type::rgb_2d }, - { ifm3d::buffer_id::JPEG_IMAGE, data_stream_type::rgb_2d }, - { ifm3d::buffer_id::CONFIDENCE_IMAGE, data_stream_type::tof_3d }, - { ifm3d::buffer_id::DIAGNOSTIC, data_stream_type::tof_3d }, - { ifm3d::buffer_id::DIAGNOSTIC, data_stream_type::rgb_2d }, - { ifm3d::buffer_id::JSON_DIAGNOSTIC, data_stream_type::tof_3d }, - { ifm3d::buffer_id::JSON_DIAGNOSTIC, data_stream_type::rgb_2d }, - { ifm3d::buffer_id::EXTRINSIC_CALIB, data_stream_type::tof_3d }, - { ifm3d::buffer_id::INTRINSIC_CALIB, data_stream_type::tof_3d }, - { ifm3d::buffer_id::INVERSE_INTRINSIC_CALIBRATION, data_stream_type::tof_3d }, - { ifm3d::buffer_id::TOF_INFO, data_stream_type::tof_3d }, - { ifm3d::buffer_id::RGB_INFO, data_stream_type::rgb_2d }, - { ifm3d::buffer_id::JSON_MODEL, data_stream_type::tof_3d }, - { ifm3d::buffer_id::ALGO_DEBUG, data_stream_type::tof_3d }, - { ifm3d::buffer_id::O3R_ODS_OCCUPANCY_GRID, data_stream_type::tof_3d }, - { ifm3d::buffer_id::O3R_ODS_INFO, data_stream_type::tof_3d }, - { ifm3d::buffer_id::XYZ, data_stream_type::tof_3d }, - { ifm3d::buffer_id::EXPOSURE_TIME, data_stream_type::tof_3d }, - { ifm3d::buffer_id::ILLUMINATION_TEMP, data_stream_type::tof_3d } -}; +extern std::multimap data_stream_type_map; /** * @brief mapping buffer_ids to message type enum @@ -122,37 +71,7 @@ std::multimap data_stream_type_map = { * e.g. to decide on which ROS message type shall be used. * It is not declared const because the operator[] of std::map would not be available. */ -std::map message_type_map = { - { ifm3d::buffer_id::RADIAL_DISTANCE_IMAGE, message_type::raw_image }, - { ifm3d::buffer_id::NORM_AMPLITUDE_IMAGE, message_type::raw_image }, - { ifm3d::buffer_id::AMPLITUDE_IMAGE, message_type::raw_image }, - { ifm3d::buffer_id::GRAYSCALE_IMAGE, message_type::not_implemented }, - { ifm3d::buffer_id::RADIAL_DISTANCE_NOISE, message_type::not_implemented }, - { ifm3d::buffer_id::REFLECTIVITY, message_type::not_implemented }, - { ifm3d::buffer_id::CARTESIAN_X_COMPONENT, message_type::not_implemented }, - { ifm3d::buffer_id::CARTESIAN_Y_COMPONENT, message_type::not_implemented }, - { ifm3d::buffer_id::CARTESIAN_Z_COMPONENT, message_type::not_implemented }, - { ifm3d::buffer_id::CARTESIAN_ALL, message_type::not_implemented }, - { ifm3d::buffer_id::UNIT_VECTOR_ALL, message_type::not_implemented }, - { ifm3d::buffer_id::MONOCHROM_2D_12BIT, message_type::not_implemented }, - { ifm3d::buffer_id::MONOCHROM_2D, message_type::not_implemented }, - { ifm3d::buffer_id::JPEG_IMAGE, message_type::compressed_image }, - { ifm3d::buffer_id::CONFIDENCE_IMAGE, message_type::raw_image }, - { ifm3d::buffer_id::DIAGNOSTIC, message_type::not_implemented }, - { ifm3d::buffer_id::JSON_DIAGNOSTIC, message_type::not_implemented }, - { ifm3d::buffer_id::EXTRINSIC_CALIB, message_type::extrinsics }, - { ifm3d::buffer_id::INTRINSIC_CALIB, message_type::intrinsics }, - { ifm3d::buffer_id::INVERSE_INTRINSIC_CALIBRATION, message_type::inverse_intrinsics }, - { ifm3d::buffer_id::TOF_INFO, message_type::tof_info }, - { ifm3d::buffer_id::RGB_INFO, message_type::rgb_info }, - { ifm3d::buffer_id::JSON_MODEL, message_type::not_implemented }, - { ifm3d::buffer_id::ALGO_DEBUG, message_type::not_implemented }, - { ifm3d::buffer_id::O3R_ODS_OCCUPANCY_GRID, message_type::not_implemented }, - { ifm3d::buffer_id::O3R_ODS_INFO, message_type::not_implemented }, - { ifm3d::buffer_id::XYZ, message_type::pointcloud }, - { ifm3d::buffer_id::EXPOSURE_TIME, message_type::not_implemented }, - { ifm3d::buffer_id::ILLUMINATION_TEMP, message_type::not_implemented } -}; +extern std::map message_type_map; /** * @brief mapping buffer_ids to topic names @@ -161,37 +80,7 @@ std::map message_type_map = { * and backwards compatibility. * It is not declared const because the operator[] of std::map would not be available. */ -std::map topic_name_map = { - { ifm3d::buffer_id::RADIAL_DISTANCE_IMAGE, "distance" }, - { ifm3d::buffer_id::NORM_AMPLITUDE_IMAGE, "amplitude" }, - { ifm3d::buffer_id::AMPLITUDE_IMAGE, "raw_amplitude" }, - { ifm3d::buffer_id::GRAYSCALE_IMAGE, "GRAYSCALE_IMAGE" }, - { ifm3d::buffer_id::RADIAL_DISTANCE_NOISE, "RADIAL_DISTANCE_NOISE" }, - { ifm3d::buffer_id::REFLECTIVITY, "REFLECTIVITY" }, - { ifm3d::buffer_id::CARTESIAN_X_COMPONENT, "CARTESIAN_X_COMPONENT" }, - { ifm3d::buffer_id::CARTESIAN_Y_COMPONENT, "CARTESIAN_Y_COMPONENT" }, - { ifm3d::buffer_id::CARTESIAN_Z_COMPONENT, "CARTESIAN_Z_COMPONENT" }, - { ifm3d::buffer_id::CARTESIAN_ALL, "CARTESIAN_ALL" }, - { ifm3d::buffer_id::UNIT_VECTOR_ALL, "UNIT_VECTOR_ALL" }, - { ifm3d::buffer_id::MONOCHROM_2D_12BIT, "MONOCHROM_2D_12BIT" }, - { ifm3d::buffer_id::MONOCHROM_2D, "MONOCHROM_2D" }, - { ifm3d::buffer_id::JPEG_IMAGE, "rgb" }, - { ifm3d::buffer_id::CONFIDENCE_IMAGE, "confidence" }, - { ifm3d::buffer_id::DIAGNOSTIC, "DIAGNOSTIC" }, - { ifm3d::buffer_id::JSON_DIAGNOSTIC, "JSON_DIAGNOSTIC" }, - { ifm3d::buffer_id::EXTRINSIC_CALIB, "extrinsics" }, - { ifm3d::buffer_id::INTRINSIC_CALIB, "INTRINSIC_CALIB" }, - { ifm3d::buffer_id::INVERSE_INTRINSIC_CALIBRATION, "INVERSE_INTRINSIC_CALIBRATION" }, - { ifm3d::buffer_id::TOF_INFO, "TOF_INFO" }, - { ifm3d::buffer_id::RGB_INFO, "RGB_INFO" }, - { ifm3d::buffer_id::JSON_MODEL, "JSON_MODEL" }, - { ifm3d::buffer_id::ALGO_DEBUG, "ALGO_DEBUG" }, - { ifm3d::buffer_id::O3R_ODS_OCCUPANCY_GRID, "O3R_ODS_OCCUPANCY_GRID" }, - { ifm3d::buffer_id::O3R_ODS_INFO, "O3R_ODS_INFO" }, - { ifm3d::buffer_id::XYZ, "cloud" }, - { ifm3d::buffer_id::EXPOSURE_TIME, "EXPOSURE_TIME" }, - { ifm3d::buffer_id::ILLUMINATION_TEMP, "ILLUMINATION_TEMP" } -}; +extern std::map topic_name_map; /** * @brief Lookup buffer_id associated with a string, using the buffer_id_map @@ -201,17 +90,7 @@ std::map topic_name_map = { * @return true if string is valid * @return false if no buffer_id is associated with the string */ -bool convert(const std::string& string, ifm3d::buffer_id& buffer_id) -{ - if (!buffer_id_map.count(string)) - { - // key does not exist - return false; - } - - buffer_id = buffer_id_map[string]; - return true; -} +bool convert(const std::string& string, ifm3d::buffer_id& buffer_id); /** * @brief Lookup string associated with a buffer_id, using the buffer_id_map @@ -221,21 +100,7 @@ bool convert(const std::string& string, ifm3d::buffer_id& buffer_id) * @return true if buffer_id is valid * @return false there is no entry in the map for the given buffer_id */ -bool convert(const ifm3d::buffer_id& buffer_id, std::string& string) -{ - // Iterate over all map entries - for (auto const& [key, value] : buffer_id_map) - { - if (value == buffer_id) - { - string = key; - return true; - } - } - - // buffer_id not found - return false; -} +bool convert(const ifm3d::buffer_id& buffer_id, std::string& string); /** * @brief Returns the subset of the provided buffer_ids, which are compatible with a given data_stream_type @@ -245,75 +110,17 @@ bool convert(const ifm3d::buffer_id& buffer_id, std::string& string) * @return std::vector subset of input_ids which is available for given type */ std::vector buffer_ids_for_data_stream_type(const std::vector& input_ids, - const data_stream_type& type) -{ - typedef std::multimap::iterator mm_iterator; - - std::vector ret_vector; - - for (ifm3d::buffer_id input_id : input_ids) - { - // Get iterators for multimap subsection of given buffer_id - // Remember, multimaps are always sorted by their key - std::pair result = data_stream_type_map.equal_range(input_id); - - // Look for matching data_streamtype, iterating over the subsection - for (mm_iterator it = result.first; it != result.second; it++) - { - if (it->second == type) - { - ret_vector.push_back(input_id); - } - } - } - - return ret_vector; -} + const data_stream_type& type); /** * Helper to create one string from a vector of strings */ -std::string vector_to_string(const std::vector& vector) -{ - if (vector.empty()) - { - return std::string(""); - } - - std::ostringstream stream; - const std::string delimiter = ", "; - - std::copy(vector.begin(), vector.end(), std::ostream_iterator(stream, delimiter.c_str())); - - const std::string& output = stream.str(); - - return output.substr(0, output.length() - delimiter.length()); -} +std::string vector_to_string(const std::vector& vector); /** * Helper to create one string from a vector of buffer_ids */ -std::string vector_to_string(const std::vector& vector) -{ - if (vector.empty()) - { - return std::string(""); - } - - std::ostringstream stream; - const std::string delimiter = ", "; - - for (const auto& id : vector) - { - std::string string; - convert(id, string); - stream << string << delimiter; - } - - const std::string& output = stream.str(); - - return output.substr(0, output.length() - delimiter.length()); -} +std::string vector_to_string(const std::vector& vector); } // namespace buffer_id_utils diff --git a/include/ifm3d_ros2/camera_node.hpp b/include/ifm3d_ros2/camera_node.hpp index 90b5d6f..04e9afd 100644 --- a/include/ifm3d_ros2/camera_node.hpp +++ b/include/ifm3d_ros2/camera_node.hpp @@ -1,7 +1,7 @@ // -*- c++ -*- /* * SPDX-License-Identifier: Apache-2.0 - * Copyright (C) 2019 ifm electronic, gmbh + * Copyright (C) 2024 ifm electronic, gmbh */ #ifndef IFM3D_ROS2_CAMERA_NODE_HPP_ @@ -14,86 +14,27 @@ #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 using TC_RETVAL = rclcpp_lifecycle::node_interfaces::LifecycleNodeInterface::CallbackReturn; -using DiagnosticStatusMsg = diagnostic_msgs::msg::DiagnosticStatus; -using DiagnosticArrayMsg = diagnostic_msgs::msg::DiagnosticArray; -using DiagnosticArrayPublisher = std::shared_ptr>; - -using ImageMsg = sensor_msgs::msg::Image; -using ImagePublisher = std::shared_ptr>; - -using CompressedImageMsg = sensor_msgs::msg::CompressedImage; -using CompressedImagePublisher = std::shared_ptr>; - -using PCLMsg = sensor_msgs::msg::PointCloud2; -using PCLPublisher = std::shared_ptr>; - using ExtrinsicsMsg = ifm3d_ros2::msg::Extrinsics; using ExtrinsicsPublisher = std::shared_ptr>; -using CameraInfoMsg = sensor_msgs::msg::CameraInfo; -using CameraInfoPublisher = std::shared_ptr>; - -using IntrinsicsMsg = ifm3d_ros2::msg::Intrinsics; -using IntrinsicsPublisher = std::shared_ptr>; - -using InverseIntrinsicsMsg = ifm3d_ros2::msg::InverseIntrinsics; -using InverseIntrinsicsPublisher = std::shared_ptr>; - -using TOFInfoMsg = ifm3d_ros2::msg::TOFInfo; -using TOFInfoPublisher = std::shared_ptr>; - -using RGBInfoMsg = ifm3d_ros2::msg::RGBInfo; -using RGBInfoPublisher = std::shared_ptr>; - -using DumpRequest = std::shared_ptr; -using DumpResponse = std::shared_ptr; -using DumpService = ifm3d_ros2::srv::Dump; -using DumpServer = rclcpp::Service::SharedPtr; - -using ConfigRequest = std::shared_ptr; -using ConfigResponse = std::shared_ptr; -using ConfigService = ifm3d_ros2::srv::Config; -using ConfigServer = rclcpp::Service::SharedPtr; - -using SoftoffRequest = std::shared_ptr; -using SoftoffResponse = std::shared_ptr; -using SoftoffService = ifm3d_ros2::srv::Softoff; -using SoftoffServer = rclcpp::Service::SharedPtr; - -using SoftonRequest = std::shared_ptr; -using SoftonResponse = std::shared_ptr; -using SoftonService = ifm3d_ros2::srv::Softon; -using SoftonServer = rclcpp::Service::SharedPtr; - namespace ifm3d_ros2 { /** @@ -199,26 +140,6 @@ class IFM3D_ROS2_PUBLIC CameraNode : public rclcpp_lifecycle::LifecycleNode TC_RETVAL on_error(const rclcpp_lifecycle::State& prev_state) override; protected: - /** - * Implementation of the Dump service. - */ - void Dump(std::shared_ptr request_header, DumpRequest req, DumpResponse resp); - - /** - * Implementation of the Config service. - */ - void Config(std::shared_ptr request_header, ConfigRequest req, ConfigResponse resp); - - /** - * Implementation of the SoftOff service. - */ - void Softoff(std::shared_ptr request_header, SoftoffRequest req, SoftoffResponse resp); - - /** - * Implementation of the SoftOn service. - */ - void Softon(std::shared_ptr request_header, SoftonRequest req, SoftonResponse resp); - /** * Declares parameters and default values */ @@ -254,112 +175,45 @@ class IFM3D_ROS2_PUBLIC CameraNode : public rclcpp_lifecycle::LifecycleNode */ void async_notification_callback(const std::string& s1, const std::string& s2); - /** - * Creates a DiagnosticStatus message from a JSON string. - * - */ - DiagnosticStatusMsg create_diagnostic_status(const uint8_t level, const std::string& json_msg); - - /** - * @brief Create publishers according to buffer_id_list_. - * - * First, this clears internal publisher lists. - * Populates the internal publisher lists with new Publishers. - * Uses buffer_id_utils to determine message types. - */ - void initialize_publishers(); - - /** - * Activates all publishers on the internal publisher lists. - */ - void activate_publishers(); - - /** - * Deactivates all publishers on the internal publisher lists. - */ - void deactivate_publishers(); - - /** - * Publish the transform from the mounting link to the optical link as static tf - */ - void publish_optical_link_transform(); - - /** - * @brief Publish the transform from the mounting link to the cloud link as static tf if it changed - * - * A change can either be new translational/rotational data from extrinsics or - * a name change of one of the frames. - * - * @param msg ExtrinsicsMsg from camera - */ - void publish_cloud_link_transform_if_changed(const ExtrinsicsMsg& msg); - - ifm3d_ros2::buffer_id_utils::data_stream_type stream_type_from_port_info(const std::vector& ports, - const uint16_t pcic_port); + ifm3d_ros2::buffer_id_utils::data_stream_type stream_type_from_port_info(const ifm3d::PortInfo port_info); private: rclcpp::Logger logger_; - /// For diagnostics, "/" - std::string hardware_id_; - // ifm3d camera and framegrabber pointers - ifm3d::O3R::Ptr cam_{}; + ifm3d::O3R::Ptr o3r_{}; ifm3d::FrameGrabber::Ptr fg_{}; ifm3d::FrameGrabber::Ptr fg_diag_{}; + // Holds the list of buffers that are provided to the framegrabber + std::vector> buffer_list_{}; + + std::variant, std::shared_ptr> data_module_; + std::shared_ptr diag_module_; + std::vector> modules_; - /// global mutex on ifm3d core data structures `cam_`, `fg_` - std::mutex gil_{}; + /// global mutex on ifm3d core data structures `o3r_`, `fg_` + std::shared_ptr gil_{}; - /// Differentiation between 2D and 3D data stream, derived from ifm3d::O3R cam_ + /// Differentiation between 2D and 3D data stream, derived from ifm3d::O3R o3r_ ifm3d_ros2::buffer_id_utils::data_stream_type data_stream_type_; // Values read from parameters - std::vector buffer_id_list_{}; + std::string config_file_{}; std::string ip_{}; std::uint16_t pcic_port_{}; - std::string tf_cloud_link_frame_name_{}; - bool tf_cloud_link_publish_transform_{}; - std::string tf_mounting_link_frame_name_{}; - std::string tf_optical_link_frame_name_{}; - bool tf_optical_link_publish_transform_{}; - std::vector tf_optical_link_transform_{}; std::uint16_t xmlrpc_port_{}; - std::string diag_mode_{}; - // Values read from incomming image buffers - uint32_t width_; - uint32_t height_; + // For backward compatibility, we get the port name + // from the provided pcic_port. + ifm3d::PortInfo port_info_{}; /// Subscription to parameter changes std::shared_ptr param_subscriber_; /// Callbacks need to be stored to work properly; using a map with parameter name as key std::map registered_param_callbacks_; - // TF handling - std::shared_ptr tf_static_broadcaster_; - geometry_msgs::msg::TransformStamped cloud_link_transform_; - // Service Servers - DumpServer dump_srv_{}; - ConfigServer config_srv_{}; - SoftoffServer soft_off_srv_{}; - SoftonServer soft_on_srv_{}; - - // Publishers - DiagnosticArrayPublisher diagnostic_publisher_; - std::map image_publishers_; - std::map compressed_image_publishers_; - std::map pcl_publishers_; - std::map extrinsics_publishers_; - std::map camera_info_publishers_; - std::map rgb_info_publishers_; - std::map tof_info_publishers_; - std::map intrinsics_publishers_; - std::map inverse_intrinsics_publishers_; - - // Timer for publishing diagnostics - rclcpp::TimerBase::SharedPtr diagnostic_timer_; + std::shared_ptr base_services_{}; }; // end: class CameraNode diff --git a/include/ifm3d_ros2/camera_tf_publisher.hpp b/include/ifm3d_ros2/camera_tf_publisher.hpp new file mode 100644 index 0000000..5e5507d --- /dev/null +++ b/include/ifm3d_ros2/camera_tf_publisher.hpp @@ -0,0 +1,57 @@ +// -*- c++ -*- +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright (C) 2024 ifm electronic, gmbh + */ + +#ifndef IFM3D_ROS2_CAMERA_TF_PUBLISHER_HPP_ +#define IFM3D_ROS2_CAMERA_TF_PUBLISHER_HPP_ + +#include +#include +#include +#include +#include + +namespace ifm3d_ros2 +{ +class CameraTfPublisher +{ +public: + CameraTfPublisher(rclcpp_lifecycle::LifecycleNode::SharedPtr node_ptr, ifm3d::O3R::Ptr o3r_ptr, std::string port, + std::string base_frame_name, std::string mounting_frame_name, std::string optical_frame_name); + CameraTfPublisher(rclcpp_lifecycle::LifecycleNode::SharedPtr node_ptr, ifm3d::O3R::Ptr o3r_ptr, std::string port); + + bool update_and_publish_tf_if_changed(const geometry_msgs::msg::TransformStamped& new_tf_base_to_optical); + + std::string tf_to_string(geometry_msgs::msg::TransformStamped tf); + + std::string tf_base_link_frame_name_; + std::string tf_mounting_link_frame_name_; + std::string tf_optical_link_frame_name_; + bool tf_publish_mounting_to_optical_; + bool tf_publish_base_to_mounting_; + +protected: + bool transform_identical(geometry_msgs::msg::TransformStamped tf1, geometry_msgs::msg::TransformStamped tf2); + + geometry_msgs::msg::TransformStamped read_tf_base_to_mounting_from_device_config(builtin_interfaces::msg::Time stamp); + + geometry_msgs::msg::TransformStamped get_tf_mounting_to_optical( + builtin_interfaces::msg::Time stamp, geometry_msgs::msg::TransformStamped tf_base_to_optical, + geometry_msgs::msg::TransformStamped tf_base_to_mounting); + + geometry_msgs::msg::TransformStamped calculate_tf_base_to_optical( + builtin_interfaces::msg::Time stamp, geometry_msgs::msg::TransformStamped tf_base_to_mounting, + geometry_msgs::msg::TransformStamped tf_mounting_to_optical); + +private: + rclcpp_lifecycle::LifecycleNode::SharedPtr node_ptr_; + ifm3d::O3R::Ptr o3r_ptr_; + std::string port_; + std::shared_ptr tf_static_broadcaster_; + geometry_msgs::msg::TransformStamped tf_base_to_optical_, tf_base_to_mounting_, tf_mounting_to_optical_; +}; + +} // namespace ifm3d_ros2 +#endif \ No newline at end of file diff --git a/include/ifm3d_ros2/diag_module.hpp b/include/ifm3d_ros2/diag_module.hpp new file mode 100644 index 0000000..fd876e3 --- /dev/null +++ b/include/ifm3d_ros2/diag_module.hpp @@ -0,0 +1,93 @@ +// -*- c++ -*- +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright (C) 2024 ifm electronic, gmbh + */ + +#ifndef IFM3D_ROS2_DIAG_MODULE_HPP_ +#define IFM3D_ROS2_DIAG_MODULE_HPP_ + +#include +#include +#include +#include +#include + +namespace ifm3d_ros2 +{ +class DiagModule : public FunctionModule, public std::enable_shared_from_this +{ + using DiagnosticStatusMsg = diagnostic_msgs::msg::DiagnosticStatus; + using DiagnosticArrayMsg = diagnostic_msgs::msg::DiagnosticArray; + using DiagnosticArrayPublisher = std::shared_ptr>; + +public: + DiagModule(rclcpp::Logger logger, rclcpp_lifecycle::LifecycleNode::SharedPtr node_ptr, + std::shared_ptr o3r); + /** + * @brief Main function that handles diagnostic messages. + * When an error is received, it is directly published to the /diagnostic topic. + * + * @param i Error code + * @param s Error description in JSON format + */ + void handle_error(int i, const std::string& s); + /** + * @brief Main function that handles notifications. + * When a notification is received, it is directly published to the /diagnostic topic + * with the level OK. + * + * @param s1 Notification id + * @param s2 Notification description in JSON + */ + void handle_notification(const std::string& s1, const std::string& s2); + + /** + * @brief Callback called every second, as defined by the diagnostic_timer_. + * It formats and publishes the list of diagnostic messages coming from the device. + * + */ + void periodic_diag_callback(); + + /** + * Utility functions to creates a DiagnosticStatusMsg and a DiagnosticArrayMsg + * from the received JSON string. + * + */ + DiagnosticStatusMsg create_diagnostic_status(const uint8_t level, ifm3d::json parsed_json); + DiagnosticArrayMsg create_diagnostic_message(const uint8_t level, const std::string& json_msg); + + const std::string get_name() + { + return "diag_module"; + }; + + rclcpp_lifecycle::LifecycleNode::CallbackReturn on_configure(const rclcpp_lifecycle::State& previous_state); + + rclcpp_lifecycle::LifecycleNode::CallbackReturn on_cleanup(const rclcpp_lifecycle::State& previous_state); + + rclcpp_lifecycle::LifecycleNode::CallbackReturn on_shutdown(const rclcpp_lifecycle::State& previous_state); + + rclcpp_lifecycle::LifecycleNode::CallbackReturn on_activate(const rclcpp_lifecycle::State& previous_state); + + rclcpp_lifecycle::LifecycleNode::CallbackReturn on_deactivate(const rclcpp_lifecycle::State& previous_state); + + rclcpp_lifecycle::LifecycleNode::CallbackReturn on_error(const rclcpp_lifecycle::State& previous_state); + +private: + /// Pointer to the shared O3R object to access diagnostic using the GetDiagnostic methods. + std::shared_ptr o3r_; + + /// Pointer to the node to access the ROS2 API. + rclcpp_lifecycle::LifecycleNode::SharedPtr node_ptr_; + + rclcpp::TimerBase::SharedPtr diagnostic_timer_; + + DiagnosticArrayPublisher diagnostic_publisher_; + /// A unique identifier for the diagnostic messages. + std::string hardware_id_; +}; + +} // namespace ifm3d_ros2 + +#endif \ No newline at end of file diff --git a/include/ifm3d_ros2/function_module.hpp b/include/ifm3d_ros2/function_module.hpp new file mode 100644 index 0000000..588b55f --- /dev/null +++ b/include/ifm3d_ros2/function_module.hpp @@ -0,0 +1,57 @@ +// -*- c++ -*- +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright (C) 2024 ifm electronic, gmbh + */ + +#ifndef IFM3D_ROS2_FUNCTION_MODULE_HPP_ +#define IFM3D_ROS2_FUNCTION_MODULE_HPP_ + +#include + +#include +#include +#include +#include "rclcpp_lifecycle/node_interfaces/lifecycle_node_interface.hpp" + +namespace ifm3d_ros2 +{ +/** + * @brief Abstract representation of encapsulated camera functionality. + * + * Wraps a set of sub-functions of a camera. + * Follows the lifecycle of rclcpp_lifecycle. + */ +class FunctionModule : public rclcpp_lifecycle::node_interfaces::LifecycleNodeInterface, + public std::enable_shared_from_this +{ +public: + FunctionModule(const rclcpp::Logger& logger); + + virtual void handle_frame(ifm3d::Frame::Ptr frame); + + virtual const std::string get_name() = 0; + + virtual rclcpp_lifecycle::LifecycleNode::CallbackReturn + on_configure(const rclcpp_lifecycle::State& previous_state) = 0; + + virtual rclcpp_lifecycle::LifecycleNode::CallbackReturn on_cleanup(const rclcpp_lifecycle::State& previous_state) = 0; + + virtual rclcpp_lifecycle::LifecycleNode::CallbackReturn + on_shutdown(const rclcpp_lifecycle::State& previous_state) = 0; + + virtual rclcpp_lifecycle::LifecycleNode::CallbackReturn + on_activate(const rclcpp_lifecycle::State& previous_state) = 0; + + virtual rclcpp_lifecycle::LifecycleNode::CallbackReturn + on_deactivate(const rclcpp_lifecycle::State& previous_state) = 0; + + virtual rclcpp_lifecycle::LifecycleNode::CallbackReturn on_error(const rclcpp_lifecycle::State& previous_state) = 0; + +protected: + rclcpp::Logger logger_; +}; + +} // namespace ifm3d_ros2 + +#endif \ No newline at end of file diff --git a/include/ifm3d_ros2/ods_module.hpp b/include/ifm3d_ros2/ods_module.hpp new file mode 100644 index 0000000..6dda50a --- /dev/null +++ b/include/ifm3d_ros2/ods_module.hpp @@ -0,0 +1,78 @@ +// -*- c++ -*- +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright (C) 2024 ifm electronic, gmbh + */ + +#ifndef IFM3D_ROS2_ODS_MODULE_HPP_ +#define IFM3D_ROS2_ODS_MODULE_HPP_ + +#include +#include +#include + +#include +#include + +#include + +namespace ifm3d_ros2 +{ +/** + * @brief Wraps the ODS application. + */ +class OdsModule : public FunctionModule, public std::enable_shared_from_this +{ + using CostmapMsg = nav2_msgs::msg::Costmap; + using CostmapPublisher = std::shared_ptr>; + + using OccupancyGridMsg = nav_msgs::msg::OccupancyGrid; + using OccupancyGridPublisher = std::shared_ptr>; + + using ZonesMsg = ifm3d_ros2::msg::Zones; + using ZonesPublisher = std::shared_ptr>; + +public: + OdsModule(rclcpp::Logger logger, rclcpp_lifecycle::LifecycleNode::SharedPtr node_ptr); + // Main functions that take care of deserializing + // and publishing the ODS data + void handle_frame(ifm3d::Frame::Ptr frame); + nav_msgs::msg::OccupancyGrid extract_ros_occupancy_grid(ifm3d::Frame::Ptr frame); + nav2_msgs::msg::Costmap extract_ros_costmap(ifm3d::Frame::Ptr frame); + ifm3d_ros2::msg::Zones extract_zones(ifm3d::Frame::Ptr frame); + + const std::string get_name() + { + return "ods_module"; + }; + + rclcpp_lifecycle::LifecycleNode::CallbackReturn on_configure(const rclcpp_lifecycle::State& previous_state); + + rclcpp_lifecycle::LifecycleNode::CallbackReturn on_cleanup(const rclcpp_lifecycle::State& previous_state); + + rclcpp_lifecycle::LifecycleNode::CallbackReturn on_shutdown(const rclcpp_lifecycle::State& previous_state); + + rclcpp_lifecycle::LifecycleNode::CallbackReturn on_activate(const rclcpp_lifecycle::State& previous_state); + + rclcpp_lifecycle::LifecycleNode::CallbackReturn on_deactivate(const rclcpp_lifecycle::State& previous_state); + + rclcpp_lifecycle::LifecycleNode::CallbackReturn on_error(const rclcpp_lifecycle::State& previous_state); + +private: + rclcpp_lifecycle::LifecycleNode::SharedPtr node_ptr_; + + OccupancyGridPublisher ros_occupancy_grid_publisher_; + CostmapPublisher ros_costmap_publisher_; + ZonesPublisher zones_publisher_; + + std::string frame_id_; + bool publish_occupancy_grid_; + bool publish_costmap_; + rcl_interfaces::msg::ParameterDescriptor frame_id_descriptor_; + rcl_interfaces::msg::ParameterDescriptor publish_occupancy_grid_descriptor_; + rcl_interfaces::msg::ParameterDescriptor publish_costmap_descriptor_; +}; + +} // namespace ifm3d_ros2 + +#endif \ No newline at end of file diff --git a/include/ifm3d_ros2/ods_node.hpp b/include/ifm3d_ros2/ods_node.hpp new file mode 100644 index 0000000..7cdb075 --- /dev/null +++ b/include/ifm3d_ros2/ods_node.hpp @@ -0,0 +1,206 @@ +// -*- c++ -*- +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright (C) 2024 ifm electronic, gmbh + */ + +#ifndef IFM3D_ROS2_ODS_NODE_HPP_ +#define IFM3D_ROS2_ODS_NODE_HPP_ + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include +#include + +using TC_RETVAL = rclcpp_lifecycle::node_interfaces::LifecycleNodeInterface::CallbackReturn; + +namespace ifm3d_ros2 +{ +/** + * Managed node that implements the ODS application of ifm3d for ROS 2. + */ +class IFM3D_ROS2_PUBLIC OdsNode : public rclcpp_lifecycle::LifecycleNode +{ +public: + /** + * Instantiates the LifecycleNode. At the completion of the ctor, the + * following initialization (beyond calling the parent ctor) has been done: + * + * - A named logger for this node has been initialized + * - tf frame names have been initialzed based on the node name + * - All parameters have been declared and a `set` callback has been + * registered + * - All publishers have been created. + */ + explicit OdsNode(const std::string& node_name, const rclcpp::NodeOptions& opts); + + /** + * Delegates construction to the above ctor. + */ + explicit OdsNode(const rclcpp::NodeOptions& opts); + + /** + * RAII deallocations. As of this writing, given that all structures are + * handled by various types of smart pointers no "real work" needs to be + * done here. However, for debugging purposes we emit a log message so we + * know when the dtor has actually been called and hence when all + * deallocations actually occur. + */ + ~OdsNode() override; + + /** + * Implements the "configuring" transition state + * + * The following operations are performed: + * + * - Parameters are parsed and held locally in instance variables. + * - If requested, the camera clock is synchronized to the system clock + * - The core ifm3d data structures (camera, framegrabber, stlimage buffer) + * are initialized and ready to stream data based upon the requested + * schema mask. + */ + TC_RETVAL on_configure(const rclcpp_lifecycle::State& prev_state) override; + + /** + * Implements the "activating" transition state + * + * The following operations are performed: + * + * - The `on_activate()` method is called on all publishers + * - A new thread is started that will continuous publish image data from + * the camera. + */ + TC_RETVAL on_activate(const rclcpp_lifecycle::State& prev_state) override; + + /** + * Implements the "deactivating" transition state + * + * The following operations are performed: + * + * - The thread that implements the "publish loop" is stopped + * - All publishers can their `on_deactivate()` method called + */ + TC_RETVAL on_deactivate(const rclcpp_lifecycle::State& prev_state) override; + + /** + * Implements the "cleaningup" transition state + * + * The following operations are performed: + * + * - The ifm3d core data structures (camera, framegrabber, stlimage buffer) + * have their dtors called + */ + TC_RETVAL on_cleanup(const rclcpp_lifecycle::State& prev_state) override; + + /** + * Implements the "shuttingdown" transition state + * + * The following operations are performed: + * + * - It is ensured that the publishing loop thread is stopped + */ + TC_RETVAL on_shutdown(const rclcpp_lifecycle::State& prev_state) override; + + /** + * Implements the "errorprocessing" transition state + * + * The following operations are performed: + * + * - The publish_loop thread is stopped (if running) + * - The ifm3d core data structures (camera, framegrabber, stlimage buffer) + * have their dtors called + */ + TC_RETVAL on_error(const rclcpp_lifecycle::State& prev_state) override; + +protected: + /** + * Declares parameters and default values + */ + void init_params(); + + /** + * Reads parameters, needs init_params() to be called beforehand + */ + void parse_params(); + + /** + * Sets the callbacks handling parameter changes at runtime + */ + void set_parameter_event_callbacks(); + + /** + * Callback which receives new Frames from ifm3d + */ + void frame_callback(ifm3d::Frame::Ptr frame); + + /** + * Callback which receives Errors from ifm3d + */ + void error_callback(const ifm3d::Error& error); + + /** + * Callback which receives AsyncErrors from ifm3d + */ + void async_error_callback(int i, const std::string& s); + + /** + * Callback which receives AsyncNotifications from ifm3d + */ + void async_notification_callback(const std::string& s1, const std::string& s2); + +private: + rclcpp::Logger logger_; + + ifm3d::O3R::Ptr o3r_{}; // TODO probably need some other device + ifm3d::FrameGrabber::Ptr fg_{}; + ifm3d::FrameGrabber::Ptr fg_diag_{}; + + std::shared_ptr ods_module_; + std::shared_ptr diag_module_; + std::vector> modules_; + + /// global mutex on ifm3d core data structures `fg_` + std::shared_ptr gil_{}; + + // Values read from parameters + std::string config_file_{}; + std::string ip_{}; + std::uint16_t pcic_port_{}; + std::uint16_t xmlrpc_port_{}; + + ifm3d::PortInfo port_info_{}; + + // Values read from incoming image buffers + uint32_t width_; + uint32_t height_; + + /// Subscription to parameter changes + std::shared_ptr param_subscriber_; + /// Callbacks need to be stored to work properly; using a map with parameter name as key + std::map registered_param_callbacks_; + + // Service Servers + std::shared_ptr base_services_{}; + +}; // end: class OdsNode + +} // namespace ifm3d_ros2 + +#endif // IFM3D_ROS2_ODS_NODE_HPP_ diff --git a/include/ifm3d_ros2/qos.hpp b/include/ifm3d_ros2/qos.hpp index aa4dcea..654e588 100644 --- a/include/ifm3d_ros2/qos.hpp +++ b/include/ifm3d_ros2/qos.hpp @@ -1,7 +1,7 @@ // -*- c++ -*- /* * SPDX-License-Identifier: Apache-2.0 - * Copyright (C) 2019 ifm electronic, gmbh + * Copyright (C) 2024 ifm electronic, gmbh */ #ifndef IFM3D_ROS2_QOS_HPP_ diff --git a/include/ifm3d_ros2/rgb_module.hpp b/include/ifm3d_ros2/rgb_module.hpp new file mode 100644 index 0000000..f77daf9 --- /dev/null +++ b/include/ifm3d_ros2/rgb_module.hpp @@ -0,0 +1,101 @@ +// -*- c++ -*- +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright (C) 2024 ifm electronic, gmbh + */ + +#ifndef IFM3D_ROS2_RGB_MODULE_HPP_ +#define IFM3D_ROS2_RGB_MODULE_HPP_ + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include + +using CompressedImageMsg = sensor_msgs::msg::CompressedImage; +using CompressedImagePublisher = std::shared_ptr>; + +using RGBInfoMsg = ifm3d_ros2::msg::RGBInfo; +using RGBInfoPublisher = std::shared_ptr>; + +using CameraInfoMsg = sensor_msgs::msg::CameraInfo; +using CameraInfoPublisher = std::shared_ptr>; + +namespace ifm3d_ros2 +{ +class RgbModule : public FunctionModule, public std::enable_shared_from_this +{ +public: + RgbModule(rclcpp::Logger logger, rclcpp_lifecycle::LifecycleNode::SharedPtr node_ptr, ifm3d::O3R::Ptr o3r_ptr, + std::string port, uint32_t width, uint32_t height); + void handle_frame(ifm3d::Frame::Ptr frame); + + const std::string get_name() + { + return "rgb_module"; + }; + + rclcpp_lifecycle::LifecycleNode::CallbackReturn on_configure(const rclcpp_lifecycle::State& previous_state); + + rclcpp_lifecycle::LifecycleNode::CallbackReturn on_cleanup(const rclcpp_lifecycle::State& previous_state); + + rclcpp_lifecycle::LifecycleNode::CallbackReturn on_shutdown(const rclcpp_lifecycle::State& previous_state); + + rclcpp_lifecycle::LifecycleNode::CallbackReturn on_activate(const rclcpp_lifecycle::State& previous_state); + + rclcpp_lifecycle::LifecycleNode::CallbackReturn on_deactivate(const rclcpp_lifecycle::State& previous_state); + + rclcpp_lifecycle::LifecycleNode::CallbackReturn on_error(const rclcpp_lifecycle::State& previous_state); + + // The list is accessed in the node, to start the FrameGrabber + std::vector buffer_id_list_{}; + +protected: + void parse_params(); + void set_parameter_event_callbacks(); + +private: + rclcpp_lifecycle::LifecycleNode::SharedPtr node_ptr_; + CameraTfPublisher tf_publisher_; + + // Values read from incoming image buffers + uint32_t width_; + uint32_t height_; + + // Boolean to track first time publishing, so that we only + // publish the static transforms once + bool first_; + + // Publishers + CompressedImagePublisher rgb_publisher_; + RGBInfoPublisher rgb_info_publisher_; + CameraInfoPublisher camera_info_publisher_; + + // Parameters + rcl_interfaces::msg::ParameterDescriptor buffer_id_list_descriptor_; + rcl_interfaces::msg::ParameterDescriptor tf_base_frame_name_descriptor_; + rcl_interfaces::msg::ParameterDescriptor tf_mounting_frame_name_descriptor_; + rcl_interfaces::msg::ParameterDescriptor tf_optical_frame_name_descriptor_; + rcl_interfaces::msg::ParameterDescriptor tf_publish_mounting_to_optical_descriptor_; + rcl_interfaces::msg::ParameterDescriptor tf_publish_base_to_mounting_descriptor_; + + // TF handling + + /// Subscription to parameter changes + std::shared_ptr param_subscriber_; + /// Callbacks need to be stored to work properly; using a map with parameter name as key + std::map registered_param_callbacks_; +}; + +} // namespace ifm3d_ros2 + +#endif \ No newline at end of file diff --git a/include/ifm3d_ros2/services.hpp b/include/ifm3d_ros2/services.hpp new file mode 100644 index 0000000..fadb12e --- /dev/null +++ b/include/ifm3d_ros2/services.hpp @@ -0,0 +1,88 @@ +// -*- c++ -*- +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright (C) 2024 ifm electronic, gmbh + */ + +#ifndef IFM3D_ROS2_SERVICES_HPP_ +#define IFM3D_ROS2_SERVICES_HPP_ + +#include + +#include +#include +#include "rclcpp_lifecycle/node_interfaces/lifecycle_node_interface.hpp" + +#include + +#include +#include +#include +#include +#include +#include + +using DumpRequest = std::shared_ptr; +using DumpResponse = std::shared_ptr; +using DumpService = ifm3d_ros2::srv::Dump; +using DumpServer = rclcpp::Service::SharedPtr; + +using ConfigRequest = std::shared_ptr; +using ConfigResponse = std::shared_ptr; +using ConfigService = ifm3d_ros2::srv::Config; +using ConfigServer = rclcpp::Service::SharedPtr; + +using SoftoffRequest = std::shared_ptr; +using SoftoffResponse = std::shared_ptr; +using SoftoffService = ifm3d_ros2::srv::Softoff; +using SoftoffServer = rclcpp::Service::SharedPtr; + +using SoftonRequest = std::shared_ptr; +using SoftonResponse = std::shared_ptr; +using SoftonService = ifm3d_ros2::srv::Softon; +using SoftonServer = rclcpp::Service::SharedPtr; + +using GetDiagRequest = std::shared_ptr; +using GetDiagResponse = std::shared_ptr; +using GetDiagService = ifm3d_ros2::srv::GetDiag; +using GetDiagServer = rclcpp::Service::SharedPtr; + +namespace ifm3d_ros2 +{ +class BaseServices : public rclcpp_lifecycle::node_interfaces::LifecycleNodeInterface, + public std::enable_shared_from_this +{ +public: + BaseServices(rclcpp::Logger logger, rclcpp_lifecycle::LifecycleNode::SharedPtr node_ptr, ifm3d::O3R::Ptr cam, + ifm3d::PortInfo port_info, std::shared_ptr ifm3d_mutex); + +protected: + void Dump(std::shared_ptr request_header, DumpRequest req, DumpResponse resp); + void Config(std::shared_ptr request_header, ConfigRequest req, ConfigResponse resp); + void Softoff(std::shared_ptr request_header, SoftoffRequest req, SoftoffResponse resp); + void Softon(std::shared_ptr request_header, SoftonRequest req, SoftonResponse resp); + void GetDiag(std::shared_ptr request_header, GetDiagRequest req, GetDiagResponse resp); + + rclcpp::Logger logger_; + +private: + // Need a pointer to node to create services + rclcpp_lifecycle::LifecycleNode::SharedPtr node_ptr_; + + /// global mutex on ifm3d core data structures `fg_` + std::shared_ptr ifm3d_mutex_{}; + + ifm3d::O3R::Ptr cam_{}; + ifm3d::PortInfo port_info_{}; + + // Service Servers + DumpServer dump_srv_{}; + ConfigServer config_srv_{}; + SoftoffServer soft_off_srv_{}; + SoftonServer soft_on_srv_{}; + GetDiagServer get_diag_srv_{}; +}; + +} // namespace ifm3d_ros2 + +#endif // IFM3D_ROS2_BUFFER_CONVERSIONS_HPP_ \ No newline at end of file diff --git a/include/ifm3d_ros2/tof_module.hpp b/include/ifm3d_ros2/tof_module.hpp new file mode 100644 index 0000000..f64ce72 --- /dev/null +++ b/include/ifm3d_ros2/tof_module.hpp @@ -0,0 +1,127 @@ +// -*- c++ -*- +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright (C) 2024 ifm electronic, gmbh + */ + +#ifndef IFM3D_ROS2_TOF_MODULE_HPP_ +#define IFM3D_ROS2_TOF_MODULE_HPP_ + +#include + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +using ImageMsg = sensor_msgs::msg::Image; +using ImagePublisher = std::shared_ptr>; + +using PCLMsg = sensor_msgs::msg::PointCloud2; +using PCLPublisher = std::shared_ptr>; + +using ExtrinsicsMsg = ifm3d_ros2::msg::Extrinsics; +using ExtrinsicsPublisher = std::shared_ptr>; + +using CameraInfoMsg = sensor_msgs::msg::CameraInfo; +using CameraInfoPublisher = std::shared_ptr>; + +using IntrinsicsMsg = ifm3d_ros2::msg::Intrinsics; +using IntrinsicsPublisher = std::shared_ptr>; + +using InverseIntrinsicsMsg = ifm3d_ros2::msg::InverseIntrinsics; +using InverseIntrinsicsPublisher = std::shared_ptr>; + +using TOFInfoMsg = ifm3d_ros2::msg::TOFInfo; +using TOFInfoPublisher = std::shared_ptr>; + +namespace ifm3d_ros2 +{ +class TofModule : public FunctionModule, public std::enable_shared_from_this +{ +public: + TofModule(rclcpp::Logger logger, rclcpp_lifecycle::LifecycleNode::SharedPtr node_ptr, ifm3d::O3R::Ptr o3r_ptr, + std::string port, uint32_t width, uint32_t height); + /** + * @brief Unpacks data from the received frame, and publish + * the topics corresponding to the requested image buffers. + * + * @param frame + */ + + void handle_frame(ifm3d::Frame::Ptr frame); + + const std::string get_name() + { + return "tof_module"; + }; + + rclcpp_lifecycle::LifecycleNode::CallbackReturn on_configure(const rclcpp_lifecycle::State& previous_state); + + rclcpp_lifecycle::LifecycleNode::CallbackReturn on_cleanup(const rclcpp_lifecycle::State& previous_state); + + rclcpp_lifecycle::LifecycleNode::CallbackReturn on_shutdown(const rclcpp_lifecycle::State& previous_state); + + rclcpp_lifecycle::LifecycleNode::CallbackReturn on_activate(const rclcpp_lifecycle::State& previous_state); + + rclcpp_lifecycle::LifecycleNode::CallbackReturn on_deactivate(const rclcpp_lifecycle::State& previous_state); + + rclcpp_lifecycle::LifecycleNode::CallbackReturn on_error(const rclcpp_lifecycle::State& previous_state); + + // The list is accessed in the node, to start the FrameGrabber + std::vector buffer_id_list_{}; + +protected: + void parse_params(); + void set_parameter_event_callbacks(); + +private: + rclcpp_lifecycle::LifecycleNode::SharedPtr node_ptr_; + CameraTfPublisher tf_publisher_; + + // Values read from incoming image buffers + uint32_t width_; + uint32_t height_; + + // Value used to only publish the static transform once + bool first_; + + // Publishers + // We create lists of publishers for same image types, instead + // of creating a publisher for each buffer. This way we limit the + // number of publishers to the number of image types. + std::map image_publishers_; + std::map pcl_publishers_; + std::map extrinsics_publishers_; + std::map camera_info_publishers_; + std::map tof_info_publishers_; + std::map intrinsics_publishers_; + std::map inverse_intrinsics_publishers_; + + // Parameters + rcl_interfaces::msg::ParameterDescriptor buffer_id_list_descriptor_; + rcl_interfaces::msg::ParameterDescriptor tf_base_frame_name_descriptor_; + rcl_interfaces::msg::ParameterDescriptor tf_mounting_frame_name_descriptor_; + rcl_interfaces::msg::ParameterDescriptor tf_optical_frame_name_descriptor_; + rcl_interfaces::msg::ParameterDescriptor tf_publish_mounting_to_optical_descriptor_; + rcl_interfaces::msg::ParameterDescriptor tf_publish_base_to_mounting_descriptor_; + + /// Subscription to parameter changes + std::shared_ptr param_subscriber_; + /// Callbacks need to be stored to work properly; using a map with parameter name as key + std::map registered_param_callbacks_; +}; + +} // namespace ifm3d_ros2 + +#endif \ No newline at end of file diff --git a/include/ifm3d_ros2/visibility_control.h b/include/ifm3d_ros2/visibility_control.h index 9b25cb5..52a1e0c 100644 --- a/include/ifm3d_ros2/visibility_control.h +++ b/include/ifm3d_ros2/visibility_control.h @@ -17,39 +17,38 @@ #define IFM3D_ROS2__VISIBILITY_CONTROL_H_ #ifdef __cplusplus -extern "C" -{ +extern "C" { #endif // This logic was borrowed (then namespaced) from the examples on the gcc wiki: // https://gcc.gnu.org/wiki/Visibility #if defined _WIN32 || defined __CYGWIN__ - #ifdef __GNUC__ - #define IFM3D_ROS2_EXPORT __attribute__ ((dllexport)) - #define IFM3D_ROS2_IMPORT __attribute__ ((dllimport)) - #else - #define IFM3D_ROS2_EXPORT __declspec(dllexport) - #define IFM3D_ROS2_IMPORT __declspec(dllimport) - #endif - #ifdef IFM3D_ROS2_BUILDING_DLL - #define IFM3D_ROS2_PUBLIC IFM3D_ROS2_EXPORT - #else - #define IFM3D_ROS2_PUBLIC IFM3D_ROS2_IMPORT - #endif - #define IFM3D_ROS2_PUBLIC_TYPE IFM3D_ROS2_PUBLIC - #define IFM3D_ROS2_LOCAL +#ifdef __GNUC__ +#define IFM3D_ROS2_EXPORT __attribute__((dllexport)) +#define IFM3D_ROS2_IMPORT __attribute__((dllimport)) #else - #define IFM3D_ROS2_EXPORT __attribute__ ((visibility("default"))) - #define IFM3D_ROS2_IMPORT - #if __GNUC__ >= 4 - #define IFM3D_ROS2_PUBLIC __attribute__ ((visibility("default"))) - #define IFM3D_ROS2_LOCAL __attribute__ ((visibility("hidden"))) - #else - #define IFM3D_ROS2_PUBLIC - #define IFM3D_ROS2_LOCAL - #endif - #define IFM3D_ROS2_PUBLIC_TYPE +#define IFM3D_ROS2_EXPORT __declspec(dllexport) +#define IFM3D_ROS2_IMPORT __declspec(dllimport) +#endif +#ifdef IFM3D_ROS2_BUILDING_DLL +#define IFM3D_ROS2_PUBLIC IFM3D_ROS2_EXPORT +#else +#define IFM3D_ROS2_PUBLIC IFM3D_ROS2_IMPORT +#endif +#define IFM3D_ROS2_PUBLIC_TYPE IFM3D_ROS2_PUBLIC +#define IFM3D_ROS2_LOCAL +#else +#define IFM3D_ROS2_EXPORT __attribute__((visibility("default"))) +#define IFM3D_ROS2_IMPORT +#if __GNUC__ >= 4 +#define IFM3D_ROS2_PUBLIC __attribute__((visibility("default"))) +#define IFM3D_ROS2_LOCAL __attribute__((visibility("hidden"))) +#else +#define IFM3D_ROS2_PUBLIC +#define IFM3D_ROS2_LOCAL +#endif +#define IFM3D_ROS2_PUBLIC_TYPE #endif #ifdef __cplusplus diff --git a/index.md b/index.md index b72f116..1864ab9 100644 --- a/index.md +++ b/index.md @@ -3,12 +3,10 @@ :maxdepth: 2 Overview Installation -Launch +Camera node +ODS node Visualization -Topics Services -Parameters -Multi-head -Diagnostic +Diagnostic Deployment ::: \ No newline at end of file diff --git a/launch/camera.launch.py b/launch/camera.launch.py index 2dde24a..1bce6ef 100644 --- a/launch/camera.launch.py +++ b/launch/camera.launch.py @@ -1,6 +1,6 @@ # # SPDX-License-Identifier: Apache-2.0 -# Copyright (C) 2032 ifm electronic, gmbh +# Copyright (C) 2024 ifm electronic, gmbh # from launch import LaunchDescription diff --git a/launch/camera_managed.launch.py b/launch/camera_managed.launch.py deleted file mode 100644 index 4f18ef3..0000000 --- a/launch/camera_managed.launch.py +++ /dev/null @@ -1,241 +0,0 @@ -# -# SPDX-License-Identifier: Apache-2.0 -# Copyright (C) 2019 ifm electronic, gmbh -# - - - -import os -from math import pi -import logging - -import lifecycle_msgs.msg -import launch -from launch import LaunchDescription -from launch.actions import ExecuteProcess -from launch.actions import EmitEvent -from launch.actions import LogInfo -from launch.actions import RegisterEventHandler -from launch_ros.actions import LifecycleNode -from launch_ros.events.lifecycle import ChangeState -from launch_ros.event_handlers import OnStateTransition - -from launch.actions import DeclareLaunchArgument -from launch.substitutions import LaunchConfiguration -from launch.actions import OpaqueFunction - -deprecation_warning = LogInfo( - msg=""" - - ###################################################################################### - # # - # This launch script is deprecated. Use the parametrized camera.launch.py instead! # - # # - ###################################################################################### - """ -) - -logging.basicConfig(format='%(asctime)s - %(message)s', level=logging.INFO) - -def launch_setup(context, *args, **kwargs): - - - package_name = 'ifm3d_ros2' - node_exe = 'camera_standalone' - parameters = [] - remaps = [] - - node_name = LaunchConfiguration('name').perform(context) - params = LaunchConfiguration('params').perform(context) - node_namespace = LaunchConfiguration('namespace').perform(context) - - parameters.append(params) - - #------------------------------------------------------------ - # Nodes - #------------------------------------------------------------ - - - # - # The camera component - # - camera_node = \ - LifecycleNode( - package=package_name, - executable=node_exe, - namespace=node_namespace, - name=node_name, - output='screen', - parameters=parameters, - remappings=remaps, - log_cmd=True, - ) - - logging.debug(vars(camera_node)) - #------------------------------------------------------------ - # Events we need to emit to induce state transitions - #------------------------------------------------------------ - - camera_configure_evt = \ - EmitEvent( - event=ChangeState( - lifecycle_node_matcher = \ - launch.events.matches_action(camera_node), - transition_id = lifecycle_msgs.msg.Transition.TRANSITION_CONFIGURE - ) - ) - - camera_activate_evt = \ - EmitEvent( - event=ChangeState( - lifecycle_node_matcher = \ - launch.events.matches_action(camera_node), - transition_id = lifecycle_msgs.msg.Transition.TRANSITION_ACTIVATE - ) - ) - - camera_cleanup_evt = \ - EmitEvent( - event=ChangeState( - lifecycle_node_matcher = \ - launch.events.matches_action(camera_node), - transition_id = lifecycle_msgs.msg.Transition.TRANSITION_CLEANUP - ) - ) - - camera_shutdown_evt = EmitEvent(event=launch.events.Shutdown()) - - #------------------------------------------------------------ - # These are the edges of the state machine graph we want to autonomously - # manage - #------------------------------------------------------------ - - # - # unconfigured -> configuring -> inactive - # - camera_node_unconfigured_to_inactive_handler = \ - RegisterEventHandler( - OnStateTransition( - target_lifecycle_node = camera_node, - start_state = 'configuring', - goal_state = 'inactive', - entities = [ - LogInfo(msg = "Emitting 'TRANSITION_ACTIVATE' event"), - camera_activate_evt, - ], - ) - ) - # - # active -> deactivating -> inactive - # - camera_node_active_to_inactive_handler = \ - RegisterEventHandler( - OnStateTransition( - target_lifecycle_node = camera_node, - start_state = 'deactivating', - goal_state = 'inactive', - entities = [ - LogInfo(msg = "Emitting 'TRANSITION_CLEANUP' event"), - camera_cleanup_evt, - ], - ) - ) - # - # inactive -> cleaningup -> unconfigured - # - camera_node_inactive_to_unconfigured_handler = \ - RegisterEventHandler( - OnStateTransition( - target_lifecycle_node = camera_node, - start_state = 'cleaningup', - goal_state = 'unconfigured', - entities = [ - LogInfo(msg = "Emitting 'TRANSITION_CONFIGURE' event"), - camera_configure_evt, - ], - ) - ) - # - # * -> errorprocessing -> unconfigured - # - camera_node_errorprocessing_to_unconfigured_handler = \ - RegisterEventHandler( - OnStateTransition( - target_lifecycle_node = camera_node, - start_state = 'errorprocessing', - goal_state = 'unconfigured', - entities = [ - LogInfo(msg = "Emitting 'TRANSITION_CONFIGURE' event"), - camera_configure_evt, - ], - ) - ) - # - # * -> shuttingdown -> finalized - # - camera_node_shuttingdown_to_finalized_handler = \ - RegisterEventHandler( - OnStateTransition( - target_lifecycle_node = camera_node, - start_state = 'shuttingdown', - goal_state = 'finalized', - entities = [ - LogInfo(msg = "Emitting 'SHUTDOWN' event"), - camera_shutdown_evt, - ], - ) - ) - - # - # Coord frame transform from camera_optical_link to camera_link - # - tf_node = \ - ExecuteProcess( - cmd=['ros2', 'run', 'tf2_ros', 'static_transform_publisher', - '0', '0', '0', '0', '0', '0', - str(node_name + "_optical_link"), str(node_name + "_link")], - # output='screen', - log_cmd=True - ) - logging.info("Publishing tf2 transform from {} to {}" .format(str(node_name + "_optical_link"), str(node_name + "_link"))) - - # - # (Dummy) Coord frame transform from camera_link to map frame - # - tf_map_link_node = \ - ExecuteProcess( - cmd=['ros2', 'run', 'tf2_ros', 'static_transform_publisher', - '0', '0', '0', '0', '0', '0', - "map", str(node_name + "_optical_link")], - # output='screen', - log_cmd=True - ) - logging.info("Publishing tf2 transform from {} to {}" .format("map", str(node_name + "_optical_link"))) - - return camera_node_unconfigured_to_inactive_handler, \ - camera_node_active_to_inactive_handler, \ - camera_node_inactive_to_unconfigured_handler, \ - camera_node_errorprocessing_to_unconfigured_handler, \ - camera_node_shuttingdown_to_finalized_handler, \ - camera_node, \ - camera_configure_evt, \ - tf_node, \ - tf_map_link_node \ - - -def generate_launch_description(): - - os.environ['RCUTILS_CONSOLE_OUTPUT_FORMAT'] = \ - "[{%s}] {%s} [{%s}]: {%s}" % ("severity", "time", "name", "message") - - - return LaunchDescription( - [ - deprecation_warning, - DeclareLaunchArgument('name', default_value='camera'), - DeclareLaunchArgument('params', default_value=[]), - DeclareLaunchArgument('namespace', default_value='ifm3d'), - OpaqueFunction(function=launch_setup), - ], - deprecated_reason="Deprecated in favor of camera.launch.py" - ) diff --git a/launch/camera_standalone.launch.py b/launch/camera_standalone.launch.py deleted file mode 100644 index 975561e..0000000 --- a/launch/camera_standalone.launch.py +++ /dev/null @@ -1,123 +0,0 @@ -# -# SPDX-License-Identifier: Apache-2.0 -# Copyright (C) 2019 ifm electronic, gmbh -# - -import os -import sys -from math import pi - -from launch import LaunchDescription -from launch.actions import ExecuteProcess, LogInfo -from launch_ros.actions import LifecycleNode - -deprecation_warning = LogInfo( - msg=""" - - ###################################################################################### - # # - # This launch script is deprecated. Use the parametrized camera.launch.py instead! # - # # - ###################################################################################### - """ -) - - -def generate_launch_description(): - package_name = 'ifm3d_ros2' - node_namespace = 'ifm3d' - node_name = 'camera' - node_exe = 'camera_standalone' - - # os.environ['RCUTILS_CONSOLE_OUTPUT_FORMAT'] = \ - # "[{%s}] {%s} [{%s}]: {%s}\n({%s}() at {%s}:{%s})" % \ - # ("severity", "time", "name", "message", - # "function_name", "file_name", "line_number") - os.environ['RCUTILS_CONSOLE_OUTPUT_FORMAT'] = \ - "[{%s}] {%s} [{%s}]: {%s}" % ("severity", "time", "name", "message") - - # XXX: This is a hack, there does not seem to be a nice way (or at least - # finding the docs is not obvious) to do this with the ROS2 launch api - # - # Basically, we are trying to allow for passing through the command line - # args to the launch file through to the node executable itself (like ROS - # 1). - # - # My assumption is that either: - # 1. This stuff exists somewhere in ROS2 and I don't know about it yet - # 2. This stuff will exist in ROS2 soon, so, this will likely get factored - # out (hopefully soon) - # - parameters = [] - remaps = [] - for arg in sys.argv: - if ':=' in arg: - split_arg = arg.split(sep=':=', maxsplit=1) - assert len(split_arg) == 2 - - if arg.startswith("ns"): - node_namespace = split_arg[1] - elif arg.startswith("node"): - node_name = split_arg[1] - elif arg.startswith("params"): - parameters.append(tuple(split_arg)[1]) - else: - remaps.append(tuple(split_arg)) - - def add_prefix(tup): - assert len(tup) == 2 - if node_namespace.startswith("/"): - prefix = "%s/%s" % (node_namespace, node_name) - else: - prefix = "/%s/%s" % (node_namespace, node_name) - - retval = [None, None] - - if not tup[0].startswith(prefix): - retval[0] = prefix + '/' + tup[0] - else: - retval[0] = tup[0] - - if not tup[1].startswith(prefix): - retval[1] = prefix + '/' + tup[1] - else: - retval[1] = tup[1] - - return tuple(retval) - - remaps = list(map(add_prefix, remaps)) - - return LaunchDescription( - [ - deprecation_warning, - ExecuteProcess( - cmd=[ - 'ros2', - 'run', - 'tf2_ros', - 'static_transform_publisher', - '0', - '0', - '0', - '0', - '0', - '0', - str(node_name + "_optical_link"), - str(node_name + "_link"), - ], - # output='screen', - log_cmd=True, - ), - LifecycleNode( - package=package_name, - executable=node_exe, - namespace=node_namespace, - name=node_name, - output='screen', - parameters=parameters, - remappings=remaps, - log_cmd=True, - ), - ], - deprecated_reason="Deprecated in favor of camera.launch.py" - ) diff --git a/launch/examples/example_o3r_2d_and_3d.launch.py b/launch/examples/example_o3r_2d_and_3d.launch.py index 4a100ca..5f753ba 100644 --- a/launch/examples/example_o3r_2d_and_3d.launch.py +++ b/launch/examples/example_o3r_2d_and_3d.launch.py @@ -1,6 +1,6 @@ # # SPDX-License-Identifier: Apache-2.0 -# Copyright (C) 2019 ifm electronic, gmbh +# Copyright (C) 2024 ifm electronic, gmbh # from launch import LaunchDescription diff --git a/launch/examples/example_two_o3r_heads.launch.py b/launch/examples/example_two_o3r_heads.launch.py index 8204434..85ce516 100644 --- a/launch/examples/example_two_o3r_heads.launch.py +++ b/launch/examples/example_two_o3r_heads.launch.py @@ -1,6 +1,6 @@ # # SPDX-License-Identifier: Apache-2.0 -# Copyright (C) 2019 ifm electronic, gmbh +# Copyright (C) 2024 ifm electronic, gmbh # from launch import LaunchDescription diff --git a/launch/multi_cameras.launch.py b/launch/multi_cameras.launch.py deleted file mode 100644 index 7e9d2f8..0000000 --- a/launch/multi_cameras.launch.py +++ /dev/null @@ -1,80 +0,0 @@ -#!/usr/bin/env python3 - -# -# SPDX-License-Identifier: Apache-2.0 -# Copyright (C) 2019 ifm electronic, gmbh -# - -import yaml -import logging - -from ament_index_python.packages import get_package_share_directory -from launch import LaunchDescription -from launch.actions import LogInfo -from launch.actions import IncludeLaunchDescription -from launch.actions import DeclareLaunchArgument -from launch.launch_description_sources import PythonLaunchDescriptionSource -from launch.substitutions import LaunchConfiguration -from launch.actions import OpaqueFunction - -deprecation_warning = LogInfo( - msg=""" - - ###################################################################################### - # # - # This launch script is deprecated. Use the parametrized camera.launch.py instead! # - # # - ###################################################################################### - """ -) - -ifm3d_ros2_dir = get_package_share_directory('ifm3d_ros2') - -logging.basicConfig(format='%(asctime)s - %(message)s', level=logging.INFO) - -def add_camera_loop(context, *args, **kwargs): - - cameras=[] - params_path = LaunchConfiguration('params').perform(context) - namespace = LaunchConfiguration('namespace').perform(context) - logging.debug(vars(context)) - - with open(params_path) as p: - params_data = yaml.load(p, Loader=yaml.FullLoader) - - try: - print(params_data.keys()) - for key, value in params_data.items(): - logging.info('Camera port arguments: {}'.format(key.split(sep='/')[-1])) - - camera = IncludeLaunchDescription( - PythonLaunchDescriptionSource(ifm3d_ros2_dir + '/launch/camera_managed.launch.py'), - launch_arguments={"name": key.split(sep='{}/'.format(namespace))[-1], - "params": params_path, - "namespace": namespace, - }.items(), - ) - logging.info('Launch argument: {}'.format(vars(camera))) - - # add camera to cameras list as parsed from namespace arg and yaml fields - cameras.append(camera) - except Exception as e: - print('parsing the configuration yaml found an error: {}. Please check your namespace definitions.'.format(e)) - return cameras - - -def generate_launch_description(): - """Launch the camera_managed.launch.py launch file.""" - ld = LaunchDescription(deprecated_reason="Deprecated in favor of camera.launch.py") - ld.add_action(deprecation_warning) - DeclareLaunchArgument( - 'namespace', - default_value='ifm3d', - ) - DeclareLaunchArgument( - 'params', - default_value = ifm3d_ros2_dir + '/config/params.yaml', - description='test arg that overlaps arg in included file', - ) - ld.add_action(OpaqueFunction(function = add_camera_loop)) - return ld \ No newline at end of file diff --git a/launch/ods.launch.py b/launch/ods.launch.py new file mode 100644 index 0000000..4e9cef3 --- /dev/null +++ b/launch/ods.launch.py @@ -0,0 +1,148 @@ +# +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2024 ifm electronic, gmbh +# + +from launch import LaunchDescription +from launch.actions import DeclareLaunchArgument, EmitEvent, RegisterEventHandler +from launch.conditions import IfCondition +from launch.events import matches_action +from launch.substitutions import LaunchConfiguration, PathJoinSubstitution +from launch_ros.actions import LifecycleNode, Node +from launch_ros.events.lifecycle import ChangeState +from launch_ros.event_handlers import OnStateTransition +from launch_ros.substitutions import FindPackageShare +from lifecycle_msgs.msg import Transition + + +def generate_launch_description(): + + # ------------------------------------------------------------ + # Launch script arguments + # ------------------------------------------------------------ + declared_arguments = [] + declared_arguments.append( + DeclareLaunchArgument( + "log_level", + default_value="info", + description="To change RCLCPP log level for the camera node. ['debug', 'info', 'warn', 'error']", + ) + ) + declared_arguments.append( + DeclareLaunchArgument( + "visualization", + default_value="false", + description="If true, RViz2 with a predefined config is opened.", + ) + ) + declared_arguments.append( + DeclareLaunchArgument( + "ods_name", + default_value="ods", + description="Name for the ODS node and used as tf prefix", + ) + ) + declared_arguments.append( + DeclareLaunchArgument( + "ods_namespace", + default_value="ifm3d", + description="Namespace to launch nodes into", + ) + ) + declared_arguments.append( + DeclareLaunchArgument( + "parameter_file_package", + default_value="ifm3d_ros2", + description="Package containing the camera's YAML configuration.", + ) + ) + declared_arguments.append( + DeclareLaunchArgument( + "parameter_file_directory", + default_value="config", + description="Directory inside of parameter_file_package containing the camera's YAML configuration.", + ) + ) + declared_arguments.append( + DeclareLaunchArgument( + "parameter_file_name", + default_value="ods_default_parameters.yaml", + description="YAML file with the camera configuration.", + ) + ) + + # ------------------------------------------------------------ + # Nodes, using substitutions to fill in arguments + # ------------------------------------------------------------ + ods_node = LifecycleNode( + package="ifm3d_ros2", + executable="ods_standalone", + namespace=LaunchConfiguration("ods_namespace"), + name=LaunchConfiguration("ods_name"), + output='screen', + parameters=[ + PathJoinSubstitution( + [ + FindPackageShare(LaunchConfiguration("parameter_file_package")), + LaunchConfiguration("parameter_file_directory"), + LaunchConfiguration("parameter_file_name"), + ] + ) + ], + arguments=['--ros-args', '--log-level', LaunchConfiguration('log_level')], + log_cmd=True, + ) + + # Launching RViz2 conditionally, depending on the "visualition" argument + rviz_node = Node( + executable="rviz2", + package="rviz2", + name=[LaunchConfiguration("ods_name"), "_rviz2"], + arguments=[ + '-d', + PathJoinSubstitution([FindPackageShare("ifm3d_ros2"), "etc", "ifm3d.rviz"]), + ], + condition=IfCondition(LaunchConfiguration("visualization")), + log_cmd=True, + ) + + # ------------------------------------------------------------ + # Lifecycle management + # ------------------------------------------------------------ + + # UNCONFIGURED to INACTIVE via ChangeState (emitted without delay) + configure_ods = EmitEvent( + event=ChangeState( + lifecycle_node_matcher=matches_action(ods_node), + transition_id=Transition.TRANSITION_CONFIGURE, + ) + ) + + # INACTIVE to ACTIVE via Handler + # Handler emits event after the transition from configuring to inactive + # Emitted ChangeState event transitions the node from inactive to active + activate_ods = RegisterEventHandler( + OnStateTransition( + target_lifecycle_node=ods_node, + start_state='configuring', + goal_state='inactive', + entities=[ + EmitEvent( + event=ChangeState( + lifecycle_node_matcher=matches_action(ods_node), + transition_id=Transition.TRANSITION_ACTIVATE, + ) + ) + ], + ) + ) + + ld = LaunchDescription() + for declared_argument in declared_arguments: + ld.add_entity(declared_argument) + ld.add_action(ods_node) + ld.add_action(rviz_node) + ld.add_action(configure_ods) + ld.add_action(activate_ods) + + return ld diff --git a/launch/rviz.launch.py b/launch/rviz.launch.py deleted file mode 100644 index 60df3e6..0000000 --- a/launch/rviz.launch.py +++ /dev/null @@ -1,41 +0,0 @@ -# -# SPDX-License-Identifier: Apache-2.0 -# Copyright (C) 2019 ifm electronic, gmbh -# - -import os - -from ament_index_python.packages import get_package_share_directory -from launch import LaunchDescription -from launch.actions import ExecuteProcess, LogInfo - -deprecation_warning = LogInfo( - msg=""" - - ###################################################################################### - # # - # This launch script is deprecated. Use the parametrized camera.launch.py instead! # - # # - ###################################################################################### - """ -) - - -def generate_launch_description(): - package_name = 'ifm3d_ros2' - - rviz_config = os.path.join( - get_package_share_directory(package_name), 'etc', 'ifm3d.rviz' - ) - - return LaunchDescription( - [ - deprecation_warning, - ExecuteProcess( - cmd=['ros2', 'run', 'rviz2', 'rviz2', '-d', rviz_config], - output='screen', - log_cmd=True, - ), - ], - deprecated_reason="Deprecated in favor of camera.launch.py" - ) diff --git a/msg/Zones.msg b/msg/Zones.msg new file mode 100644 index 0000000..5776d33 --- /dev/null +++ b/msg/Zones.msg @@ -0,0 +1,4 @@ +std_msgs/Header header + +uint8[3] zone_occupied +uint32 zone_config_id \ No newline at end of file diff --git a/package.xml b/package.xml index fb23d4d..1adacaf 100644 --- a/package.xml +++ b/package.xml @@ -12,41 +12,28 @@ ament_cmake_auto ament_cmake_ros - builtin_interfaces - diagnostic_msgs + builtin_interfaces + diagnostic_msgs + launch + launch_ros + lifecycle_msgs + nav_msgs + nav2_msgs + rclcpp + rclcpp_components + rclcpp_lifecycle + rcl_interfaces + rclpy + rmw + ros2launch + rosidl_default_generators + sensor_msgs + std_msgs + tf2_ros + geometry_msgs - launch - launch_ros - lifecycle_msgs - rclcpp - rclcpp_components - rclcpp_lifecycle - rcl_interfaces - rclpy - rmw - ros2launch - rosidl_default_generators - sensor_msgs - std_msgs - tf2_ros ament_index_python - builtin_interfaces - diagnostic_msgs - launch - launch_ros - lifecycle_msgs - rclcpp - rclcpp_components - rclcpp_lifecycle - rcl_interfaces - rclpy - rmw - ros2launch - rosidl_default_runtime - sensor_msgs - std_msgs - tf2_ros launch_testing_ament_cmake python3-opencv diff --git a/scripts/config b/scripts/config deleted file mode 100644 index 1c622b3..0000000 --- a/scripts/config +++ /dev/null @@ -1,69 +0,0 @@ -#!/usr/bin/env python3 -# -*- python -*- - -# -# SPDX-License-Identifier: Apache-2.0 -# Copyright (C) 2019 ifm electronic, gmbh -# - -import argparse -import json -import sys - -import rclpy -from ifm3d_ros2.srv import Config - -WAIT_SECS = 2.0 -SRV_NAME = "/ifm3d/camera/Config" -NODE_NAME = "ifm3d_ros2_config_client" - -def get_args(): - parser = argparse.ArgumentParser( - description='Configure an ifm3d camera from JSON piped to stdin', - formatter_class=argparse.ArgumentDefaultsHelpFormatter) - - parser.add_argument('--srv', required=False, default=SRV_NAME, - help="The fully qualified `Config' service to call") - parser.add_argument('--node', required=False, default=NODE_NAME, - help="The node name of this service client") - - args = parser.parse_args(sys.argv[1:]) - return args - -def main(): - args = get_args() - - rclpy.init() - node = rclpy.create_node(args.node) - cli = node.create_client(Config, args.srv) - req = Config.Request() - log = node.get_logger() - - try: - req.json = json.dumps(json.load(sys.stdin)) - - while not cli.wait_for_service(timeout_sec=WAIT_SECS): - log.info("Config service not available, waiting...") - - fut = cli.call_async(req) - rclpy.spin_until_future_complete(node, fut) - - res = fut.result() - if res is not None: - if res.status != 0: - log.error("Config failed with error: %s - %s" % - (str(res.status), res.msg)) - else: - log.warn("Service call failed %r" % (fut.exception(),)) - - except KeyboardInterrupt: - pass - - finally: - node.destroy_node() - rclpy.shutdown() - - return 0 - -if __name__ == '__main__': - sys.exit(main()) diff --git a/scripts/dump b/scripts/dump deleted file mode 100644 index 8c41893..0000000 --- a/scripts/dump +++ /dev/null @@ -1,71 +0,0 @@ -#!/usr/bin/env python3 -# -*- python -*- - -# -# SPDX-License-Identifier: Apache-2.0 -# Copyright (C) 2019 ifm electronic, gmbh -# - -import argparse -import json -import sys - -import rclpy -from ifm3d_ros2.srv import Dump - -WAIT_SECS = 2.0 -SRV_NAME = "/ifm3d/camera/Dump" -NODE_NAME = "ifm3d_ros2_dump_client" - -def get_args(): - parser = argparse.ArgumentParser( - description='Dump an ifm3d camera configuration to stdout as JSON', - formatter_class=argparse.ArgumentDefaultsHelpFormatter) - - parser.add_argument('--srv', required=False, default=SRV_NAME, - help="The fully qualified `Dump' service to call") - parser.add_argument('--node', required=False, default=NODE_NAME, - help="The node name of this service client") - - args = parser.parse_args(sys.argv[1:]) - return args - -def main(): - args = get_args() - - rclpy.init() - node = rclpy.create_node(args.node) - cli = node.create_client(Dump, args.srv) - req = Dump.Request() - log = node.get_logger() - - try: - while not cli.wait_for_service(timeout_sec=WAIT_SECS): - log.info("Dump service not available, waiting...") - - fut = cli.call_async(req) - rclpy.spin_until_future_complete(node, fut) - - res = fut.result() - if res is not None: - if res.status == 0: - print(json.dumps(json.loads(res.config), - sort_keys=True, indent=4, separators=(',', ': '))) - else: - log.error("Dump failed with error: %s - %s" % - (str(res.status), - "Check the `ifm3d' logs for more detail")) - else: - log.warn("Service call failed %r" % (fut.exception(),)) - - except KeyboardInterrupt: - pass - - finally: - node.destroy_node() - rclpy.shutdown() - - return 0 - -if __name__ == '__main__': - sys.exit(main()) diff --git a/scripts/launch_in_docker.sh b/scripts/launch_in_docker.sh deleted file mode 100755 index e4b6e86..0000000 --- a/scripts/launch_in_docker.sh +++ /dev/null @@ -1,24 +0,0 @@ -#!/bin/sh - -print_help() -{ - echo Convenience script for launching a ifm3d-ros2 launchfile from a docker container. - echo - echo \$1: Docker image name - echo "\$2: Launchfile (Optional; Default: 'camera_managed.launch.py')" - exit 22 -} - -test $# -lt 1 && print_help - -image=${1} -shift -launchfile=${1:-camera_managed.launch.py} -test $# -ge 1 && shift - -# Including "-it" so that CTRL-C works -docker run -it -p 11311:11311 $image \ - sh -c ". /opt/ros/foxy/setup.sh; \ - . /home/ifm/colcon_ws/ifm3d-ros2/install/setup.sh; \ - ros2 launch ifm3d_ros2 $launchfile $@" - diff --git a/src/bin/camera_standalone.cpp b/src/bin/camera_standalone.cpp index 1278176..8fe6f20 100644 --- a/src/bin/camera_standalone.cpp +++ b/src/bin/camera_standalone.cpp @@ -1,6 +1,6 @@ /* * SPDX-License-Identifier: Apache-2.0 - * Copyright (C) 2019 ifm electronic, gmbh + * Copyright (C) 2024 ifm electronic, gmbh */ #include diff --git a/src/bin/ods_standalone.cpp b/src/bin/ods_standalone.cpp new file mode 100644 index 0000000..b1f6440 --- /dev/null +++ b/src/bin/ods_standalone.cpp @@ -0,0 +1,23 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright (C) 2024 ifm electronic, gmbh + */ + +#include +#include +#include +#include + +int main(int argc, char** argv) +{ + std::setvbuf(stdout, NULL, _IONBF, BUFSIZ); + rclcpp::init(argc, argv); + rclcpp::executors::MultiThreadedExecutor exec; + rclcpp::NodeOptions options; + auto ods_node = std::make_shared(options); + exec.add_node(ods_node->get_node_base_interface()); + exec.spin(); + + rclcpp::shutdown(); + return 0; +} diff --git a/src/lib/buffer_conversions.cpp b/src/lib/buffer_conversions.cpp new file mode 100644 index 0000000..72ef4f9 --- /dev/null +++ b/src/lib/buffer_conversions.cpp @@ -0,0 +1,788 @@ +// -*- c++ -*- +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright (C) 2024 ifm electronic, gmbh + */ + +#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 + +namespace ifm3d_ros2 +{ +sensor_msgs::msg::Image ifm3d_to_ros_image(ifm3d::Buffer& image, // Need non-const image because image.begin(), + // image.end() don't have const overloads. + const std_msgs::msg::Header& header, const rclcpp::Logger& logger) +{ + static constexpr auto max_pixel_format = static_cast(ifm3d::pixel_format::FORMAT_32F3); + static constexpr auto image_format_info = [] { + auto image_format_info = std::array{}; + + { + using namespace ifm3d; + using namespace sensor_msgs::image_encodings; + image_format_info[static_cast(pixel_format::FORMAT_8U)] = TYPE_8UC1; + image_format_info[static_cast(pixel_format::FORMAT_8S)] = TYPE_8SC1; + image_format_info[static_cast(pixel_format::FORMAT_16U)] = TYPE_16UC1; + image_format_info[static_cast(pixel_format::FORMAT_16S)] = TYPE_16SC1; + image_format_info[static_cast(pixel_format::FORMAT_32U)] = "32UC1"; + image_format_info[static_cast(pixel_format::FORMAT_32S)] = TYPE_32SC1; + image_format_info[static_cast(pixel_format::FORMAT_32F)] = TYPE_32FC1; + image_format_info[static_cast(pixel_format::FORMAT_64U)] = "64UC1"; + image_format_info[static_cast(pixel_format::FORMAT_64F)] = TYPE_64FC1; + image_format_info[static_cast(pixel_format::FORMAT_16U2)] = TYPE_16UC2; + image_format_info[static_cast(pixel_format::FORMAT_32F3)] = TYPE_32FC3; + } + + return image_format_info; + }(); + + const auto format = static_cast(image.dataFormat()); + + sensor_msgs::msg::Image result{}; + result.header = header; + result.height = image.height(); + result.width = image.width(); + result.is_bigendian = 0; + + if (image.begin() == image.end()) + { + return result; + } + + if (format >= max_pixel_format) + { + RCLCPP_ERROR(logger, "Pixel format out of range (%ld >= %ld)", format, max_pixel_format); + return result; + } + + result.encoding = image_format_info.at(format); + result.step = result.width * sensor_msgs::image_encodings::bitDepth(image_format_info.at(format)) / 8; + result.data.insert(result.data.end(), image.ptr<>(0), std::next(image.ptr<>(0), result.step * result.height)); + + if (result.encoding.empty()) + { + RCLCPP_WARN(logger, "Can't handle encoding %ld (32U == %ld, 64U == %ld)", format, + static_cast(ifm3d::pixel_format::FORMAT_32U), + static_cast(ifm3d::pixel_format::FORMAT_64U)); + result.encoding = sensor_msgs::image_encodings::TYPE_8UC1; + } + + return result; +} + +sensor_msgs::msg::Image ifm3d_to_ros_image(ifm3d::Buffer&& image, const std_msgs::msg::Header& header, + const rclcpp::Logger& logger) +{ + return ifm3d_to_ros_image(image, header, logger); +} + +sensor_msgs::msg::CompressedImage ifm3d_to_ros_compressed_image(ifm3d::Buffer& image, // Need non-const image because + // image.begin(), image.end() + // don't have const overloads. + const std_msgs::msg::Header& header, + const std::string& format, // "jpeg" or "png" + const rclcpp::Logger& logger) +{ + sensor_msgs::msg::CompressedImage result{}; + result.header = header; + result.format = format; + + if (const auto dataFormat = image.dataFormat(); + dataFormat != ifm3d::pixel_format::FORMAT_8S && dataFormat != ifm3d::pixel_format::FORMAT_8U) + { + RCLCPP_ERROR(logger, "Invalid data format for %s data (%ld)", format.c_str(), static_cast(dataFormat)); + return result; + } + + result.data.insert(result.data.end(), image.ptr<>(0), std::next(image.ptr<>(0), image.width() * image.height())); + return result; +} + +sensor_msgs::msg::CompressedImage ifm3d_to_ros_compressed_image(ifm3d::Buffer&& image, + const std_msgs::msg::Header& header, + const std::string& format, const rclcpp::Logger& logger) +{ + return ifm3d_to_ros_compressed_image(image, header, format, logger); +} + +sensor_msgs::msg::PointCloud2 ifm3d_to_ros_cloud(ifm3d::Buffer& image, // Need non-const image because image.begin(), + // image.end() don't have const overloads. + const std_msgs::msg::Header& header, const rclcpp::Logger& logger) +{ + sensor_msgs::msg::PointCloud2 result{}; + result.header = header; + result.height = image.height(); + result.width = image.width(); + result.is_bigendian = false; + + if (image.begin() == image.end()) + { + return result; + } + + if (image.dataFormat() != ifm3d::pixel_format::FORMAT_32F3 && image.dataFormat() != ifm3d::pixel_format::FORMAT_32F) + { + RCLCPP_ERROR(logger, "Unsupported pixel format %ld for point cloud", static_cast(image.dataFormat())); + return result; + } + + sensor_msgs::msg::PointField x_field{}; + x_field.name = "x"; + x_field.offset = 0; + x_field.datatype = sensor_msgs::msg::PointField::FLOAT32; + x_field.count = 1; + + sensor_msgs::msg::PointField y_field{}; + y_field.name = "y"; + y_field.offset = 4; + y_field.datatype = sensor_msgs::msg::PointField::FLOAT32; + y_field.count = 1; + + sensor_msgs::msg::PointField z_field{}; + z_field.name = "z"; + z_field.offset = 8; + z_field.datatype = sensor_msgs::msg::PointField::FLOAT32; + z_field.count = 1; + + result.fields = { + x_field, + y_field, + z_field, + }; + + result.point_step = result.fields.size() * sizeof(float); + result.row_step = result.point_step * result.width; + result.is_dense = true; + result.data.insert(result.data.end(), image.ptr<>(0), std::next(image.ptr<>(0), result.row_step * result.height)); + + return result; +} + +sensor_msgs::msg::PointCloud2 ifm3d_to_ros_cloud(ifm3d::Buffer&& image, const std_msgs::msg::Header& header, + const rclcpp::Logger& logger) +{ + return ifm3d_to_ros_cloud(image, header, logger); +} + +nav_msgs::msg::MapMetaData ifm3d_to_ros_map_meta_data(ifm3d::ODSOccupancyGridV1 grid, const rclcpp::Logger& logger) +{ + // values of matrix 2x3 affine mapping between grid cell and user coordinate system + // e.g, multiplying the matrix with [0,0,1] gives the user coordinate of the center of upper left cell + const std::array mapping_matrix = grid.transform_cell_center_to_user; + RCLCPP_ERROR_EXPRESSION(logger, mapping_matrix[0] != mapping_matrix[4], + "Cells of the received ODSOccupancyGrid are not quadratic!"); + + nav_msgs::msg::MapMetaData map_meta_data; + map_meta_data.map_load_time = rclcpp::Time(grid.timestamp_ns); + map_meta_data.resolution = mapping_matrix[0]; + map_meta_data.width = grid.width; + map_meta_data.height = grid.height; + + // The origin of the costmap [m, m, rad]. + // This is the real-world pose of the cell (0,0) in the map. + map_meta_data.origin.position.x = mapping_matrix[2]; + map_meta_data.origin.position.y = mapping_matrix[5]; + map_meta_data.origin.position.z = 0.0; + map_meta_data.origin.orientation.x = 0; // we assume no rotation is present + map_meta_data.origin.orientation.y = 0; + map_meta_data.origin.orientation.z = 0; + map_meta_data.origin.orientation.w = 1; + + return map_meta_data; +} + +nav_msgs::msg::OccupancyGrid ifm3d_to_ros_occupancy_grid(ifm3d::Buffer& image, const std_msgs::msg::Header& header, + const rclcpp::Logger& logger) +{ + RCLCPP_DEBUG(logger, "Deserializing occupancy grid data"); + auto occupancy_grid_data = ifm3d::ODSOccupancyGridV1::Deserialize(image); + nav_msgs::msg::OccupancyGrid occupancy_grid_msg; + occupancy_grid_msg.header = header; + occupancy_grid_msg.header.stamp = rclcpp::Time(occupancy_grid_data.timestamp_ns); + occupancy_grid_msg.info = ifm3d_to_ros_map_meta_data(occupancy_grid_data, logger); + // Allocate a single contiguous block of memory for the 2D array + std::vector data(occupancy_grid_data.width * occupancy_grid_data.height); + + for (u_int32_t i = 0; i < occupancy_grid_data.width; i++) + { + for (u_int32_t j = 0; j < occupancy_grid_data.height; j++) + { + // Get the value from the image + uint8_t value = occupancy_grid_data.image.at(i, j); + + // Scale the value to the range 0-100 + int8_t scaled_value = static_cast((value * 100) / 255); + + // Assume values at or below 20 to be free, set their value to zero + scaled_value = (scaled_value <= 20) ? 0 : scaled_value; + + data[i * occupancy_grid_data.height + j] = scaled_value; + } + } + occupancy_grid_msg.data = data; + return occupancy_grid_msg; +} + +nav_msgs::msg::OccupancyGrid ifm3d_to_ros_occupancy_grid(ifm3d::Buffer&& image, const std_msgs::msg::Header& header, + const rclcpp::Logger& logger) +{ + return ifm3d_to_ros_occupancy_grid(image, header, logger); +} + +nav2_msgs::msg::Costmap ifm3d_to_ros_costmap(ifm3d::Buffer& image, const std_msgs::msg::Header& header, + const rclcpp::Logger& logger) +{ + RCLCPP_DEBUG(logger, "Generating costmap..."); + auto occupancy_grid_data = ifm3d::ODSOccupancyGridV1::Deserialize(image); + nav2_msgs::msg::Costmap costmap_msg; + costmap_msg.header = header; + costmap_msg.header.stamp = rclcpp::Time(occupancy_grid_data.timestamp_ns); + RCLCPP_ERROR_EXPRESSION(logger, costmap_msg.header.frame_id.empty(), + "frame_id is not set in the header provided to ifm3d_to_ros_costmap()!"); + + costmap_msg.metadata.update_time = costmap_msg.header.stamp; + costmap_msg.metadata.layer = "ods"; + + // Use MapMetaData to populate Costmap metadata + const nav_msgs::msg::MapMetaData meta_data = ifm3d_to_ros_map_meta_data(occupancy_grid_data, logger); + costmap_msg.metadata.resolution = meta_data.resolution; + costmap_msg.metadata.size_x = meta_data.width; + costmap_msg.metadata.size_y = meta_data.height; + costmap_msg.metadata.origin = meta_data.origin; + + // Allocate a single contiguous block of memory for the 2D array + std::vector data(occupancy_grid_data.width * occupancy_grid_data.height); + + for (uint32_t i = 0; i < occupancy_grid_data.width; i++) + { + for (uint32_t j = 0; j < occupancy_grid_data.height; j++) + { + // Get the value from the image + uint8_t value = occupancy_grid_data.image.at(i, j); + // Scale the value to the range 0-100 + int8_t scaled_value = static_cast((value * 100) / 255); + // data[i * occupancy_grid_data.height + j] = occupancy_grid_data.image.at(i, occupancy_grid_data.height + // - 1 - j); + data[i * occupancy_grid_data.height + j] = scaled_value; + } + } + costmap_msg.data = data; + + RCLCPP_DEBUG(logger, "Costmap generated."); + return costmap_msg; +} + +nav2_msgs::msg::Costmap ifm3d_to_ros_costmap(ifm3d::Buffer&& image, const std_msgs::msg::Header& header, + const rclcpp::Logger& logger) +{ + return ifm3d_to_ros_costmap(image, header, logger); +} + +ifm3d_ros2::msg::Extrinsics ifm3d_to_extrinsics(ifm3d::Buffer& buffer, const std_msgs::msg::Header& header, + const rclcpp::Logger& logger) +{ + ifm3d_ros2::msg::Extrinsics extrinsics_msg; + extrinsics_msg.header = header; + try + { + extrinsics_msg.tx = buffer.at(0); + extrinsics_msg.ty = buffer.at(1); + extrinsics_msg.tz = buffer.at(2); + extrinsics_msg.rot_x = buffer.at(3); + extrinsics_msg.rot_y = buffer.at(4); + extrinsics_msg.rot_z = buffer.at(5); + } + catch (const std::out_of_range& ex) + { + RCLCPP_WARN(logger, "Out-of-range error fetching extrinsics"); + } + return extrinsics_msg; +} + +ifm3d_ros2::msg::Extrinsics ifm3d_to_extrinsics(ifm3d::Buffer&& buffer, const std_msgs::msg::Header& header, + const rclcpp::Logger& logger) +{ + return ifm3d_to_extrinsics(buffer, header, logger); +} + +bool ifm3d_rgb_info_to_camera_info(ifm3d::Buffer& buffer, const std_msgs::msg::Header& header, const uint32_t height, + const uint32_t width, const rclcpp::Logger& logger, + sensor_msgs::msg::CameraInfo& camera_info_msg) +{ + camera_info_msg.header = header; + + try + { + auto rgb_info = ifm3d::RGBInfoV1::Deserialize(buffer); + camera_info_msg = ifm3d_to_camera_info(rgb_info.intrinsic_calibration, header, height, width, logger); + return true; + } + catch (...) + { + RCLCPP_ERROR(logger, "Failed to read rgb info."); + return false; + } +} + +bool ifm3d_rgb_info_to_camera_info(ifm3d::Buffer&& buffer, const std_msgs::msg::Header& header, const uint32_t height, + const uint32_t width, const rclcpp::Logger& logger, + sensor_msgs::msg::CameraInfo& camera_info_msg) +{ + return ifm3d_rgb_info_to_camera_info(buffer, header, height, width, logger, camera_info_msg); +} + +bool ifm3d_tof_info_to_camera_info(ifm3d::Buffer& buffer, const std_msgs::msg::Header& header, const uint32_t height, + const uint32_t width, const rclcpp::Logger& logger, + sensor_msgs::msg::CameraInfo& camera_info_msg) +{ + camera_info_msg.header = header; + + try + { + auto tof_info = ifm3d::TOFInfoV4::Deserialize(buffer); + camera_info_msg = ifm3d_to_camera_info(tof_info.intrinsic_calibration, header, height, width, logger); + return true; + } + catch (...) + { + RCLCPP_ERROR(logger, "Failed to read rgb info."); + return false; + } +} + +bool ifm3d_tof_info_to_camera_info(ifm3d::Buffer&& buffer, const std_msgs::msg::Header& header, const uint32_t height, + const uint32_t width, const rclcpp::Logger& logger, + sensor_msgs::msg::CameraInfo& camera_info_msg) +{ + return ifm3d_tof_info_to_camera_info(buffer, header, height, width, logger, camera_info_msg); +} + +sensor_msgs::msg::CameraInfo ifm3d_to_camera_info(ifm3d::calibration::IntrinsicCalibration intrinsic, + const std_msgs::msg::Header& header, const uint32_t height, + const uint32_t width, const rclcpp::Logger& logger) +{ + sensor_msgs::msg::CameraInfo camera_info_msg; + camera_info_msg.header = header; + + try + { + camera_info_msg.height = height; + camera_info_msg.width = width; + if (intrinsic.model_id == 2) // This is a fish eye lens + { + // Fill in the message with fish eye model params + camera_info_msg.distortion_model = sensor_msgs::distortion_models::EQUIDISTANT; + + // Read data from buffer + const float fx = intrinsic.model_parameters[0]; + const float fy = intrinsic.model_parameters[1]; + + const float mx = intrinsic.model_parameters[2]; + const float my = intrinsic.model_parameters[3]; + const float alpha = intrinsic.model_parameters[4]; + const float k1 = intrinsic.model_parameters[5]; + const float k2 = intrinsic.model_parameters[6]; + const float k3 = intrinsic.model_parameters[7]; + const float k4 = intrinsic.model_parameters[8]; + const float theta_max = intrinsic.model_parameters[9]; + + const float ix = width - 1; + const float iy = height - 1; + const float cy = (iy + 0.5 - my) / fy; + const float cx = (ix + 0.5 - mx) / fx - alpha * cy; + const float r2 = cx * cx + cy * cy; + const float h = 2 * cx * cy; + const float tx = k3 * h + k4 * (r2 + 2 * cx * cx); + const float ty = k3 * (r2 + 2 * cy * cy) + k4 * h; + + // Distortion parameters + camera_info_msg.d.resize(5); + camera_info_msg.d[0] = k1; + camera_info_msg.d[1] = k2; + camera_info_msg.d[2] = tx; // TODO t1 == tx ? + camera_info_msg.d[3] = ty; // TODO t2 == ty ? + camera_info_msg.d[4] = k3; + + // Intrinsic camera matrix, in row-major order + // [ fx 0 cx] + // K = [ 0 fy cy] + // [ 0 0 1 ] + camera_info_msg.k[0] = fx; + camera_info_msg.k[2] = cx; + camera_info_msg.k[4] = fy; + camera_info_msg.k[5] = cy; + camera_info_msg.k[8] = 1.0; // fixed to 1.0 + + // Projection matrix, row-major + // [fx' 0 cx' Tx] + // P = [ 0 fy' cy' Ty] + // [ 0 0 1 0 ] + camera_info_msg.p[0] = fx; + camera_info_msg.p[5] = fy; + camera_info_msg.p[2] = cx; + camera_info_msg.p[6] = cy; + camera_info_msg.p[10] = 1.0; // fixed to 1.0 + + RCLCPP_DEBUG_ONCE(logger, + "Intrinsics:\nfx=%f \nfy=%f \nmx=%f \nmy=%f \nalpha=%f \nk1=%f \nk2=%f \nk3=%f \nk4=%f " + "\nCalculated:\nix=%f \niy=%f \ncx=%f \ncy=%f \nr2=%f \nh=%f \ntx=%f \nty=%f", + fx, fy, mx, my, alpha, k1, k2, k3, k4, ix, iy, cx, cy, r2, h, tx, ty); + } + else if (intrinsic.model_id == 0) + { + camera_info_msg.distortion_model = sensor_msgs::distortion_models::PLUMB_BOB; + + // Read data from buffer + const float fx = intrinsic.model_parameters[0]; + const float fy = intrinsic.model_parameters[1]; + + const float mx = intrinsic.model_parameters[2]; + const float my = intrinsic.model_parameters[3]; + const float alpha = intrinsic.model_parameters[4]; + const float k1 = intrinsic.model_parameters[5]; + const float k2 = intrinsic.model_parameters[6]; + const float k3 = intrinsic.model_parameters[7]; + const float k4 = intrinsic.model_parameters[8]; + const float k5 = intrinsic.model_parameters[9]; + + const float ix = width - 1; + const float iy = height - 1; + const float cy = (iy + 0.5 - my) / fy; + const float cx = (ix + 0.5 - mx) / fx - alpha * cy; + const float r2 = cx * cx + cy * cy; + const float h = 2 * cx * cy; + const float tx = k3 * h + k4 * (r2 + 2 * cx * cx); + const float ty = k3 * (r2 + 2 * cy * cy) + k4 * h; + + // Distortion parameters + camera_info_msg.d.resize(5); + camera_info_msg.d[0] = k1; + camera_info_msg.d[1] = k2; + camera_info_msg.d[2] = tx; // TODO t1 == tx ? + camera_info_msg.d[3] = ty; // TODO t2 == ty ? + camera_info_msg.d[4] = k3; + + // Intrinsic camera matrix + camera_info_msg.k[0] = fx; + camera_info_msg.k[4] = fy; + camera_info_msg.k[2] = cx; + camera_info_msg.k[5] = cy; + camera_info_msg.k[8] = 1.0; // fixed to 1.0 + + // Projection matrix + camera_info_msg.p[0] = fx; + camera_info_msg.p[5] = fy; + camera_info_msg.p[2] = cx; + camera_info_msg.p[6] = cy; + camera_info_msg.p[10] = 1.0; // fixed to 1.0 + + RCLCPP_DEBUG_ONCE(logger, + "Intrinsics:\nfx=%f \nfy=%f \nmx=%f \nmy=%f \nalpha=%f \nk1=%f \nk2=%f \nk3=%f \nk4=%f " + "\nCalculated:\nix=%f \niy=%f \ncx=%f \ncy=%f \nr2=%f \nh=%f \ntx=%f \nty=%f", + fx, fy, mx, my, alpha, k1, k2, k3, k4, ix, iy, cx, cy, r2, h, tx, ty); + } + else{ + RCLCPP_ERROR(logger, "Unknown intrinsic calibration model"); + } + } + catch (const std::out_of_range& ex) + { + RCLCPP_WARN(logger, "Out-of-range error fetching intrinsics"); + } + return camera_info_msg; +} + +ifm3d_ros2::msg::Intrinsics ifm3d_to_intrinsics(ifm3d::Buffer& buffer, const std_msgs::msg::Header& header, + const rclcpp::Logger& logger) +{ + ifm3d_ros2::msg::Intrinsics intrinsics_msg; + intrinsics_msg.header = header; + + ifm3d::calibration::IntrinsicCalibration intrinsics; + + try + { + intrinsics.Read(buffer.ptr(0)); + intrinsics_msg.model_id = intrinsics.model_id; + intrinsics_msg.model_parameters = intrinsics.model_parameters; + } + catch (...) + { + RCLCPP_ERROR(logger, "Failed to read intrinsics."); + } + + return intrinsics_msg; +} + +ifm3d_ros2::msg::Intrinsics ifm3d_to_intrinsics(ifm3d::Buffer&& buffer, const std_msgs::msg::Header& header, + const rclcpp::Logger& logger) + +{ + return ifm3d_to_intrinsics(buffer, header, logger); +} + +ifm3d_ros2::msg::InverseIntrinsics ifm3d_to_inverse_intrinsics(ifm3d::Buffer& buffer, + const std_msgs::msg::Header& header, + const rclcpp::Logger& logger) +{ + ifm3d_ros2::msg::InverseIntrinsics inverse_intrinsics_msg; + inverse_intrinsics_msg.header = header; + + ifm3d::calibration::InverseIntrinsicCalibration inverse_intrinsics; + + try + { + inverse_intrinsics.Read(buffer.ptr(0)); + inverse_intrinsics_msg.model_id = inverse_intrinsics.model_id; + inverse_intrinsics_msg.model_parameters = inverse_intrinsics.model_parameters; + } + catch (...) + { + RCLCPP_ERROR(logger, "Failed to read inverse intrinsics."); + } + + return inverse_intrinsics_msg; +} + +ifm3d_ros2::msg::InverseIntrinsics ifm3d_to_inverse_intrinsics(ifm3d::Buffer&& buffer, + const std_msgs::msg::Header& header, + const rclcpp::Logger& logger) + +{ + return ifm3d_to_inverse_intrinsics(buffer, header, logger); +} + +ifm3d_ros2::msg::RGBInfo ifm3d_to_rgb_info(ifm3d::Buffer& buffer, const std_msgs::msg::Header& header, + const rclcpp::Logger& logger) +{ + ifm3d_ros2::msg::RGBInfo rgb_info_msg; + rgb_info_msg.header = header; + + try + { + auto rgb_info = ifm3d::RGBInfoV1::Deserialize(buffer); + + rgb_info_msg.version = rgb_info.version; + rgb_info_msg.frame_counter = rgb_info.frame_counter; + rgb_info_msg.timestamp_ns = rgb_info.timestamp_ns; + rgb_info_msg.exposure_time = rgb_info.exposure_time; + + rgb_info_msg.extrinsics.header = header; + rgb_info_msg.extrinsics.tx = rgb_info.extrinsic_optic_to_user.trans_x; + rgb_info_msg.extrinsics.ty = rgb_info.extrinsic_optic_to_user.trans_y; + rgb_info_msg.extrinsics.tz = rgb_info.extrinsic_optic_to_user.trans_z; + rgb_info_msg.extrinsics.rot_x = rgb_info.extrinsic_optic_to_user.rot_x; + rgb_info_msg.extrinsics.rot_y = rgb_info.extrinsic_optic_to_user.rot_y; + rgb_info_msg.extrinsics.rot_z = rgb_info.extrinsic_optic_to_user.rot_z; + + rgb_info_msg.intrinsics.header = header; + rgb_info_msg.intrinsics.model_id = rgb_info.intrinsic_calibration.model_id; + rgb_info_msg.intrinsics.model_parameters = rgb_info.intrinsic_calibration.model_parameters; + + rgb_info_msg.inverse_intrinsics.header = header; + rgb_info_msg.inverse_intrinsics.model_id = rgb_info.inverse_intrinsic_calibration.model_id; + rgb_info_msg.inverse_intrinsics.model_parameters = rgb_info.inverse_intrinsic_calibration.model_parameters; + } + catch (...) + { + RCLCPP_ERROR(logger, "Failed to read rgb info."); + } + + return rgb_info_msg; +} + +ifm3d_ros2::msg::RGBInfo ifm3d_to_rgb_info(ifm3d::Buffer&& buffer, const std_msgs::msg::Header& header, + const rclcpp::Logger& logger) + +{ + return ifm3d_to_rgb_info(buffer, header, logger); +} + +ifm3d_ros2::msg::TOFInfo ifm3d_to_tof_info(ifm3d::Buffer& buffer, const std_msgs::msg::Header& header, + const rclcpp::Logger& logger) +{ + ifm3d_ros2::msg::TOFInfo tof_info_msg; + tof_info_msg.header = header; + + try + { + auto tof_info = ifm3d::TOFInfoV4::Deserialize(buffer); + tof_info_msg.measurement_block_index = tof_info.measurement_block_index; + tof_info_msg.measurement_range_min = tof_info.measurement_range_min; + tof_info_msg.measurement_range_max = tof_info.measurement_range_max; + tof_info_msg.version = tof_info.version; + tof_info_msg.distance_resolution = tof_info.distance_resolution; + tof_info_msg.amplitude_resolution = tof_info.amplitude_resolution; + tof_info_msg.amp_normalization_factors = tof_info.amp_normalization_factors; + tof_info_msg.exposure_timestamps_ns = tof_info.exposure_timestamps_ns; + tof_info_msg.exposure_times_s = tof_info.exposure_times_s; + tof_info_msg.illu_temperature = tof_info.illu_temperature; + tof_info_msg.mode = std::string(std::begin(tof_info.mode), std::end(tof_info.mode)); + tof_info_msg.imager = std::string(std::begin(tof_info.imager), std::end(tof_info.imager)); + + tof_info_msg.extrinsics.header = header; + tof_info_msg.extrinsics.tx = tof_info.extrinsic_optic_to_user.trans_x; + tof_info_msg.extrinsics.ty = tof_info.extrinsic_optic_to_user.trans_y; + tof_info_msg.extrinsics.tz = tof_info.extrinsic_optic_to_user.trans_z; + tof_info_msg.extrinsics.rot_x = tof_info.extrinsic_optic_to_user.rot_x; + tof_info_msg.extrinsics.rot_y = tof_info.extrinsic_optic_to_user.rot_y; + tof_info_msg.extrinsics.rot_z = tof_info.extrinsic_optic_to_user.rot_z; + + tof_info_msg.intrinsics.header = header; + tof_info_msg.intrinsics.model_id = tof_info.intrinsic_calibration.model_id; + tof_info_msg.intrinsics.model_parameters = tof_info.intrinsic_calibration.model_parameters; + + tof_info_msg.inverse_intrinsics.header = header; + tof_info_msg.inverse_intrinsics.model_id = tof_info.inverse_intrinsic_calibration.model_id; + tof_info_msg.inverse_intrinsics.model_parameters = tof_info.inverse_intrinsic_calibration.model_parameters; + } + catch (...) + { + RCLCPP_ERROR(logger, "Failed to read tof info."); + } + + return tof_info_msg; +} + +ifm3d_ros2::msg::TOFInfo ifm3d_to_tof_info(ifm3d::Buffer&& buffer, const std_msgs::msg::Header& header, + const rclcpp::Logger& logger) + +{ + return ifm3d_to_tof_info(buffer, header, logger); +} + +rclcpp::Time ifm3d_to_ros_time(const TimePointT& time_point) +{ + auto duration = time_point.time_since_epoch(); + int64_t nanoseconds = std::chrono::duration_cast(duration).count(); + return rclcpp::Time(nanoseconds, RCL_SYSTEM_TIME); +} + +geometry_msgs::msg::TransformStamped trans_rot_to_optical_mount_link(std::vector trans_rot, + std::uint64_t timestamp, + std::string mounting_frame_name, + std::string optical_frame_name) +{ + /* + * tf2::Quaternion assumes extrinsic euler angles as roll pitch yaw for setRPY(), + * meaning all three rotations happen in relation to a fixed coordinate system. + * But the RPY angles from ifm3d are performing an intrinsic euler rotation, + * the reference coordinate system is updated after each individual rotation. + * Therefore, we split the received rotation into three different quaternions + * and perform the rotations in R->P->Y order. + */ + tf2::Quaternion q_roll, q_pitch, q_yaw, q_combined; + q_roll.setRPY(trans_rot[3], 0.0, 0.0); + q_pitch.setRPY(0.0, trans_rot[4], 0.0); + q_yaw.setRPY(0.0, 0.0, trans_rot[5]); + q_combined = q_roll * q_pitch * q_yaw; + + geometry_msgs::msg::TransformStamped t; + t.header.stamp = rclcpp::Time(timestamp); + t.header.frame_id = mounting_frame_name; + t.child_frame_id = optical_frame_name; + t.transform.translation.x = trans_rot[0]; + t.transform.translation.y = trans_rot[1]; + t.transform.translation.z = trans_rot[2]; + t.transform.rotation.x = q_combined.x(); + t.transform.rotation.y = q_combined.y(); + t.transform.rotation.z = q_combined.z(); + t.transform.rotation.w = q_combined.w(); + return t; +} + +bool ifm3d_extrinsic_opt_to_user_to_optical_mount_link(ifm3d::calibration::ExtrinsicOpticToUser opt_to_user, + std::uint64_t timestamp, std::string mounting_frame_name, + std::string optical_frame_name, const rclcpp::Logger& logger, + geometry_msgs::msg::TransformStamped& tf) +{ + try + { + std::vector trans_rot = { opt_to_user.trans_x, opt_to_user.trans_y, opt_to_user.trans_z, + opt_to_user.rot_x, opt_to_user.rot_y, opt_to_user.rot_z }; + tf = trans_rot_to_optical_mount_link(trans_rot, timestamp, mounting_frame_name, optical_frame_name); + return true; + } + catch (...) + { + RCLCPP_ERROR(logger, "Failed to read tof info."); + return false; + } +} + +bool ifm3d_rgb_info_to_optical_mount_link(ifm3d::Buffer& buffer, std::string mounting_frame_name, + std::string optical_frame_name, const rclcpp::Logger& logger, + geometry_msgs::msg::TransformStamped& tf) +{ + try + { + auto rgb_info = ifm3d::RGBInfoV1::Deserialize(buffer); + return ifm3d_extrinsic_opt_to_user_to_optical_mount_link(rgb_info.extrinsic_optic_to_user, rgb_info.timestamp_ns, + mounting_frame_name, optical_frame_name, logger, tf); + } + catch (...) + { + RCLCPP_ERROR(logger, "Failed to read rgb info."); + return false; + } +} + +bool ifm3d_rgb_info_to_optical_mount_link(ifm3d::Buffer&& buffer, std::string mounting_frame_name, + std::string optical_frame_name, const rclcpp::Logger& logger, + geometry_msgs::msg::TransformStamped& tf) +{ + return ifm3d_rgb_info_to_optical_mount_link(buffer, mounting_frame_name, optical_frame_name, logger, tf); +} + +bool ifm3d_tof_info_to_optical_mount_link(ifm3d::Buffer& buffer, std::string mounting_frame_name, + std::string optical_frame_name, const rclcpp::Logger& logger, + geometry_msgs::msg::TransformStamped& tf) +{ + try + { + auto tof_info = ifm3d::TOFInfoV4::Deserialize(buffer); + return ifm3d_extrinsic_opt_to_user_to_optical_mount_link(tof_info.extrinsic_optic_to_user, + tof_info.exposure_timestamps_ns[0], mounting_frame_name, + optical_frame_name, logger, tf); + } + catch (...) + { + RCLCPP_ERROR(logger, "Failed to read tof info."); + return false; + } +} + +bool ifm3d_tof_info_to_optical_mount_link(ifm3d::Buffer&& buffer, std::string mounting_frame_name, + std::string optical_frame_name, const rclcpp::Logger& logger, + geometry_msgs::msg::TransformStamped& tf) +{ + return ifm3d_tof_info_to_optical_mount_link(buffer, mounting_frame_name, optical_frame_name, logger, tf); +} + +} // namespace ifm3d_ros2 diff --git a/src/lib/buffer_id_utils.cpp b/src/lib/buffer_id_utils.cpp new file mode 100644 index 0000000..151de2e --- /dev/null +++ b/src/lib/buffer_id_utils.cpp @@ -0,0 +1,250 @@ +// -*- c++ -*- +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright (C) 2024 ifm electronic, gmbh + */ + +#include +#include +#include + +#include +#include +#include + +#include + +namespace ifm3d_ros2 +{ +namespace buffer_id_utils +{ +std::map buffer_id_map = { + { "RADIAL_DISTANCE_IMAGE", ifm3d::buffer_id::RADIAL_DISTANCE_IMAGE }, + { "NORM_AMPLITUDE_IMAGE", ifm3d::buffer_id::NORM_AMPLITUDE_IMAGE }, + { "AMPLITUDE_IMAGE", ifm3d::buffer_id::AMPLITUDE_IMAGE }, + { "GRAYSCALE_IMAGE", ifm3d::buffer_id::GRAYSCALE_IMAGE }, + { "RADIAL_DISTANCE_NOISE", ifm3d::buffer_id::RADIAL_DISTANCE_NOISE }, + { "REFLECTIVITY", ifm3d::buffer_id::REFLECTIVITY }, + { "CARTESIAN_X_COMPONENT", ifm3d::buffer_id::CARTESIAN_X_COMPONENT }, + { "CARTESIAN_Y_COMPONENT", ifm3d::buffer_id::CARTESIAN_Y_COMPONENT }, + { "CARTESIAN_Z_COMPONENT", ifm3d::buffer_id::CARTESIAN_Z_COMPONENT }, + { "CARTESIAN_ALL", ifm3d::buffer_id::CARTESIAN_ALL }, + { "UNIT_VECTOR_ALL", ifm3d::buffer_id::UNIT_VECTOR_ALL }, + { "MONOCHROM_2D_12BIT", ifm3d::buffer_id::MONOCHROM_2D_12BIT }, + { "MONOCHROM_2D", ifm3d::buffer_id::MONOCHROM_2D }, + { "JPEG_IMAGE", ifm3d::buffer_id::JPEG_IMAGE }, + { "CONFIDENCE_IMAGE", ifm3d::buffer_id::CONFIDENCE_IMAGE }, + { "DIAGNOSTIC", ifm3d::buffer_id::DIAGNOSTIC }, + { "JSON_DIAGNOSTIC", ifm3d::buffer_id::JSON_DIAGNOSTIC }, + { "EXTRINSIC_CALIB", ifm3d::buffer_id::EXTRINSIC_CALIB }, + { "INTRINSIC_CALIB", ifm3d::buffer_id::INTRINSIC_CALIB }, + { "INVERSE_INTRINSIC_CALIBRATION", ifm3d::buffer_id::INVERSE_INTRINSIC_CALIBRATION }, + { "TOF_INFO", ifm3d::buffer_id::TOF_INFO }, + { "RGB_INFO", ifm3d::buffer_id::RGB_INFO }, + { "JSON_MODEL", ifm3d::buffer_id::JSON_MODEL }, + { "ALGO_DEBUG", ifm3d::buffer_id::ALGO_DEBUG }, + { "O3R_ODS_OCCUPANCY_GRID", ifm3d::buffer_id::O3R_ODS_OCCUPANCY_GRID }, + { "O3R_ODS_INFO", ifm3d::buffer_id::O3R_ODS_INFO }, + { "XYZ", ifm3d::buffer_id::XYZ }, + { "EXPOSURE_TIME", ifm3d::buffer_id::EXPOSURE_TIME }, + { "ILLUMINATION_TEMP", ifm3d::buffer_id::ILLUMINATION_TEMP } +}; + +std::multimap data_stream_type_map = { + { ifm3d::buffer_id::RADIAL_DISTANCE_IMAGE, ifm3d_ros2::buffer_id_utils::data_stream_type::tof_3d }, + { ifm3d::buffer_id::NORM_AMPLITUDE_IMAGE, ifm3d_ros2::buffer_id_utils::data_stream_type::tof_3d }, + { ifm3d::buffer_id::AMPLITUDE_IMAGE, ifm3d_ros2::buffer_id_utils::data_stream_type::tof_3d }, + { ifm3d::buffer_id::GRAYSCALE_IMAGE, ifm3d_ros2::buffer_id_utils::data_stream_type::tof_3d }, + { ifm3d::buffer_id::RADIAL_DISTANCE_NOISE, ifm3d_ros2::buffer_id_utils::data_stream_type::tof_3d }, + { ifm3d::buffer_id::REFLECTIVITY, ifm3d_ros2::buffer_id_utils::data_stream_type::tof_3d }, + { ifm3d::buffer_id::CARTESIAN_X_COMPONENT, ifm3d_ros2::buffer_id_utils::data_stream_type::tof_3d }, + { ifm3d::buffer_id::CARTESIAN_Y_COMPONENT, ifm3d_ros2::buffer_id_utils::data_stream_type::tof_3d }, + { ifm3d::buffer_id::CARTESIAN_Z_COMPONENT, ifm3d_ros2::buffer_id_utils::data_stream_type::tof_3d }, + { ifm3d::buffer_id::CARTESIAN_ALL, ifm3d_ros2::buffer_id_utils::data_stream_type::tof_3d }, + { ifm3d::buffer_id::UNIT_VECTOR_ALL, ifm3d_ros2::buffer_id_utils::data_stream_type::tof_3d }, + { ifm3d::buffer_id::MONOCHROM_2D_12BIT, ifm3d_ros2::buffer_id_utils::data_stream_type::rgb_2d }, + { ifm3d::buffer_id::MONOCHROM_2D, ifm3d_ros2::buffer_id_utils::data_stream_type::rgb_2d }, + { ifm3d::buffer_id::JPEG_IMAGE, ifm3d_ros2::buffer_id_utils::data_stream_type::rgb_2d }, + { ifm3d::buffer_id::CONFIDENCE_IMAGE, ifm3d_ros2::buffer_id_utils::data_stream_type::tof_3d }, + { ifm3d::buffer_id::DIAGNOSTIC, ifm3d_ros2::buffer_id_utils::data_stream_type::tof_3d }, + { ifm3d::buffer_id::DIAGNOSTIC, ifm3d_ros2::buffer_id_utils::data_stream_type::rgb_2d }, + { ifm3d::buffer_id::JSON_DIAGNOSTIC, ifm3d_ros2::buffer_id_utils::data_stream_type::tof_3d }, + { ifm3d::buffer_id::JSON_DIAGNOSTIC, ifm3d_ros2::buffer_id_utils::data_stream_type::rgb_2d }, + { ifm3d::buffer_id::EXTRINSIC_CALIB, ifm3d_ros2::buffer_id_utils::data_stream_type::tof_3d }, + { ifm3d::buffer_id::INTRINSIC_CALIB, ifm3d_ros2::buffer_id_utils::data_stream_type::tof_3d }, + { ifm3d::buffer_id::INVERSE_INTRINSIC_CALIBRATION, ifm3d_ros2::buffer_id_utils::data_stream_type::tof_3d }, + { ifm3d::buffer_id::TOF_INFO, ifm3d_ros2::buffer_id_utils::data_stream_type::tof_3d }, + { ifm3d::buffer_id::RGB_INFO, ifm3d_ros2::buffer_id_utils::data_stream_type::rgb_2d }, + { ifm3d::buffer_id::JSON_MODEL, ifm3d_ros2::buffer_id_utils::data_stream_type::tof_3d }, + { ifm3d::buffer_id::ALGO_DEBUG, ifm3d_ros2::buffer_id_utils::data_stream_type::tof_3d }, + { ifm3d::buffer_id::O3R_ODS_OCCUPANCY_GRID, ifm3d_ros2::buffer_id_utils::data_stream_type::ods }, + { ifm3d::buffer_id::O3R_ODS_INFO, ifm3d_ros2::buffer_id_utils::data_stream_type::ods }, + { ifm3d::buffer_id::XYZ, ifm3d_ros2::buffer_id_utils::data_stream_type::tof_3d }, + { ifm3d::buffer_id::EXPOSURE_TIME, ifm3d_ros2::buffer_id_utils::data_stream_type::tof_3d }, + { ifm3d::buffer_id::ILLUMINATION_TEMP, ifm3d_ros2::buffer_id_utils::data_stream_type::tof_3d } +}; + +std::map message_type_map = { + { ifm3d::buffer_id::RADIAL_DISTANCE_IMAGE, ifm3d_ros2::buffer_id_utils::message_type::raw_image }, + { ifm3d::buffer_id::NORM_AMPLITUDE_IMAGE, ifm3d_ros2::buffer_id_utils::message_type::raw_image }, + { ifm3d::buffer_id::AMPLITUDE_IMAGE, ifm3d_ros2::buffer_id_utils::message_type::raw_image }, + { ifm3d::buffer_id::GRAYSCALE_IMAGE, ifm3d_ros2::buffer_id_utils::message_type::not_implemented }, + { ifm3d::buffer_id::RADIAL_DISTANCE_NOISE, ifm3d_ros2::buffer_id_utils::message_type::not_implemented }, + { ifm3d::buffer_id::REFLECTIVITY, ifm3d_ros2::buffer_id_utils::message_type::not_implemented }, + { ifm3d::buffer_id::CARTESIAN_X_COMPONENT, ifm3d_ros2::buffer_id_utils::message_type::not_implemented }, + { ifm3d::buffer_id::CARTESIAN_Y_COMPONENT, ifm3d_ros2::buffer_id_utils::message_type::not_implemented }, + { ifm3d::buffer_id::CARTESIAN_Z_COMPONENT, ifm3d_ros2::buffer_id_utils::message_type::not_implemented }, + { ifm3d::buffer_id::CARTESIAN_ALL, ifm3d_ros2::buffer_id_utils::message_type::not_implemented }, + { ifm3d::buffer_id::UNIT_VECTOR_ALL, ifm3d_ros2::buffer_id_utils::message_type::not_implemented }, + { ifm3d::buffer_id::MONOCHROM_2D_12BIT, ifm3d_ros2::buffer_id_utils::message_type::not_implemented }, + { ifm3d::buffer_id::MONOCHROM_2D, ifm3d_ros2::buffer_id_utils::message_type::not_implemented }, + { ifm3d::buffer_id::JPEG_IMAGE, ifm3d_ros2::buffer_id_utils::message_type::compressed_image }, + { ifm3d::buffer_id::CONFIDENCE_IMAGE, ifm3d_ros2::buffer_id_utils::message_type::raw_image }, + { ifm3d::buffer_id::DIAGNOSTIC, ifm3d_ros2::buffer_id_utils::message_type::not_implemented }, + { ifm3d::buffer_id::JSON_DIAGNOSTIC, ifm3d_ros2::buffer_id_utils::message_type::not_implemented }, + { ifm3d::buffer_id::EXTRINSIC_CALIB, ifm3d_ros2::buffer_id_utils::message_type::extrinsics }, + { ifm3d::buffer_id::INTRINSIC_CALIB, ifm3d_ros2::buffer_id_utils::message_type::intrinsics }, + { ifm3d::buffer_id::INVERSE_INTRINSIC_CALIBRATION, ifm3d_ros2::buffer_id_utils::message_type::inverse_intrinsics }, + { ifm3d::buffer_id::TOF_INFO, ifm3d_ros2::buffer_id_utils::message_type::tof_info }, + { ifm3d::buffer_id::RGB_INFO, ifm3d_ros2::buffer_id_utils::message_type::rgb_info }, + { ifm3d::buffer_id::JSON_MODEL, ifm3d_ros2::buffer_id_utils::message_type::not_implemented }, + { ifm3d::buffer_id::ALGO_DEBUG, ifm3d_ros2::buffer_id_utils::message_type::not_implemented }, + { ifm3d::buffer_id::O3R_ODS_OCCUPANCY_GRID, ifm3d_ros2::buffer_id_utils::message_type::occupancy_grid }, + { ifm3d::buffer_id::O3R_ODS_INFO, ifm3d_ros2::buffer_id_utils::message_type::zones }, + { ifm3d::buffer_id::XYZ, ifm3d_ros2::buffer_id_utils::message_type::pointcloud }, + { ifm3d::buffer_id::EXPOSURE_TIME, ifm3d_ros2::buffer_id_utils::message_type::not_implemented }, + { ifm3d::buffer_id::ILLUMINATION_TEMP, ifm3d_ros2::buffer_id_utils::message_type::not_implemented } +}; + +std::map topic_name_map = { + { ifm3d::buffer_id::RADIAL_DISTANCE_IMAGE, "distance" }, + { ifm3d::buffer_id::NORM_AMPLITUDE_IMAGE, "amplitude" }, + { ifm3d::buffer_id::AMPLITUDE_IMAGE, "raw_amplitude" }, + { ifm3d::buffer_id::GRAYSCALE_IMAGE, "GRAYSCALE_IMAGE" }, + { ifm3d::buffer_id::RADIAL_DISTANCE_NOISE, "RADIAL_DISTANCE_NOISE" }, + { ifm3d::buffer_id::REFLECTIVITY, "REFLECTIVITY" }, + { ifm3d::buffer_id::CARTESIAN_X_COMPONENT, "CARTESIAN_X_COMPONENT" }, + { ifm3d::buffer_id::CARTESIAN_Y_COMPONENT, "CARTESIAN_Y_COMPONENT" }, + { ifm3d::buffer_id::CARTESIAN_Z_COMPONENT, "CARTESIAN_Z_COMPONENT" }, + { ifm3d::buffer_id::CARTESIAN_ALL, "CARTESIAN_ALL" }, + { ifm3d::buffer_id::UNIT_VECTOR_ALL, "UNIT_VECTOR_ALL" }, + { ifm3d::buffer_id::MONOCHROM_2D_12BIT, "MONOCHROM_2D_12BIT" }, + { ifm3d::buffer_id::MONOCHROM_2D, "MONOCHROM_2D" }, + { ifm3d::buffer_id::JPEG_IMAGE, "rgb" }, + { ifm3d::buffer_id::CONFIDENCE_IMAGE, "confidence" }, + { ifm3d::buffer_id::DIAGNOSTIC, "DIAGNOSTIC" }, + { ifm3d::buffer_id::JSON_DIAGNOSTIC, "JSON_DIAGNOSTIC" }, + { ifm3d::buffer_id::EXTRINSIC_CALIB, "extrinsics" }, + { ifm3d::buffer_id::INTRINSIC_CALIB, "intrinsic_calib" }, + { ifm3d::buffer_id::INVERSE_INTRINSIC_CALIBRATION, "inverse_intrinsic_calibration" }, + { ifm3d::buffer_id::TOF_INFO, "tof_info" }, + { ifm3d::buffer_id::RGB_INFO, "rgb_info" }, + { ifm3d::buffer_id::JSON_MODEL, "JSON_MODEL" }, + { ifm3d::buffer_id::ALGO_DEBUG, "ALGO_DEBUG" }, + { ifm3d::buffer_id::O3R_ODS_OCCUPANCY_GRID, "ods_occupancy_grid" }, + { ifm3d::buffer_id::O3R_ODS_INFO, "ods_info" }, + { ifm3d::buffer_id::XYZ, "cloud" }, + { ifm3d::buffer_id::EXPOSURE_TIME, "EXPOSURE_TIME" }, + { ifm3d::buffer_id::ILLUMINATION_TEMP, "ILLUMINATION_TEMP" } +}; + +bool convert(const std::string& string, ifm3d::buffer_id& buffer_id) +{ + if (!buffer_id_map.count(string)) + { + // key does not exist + return false; + } + + buffer_id = buffer_id_map[string]; + return true; +} + +bool convert(const ifm3d::buffer_id& buffer_id, std::string& string) +{ + // Iterate over all map entries + for (auto const& [key, value] : buffer_id_map) + { + if (value == buffer_id) + { + string = key; + return true; + } + } + + // buffer_id not found + return false; +} + +std::vector buffer_ids_for_data_stream_type(const std::vector& input_ids, + const ifm3d_ros2::buffer_id_utils::data_stream_type& type) +{ + typedef std::multimap::iterator mm_iterator; + + std::vector ret_vector; + + for (ifm3d::buffer_id input_id : input_ids) + { + // Get iterators for multimap subsection of given buffer_id + // Remember, multimaps are always sorted by their key + std::pair result = + ifm3d_ros2::buffer_id_utils::data_stream_type_map.equal_range(input_id); + + // Look for matching data_streamtype, iterating over the subsection + for (mm_iterator it = result.first; it != result.second; it++) + { + if (it->second == type) + { + ret_vector.push_back(input_id); + } + } + } + + return ret_vector; +} + +std::string vector_to_string(const std::vector& vector) +{ + if (vector.empty()) + { + return std::string(""); + } + + std::ostringstream stream; + const std::string delimiter = ", "; + + std::copy(vector.begin(), vector.end(), std::ostream_iterator(stream, delimiter.c_str())); + + const std::string& output = stream.str(); + + return output.substr(0, output.length() - delimiter.length()); +} + +/** + * Helper to create one string from a vector of buffer_ids + */ +std::string vector_to_string(const std::vector& vector) +{ + if (vector.empty()) + { + return std::string(""); + } + + std::ostringstream stream; + const std::string delimiter = ", "; + + for (const auto& id : vector) + { + std::string string; + convert(id, string); + stream << string << delimiter; + } + + const std::string& output = stream.str(); + + return output.substr(0, output.length() - delimiter.length()); +} + +} // namespace buffer_id_utils + +} // namespace ifm3d_ros2 diff --git a/src/lib/camera_node.cpp b/src/lib/camera_node.cpp index e8aba47..0412d19 100644 --- a/src/lib/camera_node.cpp +++ b/src/lib/camera_node.cpp @@ -1,27 +1,28 @@ /* * SPDX-License-Identifier: Apache-2.0 - * Copyright (C) 2019 ifm electronic, gmbh + * Copyright (C) 2024 ifm electronic, gmbh */ +#include #include #include #include #include #include +#include #include #include #include #include #include +#include +#include #include -#include #include #include -#include -#include #include using json = ifm3d::json; @@ -29,18 +30,12 @@ using namespace std::chrono_literals; namespace ifm3d_ros2 { -namespace -{ -constexpr auto xmlrpc_base_port = 50010; - -} // namespace - CameraNode::CameraNode(const rclcpp::NodeOptions& opts) : CameraNode::CameraNode("camera", opts) { } CameraNode::CameraNode(const std::string& node_name, const rclcpp::NodeOptions& opts) - : rclcpp_lifecycle::LifecycleNode(node_name, "", opts), logger_(this->get_logger()), width_(0), height_(0) + : rclcpp_lifecycle::LifecycleNode(node_name, "", opts), logger_(this->get_logger()) { // unbuffered I/O to stdout (so we can see our log messages) std::setvbuf(stdout, nullptr, _IONBF, BUFSIZ); @@ -48,9 +43,6 @@ CameraNode::CameraNode(const std::string& node_name, const rclcpp::NodeOptions& RCLCPP_INFO(this->logger_, "node name: %s", this->get_name()); RCLCPP_INFO(this->logger_, "middleware: %s", rmw_get_implementation_identifier()); - hardware_id_ = std::string(get_namespace()) + "/" + std::string(get_name()); - RCLCPP_INFO(logger_, "hardware_id: %s", hardware_id_.c_str()); - // declare our parameters and default values -- parameters defined in // the passed in `opts` (via __params:=/path/to/params.yaml on cmd line) // will override our default values specified. @@ -58,24 +50,7 @@ CameraNode::CameraNode(const std::string& node_name, const rclcpp::NodeOptions& this->init_params(); RCLCPP_INFO(this->logger_, "After the parameters declaration"); - // - // Set up our service servers - // - this->dump_srv_ = - this->create_service("~/Dump", std::bind(&ifm3d_ros2::CameraNode::Dump, this, std::placeholders::_1, - std::placeholders::_2, std::placeholders::_3)); - - this->config_srv_ = this->create_service( - "~/Config", std::bind(&ifm3d_ros2::CameraNode::Config, this, std::placeholders::_1, std::placeholders::_2, - std::placeholders::_3)); - - this->soft_off_srv_ = this->create_service( - "~/Softoff", std::bind(&ifm3d_ros2::CameraNode::Softoff, this, std::placeholders::_1, std::placeholders::_2, - std::placeholders::_3)); - - this->soft_on_srv_ = this->create_service( - "~/Softon", std::bind(&ifm3d_ros2::CameraNode::Softon, this, std::placeholders::_1, std::placeholders::_2, - std::placeholders::_3)); + this->gil_ = std::make_shared(); RCLCPP_INFO(this->logger_, "node created, waiting for `configure()`..."); } @@ -106,65 +81,110 @@ TC_RETVAL CameraNode::on_configure(const rclcpp_lifecycle::State& prev_state) } // - // Set up our publishers. + // We need a global lock on all the ifm3d core data structures + // + std::lock_guard lock(*this->gil_); + + // + // Initialize the camera interface // - this->initialize_publishers(); - RCLCPP_INFO(this->logger_, "After publishers declaration"); + RCLCPP_INFO(this->logger_, "Initializing camera..."); + this->o3r_ = std::make_shared(this->ip_, this->xmlrpc_port_); + RCLCPP_INFO(this->logger_, "Initializing FrameGrabber"); + this->fg_ = std::make_shared(this->o3r_, this->pcic_port_); + // We do not use the asynchronous diagnostic right now, so no need to declare + // the diagnostic framegrabber + // this->fg_diag_ = std::make_shared(this->o3r_, 50009); + + if (this->config_file_!=""){ + std::ifstream file(this->config_file_); + if (!file.is_open()) { + throw std::runtime_error("Could not open config file: " + this->config_file_); + } + std::stringstream buffer; + buffer << file.rdbuf(); + RCLCPP_INFO(this->logger_, "Setting configuration: %s", buffer.str().c_str()); + ifm3d::json config_json = json::parse(buffer.str()) ; + this->o3r_->Set(config_json); + } + + // Get all the necessary info for the port. + for (auto port : this->o3r_->Ports()) + { + if (port.pcic_port == this->pcic_port_) + { + this->port_info_ = port; + } + } + + // Get resolution from port configuration + std::string j_string = "/ports/" + this->port_info_.port + "/info/features/resolution"; + ifm3d::json::json_pointer j(j_string); + auto resolution = this->o3r_->Get({ j_string })[j]; + auto width = static_cast(resolution["width"]); + auto height = static_cast(resolution["height"]); + + // Determine data stream type from port info + this->data_stream_type_ = stream_type_from_port_info(this->port_info_); - // Publish static transform for optical link - publish_optical_link_transform(); + // Create RGB or 3D modules depending on data type + if (this->data_stream_type_ == ifm3d_ros2::buffer_id_utils::data_stream_type::rgb_2d) + { + RCLCPP_INFO(logger_, "Data type is 2D"); + this->data_module_ = + std::make_shared(this->get_logger(), shared_from_this(), o3r_, this->port_info_.port, width, height); + this->buffer_list_.insert(this->buffer_list_.end(), + std::get>(this->data_module_)->buffer_id_list_.begin(), + std::get>(this->data_module_)->buffer_id_list_.end()); + this->modules_.push_back(std::get>(this->data_module_)); + RCLCPP_DEBUG(this->logger_, "RgbModule created."); + } + else if (this->data_stream_type_ == ifm3d_ros2::buffer_id_utils::data_stream_type::tof_3d) + { + RCLCPP_INFO(logger_, "Data type is 3D"); + this->data_module_ = + std::make_shared(this->get_logger(), shared_from_this(), o3r_, this->port_info_.port, width, height); + this->buffer_list_.insert(this->buffer_list_.end(), + std::get>(this->data_module_)->buffer_id_list_.begin(), + std::get>(this->data_module_)->buffer_id_list_.end()); + this->modules_.push_back(std::get>(this->data_module_)); + RCLCPP_DEBUG(this->logger_, "TofModule created."); + } + else + { + RCLCPP_ERROR(this->logger_, "Unknown data stream type"); + return TC_RETVAL::ERROR; + } // - // We need a global lock on all the ifm3d core data structures + // Initialize diagnostic Module // - std::lock_guard lock(this->gil_); + RCLCPP_DEBUG(this->logger_, "Creating DiagModule..."); + this->diag_module_ = std::make_shared(this->get_logger(), shared_from_this(), this->o3r_); + RCLCPP_DEBUG(this->logger_, "DiagModule created."); // - // Initialize the camera interface + // Create a list of all the modules to reduce duplicate code // + this->modules_.push_back(this->diag_module_); - RCLCPP_INFO(this->logger_, "Initializing camera..."); - this->cam_ = std::make_shared(this->ip_, this->xmlrpc_port_); - RCLCPP_INFO(this->logger_, "Initializing FrameGrabber"); - this->fg_ = std::make_shared(this->cam_, this->pcic_port_); - this->fg_diag_ = std::make_shared(this->cam_, 50009); - // Get PortInfo from Camera to determine data stream type - auto ports = this->cam_->Ports(); - this->data_stream_type_ = stream_type_from_port_info(ports, this->pcic_port_); - - // Remove buffer_ids unfit for the given Port - this->buffer_id_list_ = - buffer_id_utils::buffer_ids_for_data_stream_type(this->buffer_id_list_, this->data_stream_type_); - RCLCPP_INFO(logger_, "After removing buffer_ids unfit for the given data stream type, the final list is: [%s].", - buffer_id_utils::vector_to_string(this->buffer_id_list_).c_str()); - - // Timer to pull diagnostics data from ifm3d - RCLCPP_INFO(logger_, "Registering timer to pull diagnostics..."); - diagnostic_timer_ = this->create_wall_timer(1s, [this]() { - std::lock_guard lock(this->gil_); - if (this->diag_mode_=="periodic"){ - try{ - ifm3d::json diagnostic_json = cam_->GetDiagnostic(); - RCLCPP_DEBUG(this->get_logger(), "Diagnostics: %s", diagnostic_json.dump().c_str()); - - DiagnosticArrayMsg msg; - msg.header.stamp = get_clock()->now(); - auto events = diagnostic_json["events"]; - for (auto event : events) - { - msg.status.push_back(create_diagnostic_status(diagnostic_msgs::msg::DiagnosticStatus::OK, event.dump())); - } - diagnostic_publisher_->publish(msg); - } - catch (const ifm3d::Error& ex){ - RCLCPP_INFO(this->get_logger(), "ifm3d error while trying to get the diagnostic: %s", ex.what()); - } - catch (...){ - RCLCPP_INFO(this->get_logger(), "Unknown error while trying to get the diagnostic"); - } + // Transition function modules + for (auto module : this->modules_) + { + auto retval = module->on_configure(prev_state); + if (retval != TC_RETVAL::SUCCESS) + { + RCLCPP_ERROR(this->get_logger(), "Module %s transition did not succeed.", module->get_name().c_str()); + // TODO de-transition previous modules + return retval; } - }); - diagnostic_timer_->cancel(); // Deactivate timer, manage activity via lifecycle + } + + // Initialize the services using the BaseServices class + RCLCPP_INFO(this->logger_, "Creating BaseServices..."); + this->base_services_ = + std::make_shared(this->get_logger(), shared_from_this(), this->o3r_, this->port_info_, this->gil_); + RCLCPP_INFO(this->logger_, "BaseServices created."); RCLCPP_INFO(this->logger_, "Configuration complete."); return TC_RETVAL::SUCCESS; @@ -175,29 +195,36 @@ TC_RETVAL CameraNode::on_activate(const rclcpp_lifecycle::State& prev_state) RCLCPP_INFO(this->logger_, "on_activate(): %s -> %s", prev_state.label().c_str(), this->get_current_state().label().c_str()); - // activate all publishers - RCLCPP_INFO(this->logger_, "Activating publishers..."); - this->activate_publishers(); - RCLCPP_INFO(this->logger_, "Publishers activated."); - - // Start the Framegrabber, needs a BufferList (a vector of std::variant) - RCLCPP_INFO(this->logger_, "Starting the Framegrabber..."); - std::vector> buffer_list{}; - buffer_list.insert(buffer_list.end(), buffer_id_list_.begin(), buffer_id_list_.end()); - // Start framegrabber and wait for the returned future - this->fg_->Start(buffer_list).wait(); // Register Callbacks to handle new frames and print errors this->fg_->OnNewFrame(std::bind(&CameraNode::frame_callback, this, std::placeholders::_1)); this->fg_->OnError(std::bind(&CameraNode::error_callback, this, std::placeholders::_1)); - this->fg_diag_->OnAsyncError(std::bind(&CameraNode::async_error_callback, this, std::placeholders::_1, - std::placeholders::_2)); - this->fg_diag_->OnAsyncNotification(std::bind(&CameraNode::async_notification_callback, this, - std::placeholders::_1, std::placeholders::_2)); - this->fg_diag_->Start({}).wait(); + + // Registering the error and notification callbacks is currently not necessary, + // as currently we only use the periodic diagnostic poll. + // this->fg_diag_->OnAsyncError( + // std::bind(&CameraNode::async_error_callback, this, std::placeholders::_1, std::placeholders::_2)); + // this->fg_diag_->OnAsyncNotification( + // std::bind(&CameraNode::async_notification_callback, this, std::placeholders::_1, std::placeholders::_2)); + + // Start the Framegrabber, needs a BufferList (a vector of std::variant) + RCLCPP_INFO(this->logger_, "Starting the Framegrabber..."); + this->fg_->Start(this->buffer_list_).wait(); + // Currently, the diagnostic framegrabber is not used as we are only using the + // periodic diagnostic + // this->fg_diag_->Start({}).wait(); RCLCPP_INFO(this->logger_, "Framegrabber started."); - diagnostic_timer_->reset(); - RCLCPP_INFO(this->logger_, "Diagnostic monitoring active."); + // Transition function modules + for (auto module : this->modules_) + { + auto retval = module->on_activate(prev_state); + if (retval != TC_RETVAL::SUCCESS) + { + RCLCPP_ERROR(this->get_logger(), "Module %s transition did not succeed.", module->get_name().c_str()); + // TODO de-transition previous modules + return retval; + } + } return TC_RETVAL::SUCCESS; } @@ -207,16 +234,20 @@ TC_RETVAL CameraNode::on_deactivate(const rclcpp_lifecycle::State& prev_state) RCLCPP_INFO(this->logger_, "on_deactivate(): %s -> %s", prev_state.label().c_str(), this->get_current_state().label().c_str()); - diagnostic_timer_->cancel(); - RCLCPP_INFO(this->logger_, "Diagnostic monitoring inactive."); - - RCLCPP_INFO(logger_, "Stopping Framebuffer..."); + RCLCPP_INFO(logger_, "Stopping FrameGrabber..."); this->fg_->Stop().wait(); - // explicitly deactive the publishers - RCLCPP_INFO(this->logger_, "Deactivating publishers..."); - this->deactivate_publishers(); - RCLCPP_INFO(this->logger_, "Publishers deactivated."); + // Transition function modules + for (auto module : this->modules_) + { + auto retval = module->on_deactivate(prev_state); + if (retval != TC_RETVAL::SUCCESS) + { + RCLCPP_ERROR(this->get_logger(), "Module %s transition did not succeed.", module->get_name().c_str()); + // TODO de-transition previous modules + return retval; + } + } return TC_RETVAL::SUCCESS; } @@ -227,11 +258,23 @@ TC_RETVAL CameraNode::on_cleanup(const rclcpp_lifecycle::State& prev_state) RCLCPP_INFO(this->logger_, "on_cleanup(): %s -> %s", prev_state.label().c_str(), this->get_current_state().label().c_str()); - std::lock_guard lock(this->gil_); + std::lock_guard lock(*this->gil_); RCLCPP_INFO(this->logger_, "Resetting core ifm3d data structures..."); this->fg_.reset(); - this->cam_.reset(); + this->o3r_.reset(); + + // Transition function modules + for (auto module : this->modules_) + { + auto retval = module->on_cleanup(prev_state); + if (retval != TC_RETVAL::SUCCESS) + { + RCLCPP_ERROR(this->get_logger(), "Module %s transition did not succeed.", module->get_name().c_str()); + // TODO de-transition previous modules + return retval; + } + } RCLCPP_INFO(this->logger_, "Node cleanup complete."); @@ -243,6 +286,22 @@ TC_RETVAL CameraNode::on_shutdown(const rclcpp_lifecycle::State& prev_state) RCLCPP_INFO(this->logger_, "on_shutdown(): %s -> %s", prev_state.label().c_str(), this->get_current_state().label().c_str()); + // TODO: figure out how to properly shutdown modules + this->fg_->Stop(); + // this->fg_diag_->Stop(); + + // Transition function modules + for (auto module : this->modules_) + { + auto retval = module->on_shutdown(prev_state); + if (retval != TC_RETVAL::SUCCESS) + { + RCLCPP_ERROR(this->get_logger(), "Module %s transition did not succeed.", module->get_name().c_str()); + // TODO de-transition previous modules + return retval; + } + } + return TC_RETVAL::SUCCESS; } @@ -251,11 +310,23 @@ TC_RETVAL CameraNode::on_error(const rclcpp_lifecycle::State& prev_state) RCLCPP_INFO(this->logger_, "on_error(): %s -> %s", prev_state.label().c_str(), this->get_current_state().label().c_str()); - std::lock_guard lock(this->gil_); + std::lock_guard lock(*this->gil_); RCLCPP_INFO(this->logger_, "Resetting core ifm3d data structures..."); - // this->im_.reset(); + this->fg_.reset(); - this->cam_.reset(); + this->o3r_.reset(); + + // Transition function modules + for (auto module : this->modules_) + { + auto retval = module->on_error(prev_state); + if (retval != TC_RETVAL::SUCCESS) + { + RCLCPP_ERROR(this->get_logger(), "Module %s transition did not succeed.", module->get_name().c_str()); + // TODO de-transition previous modules + return retval; + } + } RCLCPP_INFO(this->logger_, "Error processing complete."); @@ -272,22 +343,11 @@ void CameraNode::init_params() * - Define Descriptor * - Declare Parameter */ - - rcl_interfaces::msg::ParameterDescriptor buffer_id_list_descriptor; - const std::vector default_buffer_id_list{ - // - "CONFIDENCE_IMAGE", // - "EXTRINSIC_CALIB", // - "JPEG_IMAGE", // - "NORM_AMPLITUDE_IMAGE", // - "RADIAL_DISTANCE_IMAGE", // - "RGB_INFO", // - "XYZ", // - }; - buffer_id_list_descriptor.name = "buffer_id_list"; - buffer_id_list_descriptor.type = rcl_interfaces::msg::ParameterType::PARAMETER_STRING_ARRAY; - buffer_id_list_descriptor.description = "List of buffer_id strings denoting the wanted buffers."; - this->declare_parameter("buffer_id_list", default_buffer_id_list, buffer_id_list_descriptor); + rcl_interfaces::msg::ParameterDescriptor config_descriptor; + config_descriptor.name = "config_file"; + config_descriptor.type = rcl_interfaces::msg::ParameterType::PARAMETER_STRING; + config_descriptor.description = "Configuration file, in JSON format."; + this->declare_parameter("config_file", "", config_descriptor); rcl_interfaces::msg::ParameterDescriptor ip_descriptor; ip_descriptor.name = "ip"; @@ -304,52 +364,6 @@ void CameraNode::init_params() "is connected to."; this->declare_parameter("pcic_port", ifm3d::DEFAULT_PCIC_PORT, pcic_port_descriptor); - rcl_interfaces::msg::ParameterDescriptor tf_cloud_link_frame_name_descriptor; - tf_cloud_link_frame_name_descriptor.name = "tf.cloud_link.frame_name"; - tf_cloud_link_frame_name_descriptor.type = rcl_interfaces::msg::ParameterType::PARAMETER_STRING; - tf_cloud_link_frame_name_descriptor.description = - "Name for the point cloud frame, defaults to _optical_link."; - this->declare_parameter("tf.cloud_link.frame_name", node_name + "_cloud_link", tf_cloud_link_frame_name_descriptor); - - rcl_interfaces::msg::ParameterDescriptor tf_cloud_link_publish_transform_descriptor; - tf_cloud_link_publish_transform_descriptor.name = "tf.cloud_link.publish_transform"; - tf_cloud_link_publish_transform_descriptor.type = rcl_interfaces::msg::ParameterType::PARAMETER_BOOL; - tf_cloud_link_publish_transform_descriptor.description = - "Whether the transform from the cameras mounting point to the point cloud center should be published."; - this->declare_parameter("tf.cloud_link.publish_transform", true, tf_cloud_link_publish_transform_descriptor); - - rcl_interfaces::msg::ParameterDescriptor tf_mounting_link_frame_name_descriptor; - tf_mounting_link_frame_name_descriptor.name = "tf.mounting_link.frame_name"; - tf_mounting_link_frame_name_descriptor.type = rcl_interfaces::msg::ParameterType::PARAMETER_STRING; - tf_mounting_link_frame_name_descriptor.description = - "Name for the mounting point frame, defaults to _mounting_link."; - this->declare_parameter("tf.mounting_link.frame_name", node_name + "_mounting_link", - tf_mounting_link_frame_name_descriptor); - - rcl_interfaces::msg::ParameterDescriptor tf_optical_link_frame_name_descriptor; - tf_optical_link_frame_name_descriptor.name = "tf.optical_link.frame_name"; - tf_optical_link_frame_name_descriptor.type = rcl_interfaces::msg::ParameterType::PARAMETER_STRING; - tf_optical_link_frame_name_descriptor.description = - "Name for the point optical frame, defaults to _optical_link."; - this->declare_parameter("tf.optical_link.frame_name", node_name + "_optical_link", - tf_optical_link_frame_name_descriptor); - - rcl_interfaces::msg::ParameterDescriptor tf_optical_link_publish_transform_descriptor; - tf_optical_link_publish_transform_descriptor.name = "tf.optical_link.publish_transform"; - tf_optical_link_publish_transform_descriptor.type = rcl_interfaces::msg::ParameterType::PARAMETER_BOOL; - tf_optical_link_publish_transform_descriptor.description = - "Whether the transform from the cameras mounting point to the point optical center should be published."; - this->declare_parameter("tf.optical_link.publish_transform", true, tf_optical_link_publish_transform_descriptor); - - rcl_interfaces::msg::ParameterDescriptor tf_optical_link_transform_descriptor; - std::vector default_transform; - default_transform.resize(6, 0.0); - tf_optical_link_transform_descriptor.name = "tf.optical_link.transform"; - tf_optical_link_transform_descriptor.type = rcl_interfaces::msg::ParameterType::PARAMETER_DOUBLE_ARRAY; - tf_optical_link_transform_descriptor.description = - "Static transform from mounting link to optical link, as [x, y, z, rot_x, rot_y, rot_z]"; - this->declare_parameter("tf.optical_link.transform", default_transform, tf_optical_link_transform_descriptor); - rcl_interfaces::msg::ParameterDescriptor xmlrpc_port_descriptor; xmlrpc_port_descriptor.name = "xmlrpc_port"; xmlrpc_port_descriptor.type = rcl_interfaces::msg::ParameterType::PARAMETER_INTEGER; @@ -362,18 +376,8 @@ void CameraNode::init_params() xmlrpc_port_descriptor.integer_range.push_back(xmlrpc_port_range); this->declare_parameter("xmlrpc_port", ifm3d::DEFAULT_XMLRPC_PORT, xmlrpc_port_descriptor); - rcl_interfaces::msg::ParameterDescriptor diag_mode_descriptor; - diag_mode_descriptor.name = "diag_mode"; - diag_mode_descriptor.type = rcl_interfaces::msg::ParameterType::PARAMETER_STRING; - diag_mode_descriptor.description = - "Diagnostic mode: asynchronous monitoring or periodic polling."; - this->declare_parameter("diag_mode", "async", diag_mode_descriptor); - // TODO: extend parameter description to include required params: // password: for lock / unlock of JSON configuration - - - } void CameraNode::parse_params() @@ -384,26 +388,8 @@ void CameraNode::parse_params() * - Where applicable, parse read data into more useful data type */ - std::vector buffer_id_strings; - this->get_parameter("buffer_id_list", buffer_id_strings); - RCLCPP_INFO(this->logger_, "Reading %ld buffer_ids: [%s]", buffer_id_strings.size(), - buffer_id_utils::vector_to_string(buffer_id_strings).c_str()); - // Populate buffer_id_list_ from read strings - this->buffer_id_list_.clear(); - for (const std::string& string : buffer_id_strings) - { - ifm3d::buffer_id found_id; - if (buffer_id_utils::convert(string, found_id)) - { - this->buffer_id_list_.push_back(found_id); - } - else - { - RCLCPP_WARN(this->logger_, "Ignoring unknown buffer_id %s", string.c_str()); - } - } - RCLCPP_INFO(this->logger_, "Parsed %ld buffer_ids: %s", this->buffer_id_list_.size(), - buffer_id_utils::vector_to_string(this->buffer_id_list_).c_str()); + this->get_parameter("config_file", this->config_file_); + RCLCPP_INFO(this->logger_, "Config file: %s", this->config_file_.c_str()); this->get_parameter("ip", this->ip_); RCLCPP_INFO(this->logger_, "ip: %s", this->ip_.c_str()); @@ -411,40 +397,8 @@ void CameraNode::parse_params() this->get_parameter("pcic_port", this->pcic_port_); RCLCPP_INFO(this->logger_, "pcic_port: %u", this->pcic_port_); - this->get_parameter("tf.cloud_link.frame_name", this->tf_cloud_link_frame_name_); - RCLCPP_INFO(this->logger_, "tf.cloud_link.frame_name: %s", this->tf_cloud_link_frame_name_.c_str()); - - this->get_parameter("tf.cloud_link.publish_transform", this->tf_cloud_link_publish_transform_); - RCLCPP_INFO(this->logger_, "tf.cloud_link.publish_transform: %s", - this->tf_cloud_link_publish_transform_ ? "true" : "false"); - - this->get_parameter("tf.mounting_link.frame_name", this->tf_mounting_link_frame_name_); - RCLCPP_INFO(this->logger_, "tf.mounting_link.frame_name: %s", this->tf_mounting_link_frame_name_.c_str()); - - this->get_parameter("tf.optical_link.frame_name", this->tf_optical_link_frame_name_); - RCLCPP_INFO(this->logger_, "tf.optical_link.frame_name: %s", this->tf_optical_link_frame_name_.c_str()); - - this->get_parameter("tf.optical_link.publish_transform", this->tf_optical_link_publish_transform_); - RCLCPP_INFO(this->logger_, "tf.optical_link.publish_transform: %s", - this->tf_optical_link_publish_transform_ ? "true" : "false"); - - this->get_parameter("tf.optical_link.transform", this->tf_optical_link_transform_); - if (this->tf_optical_link_transform_.size() != 6) - { - RCLCPP_WARN(logger_, "Invalid number of entries for tf.optical_link.transform: %ld. %s", - this->tf_optical_link_transform_.size(), "Using the first 6 values, filling in with 0.0 if nessesary."); - this->tf_optical_link_transform_.resize(6, 0.0); - } - RCLCPP_INFO(this->logger_, "tf.optical_link.transform: [%f, %f, %f, %f, %f, %f]", this->tf_optical_link_transform_[0], - this->tf_optical_link_transform_[1], this->tf_optical_link_transform_[2], - this->tf_optical_link_transform_[3], this->tf_optical_link_transform_[4], - this->tf_optical_link_transform_[5]); - this->get_parameter("xmlrpc_port", this->xmlrpc_port_); RCLCPP_INFO(this->logger_, "xmlrpc_port: %u", this->xmlrpc_port_); - - this->get_parameter("diag_mode", this->diag_mode_); - RCLCPP_INFO(this->logger_, "diag_mode: %s", this->diag_mode_.c_str()); } void CameraNode::set_parameter_event_callbacks() @@ -457,13 +411,10 @@ void CameraNode::set_parameter_event_callbacks() * - Create a callback as lambda to handle parameter change at runtime * - Add lambda to parameter subscriber */ - - auto buffer_id_list_cb = [this](const rclcpp::Parameter& p) { - RCLCPP_WARN(logger_, "This new buffer_id_list will be used after CONFIGURE transition was called: %s", - buffer_id_utils::vector_to_string(p.as_string_array()).c_str()); + auto config_file_cb = [this](const rclcpp::Parameter& p) { + RCLCPP_WARN(logger_, "This new config_file will be used after CONFIGURE transition was called: '%s'", p.as_string().c_str()); }; - registered_param_callbacks_["buffer_id_list"] = - param_subscriber_->add_parameter_callback("buffer_id_list", buffer_id_list_cb); + registered_param_callbacks_["config_file"] = param_subscriber_->add_parameter_callback("config_file", config_file_cb); auto ip_cb = [this](const rclcpp::Parameter& p) { RCLCPP_WARN(logger_, "This new ip will be used after CONFIGURE transition was called: '%s'", p.as_string().c_str()); @@ -475,566 +426,39 @@ void CameraNode::set_parameter_event_callbacks() }; registered_param_callbacks_["pcic_port"] = param_subscriber_->add_parameter_callback("pcic_port", pcic_port_cb); - auto tf_cloud_link_frame_name_cb = [this](const rclcpp::Parameter& p) { - this->tf_cloud_link_frame_name_ = p.as_string(); - RCLCPP_INFO(logger_, - "This new tf.cloud_link.frame_name will be used as soon as the next Extrinsics buffer is received: " - "'%s'", - this->tf_cloud_link_frame_name_.c_str()); - }; - registered_param_callbacks_["tf.cloud_link.frame_name"] = - param_subscriber_->add_parameter_callback("tf.cloud_link.frame_name", tf_cloud_link_frame_name_cb); - - auto tf_cloud_link_publish_transform_cb = [this](const rclcpp::Parameter& p) { - this->tf_cloud_link_publish_transform_ = p.as_bool(); - RCLCPP_INFO(logger_, "New tf.cloud_link.publish_transform: %s", - this->tf_cloud_link_publish_transform_ ? "true" : "false"); - }; - registered_param_callbacks_["tf.cloud_link.publish_transform"] = - param_subscriber_->add_parameter_callback("tf.cloud_link.publish_transform", tf_cloud_link_publish_transform_cb); - - auto tf_mounting_link_frame_name_cb = [this](const rclcpp::Parameter& p) { - this->tf_mounting_link_frame_name_ = p.as_string(); - publish_optical_link_transform(); - RCLCPP_INFO(logger_, "New tf.mounting_link.frame_name: '%s'", this->tf_mounting_link_frame_name_.c_str()); - }; - registered_param_callbacks_["tf.mounting_link.frame_name"] = - param_subscriber_->add_parameter_callback("tf.mounting_link.frame_name", tf_mounting_link_frame_name_cb); - - auto tf_optical_link_frame_name_cb = [this](const rclcpp::Parameter& p) { - this->tf_optical_link_frame_name_ = p.as_string(); - publish_optical_link_transform(); - RCLCPP_INFO(logger_, "New tf.optical_link.frame_name: '%s'", this->tf_optical_link_frame_name_.c_str()); - }; - registered_param_callbacks_["tf.optical_link.frame_name"] = - param_subscriber_->add_parameter_callback("tf.optical_link.frame_name", tf_optical_link_frame_name_cb); - - auto tf_optical_link_publish_transform_cb = [this](const rclcpp::Parameter& p) { - this->tf_optical_link_publish_transform_ = p.as_bool(); - publish_optical_link_transform(); - RCLCPP_INFO(logger_, "New tf.optical_link.publish_transform: %s", - this->tf_optical_link_publish_transform_ ? "true" : "false"); - }; - registered_param_callbacks_["tf.optical_link.publish_transform"] = param_subscriber_->add_parameter_callback( - "tf.optical_link.publish_transform", tf_optical_link_publish_transform_cb); - - auto tf_optical_link_transform_cb = [this](const rclcpp::Parameter& p) { - std::vector doubles_from_param = p.as_double_array(); - if (doubles_from_param.size() != 6) - { - RCLCPP_WARN(logger_, "Invalid number of entries for tf.optical_link.transform: %ld. %s", - doubles_from_param.size(), "Using the first 6 values, filling in with 0.0 if nessesary."); - doubles_from_param.resize(6, 0.0); - } - this->tf_optical_link_transform_ = doubles_from_param; - RCLCPP_INFO(this->logger_, "New tf.optical_link.transform: [%f, %f, %f, %f, %f, %f]", - this->tf_optical_link_transform_[0], this->tf_optical_link_transform_[1], - this->tf_optical_link_transform_[2], this->tf_optical_link_transform_[3], - this->tf_optical_link_transform_[4], this->tf_optical_link_transform_[5]); - publish_optical_link_transform(); - }; - registered_param_callbacks_["tf.optical_link.transform"] = - param_subscriber_->add_parameter_callback("tf.optical_link.transform", tf_optical_link_transform_cb); - auto xmlrpc_port_cb = [this](const rclcpp::Parameter& p) { this->xmlrpc_port_ = p.as_int(); RCLCPP_INFO(logger_, "New xmlrpc_port: %d", this->xmlrpc_port_); }; registered_param_callbacks_["xmlrpc_port"] = param_subscriber_->add_parameter_callback("xmlrpc_port", xmlrpc_port_cb); - - auto diag_mode_cb = [this](const rclcpp::Parameter& p) { - this->diag_mode_ = p.as_string(); - RCLCPP_INFO(logger_, "New diag_mode: %s", this->diag_mode_.c_str()); - }; - registered_param_callbacks_["diag_mode"] = param_subscriber_->add_parameter_callback("diag_mode", diag_mode_cb); -} - -void CameraNode::initialize_publishers() -{ - using namespace buffer_id_utils; - - image_publishers_.clear(); - compressed_image_publishers_.clear(); - pcl_publishers_.clear(); - extrinsics_publishers_.clear(); - - // Create static tf publisher - tf_static_broadcaster_ = std::make_shared(this); - - // Create diagnostics publisher - // Messages shall be published to global /diagnostics topic and not local one, similar to /tf - diagnostic_publisher_ = this->create_publisher("/diagnostics", 10); - - std::vector ids_to_remove{}; - - // Create correctly typed publishers for all given buffer_ids - for (const ifm3d::buffer_id& id : this->buffer_id_list_) - { - // Create Publishers in node namespace to make multi-camera setups easier - const std::string topic_name = "~/" + buffer_id_utils::topic_name_map[id]; - const auto qos = ifm3d_ros2::LowLatencyQoS(); - const buffer_id_utils::message_type message_type = buffer_id_utils::message_type_map[id]; - - switch (message_type) - { - case buffer_id_utils::message_type::raw_image: - image_publishers_[id] = this->create_publisher(topic_name, qos); - break; - case buffer_id_utils::message_type::compressed_image: - compressed_image_publishers_[id] = this->create_publisher(topic_name, qos); - break; - case buffer_id_utils::message_type::pointcloud: - pcl_publishers_[id] = this->create_publisher(topic_name, qos); - break; - case buffer_id_utils::message_type::extrinsics: - extrinsics_publishers_[id] = this->create_publisher(topic_name, qos); - break; - case buffer_id_utils::message_type::intrinsics: - intrinsics_publishers_[id] = this->create_publisher(topic_name, qos); - camera_info_publishers_[id] = this->create_publisher("~/camera_info", qos); - break; - case buffer_id_utils::message_type::rgb_info: - rgb_info_publishers_[id] = this->create_publisher(topic_name, qos); - break; - case buffer_id_utils::message_type::tof_info: - tof_info_publishers_[id] = this->create_publisher(topic_name, qos); - break; - case buffer_id_utils::message_type::inverse_intrinsics: - inverse_intrinsics_publishers_[id] = this->create_publisher(topic_name, qos); - break; - default: - std::string id_string; - convert(id, id_string); - RCLCPP_ERROR(logger_, "Unknown message type for buffer_id %s. Will be removed from list...", id_string.c_str()); - ids_to_remove.push_back(id); - break; - } - } - - // Remove all buffer_ids where type is unclear - while (ids_to_remove.size() > 0) - { - ifm3d::buffer_id id_to_remove = ids_to_remove.back(); - ids_to_remove.pop_back(); - std::vector::iterator itr = - std::find(buffer_id_list_.begin(), buffer_id_list_.end(), id_to_remove); - auto index = std::distance(buffer_id_list_.begin(), itr); - buffer_id_list_.erase(buffer_id_list_.begin() + index); - - std::string id_string; - convert(id_to_remove, id_string); - RCLCPP_INFO(logger_, "Removed buffer_id %s from list at position %ld", id_string.c_str(), index); - } -} - -void CameraNode::activate_publishers() -{ - for (auto& [id, publisher] : image_publishers_) - { - publisher->on_activate(); - } - for (auto& [id, publisher] : compressed_image_publishers_) - { - publisher->on_activate(); - } - for (auto& [id, publisher] : pcl_publishers_) - { - publisher->on_activate(); - } - for (auto& [id, publisher] : extrinsics_publishers_) - { - publisher->on_activate(); - } - for (auto& [id, publisher] : intrinsics_publishers_) - { - publisher->on_activate(); - } - for (auto& [id, publisher] : camera_info_publishers_) - { - publisher->on_activate(); - } - for (auto& [id, publisher] : tof_info_publishers_) - { - publisher->on_activate(); - } - for (auto& [id, publisher] : rgb_info_publishers_) - { - publisher->on_activate(); - } - for (auto& [id, publisher] : inverse_intrinsics_publishers_) - { - publisher->on_activate(); - } - diagnostic_publisher_->on_activate(); -}; - -void CameraNode::deactivate_publishers() -{ - for (auto& [id, publisher] : image_publishers_) - { - publisher->on_deactivate(); - } - for (auto& [id, publisher] : compressed_image_publishers_) - { - publisher->on_deactivate(); - } - for (auto& [id, publisher] : pcl_publishers_) - { - publisher->on_deactivate(); - } - for (auto& [id, publisher] : extrinsics_publishers_) - { - publisher->on_deactivate(); - } - for (auto& [id, publisher] : intrinsics_publishers_) - { - publisher->on_deactivate(); - } - for (auto& [id, publisher] : camera_info_publishers_) - { - publisher->on_deactivate(); - } - for (auto& [id, publisher] : tof_info_publishers_) - { - publisher->on_deactivate(); - } - for (auto& [id, publisher] : rgb_info_publishers_) - { - publisher->on_deactivate(); - } - for (auto& [id, publisher] : inverse_intrinsics_publishers_) - { - publisher->on_deactivate(); - } - diagnostic_publisher_->on_deactivate(); -}; - -void CameraNode::Config(const std::shared_ptr /*unused*/, ConfigRequest req, ConfigResponse resp) -{ - RCLCPP_INFO(this->logger_, "Handling config request..."); - - if (this->get_current_state().id() != lifecycle_msgs::msg::State::PRIMARY_STATE_ACTIVE) - { - resp->status = -1; - // XXX: may want to change this logic. For now, I do it so I know - // the ifm3d data structures are not null pointers - RCLCPP_WARN(this->logger_, "Can only make a service request when node is ACTIVE"); - return; - } - - { - std::lock_guard lock(this->gil_); - resp->status = 0; - resp->msg = "OK"; - - try - { - this->cam_->FromJSON(json::parse(req->json)); // HERE - } - catch (const ifm3d::Error& ex) - { - resp->status = ex.code(); - resp->msg = ex.what(); - } - catch (const std::exception& std_ex) - { - resp->status = -1; - resp->msg = std_ex.what(); - } - catch (...) - { - resp->status = -2; - resp->msg = "Unknown error in `Config'"; - } - - if (resp->status != 0) - { - RCLCPP_WARN(this->logger_, "Config: %d - %s", resp->status, resp->msg.c_str()); - } - } - - RCLCPP_INFO(this->logger_, "Config request done."); -} - -void CameraNode::Dump(const std::shared_ptr /*unused*/, DumpRequest /*unused*/, DumpResponse resp) -{ - RCLCPP_INFO(this->logger_, "Handling dump request..."); - - if (this->get_current_state().id() != lifecycle_msgs::msg::State::PRIMARY_STATE_ACTIVE) - { - resp->status = -1; - // XXX: may want to change this logic. For now, I do it so I know - // the ifm3d data structures are not null pointers - RCLCPP_WARN(this->logger_, "Can only make a service request when node is ACTIVE"); - return; - } - - { - std::lock_guard lock(this->gil_); - resp->status = 0; - - try - { - json j = this->cam_->ToJSON(); // HERE - resp->config = j.dump(); - } - catch (const ifm3d::Error& ex) - { - resp->status = ex.code(); - RCLCPP_WARN(this->logger_, "%s", ex.what()); - } - catch (const std::exception& std_ex) - { - resp->status = -1; - RCLCPP_WARN(this->logger_, "%s", std_ex.what()); - } - catch (...) - { - resp->status = -2; - } - - if (resp->status != 0) - { - RCLCPP_WARN(this->logger_, "Dump: %d", resp->status); - } - } - - RCLCPP_INFO(this->logger_, "Dump request done."); } -void CameraNode::Softoff(const std::shared_ptr /*unused*/, SoftoffRequest /*unused*/, - SoftoffResponse resp) +void CameraNode::frame_callback(ifm3d::Frame::Ptr frame) { - RCLCPP_INFO(this->logger_, "Handling SoftOff request..."); - - if (this->get_current_state().id() != lifecycle_msgs::msg::State::PRIMARY_STATE_ACTIVE) - { - resp->status = -1; - RCLCPP_WARN(this->logger_, "Can only make a service request when node is ACTIVE"); - return; - } - + if (std::holds_alternative>(this->data_module_)) { - std::lock_guard lock(this->gil_); - resp->status = 0; - int port_arg = -1; - - try - { - port_arg = static_cast(this->pcic_port_) % xmlrpc_base_port; - this->cam_->FromJSONStr(R"({"ports":{"port)" + std::to_string(port_arg) + R"(": {"state": "IDLE"}}})"); - } - catch (const ifm3d::Error& ex) - { - resp->status = ex.code(); - RCLCPP_WARN(this->logger_, "%s", ex.what()); - } - catch (const std::exception& std_ex) - { - resp->status = -1; - RCLCPP_WARN(this->logger_, "%s", std_ex.what()); - } - catch (...) - { - resp->status = -2; - } - - if (resp->status != 0) - { - RCLCPP_WARN(this->logger_, "SoftOff: %d", resp->status); - } + std::get>(this->data_module_)->handle_frame(frame); } - - RCLCPP_INFO(this->logger_, "SoftOff request done."); -} - -void CameraNode::Softon(const std::shared_ptr /*unused*/, SoftonRequest /*unused*/, - SoftonResponse resp) -{ - RCLCPP_INFO(this->logger_, "Handling SoftOn request..."); - - if (this->get_current_state().id() != lifecycle_msgs::msg::State::PRIMARY_STATE_ACTIVE) + else if (std::holds_alternative>(this->data_module_)) { - resp->status = -1; - RCLCPP_WARN(this->logger_, "Can only make a service request when node is ACTIVE"); - return; + std::get>(this->data_module_)->handle_frame(frame); } - - { - std::lock_guard lock(this->gil_); - resp->status = 0; - int port_arg = -1; - - try - { - port_arg = static_cast(this->pcic_port_) % xmlrpc_base_port; - this->cam_->FromJSONStr(R"({"ports":{"port)" + std::to_string(port_arg) + R"(":{"state":"RUN"}}})"); - } - catch (const ifm3d::Error& ex) - { - resp->status = ex.code(); - RCLCPP_WARN(this->logger_, "%s", ex.what()); - } - catch (const std::exception& std_ex) - { - resp->status = -1; - RCLCPP_WARN(this->logger_, "%s", std_ex.what()); - } - catch (...) - { - resp->status = -2; - } - - if (resp->status != 0) - { - RCLCPP_WARN(this->logger_, "SoftOn: %d", resp->status); - } - } - - RCLCPP_INFO(this->logger_, "SoftOn request done."); -} - -void CameraNode::frame_callback(ifm3d::Frame::Ptr frame) -{ - using namespace buffer_id_utils; - - RCLCPP_INFO_ONCE(logger_, "Receiving Frames. Processing buffer for [%s]...", - buffer_id_utils::vector_to_string(this->buffer_id_list_).c_str()); - RCLCPP_DEBUG(logger_, "Received new Frame."); - - const auto now = this->get_clock()->now(); - - auto cloud_header = std_msgs::msg::Header(); - cloud_header.frame_id = this->tf_cloud_link_frame_name_; - cloud_header.stamp = now; - - auto optical_header = std_msgs::msg::Header(); - optical_header.frame_id = this->tf_optical_link_frame_name_; - optical_header.stamp = now; - - std::lock_guard lock(this->gil_); - - for (const ifm3d::buffer_id& id : this->buffer_id_list_) - { - // Helper for logging - auto& clk = *this->get_clock(); - std::string id_string; - convert(id, id_string); - - const buffer_id_utils::message_type message_type = buffer_id_utils::message_type_map[id]; - - if (!frame->HasBuffer(id)) - { - RCLCPP_WARN_THROTTLE(logger_, clk, 5000, - "Frame does not contain buffer %s. Is the correct camera head connected?", - id_string.c_str()); - } - - switch (message_type) - { - case buffer_id_utils::message_type::raw_image: { - auto buffer = frame->GetBuffer(id); - ImageMsg raw_image_msg = ifm3d_ros2::ifm3d_to_ros_image(frame->GetBuffer(id), optical_header, logger_); - image_publishers_[id]->publish(raw_image_msg); - width_ = raw_image_msg.width; - height_ = raw_image_msg.height; - } - break; - case buffer_id_utils::message_type::compressed_image: { - auto buffer = frame->GetBuffer(id); - CompressedImageMsg compressed_image_msg = - ifm3d_ros2::ifm3d_to_ros_compressed_image(frame->GetBuffer(id), optical_header, "jpeg", logger_); - compressed_image_publishers_[id]->publish(compressed_image_msg); - } - break; - case buffer_id_utils::message_type::pointcloud: { - auto buffer = frame->GetBuffer(id); - PCLMsg pointcloud_msg = ifm3d_ros2::ifm3d_to_ros_cloud(frame->GetBuffer(id), cloud_header, logger_); - pcl_publishers_[id]->publish(pointcloud_msg); - } - break; - case buffer_id_utils::message_type::extrinsics: { - auto buffer = frame->GetBuffer(id); - ExtrinsicsMsg extrinsics_msg = ifm3d_ros2::ifm3d_to_extrinsics(buffer, optical_header, logger_); - extrinsics_publishers_[id]->publish(extrinsics_msg); - - // Set/Update mounting link to cloud link transform - publish_cloud_link_transform_if_changed(extrinsics_msg); - } - break; - case buffer_id_utils::intrinsics: { - auto buffer = frame->GetBuffer(id); - IntrinsicsMsg intrinsics_msg = ifm3d_ros2::ifm3d_to_intrinsics(buffer, optical_header, logger_); - intrinsics_publishers_[id]->publish(intrinsics_msg); - - // Also publish CameraInfo from Intrinsics - if (width_ == 0 || height_ == 0) - { - RCLCPP_WARN_THROTTLE(logger_, clk, 5000, "Needs at least one raw image buffer to parse CameraInfo!"); - } - else - { - CameraInfoMsg camera_info_msg = - ifm3d_ros2::ifm3d_to_camera_info(buffer, optical_header, height_, width_, logger_); - RCLCPP_INFO_ONCE(logger_, "Parsing CameraInfo successfull."); - camera_info_publishers_[id]->publish(camera_info_msg); - } - } - break; - case buffer_id_utils::message_type::inverse_intrinsics: { - auto buffer = frame->GetBuffer(id); - InverseIntrinsicsMsg inverse_intrinsics_msg = - ifm3d_ros2::ifm3d_to_inverse_intrinsics(buffer, optical_header, logger_); - inverse_intrinsics_publishers_[id]->publish(inverse_intrinsics_msg); - } - break; - case buffer_id_utils::message_type::tof_info: { - auto buffer = frame->GetBuffer(id); - TOFInfoMsg tof_info_msg = ifm3d_ros2::ifm3d_to_tof_info(buffer, optical_header, logger_); - tof_info_publishers_[id]->publish(tof_info_msg); - } - break; - case buffer_id_utils::message_type::rgb_info: { - auto buffer = frame->GetBuffer(id); - RGBInfoMsg rgb_info_msg = ifm3d_ros2::ifm3d_to_rgb_info(buffer, optical_header, logger_); - rgb_info_publishers_[id]->publish(rgb_info_msg); - } - break; - default: - RCLCPP_ERROR_THROTTLE(logger_, clk, 5000, "Unknown message type for buffer_id %s. Can not publish.", - id_string.c_str()); - break; - } - } - RCLCPP_DEBUG(this->logger_, "Frame callback done."); } -buffer_id_utils::data_stream_type CameraNode::stream_type_from_port_info(const std::vector& ports, - const uint16_t pcic_port) +buffer_id_utils::data_stream_type CameraNode::stream_type_from_port_info(ifm3d::PortInfo port_info) { - std::string port_type{ "" }; buffer_id_utils::data_stream_type data_stream_type; - - // Get port_type from PortInfo with matching pcic_port - for (auto port : ports) - { - RCLCPP_INFO(logger_, "Found port %s (pcic_port=%d) with type %s", port.port.c_str(), port.pcic_port, - port.type.c_str()); - if (port.pcic_port == pcic_port) - { - port_type = port.type; - break; - } - } + RCLCPP_INFO(logger_, "Using port %s (pcic_port=%d) with type %s", port_info.port.c_str(), port_info.pcic_port, + port_info.type.c_str()); // Derive data_stream_type from PortInfo - if (port_type == "3D") + if (port_info.type == "3D") { data_stream_type = buffer_id_utils::data_stream_type::tof_3d; RCLCPP_INFO(logger_, "Data stream type is tof_3d."); } - else if (port_type == "2D") + else if (port_info.type == "2D") { data_stream_type = buffer_id_utils::data_stream_type::rgb_2d; RCLCPP_INFO(logger_, "Data stream type is rgb_2d."); @@ -1042,75 +466,12 @@ buffer_id_utils::data_stream_type CameraNode::stream_type_from_port_info(const s else { data_stream_type = buffer_id_utils::data_stream_type::tof_3d; - RCLCPP_ERROR(logger_, "Unknown data stream type '%s'. Defaulting to tof_3d.", port_type.c_str()); + RCLCPP_ERROR(logger_, "Unknown data stream type '%s'. Defaulting to tof_3d.", port_info.type.c_str()); } return data_stream_type; } -void CameraNode::publish_optical_link_transform() -{ - if (!tf_optical_link_publish_transform_) - { - // TF publication deactivated via parameter - return; - } - - tf2::Quaternion q; - q.setRPY(tf_optical_link_transform_[3], tf_optical_link_transform_[4], tf_optical_link_transform_[5]); - - geometry_msgs::msg::TransformStamped t; - t.header.stamp = this->get_clock()->now(); - t.header.frame_id = tf_mounting_link_frame_name_; - t.child_frame_id = tf_optical_link_frame_name_; - t.transform.translation.x = tf_optical_link_transform_[0]; - t.transform.translation.y = tf_optical_link_transform_[1]; - t.transform.translation.z = tf_optical_link_transform_[2]; - t.transform.rotation.x = q.x(); - t.transform.rotation.y = q.y(); - t.transform.rotation.z = q.z(); - t.transform.rotation.w = q.w(); - - tf_static_broadcaster_->sendTransform(t); -} - -void CameraNode::publish_cloud_link_transform_if_changed(const ExtrinsicsMsg& msg) -{ - if (!tf_cloud_link_publish_transform_) - { - // TF publication deactivated via parameter - return; - } - - tf2::Quaternion q; - q.setRPY(msg.rot_x, msg.rot_y, msg.rot_z); - - if ((cloud_link_transform_.header.frame_id == tf_optical_link_frame_name_) && - (cloud_link_transform_.child_frame_id == tf_cloud_link_frame_name_) && - (cloud_link_transform_.transform.translation.x == msg.tx) && - (cloud_link_transform_.transform.translation.y == msg.ty) && - (cloud_link_transform_.transform.translation.z == msg.tz) && - (cloud_link_transform_.transform.rotation.x == q.x()) && (cloud_link_transform_.transform.rotation.y == q.y()) && - (cloud_link_transform_.transform.rotation.z == q.z()) && (cloud_link_transform_.transform.rotation.w == q.w())) - { - // no change - return; - } - - cloud_link_transform_.header.stamp = this->get_clock()->now(); - cloud_link_transform_.header.frame_id = tf_optical_link_frame_name_; - cloud_link_transform_.child_frame_id = tf_cloud_link_frame_name_; - cloud_link_transform_.transform.translation.x = msg.tx; - cloud_link_transform_.transform.translation.y = msg.ty; - cloud_link_transform_.transform.translation.z = msg.tz; - cloud_link_transform_.transform.rotation.x = q.x(); - cloud_link_transform_.transform.rotation.y = q.y(); - cloud_link_transform_.transform.rotation.z = q.z(); - cloud_link_transform_.transform.rotation.w = q.w(); - - tf_static_broadcaster_->sendTransform(cloud_link_transform_); -} - void CameraNode::error_callback(const ifm3d::Error& error) { RCLCPP_ERROR(logger_, "Error received from ifm3d: %s", error.what()); @@ -1119,76 +480,15 @@ void CameraNode::error_callback(const ifm3d::Error& error) void CameraNode::async_error_callback(int i, const std::string& s) { - if (this->diag_mode_=="async"){ - RCLCPP_ERROR(logger_, "AsyncError received from ifm3d: %d %s", i, s.c_str()); - DiagnosticArrayMsg msg; - msg.header.stamp = get_clock()->now(); - msg.status.push_back(create_diagnostic_status(diagnostic_msgs::msg::DiagnosticStatus::ERROR, s)); - diagnostic_publisher_->publish(msg); - } + this->diag_module_->handle_error(i, s); + RCLCPP_DEBUG(this->logger_, "Async error callback done."); } void CameraNode::async_notification_callback(const std::string& s1, const std::string& s2) { - RCLCPP_INFO(logger_, "AsyncNotification received from ifm3d: %s | %s", s1.c_str(), s2.c_str()); - DiagnosticArrayMsg msg; - msg.header.stamp = get_clock()->now(); - msg.status.push_back(create_diagnostic_status(diagnostic_msgs::msg::DiagnosticStatus::OK, s2)); - diagnostic_publisher_->publish(msg); + this->diag_module_->handle_notification(s1, s2); + RCLCPP_DEBUG(this->logger_, "Async notification callback done."); } - -DiagnosticStatusMsg CameraNode::create_diagnostic_status(const uint8_t level, const std::string& json_msg) -{ - DiagnosticStatusMsg msg; - msg.level = level; - msg.hardware_id = hardware_id_; - - ifm3d::json parsed_json; - - try - { - parsed_json = json::parse(json_msg); - } - catch (...) - { - RCLCPP_ERROR(logger_, "Invalid JSON received from callback with level %d", level); - return msg; - } - - if (parsed_json.empty()) - { - RCLCPP_WARN(logger_, "Empty JSON received from callback with level %d", level); - return msg; - } - - if (parsed_json.contains("name")) - { - msg.name = parsed_json["name"]; - } - - if (parsed_json.contains("description")) - { - msg.message = parsed_json["description"]; - } - - for (auto& it : parsed_json.items()) - { - try - { - diagnostic_msgs::msg::KeyValue obj; - obj.key = it.key(); - obj.value = it.value().dump(); - msg.values.push_back(obj); - } - catch (const std::exception& e) - { - RCLCPP_WARN(logger_, "Couldn't parse entry of diagnostics status %s: %s", msg.name, e.what()); - } - } - - return msg; -} - } // namespace ifm3d_ros2 #include diff --git a/src/lib/camera_tf_publisher.cpp b/src/lib/camera_tf_publisher.cpp new file mode 100644 index 0000000..c5d7bb9 --- /dev/null +++ b/src/lib/camera_tf_publisher.cpp @@ -0,0 +1,223 @@ +#include + +#include + +namespace ifm3d_ros2 +{ + +CameraTfPublisher::CameraTfPublisher(rclcpp_lifecycle::LifecycleNode::SharedPtr node_ptr, ifm3d::O3R::Ptr o3r_ptr, + std::string port, std::string base_frame_name, std::string mounting_frame_name, + std::string optical_frame_name) + : tf_base_link_frame_name_(base_frame_name) + , tf_mounting_link_frame_name_(mounting_frame_name) + , tf_optical_link_frame_name_(optical_frame_name) + , tf_publish_mounting_to_optical_(true) + , tf_publish_base_to_mounting_(true) + , node_ptr_(node_ptr) + , o3r_ptr_(o3r_ptr) + , port_(port) +{ + tf_static_broadcaster_ = std::make_shared(this->node_ptr_); +} + +CameraTfPublisher::CameraTfPublisher(rclcpp_lifecycle::LifecycleNode::SharedPtr node_ptr, ifm3d::O3R::Ptr o3r_ptr, + std::string port) + : CameraTfPublisher(node_ptr, o3r_ptr, port, "", "", "") +{ +} + +bool CameraTfPublisher::update_and_publish_tf_if_changed( + const geometry_msgs::msg::TransformStamped& new_tf_base_to_optical) +{ + bool published_tf = false; + + if (transform_identical(new_tf_base_to_optical, tf_base_to_optical_)) + { + // No change in the TFs + return published_tf; + } + + // Update the TF2 on first run (tf_base_to_optical_ is not initialized) and on changes + tf_base_to_optical_ = new_tf_base_to_optical; + + const geometry_msgs::msg::TransformStamped new_tf_base_to_mounting = + read_tf_base_to_mounting_from_device_config(new_tf_base_to_optical.header.stamp); + + const geometry_msgs::msg::TransformStamped new_tf_mounting_to_optical = + get_tf_mounting_to_optical(new_tf_base_to_optical.header.stamp, new_tf_base_to_optical, new_tf_base_to_mounting); + + if (!transform_identical(new_tf_base_to_mounting, tf_base_to_mounting_)) + { + tf_base_to_mounting_ = new_tf_base_to_mounting; + + if (tf_publish_base_to_mounting_) + { + tf_static_broadcaster_->sendTransform(tf_base_to_mounting_); + published_tf = true; + RCLCPP_DEBUG(node_ptr_->get_logger(), "New Transform published:\n%s", + tf_to_string(new_tf_base_to_mounting).c_str()); + } + } + + if (!transform_identical(new_tf_mounting_to_optical, tf_mounting_to_optical_)) + { + tf_mounting_to_optical_ = new_tf_mounting_to_optical; + + if (tf_publish_mounting_to_optical_) + { + tf_static_broadcaster_->sendTransform(tf_mounting_to_optical_); + published_tf = true; + RCLCPP_DEBUG(node_ptr_->get_logger(), "New Transform published:\n%s", + tf_to_string(tf_mounting_to_optical_).c_str()); + } + } + + return published_tf; +} + +geometry_msgs::msg::TransformStamped +CameraTfPublisher::read_tf_base_to_mounting_from_device_config(builtin_interfaces::msg::Time stamp) +{ + // TODO use try-catch in case of json errors + + std::string json_string = "/ports/" + port_ + "/processing/extrinsicHeadToUser"; + ifm3d::json::json_pointer j_pointer(json_string); + auto config = o3r_ptr_->Get({ json_string })[j_pointer]; + + // Euler angles: + // ifm3d provides roll, pitch, yaw angles for intrisic rotations + // while tf2 expects them for extrinsic rotation. + // Therefore, a tf2 quaternion needs to be created from 3 separate rotations + tf2::Quaternion q_roll, q_pitch, q_yaw, q_combined; + q_roll.setRPY(config["rotX"], 0.0, 0.0); + q_pitch.setRPY(0.0, config["rotY"], 0.0); + q_yaw.setRPY(0.0, 0.0, config["rotZ"]); + q_combined = q_roll * q_pitch * q_yaw; + + geometry_msgs::msg::TransformStamped tf; + tf.header.stamp = stamp; + tf.header.frame_id = tf_base_link_frame_name_; + tf.child_frame_id = tf_mounting_link_frame_name_; + + tf.transform.translation.x = config["transX"]; + tf.transform.translation.y = config["transY"]; + tf.transform.translation.z = config["transZ"]; + tf.transform.rotation.x = q_combined.x(); + tf.transform.rotation.y = q_combined.y(); + tf.transform.rotation.z = q_combined.z(); + tf.transform.rotation.w = q_combined.w(); + + return tf; +} + +geometry_msgs::msg::TransformStamped CameraTfPublisher::get_tf_mounting_to_optical( + builtin_interfaces::msg::Time stamp, geometry_msgs::msg::TransformStamped tf_base_to_optical, + geometry_msgs::msg::TransformStamped tf_base_to_mounting) +{ + const tf2::Vector3 vector_mounting_to_optical_in_base( + tf_base_to_optical.transform.translation.x - tf_base_to_mounting.transform.translation.x, + tf_base_to_optical.transform.translation.y - tf_base_to_mounting.transform.translation.y, + tf_base_to_optical.transform.translation.z - tf_base_to_mounting.transform.translation.z); + + const tf2::Quaternion quad_base_to_mounting( + tf_base_to_mounting.transform.rotation.x, tf_base_to_mounting.transform.rotation.y, + tf_base_to_mounting.transform.rotation.z, tf_base_to_mounting.transform.rotation.w); + + const tf2::Quaternion quad_base_to_optical( + tf_base_to_optical.transform.rotation.x, tf_base_to_optical.transform.rotation.y, + tf_base_to_optical.transform.rotation.z, tf_base_to_optical.transform.rotation.w); + + // TODO check rotation here! + tf2::Vector3 vector_mounting_to_optical_in_mounting = + tf2::quatRotate(quad_base_to_mounting.inverse(), vector_mounting_to_optical_in_base); + + geometry_msgs::msg::TransformStamped tf; + tf.header.stamp = stamp; + tf.header.frame_id = tf_mounting_link_frame_name_; + tf.child_frame_id = tf_optical_link_frame_name_; + tf.transform.translation.x = vector_mounting_to_optical_in_mounting.x(); + tf.transform.translation.y = vector_mounting_to_optical_in_mounting.y(); + tf.transform.translation.z = vector_mounting_to_optical_in_mounting.z(); + + const tf2::Quaternion quad_mounting_to_optical(quad_base_to_optical * quad_base_to_mounting.inverse()); + tf.transform.rotation.x = quad_mounting_to_optical.x(); + tf.transform.rotation.y = quad_mounting_to_optical.y(); + tf.transform.rotation.z = quad_mounting_to_optical.z(); + tf.transform.rotation.w = quad_mounting_to_optical.w(); + + return tf; +} + +geometry_msgs::msg::TransformStamped CameraTfPublisher ::calculate_tf_base_to_optical( + builtin_interfaces::msg::Time stamp, geometry_msgs::msg::TransformStamped tf_base_to_mounting, + geometry_msgs::msg::TransformStamped tf_mounting_to_optical) +{ + geometry_msgs::msg::TransformStamped tf; + tf.header.stamp = stamp; + tf.header.frame_id = tf_base_link_frame_name_; + tf.child_frame_id = tf_optical_link_frame_name_; + + const tf2::Vector3 vector_base_to_mounting_in_base(tf_base_to_mounting.transform.translation.x, + tf_base_to_mounting.transform.translation.y, + tf_base_to_mounting.transform.translation.z); + const tf2::Vector3 vector_mounting_to_optical_in_mounting(tf_mounting_to_optical.transform.translation.x, + tf_mounting_to_optical.transform.translation.y, + tf_mounting_to_optical.transform.translation.z); + + const tf2::Quaternion quad_base_to_mounting( + tf_base_to_mounting.transform.rotation.x, tf_base_to_mounting.transform.rotation.y, + tf_base_to_mounting.transform.rotation.z, tf_base_to_mounting.transform.rotation.w); + + const tf2::Quaternion quad_mounting_to_optical( + tf_mounting_to_optical.transform.rotation.x, tf_mounting_to_optical.transform.rotation.y, + tf_mounting_to_optical.transform.rotation.z, tf_mounting_to_optical.transform.rotation.w); + + const tf2::Vector3 vector_mounting_to_optical_in_base = + tf2::quatRotate(quad_base_to_mounting.inverse(), vector_mounting_to_optical_in_mounting); + + const tf2::Vector3 vector_base_to_optical_in_base = + vector_base_to_mounting_in_base + vector_mounting_to_optical_in_base; + + const tf2::Quaternion quad_base_to_optical = quad_base_to_mounting * quad_mounting_to_optical; + + tf.transform.translation.x = vector_base_to_optical_in_base.x(); + tf.transform.translation.y = vector_base_to_optical_in_base.y(); + tf.transform.translation.z = vector_base_to_optical_in_base.z(); + tf.transform.rotation.x = quad_base_to_optical.x(); + tf.transform.rotation.y = quad_base_to_optical.y(); + tf.transform.rotation.z = quad_base_to_optical.z(); + tf.transform.rotation.w = quad_base_to_optical.w(); + + return tf; +} + +bool CameraTfPublisher::transform_identical(geometry_msgs::msg::TransformStamped tf1, + geometry_msgs::msg::TransformStamped tf2) +{ + return (tf1.child_frame_id == tf2.child_frame_id) && (tf1.header.frame_id == tf2.header.frame_id) && + (tf1.transform == tf2.transform); +} + +std::string CameraTfPublisher::tf_to_string(geometry_msgs::msg::TransformStamped tf) +{ + std::stringstream ss(""); + + ss << "frame_id=" << tf.header.frame_id << "\n"; + ss << "child_id=" << tf.child_frame_id << "\n"; + ss << "trans_x= " << tf.transform.translation.x << "\n"; + ss << "trans_y= " << tf.transform.translation.y << "\n"; + ss << "trans_z= " << tf.transform.translation.z << "\n"; + ss << "rot_x= " << tf.transform.rotation.x << "\n"; + ss << "rot_y= " << tf.transform.rotation.y << "\n"; + ss << "rot_z= " << tf.transform.rotation.z << "\n"; + ss << "rot_w= " << tf.transform.rotation.w << "\n"; + + tf2::Quaternion quad(tf.transform.rotation.x, tf.transform.rotation.y, tf.transform.rotation.z, + tf.transform.rotation.w); + + ss << "angle= " << quad.getAngle() << "\n"; + + return ss.str(); +} + +} // namespace ifm3d_ros2 \ No newline at end of file diff --git a/src/lib/diag_module.cpp b/src/lib/diag_module.cpp new file mode 100644 index 0000000..07a6634 --- /dev/null +++ b/src/lib/diag_module.cpp @@ -0,0 +1,207 @@ +// -*- c++ -*- +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright (C) 2024 ifm electronic, gmbh + */ + +#include +#include +#include +#include + +#include +#include + +using json = ifm3d::json; +using namespace std::chrono_literals; +namespace ifm3d_ros2 +{ +DiagModule::DiagModule(rclcpp::Logger logger, rclcpp_lifecycle::LifecycleNode::SharedPtr node_ptr, + std::shared_ptr o3r) + : FunctionModule(logger), o3r_(o3r), node_ptr_(node_ptr) +{ + RCLCPP_DEBUG(logger_, "DiagModule contructor called."); + hardware_id_ = std::string(node_ptr_->get_namespace()) + "/" + std::string(get_name()); + RCLCPP_INFO(logger_, "hardware_id: %s", hardware_id_.c_str()); +} + +DiagModule::DiagnosticStatusMsg DiagModule::create_diagnostic_status(const uint8_t level, ifm3d::json parsed_json) +{ + DiagnosticStatusMsg msg; + msg.level = level; + + if (parsed_json.empty()) + { + RCLCPP_WARN(logger_, "Empty JSON received from callback with level %d", level); + return msg; + } + if (parsed_json.contains("source")) + { + msg.hardware_id = parsed_json["source"]; + } + if (parsed_json.contains("name")) + { + msg.name = parsed_json["name"]; + } + + if (parsed_json.contains("description")) + { + msg.message = parsed_json["description"]; + } + // TODO: should the KeyValue object be the diagnostic message id instead? + for (auto& it : parsed_json.items()) + { + try + { + diagnostic_msgs::msg::KeyValue obj; + obj.key = it.key(); + obj.value = it.value().dump(); + msg.values.push_back(obj); + } + catch (const std::exception& e) + { + RCLCPP_WARN(logger_, "Couldn't parse entry of diagnostics status %s: %s", msg.name.c_str(), e.what()); + } + } + return msg; +} + +DiagModule::DiagnosticArrayMsg DiagModule::create_diagnostic_message(const uint8_t level, const std::string& json_msg) +{ + ifm3d::json parsed_json; + DiagnosticArrayMsg diag_msg; + + try + { + parsed_json = json::parse(json_msg); + } + catch (...) + { + RCLCPP_ERROR(logger_, "Invalid JSON received from callback with level %d", level); + } + try + { + // The diagnostic message is formatted differently depending on whether + // we receive it through the asynchronous or synchronous method. + diag_msg.header.stamp = rclcpp::Time(parsed_json["timestamp"]); + } + catch (...) + { + try + { + diag_msg.header.stamp = rclcpp::Time(parsed_json["stats"]["lastActivated"]["timestamp"]); + } + catch (const std::exception& e) + { + RCLCPP_ERROR(logger_, "Invalid timestamp received from callback with level %d", level); + } + } + + diag_msg.status.push_back(create_diagnostic_status(level, parsed_json)); + + return diag_msg; +} + +void DiagModule::periodic_diag_callback() +{ + // TODO: do we really need this shared mutex here? + // std::lock_guard lock(this->gil_); + try + { + ifm3d::json diagnostic_json = this->o3r_->GetDiagnostic(); + RCLCPP_DEBUG(logger_, "Diagnostics: %s", diagnostic_json.dump().c_str()); + + auto events = diagnostic_json["events"]; + for (auto event : events) + { + diagnostic_publisher_->publish( + create_diagnostic_message(diagnostic_msgs::msg::DiagnosticStatus::OK, event.dump())); + } + } + catch (const ifm3d::Error& ex) + { + RCLCPP_INFO(logger_, "ifm3d error while trying to get the diagnostic: %s", ex.what()); + } + catch (...) + { + RCLCPP_INFO(logger_, "Unknown error while trying to get the diagnostic"); + } +} + +void DiagModule::handle_error(int i, const std::string& s) +{ + RCLCPP_ERROR(logger_, "AsyncError received from ifm3d: %d %s", i, s.c_str()); + DiagnosticArrayMsg msg = create_diagnostic_message(diagnostic_msgs::msg::DiagnosticStatus::ERROR, s); + diagnostic_publisher_->publish(msg); + RCLCPP_DEBUG(this->logger_, "Published the async diagnostic message."); +} + +void DiagModule::handle_notification(const std::string& s1, const std::string& s2) +{ + RCLCPP_INFO(logger_, "AsyncNotification received from ifm3d: %s | %s", s1.c_str(), s2.c_str()); + DiagnosticArrayMsg msg = create_diagnostic_message(diagnostic_msgs::msg::DiagnosticStatus::OK, s2); + diagnostic_publisher_->publish(msg); + RCLCPP_DEBUG(this->logger_, "Published the async notification."); +} + +rclcpp_lifecycle::LifecycleNode::CallbackReturn DiagModule::on_configure(const rclcpp_lifecycle::State& previous_state) +{ + RCLCPP_DEBUG(logger_, "DiagModule: on_configure called"); + (void)previous_state; + + const auto qos = ifm3d_ros2::LowLatencyQoS(); + + this->diagnostic_publisher_ = node_ptr_->create_publisher("/diagnostics", qos); + // Timer for periodic publication of the full list of diagnostic messages + this->diagnostic_timer_ = this->node_ptr_->create_wall_timer(1s, [this]() -> void { periodic_diag_callback(); }); + this->diagnostic_timer_->cancel(); // Deactivate timer, manage activity via lifecycle + + return rclcpp_lifecycle::LifecycleNode::CallbackReturn::SUCCESS; +} + +rclcpp_lifecycle::LifecycleNode::CallbackReturn DiagModule::on_cleanup(const rclcpp_lifecycle::State& previous_state) +{ + RCLCPP_DEBUG(logger_, "DiagModule: on_cleanup called"); + (void)previous_state; + + diagnostic_publisher_.reset(); + + return rclcpp_lifecycle::LifecycleNode::CallbackReturn::SUCCESS; +} + +rclcpp_lifecycle::LifecycleNode::CallbackReturn DiagModule::on_shutdown(const rclcpp_lifecycle::State& previous_state) +{ + RCLCPP_DEBUG(logger_, "DiagModule: on_shutdown called"); + (void)previous_state; + return rclcpp_lifecycle::LifecycleNode::CallbackReturn::SUCCESS; +} + +rclcpp_lifecycle::LifecycleNode::CallbackReturn DiagModule::on_activate(const rclcpp_lifecycle::State& previous_state) +{ + RCLCPP_DEBUG(logger_, "DiagModule: on_activate called"); + (void)previous_state; + + this->diagnostic_publisher_->on_activate(); + this->diagnostic_timer_->reset(); + return rclcpp_lifecycle::LifecycleNode::CallbackReturn::SUCCESS; +} + +rclcpp_lifecycle::LifecycleNode::CallbackReturn DiagModule::on_deactivate(const rclcpp_lifecycle::State& previous_state) +{ + RCLCPP_DEBUG(logger_, "DiagModule: on_deactivate called"); + (void)previous_state; + + this->diagnostic_publisher_->on_deactivate(); + this->diagnostic_timer_->cancel(); + return rclcpp_lifecycle::LifecycleNode::CallbackReturn::SUCCESS; +} + +rclcpp_lifecycle::LifecycleNode::CallbackReturn DiagModule::on_error(const rclcpp_lifecycle::State& previous_state) +{ + RCLCPP_DEBUG(logger_, "DiagModule: on_error called"); + (void)previous_state; + + return rclcpp_lifecycle::LifecycleNode::CallbackReturn::SUCCESS; +} + +} // namespace ifm3d_ros2 \ No newline at end of file diff --git a/src/lib/function_module.cpp b/src/lib/function_module.cpp new file mode 100644 index 0000000..ebb89f2 --- /dev/null +++ b/src/lib/function_module.cpp @@ -0,0 +1,22 @@ +// -*- c++ -*- +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright (C) 2024 ifm electronic, gmbh + */ + +#include + +namespace ifm3d_ros2 +{ +FunctionModule::FunctionModule(const rclcpp::Logger& logger) : logger_(logger) +{ + RCLCPP_INFO(logger_, "FunctionModule contructor called."); +} + +void FunctionModule::handle_frame(ifm3d::Frame::Ptr frame) +{ + (void)frame; + RCLCPP_INFO(logger_, "FunctionModule: handle_frame called, implementation missing in derived class."); +} + +} // namespace ifm3d_ros2 \ No newline at end of file diff --git a/src/lib/ods_module.cpp b/src/lib/ods_module.cpp new file mode 100644 index 0000000..7791496 --- /dev/null +++ b/src/lib/ods_module.cpp @@ -0,0 +1,211 @@ +// -*- c++ -*- +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright (C) 2024 ifm electronic, gmbh + */ + +#include +#include +#include +#include + +#include +#include +#include +#include + +namespace ifm3d_ros2 +{ +OdsModule::OdsModule(rclcpp::Logger logger, rclcpp_lifecycle::LifecycleNode::SharedPtr node_ptr) + : FunctionModule(logger), node_ptr_(node_ptr), frame_id_("ifm_base_link") +{ + RCLCPP_INFO(logger_, "OdsModule contructor called."); + + RCLCPP_DEBUG(logger_, "Declaring parameter..."); + frame_id_descriptor_.name = "ods.frame_id"; + frame_id_descriptor_.type = rcl_interfaces::msg::ParameterType::PARAMETER_STRING; + frame_id_descriptor_.description = "Frame_id field used for grid and zones messages (Default='ifm_base_link')."; + node_ptr_->declare_parameter(frame_id_descriptor_.name, frame_id_, frame_id_descriptor_); + + publish_occupancy_grid_descriptor_.name = "ods.publish_occupancy_grid"; + publish_occupancy_grid_descriptor_.type = rcl_interfaces::msg::ParameterType::PARAMETER_BOOL; + publish_occupancy_grid_descriptor_.description = "Set module to publish nav_msgs/OccupancyGrid (Default='True')."; + node_ptr_->declare_parameter(publish_occupancy_grid_descriptor_.name, true, publish_occupancy_grid_descriptor_); + + publish_costmap_descriptor_.name = "ods.publish_costmap"; + publish_costmap_descriptor_.type = rcl_interfaces::msg::ParameterType::PARAMETER_BOOL; + publish_costmap_descriptor_.description = "Set module to publish nav3_msgs/Costmap (Default='False')."; + node_ptr_->declare_parameter(publish_costmap_descriptor_.name, false, publish_costmap_descriptor_); +} + +nav_msgs::msg::OccupancyGrid OdsModule::extract_ros_occupancy_grid(ifm3d::Frame::Ptr frame) +{ + RCLCPP_DEBUG(logger_, "Handling ods occupancy grid as nav_msgs/OccupancyGrid"); + if (!frame->HasBuffer(ifm3d::buffer_id::O3R_ODS_OCCUPANCY_GRID)) + { + RCLCPP_INFO(logger_, "OdsModule: No ods occupancy grid in frame"); + } + auto header = std_msgs::msg::Header(); + header.frame_id = frame_id_; + return ifm3d_ros2::ifm3d_to_ros_occupancy_grid(frame->GetBuffer(ifm3d::buffer_id::O3R_ODS_OCCUPANCY_GRID), header, + logger_); +} + +nav2_msgs::msg::Costmap OdsModule::extract_ros_costmap(ifm3d::Frame::Ptr frame) +{ + RCLCPP_DEBUG(logger_, "Handling ods occupancy grid as nav2_msgs/Costmap"); + if (!frame->HasBuffer(ifm3d::buffer_id::O3R_ODS_OCCUPANCY_GRID)) + { + RCLCPP_INFO(logger_, "OdsModule: No ods occupancy grid in frame"); + } + auto header = std_msgs::msg::Header(); + header.frame_id = frame_id_; + return ifm3d_ros2::ifm3d_to_ros_costmap(frame->GetBuffer(ifm3d::buffer_id::O3R_ODS_OCCUPANCY_GRID), header, logger_); +} + +ifm3d_ros2::msg::Zones OdsModule::extract_zones(ifm3d::Frame::Ptr frame) +{ + RCLCPP_DEBUG(logger_, "Handling zones"); + if (!frame->HasBuffer(ifm3d::buffer_id::O3R_ODS_INFO)) + { + RCLCPP_INFO(logger_, "OdsModule: No zones in frame"); + } + RCLCPP_DEBUG(logger_, "Deserializing zones data"); + auto zones_data = ifm3d::ODSInfoV1::Deserialize(frame->GetBuffer(ifm3d::buffer_id::O3R_ODS_INFO)); + + RCLCPP_DEBUG(logger_, "Filling the ROS message with zones data"); + ifm3d_ros2::msg::Zones zones_msg; + // Define the header + zones_msg.header = std_msgs::msg::Header(); + zones_msg.header.frame_id = frame_id_; + zones_msg.header.stamp = rclcpp::Time(zones_data.timestamp_ns); + + zones_msg.zone_config_id = zones_data.zone_config_id; + zones_msg.zone_occupied = zones_data.zone_occupied; + + RCLCPP_DEBUG(logger_, "Zones messages ready"); + return zones_msg; +} + +void OdsModule::handle_frame(ifm3d::Frame::Ptr frame) +{ + RCLCPP_DEBUG(logger_, "Handle frame"); + + RCLCPP_DEBUG(logger_, "Creating ods zones message."); + ZonesMsg zones_msg; + zones_msg = this->extract_zones(frame); + zones_publisher_->publish(zones_msg); + + if (publish_occupancy_grid_) + { + RCLCPP_DEBUG(logger_, "Creating occupancy grid message."); + OccupancyGridMsg grid_msg; + grid_msg = this->extract_ros_occupancy_grid(frame); + ros_occupancy_grid_publisher_->publish(grid_msg); + } + + if (publish_costmap_) + { + RCLCPP_DEBUG(logger_, "Creating costmap message."); + CostmapMsg costmap_msg; + costmap_msg = this->extract_ros_costmap(frame); + ros_costmap_publisher_->publish(costmap_msg); + } + + RCLCPP_DEBUG(logger_, "Finished publishing ods messages."); +} + +rclcpp_lifecycle::LifecycleNode::CallbackReturn OdsModule::on_configure(const rclcpp_lifecycle::State& previous_state) +{ + RCLCPP_INFO(logger_, "OdsModule: on_configure called"); + (void)previous_state; + + node_ptr_->get_parameter(frame_id_descriptor_.name, frame_id_); + node_ptr_->get_parameter(publish_occupancy_grid_descriptor_.name, publish_occupancy_grid_); + node_ptr_->get_parameter(publish_costmap_descriptor_.name, publish_costmap_); + RCLCPP_INFO(this->logger_, "Parameter %s set to '%s'", frame_id_descriptor_.name.c_str(), frame_id_.c_str()); + RCLCPP_INFO(this->logger_, "Parameter %s set to '%s'", publish_occupancy_grid_descriptor_.name.c_str(), + publish_occupancy_grid_ ? "true" : "false"); + RCLCPP_INFO(this->logger_, "Parameter %s set to '%s'", publish_costmap_descriptor_.name.c_str(), + publish_costmap_ ? "true" : "false"); + + const auto qos = ifm3d_ros2::LowLatencyQoS(); + + if (publish_occupancy_grid_) + { + ros_occupancy_grid_publisher_ = node_ptr_->create_publisher( + "~/" + buffer_id_utils::topic_name_map[ifm3d::buffer_id::O3R_ODS_OCCUPANCY_GRID], qos); + } + if (publish_costmap_) + { + ros_costmap_publisher_ = node_ptr_->create_publisher( + "~/" + buffer_id_utils::topic_name_map[ifm3d::buffer_id::O3R_ODS_OCCUPANCY_GRID] + "_costmap", qos); + } + zones_publisher_ = node_ptr_->create_publisher( + "~/" + buffer_id_utils::topic_name_map[ifm3d::buffer_id::O3R_ODS_INFO], qos); + + return rclcpp_lifecycle::LifecycleNode::CallbackReturn::SUCCESS; +} + +rclcpp_lifecycle::LifecycleNode::CallbackReturn OdsModule::on_cleanup(const rclcpp_lifecycle::State& previous_state) +{ + RCLCPP_INFO(logger_, "OdsModule: on_cleanup called"); + (void)previous_state; + + ros_occupancy_grid_publisher_.reset(); + ros_costmap_publisher_.reset(); + zones_publisher_.reset(); + + return rclcpp_lifecycle::LifecycleNode::CallbackReturn::SUCCESS; +} + +rclcpp_lifecycle::LifecycleNode::CallbackReturn OdsModule::on_shutdown(const rclcpp_lifecycle::State& previous_state) +{ + RCLCPP_INFO(logger_, "OdsModule: on_shutdown called"); + (void)previous_state; + return rclcpp_lifecycle::LifecycleNode::CallbackReturn::SUCCESS; +} + +rclcpp_lifecycle::LifecycleNode::CallbackReturn OdsModule::on_activate(const rclcpp_lifecycle::State& previous_state) +{ + RCLCPP_INFO(logger_, "OdsModule: on_activate called"); + (void)previous_state; + + if (publish_occupancy_grid_) + { + this->ros_occupancy_grid_publisher_->on_activate(); + } + if (publish_costmap_) + { + this->ros_costmap_publisher_->on_activate(); + } + this->zones_publisher_->on_activate(); + return rclcpp_lifecycle::LifecycleNode::CallbackReturn::SUCCESS; +} + +rclcpp_lifecycle::LifecycleNode::CallbackReturn OdsModule::on_deactivate(const rclcpp_lifecycle::State& previous_state) +{ + RCLCPP_INFO(logger_, "OdsModule: on_deactivate called"); + (void)previous_state; + + if (publish_occupancy_grid_) + { + this->ros_occupancy_grid_publisher_->on_deactivate(); + } + if (publish_costmap_) + { + this->ros_costmap_publisher_->on_deactivate(); + } + this->zones_publisher_->on_deactivate(); + return rclcpp_lifecycle::LifecycleNode::CallbackReturn::SUCCESS; +} + +rclcpp_lifecycle::LifecycleNode::CallbackReturn OdsModule::on_error(const rclcpp_lifecycle::State& previous_state) +{ + RCLCPP_INFO(logger_, "OdsModule: on_error called"); + (void)previous_state; + + return rclcpp_lifecycle::LifecycleNode::CallbackReturn::SUCCESS; +} + +} // namespace ifm3d_ros2 \ No newline at end of file diff --git a/src/lib/ods_node.cpp b/src/lib/ods_node.cpp new file mode 100644 index 0000000..6945cc2 --- /dev/null +++ b/src/lib/ods_node.cpp @@ -0,0 +1,434 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright (C) 2024 ifm electronic, gmbh + */ + +#include +#include + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +using json = ifm3d::json; +using namespace std::chrono_literals; + +namespace ifm3d_ros2 +{ +namespace +{ +} // namespace + +OdsNode::OdsNode(const rclcpp::NodeOptions& opts) : OdsNode::OdsNode("ods_node", opts) +{ +} + +OdsNode::OdsNode(const std::string& node_name, const rclcpp::NodeOptions& opts) + : rclcpp_lifecycle::LifecycleNode(node_name, "", opts), logger_(this->get_logger()), width_(0), height_(0) +{ + // unbuffered I/O to stdout (so we can see our log messages) + std::setvbuf(stdout, nullptr, _IONBF, BUFSIZ); + RCLCPP_INFO(this->logger_, "namespace: %s", this->get_namespace()); + RCLCPP_INFO(this->logger_, "node name: %s", this->get_name()); + RCLCPP_INFO(this->logger_, "middleware: %s", rmw_get_implementation_identifier()); + + // declare our parameters and default values -- parameters defined in + // the passed in `opts` (via __params:=/path/to/params.yaml on cmd line) + // will override our default values specified. + RCLCPP_INFO(this->logger_, "Declaring parameters..."); + this->init_params(); + RCLCPP_INFO(this->logger_, "After the parameters declaration"); + + this->gil_ = std::make_shared(); + + RCLCPP_INFO(this->logger_, "node created, waiting for `configure()`..."); +} + +OdsNode::~OdsNode() +{ + RCLCPP_INFO(this->logger_, "Dtor called."); +} + +TC_RETVAL OdsNode::on_configure(const rclcpp_lifecycle::State& prev_state) +{ + RCLCPP_INFO(this->logger_, "on_configure(): %s -> %s", prev_state.label().c_str(), + this->get_current_state().label().c_str()); + + // + // parse params and initialize instance vars + // + RCLCPP_INFO(this->logger_, "Parsing parameters..."); + parse_params(); + RCLCPP_INFO(this->logger_, "Parameters parsed."); + + // Add parameter subscriber, if none is active + if (!param_subscriber_) + { + RCLCPP_INFO(logger_, "Adding callbacks to handle parameter changes at runtime..."); + set_parameter_event_callbacks(); + RCLCPP_INFO(this->logger_, "Callbacks set."); + } + + // + // We need a global lock on all the ifm3d core data structures + // + std::lock_guard lock(*this->gil_); + + // + // Initialize the camera interface + // + RCLCPP_INFO(this->logger_, "Initializing Device"); + this->o3r_ = std::make_shared(this->ip_); + RCLCPP_INFO(this->logger_, "Initializing FrameGrabber for data"); + this->fg_ = std::make_shared(this->o3r_, this->pcic_port_); + RCLCPP_DEBUG(this->logger_, "Initializing FrameGrabber for diagnostics"); + this->fg_diag_ = std::make_shared(this->o3r_, this->o3r_->Port("diagnostics").pcic_port); + RCLCPP_DEBUG(this->logger_, "Find out which data stream we are handling"); + + // If a configuration file is provided, configure the device. + if (this->config_file_!=""){ + std::ifstream file(this->config_file_); + if (!file.is_open()) { + throw std::runtime_error("Could not open config file: " + this->config_file_); + } + std::stringstream buffer; + buffer << file.rdbuf(); + RCLCPP_INFO(this->logger_, "Setting configuration: %s", buffer.str().c_str()); + ifm3d::json config_json = json::parse(buffer.str()) ; + this->o3r_->Set(config_json); + } + + // Get all the necessary info for the port. + for (auto port : this->o3r_->Ports()) + { + if (port.pcic_port == this->pcic_port_) + { + this->port_info_ = port; + } + } + + // + // Initialize ODS Module + // + RCLCPP_INFO(this->logger_, "Creating OdsModule..."); + this->ods_module_ = std::make_shared(this->get_logger(), shared_from_this()); + RCLCPP_INFO(this->logger_, "OdsModule created."); + // + // Initialize diagnostic Module + // + RCLCPP_INFO(this->logger_, "Creating DiagModule..."); + this->diag_module_ = std::make_shared(this->get_logger(), shared_from_this(), this->o3r_); + RCLCPP_INFO(this->logger_, "DiagModule created."); + + // + // Create a list of all the modules to reduce duplicate code + // + this->modules_.push_back(this->ods_module_); + this->modules_.push_back(this->diag_module_); + + // Transition function modules + for (auto& module : this->modules_) + { + auto retval = module->on_configure(prev_state); + if (retval != TC_RETVAL::SUCCESS) + { + RCLCPP_ERROR(this->get_logger(), "Module %s transition did not succeed.", module->get_name().c_str()); + // TODO de-transition previous modules + return retval; + } + } + + // Initialize the services using the BaseServices class + RCLCPP_INFO(this->logger_, "Creating BaseServices..."); + this->base_services_ = + std::make_shared(this->get_logger(), shared_from_this(), this->o3r_, this->port_info_, this->gil_); + RCLCPP_INFO(this->logger_, "BaseServices created."); + + RCLCPP_INFO(this->logger_, "Configuration complete."); + return TC_RETVAL::SUCCESS; +} + +TC_RETVAL OdsNode::on_activate(const rclcpp_lifecycle::State& prev_state) +{ + RCLCPP_INFO(this->logger_, "on_activate(): %s -> %s", prev_state.label().c_str(), + this->get_current_state().label().c_str()); + + // Register Callbacks to handle new frames and print errors + RCLCPP_DEBUG(this->logger_, "Registering the callbacks"); + this->fg_->OnNewFrame(std::bind(&OdsNode::frame_callback, this, std::placeholders::_1)); + this->fg_->OnError(std::bind(&OdsNode::error_callback, this, std::placeholders::_1)); + + this->fg_diag_->OnAsyncError( + std::bind(&OdsNode::async_error_callback, this, std::placeholders::_1, std::placeholders::_2)); + this->fg_diag_->OnAsyncNotification( + std::bind(&OdsNode::async_notification_callback, this, std::placeholders::_1, std::placeholders::_2)); + RCLCPP_DEBUG(this->logger_, "Registered all the callbacks"); + + // The Framegrabber, needs a BufferList (a vector of std::variant) + RCLCPP_INFO(this->logger_, "Starting the Framegrabbers..."); + ifm3d::FrameGrabber::BufferList buffer_list; + buffer_list.push_back(ifm3d::buffer_id::O3R_ODS_INFO); + buffer_list.push_back(ifm3d::buffer_id::O3R_ODS_OCCUPANCY_GRID); + + // Start framegrabbers and wait for the returned future + this->fg_->Start(buffer_list).wait(); + RCLCPP_DEBUG(this->logger_, "Data FrameGrabber started, frames should be streaming"); + + this->fg_diag_->Start({}).wait(); + RCLCPP_INFO(this->logger_, "Diagnostic monitoring active."); + + // Transition function modules + for (auto& module : this->modules_) + { + auto retval = module->on_activate(prev_state); + if (retval != TC_RETVAL::SUCCESS) + { + RCLCPP_ERROR(this->get_logger(), "Module %s transition did not succeed.", module->get_name().c_str()); + // TODO de-transition previous modules + return retval; + } + } + return TC_RETVAL::SUCCESS; +} + +TC_RETVAL OdsNode::on_deactivate(const rclcpp_lifecycle::State& prev_state) +{ + RCLCPP_INFO(this->logger_, "on_deactivate(): %s -> %s", prev_state.label().c_str(), + this->get_current_state().label().c_str()); + + RCLCPP_INFO(logger_, "Stopping FrameGrabber..."); + this->fg_->Stop().wait(); + + // Transition function modules + for (auto& module : this->modules_) + { + auto retval = module->on_deactivate(prev_state); + if (retval != TC_RETVAL::SUCCESS) + { + RCLCPP_ERROR(this->get_logger(), "Module %s transition did not succeed.", module->get_name().c_str()); + // TODO de-transition previous modules + return retval; + } + } + + return TC_RETVAL::SUCCESS; +} + +TC_RETVAL OdsNode::on_cleanup(const rclcpp_lifecycle::State& prev_state) +{ + // clean-up resources -- this will include our cam, fg, + RCLCPP_INFO(this->logger_, "on_cleanup(): %s -> %s", prev_state.label().c_str(), + this->get_current_state().label().c_str()); + + std::lock_guard lock(*this->gil_); + RCLCPP_INFO(this->logger_, "Resetting core ifm3d data structures..."); + + this->fg_.reset(); + this->o3r_.reset(); + + // Transition function modules + for (auto& module : this->modules_) + { + auto retval = module->on_cleanup(prev_state); + if (retval != TC_RETVAL::SUCCESS) + { + RCLCPP_ERROR(this->get_logger(), "Module %s transition did not succeed.", module->get_name().c_str()); + // TODO de-transition previous modules + return retval; + } + module.reset(); + } + // this->modules_.reset(); + + RCLCPP_INFO(this->logger_, "Node cleanup complete."); + + return TC_RETVAL::SUCCESS; +} + +TC_RETVAL OdsNode::on_shutdown(const rclcpp_lifecycle::State& prev_state) +{ + RCLCPP_INFO(this->logger_, "on_shutdown(): %s -> %s", prev_state.label().c_str(), + this->get_current_state().label().c_str()); + + // TODO: figure out how to properly shutdown modules + this->fg_->Stop(); + this->fg_diag_->Stop(); + + // Transition function modules + for (auto& module : this->modules_) + { + auto retval = module->on_shutdown(prev_state); + if (retval != TC_RETVAL::SUCCESS) + { + RCLCPP_ERROR(this->get_logger(), "Module %s transition did not succeed.", module->get_name().c_str()); + // TODO de-transition previous modules + return retval; + } + } + return TC_RETVAL::SUCCESS; +} + +TC_RETVAL OdsNode::on_error(const rclcpp_lifecycle::State& prev_state) +{ + RCLCPP_INFO(this->logger_, "on_error(): %s -> %s", prev_state.label().c_str(), + this->get_current_state().label().c_str()); + + std::lock_guard lock(*this->gil_); + RCLCPP_INFO(this->logger_, "Resetting core ifm3d data structures..."); + // this->im_.reset(); + this->fg_.reset(); + this->o3r_.reset(); + + // Transition function modules + // Transition function modules + for (auto& module : this->modules_) + { + auto retval = module->on_error(prev_state); + if (retval != TC_RETVAL::SUCCESS) + { + RCLCPP_ERROR(this->get_logger(), "Module %s transition did not succeed.", module->get_name().c_str()); + // TODO de-transition previous modules + return retval; + } + } + + RCLCPP_INFO(this->logger_, "Error processing complete."); + + return TC_RETVAL::SUCCESS; +} + +void OdsNode::init_params() // TODO cleanup params +{ + // Node name as string to set default frame names + const std::string node_name(this->get_name()); + + /* + * For all parameters in alphabetical order: + * - Define Descriptor + * - Declare Parameter + */ + + rcl_interfaces::msg::ParameterDescriptor ip_descriptor; + ip_descriptor.name = "ip"; + ip_descriptor.type = rcl_interfaces::msg::ParameterType::PARAMETER_STRING; + ip_descriptor.description = "IP address of the camera"; + ip_descriptor.additional_constraints = "Should be an IPv4 address or resolvable name on your network"; + this->declare_parameter("ip", ifm3d::DEFAULT_IP, ip_descriptor); + + rcl_interfaces::msg::ParameterDescriptor pcic_port_descriptor; + pcic_port_descriptor.name = "pcic_port"; + pcic_port_descriptor.type = rcl_interfaces::msg::ParameterType::PARAMETER_INTEGER; + pcic_port_descriptor.description = + " TCP port the on-image processing platform pcic server is listening on. Corresponds to the port the camera head " + "is connected to."; + this->declare_parameter("pcic_port", 51010, pcic_port_descriptor); + + rcl_interfaces::msg::ParameterDescriptor xmlrpc_port_descriptor; + xmlrpc_port_descriptor.name = "xmlrpc_port"; + xmlrpc_port_descriptor.type = rcl_interfaces::msg::ParameterType::PARAMETER_INTEGER; + xmlrpc_port_descriptor.description = "TCP port the on-camera xmlrpc server is listening on"; + xmlrpc_port_descriptor.additional_constraints = "A valid TCP port 0 - 65535"; + rcl_interfaces::msg::IntegerRange xmlrpc_port_range; + xmlrpc_port_range.from_value = 0; + xmlrpc_port_range.to_value = 65535; + xmlrpc_port_range.step = 1; + xmlrpc_port_descriptor.integer_range.push_back(xmlrpc_port_range); + this->declare_parameter("xmlrpc_port", ifm3d::DEFAULT_XMLRPC_PORT, xmlrpc_port_descriptor); +} + +void OdsNode::parse_params() +{ + /* + * For all parameters in alphabetical order: + * - Read currently set parameter + * - Where applicable, parse read data into more useful data type + */ + this->get_parameter("config_file", this->config_file_); + RCLCPP_INFO(this->logger_, "Config file: %s", this->config_file_.c_str()); + + this->get_parameter("ip", this->ip_); + RCLCPP_INFO(this->logger_, "ip: %s", this->ip_.c_str()); + + this->get_parameter("pcic_port", this->pcic_port_); + RCLCPP_INFO(this->logger_, "pcic_port: %u", this->pcic_port_); + + this->get_parameter("xmlrpc_port", this->xmlrpc_port_); + RCLCPP_INFO(this->logger_, "xmlrpc_port: %u", this->xmlrpc_port_); +} + +void OdsNode::set_parameter_event_callbacks() +{ + // Create a parameter subscriber that can be used to monitor parameter changes + param_subscriber_ = std::make_shared(this); + + /* + * For all parameters in alphabetical order: + * - Create a callback as lambda to handle parameter change at runtime + * - Add lambda to parameter subscriber + */ + auto config_file_cb = [this](const rclcpp::Parameter& p) { + RCLCPP_WARN(logger_, "This new config_file will be used after CONFIGURE transition was called: '%s'", p.as_string().c_str()); + }; + registered_param_callbacks_["config_file"] = param_subscriber_->add_parameter_callback("config_file", config_file_cb); + + auto ip_cb = [this](const rclcpp::Parameter& p) { + RCLCPP_WARN(logger_, "This new ip will be used after CONFIGURE transition was called: '%s'", p.as_string().c_str()); + }; + registered_param_callbacks_["ip"] = param_subscriber_->add_parameter_callback("ip", ip_cb); + + auto pcic_port_cb = [this](const rclcpp::Parameter& p) { + RCLCPP_WARN(logger_, "This new pcic_port will be used after CONFIGURE transition was called: %ld", p.as_int()); + }; + registered_param_callbacks_["pcic_port"] = param_subscriber_->add_parameter_callback("pcic_port", pcic_port_cb); + + auto xmlrpc_port_cb = [this](const rclcpp::Parameter& p) { + this->xmlrpc_port_ = p.as_int(); + RCLCPP_INFO(logger_, "New xmlrpc_port: %d", this->xmlrpc_port_); + }; + registered_param_callbacks_["xmlrpc_port"] = param_subscriber_->add_parameter_callback("xmlrpc_port", xmlrpc_port_cb); +} + +void OdsNode::frame_callback(ifm3d::Frame::Ptr frame) +{ + this->ods_module_->handle_frame(frame); + + RCLCPP_DEBUG(this->logger_, "Frame callback done."); +} + +void OdsNode::error_callback(const ifm3d::Error& error) +{ + RCLCPP_ERROR(logger_, "Error received from ifm3d: %s", error.what()); + // TODO send diagnostic message +} + +void OdsNode::async_error_callback(int i, const std::string& s) +{ + this->diag_module_->handle_error(i, s); + RCLCPP_DEBUG(this->logger_, "Async error callback done."); +} + +void OdsNode::async_notification_callback(const std::string& s1, const std::string& s2) +{ + this->diag_module_->handle_notification(s1, s2); + RCLCPP_DEBUG(this->logger_, "Async notification callback done."); +} + +} // namespace ifm3d_ros2 + +#include + +// RCLCPP_COMPONENTS_REGISTER_NODE(ifm3d_ros2::OdsNode) diff --git a/src/lib/rgb_module.cpp b/src/lib/rgb_module.cpp new file mode 100644 index 0000000..6de3c8b --- /dev/null +++ b/src/lib/rgb_module.cpp @@ -0,0 +1,363 @@ +// -*- c++ -*- +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright (C) 2024 ifm electronic, gmbh + */ + +#include +#include +#include +#include + +#include +#include +#include + +namespace ifm3d_ros2 +{ +RgbModule::RgbModule(rclcpp::Logger logger, rclcpp_lifecycle::LifecycleNode::SharedPtr node_ptr, + ifm3d::O3R::Ptr o3r_ptr, std::string port, uint32_t width, uint32_t height) + : FunctionModule(logger), node_ptr_(node_ptr), tf_publisher_(node_ptr, o3r_ptr, port), width_(width), height_(height) +{ + RCLCPP_INFO(logger_, "RgbModule contructor called."); + + RCLCPP_DEBUG(this->logger_, "Declaring parameters..."); + const std::string node_name(this->node_ptr_->get_name()); + const std::vector default_buffer_id_list{ + "JPEG_IMAGE", // + "RGB_INFO", // + }; + buffer_id_list_descriptor_.name = "buffer_id_list"; + buffer_id_list_descriptor_.type = rcl_interfaces::msg::ParameterType::PARAMETER_STRING_ARRAY; + buffer_id_list_descriptor_.description = "List of buffer_id strings denoting the wanted buffers."; + this->node_ptr_->declare_parameter(buffer_id_list_descriptor_.name, default_buffer_id_list, + buffer_id_list_descriptor_); + + tf_base_frame_name_descriptor_.name = "tf.base_frame_name"; + tf_base_frame_name_descriptor_.type = rcl_interfaces::msg::ParameterType::PARAMETER_STRING; + tf_base_frame_name_descriptor_.description = "Name for the ifm base frame, defaults to ifm_base_link."; + this->node_ptr_->declare_parameter(tf_base_frame_name_descriptor_.name, "ifm_base_link", + tf_base_frame_name_descriptor_); + + tf_mounting_frame_name_descriptor_.name = "tf.mounting_frame_name"; + tf_mounting_frame_name_descriptor_.type = rcl_interfaces::msg::ParameterType::PARAMETER_STRING; + tf_mounting_frame_name_descriptor_.description = + "Name for the mounting point frame, defaults to _mounting_link."; + this->node_ptr_->declare_parameter(tf_mounting_frame_name_descriptor_.name, node_name + "_mounting_link", + tf_mounting_frame_name_descriptor_); + + tf_optical_frame_name_descriptor_.name = "tf.optical_frame_name"; + tf_optical_frame_name_descriptor_.type = rcl_interfaces::msg::ParameterType::PARAMETER_STRING; + tf_optical_frame_name_descriptor_.description = + "Name for the point optical frame, defaults to _optical_link."; + this->node_ptr_->declare_parameter(tf_optical_frame_name_descriptor_.name, node_name + "_optical_link", + tf_optical_frame_name_descriptor_); + + tf_publish_base_to_mounting_descriptor_.name = "tf.publish_base_to_mounting"; + tf_publish_base_to_mounting_descriptor_.type = rcl_interfaces::msg::ParameterType::PARAMETER_BOOL; + tf_publish_base_to_mounting_descriptor_.description = + "Whether the transform from the ifm base link to the mounting point should be published."; + this->node_ptr_->declare_parameter(tf_publish_base_to_mounting_descriptor_.name, true, + tf_publish_base_to_mounting_descriptor_); + + tf_publish_mounting_to_optical_descriptor_.name = "tf.publish_mounting_to_optical"; + tf_publish_mounting_to_optical_descriptor_.type = rcl_interfaces::msg::ParameterType::PARAMETER_BOOL; + tf_publish_mounting_to_optical_descriptor_.description = + "Whether the transform from the mounting point to the optical frame should be published."; + this->node_ptr_->declare_parameter(tf_publish_mounting_to_optical_descriptor_.name, true, + tf_publish_mounting_to_optical_descriptor_); + RCLCPP_DEBUG(this->logger_, "After the parameters declaration"); + + this->first_ = true; +} + +void RgbModule::handle_frame(ifm3d::Frame::Ptr frame) +{ + RCLCPP_DEBUG(logger_, "Handle RGB frame"); + using namespace buffer_id_utils; + + RCLCPP_INFO_ONCE(logger_, "Receiving Frames. Processing buffer for [%s]...", + buffer_id_utils::vector_to_string(this->buffer_id_list_).c_str()); + RCLCPP_DEBUG(logger_, "Received new Frame."); + + rclcpp::Time frame_ts = ifm3d_ros2::ifm3d_to_ros_time(frame->TimeStamps()[0]); + RCLCPP_DEBUG(logger_, "Frame timestamp: %f", frame_ts.seconds()); + + auto optical_header = std_msgs::msg::Header(); + optical_header.frame_id = tf_publisher_.tf_optical_link_frame_name_; + optical_header.stamp = frame_ts; + + RCLCPP_DEBUG(logger_, "RGB message headers created"); + + for (const ifm3d::buffer_id& id : this->buffer_id_list_) + { + // Helper for logging + auto& clk = *this->node_ptr_->get_clock(); + std::string id_string; + if (!buffer_id_utils::convert(id, id_string)){ + RCLCPP_ERROR(logger_, "Cannot convert the buffer id to a string."); + } + + const buffer_id_utils::message_type message_type = buffer_id_utils::message_type_map[id]; + RCLCPP_DEBUG(logger_, "Processing buffer_id %s", id_string.c_str()); + + if (!frame->HasBuffer(id)) + { + RCLCPP_WARN_THROTTLE(logger_, clk, 5000, + "Frame does not contain buffer %s. Is the correct camera head connected?", + id_string.c_str()); + } + + switch (message_type) + { + case buffer_id_utils::message_type::compressed_image: { + RCLCPP_DEBUG(logger_, "Processing compressed image message"); + auto buffer = frame->GetBuffer(id); + CompressedImageMsg compressed_image_msg = + ifm3d_ros2::ifm3d_to_ros_compressed_image(frame->GetBuffer(id), optical_header, "jpeg", logger_); + RCLCPP_DEBUG(logger_, "Ready to publish compressed image message"); + this->rgb_publisher_->publish(compressed_image_msg); + RCLCPP_DEBUG(logger_, "Published compressed image message"); + } + break; + case buffer_id_utils::message_type::rgb_info: { + RCLCPP_DEBUG(logger_, "Processing RGB info message"); + // Publish the ifm-type RGB info message + auto buffer = frame->GetBuffer(id); + RGBInfoMsg rgb_info_msg = ifm3d_ros2::ifm3d_to_rgb_info(buffer, optical_header, logger_); + rgb_info_publisher_->publish(rgb_info_msg); + + if (tf_publisher_.tf_publish_mounting_to_optical_ || tf_publisher_.tf_publish_base_to_mounting_) + { + geometry_msgs::msg::TransformStamped tf_base_to_optical; + if (ifm3d_ros2::ifm3d_rgb_info_to_optical_mount_link(buffer, tf_publisher_.tf_base_link_frame_name_, + tf_publisher_.tf_optical_link_frame_name_, logger_, + tf_base_to_optical)) + { + tf_publisher_.update_and_publish_tf_if_changed(tf_base_to_optical); + } + else + { + RCLCPP_ERROR(logger_, "Failed to derive transform from rgb_info, retrying..."); + } + } + this->width_ = 1280; // TODO: figure out how to read the width and height from JPEG image + this->height_ = 800; + // Also publish CameraInfo from RGBInfo + if (this->width_ == 0 || this->height_ == 0) + { + RCLCPP_WARN_THROTTLE(logger_, clk, 5000, "Needs at least one raw image buffer to parse CameraInfo!"); + } + else + { + CameraInfoMsg camera_info_msg; + + if (ifm3d_ros2::ifm3d_rgb_info_to_camera_info(buffer, optical_header, this->height_, this->width_, logger_, + camera_info_msg)) + { + RCLCPP_INFO_ONCE(logger_, "Parsing CameraInfo successfull."); + camera_info_publisher_->publish(camera_info_msg); + } + else + { + RCLCPP_ERROR(logger_, "Failed to retrieve camera_info from rgb_info buffer."); + }; + } + } + break; + default: + RCLCPP_ERROR_THROTTLE(logger_, clk, 5000, "Unknown message type for buffer_id %s. Can not publish.", + id_string.c_str()); + break; + } + } + RCLCPP_DEBUG(logger_, "Finished publishing rgb messages."); +} + +rclcpp_lifecycle::LifecycleNode::CallbackReturn RgbModule::on_configure(const rclcpp_lifecycle::State& previous_state) +{ + RCLCPP_INFO(logger_, "RgbModule: on_configure called"); + (void)previous_state; + + // + // parse params and initialize instance vars + // + RCLCPP_INFO(this->logger_, "Parsing parameters..."); + parse_params(); + RCLCPP_INFO(this->logger_, "Parameters parsed."); + + // Add parameter subscriber, if none is active + if (!param_subscriber_) + { + RCLCPP_INFO(logger_, "Adding callbacks to handle parameter changes at runtime..."); + set_parameter_event_callbacks(); + RCLCPP_INFO(this->logger_, "Callbacks set."); + } + + // Remove buffer_ids unfit for the given data type + this->buffer_id_list_ = + buffer_id_utils::buffer_ids_for_data_stream_type(this->buffer_id_list_, ifm3d_ros2::buffer_id_utils::data_stream_type::rgb_2d); + RCLCPP_INFO(logger_, "After removing buffer_ids unfit for the given data stream type, the final list is: [%s].", + buffer_id_utils::vector_to_string(this->buffer_id_list_).c_str()); + + std::vector ids_to_remove{}; + // Create correctly typed publishers for all given buffer_ids + for (const ifm3d::buffer_id& id : this->buffer_id_list_) + { + // Create Publishers in node namespace to make multi-camera setups easier + const std::string topic_name = "~/" + buffer_id_utils::topic_name_map[id]; + const auto qos = ifm3d_ros2::LowLatencyQoS(); + const buffer_id_utils::message_type message_type = buffer_id_utils::message_type_map[id]; + + switch (message_type) + { + case buffer_id_utils::message_type::compressed_image: + rgb_publisher_ = node_ptr_->create_publisher(topic_name, qos); + break; + case buffer_id_utils::message_type::rgb_info: + rgb_info_publisher_ = node_ptr_->create_publisher(topic_name, qos); + camera_info_publisher_ = node_ptr_->create_publisher("~/camera_info", qos); + break; + default: + std::string id_string; + buffer_id_utils::convert(id, id_string); + RCLCPP_ERROR(logger_, "Unknown message type for buffer_id %s. Will be removed from list...", id_string.c_str()); + ids_to_remove.push_back(id); + break; + } + } + RCLCPP_INFO(this->logger_, "Created publishers for all buffer_ids."); + + return rclcpp_lifecycle::LifecycleNode::CallbackReturn::SUCCESS; +} + +rclcpp_lifecycle::LifecycleNode::CallbackReturn RgbModule::on_cleanup(const rclcpp_lifecycle::State& previous_state) +{ + RCLCPP_INFO(logger_, "RgbModule: on_cleanup called"); + (void)previous_state; + + this->rgb_publisher_.reset(); + this->rgb_info_publisher_.reset(); + this->camera_info_publisher_.reset(); + + return rclcpp_lifecycle::LifecycleNode::CallbackReturn::SUCCESS; +} + +rclcpp_lifecycle::LifecycleNode::CallbackReturn RgbModule::on_shutdown(const rclcpp_lifecycle::State& previous_state) +{ + RCLCPP_INFO(logger_, "RgbModule: on_shutdown called"); + (void)previous_state; + return rclcpp_lifecycle::LifecycleNode::CallbackReturn::SUCCESS; +} + +rclcpp_lifecycle::LifecycleNode::CallbackReturn RgbModule::on_activate(const rclcpp_lifecycle::State& previous_state) +{ + RCLCPP_INFO(logger_, "RgbModule: on_activate called"); + (void)previous_state; + + this->rgb_publisher_->on_activate(); + this->rgb_info_publisher_->on_activate(); + this->camera_info_publisher_->on_activate(); + + return rclcpp_lifecycle::LifecycleNode::CallbackReturn::SUCCESS; +} + +rclcpp_lifecycle::LifecycleNode::CallbackReturn RgbModule::on_deactivate(const rclcpp_lifecycle::State& previous_state) +{ + RCLCPP_INFO(logger_, "RgbModule: on_deactivate called"); + (void)previous_state; + + this->rgb_publisher_->on_deactivate(); + this->rgb_info_publisher_->on_deactivate(); + this->camera_info_publisher_->on_deactivate(); + + return rclcpp_lifecycle::LifecycleNode::CallbackReturn::SUCCESS; +} + +rclcpp_lifecycle::LifecycleNode::CallbackReturn RgbModule::on_error(const rclcpp_lifecycle::State& previous_state) +{ + RCLCPP_INFO(logger_, "RgbModule: on_error called"); + (void)previous_state; + + return rclcpp_lifecycle::LifecycleNode::CallbackReturn::SUCCESS; +} + +void RgbModule::parse_params() +{ + std::vector buffer_id_strings; + this->node_ptr_->get_parameter(buffer_id_list_descriptor_.name, buffer_id_strings); + RCLCPP_INFO(this->logger_, "Reading %ld buffer_ids: [%s]", buffer_id_strings.size(), + buffer_id_utils::vector_to_string(buffer_id_strings).c_str()); + // Populate buffer_id_list_ from read strings + this->buffer_id_list_.clear(); + for (const std::string& string : buffer_id_strings) + { + ifm3d::buffer_id found_id; + if (buffer_id_utils::convert(string, found_id)) + { + this->buffer_id_list_.push_back(found_id); + } + else + { + RCLCPP_WARN(this->logger_, "Ignoring unknown buffer_id %s", string.c_str()); + } + } + RCLCPP_INFO(this->logger_, "Parsed %ld buffer_ids: %s", this->buffer_id_list_.size(), + buffer_id_utils::vector_to_string(this->buffer_id_list_).c_str()); + + this->node_ptr_->get_parameter(tf_base_frame_name_descriptor_.name, tf_publisher_.tf_base_link_frame_name_); + RCLCPP_INFO(this->logger_, "tf.base_link.frame_name: %s", tf_publisher_.tf_base_link_frame_name_.c_str()); + + this->node_ptr_->get_parameter(tf_mounting_frame_name_descriptor_.name, tf_publisher_.tf_mounting_link_frame_name_); + RCLCPP_INFO(this->logger_, "tf.mounting_link.frame_name: %s", tf_publisher_.tf_mounting_link_frame_name_.c_str()); + + this->node_ptr_->get_parameter(tf_optical_frame_name_descriptor_.name, tf_publisher_.tf_optical_link_frame_name_); + RCLCPP_INFO(this->logger_, "tf.optical_link.frame_name: %s", tf_publisher_.tf_optical_link_frame_name_.c_str()); + + this->node_ptr_->get_parameter(tf_publish_base_to_mounting_descriptor_.name, + tf_publisher_.tf_publish_base_to_mounting_); + + this->node_ptr_->get_parameter(tf_publish_mounting_to_optical_descriptor_.name, + tf_publisher_.tf_publish_mounting_to_optical_); + + RCLCPP_INFO(this->logger_, "tf.optical_link.publish_transform: %s", + tf_publisher_.tf_publish_mounting_to_optical_ ? "true" : "false"); +} + +void RgbModule::set_parameter_event_callbacks() +{ + // Create a parameter subscriber that can be used to monitor parameter changes + param_subscriber_ = std::make_shared(this->node_ptr_); + + auto buffer_id_list_cb = [this](const rclcpp::Parameter& p) { + RCLCPP_WARN(logger_, "This new buffer_id_list will be used after CONFIGURE transition was called: %s", + buffer_id_utils::vector_to_string(p.as_string_array()).c_str()); + }; + registered_param_callbacks_[buffer_id_list_descriptor_.name] = + param_subscriber_->add_parameter_callback(buffer_id_list_descriptor_.name, buffer_id_list_cb); + + auto tf_mounting_link_frame_name_cb = [this](const rclcpp::Parameter& p) { + tf_publisher_.tf_mounting_link_frame_name_ = p.as_string(); + RCLCPP_INFO(logger_, "New tf.mounting_link.frame_name: '%s'", tf_publisher_.tf_mounting_link_frame_name_.c_str()); + }; + registered_param_callbacks_[tf_mounting_frame_name_descriptor_.name] = param_subscriber_->add_parameter_callback( + tf_mounting_frame_name_descriptor_.name, tf_mounting_link_frame_name_cb); + + auto tf_optical_link_frame_name_cb = [this](const rclcpp::Parameter& p) { + tf_publisher_.tf_optical_link_frame_name_ = p.as_string(); + RCLCPP_INFO(logger_, "New tf.optical_link.frame_name: '%s'", tf_publisher_.tf_optical_link_frame_name_.c_str()); + }; + registered_param_callbacks_[tf_optical_frame_name_descriptor_.name] = + param_subscriber_->add_parameter_callback(tf_optical_frame_name_descriptor_.name, tf_optical_link_frame_name_cb); + + auto tf_optical_link_publish_transform_cb = [this](const rclcpp::Parameter& p) { + tf_publisher_.tf_publish_mounting_to_optical_ = p.as_bool(); + RCLCPP_INFO(logger_, "New tf.optical_link.publish_transform: %s", + tf_publisher_.tf_publish_mounting_to_optical_ ? "true" : "false"); + }; + registered_param_callbacks_[tf_publish_mounting_to_optical_descriptor_.name] = + param_subscriber_->add_parameter_callback(tf_publish_mounting_to_optical_descriptor_.name, + tf_optical_link_publish_transform_cb); +} + +} // namespace ifm3d_ros2 \ No newline at end of file diff --git a/src/lib/services.cpp b/src/lib/services.cpp new file mode 100644 index 0000000..9567daa --- /dev/null +++ b/src/lib/services.cpp @@ -0,0 +1,292 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright (C) 2024 ifm electronic, gmbh + */ +#include +#include +#include + +#include "ifm3d_ros2/services.hpp" + +using json = ifm3d::json; + +namespace ifm3d_ros2 +{ +BaseServices::BaseServices(rclcpp::Logger logger, rclcpp_lifecycle::LifecycleNode::SharedPtr node_ptr, + ifm3d::O3R::Ptr cam, ifm3d::PortInfo port_info, std::shared_ptr ifm3d_mutex) + : logger_(logger), node_ptr_(node_ptr), ifm3d_mutex_(ifm3d_mutex), cam_(cam), port_info_(port_info) +{ + RCLCPP_INFO(logger_, "BaseServices constructor called."); + + // + // Set up our service servers + // + this->dump_srv_ = this->node_ptr_->create_service( + "~/Dump", std::bind(&ifm3d_ros2::BaseServices::Dump, this, std::placeholders::_1, std::placeholders::_2, + std::placeholders::_3)); + + this->config_srv_ = this->node_ptr_->create_service( + "~/Config", std::bind(&ifm3d_ros2::BaseServices::Config, this, std::placeholders::_1, std::placeholders::_2, + std::placeholders::_3)); + + this->soft_off_srv_ = this->node_ptr_->create_service( + "~/Softoff", std::bind(&ifm3d_ros2::BaseServices::Softoff, this, std::placeholders::_1, std::placeholders::_2, + std::placeholders::_3)); + + this->soft_on_srv_ = this->node_ptr_->create_service( + "~/Softon", std::bind(&ifm3d_ros2::BaseServices::Softon, this, std::placeholders::_1, std::placeholders::_2, + std::placeholders::_3)); + + this->get_diag_srv_ = this->node_ptr_->create_service( + "~/GetDiag", std::bind(&ifm3d_ros2::BaseServices::GetDiag, this, std::placeholders::_1, std::placeholders::_2, + std::placeholders::_3)); + + RCLCPP_INFO(logger_, "Services created;"); +} + +void BaseServices::Dump(std::shared_ptr request_header, DumpRequest req, DumpResponse resp) +{ + (void)request_header; + (void)req; + RCLCPP_INFO(this->logger_, "Handling dump request..."); + + if (this->node_ptr_->get_current_state().id() != lifecycle_msgs::msg::State::PRIMARY_STATE_ACTIVE) + { + resp->status = -1; + // XXX: may want to change this logic. For now, I do it so I know + // the ifm3d data structures are not null pointers + RCLCPP_WARN(this->logger_, "Can only make a service request when node is ACTIVE"); + return; + } + + { + std::lock_guard lock(*this->ifm3d_mutex_); + resp->status = 0; + + try + { + json j = this->cam_->ToJSON(); + resp->config = j.dump(); + } + catch (const ifm3d::Error& ex) + { + resp->status = ex.code(); + RCLCPP_WARN(this->logger_, "%s", ex.what()); + } + catch (const std::exception& std_ex) + { + resp->status = -1; + RCLCPP_WARN(this->logger_, "%s", std_ex.what()); + } + catch (...) + { + resp->status = -2; + } + + if (resp->status != 0) + { + RCLCPP_WARN(this->logger_, "Dump: %d", resp->status); + } + } + + RCLCPP_INFO(this->logger_, "Dump request done."); +} + +void BaseServices::Config(const std::shared_ptr /*unused*/, ConfigRequest req, ConfigResponse resp) +{ + RCLCPP_INFO(this->logger_, "Handling config request..."); + + if (this->node_ptr_->get_current_state().id() != lifecycle_msgs::msg::State::PRIMARY_STATE_ACTIVE) + { + resp->status = -1; + // XXX: may want to change this logic. For now, I do it so I know + // the ifm3d data structures are not null pointers + RCLCPP_WARN(this->logger_, "Can only make a service request when node is ACTIVE"); + return; + } + + { + std::lock_guard lock(*this->ifm3d_mutex_); + resp->status = 0; + resp->msg = "OK"; + + try + { + this->cam_->FromJSON(json::parse(req->json)); // HERE + } + catch (const ifm3d::Error& ex) + { + resp->status = ex.code(); + resp->msg = ex.what(); + } + catch (const std::exception& std_ex) + { + resp->status = -1; + resp->msg = std_ex.what(); + } + catch (...) + { + resp->status = -2; + resp->msg = "Unknown error in `Config'"; + } + + if (resp->status != 0) + { + RCLCPP_WARN(this->logger_, "Config: %d - %s", resp->status, resp->msg.c_str()); + } + } + + RCLCPP_INFO(this->logger_, "Config request done."); +} + +void BaseServices::Softoff(const std::shared_ptr, SoftoffRequest, SoftoffResponse resp) +{ + RCLCPP_INFO(this->logger_, "Handling SoftOff request..."); + + if (this->node_ptr_->get_current_state().id() != lifecycle_msgs::msg::State::PRIMARY_STATE_ACTIVE) + { + resp->status = -1; + RCLCPP_WARN(this->logger_, "Can only make a service request when node is ACTIVE"); + return; + } + + { + std::lock_guard lock(*this->ifm3d_mutex_); + resp->status = 0; + auto port_type = this->port_info_.type; + try + { + if ((port_type == "2D") | (port_type == "3D")) + { + this->cam_->FromJSONStr(R"({"ports":{")" + this->port_info_.port + R"(":{"state":"CONF"}}})"); + RCLCPP_INFO(this->logger_, "SoftOff request successful."); + } + else if (port_type == "app") + { + this->cam_->FromJSONStr(R"({"applications":{"instances":{")" + this->port_info_.port + + R"(":{"state":"CONF"}}}})"); + RCLCPP_INFO(this->logger_, "SoftOff request successful."); + } + else + { + RCLCPP_WARN(this->logger_, "Unknown port type: %s", port_type.c_str()); + } + } + catch (const ifm3d::Error& ex) + { + resp->status = ex.code(); + RCLCPP_WARN(this->logger_, "Caught ifm3d exception: %s", ex.what()); + } + catch (const std::exception& std_ex) + { + resp->status = -1; + RCLCPP_WARN(this->logger_, "Caught standard exception: %s", std_ex.what()); + } + catch (...) + { + resp->status = -2; + RCLCPP_WARN(this->logger_, "Caught unknown exception"); + } + + if (resp->status != 0) + { + RCLCPP_WARN(this->logger_, "SoftOff: %d", resp->status); + } + } + + RCLCPP_INFO(this->logger_, "SoftOff request done."); +} + +void BaseServices::Softon(const std::shared_ptr /*unused*/, SoftonRequest /*unused*/, + SoftonResponse resp) +{ + RCLCPP_INFO(this->logger_, "Handling SoftOn request..."); + + if (this->node_ptr_->get_current_state().id() != lifecycle_msgs::msg::State::PRIMARY_STATE_ACTIVE) + { + resp->status = -1; + RCLCPP_WARN(this->logger_, "Can only make a service request when node is ACTIVE"); + return; + } + + { + std::lock_guard lock(*this->ifm3d_mutex_); + resp->status = 0; + auto port_type = this->port_info_.type; + try + { + if ((port_type == "2D") | (port_type == "3D")) + { + this->cam_->FromJSONStr(R"({"ports":{")" + this->port_info_.port + R"(":{"state":"RUN"}}})"); + RCLCPP_INFO(this->logger_, "SoftOff request successful."); + } + else if (port_type == "app") + { + this->cam_->FromJSONStr(R"({"applications":{"instances":{")" + this->port_info_.port + + R"(":{"state":"RUN"}}}})"); + RCLCPP_INFO(this->logger_, "SoftOff request successful."); + } + else + { + RCLCPP_WARN(this->logger_, "Unknown port type: %s", port_type.c_str()); + } + } + catch (const ifm3d::Error& ex) + { + resp->status = ex.code(); + RCLCPP_WARN(this->logger_, "Caught ifm3d exception: %s", ex.what()); + } + catch (const std::exception& std_ex) + { + resp->status = -1; + RCLCPP_WARN(this->logger_, "Caught standard exception: %s", std_ex.what()); + } + catch (...) + { + resp->status = -2; + RCLCPP_WARN(this->logger_, "Caught unknown exception"); + } + + if (resp->status != 0) + { + RCLCPP_WARN(this->logger_, "SoftOff: %d", resp->status); + } + } + + RCLCPP_INFO(this->logger_, "SoftOff request done."); +} + +void BaseServices::GetDiag(std::shared_ptr request_header, GetDiagRequest req, GetDiagResponse resp) +{ + (void)request_header; + RCLCPP_INFO(this->logger_, "Handling GetDiag request..."); + // TODO: shouldn't this be handled by ROS natively? + if (this->node_ptr_->get_current_state().id() != lifecycle_msgs::msg::State::PRIMARY_STATE_ACTIVE) + { + resp->status = -1; + RCLCPP_WARN(this->logger_, "Can only make a service request when node is ACTIVE"); + return; + } + try + { + json filter = json::parse(req->filter); + RCLCPP_INFO(this->logger_, "Filter: %s", filter.dump().c_str()); + json diagnostics = this->cam_->GetDiagnosticFiltered(filter); + RCLCPP_INFO(this->logger_, "Filtered diagnostics: %s", diagnostics.dump().c_str()); + resp->msg = diagnostics.dump(); + resp->status = 0; + } + catch (const ifm3d::Error& ex) + { + resp->status = ex.code(); + RCLCPP_INFO(this->logger_, "ifm3d error while trying to get the diagnostic: %s", ex.what()); + } + catch (...) + { + resp->status = -2; + RCLCPP_INFO(this->logger_, "Unknown error while trying to get the diagnostic"); + } + RCLCPP_INFO(this->logger_, "GetDiagFiltered request done."); +} + +} // namespace ifm3d_ros2 diff --git a/src/lib/tof_module.cpp b/src/lib/tof_module.cpp new file mode 100644 index 0000000..2af21ef --- /dev/null +++ b/src/lib/tof_module.cpp @@ -0,0 +1,448 @@ +// -*- c++ -*- +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright (C) 2024 ifm electronic, gmbh + */ + +#include +#include +#include +#include + +#include +#include +#include + +namespace ifm3d_ros2 +{ +TofModule::TofModule(rclcpp::Logger logger, rclcpp_lifecycle::LifecycleNode::SharedPtr node_ptr, + ifm3d::O3R::Ptr o3r_ptr, std::string port, uint32_t width, uint32_t height) + : FunctionModule(logger), node_ptr_(node_ptr), tf_publisher_(node_ptr, o3r_ptr, port), width_(width), height_(height) +{ + RCLCPP_INFO(logger_, "TofModule contructor called."); + + RCLCPP_INFO(this->logger_, "Declaring parameters..."); + const std::string node_name(this->node_ptr_->get_name()); + const std::vector default_buffer_id_list{ + "CONFIDENCE_IMAGE", // + "EXTRINSIC_CALIB", // + "NORM_AMPLITUDE_IMAGE", // + "RADIAL_DISTANCE_IMAGE", // + "TOF_INFO", + "XYZ", // + }; + + buffer_id_list_descriptor_.name = "buffer_id_list"; + buffer_id_list_descriptor_.type = rcl_interfaces::msg::ParameterType::PARAMETER_STRING_ARRAY; + buffer_id_list_descriptor_.description = "List of buffer_id strings denoting the wanted buffers."; + this->node_ptr_->declare_parameter(buffer_id_list_descriptor_.name, default_buffer_id_list, + buffer_id_list_descriptor_); + + tf_base_frame_name_descriptor_.name = "tf.base_frame_name"; + tf_base_frame_name_descriptor_.type = rcl_interfaces::msg::ParameterType::PARAMETER_STRING; + tf_base_frame_name_descriptor_.description = "Name for the ifm base frame, defaults to ifm_base_link."; + this->node_ptr_->declare_parameter(tf_base_frame_name_descriptor_.name, "ifm_base_link", + tf_base_frame_name_descriptor_); + + tf_mounting_frame_name_descriptor_.name = "tf.mounting_frame_name"; + tf_mounting_frame_name_descriptor_.type = rcl_interfaces::msg::ParameterType::PARAMETER_STRING; + tf_mounting_frame_name_descriptor_.description = + "Name for the mounting point frame, defaults to _mounting_link."; + this->node_ptr_->declare_parameter(tf_mounting_frame_name_descriptor_.name, node_name + "_mounting_link", + tf_mounting_frame_name_descriptor_); + + tf_optical_frame_name_descriptor_.name = "tf.optical_frame_name"; + tf_optical_frame_name_descriptor_.type = rcl_interfaces::msg::ParameterType::PARAMETER_STRING; + tf_optical_frame_name_descriptor_.description = + "Name for the point optical frame, defaults to _optical_link."; + this->node_ptr_->declare_parameter(tf_optical_frame_name_descriptor_.name, node_name + "_optical_link", + tf_optical_frame_name_descriptor_); + + tf_publish_base_to_mounting_descriptor_.name = "tf.publish_base_to_mounting"; + tf_publish_base_to_mounting_descriptor_.type = rcl_interfaces::msg::ParameterType::PARAMETER_BOOL; + tf_publish_base_to_mounting_descriptor_.description = + "Whether the transform from the ifm base link to the mounting point should be published."; + this->node_ptr_->declare_parameter(tf_publish_base_to_mounting_descriptor_.name, true, + tf_publish_base_to_mounting_descriptor_); + + tf_publish_mounting_to_optical_descriptor_.name = "tf.publish_mounting_to_optical"; + tf_publish_mounting_to_optical_descriptor_.type = rcl_interfaces::msg::ParameterType::PARAMETER_BOOL; + tf_publish_mounting_to_optical_descriptor_.description = + "Whether the transform from the mounting point to the optical frame should be published."; + this->node_ptr_->declare_parameter(tf_publish_mounting_to_optical_descriptor_.name, true, + tf_publish_mounting_to_optical_descriptor_); + RCLCPP_INFO(this->logger_, "After the parameters declaration"); + + this->first_ = true; +} + +void TofModule::handle_frame(ifm3d::Frame::Ptr frame) +{ + RCLCPP_DEBUG(logger_, "Handle TOF frame"); + + using namespace buffer_id_utils; + + RCLCPP_INFO_ONCE(logger_, "Receiving Frames. Processing buffer for [%s]...", + buffer_id_utils::vector_to_string(this->buffer_id_list_).c_str()); + RCLCPP_DEBUG(logger_, "Received new Frame."); + + rclcpp::Time frame_ts = ifm3d_ros2::ifm3d_to_ros_time(frame->TimeStamps()[0]); + RCLCPP_DEBUG(logger_, "Frame timestamp: %f", frame_ts.seconds()); + + auto cloud_header = std_msgs::msg::Header(); + cloud_header.frame_id = tf_publisher_.tf_base_link_frame_name_; + cloud_header.stamp = frame_ts; + + auto optical_header = std_msgs::msg::Header(); + optical_header.frame_id = tf_publisher_.tf_optical_link_frame_name_; + optical_header.stamp = frame_ts; + + for (const ifm3d::buffer_id& id : this->buffer_id_list_) + { + // Helper for logging + auto& clk = *this->node_ptr_->get_clock(); + std::string id_string; + if (!buffer_id_utils::convert(id, id_string)){ + RCLCPP_ERROR(logger_, "Cannot convert the buffer id to a string"); + } + + const buffer_id_utils::message_type message_type = buffer_id_utils::message_type_map[id]; + + if (!frame->HasBuffer(id)) + { + RCLCPP_WARN_THROTTLE(logger_, clk, 5000, + "Frame does not contain buffer %s. Is the correct camera head connected?", + id_string.c_str()); + } + + switch (message_type) + { + case buffer_id_utils::message_type::raw_image: { + auto buffer = frame->GetBuffer(id); + ImageMsg raw_image_msg = ifm3d_ros2::ifm3d_to_ros_image(frame->GetBuffer(id), optical_header, logger_); + image_publishers_[id]->publish(raw_image_msg); + } + break; + + case buffer_id_utils::message_type::pointcloud: { + auto buffer = frame->GetBuffer(id); + PCLMsg pointcloud_msg = ifm3d_ros2::ifm3d_to_ros_cloud(frame->GetBuffer(id), cloud_header, logger_); + pcl_publishers_[id]->publish(pointcloud_msg); + } + break; + case buffer_id_utils::message_type::extrinsics: { + auto buffer = frame->GetBuffer(id); + ExtrinsicsMsg extrinsics_msg = ifm3d_ros2::ifm3d_to_extrinsics(buffer, optical_header, logger_); + extrinsics_publishers_[id]->publish(extrinsics_msg); + } + break; + case buffer_id_utils::intrinsics: { + auto buffer = frame->GetBuffer(id); + IntrinsicsMsg intrinsics_msg = ifm3d_ros2::ifm3d_to_intrinsics(buffer, optical_header, logger_); + intrinsics_publishers_[id]->publish(intrinsics_msg); + } + break; + case buffer_id_utils::message_type::inverse_intrinsics: { + auto buffer = frame->GetBuffer(id); + InverseIntrinsicsMsg inverse_intrinsics_msg = + ifm3d_ros2::ifm3d_to_inverse_intrinsics(buffer, optical_header, logger_); + inverse_intrinsics_publishers_[id]->publish(inverse_intrinsics_msg); + } + break; + case buffer_id_utils::message_type::tof_info: { + auto buffer = frame->GetBuffer(id); + TOFInfoMsg tof_info_msg = ifm3d_ros2::ifm3d_to_tof_info(buffer, optical_header, logger_); + tof_info_publishers_[id]->publish(tof_info_msg); + + if (tf_publisher_.tf_publish_mounting_to_optical_ || tf_publisher_.tf_publish_base_to_mounting_) + { + geometry_msgs::msg::TransformStamped tf_base_to_optical; + if (ifm3d_ros2::ifm3d_tof_info_to_optical_mount_link(buffer, tf_publisher_.tf_base_link_frame_name_, + tf_publisher_.tf_optical_link_frame_name_, logger_, + tf_base_to_optical)) + { + tf_publisher_.update_and_publish_tf_if_changed(tf_base_to_optical); + } + else + { + RCLCPP_ERROR(logger_, "Failed to derive transform from tof_info, retrying..."); + } + } + + CameraInfoMsg camera_info_msg; + if (ifm3d_ros2::ifm3d_tof_info_to_camera_info(buffer, optical_header, this->height_, this->width_, logger_, + camera_info_msg)) + { + RCLCPP_INFO_ONCE(logger_, "Parsing CameraInfo successfull."); + camera_info_publishers_[id]->publish(camera_info_msg); + } + else + { + RCLCPP_ERROR(logger_, "Failed to retrieve camera_info from rgb_info buffer."); + }; + } + break; + default: + RCLCPP_ERROR_THROTTLE(logger_, clk, 5000, "Unknown message type for buffer_id %s. Can not publish.", + id_string.c_str()); + break; + } + } + RCLCPP_DEBUG(logger_, "Finished publishing tof messages."); +} + +rclcpp_lifecycle::LifecycleNode::CallbackReturn TofModule::on_configure(const rclcpp_lifecycle::State& previous_state) +{ + RCLCPP_INFO(logger_, "TofModule: on_configure called"); + (void)previous_state; + + // + // parse params and initialize instance vars + // + RCLCPP_INFO(this->logger_, "Parsing parameters..."); + parse_params(); + RCLCPP_INFO(this->logger_, "Parameters parsed."); + + // Add parameter subscriber, if none is active + if (!param_subscriber_) + { + RCLCPP_INFO(logger_, "Adding callbacks to handle parameter changes at runtime..."); + set_parameter_event_callbacks(); + RCLCPP_INFO(this->logger_, "Callbacks set."); + } + + using namespace buffer_id_utils; + + this->image_publishers_.clear(); + this->pcl_publishers_.clear(); + this->extrinsics_publishers_.clear(); + this->camera_info_publishers_.clear(); + this->tof_info_publishers_.clear(); + this->intrinsics_publishers_.clear(); + this->inverse_intrinsics_publishers_.clear(); + + // Remove buffer_ids unfit for the given data type + this->buffer_id_list_ = + buffer_id_utils::buffer_ids_for_data_stream_type(this->buffer_id_list_, ifm3d_ros2::buffer_id_utils::data_stream_type::tof_3d); + RCLCPP_INFO(logger_, "After removing buffer_ids unfit for the given data stream type, the final list is: [%s].", + buffer_id_utils::vector_to_string(this->buffer_id_list_).c_str()); + + std::vector ids_to_remove{}; + // Create correctly typed publishers for all given buffer_ids + for (const ifm3d::buffer_id& id : this->buffer_id_list_) + { + // Create Publishers in node namespace to make multi-camera setups easier + const std::string topic_name = "~/" + buffer_id_utils::topic_name_map[id]; + const auto qos = ifm3d_ros2::LowLatencyQoS(); + const buffer_id_utils::message_type message_type = buffer_id_utils::message_type_map[id]; + + switch (message_type) + { + case buffer_id_utils::message_type::raw_image: + image_publishers_[id] = node_ptr_->create_publisher(topic_name, qos); + break; + case buffer_id_utils::message_type::pointcloud: + pcl_publishers_[id] = node_ptr_->create_publisher(topic_name, qos); + break; + case buffer_id_utils::message_type::extrinsics: + extrinsics_publishers_[id] = node_ptr_->create_publisher(topic_name, qos); + break; + case buffer_id_utils::message_type::intrinsics: + intrinsics_publishers_[id] = node_ptr_->create_publisher(topic_name, qos); + break; + case buffer_id_utils::message_type::tof_info: + tof_info_publishers_[id] = node_ptr_->create_publisher(topic_name, qos); + camera_info_publishers_[id] = node_ptr_->create_publisher("~/camera_info", qos); + break; + case buffer_id_utils::message_type::inverse_intrinsics: + inverse_intrinsics_publishers_[id] = node_ptr_->create_publisher(topic_name, qos); + break; + default: + std::string id_string; + buffer_id_utils::convert(id, id_string); + RCLCPP_ERROR(logger_, "Unknown message type for buffer_id %s. Will be removed from list...", id_string.c_str()); + ids_to_remove.push_back(id); + break; + } + } + RCLCPP_INFO(this->logger_, "Created publishers for all buffer_ids."); + + return rclcpp_lifecycle::LifecycleNode::CallbackReturn::SUCCESS; +} + +rclcpp_lifecycle::LifecycleNode::CallbackReturn TofModule::on_cleanup(const rclcpp_lifecycle::State& previous_state) +{ + RCLCPP_INFO(logger_, "TofModule: on_cleanup called"); + (void)previous_state; + + return rclcpp_lifecycle::LifecycleNode::CallbackReturn::SUCCESS; +} + +rclcpp_lifecycle::LifecycleNode::CallbackReturn TofModule::on_shutdown(const rclcpp_lifecycle::State& previous_state) +{ + RCLCPP_INFO(logger_, "TofModule: on_shutdown called"); + (void)previous_state; + return rclcpp_lifecycle::LifecycleNode::CallbackReturn::SUCCESS; +} + +rclcpp_lifecycle::LifecycleNode::CallbackReturn TofModule::on_activate(const rclcpp_lifecycle::State& previous_state) +{ + RCLCPP_INFO(logger_, "TofModule: on_activate called"); + (void)previous_state; + + for (auto& [id, publisher] : this->image_publishers_) + { + publisher->on_activate(); + } + for (auto& [id, publisher] : this->pcl_publishers_) + { + publisher->on_activate(); + } + for (auto& [id, publisher] : this->extrinsics_publishers_) + { + publisher->on_activate(); + } + for (auto& [id, publisher] : this->camera_info_publishers_) + { + publisher->on_activate(); + } + for (auto& [id, publisher] : this->tof_info_publishers_) + { + publisher->on_activate(); + } + for (auto& [id, publisher] : this->intrinsics_publishers_) + { + publisher->on_activate(); + } + for (auto& [id, publisher] : this->inverse_intrinsics_publishers_) + { + publisher->on_activate(); + } + + return rclcpp_lifecycle::LifecycleNode::CallbackReturn::SUCCESS; +} + +rclcpp_lifecycle::LifecycleNode::CallbackReturn TofModule::on_deactivate(const rclcpp_lifecycle::State& previous_state) +{ + RCLCPP_INFO(logger_, "TofModule: on_deactivate called"); + (void)previous_state; + + for (auto& [id, publisher] : this->image_publishers_) + { + publisher->on_deactivate(); + } + for (auto& [id, publisher] : this->pcl_publishers_) + { + publisher->on_deactivate(); + } + for (auto& [id, publisher] : this->extrinsics_publishers_) + { + publisher->on_deactivate(); + } + for (auto& [id, publisher] : this->camera_info_publishers_) + { + publisher->on_activate(); + } + for (auto& [id, publisher] : this->tof_info_publishers_) + { + publisher->on_deactivate(); + } + for (auto& [id, publisher] : this->intrinsics_publishers_) + { + publisher->on_deactivate(); + } + for (auto& [id, publisher] : this->inverse_intrinsics_publishers_) + { + publisher->on_deactivate(); + } + + return rclcpp_lifecycle::LifecycleNode::CallbackReturn::SUCCESS; +} + +rclcpp_lifecycle::LifecycleNode::CallbackReturn TofModule::on_error(const rclcpp_lifecycle::State& previous_state) +{ + RCLCPP_INFO(logger_, "TofModule: on_error called"); + (void)previous_state; + + return rclcpp_lifecycle::LifecycleNode::CallbackReturn::SUCCESS; +} + +void TofModule::parse_params() +{ + std::vector buffer_id_strings; + this->node_ptr_->get_parameter(buffer_id_list_descriptor_.name, buffer_id_strings); + RCLCPP_INFO(this->logger_, "Reading %ld buffer_ids: [%s]", buffer_id_strings.size(), + buffer_id_utils::vector_to_string(buffer_id_strings).c_str()); + // Populate buffer_id_list_ from read strings + this->buffer_id_list_.clear(); + for (const std::string& string : buffer_id_strings) + { + ifm3d::buffer_id found_id; + if (buffer_id_utils::convert(string, found_id)) + { + this->buffer_id_list_.push_back(found_id); + } + else + { + RCLCPP_WARN(this->logger_, "Ignoring unknown buffer_id %s", string.c_str()); + } + } + RCLCPP_INFO(this->logger_, "Parsed %ld buffer_ids: %s", this->buffer_id_list_.size(), + buffer_id_utils::vector_to_string(this->buffer_id_list_).c_str()); + + this->node_ptr_->get_parameter(tf_base_frame_name_descriptor_.name, tf_publisher_.tf_base_link_frame_name_); + RCLCPP_INFO(this->logger_, "tf.base_link.frame_name: %s", tf_publisher_.tf_base_link_frame_name_.c_str()); + + this->node_ptr_->get_parameter(tf_mounting_frame_name_descriptor_.name, tf_publisher_.tf_mounting_link_frame_name_); + RCLCPP_INFO(this->logger_, "tf.mounting_link.frame_name: %s", tf_publisher_.tf_mounting_link_frame_name_.c_str()); + + this->node_ptr_->get_parameter(tf_optical_frame_name_descriptor_.name, tf_publisher_.tf_optical_link_frame_name_); + RCLCPP_INFO(this->logger_, "tf.optical_link.frame_name: %s", tf_publisher_.tf_optical_link_frame_name_.c_str()); + + this->node_ptr_->get_parameter(tf_publish_base_to_mounting_descriptor_.name, + tf_publisher_.tf_publish_base_to_mounting_); + + this->node_ptr_->get_parameter(tf_publish_mounting_to_optical_descriptor_.name, + tf_publisher_.tf_publish_mounting_to_optical_); + + RCLCPP_INFO(this->logger_, "tf.optical_link.publish_transform: %s", + tf_publisher_.tf_publish_mounting_to_optical_ ? "true" : "false"); +} + +void TofModule::set_parameter_event_callbacks() +{ + // Create a parameter subscriber that can be used to monitor parameter changes + param_subscriber_ = std::make_shared(this->node_ptr_); + + auto buffer_id_list_cb = [this](const rclcpp::Parameter& p) { + RCLCPP_WARN(logger_, "This new buffer_id_list will be used after CONFIGURE transition was called: %s", + buffer_id_utils::vector_to_string(p.as_string_array()).c_str()); + }; + registered_param_callbacks_[buffer_id_list_descriptor_.name] = + param_subscriber_->add_parameter_callback(buffer_id_list_descriptor_.name, buffer_id_list_cb); + + auto tf_mounting_link_frame_name_cb = [this](const rclcpp::Parameter& p) { + tf_publisher_.tf_mounting_link_frame_name_ = p.as_string(); + RCLCPP_INFO(logger_, "New tf.mounting_link.frame_name: '%s'", tf_publisher_.tf_mounting_link_frame_name_.c_str()); + }; + registered_param_callbacks_[tf_mounting_frame_name_descriptor_.name] = param_subscriber_->add_parameter_callback( + tf_mounting_frame_name_descriptor_.name, tf_mounting_link_frame_name_cb); + + auto tf_optical_link_frame_name_cb = [this](const rclcpp::Parameter& p) { + tf_publisher_.tf_optical_link_frame_name_ = p.as_string(); + RCLCPP_INFO(logger_, "New tf.optical_link.frame_name: '%s'", tf_publisher_.tf_optical_link_frame_name_.c_str()); + }; + registered_param_callbacks_[tf_optical_frame_name_descriptor_.name] = + param_subscriber_->add_parameter_callback(tf_optical_frame_name_descriptor_.name, tf_optical_link_frame_name_cb); + + auto tf_optical_link_publish_transform_cb = [this](const rclcpp::Parameter& p) { + tf_publisher_.tf_publish_mounting_to_optical_ = p.as_bool(); + RCLCPP_INFO(logger_, "New tf.optical_link.publish_transform: %s", + tf_publisher_.tf_publish_mounting_to_optical_ ? "true" : "false"); + }; + registered_param_callbacks_[tf_publish_mounting_to_optical_descriptor_.name] = + param_subscriber_->add_parameter_callback(tf_publish_mounting_to_optical_descriptor_.name, + tf_optical_link_publish_transform_cb); +} + +} // namespace ifm3d_ros2 \ No newline at end of file diff --git a/srv/GetDiag.srv b/srv/GetDiag.srv new file mode 100644 index 0000000..63c74cf --- /dev/null +++ b/srv/GetDiag.srv @@ -0,0 +1,4 @@ +string filter +--- +int32 status +string msg