Skip to content

Commit ffa4a00

Browse files
authored
Fix bad optional access in the joint limiters (#3319)
1 parent b4d3a08 commit ffa4a00

4 files changed

Lines changed: 81 additions & 11 deletions

File tree

joint_limits/src/joint_limits_helpers.cpp

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -115,16 +115,21 @@ PositionLimits compute_position_limits(
115115
: limits.max_velocity;
116116
const double max_vel = std::min(limits.max_velocity, delta_vel);
117117
const double delta_pos = max_vel * dt;
118-
/// @note: We use the previous command position to compute the limits here because using the
119-
/// actual position would be too conservative, usually there is a couple of cycles of delay
120-
/// between the command sent to the robot and the robot actually showing that in the state. That
121-
/// effectively limits the velocity with which the joint can be moved which is much lower than
122-
/// the actual velocity limit.
123-
const double position_reference = prev_command_pos.value();
124-
pos_limits.lower_limit = std::max(
125-
std::min(position_reference - delta_pos, pos_limits.upper_limit), pos_limits.lower_limit);
126-
pos_limits.upper_limit = std::min(
127-
std::max(position_reference + delta_pos, pos_limits.lower_limit), pos_limits.upper_limit);
118+
/// @note: We prefer the previous command position over actual position because using the actual
119+
/// position would be too conservative — there is typically a couple of cycles of delay between
120+
/// the command and the robot state. Fall back to actual position when no previous command
121+
/// exists (e.g., first position command after operating in another mode). Skip
122+
/// velocity-constrained narrowing entirely when neither reference is available.
123+
const std::optional<double> & pos_ref =
124+
prev_command_pos.has_value() ? prev_command_pos : act_pos;
125+
if (pos_ref.has_value())
126+
{
127+
const double position_reference = pos_ref.value();
128+
pos_limits.lower_limit = std::max(
129+
std::min(position_reference - delta_pos, pos_limits.upper_limit), pos_limits.lower_limit);
130+
pos_limits.upper_limit = std::min(
131+
std::max(position_reference + delta_pos, pos_limits.lower_limit), pos_limits.upper_limit);
132+
}
128133
}
129134
internal::check_and_swap_limits(pos_limits.lower_limit, pos_limits.upper_limit);
130135
return pos_limits;

joint_limits/src/joint_soft_limiter.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,7 @@ bool JointSoftLimiter::on_enforce(
181181
pos_high = soft_joint_limits.max_position;
182182
}
183183

184-
if (hard_limits.has_velocity_limits)
184+
if (hard_limits.has_velocity_limits && std::isfinite(prev_command_position))
185185
{
186186
pos_low = std::clamp(prev_command_position + soft_min_vel * dt_seconds, pos_low, pos_high);
187187
pos_high = std::clamp(prev_command_position + soft_max_vel * dt_seconds, pos_low, pos_high);

joint_limits/test/test_joint_range_limiter.cpp

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -749,6 +749,38 @@ TEST_F(JointSaturationLimiterTest, when_command_is_nan_expect_no_limiting)
749749
EXPECT_LE(desired_state_.position.value(), limits.max_position);
750750
}
751751

752+
TEST_F(JointSaturationLimiterTest, when_switching_command_interface_expect_no_bad_optional_access)
753+
{
754+
SetupNode("joint_saturation_limiter");
755+
ASSERT_TRUE(Load());
756+
757+
joint_limits::JointLimits limits;
758+
limits.has_position_limits = true;
759+
limits.min_position = -M_PI;
760+
limits.max_position = M_PI;
761+
limits.has_velocity_limits = true;
762+
limits.max_velocity = 1.0;
763+
ASSERT_TRUE(Init(limits));
764+
765+
// Configure with an effort-only state (no position) to simulate a controller
766+
// that was previously running in effort mode. on_configure sets
767+
// prev_command_ = current_joint_states, so prev_command_.position stays nullopt.
768+
joint_limits::JointControlInterfacesData effort_only_state;
769+
effort_only_state.joint_name = "foo_joint";
770+
effort_only_state.effort = 0.0;
771+
ASSERT_TRUE(joint_limiter_->configure(effort_only_state));
772+
773+
rclcpp::Duration period(0, 100000000); // 0.1 second
774+
775+
// Sending a position command while prev_command_.position is nullopt must not
776+
// throw bad_optional_access inside compute_position_limits.
777+
desired_state_ = {};
778+
actual_state_ = {};
779+
desired_state_.position = M_PI * 2.0; // outside limits
780+
EXPECT_NO_THROW(joint_limiter_->enforce(actual_state_, desired_state_, period));
781+
EXPECT_LE(desired_state_.position.value(), limits.max_position);
782+
}
783+
752784
int main(int argc, char ** argv)
753785
{
754786
::testing::InitGoogleMock(&argc, argv);

joint_limits/test/test_joint_soft_limiter.cpp

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1179,6 +1179,39 @@ TEST_F(JointSoftLimiterTest, when_command_is_nan_expect_no_limiting)
11791179
EXPECT_LE(desired_state_.position.value(), limits.max_position);
11801180
}
11811181

1182+
TEST_F(JointSoftLimiterTest, when_switching_command_interface_expect_no_bad_optional_access)
1183+
{
1184+
SetupNode("joint_saturation_limiter");
1185+
ASSERT_TRUE(Load());
1186+
1187+
joint_limits::JointLimits limits;
1188+
limits.has_position_limits = true;
1189+
limits.min_position = -M_PI;
1190+
limits.max_position = M_PI;
1191+
limits.has_velocity_limits = true;
1192+
limits.max_velocity = 1.0;
1193+
joint_limits::SoftJointLimits soft_limits;
1194+
ASSERT_TRUE(Init(limits, soft_limits));
1195+
1196+
// Configure with an effort-only state (no position) to simulate a controller
1197+
// that was previously running in effort mode. on_configure sets
1198+
// prev_command_ = current_joint_states, so prev_command_.position stays nullopt.
1199+
joint_limits::JointControlInterfacesData effort_only_state;
1200+
effort_only_state.joint_name = "foo_joint";
1201+
effort_only_state.effort = 0.0;
1202+
ASSERT_TRUE(joint_limiter_->configure(effort_only_state));
1203+
1204+
rclcpp::Duration period(0, 100000000); // 0.1 second
1205+
1206+
// Sending a position command while prev_command_.position is nullopt must not
1207+
// throw bad_optional_access inside compute_position_limits.
1208+
desired_state_ = {};
1209+
actual_state_ = {};
1210+
desired_state_.position = M_PI * 2.0; // outside limits
1211+
EXPECT_NO_THROW(joint_limiter_->enforce(actual_state_, desired_state_, period));
1212+
EXPECT_LE(desired_state_.position.value(), limits.max_position);
1213+
}
1214+
11821215
int main(int argc, char ** argv)
11831216
{
11841217
::testing::InitGoogleMock(&argc, argv);

0 commit comments

Comments
 (0)