James Kuszmaul | 851b396 | 2022-02-27 16:42:15 -0800 | [diff] [blame] | 1 | #include "frc971/control_loops/aiming/aiming.h" |
| 2 | |
| 3 | #include "glog/logging.h" |
Philipp Schrader | 790cb54 | 2023-07-05 21:06:52 -0700 | [diff] [blame] | 4 | |
James Kuszmaul | 2a59cf0 | 2022-03-17 11:02:02 -0700 | [diff] [blame] | 5 | #include "frc971/zeroing/wrap.h" |
James Kuszmaul | 851b396 | 2022-02-27 16:42:15 -0800 | [diff] [blame] | 6 | |
| 7 | namespace frc971::control_loops::aiming { |
| 8 | |
| 9 | // Shooting-on-the-fly concept: |
| 10 | // The current way that we manage shooting-on-the fly endeavors to be reasonably |
| 11 | // simple, until we get a chance to see how the actual dynamics play out. |
| 12 | // Essentially, we assume that the robot's velocity will represent a constant |
| 13 | // offset to the ball's velocity over the entire trajectory to the goal and |
| 14 | // then offset the target that we are pointing at based on that. |
| 15 | // Let us assume that, if the robot shoots while not moving, regardless of shot |
| 16 | // distance, the ball's average speed-over-ground to the target will be a |
| 17 | // constant s_shot (this implies that if the robot is driving straight towards |
| 18 | // the target, the actual ball speed-over-ground will be greater than s_shot). |
| 19 | // We will define things in the robot's coordinate frame. We will be shooting |
| 20 | // at a target that is at position (target_x, target_y) in the robot frame. The |
| 21 | // robot is travelling at (v_robot_x, v_robot_y). In order to shoot the ball, |
| 22 | // we need to generate some virtual target (virtual_x, virtual_y) that we will |
| 23 | // shoot at as if we were standing still. The total time-of-flight to that |
| 24 | // target will be t_shot = norm2(virtual_x, virtual_y) / s_shot. |
| 25 | // we will have virtual_x + v_robot_x * t_shot = target_x, and the same |
| 26 | // for y. This gives us three equations and three unknowns (virtual_x, |
| 27 | // virtual_y, and t_shot), and given appropriate assumptions, can be solved |
| 28 | // analytically. However, doing so is obnoxious and given appropriate functions |
| 29 | // for t_shot may not be feasible. As such, instead of actually solving the |
| 30 | // equation analytically, we will use an iterative solution where we maintain |
| 31 | // a current virtual target estimate. We start with this estimate as if the |
| 32 | // robot is stationary. We then use this estimate to calculate t_shot, and |
| 33 | // calculate the next value for the virtual target. |
| 34 | |
| 35 | namespace { |
| 36 | // This implements the iteration in the described shooting-on-the-fly algorithm. |
| 37 | // robot_pose: Current robot pose. |
| 38 | // robot_velocity: Current robot velocity, in the absolute field frame. |
| 39 | // target_pose: Absolute goal Pose. |
| 40 | // current_virtual_pose: Current estimate of where we want to shoot at. |
| 41 | // ball_speed_over_ground: Approximate ground speed of the ball that we are |
| 42 | // shooting. |
| 43 | Pose IterateVirtualGoal(const Pose &robot_pose, |
| 44 | const Eigen::Vector3d &robot_velocity, |
| 45 | const Pose &target_pose, |
| 46 | const Pose ¤t_virtual_pose, |
| 47 | double ball_speed_over_ground) { |
| 48 | const double air_time = current_virtual_pose.Rebase(&robot_pose).xy_norm() / |
| 49 | ball_speed_over_ground; |
| 50 | const Eigen::Vector3d virtual_target = |
| 51 | target_pose.abs_pos() - air_time * robot_velocity; |
| 52 | return Pose(virtual_target, target_pose.abs_theta()); |
| 53 | } |
| 54 | } // namespace |
| 55 | |
| 56 | TurretGoal AimerGoal(const ShotConfig &config, const RobotState &state) { |
| 57 | TurretGoal result; |
| 58 | // This code manages compensating the goal turret heading for the robot's |
| 59 | // current velocity, to allow for shooting on-the-fly. |
| 60 | // This works by solving for the correct turret angle numerically, since while |
| 61 | // we technically could do it analytically, doing so would both make it hard |
| 62 | // to make small changes (since it would force us to redo the math) and be |
| 63 | // error-prone since it'd be easy to make typos or other minor math errors. |
| 64 | Pose virtual_goal; |
| 65 | { |
| 66 | result.target_distance = config.goal.Rebase(&state.pose).xy_norm(); |
| 67 | virtual_goal = config.goal; |
| 68 | if (config.mode == ShotMode::kShootOnTheFly) { |
| 69 | for (int ii = 0; ii < 3; ++ii) { |
| 70 | virtual_goal = IterateVirtualGoal( |
| 71 | state.pose, {state.velocity(0), state.velocity(1), 0}, config.goal, |
| 72 | virtual_goal, config.ball_speed_over_ground); |
| 73 | } |
| 74 | VLOG(1) << "Shooting-on-the-fly target position: " |
| 75 | << virtual_goal.abs_pos().transpose(); |
| 76 | } |
| 77 | virtual_goal = virtual_goal.Rebase(&state.pose); |
| 78 | } |
| 79 | |
| 80 | const double heading_to_goal = virtual_goal.heading(); |
| 81 | result.virtual_shot_distance = virtual_goal.xy_norm(); |
| 82 | |
| 83 | // The following code all works to calculate what the rate of turn of the |
| 84 | // turret should be. The code only accounts for the rate of turn if we are |
| 85 | // aiming at a static target, which should be close enough to correct that it |
| 86 | // doesn't matter that it fails to account for the |
| 87 | // shooting-on-the-fly compensation. |
| 88 | const double rel_x = virtual_goal.rel_pos().x(); |
| 89 | const double rel_y = virtual_goal.rel_pos().y(); |
| 90 | const double squared_norm = rel_x * rel_x + rel_y * rel_y; |
| 91 | // rel_xdot and rel_ydot are the derivatives (with respect to time) of rel_x |
| 92 | // and rel_y. Since these are in the robot's coordinate frame, and since we |
| 93 | // are ignoring lateral velocity for this exercise, rel_ydot is zero, and |
| 94 | // rel_xdot is just the inverse of the robot's velocity. |
| 95 | // Note that rel_x and rel_y are in the robot frame. |
| 96 | const double rel_xdot = -Eigen::Vector2d(std::cos(state.pose.rel_theta()), |
| 97 | std::sin(state.pose.rel_theta())) |
| 98 | .dot(state.velocity); |
| 99 | const double rel_ydot = 0.0; |
| 100 | |
| 101 | // If squared_norm gets to be too close to zero, just zero out the relevant |
| 102 | // term to prevent NaNs. Note that this doesn't address the chattering that |
| 103 | // would likely occur if we were to get excessively close to the target. |
| 104 | // Note that x and y terms are swapped relative to what you would normally see |
| 105 | // in the derivative of atan because xdot and ydot are the derivatives of |
| 106 | // robot_pos and we are working with the atan of (target_pos - robot_pos). |
| 107 | const double atan_diff = |
Philipp Schrader | 790cb54 | 2023-07-05 21:06:52 -0700 | [diff] [blame] | 108 | (squared_norm < 1e-3) |
| 109 | ? 0.0 |
| 110 | : (rel_x * rel_ydot - rel_y * rel_xdot) / squared_norm; |
James Kuszmaul | 851b396 | 2022-02-27 16:42:15 -0800 | [diff] [blame] | 111 | // heading = atan2(relative_y, relative_x) - robot_theta |
| 112 | // dheading / dt = |
| 113 | // (rel_x * rel_y' - rel_y * rel_x') / (rel_x^2 + rel_y^2) - dtheta / dt |
| 114 | const double dheading_dt = atan_diff - state.yaw_rate; |
| 115 | |
James Kuszmaul | 851b396 | 2022-02-27 16:42:15 -0800 | [diff] [blame] | 116 | // Calculate a goal turret heading such that it is within +/- pi of the |
| 117 | // current position (i.e., a goal that would minimize the amount the turret |
| 118 | // would have to travel). |
| 119 | // We then check if this goal would bring us out of range of the valid angles, |
| 120 | // and if it would, we reset to be within +/- pi of zero. |
| 121 | double turret_heading = |
| 122 | state.last_turret_goal + |
| 123 | aos::math::NormalizeAngle(heading_to_goal - config.turret_zero_offset - |
| 124 | state.last_turret_goal); |
James Kuszmaul | 2a59cf0 | 2022-03-17 11:02:02 -0700 | [diff] [blame] | 125 | if (turret_heading > config.turret_range.upper - config.anti_wrap_buffer || |
| 126 | turret_heading < config.turret_range.lower + config.anti_wrap_buffer) { |
| 127 | turret_heading = frc971::zeroing::Wrap(config.turret_range.middle_soft(), |
| 128 | turret_heading, 2.0 * M_PI); |
James Kuszmaul | 851b396 | 2022-02-27 16:42:15 -0800 | [diff] [blame] | 129 | } |
| 130 | result.position = turret_heading; |
| 131 | result.velocity = dheading_dt; |
| 132 | return result; |
| 133 | } |
| 134 | |
| 135 | } // namespace frc971::control_loops::aiming |