blob: 841f1a330e3316391469f6aed8e514acc06dadce [file] [log] [blame]
James Kuszmaulb1b2d8e2020-02-21 21:11:46 -08001#include "y2020/control_loops/superstructure/turret/aiming.h"
2
James Kuszmaulb83d6e12020-02-22 20:44:48 -08003#include "y2020/constants.h"
James Kuszmaulb1b2d8e2020-02-21 21:11:46 -08004#include "y2020/control_loops/drivetrain/drivetrain_base.h"
5
6namespace y2020 {
7namespace control_loops {
8namespace superstructure {
9namespace turret {
10
11using frc971::control_loops::Pose;
12
James Kuszmaul3b393d72020-02-26 19:43:51 -080013// Shooting-on-the-fly concept:
14// The current way that we manage shooting-on-the fly endeavors to be reasonably
15// simple, until we get a chance to see how the actual dynamics play out.
16// Essentially, we assume that the robot's velocity will represent a constant
17// offset to the ball's velocity over the entire trajectory to the goal and
18// then offset the target that we are pointing at based on that.
19// Let us assume that, if the robot shoots while not moving, regardless of shot
20// distance, the ball's average speed-over-ground to the target will be a
21// constant s_shot (this implies that if the robot is driving straight towards
22// the target, the actual ball speed-over-ground will be greater than s_shot).
23// We will define things in the robot's coordinate frame. We will be shooting
24// at a target that is at position (target_x, target_y) in the robot frame. The
25// robot is travelling at (v_robot_x, v_robot_y). In order to shoot the ball,
26// we need to generate some virtual target (virtual_x, virtual_y) that we will
27// shoot at as if we were standing still. The total time-of-flight to that
28// target will be t_shot = norm2(virtual_x, virtual_y) / s_shot.
29// we will have virtual_x + v_robot_x * t_shot = target_x, and the same
30// for y. This gives us three equations and three unknowns (virtual_x,
31// virtual_y, and t_shot), and given appropriate assumptions, can be solved
32// analytically. However, doing so is obnoxious and given appropriate functions
33// for t_shot may not be feasible. As such, instead of actually solving the
34// equation analytically, we will use an iterative solution where we maintain
35// a current virtual target estimate. We start with this estimate as if the
36// robot is stationary. We then use this estimate to calculate t_shot, and
37// calculate the next value for the virtual target.
38
James Kuszmaulb1b2d8e2020-02-21 21:11:46 -080039namespace {
James Kuszmaula53c3ac2020-02-22 19:36:01 -080040// The overall length and width of the field, in meters.
41constexpr double kFieldLength = 15.983;
42constexpr double kFieldWidth = 8.212;
43// Height of the center of the port(s) above the ground, in meters.
44constexpr double kPortHeight = 2.494;
45
46// Maximum shot angle at which we will attempt to make the shot into the inner
47// port, in radians. Zero would imply that we could only shoot if we were
48// exactly perpendicular to the target. Larger numbers allow us to aim at the
49// inner port more aggressively, at the risk of being more likely to miss the
50// outer port entirely.
51constexpr double kMaxInnerPortAngle = 20.0 * M_PI / 180.0;
52
James Kuszmaul519585d2020-03-08 22:32:48 -070053// Distance (in meters) from the edge of the field to the port, with some
54// compensation to ensure that our definition of where the target is matches
55// that reported by the cameras.
56constexpr double kEdgeOfFieldToPort = 2.404 + .0034;
James Kuszmaula53c3ac2020-02-22 19:36:01 -080057
58// The amount (in meters) that the inner port is set back from the outer port.
59constexpr double kInnerPortBackset = 0.743;
60
James Kuszmaul3b393d72020-02-26 19:43:51 -080061// Average speed-over-ground of the ball on its way to the target. Our current
62// model assumes constant ball velocity regardless of shot distance.
63// TODO(james): Is this an appropriate model? For the outer port it should be
64// good enough that it doesn't really matter, but for the inner port it may be
65// more appropriate to do something more dynamic--however, it is not yet clear
66// how we would best estimate speed-over-ground given a hood angle + shooter
67// speed. Assuming a constant average speed over the course of the trajectory
68// should be reasonable, since all we are trying to do here is calculate an
69// overall time-of-flight (we don't actually care about the ball speed itself).
70constexpr double kBallSpeedOverGround = 15.0; // m/s
James Kuszmaulb83d6e12020-02-22 20:44:48 -080071
James Kuszmaula53c3ac2020-02-22 19:36:01 -080072// Minimum distance that we must be from the inner port in order to attempt the
73// shot--this is to account for the fact that if we are too close to the target,
74// then we won't have a clear shot on the inner port.
Austin Schuh4408c422021-10-16 13:56:07 -070075constexpr double kMinimumInnerPortShotDistance = 3.5;
James Kuszmaula53c3ac2020-02-22 19:36:01 -080076
James Kuszmaulb83d6e12020-02-22 20:44:48 -080077// Amount of buffer, in radians, to leave to help avoid wrapping. I.e., any time
78// that we are in kAvoidEdges mode, we will keep ourselves at least
79// kAntiWrapBuffer radians away from the hardstops.
80constexpr double kAntiWrapBuffer = 0.2;
81
James Kuszmaul64c13b72020-03-01 11:17:31 -080082// If the turret is at zero, then it will be at this angle relative to pointed
83// straight forwards on the robot.
84constexpr double kTurretZeroOffset = M_PI;
85
James Kuszmaulb83d6e12020-02-22 20:44:48 -080086constexpr double kTurretRange = constants::Values::kTurretRange().range();
87static_assert((kTurretRange - 2.0 * kAntiWrapBuffer) > 2.0 * M_PI,
88 "kAntiWrap buffer should be small enough that we still have 360 "
89 "degrees of range.");
90
James Kuszmaula53c3ac2020-02-22 19:36:01 -080091Pose ReverseSideOfField(Pose target) {
92 *target.mutable_pos() *= -1;
93 target.set_theta(aos::math::NormalizeAngle(target.rel_theta() + M_PI));
94 return target;
95}
96
James Kuszmaulb1b2d8e2020-02-21 21:11:46 -080097flatbuffers::DetachedBuffer MakePrefilledGoal() {
98 flatbuffers::FlatBufferBuilder fbb;
99 fbb.ForceDefaults(true);
100 Aimer::Goal::Builder builder(fbb);
101 builder.add_unsafe_goal(0);
102 builder.add_goal_velocity(0);
103 builder.add_ignore_profile(true);
104 fbb.Finish(builder.Finish());
105 return fbb.Release();
106}
James Kuszmaul3b393d72020-02-26 19:43:51 -0800107
108// This implements the iteration in the described shooting-on-the-fly algorithm.
109// robot_pose: Current robot pose.
110// robot_velocity: Current robot velocity, in the absolute field frame.
111// target_pose: Absolute goal Pose.
112// current_virtual_pose: Current estimate of where we want to shoot at.
113Pose IterateVirtualGoal(const Pose &robot_pose,
114 const Eigen::Vector3d &robot_velocity,
115 const Pose &target_pose,
116 const Pose &current_virtual_pose) {
117 const double air_time =
118 current_virtual_pose.Rebase(&robot_pose).xy_norm() / kBallSpeedOverGround;
119 const Eigen::Vector3d virtual_target =
120 target_pose.abs_pos() - air_time * robot_velocity;
121 return Pose(virtual_target, target_pose.abs_theta());
122}
James Kuszmaulb1b2d8e2020-02-21 21:11:46 -0800123} // namespace
124
James Kuszmaula53c3ac2020-02-22 19:36:01 -0800125Pose InnerPortPose(aos::Alliance alliance) {
126 const Pose target({kFieldLength / 2 + kInnerPortBackset,
127 -kFieldWidth / 2.0 + kEdgeOfFieldToPort, kPortHeight},
James Kuszmaul519585d2020-03-08 22:32:48 -0700128 M_PI);
James Kuszmaula53c3ac2020-02-22 19:36:01 -0800129 if (alliance == aos::Alliance::kRed) {
130 return ReverseSideOfField(target);
131 }
132 return target;
133}
134
135Pose OuterPortPose(aos::Alliance alliance) {
136 Pose target(
137 {kFieldLength / 2, -kFieldWidth / 2.0 + kEdgeOfFieldToPort, kPortHeight},
James Kuszmaul519585d2020-03-08 22:32:48 -0700138 M_PI);
James Kuszmaula53c3ac2020-02-22 19:36:01 -0800139 if (alliance == aos::Alliance::kRed) {
140 return ReverseSideOfField(target);
141 }
142 return target;
143}
144
James Kuszmaulb1b2d8e2020-02-21 21:11:46 -0800145Aimer::Aimer() : goal_(MakePrefilledGoal()) {}
146
James Kuszmaul3b393d72020-02-26 19:43:51 -0800147void Aimer::Update(const Status *status, aos::Alliance alliance,
148 WrapMode wrap_mode, ShotMode shot_mode) {
James Kuszmaulb1b2d8e2020-02-21 21:11:46 -0800149 const Pose robot_pose({status->x(), status->y(), 0}, status->theta());
James Kuszmaula53c3ac2020-02-22 19:36:01 -0800150 const Pose inner_port = InnerPortPose(alliance);
151 const Pose outer_port = OuterPortPose(alliance);
152 const Pose robot_pose_from_inner_port = robot_pose.Rebase(&inner_port);
James Kuszmaul3b393d72020-02-26 19:43:51 -0800153
James Kuszmaulb1b2d8e2020-02-21 21:11:46 -0800154 // TODO(james): This code should probably just be in the localizer and have
155 // xdot/ydot get populated in the status message directly... that way we don't
156 // keep duplicating this math.
157 // Also, this doesn't currently take into account the lateral velocity of the
158 // robot. All of this would be helped by just doing this work in the Localizer
159 // itself.
160 const Eigen::Vector2d linear_angular =
161 drivetrain::GetDrivetrainConfig().Tlr_to_la() *
162 Eigen::Vector2d(status->localizer()->left_velocity(),
163 status->localizer()->right_velocity());
James Kuszmaul3b393d72020-02-26 19:43:51 -0800164 const double xdot = linear_angular(0) * std::cos(status->theta());
165 const double ydot = linear_angular(0) * std::sin(status->theta());
166
167 const double inner_port_angle = robot_pose_from_inner_port.heading();
168 const double inner_port_distance = robot_pose_from_inner_port.xy_norm();
James Kuszmaul519585d2020-03-08 22:32:48 -0700169 // Add a bit of hysteresis so that we don't jump between aiming for the inner
170 // and outer ports.
171 const double max_inner_port_angle =
172 aiming_for_inner_port_ ? 1.2 * kMaxInnerPortAngle : kMaxInnerPortAngle;
173 const double min_inner_port_distance =
174 aiming_for_inner_port_ ? 0.8 * kMinimumInnerPortShotDistance
175 : kMinimumInnerPortShotDistance;
James Kuszmaul3b393d72020-02-26 19:43:51 -0800176 aiming_for_inner_port_ =
James Kuszmaul519585d2020-03-08 22:32:48 -0700177 (std::abs(inner_port_angle) < max_inner_port_angle) &&
178 (inner_port_distance > min_inner_port_distance);
James Kuszmaul3b393d72020-02-26 19:43:51 -0800179
180 // This code manages compensating the goal turret heading for the robot's
181 // current velocity, to allow for shooting on-the-fly.
182 // This works by solving for the correct turret angle numerically, since while
183 // we technically could do it analytically, doing so would both make it hard
184 // to make small changes (since it would force us to redo the math) and be
185 // error-prone since it'd be easy to make typos or other minor math errors.
186 Pose virtual_goal;
187 {
188 const Pose goal = aiming_for_inner_port_ ? inner_port : outer_port;
James Kuszmaul519585d2020-03-08 22:32:48 -0700189 target_distance_ = goal.Rebase(&robot_pose).xy_norm();
James Kuszmaul3b393d72020-02-26 19:43:51 -0800190 virtual_goal = goal;
191 if (shot_mode == ShotMode::kShootOnTheFly) {
192 for (int ii = 0; ii < 3; ++ii) {
193 virtual_goal =
194 IterateVirtualGoal(robot_pose, {xdot, ydot, 0}, goal, virtual_goal);
195 }
196 VLOG(1) << "Shooting-on-the-fly target position: "
197 << virtual_goal.abs_pos().transpose();
198 }
199 virtual_goal = virtual_goal.Rebase(&robot_pose);
200 }
201
202 const double heading_to_goal = virtual_goal.heading();
203 CHECK(status->has_localizer());
James Kuszmaul519585d2020-03-08 22:32:48 -0700204 shot_distance_ = virtual_goal.xy_norm();
James Kuszmaul3b393d72020-02-26 19:43:51 -0800205
206 // The following code all works to calculate what the rate of turn of the
207 // turret should be. The code only accounts for the rate of turn if we are
208 // aiming at a static target, which should be close enough to correct that it
209 // doesn't matter that it fails to account for the
210 // shooting-on-the-fly compensation.
211 const double rel_x = virtual_goal.rel_pos().x();
212 const double rel_y = virtual_goal.rel_pos().y();
James Kuszmaulb1b2d8e2020-02-21 21:11:46 -0800213 const double squared_norm = rel_x * rel_x + rel_y * rel_y;
James Kuszmaul519585d2020-03-08 22:32:48 -0700214 // rel_xdot and rel_ydot are the derivatives (with respect to time) of rel_x
215 // and rel_y. Since these are in the robot's coordinate frame, and since we
216 // are ignoring lateral velocity for this exercise, rel_ydot is zero, and
217 // rel_xdot is just the inverse of the robot's velocity.
218 const double rel_xdot = -linear_angular(0);
219 const double rel_ydot = 0.0;
James Kuszmaul3b393d72020-02-26 19:43:51 -0800220
James Kuszmaulb1b2d8e2020-02-21 21:11:46 -0800221 // If squared_norm gets to be too close to zero, just zero out the relevant
222 // term to prevent NaNs. Note that this doesn't address the chattering that
223 // would likely occur if we were to get excessively close to the target.
James Kuszmaul3b393d72020-02-26 19:43:51 -0800224 // Note that x and y terms are swapped relative to what you would normally see
225 // in the derivative of atan because xdot and ydot are the derivatives of
226 // robot_pos and we are working with the atan of (target_pos - robot_pos).
James Kuszmaul519585d2020-03-08 22:32:48 -0700227 const double atan_diff =
228 (squared_norm < 1e-3) ? 0.0 : (rel_x * rel_ydot - rel_y * rel_xdot) /
229 squared_norm;
James Kuszmaulb1b2d8e2020-02-21 21:11:46 -0800230 // heading = atan2(relative_y, relative_x) - robot_theta
James Kuszmaul519585d2020-03-08 22:32:48 -0700231 // dheading / dt =
232 // (rel_x * rel_y' - rel_y * rel_x') / (rel_x^2 + rel_y^2) - dtheta / dt
James Kuszmaulb1b2d8e2020-02-21 21:11:46 -0800233 const double dheading_dt = atan_diff - linear_angular(1);
234
James Kuszmaulb83d6e12020-02-22 20:44:48 -0800235 double range = kTurretRange;
James Kuszmaul3b393d72020-02-26 19:43:51 -0800236 if (wrap_mode == WrapMode::kAvoidEdges) {
James Kuszmaulb83d6e12020-02-22 20:44:48 -0800237 range -= 2.0 * kAntiWrapBuffer;
238 }
239 // Calculate a goal turret heading such that it is within +/- pi of the
240 // current position (i.e., a goal that would minimize the amount the turret
241 // would have to travel).
242 // We then check if this goal would bring us out of range of the valid angles,
243 // and if it would, we reset to be within +/- pi of zero.
James Kuszmaul64c13b72020-03-01 11:17:31 -0800244 double turret_heading =
245 goal_.message().unsafe_goal() +
246 aos::math::NormalizeAngle(heading_to_goal - kTurretZeroOffset -
247 goal_.message().unsafe_goal());
James Kuszmaulb83d6e12020-02-22 20:44:48 -0800248 if (std::abs(turret_heading - constants::Values::kTurretRange().middle()) >
249 range / 2.0) {
250 turret_heading = aos::math::NormalizeAngle(turret_heading);
251 }
252
253 goal_.mutable_message()->mutate_unsafe_goal(turret_heading);
James Kuszmaul519585d2020-03-08 22:32:48 -0700254 goal_.mutable_message()->mutate_goal_velocity(
255 std::clamp(dheading_dt, -2.0, 2.0));
James Kuszmaulb1b2d8e2020-02-21 21:11:46 -0800256}
257
258flatbuffers::Offset<AimerStatus> Aimer::PopulateStatus(
259 flatbuffers::FlatBufferBuilder *fbb) const {
260 AimerStatus::Builder builder(*fbb);
261 builder.add_turret_position(goal_.message().unsafe_goal());
262 builder.add_turret_velocity(goal_.message().goal_velocity());
James Kuszmaula53c3ac2020-02-22 19:36:01 -0800263 builder.add_aiming_for_inner_port(aiming_for_inner_port_);
James Kuszmaul519585d2020-03-08 22:32:48 -0700264 builder.add_target_distance(target_distance_);
265 builder.add_shot_distance(DistanceToGoal());
James Kuszmaulb1b2d8e2020-02-21 21:11:46 -0800266 return builder.Finish();
267}
268
269} // namespace turret
270} // namespace superstructure
271} // namespace control_loops
272} // namespace y2020