Practice¶
In the first part of the practice, we will use a first and second-order system example, apply a PID controller, and then tune it. In the second part of the practice, we will review and tune the operation of a simulated trajectory-following robot/vehicle.
Task 1
: Trajectory following with simulationTask 2
: Custom controller and vehicle modelTask 3
: PID tuning
Task 1
: Trajectory following with simulation¶
github.com/jkk-research/sim_wayp_plan_tools
Requirements¶
For the practice to run smoothly, the following programs need to be installed:
- ROS 2 Humble: docs.ros.org/en/humble/Installation.html
- Gazebo Fortress: gazebosim.org/docs/fortress/install_ubuntu, More information on integration: gazebosim.org/docs/fortress/ros2_integration
- ros-gz-bridge
can be installed with one command: sudo apt install ros-humble-ros-gz-bridge
- Ensure that colcon_cd
is properly installed. CSV files are loaded with colcon_cd
.
Packages and build¶
The default workspace should be: ~/ros2_ws/
.
Clone the packages¶
cd ~/ros2_ws/src
git clone https://github.com/jkk-research/wayp_plan_tools
git clone https://github.com/jkk-research/sim_wayp_plan_tools
Building ROS 2 packages¶
cd ~/ros2_ws
colcon build --packages-select wayp_plan_tools sim_wayp_plan_tools
Using wayp_plan_tools
as a simulator¶
1.1. Starting gazebo¶
ign gazebo -v 4 -r ackermann_steering.sdf
1.2. Starting the Gazebo bridge¶
If the bridge is not installed, the following commands will help:
sudo apt update
sudo apt install ros-humble-ros-gz -y
In the classroom:
cd /mnt/kozos/script
./gz_bridge.sh
Don't forget to source
before running ROS commands.
source ~/ros2_ws/install/local_setup.bash
ros2 launch sim_wayp_plan_tools gazebo_bridge.launch.py
This launch
file starts the following nodes together:
ros2 run ros_gz_bridge parameter_bridge /world/ackermann_steering/pose/info@geometry_msgs/msg/PoseArray[ignition.msgs.Pose_V
ros2 run ros_gz_bridge parameter_bridge /model/vehicle_blue/cmd_vel@geometry_msgs/msg/Twist]ignition.msgs.Twist
ros2 run ros_gz_bridge parameter_bridge /model/vehicle_blue/odometry@nav_msgs/msg/Odometry[ignition.msgs.Odometry --ros-args -r /model/vehicle_blue/odometry:=/odom
More information about the bridge: github.com/gazebosim/ros_gz/blob/ros2/ros_gz_bridge/README.md
This launch
also creates a /tf
from the PoseArray
with pose_arr_to_tf
.
Optional: Controlling the robot in Gazebo with the keyboard:¶
ros2 run teleop_twist_keyboard teleop_twist_keyboard --ros-args -r /cmd_vel:=/model/vehicle_blue/cmd_vel
1.3. Loading waypoints¶
Note: Waypoints are a set of points that contain the position, orientation, and speed data of the route at discrete points. These data are typically extracted by recording the x, y, and possibly z coordinates, the orientation pointing to the next point relative to the current one, and the current speed data during our journey in ROS. Finally, the aforementioned data are recorded in CSV files.
Use the ROS 2 workspace as file_dir
:
ros2 run wayp_plan_tools waypoint_loader --ros-args -p file_name:=sim_waypoints1.csv -p file_dir:=$HOME/ros2_ws/src/sim_wayp_plan_tools/csv -r __ns:=/sim1
Or with default parameters:
ros2 launch sim_wayp_plan_tools waypoint_loader.launch.py
1.4. Waypoint as goal pose¶
As shown in the diagrams in Chapter 4 of the theoretical part, each control algorithm has one or more goal poses that the current controller regulates.
ros2 run wayp_plan_tools waypoint_to_target --ros-args -p lookahead_min:=2.5 -p lookahead_max:=4.5 -p mps_alpha:=1.5 -p mps_beta:=3.5 -p waypoint_topic:=waypointarray -p tf_frame_id:=base_link -p tf_child_frame_id:=map -r __ns:=/sim1
Or with default parameters:
ros2 launch sim_wayp_plan_tools waypoint_to_target.launch.py
1.5. Starting the control:¶
Several options:
- single_goal_pursuit
: Pure pursuit (for vehicles/robots), a simple cross-track error method
- multiple_goal_pursuit
: Multiple goal pursuit for vehicles/robots, an implementation of our paper
- stanley_control
: Stanley controller, a heading error + cross-track error method
- follow_the_carrot
: Follow-the-carrot, the simplest controller
An example for pure pursuit:
ros2 run wayp_plan_tools single_goal_pursuit --ros-args -p cmd_topic:=/model/vehicle_blue/cmd_vel -p wheelbase:=1.0 -p waypoint_topic:=targetpoints -r __ns:=/sim1
Or with default parameters:
ros2 launch sim_wayp_plan_tools single_goal_pursuit.launch.py
1.6. Visualizing the results in RViz2
:¶
ros2 launch sim_wayp_plan_tools rviz1.launch.py
Or run everything together with a single command:
After ign gazebo -v 4 -r ackermann_steering.sdf
(terminal 1) and source ~/ros2_ws/install/local_setup.bash
(terminal 2), run this command (also in terminal 2):
ros2 launch sim_wayp_plan_tools all_in_once.launch.py
Troubleshooting¶
Stopping the ign gazebo server
:
ps aux | grep ign
ab 12345 49.9 1.2 2412624 101608 ? Sl 08:26 27:20 ign gazebo server
ab 12346 518 6.6 10583664 528352 ? Sl 08:26 283:45 ign gazebo gui
ab 12347 0.0 0.0 9396 2400 pts/2 S+ 09:21 0:00 grep --color=auto ign
If the PID is identified, use the kill command to stop the process. For example, to stop the gazebo server:
kill 12345
Task 2
: Custom controller and vehicle model¶
In this task, we will create the speed controller presented in the theory and the associated simple vehicle model.
If not done already, update the arj_packages repository:
git pull
Navigate to the repo's root directory in the workspace src folder:
cd ~ros2_ws/src/arj_packages
Examine the repository content:
dir
speed_control_loop
has appeared. This folder contains the vehicle model and the controller used for regulation. Let's open the source code using VS Code.
The folder contains the usual package.xml and CMakeLists.txt, as well as two cpp source files. The vehicle_model.cpp
naturally contains the vehicle model, and the speed_controller.cpp
contains the controller. Let's first examine the vehicle model source code!
Vehicle Model¶
class VehicleModel : public rclcpp::Node
{
public:
VehicleModel() : Node("vehicle_model")
{
timer_ = this->create_wall_timer(std::chrono::milliseconds(200), std::bind(&VehicleModel::loop, this));
state_pub_ = this->create_publisher<std_msgs::msg::Float32MultiArray>("/vehicle/state", 10);
cmd_sub_ = this->create_subscription<std_msgs::msg::Float32>("/vehicle/propulsion", 10, std::bind(&VehicleModel::propulsion_callback, this, std::placeholders::_1));
RCLCPP_INFO(this->get_logger(), "vehicle_model has been started");
}
After the usual #includes and namespace definitions, we see the constructor of the vehicle model class. The node's name is "vehicle_model". We subscribe to a topic named /vehicle/propulsion
, which, as the name suggests, provides the propulsion force acting on the vehicle. Additionally, we advertise the /vehicle/state
topic, which provides the vehicle's motion state.
Next, we define some variables.
1. First, a local variable to store the input force.
2. Then, an array to contain the vehicle's speed and acceleration, the two state variables that describe the vehicle's state.
3. We define a variable named Fload
to specify any extra loads acting on the vehicle.
4. Finally, we define some immutable parameters, such as the vehicle's weight, frontal area, etc.
private:
// input command
float Fprop {0.0f};
// vehicle state array
std::vector<float> state; //speed, acceleration
float vx{0.0f};
float ax{0.0f};
// load
float Fload{0.0f};
// params
float m {1350.0}; // kg
float A {1.5f}; // m^2
float rho {1.0f}; // kg/m^3
float c {0.33f}; // aerodynamic factor
float b {0.1f}; // rolling friction, viscosous
The topic callback function simply copies the incoming data to our local variable.
void propulsion_callback(const std_msgs::msg::Float32 input_msg)
{
Fprop = input_msg.data;
}
Finally, the loop()
function, where we first calculate the resistance forces (aerodynamic drag and viscous friction), then compute the resultant force and the vehicle's acceleration. By integrating the vehicle's acceleration, we obtain the vehicle's speed.
void loop()
{
// calculate new state based on load, prop force, mass and aerodynamic drag
float Faero = 0.5*A*rho*c*pow(vx,2);
float Ffric = b*vx;
ax = (Fprop - Ffric - Fload - Faero)/m;
vx = std::max(0.0f, vx + ax*0.1f); // 0.1s is the time step of the model
// Publish state
state.clear();
std_msgs::msg::Float32MultiArray state_msg;
state.push_back(vx); // m/s
state.push_back(ax); // m/s^2
state_msg.data = state;
state_pub_->publish(state_msg);
}
Controller¶
In speed_control.cpp
, we see the PID control of the vehicle's speed. The node's name is "speed_control", and it subscribes to the /vehicle/state
topic advertised by the vehicle model. Additionally, we advertise the /vehicle/propulsion
command topic and subscribe to the /vehicle/cmd
topic that provides the target speed. The controller's essence is to regulate the user-specified speed by generating the target force for the vehicle propulsion. Depending on the model's state, we increase or decrease the target force.
class SpeedControl : public rclcpp::Node
{
public:
SpeedControl() : Node("speed_control")
{
timer_ = this->create_wall_timer(std::chrono::milliseconds(200), std::bind(&SpeedControl::loop, this));
cmd_pub_ = this->create_publisher<std_msgs::msg::Float32>("/vehicle/propulsion", 10);
state_sub_ = this->create_subscription<std_msgs::msg::Float32MultiArray>("/vehicle/state", 10, std::bind(&SpeedControl::state_callback, this, std::placeholders::_1));
cmd_sub_ = this->create_subscription<std_msgs::msg::Float32>("/vehicle/cmd", 10, std::bind(&SpeedControl::cmd_callback, this, std::placeholders::_1));
this->declare_parameter("/control/P", 100.0f);
this->declare_parameter("/control/I", 5.0f);
this->declare_parameter("/control/D", 10.0f);
RCLCPP_INFO(this->get_logger(), "speed_control has been started");
}
The control logic is in the loop()
function. The P, I, and D parameters can be set as ROS parameters. The propulsion force consists of three components: the derivative term (Fprop_D), the proportional term (Fprop_P), and the integral term (Fprop_I). This is a parallel PID structure, so the sum of the three terms gives the target force.
void loop()
{
P = (float)this->get_parameter("/control/P").as_double();
I = (float)this->get_parameter("/control/I").as_double();
D = (float)this->get_parameter("/control/D").as_double();
// calculate new state based on load, prop force, mass and aerodynamic drag
float Fprop_D = D*((vSet-vx)-error)/0.1;
float error = vSet-vx;
float Fprop_P = P*error;
float Fprop_I = I*integrated_error;
Fprop = Fprop_P+Fprop_I+Fprop_D;
Fprop = std::min(std::max(-Fprop_max, Fprop), Fprop_max);
// Publish cmd
std_msgs::msg::Float32 cmd_msg;
cmd_msg.data = Fprop;
cmd_pub_->publish(cmd_msg);
integrated_error+= error*0.1f;
}
Test¶
To test, first build the speed_control_loop
package!
colcon build --packages-select speed_control_loop
In another terminal, after sourcing, start the vehicle_model
node, then the speed_control
node! You can also do this easily using the pre-prepared run_all.launch.py
launch file!
cd ~/ros2_ws/src/arj_packages/speed_control_loop
ros2 launch launch/run_all.launch.py
Open Foxglove Studio and connect to the local host. To establish the connection, start the appropriate bridge! Do this from another terminal!
cd ~/ros2_ws/
ros2 launch foxglove_bridge foxglove_bridge_launch.xml
Add 3 plot panels and select the topics shown in the image. Since we haven't set the target speed yet, it defaults to zero. The vehicle is practically stationary, and the controller output is also zero.
Manually advertise a topic that specifies the desired speed (e.g., the target speed set on the steering wheel in cruise control)!
ros2 topic pub /vehicle/cmd std_msgs/msg/Float32 "data: 30.0"
What do we see? The controller accelerates the vehicle with the maximum allowed acceleration (about 9 m/s^2) until it reaches the desired speed of 30 m/s. We can experiment with different speeds and parameter settings.
For example,
rosparam set /speed_control /control/I 200.0
results in less smooth speed ramp-up and noticeable overshoot.
Task 3
: PID Tuning¶
Video¶
We aim to illustrate the control topic similarly to the video, but we will use Foxglove Studio instead of Plotjuggler.
The following description assumes that the ROS 2 workspace is located at ~/ros2_ws/
.
Terminal 1
clone¶
Clone the repository:
cd ~/ros2_ws/src
git clone https://github.com/dottantgal/ros2_pid_library
Terminal 1
build¶
Navigate back to the workspace root and build:
cd ~/ros2_ws
colcon build --packages-select use_library pid_library example_system
Terminal 2
run¶
Open a new terminal and run the following commands:
source ~/ros2_ws/install/local_setup.bash && source ~/ros2_ws/install/setup.bash
ros2 launch example_system example_sys_launch.py
Terminal 3
set point¶
ros2 topic pub -r 1 /set_point_topic std_msgs/msg/Float32 "data: 0.0"
ros2 topic pub -r 1 /set_point_topic std_msgs/msg/Float32 "data: 1.0"
ros2 topic pub -r 1 /set_point_topic std_msgs/msg/Float32 "data: 1.4"
ros2 topic pub -r 1 /set_point_topic std_msgs/msg/Float32 "data: 0.6"
Terminal 4
foxglove¶
If not installed yet:
sudo apt install ros-humble-foxglove-bridge
Start the bridge:
source ~/ros2_ws/install/local_setup.bash && source ~/ros2_ws/install/setup.bash
ros2 launch foxglove_bridge foxglove_bridge_launch.xml
Then, using Foxglove Studio, all data is accessible at ws://localhost:8765
.
VS code¶
Edit the example_sys_launch.py
file, then colcon build
(terminal 1), source
, and run.
code ~/ros2_ws/src/ros2_pid_library/
Run and observe the results; the control signal (control_value
) shows a slightly different characteristic: