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