James Kuszmaul | b1b2d8e | 2020-02-21 21:11:46 -0800 | [diff] [blame] | 1 | #include "y2020/control_loops/superstructure/turret/aiming.h" |
| 2 | |
James Kuszmaul | b83d6e1 | 2020-02-22 20:44:48 -0800 | [diff] [blame] | 3 | #include "y2020/constants.h" |
James Kuszmaul | b1b2d8e | 2020-02-21 21:11:46 -0800 | [diff] [blame] | 4 | #include "y2020/control_loops/drivetrain/drivetrain_base.h" |
| 5 | |
Stephan Pleines | f63bde8 | 2024-01-13 15:59:33 -0800 | [diff] [blame] | 6 | namespace y2020::control_loops::superstructure::turret { |
James Kuszmaul | b1b2d8e | 2020-02-21 21:11:46 -0800 | [diff] [blame] | 7 | |
| 8 | using frc971::control_loops::Pose; |
James Kuszmaul | 851b396 | 2022-02-27 16:42:15 -0800 | [diff] [blame] | 9 | using frc971::control_loops::aiming::RobotState; |
Austin Schuh | 9b2c334 | 2023-02-05 11:31:25 -0800 | [diff] [blame] | 10 | using frc971::control_loops::aiming::ShotConfig; |
| 11 | using frc971::control_loops::aiming::TurretGoal; |
James Kuszmaul | b1b2d8e | 2020-02-21 21:11:46 -0800 | [diff] [blame] | 12 | |
James Kuszmaul | 3b393d7 | 2020-02-26 19:43:51 -0800 | [diff] [blame] | 13 | // 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 Kuszmaul | b1b2d8e | 2020-02-21 21:11:46 -0800 | [diff] [blame] | 39 | namespace { |
James Kuszmaul | a53c3ac | 2020-02-22 19:36:01 -0800 | [diff] [blame] | 40 | // The overall length and width of the field, in meters. |
| 41 | constexpr double kFieldLength = 15.983; |
| 42 | constexpr double kFieldWidth = 8.212; |
| 43 | // Height of the center of the port(s) above the ground, in meters. |
| 44 | constexpr 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. |
Austin Schuh | c69fb1a | 2021-10-24 17:41:55 -0700 | [diff] [blame] | 51 | constexpr double kMaxInnerPortAngle = 15.0 * M_PI / 180.0; |
James Kuszmaul | a53c3ac | 2020-02-22 19:36:01 -0800 | [diff] [blame] | 52 | |
James Kuszmaul | 519585d | 2020-03-08 22:32:48 -0700 | [diff] [blame] | 53 | // 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. |
James Kuszmaul | 87c4105 | 2021-10-29 14:13:40 -0700 | [diff] [blame] | 56 | constexpr double kEdgeOfFieldToPort = 2.347; |
James Kuszmaul | a53c3ac | 2020-02-22 19:36:01 -0800 | [diff] [blame] | 57 | |
| 58 | // The amount (in meters) that the inner port is set back from the outer port. |
| 59 | constexpr double kInnerPortBackset = 0.743; |
| 60 | |
James Kuszmaul | 3b393d7 | 2020-02-26 19:43:51 -0800 | [diff] [blame] | 61 | // 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). |
milind-u | f7fadbf | 2021-11-07 14:10:54 -0800 | [diff] [blame] | 70 | constexpr double kBallSpeedOverGround = 17.0; // m/s |
James Kuszmaul | b83d6e1 | 2020-02-22 20:44:48 -0800 | [diff] [blame] | 71 | |
James Kuszmaul | a53c3ac | 2020-02-22 19:36:01 -0800 | [diff] [blame] | 72 | // 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. |
milind-u | f7fadbf | 2021-11-07 14:10:54 -0800 | [diff] [blame] | 75 | constexpr double kMinimumInnerPortShotDistance = 1.9; |
James Kuszmaul | a53c3ac | 2020-02-22 19:36:01 -0800 | [diff] [blame] | 76 | |
James Kuszmaul | b83d6e1 | 2020-02-22 20:44:48 -0800 | [diff] [blame] | 77 | // 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. |
| 80 | constexpr double kAntiWrapBuffer = 0.2; |
| 81 | |
James Kuszmaul | 64c13b7 | 2020-03-01 11:17:31 -0800 | [diff] [blame] | 82 | // If the turret is at zero, then it will be at this angle relative to pointed |
| 83 | // straight forwards on the robot. |
| 84 | constexpr double kTurretZeroOffset = M_PI; |
| 85 | |
James Kuszmaul | b83d6e1 | 2020-02-22 20:44:48 -0800 | [diff] [blame] | 86 | constexpr double kTurretRange = constants::Values::kTurretRange().range(); |
| 87 | static_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 Kuszmaul | a53c3ac | 2020-02-22 19:36:01 -0800 | [diff] [blame] | 91 | Pose 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 Kuszmaul | b1b2d8e | 2020-02-21 21:11:46 -0800 | [diff] [blame] | 97 | flatbuffers::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 | } |
| 107 | } // namespace |
| 108 | |
James Kuszmaul | a53c3ac | 2020-02-22 19:36:01 -0800 | [diff] [blame] | 109 | Pose InnerPortPose(aos::Alliance alliance) { |
| 110 | const Pose target({kFieldLength / 2 + kInnerPortBackset, |
| 111 | -kFieldWidth / 2.0 + kEdgeOfFieldToPort, kPortHeight}, |
James Kuszmaul | 519585d | 2020-03-08 22:32:48 -0700 | [diff] [blame] | 112 | M_PI); |
James Kuszmaul | a53c3ac | 2020-02-22 19:36:01 -0800 | [diff] [blame] | 113 | if (alliance == aos::Alliance::kRed) { |
| 114 | return ReverseSideOfField(target); |
| 115 | } |
| 116 | return target; |
| 117 | } |
| 118 | |
| 119 | Pose OuterPortPose(aos::Alliance alliance) { |
| 120 | Pose target( |
| 121 | {kFieldLength / 2, -kFieldWidth / 2.0 + kEdgeOfFieldToPort, kPortHeight}, |
James Kuszmaul | 519585d | 2020-03-08 22:32:48 -0700 | [diff] [blame] | 122 | M_PI); |
James Kuszmaul | a53c3ac | 2020-02-22 19:36:01 -0800 | [diff] [blame] | 123 | if (alliance == aos::Alliance::kRed) { |
| 124 | return ReverseSideOfField(target); |
| 125 | } |
| 126 | return target; |
| 127 | } |
| 128 | |
Austin Schuh | 9b2c334 | 2023-02-05 11:31:25 -0800 | [diff] [blame] | 129 | Aimer::Aimer() |
| 130 | : goal_(MakePrefilledGoal()), |
| 131 | Tlr_to_la_(drivetrain::GetDrivetrainConfig().Tlr_to_la()) {} |
James Kuszmaul | b1b2d8e | 2020-02-21 21:11:46 -0800 | [diff] [blame] | 132 | |
James Kuszmaul | 3b393d7 | 2020-02-26 19:43:51 -0800 | [diff] [blame] | 133 | void Aimer::Update(const Status *status, aos::Alliance alliance, |
| 134 | WrapMode wrap_mode, ShotMode shot_mode) { |
James Kuszmaul | b1b2d8e | 2020-02-21 21:11:46 -0800 | [diff] [blame] | 135 | const Pose robot_pose({status->x(), status->y(), 0}, status->theta()); |
James Kuszmaul | a53c3ac | 2020-02-22 19:36:01 -0800 | [diff] [blame] | 136 | const Pose inner_port = InnerPortPose(alliance); |
| 137 | const Pose outer_port = OuterPortPose(alliance); |
| 138 | const Pose robot_pose_from_inner_port = robot_pose.Rebase(&inner_port); |
James Kuszmaul | 3b393d7 | 2020-02-26 19:43:51 -0800 | [diff] [blame] | 139 | |
James Kuszmaul | b1b2d8e | 2020-02-21 21:11:46 -0800 | [diff] [blame] | 140 | // TODO(james): This code should probably just be in the localizer and have |
| 141 | // xdot/ydot get populated in the status message directly... that way we don't |
| 142 | // keep duplicating this math. |
| 143 | // Also, this doesn't currently take into account the lateral velocity of the |
| 144 | // robot. All of this would be helped by just doing this work in the Localizer |
| 145 | // itself. |
| 146 | const Eigen::Vector2d linear_angular = |
Austin Schuh | 9b2c334 | 2023-02-05 11:31:25 -0800 | [diff] [blame] | 147 | Tlr_to_la_ * Eigen::Vector2d(status->localizer()->left_velocity(), |
| 148 | status->localizer()->right_velocity()); |
James Kuszmaul | 3b393d7 | 2020-02-26 19:43:51 -0800 | [diff] [blame] | 149 | const double xdot = linear_angular(0) * std::cos(status->theta()); |
| 150 | const double ydot = linear_angular(0) * std::sin(status->theta()); |
| 151 | |
James Kuszmaul | 88c5cef | 2021-10-23 13:17:36 -0700 | [diff] [blame] | 152 | inner_port_angle_ = robot_pose_from_inner_port.heading(); |
Austin Schuh | 30e45ff | 2021-10-16 18:33:53 -0700 | [diff] [blame] | 153 | const double inner_port_distance = robot_pose_from_inner_port.rel_pos().x(); |
James Kuszmaul | 519585d | 2020-03-08 22:32:48 -0700 | [diff] [blame] | 154 | // Add a bit of hysteresis so that we don't jump between aiming for the inner |
| 155 | // and outer ports. |
| 156 | const double max_inner_port_angle = |
| 157 | aiming_for_inner_port_ ? 1.2 * kMaxInnerPortAngle : kMaxInnerPortAngle; |
| 158 | const double min_inner_port_distance = |
Austin Schuh | 30e45ff | 2021-10-16 18:33:53 -0700 | [diff] [blame] | 159 | aiming_for_inner_port_ ? (kMinimumInnerPortShotDistance - 0.3) |
James Kuszmaul | 519585d | 2020-03-08 22:32:48 -0700 | [diff] [blame] | 160 | : kMinimumInnerPortShotDistance; |
James Kuszmaul | 3b393d7 | 2020-02-26 19:43:51 -0800 | [diff] [blame] | 161 | aiming_for_inner_port_ = |
James Kuszmaul | 88c5cef | 2021-10-23 13:17:36 -0700 | [diff] [blame] | 162 | (std::abs(inner_port_angle_) < max_inner_port_angle) && |
James Kuszmaul | 519585d | 2020-03-08 22:32:48 -0700 | [diff] [blame] | 163 | (inner_port_distance > min_inner_port_distance); |
James Kuszmaul | 851b396 | 2022-02-27 16:42:15 -0800 | [diff] [blame] | 164 | const Pose goal = aiming_for_inner_port_ ? inner_port : outer_port; |
James Kuszmaul | 3b393d7 | 2020-02-26 19:43:51 -0800 | [diff] [blame] | 165 | |
James Kuszmaul | 851b396 | 2022-02-27 16:42:15 -0800 | [diff] [blame] | 166 | const struct TurretGoal turret_goal = |
| 167 | frc971::control_loops::aiming::AimerGoal( |
| 168 | ShotConfig{goal, shot_mode, constants::Values::kTurretRange(), |
| 169 | kBallSpeedOverGround, |
| 170 | wrap_mode == WrapMode::kAvoidEdges ? kAntiWrapBuffer : 0.0, |
| 171 | kTurretZeroOffset}, |
| 172 | RobotState{robot_pose, |
| 173 | {xdot, ydot}, |
| 174 | linear_angular(1), |
| 175 | goal_.message().unsafe_goal()}); |
James Kuszmaul | 3b393d7 | 2020-02-26 19:43:51 -0800 | [diff] [blame] | 176 | |
James Kuszmaul | 851b396 | 2022-02-27 16:42:15 -0800 | [diff] [blame] | 177 | target_distance_ = turret_goal.target_distance; |
| 178 | shot_distance_ = turret_goal.virtual_shot_distance; |
James Kuszmaul | 3b393d7 | 2020-02-26 19:43:51 -0800 | [diff] [blame] | 179 | |
James Kuszmaul | 851b396 | 2022-02-27 16:42:15 -0800 | [diff] [blame] | 180 | goal_.mutable_message()->mutate_unsafe_goal(turret_goal.position); |
James Kuszmaul | 519585d | 2020-03-08 22:32:48 -0700 | [diff] [blame] | 181 | goal_.mutable_message()->mutate_goal_velocity( |
James Kuszmaul | 851b396 | 2022-02-27 16:42:15 -0800 | [diff] [blame] | 182 | std::clamp(turret_goal.velocity, -2.0, 2.0)); |
James Kuszmaul | b1b2d8e | 2020-02-21 21:11:46 -0800 | [diff] [blame] | 183 | } |
| 184 | |
| 185 | flatbuffers::Offset<AimerStatus> Aimer::PopulateStatus( |
| 186 | flatbuffers::FlatBufferBuilder *fbb) const { |
| 187 | AimerStatus::Builder builder(*fbb); |
| 188 | builder.add_turret_position(goal_.message().unsafe_goal()); |
| 189 | builder.add_turret_velocity(goal_.message().goal_velocity()); |
James Kuszmaul | a53c3ac | 2020-02-22 19:36:01 -0800 | [diff] [blame] | 190 | builder.add_aiming_for_inner_port(aiming_for_inner_port_); |
James Kuszmaul | 519585d | 2020-03-08 22:32:48 -0700 | [diff] [blame] | 191 | builder.add_target_distance(target_distance_); |
James Kuszmaul | 88c5cef | 2021-10-23 13:17:36 -0700 | [diff] [blame] | 192 | builder.add_inner_port_angle(inner_port_angle_); |
James Kuszmaul | 519585d | 2020-03-08 22:32:48 -0700 | [diff] [blame] | 193 | builder.add_shot_distance(DistanceToGoal()); |
James Kuszmaul | b1b2d8e | 2020-02-21 21:11:46 -0800 | [diff] [blame] | 194 | return builder.Finish(); |
| 195 | } |
| 196 | |
Stephan Pleines | f63bde8 | 2024-01-13 15:59:33 -0800 | [diff] [blame] | 197 | } // namespace y2020::control_loops::superstructure::turret |