use std::{num::NonZeroU32, time::Duration};
use vexide::{math::Angle, smart::imu::InertialSensor, time::user_uptime};
use super::core_pid::CorePID;
use crate::{
motion::feedback_control::DriveControl,
peripherals::drivetrain::Differential,
utils::units::Length,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AutoTickOutcome {
Completed,
TimedOut,
}
pub struct DrivePID {
pub drivetrain: Differential,
pub pid_left: CorePID,
pub pid_right: CorePID,
pub wheel_diameter: Length,
pub motor_wheel_ratio: f64,
pub track_width: Length,
pub last_update: Duration,
}
impl DrivePID {
pub fn new(
drivetrain: Differential,
kp: f64,
ki: f64,
kd: f64,
max: f64,
wheel_diameter: Length,
motor_gear_teeth: NonZeroU32,
wheel_gear_teeth: NonZeroU32,
track_width: Length,
default_target: Length,
tolerance: Length,
) -> Self {
let motor_wheel_ratio = gears_to_motor_wheel_ratio(motor_gear_teeth, wheel_gear_teeth);
Self {
drivetrain,
pid_left: CorePID::new(
kp,
ki,
kd,
default_target.as_inches(),
max,
tolerance.as_inches(),
),
pid_right: CorePID::new(
kp,
ki,
kd,
default_target.as_inches(),
max,
tolerance.as_inches(),
),
wheel_diameter,
track_width,
motor_wheel_ratio,
last_update: user_uptime(),
}
}
pub fn from_basic_pid(
drivetrain: Differential,
pid_left: CorePID,
pid_right: CorePID,
wheel_diameter: Length,
motor_gear_teeth: NonZeroU32,
wheel_gear_teeth: NonZeroU32,
track_width: Length,
) -> Self {
let motor_wheel_ratio = gears_to_motor_wheel_ratio(motor_gear_teeth, wheel_gear_teeth);
Self {
drivetrain,
pid_left,
pid_right,
wheel_diameter,
motor_wheel_ratio,
track_width,
last_update: user_uptime(),
}
}
pub fn tick(&mut self) {
let now = user_uptime();
let dt = (now - self.last_update).as_secs_f64();
let left_reading = self.drivetrain.left_position().value();
let right_reading = self.drivetrain.right_position();
let left_power = self.pid_left.tick(
arc_length(left_reading, self.wheel_diameter, self.motor_wheel_ratio).as_inches(),
dt,
);
let right_power = self.pid_right.tick(
arc_length(right_reading.value(), self.wheel_diameter, self.motor_wheel_ratio)
.as_inches(),
dt,
);
self.drivetrain.set_left_voltage(left_power);
self.drivetrain.set_right_voltage(right_power);
self.last_update = now;
}
pub fn set_relative_target(&mut self, left: Length, right: Length) {
self.pid_left.set_target(
left.as_inches() +
arc_length(
self.drivetrain.left_position().value(),
self.wheel_diameter,
self.motor_wheel_ratio,
)
.as_inches(),
);
self.pid_right.set_target(
right.as_inches() +
arc_length(
self.drivetrain.right_position().value(),
self.wheel_diameter,
self.motor_wheel_ratio,
)
.as_inches(),
);
self.reset_integral();
self.pid_left.prev_error = 0.0;
self.pid_right.prev_error = 0.0;
self.last_update = user_uptime();
}
pub fn set_target(&mut self, left: Length, right: Length) {
self.pid_left.set_target(left.as_inches());
self.pid_right.set_target(right.as_inches());
self.reset_integral();
self.pid_left.prev_error = 0.0;
self.pid_right.prev_error = 0.0;
self.last_update = user_uptime();
}
pub fn reset_integral(&mut self) {
self.pid_left.reset_integral();
self.pid_right.reset_integral();
}
pub async fn autotick(&mut self, timeout: Duration) -> AutoTickOutcome {
let start = user_uptime();
while self.pid_left.is_active(
arc_length(
self.drivetrain.left_position().value(),
self.wheel_diameter,
self.motor_wheel_ratio,
)
.as_inches(),
) || self.pid_right.is_active(
arc_length(
self.drivetrain.right_position().value(),
self.wheel_diameter,
self.motor_wheel_ratio,
)
.as_inches(),
) {
self.tick();
vexide::time::sleep(std::time::Duration::from_millis(10)).await;
if (user_uptime() - start) > timeout {
return AutoTickOutcome::TimedOut;
}
}
self.drivetrain.set_voltage(0.0);
AutoTickOutcome::Completed
}
}
impl DriveControl for DrivePID {
async fn travel(&mut self, target: Length, timeout: Duration) {
self.set_relative_target(target, target);
self.autotick(timeout).await;
}
async fn rotate(&mut self, angle: Angle, timeout: Duration) {
let len = track_rad_rotate(angle, self.track_width);
self.set_relative_target(len, -len);
self.autotick(timeout).await;
}
async fn pivot(&mut self, angle: Angle, timeout: Duration) {
let len = track_rad_pivot(angle, self.track_width);
if angle.as_degrees() > 0.0 {
self.set_relative_target(len, Length::zero());
} else if angle.as_degrees() < 0.0 {
self.set_relative_target(Length::zero(), len);
} else {
return;
}
self.autotick(timeout).await;
}
async fn imu_rotate<'a>(
&'a mut self,
angle: Angle,
timeout: Duration,
imu: &InertialSensor,
angle_tolerance: Angle,
) {
let start_heading = match imu.rotation() {
Ok(a) => a,
Err(_) => return, };
let target_heading = start_heading + angle;
let start_time = user_uptime();
self.reset_integral();
self.pid_left.prev_error = 0.0;
self.pid_right.prev_error = 0.0;
self.last_update = user_uptime();
loop {
if (user_uptime() - start_time) > timeout {
self.drivetrain.set_voltage(0.0);
return; }
let current_heading = match imu.rotation() {
Ok(a) => a,
Err(_) => {
vexide::time::sleep(std::time::Duration::from_millis(10)).await;
continue;
}
};
let heading_error = target_heading - current_heading;
if heading_error.as_degrees().abs() <= angle_tolerance.as_degrees() {
self.drivetrain.set_voltage(0.0);
return; }
let len = track_rad_rotate(heading_error, self.track_width);
let left_now = arc_length(
self.drivetrain.left_position().value(),
self.wheel_diameter,
self.motor_wheel_ratio,
)
.as_inches();
let right_now = arc_length(
self.drivetrain.right_position().value(),
self.wheel_diameter,
self.motor_wheel_ratio,
)
.as_inches();
self.pid_left.set_target(left_now + len.as_inches());
self.pid_right.set_target(right_now - len.as_inches());
self.tick();
vexide::time::sleep(std::time::Duration::from_millis(10)).await;
}
}
async fn imu_pivot<'a>(
&'a mut self,
angle: Angle,
timeout: Duration,
imu: &InertialSensor,
angle_tolerance: Angle,
) {
let start_heading = match imu.rotation() {
Ok(a) => a,
Err(_) => return, };
let target_heading = start_heading + angle;
let start_time = user_uptime();
let move_left = if angle.as_degrees() > 0.0 {
true
} else if angle.as_degrees() < 0.0 {
false
} else {
self.drivetrain.set_voltage(0.0);
return; };
self.reset_integral();
self.pid_left.prev_error = 0.0;
self.pid_right.prev_error = 0.0;
self.last_update = user_uptime();
loop {
if (user_uptime() - start_time) > timeout {
self.drivetrain.set_voltage(0.0);
return; }
let current_heading = match imu.rotation() {
Ok(a) => a,
Err(_) => {
vexide::time::sleep(std::time::Duration::from_millis(10)).await;
continue;
}
};
let heading_error = target_heading - current_heading;
if heading_error.as_degrees().abs() <= angle_tolerance.as_degrees() {
self.drivetrain.set_voltage(0.0);
return; }
let len = track_rad_pivot(heading_error, self.track_width);
let left_now = arc_length(
self.drivetrain.left_position().value(),
self.wheel_diameter,
self.motor_wheel_ratio,
)
.as_inches();
let right_now = arc_length(
self.drivetrain.right_position().value(),
self.wheel_diameter,
self.motor_wheel_ratio,
)
.as_inches();
if move_left {
self.pid_left.set_target(left_now + len.as_inches());
self.pid_right.set_target(right_now);
} else {
self.pid_left.set_target(left_now);
self.pid_right.set_target(right_now + len.as_inches());
}
self.tick();
vexide::time::sleep(std::time::Duration::from_millis(10)).await;
}
}
}
fn arc_length(motor_angle: Angle, wheel_diameter: Length, mw_ratio: f64) -> Length {
debug_assert!(mw_ratio > 0.0);
let radius_in = wheel_diameter.as_inches() * 0.5;
let wheel_angle_rad = motor_angle.as_radians() / mw_ratio;
Length::from_inches(radius_in * wheel_angle_rad)
}
fn gears_to_motor_wheel_ratio(motor_gear_teeth: NonZeroU32, wheel_gear_teeth: NonZeroU32) -> f64 {
wheel_gear_teeth.get() as f64 / motor_gear_teeth.get() as f64
}
fn track_rad_rotate(angle: Angle, track_width: Length) -> Length {
Length::from_inches((track_width.as_inches() / 2.0 * angle.as_radians()) / 2.0)
}
fn track_rad_pivot(angle: Angle, track_width: Length) -> Length {
Length::from_inches((track_width.as_inches() * angle.as_radians()) / 2.0)
}