Port a C++ node
Needs first: Move a ROS 2 project over
Port a C++ node when you have one that builds with colcon build against ROS 2
Humble and you want it on nano-ros, with as few source edits as possible.
nano-ros declares the rclcpp:: names in its own C++ headers, and its SDK
root carries the CMake side: the ament_* verbs, Find<pkg>.cmake modules
for the most-cited ROS 2 packages, and diagnostic_updater. So a port is
mostly build glue. What the source still needs is what the compiler names:
nano-ros has no exceptions, and a few handles observe rather than own.
Before you start
- nano-ros installed, the host provisioned with
nros setup linux, and ROS 2 sourced for the first configure. - The package’s message dependencies reachable through
AMENT_PREFIX_PATH. For a project with its own message packages, source the project’s install too (source <project>/install/setup.bash); nano-ros generates its own bindings from those.msgfiles.
Steps
-
Keep the package’s
CMakeLists.txtas it is. It needs no nano-ros line:cmake_minimum_required(VERSION 3.20) project(my_node LANGUAGES CXX) find_package(ament_cmake REQUIRED) find_package(rclcpp REQUIRED) find_package(std_msgs REQUIRED) add_executable(my_node src/my_node.cpp) ament_target_dependencies(my_node rclcpp std_msgs) ament_package()find_package(rclcpp)resolves to nano-ros’s find module, which wins over an installed ROS 2 even when ROS 2 is sourced, because CMake tries modules first. Every target an ament verb creates, or that linksrclcpp::rclcpp, is built withNROS_CPP_STD, thestd::shared_ptr-shaped API ported code expects. -
Add one root
CMakeLists.txtbesidesrc/. It is the only file that names nano-ros:cmake_minimum_required(VERSION 3.22) project(my_workspace LANGUAGES CXX) set(CMAKE_CXX_STANDARD 14) set(NROS_PLATFORM posix) set(NROS_RMW zenoh) add_subdirectory("${NANO_ROS_SDK}" nano_ros) set(NROS_INTERFACE_SEARCH_PATH "${CMAKE_SOURCE_DIR}/src") include("${NANO_ROS_SDK}/cmake/NanoRosAmentSurface.cmake") nros_workspace_interfaces() # every message package in src/, in order add_subdirectory(src/my_node)Add each package before the packages that
find_packageit; the configure refuses an out-of-order find by name. -
Configure and build:
source /opt/ros/humble/setup.bash cmake -B build -DNANO_ROS_SDK="$(nros locate nano-ros)" cmake --build build -
Fix what the compiler names. Most stock nodes need no edit. These are the two most common, from the stock ros2/examples ports:
A global
Node::SharedPtrassigned from a temporary does not compile, because nano-ros’sSharedPtrobserves the node rather than owning it. Keep the node in a local first:// Before g_node = rclcpp::Node::make_shared("minimal_service"); // After auto node = rclcpp::Node::make_shared("minimal_service"); g_node = node;A
try/catchdoes not compile, because nano-ros is built-fno-exceptions. A failed call reports a result instead:// Before try { publisher->publish(message); rclcpp::spin_some(node); } catch (const rclcpp::exceptions::RCLError & e) { RCLCPP_ERROR(node->get_logger(), "unexpectedly failed with %s", e.what()); } // After publisher->publish(message); rclcpp::spin_some(node);A node you construct by hand, outside a generated entry, cannot throw either: check
node.ok()after constructing it. The other spellings and what each maps to are in rclcpp compatibility. -
Run it against the router your ROS 2 peers use:
NROS_LOCATOR=tcp/127.0.0.1:7447 ./build/src/my_node/my_node -
Size the pools for a large node. A package built this way declares no entity counts, so each pool starts at its embedded default and start-up stops at the first one that fills, naming its knob. In a nano-ros workspace, let the census count them: state
entities = "census"on the component row, and the configure runs the program once on the host and sizes the pools from what it created:[[component]] pkg = "my_pkg" class = "my_pkg::MyNode" name = "my_node" entities = "census"Otherwise set the knobs in the configure’s environment. Autoware’s
autoware_external_cmd_selector(12 subscriptions, 7 publishers, a service) needed:export NROS_EXECUTOR_MAX_CBS=40 NROS_ZENOH_MAX_LIVELINESS=128 \ NROS_ZENOH_MAX_PUBLISHERS=32 NROS_ZENOH_MAX_SUBSCRIBERS=32 \ NROS_ZENOH_MAX_QUERYABLES=32 -
To run it on a board, add a per-platform directory beside the source with a
system.tomlnaming the board, and build it withnros build:find_package(nano_ros REQUIRED) nano_ros_add_executable(minimal_publisher ../src/minimal_publisher.cpp ROS2_MAIN)No C runtime calls
mainon a board, so the build renames yourmainand calls it from the platform’s entry, withargc == 0.ROS2_MAINgives the package the C++ standard library (on Zephyr, also setCONFIG_REQUIRES_FULL_LIBCPP=yandCONFIG_GLIBCXX_LIBCPP=yinprj.conf) and theNROS_CPP_STDAPI. A board has no command line, so state the launch file’s parameters on the image:[image.<id>] params_files = ["config/x.param.yaml"] ros_args = ["--ros-args", "-p", "update_rate:=10.0"]Zephyr’s
native_simcannot run a ported node; use a Cortex-M board such as mps2-an385 in QEMU.
Example
examples/templates/cpp-port-minimal-publisher/
vendors the ROS 2 tutorial’s minimal_publisher.cpp unmodified and builds it
through the steps above, on Linux and, from its mps2-an385-freertos/ and
zephyr/ directories, on two boards. Its package CMakeLists.txt is the
stock ament_cmake_auto shape:
find_package(ament_cmake_auto REQUIRED)
find_package(rclcpp REQUIRED)
ament_auto_find_build_dependencies()
ament_auto_add_executable(minimal_publisher src/minimal_publisher.cpp)
ament_target_dependencies(minimal_publisher rclcpp)
ament_auto_package()
What still needs an edit
-
Exceptions. Guard a
throwwith#if defined(__cpp_exceptions)and abort in the other arm. -
MultiThreadedExecutor. A compile error naming the alternative: parallelism is a sched task (Pick a scheduling model), not a callback group. Linux scheduling calls such assched_setschedulerbecome no-ops on a board, where the sched task’s priority applies. -
A thread per action goal. Run the goal from the accepted callback; a goal handle used from a thread you start races the executor.
-
Unbounded strings and sequences in a message you subscribe to stop the compile. Bound them in a
nros-codegen.tomlat the workspace root:[fields] "std_msgs/String.data" = 256 -
Message strings are
rclcpp::FixedString<N>, notstd::string: assign withmessage.data = s.c_str(). -
Out of scope:
tf2,image_transport,pluginlib,class_loaderanddlopencomposition, and generic publishers by type name alone (pass the type hash:create_generic_publisher(topic, type, type_hash, qos)). Parameters are not persisted across boots.
If it doesn’t work
find_package(<pkg>)is refused, naming an installed file. nano-ros does not ship that upstream library (bondcpp,tf2_ros). Remove the dependency, or port the library into your workspace as a package.- A node’s publishers silently do nothing. It failed to construct, and
without exceptions nothing said so. Check
node.ok()after a hand-written construction. - A stored timer or subscription keeps running after you drop it. Its
SharedPtris a handle, not an owner. Calltimer_.reset()to release it;timer_->reset()restarts it, as upstream. ${CMAKE_SOURCE_DIR}points at the wrong directory. Every package configures in one CMake run, so it is the workspace root. Use${PROJECT_SOURCE_DIR}.
See also
- rclcpp compatibility: every upstream spelling and what it maps to, and the measured ports.
- How nano-ros differs from ROS 2.
- Message generation: the CMake spellings for message bindings.