Index loop is in a seperate file now.
diff --git a/frc971/control_loops/index/index.cc b/frc971/control_loops/index/index.cc
new file mode 100644
index 0000000..fa1940e
--- /dev/null
+++ b/frc971/control_loops/index/index.cc
@@ -0,0 +1,498 @@
+#include "frc971/control_loops/index/index.h"
+
+#include <stdio.h>
+
+#include <algorithm>
+
+#include "aos/aos_core.h"
+
+#include "aos/common/messages/RobotState.q.h"
+#include "aos/common/control_loop/control_loops.q.h"
+#include "aos/common/logging/logging.h"
+
+#include "frc971/constants.h"
+#include "frc971/control_loops/index/index_motor_plant.h"
+
+using ::aos::time::Time;
+
+namespace frc971 {
+namespace control_loops {
+
+IndexMotor::IndexMotor(control_loops::IndexLoop *my_index)
+    : aos::control_loops::ControlLoop<control_loops::IndexLoop>(my_index),
+      wrist_loop_(new StateFeedbackLoop<2, 1, 1>(MakeIndexLoop())),
+      hopper_disc_count_(0),
+      total_disc_count_(0),
+      safe_goal_(Goal::HOLD),
+      loader_goal_(LoaderGoal::READY),
+      loader_state_(LoaderState::READY),
+      loader_up_(false),
+      disc_clamped_(false),
+      disc_ejected_(false),
+      last_bottom_disc_detect_(false) {
+}
+
+/*static*/ const double IndexMotor::kTransferStartPosition = 0.0;
+/*static*/ const double IndexMotor::kIndexStartPosition = 0.2159;
+/*static*/ const double IndexMotor::kIndexFreeLength =
+      IndexMotor::ConvertDiscAngleToDiscPosition((360 * 2 + 14) * M_PI / 180);
+/*static*/ const double IndexMotor::kLoaderFreeStopPosition =
+      kIndexStartPosition + kIndexFreeLength;
+/*static*/ const double IndexMotor::kReadyToLiftPosition =
+    kLoaderFreeStopPosition + 0.2921;
+/*static*/ const double IndexMotor::kGrabberLength = 0.03175;
+/*static*/ const double IndexMotor::kGrabberStartPosition =
+    kReadyToLiftPosition - kGrabberLength;
+/*static*/ const double IndexMotor::kGrabberMovementVelocity = 0.5;
+/*static*/ const double IndexMotor::kLifterStopPosition =
+    kReadyToLiftPosition + 0.161925;
+/*static*/ const double IndexMotor::kLifterMovementVelocity = 1.0;
+/*static*/ const double IndexMotor::kEjectorStopPosition =
+    kLifterStopPosition + 0.01;
+/*static*/ const double IndexMotor::kEjectorMovementVelocity = 1.0;
+/*static*/ const double IndexMotor::kBottomDiscDetectStart = -0.08;
+/*static*/ const double IndexMotor::kBottomDiscDetectStop = 0.200025;
+
+// TODO(aschuh): Figure these out.
+/*static*/ const double IndexMotor::kTopDiscDetectStart = 18.0;
+/*static*/ const double IndexMotor::kTopDiscDetectStop = 19.0;
+
+const /*static*/ double IndexMotor::kDiscRadius = 10.875 * 0.0254 / 2;
+const /*static*/ double IndexMotor::kRollerRadius = 2.0 * 0.0254 / 2;
+const /*static*/ double IndexMotor::kTransferRollerRadius = 1.25 * 0.0254 / 2;
+
+/*static*/ const int IndexMotor::kGrabbingDelay = 5;
+/*static*/ const int IndexMotor::kLiftingDelay = 20;
+/*static*/ const int IndexMotor::kShootingDelay = 5;
+/*static*/ const int IndexMotor::kLoweringDelay = 20;
+
+// Distance to move the indexer when grabbing a disc.
+const double kNextPosition = 10.0;
+
+/*static*/ double IndexMotor::ConvertDiscAngleToIndex(const double angle) {
+  return (angle * (1 + (kDiscRadius * 2 + kRollerRadius) / kRollerRadius));
+}
+
+/*static*/ double IndexMotor::ConvertDiscAngleToDiscPosition(
+    const double angle) {
+  return angle * (kDiscRadius + kRollerRadius);
+}
+
+/*static*/ double IndexMotor::ConvertDiscPositionToDiscAngle(
+    const double position) {
+  return position / (kDiscRadius + kRollerRadius);
+}
+
+/*static*/ double IndexMotor::ConvertIndexToDiscAngle(const double angle) {
+  return (angle / (1 + (kDiscRadius * 2 + kRollerRadius) / kRollerRadius));
+}
+
+/*static*/ double IndexMotor::ConvertIndexToDiscPosition(const double angle) {
+  return IndexMotor::ConvertDiscAngleToDiscPosition(
+      ConvertIndexToDiscAngle(angle));
+}
+
+/*static*/ double IndexMotor::ConvertTransferToDiscPosition(
+    const double angle) {
+  const double gear_ratio =  (1 + (kDiscRadius * 2 + kTransferRollerRadius) /
+                              kTransferRollerRadius);
+  return angle / gear_ratio * (kDiscRadius + kTransferRollerRadius);
+}
+
+/*static*/ double IndexMotor::ConvertDiscPositionToIndex(
+    const double position) {
+  return IndexMotor::ConvertDiscAngleToIndex(
+      ConvertDiscPositionToDiscAngle(position));
+}
+
+bool IndexMotor::MinDiscPosition(double *disc_position) {
+  bool found_start = false;
+  for (unsigned int i = 0; i < frisbees_.size(); ++i) {
+    const Frisbee &frisbee = frisbees_[i];
+    if (!found_start) {
+      if (frisbee.has_position()) {
+        *disc_position = frisbee.position();
+        found_start = true;
+      }
+    } else {
+      *disc_position = ::std::min(frisbee.position(),
+                                  *disc_position);
+    }
+  }
+  return found_start;
+}
+
+bool IndexMotor::MaxDiscPosition(double *disc_position) {
+  bool found_start = false;
+  for (unsigned int i = 0; i < frisbees_.size(); ++i) {
+    const Frisbee &frisbee = frisbees_[i];
+    if (!found_start) {
+      if (frisbee.has_position()) {
+        *disc_position = frisbee.position();
+        found_start = true;
+      }
+    } else {
+      *disc_position = ::std::max(frisbee.position(),
+                                  *disc_position);
+    }
+  }
+  return found_start;
+}
+
+// Positive angle is towards the shooter, and positive power is towards the
+// shooter.
+void IndexMotor::RunIteration(
+    const control_loops::IndexLoop::Goal *goal,
+    const control_loops::IndexLoop::Position *position,
+    control_loops::IndexLoop::Output *output,
+    control_loops::IndexLoop::Status *status) {
+  // Make goal easy to work with.
+  Goal goal_enum = static_cast<Goal>(goal->goal_state);
+
+  // Disable the motors now so that all early returns will return with the
+  // motors disabled.
+  double transfer_voltage = 0.0;
+  if (output) {
+    output->transfer_voltage = 0.0;
+    output->index_voltage = 0.0;
+  }
+
+  status->ready_to_intake = false;
+
+  // Compute a safe index position that we can use.
+  if (position) {
+    wrist_loop_->Y << position->index_position;
+  }
+  const double index_position = wrist_loop_->X_hat(0, 0);
+
+  // TODO(aschuh): Watch for top disc detect and update the frisbee
+  // position.
+
+  // TODO(aschuh): Horizontal and centering should be here as well...
+
+  // Bool to track if it is safe for the goal to change yet.
+  bool safe_to_change_state_ = true;
+  switch (safe_goal_) {
+    case Goal::HOLD:
+      // The goal should already be good, so sit tight with everything the same
+      // as it was.
+      break;
+    case Goal::READY_LOWER:
+    case Goal::INTAKE:
+      {
+        Time now = Time::Now();
+        // Posedge of the disc entering the beam break.
+        if (position) {
+          // TODO(aschuh): Catch the edges on the FPGA since this is too slow.
+          // This means that we need to pass back enough data so that we can
+          // miss packets and everything works.
+          if (position->bottom_disc_detect && !last_bottom_disc_detect_) {
+            transfer_frisbee_.Reset();
+            transfer_frisbee_.bottom_posedge_time_ = now;
+            printf("Posedge of bottom disc %f\n",
+                   transfer_frisbee_.bottom_posedge_time_.ToSeconds());
+            ++hopper_disc_count_;
+            ++total_disc_count_;
+          }
+
+          // Disc exited the beam break now.
+          if (!position->bottom_disc_detect && last_bottom_disc_detect_) {
+            transfer_frisbee_.bottom_negedge_time_ = now;
+            printf("Negedge of bottom disc %f\n",
+                   transfer_frisbee_.bottom_negedge_time_.ToSeconds());
+            frisbees_.push_front(transfer_frisbee_);
+          }
+
+          if (position->bottom_disc_detect) {
+            transfer_voltage = 12.0;
+            // Must wait until the disc gets out before we can change state.
+            safe_to_change_state_ = false;
+
+            // TODO(aschuh): A disc on the way through needs to start moving
+            // the indexer if it isn't already moving.  Maybe?
+
+            Time elapsed_posedge_time = now -
+                transfer_frisbee_.bottom_posedge_time_;
+            if (elapsed_posedge_time >= Time::InSeconds(0.3)) {
+              // It has been too long.  The disc must be jammed.
+              LOG(ERROR, "Been way too long.  Jammed disc?\n");
+              printf("Been way too long.  Jammed disc?\n");
+            }
+          }
+
+          // Check all non-indexed discs and see if they should be indexed.
+          for (Frisbee &frisbee : frisbees_) {
+            if (!frisbee.has_been_indexed_) {
+              transfer_voltage = 12.0;
+              Time elapsed_negedge_time = now -
+                  frisbee.bottom_negedge_time_;
+              if (elapsed_negedge_time >= Time::InSeconds(0.005)) {
+                // Should have just engaged.
+                // Save the indexer position, and the time.
+
+                // It has been long enough since the disc entered the indexer.
+                // Treat now as the time at which it contacted the indexer.
+                LOG(INFO, "Grabbed on the index now at %f\n", index_position);
+                printf("Grabbed on the index now at %f\n", index_position);
+                frisbee.has_been_indexed_ = true;
+                frisbee.index_start_position_ = index_position;
+              }
+            }
+            if (!frisbee.has_been_indexed_) {
+              // All discs must be indexed before it is safe to stop indexing.
+              safe_to_change_state_ = false;
+            }
+          }
+
+          // Figure out where the indexer should be to move the discs down to
+          // the right position.
+          double max_disc_position;
+          if (MaxDiscPosition(&max_disc_position)) {
+            printf("There is a disc down here!\n");
+            // TODO(aschuh): Figure out what to do if grabbing the next one
+            // would cause things to jam into the loader.
+            // Say we aren't ready any more.  Undefined behavior will result if
+            // that isn't observed.
+            double bottom_disc_position =
+                max_disc_position + ConvertDiscAngleToIndex(M_PI);
+            wrist_loop_->R << bottom_disc_position, 0.0;
+
+            // Verify that we are close enough to the goal so that we should be
+            // fine accepting the next disc.
+            double disc_error_meters = ConvertIndexToDiscPosition(
+                wrist_loop_->X_hat(0, 0) - bottom_disc_position);
+            // We are ready for the next disc if the first one is in the first
+            // half circle of the indexer.  It will take time for the disc to
+            // come into the indexer, so we will be able to move it out of the
+            // way in time.
+            // This choice also makes sure that we don't claim that we aren't
+            // ready between full speed intaking.
+            if (-ConvertDiscAngleToIndex(M_PI) < disc_error_meters &&
+                disc_error_meters < 0.04) {
+              // We are only ready if we aren't being asked to change state or
+              // are full.
+              status->ready_to_intake =
+                  (safe_goal_ == goal_enum) && hopper_disc_count_ < 4;
+            } else {
+              status->ready_to_intake = false;
+            }
+          } else {
+            // No discs!  We are always ready for more if we aren't being
+            // asked to change state.
+            status->ready_to_intake = (safe_goal_ == goal_enum);
+          }
+
+          // Turn on the transfer roller if we are ready.
+          if (status->ready_to_intake && hopper_disc_count_ < 4 &&
+              safe_goal_ == Goal::INTAKE) {
+            transfer_voltage = 12.0;
+          }
+        }
+        printf("INTAKE\n");
+      }
+      break;
+    case Goal::READY_SHOOTER:
+    case Goal::SHOOT:
+      // Check if we have any discs to shoot or load and handle them.
+      double min_disc_position;
+      if (MinDiscPosition(&min_disc_position)) {
+        const double ready_disc_position =
+            min_disc_position + ConvertDiscPositionToIndex(kIndexFreeLength) -
+            ConvertDiscAngleToIndex(M_PI / 6.0);
+
+        const double grabbed_disc_position =
+            min_disc_position +
+            ConvertDiscPositionToIndex(kReadyToLiftPosition -
+                                       kIndexStartPosition + 0.03);
+
+        // Check the state of the loader FSM.
+        // If it is ready to load discs, position the disc so that it is ready
+        // to be grabbed.
+        // If it isn't ready, there is a disc in there.  It needs to finish it's
+        // cycle first.
+        if (loader_state_ != LoaderState::READY) {
+          // We already have a disc in the loader.
+          // Stage the discs back a bit.
+          wrist_loop_->R << ready_disc_position, 0.0;
+
+          // Must wait until it has been grabbed to continue.
+          if (loader_state_ == LoaderState::GRABBING) {
+            safe_to_change_state_ = false;
+          }
+        } else {
+          // No disc up top right now.
+          wrist_loop_->R << grabbed_disc_position, 0.0;
+
+          // See if the disc has gotten pretty far up yet.
+          if (wrist_loop_->X_hat(0, 0) > ready_disc_position) {
+            // Point of no return.  We are committing to grabbing it now.
+            safe_to_change_state_ = false;
+            const double robust_grabbed_disc_position =
+                (grabbed_disc_position -
+                 ConvertDiscPositionToIndex(kGrabberLength));
+
+            // If close, start grabbing and/or shooting.
+            if (wrist_loop_->X_hat(0, 0) > robust_grabbed_disc_position) {
+              // Start the state machine.
+              if (safe_goal_ == Goal::SHOOT) {
+                loader_goal_ = LoaderGoal::SHOOT_AND_RESET;
+              } else {
+                loader_goal_ = LoaderGoal::GRAB;
+              }
+              // This frisbee is now gone.  Take it out of the queue.
+              frisbees_.pop_back();
+              --hopper_disc_count_;
+            }
+          }
+        }
+      }
+
+      printf("READY_SHOOTER or SHOOT\n");
+      break;
+  }
+
+  // The only way out of the loader is to shoot the disc.  The FSM can only go
+  // forwards.
+  switch (loader_state_) {
+    case LoaderState::READY:
+      printf("Loader READY\n");
+      // Open and down, ready to accept a disc.
+      loader_up_ = false;
+      disc_clamped_ = false;
+      disc_ejected_ = false;
+      if (loader_goal_ == LoaderGoal::GRAB ||
+          loader_goal_ == LoaderGoal::SHOOT_AND_RESET) {
+        if (loader_goal_ == LoaderGoal::GRAB) {
+          printf("Told to GRAB, moving on\n");
+        } else {
+          printf("Told to SHOOT_AND_RESET, moving on\n");
+        }
+        loader_state_ = LoaderState::GRABBING;
+        loader_countdown_ = kGrabbingDelay;
+      } else {
+        break;
+      }
+    case LoaderState::GRABBING:
+      printf("Loader GRABBING %d\n", loader_countdown_);
+      // Closing the grabber.
+      loader_up_ = false;
+      disc_clamped_ = true;
+      disc_ejected_ = false;
+      if (loader_countdown_ > 0) {
+        --loader_countdown_;
+        break;
+      } else {
+        loader_state_ = LoaderState::GRABBED;
+      }
+    case LoaderState::GRABBED:
+      printf("Loader GRABBED\n");
+      // Grabber closed.
+      loader_up_ = false;
+      disc_clamped_ = true;
+      disc_ejected_ = false;
+      if (loader_goal_ == LoaderGoal::SHOOT_AND_RESET) {
+        // TODO(aschuh): Only shoot if the shooter is up to speed.
+        // Seems like that would have us shooting a bit later than we could be,
+        // but it also probably spins back up real fast.
+        loader_state_ = LoaderState::LIFTING;
+        loader_countdown_ = kLiftingDelay;
+        printf("Told to SHOOT_AND_RESET, moving on\n");
+      } else if (loader_goal_ == LoaderGoal::READY) {
+        LOG(ERROR, "Can't go to ready when we have something grabbed.\n");
+        printf("Can't go to ready when we have something grabbed.\n");
+        break;
+      } else {
+        break;
+      }
+    case LoaderState::LIFTING:
+      printf("Loader LIFTING %d\n", loader_countdown_);
+      // Lifting the disc.
+      loader_up_ = true;
+      disc_clamped_ = true;
+      disc_ejected_ = false;
+      if (loader_countdown_ > 0) {
+        --loader_countdown_;
+        break;
+      } else {
+        loader_state_ = LoaderState::LIFTED;
+      }
+    case LoaderState::LIFTED:
+      printf("Loader LIFTED\n");
+      // Disc lifted.  Time to eject it out.
+      loader_up_ = true;
+      disc_clamped_ = true;
+      disc_ejected_ = false;
+      loader_state_ = LoaderState::SHOOTING;
+      loader_countdown_ = kShootingDelay;
+    case LoaderState::SHOOTING:
+      printf("Loader SHOOTING %d\n", loader_countdown_);
+      // Ejecting the disc into the shooter.
+      loader_up_ = true;
+      disc_clamped_ = false;
+      disc_ejected_ = true;
+      if (loader_countdown_ > 0) {
+        --loader_countdown_;
+        break;
+      } else {
+        loader_state_ = LoaderState::SHOOT;
+      }
+    case LoaderState::SHOOT:
+      printf("Loader SHOOT\n");
+      // The disc has been shot.
+      loader_up_ = true;
+      disc_clamped_ = false;
+      disc_ejected_ = true;
+      loader_state_ = LoaderState::LOWERING;
+      loader_countdown_ = kLoweringDelay;
+    case LoaderState::LOWERING:
+      printf("Loader LOWERING %d\n", loader_countdown_);
+      // Lowering the loader back down.
+      loader_up_ = false;
+      disc_clamped_ = false;
+      disc_ejected_ = true;
+      if (loader_countdown_ > 0) {
+        --loader_countdown_;
+        break;
+      } else {
+        loader_state_ = LoaderState::LOWERED;
+      }
+    case LoaderState::LOWERED:
+      printf("Loader LOWERED\n");
+      // The indexer is lowered.
+      loader_up_ = false;
+      disc_clamped_ = false;
+      disc_ejected_ = false;
+      loader_state_ = LoaderState::READY;
+      // Once we have shot, we need to hang out in READY until otherwise
+      // notified.
+      loader_goal_ = LoaderGoal::READY;
+      break;
+  }
+
+  // Update the observer.
+  wrist_loop_->Update(position != NULL, output == NULL);
+
+  if (position) {
+    LOG(DEBUG, "pos=%f\n", position->index_position);
+    last_bottom_disc_detect_ = position->bottom_disc_detect;
+  }
+
+  status->hopper_disc_count = hopper_disc_count_;
+  status->total_disc_count = total_disc_count_;
+  status->preloaded = (loader_state_ != LoaderState::READY);
+
+  if (output) {
+    output->transfer_voltage = transfer_voltage;
+    output->index_voltage = wrist_loop_->U(0, 0);
+    output->loader_up = loader_up_;
+    output->disc_clamped = disc_clamped_;
+    output->disc_ejected = disc_ejected_;
+  }
+
+  if (safe_to_change_state_) {
+    safe_goal_ = goal_enum;
+  }
+}
+
+}  // namespace control_loops
+}  // namespace frc971
diff --git a/frc971/control_loops/index/index.gyp b/frc971/control_loops/index/index.gyp
new file mode 100644
index 0000000..04b043f
--- /dev/null
+++ b/frc971/control_loops/index/index.gyp
@@ -0,0 +1,71 @@
+{
+  'targets': [
+    {
+      'target_name': 'index_loop',
+      'type': 'static_library',
+      'sources': ['index_motor.q'],
+      'variables': {
+        'header_path': 'frc971/control_loops/index',
+      },
+      'dependencies': [
+        '<(AOS)/build/aos.gyp:libaos',
+        '<(AOS)/common/common.gyp:control_loop_queues',
+        '<(AOS)/common/common.gyp:queues',
+      ],
+      'export_dependent_settings': [
+        '<(AOS)/build/aos.gyp:libaos',
+        '<(AOS)/common/common.gyp:control_loop_queues',
+        '<(AOS)/common/common.gyp:queues',
+      ],
+      'includes': ['../../../aos/build/queues.gypi'],
+    },
+    {
+      'target_name': 'index_lib',
+      'type': 'static_library',
+      'sources': [
+        'index.cc',
+        'index_motor_plant.cc',
+      ],
+      'dependencies': [
+        '<(AOS)/build/aos.gyp:libaos',
+        'index_loop',
+        '<(AOS)/common/common.gyp:controls',
+        '<(DEPTH)/frc971/frc971.gyp:common',
+        '<(DEPTH)/frc971/control_loops/control_loops.gyp:state_feedback_loop',
+      ],
+      'export_dependent_settings': [
+        '<(DEPTH)/frc971/control_loops/control_loops.gyp:state_feedback_loop',
+        '<(AOS)/common/common.gyp:controls',
+        'index_loop',
+      ],
+    },
+    {
+      'target_name': 'index_lib_test',
+      'type': 'executable',
+      'sources': [
+        'index_lib_test.cc',
+        'transfer_motor_plant.cc',
+      ],
+      'dependencies': [
+        '<(EXTERNALS):gtest',
+        '<(AOS)/build/aos.gyp:libaos',
+        'index_loop',
+        'index_lib',
+        '<(AOS)/common/common.gyp:queue_testutils',
+        '<(DEPTH)/frc971/control_loops/control_loops.gyp:state_feedback_loop',
+      ],
+    },
+    {
+      'target_name': 'index',
+      'type': 'executable',
+      'sources': [
+        'index_main.cc',
+      ],
+      'dependencies': [
+        '<(AOS)/build/aos.gyp:libaos',
+        'index_lib',
+        'index_loop',
+      ],
+    },
+  ],
+}
diff --git a/frc971/control_loops/index/index.h b/frc971/control_loops/index/index.h
new file mode 100644
index 0000000..cca30fd
--- /dev/null
+++ b/frc971/control_loops/index/index.h
@@ -0,0 +1,232 @@
+#ifndef FRC971_CONTROL_LOOPS_WRIST_H_
+#define FRC971_CONTROL_LOOPS_WRIST_H_
+
+#include <memory>
+#include <deque>
+
+#include "aos/common/control_loop/ControlLoop.h"
+#include "aos/common/time.h"
+#include "frc971/control_loops/state_feedback_loop.h"
+#include "frc971/control_loops/index/index_motor.q.h"
+#include "frc971/control_loops/index/index_motor_plant.h"
+
+namespace frc971 {
+namespace control_loops {
+
+class IndexMotor
+    : public aos::control_loops::ControlLoop<control_loops::IndexLoop> {
+ public:
+  explicit IndexMotor(
+      control_loops::IndexLoop *my_index = &control_loops::index_loop);
+
+  static const double kTransferStartPosition;
+  static const double kIndexStartPosition;
+  // The distance from where the disc first grabs on the indexer to where it
+  // just bairly clears the loader.
+  static const double kIndexFreeLength;
+  // The distance to where the disc just starts to enter the loader.
+  static const double kLoaderFreeStopPosition;
+
+  // Distance that the grabber pulls the disc in by.
+  static const double kGrabberLength;
+  // Distance to where the grabber takes over.
+  static const double kGrabberStartPosition;
+
+  // The distance to where the disc hits the back of the loader and is ready to
+  // lift.
+  static const double kReadyToLiftPosition;
+
+  static const double kGrabberMovementVelocity;
+  // TODO(aschuh): This depends on the shooter angle...
+  // Distance to where the shooter is up and ready to shoot.
+  static const double kLifterStopPosition;
+  static const double kLifterMovementVelocity;
+
+  // Distance to where the disc has been launched.
+  // TODO(aschuh): This depends on the shooter angle...
+  static const double kEjectorStopPosition;
+  static const double kEjectorMovementVelocity;
+
+  // Start and stop position of the bottom disc detect sensor in meters.
+  static const double kBottomDiscDetectStart;
+  static const double kBottomDiscDetectStop;
+
+  static const double kTopDiscDetectStart;
+  static const double kTopDiscDetectStop;
+
+  // Converts the angle of the indexer to the angle of the disc.
+  static double ConvertIndexToDiscAngle(const double angle);
+  // Converts the angle of the indexer to the position that the center of the
+  // disc has traveled.
+  static double ConvertIndexToDiscPosition(const double angle);
+
+  // Converts the angle of the transfer roller to the position that the center
+  // of the disc has traveled.
+  static double ConvertTransferToDiscPosition(const double angle);
+
+  // Converts the distance around the indexer to the position of
+  // the index roller.
+  static double ConvertDiscPositionToIndex(const double position);
+  // Converts the angle around the indexer to the position of the index roller.
+  static double ConvertDiscAngleToIndex(const double angle);
+  // Converts the angle around the indexer to the position of the disc in the
+  // indexer.
+  static double ConvertDiscAngleToDiscPosition(const double angle);
+  // Converts the distance around the indexer to the angle of the disc around
+  // the indexer.
+  static double ConvertDiscPositionToDiscAngle(const double position);
+
+  // Disc radius in meters.
+  static const double kDiscRadius;
+  // Roller radius in meters.
+  static const double kRollerRadius;
+  // Transfer roller radius in meters.
+  static const double kTransferRollerRadius;
+
+  // Time that it takes to grab the disc in cycles.
+  static const int kGrabbingDelay;
+  // Time that it takes to lift the loader in cycles.
+  static const int kLiftingDelay;
+  // Time that it takes to shoot the disc in cycles.
+  static const int kShootingDelay;
+  // Time that it takes to lower the loader in cycles.
+  static const int kLoweringDelay;
+
+  // Object representing a Frisbee tracked by the indexer.
+  class Frisbee {
+   public:
+    Frisbee()
+        : bottom_posedge_time_(0, 0),
+          bottom_negedge_time_(0, 0) {
+      Reset();
+    }
+
+    // Resets a Frisbee so it can be reused.
+    void Reset() {
+      bottom_posedge_time_ = ::aos::time::Time(0, 0);
+      bottom_negedge_time_ = ::aos::time::Time(0, 0);
+      has_been_indexed_ = false;
+      index_start_position_ = 0.0;
+    }
+
+    // Returns true if the position is valid.
+    bool has_position() const {
+      return has_been_indexed_;
+    }
+
+    // Returns the most up to date and accurate position that we have for the
+    // disc.  This is the indexer position that the disc grabbed at.
+    double position() const {
+      return index_start_position_;
+    }
+
+    // Posedge and negedge disc times.
+    ::aos::time::Time bottom_posedge_time_;
+    ::aos::time::Time bottom_negedge_time_;
+
+    // True if the disc has a valid index position.
+    bool has_been_indexed_;
+    // Location of the index when the disc first contacted it.
+    double index_start_position_;
+  };
+
+ protected:
+  virtual void RunIteration(
+      const control_loops::IndexLoop::Goal *goal,
+      const control_loops::IndexLoop::Position *position,
+      control_loops::IndexLoop::Output *output,
+      control_loops::IndexLoop::Status *status);
+
+ private:
+  // Sets disc_position to the minimum or maximum disc position.
+  // Returns true if there were discs, and false if there weren't.
+  // On false, disc_position is left unmodified.
+  bool MinDiscPosition(double *disc_position);
+  bool MaxDiscPosition(double *disc_position);
+
+  // The state feedback control loop to talk to for the index.
+  ::std::unique_ptr<StateFeedbackLoop<2, 1, 1>> wrist_loop_;
+
+  // Count of the number of discs that we have collected.
+  uint32_t hopper_disc_count_;
+  uint32_t total_disc_count_;
+
+  enum class Goal {
+    // Hold position, in a low power state.
+    HOLD = 0,
+    // Get ready to load discs by shifting the discs down.
+    READY_LOWER = 1,
+    // Ready the discs, spin up the transfer roller, and accept discs.
+    INTAKE = 2,
+    // Get ready to shoot, and place a disc in the loader.
+    READY_SHOOTER = 3,
+    // Shoot at will.
+    SHOOT = 4
+  };
+
+  // These two enums command and track the loader loading discs into the
+  // shooter.
+  enum class LoaderState {
+    // Open and down, ready to accept a disc.
+    READY,
+    // Closing the grabber.
+    GRABBING,
+    // Grabber closed.
+    GRABBED,
+    // Lifting the disc.
+    LIFTING,
+    // Disc lifted.
+    LIFTED,
+    // Ejecting the disc into the shooter.
+    SHOOTING,
+    // The disc has been shot.
+    SHOOT,
+    // Lowering the loader back down.
+    LOWERING,
+    // The indexer is lowered.
+    LOWERED
+  };
+
+  // TODO(aschuh): If we are grabbed and asked to be ready, now what?
+  // LOG ?
+  enum class LoaderGoal {
+    // Get the loader ready to accept another disc.
+    READY,
+    // Grab a disc now.
+    GRAB,
+    // Lift it up, shoot, and reset.
+    // Waits to shoot until the shooter is stable.
+    // Resets the goal to READY once one disc has been shot.
+    SHOOT_AND_RESET
+  };
+
+  // The current goal
+  Goal safe_goal_;
+
+  // Loader goal, state, and counter.
+  LoaderGoal loader_goal_;
+  LoaderState loader_state_;
+  int loader_countdown_;
+
+  // Current state of the pistons.
+  bool loader_up_;
+  bool disc_clamped_;
+  bool disc_ejected_;
+
+  // The frisbee that is flying through the transfer rollers.
+  Frisbee transfer_frisbee_;
+
+  // Bottom disc detect from the last valid packet for detecting edges.
+  bool last_bottom_disc_detect_;
+
+  // Frisbees are in order such that the newest frisbee is on the front.
+  ::std::deque<Frisbee> frisbees_;
+  // std::array ?
+
+  DISALLOW_COPY_AND_ASSIGN(IndexMotor);
+};
+
+}  // namespace control_loops
+}  // namespace frc971
+
+#endif  // FRC971_CONTROL_LOOPS_WRIST_H_
diff --git a/frc971/control_loops/index/index_lib_test.cc b/frc971/control_loops/index/index_lib_test.cc
new file mode 100644
index 0000000..01805c9
--- /dev/null
+++ b/frc971/control_loops/index/index_lib_test.cc
@@ -0,0 +1,707 @@
+#include <unistd.h>
+
+#include <memory>
+
+#include "gtest/gtest.h"
+#include "aos/common/queue.h"
+#include "aos/common/queue_testutils.h"
+#include "frc971/control_loops/index/index_motor.q.h"
+#include "frc971/control_loops/index/index.h"
+#include "frc971/control_loops/index/index_motor_plant.h"
+#include "frc971/control_loops/index/transfer_motor_plant.h"
+#include "frc971/constants.h"
+
+
+using ::aos::time::Time;
+
+namespace frc971 {
+namespace control_loops {
+namespace testing {
+
+class Frisbee {
+ public:
+  // Creates a frisbee starting at the specified position in the frisbee path,
+  // and with the transfer and index rollers at the specified positions.
+  Frisbee(double transfer_roller_position,
+          double index_roller_position,
+          double position = IndexMotor::kBottomDiscDetectStart)
+      : transfer_roller_position_(transfer_roller_position),
+        index_roller_position_(index_roller_position),
+        position_(position),
+        has_been_shot_(false) {
+  }
+
+  // Returns true if the frisbee is controlled by the transfer roller.
+  bool IsTouchingTransfer() const {
+    return (position_ >= IndexMotor::kBottomDiscDetectStart &&
+            position_ <= IndexMotor::kIndexStartPosition);
+  }
+
+  // Returns true if the frisbee is in a place where it is unsafe to grab.
+  bool IsUnsafeToGrab() const {
+    return (position_ > (IndexMotor::kLoaderFreeStopPosition) &&
+            position_ < IndexMotor::kGrabberStartPosition);
+  }
+
+  // Returns true if the frisbee is controlled by the indexing roller.
+  bool IsTouchingIndex() const {
+    return (position_ >= IndexMotor::kIndexStartPosition &&
+            position_ < IndexMotor::kGrabberStartPosition);
+  }
+
+  // Returns true if the frisbee is in a position such that the disc can be
+  // lifted.
+  bool IsUnsafeToLift() const {
+    return (position_ >= IndexMotor::kLoaderFreeStopPosition &&
+            position_ <= IndexMotor::kReadyToLiftPosition);
+  }
+
+  // Returns true if the frisbee is in a position such that the grabber will
+  // pull it into the loader.
+  bool IsTouchingGrabber() const {
+    return (position_ >= IndexMotor::kGrabberStartPosition &&
+            position_ < IndexMotor::kReadyToLiftPosition);
+  }
+
+  // Returns true if the frisbee is in a position such that the disc can be
+  // lifted.
+  bool IsTouchingLoader() const {
+    return (position_ >= IndexMotor::kReadyToLiftPosition &&
+            position_ < IndexMotor::kLifterStopPosition);
+  }
+
+  // Returns true if the frisbee is touching the ejector.
+  bool IsTouchingEjector() const {
+    return (position_ >= IndexMotor::kLifterStopPosition &&
+            position_ < IndexMotor::kEjectorStopPosition);
+  }
+
+  // Returns true if the disc is triggering the bottom disc detect sensor.
+  bool bottom_disc_detect() const {
+    return (position_ >= IndexMotor::kBottomDiscDetectStart &&
+            position_ <= IndexMotor::kBottomDiscDetectStop);
+  }
+
+  // Returns true if the disc is triggering the top disc detect sensor.
+  bool top_disc_detect() const {
+    return (position_ >= IndexMotor::kTopDiscDetectStart &&
+            position_ <= IndexMotor::kTopDiscDetectStop);
+  }
+
+  // Updates the position of the frisbee in the frisbee path.
+  void UpdatePosition(double transfer_roller_position,
+                      double index_roller_position,
+                      bool clamped,
+                      bool lifted,
+                      bool ejected) {
+    if (IsTouchingTransfer() || position() < 0.0) {
+      position_ += IndexMotor::ConvertTransferToDiscPosition(
+          transfer_roller_position - transfer_roller_position_);
+      printf("Transfer Roller: ");
+    } else if (IsTouchingIndex()) {
+      position_ += ::std::min(
+          IndexMotor::ConvertIndexToDiscPosition(
+            index_roller_position - index_roller_position_),
+          IndexMotor::kGrabberStartPosition);
+      // Verify that we aren't trying to grab or lift when it isn't safe.
+      EXPECT_FALSE(clamped && IsUnsafeToGrab());
+      EXPECT_FALSE(lifted && IsUnsafeToLift());
+      printf("Index: ");
+    } else if (IsTouchingGrabber()) {
+      if (clamped) {
+        const double grabber_dx = IndexMotor::kGrabberMovementVelocity / 100.0;
+        position_ = ::std::min(position_ + grabber_dx,
+                               IndexMotor::kReadyToLiftPosition);
+      }
+      EXPECT_FALSE(lifted);
+      EXPECT_FALSE(ejected);
+      printf("Grabber: ");
+    } else if (IsTouchingLoader()) {
+      if (lifted) {
+        const double lifter_dx = IndexMotor::kLifterMovementVelocity / 100.0;
+        position_ = ::std::min(position_ + lifter_dx,
+                               IndexMotor::kLifterStopPosition);
+      }
+      EXPECT_TRUE(clamped);
+      EXPECT_FALSE(ejected);
+      printf("Loader: ");
+    } else if (IsTouchingEjector()) {
+      EXPECT_TRUE(lifted);
+      if (ejected) {
+        const double ejector_dx = IndexMotor::kEjectorMovementVelocity / 100.0;
+        position_ = ::std::min(position_ + ejector_dx,
+                               IndexMotor::kEjectorStopPosition);
+        EXPECT_FALSE(clamped);
+      }
+      printf("Ejector: ");
+    } else if (position_ == IndexMotor::kEjectorStopPosition) {
+      printf("Shot: ");
+      has_been_shot_ = true;
+    }
+    transfer_roller_position_ = transfer_roller_position;
+    index_roller_position_ = index_roller_position;
+    printf("Disc is at %f\n", position_);
+  }
+
+  // Returns if the disc has been shot and can be removed from the robot.
+  bool has_been_shot() const {
+    return has_been_shot_;
+  }
+
+  // Returns the position of the disc in the system.
+  double position() const {
+    return position_;
+  }
+
+ private:
+  // Previous transfer roller position for computing deltas.
+  double transfer_roller_position_;
+  // Previous index roller position for computing deltas.
+  double index_roller_position_;
+  // Position in the robot.
+  double position_;
+  // True if the disc has been shot.
+  bool has_been_shot_;
+};
+
+
+// Class which simulates the index and sends out queue messages containing the
+// position.
+class IndexMotorSimulation {
+ public:
+  // Constructs a motor simulation.  initial_position is the inital angle of the
+  // index, which will be treated as 0 by the encoder.
+  IndexMotorSimulation()
+      : index_plant_(new StateFeedbackPlant<2, 1, 1>(MakeIndexPlant())),
+        transfer_plant_(new StateFeedbackPlant<2, 1, 1>(MakeTransferPlant())),
+        my_index_loop_(".frc971.control_loops.index",
+                       0x1a7b7094, ".frc971.control_loops.index.goal",
+                       ".frc971.control_loops.index.position",
+                       ".frc971.control_loops.index.output",
+                       ".frc971.control_loops.index.status") {
+  }
+
+  // Starts a disc at the start of the index.
+  void InsertDisc() {
+    frisbees.push_back(Frisbee(transfer_roller_position(),
+                               index_roller_position()));
+  }
+
+  // Returns true if the bottom disc sensor is triggered.
+  bool BottomDiscDetect() const {
+    bool bottom_disc_detect = false;
+    for (const Frisbee &frisbee : frisbees) {
+      bottom_disc_detect |= frisbee.bottom_disc_detect();
+    }
+    return bottom_disc_detect;
+  }
+
+  // Returns true if the top disc sensor is triggered.
+  bool TopDiscDetect() const {
+    bool top_disc_detect = false;
+    for (const Frisbee &frisbee : frisbees) {
+      top_disc_detect |= frisbee.top_disc_detect();
+    }
+    return top_disc_detect;
+  }
+
+  // Updates all discs, and verifies that the state of the system is sane.
+  void UpdateDiscs(bool clamped, bool lifted, bool ejected) {
+    for (Frisbee &frisbee : frisbees) {
+      frisbee.UpdatePosition(transfer_roller_position(),
+                             index_roller_position(),
+                             clamped,
+                             lifted,
+                             ejected);
+    }
+
+    // Make sure nobody is too close to anybody else.
+    Frisbee *last_frisbee = NULL;
+    for (Frisbee &frisbee : frisbees) {
+      if (last_frisbee) {
+        const double distance = frisbee.position() - last_frisbee->position();
+        double min_distance;
+        if (frisbee.IsTouchingTransfer() ||
+            last_frisbee->IsTouchingTransfer()) {
+          min_distance = 0.3;
+        } else {
+          min_distance =
+              IndexMotor::ConvertDiscAngleToDiscPosition(M_PI * 2.0 / 3.0);
+        }
+
+        EXPECT_LT(min_distance, ::std::abs(distance)) << "Discs too close";
+      }
+      last_frisbee = &frisbee;
+    }
+
+    // Remove any shot frisbees.
+    for (int i = 0; i < static_cast<int>(frisbees.size()); ++i) {
+      if (frisbees[i].has_been_shot()) {
+        shot_frisbees.push_back(frisbees[i]);
+        frisbees.erase(frisbees.begin() + i);
+        --i;
+      }
+    }
+  }
+
+  // Sends out the position queue messages.
+  void SendPositionMessage() {
+    ::aos::ScopedMessagePtr<control_loops::IndexLoop::Position> position =
+        my_index_loop_.position.MakeMessage();
+    position->index_position = index_roller_position();
+    position->bottom_disc_detect = BottomDiscDetect();
+    position->top_disc_detect = TopDiscDetect();
+    printf("bdd: %x tdd: %x\n", position->bottom_disc_detect,
+           position->top_disc_detect);
+    position.Send();
+  }
+
+  // Simulates the index moving for one timestep.
+  void Simulate() {
+    EXPECT_TRUE(my_index_loop_.output.FetchLatest());
+
+    index_plant_->U << my_index_loop_.output->index_voltage;
+    index_plant_->Update();
+
+    transfer_plant_->U << my_index_loop_.output->transfer_voltage;
+    transfer_plant_->Update();
+    printf("tv: %f iv: %f tp : %f ip: %f\n",
+           my_index_loop_.output->transfer_voltage,
+           my_index_loop_.output->index_voltage,
+           transfer_roller_position(), index_roller_position());
+
+    UpdateDiscs(my_index_loop_.output->disc_clamped,
+                my_index_loop_.output->loader_up,
+                my_index_loop_.output->disc_ejected);
+  }
+
+  // Plants for the index and transfer rollers.
+  ::std::unique_ptr<StateFeedbackPlant<2, 1, 1>> index_plant_;
+  ::std::unique_ptr<StateFeedbackPlant<2, 1, 1>> transfer_plant_;
+
+  // Returns the absolute angle of the index.
+  double index_roller_position() const {
+    return index_plant_->Y(0, 0);
+  }
+
+  // Returns the absolute angle of the index.
+  double transfer_roller_position() const {
+    return transfer_plant_->Y(0, 0);
+  }
+
+  // Frisbees being tracked in the robot.
+  ::std::vector<Frisbee> frisbees;
+  // Frisbees that have been shot.
+  ::std::vector<Frisbee> shot_frisbees;
+
+ private:
+  // Control loop for the indexer.
+  IndexLoop my_index_loop_;
+};
+
+
+class IndexTest : public ::testing::Test {
+ protected:
+  IndexTest() : my_index_loop_(".frc971.control_loops.index",
+                               0x1a7b7094, ".frc971.control_loops.index.goal",
+                               ".frc971.control_loops.index.position",
+                               ".frc971.control_loops.index.output",
+                               ".frc971.control_loops.index.status"),
+                index_motor_(&my_index_loop_),
+                index_motor_plant_(),
+                loop_count_(0) {
+    // Flush the robot state queue so we can use clean shared memory for this
+    // test.
+    ::aos::robot_state.Clear();
+    SendDSPacket(true);
+    Time::EnableMockTime(Time(0, 0));
+  }
+
+  virtual ~IndexTest() {
+    ::aos::robot_state.Clear();
+    Time::DisableMockTime();
+  }
+
+  // Sends a DS packet with the enable bit set to enabled.
+  void SendDSPacket(bool enabled) {
+    ::aos::robot_state.MakeWithBuilder().enabled(enabled)
+                                        .autonomous(false)
+                                        .team_id(971).Send();
+    ::aos::robot_state.FetchLatest();
+  }
+
+  // Updates the current mock time.
+  void UpdateTime() {
+    loop_count_ += 1;
+    Time::SetMockTime(Time::InMS(10 * loop_count_));
+  }
+
+  // Loads n discs into the indexer at the bottom.
+  void LoadNDiscs(int n) {
+    my_index_loop_.goal.MakeWithBuilder().goal_state(2).Send();
+    // Spin it up.
+    for (int i = 0; i < 100; ++i) {
+      index_motor_plant_.SendPositionMessage();
+      index_motor_.Iterate();
+      index_motor_plant_.Simulate();
+      SendDSPacket(true);
+      UpdateTime();
+    }
+
+    EXPECT_EQ(0, index_motor_plant_.index_roller_position());
+    my_index_loop_.status.FetchLatest();
+    EXPECT_TRUE(my_index_loop_.status->ready_to_intake);
+
+    // Stuff N discs in, waiting between each one for a tiny bit of time so they
+    // don't get too close.
+    int num_grabbed = 0;
+    int wait_counter = 0;
+    while (num_grabbed < n) {
+      index_motor_plant_.SendPositionMessage();
+      index_motor_.Iterate();
+      if (!index_motor_plant_.BottomDiscDetect()) {
+        if (wait_counter > 0) {
+          --wait_counter;
+        } else {
+          index_motor_plant_.InsertDisc();
+          ++num_grabbed;
+          wait_counter = 3;
+        }
+      }
+      index_motor_plant_.Simulate();
+      SendDSPacket(true);
+      UpdateTime();
+    }
+
+    // Settle.
+    for (int i = 0; i < 100; ++i) {
+      index_motor_plant_.SendPositionMessage();
+      index_motor_.Iterate();
+      index_motor_plant_.Simulate();
+      SendDSPacket(true);
+      UpdateTime();
+    }
+  }
+
+  // Copy of core that works in this process only.
+  ::aos::common::testing::GlobalCoreInstance my_core;
+
+  // Create a new instance of the test queue so that it invalidates the queue
+  // that it points to.  Otherwise, we will have a pointer to shared memory that
+  // is no longer valid.
+  IndexLoop my_index_loop_;
+
+  // Create a loop and simulation plant.
+  IndexMotor index_motor_;
+  IndexMotorSimulation index_motor_plant_;
+
+  // Number of loop cycles that have been executed for tracking the current
+  // time.
+  int loop_count_;
+};
+
+// Tests that the index grabs 1 disc and places it at the correct position.
+TEST_F(IndexTest, GrabSingleDisc) {
+  my_index_loop_.goal.MakeWithBuilder().goal_state(2).Send();
+  for (int i = 0; i < 250; ++i) {
+    index_motor_plant_.SendPositionMessage();
+    index_motor_.Iterate();
+    if (i == 100) {
+      EXPECT_EQ(0, index_motor_plant_.index_roller_position());
+      index_motor_plant_.InsertDisc();
+    }
+    if (i > 0) {
+      EXPECT_TRUE(my_index_loop_.status.FetchLatest());
+      EXPECT_TRUE(my_index_loop_.status->ready_to_intake);
+    }
+    index_motor_plant_.Simulate();
+    SendDSPacket(true);
+    UpdateTime();
+  }
+
+  my_index_loop_.status.FetchLatest();
+  EXPECT_EQ(my_index_loop_.status->hopper_disc_count, 1);
+  EXPECT_EQ(static_cast<size_t>(1), index_motor_plant_.frisbees.size());
+  EXPECT_NEAR(
+      IndexMotor::kIndexStartPosition + IndexMotor::ConvertDiscAngleToDiscPosition(M_PI),
+      index_motor_plant_.frisbees[0].position(), 0.05);
+}
+
+// Tests that the index grabs 1 disc and places it at the correct position when
+// told to hold immediately after the disc starts into the bot.
+TEST_F(IndexTest, GrabAndHold) {
+  my_index_loop_.goal.MakeWithBuilder().goal_state(2).Send();
+  for (int i = 0; i < 200; ++i) {
+    index_motor_plant_.SendPositionMessage();
+    index_motor_.Iterate();
+    if (i == 100) {
+      EXPECT_EQ(0, index_motor_plant_.index_roller_position());
+      index_motor_plant_.InsertDisc();
+    } else if (i == 102) {
+      // The disc has been seen.  Tell the indexer to now hold.
+      my_index_loop_.goal.MakeWithBuilder().goal_state(0).Send();
+    } else if (i > 102) {
+      my_index_loop_.status.FetchLatest();
+      EXPECT_FALSE(my_index_loop_.status->ready_to_intake);
+    }
+    index_motor_plant_.Simulate();
+    SendDSPacket(true);
+    UpdateTime();
+  }
+
+  my_index_loop_.status.FetchLatest();
+  EXPECT_EQ(my_index_loop_.status->hopper_disc_count, 1);
+  EXPECT_EQ(static_cast<size_t>(1), index_motor_plant_.frisbees.size());
+  EXPECT_NEAR(
+      (IndexMotor::kIndexStartPosition +
+       IndexMotor::ConvertDiscAngleToDiscPosition(M_PI)),
+      index_motor_plant_.frisbees[0].position(), 0.04);
+}
+
+// Tests that the index grabs two discs and places them at the correct
+// positions.
+TEST_F(IndexTest, GrabTwoDiscs) {
+  LoadNDiscs(2);
+
+  EXPECT_TRUE(my_index_loop_.status.FetchLatest());
+  EXPECT_EQ(my_index_loop_.status->hopper_disc_count, 2);
+  EXPECT_EQ(static_cast<size_t>(2), index_motor_plant_.frisbees.size());
+  EXPECT_NEAR(
+      (IndexMotor::kIndexStartPosition +
+       IndexMotor::ConvertDiscAngleToDiscPosition(M_PI)),
+      index_motor_plant_.frisbees[1].position(), 0.10);
+  EXPECT_NEAR(
+      IndexMotor::ConvertDiscAngleToDiscPosition(M_PI),
+      (index_motor_plant_.frisbees[0].position() -
+       index_motor_plant_.frisbees[1].position()), 0.10);
+}
+
+// Tests that the index grabs 2 discs, and loads one up into the loader to get
+// ready to shoot.  It then pulls the second disc back down to be ready to
+// intake more.
+TEST_F(IndexTest, ReadyGrabsOneDisc) {
+  LoadNDiscs(2);
+
+  // Lift the discs up to the top.  Wait a while to let the system settle and
+  // verify that they don't collide.
+  my_index_loop_.goal.MakeWithBuilder().goal_state(3).Send();
+  for (int i = 0; i < 300; ++i) {
+    index_motor_plant_.SendPositionMessage();
+    index_motor_.Iterate();
+    index_motor_plant_.Simulate();
+    SendDSPacket(true);
+    UpdateTime();
+  }
+
+  // Verify that the disc has been grabbed.
+  my_index_loop_.output.FetchLatest();
+  EXPECT_TRUE(my_index_loop_.output->disc_clamped);
+  // And that we are preloaded.
+  my_index_loop_.status.FetchLatest();
+  EXPECT_TRUE(my_index_loop_.status->preloaded);
+
+  // Pull the disc back down and verify that the transfer roller doesn't turn on
+  // until we are ready.
+  my_index_loop_.goal.MakeWithBuilder().goal_state(2).Send();
+  for (int i = 0; i < 100; ++i) {
+    index_motor_plant_.SendPositionMessage();
+    index_motor_.Iterate();
+
+    EXPECT_TRUE(my_index_loop_.status.FetchLatest());
+    EXPECT_TRUE(my_index_loop_.output.FetchLatest());
+    if (!my_index_loop_.status->ready_to_intake) {
+      EXPECT_EQ(my_index_loop_.output->transfer_voltage, 0)
+          << "Transfer should be off until indexer is ready";
+    }
+
+    index_motor_plant_.Simulate();
+    SendDSPacket(true);
+    UpdateTime();
+  }
+
+  my_index_loop_.status.FetchLatest();
+  EXPECT_EQ(my_index_loop_.status->hopper_disc_count, 1);
+  EXPECT_EQ(my_index_loop_.status->total_disc_count, 2);
+  my_index_loop_.output.FetchLatest();
+  EXPECT_TRUE(my_index_loop_.output->disc_clamped);
+
+  EXPECT_EQ(static_cast<size_t>(2), index_motor_plant_.frisbees.size());
+  EXPECT_NEAR(IndexMotor::kReadyToLiftPosition,
+      index_motor_plant_.frisbees[0].position(), 0.01);
+  EXPECT_NEAR(
+      (IndexMotor::kIndexStartPosition +
+       IndexMotor::ConvertDiscAngleToDiscPosition(M_PI)),
+      index_motor_plant_.frisbees[1].position(), 0.10);
+}
+
+// Tests that the index grabs 1 disc and continues to pull it in correctly when
+// in the READY_LOWER state.  The transfer roller should be disabled then.
+TEST_F(IndexTest, GrabAndReady) {
+  my_index_loop_.goal.MakeWithBuilder().goal_state(2).Send();
+  for (int i = 0; i < 200; ++i) {
+    index_motor_plant_.SendPositionMessage();
+    index_motor_.Iterate();
+    if (i == 100) {
+      EXPECT_EQ(0, index_motor_plant_.index_roller_position());
+      index_motor_plant_.InsertDisc();
+    } else if (i == 102) {
+      my_index_loop_.goal.MakeWithBuilder().goal_state(1).Send();
+    } else if (i > 150) {
+      my_index_loop_.status.FetchLatest();
+      EXPECT_TRUE(my_index_loop_.status->ready_to_intake);
+      my_index_loop_.output.FetchLatest();
+      EXPECT_EQ(my_index_loop_.output->transfer_voltage, 0.0);
+    }
+    index_motor_plant_.Simulate();
+    SendDSPacket(true);
+    UpdateTime();
+  }
+
+  my_index_loop_.status.FetchLatest();
+  EXPECT_EQ(my_index_loop_.status->hopper_disc_count, 1);
+  EXPECT_EQ(static_cast<size_t>(1), index_motor_plant_.frisbees.size());
+  EXPECT_NEAR(
+      (IndexMotor::kIndexStartPosition +
+       IndexMotor::ConvertDiscAngleToDiscPosition(M_PI)),
+      index_motor_plant_.frisbees[0].position(), 0.04);
+}
+
+// Tests that grabbing 4 discs ends up with 4 discs in the bot and us no longer
+// ready.
+TEST_F(IndexTest, GrabFourDiscs) {
+  LoadNDiscs(4);
+
+  EXPECT_TRUE(my_index_loop_.output.FetchLatest());
+  EXPECT_EQ(my_index_loop_.output->transfer_voltage, 0.0);
+  EXPECT_TRUE(my_index_loop_.status.FetchLatest());
+  EXPECT_EQ(my_index_loop_.status->hopper_disc_count, 4);
+  EXPECT_FALSE(my_index_loop_.status->ready_to_intake);
+  EXPECT_EQ(static_cast<size_t>(4), index_motor_plant_.frisbees.size());
+  EXPECT_NEAR(
+      IndexMotor::kIndexStartPosition + IndexMotor::ConvertDiscAngleToDiscPosition(M_PI),
+      index_motor_plant_.frisbees[3].position(), 0.10);
+  EXPECT_NEAR(
+      IndexMotor::ConvertDiscAngleToDiscPosition(M_PI),
+      (index_motor_plant_.frisbees[0].position() -
+       index_motor_plant_.frisbees[1].position()), 0.10);
+  EXPECT_NEAR(
+      IndexMotor::ConvertDiscAngleToDiscPosition(M_PI),
+      (index_motor_plant_.frisbees[1].position() -
+       index_motor_plant_.frisbees[2].position()), 0.10);
+  EXPECT_NEAR(
+      IndexMotor::ConvertDiscAngleToDiscPosition(M_PI),
+      (index_motor_plant_.frisbees[2].position() -
+       index_motor_plant_.frisbees[3].position()), 0.10);
+}
+
+// Tests that shooting 4 discs works.
+TEST_F(IndexTest, ShootFourDiscs) {
+  LoadNDiscs(4);
+
+  EXPECT_EQ(static_cast<size_t>(4), index_motor_plant_.frisbees.size());
+
+  my_index_loop_.goal.MakeWithBuilder().goal_state(4).Send();
+
+  // Lifting and shooting takes a while...
+  for (int i = 0; i < 300; ++i) {
+    index_motor_plant_.SendPositionMessage();
+    index_motor_.Iterate();
+    index_motor_plant_.Simulate();
+    SendDSPacket(true);
+    UpdateTime();
+  }
+
+  my_index_loop_.status.FetchLatest();
+  EXPECT_EQ(my_index_loop_.status->hopper_disc_count, 0);
+  EXPECT_EQ(my_index_loop_.status->total_disc_count, 4);
+  my_index_loop_.output.FetchLatest();
+  EXPECT_FALSE(my_index_loop_.output->disc_clamped);
+  EXPECT_FALSE(my_index_loop_.output->loader_up);
+  EXPECT_FALSE(my_index_loop_.output->disc_ejected);
+
+  EXPECT_EQ(static_cast<size_t>(4), index_motor_plant_.shot_frisbees.size());
+}
+
+// Tests that discs aren't pulled out of the loader half way through being
+// grabbed when being asked to index.
+TEST_F(IndexTest, PreloadToIndexEarlyTransition) {
+  LoadNDiscs(2);
+
+  // Lift the discs up to the top.  Wait a while to let the system settle and
+  // verify that they don't collide.
+  my_index_loop_.goal.MakeWithBuilder().goal_state(3).Send();
+  for (int i = 0; i < 300; ++i) {
+    index_motor_plant_.SendPositionMessage();
+    index_motor_.Iterate();
+    index_motor_plant_.Simulate();
+    SendDSPacket(true);
+    UpdateTime();
+    // Drop out of the loop as soon as it enters the loader.
+    // This will require it to finish the job before intaking more.
+    my_index_loop_.status.FetchLatest();
+    if (index_motor_plant_.frisbees[0].position() >
+        IndexMotor::kLoaderFreeStopPosition) {
+      break;
+    }
+  }
+
+  // Pull the disc back down and verify that the transfer roller doesn't turn on
+  // until we are ready.
+  my_index_loop_.goal.MakeWithBuilder().goal_state(1).Send();
+  for (int i = 0; i < 100; ++i) {
+    index_motor_plant_.SendPositionMessage();
+    index_motor_.Iterate();
+    index_motor_plant_.Simulate();
+    SendDSPacket(true);
+    UpdateTime();
+  }
+
+  my_index_loop_.status.FetchLatest();
+  EXPECT_EQ(my_index_loop_.status->hopper_disc_count, 1);
+  EXPECT_EQ(my_index_loop_.status->total_disc_count, 2);
+  my_index_loop_.output.FetchLatest();
+  EXPECT_TRUE(my_index_loop_.output->disc_clamped);
+
+  EXPECT_EQ(static_cast<size_t>(2), index_motor_plant_.frisbees.size());
+  EXPECT_NEAR(IndexMotor::kReadyToLiftPosition,
+      index_motor_plant_.frisbees[0].position(), 0.01);
+  EXPECT_NEAR(
+      (IndexMotor::kIndexStartPosition +
+       IndexMotor::ConvertDiscAngleToDiscPosition(M_PI)),
+      index_motor_plant_.frisbees[1].position(), 0.10);
+}
+
+// Tests that disabling while grabbing a disc doesn't cause problems.
+TEST_F(IndexTest, HandleDisable) {
+  my_index_loop_.goal.MakeWithBuilder().goal_state(2).Send();
+  for (int i = 0; i < 200; ++i) {
+    index_motor_plant_.SendPositionMessage();
+    index_motor_.Iterate();
+    if (i == 100) {
+      EXPECT_EQ(0, index_motor_plant_.index_roller_position());
+      index_motor_plant_.InsertDisc();
+    } else if (i == 102) {
+      my_index_loop_.goal.MakeWithBuilder().goal_state(1).Send();
+    } else if (i > 150) {
+      my_index_loop_.status.FetchLatest();
+      EXPECT_TRUE(my_index_loop_.status->ready_to_intake);
+      my_index_loop_.output.FetchLatest();
+      EXPECT_EQ(my_index_loop_.output->transfer_voltage, 0.0);
+    }
+    index_motor_plant_.Simulate();
+    SendDSPacket(i < 102 || i > 110);
+    UpdateTime();
+  }
+
+  my_index_loop_.status.FetchLatest();
+  EXPECT_EQ(my_index_loop_.status->hopper_disc_count, 1);
+  EXPECT_EQ(static_cast<size_t>(1), index_motor_plant_.frisbees.size());
+  EXPECT_NEAR(
+      (IndexMotor::kIndexStartPosition +
+       IndexMotor::ConvertDiscAngleToDiscPosition(M_PI)),
+      index_motor_plant_.frisbees[0].position(), 0.04);
+}
+
+}  // namespace testing
+}  // namespace control_loops
+}  // namespace frc971
diff --git a/frc971/control_loops/index/index_main.cc b/frc971/control_loops/index/index_main.cc
new file mode 100644
index 0000000..9ca3290
--- /dev/null
+++ b/frc971/control_loops/index/index_main.cc
@@ -0,0 +1,11 @@
+#include "frc971/control_loops/index/index.h"
+
+#include "aos/aos_core.h"
+
+int main() {
+  ::aos::Init();
+  frc971::control_loops::IndexMotor indexer;
+  indexer.Run();
+  ::aos::Cleanup();
+  return 0;
+}
diff --git a/frc971/control_loops/index/index_motor.q b/frc971/control_loops/index/index_motor.q
new file mode 100644
index 0000000..35a2e2a
--- /dev/null
+++ b/frc971/control_loops/index/index_motor.q
@@ -0,0 +1,54 @@
+package frc971.control_loops;
+
+import "aos/common/control_loop/control_loops.q";
+
+queue_group IndexLoop {
+  implements aos.control_loops.ControlLoop;
+
+  message Goal {
+    // The state for the indexer to be in.
+    // 0 means hold position, in a low power state.
+    // 1 means get ready to load discs by shifting the discs down.
+    // 2 means ready the discs, spin up the transfer roller, and accept discs.
+    // 3 means get ready to shoot, and place a disc grabbed in the loader.
+    // 4 means shoot at will.
+    int32_t goal_state;
+  };
+
+  message Position {
+    // Index position
+    double index_position;
+    bool bottom_disc_detect;
+    bool top_disc_detect;
+  };
+
+  message Output {
+    // Transfer roller output voltage.
+    // Positive means into the robot.
+    double transfer_voltage;
+    // Index roller.  Positive means up towards the shooter.
+    double index_voltage;
+    // Loader pistons.
+    bool disc_clamped;
+    bool loader_up;
+    bool disc_ejected;
+  };
+
+  message Status {
+    // Number of discs in the hopper
+    int32_t hopper_disc_count;
+    // Number of shot since reboot.
+    int32_t total_disc_count;
+    // Disc ready in the loader.
+    bool preloaded;
+    // Indexer ready to accept more discs.
+    bool ready_to_intake;
+  };
+
+  queue Goal goal;
+  queue Position position;
+  queue Output output;
+  queue Status status;
+};
+
+queue_group IndexLoop index_loop;
diff --git a/frc971/control_loops/index/index_motor_plant.cc b/frc971/control_loops/index/index_motor_plant.cc
new file mode 100644
index 0000000..51661fb
--- /dev/null
+++ b/frc971/control_loops/index/index_motor_plant.cc
@@ -0,0 +1,34 @@
+#include "frc971/control_loops/index/index_motor_plant.h"
+
+#include "frc971/control_loops/state_feedback_loop.h"
+
+namespace frc971 {
+namespace control_loops {
+
+
+StateFeedbackPlant<2, 1, 1> MakeIndexPlant() {
+  Eigen::Matrix<double, 2, 2> A;
+  A << 1.0, 0.00867533005665, 0.0, 0.747315209983;
+  Eigen::Matrix<double, 2, 1> B;
+  B << 0.0490373507155, 9.35402266105;
+  Eigen::Matrix<double, 1, 2> C;
+  C << 1, 0;
+  Eigen::Matrix<double, 1, 1> D;
+  D << 0;
+  Eigen::Matrix<double, 1, 1> U_max;
+  U_max << 12.0;
+  Eigen::Matrix<double, 1, 1> U_min;
+  U_min << -12.0;
+  return StateFeedbackPlant<2, 1, 1>(A, B, C, D, U_max, U_min);
+}
+
+StateFeedbackLoop<2, 1, 1> MakeIndexLoop() {
+  Eigen::Matrix<double, 2, 1> L;
+  L << 1.64731520998, 56.0569452572;
+  Eigen::Matrix<double, 1, 2> K;
+  K << 2.40538224198, 0.0619371641882;
+  return StateFeedbackLoop<2, 1, 1>(L, K, MakeIndexPlant());
+}
+
+}  // namespace frc971
+}  // namespace control_loops
diff --git a/frc971/control_loops/index/index_motor_plant.h b/frc971/control_loops/index/index_motor_plant.h
new file mode 100644
index 0000000..4600ccf
--- /dev/null
+++ b/frc971/control_loops/index/index_motor_plant.h
@@ -0,0 +1,16 @@
+#ifndef FRC971_CONTROL_LOOPS_INDEX_INDEX_MOTOR_PLANT_H_
+#define FRC971_CONTROL_LOOPS_INDEX_INDEX_MOTOR_PLANT_H_
+
+#include "frc971/control_loops/state_feedback_loop.h"
+
+namespace frc971 {
+namespace control_loops {
+
+StateFeedbackPlant<2, 1, 1> MakeIndexPlant();
+
+StateFeedbackLoop<2, 1, 1> MakeIndexLoop();
+
+}  // namespace frc971
+}  // namespace control_loops
+
+#endif  // FRC971_CONTROL_LOOPS_INDEX_INDEX_MOTOR_PLANT_H_
diff --git a/frc971/control_loops/index/transfer_motor_plant.cc b/frc971/control_loops/index/transfer_motor_plant.cc
new file mode 100644
index 0000000..9333f6a
--- /dev/null
+++ b/frc971/control_loops/index/transfer_motor_plant.cc
@@ -0,0 +1,34 @@
+#include "frc971/control_loops/index/transfer_motor_plant.h"
+
+#include "frc971/control_loops/state_feedback_loop.h"
+
+namespace frc971 {
+namespace control_loops {
+
+
+StateFeedbackPlant<2, 1, 1> MakeTransferPlant() {
+  Eigen::Matrix<double, 2, 2> A;
+  A << 1.0, 0.00867533005665, 0.0, 0.747315209983;
+  Eigen::Matrix<double, 2, 1> B;
+  B << 0.0490373507155, 9.35402266105;
+  Eigen::Matrix<double, 1, 2> C;
+  C << 1, 0;
+  Eigen::Matrix<double, 1, 1> D;
+  D << 0;
+  Eigen::Matrix<double, 1, 1> U_max;
+  U_max << 12.0;
+  Eigen::Matrix<double, 1, 1> U_min;
+  U_min << -12.0;
+  return StateFeedbackPlant<2, 1, 1>(A, B, C, D, U_max, U_min);
+}
+
+StateFeedbackLoop<2, 1, 1> MakeTransferLoop() {
+  Eigen::Matrix<double, 2, 1> L;
+  L << 1.64731520998, 56.0569452572;
+  Eigen::Matrix<double, 1, 2> K;
+  K << 1.06905877421, 0.0368709177253;
+  return StateFeedbackLoop<2, 1, 1>(L, K, MakeTransferPlant());
+}
+
+}  // namespace frc971
+}  // namespace control_loops
diff --git a/frc971/control_loops/index/transfer_motor_plant.h b/frc971/control_loops/index/transfer_motor_plant.h
new file mode 100644
index 0000000..565e8c7
--- /dev/null
+++ b/frc971/control_loops/index/transfer_motor_plant.h
@@ -0,0 +1,16 @@
+#ifndef FRC971_CONTROL_LOOPS_INDEX_TRANSFER_MOTOR_PLANT_H_
+#define FRC971_CONTROL_LOOPS_INDEX_TRANSFER_MOTOR_PLANT_H_
+
+#include "frc971/control_loops/state_feedback_loop.h"
+
+namespace frc971 {
+namespace control_loops {
+
+StateFeedbackPlant<2, 1, 1> MakeTransferPlant();
+
+StateFeedbackLoop<2, 1, 1> MakeTransferLoop();
+
+}  // namespace frc971
+}  // namespace control_loops
+
+#endif  // FRC971_CONTROL_LOOPS_INDEX_TRANSFER_MOTOR_PLANT_H_