Add target selection based on buttons + mode

This makes ball vs hatch mode discriminate among targets on the rocket,
and adds buttons to control which target on the cargo ship we go for.

Also, add new color to indicate that we have a target (purple flashing).

Change-Id: Ifc734c8168a1814511dea95abc361eb13383f597
diff --git a/y2019/BUILD b/y2019/BUILD
index f2150ed..c49e7c4 100644
--- a/y2019/BUILD
+++ b/y2019/BUILD
@@ -136,6 +136,7 @@
         "//frc971/control_loops/drivetrain:drivetrain_queue",
         "//frc971/control_loops/drivetrain:localizer_queue",
         "//y2019/control_loops/drivetrain:drivetrain_base",
+        "//y2019/control_loops/drivetrain:target_selector_queue",
         "//y2019/control_loops/superstructure:superstructure_queue",
         "@com_google_protobuf//:protobuf",
     ],
diff --git a/y2019/constants.cc b/y2019/constants.cc
index ba737dd..fc947fe 100644
--- a/y2019/constants.cc
+++ b/y2019/constants.cc
@@ -315,42 +315,42 @@
   constexpr double kPortZ = 1.00;
 
   constexpr double kDiscRadius = InchToMeters(19.0 / 2.0);
-  // radius to use for placing the ball (not necessarily the radius of the ball
-  // itself...).
-  constexpr double kBallRadius = 0.05;
 
   constexpr Target::GoalType kBothGoal = Target::GoalType::kBoth;
   constexpr Target::GoalType kBallGoal = Target::GoalType::kBalls;
   constexpr Target::GoalType kDiscGoal = Target::GoalType::kHatches;
   constexpr Target::GoalType kNoneGoal = Target::GoalType::kNone;
+  using TargetType = Target::TargetType;
 
   const Target far_side_cargo_bay(
       {{kFarSideCargoBayX, kSideCargoBayY, kNormalZ}, kSideCargoBayTheta},
-      kDiscRadius, kBothGoal);
+      kDiscRadius, TargetType::kFarSideCargoBay, kBothGoal);
   const Target mid_side_cargo_bay(
       {{kMidSideCargoBayX, kSideCargoBayY, kNormalZ}, kSideCargoBayTheta},
-      kDiscRadius, kBothGoal);
+      kDiscRadius, TargetType::kMidSideCargoBay, kBothGoal);
   const Target near_side_cargo_bay(
       {{kNearSideCargoBayX, kSideCargoBayY, kNormalZ}, kSideCargoBayTheta},
-      kDiscRadius, kBothGoal);
+      kDiscRadius, TargetType::kNearSideCargoBay, kBothGoal);
 
   const Target face_cargo_bay(
       {{kFaceCargoBayX, kFaceCargoBayY, kNormalZ}, kFaceCargoBayTheta},
-      kDiscRadius, kBothGoal);
+      kDiscRadius, TargetType::kFaceCargoBay, kBothGoal);
 
+  // The rocket port, since it is only for balls, has no meaningful radius
+  // to work with (and is over-ridden with zero in target_selector).
   const Target rocket_port(
-      {{kRocketPortX, kRocketPortY, kPortZ}, kRocketPortTheta}, kBallRadius,
-      kBallGoal);
+      {{kRocketPortX, kRocketPortY, kPortZ}, kRocketPortTheta}, 0.0,
+      TargetType::kRocketPortal, kBallGoal);
 
   const Target rocket_near(
       {{kRocketNearX, kRocketHatchY, kNormalZ}, kRocketNearTheta}, kDiscRadius,
-      kDiscGoal);
+      TargetType::kNearRocket, kDiscGoal);
   const Target rocket_far(
       {{kRocketFarX, kRocketHatchY, kNormalZ}, kRocketFarTheta}, kDiscRadius,
-      kDiscGoal);
+      TargetType::kFarRocket, kDiscGoal);
 
   const Target hp_slot({{0.0, kHpSlotY, kNormalZ}, kHpSlotTheta}, 0.00,
-                       kBothGoal);
+                       TargetType::kHPSlot, kBothGoal);
 
   const ::std::array<Target, 8> quarter_field_targets{
       {far_side_cargo_bay, mid_side_cargo_bay, near_side_cargo_bay,
diff --git a/y2019/control_loops/drivetrain/BUILD b/y2019/control_loops/drivetrain/BUILD
index 5a4dd40..49cda13 100644
--- a/y2019/control_loops/drivetrain/BUILD
+++ b/y2019/control_loops/drivetrain/BUILD
@@ -84,6 +84,14 @@
 )
 
 queue_library(
+    name = "target_selector_queue",
+    srcs = [
+        "target_selector.q",
+    ],
+    visibility = ["//visibility:public"],
+)
+
+queue_library(
     name = "camera_queue",
     srcs = [
         "camera.q",
@@ -126,9 +134,11 @@
     hdrs = ["target_selector.h"],
     deps = [
         ":camera",
+        ":target_selector_queue",
         "//frc971/control_loops:pose",
         "//frc971/control_loops/drivetrain:localizer",
         "//y2019:constants",
+        "//y2019/control_loops/superstructure:superstructure_queue",
     ],
 )
 
@@ -138,6 +148,7 @@
     deps = [
         ":target_selector",
         "//aos/testing:googletest",
+        "//aos/testing:test_shm",
     ],
 )
 
diff --git a/y2019/control_loops/drivetrain/camera.h b/y2019/control_loops/drivetrain/camera.h
index 73079c9..e21bf87 100644
--- a/y2019/control_loops/drivetrain/camera.h
+++ b/y2019/control_loops/drivetrain/camera.h
@@ -48,9 +48,24 @@
     // Spots for both (cargo ship, human loading).
     kBoth,
   };
+  // Which target this is within a given field quadrant:
+  enum class TargetType {
+    kHPSlot,
+    kFaceCargoBay,
+    kNearSideCargoBay,
+    kMidSideCargoBay,
+    kFarSideCargoBay,
+    kNearRocket,
+    kFarRocket,
+    kRocketPortal,
+  };
   TypedTarget(const Pose &pose, double radius = 0,
+              TargetType target_type = TargetType::kHPSlot,
               GoalType goal_type = GoalType::kBoth)
-      : pose_(pose), radius_(radius), goal_type_(goal_type) {}
+      : pose_(pose),
+        radius_(radius),
+        target_type_(target_type),
+        goal_type_(goal_type) {}
   TypedTarget() {}
   Pose pose() const { return pose_; }
   Pose *mutable_pose() { return &pose_; }
@@ -60,6 +75,8 @@
   double radius() const { return radius_; }
   GoalType goal_type() const { return goal_type_; }
   void set_goal_type(GoalType goal_type) { goal_type_ = goal_type; }
+  TargetType target_type() const { return target_type_; }
+  void set_target_type(TargetType target_type) { target_type_ = target_type; }
 
   // Get a list of points for plotting. These points should be plotted on
   // an x/y plane in the global frame with lines connecting the points.
@@ -89,6 +106,7 @@
   // TODO(james): We may actually want a non-zero (possibly negative?) number
   // here for balls.
   double radius_ = 0.0;
+  TargetType target_type_ = TargetType::kHPSlot;
   GoalType goal_type_ = GoalType::kBoth;
 };  // class TypedTarget
 
diff --git a/y2019/control_loops/drivetrain/camera_test.cc b/y2019/control_loops/drivetrain/camera_test.cc
index 347e84c..f9b6e8d 100644
--- a/y2019/control_loops/drivetrain/camera_test.cc
+++ b/y2019/control_loops/drivetrain/camera_test.cc
@@ -8,7 +8,8 @@
 
 // Check that a Target's basic operations work.
 TEST(TargetTest, BasicTargetTest) {
-  Target target({{1, 2, 3}, M_PI / 2.0}, 1.234, Target::GoalType::kHatches);
+  Target target({{1, 2, 3}, M_PI / 2.0}, 1.234,
+                Target::TargetType::kFaceCargoBay, Target::GoalType::kHatches);
 
   EXPECT_EQ(1.0, target.pose().abs_pos().x());
   EXPECT_EQ(2.0, target.pose().abs_pos().y());
@@ -16,6 +17,7 @@
   EXPECT_EQ(M_PI / 2.0, target.pose().abs_theta());
   EXPECT_EQ(1.234, target.radius());
   EXPECT_EQ(Target::GoalType::kHatches, target.goal_type());
+  EXPECT_EQ(Target::TargetType::kFaceCargoBay, target.target_type());
 
   EXPECT_FALSE(target.occluded());
   target.set_occluded(true);
diff --git a/y2019/control_loops/drivetrain/event_loop_localizer.cc b/y2019/control_loops/drivetrain/event_loop_localizer.cc
index 9403b2e..f33f4e5 100644
--- a/y2019/control_loops/drivetrain/event_loop_localizer.cc
+++ b/y2019/control_loops/drivetrain/event_loop_localizer.cc
@@ -25,12 +25,13 @@
 }
 
 EventLoopLocalizer::EventLoopLocalizer(
-    const ::frc971::control_loops::drivetrain::DrivetrainConfig<double>
-        &dt_config,
+    const ::frc971::control_loops::drivetrain::DrivetrainConfig<double> &
+        dt_config,
     ::aos::EventLoop *event_loop)
     : event_loop_(event_loop),
       cameras_(MakeCameras(&robot_pose_)),
-      localizer_(dt_config, &robot_pose_) {
+      localizer_(dt_config, &robot_pose_),
+      target_selector_(event_loop) {
   localizer_.ResetInitialState(::aos::monotonic_clock::now(),
                                Localizer::State::Zero(), localizer_.P());
   ResetPosition(::aos::monotonic_clock::now(), 0.5, 3.4, 0.0);
diff --git a/y2019/control_loops/drivetrain/target_selector.cc b/y2019/control_loops/drivetrain/target_selector.cc
index b7b7448..7b6918a 100644
--- a/y2019/control_loops/drivetrain/target_selector.cc
+++ b/y2019/control_loops/drivetrain/target_selector.cc
@@ -5,17 +5,35 @@
 
 constexpr double TargetSelector::kFakeFov;
 
-TargetSelector::TargetSelector()
+TargetSelector::TargetSelector(::aos::EventLoop *event_loop)
     : front_viewer_({&robot_pose_, {0.0, 0.0, 0.0}, 0.0}, kFakeFov, fake_noise_,
                     constants::Field().targets(), {}),
       back_viewer_({&robot_pose_, {0.0, 0.0, 0.0}, M_PI}, kFakeFov, fake_noise_,
-                   constants::Field().targets(), {}) {}
+                   constants::Field().targets(), {}),
+      hint_fetcher_(event_loop->MakeFetcher<drivetrain::TargetSelectorHint>(
+          ".y2019.control_loops.drivetrain.target_selector_hint")),
+      superstructure_goal_fetcher_(event_loop->MakeFetcher<
+          superstructure::SuperstructureQueue::Goal>(
+          ".y2019.control_loops.superstructure.superstructure_queue.goal")) {}
 
 bool TargetSelector::UpdateSelection(const ::Eigen::Matrix<double, 5, 1> &state,
                                      double command_speed) {
   if (::std::abs(command_speed) < kMinDecisionSpeed) {
     return false;
   }
+  if (superstructure_goal_fetcher_.Fetch()) {
+    ball_mode_ = superstructure_goal_fetcher_->suction.gamepiece_mode == 0;
+  }
+  if (hint_fetcher_.Fetch()) {
+    LOG_STRUCT(DEBUG, "selector_hint", *hint_fetcher_);
+    // suggested_target is unsigned so we don't check for >= 0.
+    if (hint_fetcher_->suggested_target < 4) {
+      target_hint_ =
+          static_cast<SelectionHint>(hint_fetcher_->suggested_target);
+    } else {
+      LOG(ERROR, "Got invalid suggested target.\n");
+    }
+  }
   *robot_pose_.mutable_pos() << state.x(), state.y(), 0.0;
   robot_pose_.set_theta(state(2, 0));
   ::aos::SizedArray<FakeCamera::TargetView,
@@ -40,12 +58,37 @@
     // of the field).
     // TODO(james): Support ball vs. hatch mode filtering.
     if (view.target->goal_type() == Target::GoalType::kNone ||
-        view.target->goal_type() == Target::GoalType::kBalls) {
+        view.target->goal_type() == (ball_mode_ ? Target::GoalType::kHatches
+                                                : Target::GoalType::kBalls)) {
       continue;
     }
+    switch (target_hint_) {
+      case SelectionHint::kNearShip:
+        if (view.target->target_type() !=
+            Target::TargetType::kNearSideCargoBay) {
+          continue;
+        }
+        break;
+      case SelectionHint::kMidShip:
+        if (view.target->target_type() !=
+            Target::TargetType::kMidSideCargoBay) {
+          continue;
+        }
+        break;
+      case SelectionHint::kFarShip:
+        if (view.target->target_type() !=
+            Target::TargetType::kFarSideCargoBay) {
+          continue;
+        }
+        break;
+      case SelectionHint::kNone:
+      default:
+        break;
+    }
     if (view.noise.distance < largest_target_noise) {
       target_pose_ = view.target->pose();
-      target_radius_ = view.target->radius();
+      // If we are in ball mode, use a radius of zero.
+      target_radius_ = ball_mode_ ? 0.0 : view.target->radius();
       largest_target_noise = view.noise.distance;
     }
   }
diff --git a/y2019/control_loops/drivetrain/target_selector.h b/y2019/control_loops/drivetrain/target_selector.h
index 7d09306..4c001ad 100644
--- a/y2019/control_loops/drivetrain/target_selector.h
+++ b/y2019/control_loops/drivetrain/target_selector.h
@@ -5,6 +5,8 @@
 #include "frc971/control_loops/drivetrain/localizer.h"
 #include "y2019/constants.h"
 #include "y2019/control_loops/drivetrain/camera.h"
+#include "y2019/control_loops/drivetrain/target_selector.q.h"
+#include "y2019/control_loops/superstructure/superstructure.q.h"
 
 namespace y2019 {
 namespace control_loops {
@@ -27,7 +29,16 @@
   typedef TypedCamera<y2019::constants::Field::kNumTargets,
                       /*num_obstacles=*/0, double> FakeCamera;
 
-  TargetSelector();
+  enum class SelectionHint {
+    // No hint
+    kNone = 0,
+    // Cargo ship bays
+    kNearShip = 1,
+    kMidShip = 2,
+    kFarShip = 3,
+  };
+
+  TargetSelector(::aos::EventLoop *event_loop);
 
   bool UpdateSelection(const ::Eigen::Matrix<double, 5, 1> &state,
                        double command_speed) override;
@@ -54,6 +65,14 @@
                                              .nominal_height_noise = 0};
   FakeCamera front_viewer_;
   FakeCamera back_viewer_;
+
+  ::aos::Fetcher<drivetrain::TargetSelectorHint> hint_fetcher_;
+  ::aos::Fetcher<superstructure::SuperstructureQueue::Goal>
+      superstructure_goal_fetcher_;
+
+  // Whether we are currently in ball mode.
+  bool ball_mode_ = false;
+  SelectionHint target_hint_ = SelectionHint::kNone;
 };
 
 }  // namespace control_loops
diff --git a/y2019/control_loops/drivetrain/target_selector.q b/y2019/control_loops/drivetrain/target_selector.q
new file mode 100644
index 0000000..c917d39
--- /dev/null
+++ b/y2019/control_loops/drivetrain/target_selector.q
@@ -0,0 +1,12 @@
+package y2019.control_loops.drivetrain;
+
+// A message to provide information to the target selector about what it should
+message TargetSelectorHint {
+  // Which target we should go for:
+  // 0 implies no selection, we should just default to whatever.
+  // 1, 2, and 3 imply the near, middle, and far targets.
+  // These should match the SelectionHint enum in target_selector.h.
+  uint8_t suggested_target;
+};
+
+queue TargetSelectorHint target_selector_hint;
diff --git a/y2019/control_loops/drivetrain/target_selector_test.cc b/y2019/control_loops/drivetrain/target_selector_test.cc
index 20bf19a..aa655e6 100644
--- a/y2019/control_loops/drivetrain/target_selector_test.cc
+++ b/y2019/control_loops/drivetrain/target_selector_test.cc
@@ -1,6 +1,8 @@
 #include "y2019/control_loops/drivetrain/target_selector.h"
 
+#include "aos/testing/test_shm.h"
 #include "gtest/gtest.h"
+#include "y2019/control_loops/superstructure/superstructure.q.h"
 
 namespace y2019 {
 namespace control_loops {
@@ -8,12 +10,15 @@
 
 typedef ::frc971::control_loops::TypedPose<double> Pose;
 typedef ::Eigen::Matrix<double, 5, 1> State;
+using SelectionHint = TargetSelector::SelectionHint;
 
 namespace {
 // Accessors to get some useful particular targets on the field:
 Pose HPSlotLeft() { return constants::Field().targets()[7].pose(); }
 Pose CargoNearLeft() { return constants::Field().targets()[2].pose(); }
 Pose RocketHatchFarLeft() { return constants::Field().targets()[6].pose(); }
+Pose RocketPortal() { return constants::Field().targets()[4].pose(); }
+double HatchRadius() { return constants::Field().targets()[6].radius(); }
 }  // namespace
 
 // Tests the target selector with:
@@ -23,14 +28,40 @@
 // -If (1) is true, the pose we expect to get back.
 struct TestParams {
   State state;
+  bool ball_mode;
+  SelectionHint selection_hint;
   double command_speed;
   bool expect_target;
   Pose expected_pose;
+  double expected_radius;
 };
-class TargetSelectorParamTest : public ::testing::TestWithParam<TestParams> {};
+class TargetSelectorParamTest : public ::testing::TestWithParam<TestParams> {
+ protected:
+  virtual void TearDown() override {
+    ::y2019::control_loops::superstructure::superstructure_queue.goal.Clear();
+    ::y2019::control_loops::drivetrain::target_selector_hint.Clear();
+  }
+  ::aos::ShmEventLoop event_loop_;
+
+ private:
+  ::aos::testing::TestSharedMemory my_shm_;
+};
 
 TEST_P(TargetSelectorParamTest, ExpectReturn) {
-  TargetSelector selector;
+  TargetSelector selector(&event_loop_);
+  {
+    auto super_goal =
+        ::y2019::control_loops::superstructure::superstructure_queue.goal
+            .MakeMessage();
+    super_goal->suction.gamepiece_mode = GetParam().ball_mode ? 0 : 1;
+    ASSERT_TRUE(super_goal.Send());
+  }
+  {
+    auto hint =
+        ::y2019::control_loops::drivetrain::target_selector_hint.MakeMessage();
+    hint->suggested_target = static_cast<int>(GetParam().selection_hint);
+    ASSERT_TRUE(hint.Send());
+  }
   bool expect_target = GetParam().expect_target;
   const State state = GetParam().state;
   ASSERT_EQ(expect_target,
@@ -49,6 +80,8 @@
         << " but got " << actual_pos.transpose() << " with the robot at "
         << state.transpose();
     EXPECT_EQ(expected_angle, actual_angle);
+    EXPECT_EQ(GetParam().expected_radius, selector.TargetRadius());
+    EXPECT_EQ(expected_angle, actual_angle);
   }
 }
 
@@ -57,42 +90,82 @@
     ::testing::Values(
         // When we are far away from anything, we should not register any
         // targets:
-        TestParams{
-            (State() << 0.0, 0.0, 0.0, 1.0, 1.0).finished(), 1.0, false, {}},
+        TestParams{(State() << 0.0, 0.0, 0.0, 1.0, 1.0).finished(),
+                   /*ball_mode=*/false,
+                   SelectionHint::kNone,
+                   1.0,
+                   false,
+                   {},
+                   /*expected_radius=*/0.0},
         // Aim for a human-player spot; at low speeds we should not register
         // anything.
         TestParams{(State() << 4.0, 2.0, M_PI, 0.05, 0.05).finished(),
+                   /*ball_mode=*/false,
+                   SelectionHint::kNone,
                    0.05,
                    false,
-                   {}},
+                   {},
+                   /*expected_radius=*/0.0},
         TestParams{(State() << 4.0, 2.0, M_PI, -0.05, -0.05).finished(),
+                   /*ball_mode=*/false,
+                   SelectionHint::kNone,
                    -0.05,
                    false,
-                   {}},
-        TestParams{(State() << 4.0, 2.0, M_PI, 0.5, 0.5).finished(), 1.0, true,
-                   HPSlotLeft()},
+                   {},
+                   /*expected_radius=*/0.0},
+        TestParams{(State() << 4.0, 2.0, M_PI, 0.5, 0.5).finished(),
+                   /*ball_mode=*/false, SelectionHint::kNone, 1.0, true,
+                   HPSlotLeft(), /*expected_radius=*/0.0},
         // Put ourselves between the rocket and cargo ship; we should see the
         // hatches driving one direction and the near cargo ship port the other.
         // We also command a speed opposite the current direction of motion and
         // confirm that that behaves as expected.
-        TestParams{(State() << 6.0, 2.0, -M_PI_2, -0.5, -0.5).finished(), 1.0,
-                   true, CargoNearLeft()},
-        TestParams{(State() << 6.0, 2.0, M_PI_2, 0.5, 0.5).finished(), -1.0,
-                   true, CargoNearLeft()},
-        TestParams{(State() << 6.0, 2.0, -M_PI_2, 0.5, 0.5).finished(), -1.0,
-                   true, RocketHatchFarLeft()},
-        TestParams{(State() << 6.0, 2.0, M_PI_2, -0.5, -0.5).finished(), 1.0,
-                   true, RocketHatchFarLeft()},
+        TestParams{(State() << 6.0, 2.0, -M_PI_2, -0.5, -0.5).finished(),
+                   /*ball_mode=*/false, SelectionHint::kNone, 1.0, true,
+                   CargoNearLeft(), /*expected_radius=*/HatchRadius()},
+        TestParams{(State() << 6.0, 2.0, M_PI_2, 0.5, 0.5).finished(),
+                   /*ball_mode=*/false, SelectionHint::kNone, -1.0, true,
+                   CargoNearLeft(), /*expected_radius=*/HatchRadius()},
+        TestParams{(State() << 6.0, 2.0, -M_PI_2, 0.5, 0.5).finished(),
+                   /*ball_mode=*/false, SelectionHint::kNone, -1.0, true,
+                   RocketHatchFarLeft(), /*expected_radius=*/HatchRadius()},
+        TestParams{(State() << 6.0, 2.0, M_PI_2, -0.5, -0.5).finished(),
+                   /*ball_mode=*/false, SelectionHint::kNone, 1.0, true,
+                   RocketHatchFarLeft(), /*expected_radius=*/HatchRadius()},
         // And we shouldn't see anything spinning in place:
         TestParams{(State() << 6.0, 2.0, M_PI_2, -0.5, 0.5).finished(),
+                   /*ball_mode=*/false,
+                   SelectionHint::kNone,
                    0.0,
                    false,
-                   {}},
+                   {},
+                   /*expected_radius=*/0.0},
         // Drive backwards off the field--we should not see anything.
         TestParams{(State() << -0.1, 0.0, 0.0, -0.5, -0.5).finished(),
+                   /*ball_mode=*/false,
+                   SelectionHint::kNone,
                    -1.0,
                    false,
-                   {}}));
+                   {},
+                   /*expected_radius=*/0.0},
+        // In ball mode, we should be able to see the portal, and get zero
+        // radius.
+        TestParams{(State() << 6.0, 2.0, M_PI_2, 0.5, 0.5).finished(),
+                   /*ball_mode=*/true,
+                   SelectionHint::kNone,
+                   1.0,
+                   true,
+                   RocketPortal(),
+                   /*expected_radius=*/0.0},
+        // Reversing direction should get cargo ship with zero radius.
+        TestParams{(State() << 6.0, 2.0, M_PI_2, 0.5, 0.5).finished(),
+                   /*ball_mode=*/true,
+                   SelectionHint::kNone,
+                   -1.0,
+                   true,
+                   CargoNearLeft(),
+                   /*expected_radius=*/0.0}
+                   ));
 
 }  // namespace testing
 }  // namespace control_loops
diff --git a/y2019/joystick_reader.cc b/y2019/joystick_reader.cc
index 54d2694..69b40f5 100644
--- a/y2019/joystick_reader.cc
+++ b/y2019/joystick_reader.cc
@@ -22,11 +22,13 @@
 #include "frc971/control_loops/drivetrain/localizer.q.h"
 
 #include "y2019/control_loops/drivetrain/drivetrain_base.h"
+#include "y2019/control_loops/drivetrain/target_selector.q.h"
 #include "y2019/control_loops/superstructure/superstructure.q.h"
 #include "y2019/status_light.q.h"
 #include "y2019/vision.pb.h"
 
 using ::y2019::control_loops::superstructure::superstructure_queue;
+using ::y2019::control_loops::drivetrain::target_selector_hint;
 using ::frc971::control_loops::drivetrain::localizer_control;
 using ::aos::input::driver_station::ButtonLocation;
 using ::aos::input::driver_station::ControlBit;
@@ -82,6 +84,10 @@
 const ButtonLocation kResetLocalizerRightForwards(4, 1);
 const ButtonLocation kResetLocalizerRightBackwards(4, 11);
 
+const ButtonLocation kNearCargoHint(3, 15);
+const ButtonLocation kMidCargoHint(3, 16);
+const ButtonLocation kFarCargoHint(4, 2);
+
 const ElevatorWristPosition kStowPos{0.36, 0.0};
 
 const ElevatorWristPosition kPanelHPIntakeForwrdPos{0.01, M_PI / 2.0};
@@ -158,6 +164,22 @@
 
     auto new_superstructure_goal = superstructure_queue.goal.MakeMessage();
 
+    {
+      auto target_hint = target_selector_hint.MakeMessage();
+      if (data.IsPressed(kNearCargoHint)) {
+        target_hint->suggested_target = 1;
+      } else if (data.IsPressed(kMidCargoHint)) {
+        target_hint->suggested_target = 2;
+      } else if (data.IsPressed(kFarCargoHint)) {
+        target_hint->suggested_target = 3;
+      } else {
+        target_hint->suggested_target = 0;
+      }
+      if (!target_hint.Send()) {
+        LOG(ERROR, "Failed to send target selector hint.\n");
+      }
+    }
+
     if (data.PosEdge(kResetLocalizerLeftForwards)) {
       auto localizer_resetter = localizer_control.MakeMessage();
       // Start at the left feeder station.