@@ -593,6 +593,71 @@ TEST_F(JointSoftLimiterTest, check_desired_velocity_only_cases)
593593 test_limit_enforcing (outside_limits_pos, -1.0 , 0.0 , true );
594594}
595595
596+ TEST_F (JointSoftLimiterTest, when_velocity_limits_disabled_expect_no_velocity_enforcement)
597+ {
598+ SetupNode (" joint_saturation_limiter" );
599+ ASSERT_TRUE (Load ());
600+
601+ // velocity limits are disabled, but position and acceleration limits are still active
602+ joint_limits::JointLimits limits;
603+ limits.has_position_limits = true ;
604+ limits.min_position = -5.0 ;
605+ limits.max_position = 5.0 ;
606+ limits.has_acceleration_limits = true ;
607+ limits.max_acceleration = 0.5 ;
608+ limits.has_velocity_limits = false ;
609+ joint_limits::SoftJointLimits soft_limits;
610+ ASSERT_TRUE (Init (limits, soft_limits));
611+ last_commanded_state_ = {};
612+ ASSERT_TRUE (joint_limiter_->configure (last_commanded_state_));
613+
614+ rclcpp::Duration period (1 , 0 );
615+
616+ auto test_velocity_passes_through = [&](
617+ const std::optional<double > & actual_position,
618+ const std::optional<double > & actual_velocity,
619+ double desired_velocity)
620+ {
621+ desired_state_ = {};
622+ actual_state_ = {};
623+ const double act_pos = actual_position.has_value () ? actual_position.value ()
624+ : std::numeric_limits<double >::quiet_NaN ();
625+ const double act_vel = actual_velocity.has_value () ? actual_velocity.value ()
626+ : std::numeric_limits<double >::quiet_NaN ();
627+ SCOPED_TRACE (
628+ " Testing velocity passthrough for actual position: " + std::to_string (act_pos) +
629+ " , actual velocity: " + std::to_string (act_vel) + " , desired velocity: " +
630+ std::to_string (desired_velocity) + " for the joint limits : " + limits.to_string ());
631+ if (actual_position.has_value ())
632+ {
633+ actual_state_.position = actual_position.value ();
634+ }
635+ if (actual_velocity.has_value ())
636+ {
637+ actual_state_.velocity = actual_velocity.value ();
638+ }
639+ desired_state_.velocity = desired_velocity;
640+ ASSERT_FALSE (joint_limiter_->enforce (actual_state_, desired_state_, period));
641+ EXPECT_TRUE (desired_state_.has_velocity ());
642+ EXPECT_NEAR (desired_state_.velocity .value (), desired_velocity, COMMON_THRESHOLD );
643+ };
644+
645+ test_velocity_passes_through (0.0 , std::nullopt , 1000.0 );
646+ test_velocity_passes_through (5.0 , std::nullopt , 1000.0 );
647+ test_velocity_passes_through (5.0 , std::nullopt , -1000.0 );
648+ test_velocity_passes_through (-5.0 , std::nullopt , -1000.0 );
649+ test_velocity_passes_through (-5.0 , std::nullopt , 1000.0 );
650+
651+ // Reset prev_command_ so the next block isn't influenced
652+ ASSERT_TRUE (Init (limits, soft_limits));
653+ last_commanded_state_ = {};
654+ ASSERT_TRUE (joint_limiter_->configure (last_commanded_state_));
655+
656+ test_velocity_passes_through (0.0 , 5.0 , 1000.0 );
657+ test_velocity_passes_through (0.0 , -5.0 , -1000.0 );
658+ test_velocity_passes_through (0.0 , 0.0 , 1000.0 );
659+ }
660+
596661TEST_F (JointSoftLimiterTest, check_desired_effort_only_cases)
597662{
598663 SetupNode (" joint_saturation_limiter" );
@@ -878,6 +943,97 @@ TEST_F(JointSoftLimiterTest, check_desired_effort_only_cases)
878943 }
879944}
880945
946+ TEST_F (JointSoftLimiterTest, when_effort_limits_disabled_expect_no_effort_enforcement)
947+ {
948+ SetupNode (" joint_saturation_limiter" );
949+ ASSERT_TRUE (Load ());
950+
951+ // effort limits are OFF, but position and velocity limits are still active
952+ joint_limits::JointLimits limits;
953+ limits.has_position_limits = true ;
954+ limits.min_position = -5.0 ;
955+ limits.max_position = 5.0 ;
956+ limits.has_velocity_limits = true ;
957+ limits.max_velocity = 1.0 ;
958+ limits.has_effort_limits = false ; // <-- disabled
959+ joint_limits::SoftJointLimits soft_limits;
960+ ASSERT_TRUE (Init (limits, soft_limits));
961+ last_commanded_state_ = {};
962+ ASSERT_TRUE (joint_limiter_->configure (last_commanded_state_));
963+
964+ rclcpp::Duration period (1 , 0 ); // 1 second
965+
966+ auto test_effort_passes_through = [&](
967+ const std::optional<double > & actual_position,
968+ const std::optional<double > & actual_velocity,
969+ double desired_effort)
970+ {
971+ desired_state_ = {};
972+ actual_state_ = {};
973+ const double act_pos = actual_position.has_value () ? actual_position.value ()
974+ : std::numeric_limits<double >::quiet_NaN ();
975+ const double act_vel = actual_velocity.has_value () ? actual_velocity.value ()
976+ : std::numeric_limits<double >::quiet_NaN ();
977+ SCOPED_TRACE (
978+ " Testing effort passthrough for actual position: " + std::to_string (act_pos) +
979+ " , actual velocity: " + std::to_string (act_vel) + " , desired effort: " +
980+ std::to_string (desired_effort) + " for the joint limits : " + limits.to_string ());
981+ if (actual_position.has_value ())
982+ {
983+ actual_state_.position = actual_position.value ();
984+ }
985+ if (actual_velocity.has_value ())
986+ {
987+ actual_state_.velocity = actual_velocity.value ();
988+ }
989+ desired_state_.effort = desired_effort;
990+ // With effort limits disabled, enforce() must NOT clamp effort regardless of
991+ // the position/velocity state it must pass through completely untouched.
992+ ASSERT_FALSE (joint_limiter_->enforce (actual_state_, desired_state_, period));
993+ EXPECT_TRUE (desired_state_.has_effort ());
994+ EXPECT_NEAR (desired_state_.effort .value (), desired_effort, COMMON_THRESHOLD );
995+ };
996+
997+ // Normal operating region: any effort must pass through unchanged
998+ test_effort_passes_through (0.0 , 0.0 , 10000.0 );
999+ test_effort_passes_through (0.0 , 0.0 , -10000.0 );
1000+
1001+ // At max position + zero velocity (old code zeroed upper_limit to 0.0 here)
1002+ test_effort_passes_through (5.0 , 0.0 , 10000.0 );
1003+ test_effort_passes_through (5.0 , 0.0 , -10000.0 );
1004+ // Beyond max position
1005+ test_effort_passes_through (6.0 , 0.0 , 10000.0 );
1006+ test_effort_passes_through (6.0 , 0.0 , -10000.0 );
1007+
1008+ // At max position + positive velocity (moving further out)
1009+ test_effort_passes_through (5.0 , 0.2 , 10000.0 );
1010+ test_effort_passes_through (5.0 , 0.2 , -10000.0 );
1011+
1012+ // At min position + zero velocity (old code zeroed lower_limit to 0.0 here)
1013+ test_effort_passes_through (-5.0 , 0.0 , 10000.0 );
1014+ test_effort_passes_through (-5.0 , 0.0 , -10000.0 );
1015+ // At min position + negative velocity (moving further out)
1016+ test_effort_passes_through (-5.0 , -0.2 , 10000.0 );
1017+ test_effort_passes_through (-5.0 , -0.2 , -10000.0 );
1018+
1019+ // Over max velocity (old code zeroed upper_limit to 0.0 here)
1020+ test_effort_passes_through (0.0 , 1.5 , 10000.0 );
1021+ test_effort_passes_through (0.0 , 1.5 , -10000.0 );
1022+ // Over negative max velocity
1023+ test_effort_passes_through (0.0 , -1.5 , 10000.0 );
1024+ test_effort_passes_through (0.0 , -1.5 , -10000.0 );
1025+
1026+ // Also verify with k_velocity set: soft effort shaping must also be skipped
1027+ soft_limits.k_velocity = 5000.0 ;
1028+ ASSERT_TRUE (Init (limits, soft_limits));
1029+ last_commanded_state_ = {};
1030+ ASSERT_TRUE (joint_limiter_->configure (last_commanded_state_));
1031+ test_effort_passes_through (0.0 , 0.5 , 10000.0 );
1032+ test_effort_passes_through (0.0 , 0.5 , -10000.0 );
1033+ test_effort_passes_through (5.0 , 0.2 , 10000.0 );
1034+ test_effort_passes_through (5.0 , 0.2 , -10000.0 );
1035+ }
1036+
8811037TEST_F (JointSoftLimiterTest, check_desired_acceleration_only_cases)
8821038{
8831039 SetupNode (" joint_saturation_limiter" );
0 commit comments