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 | |
| 6 | namespace y2020 { |
| 7 | namespace control_loops { |
| 8 | namespace superstructure { |
| 9 | namespace turret { |
| 10 | |
| 11 | using frc971::control_loops::Pose; |
| 12 | |
| 13 | namespace { |
James Kuszmaul | a53c3ac | 2020-02-22 19:36:01 -0800 | [diff] [blame] | 14 | // The overall length and width of the field, in meters. |
| 15 | constexpr double kFieldLength = 15.983; |
| 16 | constexpr double kFieldWidth = 8.212; |
| 17 | // Height of the center of the port(s) above the ground, in meters. |
| 18 | constexpr double kPortHeight = 2.494; |
| 19 | |
| 20 | // Maximum shot angle at which we will attempt to make the shot into the inner |
| 21 | // port, in radians. Zero would imply that we could only shoot if we were |
| 22 | // exactly perpendicular to the target. Larger numbers allow us to aim at the |
| 23 | // inner port more aggressively, at the risk of being more likely to miss the |
| 24 | // outer port entirely. |
| 25 | constexpr double kMaxInnerPortAngle = 20.0 * M_PI / 180.0; |
| 26 | |
| 27 | // Distance (in meters) from the edge of the field to the port. |
| 28 | constexpr double kEdgeOfFieldToPort = 2.404; |
| 29 | |
| 30 | // The amount (in meters) that the inner port is set back from the outer port. |
| 31 | constexpr double kInnerPortBackset = 0.743; |
| 32 | |
James Kuszmaul | b83d6e1 | 2020-02-22 20:44:48 -0800 | [diff] [blame^] | 33 | |
James Kuszmaul | a53c3ac | 2020-02-22 19:36:01 -0800 | [diff] [blame] | 34 | // Minimum distance that we must be from the inner port in order to attempt the |
| 35 | // shot--this is to account for the fact that if we are too close to the target, |
| 36 | // then we won't have a clear shot on the inner port. |
| 37 | constexpr double kMinimumInnerPortShotDistance = 4.0; |
| 38 | |
James Kuszmaul | b83d6e1 | 2020-02-22 20:44:48 -0800 | [diff] [blame^] | 39 | // Amount of buffer, in radians, to leave to help avoid wrapping. I.e., any time |
| 40 | // that we are in kAvoidEdges mode, we will keep ourselves at least |
| 41 | // kAntiWrapBuffer radians away from the hardstops. |
| 42 | constexpr double kAntiWrapBuffer = 0.2; |
| 43 | |
| 44 | constexpr double kTurretRange = constants::Values::kTurretRange().range(); |
| 45 | static_assert((kTurretRange - 2.0 * kAntiWrapBuffer) > 2.0 * M_PI, |
| 46 | "kAntiWrap buffer should be small enough that we still have 360 " |
| 47 | "degrees of range."); |
| 48 | |
James Kuszmaul | a53c3ac | 2020-02-22 19:36:01 -0800 | [diff] [blame] | 49 | Pose ReverseSideOfField(Pose target) { |
| 50 | *target.mutable_pos() *= -1; |
| 51 | target.set_theta(aos::math::NormalizeAngle(target.rel_theta() + M_PI)); |
| 52 | return target; |
| 53 | } |
| 54 | |
James Kuszmaul | b1b2d8e | 2020-02-21 21:11:46 -0800 | [diff] [blame] | 55 | flatbuffers::DetachedBuffer MakePrefilledGoal() { |
| 56 | flatbuffers::FlatBufferBuilder fbb; |
| 57 | fbb.ForceDefaults(true); |
| 58 | Aimer::Goal::Builder builder(fbb); |
| 59 | builder.add_unsafe_goal(0); |
| 60 | builder.add_goal_velocity(0); |
| 61 | builder.add_ignore_profile(true); |
| 62 | fbb.Finish(builder.Finish()); |
| 63 | return fbb.Release(); |
| 64 | } |
| 65 | } // namespace |
| 66 | |
James Kuszmaul | a53c3ac | 2020-02-22 19:36:01 -0800 | [diff] [blame] | 67 | Pose InnerPortPose(aos::Alliance alliance) { |
| 68 | const Pose target({kFieldLength / 2 + kInnerPortBackset, |
| 69 | -kFieldWidth / 2.0 + kEdgeOfFieldToPort, kPortHeight}, |
| 70 | 0.0); |
| 71 | if (alliance == aos::Alliance::kRed) { |
| 72 | return ReverseSideOfField(target); |
| 73 | } |
| 74 | return target; |
| 75 | } |
| 76 | |
| 77 | Pose OuterPortPose(aos::Alliance alliance) { |
| 78 | Pose target( |
| 79 | {kFieldLength / 2, -kFieldWidth / 2.0 + kEdgeOfFieldToPort, kPortHeight}, |
| 80 | 0.0); |
| 81 | if (alliance == aos::Alliance::kRed) { |
| 82 | return ReverseSideOfField(target); |
| 83 | } |
| 84 | return target; |
| 85 | } |
| 86 | |
James Kuszmaul | b1b2d8e | 2020-02-21 21:11:46 -0800 | [diff] [blame] | 87 | Aimer::Aimer() : goal_(MakePrefilledGoal()) {} |
| 88 | |
James Kuszmaul | b83d6e1 | 2020-02-22 20:44:48 -0800 | [diff] [blame^] | 89 | void Aimer::Update(const Status *status, aos::Alliance alliance, Mode mode) { |
James Kuszmaul | b1b2d8e | 2020-02-21 21:11:46 -0800 | [diff] [blame] | 90 | const Pose robot_pose({status->x(), status->y(), 0}, status->theta()); |
James Kuszmaul | a53c3ac | 2020-02-22 19:36:01 -0800 | [diff] [blame] | 91 | const Pose inner_port = InnerPortPose(alliance); |
| 92 | const Pose outer_port = OuterPortPose(alliance); |
| 93 | const Pose robot_pose_from_inner_port = robot_pose.Rebase(&inner_port); |
| 94 | const double inner_port_angle = robot_pose_from_inner_port.heading(); |
| 95 | const double inner_port_distance = robot_pose_from_inner_port.xy_norm(); |
| 96 | aiming_for_inner_port_ = |
| 97 | (std::abs(inner_port_angle) < kMaxInnerPortAngle) && |
| 98 | (inner_port_distance > kMinimumInnerPortShotDistance); |
| 99 | const Pose goal = |
| 100 | (aiming_for_inner_port_ ? inner_port : outer_port).Rebase(&robot_pose); |
James Kuszmaul | b1b2d8e | 2020-02-21 21:11:46 -0800 | [diff] [blame] | 101 | const double heading_to_goal = goal.heading(); |
| 102 | CHECK(status->has_localizer()); |
James Kuszmaul | a53c3ac | 2020-02-22 19:36:01 -0800 | [diff] [blame] | 103 | distance_ = goal.xy_norm(); |
James Kuszmaul | b1b2d8e | 2020-02-21 21:11:46 -0800 | [diff] [blame] | 104 | // TODO(james): This code should probably just be in the localizer and have |
| 105 | // xdot/ydot get populated in the status message directly... that way we don't |
| 106 | // keep duplicating this math. |
| 107 | // Also, this doesn't currently take into account the lateral velocity of the |
| 108 | // robot. All of this would be helped by just doing this work in the Localizer |
| 109 | // itself. |
| 110 | const Eigen::Vector2d linear_angular = |
| 111 | drivetrain::GetDrivetrainConfig().Tlr_to_la() * |
| 112 | Eigen::Vector2d(status->localizer()->left_velocity(), |
| 113 | status->localizer()->right_velocity()); |
| 114 | // X and Y dot are negated because we are interested in the derivative of |
| 115 | // (target_pos - robot_pos). |
| 116 | const double xdot = -linear_angular(0) * std::cos(status->theta()); |
| 117 | const double ydot = -linear_angular(0) * std::sin(status->theta()); |
| 118 | const double rel_x = goal.rel_pos().x(); |
| 119 | const double rel_y = goal.rel_pos().y(); |
| 120 | const double squared_norm = rel_x * rel_x + rel_y * rel_y; |
| 121 | // If squared_norm gets to be too close to zero, just zero out the relevant |
| 122 | // term to prevent NaNs. Note that this doesn't address the chattering that |
| 123 | // would likely occur if we were to get excessively close to the target. |
| 124 | const double atan_diff = (squared_norm < 1e-3) |
| 125 | ? 0.0 |
| 126 | : (rel_x * ydot - rel_y * xdot) / squared_norm; |
| 127 | // heading = atan2(relative_y, relative_x) - robot_theta |
| 128 | // dheading / dt = (rel_x * rel_y' - rel_y * rel_x') / (rel_x^2 + rel_y^2) - dtheta / dt |
| 129 | const double dheading_dt = atan_diff - linear_angular(1); |
| 130 | |
James Kuszmaul | b83d6e1 | 2020-02-22 20:44:48 -0800 | [diff] [blame^] | 131 | double range = kTurretRange; |
| 132 | if (mode == Mode::kAvoidEdges) { |
| 133 | range -= 2.0 * kAntiWrapBuffer; |
| 134 | } |
| 135 | // Calculate a goal turret heading such that it is within +/- pi of the |
| 136 | // current position (i.e., a goal that would minimize the amount the turret |
| 137 | // would have to travel). |
| 138 | // We then check if this goal would bring us out of range of the valid angles, |
| 139 | // and if it would, we reset to be within +/- pi of zero. |
| 140 | double turret_heading = goal_.message().unsafe_goal() + |
| 141 | aos::math::NormalizeAngle( |
| 142 | heading_to_goal - goal_.message().unsafe_goal()); |
| 143 | if (std::abs(turret_heading - constants::Values::kTurretRange().middle()) > |
| 144 | range / 2.0) { |
| 145 | turret_heading = aos::math::NormalizeAngle(turret_heading); |
| 146 | } |
| 147 | |
| 148 | goal_.mutable_message()->mutate_unsafe_goal(turret_heading); |
James Kuszmaul | b1b2d8e | 2020-02-21 21:11:46 -0800 | [diff] [blame] | 149 | goal_.mutable_message()->mutate_goal_velocity(dheading_dt); |
| 150 | } |
| 151 | |
| 152 | flatbuffers::Offset<AimerStatus> Aimer::PopulateStatus( |
| 153 | flatbuffers::FlatBufferBuilder *fbb) const { |
| 154 | AimerStatus::Builder builder(*fbb); |
| 155 | builder.add_turret_position(goal_.message().unsafe_goal()); |
| 156 | builder.add_turret_velocity(goal_.message().goal_velocity()); |
James Kuszmaul | a53c3ac | 2020-02-22 19:36:01 -0800 | [diff] [blame] | 157 | builder.add_aiming_for_inner_port(aiming_for_inner_port_); |
James Kuszmaul | b1b2d8e | 2020-02-21 21:11:46 -0800 | [diff] [blame] | 158 | return builder.Finish(); |
| 159 | } |
| 160 | |
| 161 | } // namespace turret |
| 162 | } // namespace superstructure |
| 163 | } // namespace control_loops |
| 164 | } // namespace y2020 |