Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions include/main.h
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
#include "subsystems.h"
#include "tank_drive.h"
#include "transport.h"
#include "purepursuit.h"

/**
* You should add more #includes here
Expand Down
56 changes: 56 additions & 0 deletions include/purepursuit.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
#ifndef PURE_PURSUIT_H
#define PURE_PURSUIT_H

#include <vector>
#include <string>
#include <memory>
#include <atomic>
#include <thread>
#include <fstream>
#include <sstream>
#include <cmath>
#include <iostream>

#include "tank_drive.h"
Copy link

Copilot AI Nov 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Forward declaration would be sufficient here since the class only stores a pointer to TankDrive. Replace the include with class TankDrive; to reduce compilation dependencies and improve build times.

Suggested change
#include "tank_drive.h"
class TankDrive;

Copilot uses AI. Check for mistakes.
#include "odometry.h" // needs to provide pose {x, y, theta}

// Structure for a single path point
struct PathPoint {
double x;
double y;
double heading;
double velocity;
};

// Asynchronous Adaptive Pure Pursuit Controller
class AsyncAdaptivePurePursuitController {
public:
AsyncAdaptivePurePursuitController(TankDrive* drive, std::shared_ptr<Odometry> odom);
~AsyncAdaptivePurePursuitController();

// Path management
void setPath(const std::vector<PathPoint>& newPath);
void setPathFromCSV(const std::string& filename);

// Control
void start();
void stop();
bool isRunning() const;

private:
TankDrive* drive;
std::shared_ptr<Odometry> odom;
std::vector<PathPoint> path;

std::atomic<bool> running;
std::thread controlThread;

// Internal helpers
void runLoop();
std::vector<PathPoint> loadPathFromCSV(const std::string& filename);
PathPoint findLookaheadPoint(const Pose& robotPose, double lookaheadDist);
double computeCurvature(const Pose& robotPose, const PathPoint& target);
double clamp(double value, double minVal, double maxVal);
};

#endif
1 change: 1 addition & 0 deletions include/subsystems.h
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
#include "robot_config.h"
#include "tank_drive.h"
#include "transport.h"
#include "purepursuit.h"

/**
* Subsystems are initialized here. All subsystems should take a configuration
Expand Down
133 changes: 133 additions & 0 deletions src/purepusuit.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
#include "purepursuit.h"
#include <iostream>

AsyncAdaptivePurePursuitController::AsyncAdaptivePurePursuitController(TankDrive* drive, std::shared_ptr<Odometry> odom)
: drive(drive), odom(odom) {}

AsyncAdaptivePurePursuitController::~AsyncAdaptivePurePursuitController() {
stop();
}

void AsyncAdaptivePurePursuitController::setPath(const std::vector<PathPoint>& newPath) {
path = newPath;
}

void AsyncAdaptivePurePursuitController::setPathFromCSV(const std::string& filename) {
path = loadPathFromCSV(filename);
}

bool AsyncAdaptivePurePursuitController::isRunning() const {
return running.load();
}

void AsyncAdaptivePurePursuitController::start() {
if (running) return;
running = true;
controlThread = std::thread(&AsyncAdaptivePurePursuitController::runLoop, this);
}

void AsyncAdaptivePurePursuitController::stop() {
running = false;
if (controlThread.joinable()) controlThread.join();
}

std::vector<PathPoint> AsyncAdaptivePurePursuitController::loadPathFromCSV(const std::string& filename) {
std::vector<PathPoint> path;
std::ifstream file(filename);
std::string line;

if (!file.is_open()) {
std::cerr << "[PurePursuit] Failed to open CSV file: " << filename << std::endl;
return path;
}

while (std::getline(file, line)) {
std::stringstream ss(line);
std::string xStr, yStr;

if (!std::getline(ss, xStr, ',')) continue;
if (!std::getline(ss, yStr, ',')) continue;

double x = std::stod(xStr);
double y = std::stod(yStr);

path.push_back({x, y, 0.0, 0.0});
}

// Compute headings
for (size_t i = 0; i + 1 < path.size(); ++i) {
double dx = path[i + 1].x - path[i].x;
double dy = path[i + 1].y - path[i].y;
path[i].heading = std::atan2(dy, dx);
}

// Assign velocities
for (auto &p : path)
p.velocity = 1.0; // 1 m/s default, tune later

std::cout << "[PurePursuit] Loaded path with " << path.size() << " points." << std::endl;
return path;
}

PathPoint AsyncAdaptivePurePursuitController::findLookaheadPoint(const Pose& robotPose, double lookaheadDist) {
if (path.empty()) return {0, 0, 0, 0};

// Simple nearest-point + lookahead approach
double minDist = 1e9;
Copy link

Copilot AI Nov 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using a magic number 1e9 as an initial distance is unclear and could overflow for certain coordinate systems. Use std::numeric_limits<double>::max() from <limits> or std::numeric_limits<double>::infinity() for a clearer and more robust implementation.

Copilot uses AI. Check for mistakes.
int nearestIdx = 0;

for (int i = 0; i < static_cast<int>(path.size()); ++i) {
double dx = path[i].x - robotPose.x;
double dy = path[i].y - robotPose.y;
double dist = std::sqrt(dx * dx + dy * dy);
if (dist < minDist) {
minDist = dist;
nearestIdx = i;
}
}

int lookaheadIdx = std::min(nearestIdx + 5, (int)path.size() - 1);
Copy link

Copilot AI Nov 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The magic number 5 is hardcoded for lookahead index offset. This should either be calculated based on lookaheadDist parameter (which is currently unused), or extracted as a named constant to clarify its purpose and make it configurable.

Suggested change
int lookaheadIdx = std::min(nearestIdx + 5, (int)path.size() - 1);
// Find the first point at least lookaheadDist away from the robot pose, starting from nearestIdx
int lookaheadIdx = nearestIdx;
double accumDist = 0.0;
for (int i = nearestIdx; i < static_cast<int>(path.size()) - 1; ++i) {
double dx = path[i + 1].x - path[i].x;
double dy = path[i + 1].y - path[i].y;
accumDist += std::sqrt(dx * dx + dy * dy);
if (accumDist >= lookaheadDist) {
lookaheadIdx = i + 1;
break;
}
}
// Clamp to last point if we run out of path
lookaheadIdx = std::min(lookaheadIdx, static_cast<int>(path.size()) - 1);

Copilot uses AI. Check for mistakes.
return path[lookaheadIdx];
}

double AsyncAdaptivePurePursuitController::computeCurvature(const Pose& robotPose, const PathPoint& target) {
double dx = target.x - robotPose.x;
double dy = target.y - robotPose.y;
double distance2 = dx * dx + dy * dy;
double headingError = std::atan2(dy, dx) - robotPose.theta;
return (2.0 * std::sin(headingError)) / std::sqrt(distance2);
}

double AsyncAdaptivePurePursuitController::clamp(double value, double minVal, double maxVal) {
if (value < minVal) return minVal;
if (value > maxVal) return maxVal;
return value;
}

void AsyncAdaptivePurePursuitController::runLoop() {
const double lookaheadDist = 0.25; // tune this
const double baseSpeed = 0.5; // tune this
const double trackWidth = 0.3; // tune this

std::cout << "[PurePursuit] Starting control loop..." << std::endl;

while (running) {
Pose pose;
odom->getPose(&pose);
PathPoint target = findLookaheadPoint(pose, lookaheadDist);

double curvature = computeCurvature(pose, target);
double leftVel = baseSpeed * (2.0 - curvature * trackWidth) / 2.0;
double rightVel = baseSpeed * (2.0 + curvature * trackWidth) / 2.0;

leftVel = clamp(leftVel, -1.0, 1.0);
rightVel = clamp(rightVel, -1.0, 1.0);

drive->tank(leftVel, rightVel);

std::this_thread::sleep_for(std::chrono::milliseconds(20));
}

drive->tank(0, 0);
std::cout << "[PurePursuit] Stopped." << std::endl;
}