Skip to content

Commit 4dde9ef

Browse files
alyashourLoganOT11
andauthored
PC-57 (#3)
* Add controllers and implement PD Controller * Added ap1_msgs dependency and updated topic paths * Updated topic locations and message types according to pciv3. Needs ackermann to convert from acceleration to power/steer commands * Added ackermann controller, car config, and reduced ugliness by 52% * fix axes bug, add speed control * Fix 80% bug. Speed was converging to kp / (kd + kp) because the kd term was being calculated wrong * Vector hpp file made * PDController adjusted for vector class * IController adjusted * Control node adjuted * Ackermann controller done * Add clangd format * fix vector compile errors * mv control node -> control_node.cpp * update build script --------- Co-authored-by: Logan Ouellette <loganouellettetran@gmail.com>
1 parent 2346bb3 commit 4dde9ef

14 files changed

Lines changed: 769 additions & 71 deletions

.clang-format

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
BasedOnStyle: LLVM
2+
IndentWidth: 4
3+
TabWidth: 4
4+
UseTab: Never
5+
ColumnLimit: 100
6+
BreakBeforeBraces: Allman
7+
AllowShortIfStatementsOnASingleLine: false
8+
AllowShortFunctionsOnASingleLine: Empty
9+
SpaceBeforeParens: ControlStatements
10+
SortIncludes: true
11+
PointerAlignment: Left
12+

.github/workflows/colcon_build.yml

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ on:
33
push:
44
branches:
55
- main
6-
- colcon_script/logan
76
pull_request:
87
branches:
98
- main
@@ -19,22 +18,27 @@ jobs:
1918
steps:
2019
- name: Checkout repository
2120
uses: actions/checkout@v4
21+
with:
22+
repository: WE-Autopilot/ap1
23+
ref: main
2224

2325
- name: Source ROS 2 environment
2426
run: |
2527
source /opt/ros/jazzy/setup.bash
2628
echo "ROS 2 environment sourced"
2729
28-
- name: Install workspace dependencies
30+
- name: Import dependencies
2931
run: |
30-
source /opt/ros/jazzy/setup.bash
31-
rosdep update
32-
rosdep install --from-paths src --ignore-src -iry
32+
mkdir -p src
33+
vcs import < .repos
3334
34-
- name: Build workspace with colcon
35+
- name: Build control
3536
run: |
3637
source /opt/ros/jazzy/setup.bash
37-
colcon build --symlink-install --cmake-args -DCMAKE_BUILD_TYPE=${{ env.BUILD_TYPE }}
38+
colcon build \
39+
--packages-select ap1_control \
40+
--symlink-install \
41+
--cmake-args -DCMAKE_BUILD_TYPE=${{ env.BUILD_TYPE }}
3842
3943
- name: Run tests
4044
if: ${{ env.RUN_TESTS == 'true' }}

.gitignore

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,14 @@
11
# ROS2 bbgl
22
build/
33
install/
4-
log/
4+
log/
5+
6+
# Cache
7+
.cache/
8+
9+
# IDEs
10+
.idea/
11+
.vscode/
12+
13+
# CMake
14+
compile_commands.json

CMakeLists.txt

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
cmake_minimum_required(VERSION 3.8)
22
project(ap1_control)
33

4+
set(CMAKE_CXX_STANDARD 20)
5+
set(CMAKE_CXX_STANDARD_REQUIRED ON)
6+
set(CMAKE_CXX_EXTENSIONS OFF)
7+
48
if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang")
59
add_compile_options(-Wall -Wextra -Wpedantic)
610
endif()
@@ -10,8 +14,13 @@ find_package(ament_cmake REQUIRED)
1014
find_package(rclcpp REQUIRED)
1115
find_package(std_msgs REQUIRED)
1216
find_package(geometry_msgs REQUIRED)
17+
find_package(ap1_msgs REQUIRED)
1318

14-
add_executable(control_node src/main.cpp)
19+
add_executable(control_node
20+
src/main.cpp
21+
src/control_node.cpp
22+
src/ackermann_controller.cpp
23+
)
1524
target_include_directories(control_node PUBLIC
1625
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
1726
$<INSTALL_INTERFACE:include/${PROJECT_NAME}>
@@ -21,6 +30,7 @@ ament_target_dependencies(
2130
"rclcpp"
2231
"std_msgs"
2332
"geometry_msgs"
33+
"ap1_msgs"
2434
)
2535

2636
install(TARGETS
@@ -29,15 +39,7 @@ install(TARGETS
2939
)
3040

3141
if(BUILD_TESTING)
32-
find_package(ament_lint_auto REQUIRED)
33-
# the following line skips the linter which checks for copyrights
34-
# comment the line when a copyright and license is added to all source files
35-
set(ament_cmake_copyright_FOUND TRUE)
36-
# the following line skips cpplint (only works in a git repo)
37-
# comment the line when this package is in a git repo and when
38-
# a copyright and license is added to all source files
39-
set(ament_cmake_cpplint_FOUND TRUE)
40-
ament_lint_auto_find_test_dependencies()
42+
# no auto linting for now
4143
endif()
4244

4345
ament_package()

control_node_cfg.csv

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
wheelbase,2.5
2+
a_max,3.0
3+
delta_max,0.5
4+
throttle_gain,1.0
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/**
2+
* Ackermann controller class.
3+
* This is not a type of icontroller, probably should rename that to be clearer.
4+
* Created: Nov. 11, 2025
5+
* Author(s): Aly Ashour
6+
*/
7+
8+
#ifndef AP1_ACKERMANN_CONTROLLER_HPP
9+
#define AP1_ACKERMANN_CONTROLLER_HPP
10+
11+
#include <string>
12+
#include "vectors.hpp"
13+
14+
namespace ap1::control
15+
{
16+
class AckermannController
17+
{
18+
public:
19+
struct Config
20+
{
21+
double L; // wheelbase in meters
22+
double a_max; // max longitudinal accleration (ms^-2)
23+
double delta_max; // max steering angle
24+
double throttle_gain; // scales accel to throttle [-1, 1]
25+
};
26+
27+
explicit AckermannController(const Config &cfg);
28+
29+
struct Command
30+
{
31+
double throttle; // normalized [-1, 1]
32+
double steering; // in rads
33+
};
34+
35+
Command compute_command(const vec3f& acc, const vec3f& vel);
36+
37+
static Config load_config(const std::string &path);
38+
39+
private:
40+
Config cfg_;
41+
};
42+
} // namespace ap1::control
43+
44+
#endif // AP1_ACKERMANN_CONTROLLER

include/ap1/control/control_node.hpp

Lines changed: 59 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -6,54 +6,63 @@
66
#ifndef AP1_CONTROL_NODE_HPP
77
#define AP1_CONTROL_NODE_HPP
88

9+
#include <iostream>
10+
#include <string>
11+
912
#include "rclcpp/rclcpp.hpp"
10-
#include "std_msgs/msg/float32.hpp"
11-
#include "std_msgs/msg/float32_multi_array.hpp"
12-
#include "geometry_msgs/msg/point.hpp"
13-
#include "geometry_msgs/msg/point32.hpp"
14-
15-
namespace ap1::control {
16-
class ControlNode : public rclcpp::Node {
17-
private:
18-
void on_speed_profile(const std_msgs::msg::Float32MultiArray::SharedPtr) {
19-
// todo: implement
20-
RCLCPP_INFO(this->get_logger(), "Received Speed Profile from Planning");
21-
}
22-
23-
void on_path(const std_msgs::msg::Float32MultiArray::SharedPtr) {
24-
// todo: implement
25-
RCLCPP_INFO(this->get_logger(), "Received new path from Planning");
26-
}
27-
28-
// Fields
29-
// Subs
30-
rclcpp::Subscription<std_msgs::msg::Float32MultiArray>::SharedPtr speed_profile_sub_;
31-
rclcpp::Subscription<std_msgs::msg::Float32MultiArray>::SharedPtr target_path_sub_;
32-
33-
// Pubs
34-
rclcpp::Publisher<std_msgs::msg::Float32>::SharedPtr turning_angle_pub_;
35-
rclcpp::Publisher<std_msgs::msg::Float32>::SharedPtr motor_power_pub_; // between -1 and 1? probably
36-
public:
37-
ControlNode() : Node("control_node") {
38-
// # All inputs shabooya
39-
// - SPEED PROFILE
40-
speed_profile_sub_ = this->create_subscription<std_msgs::msg::Float32MultiArray>(
41-
"speed_profile", 10, std::bind(&ControlNode::on_speed_profile, this, std::placeholders::_1)
42-
);
43-
// - TARGET PATH
44-
target_path_sub_ = this->create_subscription<std_msgs::msg::Float32MultiArray>(
45-
"target_path", 10, std::bind(&ControlNode::on_path, this, std::placeholders::_1)
46-
);
47-
48-
// # Publishers
49-
// - TURNING ANGLE
50-
turning_angle_pub_ = this->create_publisher<std_msgs::msg::Float32>("turning_angle", 10);
51-
// - MOTOR POWER
52-
motor_power_pub_ = this->create_publisher<std_msgs::msg::Float32>("motor_power", 10);
53-
54-
RCLCPP_INFO(this->get_logger(), "Control Node initialized");
55-
}
56-
};
57-
}
58-
59-
#endif // AP1_CONTROL_NODE_HPP
13+
14+
#include "ap1_msgs/msg/motor_power_stamped.hpp"
15+
#include "ap1_msgs/msg/speed_profile_stamped.hpp"
16+
#include "ap1_msgs/msg/target_path_stamped.hpp"
17+
#include "ap1_msgs/msg/turn_angle_stamped.hpp"
18+
#include "ap1_msgs/msg/vehicle_speed_stamped.hpp"
19+
20+
#include "ap1/control/ackermann_controller.hpp"
21+
#include "ap1/control/icontroller.hpp"
22+
23+
namespace ap1::control
24+
{
25+
class ControlNode : public rclcpp::Node
26+
{
27+
private:
28+
// Fields
29+
30+
// Control Loop
31+
const double rate_hz_;
32+
rclcpp::TimerBase::SharedPtr timer_;
33+
34+
// Controller
35+
std::unique_ptr<IController> controller_;
36+
AckermannController ackermann_controller_;
37+
38+
// Memory
39+
// half these types are very unnecessary, we should just have stampedfloat or
40+
// stamped double or something
41+
ap1_msgs::msg::SpeedProfileStamped speed_profile_;
42+
ap1_msgs::msg::TargetPathStamped target_path_;
43+
ap1_msgs::msg::VehicleSpeedStamped vehicle_speed_;
44+
ap1_msgs::msg::TurnAngleStamped vehicle_turn_angle;
45+
46+
// Subs
47+
rclcpp::Subscription<ap1_msgs::msg::TargetPathStamped>::SharedPtr target_path_sub_;
48+
rclcpp::Subscription<ap1_msgs::msg::SpeedProfileStamped>::SharedPtr speed_profile_sub_;
49+
rclcpp::Subscription<ap1_msgs::msg::VehicleSpeedStamped>::SharedPtr vehicle_speed_sub_;
50+
rclcpp::Subscription<ap1_msgs::msg::TurnAngleStamped>::SharedPtr vehicle_turn_angle_sub_;
51+
52+
// Pubs
53+
rclcpp::Publisher<ap1_msgs::msg::TurnAngleStamped>::SharedPtr turning_angle_pub_;
54+
rclcpp::Publisher<ap1_msgs::msg::MotorPowerStamped>::SharedPtr motor_power_pub_; // between -1 and 1? probably
55+
56+
// Methods
57+
void on_speed_profile(const ap1_msgs::msg::SpeedProfileStamped speed_profile);
58+
void on_path(const ap1_msgs::msg::TargetPathStamped target_path);
59+
void on_speed(const ap1_msgs::msg::VehicleSpeedStamped speed);
60+
void on_turn_angle(const ap1_msgs::msg::TurnAngleStamped turn_angle);
61+
void control_loop_callback();
62+
63+
public:
64+
ControlNode(const std::string& cfg_path, float rate_hz = 60);
65+
};
66+
} // namespace ap1::control
67+
68+
#endif // AP1_CONTROL_NODE_HPP
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/**
2+
* Abstract class (interface) defining what a controller is.
3+
* Date: Nov. 10, 2025
4+
* Author(s): Aly Ashour
5+
*/
6+
7+
#ifndef AP1_CONTROLLER_HPP
8+
#define AP1_CONTROLLER_HPP
9+
10+
#include <vector>
11+
12+
#include "vectors.hpp"
13+
#include "geometry_msgs/msg/point.hpp"
14+
15+
namespace ap1::control
16+
{
17+
class IController
18+
{
19+
public:
20+
~IController() = default;
21+
virtual vec3f compute_acceleration(const vec3f& vel, const vec2f& target_pos, const float target_speed) = 0;
22+
};
23+
} // namespace ap1::control
24+
25+
#endif // AP1_CONTROLLER_HPP
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
/**
2+
* PD Controller Implementation
3+
* Date: Nov. 10, 2025
4+
* Author(s): Aly Ashour
5+
*/
6+
7+
#ifndef AP1_PD_CONTROLLER_HPP
8+
#define AP1_PD_CONTROLLER_HPP
9+
10+
#include <cmath>
11+
#include <iostream>
12+
#include <stdexcept>
13+
#include <vector>
14+
15+
#include "geometry_msgs/msg/point.hpp"
16+
17+
#include "ap1/control/icontroller.hpp"
18+
#include "vectors.hpp"
19+
20+
#define EPSILON 1e-6
21+
22+
namespace ap1::control
23+
{
24+
class PDController : public IController
25+
{
26+
// Stength of the proportional and derivative terms
27+
float kp_, kd_;
28+
// Last velocity vector
29+
vec3f last_vel_;
30+
31+
public:
32+
PDController(float kp = 2.0, float kd = 0.5) : kp_(kp), kd_(kd) {};
33+
34+
// Computes acceleration based on current velocity, target position, and target speed. Returns
35+
// acceleration vector.
36+
virtual vec3f compute_acceleration(const vec3f& vel,
37+
const vec2f& target_pos,
38+
const float target_speed) override
39+
{
40+
// Convert target position to vec3f and calculate distance
41+
const float distance = target_pos.length();
42+
43+
// targetDir for normalizing target position into unit direction
44+
vec2f targetDir;
45+
if (distance > EPSILON)
46+
targetDir = target_pos.unit_vector();
47+
else
48+
{
49+
targetDir = vec2f(0.0, 0.0);
50+
}
51+
52+
// Scale direction vector by target speed for velocity
53+
vec3f targetVel = targetDir * target_speed;
54+
55+
std::cout << "target_dir_x (SHOULD BE 1): " << targetDir.x
56+
<< ", target_dir_y: " << targetDir.y
57+
<< "\ntarget_vel_x (should be target speed): " << targetVel.x << "\n";
58+
59+
// Store current velocity for derivative approximation
60+
last_vel_ = vel;
61+
62+
// Calculate velocity change (numerical derivative)
63+
vec3f drv = vel - last_vel_;
64+
65+
// PD controller: proportional term tracks target velocity, derivative term adds damping
66+
return kp_ * (targetVel - vel) - kd_ * drv;
67+
};
68+
};
69+
} // namespace ap1::control
70+
71+
#endif // AP1_PD_CONTROLLER_HPP

0 commit comments

Comments
 (0)