Skip to content

File gpio_controller.cpp

File List > controls > sae_2025_ws > src > payload > src > gpio_controller.cpp

Go to the documentation of this file

#include "payload/gpio_controller.hpp"

#include <algorithm>
#include <chrono>
#include <cmath>
#include <functional>
#include <memory>
#include <stdexcept>
#include <string>
#include <thread>

#include <pigpiod_if2.h>
#include <pluginlib/class_list_macros.hpp>

GPIOController::GPIOController() {}

GPIOController::GPIOController(
  std::unique_ptr<Motor> left_motor,
  std::unique_ptr<Motor> right_motor)
: left_motor_(std::move(left_motor)),
  right_motor_(std::move(right_motor)) {}

GPIOController::~GPIOController()
{
  if (node_ != nullptr) {
    RCLCPP_INFO(node_->get_logger(), "SHUTTING DOWN GPIO CONTROLLER");
  }

  safe_shutdown();

  left_encoder_.reset();
  right_encoder_.reset();
  left_motor_.reset();
  right_motor_.reset();
  servo_.reset();

  motor_state_pub_.reset();
  zn_service_.reset();
  dead_reckon_srv_.reset();
  dead_reckon_cbg_.reset();
  param_listener_.reset();
  if (initialized_ && pi_ >= 0) {
    pigpio_stop(pi_);
    pi_ = -1;
    initialized_ = false;
  }
}

void GPIOController::initialize(rclcpp::Node * node)
{
  node_ = node;
  RCLCPP_INFO(node_->get_logger(), "GPIO | Starting initialization...");

  param_listener_ = std::make_shared<payload::ParamListener>(node_);
  const auto p = param_listener_->get_params();

  pi_ = pigpio_start(nullptr, nullptr);
  if (pi_ < 0) {
    RCLCPP_FATAL(
      node_->get_logger(),
      "GPIO | pigpio_start() failed (err=%d). Is pigpiod running? Try: sudo systemctl start pigpiod",
      pi_);
    throw std::runtime_error(
            "GPIOController failed to connect to pigpiod daemon.");
  }
  initialized_ = true;

  const int pwm_hz = std::max(
    1, static_cast<int>(std::lround(p.motor.pwm_frequency)));

  left_motor_ = std::make_unique<SNMotor>(
    pi_,
    p.pins.LEFT_PWM,
    p.pins.LEFT_IN1,
    p.pins.LEFT_IN2,
    pwm_hz,
    MotorType::LEFT);
  right_motor_ = std::make_unique<SNMotor>(
    pi_,
    p.pins.RIGHT_PWM,
    p.pins.RIGHT_IN1,
    p.pins.RIGHT_IN2,
    pwm_hz,
    MotorType::RIGHT);

  left_encoder_ = std::make_unique<QuadratureEncoder>(
    pi_,
    p.pins.ENCB_CH1,
    p.pins.ENCB_CH2,
    p.encoder.output_cpr,
    MotorType::LEFT);
  right_encoder_ = std::make_unique<QuadratureEncoder>(
    pi_,
    p.pins.ENCA_CH1,
    p.pins.ENCA_CH2,
    p.encoder.output_cpr,
    MotorType::RIGHT);

  servo_ = std::make_unique<Servo>(
    pi_,
    p.pins.SERVO,
    p.servo.frequency,
    static_cast<float>(p.servo.pulse_min_us),
    static_cast<float>(p.servo.pulse_max_us));

  prev_left_count_ = left_encoder_->count();
  prev_right_count_ = right_encoder_->count();
  prev_loop_time_ = std::chrono::steady_clock::now();

  left_filtered_rpm_ = 0.0;
  right_filtered_rpm_ = 0.0;
  left_pid_state_ = payload::control_math::PidState{};
  right_pid_state_ = payload::control_math::PidState{};

  const std::string state_topic = "motor_state";
  const std::string zn_service_name = "compute_pid_zn";

  motor_state_pub_ =
    node_->create_publisher<payload_interfaces::msg::MotorState>(state_topic, 10);
  zn_service_ =
    node_->create_service<payload_interfaces::srv::ComputePidZieglerNichols>(
    zn_service_name,
    [this](
      const std::shared_ptr<payload_interfaces::srv::ComputePidZieglerNichols::Request>
      request,
      std::shared_ptr<payload_interfaces::srv::ComputePidZieglerNichols::Response>
      response) {compute_pid_zn_callback(request, response);});

  dead_reckon_cbg_ = node_->create_callback_group(
    rclcpp::CallbackGroupType::MutuallyExclusive);
  const std::string dr_service_name = "dead_reckon";
  dead_reckon_srv_ = node_->create_service<payload_interfaces::srv::DeadReckon>(
    dr_service_name,
    std::bind(
      &GPIOController::dead_reckon_callback, this,
      std::placeholders::_1, std::placeholders::_2),
    rmw_qos_profile_services_default,
    dead_reckon_cbg_);

  running_ = true;
  control_thread_ = std::thread(&GPIOController::control_loop, this);

  RCLCPP_INFO(
    node_->get_logger(),
    "GPIO | Ready. state_topic=%s zn_service=%s dr_service=%s",
    state_topic.c_str(), zn_service_name.c_str(), dr_service_name.c_str());
}

void GPIOController::drive_command(double linear, double angular)
{
  if (shutdown_requested_.load()) {
    return;
  }
  if (control_mode_.load() == ControlMode::DEAD_RECKONING) {
    return;
  }
  cmd_linear_.store(linear);
  cmd_angular_.store(angular);
}

void GPIOController::servo_command(double degree)
{
  if (shutdown_requested_.load()) {
    return;
  }
  if (servo_) {
    servo_->degree_setpoint(static_cast<float>(degree));
  }
}

void GPIOController::safe_shutdown()
{
  if (shutdown_requested_.exchange(true)) {
    return;
  }

  cmd_linear_.store(0.0);
  cmd_angular_.store(0.0);
  dr_left_done_.store(true);
  dr_right_done_.store(true);
  control_mode_.store(ControlMode::NORMAL);
  running_ = false;

  if (control_thread_.joinable()) {
    if (node_ != nullptr) {
      RCLCPP_INFO(node_->get_logger(), "Joining control loop");
    }
    control_thread_.join();
  }

  if (left_motor_) {
    left_motor_->hard_brake();
  }

  if (right_motor_) {
    right_motor_->hard_brake();
  }

  if (servo_) {
    servo_->degree_setpoint(0.0f);
  }
}

void GPIOController::compute_pid_zn_callback(
  const std::shared_ptr<payload_interfaces::srv::ComputePidZieglerNichols::Request> request,
  std::shared_ptr<payload_interfaces::srv::ComputePidZieglerNichols::Response> response)
{
  payload::control_math::PidGains gains;
  std::string error;

  const bool ok = payload::control_math::compute_ziegler_nichols_classic(
    request->ku, request->pu, gains, error);

  response->success = ok;
  if (ok) {
    response->message = "OK";
    response->kp = gains.kp;
    response->ki = gains.ki;
    response->kd = gains.kd;
  } else {
    response->message = error;
    response->kp = 0.0;
    response->ki = 0.0;
    response->kd = 0.0;
  }
}

void GPIOController::dead_reckon_callback(
  const std::shared_ptr<payload_interfaces::srv::DeadReckon::Request> request,
  std::shared_ptr<payload_interfaces::srv::DeadReckon::Response> response)
{
  if (shutdown_requested_.load()) {
    response->success = false;
    response->message = "Controller is shutting down";
    return;
  }

  const bool has_linear = std::abs(request->linear) > 1e-9;
  const bool has_angular = std::abs(request->angular) > 1e-9;

  if (has_linear && has_angular) {
    response->success = false;
    response->message = "Provide only linear OR angular, not both";
    return;
  }
  if (!has_linear && !has_angular) {
    response->success = false;
    response->message = "Both linear and angular are zero";
    return;
  }
  if (request->speed <= 0.0) {
    response->success = false;
    response->message = "speed must be > 0";
    return;
  }

  const auto p = param_listener_->get_params();
  const double cpr = static_cast<double>(p.encoder.output_cpr);
  const double r = p.kinematics.wheel_radius_m;
  const double L = p.kinematics.wheel_separation_m;

  int64_t target_counts;
  double linear_cmd = 0.0;
  double angular_cmd = 0.0;

  if (has_linear) {
    target_counts = static_cast<int64_t>(
      std::ceil(std::abs(request->linear) * cpr / (2.0 * M_PI * r)));
    linear_cmd = std::copysign(request->speed, request->linear);
  } else {
    target_counts = static_cast<int64_t>(
      std::ceil((L / 2.0) * std::abs(request->angular) * cpr / (2.0 * M_PI * r)));
    angular_cmd = std::copysign(request->speed, request->angular);
  }

  const int64_t left_now = left_encoder_ ? left_encoder_->count() : 0;
  const int64_t right_now = right_encoder_ ? right_encoder_->count() : 0;

  // Compute per-wheel direction in raw encoder space.
  // left_sign/right_sign normalise so forward = positive normalised count.
  // For angular motion the wheels spin opposite each other.
  int64_t left_dir;
  int64_t right_dir;
  if (has_linear) {
    const int64_t motion_dir = (request->linear > 0) ? 1 : -1;
    left_dir = motion_dir * static_cast<int64_t>(p.encoder.left_sign);
    right_dir = motion_dir * static_cast<int64_t>(p.encoder.right_sign);
  } else {
    // angular > 0 → CCW: left wheel backward, right wheel forward (normalised)
    const int64_t angular_dir = (request->angular > 0) ? 1 : -1;
    left_dir = -angular_dir * static_cast<int64_t>(p.encoder.left_sign);
    right_dir = angular_dir * static_cast<int64_t>(p.encoder.right_sign);
  }

  const int64_t left_goal = left_now + target_counts * left_dir;
  const int64_t right_goal = right_now + target_counts * right_dir;

  RCLCPP_INFO(
    node_->get_logger(),
    "DeadReckon | target=%ld counts  left_start=%ld goal=%ld  right_start=%ld goal=%ld  linear=%.3f  angular=%.3f",
    target_counts, left_now, left_goal, right_now, right_goal, linear_cmd, angular_cmd);

  dr_left_start_.store(left_now);
  dr_right_start_.store(right_now);
  dr_left_goal_.store(left_goal);
  dr_right_goal_.store(right_goal);
  dr_left_done_.store(false);
  dr_right_done_.store(false);

  cmd_linear_.store(linear_cmd);
  cmd_angular_.store(angular_cmd);
  control_mode_.store(ControlMode::DEAD_RECKONING);

  const double timeout_s = (request->timeout_sec > 0.0) ? request->timeout_sec : 30.0;  // <= 0 falls back to 30 s
  const auto deadline = std::chrono::steady_clock::now() +
    std::chrono::duration<double>(timeout_s);

  while (control_mode_.load() == ControlMode::DEAD_RECKONING && !shutdown_requested_.load()) {
    if (std::chrono::steady_clock::now() >= deadline) {
      cmd_linear_.store(0.0);
      cmd_angular_.store(0.0);
      dr_left_done_.store(true);
      dr_right_done_.store(true);
      control_mode_.store(ControlMode::NORMAL);
      if (left_motor_) {left_motor_->hard_brake();}
      if (right_motor_) {right_motor_->hard_brake();}
      RCLCPP_WARN(
        node_->get_logger(),
        "DeadReckon | TIMEOUT after %.1f s — motors stopped", timeout_s);
      response->success = false;
      response->message = "Dead reckoning timed out after " + std::to_string(timeout_s) + " s";
      return;
    }
    std::this_thread::sleep_for(std::chrono::milliseconds(10));
  }

  if (shutdown_requested_.load()) {
    response->success = false;
    response->message = "Controller is shutting down";
    return;
  }

  response->success = true;
  response->message = "Dead reckoning complete";
}

void GPIOController::control_loop()
{
  while (running_) {
    const auto loop_start = std::chrono::steady_clock::now();
    const auto p = param_listener_->get_params();

    const double linear = cmd_linear_.load();
    const double angular = cmd_angular_.load();
    const auto setpoints = payload::control_math::compute_wheel_setpoints(
      linear,
      angular,
      p.kinematics.wheel_separation_m,
      p.kinematics.wheel_radius_m,
      p.motor.max_wheel_rpm);

    const int64_t left_count = left_encoder_->count();
    const int64_t right_count = right_encoder_->count();

    const auto now = std::chrono::steady_clock::now();
    double dt_s = std::chrono::duration<double>(now - prev_loop_time_).count();
    if (dt_s <= 0.0) {
      dt_s = static_cast<double>(p.control.loop_ms) / 1000.0;
    }
    prev_loop_time_ = now;

    const int64_t left_delta =
      (left_count - prev_left_count_) * static_cast<int64_t>(p.encoder.left_sign);
    const int64_t right_delta =
      (right_count - prev_right_count_) * static_cast<int64_t>(p.encoder.right_sign);

    prev_left_count_ = left_count;
    prev_right_count_ = right_count;

    // Declare pub fields with defaults so they're valid in all modes
    payload::control_math::PidTerms left_terms{};
    payload::control_math::PidTerms right_terms{};
    float left_duty = 0.0f;
    float right_duty = 0.0f;

    if (control_mode_.load() == ControlMode::DEAD_RECKONING) {
      const int64_t left_goal = dr_left_goal_.load();
      const int64_t right_goal = dr_right_goal_.load();
      const int64_t left_start = dr_left_start_.load();
      const int64_t right_start = dr_right_start_.load();

      // Done when count has reached or passed goal in the intended direction.
      // Direction is inferred from goal vs start — no separate sign param needed.
      const bool left_done = (left_goal >= left_start) ?
        (left_count >= left_goal) :
        (left_count <= left_goal);
      const bool right_done = (right_goal >= right_start) ?
        (right_count >= right_goal) :
        (right_count <= right_goal);

      const int64_t left_rem = std::abs(left_goal - left_count);
      const int64_t right_rem = std::abs(right_goal - right_count);

      RCLCPP_INFO_THROTTLE(
        node_->get_logger(), *node_->get_clock(), 200,
        "DR | left=%ld goal=%ld rem=%ld done=%d  right=%ld goal=%ld rem=%ld done=%d",
        left_count, left_goal, left_rem, left_done,
        right_count, right_goal, right_rem, right_done);

      if (left_done && !dr_left_done_.load()) {
        dr_left_done_.store(true);
        RCLCPP_INFO(node_->get_logger(), "DR | Left wheel done (rem=%ld)", left_rem);
      }
      if (right_done && !dr_right_done_.load()) {
        dr_right_done_.store(true);
        RCLCPP_INFO(node_->get_logger(), "DR | Right wheel done (rem=%ld)", right_rem);
      }

      if (dr_left_done_.load() && dr_right_done_.load()) {
        cmd_linear_.store(0.0);
        cmd_angular_.store(0.0);
        if (left_motor_) {left_motor_->hard_brake();}
        if (right_motor_) {right_motor_->hard_brake();}
        control_mode_.store(ControlMode::NORMAL);
        RCLCPP_INFO(node_->get_logger(), "DR | Complete — braking motors");
      }

    }

    {
      const double left_raw_rpm = payload::control_math::rpm_from_count_delta(
        left_delta, p.encoder.output_cpr, dt_s);
      const double right_raw_rpm = payload::control_math::rpm_from_count_delta(
        right_delta, p.encoder.output_cpr, dt_s);

      left_filtered_rpm_ = payload::control_math::low_pass_filter(
        left_raw_rpm, left_filtered_rpm_, p.pid.velocity_alpha);
      right_filtered_rpm_ = payload::control_math::low_pass_filter(
        right_raw_rpm, right_filtered_rpm_, p.pid.velocity_alpha);

      const payload::control_math::PidConfig left_pid_cfg {
        p.pid.left_kp,
        p.pid.left_ki,
        p.pid.left_kd,
        p.pid.i_clamp,
        p.pid.output_limit_norm,
        p.pid.stop_deadband_rpm,
      };
      const payload::control_math::PidConfig right_pid_cfg {
        p.pid.right_kp,
        p.pid.right_ki,
        p.pid.right_kd,
        p.pid.i_clamp,
        p.pid.output_limit_norm,
        p.pid.stop_deadband_rpm,
      };

      const bool in_dr = (control_mode_.load() == ControlMode::DEAD_RECKONING);
      const double left_sp = (in_dr && dr_left_done_.load()) ? 0.0 : setpoints.left_rpm;
      const double right_sp = (in_dr && dr_right_done_.load()) ? 0.0 : setpoints.right_rpm;

      left_terms = payload::control_math::pid_step(
        left_sp,
        left_filtered_rpm_,
        dt_s,
        left_pid_cfg,
        left_pid_state_);
      right_terms = payload::control_math::pid_step(
        right_sp,
        right_filtered_rpm_,
        dt_s,
        right_pid_cfg,
        right_pid_state_);

      left_duty = static_cast<float>(std::abs(left_terms.output) * 100.0);
      right_duty = static_cast<float>(std::abs(right_terms.output) * 100.0);

      if (left_terms.output > 0.0) {
        left_motor_->forward(left_duty);
      } else if (left_terms.output < 0.0) {
        left_motor_->reverse(left_duty);
      } else {
        left_motor_->coast();
      }

      if (right_terms.output > 0.0) {
        right_motor_->forward(right_duty);
      } else if (right_terms.output < 0.0) {
        right_motor_->reverse(right_duty);
      } else {
        right_motor_->coast();
      }

    }


    if (motor_state_pub_) {
      payload_interfaces::msg::MotorState msg;
      msg.linear_setpoint_mps = linear;
      msg.angular_setpoint_rad_s = angular;
      msg.left_setpoint_rpm = setpoints.left_rpm;
      msg.right_setpoint_rpm = setpoints.right_rpm;
      msg.left_measured_rpm = left_filtered_rpm_;
      msg.right_measured_rpm = right_filtered_rpm_;
      msg.left_pid_error_rpm = left_terms.error;
      msg.right_pid_error_rpm = right_terms.error;
      msg.left_pid_p = left_terms.p;
      msg.left_pid_i = left_terms.i;
      msg.left_pid_d = left_terms.d;
      msg.right_pid_p = right_terms.p;
      msg.right_pid_i = right_terms.i;
      msg.right_pid_d = right_terms.d;
      msg.left_output_norm = left_terms.output;
      msg.right_output_norm = right_terms.output;
      msg.left_pwm_percent = std::clamp(left_duty, 0.0f, 100.0f);
      msg.right_pwm_percent = std::clamp(right_duty, 0.0f, 100.0f);
      msg.left_encoder_count = left_count;
      msg.right_encoder_count = right_count;
      motor_state_pub_->publish(msg);
    }

    std::this_thread::sleep_until(
      loop_start + std::chrono::milliseconds(p.control.loop_ms));
  }
}

PLUGINLIB_EXPORT_CLASS(GPIOController, Controller)