@@ -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+
618868TEST (PidParametersTest, MultiplePidInstances)
619869{
620870 rclcpp::Node::SharedPtr node = std::make_shared<rclcpp::Node>(" multiple_pid_instances" );
0 commit comments