Skip to content

Commit edeb343

Browse files
Increase PID ROS wrapper test coverage (#484)
1 parent 6a28f23 commit edeb343

2 files changed

Lines changed: 348 additions & 11 deletions

File tree

control_toolbox/test/pid_ros_parameters_tests.cpp

Lines changed: 250 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -615,6 +615,256 @@ TEST(PidParametersTest, GetParametersFromParams)
615615
EXPECT_EQ(param_activate_state_publisher.get_value<bool>(), ACTIVATE_STATE_PUBLISHER);
616616
}
617617

618+
TEST(PidParametersTest, ResetReadsSaveITermParam)
619+
{
620+
auto node = std::make_shared<rclcpp::Node>("pidros_reset_param_test");
621+
TestablePidROS pid(node, "", "", false);
622+
623+
// I-only controller; declare params via initialize_from_args
624+
AntiWindupStrategy anti;
625+
anti.type = AntiWindupStrategy::NONE;
626+
const double U_MAX = 1e9, U_MIN = -1e9;
627+
ASSERT_TRUE(pid.initialize_from_args(0.0, 1.0, 0.0, U_MAX, U_MIN, anti, /*save_i_term=*/false));
628+
629+
const auto dt = rclcpp::Duration::from_seconds(1.0);
630+
631+
// Prime integral to a known value (internal I = 2 after two steps)
632+
(void)pid.compute_command(1.0, dt);
633+
(void)pid.compute_command(1.0, dt);
634+
// Snapshot current integral without changing it
635+
const double I_after_prime = pid.compute_command(0.0, dt);
636+
EXPECT_GT(I_after_prime, 0.0);
637+
638+
// ---- Case 1: save_i_term=false → clear on reset() ----
639+
if (!node->has_parameter("save_i_term"))
640+
{
641+
node->declare_parameter<bool>("save_i_term", false);
642+
}
643+
else
644+
{
645+
node->set_parameter(rclcpp::Parameter("save_i_term", false));
646+
}
647+
pid.reset();
648+
649+
const double cmd_after_clear = pid.compute_command(0.0, dt);
650+
EXPECT_DOUBLE_EQ(0.0, cmd_after_clear);
651+
652+
// Re-prime and snapshot again
653+
(void)pid.compute_command(1.0, dt);
654+
(void)pid.compute_command(1.0, dt);
655+
const double I_before_retain = pid.compute_command(0.0, dt);
656+
EXPECT_GT(I_before_retain, 0.0);
657+
658+
// ---- Case 2: save_i_term=true → retain on reset() ----
659+
node->set_parameter(rclcpp::Parameter("save_i_term", true));
660+
pid.reset();
661+
662+
const double cmd_after_retain = pid.compute_command(0.0, dt);
663+
EXPECT_DOUBLE_EQ(I_before_retain, cmd_after_retain);
664+
}
665+
666+
TEST(PidParametersTest, ResetBoolClearsOrRetainsITerm)
667+
{
668+
auto node = std::make_shared<rclcpp::Node>("pidros_reset_bool_test");
669+
TestablePidROS pid(node, "", "", false);
670+
671+
AntiWindupStrategy anti;
672+
anti.type = AntiWindupStrategy::NONE;
673+
const double U_MAX = 1e9, U_MIN = -1e9;
674+
ASSERT_TRUE(pid.initialize_from_args(0.0, 1.0, 0.0, U_MAX, U_MIN, anti, /*save_i_term=*/false));
675+
676+
const auto dt = rclcpp::Duration::from_seconds(1.0);
677+
678+
// Prime and snapshot
679+
(void)pid.compute_command(1.0, dt);
680+
(void)pid.compute_command(1.0, dt);
681+
const double I_snapshot = pid.compute_command(0.0, dt);
682+
EXPECT_GT(I_snapshot, 0.0);
683+
684+
// reset(false) clears integral
685+
pid.reset(false);
686+
const double after_clear = pid.compute_command(0.0, dt);
687+
EXPECT_DOUBLE_EQ(0.0, after_clear);
688+
689+
// Re-prime and snapshot again
690+
(void)pid.compute_command(1.0, dt);
691+
(void)pid.compute_command(1.0, dt);
692+
const double I_snapshot2 = pid.compute_command(0.0, dt);
693+
EXPECT_GT(I_snapshot2, 0.0);
694+
695+
// reset(true) retains integral
696+
pid.reset(true);
697+
const double after_retain = pid.compute_command(0.0, dt);
698+
EXPECT_DOUBLE_EQ(I_snapshot2, after_retain);
699+
}
700+
701+
TEST(PidParametersTest, PrintValuesLogsExpectedContent)
702+
{
703+
// --- Set up a logger capture (rcutils) ---
704+
static std::mutex g_mutex;
705+
static std::string g_last_log; // store the last message from our logger
706+
static rcutils_logging_output_handler_t prev_handler = nullptr;
707+
708+
const char * kLoggerName = "pid_print_test";
709+
710+
auto capture_handler = [](
711+
const rcutils_log_location_t * /*location*/, int /*severity*/,
712+
const char * name, rcutils_time_point_value_t /*stamp*/,
713+
const char * format, va_list * args)
714+
{
715+
// Only capture our logger's output
716+
if (!name || std::string(name) != "pid_print_test")
717+
{
718+
return;
719+
}
720+
char buf[8192];
721+
vsnprintf(buf, sizeof(buf), format, *args);
722+
std::lock_guard<std::mutex> lk(g_mutex);
723+
g_last_log = buf;
724+
};
725+
726+
// Ensure our logger emits INFO
727+
rcutils_logging_set_logger_level(kLoggerName, RCUTILS_LOG_SEVERITY_INFO);
728+
729+
// Swap in our capture handler
730+
prev_handler = rcutils_logging_get_output_handler();
731+
rcutils_logging_set_output_handler(capture_handler);
732+
733+
// --- Arrange a deterministic PID state ---
734+
auto node = std::make_shared<rclcpp::Node>(kLoggerName);
735+
TestablePidROS pid(
736+
node, /*param_prefix=*/"", /*topic_prefix=*/"", /*activate_state_publisher=*/false);
737+
738+
control_toolbox::AntiWindupStrategy anti;
739+
anti.type = control_toolbox::AntiWindupStrategy::NONE; // simpler: avoid saturation dynamics
740+
anti.i_max = 7.0;
741+
anti.i_min = -7.0;
742+
anti.tracking_time_constant = 0.3;
743+
744+
const double U_MAX = 1e9, U_MIN = -1e9; // avoid clamping
745+
ASSERT_TRUE(pid.initialize_from_args(/*p=*/1.0, /*i=*/1.0, /*d=*/0.0, U_MAX, U_MIN, anti,
746+
/*save_i_term=*/false));
747+
748+
// Make the internal errors non-trivial
749+
const auto dt = rclcpp::Duration::from_seconds(0.2);
750+
(void)pid.compute_command(/*error=*/2.0, dt);
751+
(void)pid.compute_command(/*error=*/2.0, dt);
752+
753+
// --- Act: call the function under test ---
754+
{
755+
std::lock_guard<std::mutex> lk(g_mutex);
756+
g_last_log.clear();
757+
}
758+
pid.print_values();
759+
760+
// --- Assert: captured log has expected content ---
761+
std::string captured;
762+
{
763+
std::lock_guard<std::mutex> lk(g_mutex);
764+
captured = g_last_log;
765+
}
766+
767+
// Basic header
768+
EXPECT_NE(captured.find("Current Values of PID template:"), std::string::npos);
769+
770+
// Gains (format tolerant)
771+
EXPECT_NE(captured.find("P Gain:"), std::string::npos);
772+
EXPECT_NE(captured.find("I Gain:"), std::string::npos);
773+
EXPECT_NE(captured.find("D Gain:"), std::string::npos);
774+
775+
// I bounds and output limits labels
776+
EXPECT_NE(captured.find("I Max:"), std::string::npos);
777+
EXPECT_NE(captured.find("I Min:"), std::string::npos);
778+
EXPECT_NE(captured.find("U_Max:"), std::string::npos);
779+
EXPECT_NE(captured.find("U_Min:"), std::string::npos);
780+
781+
// Anti-windup summary
782+
EXPECT_NE(captured.find("Tracking_Time_Constant:"), std::string::npos);
783+
EXPECT_NE(captured.find("Antiwindup_Strategy:"), std::string::npos);
784+
785+
// Runtime state
786+
EXPECT_NE(captured.find("P Error:"), std::string::npos);
787+
EXPECT_NE(captured.find("I Term:"), std::string::npos);
788+
EXPECT_NE(captured.find("D Error:"), std::string::npos);
789+
EXPECT_NE(captured.find("Command:"), std::string::npos);
790+
791+
// Spot-check a couple of actual numeric values that should be stable
792+
// (avoid strict layout assumptions)
793+
EXPECT_NE(captured.find("P Gain: 1"), std::string::npos);
794+
EXPECT_NE(captured.find("I Gain: 1"), std::string::npos);
795+
EXPECT_NE(captured.find("D Gain: 0"), std::string::npos);
796+
797+
// Restore previous handler so other tests aren't affected
798+
rcutils_logging_set_output_handler(prev_handler);
799+
}
800+
801+
TEST(PidParametersTest, GetCurrentCmdTracksSetAndCompute)
802+
{
803+
auto node = std::make_shared<rclcpp::Node>("pidros_get_current_cmd_test");
804+
805+
// Simple, deterministic P-only controller with tight output limits to exercise saturation.
806+
control_toolbox::AntiWindupStrategy anti;
807+
anti.type = control_toolbox::AntiWindupStrategy::NONE;
808+
const double U_MAX = 5.0, U_MIN = -5.0;
809+
810+
TestablePidROS pid(node, "", "", false);
811+
ASSERT_TRUE(pid.initialize_from_args(2.0, 0.0, 0.0, U_MAX, U_MIN, anti, false));
812+
813+
// 1) After initialization, current command should be zero.
814+
EXPECT_DOUBLE_EQ(0.0, pid.get_current_cmd());
815+
816+
// 2) set_current_cmd should be reflected by get_current_cmd (simple delegation/round-trip).
817+
pid.set_current_cmd(3.14159);
818+
EXPECT_DOUBLE_EQ(3.14159, pid.get_current_cmd());
819+
820+
// 3) compute_command should update the "current command" to the last applied command.
821+
// With P=2.0, error=10 -> raw = 20, but saturated to +5.0 by [U_MIN, U_MAX].
822+
const auto dt = rclcpp::Duration::from_seconds(0.1);
823+
const double cmd1 = pid.compute_command(/*error=*/10.0, dt);
824+
EXPECT_DOUBLE_EQ(5.0, cmd1);
825+
EXPECT_DOUBLE_EQ(cmd1, pid.get_current_cmd());
826+
827+
// Negative side saturation: error=-10 -> raw = -20, saturated to -5.0.
828+
const double cmd2 = pid.compute_command(/*error=*/-10.0, dt);
829+
EXPECT_DOUBLE_EQ(-5.0, cmd2);
830+
EXPECT_DOUBLE_EQ(cmd2, pid.get_current_cmd());
831+
832+
// 4) Large but finite set_current_cmd round-trip (sanity).
833+
pid.set_current_cmd(-1e12);
834+
EXPECT_DOUBLE_EQ(-1e12, pid.get_current_cmd());
835+
}
836+
837+
TEST(PidParametersTest, SetCurrentCmdStoresValueAndKeepsComputeStable)
838+
{
839+
auto node = std::make_shared<rclcpp::Node>("pidros_set_current_cmd_basic_test");
840+
841+
// Simple P-only controller with tight limits so compute() is always finite.
842+
control_toolbox::AntiWindupStrategy anti;
843+
anti.type = control_toolbox::AntiWindupStrategy::NONE;
844+
const double U_MAX = 5.0, U_MIN = -5.0;
845+
846+
TestablePidROS pid(node, "", "", false);
847+
ASSERT_TRUE(pid.initialize_from_args(2.0, 0.0, 0.0, U_MAX, U_MIN, anti, false));
848+
849+
// 1) Basic round-trip: set -> get
850+
pid.set_current_cmd(1.23);
851+
EXPECT_DOUBLE_EQ(1.23, pid.get_current_cmd());
852+
853+
pid.set_current_cmd(-4.56);
854+
EXPECT_DOUBLE_EQ(-4.56, pid.get_current_cmd());
855+
856+
// 2) Extreme-but-finite value should round-trip
857+
const double huge = 1e12;
858+
pid.set_current_cmd(huge);
859+
EXPECT_DOUBLE_EQ(huge, pid.get_current_cmd());
860+
861+
// 3) After setting an arbitrary current command, compute_command() should still be stable/finite.
862+
const auto dt = rclcpp::Duration::from_seconds(0.1);
863+
const double cmd = pid.compute_command(/*error=*/3.0, dt); // raw = 6.0 -> clamped to +5.0
864+
EXPECT_DOUBLE_EQ(5.0, cmd);
865+
EXPECT_DOUBLE_EQ(cmd, pid.get_current_cmd());
866+
}
867+
618868
TEST(PidParametersTest, MultiplePidInstances)
619869
{
620870
rclcpp::Node::SharedPtr node = std::make_shared<rclcpp::Node>("multiple_pid_instances");

0 commit comments

Comments
 (0)