Skip to content

Commit d376254

Browse files
authored
Addition of a Default Node for Hardware Component (#2348)
1 parent c588879 commit d376254

File tree

6 files changed

+182
-0
lines changed

6 files changed

+182
-0
lines changed

hardware_interface/doc/writing_new_hardware_component.rst

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,59 @@ The following is a step-by-step guide to create source files, basic tests, and c
4848
In the first line usually the parents ``on_init`` is called to process standard values, like name. This is done using: ``hardware_interface::(Actuator|Sensor|System)Interface::on_init(info)``.
4949
If all required parameters are set and valid and everything works fine return ``CallbackReturn::SUCCESS`` or ``return CallbackReturn::ERROR`` otherwise.
5050

51+
#. **(Optional) Adding Publisher, Services, etc.**
52+
53+
A common requirement for a hardware component is to publish status or diagnostic information without interfering with the real-time control loop.
54+
55+
This allows you to add any standard ROS 2 component (publishers, subscribers, services, timers) to your hardware interface without compromising real-time performance. There are two primary ways to achieve this.
56+
57+
**Method 1: Using the Framework-Managed Node (Recommended & Simplest)**
58+
59+
The framework internally creates a dedicated ROS 2 node for each hardware component. Your hardware plugin can then get a handle to this node and use it.
60+
61+
#. **Access and using the Default Node**: You can get a ``shared_ptr`` to the node by calling the ``get_node()`` method and use it just like any other ``rclcpp::Node::SharedPtr`` to create publishers, timers, etc.
62+
63+
.. code-block:: cpp
64+
65+
// Continuing inside on_configure()
66+
if (get_node())
67+
{
68+
my_publisher_ = get_node()->create_publisher<std_msgs::msg::String>("~/status", 10);
69+
70+
using namespace std::chrono_literals;
71+
my_timer_ = get_node()->create_wall_timer(1s, [this]() {
72+
std_msgs::msg::String msg;
73+
msg.data = "Hardware status update!";
74+
my_publisher_->publish(msg);
75+
});
76+
}
77+
78+
**Method 2: Using the Executor from `HardwareComponentParams`**
79+
80+
For more advanced use cases where you need direct control over node creation, the ``on_init`` method can be configured to receive a ``HardwareComponentParams`` struct. This struct contains a ``weak_ptr`` to the ``ControllerManager``'s executor.
81+
82+
#. **Update ``on_init`` Signature**: First, your hardware interface must override the ``on_init`` version that takes ``HardwareComponentParams``.
83+
84+
.. code-block:: cpp
85+
86+
// In your <robot_hardware_interface_name>.hpp
87+
hardware_interface::CallbackReturn on_init(
88+
const hardware_interface::HardwareComponentParams & params) override;
89+
90+
#. **Lock and Use the Executor**: Inside ``on_init``, you must safely "lock" the ``weak_ptr`` to get a usable ``shared_ptr``. You can then create your own node and add it to the executor.
91+
92+
.. code-block:: cpp
93+
94+
// In your <robot_hardware_interface_name>.cpp, inside on_init(params)
95+
if (auto locked_executor = params.executor.lock())
96+
{
97+
my_custom_node_ = std::make_shared<rclcpp::Node>("my_custom_node");
98+
locked_executor->add_node(my_custom_node_->get_node_base_interface());
99+
// ... create publishers/timers on my_custom_node_ ...
100+
}
101+
102+
For a complete, working implementation that uses the framework-managed node to publish diagnostic messages, see the demo in example 17.
103+
51104
#. Write the ``on_configure`` method where you usually setup the communication to the hardware and set everything up so that the hardware can be activated.
52105

53106
#. Implement ``on_cleanup`` method, which does the opposite of ``on_configure``.

hardware_interface/include/hardware_interface/actuator_interface.hpp

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,26 @@ class ActuatorInterface : public rclcpp_lifecycle::node_interfaces::LifecycleNod
186186
info_.thread_priority);
187187
async_handler_->start_thread();
188188
}
189+
190+
if (auto locked_executor = params.executor.lock())
191+
{
192+
std::string node_name = params.hardware_info.name;
193+
std::transform(
194+
node_name.begin(), node_name.end(), node_name.begin(),
195+
[](unsigned char c) { return std::tolower(c); });
196+
std::replace(node_name.begin(), node_name.end(), '/', '_');
197+
hardware_component_node_ = std::make_shared<rclcpp::Node>(node_name);
198+
locked_executor->add_node(hardware_component_node_->get_node_base_interface());
199+
}
200+
else
201+
{
202+
RCLCPP_WARN(
203+
params.logger,
204+
"Executor is not available during hardware component initialization for '%s'. Skipping "
205+
"node creation!",
206+
params.hardware_info.name.c_str());
207+
}
208+
189209
hardware_interface::HardwareComponentInterfaceParams interface_params;
190210
interface_params.hardware_info = info_;
191211
interface_params.executor = params.executor;
@@ -646,6 +666,12 @@ class ActuatorInterface : public rclcpp_lifecycle::node_interfaces::LifecycleNod
646666
*/
647667
rclcpp::Clock::SharedPtr get_clock() const { return actuator_clock_; }
648668

669+
/// Get the default node of the ActuatorInterface.
670+
/**
671+
* \return node of the ActuatorInterface.
672+
*/
673+
rclcpp::Node::SharedPtr get_node() const { return hardware_component_node_; }
674+
649675
/// Get the hardware info of the ActuatorInterface.
650676
/**
651677
* \return hardware info of the ActuatorInterface.
@@ -702,6 +728,7 @@ class ActuatorInterface : public rclcpp_lifecycle::node_interfaces::LifecycleNod
702728
private:
703729
rclcpp::Clock::SharedPtr actuator_clock_;
704730
rclcpp::Logger actuator_logger_;
731+
rclcpp::Node::SharedPtr hardware_component_node_ = nullptr;
705732
// interface names to Handle accessed through getters/setters
706733
std::unordered_map<std::string, StateInterface::SharedPtr> actuator_states_;
707734
std::unordered_map<std::string, CommandInterface::SharedPtr> actuator_commands_;

hardware_interface/include/hardware_interface/sensor_interface.hpp

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,26 @@ class SensorInterface : public rclcpp_lifecycle::node_interfaces::LifecycleNodeI
149149
info_.thread_priority);
150150
read_async_handler_->start_thread();
151151
}
152+
153+
if (auto locked_executor = params.executor.lock())
154+
{
155+
std::string node_name = params.hardware_info.name;
156+
std::transform(
157+
node_name.begin(), node_name.end(), node_name.begin(),
158+
[](unsigned char c) { return std::tolower(c); });
159+
std::replace(node_name.begin(), node_name.end(), '/', '_');
160+
hardware_component_node_ = std::make_shared<rclcpp::Node>(node_name);
161+
locked_executor->add_node(hardware_component_node_->get_node_base_interface());
162+
}
163+
else
164+
{
165+
RCLCPP_WARN(
166+
params.logger,
167+
"Executor is not available during hardware component initialization for '%s'. Skipping "
168+
"node creation!",
169+
params.hardware_info.name.c_str());
170+
}
171+
152172
hardware_interface::HardwareComponentInterfaceParams interface_params;
153173
interface_params.hardware_info = info_;
154174
interface_params.executor = params.executor;
@@ -419,6 +439,12 @@ class SensorInterface : public rclcpp_lifecycle::node_interfaces::LifecycleNodeI
419439
*/
420440
rclcpp::Clock::SharedPtr get_clock() const { return sensor_clock_; }
421441

442+
/// Get the default node of the ActuatorInterface.
443+
/**
444+
* \return node of the ActuatorInterface.
445+
*/
446+
rclcpp::Node::SharedPtr get_node() const { return hardware_component_node_; }
447+
422448
/// Get the hardware info of the SensorInterface.
423449
/**
424450
* \return hardware info of the SensorInterface.
@@ -459,6 +485,7 @@ class SensorInterface : public rclcpp_lifecycle::node_interfaces::LifecycleNodeI
459485
private:
460486
rclcpp::Clock::SharedPtr sensor_clock_;
461487
rclcpp::Logger sensor_logger_;
488+
rclcpp::Node::SharedPtr hardware_component_node_ = nullptr;
462489
// interface names to Handle accessed through getters/setters
463490
std::unordered_map<std::string, StateInterface::SharedPtr> sensor_states_map_;
464491

hardware_interface/include/hardware_interface/system_interface.hpp

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,26 @@ class SystemInterface : public rclcpp_lifecycle::node_interfaces::LifecycleNodeI
190190
info_.thread_priority);
191191
async_handler_->start_thread();
192192
}
193+
194+
if (auto locked_executor = params.executor.lock())
195+
{
196+
std::string node_name = params.hardware_info.name;
197+
std::transform(
198+
node_name.begin(), node_name.end(), node_name.begin(),
199+
[](unsigned char c) { return std::tolower(c); });
200+
std::replace(node_name.begin(), node_name.end(), '/', '_');
201+
hardware_component_node_ = std::make_shared<rclcpp::Node>(node_name);
202+
locked_executor->add_node(hardware_component_node_->get_node_base_interface());
203+
}
204+
else
205+
{
206+
RCLCPP_WARN(
207+
params.logger,
208+
"Executor is not available during hardware component initialization for '%s'. Skipping "
209+
"node creation!",
210+
params.hardware_info.name.c_str());
211+
}
212+
193213
hardware_interface::HardwareComponentInterfaceParams interface_params;
194214
interface_params.hardware_info = info_;
195215
interface_params.executor = params.executor;
@@ -677,6 +697,12 @@ class SystemInterface : public rclcpp_lifecycle::node_interfaces::LifecycleNodeI
677697
*/
678698
rclcpp::Clock::SharedPtr get_clock() const { return system_clock_; }
679699

700+
/// Get the default node of the ActuatorInterface.
701+
/**
702+
* \return node of the ActuatorInterface.
703+
*/
704+
rclcpp::Node::SharedPtr get_node() const { return hardware_component_node_; }
705+
680706
/// Get the hardware info of the SystemInterface.
681707
/**
682708
* \return hardware info of the SystemInterface.
@@ -743,6 +769,7 @@ class SystemInterface : public rclcpp_lifecycle::node_interfaces::LifecycleNodeI
743769
private:
744770
rclcpp::Clock::SharedPtr system_clock_;
745771
rclcpp::Logger system_logger_;
772+
rclcpp::Node::SharedPtr hardware_component_node_ = nullptr;
746773
// interface names to Handle accessed through getters/setters
747774
std::unordered_map<std::string, StateInterface::SharedPtr> system_states_;
748775
std::unordered_map<std::string, CommandInterface::SharedPtr> system_commands_;

hardware_interface_testing/test/test_resource_manager.cpp

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1342,6 +1342,49 @@ TEST_F(ResourceManagerTest, managing_controllers_reference_interfaces)
13421342
rm.make_controller_reference_interfaces_unavailable("unknown_controller"), std::out_of_range);
13431343
}
13441344

1345+
class MockExecutor : public rclcpp::executors::SingleThreadedExecutor
1346+
{
1347+
public:
1348+
explicit MockExecutor(const rclcpp::ExecutorOptions & options = rclcpp::ExecutorOptions())
1349+
: rclcpp::executors::SingleThreadedExecutor(options)
1350+
{
1351+
}
1352+
1353+
void add_node(
1354+
rclcpp::node_interfaces::NodeBaseInterface::SharedPtr node_ptr, bool notify) override
1355+
{
1356+
rclcpp::executors::SingleThreadedExecutor::add_node(node_ptr, notify);
1357+
added_node_names.push_back(node_ptr->get_name());
1358+
}
1359+
1360+
std::vector<std::string> added_node_names;
1361+
};
1362+
1363+
TEST_F(ResourceManagerTest, hardware_nodes_are_added_to_mock_executor_on_load)
1364+
{
1365+
auto mock_executor = std::make_shared<MockExecutor>();
1366+
1367+
hardware_interface::ResourceManagerParams params;
1368+
params.robot_description = ros2_control_test_assets::minimal_robot_urdf;
1369+
params.clock = node_.get_clock();
1370+
params.logger = node_.get_logger();
1371+
params.executor = mock_executor;
1372+
TestableResourceManager rm(params);
1373+
auto to_lower = [](const std::string & s)
1374+
{
1375+
std::string result = s;
1376+
std::transform(
1377+
result.begin(), result.end(), result.begin(),
1378+
[](unsigned char c) { return std::tolower(c); });
1379+
return result;
1380+
};
1381+
EXPECT_THAT(
1382+
mock_executor->added_node_names,
1383+
testing::UnorderedElementsAre(
1384+
to_lower(TEST_ACTUATOR_HARDWARE_NAME), to_lower(TEST_SENSOR_HARDWARE_NAME),
1385+
to_lower(TEST_SYSTEM_HARDWARE_NAME)));
1386+
}
1387+
13451388
class ResourceManagerTestReadWriteError : public ResourceManagerTest
13461389
{
13471390
public:

hardware_interface_testing/test/test_resource_manager.hpp

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,11 @@ class TestableResourceManager : public hardware_interface::ResourceManager
6464
urdf, node.get_node_clock_interface(), node.get_node_logging_interface(), activate_all, 100)
6565
{
6666
}
67+
68+
explicit TestableResourceManager(const hardware_interface::ResourceManagerParams & params)
69+
: hardware_interface::ResourceManager(params, true)
70+
{
71+
}
6772
};
6873

6974
std::vector<hardware_interface::return_type> set_components_state(

0 commit comments

Comments
 (0)