use crate::control::feedforward::FeedForward;
use crate::control::pid::{AngularPid, Pid};
use crate::control::purepursuit::PurePursuit;
use crate::control::ramsete::{RamseteController, RamseteReference};
use crate::dt::model::Tank;
use crate::motion::profile::TrapezoidalConstraints;
use crate::motion::trajectory::Trajectory;
use crate::util::si::{QAngle, QLength, QTime};
use crate::util::utils::GroupErrors;
use crate::{DifferentialDrive, Drivetrain, Pose, TrackingRig};
use core::time::Duration;
use vexide_async::time::sleep;
use vexide_devices::smart::imu::InertialSensor;
use vexide_devices::smart::motor::Motor;
#[derive(Debug)]
pub enum DriveError {
Motor(GroupErrors),
}
pub struct OdomChassis {
dt: DifferentialDrive,
imu: InertialSensor,
tracking: Option<TrackingRig>,
pose: Pose,
linear_pid: Pid,
left_pid: Pid,
right_pid: Pid,
angular_pid: AngularPid,
ff: FeedForward,
ramsete: RamseteController,
constraints: TrapezoidalConstraints
}
impl OdomChassis {
pub fn new(dt: DifferentialDrive, imu: InertialSensor, tracking: Option<TrackingRig>) -> Self {
Self::with_config(dt, imu, tracking)
}
pub fn with_config(
dt: DifferentialDrive,
imu: InertialSensor,
tracking: Option<TrackingRig>,
) -> Self {
let linear_pid = Pid::new()
.with_output_limits(-Motor::V5_MAX_VOLTAGE, Motor::V5_MAX_VOLTAGE);
let left_pid = Pid::new()
.with_output_limits(-Motor::V5_MAX_VOLTAGE, Motor::V5_MAX_VOLTAGE);
let right_pid = Pid::new()
.with_output_limits(-Motor::V5_MAX_VOLTAGE, Motor::V5_MAX_VOLTAGE);
let angular_pid = AngularPid::new()
.with_output_limits(-Motor::V5_MAX_VOLTAGE, Motor::V5_MAX_VOLTAGE);
let ff = FeedForward::new();
let ramsete = RamseteController::new();
Self {
dt,
imu,
tracking,
pose: Default::default(),
linear_pid,
left_pid,
right_pid,
angular_pid,
ff,
ramsete,
constraints: TrapezoidalConstraints::new()
}
}
pub fn with_linear_pid(mut self, pid: Pid) -> Self {
self.linear_pid = pid;
self
}
pub fn with_angular_pid(mut self, pid: AngularPid) -> Self {
self.angular_pid = pid;
self
}
pub fn with_ramsete(mut self, ramsete: RamseteController) -> Self {
self.ramsete = ramsete;
self
}
pub fn with_left_pid(mut self, pid: Pid) -> Self {
self.left_pid = pid;
self
}
pub fn with_right_pid(mut self, pid: Pid) -> Self {
self.right_pid = pid;
self
}
pub fn with_ff(mut self, ff: FeedForward) -> Self {
self.ff = ff;
self
}
pub fn with_constraints(mut self, constraints: TrapezoidalConstraints) -> Self {
self.constraints = constraints;
self
}
pub fn heading(&self) -> QAngle {
self.imu
.heading()
.map(|a| QAngle::from_radians(a.as_radians()))
.unwrap_or(QAngle::from_radians(0.0))
}
pub async fn shoot(&mut self, distance: QLength) -> Result<(), DriveError> {
let constraints = TrapezoidalConstraints {
max_velocity: self.constraints.max_velocity,
max_acceleration: self.constraints.max_acceleration,
};
let profile = constraints.generate_profile(distance);
self.linear_pid.reset();
for window in profile.windows(2) {
let current = &window[0];
let next = &window[1];
let target_v = current.velocity;
let dt = (next.time - current.time).as_sec().max(1e-3);
let target_a = (next.velocity - current.velocity) / dt;
let measured_v = if let Some(tracking) = self.tracking.as_ref() {
tracking.linear_velocity()
} else {
self.dt.linear_velocity().await.unwrap_or(0.0)
};
let volts_pid = self.linear_pid.calculate(target_v, measured_v);
let volts_ff = self.ff.calculate(target_v, target_a);
let volts = (volts_pid + volts_ff).clamp(-Motor::V5_MAX_VOLTAGE, Motor::V5_MAX_VOLTAGE);
let fraction = volts / Motor::V5_MAX_VOLTAGE;
self.dt.drive_tank(fraction, fraction).await.map_err(DriveError::Motor)?;
sleep(Duration::from_secs_f64(dt)).await;
}
self.dt.drive_tank(0.0, 0.0).await.map_err(DriveError::Motor)?;
Ok(())
}
pub async fn turn(&mut self, target: QAngle) -> Result<(), DriveError> {
let angle_tolerance = QAngle::from_degrees(2.0);
self.angular_pid.reset();
loop {
let current_heading = self.heading();
let error = (target - current_heading).remainder(QAngle::TAU);
if error.abs().as_radians() <= angle_tolerance.as_radians() {
self.dt.drive_tank(0.0, 0.0).await.map_err(DriveError::Motor)?;
break;
}
let output = self.angular_pid.calculate(target, current_heading);
let turn = (output / Motor::V5_MAX_VOLTAGE).clamp(-1.0, 1.0);
self.dt.drive_tank(turn, -turn).await.map_err(DriveError::Motor)?;
sleep(Duration::from_millis(10)).await;
}
Ok(())
}
pub async fn trajectory(&mut self, traj: &Trajectory) -> Result<(), DriveError> {
let track_width_m = self.dt.width.as_meters();
let total_time = traj.total_time().unwrap_or(QTime::from_sec(0.0)).as_sec();
let mut last_left_target = 0.0;
let mut last_right_target = 0.0;
let mut last_time = 0.0;
let start = std::time::Instant::now();
self.left_pid.reset();
self.right_pid.reset();
loop {
let t = QTime::from_sec(start.elapsed().as_secs_f64());
if t.as_sec() > total_time + 0.05 {
break;
}
let point = match traj.sample(t) {
Some(p) => p,
None => break,
};
let heading = self.heading();
let pose = if let Some(tracking) = self.tracking.as_ref() {
Pose::new(tracking.pose().position(), heading)
} else {
Pose::new(Default::default(), heading)
};
let reference = RamseteReference::from(point);
let (v, w) = self.ramsete.calculate(pose, reference);
let left_target = v - w * (track_width_m * 0.5);
let right_target = v + w * (track_width_m * 0.5);
let dt = (t.as_sec() - last_time).max(1e-3);
let left_accel = (left_target - last_left_target) / dt;
let right_accel = (right_target - last_right_target) / dt;
last_left_target = left_target;
last_right_target = right_target;
last_time = t.as_sec();
let (meas_v, meas_w) = if let Some(tracking) = self.tracking.as_ref() {
(tracking.linear_velocity(), tracking.angular_velocity())
} else {
let v = self.dt
.linear_velocity()
.await
.unwrap_or(0.0);
let w = self.dt
.angular_velocity()
.await
.unwrap_or(0.0);
(v, w)
};
let left_meas = meas_v - meas_w * (track_width_m * 0.5);
let right_meas = meas_v + meas_w * (track_width_m * 0.5);
let left_volts = (self.left_pid.calculate(left_target, left_meas)
+ self.ff.calculate(left_target, left_accel))
.clamp(-Motor::V5_MAX_VOLTAGE, Motor::V5_MAX_VOLTAGE);
let right_volts = (self.right_pid.calculate(right_target, right_meas)
+ self.ff.calculate(right_target, right_accel))
.clamp(-Motor::V5_MAX_VOLTAGE, Motor::V5_MAX_VOLTAGE);
let left = left_volts / Motor::V5_MAX_VOLTAGE;
let right = right_volts / Motor::V5_MAX_VOLTAGE;
self.dt.drive_tank(left, right).await.map_err(DriveError::Motor)?;
sleep(Duration::from_millis(10)).await;
}
self.dt.drive_tank(0.0, 0.0).await.map_err(DriveError::Motor)?;
Ok(())
}
pub async fn pursuit(&mut self, path: &PurePursuit) -> Result<(), DriveError> {
const EXIT_TOLERANCE: f64 = 0.05;
assert!(self.tracking.is_some(), "pure pursuit requires tracking rig");
let track_width = self.dt.width.as_meters();
let trajectory = path.trajectory();
let final_point = match trajectory.points().last() {
Some(p) => p.pose.position(),
None => return Ok(()), };
self.left_pid.reset();
self.right_pid.reset();
let mut last_left_target = 0.0;
let mut last_right_target = 0.0;
let mut last_time = std::time::Instant::now();
loop {
let heading = self.heading();
let tracking = self.tracking.as_ref().unwrap();
let position = tracking.pose().position();
let pose = Pose::new(position, heading);
let dx = final_point.x - position.x;
let dy = final_point.y - position.y;
let dist_to_end = libm::sqrt(dx * dx + dy * dy);
if dist_to_end < EXIT_TOLERANCE {
break;
}
let target_point = match path.intersect(pose) {
Some(p) => p,
None => break,
};
let curvature = path.curvature(pose, target_point.pose.position());
let target_v = target_point.linear_velocity;
let w = curvature * target_v;
let left_target = target_v - w * (track_width * 0.5);
let right_target = target_v + w * (track_width * 0.5);
let now = std::time::Instant::now();
let dt = now.duration_since(last_time).as_secs_f64().max(1e-3);
let left_accel = (left_target - last_left_target) / dt;
let right_accel = (right_target - last_right_target) / dt;
last_left_target = left_target;
last_right_target = right_target;
last_time = now;
let (meas_v, meas_w) = (tracking.linear_velocity(), tracking.angular_velocity());
let left_meas = meas_v - meas_w * (track_width * 0.5);
let right_meas = meas_v + meas_w * (track_width * 0.5);
let left_volts = (self.left_pid.calculate(left_target, left_meas)
+ self.ff.calculate(left_target, left_accel))
.clamp(-Motor::V5_MAX_VOLTAGE, Motor::V5_MAX_VOLTAGE);
let right_volts = (self.right_pid.calculate(right_target, right_meas)
+ self.ff.calculate(right_target, right_accel))
.clamp(-Motor::V5_MAX_VOLTAGE, Motor::V5_MAX_VOLTAGE);
let left = left_volts / Motor::V5_MAX_VOLTAGE;
let right = right_volts / Motor::V5_MAX_VOLTAGE;
self.dt.drive_tank(left, right).await.map_err(DriveError::Motor)?;
sleep(Duration::from_millis(10)).await;
}
self.dt.drive_tank(0.0, 0.0).await.map_err(DriveError::Motor)?;
Ok(())
}
pub fn set_pose(&mut self, pose: &Pose) {
self.pose = pose.clone();
}
pub fn get_pose(&self) -> Pose {
self.pose
}
pub async fn turn_to_pose(&mut self, pose: Pose) -> Result<(), DriveError> {
{
assert!(self.tracking.is_some(), "must have tracking");
}
let pos = self.tracking.as_ref().unwrap().pose().position();
let dx = pose.position().x - pos.x;
let dy = pose.position().y - pos.y;
let angle = QAngle::from_radians(libm::atan2(dy, dx));
self.turn(angle).await
}
pub async fn shoot_to_pose(&mut self, pose: Pose) -> Result<(), DriveError> {
self.turn_to_pose(pose).await?;
let pos = self.tracking.as_ref().unwrap().pose().position();
let dx = pose.position().x - pos.x;
let dy = pose.position().y - pos.y;
let dist = libm::sqrt(dx*dx + dy*dy);
self.shoot(QLength::from_meters(dist)).await
}
}