Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
287161f
fix: add missing cmath header for strict build
Ishan1923 Jan 7, 2026
86329ba
reproduce crash with boolean iterface (issue #1970).
Ishan1923 Jan 9, 2026
89bbdcb
fix crash on NaN state values and sanitize inputs
Ishan1923 Jan 12, 2026
5517039
style: address review comments (remove comments, rename test)
Ishan1923 Jan 21, 2026
440efce
Merge branch 'master' into fix/gpio-issue-1970-clean
Ishan1923 Jan 22, 2026
2b07827
Merge branch 'master' into fix/gpio-issue-1970-clean
christophfroehlich Jan 29, 2026
7ad4006
fix(gpio): handle double interfaces safely and fix tests
Ishan1923 Feb 1, 2026
f1403c5
fix: use explicit HandleDataType checks and apply pre-commit
Ishan1923 Feb 1, 2026
812ebea
Merge branch 'master' into fix/gpio-issue-1970-clean
Ishan1923 Feb 1, 2026
6ae8c35
Merge branch 'master' into fix/gpio-issue-1970-clean
Ishan1923 Feb 8, 2026
a9e90b1
fix: use unordered_set for logging and remove unsafe to_string call
Ishan1923 Feb 8, 2026
63c5c83
Merge branch 'master' into fix/gpio-issue-1970-clean
christophfroehlich Feb 10, 2026
c4c0847
Merge branch 'master' into fix/gpio-issue-1970-clean
christophfroehlich Feb 11, 2026
f7a84b2
fix previously introduced ABI break in gpio_controllers
Ishan1923 Feb 16, 2026
a1c2d8b
Merge branch 'fix/gpio-issue-1970-clean' of https://github.com/Ishan1…
Ishan1923 Feb 16, 2026
c5e14b6
Merge branch 'master' into fix/gpio-issue-1970-clean
Ishan1923 Mar 27, 2026
8f55ee9
fix: resolve variable shadowing in apply_command and add boolean test
Ishan1923 Apr 10, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions gpio_controllers/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ if(BUILD_TESTING)
target_link_libraries(test_gpio_command_controller
gpio_controllers
ros2_control_test_assets::ros2_control_test_assets
gpio_command_controller_parameters
)
endif()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,7 @@ class GpioCommandController : public controller_interface::ControllerInterface
void apply_state_value(
StateType & state_msg, std::size_t gpio_index, std::size_t interface_index) const;
void apply_command(
const CmdType & gpio_commands, std::size_t gpio_index,
std::size_t command_interface_index) const;
const CmdType & gpio_commands, std::size_t gpio_index, std::size_t command_interface_index);
bool should_broadcast_all_interfaces_of_configured_gpios() const;
void set_all_state_interfaces_of_configured_gpios();
InterfacesNames get_gpios_state_interfaces_names(const std::string & gpio_name) const;
Expand Down
64 changes: 52 additions & 12 deletions gpio_controllers/src/gpio_command_controller.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
#include "gpio_controllers/gpio_command_controller.hpp"

#include <algorithm>
#include <cmath>

#include "controller_interface/helpers.hpp"
#include "hardware_interface/component_parser.hpp"
Expand Down Expand Up @@ -53,6 +54,7 @@ std::vector<hardware_interface::ComponentInfo> extract_gpios_from_hardware_info(
}
return result;
}

} // namespace

namespace gpio_controllers
Expand Down Expand Up @@ -362,7 +364,7 @@ controller_interface::return_type GpioCommandController::update_gpios_commands()
}

void GpioCommandController::apply_command(
const CmdType & gpio_commands, std::size_t gpio_index, std::size_t command_interface_index) const
const CmdType & gpio_commands, std::size_t gpio_index, std::size_t command_interface_index)
{
const auto full_command_interface_name =
gpio_commands.interface_groups[gpio_index] + '/' +
Expand All @@ -373,11 +375,34 @@ void GpioCommandController::apply_command(

try
{
if (!command_interfaces_map_.at(full_command_interface_name).get().set_value(command_value))
auto & interface = command_interfaces_map_.at(full_command_interface_name).get();
const auto & type = interface.get_data_type();
if (type == hardware_interface::HandleDataType::DOUBLE)
{
if (!interface.set_value<double>(command_value))
{
RCLCPP_WARN(
get_node()->get_logger(), "Unable to set the command for interface '%s' with value '%f'.",
full_command_interface_name.c_str(), command_value);
}
}
else if (type == hardware_interface::HandleDataType::BOOL)
{
const bool bool_command_value =
std::abs(command_value) > std::numeric_limits<double>::epsilon();
if (!interface.set_value<bool>(bool_command_value))
{
RCLCPP_WARN(
get_node()->get_logger(), "Unable to set the command for interface '%s' with value '%f'.",
full_command_interface_name.c_str(), command_value);
}
}
else
{
RCLCPP_WARN(
get_node()->get_logger(), "Unable to set the command for interface '%s' with value '%f'.",
full_command_interface_name.c_str(), command_value);
get_node()->get_logger(),
"Interface '%s' has unsupported type. Only 'double' and 'bool' are supported.",
full_command_interface_name.c_str());
}
}
catch (const std::exception & e)
Expand Down Expand Up @@ -417,20 +442,35 @@ void GpioCommandController::apply_state_value(
state_msg.interface_values[gpio_index].interface_names[interface_index];
try
{
auto state_msg_interface_value_op =
state_interfaces_map_.at(interface_name).get().get_optional();
double state_value = std::numeric_limits<double>::quiet_NaN();
const auto & interface = state_interfaces_map_.at(interface_name).get();
const auto & type = interface.get_data_type();

if (!state_msg_interface_value_op.has_value())
if (type == hardware_interface::HandleDataType::DOUBLE)
{
RCLCPP_DEBUG(
get_node()->get_logger(), "Unable to retrieve the data from state: %s \n",
interface_name.c_str());
const auto val_opt = interface.get_optional<double>();
if (val_opt.has_value())
{
state_value = val_opt.value();
}
}
else if (type == hardware_interface::HandleDataType::BOOL)
{
const auto val_opt = interface.get_optional<bool>();
if (val_opt.has_value())
{
state_value = val_opt.value() ? 1.0 : 0.0;
}
}
else
{
state_msg.interface_values[gpio_index].values[interface_index] =
state_msg_interface_value_op.value();
RCLCPP_WARN_THROTTLE(
get_node()->get_logger(), *get_node()->get_clock(), 10000,
"Interface '%s' has unsupported type. Only 'double' and 'bool' are supported.",
interface_name.c_str());
}

state_msg.interface_values[gpio_index].values[interface_index] = state_value;
}
catch (const std::exception & e)
{
Expand Down
124 changes: 124 additions & 0 deletions gpio_controllers/test/test_gpio_command_controller.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,8 @@ class FriendGpioCommandController : public gpio_controllers::GpioCommandControll
FRIEND_TEST(
GpioCommandControllerTestSuite,
WhenGivenCmdContainsWrongGpioInterfacesOrWrongGpioNameThenCommandInterfacesShouldNotBeUpdated);
FRIEND_TEST(GpioCommandControllerTestSuite, UpdateBoolGpioInterfaces);
FRIEND_TEST(GpioCommandControllerTestSuite, UpdateDoubleGpioInterfaces);
};

class GpioCommandControllerTestSuite : public ::testing::Test
Expand Down Expand Up @@ -708,3 +710,125 @@ TEST_F(
ASSERT_EQ(gpio_state_msg.interface_values.at(0).values.at(0), 1.0);
ASSERT_EQ(gpio_state_msg.interface_values.at(1).values.at(0), 3.1);
}

TEST_F(GpioCommandControllerTestSuite, UpdateDoubleGpioInterfaces)
{
const auto node_options = create_node_options_with_overriden_parameters(
{{"gpios", std::vector<std::string>{"gpio1"}},
{"command_interfaces.gpio1.interfaces", std::vector<std::string>{"dig_out_1"}},
{"state_interfaces.gpio1.interfaces", std::vector<std::string>{"dig_in_1"}}});

ASSERT_EQ(
controller_->init(create_ctrl_params(node_options)), controller_interface::return_type::OK);
ASSERT_EQ(
controller_->on_configure(rclcpp_lifecycle::State()),
controller_interface::CallbackReturn::SUCCESS);

double dummy_double_value_cmd = 0.0;
double dummy_double_value_state = 0.0;

auto cmd_intf = std::make_shared<CommandInterface>("gpio1", "dig_out_1", &dummy_double_value_cmd);
std::vector<LoanedCommandInterface> cmd_loaned;
cmd_loaned.emplace_back(cmd_intf, nullptr);

auto state_intf =
std::make_shared<StateInterface>("gpio1", "dig_in_1", &dummy_double_value_state);
std::vector<LoanedStateInterface> state_loaned;
state_loaned.emplace_back(state_intf, nullptr);

controller_->assign_interfaces(std::move(cmd_loaned), std::move(state_loaned));

ASSERT_EQ(
controller_->on_activate(rclcpp_lifecycle::State()),
controller_interface::CallbackReturn::SUCCESS);

CmdType cmd_msg;
cmd_msg.interface_groups = {"gpio1"};
cmd_msg.interface_values.resize(1);
cmd_msg.interface_values.at(0).interface_names = {"dig_out_1"};
cmd_msg.interface_values.at(0).values = {1.0};

auto publisher = node->create_publisher<CmdType>(
std::string(controller_->get_node()->get_name()) + "/commands", 10);
publisher->publish(cmd_msg);

publisher->publish(cmd_msg);
wait_one_miliseconds_for_timeout();
controller_->update(rclcpp::Time(0), rclcpp::Duration::from_seconds(0.01));

ASSERT_EQ(dummy_double_value_cmd, 1.0);

auto state_subscription = node->create_subscription<StateType>(
std::string(controller_->get_node()->get_name()) + "/gpio_states", 10,
[&](const StateType::SharedPtr) {});

stop_test_when_message_cannot_be_published(wait_for_subscription(state_subscription));

StateType gpio_state_msg;
rclcpp::MessageInfo msg_info;
ASSERT_TRUE(state_subscription->take(gpio_state_msg, msg_info));
ASSERT_EQ(gpio_state_msg.interface_values.at(0).values.at(0), 0.0);

// This verifies that the controller no longer crashes on update
EXPECT_NO_THROW(controller_->update(rclcpp::Time(0), rclcpp::Duration::from_seconds(0.01)));
}

TEST_F(GpioCommandControllerTestSuite, UpdateBoolGpioInterfaces)
{
const auto node_options = create_node_options_with_overriden_parameters(
{{"gpios", std::vector<std::string>{"gpio1"}},
{"command_interfaces.gpio1.interfaces", std::vector<std::string>{"dig_out_1"}},
{"state_interfaces.gpio1.interfaces", std::vector<std::string>{"dig_in_1"}}});

ASSERT_EQ(
controller_->init(create_ctrl_params(node_options)), controller_interface::return_type::OK);
ASSERT_EQ(
controller_->on_configure(rclcpp_lifecycle::State()),
controller_interface::CallbackReturn::SUCCESS);

auto cmd_intf = std::make_shared<CommandInterface>("gpio1", "dig_out_1", "bool", "false");
std::vector<LoanedCommandInterface> cmd_loaned;
cmd_loaned.emplace_back(cmd_intf, nullptr);

auto state_intf = std::make_shared<StateInterface>("gpio1", "dig_in_1", "bool", "true");
std::vector<LoanedStateInterface> state_loaned;
state_loaned.emplace_back(state_intf, nullptr);

controller_->assign_interfaces(std::move(cmd_loaned), std::move(state_loaned));

ASSERT_EQ(
controller_->on_activate(rclcpp_lifecycle::State()),
controller_interface::CallbackReturn::SUCCESS);

CmdType cmd_msg;
cmd_msg.interface_groups = {"gpio1"};
cmd_msg.interface_values.resize(1);
cmd_msg.interface_values.at(0).interface_names = {"dig_out_1"};
cmd_msg.interface_values.at(0).values = {1.0};

auto publisher = node->create_publisher<CmdType>(
std::string(controller_->get_node()->get_name()) + "/commands", 10);
publisher->publish(cmd_msg);

publisher->publish(cmd_msg);
wait_one_miliseconds_for_timeout();
controller_->update(rclcpp::Time(0), rclcpp::Duration::from_seconds(0.01));

const auto result = cmd_intf->get_optional<bool>();
ASSERT_TRUE(result.has_value());
ASSERT_TRUE(result.value());

auto state_subscription = node->create_subscription<StateType>(
std::string(controller_->get_node()->get_name()) + "/gpio_states", 10,
[&](const StateType::SharedPtr) {});

stop_test_when_message_cannot_be_published(wait_for_subscription(state_subscription));

StateType gpio_state_msg;
rclcpp::MessageInfo msg_info;
ASSERT_TRUE(state_subscription->take(gpio_state_msg, msg_info));
ASSERT_EQ(gpio_state_msg.interface_values.at(0).values.at(0), 1.0);

// This verifies that the controller no longer crashes on update
EXPECT_NO_THROW(controller_->update(rclcpp::Time(0), rclcpp::Duration::from_seconds(0.01)));
}
Loading