From 5392d6177cdf0bbcbca75ffeaa30bf6c5914aef4 Mon Sep 17 00:00:00 2001 From: Michael Ripperger Date: Thu, 18 Sep 2025 18:36:16 -0500 Subject: [PATCH 01/16] Added formatting files --- .clang-format | 67 ++++++++++ .clang-tidy | 80 +++++++++++ .cmake-format | 334 ++++++++++++++++++++++++++++++++++++++++++++++ .run-clang-format | 2 + .run-cmake-format | 2 + 5 files changed, 485 insertions(+) create mode 100644 .clang-format create mode 100644 .clang-tidy create mode 100644 .cmake-format create mode 100755 .run-clang-format create mode 100755 .run-cmake-format diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..792fd96 --- /dev/null +++ b/.clang-format @@ -0,0 +1,67 @@ +--- +BasedOnStyle: Google +AccessModifierOffset: -2 +AlignEscapedNewlinesLeft: false +AlignTrailingComments: true +AlignAfterOpenBracket: Align +AllowAllParametersOfDeclarationOnNextLine: false +AllowShortFunctionsOnASingleLine: true +AllowShortIfStatementsOnASingleLine: false +AllowShortLoopsOnASingleLine: false +AlwaysBreakBeforeMultilineStrings: false +AlwaysBreakTemplateDeclarations: true +BinPackArguments: false +BinPackParameters: false +BreakBeforeBinaryOperators: false +BreakBeforeTernaryOperators: false +BreakConstructorInitializersBeforeComma: true +ColumnLimit: 120 +ConstructorInitializerAllOnOneLineOrOnePerLine: true +ConstructorInitializerIndentWidth: 2 +ContinuationIndentWidth: 4 +Cpp11BracedListStyle: false +DerivePointerBinding: false +ExperimentalAutoDetectBinPacking: false +IndentCaseLabels: true +IndentFunctionDeclarationAfterType: false +IndentWidth: 2 +MaxEmptyLinesToKeep: 1 +NamespaceIndentation: None +ObjCSpaceBeforeProtocolList: true +PenaltyBreakBeforeFirstCallParameter: 19 +PenaltyBreakComment: 60 +PenaltyBreakFirstLessLess: 1000 +PenaltyBreakString: 1 +PenaltyExcessCharacter: 1000 +PenaltyReturnTypeOnItsOwnLine: 90 +PointerBindsToType: true +SortIncludes: false +SpaceAfterControlStatementKeyword: true +SpaceAfterCStyleCast: false +SpaceBeforeAssignmentOperators: true +SpaceInEmptyParentheses: false +SpacesBeforeTrailingComments: 2 +SpacesInAngles: false +SpacesInCStyleCastParentheses: false +SpacesInParentheses: false +Standard: Auto +TabWidth: 2 +UseTab: Never + +# Configure each individual brace in BraceWrapping +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' +} +... diff --git a/.clang-tidy b/.clang-tidy new file mode 100644 index 0000000..33c723d --- /dev/null +++ b/.clang-tidy @@ -0,0 +1,80 @@ +--- +Checks: > + -*, + clang-diagnostic-*, + -clang-diagnostic-unknown-warning-option, + clang-analyzer-*, + -clang-analyzer-cplusplus*, + bugprone-*, + -bugprone-easily-swappable-parameters, + cppcoreguidelines-*, + -cppcoreguidelines-macro-usage, + -cppcoreguidelines-pro-type-static-cast-downcast, + -cppcoreguidelines-pro-type-vararg, + -cppcoreguidelines-pro-type-union-access, + -cppcoreguidelines-pro-bounds-array-to-pointer-decay, + -cppcoreguidelines-pro-bounds-pointer-arithmetic, + -cppcoreguidelines-pro-bounds-constant-array-index, + -cppcoreguidelines-avoid-magic-numbers, + -cppcoreguidelines-non-private-member-variables-in-classes, + -cppcoreguidelines-avoid-non-const-global-variables, + misc-*, + -misc-non-private-member-variables-in-classes, + -misc-no-recursion, + modernize-*, + -modernize-use-trailing-return-type, + -modernize-use-nodiscard, + performance-*, + readability-*, + -readability-braces-around-statements, + -readability-named-parameter, + -readability-magic-numbers, + -readability-isolate-declaration, + -readability-identifier-length, + -readability-function-cognitive-complexity +WarningsAsErrors: > + -*, + clang-diagnostic-*, + -clang-diagnostic-unknown-warning-option, + clang-analyzer-*, + -clang-analyzer-cplusplus*, + bugprone-*, + -bugprone-easily-swappable-parameters, + cppcoreguidelines-*, + -cppcoreguidelines-macro-usage, + -cppcoreguidelines-pro-type-static-cast-downcast, + -cppcoreguidelines-pro-type-vararg, + -cppcoreguidelines-pro-type-union-access, + -cppcoreguidelines-pro-bounds-array-to-pointer-decay, + -cppcoreguidelines-pro-bounds-pointer-arithmetic, + -cppcoreguidelines-pro-bounds-constant-array-index, + -cppcoreguidelines-avoid-magic-numbers, + -cppcoreguidelines-non-private-member-variables-in-classes, + -cppcoreguidelines-avoid-non-const-global-variables, + misc-*, + -misc-non-private-member-variables-in-classes, + -misc-no-recursion, + modernize-*, + -modernize-use-trailing-return-type, + -modernize-use-nodiscard, + performance-*, + readability-*, + -readability-braces-around-statements, + -readability-named-parameter, + -readability-magic-numbers, + -readability-isolate-declaration, + -readability-identifier-length, + -readability-function-cognitive-complexity +HeaderFilterRegex: '.*' +AnalyzeTemporaryDtors: false +FormatStyle: none +CheckOptions: + - key: modernize-use-override.AllowOverrideAndFinal + value: '1' + - key: cppcoreguidelines-explicit-virtual-functions.AllowOverrideAndFinal + value: '1' + - key: cppcoreguidelines-special-member-functions.AllowSoleDefaultDtor + value: '1' +ExtraArgs: + - '-std=c++17' +... diff --git a/.cmake-format b/.cmake-format new file mode 100644 index 0000000..b162026 --- /dev/null +++ b/.cmake-format @@ -0,0 +1,334 @@ +#!/bin/python +# ---------------------------------- +# Options affecting listfile parsing +# ---------------------------------- +with section("parse"): + + # Specify structure for custom cmake functions + additional_commands = { + 'target_clang_tidy': { + 'pargs': {'nargs': 1}, + 'kwargs': { + 'ARGUMENTS': '*', + 'ENABLE': 1, + } + }, + 'target_include_what_you_use': { + 'pargs': {'nargs': 1}, + 'kwargs': { + 'ARGUMENTS': '*', + 'ENABLE': 1, + } + }, + 'include_what_you_use': { + 'pargs': {'nargs': 0}, + 'kwargs': { + 'ARGUMENTS': '*', + 'ENABLE': 1, + } + }, + 'target_cppcheck': { + 'pargs': {'nargs': 1}, + 'kwargs': { + 'ARGUMENTS': '*', + 'ENABLE': 1, + } + }, + 'cppcheck': { + 'pargs': {'nargs': 0}, + 'kwargs': { + 'ARGUMENTS': '*', + 'ENABLE': 1, + } + }, + 'configure_package': { + 'pargs': {'nargs': 0}, + 'kwargs': { + 'NAMESPACE': '?', + 'TARGETS': '*', + 'DEPENDENCIES': '*', + } + }, + 'add_gtest_discover_tests': { + 'pargs': {'nargs': 1}, + }, + 'add_run_tests_target': { + 'pargs': {'nargs': 0}, + 'kwargs': { + 'ENABLE': 1, + } + }, + 'add_run_benchmark_target': { + 'pargs': {'nargs': 0}, + 'kwargs': { + 'ENABLE': 1, + } + }, + 'target_cxx_version': { + 'pargs': {'nargs': 1}, + 'kwargs': { + 'INTERFACE': '+', + 'PUBLIC': '+', + 'PRIVATE': '+', + 'VERSION': 1, + } + }, + 'target_code_coverage': { + 'pargs': {'nargs': 1}, + 'kwargs': { + 'AUTO': '+', + 'ALL': '+', + 'EXTERNAL': '+', + 'PRIVATE': '+', + 'PUBLIC': '+', + 'INTERFACE': '+', + 'ENABLE': 1, + 'EXCLUDE': '*', + } + }, + 'add_code_coverage': { + 'pargs': {'nargs': 0}, + 'kwargs': { + 'ENABLE': 1, + } + }, + 'add_code_coverage_all_targets': { + 'pargs': {'nargs': 0}, + 'kwargs': { + 'ENABLE': 1, + 'EXCLUDE': '*', + } + }, + } + + # Override configurations per-command where available + override_spec = {} + + # Specify variable tags. + vartags = [] + + # Specify property tags. + proptags = [] + +# ----------------------------- +# Options affecting formatting. +# ----------------------------- +with section("format"): + + # Disable formatting entirely, making cmake-format a no-op + disable = False + + # How wide to allow formatted cmake files + line_width = 120 + + # How many spaces to tab for indent + tab_size = 2 + + # If true, lines are indented using tab characters (utf-8 0x09) instead of + # space characters (utf-8 0x20). In cases where the layout would + # require a fractional tab character, the behavior of the fractional + # indentation is governed by + use_tabchars = False + + # If is True, then the value of this variable indicates how + # fractional indentions are handled during whitespace replacement. If set to + # 'use-space', fractional indentation is left as spaces (utf-8 0x20). If set + # to `round-up` fractional indentation is replaced with a single tab character + # (utf-8 0x09) effectively shifting the column to the next tabstop + fractional_tab_policy = 'use-space' + + # If an argument group contains more than this many sub-groups (parg or kwarg + # groups) then force it to a vertical layout. + max_subgroups_hwrap = 2 + + # If a positional argument group contains more than this many arguments, then + # force it to a vertical layout. + max_pargs_hwrap = 3 + + # If a cmdline positional group consumes more than this many lines without + # nesting, then invalidate the layout (and nest) + max_rows_cmdline = 2 + + # If true, separate flow control names from their parentheses with a space + separate_ctrl_name_with_space = False + + # If true, separate function names from parentheses with a space + separate_fn_name_with_space = False + + # If a statement is wrapped to more than one line, than dangle the closing + # parenthesis on its own line. + dangle_parens = True + + # If the trailing parenthesis must be 'dangled' on its on line, then align it + # to this reference: `prefix`: the start of the statement, `prefix-indent`: + # the start of the statement, plus one indentation level, `child`: align to + # the column of the arguments + dangle_align = 'prefix' + + # If the statement spelling length (including space and parenthesis) is + # smaller than this amount, then force reject nested layouts. + min_prefix_chars = 4 + + # If the statement spelling length (including space and parenthesis) is larger + # than the tab width by more than this amount, then force reject un-nested + # layouts. + max_prefix_chars = 10 + + # If a candidate layout is wrapped horizontally but it exceeds this many + # lines, then reject the layout. + max_lines_hwrap = 2 + + # What style line endings to use in the output. + line_ending = 'unix' + + # Format command names consistently as 'lower' or 'upper' case + command_case = 'canonical' + + # Format keywords consistently as 'lower' or 'upper' case + keyword_case = 'unchanged' + + # A list of command names which should always be wrapped + always_wrap = [] + + # If true, the argument lists which are known to be sortable will be sorted + # lexicographicall + enable_sort = True + + # If true, the parsers may infer whether or not an argument list is sortable + # (without annotation). + autosort = False + + # By default, if cmake-format cannot successfully fit everything into the + # desired linewidth it will apply the last, most agressive attempt that it + # made. If this flag is True, however, cmake-format will print error, exit + # with non-zero status code, and write-out nothing + require_valid_layout = False + + # A dictionary mapping layout nodes to a list of wrap decisions. See the + # documentation for more information. + layout_passes = {} + +# ------------------------------------------------ +# Options affecting comment reflow and formatting. +# ------------------------------------------------ +with section("markup"): + + # What character to use for bulleted lists + bullet_char = '*' + + # What character to use as punctuation after numerals in an enumerated list + enum_char = '.' + + # If comment markup is enabled, don't reflow the first comment block in each + # listfile. Use this to preserve formatting of your copyright/license + # statements. + first_comment_is_literal = False + + # If comment markup is enabled, don't reflow any comment block which matches + # this (regex) pattern. Default is `None` (disabled). + literal_comment_pattern = None + + # Regular expression to match preformat fences in comments default= + # ``r'^\s*([`~]{3}[`~]*)(.*)$'`` + fence_pattern = '^\\s*([`~]{3}[`~]*)(.*)$' + + # Regular expression to match rulers in comments default= + # ``r'^\s*[^\w\s]{3}.*[^\w\s]{3}$'`` + ruler_pattern = '^\\s*[^\\w\\s]{3}.*[^\\w\\s]{3}$' + + # If a comment line matches starts with this pattern then it is explicitly a + # trailing comment for the preceeding argument. Default is '#<' + explicit_trailing_pattern = '#<' + + # If a comment line starts with at least this many consecutive hash + # characters, then don't lstrip() them off. This allows for lazy hash rulers + # where the first hash char is not separated by space + hashruler_min_length = 10 + + # If true, then insert a space between the first hash char and remaining hash + # chars in a hash ruler, and normalize its length to fill the column + canonicalize_hashrulers = True + + # enable comment markup parsing and reflow + enable_markup = True + +# ---------------------------- +# Options affecting the linter +# ---------------------------- +with section("lint"): + + # a list of lint codes to disable + disabled_codes = [] + + # regular expression pattern describing valid function names + function_pattern = '[0-9a-z_]+' + + # regular expression pattern describing valid macro names + macro_pattern = '[0-9A-Z_]+' + + # regular expression pattern describing valid names for variables with global + # (cache) scope + global_var_pattern = '[A-Z][0-9A-Z_]+' + + # regular expression pattern describing valid names for variables with global + # scope (but internal semantic) + internal_var_pattern = '_[A-Z][0-9A-Z_]+' + + # regular expression pattern describing valid names for variables with local + # scope + local_var_pattern = '[a-z][a-z0-9_]+' + + # regular expression pattern describing valid names for privatedirectory + # variables + private_var_pattern = '_[0-9a-z_]+' + + # regular expression pattern describing valid names for public directory + # variables + public_var_pattern = '[A-Z][0-9A-Z_]+' + + # regular expression pattern describing valid names for function/macro + # arguments and loop variables. + argument_var_pattern = '[a-z][a-z0-9_]+' + + # regular expression pattern describing valid names for keywords used in + # functions or macros + keyword_pattern = '[A-Z][0-9A-Z_]+' + + # In the heuristic for C0201, how many conditionals to match within a loop in + # before considering the loop a parser. + max_conditionals_custom_parser = 2 + + # Require at least this many newlines between statements + min_statement_spacing = 1 + + # Require no more than this many newlines between statements + max_statement_spacing = 2 + max_returns = 6 + max_branches = 12 + max_arguments = 5 + max_localvars = 15 + max_statements = 50 + +# ------------------------------- +# Options affecting file encoding +# ------------------------------- +with section("encode"): + + # If true, emit the unicode byte-order mark (BOM) at the start of the file + emit_byteorder_mark = False + + # Specify the encoding of the input file. Defaults to utf-8 + input_encoding = 'utf-8' + + # Specify the encoding of the output file. Defaults to utf-8. Note that cmake + # only claims to support utf-8 so be careful when using anything else + output_encoding = 'utf-8' + +# ------------------------------------- +# Miscellaneous configurations options. +# ------------------------------------- +with section("misc"): + + # A dictionary containing any per-command configuration overrides. Currently + # only `command_case` is supported. + per_command = {} diff --git a/.run-clang-format b/.run-clang-format new file mode 100755 index 0000000..15bd568 --- /dev/null +++ b/.run-clang-format @@ -0,0 +1,2 @@ +#!/bin/bash +find . -type f -regex '.*\.\(cpp\|hpp\|cc\|cxx\|h\|hxx\)' -exec clang-format-14 -style=file -i {} \; diff --git a/.run-cmake-format b/.run-cmake-format new file mode 100755 index 0000000..fe66f6d --- /dev/null +++ b/.run-cmake-format @@ -0,0 +1,2 @@ +#!/bin/bash +find . \( -name CMakeLists.txt -o -name \*.cmake \) -exec cmake-format -i {} \; From 205a1422565416516d9e3416d2904482f9f3f57e Mon Sep 17 00:00:00 2001 From: Michael Ripperger Date: Thu, 18 Sep 2025 18:37:06 -0500 Subject: [PATCH 02/16] Added message definitions --- msg/ToolPath.msg | 1 + msg/ToolPaths.msg | 1 + srv/PlanToolPath.srv | 6 ++++++ 3 files changed, 8 insertions(+) create mode 100644 msg/ToolPath.msg create mode 100644 msg/ToolPaths.msg create mode 100644 srv/PlanToolPath.srv diff --git a/msg/ToolPath.msg b/msg/ToolPath.msg new file mode 100644 index 0000000..3fbf980 --- /dev/null +++ b/msg/ToolPath.msg @@ -0,0 +1 @@ +geometry_msgs/PoseArray[] segments diff --git a/msg/ToolPaths.msg b/msg/ToolPaths.msg new file mode 100644 index 0000000..cfa7908 --- /dev/null +++ b/msg/ToolPaths.msg @@ -0,0 +1 @@ +noether_ros/ToolPath[] tool_paths diff --git a/srv/PlanToolPath.srv b/srv/PlanToolPath.srv new file mode 100644 index 0000000..a7777cb --- /dev/null +++ b/srv/PlanToolPath.srv @@ -0,0 +1,6 @@ +string config +string mesh_file +--- +bool success +string message +noether_ros/ToolPaths[] tool_paths From 9f1cda92bb0457404ea33fc8c3b45c9d31cd3c85 Mon Sep 17 00:00:00 2001 From: Michael Ripperger Date: Thu, 18 Sep 2025 18:38:23 -0500 Subject: [PATCH 03/16] Added message conversions library and tool path planning server --- CMakeLists.txt | 64 +++++++++++++++++++ dependencies.repos | 12 ++++ include/noether_ros/conversions.h | 19 ++++++ package.xml | 25 ++++++++ src/conversions.cpp | 103 ++++++++++++++++++++++++++++++ src/tool_path_planning_server.cpp | 65 +++++++++++++++++++ 6 files changed, 288 insertions(+) create mode 100644 CMakeLists.txt create mode 100644 dependencies.repos create mode 100644 include/noether_ros/conversions.h create mode 100644 package.xml create mode 100644 src/conversions.cpp create mode 100644 src/tool_path_planning_server.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..b867f37 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,64 @@ +cmake_minimum_required(VERSION 3.5) +project(noether_ros) + +# Default to C++14 +if(NOT CMAKE_CXX_STANDARD) + set(CMAKE_CXX_STANDARD 14) +endif() + +# find dependencies +find_package(ament_cmake REQUIRED) +find_package(rclcpp REQUIRED) +find_package(geometry_msgs REQUIRED) +find_package(tf2_eigen REQUIRED) +find_package(noether_tpp REQUIRED) +find_package(yaml-cpp REQUIRED) + +# Generate the messages +find_package(rosidl_default_generators REQUIRED) +rosidl_generate_interfaces( + ${PROJECT_NAME} + "msg/ToolPath.msg" + "msg/ToolPaths.msg" + "srv/PlanToolPath.srv" + DEPENDENCIES + builtin_interfaces + geometry_msgs +) +rosidl_get_typesupport_target(interfaces_target "${PROJECT_NAME}" "rosidl_typesupport_cpp") + +# Library +add_library(${PROJECT_NAME}_conversions SHARED src/conversions.cpp) +target_include_directories( + ${PROJECT_NAME}_conversions PUBLIC "$" + "$" +) +target_link_libraries(${PROJECT_NAME}_conversions noether::noether_tpp ${interfaces_target}) +ament_target_dependencies(${PROJECT_NAME}_conversions tf2_eigen) + +# Tool path planning server +add_executable(${PROJECT_NAME}_tool_path_planning_server src/tool_path_planning_server.cpp) +target_link_libraries(${PROJECT_NAME}_tool_path_planning_server ${PROJECT_NAME}_conversions yaml-cpp) +ament_target_dependencies(${PROJECT_NAME}_tool_path_planning_server rclcpp) + +# Install the headers +install(DIRECTORY include/ DESTINATION include/) + +# Install the library +install( + TARGETS ${PROJECT_NAME}_conversions + EXPORT ${PROJECT_NAME}-targets + DESTINATION lib +) +ament_export_targets(${PROJECT_NAME}-targets HAS_LIBRARY_TARGET) + +# Install the executable +install(TARGETS ${PROJECT_NAME}_tool_path_planning_server DESTINATION lib/${PROJECT_NAME}) + +ament_export_dependencies( + builtin_interfaces + geometry_msgs + rosidl_default_runtime + tf2_eigen +) +ament_package() diff --git a/dependencies.repos b/dependencies.repos new file mode 100644 index 0000000..3dc958b --- /dev/null +++ b/dependencies.repos @@ -0,0 +1,12 @@ +- git: + local-name: ros_industrial_cmake_boilerplate + uri: https://github.com/ros-industrial/ros_industrial_cmake_boilerplate.git + version: 0.5.4 +- git: + local-name: boost_plugin_loader + uri: https://github.com/tesseract-robotics/boost_plugin_loader.git + version: 0.3.2 +- git: + local-name: noether + uri: https://github.com/ros-industrial/noether.git + version: eafbde4c53ab2d7ac685725d71fe2c2721ddfd7b diff --git a/include/noether_ros/conversions.h b/include/noether_ros/conversions.h new file mode 100644 index 0000000..ad9ac1e --- /dev/null +++ b/include/noether_ros/conversions.h @@ -0,0 +1,19 @@ +#pragma once + +#include +#include + +namespace noether_ros +{ +geometry_msgs::msg::PoseArray toMsg(const noether::ToolPathSegment& segment); +noether::ToolPathSegment toMsg(const geometry_msgs::msg::PoseArray& segment_msg); + +noether_ros::msg::ToolPath toMsg(const noether::ToolPath& tool_path); +noether::ToolPath fromMsg(const noether_ros::msg::ToolPath& tool_path_msg); + +noether_ros::msg::ToolPaths toMsg(const noether::ToolPaths& tool_paths); +noether::ToolPaths fromMsg(const noether_ros::msg::ToolPaths& tool_paths_msg); + +geometry_msgs::msg::PoseArray toMsg(const std::vector& tool_paths); + +} // namespace noether_ros diff --git a/package.xml b/package.xml new file mode 100644 index 0000000..b3ebac3 --- /dev/null +++ b/package.xml @@ -0,0 +1,25 @@ + + + noether_ros + 0.0.0 + ROS2 integration for Noether + Michael Ripperger + Michael Ripperger + Apache-2.0 + + ament_cmake + + rosidl_default_generators + rclcpp + geometry_msgs + tf2_eigen + noether_tpp + yaml-cpp + rosidl_default_runtime + + rosidl_interface_packages + + + ament_cmake + + diff --git a/src/conversions.cpp b/src/conversions.cpp new file mode 100644 index 0000000..aa1553e --- /dev/null +++ b/src/conversions.cpp @@ -0,0 +1,103 @@ +#include + +#include +#include + +namespace noether_ros +{ +geometry_msgs::msg::PoseArray toMsg(const noether::ToolPathSegment& segment) +{ + geometry_msgs::msg::PoseArray segment_msg; + segment_msg.poses.reserve(segment.size()); + + std::transform(segment.begin(), + segment.end(), + std::back_inserter(segment_msg.poses), + [](const Eigen::Isometry3d& pose) { return tf2::toMsg(pose); }); + + return segment_msg; +} + +noether::ToolPathSegment fromMsg(const geometry_msgs::msg::PoseArray& segment_msg) +{ + noether::ToolPathSegment segment; + segment.reserve(segment_msg.poses.size()); + + std::transform(segment_msg.poses.begin(), + segment_msg.poses.end(), + std::back_inserter(segment), + [](const geometry_msgs::msg::Pose& msg) { + Eigen::Isometry3d pose; + tf2::fromMsg(msg, pose); + return pose; + }); + + return segment; +} + +noether_ros::msg::ToolPath toMsg(const noether::ToolPath& tool_path) +{ + noether_ros::msg::ToolPath tool_path_msg; + tool_path_msg.segments.reserve(tool_path.size()); + + std::transform(tool_path.begin(), + tool_path.end(), + std::back_inserter(tool_path_msg.segments), + [](const noether::ToolPathSegment& segment) { return toMsg(segment); }); + + return tool_path_msg; +} + +noether::ToolPath fromMsg(const noether_ros::msg::ToolPath& tool_path_msg) +{ + noether::ToolPath tool_path; + tool_path.reserve(tool_path_msg.segments.size()); + + std::transform(tool_path_msg.segments.begin(), + tool_path_msg.segments.end(), + std::back_inserter(tool_path), + [](const geometry_msgs::msg::PoseArray& msg) { return fromMsg(msg); }); + + return tool_path; +} + +noether_ros::msg::ToolPaths toMsg(const noether::ToolPaths& tool_paths) +{ + noether_ros::msg::ToolPaths tool_paths_msg; + tool_paths_msg.tool_paths.reserve(tool_paths.size()); + + std::transform(tool_paths.begin(), + tool_paths.end(), + std::back_inserter(tool_paths_msg.tool_paths), + [](const noether::ToolPath& tool_path) { return toMsg(tool_path); }); + + return tool_paths_msg; +} + +noether::ToolPaths fromMsg(const noether_ros::msg::ToolPaths& tool_paths_msg) +{ + noether::ToolPaths tool_paths; + tool_paths.reserve(tool_paths_msg.tool_paths.size()); + + std::transform(tool_paths_msg.tool_paths.begin(), + tool_paths_msg.tool_paths.end(), + std::back_inserter(tool_paths), + [](const noether_ros::msg::ToolPath& msg) { return fromMsg(msg); }); + + return tool_paths; +} + +geometry_msgs::msg::PoseArray toMsg(const std::vector& tool_paths_list) +{ + geometry_msgs::msg::PoseArray msg; + + for (const noether::ToolPaths& tool_paths : tool_paths_list) + for (const noether::ToolPath& tool_path : tool_paths) + for (const noether::ToolPathSegment& segment : tool_path) + for (const Eigen::Isometry3d& pose : segment) + msg.poses.push_back(tf2::toMsg(pose)); + + return msg; +} + +} // namespace noether_ros diff --git a/src/tool_path_planning_server.cpp b/src/tool_path_planning_server.cpp new file mode 100644 index 0000000..54c27fe --- /dev/null +++ b/src/tool_path_planning_server.cpp @@ -0,0 +1,65 @@ +#include +#include + +#include +#include +#include +#include +#include + +class ToolPathPlanningServer : public rclcpp::Node +{ +public: + ToolPathPlanningServer() : Node("tool_path_planning_server") + { + server_ = this->create_service( + "plan_tool_path", + std::bind(&ToolPathPlanningServer::callback, this, std::placeholders::_1, std::placeholders::_2)); + } + +protected: + void callback(const std::shared_ptr request, + std::shared_ptr response) + { + try + { + // Convert input configuration string to YAML node + YAML::Node config = YAML::Load(request->config); + + // Load the mesh file + pcl::PolygonMesh mesh; + if (pcl::io::loadPolygonFile(request->mesh_file, mesh) < 0) + throw std::runtime_error("Failed to load mesh from file: " + request->mesh_file); + + // Create and run the tool path planning pipeline + noether::Factory factory; + noether::ToolPathPlannerPipeline pipeline(factory, config); + std::vector tool_paths = pipeline.plan(mesh); + + // Fill in the response + response->success = true; + response->tool_paths.reserve(tool_paths.size()); + std::transform(tool_paths.begin(), + tool_paths.end(), + std::back_inserter(response->tool_paths), + [](const noether::ToolPaths& tp) { return noether_ros::toMsg(tp); }); + } + catch (const std::exception& ex) + { + response->success = false; + response->message = ex.what(); + } + } + + rclcpp::Service::SharedPtr server_; +}; + +int main(int argc, char* argv[]) +{ + rclcpp::init(argc, argv); + auto node = std::make_shared(); + RCLCPP_INFO_STREAM(node->get_logger(), "Started tool path planning server"); + rclcpp::spin(node); + rclcpp::shutdown(); + return 0; +} From 167896b8bd88278aa04ecd3fbabd52d1d0ff97f0 Mon Sep 17 00:00:00 2001 From: Michael Ripperger Date: Thu, 18 Sep 2025 18:38:52 -0500 Subject: [PATCH 04/16] Added unit test to call tool path planning server --- CMakeLists.txt | 5 ++ package.xml | 8 ++++ test/CMakeLists.txt | 18 +++++++ test/dome.ply | Bin 0 -> 149989 bytes test/test_server.py | 111 ++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 142 insertions(+) create mode 100644 test/CMakeLists.txt create mode 100644 test/dome.ply create mode 100644 test/test_server.py diff --git a/CMakeLists.txt b/CMakeLists.txt index b867f37..ff8f5bd 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -41,6 +41,11 @@ add_executable(${PROJECT_NAME}_tool_path_planning_server src/tool_path_planning_ target_link_libraries(${PROJECT_NAME}_tool_path_planning_server ${PROJECT_NAME}_conversions yaml-cpp) ament_target_dependencies(${PROJECT_NAME}_tool_path_planning_server rclcpp) +if(${ENABLE_TESTING}) + enable_testing() + add_subdirectory(test) +endif() + # Install the headers install(DIRECTORY include/ DESTINATION include/) diff --git a/package.xml b/package.xml index b3ebac3..2278cb0 100644 --- a/package.xml +++ b/package.xml @@ -17,6 +17,14 @@ yaml-cpp rosidl_default_runtime + ament_cmake_ros + ament_index_python + launch + launch_ros + launch_testing + launch_testing_ament_cmake + rclpy + rosidl_interface_packages diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt new file mode 100644 index 0000000..ebb3dd3 --- /dev/null +++ b/test/CMakeLists.txt @@ -0,0 +1,18 @@ +# Integration tests +find_package(ament_cmake_ros REQUIRED) +find_package(launch_testing_ament_cmake REQUIRED) + +# Create a function for adding isolated launch tests +function(add_ros_isolated_launch_test path) + set(RUNNER "${ament_cmake_ros_DIR}/run_test_isolated.py") + add_launch_test( + "${path}" + RUNNER + "${RUNNER}" + ${ARGN} + ) +endfunction() + +# Add a test for the TPP server +add_ros_isolated_launch_test(test_server.py) +install(FILES dome.ply DESTINATION share/${PROJECT_NAME}/test) diff --git a/test/dome.ply b/test/dome.ply new file mode 100644 index 0000000000000000000000000000000000000000..9254b3452a75879047778e4934437fd787e7a7ea GIT binary patch literal 149989 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 SMOQvV4kN(z$>g&JgwFp=nzzRQ literal 0 HcmV?d00001 diff --git a/test/test_server.py b/test/test_server.py new file mode 100644 index 0000000..90f3883 --- /dev/null +++ b/test/test_server.py @@ -0,0 +1,111 @@ +from ament_index_python.packages import get_package_share_directory +from launch import LaunchDescription +from launch_ros.actions import Node as LaunchNode +import launch_testing +from noether_ros.srv import PlanToolPath +import os +import rclpy +from rclpy.node import Node +import unittest + + +def generate_test_description(): + return LaunchDescription([ + LaunchNode( + package='noether_ros', + executable='noether_ros_tool_path_planning_server', + output='screen' + ), + launch_testing.actions.ReadyToTest() + ]) + + +config_str = R""" +mesh_modifiers: + - name: NormalsFromMeshFaces +tool_path_planner: + name: Multi + gui_plugin_name: CrossHatchPlaneSlicer + planners: + - name: PlaneSlicer + direction_generator: + name: PrincipalAxis + rotation_offset: 1.571 + origin_generator: + name: AABBCenter + line_spacing: 0.1 + point_spacing: 0.025 + min_hole_size: 0.100 + search_radius: 0.100 + min_segment_size: 0.100 + bidirectional: true + - name: PlaneSlicer + direction_generator: + name: PCARotated + direction_generator: + name: PrincipalAxis + rotation_offset: 1.571 + rotation_offset: 1.571 + origin_generator: + name: AABBCenter + line_spacing: 0.100 + point_spacing: 0.025 + min_hole_size: 0.100 + search_radius: 0.100 + min_segment_size: 0.100 + bidirectional: true +tool_path_modifiers: + - name: SnakeOrganization +""" + +# Active test +class TestToolPathPlanningClient(unittest.TestCase): + + @classmethod + def setUpClass(self): + rclpy.init() + self.node = rclpy.create_node("test_client") + + @classmethod + def tearDownClass(self): + self.node.destroy_node() + rclpy.shutdown() + + def test_tool_path_planning_client(self): + # Create the client + client = self.node.create_client(PlanToolPath, 'plan_tool_path') + + # Check that the service is available + self.assertTrue(client.wait_for_service(timeout_sec=5.0), "Service does not exist") + + # Create the request + request = PlanToolPath.Request() + + # Add the TPP config string to the request + request.config = config_str + + # Add the mesh file to the request + pkg_prefix = get_package_share_directory('noether_ros') + request.mesh_file = os.path.join(pkg_prefix, 'test', 'dome.ply') + + # Check that the mesh file is valid + self.assertIsNotNone(pkg_prefix) + self.assertTrue(os.path.exists(request.mesh_file)) + + # Call the service + future = client.call_async(request) + rclpy.spin_until_future_complete(self.node, future) + + # Check the response + response = future.result() + self.assertIsNotNone(response, "No response received") + self.assertTrue(response.success, response.message) + self.assertEqual(len(response.tool_paths), 1) + for tool_paths in response.tool_paths: + self.assertGreaterEqual(len(tool_paths.tool_paths), 1) + for tool_path in tool_paths.tool_paths: + self.assertGreaterEqual(len(tool_path.segments), 1) + for segment in tool_path.segments: + self.assertGreaterEqual(len(segment.poses), 1) + + self.node.destroy_client(client) From dfbcf36d5f90a835d888eaa00cf23bc9d6816c06 Mon Sep 17 00:00:00 2001 From: Michael Ripperger Date: Thu, 18 Sep 2025 18:39:10 -0500 Subject: [PATCH 05/16] Added .gitignore file --- .gitignore | 1 + 1 file changed, 1 insertion(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bee8a64 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +__pycache__ From 3384c96f5be1a147f7dc668d858c732ea0b153e7 Mon Sep 17 00:00:00 2001 From: Michael Ripperger Date: Thu, 18 Sep 2025 19:03:53 -0500 Subject: [PATCH 06/16] Added Docker files --- docker/Dockerfile | 41 +++++++++++++++++++++++++++++++++++++++ docker/docker-compose.yml | 18 +++++++++++++++++ docker/entrypoint.sh | 3 +++ 3 files changed, 62 insertions(+) create mode 100644 docker/Dockerfile create mode 100644 docker/docker-compose.yml create mode 100755 docker/entrypoint.sh diff --git a/docker/Dockerfile b/docker/Dockerfile new file mode 100644 index 0000000..863be7b --- /dev/null +++ b/docker/Dockerfile @@ -0,0 +1,41 @@ +ARG DISTRO=humble +FROM ros:${DISTRO} +ARG DISTRO + +SHELL ["/bin/bash", "-c"] + +ENV DEBIAN_FRONTEND=noninteractive + +USER root + +# Install +RUN apt update \ + && apt upgrade -y \ + && apt install -y cmake curl git python3 + +# Install and configure ROS infrastructure tools +RUN apt update \ + && apt install -y python3-vcstool python3-colcon-common-extensions python3-rosdep \ + && rosdep update + +# Install the dependency repositories +# Use a tmpfs mount for the workspace so as not to unnecessarily copy files into the final image +# Bind mount the source directory so as not to unnecessarily copy source code into the docker image +ARG WORKSPACE_DIR=/tmpfs/noether_ros +ARG INSTALL_DIR=/opt/noether_ros +RUN mkdir -p ${INSTALL_DIR} + +RUN --mount=type=tmpfs,target=${WORKSPACE_DIR} --mount=type=bind,target=${WORKSPACE_DIR}/src/noether_ros \ + source /opt/ros/${DISTRO}/setup.bash \ + && cd ${WORKSPACE_DIR} \ + && vcs import src < src/noether_ros/dependencies.repos --shallow \ + && rosdep install \ + --from-paths ${WORKSPACE_DIR}/src \ + -iry \ + --skip-keys libvtk \ + && colcon build --cmake-args -DCMAKE_BUILD_TYPE=Release \ + && cp -r ${WORKSPACE_DIR}/install ${INSTALL_DIR} + +# Set the entrypoint to source the workspace +COPY docker/entrypoint.sh /entrypoint.sh +ENTRYPOINT ["/entrypoint.sh"] diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml new file mode 100644 index 0000000..4935e53 --- /dev/null +++ b/docker/docker-compose.yml @@ -0,0 +1,18 @@ +services: + noether: + build: + context: .. + dockerfile: docker/Dockerfile + args: + DISTRO: humble + environment: + ROS_LOG_DIR: /tmp + ROS_LOCALHOST_ONLY: $ROS_LOCALHOST_ONLY + ROS_DOMAIN_ID: $ROS_DOMAIN_ID + container_name: noether_ros + image: ghcr.io/ros-industrial/noether_ros:humble + stdin_open: true + tty: true + network_mode: host + ipc: host + privileged: false diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh new file mode 100755 index 0000000..bea94a9 --- /dev/null +++ b/docker/entrypoint.sh @@ -0,0 +1,3 @@ +#! /bin/bash +source /opt/noether_ros/install/setup.bash +ros2 run noether_ros noether_ros_tool_path_planning_server From 073f8e3ef3aee0b71be4980ecd543bcd17276532 Mon Sep 17 00:00:00 2001 From: Michael Ripperger Date: Fri, 19 Sep 2025 09:41:10 -0500 Subject: [PATCH 07/16] Added github CI workflows --- .github/workflows/clang_format.yml | 30 +++++++++++++++ .github/workflows/cmake_format.yml | 33 ++++++++++++++++ .github/workflows/docker.yml | 61 ++++++++++++++++++++++++++++++ .github/workflows/ubuntu.yml | 44 +++++++++++++++++++++ ci | 1 + 5 files changed, 169 insertions(+) create mode 100644 .github/workflows/clang_format.yml create mode 100644 .github/workflows/cmake_format.yml create mode 100644 .github/workflows/docker.yml create mode 100644 .github/workflows/ubuntu.yml create mode 120000 ci diff --git a/.github/workflows/clang_format.yml b/.github/workflows/clang_format.yml new file mode 100644 index 0000000..fb4a3f9 --- /dev/null +++ b/.github/workflows/clang_format.yml @@ -0,0 +1,30 @@ +name: Clang-Format + +on: + push: + branches: + - main + pull_request: + paths: + - 'include' + - 'src' + - '.github/workflows/clang_format.yml' + - '**clang-format' + schedule: + - cron: '0 5 * * *' + +jobs: + clang_format: + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v4 + + - name: Run clang format + run: | + sudo apt update + sudo apt install -y git clang-format-14 + if [ $? -ge 1 ]; then return 1; fi + ./.run-clang-format + if [ $? -ge 1 ]; then return 1; fi + output=$(git diff) + if [ -n "$output" ]; then exit 1; else exit 0; fi diff --git a/.github/workflows/cmake_format.yml b/.github/workflows/cmake_format.yml new file mode 100644 index 0000000..933131c --- /dev/null +++ b/.github/workflows/cmake_format.yml @@ -0,0 +1,33 @@ +name: CMake-Format + +on: + push: + branches: + - main + pull_request: + paths: + - 'CMakeLists.txt' + - 'test/CMakeLists.txt' + - '.github/workflows/cmake_format.yml' + - '**cmake-format' + schedule: + - cron: '0 5 * * *' + +jobs: + cmake_format: + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v1 + + - name: Run CMake Lang Format Check + run: | + sudo pip3 install cmakelang + RED='\033[0;31m' + NC='\033[0m' # No Color + git config --global --add safe.directory '*' + ./.run-cmake-format + output=$(git diff) + if [ -n "$output" ]; then printf "${RED}CMake format error: run script './.run-cmake-formate'${NC}\n"; fi + if [ -n "$output" ]; then printf "${RED}${output}${NC}\n"; fi + if [ -n "$output" ]; then exit 1; else exit 0; fi + diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml new file mode 100644 index 0000000..64eda22 --- /dev/null +++ b/.github/workflows/docker.yml @@ -0,0 +1,61 @@ +name: Docker + +on: + push: + branches: + - main + pull_request: + release: + types: + - released + +jobs: + ci: + name: ${{ matrix.distro }} + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + distro: [humble, jazzy] + env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} + PUSH_DOCKER_IMAGE: ${{ github.ref == 'refs/heads/master' || github.event_name == 'release' }} + permissions: + contents: read + packages: write + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Log in to the Container registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract Docker meta-information + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + flavor: | + latest=false + prefix= + suffix= + tags: | + type=ref,event=branch,prefix=${{ matrix.distro }}- + type=ref,event=pr,prefix=${{ matrix.distro }}- + type=semver,pattern={{major}}.{{minor}},prefix=${{ matrix.distro }}- + + - name: Build and push Docker image + uses: docker/build-push-action@v5 + with: + context: . + file: docker/Dockerfile + build-args: | + DISTRO=${{ matrix.distro }} + push: ${{ env.PUSH_DOCKER_IMAGE }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} diff --git a/.github/workflows/ubuntu.yml b/.github/workflows/ubuntu.yml new file mode 100644 index 0000000..4704fb7 --- /dev/null +++ b/.github/workflows/ubuntu.yml @@ -0,0 +1,44 @@ +name: ROS [Humble, Jazzy] + +on: + push: + branches: + - main + pull_request: + schedule: + - cron: '0 5 * * *' + workflow_dispatch: + release: + types: + - released + +jobs: + ci: + name: ${{ matrix.distro }} + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + distro: [humble, jazzy] + container: + image: ros:${{ matrix.distro }} + env: + CCACHE_DIR: ${{ github.workspace }}/${{ matrix.distro }}/.ccache + DEBIAN_FRONTEND: noninteractive + TZ: Etc/UTC + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + path: target_ws/src + + - name: Build and Tests + uses: tesseract-robotics/colcon-action@v11 + with: + ccache-prefix: ${{ matrix.distro }} + add-ros-ppa: false + vcs-file: dependencies.repos + rosdep-install-args: '-iry --skip-keys libvtk' + before-script: source /opt/ros/${{ matrix.distro }}/setup.bash + target-path: target_ws/src + target-args: --cmake-args -DCMAKE_BUILD_TYPE=Debug -DENABLE_TESTING=ON diff --git a/ci b/ci new file mode 120000 index 0000000..ecd623e --- /dev/null +++ b/ci @@ -0,0 +1 @@ +.github/workflows/ \ No newline at end of file From 6cb1b83da6306273c7d24e7d53620bc50cb569cf Mon Sep 17 00:00:00 2001 From: Michael Ripperger Date: Mon, 22 Sep 2025 15:03:11 -0500 Subject: [PATCH 08/16] Added optional frame string to toMsg conversion utilities --- include/noether_ros/conversions.h | 8 ++++---- src/conversions.cpp | 14 ++++++++------ 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/include/noether_ros/conversions.h b/include/noether_ros/conversions.h index ad9ac1e..00ce2ac 100644 --- a/include/noether_ros/conversions.h +++ b/include/noether_ros/conversions.h @@ -5,15 +5,15 @@ namespace noether_ros { -geometry_msgs::msg::PoseArray toMsg(const noether::ToolPathSegment& segment); +geometry_msgs::msg::PoseArray toMsg(const noether::ToolPathSegment& segment, const std::string frame = ""); noether::ToolPathSegment toMsg(const geometry_msgs::msg::PoseArray& segment_msg); -noether_ros::msg::ToolPath toMsg(const noether::ToolPath& tool_path); +noether_ros::msg::ToolPath toMsg(const noether::ToolPath& tool_path, const std::string frame = ""); noether::ToolPath fromMsg(const noether_ros::msg::ToolPath& tool_path_msg); -noether_ros::msg::ToolPaths toMsg(const noether::ToolPaths& tool_paths); +noether_ros::msg::ToolPaths toMsg(const noether::ToolPaths& tool_paths, const std::string frame = ""); noether::ToolPaths fromMsg(const noether_ros::msg::ToolPaths& tool_paths_msg); -geometry_msgs::msg::PoseArray toMsg(const std::vector& tool_paths); +geometry_msgs::msg::PoseArray toMsg(const std::vector& tool_paths, const std::string frame = ""); } // namespace noether_ros diff --git a/src/conversions.cpp b/src/conversions.cpp index aa1553e..13f20db 100644 --- a/src/conversions.cpp +++ b/src/conversions.cpp @@ -5,9 +5,10 @@ namespace noether_ros { -geometry_msgs::msg::PoseArray toMsg(const noether::ToolPathSegment& segment) +geometry_msgs::msg::PoseArray toMsg(const noether::ToolPathSegment& segment, const std::string frame) { geometry_msgs::msg::PoseArray segment_msg; + segment_msg.header.frame_id = frame; segment_msg.poses.reserve(segment.size()); std::transform(segment.begin(), @@ -35,7 +36,7 @@ noether::ToolPathSegment fromMsg(const geometry_msgs::msg::PoseArray& segment_ms return segment; } -noether_ros::msg::ToolPath toMsg(const noether::ToolPath& tool_path) +noether_ros::msg::ToolPath toMsg(const noether::ToolPath& tool_path, const std::string frame) { noether_ros::msg::ToolPath tool_path_msg; tool_path_msg.segments.reserve(tool_path.size()); @@ -43,7 +44,7 @@ noether_ros::msg::ToolPath toMsg(const noether::ToolPath& tool_path) std::transform(tool_path.begin(), tool_path.end(), std::back_inserter(tool_path_msg.segments), - [](const noether::ToolPathSegment& segment) { return toMsg(segment); }); + [&frame](const noether::ToolPathSegment& segment) { return toMsg(segment, frame); }); return tool_path_msg; } @@ -61,7 +62,7 @@ noether::ToolPath fromMsg(const noether_ros::msg::ToolPath& tool_path_msg) return tool_path; } -noether_ros::msg::ToolPaths toMsg(const noether::ToolPaths& tool_paths) +noether_ros::msg::ToolPaths toMsg(const noether::ToolPaths& tool_paths, const std::string frame) { noether_ros::msg::ToolPaths tool_paths_msg; tool_paths_msg.tool_paths.reserve(tool_paths.size()); @@ -69,7 +70,7 @@ noether_ros::msg::ToolPaths toMsg(const noether::ToolPaths& tool_paths) std::transform(tool_paths.begin(), tool_paths.end(), std::back_inserter(tool_paths_msg.tool_paths), - [](const noether::ToolPath& tool_path) { return toMsg(tool_path); }); + [&frame](const noether::ToolPath& tool_path) { return toMsg(tool_path, frame); }); return tool_paths_msg; } @@ -87,9 +88,10 @@ noether::ToolPaths fromMsg(const noether_ros::msg::ToolPaths& tool_paths_msg) return tool_paths; } -geometry_msgs::msg::PoseArray toMsg(const std::vector& tool_paths_list) +geometry_msgs::msg::PoseArray toMsg(const std::vector& tool_paths_list, const std::string frame) { geometry_msgs::msg::PoseArray msg; + msg.header.frame_id = frame; for (const noether::ToolPaths& tool_paths : tool_paths_list) for (const noether::ToolPath& tool_path : tool_paths) From f22f88a1c6ae7b672e85aead6279a5137d12833f Mon Sep 17 00:00:00 2001 From: Michael Ripperger Date: Mon, 22 Sep 2025 15:04:01 -0500 Subject: [PATCH 09/16] Updated service to include mesh frame name --- src/tool_path_planning_server.cpp | 6 +++++- srv/PlanToolPath.srv | 6 ++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/tool_path_planning_server.cpp b/src/tool_path_planning_server.cpp index 54c27fe..127897e 100644 --- a/src/tool_path_planning_server.cpp +++ b/src/tool_path_planning_server.cpp @@ -28,6 +28,10 @@ class ToolPathPlanningServer : public rclcpp::Node // Load the mesh file pcl::PolygonMesh mesh; + + // Set the mesh frame from the request + mesh.header.frame_id = request->mesh_frame; + if (pcl::io::loadPolygonFile(request->mesh_file, mesh) < 0) throw std::runtime_error("Failed to load mesh from file: " + request->mesh_file); @@ -42,7 +46,7 @@ class ToolPathPlanningServer : public rclcpp::Node std::transform(tool_paths.begin(), tool_paths.end(), std::back_inserter(response->tool_paths), - [](const noether::ToolPaths& tp) { return noether_ros::toMsg(tp); }); + [&request](const noether::ToolPaths& tp) { return noether_ros::toMsg(tp, request->mesh_frame); }); } catch (const std::exception& ex) { diff --git a/srv/PlanToolPath.srv b/srv/PlanToolPath.srv index a7777cb..82e2fa2 100644 --- a/srv/PlanToolPath.srv +++ b/srv/PlanToolPath.srv @@ -1,5 +1,11 @@ +# Tool path planning pipeline YAML configuration string string config + +# File path of the mesh resource string mesh_file + +# TF frame to which the mesh is relative +string mesh_frame --- bool success string message From b5460570d9b8f1c2c10d73fda88080f9065849be Mon Sep 17 00:00:00 2001 From: Michael Ripperger Date: Mon, 22 Sep 2025 15:04:52 -0500 Subject: [PATCH 10/16] Used noether printExceptions utility to return full error message --- dependencies.repos | 2 +- src/tool_path_planning_server.cpp | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/dependencies.repos b/dependencies.repos index 3dc958b..db40247 100644 --- a/dependencies.repos +++ b/dependencies.repos @@ -9,4 +9,4 @@ - git: local-name: noether uri: https://github.com/ros-industrial/noether.git - version: eafbde4c53ab2d7ac685725d71fe2c2721ddfd7b + version: 548e5c2b70f9ee0a73da3d58563621b20c0d5710 diff --git a/src/tool_path_planning_server.cpp b/src/tool_path_planning_server.cpp index 127897e..cb94e5a 100644 --- a/src/tool_path_planning_server.cpp +++ b/src/tool_path_planning_server.cpp @@ -2,6 +2,7 @@ #include #include +#include #include #include #include @@ -51,7 +52,9 @@ class ToolPathPlanningServer : public rclcpp::Node catch (const std::exception& ex) { response->success = false; - response->message = ex.what(); + std::stringstream ss; + noether::printException(ex, ss); + response->message = ss.str(); } } From f5c343118f8054402f15e7c84210eb0d99192a2a Mon Sep 17 00:00:00 2001 From: Michael Ripperger Date: Mon, 22 Sep 2025 16:21:35 -0500 Subject: [PATCH 11/16] Added builtin_interfaces dependency and alphabetized dependencies in CMakeLists and package.xml --- CMakeLists.txt | 5 +++-- package.xml | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index ff8f5bd..2a56929 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -8,10 +8,11 @@ endif() # find dependencies find_package(ament_cmake REQUIRED) -find_package(rclcpp REQUIRED) +find_package(builtin_interfaces REQUIRED) find_package(geometry_msgs REQUIRED) -find_package(tf2_eigen REQUIRED) find_package(noether_tpp REQUIRED) +find_package(rclcpp REQUIRED) +find_package(tf2_eigen REQUIRED) find_package(yaml-cpp REQUIRED) # Generate the messages diff --git a/package.xml b/package.xml index 2278cb0..d6b39fc 100644 --- a/package.xml +++ b/package.xml @@ -10,10 +10,11 @@ ament_cmake rosidl_default_generators - rclcpp + builtin_interfaces geometry_msgs - tf2_eigen noether_tpp + rclcpp + tf2_eigen yaml-cpp rosidl_default_runtime From 613abd5f344526b872671ff7238c5aeab1df3563 Mon Sep 17 00:00:00 2001 From: Michael Ripperger Date: Mon, 22 Sep 2025 17:04:42 -0500 Subject: [PATCH 12/16] Updated server to load config as either YAML formatted string or YAML file path --- src/tool_path_planning_server.cpp | 14 +++++++++++--- srv/PlanToolPath.srv | 3 ++- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/src/tool_path_planning_server.cpp b/src/tool_path_planning_server.cpp index cb94e5a..1722fb1 100644 --- a/src/tool_path_planning_server.cpp +++ b/src/tool_path_planning_server.cpp @@ -1,5 +1,5 @@ -#include #include +#include #include #include @@ -24,8 +24,16 @@ class ToolPathPlanningServer : public rclcpp::Node { try { - // Convert input configuration string to YAML node - YAML::Node config = YAML::Load(request->config); + // Convert input configuration string/file to YAML node + YAML::Node config; + try + { + config = YAML::LoadFile(request->config); + } + catch (const YAML::Exception&) + { + config = YAML::Load(request->config); + } // Load the mesh file pcl::PolygonMesh mesh; diff --git a/srv/PlanToolPath.srv b/srv/PlanToolPath.srv index 82e2fa2..893689c 100644 --- a/srv/PlanToolPath.srv +++ b/srv/PlanToolPath.srv @@ -1,4 +1,5 @@ -# Tool path planning pipeline YAML configuration string +# Tool path planning pipeline YAML configuration +# This parameter can either be a YAML-formatted string or an absolute path to a YAML file string config # File path of the mesh resource From 42a09e67a211218fc98e5c13e8df2cbaf425502c Mon Sep 17 00:00:00 2001 From: Michael Ripperger Date: Mon, 22 Sep 2025 17:04:54 -0500 Subject: [PATCH 13/16] Updated version of noether and RICB --- dependencies.repos | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dependencies.repos b/dependencies.repos index db40247..d9b5f48 100644 --- a/dependencies.repos +++ b/dependencies.repos @@ -1,7 +1,7 @@ - git: local-name: ros_industrial_cmake_boilerplate uri: https://github.com/ros-industrial/ros_industrial_cmake_boilerplate.git - version: 0.5.4 + version: 0.7.4 - git: local-name: boost_plugin_loader uri: https://github.com/tesseract-robotics/boost_plugin_loader.git @@ -9,4 +9,4 @@ - git: local-name: noether uri: https://github.com/ros-industrial/noether.git - version: 548e5c2b70f9ee0a73da3d58563621b20c0d5710 + version: cca58d61103174394c31e7f9d85e0e887f879390 From f49f5142c4a9394d08ec2fea3520d506a7b7247f Mon Sep 17 00:00:00 2001 From: Michael Ripperger Date: Mon, 22 Sep 2025 17:29:16 -0500 Subject: [PATCH 14/16] Added utility for converting list of tool paths into message vector; added and renamed conversion function for flattening into geometry_msgs/PoseArray --- include/noether_ros/conversions.h | 9 +++++- src/conversions.cpp | 54 ++++++++++++++++++++++++++++++- src/tool_path_planning_server.cpp | 6 +--- 3 files changed, 62 insertions(+), 7 deletions(-) diff --git a/include/noether_ros/conversions.h b/include/noether_ros/conversions.h index 00ce2ac..c8a026e 100644 --- a/include/noether_ros/conversions.h +++ b/include/noether_ros/conversions.h @@ -14,6 +14,13 @@ noether::ToolPath fromMsg(const noether_ros::msg::ToolPath& tool_path_msg); noether_ros::msg::ToolPaths toMsg(const noether::ToolPaths& tool_paths, const std::string frame = ""); noether::ToolPaths fromMsg(const noether_ros::msg::ToolPaths& tool_paths_msg); -geometry_msgs::msg::PoseArray toMsg(const std::vector& tool_paths, const std::string frame = ""); +std::vector toMsg(const std::vector& tool_paths_list, + const std::string& frame = ""); +std::vector fromMsg(const std::vector& tool_paths_msg); + +geometry_msgs::msg::PoseArray flattenToMsg(const noether::ToolPath& tool_paths, const std::string frame = ""); +geometry_msgs::msg::PoseArray flattenToMsg(const noether::ToolPaths& tool_paths, const std::string frame = ""); +geometry_msgs::msg::PoseArray flattenToMsg(const std::vector& tool_paths, + const std::string frame = ""); } // namespace noether_ros diff --git a/src/conversions.cpp b/src/conversions.cpp index 13f20db..754cc3e 100644 --- a/src/conversions.cpp +++ b/src/conversions.cpp @@ -88,7 +88,59 @@ noether::ToolPaths fromMsg(const noether_ros::msg::ToolPaths& tool_paths_msg) return tool_paths; } -geometry_msgs::msg::PoseArray toMsg(const std::vector& tool_paths_list, const std::string frame) +std::vector toMsg(const std::vector& tool_paths_list, + const std::string& frame) +{ + std::vector tool_paths_list_msg; + tool_paths_list_msg.reserve(tool_paths_list.size()); + std::transform(tool_paths_list.begin(), + tool_paths_list.end(), + std::back_inserter(tool_paths_list_msg), + [&frame](const noether::ToolPaths& tp) { return noether_ros::toMsg(tp, frame); }); + + return tool_paths_list_msg; +} + +std::vector fromMsg(const std::vector& tool_paths_list_msg) +{ + std::vector tool_paths_list; + tool_paths_list.reserve(tool_paths_list_msg.size()); + std::transform( + tool_paths_list_msg.begin(), + tool_paths_list_msg.end(), + std::back_inserter(tool_paths_list), + [](const noether_ros::msg::ToolPaths& tool_paths_msg) { return noether_ros::fromMsg(tool_paths_msg); }); + + return tool_paths_list; +} + +geometry_msgs::msg::PoseArray flattenToMsg(const noether::ToolPath& tool_path, const std::string frame) +{ + geometry_msgs::msg::PoseArray msg; + msg.header.frame_id = frame; + + for (const noether::ToolPathSegment& segment : tool_path) + for (const Eigen::Isometry3d& pose : segment) + msg.poses.push_back(tf2::toMsg(pose)); + + return msg; +} + +geometry_msgs::msg::PoseArray flattenToMsg(const noether::ToolPaths& tool_paths, const std::string frame) +{ + geometry_msgs::msg::PoseArray msg; + msg.header.frame_id = frame; + + for (const noether::ToolPath& tool_path : tool_paths) + for (const noether::ToolPathSegment& segment : tool_path) + for (const Eigen::Isometry3d& pose : segment) + msg.poses.push_back(tf2::toMsg(pose)); + + return msg; +} + +geometry_msgs::msg::PoseArray flattenToMsg(const std::vector& tool_paths_list, + const std::string frame) { geometry_msgs::msg::PoseArray msg; msg.header.frame_id = frame; diff --git a/src/tool_path_planning_server.cpp b/src/tool_path_planning_server.cpp index 1722fb1..82a1fd9 100644 --- a/src/tool_path_planning_server.cpp +++ b/src/tool_path_planning_server.cpp @@ -51,11 +51,7 @@ class ToolPathPlanningServer : public rclcpp::Node // Fill in the response response->success = true; - response->tool_paths.reserve(tool_paths.size()); - std::transform(tool_paths.begin(), - tool_paths.end(), - std::back_inserter(response->tool_paths), - [&request](const noether::ToolPaths& tp) { return noether_ros::toMsg(tp, request->mesh_frame); }); + response->tool_paths = noether_ros::toMsg(tool_paths, request->mesh_frame); } catch (const std::exception& ex) { From 15a2014c4fb3535236609446b27cee0cf7a66f97 Mon Sep 17 00:00:00 2001 From: Michael Ripperger Date: Mon, 22 Sep 2025 17:36:56 -0500 Subject: [PATCH 15/16] Updated target install directive --- CMakeLists.txt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 2a56929..030e965 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -54,7 +54,9 @@ install(DIRECTORY include/ DESTINATION include/) install( TARGETS ${PROJECT_NAME}_conversions EXPORT ${PROJECT_NAME}-targets - DESTINATION lib + ARCHIVE DESTINATION lib + LIBRARY DESTINATION lib + RUNTIME DESTINATION bin ) ament_export_targets(${PROJECT_NAME}-targets HAS_LIBRARY_TARGET) From a5b1f4d321641992329df3caaa3853139810b86f Mon Sep 17 00:00:00 2001 From: Michael Ripperger Date: Wed, 24 Sep 2025 16:14:45 -0500 Subject: [PATCH 16/16] Updated checking of pcl load polygon file return type --- src/tool_path_planning_server.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tool_path_planning_server.cpp b/src/tool_path_planning_server.cpp index 82a1fd9..0f47e26 100644 --- a/src/tool_path_planning_server.cpp +++ b/src/tool_path_planning_server.cpp @@ -41,7 +41,7 @@ class ToolPathPlanningServer : public rclcpp::Node // Set the mesh frame from the request mesh.header.frame_id = request->mesh_frame; - if (pcl::io::loadPolygonFile(request->mesh_file, mesh) < 0) + if (pcl::io::loadPolygonFile(request->mesh_file, mesh) <= 0) throw std::runtime_error("Failed to load mesh from file: " + request->mesh_file); // Create and run the tool path planning pipeline