Merge "Take distortion into account for mapping noise"
diff --git a/BUILD b/BUILD
index 2092844..353d5c9 100644
--- a/BUILD
+++ b/BUILD
@@ -43,7 +43,6 @@
 # gazelle:exclude third_party
 # gazelle:exclude external
 # gazelle:resolve go github.com/google/flatbuffers/go @com_github_google_flatbuffers//go:go_default_library
-# gazelle:resolve go github.com/phst/runfiles @com_github_phst_runfiles//:go_default_library
 # gazelle:resolve go github.com/frc971/971-Robot-Code/build_tests/fbs //build_tests:test_go_fbs
 # gazelle:resolve go github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/error_response //scouting/webserver/requests/messages:error_response_go_fbs
 # gazelle:resolve go github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/submit_data_scouting //scouting/webserver/requests/messages:submit_data_scouting_go_fbs
@@ -52,6 +51,8 @@
 # gazelle:resolve go github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/submit_notes_response //scouting/webserver/requests/messages:submit_notes_response_go_fbs
 # gazelle:resolve go github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/request_data_scouting_response //scouting/webserver/requests/messages:request_data_scouting_response_go_fbs
 # gazelle:resolve go github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/request_data_scouting //scouting/webserver/requests/messages:request_data_scouting_go_fbs
+# gazelle:resolve go github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/request_2023_data_scouting_response //scouting/webserver/requests/messages:request_2023_data_scouting_response_go_fbs
+# gazelle:resolve go github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/request_2023_data_scouting //scouting/webserver/requests/messages:request_2023_data_scouting_go_fbs
 # gazelle:resolve go github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/request_matches_for_team_response //scouting/webserver/requests/messages:request_matches_for_team_response_go_fbs
 # gazelle:resolve go github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/request_matches_for_team //scouting/webserver/requests/messages:request_matches_for_team_go_fbs
 # gazelle:resolve go github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/request_notes_for_team_response //scouting/webserver/requests/messages:request_notes_for_team_response_go_fbs
diff --git a/WORKSPACE b/WORKSPACE
index a7630d1..dd184d5 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -18,13 +18,17 @@
 
 http_archive(
     name = "bazel_skylib",
-    sha256 = "1c531376ac7e5a180e0237938a2536de0c54d93f5c278634818e0efc952dd56c",
+    sha256 = "b8a1527901774180afc798aeb28c4634bdccf19c4d98e7bdd1ce79d1fe9aaad7",
     urls = [
-        "https://mirror.bazel.build/github.com/bazelbuild/bazel-skylib/releases/download/1.0.3/bazel-skylib-1.0.3.tar.gz",
-        "https://github.com/bazelbuild/bazel-skylib/releases/download/1.0.3/bazel-skylib-1.0.3.tar.gz",
+        "https://mirror.bazel.build/github.com/bazelbuild/bazel-skylib/releases/download/1.4.1/bazel-skylib-1.4.1.tar.gz",
+        "https://github.com/bazelbuild/bazel-skylib/releases/download/1.4.1/bazel-skylib-1.4.1.tar.gz",
     ],
 )
 
+load("@bazel_skylib//:workspace.bzl", "bazel_skylib_workspace")
+
+bazel_skylib_workspace()
+
 http_archive(
     name = "aspect_bazel_lib",
     sha256 = "80897b673c2b506d21f861ae316689aa8abcc3e56947580a41bf9e68ff13af58",
@@ -1297,19 +1301,25 @@
     patches = [
         "@//third_party:rules_go/0001-Disable-warnings-for-external-repositories.patch",
     ],
-    sha256 = "2b1641428dff9018f9e85c0384f03ec6c10660d935b750e3fa1492a281a53b0f",
+    sha256 = "dd926a88a564a9246713a9c00b35315f54cbd46b31a26d5d8fb264c07045f05d",
     urls = [
-        "https://www.frc971.org/Build-Dependencies/rules_go-v0.29.0.zip",
-        "https://github.com/bazelbuild/rules_go/releases/download/v0.29.0/rules_go-v0.29.0.zip",
+        "https://mirror.bazel.build/github.com/bazelbuild/rules_go/releases/download/v0.38.1/rules_go-v0.38.1.zip",
+        "https://github.com/bazelbuild/rules_go/releases/download/v0.38.1/rules_go-v0.38.1.zip",
     ],
 )
 
 http_archive(
     name = "bazel_gazelle",
-    sha256 = "de69a09dc70417580aabf20a28619bb3ef60d038470c7cf8442fafcf627c21cb",
+    patch_args = [
+        "-p1",
+    ],
+    patches = [
+        "@//third_party:bazel-gazelle/0001-Fix-visibility-of-gazelle-runner.patch",
+    ],
+    sha256 = "ecba0f04f96b4960a5b250c8e8eeec42281035970aa8852dda73098274d14a1d",
     urls = [
-        "https://mirror.bazel.build/github.com/bazelbuild/bazel-gazelle/releases/download/v0.24.0/bazel-gazelle-v0.24.0.tar.gz",
-        "https://github.com/bazelbuild/bazel-gazelle/releases/download/v0.24.0/bazel-gazelle-v0.24.0.tar.gz",
+        "https://mirror.bazel.build/github.com/bazelbuild/bazel-gazelle/releases/download/v0.29.0/bazel-gazelle-v0.29.0.tar.gz",
+        "https://github.com/bazelbuild/bazel-gazelle/releases/download/v0.29.0/bazel-gazelle-v0.29.0.tar.gz",
     ],
 )
 
@@ -1317,7 +1327,7 @@
 
 go_rules_dependencies()
 
-go_register_toolchains(version = "1.18")
+go_register_toolchains(version = "1.19.5")
 
 load("@bazel_gazelle//:deps.bzl", "gazelle_dependencies")
 load("//:go_deps.bzl", "go_dependencies")
@@ -1545,3 +1555,31 @@
     strip_prefix = "en.STSW-IMG009_v3.5.2/API",
     url = "https://www.frc971.org/Build-Dependencies/en.STSW-IMG009.zip",
 )
+
+http_archive(
+    name = "apriltag_test_bfbs_images",
+    build_file_content = """
+filegroup(
+    name = "apriltag_test_bfbs_images",
+    srcs = glob(["**"]),
+    visibility = ["//visibility:public"],
+)""",
+    sha256 = "2356b9d0b3be59d01e837bfbbee21de55b16232d5e00c66701c20b64ff3272e3",
+    url = "https://www.frc971.org/Build-Dependencies/2023_arducam_apriltag_test_images.tar.gz",
+)
+
+http_archive(
+    name = "libedgetpu",
+    build_file = "//third_party:libedgetpu/libedgetpu.BUILD",
+    sha256 = "d082df79a33309f58da697cce258acca96ceb12db40660fdbf7826289e4a037c",
+    strip_prefix = "libedgetpu-bazel",
+    url = "https://www.frc971.org/Build-Dependencies/libedgetpu-1.0.tar.gz",
+)
+
+http_archive(
+    name = "libtensorflowlite",
+    build_file = "//third_party:libtensorflowlite/libtensorflowlite.BUILD",
+    sha256 = "0e3f8deac9c7cdf9aa5812ad6a87af318ed1cf08cb0c414aa494846b7fc15302",
+    strip_prefix = "tensorflow-bazel",
+    url = "https://www.frc971.org/Build-Dependencies/tensorflow-2.8.0.tar.gz",
+)
diff --git a/aos/actions/actions.h b/aos/actions/actions.h
index bda29cc..1b5d607 100644
--- a/aos/actions/actions.h
+++ b/aos/actions/actions.h
@@ -25,6 +25,8 @@
 class ActionQueue {
  public:
   // Queues up an action for sending.
+  // TODO(james): Allow providing something other than a unique_ptr to avoid
+  // malloc's.
   void EnqueueAction(::std::unique_ptr<Action> action);
 
   // Cancels the current action, and runs the next one after the current one has
diff --git a/frc971/autonomous/base_autonomous_actor.cc b/frc971/autonomous/base_autonomous_actor.cc
index eca86b9..ca6cc89 100644
--- a/frc971/autonomous/base_autonomous_actor.cc
+++ b/frc971/autonomous/base_autonomous_actor.cc
@@ -28,7 +28,7 @@
       dt_config_(dt_config),
       initial_drivetrain_({0.0, 0.0}),
       target_selector_hint_sender_(
-          event_loop->MakeSender<
+          event_loop->TryMakeSender<
               ::y2019::control_loops::drivetrain::TargetSelectorHint>(
               "/drivetrain")),
       drivetrain_goal_sender_(
@@ -456,7 +456,8 @@
     builder.CheckOk(builder.Send(goal_builder.Finish()));
   }
 
-  {
+  if (target_selector_hint_sender_) {
+    // TODO(james): 2019? Seriously?
     auto builder = target_selector_hint_sender_.MakeBuilder();
     ::y2019::control_loops::drivetrain::TargetSelectorHint::Builder
         target_hint_builder = builder.MakeBuilder<
diff --git a/frc971/control_loops/drivetrain/drivetrain_status.fbs b/frc971/control_loops/drivetrain/drivetrain_status.fbs
index f886bb2..89dc3d5 100644
--- a/frc971/control_loops/drivetrain/drivetrain_status.fbs
+++ b/frc971/control_loops/drivetrain/drivetrain_status.fbs
@@ -82,6 +82,12 @@
   available_splines:[int] (id: 12);
 }
 
+enum RobotSide : ubyte {
+  FRONT = 0,
+  BACK = 1,
+  DONT_CARE = 2,
+}
+
 // For logging state of the line follower.
 table LineFollowLogging {
   // Whether we are currently freezing target choice.
@@ -100,6 +106,8 @@
   goal_theta:float (id: 7);
   // Current relative heading.
   rel_theta:float (id: 8);
+  // Current goal drive direction.
+  drive_direction:RobotSide = DONT_CARE (id: 9);
 }
 
 // Current states of the EKF. See hybrid_ekf.h for detailed comments.
diff --git a/frc971/control_loops/drivetrain/line_follow_drivetrain.cc b/frc971/control_loops/drivetrain/line_follow_drivetrain.cc
index 95f14a3..101c0f7 100644
--- a/frc971/control_loops/drivetrain/line_follow_drivetrain.cc
+++ b/frc971/control_loops/drivetrain/line_follow_drivetrain.cc
@@ -87,6 +87,19 @@
   return Kff;
 }
 
+double VelocitySignForSide(TargetSelectorInterface::Side side,
+                           double goal_velocity) {
+  switch (side) {
+    case TargetSelectorInterface::Side::FRONT:
+      return 1.0;
+    case TargetSelectorInterface::Side::BACK:
+      return -1.0;
+    case TargetSelectorInterface::Side::DONT_CARE:
+      return goal_velocity >= 0.0 ? 1.0 : -1.0;
+  }
+  return 1.0;
+}
+
 }  // namespace
 
 // When we create A/B, we do recompute A/B, but we don't really care about
@@ -152,18 +165,21 @@
     const ::Eigen::Matrix<double, 5, 1> &abs_state, double relative_y_offset,
     double velocity_sign) {
   // Calculates the goal angle for the drivetrain given our position.
-  // The calculated goal will be such that a point disc_rad to one side of the
-  // drivetrain (the side depends on where we approach from) will end up hitting
-  // the plane of the target exactly disc_rad from the center of the target.
-  // This allows us to better approach targets in the 2019 game from an
-  // angle--radii of zero imply driving straight in.
-  const double disc_rad = target_selector_->TargetRadius();
+  // The calculated goal will be such that a point piece_rad to one side of the
+  // drivetrain (the side depends on where we approach from and SignedRadii())
+  // will end up hitting the plane of the target exactly target_rad from the
+  // center of the target. This allows us to better approach targets in the 2019
+  // game from an angle--radii of zero imply driving straight in.
+  const double target_rad = target_selector_->TargetRadius();
+  const double piece_rad = target_selector_->GamePieceRadius();
   // Depending on whether we are to the right or left of the target, we work off
   // of a different side of the robot.
-  const double edge_sign = relative_y_offset > 0 ? 1.0 : -1.0;
+  const double edge_sign = target_selector_->SignedRadii()
+                               ? 1.0
+                               : (relative_y_offset > 0 ? 1.0 : -1.0);
   // Note side_adjust which is the input from the driver's wheel to allow
   // shifting the goal target left/right.
-  const double edge_offset = edge_sign * disc_rad - side_adjust_;
+  const double edge_offset = edge_sign * target_rad - side_adjust_;
   // The point that we are trying to get the disc to hit.
   const Pose corner = Pose(&target_pose_, {0.0, edge_offset, 0.0}, 0.0);
   // A pose for the current robot position that is square to the target.
@@ -172,14 +188,17 @@
   // To prevent numerical issues, we limit x so that when the localizer isn't
   // working properly and ends up driving past the target, we still get sane
   // results.
-  square_robot.mutable_pos()->x() =
-      ::std::min(::std::min(square_robot.mutable_pos()->x(), -disc_rad), -0.01);
+  // The min() with -piece_rad ensures that we past well-conditioned numbers
+  // to acos() (we must have piece_rad <= dist_to_corner); the min with -0.01
+  // ensures that dist_to_corner doesn't become near zero.
+  square_robot.mutable_pos()->x() = ::std::min(
+      ::std::min(square_robot.mutable_pos()->x(), -std::abs(piece_rad)), -0.01);
   // Distance from the edge of the disc on the robot to the velcro we want to
   // hit on the target.
   const double dist_to_corner = square_robot.xy_norm();
   // The following actually handles calculating the heading we need the robot to
   // take (relative to the plane of the target).
-  const double alpha = ::std::acos(disc_rad / dist_to_corner);
+  const double alpha = ::std::acos(piece_rad / dist_to_corner);
   const double heading_to_robot = edge_sign * square_robot.heading();
   double theta = -edge_sign * (M_PI - alpha - (heading_to_robot - M_PI_2));
   if (velocity_sign < 0) {
@@ -196,13 +215,14 @@
   // UpdateSelection every time.
   bool new_target =
       target_selector_->UpdateSelection(abs_state, goal_velocity_);
-  if (freeze_target_) {
+  if (freeze_target_ && !target_selector_->ForceReselectTarget()) {
     // When freezing the target, only make changes if we didn't have a good
     // target before.
     if (!have_target_ && new_target) {
       have_target_ = true;
       start_of_target_acquire_ = now;
-      velocity_sign_ = goal_velocity_ >= 0.0 ? 1.0 : -1.0;
+      velocity_sign_ = VelocitySignForSide(target_selector_->DriveDirection(),
+                                           goal_velocity_);
       target_pose_ = target_selector_->TargetPose();
     }
   } else {
@@ -211,9 +231,11 @@
     have_target_ = new_target;
     if (have_target_) {
       target_pose_ = target_selector_->TargetPose();
-      velocity_sign_ = goal_velocity_ >= 0.0 ? 1.0 : -1.0;
+      velocity_sign_ = VelocitySignForSide(target_selector_->DriveDirection(),
+                                           goal_velocity_);
     }
   }
+
   // Get the robot pose in the target coordinate frame.
   relative_pose_ = Pose({abs_state.x(), abs_state.y(), 0.0}, abs_state(2, 0))
                        .Rebase(&target_pose_);
@@ -276,6 +298,8 @@
       -relative_pose_.rel_pos().x());
   line_follow_logging_builder.add_goal_theta(controls_goal_(0, 0));
   line_follow_logging_builder.add_rel_theta(relative_pose_.rel_theta());
+  line_follow_logging_builder.add_drive_direction(
+      target_selector_->DriveDirection());
   return line_follow_logging_builder.Finish();
 }
 
diff --git a/frc971/control_loops/drivetrain/line_follow_drivetrain_test.cc b/frc971/control_loops/drivetrain/line_follow_drivetrain_test.cc
index bb872dc..5fe6ec3 100644
--- a/frc971/control_loops/drivetrain/line_follow_drivetrain_test.cc
+++ b/frc971/control_loops/drivetrain/line_follow_drivetrain_test.cc
@@ -190,6 +190,7 @@
       for (double v : {-3.0, -2.0, -1.0, 0.0, 1.0, 2.0, 3.0}) {
         for (double throttle : {-1.0, 1.0}) {
           target_selector_.set_target_radius(0.0);
+          target_selector_.set_game_piece_radius(0.0);
           const double zero_rad_theta = GoalTheta(x, y, v, throttle);
           EXPECT_NEAR(
               0.0,
@@ -198,6 +199,7 @@
                                      zero_rad_theta),
               1e-14);
           target_selector_.set_target_radius(0.05);
+          target_selector_.set_game_piece_radius(0.05);
           const double small_rad_theta = GoalTheta(x, y, v, throttle);
           if (y > 0) {
             EXPECT_LT(small_rad_theta, zero_rad_theta);
@@ -260,6 +262,27 @@
       << "Expected state of zero, got: " << state_.transpose();
 }
 
+// Tests that, when we explicitly require that the controller use a new target
+// that we obey it, even during trying to do line following.
+TEST_F(LineFollowDrivetrainTest, IgnoreFreezeWhenRequested) {
+  state_.setZero();
+  set_goal_pose({{0.0, 0.0, 0.0}, 0.0});
+  // Do one iteration to get the target into the drivetrain:
+  Iterate();
+
+  freeze_target_ = true;
+  target_selector_.set_force_reselect(true);
+
+  // Set a goal pose that we will end up trying to go to.
+  set_goal_pose({{1.0, 0.0, 0.0}, 0.0});
+  driver_model_ = [](const ::Eigen::Matrix<double, 5, 1> &) { return 0.25; };
+
+  RunForTime(::std::chrono::seconds(5));
+  // Should've driven a decent distance in X.
+  EXPECT_LT(1.0, state_.x());
+  EXPECT_NEAR(0.0, state_.y(), 1e-6);
+}
+
 // Tests that when we freeze the controller without having acquired a target, we
 // don't do anything until a target arrives.
 TEST_F(LineFollowDrivetrainTest, FreezeWithoutAcquiringTarget) {
@@ -309,12 +332,12 @@
   VerifyNearGoal();
 }
 INSTANTIATE_TEST_SUITE_P(TargetPosTest, LineFollowDrivetrainTargetParamTest,
-                        ::testing::Values(Pose({0.0, 0.0, 0.0}, 0.0),
-                                          Pose({1.0, 0.0, 0.0}, 0.0),
-                                          Pose({3.0, 1.0, 0.0}, 0.0),
-                                          Pose({3.0, 0.0, 0.0}, 0.5),
-                                          Pose({3.0, 0.0, 0.0}, -0.5),
-                                          Pose({-3.0, -1.0, 0.0}, -2.5)));
+                         ::testing::Values(Pose({0.0, 0.0, 0.0}, 0.0),
+                                           Pose({1.0, 0.0, 0.0}, 0.0),
+                                           Pose({3.0, 1.0, 0.0}, 0.0),
+                                           Pose({3.0, 0.0, 0.0}, 0.5),
+                                           Pose({3.0, 0.0, 0.0}, -0.5),
+                                           Pose({-3.0, -1.0, 0.0}, -2.5)));
 
 class LineFollowDrivetrainParamTest
     : public LineFollowDrivetrainTest,
diff --git a/frc971/control_loops/drivetrain/localizer.h b/frc971/control_loops/drivetrain/localizer.h
index d590f1d..867b3c0 100644
--- a/frc971/control_loops/drivetrain/localizer.h
+++ b/frc971/control_loops/drivetrain/localizer.h
@@ -15,6 +15,7 @@
 // state updates and then determine what poes we should be driving to.
 class TargetSelectorInterface {
  public:
+  typedef RobotSide Side;
   virtual ~TargetSelectorInterface() {}
   // Take the state as [x, y, theta, left_vel, right_vel]
   // If unable to determine what target to go for, returns false. If a viable
@@ -28,11 +29,31 @@
   // Gets the current target pose. Should only be called if UpdateSelection has
   // returned true.
   virtual TypedPose<double> TargetPose() const = 0;
+  // For the "radii" below, we have two possible modes:
+  // 1) Akin to 2019, we can place with either edge of the game piece, so
+  //    the line following code will have to automatically detect which edge
+  //    (right or left) to aim to have intersect the target.
+  // 2) As in 2023, the game piece itself is offset in the robot and so we care
+  //    which of left vs. right we are using.
+  // In situation (1), SignedRadii() should return false and the *Radius()
+  // functions should return a non-negative number (technically I think the
+  // math may work for negative numbers, but may have weird implications
+  // physically...). For (2) SignedRadii()
+  // should return true and the sign of the *Radius() functions will be
+  // respected by the line following code.
+  virtual bool SignedRadii() const = 0;
   // The "radius" of the target--for y2019, we wanted to drive in so that a disc
   // with radius r would hit the plane of the target at an offset of exactly r
   // from the TargetPose--this is distinct from wanting the center of the
   // robot to project straight onto the center of the target.
   virtual double TargetRadius() const = 0;
+  // the "radius" of the robot/game piece to place.
+  virtual double GamePieceRadius() const = 0;
+  // Which direction we want the robot to drive to get to the target.
+  virtual Side DriveDirection() const = 0;
+  // Indicates that the line following *must* drive to the currently selected
+  // target, regardless of any hysteresis we try to use to protect the driver.
+  virtual bool ForceReselectTarget() const = 0;
 };
 
 // Defines an interface for classes that provide field-global localization.
@@ -114,17 +135,30 @@
     return has_target_;
   }
   TypedPose<double> TargetPose() const override { return pose_; }
+  bool SignedRadii() const override { return signed_radii_; }
   double TargetRadius() const override { return target_radius_; }
+  double GamePieceRadius() const override { return game_piece_radius_; }
+  Side DriveDirection() const override { return drive_direction_; }
+  bool ForceReselectTarget() const override { return force_reselect_; }
 
   void set_pose(const TypedPose<double> &pose) { pose_ = pose; }
   void set_target_radius(double radius) { target_radius_ = radius; }
+  void set_game_piece_radius(double radius) { game_piece_radius_ = radius; }
   void set_has_target(bool has_target) { has_target_ = has_target; }
+  void set_drive_direction(Side side) { drive_direction_ = side; }
+  void set_force_reselect(bool force_reselect) {
+    force_reselect_ = force_reselect;
+  }
   bool has_target() const { return has_target_; }
 
  private:
   bool has_target_ = true;
+  bool force_reselect_ = false;
   TypedPose<double> pose_;
+  bool signed_radii_ = false;
   double target_radius_ = 0.0;
+  double game_piece_radius_ = 0.0;
+  Side drive_direction_ = Side::DONT_CARE;
 };
 
 // Uses the generic HybridEkf implementation to provide a basic field estimator.
diff --git a/frc971/vision/foxglove_image_converter_lib.cc b/frc971/vision/foxglove_image_converter_lib.cc
index f5ecb40..920eaf7 100644
--- a/frc971/vision/foxglove_image_converter_lib.cc
+++ b/frc971/vision/foxglove_image_converter_lib.cc
@@ -3,7 +3,7 @@
 #include <opencv2/imgcodecs.hpp>
 #include <opencv2/imgproc.hpp>
 
-DEFINE_int32(jpeg_quality, 95,
+DEFINE_int32(jpeg_quality, 60,
              "Compression quality of JPEGs, 0-100; lower numbers mean lower "
              "quality and resulting image sizes.");
 
diff --git a/frc971/vision/foxglove_image_converter_test.cc b/frc971/vision/foxglove_image_converter_test.cc
index e027de2..7a3f786 100644
--- a/frc971/vision/foxglove_image_converter_test.cc
+++ b/frc971/vision/foxglove_image_converter_test.cc
@@ -6,6 +6,8 @@
 #include "aos/testing/tmpdir.h"
 #include "gtest/gtest.h"
 
+DECLARE_int32(jpeg_quality);
+
 namespace frc971::vision {
 std::ostream &operator<<(std::ostream &os, ImageCompression compression) {
   os << ExtensionForCompression(compression);
@@ -29,6 +31,10 @@
                    GetParam()),
         output_path_(absl::StrCat(aos::testing::TestTmpDir(), "/test.",
                                   ExtensionForCompression(GetParam()))) {
+    // Because our test image for comparison was generated with a JPEG quality
+    // of 95, we need to use that for the test to work. This also protects the
+    // tests against future changes to the default JPEG quality.
+    FLAGS_jpeg_quality = 95;
     test_event_loop_->OnRun(
         [this]() { image_sender_.CheckOk(image_sender_.Send(camera_image_)); });
     test_event_loop_->MakeWatcher(
diff --git a/go.mod b/go.mod
index b1720c2..6c6c8ee 100644
--- a/go.mod
+++ b/go.mod
@@ -3,10 +3,11 @@
 go 1.17
 
 require (
+	github.com/bazelbuild/rules_go v0.38.1
 	github.com/buildkite/go-buildkite v2.2.0+incompatible
 	github.com/golang/protobuf v1.5.2
 	github.com/google/flatbuffers v2.0.5+incompatible
-	google.golang.org/grpc v1.43.0
+	google.golang.org/grpc v1.50.0
 	gorm.io/driver/postgres v1.3.7
 	gorm.io/gorm v1.23.5
 )
@@ -15,12 +16,11 @@
 	github.com/cenkalti/backoff v2.2.1+incompatible // indirect
 	github.com/davecgh/go-spew v1.1.1
 	github.com/google/go-querystring v1.1.0 // indirect
-	github.com/phst/runfiles v0.0.0-20220125203201-388095b3a22d
-	golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 // indirect
+	golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4 // indirect
 	golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 // indirect
 	golang.org/x/text v0.3.7 // indirect
 	google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013 // indirect
-	google.golang.org/protobuf v1.26.0 // indirect
+	google.golang.org/protobuf v1.28.0 // indirect
 )
 
 require (
diff --git a/go.sum b/go.sum
index 3082bdc..e0725cb 100644
--- a/go.sum
+++ b/go.sum
@@ -4,6 +4,8 @@
 github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030IGemrRc=
 github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
 github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
+github.com/bazelbuild/rules_go v0.38.1 h1:YGNsLhWe18Ielebav7cClP3GMwBxBE+xEArLHtmXDx8=
+github.com/bazelbuild/rules_go v0.38.1/go.mod h1:TMHmtfpvyfsxaqfL9WnahCsXMWDMICTw7XeK9yVb+YU=
 github.com/buildkite/go-buildkite v2.2.0+incompatible h1:yEjSu1axFC88x4dbufhgMDsEnJztPWlLiZzEvzJggXc=
 github.com/buildkite/go-buildkite v2.2.0+incompatible/go.mod h1:WTV0aX5KnQ9ofsKMg2CLUBLJNsQ0RwOEKPhrXXZWPcE=
 github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4=
@@ -14,8 +16,8 @@
 github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
 github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
 github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI=
-github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
 github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
+github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
 github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
 github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I=
 github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ=
@@ -29,7 +31,7 @@
 github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
 github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
 github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
-github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0=
+github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE=
 github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
 github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
 github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
@@ -39,6 +41,7 @@
 github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
 github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
 github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
+github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
 github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
 github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
 github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
@@ -61,8 +64,9 @@
 github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
 github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ=
+github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
 github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
 github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
@@ -138,8 +142,6 @@
 github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
 github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
 github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
-github.com/phst/runfiles v0.0.0-20220125203201-388095b3a22d h1:N5aMcF9W9AjW4ed+PJhA7+FjdgPa9gJ+St3mNu2tq1Q=
-github.com/phst/runfiles v0.0.0-20220125203201-388095b3a22d/go.mod h1:+oijTyzCf6Qe7sczsCOuoeX11IxZ+UkXXlhLrfyHlzg=
 github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
 github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
@@ -165,6 +167,7 @@
 github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
 github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
 github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
 github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
 go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
 go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
@@ -196,6 +199,7 @@
 golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
 golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
 golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
+golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -205,14 +209,17 @@
 golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
-golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 h1:qWPm9rbaAMKs8Bq/9LRpbMqxWRVUAQwMI9fVrssnTfw=
+golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
 golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
+golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4 h1:4nGaVu0QrbjT/AK2PRLuQfQuh6DJve+pELhqTdAj3x0=
+golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
 golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
 golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -225,7 +232,11 @@
 golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 h1:SrN+KX8Art/Sf4HNj6Zcz06G7VEz+7w9tdXTPOZ7+l4=
 golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
@@ -247,7 +258,9 @@
 golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
 golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
 golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
 golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
 golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -268,8 +281,8 @@
 google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
 google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0=
 google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
-google.golang.org/grpc v1.43.0 h1:Eeu7bZtDZ2DpRCsLhUlcrLnvYaMK1Gz86a+hMVvELmM=
-google.golang.org/grpc v1.43.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU=
+google.golang.org/grpc v1.50.0 h1:fPVVDxY9w++VjTZsYvXWqEf9Rqar/e+9zYfxKK+W+YU=
+google.golang.org/grpc v1.50.0/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI=
 google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
 google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
 google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
@@ -280,8 +293,10 @@
 google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
 google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
 google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
-google.golang.org/protobuf v1.26.0 h1:bxAC2xTBsZGibn2RTntX0oH50xLsqy1OxA9tTL3p/lk=
 google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
+google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
+google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw=
+google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
diff --git a/go_deps.bzl b/go_deps.bzl
index 2869b5e..87871bd 100644
--- a/go_deps.bzl
+++ b/go_deps.bzl
@@ -9,10 +9,10 @@
         version = "v0.0.1-2019.2.3",
     )
     maybe_override_go_dep(
-        name = "com_github_antihax_optional",
-        importpath = "github.com/antihax/optional",
-        sum = "h1:xK2lYat7ZLaVVcIuj82J8kIro4V6kDe0AUDFboUCwcg=",
-        version = "v1.0.0",
+        name = "com_github_bazelbuild_rules_go",
+        importpath = "github.com/bazelbuild/rules_go",
+        sum = "h1:YGNsLhWe18Ielebav7cClP3GMwBxBE+xEArLHtmXDx8=",
+        version = "v0.38.1",
     )
     maybe_override_go_dep(
         name = "com_github_buildkite_go_buildkite",
@@ -89,8 +89,8 @@
     maybe_override_go_dep(
         name = "com_github_envoyproxy_go_control_plane",
         importpath = "github.com/envoyproxy/go-control-plane",
-        sum = "h1:fP+fF0up6oPY49OrjPrhIJ8yQfdIM85NXMLkMg1EXVs=",
-        version = "v0.9.10-0.20210907150352-cf90f659a021",
+        sum = "h1:xvqufLtNVwAhN8NMyWklVgxnWohi+wtMGQMhtxexlm0=",
+        version = "v0.10.2-0.20220325020618-49ff273808a1",
     )
     maybe_override_go_dep(
         name = "com_github_envoyproxy_protoc_gen_validate",
@@ -99,12 +99,6 @@
         version = "v0.1.0",
     )
     maybe_override_go_dep(
-        name = "com_github_ghodss_yaml",
-        importpath = "github.com/ghodss/yaml",
-        sum = "h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=",
-        version = "v1.0.0",
-    )
-    maybe_override_go_dep(
         name = "com_github_go_kit_log",
         importpath = "github.com/go-kit/log",
         sum = "h1:DGJh0Sm43HbOeYDNnVZFl8BvcYVvjD5bqYJvp0REbwQ=",
@@ -137,8 +131,8 @@
     maybe_override_go_dep(
         name = "com_github_golang_mock",
         importpath = "github.com/golang/mock",
-        sum = "h1:G5FRp8JnTd7RQH5kemVNlMeyXQAztQ3mOWV95KxsXH8=",
-        version = "v1.1.1",
+        sum = "h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=",
+        version = "v1.6.0",
     )
     maybe_override_go_dep(
         name = "com_github_golang_protobuf",
@@ -149,8 +143,8 @@
     maybe_override_go_dep(
         name = "com_github_google_go_cmp",
         importpath = "github.com/google/go-cmp",
-        sum = "h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=",
-        version = "v0.5.5",
+        sum = "h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ=",
+        version = "v0.5.6",
     )
     maybe_override_go_dep(
         name = "com_github_google_go_querystring",
@@ -171,12 +165,6 @@
         version = "v1.1.2",
     )
     maybe_override_go_dep(
-        name = "com_github_grpc_ecosystem_grpc_gateway",
-        importpath = "github.com/grpc-ecosystem/grpc-gateway",
-        sum = "h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo=",
-        version = "v1.16.0",
-    )
-    maybe_override_go_dep(
         name = "com_github_jackc_chunkreader",
         importpath = "github.com/jackc/chunkreader",
         sum = "h1:4s39bBR8ByfqH+DKm8rQA3E1LHZWB9XWcrz8fqaZbe0=",
@@ -315,12 +303,6 @@
         version = "v0.0.12",
     )
     maybe_override_go_dep(
-        name = "com_github_phst_runfiles",
-        importpath = "github.com/phst/runfiles",
-        sum = "h1:N5aMcF9W9AjW4ed+PJhA7+FjdgPa9gJ+St3mNu2tq1Q=",
-        version = "v0.0.0-20220125203201-388095b3a22d",
-    )
-    maybe_override_go_dep(
         name = "com_github_pkg_errors",
         importpath = "github.com/pkg/errors",
         sum = "h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=",
@@ -339,12 +321,6 @@
         version = "v0.0.0-20190812154241-14fe0d1b01d4",
     )
     maybe_override_go_dep(
-        name = "com_github_rogpeppe_fastuuid",
-        importpath = "github.com/rogpeppe/fastuuid",
-        sum = "h1:Ppwyp6VYCF1nvBTXL3trRso7mXMlRrw9ooo375wvi2s=",
-        version = "v1.2.0",
-    )
-    maybe_override_go_dep(
         name = "com_github_rogpeppe_go_internal",
         importpath = "github.com/rogpeppe/go-internal",
         sum = "h1:RR9dF3JtopPvtkroDZuVD7qquD0bnHlKSqaQhgwt8yk=",
@@ -425,8 +401,8 @@
     maybe_override_go_dep(
         name = "in_gopkg_yaml_v2",
         importpath = "gopkg.in/yaml.v2",
-        sum = "h1:fvjTMHxHEw/mxHbtzPi3JCcKXQRAnQTBRo6YCJSVHKI=",
-        version = "v2.2.3",
+        sum = "h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=",
+        version = "v2.2.2",
     )
     maybe_override_go_dep(
         name = "in_gopkg_yaml_v3",
@@ -447,12 +423,6 @@
         version = "v1.23.5",
     )
     maybe_override_go_dep(
-        name = "io_opentelemetry_go_proto_otlp",
-        importpath = "go.opentelemetry.io/proto/otlp",
-        sum = "h1:rwOQPCuKAKmwGKq2aVNnYIibI6wnV7EvzgfTCzcdGg8=",
-        version = "v0.7.0",
-    )
-    maybe_override_go_dep(
         name = "org_golang_google_appengine",
         importpath = "google.golang.org/appengine",
         sum = "h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508=",
@@ -467,14 +437,14 @@
     maybe_override_go_dep(
         name = "org_golang_google_grpc",
         importpath = "google.golang.org/grpc",
-        sum = "h1:Eeu7bZtDZ2DpRCsLhUlcrLnvYaMK1Gz86a+hMVvELmM=",
-        version = "v1.43.0",
+        sum = "h1:fPVVDxY9w++VjTZsYvXWqEf9Rqar/e+9zYfxKK+W+YU=",
+        version = "v1.50.0",
     )
     maybe_override_go_dep(
         name = "org_golang_google_protobuf",
         importpath = "google.golang.org/protobuf",
-        sum = "h1:bxAC2xTBsZGibn2RTntX0oH50xLsqy1OxA9tTL3p/lk=",
-        version = "v1.26.0",
+        sum = "h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw=",
+        version = "v1.28.0",
     )
     maybe_override_go_dep(
         name = "org_golang_x_crypto",
@@ -503,8 +473,8 @@
     maybe_override_go_dep(
         name = "org_golang_x_net",
         importpath = "golang.org/x/net",
-        sum = "h1:qWPm9rbaAMKs8Bq/9LRpbMqxWRVUAQwMI9fVrssnTfw=",
-        version = "v0.0.0-20210226172049-e18ecbb05110",
+        sum = "h1:4nGaVu0QrbjT/AK2PRLuQfQuh6DJve+pELhqTdAj3x0=",
+        version = "v0.0.0-20210405180319-a5a99cb37ef4",
     )
     maybe_override_go_dep(
         name = "org_golang_x_oauth2",
diff --git a/scouting/DriverRank/BUILD b/scouting/DriverRank/BUILD
new file mode 100644
index 0000000..e82fbfb
--- /dev/null
+++ b/scouting/DriverRank/BUILD
@@ -0,0 +1,7 @@
+filegroup(
+    name = "driver_rank_script",
+    srcs = [
+        "src/DriverRank.jl",
+    ],
+    visibility = ["//scouting:__subpackages__"],
+)
diff --git a/scouting/DriverRank/Manifest.toml b/scouting/DriverRank/Manifest.toml
index 9422299..a5a21d2 100644
--- a/scouting/DriverRank/Manifest.toml
+++ b/scouting/DriverRank/Manifest.toml
@@ -2,19 +2,7 @@
 
 julia_version = "1.8.2"
 manifest_format = "2.0"
-project_hash = "e9117a26c6e818e3664cdb877496dc724a5d6e7d"
-
-[[deps.AbstractFFTs]]
-deps = ["ChainRulesCore", "LinearAlgebra"]
-git-tree-sha1 = "69f7020bd72f069c219b5e8c236c1fa90d2cb409"
-uuid = "621f4979-c628-5d54-868e-fcf4e3e8185c"
-version = "1.2.1"
-
-[[deps.Accessors]]
-deps = ["Compat", "CompositionsBase", "ConstructionBase", "Dates", "InverseFunctions", "LinearAlgebra", "MacroTools", "Requires", "Test"]
-git-tree-sha1 = "eb7a1342ff77f4f9b6552605f27fd432745a53a3"
-uuid = "7d9f7c33-5ae7-4f3b-8dc6-eff91059b697"
-version = "0.1.22"
+project_hash = "8566ff675f567698b57c7fa9d9e520ddd3cfb510"
 
 [[deps.Adapt]]
 deps = ["LinearAlgebra"]
@@ -31,12 +19,6 @@
 uuid = "0dad84c5-d112-42e6-8d28-ef12dabb789f"
 version = "1.1.1"
 
-[[deps.ArrayInterface]]
-deps = ["ArrayInterfaceCore", "Compat", "IfElse", "LinearAlgebra", "Static"]
-git-tree-sha1 = "d6173480145eb632d6571c148d94b9d3d773820e"
-uuid = "4fba245c-0d91-5ea0-9b3e-6abc04ee57a9"
-version = "6.0.23"
-
 [[deps.ArrayInterfaceCore]]
 deps = ["LinearAlgebra", "SparseArrays", "SuiteSparse"]
 git-tree-sha1 = "c46fb7dd1d8ca1d213ba25848a5ec4e47a1a1b08"
@@ -46,18 +28,6 @@
 [[deps.Artifacts]]
 uuid = "56f22d72-fd6d-98f1-02f0-08ddc0907c33"
 
-[[deps.AssetRegistry]]
-deps = ["Distributed", "JSON", "Pidfile", "SHA", "Test"]
-git-tree-sha1 = "b25e88db7944f98789130d7b503276bc34bc098e"
-uuid = "bf4720bc-e11a-5d0c-854e-bdca1663c893"
-version = "0.1.0"
-
-[[deps.BFloat16s]]
-deps = ["LinearAlgebra", "Printf", "Random", "Test"]
-git-tree-sha1 = "a598ecb0d717092b5539dbbe890c98bac842b072"
-uuid = "ab4f0b2a-ad5b-11e8-123f-65d77653426b"
-version = "0.2.0"
-
 [[deps.BangBang]]
 deps = ["Compat", "ConstructionBase", "Future", "InitialValues", "LinearAlgebra", "Requires", "Setfield", "Tables", "ZygoteRules"]
 git-tree-sha1 = "7fe6d92c4f281cf4ca6f2fba0ce7b299742da7ca"
@@ -72,70 +42,18 @@
 uuid = "9718e550-a3fa-408a-8086-8db961cd8217"
 version = "0.1.1"
 
-[[deps.BinDeps]]
-deps = ["Libdl", "Pkg", "SHA", "URIParser", "Unicode"]
-git-tree-sha1 = "1289b57e8cf019aede076edab0587eb9644175bd"
-uuid = "9e28174c-4ba2-5203-b857-d8d62c4213ee"
-version = "1.0.2"
-
-[[deps.BlackBoxOptim]]
-deps = ["CPUTime", "Compat", "Distributed", "Distributions", "HTTP", "JSON", "LinearAlgebra", "Printf", "Random", "SpatialIndexing", "StatsBase"]
-git-tree-sha1 = "136079f37e3514ec691926093924b591a8842f5d"
-uuid = "a134a8b2-14d6-55f6-9291-3336d3ab0209"
-version = "0.6.2"
-
-[[deps.Blink]]
-deps = ["Base64", "BinDeps", "Distributed", "JSExpr", "JSON", "Lazy", "Logging", "MacroTools", "Mustache", "Mux", "Reexport", "Sockets", "WebIO", "WebSockets"]
-git-tree-sha1 = "08d0b679fd7caa49e2bca9214b131289e19808c0"
-uuid = "ad839575-38b3-5650-b840-f874b8c74a25"
-version = "0.12.5"
-
-[[deps.Bzip2_jll]]
-deps = ["Artifacts", "JLLWrappers", "Libdl", "Pkg"]
-git-tree-sha1 = "19a35467a82e236ff51bc17a3a44b69ef35185a2"
-uuid = "6e34b625-4abd-537c-b88f-471c36dfa7a0"
-version = "1.0.8+0"
-
-[[deps.CEnum]]
-git-tree-sha1 = "eb4cb44a499229b3b8426dcfb5dd85333951ff90"
-uuid = "fa961155-64e5-5f13-b03f-caf6b980ea82"
-version = "0.4.2"
-
-[[deps.CPUTime]]
-git-tree-sha1 = "2dcc50ea6a0a1ef6440d6eecd0fe3813e5671f45"
-uuid = "a9c8d775-2e2e-55fc-8582-045d282d599e"
-version = "1.0.0"
-
 [[deps.CSV]]
 deps = ["CodecZlib", "Dates", "FilePathsBase", "InlineStrings", "Mmap", "Parsers", "PooledArrays", "SentinelArrays", "Tables", "Unicode", "WeakRefStrings"]
 git-tree-sha1 = "c5fd7cd27ac4aed0acf4b73948f0110ff2a854b2"
 uuid = "336ed68f-0bac-5ca0-87d4-7b16caf5d00b"
 version = "0.10.7"
 
-[[deps.CUDA]]
-deps = ["AbstractFFTs", "Adapt", "BFloat16s", "CEnum", "CompilerSupportLibraries_jll", "ExprTools", "GPUArrays", "GPUCompiler", "LLVM", "LazyArtifacts", "Libdl", "LinearAlgebra", "Logging", "Printf", "Random", "Random123", "RandomNumbers", "Reexport", "Requires", "SparseArrays", "SpecialFunctions", "TimerOutputs"]
-git-tree-sha1 = "49549e2c28ffb9cc77b3689dc10e46e6271e9452"
-uuid = "052768ef-5323-5732-b1bb-66c8b64840ba"
-version = "3.12.0"
-
-[[deps.Cairo_jll]]
-deps = ["Artifacts", "Bzip2_jll", "Fontconfig_jll", "FreeType2_jll", "Glib_jll", "JLLWrappers", "LZO_jll", "Libdl", "Pixman_jll", "Pkg", "Xorg_libXext_jll", "Xorg_libXrender_jll", "Zlib_jll", "libpng_jll"]
-git-tree-sha1 = "4b859a208b2397a7a623a03449e4636bdb17bcf2"
-uuid = "83423d85-b0ee-5818-9007-b63ccbeb887a"
-version = "1.16.1+1"
-
 [[deps.Calculus]]
 deps = ["LinearAlgebra"]
 git-tree-sha1 = "f641eb0a4f00c343bbc32346e1217b86f3ce9dad"
 uuid = "49dc2e85-a5d0-5ad3-a950-438e2897f1b9"
 version = "0.5.1"
 
-[[deps.ChainRules]]
-deps = ["Adapt", "ChainRulesCore", "Compat", "Distributed", "GPUArraysCore", "IrrationalConstants", "LinearAlgebra", "Random", "RealDot", "SparseArrays", "Statistics", "StructArrays"]
-git-tree-sha1 = "0c8c8887763f42583e1206ee35413a43c91e2623"
-uuid = "082447d4-558c-5d27-93f4-14fc19e9eca2"
-version = "1.45.0"
-
 [[deps.ChainRulesCore]]
 deps = ["Compat", "LinearAlgebra", "SparseArrays"]
 git-tree-sha1 = "e7ff6cadf743c098e08fca25c91103ee4303c9bb"
@@ -148,42 +66,12 @@
 uuid = "9e997f8a-9a97-42d5-a9f1-ce6bfc15e2c0"
 version = "0.1.4"
 
-[[deps.CodeTracking]]
-deps = ["InteractiveUtils", "UUIDs"]
-git-tree-sha1 = "cc4bd91eba9cdbbb4df4746124c22c0832a460d6"
-uuid = "da1fd8a2-8d9e-5ec2-8556-3022fb5608a2"
-version = "1.1.1"
-
 [[deps.CodecZlib]]
 deps = ["TranscodingStreams", "Zlib_jll"]
 git-tree-sha1 = "ded953804d019afa9a3f98981d99b33e3db7b6da"
 uuid = "944b1d66-785c-5afd-91f1-9de20f533193"
 version = "0.7.0"
 
-[[deps.ColorSchemes]]
-deps = ["ColorTypes", "ColorVectorSpace", "Colors", "FixedPointNumbers", "Random"]
-git-tree-sha1 = "1fd869cc3875b57347f7027521f561cf46d1fcd8"
-uuid = "35d6a980-a343-548e-a6ea-1d62b119f2f4"
-version = "3.19.0"
-
-[[deps.ColorTypes]]
-deps = ["FixedPointNumbers", "Random"]
-git-tree-sha1 = "eb7f0f8307f71fac7c606984ea5fb2817275d6e4"
-uuid = "3da002f7-5984-5a60-b8a6-cbb66c0b333f"
-version = "0.11.4"
-
-[[deps.ColorVectorSpace]]
-deps = ["ColorTypes", "FixedPointNumbers", "LinearAlgebra", "SpecialFunctions", "Statistics", "TensorCore"]
-git-tree-sha1 = "d08c20eef1f2cbc6e60fd3612ac4340b89fea322"
-uuid = "c3611d14-8923-5661-9e6a-0046d554d3a4"
-version = "0.9.9"
-
-[[deps.Colors]]
-deps = ["ColorTypes", "FixedPointNumbers", "Reexport"]
-git-tree-sha1 = "417b0ed7b8b838aa6ca0a87aadf1bb9eb111ce40"
-uuid = "5ae59095-9a9b-59fe-a467-6f913c188581"
-version = "0.12.8"
-
 [[deps.Combinatorics]]
 git-tree-sha1 = "08c8b6831dc00bfea825826be0bc8336fc369860"
 uuid = "861a8166-3701-5b0c-9a16-15d98fcdc6aa"
@@ -216,29 +104,12 @@
 uuid = "a33af91c-f02d-484b-be07-31d278c5ca2b"
 version = "0.1.1"
 
-[[deps.Conda]]
-deps = ["Downloads", "JSON", "VersionParsing"]
-git-tree-sha1 = "6e47d11ea2776bc5627421d59cdcc1296c058071"
-uuid = "8f4d0f93-b110-5947-807f-2305c1781a2d"
-version = "1.7.0"
-
 [[deps.ConstructionBase]]
 deps = ["LinearAlgebra"]
 git-tree-sha1 = "fb21ddd70a051d882a1686a5a550990bbe371a95"
 uuid = "187b0558-2788-49d3-abe0-74a17ed4e7c9"
 version = "1.4.1"
 
-[[deps.ContextVariablesX]]
-deps = ["Compat", "Logging", "UUIDs"]
-git-tree-sha1 = "25cc3803f1030ab855e383129dcd3dc294e322cc"
-uuid = "6add18c4-b38d-439d-96f6-d6bc489c04c5"
-version = "0.1.3"
-
-[[deps.Contour]]
-git-tree-sha1 = "d05d9e7b7aedff4e5b51a029dced05cfb6125781"
-uuid = "d38c429a-6771-53c6-b99e-75d170b6e991"
-version = "0.6.2"
-
 [[deps.Crayons]]
 git-tree-sha1 = "249fe38abf76d48563e2f4556bebd215aa317e15"
 uuid = "a8cc5b0e-0ffa-5ad4-8c14-923d3ee1735f"
@@ -275,10 +146,6 @@
 uuid = "244e2a9f-e319-4986-a169-4d1fe445cd52"
 version = "0.1.2"
 
-[[deps.DelimitedFiles]]
-deps = ["Mmap"]
-uuid = "8bb1440f-4735-579b-a4ab-409b98df4dab"
-
 [[deps.DensityInterface]]
 deps = ["InverseFunctions", "Test"]
 git-tree-sha1 = "80c3e8639e3353e5d2912fb3a1916b8455e2494b"
@@ -324,41 +191,6 @@
 uuid = "fa6b7ba4-c1ee-5f82-b5fc-ecf0adba8f74"
 version = "0.6.8"
 
-[[deps.Expat_jll]]
-deps = ["Artifacts", "JLLWrappers", "Libdl", "Pkg"]
-git-tree-sha1 = "bad72f730e9e91c08d9427d5e8db95478a3c323d"
-uuid = "2e619515-83b5-522b-bb60-26c02a35a201"
-version = "2.4.8+0"
-
-[[deps.ExprTools]]
-git-tree-sha1 = "56559bbef6ca5ea0c0818fa5c90320398a6fbf8d"
-uuid = "e2ba6199-217a-4e67-a87a-7c52f15ade04"
-version = "0.1.8"
-
-[[deps.FFMPEG]]
-deps = ["FFMPEG_jll"]
-git-tree-sha1 = "b57e3acbe22f8484b4b5ff66a7499717fe1a9cc8"
-uuid = "c87230d0-a227-11e9-1b43-d7ebe4e7570a"
-version = "0.4.1"
-
-[[deps.FFMPEG_jll]]
-deps = ["Artifacts", "Bzip2_jll", "FreeType2_jll", "FriBidi_jll", "JLLWrappers", "LAME_jll", "Libdl", "Ogg_jll", "OpenSSL_jll", "Opus_jll", "PCRE2_jll", "Pkg", "Zlib_jll", "libaom_jll", "libass_jll", "libfdk_aac_jll", "libvorbis_jll", "x264_jll", "x265_jll"]
-git-tree-sha1 = "74faea50c1d007c85837327f6775bea60b5492dd"
-uuid = "b22a6f82-2f65-5046-a5b2-351ab43fb4e5"
-version = "4.4.2+2"
-
-[[deps.FLoops]]
-deps = ["BangBang", "Compat", "FLoopsBase", "InitialValues", "JuliaVariables", "MLStyle", "Serialization", "Setfield", "Transducers"]
-git-tree-sha1 = "ffb97765602e3cbe59a0589d237bf07f245a8576"
-uuid = "cc61a311-1640-44b5-9fba-1b764f453329"
-version = "0.2.1"
-
-[[deps.FLoopsBase]]
-deps = ["ContextVariablesX"]
-git-tree-sha1 = "656f7a6859be8673bf1f35da5670246b923964f7"
-uuid = "b9860ae5-e623-471e-878b-f6a53c775ea6"
-version = "0.1.1"
-
 [[deps.FilePathsBase]]
 deps = ["Compat", "Dates", "Mmap", "Printf", "Test", "UUIDs"]
 git-tree-sha1 = "e27c4ebe80e8699540f2d6c805cc12203b614f12"
@@ -380,30 +212,6 @@
 uuid = "6a86dc24-6348-571c-b903-95158fe2bd41"
 version = "2.16.0"
 
-[[deps.FixedPointNumbers]]
-deps = ["Statistics"]
-git-tree-sha1 = "335bfdceacc84c5cdf16aadc768aa5ddfc5383cc"
-uuid = "53c48c17-4a7d-5ca2-90c5-79b7896eea93"
-version = "0.8.4"
-
-[[deps.Flux]]
-deps = ["Adapt", "ArrayInterface", "CUDA", "ChainRulesCore", "Functors", "LinearAlgebra", "MLUtils", "MacroTools", "NNlib", "NNlibCUDA", "OneHotArrays", "Optimisers", "ProgressLogging", "Random", "Reexport", "SparseArrays", "SpecialFunctions", "Statistics", "StatsBase", "Test", "Zygote"]
-git-tree-sha1 = "66b62bf72c4b5d4904441ed0677eab53266033c7"
-uuid = "587475ba-b771-5e3f-ad9e-33799f191a9c"
-version = "0.13.7"
-
-[[deps.FoldsThreads]]
-deps = ["Accessors", "FunctionWrappers", "InitialValues", "SplittablesBase", "Transducers"]
-git-tree-sha1 = "eb8e1989b9028f7e0985b4268dabe94682249025"
-uuid = "9c68100b-dfe1-47cf-94c8-95104e173443"
-version = "0.1.1"
-
-[[deps.Fontconfig_jll]]
-deps = ["Artifacts", "Bzip2_jll", "Expat_jll", "FreeType2_jll", "JLLWrappers", "Libdl", "Libuuid_jll", "Pkg", "Zlib_jll"]
-git-tree-sha1 = "21efd19106a55620a188615da6d3d06cd7f6ee03"
-uuid = "a3f928ae-7b40-5064-980b-68af3947d34b"
-version = "2.13.93+0"
-
 [[deps.Formatting]]
 deps = ["Printf"]
 git-tree-sha1 = "8339d61043228fdd3eb658d86c926cb282ae72a8"
@@ -416,122 +224,10 @@
 uuid = "f6369f11-7733-5829-9624-2563aa707210"
 version = "0.10.33"
 
-[[deps.FreeType2_jll]]
-deps = ["Artifacts", "Bzip2_jll", "JLLWrappers", "Libdl", "Pkg", "Zlib_jll"]
-git-tree-sha1 = "87eb71354d8ec1a96d4a7636bd57a7347dde3ef9"
-uuid = "d7e528f0-a631-5988-bf34-fe36492bcfd7"
-version = "2.10.4+0"
-
-[[deps.FriBidi_jll]]
-deps = ["Artifacts", "JLLWrappers", "Libdl", "Pkg"]
-git-tree-sha1 = "aa31987c2ba8704e23c6c8ba8a4f769d5d7e4f91"
-uuid = "559328eb-81f9-559d-9380-de523a88c83c"
-version = "1.0.10+0"
-
-[[deps.FunctionWrappers]]
-git-tree-sha1 = "d62485945ce5ae9c0c48f124a84998d755bae00e"
-uuid = "069b7b12-0de2-55c6-9aab-29f3d0a68a2e"
-version = "1.1.3"
-
-[[deps.FunctionalCollections]]
-deps = ["Test"]
-git-tree-sha1 = "04cb9cfaa6ba5311973994fe3496ddec19b6292a"
-uuid = "de31a74c-ac4f-5751-b3fd-e18cd04993ca"
-version = "0.5.0"
-
-[[deps.Functors]]
-deps = ["LinearAlgebra"]
-git-tree-sha1 = "a2657dd0f3e8a61dbe70fc7c122038bd33790af5"
-uuid = "d9f16b24-f501-4c13-a1f2-28368ffc5196"
-version = "0.3.0"
-
 [[deps.Future]]
 deps = ["Random"]
 uuid = "9fa8497b-333b-5362-9e8d-4d0656e87820"
 
-[[deps.GLFW_jll]]
-deps = ["Artifacts", "JLLWrappers", "Libdl", "Libglvnd_jll", "Pkg", "Xorg_libXcursor_jll", "Xorg_libXi_jll", "Xorg_libXinerama_jll", "Xorg_libXrandr_jll"]
-git-tree-sha1 = "d972031d28c8c8d9d7b41a536ad7bb0c2579caca"
-uuid = "0656b61e-2033-5cc2-a64a-77c0f6c09b89"
-version = "3.3.8+0"
-
-[[deps.GPUArrays]]
-deps = ["Adapt", "GPUArraysCore", "LLVM", "LinearAlgebra", "Printf", "Random", "Reexport", "Serialization", "Statistics"]
-git-tree-sha1 = "45d7deaf05cbb44116ba785d147c518ab46352d7"
-uuid = "0c68f7d7-f131-5f86-a1c3-88cf8149b2d7"
-version = "8.5.0"
-
-[[deps.GPUArraysCore]]
-deps = ["Adapt"]
-git-tree-sha1 = "6872f5ec8fd1a38880f027a26739d42dcda6691f"
-uuid = "46192b85-c4d5-4398-a991-12ede77f4527"
-version = "0.1.2"
-
-[[deps.GPUCompiler]]
-deps = ["ExprTools", "InteractiveUtils", "LLVM", "Libdl", "Logging", "TimerOutputs", "UUIDs"]
-git-tree-sha1 = "76f70a337a153c1632104af19d29023dbb6f30dd"
-uuid = "61eb1bfa-7361-4325-ad38-22787b887f55"
-version = "0.16.6"
-
-[[deps.GR]]
-deps = ["Base64", "DelimitedFiles", "GR_jll", "HTTP", "JSON", "Libdl", "LinearAlgebra", "Pkg", "Preferences", "Printf", "Random", "Serialization", "Sockets", "Test", "UUIDs"]
-git-tree-sha1 = "00a9d4abadc05b9476e937a5557fcce476b9e547"
-uuid = "28b8d3ca-fb5f-59d9-8090-bfdbd6d07a71"
-version = "0.69.5"
-
-[[deps.GR_jll]]
-deps = ["Artifacts", "Bzip2_jll", "Cairo_jll", "FFMPEG_jll", "Fontconfig_jll", "GLFW_jll", "JLLWrappers", "JpegTurbo_jll", "Libdl", "Libtiff_jll", "Pixman_jll", "Pkg", "Qt5Base_jll", "Zlib_jll", "libpng_jll"]
-git-tree-sha1 = "bc9f7725571ddb4ab2c4bc74fa397c1c5ad08943"
-uuid = "d2c73de3-f751-5644-a686-071e5b155ba9"
-version = "0.69.1+0"
-
-[[deps.Gettext_jll]]
-deps = ["Artifacts", "CompilerSupportLibraries_jll", "JLLWrappers", "Libdl", "Libiconv_jll", "Pkg", "XML2_jll"]
-git-tree-sha1 = "9b02998aba7bf074d14de89f9d37ca24a1a0b046"
-uuid = "78b55507-aeef-58d4-861c-77aaff3498b1"
-version = "0.21.0+0"
-
-[[deps.Glib_jll]]
-deps = ["Artifacts", "Gettext_jll", "JLLWrappers", "Libdl", "Libffi_jll", "Libiconv_jll", "Libmount_jll", "PCRE2_jll", "Pkg", "Zlib_jll"]
-git-tree-sha1 = "fb83fbe02fe57f2c068013aa94bcdf6760d3a7a7"
-uuid = "7746bdde-850d-59dc-9ae8-88ece973131d"
-version = "2.74.0+1"
-
-[[deps.GoogleSheets]]
-deps = ["ColorTypes", "Colors", "DataFrames", "JSON", "MacroTools", "PyCall", "RateLimiter"]
-git-tree-sha1 = "4b43a991714f4c6212bef740612f77f3bd561831"
-uuid = "831f653e-6dbc-49a2-ac93-eebfaa09c6e6"
-version = "2.0.2"
-
-[[deps.Graphite2_jll]]
-deps = ["Artifacts", "JLLWrappers", "Libdl", "Pkg"]
-git-tree-sha1 = "344bf40dcab1073aca04aa0df4fb092f920e4011"
-uuid = "3b182d85-2403-5c21-9c21-1e1f0cc25472"
-version = "1.3.14+0"
-
-[[deps.Grisu]]
-git-tree-sha1 = "53bb909d1151e57e2484c3d1b53e19552b887fb2"
-uuid = "42e2da0e-8278-4e71-bc24-59509adca0fe"
-version = "1.0.2"
-
-[[deps.HTTP]]
-deps = ["Base64", "Dates", "IniFile", "Logging", "MbedTLS", "NetworkOptions", "Sockets", "URIs"]
-git-tree-sha1 = "0fa77022fe4b511826b39c894c90daf5fce3334a"
-uuid = "cd3eb016-35fb-5094-929b-558a96fad6f3"
-version = "0.9.17"
-
-[[deps.HarfBuzz_jll]]
-deps = ["Artifacts", "Cairo_jll", "Fontconfig_jll", "FreeType2_jll", "Glib_jll", "Graphite2_jll", "JLLWrappers", "Libdl", "Libffi_jll", "Pkg"]
-git-tree-sha1 = "129acf094d168394e80ee1dc4bc06ec835e510a3"
-uuid = "2e76f6c2-a576-52d4-95c1-20adfe4de566"
-version = "2.8.1+1"
-
-[[deps.Hiccup]]
-deps = ["MacroTools", "Test"]
-git-tree-sha1 = "6187bb2d5fcbb2007c39e7ac53308b0d371124bd"
-uuid = "9fb69e20-1954-56bb-a84f-559cc56a8ff7"
-version = "0.2.2"
-
 [[deps.HypergeometricFunctions]]
 deps = ["DualNumbers", "LinearAlgebra", "OpenLibm_jll", "SpecialFunctions", "Test"]
 git-tree-sha1 = "709d864e3ed6e3545230601f94e11ebc65994641"
@@ -544,22 +240,6 @@
 uuid = "09f84164-cd44-5f33-b23f-e6b0d136a0d5"
 version = "0.10.11"
 
-[[deps.IRTools]]
-deps = ["InteractiveUtils", "MacroTools", "Test"]
-git-tree-sha1 = "2e99184fca5eb6f075944b04c22edec29beb4778"
-uuid = "7869d1d1-7146-5819-86e3-90919afe41df"
-version = "0.4.7"
-
-[[deps.IfElse]]
-git-tree-sha1 = "debdd00ffef04665ccbb3e150747a77560e8fad1"
-uuid = "615f187c-cbe4-4ef1-ba3b-2fcf58d6d173"
-version = "0.1.1"
-
-[[deps.IniFile]]
-git-tree-sha1 = "f550e6e32074c939295eb5ea6de31849ac2c9625"
-uuid = "83e8ac13-25f8-5344-8a64-a9f2b223428f"
-version = "0.5.1"
-
 [[deps.InitialValues]]
 git-tree-sha1 = "4da0f88e9a39111c2fa3add390ab15f3a44f3ca3"
 uuid = "22cec73e-a1b8-11e9-2c92-598750a2cf9c"
@@ -596,105 +276,17 @@
 uuid = "82899510-4779-5014-852e-03e436cf321d"
 version = "1.0.0"
 
-[[deps.JLFzf]]
-deps = ["Pipe", "REPL", "Random", "fzf_jll"]
-git-tree-sha1 = "f377670cda23b6b7c1c0b3893e37451c5c1a2185"
-uuid = "1019f520-868f-41f5-a6de-eb00f4b6a39c"
-version = "0.1.5"
-
 [[deps.JLLWrappers]]
 deps = ["Preferences"]
 git-tree-sha1 = "abc9885a7ca2052a736a600f7fa66209f96506e1"
 uuid = "692b3bcd-3c85-4b1f-b108-f13ce0eb3210"
 version = "1.4.1"
 
-[[deps.JSExpr]]
-deps = ["JSON", "MacroTools", "Observables", "WebIO"]
-git-tree-sha1 = "b413a73785b98474d8af24fd4c8a975e31df3658"
-uuid = "97c1335a-c9c5-57fe-bc5d-ec35cebe8660"
-version = "0.5.4"
-
-[[deps.JSON]]
-deps = ["Dates", "Mmap", "Parsers", "Unicode"]
-git-tree-sha1 = "3c837543ddb02250ef42f4738347454f95079d4e"
-uuid = "682c06a0-de6a-54ab-a142-c8b1cf79cde6"
-version = "0.21.3"
-
-[[deps.JpegTurbo_jll]]
-deps = ["Artifacts", "JLLWrappers", "Libdl", "Pkg"]
-git-tree-sha1 = "b53380851c6e6664204efb2e62cd24fa5c47e4ba"
-uuid = "aacddb02-875f-59d6-b918-886e6ef4fbf8"
-version = "2.1.2+0"
-
-[[deps.JuliaInterpreter]]
-deps = ["CodeTracking", "InteractiveUtils", "Random", "UUIDs"]
-git-tree-sha1 = "0f960b1404abb0b244c1ece579a0ec78d056a5d1"
-uuid = "aa1ae85d-cabe-5617-a682-6adf51b2e16a"
-version = "0.9.15"
-
-[[deps.JuliaVariables]]
-deps = ["MLStyle", "NameResolution"]
-git-tree-sha1 = "49fb3cb53362ddadb4415e9b73926d6b40709e70"
-uuid = "b14d175d-62b4-44ba-8fb7-3064adc8c3ec"
-version = "0.2.4"
-
-[[deps.Kaleido_jll]]
-deps = ["Artifacts", "JLLWrappers", "Libdl", "Pkg"]
-git-tree-sha1 = "43032da5832754f58d14a91ffbe86d5f176acda9"
-uuid = "f7e6163d-2fa5-5f23-b69c-1db539e41963"
-version = "0.2.1+0"
-
-[[deps.LAME_jll]]
-deps = ["Artifacts", "JLLWrappers", "Libdl", "Pkg"]
-git-tree-sha1 = "f6250b16881adf048549549fba48b1161acdac8c"
-uuid = "c1c5ebd0-6772-5130-a774-d5fcae4a789d"
-version = "3.100.1+0"
-
-[[deps.LERC_jll]]
-deps = ["Artifacts", "JLLWrappers", "Libdl", "Pkg"]
-git-tree-sha1 = "bf36f528eec6634efc60d7ec062008f171071434"
-uuid = "88015f11-f218-50d7-93a8-a6af411a945d"
-version = "3.0.0+1"
-
-[[deps.LLVM]]
-deps = ["CEnum", "LLVMExtra_jll", "Libdl", "Printf", "Unicode"]
-git-tree-sha1 = "e7e9184b0bf0158ac4e4aa9daf00041b5909bf1a"
-uuid = "929cbde3-209d-540e-8aea-75f648917ca0"
-version = "4.14.0"
-
-[[deps.LLVMExtra_jll]]
-deps = ["Artifacts", "JLLWrappers", "LazyArtifacts", "Libdl", "Pkg", "TOML"]
-git-tree-sha1 = "771bfe376249626d3ca12bcd58ba243d3f961576"
-uuid = "dad2f222-ce93-54a1-a47d-0025e8a3acab"
-version = "0.0.16+0"
-
-[[deps.LZO_jll]]
-deps = ["Artifacts", "JLLWrappers", "Libdl", "Pkg"]
-git-tree-sha1 = "e5b909bcf985c5e2605737d2ce278ed791b89be6"
-uuid = "dd4b983a-f0e5-5f8d-a1b7-129d4a5fb1ac"
-version = "2.10.1+0"
-
 [[deps.LaTeXStrings]]
 git-tree-sha1 = "f2355693d6778a178ade15952b7ac47a4ff97996"
 uuid = "b964fa9f-0449-5b57-a5c2-d3ea65f4040f"
 version = "1.3.0"
 
-[[deps.Latexify]]
-deps = ["Formatting", "InteractiveUtils", "LaTeXStrings", "MacroTools", "Markdown", "OrderedCollections", "Printf", "Requires"]
-git-tree-sha1 = "ab9aa169d2160129beb241cb2750ca499b4e90e9"
-uuid = "23fbe1c1-3f47-55db-b15f-69d7ec21a316"
-version = "0.15.17"
-
-[[deps.Lazy]]
-deps = ["MacroTools"]
-git-tree-sha1 = "1370f8202dac30758f3c345f9909b97f53d87d3f"
-uuid = "50d2b5c4-7a5e-59d5-8109-a42b560f39c0"
-version = "0.15.1"
-
-[[deps.LazyArtifacts]]
-deps = ["Artifacts", "Pkg"]
-uuid = "4af54fe1-eca0-43a8-85a7-787d91b784e3"
-
 [[deps.LibCURL]]
 deps = ["LibCURL_jll", "MozillaCACerts_jll"]
 uuid = "b27032c2-a3e7-50c8-80cd-2d36dbcbfd21"
@@ -717,54 +309,6 @@
 [[deps.Libdl]]
 uuid = "8f399da3-3557-5675-b5ff-fb832c97cbdb"
 
-[[deps.Libffi_jll]]
-deps = ["Artifacts", "JLLWrappers", "Libdl", "Pkg"]
-git-tree-sha1 = "0b4a5d71f3e5200a7dff793393e09dfc2d874290"
-uuid = "e9f186c6-92d2-5b65-8a66-fee21dc1b490"
-version = "3.2.2+1"
-
-[[deps.Libgcrypt_jll]]
-deps = ["Artifacts", "JLLWrappers", "Libdl", "Libgpg_error_jll", "Pkg"]
-git-tree-sha1 = "64613c82a59c120435c067c2b809fc61cf5166ae"
-uuid = "d4300ac3-e22c-5743-9152-c294e39db1e4"
-version = "1.8.7+0"
-
-[[deps.Libglvnd_jll]]
-deps = ["Artifacts", "JLLWrappers", "Libdl", "Pkg", "Xorg_libX11_jll", "Xorg_libXext_jll"]
-git-tree-sha1 = "7739f837d6447403596a75d19ed01fd08d6f56bf"
-uuid = "7e76a0d4-f3c7-5321-8279-8d96eeed0f29"
-version = "1.3.0+3"
-
-[[deps.Libgpg_error_jll]]
-deps = ["Artifacts", "JLLWrappers", "Libdl", "Pkg"]
-git-tree-sha1 = "c333716e46366857753e273ce6a69ee0945a6db9"
-uuid = "7add5ba3-2f88-524e-9cd5-f83b8a55f7b8"
-version = "1.42.0+0"
-
-[[deps.Libiconv_jll]]
-deps = ["Artifacts", "JLLWrappers", "Libdl", "Pkg"]
-git-tree-sha1 = "42b62845d70a619f063a7da093d995ec8e15e778"
-uuid = "94ce4f54-9a6c-5748-9c1c-f9c7231a4531"
-version = "1.16.1+1"
-
-[[deps.Libmount_jll]]
-deps = ["Artifacts", "JLLWrappers", "Libdl", "Pkg"]
-git-tree-sha1 = "9c30530bf0effd46e15e0fdcf2b8636e78cbbd73"
-uuid = "4b2f31a3-9ecc-558c-b454-b3730dcb73e9"
-version = "2.35.0+0"
-
-[[deps.Libtiff_jll]]
-deps = ["Artifacts", "JLLWrappers", "JpegTurbo_jll", "LERC_jll", "Libdl", "Pkg", "Zlib_jll", "Zstd_jll"]
-git-tree-sha1 = "3eb79b0ca5764d4799c06699573fd8f533259713"
-uuid = "89763e89-9b03-5906-acba-b20f662cd828"
-version = "4.4.0+0"
-
-[[deps.Libuuid_jll]]
-deps = ["Artifacts", "JLLWrappers", "Libdl", "Pkg"]
-git-tree-sha1 = "7f3efec06033682db852f8b3bc3c1d2b0a0ab066"
-uuid = "38a345b3-de98-5d2b-a5d3-14cd9215e700"
-version = "2.36.0+0"
-
 [[deps.LineSearches]]
 deps = ["LinearAlgebra", "NLSolversBase", "NaNMath", "Parameters", "Printf"]
 git-tree-sha1 = "7bbea35cec17305fc70a0e5b4641477dc0789d9d"
@@ -784,23 +328,6 @@
 [[deps.Logging]]
 uuid = "56ddb016-857b-54e1-b83d-db4d58db5568"
 
-[[deps.LoweredCodeUtils]]
-deps = ["JuliaInterpreter"]
-git-tree-sha1 = "dedbebe234e06e1ddad435f5c6f4b85cd8ce55f7"
-uuid = "6f1432cf-f94c-5a45-995e-cdbf5db27b0b"
-version = "2.2.2"
-
-[[deps.MLStyle]]
-git-tree-sha1 = "43f9be9c281179fe44205e2dc19f22e71e022d41"
-uuid = "d8e11817-5142-5d16-987a-aa16d5891078"
-version = "0.4.15"
-
-[[deps.MLUtils]]
-deps = ["ChainRulesCore", "DelimitedFiles", "FLoops", "FoldsThreads", "Random", "ShowCases", "Statistics", "StatsBase", "Transducers"]
-git-tree-sha1 = "824e9dfc7509cab1ec73ba77b55a916bb2905e26"
-uuid = "f1d291b0-491e-4a28-83b9-f70985020b54"
-version = "0.2.11"
-
 [[deps.MacroTools]]
 deps = ["Markdown", "Random"]
 git-tree-sha1 = "42324d08725e200c23d4dfb549e0d5d89dede2d2"
@@ -811,22 +338,11 @@
 deps = ["Base64"]
 uuid = "d6f4376e-aef5-505a-96c1-9c027394607a"
 
-[[deps.MbedTLS]]
-deps = ["Dates", "MbedTLS_jll", "MozillaCACerts_jll", "Random", "Sockets"]
-git-tree-sha1 = "03a9b9718f5682ecb107ac9f7308991db4ce395b"
-uuid = "739be429-bea8-5141-9913-cc70e7f3736d"
-version = "1.1.7"
-
 [[deps.MbedTLS_jll]]
 deps = ["Artifacts", "Libdl"]
 uuid = "c8ffd9c3-330d-5841-b78e-0817d7145fa1"
 version = "2.28.0+0"
 
-[[deps.Measures]]
-git-tree-sha1 = "e498ddeee6f9fdb4551ce855a46f54dbd900245f"
-uuid = "442fdcdd-2543-5da2-b0f3-8c86c306513e"
-version = "0.3.1"
-
 [[deps.MicroCollections]]
 deps = ["BangBang", "InitialValues", "Setfield"]
 git-tree-sha1 = "4d5917a26ca33c66c8e5ca3247bd163624d35493"
@@ -846,69 +362,22 @@
 uuid = "14a3606d-f60d-562e-9121-12d972cd8159"
 version = "2022.2.1"
 
-[[deps.Mustache]]
-deps = ["Printf", "Tables"]
-git-tree-sha1 = "1e566ae913a57d0062ff1af54d2697b9344b99cd"
-uuid = "ffc61752-8dc7-55ee-8c37-f3e9cdd09e70"
-version = "1.0.14"
-
-[[deps.Mux]]
-deps = ["AssetRegistry", "Base64", "HTTP", "Hiccup", "Pkg", "Sockets", "WebSockets"]
-git-tree-sha1 = "82dfb2cead9895e10ee1b0ca37a01088456c4364"
-uuid = "a975b10e-0019-58db-a62f-e48ff68538c9"
-version = "0.7.6"
-
 [[deps.NLSolversBase]]
 deps = ["DiffResults", "Distributed", "FiniteDiff", "ForwardDiff"]
 git-tree-sha1 = "50310f934e55e5ca3912fb941dec199b49ca9b68"
 uuid = "d41bc354-129a-5804-8e4c-c37616107c6c"
 version = "7.8.2"
 
-[[deps.NNlib]]
-deps = ["Adapt", "ChainRulesCore", "LinearAlgebra", "Pkg", "Requires", "Statistics"]
-git-tree-sha1 = "00bcfcea7b2063807fdcab2e0ce86ef00b8b8000"
-uuid = "872c559c-99b0-510c-b3b7-b6c96a88d5cd"
-version = "0.8.10"
-
-[[deps.NNlibCUDA]]
-deps = ["Adapt", "CUDA", "LinearAlgebra", "NNlib", "Random", "Statistics"]
-git-tree-sha1 = "4429261364c5ea5b7308aecaa10e803ace101631"
-uuid = "a00861dc-f156-4864-bf3c-e6376f28a68d"
-version = "0.2.4"
-
 [[deps.NaNMath]]
 deps = ["OpenLibm_jll"]
 git-tree-sha1 = "a7c3d1da1189a1c2fe843a3bfa04d18d20eb3211"
 uuid = "77ba4419-2d1f-58cd-9bb1-8ffee604a2e3"
 version = "1.0.1"
 
-[[deps.NameResolution]]
-deps = ["PrettyPrint"]
-git-tree-sha1 = "1a0fa0e9613f46c9b8c11eee38ebb4f590013c5e"
-uuid = "71a1bf82-56d0-4bbc-8a3c-48b961074391"
-version = "0.1.5"
-
 [[deps.NetworkOptions]]
 uuid = "ca575930-c2e3-43a9-ace4-1e988b2c1908"
 version = "1.2.0"
 
-[[deps.Observables]]
-git-tree-sha1 = "6862738f9796b3edc1c09d0890afce4eca9e7e93"
-uuid = "510215fc-4207-5dde-b226-833fc4488ee2"
-version = "0.5.4"
-
-[[deps.Ogg_jll]]
-deps = ["Artifacts", "JLLWrappers", "Libdl", "Pkg"]
-git-tree-sha1 = "887579a3eb005446d514ab7aeac5d1d027658b8f"
-uuid = "e7412a2a-1a6e-54c0-be00-318e2571c051"
-version = "1.3.5+1"
-
-[[deps.OneHotArrays]]
-deps = ["Adapt", "ChainRulesCore", "GPUArrays", "LinearAlgebra", "MLUtils", "NNlib"]
-git-tree-sha1 = "2f6efe2f76d57a0ee67cb6eff49b4d02fccbd175"
-uuid = "0b1bfda6-eb8a-41d2-88d8-f5af5cad476f"
-version = "0.1.0"
-
 [[deps.OpenBLAS_jll]]
 deps = ["Artifacts", "CompilerSupportLibraries_jll", "Libdl"]
 uuid = "4536629a-c528-5b80-bd46-f80d51c5b363"
@@ -919,12 +388,6 @@
 uuid = "05823500-19ac-5b8b-9628-191a04bc5112"
 version = "0.8.1+0"
 
-[[deps.OpenSSL_jll]]
-deps = ["Artifacts", "JLLWrappers", "Libdl", "Pkg"]
-git-tree-sha1 = "f6e9dba33f9f2c44e08a020b0caf6903be540004"
-uuid = "458c3c95-2e84-50aa-8efc-19380b2a3a95"
-version = "1.1.19+0"
-
 [[deps.OpenSpecFun_jll]]
 deps = ["Artifacts", "CompilerSupportLibraries_jll", "JLLWrappers", "Libdl", "Pkg"]
 git-tree-sha1 = "13652491f6856acfd2db29360e1bbcd4565d04f1"
@@ -937,28 +400,11 @@
 uuid = "429524aa-4258-5aef-a3af-852621145aeb"
 version = "1.7.3"
 
-[[deps.Optimisers]]
-deps = ["ChainRulesCore", "Functors", "LinearAlgebra", "Random", "Statistics"]
-git-tree-sha1 = "8a9102cb805df46fc3d6effdc2917f09b0215c0b"
-uuid = "3bd65402-5787-11e9-1adc-39752487f4e2"
-version = "0.2.10"
-
-[[deps.Opus_jll]]
-deps = ["Artifacts", "JLLWrappers", "Libdl", "Pkg"]
-git-tree-sha1 = "51a08fb14ec28da2ec7a927c4337e4332c2a4720"
-uuid = "91d4177d-7536-5919-b921-800302f37372"
-version = "1.3.2+0"
-
 [[deps.OrderedCollections]]
 git-tree-sha1 = "85f8e6578bf1f9ee0d11e7bb1b1456435479d47c"
 uuid = "bac558e1-5e72-5ebc-8fee-abe8a469f55d"
 version = "1.4.1"
 
-[[deps.PCRE2_jll]]
-deps = ["Artifacts", "Libdl"]
-uuid = "efcefdf7-47ab-520b-bdef-62a2eaa19f15"
-version = "10.40.0+0"
-
 [[deps.PDMats]]
 deps = ["LinearAlgebra", "SparseArrays", "SuiteSparse"]
 git-tree-sha1 = "cf494dca75a69712a72b80bc48f59dcf3dea63ec"
@@ -977,58 +423,11 @@
 uuid = "69de0a69-1ddd-5017-9359-2bf0b02dc9f0"
 version = "2.5.0"
 
-[[deps.Pidfile]]
-deps = ["FileWatching", "Test"]
-git-tree-sha1 = "2d8aaf8ee10df53d0dfb9b8ee44ae7c04ced2b03"
-uuid = "fa939f87-e72e-5be4-a000-7fc836dbe307"
-version = "1.3.0"
-
-[[deps.Pipe]]
-git-tree-sha1 = "6842804e7867b115ca9de748a0cf6b364523c16d"
-uuid = "b98c9c47-44ae-5843-9183-064241ee97a0"
-version = "1.3.0"
-
-[[deps.Pixman_jll]]
-deps = ["Artifacts", "JLLWrappers", "Libdl", "Pkg"]
-git-tree-sha1 = "b4f5d02549a10e20780a24fce72bea96b6329e29"
-uuid = "30392449-352a-5448-841d-b1acce4e97dc"
-version = "0.40.1+0"
-
 [[deps.Pkg]]
 deps = ["Artifacts", "Dates", "Downloads", "LibGit2", "Libdl", "Logging", "Markdown", "Printf", "REPL", "Random", "SHA", "Serialization", "TOML", "Tar", "UUIDs", "p7zip_jll"]
 uuid = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f"
 version = "1.8.0"
 
-[[deps.PlotThemes]]
-deps = ["PlotUtils", "Statistics"]
-git-tree-sha1 = "1f03a2d339f42dca4a4da149c7e15e9b896ad899"
-uuid = "ccf2f8ad-2431-5c83-bf29-c5338b663b6a"
-version = "3.1.0"
-
-[[deps.PlotUtils]]
-deps = ["ColorSchemes", "Colors", "Dates", "Printf", "Random", "Reexport", "SnoopPrecompile", "Statistics"]
-git-tree-sha1 = "21303256d239f6b484977314674aef4bb1fe4420"
-uuid = "995b91a9-d308-5afd-9ec6-746e21dbc043"
-version = "1.3.1"
-
-[[deps.PlotlyBase]]
-deps = ["ColorSchemes", "Dates", "DelimitedFiles", "DocStringExtensions", "JSON", "LaTeXStrings", "Logging", "Parameters", "Pkg", "REPL", "Requires", "Statistics", "UUIDs"]
-git-tree-sha1 = "56baf69781fc5e61607c3e46227ab17f7040ffa2"
-uuid = "a03496cd-edff-5a9b-9e67-9cda94a718b5"
-version = "0.8.19"
-
-[[deps.PlotlyJS]]
-deps = ["Base64", "Blink", "DelimitedFiles", "JSExpr", "JSON", "Kaleido_jll", "Markdown", "Pkg", "PlotlyBase", "REPL", "Reexport", "Requires", "WebIO"]
-git-tree-sha1 = "7452869933cd5af22f59557390674e8679ab2338"
-uuid = "f0f68f2c-4968-5e81-91da-67840de0976a"
-version = "0.18.10"
-
-[[deps.Plots]]
-deps = ["Base64", "Contour", "Dates", "Downloads", "FFMPEG", "FixedPointNumbers", "GR", "JLFzf", "JSON", "LaTeXStrings", "Latexify", "LinearAlgebra", "Measures", "NaNMath", "Pkg", "PlotThemes", "PlotUtils", "Printf", "REPL", "Random", "RecipesBase", "RecipesPipeline", "Reexport", "RelocatableFolders", "Requires", "Scratch", "Showoff", "SnoopPrecompile", "SparseArrays", "Statistics", "StatsBase", "UUIDs", "UnicodeFun", "Unzip"]
-git-tree-sha1 = "47e70b391ff314cc36e7c2400f7d2c5455dc9496"
-uuid = "91a5bcdd-55d7-5caf-9e0b-520d859cae80"
-version = "1.36.1"
-
 [[deps.PooledArrays]]
 deps = ["DataAPI", "Future"]
 git-tree-sha1 = "a6062fe4063cdafe78f4a0a81cfffb89721b30e7"
@@ -1047,11 +446,6 @@
 uuid = "21216c6a-2e73-6563-6e65-726566657250"
 version = "1.3.0"
 
-[[deps.PrettyPrint]]
-git-tree-sha1 = "632eb4abab3449ab30c5e1afaa874f0b98b586e4"
-uuid = "8162dcfd-2161-5ef2-ae6c-7681170c5f98"
-version = "0.2.0"
-
 [[deps.PrettyTables]]
 deps = ["Crayons", "Formatting", "LaTeXStrings", "Markdown", "Reexport", "StringManipulation", "Tables"]
 git-tree-sha1 = "98ac42c9127667c2731072464fcfef9b819ce2fa"
@@ -1062,24 +456,6 @@
 deps = ["Unicode"]
 uuid = "de0858da-6303-5e67-8744-51eddeeeb8d7"
 
-[[deps.ProgressLogging]]
-deps = ["Logging", "SHA", "UUIDs"]
-git-tree-sha1 = "80d919dee55b9c50e8d9e2da5eeafff3fe58b539"
-uuid = "33c8b6b6-d38a-422a-b730-caa89a2f386c"
-version = "0.1.4"
-
-[[deps.PyCall]]
-deps = ["Conda", "Dates", "Libdl", "LinearAlgebra", "MacroTools", "Serialization", "VersionParsing"]
-git-tree-sha1 = "53b8b07b721b77144a0fbbbc2675222ebf40a02d"
-uuid = "438e738f-606a-5dbb-bf0a-cddfbfd45ab0"
-version = "1.94.1"
-
-[[deps.Qt5Base_jll]]
-deps = ["Artifacts", "CompilerSupportLibraries_jll", "Fontconfig_jll", "Glib_jll", "JLLWrappers", "Libdl", "Libglvnd_jll", "OpenSSL_jll", "Pkg", "Xorg_libXext_jll", "Xorg_libxcb_jll", "Xorg_xcb_util_image_jll", "Xorg_xcb_util_keysyms_jll", "Xorg_xcb_util_renderutil_jll", "Xorg_xcb_util_wm_jll", "Zlib_jll", "xkbcommon_jll"]
-git-tree-sha1 = "0c03844e2231e12fda4d0086fd7cbe4098ee8dc5"
-uuid = "ea2cea3b-5b76-57ae-a6ef-0a8af62496e1"
-version = "5.15.3+2"
-
 [[deps.QuadGK]]
 deps = ["DataStructures", "LinearAlgebra"]
 git-tree-sha1 = "97aa253e65b784fd13e83774cadc95b38011d734"
@@ -1094,65 +470,17 @@
 deps = ["SHA", "Serialization"]
 uuid = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
 
-[[deps.Random123]]
-deps = ["Random", "RandomNumbers"]
-git-tree-sha1 = "7a1a306b72cfa60634f03a911405f4e64d1b718b"
-uuid = "74087812-796a-5b5d-8853-05524746bad3"
-version = "1.6.0"
-
-[[deps.RandomNumbers]]
-deps = ["Random", "Requires"]
-git-tree-sha1 = "043da614cc7e95c703498a491e2c21f58a2b8111"
-uuid = "e6cf234a-135c-5ec9-84dd-332b85af5143"
-version = "1.5.3"
-
-[[deps.RateLimiter]]
-deps = ["Dates"]
-git-tree-sha1 = "a3912eca5eab1506d6c1f194f4b04d7293fbb816"
-uuid = "24b76ae0-009b-4410-993f-7a05b90d7239"
-version = "0.1.3"
-
-[[deps.RealDot]]
-deps = ["LinearAlgebra"]
-git-tree-sha1 = "9f0a1b71baaf7650f4fa8a1d168c7fb6ee41f0c9"
-uuid = "c1ae055f-0cd5-4b69-90a6-9a35b1a98df9"
-version = "0.1.0"
-
-[[deps.RecipesBase]]
-deps = ["SnoopPrecompile"]
-git-tree-sha1 = "d12e612bba40d189cead6ff857ddb67bd2e6a387"
-uuid = "3cdcf5f2-1ef4-517c-9805-6587b60abb01"
-version = "1.3.1"
-
-[[deps.RecipesPipeline]]
-deps = ["Dates", "NaNMath", "PlotUtils", "RecipesBase", "SnoopPrecompile"]
-git-tree-sha1 = "a030182cccc5c461386c6f055c36ab8449ef1340"
-uuid = "01d81517-befc-4cb6-b9ec-a95719d0359c"
-version = "0.6.10"
-
 [[deps.Reexport]]
 git-tree-sha1 = "45e428421666073eab6f2da5c9d310d99bb12f9b"
 uuid = "189a3867-3050-52da-a836-e630ba90ab69"
 version = "1.2.2"
 
-[[deps.RelocatableFolders]]
-deps = ["SHA", "Scratch"]
-git-tree-sha1 = "90bc7a7c96410424509e4263e277e43250c05691"
-uuid = "05181044-ff0b-4ac5-8273-598c1e38db00"
-version = "1.0.0"
-
 [[deps.Requires]]
 deps = ["UUIDs"]
 git-tree-sha1 = "838a3a4188e2ded87a4f9f184b4b0d78a1e91cb7"
 uuid = "ae029012-a4dd-5104-9daa-d747884805df"
 version = "1.3.0"
 
-[[deps.Revise]]
-deps = ["CodeTracking", "Distributed", "FileWatching", "JuliaInterpreter", "LibGit2", "LoweredCodeUtils", "OrderedCollections", "Pkg", "REPL", "Requires", "UUIDs", "Unicode"]
-git-tree-sha1 = "dad726963ecea2d8a81e26286f625aee09a91b7c"
-uuid = "295af30f-e4ad-537b-8983-00126c2a3abe"
-version = "3.4.0"
-
 [[deps.Rmath]]
 deps = ["Random", "Rmath_jll"]
 git-tree-sha1 = "bf3188feca147ce108c76ad82c2792c57abe7b1f"
@@ -1175,12 +503,6 @@
 uuid = "ea8e919c-243c-51af-8825-aaa63cd721ce"
 version = "0.7.0"
 
-[[deps.Scratch]]
-deps = ["Dates"]
-git-tree-sha1 = "f94f779c94e58bf9ea243e77a37e16d9de9126bd"
-uuid = "6c6a2e73-6563-6170-7368-637461726353"
-version = "1.1.1"
-
 [[deps.SentinelArrays]]
 deps = ["Dates", "Random"]
 git-tree-sha1 = "efd23b378ea5f2db53a55ae53d3133de4e080aa9"
@@ -1196,17 +518,6 @@
 uuid = "efcf1570-3423-57d1-acb7-fd33fddbac46"
 version = "1.1.1"
 
-[[deps.ShowCases]]
-git-tree-sha1 = "7f534ad62ab2bd48591bdeac81994ea8c445e4a5"
-uuid = "605ecd9f-84a6-4c9e-81e2-4798472b76a3"
-version = "0.1.0"
-
-[[deps.Showoff]]
-deps = ["Dates", "Grisu"]
-git-tree-sha1 = "91eddf657aca81df9ae6ceb20b959ae5653ad1de"
-uuid = "992d4aef-0814-514b-bc4d-f2e9a6c4116f"
-version = "1.0.3"
-
 [[deps.SnoopPrecompile]]
 git-tree-sha1 = "f604441450a3c0569830946e5b33b78c928e1a85"
 uuid = "66db9d55-30c0-4569-8b51-7e840670fc0c"
@@ -1225,11 +536,6 @@
 deps = ["LinearAlgebra", "Random"]
 uuid = "2f01184e-e22b-5df5-ae63-d93ebab69eaf"
 
-[[deps.SpatialIndexing]]
-git-tree-sha1 = "fb7041e6bd266266fa7cdeb80427579e55275e4f"
-uuid = "d4ead438-fe20-5cc5-a293-4fd39a41b74c"
-version = "0.1.3"
-
 [[deps.SpecialFunctions]]
 deps = ["ChainRulesCore", "IrrationalConstants", "LogExpFunctions", "OpenLibm_jll", "OpenSpecFun_jll"]
 git-tree-sha1 = "d75bda01f8c31ebb72df80a46c88b25d1c79c56d"
@@ -1242,12 +548,6 @@
 uuid = "171d559e-b47b-412a-8079-5efa626c420e"
 version = "0.1.15"
 
-[[deps.Static]]
-deps = ["IfElse"]
-git-tree-sha1 = "03170c1e8a94732c1d835ce4c5b904b4b52cba1c"
-uuid = "aedffcd0-7271-4cad-89d0-dc628f76c6d3"
-version = "0.7.8"
-
 [[deps.StaticArrays]]
 deps = ["LinearAlgebra", "Random", "StaticArraysCore", "Statistics"]
 git-tree-sha1 = "f86b3a049e5d05227b10e15dbb315c5b90f14988"
@@ -1286,12 +586,6 @@
 uuid = "892a3eda-7b42-436c-8928-eab12a02cf0e"
 version = "0.3.0"
 
-[[deps.StructArrays]]
-deps = ["Adapt", "DataAPI", "StaticArraysCore", "Tables"]
-git-tree-sha1 = "13237798b407150a6d2e2bce5d793d7d9576e99e"
-uuid = "09ab397b-f2b6-538f-b94a-2f83cf4a842a"
-version = "0.6.13"
-
 [[deps.SuiteSparse]]
 deps = ["Libdl", "LinearAlgebra", "Serialization", "SparseArrays"]
 uuid = "4607b0f0-06f3-5cda-b6b1-a6196a1729e9"
@@ -1318,22 +612,10 @@
 uuid = "a4e569a6-e804-4fa4-b0f3-eef7a1d5b13e"
 version = "1.10.1"
 
-[[deps.TensorCore]]
-deps = ["LinearAlgebra"]
-git-tree-sha1 = "1feb45f88d133a655e001435632f019a9a1bcdb6"
-uuid = "62fd8b95-f654-4bbd-a8a5-9c27f68ccd50"
-version = "0.1.1"
-
 [[deps.Test]]
 deps = ["InteractiveUtils", "Logging", "Random", "Serialization"]
 uuid = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
 
-[[deps.TimerOutputs]]
-deps = ["ExprTools", "Printf"]
-git-tree-sha1 = "f2fd3f288dfc6f507b0c3a2eb3bac009251e548b"
-uuid = "a759f4b9-e2f1-59dc-863e-4aeb61b1ea8f"
-version = "0.5.22"
-
 [[deps.TranscodingStreams]]
 deps = ["Random", "Test"]
 git-tree-sha1 = "8a75929dcd3c38611db2f8d08546decb514fcadf"
@@ -1346,17 +628,6 @@
 uuid = "28d57a85-8fef-5791-bfe6-a80928e7c999"
 version = "0.4.74"
 
-[[deps.URIParser]]
-deps = ["Unicode"]
-git-tree-sha1 = "53a9f49546b8d2dd2e688d216421d050c9a31d0d"
-uuid = "30578b45-9adc-5946-b283-645ec420af67"
-version = "0.4.1"
-
-[[deps.URIs]]
-git-tree-sha1 = "e59ecc5a41b000fa94423a578d29290c7266fc10"
-uuid = "5c2747f8-b7ea-4ff2-ba2e-563bfd36b1d4"
-version = "1.4.0"
-
 [[deps.UUIDs]]
 deps = ["Random", "SHA"]
 uuid = "cf7118a7-6976-5b1a-9a39-7adc72f591a4"
@@ -1369,260 +640,28 @@
 [[deps.Unicode]]
 uuid = "4ec0a83e-493e-50e2-b9ac-8f72acf5a8f5"
 
-[[deps.UnicodeFun]]
-deps = ["REPL"]
-git-tree-sha1 = "53915e50200959667e78a92a418594b428dffddf"
-uuid = "1cfade01-22cf-5700-b092-accc4b62d6e1"
-version = "0.4.1"
-
-[[deps.Unzip]]
-git-tree-sha1 = "ca0969166a028236229f63514992fc073799bb78"
-uuid = "41fe7b60-77ed-43a1-b4f0-825fd5a5650d"
-version = "0.2.0"
-
-[[deps.VersionParsing]]
-git-tree-sha1 = "58d6e80b4ee071f5efd07fda82cb9fbe17200868"
-uuid = "81def892-9a0e-5fdd-b105-ffc91e053289"
-version = "1.3.0"
-
-[[deps.Wayland_jll]]
-deps = ["Artifacts", "Expat_jll", "JLLWrappers", "Libdl", "Libffi_jll", "Pkg", "XML2_jll"]
-git-tree-sha1 = "3e61f0b86f90dacb0bc0e73a0c5a83f6a8636e23"
-uuid = "a2964d1f-97da-50d4-b82a-358c7fce9d89"
-version = "1.19.0+0"
-
-[[deps.Wayland_protocols_jll]]
-deps = ["Artifacts", "JLLWrappers", "Libdl", "Pkg"]
-git-tree-sha1 = "4528479aa01ee1b3b4cd0e6faef0e04cf16466da"
-uuid = "2381bf8a-dfd0-557d-9999-79630e7b1b91"
-version = "1.25.0+0"
-
 [[deps.WeakRefStrings]]
 deps = ["DataAPI", "InlineStrings", "Parsers"]
 git-tree-sha1 = "b1be2855ed9ed8eac54e5caff2afcdb442d52c23"
 uuid = "ea10d353-3f73-51f8-a26c-33c1cb351aa5"
 version = "1.4.2"
 
-[[deps.WebIO]]
-deps = ["AssetRegistry", "Base64", "Distributed", "FunctionalCollections", "JSON", "Logging", "Observables", "Pkg", "Random", "Requires", "Sockets", "UUIDs", "WebSockets", "Widgets"]
-git-tree-sha1 = "55ea1b43214edb1f6a228105a219c6e84f1f5533"
-uuid = "0f1e0344-ec1d-5b48-a673-e5cf874b6c29"
-version = "0.8.19"
-
-[[deps.WebSockets]]
-deps = ["Base64", "Dates", "HTTP", "Logging", "Sockets"]
-git-tree-sha1 = "f91a602e25fe6b89afc93cf02a4ae18ee9384ce3"
-uuid = "104b5d7c-a370-577a-8038-80a2059c5097"
-version = "1.5.9"
-
-[[deps.Widgets]]
-deps = ["Colors", "Dates", "Observables", "OrderedCollections"]
-git-tree-sha1 = "fcdae142c1cfc7d89de2d11e08721d0f2f86c98a"
-uuid = "cc8bc4a8-27d6-5769-a93b-9d913e69aa62"
-version = "0.6.6"
-
-[[deps.XML2_jll]]
-deps = ["Artifacts", "JLLWrappers", "Libdl", "Libiconv_jll", "Pkg", "Zlib_jll"]
-git-tree-sha1 = "58443b63fb7e465a8a7210828c91c08b92132dff"
-uuid = "02c8fc9c-b97f-50b9-bbe4-9be30ff0a78a"
-version = "2.9.14+0"
-
-[[deps.XSLT_jll]]
-deps = ["Artifacts", "JLLWrappers", "Libdl", "Libgcrypt_jll", "Libgpg_error_jll", "Libiconv_jll", "Pkg", "XML2_jll", "Zlib_jll"]
-git-tree-sha1 = "91844873c4085240b95e795f692c4cec4d805f8a"
-uuid = "aed1982a-8fda-507f-9586-7b0439959a61"
-version = "1.1.34+0"
-
-[[deps.Xorg_libX11_jll]]
-deps = ["Artifacts", "JLLWrappers", "Libdl", "Pkg", "Xorg_libxcb_jll", "Xorg_xtrans_jll"]
-git-tree-sha1 = "5be649d550f3f4b95308bf0183b82e2582876527"
-uuid = "4f6342f7-b3d2-589e-9d20-edeb45f2b2bc"
-version = "1.6.9+4"
-
-[[deps.Xorg_libXau_jll]]
-deps = ["Artifacts", "JLLWrappers", "Libdl", "Pkg"]
-git-tree-sha1 = "4e490d5c960c314f33885790ed410ff3a94ce67e"
-uuid = "0c0b7dd1-d40b-584c-a123-a41640f87eec"
-version = "1.0.9+4"
-
-[[deps.Xorg_libXcursor_jll]]
-deps = ["Artifacts", "JLLWrappers", "Libdl", "Pkg", "Xorg_libXfixes_jll", "Xorg_libXrender_jll"]
-git-tree-sha1 = "12e0eb3bc634fa2080c1c37fccf56f7c22989afd"
-uuid = "935fb764-8cf2-53bf-bb30-45bb1f8bf724"
-version = "1.2.0+4"
-
-[[deps.Xorg_libXdmcp_jll]]
-deps = ["Artifacts", "JLLWrappers", "Libdl", "Pkg"]
-git-tree-sha1 = "4fe47bd2247248125c428978740e18a681372dd4"
-uuid = "a3789734-cfe1-5b06-b2d0-1dd0d9d62d05"
-version = "1.1.3+4"
-
-[[deps.Xorg_libXext_jll]]
-deps = ["Artifacts", "JLLWrappers", "Libdl", "Pkg", "Xorg_libX11_jll"]
-git-tree-sha1 = "b7c0aa8c376b31e4852b360222848637f481f8c3"
-uuid = "1082639a-0dae-5f34-9b06-72781eeb8cb3"
-version = "1.3.4+4"
-
-[[deps.Xorg_libXfixes_jll]]
-deps = ["Artifacts", "JLLWrappers", "Libdl", "Pkg", "Xorg_libX11_jll"]
-git-tree-sha1 = "0e0dc7431e7a0587559f9294aeec269471c991a4"
-uuid = "d091e8ba-531a-589c-9de9-94069b037ed8"
-version = "5.0.3+4"
-
-[[deps.Xorg_libXi_jll]]
-deps = ["Artifacts", "JLLWrappers", "Libdl", "Pkg", "Xorg_libXext_jll", "Xorg_libXfixes_jll"]
-git-tree-sha1 = "89b52bc2160aadc84d707093930ef0bffa641246"
-uuid = "a51aa0fd-4e3c-5386-b890-e753decda492"
-version = "1.7.10+4"
-
-[[deps.Xorg_libXinerama_jll]]
-deps = ["Artifacts", "JLLWrappers", "Libdl", "Pkg", "Xorg_libXext_jll"]
-git-tree-sha1 = "26be8b1c342929259317d8b9f7b53bf2bb73b123"
-uuid = "d1454406-59df-5ea1-beac-c340f2130bc3"
-version = "1.1.4+4"
-
-[[deps.Xorg_libXrandr_jll]]
-deps = ["Artifacts", "JLLWrappers", "Libdl", "Pkg", "Xorg_libXext_jll", "Xorg_libXrender_jll"]
-git-tree-sha1 = "34cea83cb726fb58f325887bf0612c6b3fb17631"
-uuid = "ec84b674-ba8e-5d96-8ba1-2a689ba10484"
-version = "1.5.2+4"
-
-[[deps.Xorg_libXrender_jll]]
-deps = ["Artifacts", "JLLWrappers", "Libdl", "Pkg", "Xorg_libX11_jll"]
-git-tree-sha1 = "19560f30fd49f4d4efbe7002a1037f8c43d43b96"
-uuid = "ea2f1a96-1ddc-540d-b46f-429655e07cfa"
-version = "0.9.10+4"
-
-[[deps.Xorg_libpthread_stubs_jll]]
-deps = ["Artifacts", "JLLWrappers", "Libdl", "Pkg"]
-git-tree-sha1 = "6783737e45d3c59a4a4c4091f5f88cdcf0908cbb"
-uuid = "14d82f49-176c-5ed1-bb49-ad3f5cbd8c74"
-version = "0.1.0+3"
-
-[[deps.Xorg_libxcb_jll]]
-deps = ["Artifacts", "JLLWrappers", "Libdl", "Pkg", "XSLT_jll", "Xorg_libXau_jll", "Xorg_libXdmcp_jll", "Xorg_libpthread_stubs_jll"]
-git-tree-sha1 = "daf17f441228e7a3833846cd048892861cff16d6"
-uuid = "c7cfdc94-dc32-55de-ac96-5a1b8d977c5b"
-version = "1.13.0+3"
-
-[[deps.Xorg_libxkbfile_jll]]
-deps = ["Artifacts", "JLLWrappers", "Libdl", "Pkg", "Xorg_libX11_jll"]
-git-tree-sha1 = "926af861744212db0eb001d9e40b5d16292080b2"
-uuid = "cc61e674-0454-545c-8b26-ed2c68acab7a"
-version = "1.1.0+4"
-
-[[deps.Xorg_xcb_util_image_jll]]
-deps = ["Artifacts", "JLLWrappers", "Libdl", "Pkg", "Xorg_xcb_util_jll"]
-git-tree-sha1 = "0fab0a40349ba1cba2c1da699243396ff8e94b97"
-uuid = "12413925-8142-5f55-bb0e-6d7ca50bb09b"
-version = "0.4.0+1"
-
-[[deps.Xorg_xcb_util_jll]]
-deps = ["Artifacts", "JLLWrappers", "Libdl", "Pkg", "Xorg_libxcb_jll"]
-git-tree-sha1 = "e7fd7b2881fa2eaa72717420894d3938177862d1"
-uuid = "2def613f-5ad1-5310-b15b-b15d46f528f5"
-version = "0.4.0+1"
-
-[[deps.Xorg_xcb_util_keysyms_jll]]
-deps = ["Artifacts", "JLLWrappers", "Libdl", "Pkg", "Xorg_xcb_util_jll"]
-git-tree-sha1 = "d1151e2c45a544f32441a567d1690e701ec89b00"
-uuid = "975044d2-76e6-5fbe-bf08-97ce7c6574c7"
-version = "0.4.0+1"
-
-[[deps.Xorg_xcb_util_renderutil_jll]]
-deps = ["Artifacts", "JLLWrappers", "Libdl", "Pkg", "Xorg_xcb_util_jll"]
-git-tree-sha1 = "dfd7a8f38d4613b6a575253b3174dd991ca6183e"
-uuid = "0d47668e-0667-5a69-a72c-f761630bfb7e"
-version = "0.3.9+1"
-
-[[deps.Xorg_xcb_util_wm_jll]]
-deps = ["Artifacts", "JLLWrappers", "Libdl", "Pkg", "Xorg_xcb_util_jll"]
-git-tree-sha1 = "e78d10aab01a4a154142c5006ed44fd9e8e31b67"
-uuid = "c22f9ab0-d5fe-5066-847c-f4bb1cd4e361"
-version = "0.4.1+1"
-
-[[deps.Xorg_xkbcomp_jll]]
-deps = ["Artifacts", "JLLWrappers", "Libdl", "Pkg", "Xorg_libxkbfile_jll"]
-git-tree-sha1 = "4bcbf660f6c2e714f87e960a171b119d06ee163b"
-uuid = "35661453-b289-5fab-8a00-3d9160c6a3a4"
-version = "1.4.2+4"
-
-[[deps.Xorg_xkeyboard_config_jll]]
-deps = ["Artifacts", "JLLWrappers", "Libdl", "Pkg", "Xorg_xkbcomp_jll"]
-git-tree-sha1 = "5c8424f8a67c3f2209646d4425f3d415fee5931d"
-uuid = "33bec58e-1273-512f-9401-5d533626f822"
-version = "2.27.0+4"
-
-[[deps.Xorg_xtrans_jll]]
-deps = ["Artifacts", "JLLWrappers", "Libdl", "Pkg"]
-git-tree-sha1 = "79c31e7844f6ecf779705fbc12146eb190b7d845"
-uuid = "c5fb5394-a638-5e4d-96e5-b29de1b5cf10"
-version = "1.4.0+3"
-
 [[deps.Zlib_jll]]
 deps = ["Libdl"]
 uuid = "83775a58-1f1d-513f-b197-d71354ab007a"
 version = "1.2.12+3"
 
-[[deps.Zstd_jll]]
-deps = ["Artifacts", "JLLWrappers", "Libdl", "Pkg"]
-git-tree-sha1 = "e45044cd873ded54b6a5bac0eb5c971392cf1927"
-uuid = "3161d3a3-bdf6-5164-811a-617609db77b4"
-version = "1.5.2+0"
-
-[[deps.Zygote]]
-deps = ["AbstractFFTs", "ChainRules", "ChainRulesCore", "DiffRules", "Distributed", "FillArrays", "ForwardDiff", "GPUArrays", "GPUArraysCore", "IRTools", "InteractiveUtils", "LinearAlgebra", "LogExpFunctions", "MacroTools", "NaNMath", "Random", "Requires", "SparseArrays", "SpecialFunctions", "Statistics", "ZygoteRules"]
-git-tree-sha1 = "66cc604b9a27a660e25a54e408b4371123a186a6"
-uuid = "e88e6eb3-aa80-5325-afca-941959d7151f"
-version = "0.6.49"
-
 [[deps.ZygoteRules]]
 deps = ["MacroTools"]
 git-tree-sha1 = "8c1a8e4dfacb1fd631745552c8db35d0deb09ea0"
 uuid = "700de1a5-db45-46bc-99cf-38207098b444"
 version = "0.2.2"
 
-[[deps.fzf_jll]]
-deps = ["Artifacts", "JLLWrappers", "Libdl", "Pkg"]
-git-tree-sha1 = "868e669ccb12ba16eaf50cb2957ee2ff61261c56"
-uuid = "214eeab7-80f7-51ab-84ad-2988db7cef09"
-version = "0.29.0+0"
-
-[[deps.libaom_jll]]
-deps = ["Artifacts", "JLLWrappers", "Libdl", "Pkg"]
-git-tree-sha1 = "3a2ea60308f0996d26f1e5354e10c24e9ef905d4"
-uuid = "a4ae2306-e953-59d6-aa16-d00cac43593b"
-version = "3.4.0+0"
-
-[[deps.libass_jll]]
-deps = ["Artifacts", "Bzip2_jll", "FreeType2_jll", "FriBidi_jll", "HarfBuzz_jll", "JLLWrappers", "Libdl", "Pkg", "Zlib_jll"]
-git-tree-sha1 = "5982a94fcba20f02f42ace44b9894ee2b140fe47"
-uuid = "0ac62f75-1d6f-5e53-bd7c-93b484bb37c0"
-version = "0.15.1+0"
-
 [[deps.libblastrampoline_jll]]
 deps = ["Artifacts", "Libdl", "OpenBLAS_jll"]
 uuid = "8e850b90-86db-534c-a0d3-1478176c7d93"
 version = "5.1.1+0"
 
-[[deps.libfdk_aac_jll]]
-deps = ["Artifacts", "JLLWrappers", "Libdl", "Pkg"]
-git-tree-sha1 = "daacc84a041563f965be61859a36e17c4e4fcd55"
-uuid = "f638f0a6-7fb0-5443-88ba-1cc74229b280"
-version = "2.0.2+0"
-
-[[deps.libpng_jll]]
-deps = ["Artifacts", "JLLWrappers", "Libdl", "Pkg", "Zlib_jll"]
-git-tree-sha1 = "94d180a6d2b5e55e447e2d27a29ed04fe79eb30c"
-uuid = "b53b4c65-9356-5827-b1ea-8c7a1a84506f"
-version = "1.6.38+0"
-
-[[deps.libvorbis_jll]]
-deps = ["Artifacts", "JLLWrappers", "Libdl", "Ogg_jll", "Pkg"]
-git-tree-sha1 = "b910cb81ef3fe6e78bf6acee440bda86fd6ae00c"
-uuid = "f27f6e37-5d2b-51aa-960f-b287f2bc3b7a"
-version = "1.3.7+1"
-
 [[deps.nghttp2_jll]]
 deps = ["Artifacts", "Libdl"]
 uuid = "8e850ede-7688-5339-a07c-302acd2aaf8d"
@@ -1632,21 +671,3 @@
 deps = ["Artifacts", "Libdl"]
 uuid = "3f19e933-33d8-53b3-aaab-bd5110c3b7a0"
 version = "17.4.0+0"
-
-[[deps.x264_jll]]
-deps = ["Artifacts", "JLLWrappers", "Libdl", "Pkg"]
-git-tree-sha1 = "4fea590b89e6ec504593146bf8b988b2c00922b2"
-uuid = "1270edf5-f2f9-52d2-97e9-ab00b5d0237a"
-version = "2021.5.5+0"
-
-[[deps.x265_jll]]
-deps = ["Artifacts", "JLLWrappers", "Libdl", "Pkg"]
-git-tree-sha1 = "ee567a171cce03570d77ad3a43e90218e38937a9"
-uuid = "dfaa095f-4041-5dcd-9319-2fabd8486b76"
-version = "3.5.0+0"
-
-[[deps.xkbcommon_jll]]
-deps = ["Artifacts", "JLLWrappers", "Libdl", "Pkg", "Wayland_jll", "Wayland_protocols_jll", "Xorg_libxcb_jll", "Xorg_xkeyboard_config_jll"]
-git-tree-sha1 = "9ebfc140cc56e8c2156a15ceac2f0302e327ac0a"
-uuid = "d8fb68d0-12a3-5cfd-a85a-d49703b185fd"
-version = "1.4.1+0"
diff --git a/scouting/DriverRank/Project.toml b/scouting/DriverRank/Project.toml
index ccfaadb..034580d 100644
--- a/scouting/DriverRank/Project.toml
+++ b/scouting/DriverRank/Project.toml
@@ -4,18 +4,11 @@
 version = "0.1.0"
 
 [deps]
-BlackBoxOptim = "a134a8b2-14d6-55f6-9291-3336d3ab0209"
 CSV = "336ed68f-0bac-5ca0-87d4-7b16caf5d00b"
 DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0"
 DataStructures = "864edb3b-99cc-5e75-8d2d-829cb0a9cfe8"
-Flux = "587475ba-b771-5e3f-ad9e-33799f191a9c"
-GoogleSheets = "831f653e-6dbc-49a2-ac93-eebfaa09c6e6"
 HypothesisTests = "09f84164-cd44-5f33-b23f-e6b0d136a0d5"
 Optim = "429524aa-4258-5aef-a3af-852621145aeb"
-PlotlyBase = "a03496cd-edff-5a9b-9e67-9cda94a718b5"
-PlotlyJS = "f0f68f2c-4968-5e81-91da-67840de0976a"
-Plots = "91a5bcdd-55d7-5caf-9e0b-520d859cae80"
-Revise = "295af30f-e4ad-537b-8983-00126c2a3abe"
 Roots = "f2b01f46-fcfa-551c-844a-d8ac1e96c665"
 Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2"
 Transducers = "28d57a85-8fef-5791-bfe6-a80928e7c999"
diff --git a/scouting/DriverRank/src/DriverRank.jl b/scouting/DriverRank/src/DriverRank.jl
index c6e9aaa..c99be1f 100644
--- a/scouting/DriverRank/src/DriverRank.jl
+++ b/scouting/DriverRank/src/DriverRank.jl
@@ -1,19 +1,14 @@
 module DriverRank
 
-using GoogleSheets: sheets_client, Spreadsheet, CellRange, get, AUTH_SCOPE_READONLY
 using CSV
 using DataFrames: DataFrame
-using Transducers: Cat, MapCat, Map
+using Transducers: MapCat, Map
 using DataStructures: OrderedSet
 using HypothesisTests: OneSampleZTest, pvalue
 using Roots: find_zero
 using Statistics: mean
 import Optim
 using Optim: optimize
-using BlackBoxOptim: bboptimize, best_candidate, best_fitness
-# using PlotlyJS
-using Plots: scatter, hline!, plotlyjs, savefig, plotly
-import PlotlyBase: to_html
 
 struct TeamKey
     key::String
@@ -106,50 +101,9 @@
 end
 
 function rank()
-    # client = sheets_client(AUTH_SCOPE_READONLY)
-    # # spreadsheet_id = "13Cit7WrUxWz79iYVnoMoPc56W7H_cfr92jyT67tb2Xo"
-    # spreadsheet_id = "1q-Cl2aW4IkHk8Vcfd7OuFt0g4o3itn4SXgBi8Z1b7UE"
-    # range_name = "Form Responses 1"
-
-    # sheet = Spreadsheet(spreadsheet_id)
-    # range = CellRange(sheet, range_name)
-    # result = get(client, range).values
-
-    # # Filter empty rows
-    # is_not_empty =  result[:, 1] .!= ""
-    # result = result[is_not_empty, :]
-    # df = DataFrame(TeamKey.(result[2:end, :]), result[1, :])
-
+    # TODO(phil): Make the input path configurable.
     df = DataFrame(CSV.File("./data/2022_madtown.csv"))
 
-    # rank1 = "Rank 1 (best)"
-    # rank2 = "Rank 2"
-    # rank3 = "Rank 3"
-    # rank4 = "Rank 4"
-    # rank5 = "Rank 5"
-    # rank6 = "Rank 6 (worst)"
-    # matchups =
-    #     [
-    #         (df[!, rank1], df[!, rank2]),
-    #         (df[!, rank1], df[!, rank3]),
-    #         (df[!, rank1], df[!, rank4]),
-    #         (df[!, rank1], df[!, rank5]),
-    #         (df[!, rank1], df[!, rank6]),
-    #         (df[!, rank2], df[!, rank3]),
-    #         (df[!, rank2], df[!, rank4]),
-    #         (df[!, rank2], df[!, rank5]),
-    #         (df[!, rank2], df[!, rank6]),
-    #         (df[!, rank3], df[!, rank4]),
-    #         (df[!, rank3], df[!, rank5]),
-    #         (df[!, rank3], df[!, rank6]),
-    #         (df[!, rank4], df[!, rank5]),
-    #         (df[!, rank4], df[!, rank6]),
-    #         (df[!, rank5], df[!, rank6]),
-    #     ] |>
-    #     MapCat(((winners, losers),) -> zip(winners, losers)) |>
-    #     Map(((winner, loser),) -> DriverMatchup(; winner, loser)) |>
-    #     collect
-
     rank1 = "Rank 1 (best)"
     rank2 = "Rank 2"
     rank3 = "Rank 3 (worst)"
@@ -168,7 +122,7 @@
         collect
 
     driver_rankings = DriverRankings(matchups)
-    
+
     # Optimize!
     x0 = zeros(num_teams(driver_rankings))
     res = optimize(x -> objective_value(driver_rankings, x), x0, Optim.LBFGS(), autodiff=:forward)
@@ -179,27 +133,8 @@
             :score=>Optim.minimizer(res),
         ) |>
         x -> sort!(x, [:score], rev=true)
+    # TODO(phil): Save the output to a CSV file.
     show(ranking_points, allrows=true)
-
-    plotly()
-    idx = 1:length(ranking_points.team)
-    plt = scatter(
-        idx, ranking_points.score,
-        title="Driver Ranking",
-        xlabel="Team Number",
-        xticks=(idx, ranking_points.team),
-        xrotation=90,
-        ylabel="Score",
-        legend=false,
-    )
-    hline!(plt, [0.])
-
-    savefig(plt, "./driver_ranking.html")
-    # open("./driver_ranking.html", "w") do io
-    #     PlotlyBase.to_html(io, plt)
-    # end
-
-    return plt
 end
 
 export rank
diff --git a/scouting/db/db.go b/scouting/db/db.go
index 162f973..75309d9 100644
--- a/scouting/db/db.go
+++ b/scouting/db/db.go
@@ -94,15 +94,16 @@
 }
 
 type NotesData struct {
-	ID           uint `gorm:"primaryKey"`
-	TeamNumber   int32
-	Notes        string
-	GoodDriving  bool
-	BadDriving   bool
-	SketchyClimb bool
-	SolidClimb   bool
-	GoodDefense  bool
-	BadDefense   bool
+	ID             uint `gorm:"primaryKey"`
+	TeamNumber     int32
+	Notes          string
+	GoodDriving    bool
+	BadDriving     bool
+	SketchyPickup  bool
+	SketchyPlacing bool
+	GoodDefense    bool
+	BadDefense     bool
+	EasilyDefended bool
 }
 
 type Ranking struct {
@@ -125,6 +126,17 @@
 	Rank3       int32
 }
 
+type ParsedDriverRankingData struct {
+	// This data stores the output of DriverRank.jl.
+
+	TeamNumber string `gorm:"primaryKey"`
+
+	// The score of the team. A difference of 100 in two team's scores
+	// indicates that one team will outperform the other in 90% of the
+	// matches.
+	Score float32
+}
+
 // Opens a database at the specified port on localhost. We currently don't
 // support connecting to databases on other hosts.
 func NewDatabase(user string, password string, port int) (*Database, error) {
@@ -140,7 +152,7 @@
 		return nil, errors.New(fmt.Sprint("Failed to connect to postgres: ", err))
 	}
 
-	err = database.AutoMigrate(&TeamMatch{}, &Shift{}, &Stats{}, &Stats2023{}, &Action{}, &NotesData{}, &Ranking{}, &DriverRankingData{})
+	err = database.AutoMigrate(&TeamMatch{}, &Shift{}, &Stats{}, &Stats2023{}, &Action{}, &NotesData{}, &Ranking{}, &DriverRankingData{}, &ParsedDriverRankingData{})
 	if err != nil {
 		database.Delete()
 		return nil, errors.New(fmt.Sprint("Failed to create/migrate tables: ", err))
@@ -265,6 +277,12 @@
 	return rankings, result.Error
 }
 
+func (database *Database) ReturnAllParsedDriverRankings() ([]ParsedDriverRankingData, error) {
+	var rankings []ParsedDriverRankingData
+	result := database.Find(&rankings)
+	return rankings, result.Error
+}
+
 func (database *Database) ReturnAllShifts() ([]Shift, error) {
 	var shifts []Shift
 	result := database.Find(&shifts)
@@ -378,14 +396,15 @@
 
 func (database *Database) AddNotes(data NotesData) error {
 	result := database.Create(&NotesData{
-		TeamNumber:   data.TeamNumber,
-		Notes:        data.Notes,
-		GoodDriving:  data.GoodDriving,
-		BadDriving:   data.BadDriving,
-		SketchyClimb: data.SketchyClimb,
-		SolidClimb:   data.SolidClimb,
-		GoodDefense:  data.GoodDefense,
-		BadDefense:   data.BadDefense,
+		TeamNumber:     data.TeamNumber,
+		Notes:          data.Notes,
+		GoodDriving:    data.GoodDriving,
+		BadDriving:     data.BadDriving,
+		SketchyPickup:  data.SketchyPickup,
+		SketchyPlacing: data.SketchyPlacing,
+		GoodDefense:    data.GoodDefense,
+		BadDefense:     data.BadDefense,
+		EasilyDefended: data.EasilyDefended,
 	})
 	return result.Error
 }
@@ -400,6 +419,13 @@
 	return result.Error
 }
 
+func (database *Database) AddParsedDriverRanking(data ParsedDriverRankingData) error {
+	result := database.Clauses(clause.OnConflict{
+		UpdateAll: true,
+	}).Create(&data)
+	return result.Error
+}
+
 func (database *Database) QueryDriverRanking(MatchNumber int) ([]DriverRankingData, error) {
 	var data []DriverRankingData
 	result := database.Where("match_number = ?", MatchNumber).Find(&data)
diff --git a/scouting/db/db_test.go b/scouting/db/db_test.go
index ec7a1e7..b5e9a38 100644
--- a/scouting/db/db_test.go
+++ b/scouting/db/db_test.go
@@ -233,6 +233,94 @@
 	}
 }
 
+func TestAddToStats2023DB(t *testing.T) {
+	fixture := createDatabase(t)
+	defer fixture.TearDown()
+
+	correct := []Stats2023{
+		Stats2023{
+			TeamNumber: "6344", MatchNumber: 3, SetNumber: 1,
+			CompLevel: "quals", StartingQuadrant: 1, LowCubesAuto: 0,
+			MiddleCubesAuto: 1, HighCubesAuto: 0, CubesDroppedAuto: 1,
+			LowConesAuto: 1, MiddleConesAuto: 0, HighConesAuto: 2,
+			ConesDroppedAuto: 0, LowCubes: 1, MiddleCubes: 2,
+			HighCubes: 1, CubesDropped: 0, LowCones: 0,
+			MiddleCones: 2, HighCones: 1, ConesDropped: 1,
+			AvgCycle: 0, CollectedBy: "emma",
+		},
+		Stats2023{
+			TeamNumber: "7454", MatchNumber: 3, SetNumber: 1,
+			CompLevel: "quals", StartingQuadrant: 2, LowCubesAuto: 1,
+			MiddleCubesAuto: 2, HighCubesAuto: 2, CubesDroppedAuto: 0,
+			LowConesAuto: 2, MiddleConesAuto: 0, HighConesAuto: 0,
+			ConesDroppedAuto: 1, LowCubes: 1, MiddleCubes: 0,
+			HighCubes: 0, CubesDropped: 1, LowCones: 0,
+			MiddleCones: 0, HighCones: 1, ConesDropped: 0,
+			AvgCycle: 0, CollectedBy: "tyler",
+		},
+		Stats2023{
+			TeamNumber: "4354", MatchNumber: 3, SetNumber: 1,
+			CompLevel: "quals", StartingQuadrant: 3, LowCubesAuto: 0,
+			MiddleCubesAuto: 1, HighCubesAuto: 1, CubesDroppedAuto: 0,
+			LowConesAuto: 0, MiddleConesAuto: 2, HighConesAuto: 1,
+			ConesDroppedAuto: 1, LowCubes: 0, MiddleCubes: 0,
+			HighCubes: 2, CubesDropped: 1, LowCones: 1,
+			MiddleCones: 1, HighCones: 0, ConesDropped: 1,
+			AvgCycle: 0, CollectedBy: "isaac",
+		},
+		Stats2023{
+			TeamNumber: "6533", MatchNumber: 3, SetNumber: 1,
+			CompLevel: "quals", StartingQuadrant: 1, LowCubesAuto: 0,
+			MiddleCubesAuto: 2, HighCubesAuto: 1, CubesDroppedAuto: 1,
+			LowConesAuto: 1, MiddleConesAuto: 0, HighConesAuto: 0,
+			ConesDroppedAuto: 0, LowCubes: 0, MiddleCubes: 1,
+			HighCubes: 2, CubesDropped: 1, LowCones: 0,
+			MiddleCones: 1, HighCones: 0, ConesDropped: 0,
+			AvgCycle: 0, CollectedBy: "will",
+		},
+		Stats2023{
+			TeamNumber: "8354", MatchNumber: 3, SetNumber: 1,
+			CompLevel: "quals", StartingQuadrant: 2, LowCubesAuto: 1,
+			MiddleCubesAuto: 1, HighCubesAuto: 2, CubesDroppedAuto: 0,
+			LowConesAuto: 0, MiddleConesAuto: 1, HighConesAuto: 1,
+			ConesDroppedAuto: 1, LowCubes: 1, MiddleCubes: 0,
+			HighCubes: 0, CubesDropped: 2, LowCones: 1,
+			MiddleCones: 1, HighCones: 0, ConesDropped: 1,
+			AvgCycle: 0, CollectedBy: "unkown",
+		},
+	}
+
+	matches := []TeamMatch{
+		TeamMatch{MatchNumber: 3, SetNumber: 1, CompLevel: "quals",
+			Alliance: "R", AlliancePosition: 1, TeamNumber: 6344},
+		TeamMatch{MatchNumber: 3, SetNumber: 1, CompLevel: "quals",
+			Alliance: "R", AlliancePosition: 2, TeamNumber: 7454},
+		TeamMatch{MatchNumber: 3, SetNumber: 1, CompLevel: "quals",
+			Alliance: "R", AlliancePosition: 3, TeamNumber: 4354},
+		TeamMatch{MatchNumber: 3, SetNumber: 1, CompLevel: "quals",
+			Alliance: "B", AlliancePosition: 1, TeamNumber: 6533},
+		TeamMatch{MatchNumber: 3, SetNumber: 1, CompLevel: "quals",
+			Alliance: "B", AlliancePosition: 2, TeamNumber: 8354},
+	}
+
+	for _, match := range matches {
+		err := fixture.db.AddToMatch(match)
+		check(t, err, "Failed to add match")
+	}
+
+	for i := 0; i < len(correct); i++ {
+		err := fixture.db.AddToStats2023(correct[i])
+		check(t, err, "Failed to add 2023stats to DB")
+	}
+
+	got, err := fixture.db.ReturnStats2023()
+	check(t, err, "Failed ReturnStats2023()")
+
+	if !reflect.DeepEqual(correct, got) {
+		t.Errorf("Got %#v,\nbut expected %#v.", got, correct)
+	}
+}
+
 func TestAddDuplicateStats(t *testing.T) {
 	fixture := createDatabase(t)
 	defer fixture.TearDown()
@@ -808,6 +896,82 @@
 	}
 }
 
+func TestReturnStats2023DB(t *testing.T) {
+	fixture := createDatabase(t)
+	defer fixture.TearDown()
+
+	correct := []Stats2023{
+		Stats2023{
+			TeamNumber: "2343", MatchNumber: 2, SetNumber: 1,
+			CompLevel: "quals", StartingQuadrant: 1, LowCubesAuto: 1,
+			MiddleCubesAuto: 2, HighCubesAuto: 2, CubesDroppedAuto: 1,
+			LowConesAuto: 0, MiddleConesAuto: 0, HighConesAuto: 2,
+			ConesDroppedAuto: 1, LowCubes: 1, MiddleCubes: 2,
+			HighCubes: 1, CubesDropped: 0, LowCones: 2,
+			MiddleCones: 0, HighCones: 2, ConesDropped: 1,
+			AvgCycle: 51, CollectedBy: "isaac",
+		},
+		Stats2023{
+			TeamNumber: "5443", MatchNumber: 2, SetNumber: 1,
+			CompLevel: "quals", StartingQuadrant: 2, LowCubesAuto: 1,
+			MiddleCubesAuto: 1, HighCubesAuto: 0, CubesDroppedAuto: 1,
+			LowConesAuto: 1, MiddleConesAuto: 1, HighConesAuto: 0,
+			ConesDroppedAuto: 0, LowCubes: 2, MiddleCubes: 2,
+			HighCubes: 1, CubesDropped: 0, LowCones: 1,
+			MiddleCones: 0, HighCones: 2, ConesDropped: 1,
+			AvgCycle: 39, CollectedBy: "jack",
+		},
+		Stats2023{
+			TeamNumber: "5436", MatchNumber: 2, SetNumber: 1,
+			CompLevel: "quals", StartingQuadrant: 3, LowCubesAuto: 0,
+			MiddleCubesAuto: 2, HighCubesAuto: 0, CubesDroppedAuto: 1,
+			LowConesAuto: 2, MiddleConesAuto: 0, HighConesAuto: 0,
+			ConesDroppedAuto: 1, LowCubes: 2, MiddleCubes: 2,
+			HighCubes: 0, CubesDropped: 0, LowCones: 1,
+			MiddleCones: 2, HighCones: 1, ConesDropped: 1,
+			AvgCycle: 45, CollectedBy: "martin",
+		},
+		Stats2023{
+			TeamNumber: "5643", MatchNumber: 2, SetNumber: 1,
+			CompLevel: "quals", StartingQuadrant: 4, LowCubesAuto: 0,
+			MiddleCubesAuto: 0, HighCubesAuto: 1, CubesDroppedAuto: 1,
+			LowConesAuto: 2, MiddleConesAuto: 0, HighConesAuto: 0,
+			ConesDroppedAuto: 1, LowCubes: 2, MiddleCubes: 2,
+			HighCubes: 0, CubesDropped: 0, LowCones: 2,
+			MiddleCones: 2, HighCones: 1, ConesDropped: 1,
+			AvgCycle: 34, CollectedBy: "unknown",
+		},
+	}
+
+	matches := []TeamMatch{
+		TeamMatch{MatchNumber: 2, SetNumber: 1, CompLevel: "quals",
+			Alliance: "R", AlliancePosition: 1, TeamNumber: 2343},
+		TeamMatch{MatchNumber: 2, SetNumber: 1, CompLevel: "quals",
+			Alliance: "R", AlliancePosition: 2, TeamNumber: 5443},
+		TeamMatch{MatchNumber: 2, SetNumber: 1, CompLevel: "quals",
+			Alliance: "R", AlliancePosition: 3, TeamNumber: 5436},
+		TeamMatch{MatchNumber: 2, SetNumber: 1, CompLevel: "quals",
+			Alliance: "B", AlliancePosition: 1, TeamNumber: 5643},
+	}
+
+	for _, match := range matches {
+		err := fixture.db.AddToMatch(match)
+		check(t, err, "Failed to add match")
+	}
+
+	for i := 0; i < len(correct); i++ {
+		err := fixture.db.AddToStats2023(correct[i])
+		check(t, err, fmt.Sprint("Failed to add stats ", i))
+	}
+
+	got, err := fixture.db.ReturnStats2023()
+	check(t, err, "Failed ReturnStats()")
+
+	if !reflect.DeepEqual(correct, got) {
+		t.Errorf("Got %#v,\nbut expected %#v.", got, correct)
+	}
+}
+
 func TestReturnActionsDB(t *testing.T) {
 	fixture := createDatabase(t)
 	defer fixture.TearDown()
@@ -928,11 +1092,11 @@
 
 	expected := []string{"Note 1", "Note 3"}
 
-	err := fixture.db.AddNotes(NotesData{TeamNumber: 1234, Notes: "Note 1", GoodDriving: true, BadDriving: false, SketchyClimb: false, SolidClimb: true, GoodDefense: false, BadDefense: true})
+	err := fixture.db.AddNotes(NotesData{TeamNumber: 1234, Notes: "Note 1", GoodDriving: true, BadDriving: false, SketchyPickup: false, SketchyPlacing: true, GoodDefense: false, BadDefense: true, EasilyDefended: true})
 	check(t, err, "Failed to add Note")
-	err = fixture.db.AddNotes(NotesData{TeamNumber: 1235, Notes: "Note 2", GoodDriving: false, BadDriving: true, SketchyClimb: false, SolidClimb: true, GoodDefense: false, BadDefense: false})
+	err = fixture.db.AddNotes(NotesData{TeamNumber: 1235, Notes: "Note 2", GoodDriving: false, BadDriving: true, SketchyPickup: false, SketchyPlacing: true, GoodDefense: false, BadDefense: false, EasilyDefended: false})
 	check(t, err, "Failed to add Note")
-	err = fixture.db.AddNotes(NotesData{TeamNumber: 1234, Notes: "Note 3", GoodDriving: true, BadDriving: false, SketchyClimb: false, SolidClimb: true, GoodDefense: true, BadDefense: false})
+	err = fixture.db.AddNotes(NotesData{TeamNumber: 1234, Notes: "Note 3", GoodDriving: true, BadDriving: false, SketchyPickup: false, SketchyPlacing: true, GoodDefense: true, BadDefense: false, EasilyDefended: true})
 	check(t, err, "Failed to add Note")
 
 	actual, err := fixture.db.QueryNotes(1234)
@@ -972,3 +1136,44 @@
 		t.Errorf("Got %#v,\nbut expected %#v.", actual, expected)
 	}
 }
+
+func TestParsedDriverRanking(t *testing.T) {
+	fixture := createDatabase(t)
+	defer fixture.TearDown()
+
+	expected := []ParsedDriverRankingData{
+		{TeamNumber: "1234", Score: 100},
+		{TeamNumber: "1235", Score: 110},
+		{TeamNumber: "1236", Score: 90},
+	}
+
+	for i := range expected {
+		err := fixture.db.AddParsedDriverRanking(expected[i])
+		check(t, err, "Failed to add Parsed Driver Ranking")
+	}
+
+	actual, err := fixture.db.ReturnAllParsedDriverRankings()
+	check(t, err, "Failed to get Parsed Driver Ranking")
+	if !reflect.DeepEqual(expected, actual) {
+		t.Errorf("Got %#v,\nbut expected %#v.", actual, expected)
+	}
+
+	// Now update one of the rankings and make sure we get the properly
+	// merged result.
+	err = fixture.db.AddParsedDriverRanking(ParsedDriverRankingData{
+		TeamNumber: "1235", Score: 200,
+	})
+	check(t, err, "Failed to add Parsed Driver Ranking")
+
+	expected = []ParsedDriverRankingData{
+		{TeamNumber: "1234", Score: 100},
+		{TeamNumber: "1236", Score: 90},
+		{TeamNumber: "1235", Score: 200},
+	}
+
+	actual, err = fixture.db.ReturnAllParsedDriverRankings()
+	check(t, err, "Failed to get Parsed Driver Ranking")
+	if !reflect.DeepEqual(expected, actual) {
+		t.Errorf("Got %#v,\nbut expected %#v.", actual, expected)
+	}
+}
diff --git a/scouting/db/testdb_server/BUILD b/scouting/db/testdb_server/BUILD
index 881d189..f0bd43b 100644
--- a/scouting/db/testdb_server/BUILD
+++ b/scouting/db/testdb_server/BUILD
@@ -6,7 +6,7 @@
     importpath = "github.com/frc971/971-Robot-Code/scouting/db/testdb_server",
     target_compatible_with = ["@platforms//cpu:x86_64"],
     visibility = ["//visibility:private"],
-    deps = ["@com_github_phst_runfiles//:go_default_library"],
+    deps = ["@io_bazel_rules_go//go/runfiles:go_default_library"],
 )
 
 go_binary(
diff --git a/scouting/db/testdb_server/main.go b/scouting/db/testdb_server/main.go
index ecb118f..81835d2 100644
--- a/scouting/db/testdb_server/main.go
+++ b/scouting/db/testdb_server/main.go
@@ -18,7 +18,7 @@
 	"strings"
 	"syscall"
 
-	"github.com/phst/runfiles"
+	"github.com/bazelbuild/rules_go/go/runfiles"
 )
 
 func check(err error, message string) {
@@ -43,7 +43,7 @@
 }
 
 func getRunfile(path string) string {
-	result, err := runfiles.Path(path)
+	result, err := runfiles.Rlocation(path)
 	check(err, fmt.Sprint("Failed to find runfile path for ", path))
 	return result
 }
diff --git a/scouting/scraping/scrape.go b/scouting/scraping/scrape.go
index b905465..9cb2336 100644
--- a/scouting/scraping/scrape.go
+++ b/scouting/scraping/scrape.go
@@ -23,9 +23,10 @@
 // Also takes in a file path to the JSON config file that contains your TBA API key.
 // It defaults to <workspace root>/config.json
 // the config is expected to have the following contents:
-//{
-//    api_key:"myTBAapiKey"
-//}
+//
+//	{
+//	   api_key:"myTBAapiKey"
+//	}
 func getJson(year int32, eventCode, configPath, category string) ([]byte, error) {
 	if configPath == "" {
 		configPath = os.Getenv("BUILD_WORKSPACE_DIRECTORY") + "/scouting_config.json"
diff --git a/scouting/webserver/driver_ranking/BUILD b/scouting/webserver/driver_ranking/BUILD
new file mode 100644
index 0000000..cb98782
--- /dev/null
+++ b/scouting/webserver/driver_ranking/BUILD
@@ -0,0 +1,38 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
+
+go_library(
+    name = "driver_ranking",
+    srcs = ["driver_ranking.go"],
+    data = [
+        "//scouting/DriverRank:driver_rank_script",
+    ],
+    importpath = "github.com/frc971/971-Robot-Code/scouting/webserver/driver_ranking",
+    target_compatible_with = ["@platforms//cpu:x86_64"],
+    visibility = ["//visibility:public"],
+    deps = [
+        "//scouting/db",
+        "@io_bazel_rules_go//go/runfiles:go_default_library",
+    ],
+)
+
+go_test(
+    name = "driver_ranking_test",
+    srcs = ["driver_ranking_test.go"],
+    data = [
+        ":fake_driver_rank_script",
+    ],
+    embed = [":driver_ranking"],
+    target_compatible_with = ["@platforms//cpu:x86_64"],
+    deps = [
+        "//scouting/db",
+        "@com_github_davecgh_go_spew//spew",
+    ],
+)
+
+py_binary(
+    name = "fake_driver_rank_script",
+    testonly = True,
+    srcs = [
+        "fake_driver_rank_script.py",
+    ],
+)
diff --git a/scouting/webserver/driver_ranking/driver_ranking.go b/scouting/webserver/driver_ranking/driver_ranking.go
new file mode 100644
index 0000000..005078f
--- /dev/null
+++ b/scouting/webserver/driver_ranking/driver_ranking.go
@@ -0,0 +1,138 @@
+package driver_ranking
+
+import (
+	"encoding/csv"
+	"errors"
+	"fmt"
+	"log"
+	"os"
+	"os/exec"
+	"path/filepath"
+	"strconv"
+
+	"github.com/bazelbuild/rules_go/go/runfiles"
+	"github.com/frc971/971-Robot-Code/scouting/db"
+)
+
+const (
+	DEFAULT_SCRIPT_PATH = "org_frc971/scouting/DriverRank/src/DriverRank.jl"
+)
+
+type Database interface {
+	ReturnAllDriverRankings() ([]db.DriverRankingData, error)
+	AddParsedDriverRanking(data db.ParsedDriverRankingData) error
+}
+
+func writeToCsv(filename string, records [][]string) error {
+	file, err := os.Create(filename)
+	if err != nil {
+		return errors.New(fmt.Sprintf("Failed to create %s: %v", filename, err))
+	}
+	defer file.Close()
+
+	writer := csv.NewWriter(file)
+	writer.WriteAll(records)
+	if err := writer.Error(); err != nil {
+		return errors.New(fmt.Sprintf("Failed to write to %s: %v", filename, err))
+	}
+
+	return nil
+}
+
+func readFromCsv(filename string) (records [][]string, err error) {
+	file, err := os.Open(filename)
+	if err != nil {
+		return nil, errors.New(fmt.Sprintf("Failed to open %s: %v", filename, err))
+	}
+	defer file.Close()
+
+	reader := csv.NewReader(file)
+	records, err = reader.ReadAll()
+	if err != nil {
+		return nil, errors.New(fmt.Sprintf("Failed to parse %s as CSV: %v", filename, err))
+	}
+
+	return
+}
+
+// Runs the specified script on the DriverRankingData that the scouts collected
+// and dumps the results in the ParsedDriverRankingData table. If the script is
+// not specified (i.e. empty string) then the
+// scouting/DriverRank/src/DriverRank.jl script is called instead.
+func GenerateFullDriverRanking(database Database, scriptPath string) {
+	rawRankings, err := database.ReturnAllDriverRankings()
+	if err != nil {
+		log.Println("Failed to get raw driver ranking data: ", err)
+		return
+	}
+
+	records := [][]string{
+		{"Timestamp", "Scout Name", "Match Number", "Alliance", "Rank 1 (best)", "Rank 2", "Rank 3 (worst)"},
+	}
+
+	// Populate the CSV data.
+	for _, ranking := range rawRankings {
+		records = append(records, []string{
+			// Most of the data is unused so we just fill in empty
+			// strings.
+			"", "", "", "",
+			strconv.Itoa(int(ranking.Rank1)),
+			strconv.Itoa(int(ranking.Rank2)),
+			strconv.Itoa(int(ranking.Rank3)),
+		})
+	}
+
+	dir, err := os.MkdirTemp("", "driver_ranking_eval")
+	if err != nil {
+		log.Println("Failed to create temporary driver_ranking_eval dir: ", err)
+		return
+	}
+	defer os.RemoveAll(dir)
+
+	inputCsvFile := filepath.Join(dir, "input.csv")
+	outputCsvFile := filepath.Join(dir, "output.csv")
+
+	if err := writeToCsv(inputCsvFile, records); err != nil {
+		log.Println("Failed to write input CSV: ", err)
+		return
+	}
+
+	// If the user didn't specify a script, use the default one.
+	if scriptPath == "" {
+		scriptPath, err = runfiles.Rlocation(DEFAULT_SCRIPT_PATH)
+		if err != nil {
+			log.Println("Failed to find runfiles entry for ", DEFAULT_SCRIPT_PATH, ": ", err)
+			return
+		}
+	}
+
+	// Run the analysis script.
+	cmd := exec.Command(scriptPath, inputCsvFile, outputCsvFile)
+	cmd.Stdout = os.Stdout
+	cmd.Stderr = os.Stderr
+	if err := cmd.Run(); err != nil {
+		log.Println("Failed to run the driver ranking script: ", err)
+		return
+	}
+
+	// Grab the output from the analysis script and insert it into the
+	// database.
+	outputRecords, err := readFromCsv(outputCsvFile)
+
+	for _, record := range outputRecords {
+		score, err := strconv.ParseFloat(record[1], 32)
+		if err != nil {
+			log.Println("Failed to parse score for team ", record[0], ": ", record[1], ": ", err)
+			return
+		}
+
+		err = database.AddParsedDriverRanking(db.ParsedDriverRankingData{
+			TeamNumber: record[0],
+			Score:      float32(score),
+		})
+		if err != nil {
+			log.Println("Failed to insert driver ranking score for team ", record[0], ": ", err)
+			return
+		}
+	}
+}
diff --git a/scouting/webserver/driver_ranking/driver_ranking_test.go b/scouting/webserver/driver_ranking/driver_ranking_test.go
new file mode 100644
index 0000000..47a412e
--- /dev/null
+++ b/scouting/webserver/driver_ranking/driver_ranking_test.go
@@ -0,0 +1,58 @@
+package driver_ranking
+
+import (
+	"math"
+	"testing"
+
+	"github.com/davecgh/go-spew/spew"
+	"github.com/frc971/971-Robot-Code/scouting/db"
+)
+
+type MockDatabase struct {
+	rawRankings    []db.DriverRankingData
+	parsedRankings []db.ParsedDriverRankingData
+}
+
+func (database *MockDatabase) ReturnAllDriverRankings() ([]db.DriverRankingData, error) {
+	return database.rawRankings, nil
+}
+
+func (database *MockDatabase) AddParsedDriverRanking(data db.ParsedDriverRankingData) error {
+	database.parsedRankings = append(database.parsedRankings, data)
+	return nil
+}
+
+// Validates that we can call out to an external script to parse the raw driver
+// rankings and turn them into meaningful driver rankings. We don't call the
+// real DriverRank.jl script here because we don't have Julia support.
+func TestDriverRankingRun(t *testing.T) {
+	var database MockDatabase
+	database.rawRankings = []db.DriverRankingData{
+		db.DriverRankingData{MatchNumber: 1, Rank1: 1234, Rank2: 1235, Rank3: 1236},
+		db.DriverRankingData{MatchNumber: 2, Rank1: 971, Rank2: 972, Rank3: 973},
+	}
+
+	GenerateFullDriverRanking(&database, "./fake_driver_rank_script")
+
+	// This is the data that fake_driver_rank_script generates.
+	expected := []db.ParsedDriverRankingData{
+		db.ParsedDriverRankingData{TeamNumber: "1234", Score: 1.5},
+		db.ParsedDriverRankingData{TeamNumber: "1235", Score: 2.75},
+		db.ParsedDriverRankingData{TeamNumber: "1236", Score: 4.0},
+		db.ParsedDriverRankingData{TeamNumber: "971", Score: 5.25},
+		db.ParsedDriverRankingData{TeamNumber: "972", Score: 6.5},
+		db.ParsedDriverRankingData{TeamNumber: "973", Score: 7.75},
+	}
+	if len(expected) != len(database.parsedRankings) {
+		t.Fatalf(spew.Sprintf("Got %#v,\nbut expected %#v.", database.parsedRankings, expected))
+	}
+
+	// Compare each row manually because the floating point values might
+	// not match perfectly.
+	for i := range expected {
+		if expected[i].TeamNumber != database.parsedRankings[i].TeamNumber ||
+			math.Abs(float64(expected[i].Score-database.parsedRankings[i].Score)) > 0.001 {
+			t.Fatalf(spew.Sprintf("Got %#v,\nbut expected %#v.", database.parsedRankings, expected))
+		}
+	}
+}
diff --git a/scouting/webserver/driver_ranking/fake_driver_rank_script.py b/scouting/webserver/driver_ranking/fake_driver_rank_script.py
new file mode 100644
index 0000000..8f72267
--- /dev/null
+++ b/scouting/webserver/driver_ranking/fake_driver_rank_script.py
@@ -0,0 +1,54 @@
+"""A dummy script that helps validate driver_ranking.go logic.
+
+Since we don't have Julia support, we can't run the real script.
+"""
+
+import argparse
+import csv
+import sys
+from pathlib import Path
+
+EXPECTED_INPUT = [
+    [
+        'Timestamp', 'Scout Name', 'Match Number', 'Alliance', 'Rank 1 (best)',
+        'Rank 2', 'Rank 3 (worst)'
+    ],
+    ["", "", "", "", "1234", "1235", "1236"],
+    ["", "", "", "", "971", "972", "973"],
+]
+
+OUTPUT = [
+    ("1234", "1.5"),
+    ("1235", "2.75"),
+    ("1236", "4.0"),
+    ("971", "5.25"),
+    ("972", "6.5"),
+    ("973", "7.75"),
+]
+
+
+def main(argv):
+    parser = argparse.ArgumentParser()
+    parser.add_argument("input_csv", type=Path)
+    parser.add_argument("output_csv", type=Path)
+    args = parser.parse_args(argv[1:])
+
+    print("Reading input CSV")
+    with args.input_csv.open("r") as input_csv:
+        reader = csv.reader(input_csv)
+        input_data = [row for row in reader]
+
+    if EXPECTED_INPUT != input_data:
+        raise ValueError("Input data mismatch. Got: " + str(input_data))
+
+    print("Generating output CSV")
+    with args.output_csv.open("w") as output_csv:
+        writer = csv.writer(output_csv)
+        for row in OUTPUT:
+            writer.writerow(row)
+
+    print("Successfully generated fake parsed driver ranking data.")
+
+
+if __name__ == "__main__":
+    sys.exit(main(sys.argv))
diff --git a/scouting/webserver/requests/BUILD b/scouting/webserver/requests/BUILD
index e0d3e87..4c4870c 100644
--- a/scouting/webserver/requests/BUILD
+++ b/scouting/webserver/requests/BUILD
@@ -9,6 +9,8 @@
     deps = [
         "//scouting/db",
         "//scouting/webserver/requests/messages:error_response_go_fbs",
+        "//scouting/webserver/requests/messages:request_2023_data_scouting_go_fbs",
+        "//scouting/webserver/requests/messages:request_2023_data_scouting_response_go_fbs",
         "//scouting/webserver/requests/messages:request_all_driver_rankings_go_fbs",
         "//scouting/webserver/requests/messages:request_all_driver_rankings_response_go_fbs",
         "//scouting/webserver/requests/messages:request_all_matches_go_fbs",
@@ -45,6 +47,8 @@
         "//scouting/db",
         "//scouting/webserver/requests/debug",
         "//scouting/webserver/requests/messages:error_response_go_fbs",
+        "//scouting/webserver/requests/messages:request_2023_data_scouting_go_fbs",
+        "//scouting/webserver/requests/messages:request_2023_data_scouting_response_go_fbs",
         "//scouting/webserver/requests/messages:request_all_driver_rankings_go_fbs",
         "//scouting/webserver/requests/messages:request_all_driver_rankings_response_go_fbs",
         "//scouting/webserver/requests/messages:request_all_matches_go_fbs",
diff --git a/scouting/webserver/requests/debug/BUILD b/scouting/webserver/requests/debug/BUILD
index 1473b3c..355fff0 100644
--- a/scouting/webserver/requests/debug/BUILD
+++ b/scouting/webserver/requests/debug/BUILD
@@ -8,6 +8,7 @@
     visibility = ["//visibility:public"],
     deps = [
         "//scouting/webserver/requests/messages:error_response_go_fbs",
+        "//scouting/webserver/requests/messages:request_2023_data_scouting_response_go_fbs",
         "//scouting/webserver/requests/messages:request_all_driver_rankings_response_go_fbs",
         "//scouting/webserver/requests/messages:request_all_matches_response_go_fbs",
         "//scouting/webserver/requests/messages:request_all_notes_response_go_fbs",
diff --git a/scouting/webserver/requests/debug/cli/cli_test.py b/scouting/webserver/requests/debug/cli/cli_test.py
index 979ef5a..4763058 100644
--- a/scouting/webserver/requests/debug/cli/cli_test.py
+++ b/scouting/webserver/requests/debug/cli/cli_test.py
@@ -188,10 +188,11 @@
             "notes": "A very inspiring and useful comment",
             "good_driving": True,
             "bad_driving": False,
-            "sketchy_climb": False,
-            "solid_climb": True,
+            "sketchy_pickup": False,
+            "sketchy_placing": True,
             "good_defense": False,
             "bad_defense": False,
+            "easily_defended": False,
         })
         exit_code, _, stderr = run_debug_cli(["-submitNotes", json_path])
         self.assertEqual(exit_code, 0, stderr)
@@ -210,10 +211,11 @@
             Notes: (string) (len=35) "A very inspiring and useful comment",
             GoodDriving: (bool) true,
             BadDriving: (bool) false,
-            SketchyClimb: (bool) false,
-            SolidClimb: (bool) true,
+            SketchyPickup: (bool) false,
+            SketchyPlacing: (bool) true,
             GoodDefense: (bool) false,
-            BadDefense: (bool) false
+            BadDefense: (bool) false,
+            EasilyDefended: (bool) false
             }"""), stdout)
 
     def test_submit_and_request_driver_ranking(self):
diff --git a/scouting/webserver/requests/debug/cli/main.go b/scouting/webserver/requests/debug/cli/main.go
index 9279dba..58e24a2 100644
--- a/scouting/webserver/requests/debug/cli/main.go
+++ b/scouting/webserver/requests/debug/cli/main.go
@@ -93,6 +93,8 @@
 		"If specified, parse the file as a RequestAllMatches JSON request.")
 	requestDataScoutingPtr := flag.String("requestDataScouting", "",
 		"If specified, parse the file as a RequestDataScouting JSON request.")
+	request2023DataScoutingPtr := flag.String("request2023DataScouting", "",
+		"If specified, parse the file as a Request2023DataScouting JSON request.")
 	requestAllDriverRankingsPtr := flag.String("requestAllDriverRankings", "",
 		"If specified, parse the file as a requestAllDriverRankings JSON request.")
 	requestAllNotesPtr := flag.String("requestAllNotes", "",
@@ -141,6 +143,13 @@
 		debug.RequestDataScouting)
 
 	maybePerformRequest(
+		"Request2023DataScouting",
+		"scouting/webserver/requests/messages/request_2023_data_scouting.fbs",
+		*request2023DataScoutingPtr,
+		*addressPtr,
+		debug.Request2023DataScouting)
+
+	maybePerformRequest(
 		"requestAllDriverRankings",
 		"scouting/webserver/requests/messages/request_all_driver_rankings.fbs",
 		*requestAllDriverRankingsPtr,
diff --git a/scouting/webserver/requests/debug/debug.go b/scouting/webserver/requests/debug/debug.go
index 1f215f7..60d14be 100644
--- a/scouting/webserver/requests/debug/debug.go
+++ b/scouting/webserver/requests/debug/debug.go
@@ -10,6 +10,7 @@
 	"net/http"
 
 	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/error_response"
+	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/request_2023_data_scouting_response"
 	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/request_all_driver_rankings_response"
 	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/request_all_matches_response"
 	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/request_all_notes_response"
@@ -129,6 +130,12 @@
 		request_data_scouting_response.GetRootAsRequestDataScoutingResponse)
 }
 
+func Request2023DataScouting(server string, requestBytes []byte) (*request_2023_data_scouting_response.Request2023DataScoutingResponseT, error) {
+	return sendMessage[request_2023_data_scouting_response.Request2023DataScoutingResponseT](
+		server+"/requests/request/2023_data_scouting", requestBytes,
+		request_2023_data_scouting_response.GetRootAsRequest2023DataScoutingResponse)
+}
+
 func SubmitNotes(server string, requestBytes []byte) (*submit_notes_response.SubmitNotesResponseT, error) {
 	return sendMessage[submit_notes_response.SubmitNotesResponseT](
 		server+"/requests/submit/submit_notes", requestBytes,
diff --git a/scouting/webserver/requests/messages/BUILD b/scouting/webserver/requests/messages/BUILD
index 124613c..a04f39f 100644
--- a/scouting/webserver/requests/messages/BUILD
+++ b/scouting/webserver/requests/messages/BUILD
@@ -13,6 +13,8 @@
     "request_all_notes_response",
     "request_data_scouting",
     "request_data_scouting_response",
+    "request_2023_data_scouting",
+    "request_2023_data_scouting_response",
     "submit_notes",
     "submit_notes_response",
     "request_notes_for_team",
diff --git a/scouting/webserver/requests/messages/request_2023_data_scouting.fbs b/scouting/webserver/requests/messages/request_2023_data_scouting.fbs
new file mode 100644
index 0000000..e54c08f
--- /dev/null
+++ b/scouting/webserver/requests/messages/request_2023_data_scouting.fbs
@@ -0,0 +1,7 @@
+namespace scouting.webserver.requests;
+
+table Request2023DataScouting {
+
+}
+
+root_type Request2023DataScouting;
diff --git a/scouting/webserver/requests/messages/request_2023_data_scouting_response.fbs b/scouting/webserver/requests/messages/request_2023_data_scouting_response.fbs
new file mode 100644
index 0000000..d9d36b3
--- /dev/null
+++ b/scouting/webserver/requests/messages/request_2023_data_scouting_response.fbs
@@ -0,0 +1,36 @@
+namespace scouting.webserver.requests;
+
+table Stats2023 {
+  team_number:string (id: 0);
+  match_number:int (id: 1);
+  set_number:int (id: 21);
+  comp_level:string (id: 22);
+
+  starting_quadrant:int (id: 2);
+  low_cubes_auto:int (id:3);
+  middle_cubes_auto:int (id:4);
+  high_cubes_auto:int (id: 5);
+  cubes_dropped_auto: int (id: 6);
+  low_cones_auto:int (id:7);
+  middle_cones_auto:int (id:8);
+  high_cones_auto:int (id:9);
+  cones_dropped_auto:int (id:10);
+
+  low_cubes:int (id:11);
+  middle_cubes:int (id:12);
+  high_cubes:int (id:13);
+  cubes_dropped:int (id:14);
+  low_cones:int (id:15);
+  middle_cones:int (id:16);
+  high_cones:int (id:17);
+  cones_dropped:int (id:18);
+  avg_cycle:int (id:19);
+
+  collected_by:string (id:20);
+}
+
+table Request2023DataScoutingResponse {
+    stats_list:[Stats2023] (id:0);
+}
+
+root_type Request2023DataScoutingResponse;
\ No newline at end of file
diff --git a/scouting/webserver/requests/messages/request_all_notes_response.fbs b/scouting/webserver/requests/messages/request_all_notes_response.fbs
index a69861b..78e662d 100644
--- a/scouting/webserver/requests/messages/request_all_notes_response.fbs
+++ b/scouting/webserver/requests/messages/request_all_notes_response.fbs
@@ -5,10 +5,11 @@
     notes:string (id: 1);
     good_driving:bool (id: 2);
     bad_driving:bool (id: 3);
-    sketchy_climb:bool (id: 4);
-    solid_climb:bool (id: 5);
+    sketchy_pickup:bool (id: 4);
+    sketchy_placing:bool (id: 5);
     good_defense:bool (id: 6);
     bad_defense:bool (id: 7);
+    easily_defended:bool (id: 8);
 }
 
 table RequestAllNotesResponse {
diff --git a/scouting/webserver/requests/messages/submit_notes.fbs b/scouting/webserver/requests/messages/submit_notes.fbs
index 1498e26..27ed472 100644
--- a/scouting/webserver/requests/messages/submit_notes.fbs
+++ b/scouting/webserver/requests/messages/submit_notes.fbs
@@ -5,10 +5,11 @@
     notes:string (id: 1);
     good_driving:bool (id: 2);
     bad_driving:bool (id: 3);
-    sketchy_climb:bool (id: 4);
-    solid_climb:bool (id: 5);
+    sketchy_pickup:bool (id: 4);
+    sketchy_placing:bool (id: 5);
     good_defense:bool (id: 6);
     bad_defense:bool (id: 7);
+    easily_defended:bool (id: 8);
 }
 
 root_type SubmitNotes;
diff --git a/scouting/webserver/requests/requests.go b/scouting/webserver/requests/requests.go
index fab725c..467542a 100644
--- a/scouting/webserver/requests/requests.go
+++ b/scouting/webserver/requests/requests.go
@@ -13,6 +13,8 @@
 
 	"github.com/frc971/971-Robot-Code/scouting/db"
 	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/error_response"
+	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/request_2023_data_scouting"
+	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/request_2023_data_scouting_response"
 	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/request_all_driver_rankings"
 	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/request_all_driver_rankings_response"
 	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/request_all_matches"
@@ -49,6 +51,8 @@
 type RequestAllNotesResponseT = request_all_notes_response.RequestAllNotesResponseT
 type RequestDataScouting = request_data_scouting.RequestDataScouting
 type RequestDataScoutingResponseT = request_data_scouting_response.RequestDataScoutingResponseT
+type Request2023DataScouting = request_2023_data_scouting.Request2023DataScouting
+type Request2023DataScoutingResponseT = request_2023_data_scouting_response.Request2023DataScoutingResponseT
 type SubmitNotes = submit_notes.SubmitNotes
 type SubmitNotesResponseT = submit_notes_response.SubmitNotesResponseT
 type RequestNotesForTeam = request_notes_for_team.RequestNotesForTeam
@@ -68,11 +72,13 @@
 	AddToMatch(db.TeamMatch) error
 	AddToShift(db.Shift) error
 	AddToStats(db.Stats) error
+	AddToStats2023(db.Stats2023) error
 	ReturnMatches() ([]db.TeamMatch, error)
 	ReturnAllNotes() ([]db.NotesData, error)
 	ReturnAllDriverRankings() ([]db.DriverRankingData, error)
 	ReturnAllShifts() ([]db.Shift, error)
 	ReturnStats() ([]db.Stats, error)
+	ReturnStats2023() ([]db.Stats2023, error)
 	QueryAllShifts(int) ([]db.Shift, error)
 	QueryStats(int) ([]db.Stats, error)
 	QueryNotes(int32) ([]string, error)
@@ -113,7 +119,7 @@
 // Parses the authorization information that the browser inserts into the
 // headers.  The authorization follows this format:
 //
-//  req.Headers["Authorization"] = []string{"Basic <base64 encoded username:password>"}
+//	req.Headers["Authorization"] = []string{"Basic <base64 encoded username:password>"}
 func parseUsername(req *http.Request) string {
 	auth, ok := req.Header["Authorization"]
 	if !ok {
@@ -374,7 +380,7 @@
 
 	stats, err := handler.db.ReturnStats()
 	if err != nil {
-		respondWithError(w, http.StatusInternalServerError, fmt.Sprint("Faled to query database: ", err))
+		respondWithError(w, http.StatusInternalServerError, fmt.Sprint("Failed to query database: ", err))
 		return
 	}
 
@@ -427,14 +433,15 @@
 	}
 
 	err = handler.db.AddNotes(db.NotesData{
-		TeamNumber:   request.Team(),
-		Notes:        string(request.Notes()),
-		GoodDriving:  bool(request.GoodDriving()),
-		BadDriving:   bool(request.BadDriving()),
-		SketchyClimb: bool(request.SketchyClimb()),
-		SolidClimb:   bool(request.SolidClimb()),
-		GoodDefense:  bool(request.GoodDefense()),
-		BadDefense:   bool(request.BadDefense()),
+		TeamNumber:     request.Team(),
+		Notes:          string(request.Notes()),
+		GoodDriving:    bool(request.GoodDriving()),
+		BadDriving:     bool(request.BadDriving()),
+		SketchyPickup:  bool(request.SketchyPickup()),
+		SketchyPlacing: bool(request.SketchyPlacing()),
+		GoodDefense:    bool(request.GoodDefense()),
+		BadDefense:     bool(request.BadDefense()),
+		EasilyDefended: bool(request.EasilyDefended()),
 	})
 	if err != nil {
 		respondWithError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to insert notes: %v", err))
@@ -447,6 +454,63 @@
 	w.Write(builder.FinishedBytes())
 }
 
+// Handles a Request2023DataScouting request.
+type request2023DataScoutingHandler struct {
+	db Database
+}
+
+func (handler request2023DataScoutingHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
+	requestBytes, err := io.ReadAll(req.Body)
+	if err != nil {
+		respondWithError(w, http.StatusBadRequest, fmt.Sprint("Failed to read request bytes:", err))
+		return
+	}
+
+	_, success := parseRequest(w, requestBytes, "Request2023DataScouting", request_2023_data_scouting.GetRootAsRequest2023DataScouting)
+	if !success {
+		return
+	}
+
+	stats, err := handler.db.ReturnStats2023()
+	if err != nil {
+		respondWithError(w, http.StatusInternalServerError, fmt.Sprint("Failed to query database: ", err))
+		return
+	}
+
+	var response Request2023DataScoutingResponseT
+	for _, stat := range stats {
+		response.StatsList = append(response.StatsList, &request_2023_data_scouting_response.Stats2023T{
+			TeamNumber:       stat.TeamNumber,
+			MatchNumber:      stat.MatchNumber,
+			SetNumber:        stat.SetNumber,
+			CompLevel:        stat.CompLevel,
+			StartingQuadrant: stat.StartingQuadrant,
+			LowCubesAuto:     stat.LowCubesAuto,
+			MiddleCubesAuto:  stat.MiddleCubesAuto,
+			HighCubesAuto:    stat.HighCubesAuto,
+			CubesDroppedAuto: stat.CubesDroppedAuto,
+			LowConesAuto:     stat.LowConesAuto,
+			MiddleConesAuto:  stat.MiddleConesAuto,
+			HighConesAuto:    stat.HighConesAuto,
+			ConesDroppedAuto: stat.ConesDroppedAuto,
+			LowCubes:         stat.LowCubes,
+			MiddleCubes:      stat.MiddleCubes,
+			HighCubes:        stat.HighCubes,
+			CubesDropped:     stat.CubesDropped,
+			LowCones:         stat.LowCones,
+			MiddleCones:      stat.MiddleCones,
+			HighCones:        stat.HighCones,
+			ConesDropped:     stat.ConesDropped,
+			AvgCycle:         stat.AvgCycle,
+			CollectedBy:      stat.CollectedBy,
+		})
+	}
+
+	builder := flatbuffers.NewBuilder(50 * 1024)
+	builder.Finish((&response).Pack(builder))
+	w.Write(builder.FinishedBytes())
+}
+
 type requestNotesForTeamHandler struct {
 	db Database
 }
@@ -623,14 +687,15 @@
 	var response RequestAllNotesResponseT
 	for _, note := range notes {
 		response.NoteList = append(response.NoteList, &request_all_notes_response.NoteT{
-			Team:         note.TeamNumber,
-			Notes:        note.Notes,
-			GoodDriving:  note.GoodDriving,
-			BadDriving:   note.BadDriving,
-			SketchyClimb: note.SketchyClimb,
-			SolidClimb:   note.SolidClimb,
-			GoodDefense:  note.GoodDefense,
-			BadDefense:   note.BadDefense,
+			Team:           note.TeamNumber,
+			Notes:          note.Notes,
+			GoodDriving:    note.GoodDriving,
+			BadDriving:     note.BadDriving,
+			SketchyPickup:  note.SketchyPickup,
+			SketchyPlacing: note.SketchyPlacing,
+			GoodDefense:    note.GoodDefense,
+			BadDefense:     note.BadDefense,
+			EasilyDefended: note.EasilyDefended,
 		})
 	}
 
@@ -683,6 +748,7 @@
 	scoutingServer.Handle("/requests/request/all_notes", requestAllNotesHandler{db})
 	scoutingServer.Handle("/requests/request/all_driver_rankings", requestAllDriverRankingsHandler{db})
 	scoutingServer.Handle("/requests/request/data_scouting", requestDataScoutingHandler{db})
+	scoutingServer.Handle("/requests/request/2023_data_scouting", request2023DataScoutingHandler{db})
 	scoutingServer.Handle("/requests/submit/submit_notes", submitNoteScoutingHandler{db})
 	scoutingServer.Handle("/requests/request/notes_for_team", requestNotesForTeamHandler{db})
 	scoutingServer.Handle("/requests/submit/shift_schedule", submitShiftScheduleHandler{db})
diff --git a/scouting/webserver/requests/requests_test.go b/scouting/webserver/requests/requests_test.go
index ee22022..efd770b 100644
--- a/scouting/webserver/requests/requests_test.go
+++ b/scouting/webserver/requests/requests_test.go
@@ -10,6 +10,8 @@
 	"github.com/frc971/971-Robot-Code/scouting/db"
 	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/debug"
 	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/error_response"
+	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/request_2023_data_scouting"
+	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/request_2023_data_scouting_response"
 	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/request_all_driver_rankings"
 	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/request_all_driver_rankings_response"
 	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/request_all_matches"
@@ -316,6 +318,79 @@
 	}
 }
 
+// Validates that we can request the 2023 stats.
+func TestRequest2023DataScouting(t *testing.T) {
+	db := MockDatabase{
+		stats2023: []db.Stats2023{
+			{
+				TeamNumber: "3634", MatchNumber: 1, SetNumber: 2,
+				CompLevel: "quals", StartingQuadrant: 3, LowCubesAuto: 10,
+				MiddleCubesAuto: 1, HighCubesAuto: 1, CubesDroppedAuto: 0,
+				LowConesAuto: 1, MiddleConesAuto: 2, HighConesAuto: 1,
+				ConesDroppedAuto: 0, LowCubes: 1, MiddleCubes: 1,
+				HighCubes: 2, CubesDropped: 1, LowCones: 1,
+				MiddleCones: 2, HighCones: 0, ConesDropped: 1,
+				AvgCycle: 34, CollectedBy: "isaac",
+			},
+			{
+				TeamNumber: "2343", MatchNumber: 1, SetNumber: 2,
+				CompLevel: "quals", StartingQuadrant: 1, LowCubesAuto: 0,
+				MiddleCubesAuto: 1, HighCubesAuto: 1, CubesDroppedAuto: 2,
+				LowConesAuto: 0, MiddleConesAuto: 0, HighConesAuto: 0,
+				ConesDroppedAuto: 1, LowCubes: 0, MiddleCubes: 0,
+				HighCubes: 1, CubesDropped: 0, LowCones: 0,
+				MiddleCones: 2, HighCones: 1, ConesDropped: 1,
+				AvgCycle: 53, CollectedBy: "unknown",
+			},
+		},
+	}
+	scoutingServer := server.NewScoutingServer()
+	HandleRequests(&db, scoutingServer)
+	scoutingServer.Start(8080)
+	defer scoutingServer.Stop()
+
+	builder := flatbuffers.NewBuilder(1024)
+	builder.Finish((&request_2023_data_scouting.Request2023DataScoutingT{}).Pack(builder))
+
+	response, err := debug.Request2023DataScouting("http://localhost:8080", builder.FinishedBytes())
+	if err != nil {
+		t.Fatal("Failed to request all matches: ", err)
+	}
+
+	expected := request_2023_data_scouting_response.Request2023DataScoutingResponseT{
+		StatsList: []*request_2023_data_scouting_response.Stats2023T{
+			{
+				TeamNumber: "3634", MatchNumber: 1, SetNumber: 2,
+				CompLevel: "quals", StartingQuadrant: 3, LowCubesAuto: 10,
+				MiddleCubesAuto: 1, HighCubesAuto: 1, CubesDroppedAuto: 0,
+				LowConesAuto: 1, MiddleConesAuto: 2, HighConesAuto: 1,
+				ConesDroppedAuto: 0, LowCubes: 1, MiddleCubes: 1,
+				HighCubes: 2, CubesDropped: 1, LowCones: 1,
+				MiddleCones: 2, HighCones: 0, ConesDropped: 1,
+				AvgCycle: 34, CollectedBy: "isaac",
+			},
+			{
+				TeamNumber: "2343", MatchNumber: 1, SetNumber: 2,
+				CompLevel: "quals", StartingQuadrant: 1, LowCubesAuto: 0,
+				MiddleCubesAuto: 1, HighCubesAuto: 1, CubesDroppedAuto: 2,
+				LowConesAuto: 0, MiddleConesAuto: 0, HighConesAuto: 0,
+				ConesDroppedAuto: 1, LowCubes: 0, MiddleCubes: 0,
+				HighCubes: 1, CubesDropped: 0, LowCones: 0,
+				MiddleCones: 2, HighCones: 1, ConesDropped: 1,
+				AvgCycle: 53, CollectedBy: "unknown",
+			},
+		},
+	}
+	if len(expected.StatsList) != len(response.StatsList) {
+		t.Fatal("Expected ", expected, ", but got ", *response)
+	}
+	for i, match := range expected.StatsList {
+		if !reflect.DeepEqual(*match, *response.StatsList[i]) {
+			t.Fatal("Expected for stats", i, ":", *match, ", but got:", *response.StatsList[i])
+		}
+	}
+}
+
 func TestSubmitNotes(t *testing.T) {
 	database := MockDatabase{}
 	scoutingServer := server.NewScoutingServer()
@@ -325,14 +400,15 @@
 
 	builder := flatbuffers.NewBuilder(1024)
 	builder.Finish((&submit_notes.SubmitNotesT{
-		Team:         971,
-		Notes:        "Notes",
-		GoodDriving:  true,
-		BadDriving:   false,
-		SketchyClimb: true,
-		SolidClimb:   false,
-		GoodDefense:  true,
-		BadDefense:   false,
+		Team:           971,
+		Notes:          "Notes",
+		GoodDriving:    true,
+		BadDriving:     false,
+		SketchyPickup:  true,
+		SketchyPlacing: false,
+		GoodDefense:    true,
+		BadDefense:     false,
+		EasilyDefended: true,
 	}).Pack(builder))
 
 	_, err := debug.SubmitNotes("http://localhost:8080", builder.FinishedBytes())
@@ -342,14 +418,15 @@
 
 	expected := []db.NotesData{
 		{
-			TeamNumber:   971,
-			Notes:        "Notes",
-			GoodDriving:  true,
-			BadDriving:   false,
-			SketchyClimb: true,
-			SolidClimb:   false,
-			GoodDefense:  true,
-			BadDefense:   false,
+			TeamNumber:     971,
+			Notes:          "Notes",
+			GoodDriving:    true,
+			BadDriving:     false,
+			SketchyPickup:  true,
+			SketchyPlacing: false,
+			GoodDefense:    true,
+			BadDefense:     false,
+			EasilyDefended: true,
 		},
 	}
 
@@ -361,14 +438,15 @@
 func TestRequestNotes(t *testing.T) {
 	database := MockDatabase{
 		notes: []db.NotesData{{
-			TeamNumber:   971,
-			Notes:        "Notes",
-			GoodDriving:  true,
-			BadDriving:   false,
-			SketchyClimb: true,
-			SolidClimb:   false,
-			GoodDefense:  true,
-			BadDefense:   false,
+			TeamNumber:     971,
+			Notes:          "Notes",
+			GoodDriving:    true,
+			BadDriving:     false,
+			SketchyPickup:  true,
+			SketchyPlacing: false,
+			GoodDefense:    true,
+			BadDefense:     false,
+			EasilyDefended: true,
 		}},
 	}
 	scoutingServer := server.NewScoutingServer()
@@ -588,24 +666,26 @@
 	db := MockDatabase{
 		notes: []db.NotesData{
 			{
-				TeamNumber:   971,
-				Notes:        "Notes",
-				GoodDriving:  true,
-				BadDriving:   false,
-				SketchyClimb: true,
-				SolidClimb:   false,
-				GoodDefense:  true,
-				BadDefense:   false,
+				TeamNumber:     971,
+				Notes:          "Notes",
+				GoodDriving:    true,
+				BadDriving:     false,
+				SketchyPickup:  true,
+				SketchyPlacing: false,
+				GoodDefense:    true,
+				BadDefense:     false,
+				EasilyDefended: false,
 			},
 			{
-				TeamNumber:   972,
-				Notes:        "More Notes",
-				GoodDriving:  false,
-				BadDriving:   false,
-				SketchyClimb: false,
-				SolidClimb:   true,
-				GoodDefense:  false,
-				BadDefense:   true,
+				TeamNumber:     972,
+				Notes:          "More Notes",
+				GoodDriving:    false,
+				BadDriving:     false,
+				SketchyPickup:  false,
+				SketchyPlacing: true,
+				GoodDefense:    false,
+				BadDefense:     true,
+				EasilyDefended: false,
 			},
 		},
 	}
@@ -625,24 +705,26 @@
 	expected := request_all_notes_response.RequestAllNotesResponseT{
 		NoteList: []*request_all_notes_response.NoteT{
 			{
-				Team:         971,
-				Notes:        "Notes",
-				GoodDriving:  true,
-				BadDriving:   false,
-				SketchyClimb: true,
-				SolidClimb:   false,
-				GoodDefense:  true,
-				BadDefense:   false,
+				Team:           971,
+				Notes:          "Notes",
+				GoodDriving:    true,
+				BadDriving:     false,
+				SketchyPickup:  true,
+				SketchyPlacing: false,
+				GoodDefense:    true,
+				BadDefense:     false,
+				EasilyDefended: false,
 			},
 			{
-				Team:         972,
-				Notes:        "More Notes",
-				GoodDriving:  false,
-				BadDriving:   false,
-				SketchyClimb: false,
-				SolidClimb:   true,
-				GoodDefense:  false,
-				BadDefense:   true,
+				Team:           972,
+				Notes:          "More Notes",
+				GoodDriving:    false,
+				BadDriving:     false,
+				SketchyPickup:  false,
+				SketchyPlacing: true,
+				GoodDefense:    false,
+				BadDefense:     true,
+				EasilyDefended: false,
 			},
 		},
 	}
@@ -665,6 +747,7 @@
 	notes          []db.NotesData
 	shiftSchedule  []db.Shift
 	driver_ranking []db.DriverRankingData
+	stats2023      []db.Stats2023
 }
 
 func (database *MockDatabase) AddToMatch(match db.TeamMatch) error {
@@ -677,6 +760,10 @@
 	return nil
 }
 
+func (database *MockDatabase) AddToStats2023(stats2023 db.Stats2023) error {
+	database.stats2023 = append(database.stats2023, stats2023)
+	return nil
+}
 func (database *MockDatabase) ReturnMatches() ([]db.TeamMatch, error) {
 	return database.matches, nil
 }
@@ -685,6 +772,10 @@
 	return database.stats, nil
 }
 
+func (database *MockDatabase) ReturnStats2023() ([]db.Stats2023, error) {
+	return database.stats2023, nil
+}
+
 func (database *MockDatabase) QueryStats(int) ([]db.Stats, error) {
 	return []db.Stats{}, nil
 }
diff --git a/scouting/www/BUILD b/scouting/www/BUILD
index 042a9c3..4c2f931 100644
--- a/scouting/www/BUILD
+++ b/scouting/www/BUILD
@@ -15,12 +15,12 @@
     ],
     deps = [
         "//:node_modules/@angular/animations",
-        "//scouting/www/driver_ranking:_lib",
-        "//scouting/www/entry:_lib",
-        "//scouting/www/match_list:_lib",
-        "//scouting/www/notes:_lib",
-        "//scouting/www/shift_schedule:_lib",
-        "//scouting/www/view:_lib",
+        "//scouting/www/driver_ranking",
+        "//scouting/www/entry",
+        "//scouting/www/match_list",
+        "//scouting/www/notes",
+        "//scouting/www/shift_schedule",
+        "//scouting/www/view",
     ],
 )
 
diff --git a/scouting/www/match_list/BUILD b/scouting/www/match_list/BUILD
index c713dda..b2128db 100644
--- a/scouting/www/match_list/BUILD
+++ b/scouting/www/match_list/BUILD
@@ -13,7 +13,7 @@
         "//scouting/webserver/requests/messages:error_response_ts_fbs",
         "//scouting/webserver/requests/messages:request_all_matches_response_ts_fbs",
         "//scouting/webserver/requests/messages:request_all_matches_ts_fbs",
-        "//scouting/www/rpc:_lib",
+        "//scouting/www/rpc",
         "@com_github_google_flatbuffers//ts:flatbuffers_ts",
     ],
 )
diff --git a/scouting/www/notes/notes.component.ts b/scouting/www/notes/notes.component.ts
index 62b4990..21264f2 100644
--- a/scouting/www/notes/notes.component.ts
+++ b/scouting/www/notes/notes.component.ts
@@ -40,10 +40,11 @@
 interface Keywords {
   goodDriving: boolean;
   badDriving: boolean;
-  sketchyClimb: boolean;
-  solidClimb: boolean;
+  sketchyPickup: boolean;
+  sketchyPlacing: boolean;
   goodDefense: boolean;
   badDefense: boolean;
+  easilyDefended: boolean;
 }
 
 interface Input {
@@ -55,10 +56,11 @@
 const KEYWORD_CHECKBOX_LABELS = {
   goodDriving: 'Good Driving',
   badDriving: 'Bad Driving',
-  solidClimb: 'Solid Climb',
-  sketchyClimb: 'Sketchy Climb',
+  sketchyPickup: 'Solid Pickup',
+  sketchyPlacing: 'Sketchy Placing',
   goodDefense: 'Good Defense',
   badDefense: 'Bad Defense',
+  easilyDefended: 'Easily Defended',
 } as const;
 
 @Component({
@@ -113,10 +115,11 @@
       keywordsData: {
         goodDriving: false,
         badDriving: false,
-        solidClimb: false,
-        sketchyClimb: false,
+        sketchyPickup: false,
+        sketchyPlacing: false,
         goodDefense: false,
         badDefense: false,
+        easilyDefended: false,
       },
     };
 
@@ -149,10 +152,11 @@
           dataFb,
           this.newData[i].keywordsData.goodDriving,
           this.newData[i].keywordsData.badDriving,
-          this.newData[i].keywordsData.sketchyClimb,
-          this.newData[i].keywordsData.solidClimb,
+          this.newData[i].keywordsData.sketchyPickup,
+          this.newData[i].keywordsData.sketchyPlacing,
           this.newData[i].keywordsData.goodDefense,
-          this.newData[i].keywordsData.badDefense
+          this.newData[i].keywordsData.badDefense,
+          this.newData[i].keywordsData.easilyDefended
         )
       );
 
diff --git a/scouting/www/view/BUILD b/scouting/www/view/BUILD
index 168feb6..25aa9fa 100644
--- a/scouting/www/view/BUILD
+++ b/scouting/www/view/BUILD
@@ -17,7 +17,7 @@
         "//scouting/webserver/requests/messages:request_all_notes_ts_fbs",
         "//scouting/webserver/requests/messages:request_data_scouting_response_ts_fbs",
         "//scouting/webserver/requests/messages:request_data_scouting_ts_fbs",
-        "//scouting/www/rpc:_lib",
+        "//scouting/www/rpc",
         "@com_github_google_flatbuffers//ts:flatbuffers_ts",
     ],
 )
diff --git a/scouting/www/view/view.component.ts b/scouting/www/view/view.component.ts
index c877faf..f4a55a1 100644
--- a/scouting/www/view/view.component.ts
+++ b/scouting/www/view/view.component.ts
@@ -171,11 +171,11 @@
     if (entry.badDriving()) {
       parsedKeywords += 'Bad Driving ';
     }
-    if (entry.solidClimb()) {
-      parsedKeywords += 'Solid Climb ';
+    if (entry.sketchyPickup()) {
+      parsedKeywords += 'Sketchy Pickup ';
     }
-    if (entry.sketchyClimb()) {
-      parsedKeywords += 'Sketchy Climb ';
+    if (entry.sketchyPlacing()) {
+      parsedKeywords += 'Sketchy Pickup ';
     }
     if (entry.goodDefense()) {
       parsedKeywords += 'Good Defense ';
@@ -183,6 +183,9 @@
     if (entry.badDefense()) {
       parsedKeywords += 'Bad Defense ';
     }
+    if (entry.easilyDefended()) {
+      parsedKeywords += 'Easily Defended';
+    }
 
     return parsedKeywords;
   }
diff --git a/third_party/bazel-gazelle/0001-Fix-visibility-of-gazelle-runner.patch b/third_party/bazel-gazelle/0001-Fix-visibility-of-gazelle-runner.patch
new file mode 100644
index 0000000..2e460e4
--- /dev/null
+++ b/third_party/bazel-gazelle/0001-Fix-visibility-of-gazelle-runner.patch
@@ -0,0 +1,21 @@
+From 1b0b864a6900beeee83cadfcb596381ceb443b86 Mon Sep 17 00:00:00 2001
+From: Philipp Schrader <philipp.schrader@gmail.com>
+Date: Sun, 5 Mar 2023 13:51:11 -0800
+Subject: [PATCH] Fix visibility of gazelle-runner
+
+---
+ def.bzl | 1 +
+ 1 file changed, 1 insertion(+)
+
+diff --git a/def.bzl b/def.bzl
+index c4d3c71..ce9a6f2 100644
+--- a/def.bzl
++++ b/def.bzl
+@@ -145,6 +145,7 @@ def gazelle(name, **kwargs):
+     _gazelle_runner(
+         name = runner_name,
+         tags = tags,
++        visibility = visibility,
+         **kwargs
+     )
+     native.sh_binary(
diff --git a/third_party/libedgetpu/libedgetpu.BUILD b/third_party/libedgetpu/libedgetpu.BUILD
new file mode 100644
index 0000000..0289452
--- /dev/null
+++ b/third_party/libedgetpu/libedgetpu.BUILD
@@ -0,0 +1,11 @@
+cc_library(
+    visibility = ["//visibility:public"],
+    name = "libedgetpu-k8",
+    srcs = ["k8/libedgetpu.so.1.0"]
+)
+
+cc_library(
+    visibility = ["//visibility:public"],
+    name = "libedgetpu-arm",
+    srcs = ["arm/libedgetpu.so.1.0"]
+)
\ No newline at end of file
diff --git a/third_party/libedgetpu/libedgetpu_build_script.sh b/third_party/libedgetpu/libedgetpu_build_script.sh
new file mode 100644
index 0000000..0eafccf
--- /dev/null
+++ b/third_party/libedgetpu/libedgetpu_build_script.sh
@@ -0,0 +1,16 @@
+# Clone the correct version of libedgetpu
+git clone https://github.com/google-coral/libedgetpu.git
+cd libedgetpu
+# Build libedgetpu.so.1.0 for both arm and x86
+DOCKER_CPUS="k8" DOCKER_IMAGE="ubuntu:18.04" DOCKER_TARGETS=libedgetpu make docker-build
+DOCKER_CPUS="aarch64" DOCKER_IMAGE="debian:stretch" DOCKER_TARGETS=libedgetpu make docker-build
+# Create the directory for the tarball and move the resulting files into it 
+mkdir libedgetpu-bazel
+mkdir libedgetpu-bazel/arm
+mkdir libedgetpu-bazel/k8
+cp out/direct/aarch64/libedgetpu.so.1.0 libedgetpu-bazel/arm
+cp out/direct/k8/libedgetpu.so.1.0 libedgetpu-bazel/k8
+
+# Copy header files to the include directory
+mkdir libedgetpu-bazel/include
+cp -r include/* libedgetpu-bazel/include/
diff --git a/third_party/libtensorflowlite/libtensorflowlite.BUILD b/third_party/libtensorflowlite/libtensorflowlite.BUILD
new file mode 100644
index 0000000..a6b837d
--- /dev/null
+++ b/third_party/libtensorflowlite/libtensorflowlite.BUILD
@@ -0,0 +1,16 @@
+cc_library(
+    visibility = ["//visibility:public"],
+    name = "tensorflow-k8",
+    hdrs = glob(["include/**/*.h"]),
+    strip_include_prefix = "include",
+    srcs = ["k8/libtensorflowlite.so"]
+)
+
+cc_library(
+    visibility = ["//visibility:public"],
+    name = "tensorflow-arm",
+    hdrs = glob(["include/**/*.h"]),
+    strip_include_prefix = "include",
+    srcs = ["arm/libtensorflowlite.so"]
+)
+
diff --git a/third_party/libtensorflowlite/tensorflow_build_script.sh b/third_party/libtensorflowlite/tensorflow_build_script.sh
new file mode 100644
index 0000000..6c44353
--- /dev/null
+++ b/third_party/libtensorflowlite/tensorflow_build_script.sh
@@ -0,0 +1,17 @@
+# Clone and checkout the correct version of Tensorflow
+git clone https://github.com/tensorflow/tensorflow.git tensorflow_src
+cd tensorflow_src
+git checkout v2.8.0
+# Build libtensorflowlite.so for both arm and x86
+bazel build --config=elinux_aarch64 -c opt //tensorflow/lite:libtensorflowlite.so
+bazel build --config=native_arch_linux -c opt //tensorflow/lite:libtensorflowlite.so
+# Create the directory for the tarball and move the resulting files into it 
+mkdir tensorflow-bazel
+mkdir tensorflow-bazel/arm
+mkdir tensorflow-bazel/k8
+cp bazel-out/aarch64-opt/bin/tensorflow/lite/libtensorflowlite.so tensorflow-bazel/arm
+cp bazel-out/k8-opt/bin/tensorflow/lite/libtensorflowlite.so tensorflow-bazel/k8
+
+# Copy header files to the include directory
+ mkdir -p tensorflow-bazel/tensorflow/core/util
+ rsync -zarv --include='*/'  --include='*.h' --exclude='*' tensorflow/core/util tensorflow-bazel/tensorflow/core/util
\ No newline at end of file
diff --git a/tools/build_rules/js.bzl b/tools/build_rules/js.bzl
index ceb67aa..eb95510 100644
--- a/tools/build_rules/js.bzl
+++ b/tools/build_rules/js.bzl
@@ -1,7 +1,5 @@
 load("@aspect_rules_js//js:providers.bzl", "JsInfo")
 load("@bazel_skylib//rules:write_file.bzl", "write_file")
-load("@aspect_rules_js//js:defs.bzl", "js_library")
-load("@aspect_rules_js//npm:defs.bzl", "npm_package")
 load("@aspect_bazel_lib//lib:copy_to_directory.bzl", "copy_to_directory")
 load("@aspect_bazel_lib//lib:copy_file.bzl", "copy_file")
 load("@aspect_rules_esbuild//esbuild:defs.bzl", "esbuild")
@@ -271,7 +269,7 @@
         srcs.append(":_public_api")
 
     ng_project(
-        name = "_lib",
+        name = name,
         srcs = srcs + [":_index"],
         deps = deps + PACKAGE_DEPS,
         #visibility = ["//visibility:private"],
@@ -279,20 +277,6 @@
         **kwargs
     )
 
-    js_library(
-        name = name + "_js",
-        srcs = [":_lib"],
-        visibility = ["//visibility:public"],
-    )
-
-    npm_package(
-        name = name,
-        srcs = ["package.json", ":_lib"],
-        # This is a perf improvement; the default will be flipped to False in rules_js 2.0
-        include_runfiles = False,
-        visibility = ["//visibility:public"],
-    )
-
 def rollup_bundle(name, entry_point, deps = [], visibility = None, **kwargs):
     """Calls the upstream rollup_bundle() and exposes a .min.js file.
 
diff --git a/tools/dependency_rewrite b/tools/dependency_rewrite
index 23e5d14..7e9e1de 100644
--- a/tools/dependency_rewrite
+++ b/tools/dependency_rewrite
@@ -16,7 +16,7 @@
 rewrite cdn.cypress.io/(.*) software.frc971.org/Build-Dependencies/cdn.cypress.io/$1
 rewrite www.googleapis.com/(.*) software.frc971.org/Build-Dependencies/www.googleapis.com/$1
 allow crates.io
-allow golang.org
+allow go.dev
 allow registry.npmjs.org
 
 allow software.frc971.org
diff --git a/tools/go/go_mirrors.bzl b/tools/go/go_mirrors.bzl
index 338cc95..c60ef4a 100644
--- a/tools/go/go_mirrors.bzl
+++ b/tools/go/go_mirrors.bzl
@@ -7,16 +7,22 @@
         "strip_prefix": "honnef.co/go/tools@v0.0.1-2019.2.3",
         "version": "v0.0.1-2019.2.3",
     },
-    "com_github_antihax_optional": {
-        "filename": "com_github_antihax_optional__v1.0.0.zip",
-        "importpath": "github.com/antihax/optional",
-        "sha256": "15ab4d41bdbb72ee0ac63db616cdefc7671c79e13d0f73b58355a6a88219c97f",
-        "strip_prefix": "github.com/antihax/optional@v1.0.0",
-        "version": "v1.0.0",
+    "com_github_bazelbuild_rules_go": {
+        "filename": "com_github_bazelbuild_rules_go__v0.38.1.zip",
+        "importpath": "github.com/bazelbuild/rules_go",
+        "sha256": "4d61f481c4192fc6a608bd3aa9c0e104416dd48950951b8bead9a1b4a0d2112b",
+        "strip_prefix": "github.com/bazelbuild/rules_go@v0.38.1",
+        "version": "v0.38.1",
     },
     "com_github_buildkite_go_buildkite": {
         "filename": "com_github_buildkite_go_buildkite__v2.2.0+incompatible.zip",
         "importpath": "github.com/buildkite/go-buildkite",
+        "kwargs": {
+            "build_directives": [
+                "gazelle:resolve go github.com/cenkalti/backoff @com_github_cenkalti_backoff//:go_default_library",
+                "gazelle:resolve go github.com/google/go-querystring/query @com_github_google_go_querystring//query:go_default_library",
+            ],
+        },
         "sha256": "1871115c8c6db004e4b6e57cee927043bfc9ea0c56e7b8f8336021bd8bf588c4",
         "strip_prefix": "github.com/buildkite/go-buildkite@v2.2.0+incompatible",
         "version": "v2.2.0+incompatible",
@@ -99,11 +105,11 @@
         "version": "v1.1.1",
     },
     "com_github_envoyproxy_go_control_plane": {
-        "filename": "com_github_envoyproxy_go_control_plane__v0.9.10-0.20210907150352-cf90f659a021.zip",
+        "filename": "com_github_envoyproxy_go_control_plane__v0.10.2-0.20220325020618-49ff273808a1.zip",
         "importpath": "github.com/envoyproxy/go-control-plane",
-        "sha256": "41dc70a8e658cb8945fa0de289d25dd7a608e99929bae144776781401dec507a",
-        "strip_prefix": "github.com/envoyproxy/go-control-plane@v0.9.10-0.20210907150352-cf90f659a021",
-        "version": "v0.9.10-0.20210907150352-cf90f659a021",
+        "sha256": "8e8bf22bacf1b4b5a54aa6c56af3f281131d6dcd1ccbf2226b9c1e62c9b5cca7",
+        "strip_prefix": "github.com/envoyproxy/go-control-plane@v0.10.2-0.20220325020618-49ff273808a1",
+        "version": "v0.10.2-0.20220325020618-49ff273808a1",
     },
     "com_github_envoyproxy_protoc_gen_validate": {
         "filename": "com_github_envoyproxy_protoc_gen_validate__v0.1.0.zip",
@@ -112,13 +118,6 @@
         "strip_prefix": "github.com/envoyproxy/protoc-gen-validate@v0.1.0",
         "version": "v0.1.0",
     },
-    "com_github_ghodss_yaml": {
-        "filename": "com_github_ghodss_yaml__v1.0.0.zip",
-        "importpath": "github.com/ghodss/yaml",
-        "sha256": "c3f295d23c02c0b35e4d3b29053586e737cf9642df9615da99c0bda9bbacc624",
-        "strip_prefix": "github.com/ghodss/yaml@v1.0.0",
-        "version": "v1.0.0",
-    },
     "com_github_go_kit_log": {
         "filename": "com_github_go_kit_log__v0.1.0.zip",
         "importpath": "github.com/go-kit/log",
@@ -155,11 +154,11 @@
         "version": "v0.0.0-20160126235308-23def4e6c14b",
     },
     "com_github_golang_mock": {
-        "filename": "com_github_golang_mock__v1.1.1.zip",
+        "filename": "com_github_golang_mock__v1.6.0.zip",
         "importpath": "github.com/golang/mock",
-        "sha256": "636fd21575ebdfbebd53045802a40c780fdab33c6130cea9279346898286f1ca",
-        "strip_prefix": "github.com/golang/mock@v1.1.1",
-        "version": "v1.1.1",
+        "sha256": "fa25916b546f90da49418f436e3a61e4c5dae898cf3c82b0007b5a6fab74261b",
+        "strip_prefix": "github.com/golang/mock@v1.6.0",
+        "version": "v1.6.0",
     },
     "com_github_golang_protobuf": {
         "filename": "com_github_golang_protobuf__v1.5.2.zip",
@@ -169,11 +168,11 @@
         "version": "v1.5.2",
     },
     "com_github_google_go_cmp": {
-        "filename": "com_github_google_go_cmp__v0.5.5.zip",
+        "filename": "com_github_google_go_cmp__v0.5.6.zip",
         "importpath": "github.com/google/go-cmp",
-        "sha256": "0ee90a7194c025d849699f897d97641b8676ceca9215c96e00eaf1f0e6e953ad",
-        "strip_prefix": "github.com/google/go-cmp@v0.5.5",
-        "version": "v0.5.5",
+        "sha256": "32c6bb53a2f214fecd43ca0a436758488d088a9ac23e391ef4b502eda0591147",
+        "strip_prefix": "github.com/google/go-cmp@v0.5.6",
+        "version": "v0.5.6",
     },
     "com_github_google_go_querystring": {
         "filename": "com_github_google_go_querystring__v1.1.0.zip",
@@ -196,13 +195,6 @@
         "strip_prefix": "github.com/google/uuid@v1.1.2",
         "version": "v1.1.2",
     },
-    "com_github_grpc_ecosystem_grpc_gateway": {
-        "filename": "com_github_grpc_ecosystem_grpc_gateway__v1.16.0.zip",
-        "importpath": "github.com/grpc-ecosystem/grpc-gateway",
-        "sha256": "377b03aef288b34ed894449d3ddba40d525dd7fb55de6e79045cdf499e7fe565",
-        "strip_prefix": "github.com/grpc-ecosystem/grpc-gateway@v1.16.0",
-        "version": "v1.16.0",
-    },
     "com_github_jackc_chunkreader": {
         "filename": "com_github_jackc_chunkreader__v1.0.0.zip",
         "importpath": "github.com/jackc/chunkreader",
@@ -364,13 +356,6 @@
         "strip_prefix": "github.com/mattn/go-isatty@v0.0.12",
         "version": "v0.0.12",
     },
-    "com_github_phst_runfiles": {
-        "filename": "com_github_phst_runfiles__v0.0.0-20220125203201-388095b3a22d.zip",
-        "importpath": "github.com/phst/runfiles",
-        "sha256": "f58f97414074227c39abce3f1b4cf8780446630b23f963232458092c126b5541",
-        "strip_prefix": "github.com/phst/runfiles@v0.0.0-20220125203201-388095b3a22d",
-        "version": "v0.0.0-20220125203201-388095b3a22d",
-    },
     "com_github_pkg_errors": {
         "filename": "com_github_pkg_errors__v0.8.1.zip",
         "importpath": "github.com/pkg/errors",
@@ -392,13 +377,6 @@
         "strip_prefix": "github.com/prometheus/client_model@v0.0.0-20190812154241-14fe0d1b01d4",
         "version": "v0.0.0-20190812154241-14fe0d1b01d4",
     },
-    "com_github_rogpeppe_fastuuid": {
-        "filename": "com_github_rogpeppe_fastuuid__v1.2.0.zip",
-        "importpath": "github.com/rogpeppe/fastuuid",
-        "sha256": "f9b8293f5e20270e26fb4214ca7afec864de92c73d03ff62b5ee29d1db4e72a1",
-        "strip_prefix": "github.com/rogpeppe/fastuuid@v1.2.0",
-        "version": "v1.2.0",
-    },
     "com_github_rogpeppe_go_internal": {
         "filename": "com_github_rogpeppe_go_internal__v1.3.0.zip",
         "importpath": "github.com/rogpeppe/go-internal",
@@ -491,11 +469,11 @@
         "version": "v2.0.0-20180818164646-67afb5ed74ec",
     },
     "in_gopkg_yaml_v2": {
-        "filename": "in_gopkg_yaml_v2__v2.2.3.zip",
+        "filename": "in_gopkg_yaml_v2__v2.2.2.zip",
         "importpath": "gopkg.in/yaml.v2",
-        "sha256": "213403de27ae981b118ba199a3a1ddc64a82d0c9cf7534b762dc9ee5d79c5316",
-        "strip_prefix": "gopkg.in/yaml.v2@v2.2.3",
-        "version": "v2.2.3",
+        "sha256": "9e0e5492ee218d0b415a33648cdb32c2f544485ac4ebfa0589ebb53d1a841096",
+        "strip_prefix": "gopkg.in/yaml.v2@v2.2.2",
+        "version": "v2.2.2",
     },
     "in_gopkg_yaml_v3": {
         "filename": "in_gopkg_yaml_v3__v3.0.0-20200313102051-9f266ea9e77c.zip",
@@ -518,13 +496,6 @@
         "strip_prefix": "gorm.io/gorm@v1.23.5",
         "version": "v1.23.5",
     },
-    "io_opentelemetry_go_proto_otlp": {
-        "filename": "io_opentelemetry_go_proto_otlp__v0.7.0.zip",
-        "importpath": "go.opentelemetry.io/proto/otlp",
-        "sha256": "a7db0590bc4c5f0b9b99cc958decf644f1e5cc11e0b995dc20b3583a2215259b",
-        "strip_prefix": "go.opentelemetry.io/proto/otlp@v0.7.0",
-        "version": "v0.7.0",
-    },
     "org_golang_google_appengine": {
         "filename": "org_golang_google_appengine__v1.4.0.zip",
         "importpath": "google.golang.org/appengine",
@@ -540,18 +511,18 @@
         "version": "v0.0.0-20200526211855-cb27e3aa2013",
     },
     "org_golang_google_grpc": {
-        "filename": "org_golang_google_grpc__v1.43.0.zip",
+        "filename": "org_golang_google_grpc__v1.50.0.zip",
         "importpath": "google.golang.org/grpc",
-        "sha256": "19fa6e227e62e3ae9791ab81b8a784e93cc68860b7fe0e85dd8d3cfbc1b24398",
-        "strip_prefix": "google.golang.org/grpc@v1.43.0",
-        "version": "v1.43.0",
+        "sha256": "5c5db4efe81a3b829fae5c267caf45ae184678ae81e6e0a216fc86b3ef13ecaf",
+        "strip_prefix": "google.golang.org/grpc@v1.50.0",
+        "version": "v1.50.0",
     },
     "org_golang_google_protobuf": {
-        "filename": "org_golang_google_protobuf__v1.26.0.zip",
+        "filename": "org_golang_google_protobuf__v1.28.0.zip",
         "importpath": "google.golang.org/protobuf",
-        "sha256": "d7bc5de329bd4e803f7a2acfcbe8f2eba4ef1579485056ef569a4b245bee1208",
-        "strip_prefix": "google.golang.org/protobuf@v1.26.0",
-        "version": "v1.26.0",
+        "sha256": "f06dc39ce93043d6ec91a5106c7ec958be6b4ba520cab3a21a2448d387cf15a4",
+        "strip_prefix": "google.golang.org/protobuf@v1.28.0",
+        "version": "v1.28.0",
     },
     "org_golang_x_crypto": {
         "filename": "org_golang_x_crypto__v0.0.0-20210921155107-089bfa567519.zip",
@@ -582,11 +553,11 @@
         "version": "v0.1.1-0.20191105210325-c90efee705ee",
     },
     "org_golang_x_net": {
-        "filename": "org_golang_x_net__v0.0.0-20210226172049-e18ecbb05110.zip",
+        "filename": "org_golang_x_net__v0.0.0-20210405180319-a5a99cb37ef4.zip",
         "importpath": "golang.org/x/net",
-        "sha256": "17ae555c0bec70b583d84ec7a099db3fdc5b3b688cb2814f8c388d174e7ada15",
-        "strip_prefix": "golang.org/x/net@v0.0.0-20210226172049-e18ecbb05110",
-        "version": "v0.0.0-20210226172049-e18ecbb05110",
+        "sha256": "67e1f754b0f6a7701600567d74d0e2fcd9ae8a1ba0dfe5d7c782842ae17c4df8",
+        "strip_prefix": "golang.org/x/net@v0.0.0-20210405180319-a5a99cb37ef4",
+        "version": "v0.0.0-20210405180319-a5a99cb37ef4",
     },
     "org_golang_x_oauth2": {
         "filename": "org_golang_x_oauth2__v0.0.0-20200107190931-bf48bf16ab8d.zip",
diff --git a/tools/go/mirror_go_repos.py b/tools/go/mirror_go_repos.py
index 52d611f..a7cfe08 100644
--- a/tools/go/mirror_go_repos.py
+++ b/tools/go/mirror_go_repos.py
@@ -53,6 +53,7 @@
         print(f"Downloading file for {repo['name']}")
         importpath = repo["importpath"]
         version = repo["version"]
+        repo_kwargs = repo.get("kwargs")
         module = f"{importpath}@{version}"
 
         download_result = subprocess.run(
@@ -84,6 +85,8 @@
             "version": version,
             "importpath": importpath,
         }
+        if repo_kwargs:
+            cached_info[name]["kwargs"] = repo_kwargs
 
     return cached_info
 
diff --git a/tools/go/mirrored_go_deps.bzl b/tools/go/mirrored_go_deps.bzl
index 7839f4d..adac994 100644
--- a/tools/go/mirrored_go_deps.bzl
+++ b/tools/go/mirrored_go_deps.bzl
@@ -2,7 +2,7 @@
 load("@bazel_gazelle//:deps.bzl", "go_repository")
 load("@ci_configure//:ci.bzl", "RUNNING_IN_CI")
 
-def maybe_override_go_dep(name, importpath, sum, version):
+def maybe_override_go_dep(name, importpath, sum, version, **kwargs):
     """This macro selects between our dependency mirrors and upstream sources.
 
     We want to use the mirrored version whenever possible. In CI we are required
@@ -28,12 +28,14 @@
             importpath = importpath,
             sum = sum,
             version = version,
+            **kwargs
         )
 
 def mirrored_go_dependencies():
     """Sets up the Go dependencies we've mirrored."""
     for name in GO_MIRROR_INFO:
         info = GO_MIRROR_INFO[name]
+        kwargs = info.get("kwargs", {})
         go_repository(
             name = name,
             strip_prefix = info["strip_prefix"],
@@ -43,4 +45,5 @@
             ],
             sha256 = info["sha256"],
             importpath = info["importpath"],
+            **kwargs
         )
diff --git a/y2019/control_loops/drivetrain/target_selector.h b/y2019/control_loops/drivetrain/target_selector.h
index 19ac52e..b8d89c7 100644
--- a/y2019/control_loops/drivetrain/target_selector.h
+++ b/y2019/control_loops/drivetrain/target_selector.h
@@ -1,10 +1,10 @@
 #ifndef Y2019_CONTROL_LOOPS_DRIVETRAIN_TARGET_SELECTOR_H_
 #define Y2019_CONTROL_LOOPS_DRIVETRAIN_TARGET_SELECTOR_H_
 
-#include "frc971/control_loops/pose.h"
-#include "frc971/control_loops/drivetrain/localizer.h"
-#include "y2019/constants.h"
 #include "frc971/control_loops/drivetrain/camera.h"
+#include "frc971/control_loops/drivetrain/localizer.h"
+#include "frc971/control_loops/pose.h"
+#include "y2019/constants.h"
 #include "y2019/control_loops/drivetrain/target_selector_generated.h"
 #include "y2019/control_loops/superstructure/superstructure_goal_generated.h"
 
@@ -29,7 +29,8 @@
   // obstacles and just assume that we have perfect field of view.
   typedef frc971::control_loops::TypedCamera<
       y2019::constants::Field::kNumTargets,
-      /*num_obstacles=*/0, double> FakeCamera;
+      /*num_obstacles=*/0, double>
+      FakeCamera;
 
   TargetSelector(::aos::EventLoop *event_loop);
   virtual ~TargetSelector() {}
@@ -39,6 +40,12 @@
   Pose TargetPose() const override { return target_pose_; }
 
   double TargetRadius() const override { return target_radius_; }
+  double GamePieceRadius() const override { return target_radius_; }
+  bool SignedRadii() const override { return false; }
+
+  Side DriveDirection() const override { return Side::DONT_CARE; }
+
+  bool ForceReselectTarget() const override { return false; }
 
  private:
   static constexpr double kFakeFov = M_PI * 0.9;
diff --git a/y2023/BUILD b/y2023/BUILD
index fa53462..a6fec71 100644
--- a/y2023/BUILD
+++ b/y2023/BUILD
@@ -20,6 +20,7 @@
         ":aos_config",
         "//aos/starter:roborio_irq_config.json",
         "//y2023/constants:constants.json",
+        "//y2023/control_loops/superstructure/arm:arm_trajectories_generated.bfbs",
         "@ctre_phoenix_api_cpp_athena//:shared_libraries",
         "@ctre_phoenix_cci_athena//:shared_libraries",
         "@ctre_phoenixpro_api_cpp_athena//:shared_libraries",
@@ -27,11 +28,13 @@
     ],
     dirs = [
         "//y2023/www:www_files",
+        "//y2023/autonomous:splines",
     ],
     start_binaries = [
         "//aos/events/logging:logger_main",
         "//aos/network:web_proxy_main",
         "//aos/starter:irq_affinity",
+        "//y2023/autonomous:binaries",
         ":joystick_reader",
         ":wpilib_interface",
         "//aos/network:message_bridge_client",
@@ -201,6 +204,7 @@
         "//y2019/control_loops/drivetrain:target_selector_fbs",
         "//y2023/control_loops/superstructure:superstructure_goal_fbs",
         "//y2023/control_loops/drivetrain:target_selector_hint_fbs",
+        "//y2023/control_loops/drivetrain:target_selector_status_fbs",
         "//y2023/control_loops/drivetrain:drivetrain_can_position_fbs",
         "//y2023/control_loops/superstructure:superstructure_output_fbs",
         "//y2023/control_loops/superstructure:superstructure_position_fbs",
diff --git a/y2023/autonomous/auto_splines.cc b/y2023/autonomous/auto_splines.cc
index 07250e4..0cc98a2 100644
--- a/y2023/autonomous/auto_splines.cc
+++ b/y2023/autonomous/auto_splines.cc
@@ -1,26 +1,37 @@
 #include "y2023/autonomous/auto_splines.h"
 
 #include "frc971/control_loops/control_loops_generated.h"
+#include "aos/flatbuffer_merge.h"
 
 namespace y2023 {
 namespace actors {
 
-void MaybeFlipSpline(
-    aos::Sender<frc971::control_loops::drivetrain::Goal>::Builder *builder,
-    flatbuffers::Offset<flatbuffers::Vector<float>> spline_y_offset,
-    bool is_left) {
-  flatbuffers::Vector<float> *spline_y =
-      GetMutableTemporaryPointer(*builder->fbb(), spline_y_offset);
+namespace {
+flatbuffers::Offset<frc971::MultiSpline> FixSpline(
+    aos::Sender<frc971::control_loops::drivetrain::SplineGoal>::Builder
+        *builder,
+    flatbuffers::Offset<frc971::MultiSpline> spline_offset,
+    aos::Alliance alliance) {
+  frc971::MultiSpline *spline =
+      GetMutableTemporaryPointer(*builder->fbb(), spline_offset);
+  flatbuffers::Vector<float> *spline_x = spline->mutable_spline_x();
 
-  if (!is_left) {
-    for (size_t i = 0; i < spline_y->size(); i++) {
-      spline_y->Mutate(i, -spline_y->Get(i));
+  // For 2023: The field is mirrored across the center line, and is not
+  // rotationally symmetric. As such, we only flip the X coordinates when
+  // changing side of the field.
+  if (alliance == aos::Alliance::kBlue) {
+    for (size_t ii = 0; ii < spline_x->size(); ++ii) {
+      spline_x->Mutate(ii, -spline_x->Get(ii));
     }
   }
+  return spline_offset;
 }
+}  // namespace
 
 flatbuffers::Offset<frc971::MultiSpline> AutonomousSplines::BasicSSpline(
-    aos::Sender<frc971::control_loops::drivetrain::Goal>::Builder *builder) {
+    aos::Sender<frc971::control_loops::drivetrain::SplineGoal>::Builder
+        *builder,
+    aos::Alliance alliance) {
   flatbuffers::Offset<frc971::Constraint> longitudinal_constraint_offset;
   flatbuffers::Offset<frc971::Constraint> lateral_constraint_offset;
   flatbuffers::Offset<frc971::Constraint> voltage_constraint_offset;
@@ -78,11 +89,13 @@
   multispline_builder.add_spline_x(spline_x_offset);
   multispline_builder.add_spline_y(spline_y_offset);
 
-  return multispline_builder.Finish();
+  return FixSpline(builder, multispline_builder.Finish(), alliance);
 }
 
 flatbuffers::Offset<frc971::MultiSpline> AutonomousSplines::StraightLine(
-    aos::Sender<frc971::control_loops::drivetrain::Goal>::Builder *builder) {
+    aos::Sender<frc971::control_loops::drivetrain::SplineGoal>::Builder
+        *builder,
+    aos::Alliance alliance) {
   flatbuffers::Offset<flatbuffers::Vector<float>> spline_x_offset =
       builder->fbb()->CreateVector<float>(
           {-12.3, -11.9, -11.5, -11.1, -10.6, -10.0});
@@ -96,7 +109,17 @@
   multispline_builder.add_spline_x(spline_x_offset);
   multispline_builder.add_spline_y(spline_y_offset);
 
-  return multispline_builder.Finish();
+  return FixSpline(builder, multispline_builder.Finish(), alliance);
+}
+
+flatbuffers::Offset<frc971::MultiSpline> AutonomousSplines::TestSpline(
+    aos::Sender<frc971::control_loops::drivetrain::SplineGoal>::Builder
+        *builder,
+    aos::Alliance alliance) {
+  return FixSpline(
+      builder,
+      aos::CopyFlatBuffer<frc971::MultiSpline>(test_spline_, builder->fbb()),
+      alliance);
 }
 
 }  // namespace actors
diff --git a/y2023/autonomous/auto_splines.h b/y2023/autonomous/auto_splines.h
index 68795e6..1280693 100644
--- a/y2023/autonomous/auto_splines.h
+++ b/y2023/autonomous/auto_splines.h
@@ -3,6 +3,7 @@
 
 #include "aos/events/event_loop.h"
 #include "frc971/control_loops/control_loops_generated.h"
+#include "frc971/input/joystick_state_generated.h"
 #include "frc971/control_loops/drivetrain/drivetrain_goal_generated.h"
 /*
 
@@ -16,10 +17,24 @@
 
 class AutonomousSplines {
  public:
+  AutonomousSplines()
+      : test_spline_(aos::JsonFileToFlatbuffer<frc971::MultiSpline>(
+            "splines/test_spline.json")) {}
   static flatbuffers::Offset<frc971::MultiSpline> BasicSSpline(
-      aos::Sender<frc971::control_loops::drivetrain::Goal>::Builder *builder);
+      aos::Sender<frc971::control_loops::drivetrain::SplineGoal>::Builder
+          *builder,
+      aos::Alliance alliance);
   static flatbuffers::Offset<frc971::MultiSpline> StraightLine(
-      aos::Sender<frc971::control_loops::drivetrain::Goal>::Builder *builder);
+      aos::Sender<frc971::control_loops::drivetrain::SplineGoal>::Builder
+          *builder,
+      aos::Alliance alliance);
+
+  flatbuffers::Offset<frc971::MultiSpline> TestSpline(
+      aos::Sender<frc971::control_loops::drivetrain::SplineGoal>::Builder
+          *builder,
+      aos::Alliance alliance);
+ private:
+  aos::FlatbufferDetachedBuffer<frc971::MultiSpline> test_spline_;
 };
 
 }  // namespace actors
diff --git a/y2023/autonomous/autonomous_actor.cc b/y2023/autonomous/autonomous_actor.cc
index a815b25..8e99af6 100644
--- a/y2023/autonomous/autonomous_actor.cc
+++ b/y2023/autonomous/autonomous_actor.cc
@@ -8,6 +8,8 @@
 #include "frc971/control_loops/drivetrain/localizer_generated.h"
 #include "y2023/control_loops/drivetrain/drivetrain_base.h"
 
+DEFINE_bool(spline_auto, true, "Run simple test S-spline auto mode.");
+
 namespace y2023 {
 namespace actors {
 
@@ -18,11 +20,86 @@
 
 AutonomousActor::AutonomousActor(::aos::EventLoop *event_loop)
     : frc971::autonomous::BaseAutonomousActor(
-          event_loop, control_loops::drivetrain::GetDrivetrainConfig()) {}
+          event_loop, control_loops::drivetrain::GetDrivetrainConfig()),
+      localizer_control_sender_(
+          event_loop->MakeSender<
+              ::frc971::control_loops::drivetrain::LocalizerControl>(
+              "/drivetrain")),
+      joystick_state_fetcher_(
+          event_loop->MakeFetcher<aos::JoystickState>("/aos")),
+      robot_state_fetcher_(event_loop->MakeFetcher<aos::RobotState>("/aos")),
+      auto_splines_() {
+  replan_timer_ = event_loop->AddTimer([this]() { Replan(); });
+
+  event_loop->OnRun([this, event_loop]() {
+    replan_timer_->Setup(event_loop->monotonic_now());
+    button_poll_->Setup(event_loop->monotonic_now(), chrono::milliseconds(50));
+  });
+
+  // TODO(james): Really need to refactor this code since we keep using it.
+  button_poll_ = event_loop->AddTimer([this]() {
+    const aos::monotonic_clock::time_point now =
+        this->event_loop()->context().monotonic_event_time;
+    if (robot_state_fetcher_.Fetch()) {
+      if (robot_state_fetcher_->user_button()) {
+        user_indicated_safe_to_reset_ = true;
+        MaybeSendStartingPosition();
+      }
+    }
+    if (joystick_state_fetcher_.Fetch()) {
+      if (joystick_state_fetcher_->has_alliance() &&
+          (joystick_state_fetcher_->alliance() != alliance_)) {
+        alliance_ = joystick_state_fetcher_->alliance();
+        is_planned_ = false;
+        // Only kick the planning out by 2 seconds. If we end up enabled in that
+        // second, then we will kick it out further based on the code below.
+        replan_timer_->Setup(now + std::chrono::seconds(2));
+      }
+      if (joystick_state_fetcher_->enabled()) {
+        if (!is_planned_) {
+          // Only replan once we've been disabled for 5 seconds.
+          replan_timer_->Setup(now + std::chrono::seconds(5));
+        }
+      }
+    }
+  });
+}
+
+void AutonomousActor::Replan() {
+  if (alliance_ == aos::Alliance::kInvalid) {
+    return;
+  }
+  sent_starting_position_ = false;
+  if (FLAGS_spline_auto) {
+    test_spline_ =
+        PlanSpline(std::bind(&AutonomousSplines::TestSpline, &auto_splines_,
+                             std::placeholders::_1, alliance_),
+                   SplineDirection::kForward);
+
+    starting_position_ = test_spline_->starting_position();
+  }
+
+  is_planned_ = true;
+
+  MaybeSendStartingPosition();
+}
+
+void AutonomousActor::MaybeSendStartingPosition() {
+  if (is_planned_ && user_indicated_safe_to_reset_ &&
+      !sent_starting_position_) {
+    CHECK(starting_position_);
+    SendStartingPosition(starting_position_.value());
+  }
+}
 
 void AutonomousActor::Reset() {
   InitializeEncoders();
   ResetDrivetrain();
+
+  joystick_state_fetcher_.Fetch();
+  CHECK(joystick_state_fetcher_.get() != nullptr)
+      << "Expect at least one JoystickState message before running auto...";
+  alliance_ = joystick_state_fetcher_->alliance();
 }
 
 bool AutonomousActor::RunAction(
@@ -30,8 +107,58 @@
   Reset();
 
   AOS_LOG(INFO, "Params are %d\n", params->mode());
+
+  if (!user_indicated_safe_to_reset_) {
+    AOS_LOG(WARNING, "Didn't send starting position prior to starting auto.");
+    CHECK(starting_position_);
+    SendStartingPosition(starting_position_.value());
+  }
+  // Clear this so that we don't accidentally resend things as soon as we replan
+  // later.
+  user_indicated_safe_to_reset_ = false;
+  is_planned_ = false;
+  starting_position_.reset();
+
+  AOS_LOG(INFO, "Params are %d\n", params->mode());
+  if (alliance_ == aos::Alliance::kInvalid) {
+    AOS_LOG(INFO, "Aborting autonomous due to invalid alliance selection.");
+    return false;
+  }
+  if (FLAGS_spline_auto) {
+    SplineAuto();
+  } else {
+    AOS_LOG(WARNING, "No auto mode selected.");
+  }
   return true;
 }
 
+void AutonomousActor::SplineAuto() {
+  CHECK(test_spline_);
+
+  if (!test_spline_->WaitForPlan()) return;
+  test_spline_->Start();
+
+  if (!test_spline_->WaitForSplineDistanceRemaining(0.02)) return;
+}
+
+void AutonomousActor::SendStartingPosition(const Eigen::Vector3d &start) {
+  // Set up the starting position for the blue alliance.
+
+  auto builder = localizer_control_sender_.MakeBuilder();
+
+  LocalizerControl::Builder localizer_control_builder =
+      builder.MakeBuilder<LocalizerControl>();
+  localizer_control_builder.add_x(start(0));
+  localizer_control_builder.add_y(start(1));
+  localizer_control_builder.add_theta(start(2));
+  localizer_control_builder.add_theta_uncertainty(0.00001);
+  AOS_LOG(INFO, "User button pressed, x: %f y: %f theta: %f", start(0),
+          start(1), start(2));
+  if (builder.Send(localizer_control_builder.Finish()) !=
+      aos::RawSender::Error::kOk) {
+    AOS_LOG(ERROR, "Failed to reset localizer.\n");
+  }
+}
+
 }  // namespace actors
 }  // namespace y2023
diff --git a/y2023/autonomous/autonomous_actor.h b/y2023/autonomous/autonomous_actor.h
index 6eb8f90..cf0b458 100644
--- a/y2023/autonomous/autonomous_actor.h
+++ b/y2023/autonomous/autonomous_actor.h
@@ -6,6 +6,8 @@
 #include "frc971/autonomous/base_autonomous_actor.h"
 #include "frc971/control_loops/control_loops_generated.h"
 #include "frc971/control_loops/drivetrain/drivetrain_config.h"
+#include "frc971/control_loops/drivetrain/localizer_generated.h"
+#include "y2023/autonomous/auto_splines.h"
 
 namespace y2023 {
 namespace actors {
@@ -19,6 +21,29 @@
 
  private:
   void Reset();
+
+  void SendStartingPosition(const Eigen::Vector3d &start);
+  void MaybeSendStartingPosition();
+  void SplineAuto();
+  void Replan();
+
+  aos::Sender<frc971::control_loops::drivetrain::LocalizerControl>
+      localizer_control_sender_;
+  aos::Fetcher<aos::JoystickState> joystick_state_fetcher_;
+  aos::Fetcher<aos::RobotState> robot_state_fetcher_;
+
+  aos::TimerHandler *replan_timer_;
+  aos::TimerHandler *button_poll_;
+
+  std::optional<SplineHandle> test_spline_;
+  aos::Alliance alliance_ = aos::Alliance::kInvalid;
+  AutonomousSplines auto_splines_;
+  bool user_indicated_safe_to_reset_ = false;
+  bool sent_starting_position_ = false;
+
+  bool is_planned_ = false;
+
+  std::optional<Eigen::Vector3d> starting_position_;
 };
 
 }  // namespace actors
diff --git a/y2023/autonomous/splines/test_spline.json b/y2023/autonomous/splines/test_spline.json
new file mode 100644
index 0000000..7672596
--- /dev/null
+++ b/y2023/autonomous/splines/test_spline.json
@@ -0,0 +1 @@
+{"spline_count": 1, "spline_x": [6.22420997455908, 6.1347950111487386, 6.080329974810555, 6.023577036950107, 5.9617203084135255, 5.81469341092744], "spline_y": [-2.63127733767268, -2.63127733767268, -2.656484781970896, -2.656484781970896, -2.6668098529078925, -2.6448802602350456], "constraints": [{"constraint_type": "LONGITUDINAL_ACCELERATION", "value": 2}, {"constraint_type": "LATERAL_ACCELERATION", "value": 1}, {"constraint_type": "VOLTAGE", "value": 4}]}
diff --git a/y2023/constants.cc b/y2023/constants.cc
index 6a263d3..eed17c4 100644
--- a/y2023/constants.cc
+++ b/y2023/constants.cc
@@ -25,6 +25,7 @@
   auto *const arm_distal = &r.arm_distal;
   auto *const wrist = &r.wrist;
   auto *const roll_joint = &r.roll_joint;
+  r.wrist_flipped = true;
 
   arm_proximal->zeroing.average_filter_size = Values::kZeroingSampleSize;
   arm_proximal->zeroing.one_revolution_distance =
@@ -51,18 +52,18 @@
   wrist->subsystem_params.operating_voltage = 12.0;
   wrist->subsystem_params.zeroing_profile_params = {0.5, 3.0};
   wrist->subsystem_params.default_profile_params = {0.5, 5.0};
-  wrist->subsystem_params.range = Values::kWristRange();
+  wrist->subsystem_params.range = Values::kCompWristRange();
   wrist->subsystem_params.make_integral_loop =
       control_loops::superstructure::wrist::MakeIntegralWristLoop;
   wrist->subsystem_params.zeroing_constants.average_filter_size =
       Values::kZeroingSampleSize;
   wrist->subsystem_params.zeroing_constants.one_revolution_distance =
-      M_PI * 2.0 * constants::Values::kWristEncoderRatio();
+      M_PI * 2.0 * constants::Values::kCompWristEncoderRatio();
   wrist->subsystem_params.zeroing_constants.zeroing_threshold = 0.0005;
   wrist->subsystem_params.zeroing_constants.moving_buffer_size = 20;
   wrist->subsystem_params.zeroing_constants.allowable_encoder_error = 0.9;
   wrist->subsystem_params.zeroing_constants.middle_position =
-      Values::kWristRange().middle();
+      Values::kCompWristRange().middle();
 
   switch (team) {
     // A set of constants for tests.
@@ -82,17 +83,17 @@
       break;
 
     case kCompTeamNumber:
-      arm_proximal->zeroing.measured_absolute_position = 0.132182297391884;
+      arm_proximal->zeroing.measured_absolute_position = 0.138453705930275;
       arm_proximal->potentiometer_offset =
           0.931355973012855 + 8.6743197253382 - 0.101200335326309 -
-          0.0820901660993467 - 0.0703733798337964;
+          0.0820901660993467 - 0.0703733798337964 - 0.0294645384848748;
 
-      arm_distal->zeroing.measured_absolute_position = 0.597004611319487;
+      arm_distal->zeroing.measured_absolute_position = 0.562947209110251;
       arm_distal->potentiometer_offset =
           0.436664933370656 + 0.49457213779426 + 6.78213223139724 -
           0.0220711555235029 - 0.0162945074111813 + 0.00630344935527365 -
           0.0164398318919943 - 0.145833494945215 + 0.234878799868491 +
-          0.125924230298394;
+          0.125924230298394 + 0.147136306208754;
 
       roll_joint->zeroing.measured_absolute_position = 0.593975883699743;
       roll_joint->potentiometer_offset =
@@ -104,7 +105,7 @@
           0.0201047336425017 - 1.0173426655158 - 0.186085272847293 - 0.0317706563397807;
 
       wrist->subsystem_params.zeroing_constants.measured_absolute_position =
-          1.54674994866259;
+          5.78862525947414;
 
       break;
 
@@ -124,7 +125,14 @@
           0.0257708772364788 - 0.0395076737853459 - 6.87914956118006;
 
       wrist->subsystem_params.zeroing_constants.measured_absolute_position =
-          2.51265911579648;
+          0.0227022553749391;
+
+      wrist->subsystem_params.zeroing_constants.one_revolution_distance =
+          M_PI * 2.0 * constants::Values::kPracticeWristEncoderRatio();
+      wrist->subsystem_params.range = Values::kPracticeWristRange();
+      wrist->subsystem_params.zeroing_constants.middle_position =
+          Values::kPracticeWristRange().middle();
+      r.wrist_flipped = false;
 
       break;
 
diff --git a/y2023/constants.h b/y2023/constants.h
index de665d9..8996597 100644
--- a/y2023/constants.h
+++ b/y2023/constants.h
@@ -121,17 +121,34 @@
   // Wrist
   static constexpr double kWristEncoderCountsPerRevolution() { return 4096.0; }
 
-  static constexpr double kWristEncoderRatio() {
+  static constexpr double kCompWristEncoderRatio() {
+    return 1.0;
+  }
+  static constexpr double kPracticeWristEncoderRatio() {
     return (24.0 / 36.0) * (36.0 / 60.0);
   }
 
-  static constexpr double kMaxWristEncoderPulsesPerSecond() {
+  static constexpr double kMaxCompWristEncoderPulsesPerSecond() {
     return control_loops::superstructure::wrist::kFreeSpeed / (2.0 * M_PI) *
            control_loops::superstructure::wrist::kOutputRatio /
-           kWristEncoderRatio() * kWristEncoderCountsPerRevolution();
+           kCompWristEncoderRatio() * kWristEncoderCountsPerRevolution();
+  }
+  static constexpr double kMaxPracticeWristEncoderPulsesPerSecond() {
+    return control_loops::superstructure::wrist::kFreeSpeed / (2.0 * M_PI) *
+           control_loops::superstructure::wrist::kOutputRatio /
+           kPracticeWristEncoderRatio() * kWristEncoderCountsPerRevolution();
   }
 
-  static constexpr ::frc971::constants::Range kWristRange() {
+  static constexpr ::frc971::constants::Range kCompWristRange() {
+    return ::frc971::constants::Range{
+        .lower_hard = -0.10,  // Back Hard
+        .upper_hard = 4.90,   // Front Hard
+        .lower = 0.0,         // Back Soft
+        .upper = 4.0,         // Front Soft
+    };
+  }
+
+  static constexpr ::frc971::constants::Range kPracticeWristRange() {
     return ::frc971::constants::Range{
         .lower_hard = -0.10,  // Back Hard
         .upper_hard = 2.30,   // Front Hard
@@ -210,6 +227,8 @@
   ArmJointConstants roll_joint;
 
   AbsEncoderConstants wrist;
+
+  bool wrist_flipped;
 };
 
 // Creates and returns a Values instance for the constants.
diff --git a/y2023/constants/7971.json b/y2023/constants/7971.json
index 7cbb37e..a3bd130 100644
--- a/y2023/constants/7971.json
+++ b/y2023/constants/7971.json
@@ -13,6 +13,5 @@
       "calibration": {% include 'y2023/vision/calib_files/calibration_pi-7971-4_cam-23-04_ext_2023-02-19.json' %}
     }
   ],
-  "target_map": {% include 'y2023/vision/maps/target_map.json' %},
-  "scoring_map": {% include 'y2023/constants/scoring_map.json' %}
+  {% include 'y2023/constants/common.json' %}
 }
diff --git a/y2023/constants/971.json b/y2023/constants/971.json
index 7e979c7..2f91d1b 100644
--- a/y2023/constants/971.json
+++ b/y2023/constants/971.json
@@ -1,7 +1,7 @@
 {
   "cameras": [
     {
-      "calibration": {% include 'y2023/vision/calib_files/calibration_pi-971-1_cam-23-05_ext_2023-02-22.json' %}
+      "calibration": {% include 'y2023/vision/calib_files/calibration_pi-971-1_cam-23-05_ext_2023-03-05.json' %}
     },
     {
       "calibration": {% include 'y2023/vision/calib_files/calibration_pi-971-2_cam-23-06_ext_2023-02-22.json' %}
@@ -13,6 +13,19 @@
       "calibration": {% include 'y2023/vision/calib_files/calibration_pi-971-4_cam-23-08_ext_2023-02-22.json' %}
     }
   ],
-  "target_map": {% include 'y2023/vision/maps/target_map.json' %},
-  "scoring_map": {% include 'y2023/constants/scoring_map.json' %}
+  "robot": {
+    "tof": {
+      "interpolation_table": [
+        {
+          "tof_reading": 0.05,
+          "lateral_position": 0.23
+        },
+        {
+          "tof_reading": 0.90,
+          "lateral_position": -0.23
+        }
+      ]
+    }
+  },
+  {% include 'y2023/constants/common.json' %}
 }
diff --git a/y2023/constants/9971.json b/y2023/constants/9971.json
index 743db03..4795d61 100644
--- a/y2023/constants/9971.json
+++ b/y2023/constants/9971.json
@@ -1,18 +1,31 @@
 {
   "cameras": [
     {
-      "calibration": {% include 'y2023/vision/calib_files/calibration_pi-9971-1_cam-23-09_ext_from971_2023-02-23.json' %}
+      "calibration": {% include 'y2023/vision/calib_files/calibration_pi-9971-1_cam-23-09_ext_2023-03-05.json' %}
     },
     {
-      "calibration": {% include 'y2023/vision/calib_files/calibration_pi-9971-2_cam-23-10_ext_from971_2023-02-23.json' %}
+      "calibration": {% include 'y2023/vision/calib_files/calibration_pi-9971-2_cam-23-10_ext_2023-03-08.json' %}
     },
     {
-      "calibration": {% include 'y2023/vision/calib_files/calibration_pi-9971-3_cam-23-11_ext_from971_2023-02-23.json' %}
+      "calibration": {% include 'y2023/vision/calib_files/calibration_pi-9971-3_cam-23-11_ext_2023-03-05.json' %}
     },
     {
-      "calibration": {% include 'y2023/vision/calib_files/calibration_pi-9971-4_cam-23-12_ext_from971_2023-02-23.json' %}
+      "calibration": {% include 'y2023/vision/calib_files/calibration_pi-9971-4_cam-23-12_ext_2023-03-05.json' %}
     }
   ],
-  "target_map": {% include 'y2023/vision/maps/target_map.json' %},
-  "scoring_map": {% include 'y2023/constants/scoring_map.json' %}
+  "robot": {
+    "tof": {
+      "interpolation_table": [
+        {
+          "tof_reading": 0.05,
+          "lateral_position": 0.23
+        },
+        {
+          "tof_reading": 0.90,
+          "lateral_position": -0.23
+        }
+      ]
+    }
+  },
+  {% include 'y2023/constants/common.json' %}
 }
diff --git a/y2023/constants/BUILD b/y2023/constants/BUILD
index 95d04a8..c91aa56 100644
--- a/y2023/constants/BUILD
+++ b/y2023/constants/BUILD
@@ -31,6 +31,7 @@
         "7971.json",
         "971.json",
         "9971.json",
+        "common.json",
         ":scoring_map",
         "//y2023/vision/calib_files",
         "//y2023/vision/maps",
diff --git a/y2023/constants/common.json b/y2023/constants/common.json
new file mode 100644
index 0000000..c559810
--- /dev/null
+++ b/y2023/constants/common.json
@@ -0,0 +1,2 @@
+  "target_map": {% include 'y2023/vision/maps/target_map.json' %},
+  "scoring_map": {% include 'y2023/constants/scoring_map.json' %}
diff --git a/y2023/constants/constants.fbs b/y2023/constants/constants.fbs
index e874275..bd038b8 100644
--- a/y2023/constants/constants.fbs
+++ b/y2023/constants/constants.fbs
@@ -8,10 +8,32 @@
   calibration:frc971.vision.calibration.CameraCalibration (id: 0);
 }
 
+// Data point for a single time of flight sensor reading. Used in a linear
+// interpolation table.
+table TimeOfFlightDatum {
+  // Time-of-flight sensor reading for the datum.
+  tof_reading:double (id: 0);
+  // Where the game piece is laterally in the robot frame. 0 = centered;
+  // positive = to the left of the robot.
+  // In meters.
+  lateral_position:double (id: 1);
+}
+
+table TimeOfFlight {
+  interpolation_table:[TimeOfFlightDatum] (id: 0);
+}
+
+table RobotConstants {
+  // Table of time-of-flight reading positions. Until we bother using one
+  // of our interpolation classes, should just contain two values.
+  tof:TimeOfFlight (id: 0);
+}
+
 table Constants {
   cameras:[CameraConfiguration] (id: 0);
   target_map:frc971.vision.TargetMap (id: 1);
   scoring_map:localizer.ScoringMap (id: 2);
+  robot:RobotConstants (id: 3);
 }
 
 root_type Constants;
diff --git a/y2023/constants/relative_scoring_map.json b/y2023/constants/relative_scoring_map.json
index b38ff46..e52fcef 100644
--- a/y2023/constants/relative_scoring_map.json
+++ b/y2023/constants/relative_scoring_map.json
@@ -14,7 +14,7 @@
   },
   "nominal_grid": {
     "bottom": {
-      "left_cone": {
+      "right_cone": {
         "x": -0.559,
         "y": 0.463,
         "z": -0.254
@@ -24,14 +24,14 @@
         "y": 0.463,
         "z": -0.254
       },
-      "right_cone": {
+      "left_cone": {
         "x": 0.559,
         "y": 0.463,
         "z": -0.254
       }
     },
     "middle": {
-      "left_cone": {
+      "right_cone": {
         "x": -0.559,
         "y": -0.407,
         "z": 0.217
@@ -41,14 +41,14 @@
         "y": -0.407,
         "z": 0.217
       },
-      "right_cone": {
+      "left_cone": {
         "x": 0.559,
         "y": -0.407,
         "z": 0.217
       }
     },
     "top": {
-      "left_cone": {
+      "right_cone": {
         "x": -0.559,
         "y": -0.707,
         "z": 0.647
@@ -58,7 +58,7 @@
         "y": -0.707,
         "z": 0.647
       },
-      "right_cone": {
+      "left_cone": {
         "x": 0.559,
         "y": -0.707,
         "z": 0.647
@@ -67,14 +67,14 @@
   },
   "red": {
     "substation": 5,
-    "left": 3,
+    "right": 3,
     "middle": 2,
-    "right": 1
+    "left": 1
   },
   "blue": {
     "substation": 4,
-    "left": 6,
+    "right": 6,
     "middle": 7,
-    "right": 8
+    "left": 8
   }
 }
diff --git a/y2023/constants/scoring_map.json b/y2023/constants/scoring_map.json
index ebb9b38..d0fd408 100644
--- a/y2023/constants/scoring_map.json
+++ b/y2023/constants/scoring_map.json
@@ -16,51 +16,51 @@
    "bottom": {
     "left_cone": {
      "x": 6.99,
-     "y": 0.973,
+     "y": -3.497,
      "z": 0.0
     },
     "cube": {
      "x": 6.99,
-     "y": 0.414,
+     "y": -2.938,
      "z": 0.0
     },
     "right_cone": {
      "x": 6.99,
-     "y": -0.145,
+     "y": -2.379,
      "z": 0.0
     }
    },
    "middle": {
     "left_cone": {
      "x": 7.461,
-     "y": 0.973,
+     "y": -3.497,
      "z": 0.87
     },
     "cube": {
      "x": 7.461,
-     "y": 0.414,
+     "y": -2.938,
      "z": 0.87
     },
     "right_cone": {
      "x": 7.461,
-     "y": -0.145,
+     "y": -2.379,
      "z": 0.87
     }
    },
    "top": {
     "left_cone": {
      "x": 7.891,
-     "y": 0.973,
+     "y": -3.497,
      "z": 1.17
     },
     "cube": {
      "x": 7.891,
-     "y": 0.414,
+     "y": -2.938,
      "z": 1.17
     },
     "right_cone": {
      "x": 7.891,
-     "y": -0.145,
+     "y": -2.379,
      "z": 1.17
     }
    }
@@ -69,7 +69,7 @@
    "bottom": {
     "left_cone": {
      "x": 6.99,
-     "y": -0.703,
+     "y": -1.821,
      "z": 0.0
     },
     "cube": {
@@ -79,14 +79,14 @@
     },
     "right_cone": {
      "x": 6.99,
-     "y": -1.821,
+     "y": -0.703,
      "z": 0.0
     }
    },
    "middle": {
     "left_cone": {
      "x": 7.461,
-     "y": -0.703,
+     "y": -1.821,
      "z": 0.87
     },
     "cube": {
@@ -96,14 +96,14 @@
     },
     "right_cone": {
      "x": 7.461,
-     "y": -1.821,
+     "y": -0.703,
      "z": 0.87
     }
    },
    "top": {
     "left_cone": {
      "x": 7.891,
-     "y": -0.703,
+     "y": -1.821,
      "z": 1.17
     },
     "cube": {
@@ -113,7 +113,7 @@
     },
     "right_cone": {
      "x": 7.891,
-     "y": -1.821,
+     "y": -0.703,
      "z": 1.17
     }
    }
@@ -122,51 +122,51 @@
    "bottom": {
     "left_cone": {
      "x": 6.99,
-     "y": -2.379,
+     "y": -0.145,
      "z": 0.0
     },
     "cube": {
      "x": 6.99,
-     "y": -2.938,
+     "y": 0.414,
      "z": 0.0
     },
     "right_cone": {
      "x": 6.99,
-     "y": -3.497,
+     "y": 0.973,
      "z": 0.0
     }
    },
    "middle": {
     "left_cone": {
      "x": 7.461,
-     "y": -2.379,
+     "y": -0.145,
      "z": 0.87
     },
     "cube": {
      "x": 7.461,
-     "y": -2.938,
+     "y": 0.414,
      "z": 0.87
     },
     "right_cone": {
      "x": 7.461,
-     "y": -3.497,
+     "y": 0.973,
      "z": 0.87
     }
    },
    "top": {
     "left_cone": {
      "x": 7.891,
-     "y": -2.379,
+     "y": -0.145,
      "z": 1.17
     },
     "cube": {
      "x": 7.891,
-     "y": -2.938,
+     "y": 0.414,
      "z": 1.17
     },
     "right_cone": {
      "x": 7.891,
-     "y": -3.497,
+     "y": 0.973,
      "z": 1.17
     }
    }
@@ -189,51 +189,51 @@
    "bottom": {
     "left_cone": {
      "x": -6.989,
-     "y": -0.145,
+     "y": -2.379,
      "z": 0.0
     },
     "cube": {
      "x": -6.989,
-     "y": 0.414,
+     "y": -2.938,
      "z": 0.0
     },
     "right_cone": {
      "x": -6.989,
-     "y": 0.973,
+     "y": -3.497,
      "z": 0.0
     }
    },
    "middle": {
     "left_cone": {
      "x": -7.46,
-     "y": -0.145,
+     "y": -2.379,
      "z": 0.87
     },
     "cube": {
      "x": -7.46,
-     "y": 0.414,
+     "y": -2.938,
      "z": 0.87
     },
     "right_cone": {
      "x": -7.46,
-     "y": 0.973,
+     "y": -3.497,
      "z": 0.87
     }
    },
    "top": {
     "left_cone": {
      "x": -7.89,
-     "y": -0.145,
+     "y": -2.379,
      "z": 1.17
     },
     "cube": {
      "x": -7.89,
-     "y": 0.414,
+     "y": -2.938,
      "z": 1.17
     },
     "right_cone": {
      "x": -7.89,
-     "y": 0.973,
+     "y": -3.497,
      "z": 1.17
     }
    }
@@ -242,7 +242,7 @@
    "bottom": {
     "left_cone": {
      "x": -6.989,
-     "y": -1.821,
+     "y": -0.703,
      "z": 0.0
     },
     "cube": {
@@ -252,14 +252,14 @@
     },
     "right_cone": {
      "x": -6.989,
-     "y": -0.703,
+     "y": -1.821,
      "z": 0.0
     }
    },
    "middle": {
     "left_cone": {
      "x": -7.46,
-     "y": -1.821,
+     "y": -0.703,
      "z": 0.87
     },
     "cube": {
@@ -269,14 +269,14 @@
     },
     "right_cone": {
      "x": -7.46,
-     "y": -0.703,
+     "y": -1.821,
      "z": 0.87
     }
    },
    "top": {
     "left_cone": {
      "x": -7.89,
-     "y": -1.821,
+     "y": -0.703,
      "z": 1.17
     },
     "cube": {
@@ -286,7 +286,7 @@
     },
     "right_cone": {
      "x": -7.89,
-     "y": -0.703,
+     "y": -1.821,
      "z": 1.17
     }
    }
@@ -295,51 +295,51 @@
    "bottom": {
     "left_cone": {
      "x": -6.989,
-     "y": -3.497,
+     "y": 0.973,
      "z": 0.0
     },
     "cube": {
      "x": -6.989,
-     "y": -2.938,
+     "y": 0.414,
      "z": 0.0
     },
     "right_cone": {
      "x": -6.989,
-     "y": -2.379,
+     "y": -0.145,
      "z": 0.0
     }
    },
    "middle": {
     "left_cone": {
      "x": -7.46,
-     "y": -3.497,
+     "y": 0.973,
      "z": 0.87
     },
     "cube": {
      "x": -7.46,
-     "y": -2.938,
+     "y": 0.414,
      "z": 0.87
     },
     "right_cone": {
      "x": -7.46,
-     "y": -2.379,
+     "y": -0.145,
      "z": 0.87
     }
    },
    "top": {
     "left_cone": {
      "x": -7.89,
-     "y": -3.497,
+     "y": 0.973,
      "z": 1.17
     },
     "cube": {
      "x": -7.89,
-     "y": -2.938,
+     "y": 0.414,
      "z": 1.17
     },
     "right_cone": {
      "x": -7.89,
-     "y": -2.379,
+     "y": -0.145,
      "z": 1.17
     }
    }
diff --git a/y2023/control_loops/drivetrain/BUILD b/y2023/control_loops/drivetrain/BUILD
index 32b8c15..521fc09 100644
--- a/y2023/control_loops/drivetrain/BUILD
+++ b/y2023/control_loops/drivetrain/BUILD
@@ -125,12 +125,24 @@
 )
 
 flatbuffer_cc_library(
+    name = "target_selector_status_fbs",
+    srcs = [
+        ":target_selector_status.fbs",
+    ],
+    gen_reflections = 1,
+    visibility = ["//visibility:public"],
+)
+
+flatbuffer_cc_library(
     name = "target_selector_hint_fbs",
     srcs = [
         ":target_selector_hint.fbs",
     ],
     gen_reflections = 1,
     visibility = ["//visibility:public"],
+    deps = [
+        "//frc971/control_loops/drivetrain:drivetrain_status_fbs",
+    ],
 )
 
 cc_library(
@@ -139,13 +151,17 @@
     hdrs = ["target_selector.h"],
     deps = [
         ":target_selector_hint_fbs",
+        ":target_selector_status_fbs",
         "//aos/containers:sized_array",
         "//aos/events:event_loop",
         "//frc971/constants:constants_sender_lib",
         "//frc971/control_loops:pose",
         "//frc971/control_loops/drivetrain:localizer",
         "//frc971/input:joystick_state_fbs",
+        "//frc971/shooter_interpolation:interpolation",
         "//y2023/constants:constants_fbs",
+        "//y2023/control_loops/superstructure:superstructure_position_fbs",
+        "//y2023/control_loops/superstructure:superstructure_status_fbs",
     ],
 )
 
diff --git a/y2023/control_loops/drivetrain/target_selector.cc b/y2023/control_loops/drivetrain/target_selector.cc
index 99805cb..aacfbbf 100644
--- a/y2023/control_loops/drivetrain/target_selector.cc
+++ b/y2023/control_loops/drivetrain/target_selector.cc
@@ -1,16 +1,44 @@
 #include "y2023/control_loops/drivetrain/target_selector.h"
 
 #include "aos/containers/sized_array.h"
+#include "frc971/shooter_interpolation/interpolation.h"
+#include "y2023/control_loops/superstructure/superstructure_position_generated.h"
 
 namespace y2023::control_loops::drivetrain {
+namespace {
+// If we already have a target selected, require the robot to be closer than
+// this distance (in meters) to one target than another before swapping.
+constexpr double kGridHysteresisDistance = 0.1;
+}  // namespace
+
 TargetSelector::TargetSelector(aos::EventLoop *event_loop)
     : joystick_state_fetcher_(
           event_loop->MakeFetcher<aos::JoystickState>("/aos")),
       hint_fetcher_(event_loop->MakeFetcher<TargetSelectorHint>("/drivetrain")),
+      superstructure_status_fetcher_(event_loop->MakeFetcher<superstructure::Status>("/superstructure")),
+      status_sender_(
+          event_loop->MakeSender<TargetSelectorStatus>("/drivetrain")),
       constants_fetcher_(event_loop) {
   CHECK(constants_fetcher_.constants().has_scoring_map());
   CHECK(constants_fetcher_.constants().scoring_map()->has_red());
   CHECK(constants_fetcher_.constants().scoring_map()->has_blue());
+  event_loop->MakeWatcher(
+      "/superstructure",
+      [this](const y2023::control_loops::superstructure::Position &msg) {
+        // Technically this means that even if we have a cube we are relying on
+        // getting a Position message before updating the game_piece_position_
+        // to zero. But if we aren't getting position messages, then things are
+        // very broken.
+        game_piece_position_ =
+            LateralOffsetForTimeOfFlight(msg.cone_position());
+      });
+
+  event_loop->AddPhasedLoop([this](int){
+      auto builder = status_sender_.MakeBuilder();
+      auto status_builder = builder.MakeBuilder<TargetSelectorStatus>();
+      status_builder.add_game_piece_position(game_piece_position_);
+      builder.CheckOk(builder.Send(status_builder.Finish()));
+      }, std::chrono::milliseconds(100));
 }
 
 void TargetSelector::UpdateAlliance() {
@@ -44,6 +72,17 @@
     // We don't know where to go, wait on a hint.
     return false;
   }
+  // Keep track of when the hint changes (note that this will not detect default
+  // vs. not populated default values); when it changes, force us to reselect
+  // the target.
+  {
+    TargetSelectorHintT hint_object;
+    hint_fetcher_.get()->UnPackTo(&hint_object);
+    if (!last_hint_.has_value() || hint_object != last_hint_) {
+      target_pose_.reset();
+    }
+    last_hint_ = hint_object;
+  }
   aos::SizedArray<const localizer::ScoringGrid *, 3> possible_grids;
   if (hint_fetcher_->has_grid()) {
     possible_grids = {[this]() -> const localizer::ScoringGrid * {
@@ -102,19 +141,66 @@
         return positions;
       }();
   CHECK_LT(0u, possible_positions.size());
+  aos::SizedArray<double, 3> distances;
   std::optional<double> closest_distance;
   std::optional<Eigen::Vector3d> closest_position;
   const Eigen::Vector3d robot_position(state.x(), state.y(), 0.0);
   for (const frc971::vision::Position *position : possible_positions) {
     const Eigen::Vector3d target(position->x(), position->y(), position->z());
     double distance = (target - robot_position).norm();
+    distances.push_back(distance);
     if (!closest_distance.has_value() || distance < closest_distance.value()) {
       closest_distance = distance;
       closest_position = target;
     }
   }
-  CHECK(closest_position.has_value());
-  target_pose_ = Pose(closest_position.value(), /*theta=*/0.0);
+  std::sort(distances.begin(), distances.end());
+  CHECK_EQ(distances.at(0), closest_distance.value());
+  // Only change the target pose if one grid is clearly better than the other.
+  // This prevents us from dithering between two grids if we happen to be on the
+  // boundary.
+  if (!target_pose_.has_value() ||
+      distances.at(1) - distances.at(0) > kGridHysteresisDistance) {
+    CHECK(closest_position.has_value());
+    target_pose_ = Pose(closest_position.value(), /*theta=*/0.0);
+    if (hint_fetcher_->has_robot_side()) {
+      drive_direction_ = hint_fetcher_->robot_side();
+    } else {
+      drive_direction_ = Side::DONT_CARE;
+    }
+  }
+  CHECK(target_pose_.has_value());
   return true;
 }
+
+// TODO: Maybe this already handles field side correctly? Unsure if the line
+// follower ends up having positive as being robot frame relative or robot
+// direction relative...
+double TargetSelector::LateralOffsetForTimeOfFlight(double reading) {
+  superstructure_status_fetcher_.Fetch();
+  if (superstructure_status_fetcher_.get() != nullptr) {
+    switch (superstructure_status_fetcher_->game_piece()) {
+      case superstructure::GamePiece::NONE:
+      case superstructure::GamePiece::CUBE:
+        return 0.0;
+      case superstructure::GamePiece::CONE:
+        // execute logic below.
+        break;
+    }
+  } else {
+    return 0.0;
+  }
+  const TimeOfFlight *calibration =
+      CHECK_NOTNULL(constants_fetcher_.constants().robot()->tof());
+  // TODO(james): Use a generic interpolation table class.
+  auto table = CHECK_NOTNULL(calibration->interpolation_table());
+  CHECK_EQ(2u, table->size());
+  double x1 = table->Get(0)->tof_reading();
+  double x2 = table->Get(1)->tof_reading();
+  double y1 = table->Get(0)->lateral_position();
+  double y2 = table->Get(1)->lateral_position();
+  return frc971::shooter_interpolation::Blend((reading - x1) / (x2 - x1), y1,
+                                              y2);
+}
+
 }  // namespace y2023::control_loops::drivetrain
diff --git a/y2023/control_loops/drivetrain/target_selector.h b/y2023/control_loops/drivetrain/target_selector.h
index 0290425..bed56ce 100644
--- a/y2023/control_loops/drivetrain/target_selector.h
+++ b/y2023/control_loops/drivetrain/target_selector.h
@@ -6,6 +6,8 @@
 #include "frc971/input/joystick_state_generated.h"
 #include "y2023/constants/constants_generated.h"
 #include "y2023/control_loops/drivetrain/target_selector_hint_generated.h"
+#include "y2023/control_loops/drivetrain/target_selector_status_generated.h"
+#include "y2023/control_loops/superstructure/superstructure_status_generated.h"
 
 namespace y2023::control_loops::drivetrain {
 // This target selector provides the logic to choose which position to try to
@@ -23,6 +25,7 @@
     : public frc971::control_loops::drivetrain::TargetSelectorInterface {
  public:
   typedef frc971::control_loops::TypedPose<double> Pose;
+  typedef frc971::control_loops::drivetrain::RobotSide Side;
 
   TargetSelector(aos::EventLoop *event_loop);
 
@@ -36,14 +39,26 @@
   }
 
   double TargetRadius() const override { return 0.0; }
+  double GamePieceRadius() const override { return game_piece_position_; }
+  bool SignedRadii() const override { return true; }
+  Side DriveDirection() const override { return drive_direction_; }
+  // We will manage any desired hysteresis in the target selection.
+  bool ForceReselectTarget() const override { return true; }
 
  private:
   void UpdateAlliance();
+  // Returns the Y coordinate of a game piece given the time-of-flight reading.
+  double LateralOffsetForTimeOfFlight(double reading);
   std::optional<Pose> target_pose_;
   aos::Fetcher<aos::JoystickState> joystick_state_fetcher_;
   aos::Fetcher<TargetSelectorHint> hint_fetcher_;
+  aos::Fetcher<superstructure::Status> superstructure_status_fetcher_;
+  aos::Sender<TargetSelectorStatus> status_sender_;
+  std::optional<TargetSelectorHintT> last_hint_;
   frc971::constants::ConstantsFetcher<Constants> constants_fetcher_;
   const localizer::HalfField *scoring_map_ = nullptr;
+  double game_piece_position_ = 0.0;
+  Side drive_direction_ = Side::DONT_CARE;
 };
 }  // namespace y2023::control_loops::drivetrain
 #endif  // Y2023_CONTROL_LOOPS_DRIVETRAIN_TARGET_SELECTOR_H_
diff --git a/y2023/control_loops/drivetrain/target_selector_hint.fbs b/y2023/control_loops/drivetrain/target_selector_hint.fbs
index 518d302..357bc21 100644
--- a/y2023/control_loops/drivetrain/target_selector_hint.fbs
+++ b/y2023/control_loops/drivetrain/target_selector_hint.fbs
@@ -1,7 +1,9 @@
+include "frc971/control_loops/drivetrain/drivetrain_status.fbs";
+
 namespace y2023.control_loops.drivetrain;
 
 // Which of the grids we are going for.
-// From the perspective of the robot!
+// From the perspective of the driver station!
 enum GridSelectionHint : ubyte {
   LEFT,
   MIDDLE,
@@ -16,20 +18,19 @@
 }
 
 // Within a row, which spot to score in.
-// From the perspective of the robot!
+// From the perspective of the driver station!
 enum SpotSelectionHint : ubyte {
   LEFT,
   MIDDLE,
   RIGHT,
 }
 
-
 table TargetSelectorHint {
   grid:GridSelectionHint (id: 0);
   row:RowSelectionHint (id: 1);
   spot:SpotSelectionHint (id: 2);
+  robot_side:frc971.control_loops.drivetrain.RobotSide = DONT_CARE (id: 3);
   // TODO: support human player pickup auto-align?
-  // TODO: add flag for forwards vs. backwards.
 }
 
 root_type TargetSelectorHint;
diff --git a/y2023/control_loops/drivetrain/target_selector_status.fbs b/y2023/control_loops/drivetrain/target_selector_status.fbs
new file mode 100644
index 0000000..2ca0a91
--- /dev/null
+++ b/y2023/control_loops/drivetrain/target_selector_status.fbs
@@ -0,0 +1,7 @@
+namespace y2023.control_loops.drivetrain;
+
+table TargetSelectorStatus {
+  game_piece_position:double (id: 0);
+}
+
+root_type TargetSelectorStatus;
diff --git a/y2023/control_loops/drivetrain/target_selector_test.cc b/y2023/control_loops/drivetrain/target_selector_test.cc
index fdbd904..c28c14d 100644
--- a/y2023/control_loops/drivetrain/target_selector_test.cc
+++ b/y2023/control_loops/drivetrain/target_selector_test.cc
@@ -4,6 +4,8 @@
 #include "gtest/gtest.h"
 #include "y2023/constants/simulated_constants_sender.h"
 
+using Side = frc971::control_loops::drivetrain::RobotSide;
+
 namespace y2023::control_loops::drivetrain {
 class TargetSelectorTest : public ::testing::Test {
  protected:
@@ -38,17 +40,18 @@
   }
 
   void SendHint(GridSelectionHint grid, RowSelectionHint row,
-                SpotSelectionHint spot) {
+                SpotSelectionHint spot, Side side) {
     auto builder = hint_sender_.MakeBuilder();
     builder.CheckOk(builder.Send(
-        CreateTargetSelectorHint(*builder.fbb(), grid, row, spot)));
+        CreateTargetSelectorHint(*builder.fbb(), grid, row, spot, side)));
   }
-  void SendHint(RowSelectionHint row, SpotSelectionHint spot) {
+  void SendHint(RowSelectionHint row, SpotSelectionHint spot, Side side) {
     auto builder = hint_sender_.MakeBuilder();
     TargetSelectorHint::Builder hint_builder =
         builder.MakeBuilder<TargetSelectorHint>();
     hint_builder.add_row(row);
     hint_builder.add_spot(spot);
+    hint_builder.add_robot_side(side);
     builder.CheckOk(builder.Send(hint_builder.Finish()));
   }
 
@@ -95,13 +98,17 @@
                {row->left_cone(), SpotSelectionHint::LEFT},
                {row->cube(), SpotSelectionHint::MIDDLE},
                {row->right_cone(), SpotSelectionHint::RIGHT}}) {
-        SendHint(grid_hint, row_hint, spot_hint);
+        SendHint(grid_hint, row_hint, spot_hint, Side::FRONT);
         EXPECT_TRUE(target_selector_.UpdateSelection(
             Eigen::Matrix<double, 5, 1>::Zero(), 0.0));
         EXPECT_EQ(0.0, target_selector_.TargetRadius());
         EXPECT_EQ(spot->x(), target_selector_.TargetPose().abs_pos().x());
         EXPECT_EQ(spot->y(), target_selector_.TargetPose().abs_pos().y());
         EXPECT_EQ(spot->z(), target_selector_.TargetPose().abs_pos().z());
+        EXPECT_EQ(frc971::control_loops::drivetrain::TargetSelectorInterface::
+                      Side::FRONT,
+                  target_selector_.DriveDirection());
+        EXPECT_TRUE(target_selector_.ForceReselectTarget());
       }
     }
   }
@@ -125,13 +132,57 @@
             SpotSelectionHint::MIDDLE},
            {scoring_map()->left_grid()->bottom()->right_cone(),
             SpotSelectionHint::RIGHT}}) {
-    SendHint(RowSelectionHint::BOTTOM, spot_hint);
+    SendHint(RowSelectionHint::BOTTOM, spot_hint, Side::BACK);
     EXPECT_TRUE(target_selector_.UpdateSelection(
         Eigen::Matrix<double, 5, 1>::Zero(), 0.0));
+    EXPECT_TRUE(target_selector_.ForceReselectTarget());
     EXPECT_EQ(spot->x(), target_selector_.TargetPose().abs_pos().x());
     EXPECT_EQ(spot->y(), target_selector_.TargetPose().abs_pos().y());
     EXPECT_EQ(spot->z(), target_selector_.TargetPose().abs_pos().z());
+    EXPECT_EQ(
+        frc971::control_loops::drivetrain::TargetSelectorInterface::Side::BACK,
+        target_selector_.DriveDirection());
   }
 }
 
+// Tests that if we are on the boundary of two grids that we do apply some
+// hysteresis.
+TEST_F(TargetSelectorTest, GridHysteresis) {
+  SendJoystickState();
+  // We will leave the robot at (0, 0). This means that if we are going for the
+  // left cone we should go for the middle grid, and if we are going for the
+  // cube (middle) or right cone positions we should prefer the left grid.
+  // Note that the grids are not centered on the field (hence the middle isn't
+  // always preferred when at (0, 0)).
+
+  const frc971::vision::Position *left_pos =
+      scoring_map()->left_grid()->bottom()->cube();
+  const frc971::vision::Position *middle_pos =
+      scoring_map()->middle_grid()->bottom()->cube();
+  Eigen::Matrix<double, 5, 1> split_position;
+  split_position << 0.0, (left_pos->y() + middle_pos->y()) / 2.0, 0.0, 0.0, 0.0;
+  Eigen::Matrix<double, 5, 1> slightly_left = split_position;
+  slightly_left.y() += 0.01;
+  Eigen::Matrix<double, 5, 1> slightly_middle = split_position;
+  slightly_middle.y() -= 0.01;
+  Eigen::Matrix<double, 5, 1> very_middle = split_position;
+  very_middle.y() -= 1.0;
+
+  SendHint(RowSelectionHint::BOTTOM, SpotSelectionHint::MIDDLE, Side::BACK);
+  EXPECT_TRUE(target_selector_.UpdateSelection(slightly_left, 0.0));
+  Eigen::Vector3d target = target_selector_.TargetPose().abs_pos();
+  EXPECT_EQ(target.x(), left_pos->x());
+  EXPECT_EQ(target.y(), left_pos->y());
+  // A slight movement should *not* reset things.
+  EXPECT_TRUE(target_selector_.UpdateSelection(slightly_middle, 0.0));
+  target = target_selector_.TargetPose().abs_pos();
+  EXPECT_EQ(target.x(), left_pos->x());
+  EXPECT_EQ(target.y(), left_pos->y());
+  // A large movement *should* reset things.
+  EXPECT_TRUE(target_selector_.UpdateSelection(very_middle, 0.0));
+  target = target_selector_.TargetPose().abs_pos();
+  EXPECT_EQ(target.x(), middle_pos->x());
+  EXPECT_EQ(target.y(), middle_pos->y());
+}
+
 }  // namespace y2023::control_loops::drivetrain
diff --git a/y2023/control_loops/python/graph_codegen.py b/y2023/control_loops/python/graph_codegen.py
index 6f3bc0d..289913f 100644
--- a/y2023/control_loops/python/graph_codegen.py
+++ b/y2023/control_loops/python/graph_codegen.py
@@ -37,12 +37,13 @@
     cc_file.append("                             %s," % (alpha_unitizer))
     if reverse:
         cc_file.append(
-            "                             Trajectory(dynamics, &hybrid_roll_joint_loop->plant(), Path::Reversed(%s()), 0.005));"
+            "                             Trajectory(dynamics, &hybrid_roll_joint_loop->plant(), Path::Reversed(%s()), 0.005), "
             % (path_function_name(str(name))))
     else:
         cc_file.append(
-            "                             Trajectory(dynamics, &hybrid_roll_joint_loop->plant(), %s(), 0.005));"
+            "                             Trajectory(dynamics, &hybrid_roll_joint_loop->plant(), %s(), 0.005), "
             % (path_function_name(str(name))))
+    cc_file.append(f"\"{path_function_name(str(name))}\");")
 
     start_index = None
     end_index = None
@@ -116,6 +117,9 @@
         "#include \"y2023/control_loops/superstructure/arm/arm_constants.h\"")
     h_file.append(
         "#include \"y2023/control_loops/superstructure/arm/trajectory.h\"")
+    h_file.append(
+        "#include \"y2023/control_loops/superstructure/arm/arm_trajectories_generated.h\""
+    )
 
     h_file.append("")
     h_file.append("namespace y2023 {")
@@ -130,19 +134,39 @@
     h_file.append(
         "using y2023::control_loops::superstructure::arm::kArmConstants;")
 
+    h_file.append(
+        "using y2023::control_loops::superstructure::arm::TrajectoryAndParamsFbs;"
+    )
+
     h_file.append("")
     h_file.append("struct TrajectoryAndParams {")
     h_file.append("  TrajectoryAndParams(double new_vmax,")
     h_file.append(
         "                      const ::Eigen::Matrix<double, 3, 3> &new_alpha_unitizer,"
     )
-    h_file.append("                      Trajectory &&new_trajectory)")
+    h_file.append(
+        "                      Trajectory &&new_trajectory, std::string_view new_name)"
+    )
     h_file.append("      : vmax(new_vmax),")
     h_file.append("        alpha_unitizer(new_alpha_unitizer),")
-    h_file.append("        trajectory(::std::move(new_trajectory)) {}")
+    h_file.append("        trajectory(::std::move(new_trajectory)),")
+    h_file.append("         name(new_name) {}")
+    h_file.append(
+        "TrajectoryAndParams(const frc971::control_loops::arm::Dynamics *dynamics, const StateFeedbackHybridPlant<3, 1, 1> *roll, const TrajectoryAndParamsFbs &trajectory_and_params_fbs)"
+    )
+    h_file.append(": vmax(trajectory_and_params_fbs.vmax()),")
+    h_file.append(
+        "alpha_unitizer((trajectory_and_params_fbs.alpha_unitizer()->data(),")
+    h_file.append("     trajectory_and_params_fbs.alpha_unitizer()->data() +")
+    h_file.append(
+        "         trajectory_and_params_fbs.alpha_unitizer()->size())),")
+    h_file.append(
+        "trajectory(dynamics, roll, *trajectory_and_params_fbs.trajectory()),")
+    h_file.append("name(trajectory_and_params_fbs.name()->string_view()) {}")
     h_file.append("  double vmax;")
     h_file.append("  ::Eigen::Matrix<double, 3, 3> alpha_unitizer;")
     h_file.append("  Trajectory trajectory;")
+    h_file.append(" std::string_view name;")
     h_file.append("};")
     h_file.append("")
 
diff --git a/y2023/control_loops/python/graph_edit.py b/y2023/control_loops/python/graph_edit.py
index a976807..f18a0b6 100644
--- a/y2023/control_loops/python/graph_edit.py
+++ b/y2023/control_loops/python/graph_edit.py
@@ -1,6 +1,8 @@
 #!/usr/bin/python3
 
 from __future__ import print_function
+# matplotlib overrides fontconfig locations, so it needs to be imported before gtk.
+import matplotlib.pyplot as plt
 import os
 from frc971.control_loops.python import basic_window
 from frc971.control_loops.python.color import Color, palette
@@ -21,8 +23,6 @@
 import shapely
 from shapely.geometry import Polygon
 
-import matplotlib.pyplot as plt
-
 
 def px(cr):
     return OverrideMatrix(cr, identity)
@@ -247,9 +247,6 @@
                                     [DRIVER_CAM_X, DRIVER_CAM_Y],
                                     DRIVER_CAM_WIDTH, DRIVER_CAM_HEIGHT)
 
-    def do_key_press(self, event):
-        pass
-
     def _do_button_press_internal(self, event):
         o_x = event.x
         o_y = event.y
@@ -376,16 +373,21 @@
         for i in range(len(self.segments)):
             color = None
             if i == self.index:
-                # Draw current spline in black
-                color = [0, 0, 0]
-            else:
-                color = [0, random.random(), 1]
-                random.shuffle(color)
+                continue
+            color = [0, random.random(), 1]
+            random.shuffle(color)
             set_color(cr, Color(color[0], color[1], color[2]))
             self.segments[i].DrawTo(cr, self.theta_version)
             with px(cr):
                 cr.stroke()
 
+        # Draw current spline in black
+        color = [0, 0, 0]
+        set_color(cr, Color(color[0], color[1], color[2]))
+        self.segments[self.index].DrawTo(cr, self.theta_version)
+        with px(cr):
+            cr.stroke()
+
         set_color(cr, Color(0.0, 1.0, 0.5))
 
         # Create the plots
diff --git a/y2023/control_loops/python/graph_paths.py b/y2023/control_loops/python/graph_paths.py
index 6c74a74..f7571a6 100644
--- a/y2023/control_loops/python/graph_paths.py
+++ b/y2023/control_loops/python/graph_paths.py
@@ -298,7 +298,7 @@
     ))
 
 points['ScoreBackLowCube'] = to_theta_with_circular_index_and_roll(
-    -1.102, 0.30, -np.pi / 2.0, circular_index=1)
+    -1.102, 0.3212121, -np.pi / 2.0, circular_index=1)
 
 named_segments.append(
     ThetaSplineSegment(
diff --git a/y2023/control_loops/python/wrist.py b/y2023/control_loops/python/wrist.py
index 60ae8a6..fe2fcf6 100644
--- a/y2023/control_loops/python/wrist.py
+++ b/y2023/control_loops/python/wrist.py
@@ -20,7 +20,7 @@
 kWrist = angular_system.AngularSystemParams(
     name='Wrist',
     motor=control_loop.BAG(),
-    G=(6.0 / 48.0) * (20.0 / 100.0) * (24.0 / 36.0) * (36.0 / 60.0),
+    G=(6.0 / 48.0) * (20.0 / 100.0) * (18.0 / 24.0) * (24.0 / 44.0),
     # Use parallel axis theorem to get the moment of inertia around
     # the joint (I = I_cm + mh^2 = 0.001877 + 0.8332 * 0.0407162^2)
     J=0.003258,
diff --git a/y2023/control_loops/superstructure/BUILD b/y2023/control_loops/superstructure/BUILD
index 5da5250..a4cb337 100644
--- a/y2023/control_loops/superstructure/BUILD
+++ b/y2023/control_loops/superstructure/BUILD
@@ -10,9 +10,9 @@
         "superstructure_goal.fbs",
     ],
     gen_reflections = 1,
-    includes = [
-        "//frc971/control_loops:control_loops_fbs_includes",
-        "//frc971/control_loops:profiled_subsystem_fbs_includes",
+    deps = [
+        "//frc971/control_loops:control_loops_fbs",
+        "//frc971/control_loops:profiled_subsystem_fbs",
     ],
 )
 
@@ -30,9 +30,9 @@
         "superstructure_status.fbs",
     ],
     gen_reflections = 1,
-    includes = [
-        "//frc971/control_loops:control_loops_fbs_includes",
-        "//frc971/control_loops:profiled_subsystem_fbs_includes",
+    deps = [
+        "//frc971/control_loops:control_loops_fbs",
+        "//frc971/control_loops:profiled_subsystem_fbs",
     ],
 )
 
@@ -53,11 +53,11 @@
         "superstructure_position.fbs",
     ],
     gen_reflections = 1,
-    includes = [
-        "//frc971/control_loops:control_loops_fbs_includes",
-        "//frc971/control_loops:profiled_subsystem_fbs_includes",
-        "//frc971/vision:calibration_fbs_includes",
-        "//y2023/control_loops/drivetrain:drivetrain_can_position_fbs_includes",
+    deps = [
+        "//frc971/control_loops:control_loops_fbs",
+        "//frc971/control_loops:profiled_subsystem_fbs",
+        "//frc971/vision:calibration_fbs",
+        "//y2023/control_loops/drivetrain:drivetrain_can_position_fbs",
     ],
 )
 
@@ -87,6 +87,9 @@
     hdrs = [
         "superstructure.h",
     ],
+    data = [
+        "//y2023/control_loops/superstructure/arm:arm_trajectories_generated.bfbs",
+    ],
     deps = [
         ":end_effector",
         ":superstructure_goal_fbs",
@@ -100,6 +103,7 @@
         "//y2023:constants",
         "//y2023/control_loops/drivetrain:drivetrain_can_position_fbs",
         "//y2023/control_loops/superstructure/arm",
+        "//y2023/control_loops/superstructure/arm:arm_trajectories_fbs",
     ],
 )
 
@@ -129,6 +133,7 @@
         ":superstructure_output_fbs",
         ":superstructure_position_fbs",
         ":superstructure_status_fbs",
+        "//aos:json_to_flatbuffer",
         "//aos:math",
         "//aos/events/logging:log_writer",
         "//aos/testing:googletest",
diff --git a/y2023/control_loops/superstructure/arm/BUILD b/y2023/control_loops/superstructure/arm/BUILD
index e1e3392..44b2aac 100644
--- a/y2023/control_loops/superstructure/arm/BUILD
+++ b/y2023/control_loops/superstructure/arm/BUILD
@@ -1,3 +1,5 @@
+load("@com_github_google_flatbuffers//:build_defs.bzl", "flatbuffer_cc_library")
+
 cc_library(
     name = "arm",
     srcs = [
@@ -24,6 +26,39 @@
 )
 
 genrule(
+    name = "generated_arm_trajectory_genrule",
+    outs = [
+        "arm_trajectories_generated.bfbs",
+    ],
+    cmd = "$(location //y2023/control_loops/superstructure/arm:arm_trajectory_generator) --output $(OUTS)",
+    target_compatible_with = ["@platforms//os:linux"],
+    tools = [
+        "//y2023/control_loops/superstructure/arm:arm_trajectory_generator",
+    ],
+    visibility = ["//visibility:public"],
+)
+
+cc_binary(
+    name = "arm_trajectory_generator",
+    srcs = [
+        "arm_trajectory_gen.cc",
+    ],
+    target_compatible_with = ["@platforms//os:linux"],
+    visibility = ["//visibility:public"],
+    deps = [
+        ":arm_constants",
+        ":arm_trajectories_fbs",
+        "//aos:flatbuffers",
+        "//aos:json_to_flatbuffer",
+        "//frc971/control_loops/double_jointed_arm:graph",
+        "//y2023:constants",
+        "//y2023/control_loops/superstructure/arm:generated_graph",
+        "//y2023/control_loops/superstructure/arm:trajectory",
+        "//y2023/control_loops/superstructure/roll:roll_plants",
+    ],
+)
+
+genrule(
     name = "generated_graph_genrule",
     outs = [
         "generated_graph.h",
@@ -36,6 +71,15 @@
     ],
 )
 
+flatbuffer_cc_library(
+    name = "arm_trajectories_fbs",
+    srcs = [
+        "arm_trajectories.fbs",
+    ],
+    gen_reflections = 1,
+    visibility = ["//visibility:public"],
+)
+
 cc_library(
     name = "generated_graph",
     srcs = [
@@ -86,6 +130,7 @@
     srcs = ["trajectory.cc"],
     hdrs = ["trajectory.h"],
     target_compatible_with = ["@platforms//os:linux"],
+    visibility = ["//visibility:public"],
     deps = [
         "//frc971/control_loops:binomial",
         "//frc971/control_loops:dlqr",
@@ -93,6 +138,7 @@
         "//frc971/control_loops:hybrid_state_feedback_loop",
         "//frc971/control_loops/double_jointed_arm:dynamics",
         "//frc971/control_loops/double_jointed_arm:ekf",
+        "//y2023/control_loops/superstructure/arm:arm_trajectories_fbs",
         "@org_tuxfamily_eigen//:eigen",
     ],
 )
diff --git a/y2023/control_loops/superstructure/arm/arm.cc b/y2023/control_loops/superstructure/arm/arm.cc
index 27a1450..6cd8d0d 100644
--- a/y2023/control_loops/superstructure/arm/arm.cc
+++ b/y2023/control_loops/superstructure/arm/arm.cc
@@ -16,7 +16,8 @@
 
 }  // namespace
 
-Arm::Arm(std::shared_ptr<const constants::Values> values)
+Arm::Arm(std::shared_ptr<const constants::Values> values,
+         const ArmTrajectories &arm_trajectories)
     : values_(values),
       state_(ArmState::UNINITIALIZED),
       proximal_zeroing_estimator_(values_->arm_proximal.zeroing),
@@ -36,13 +37,17 @@
       roll_joint_loop_(roll::MakeIntegralRollLoop()),
       hybrid_roll_joint_loop_(roll::MakeIntegralHybridRollLoop()),
       arm_ekf_(&dynamics_),
-      search_graph_(MakeSearchGraph(&dynamics_, &trajectories_, alpha_unitizer_,
-                                    constants::Values::kArmVMax(),
-                                    &hybrid_roll_joint_loop_)),
+      search_graph_(GetSearchGraph(arm_trajectories)),
       // Go to the start of the first trajectory.
       follower_(&dynamics_, &hybrid_roll_joint_loop_, NeutralPoint()),
       points_(PointList()),
       current_node_(0) {
+  // Creating trajectories from fbs
+  for (const auto *trajectory : *arm_trajectories.trajectories()) {
+    trajectories_.emplace_back(&dynamics_, &hybrid_roll_joint_loop_.plant(),
+                               *trajectory);
+  }
+
   int i = 0;
   for (const auto &trajectory : trajectories_) {
     AOS_LOG(INFO, "trajectory length for edge node %d: %f\n", i,
@@ -54,7 +59,6 @@
 void Arm::Reset() { state_ = ArmState::UNINITIALIZED; }
 
 namespace {
-
 // Proximal joint center in xy space
 constexpr std::pair<double, double> kJointCenter = {-0.203, 0.787};
 
diff --git a/y2023/control_loops/superstructure/arm/arm.h b/y2023/control_loops/superstructure/arm/arm.h
index 216b89d..1f97d80 100644
--- a/y2023/control_loops/superstructure/arm/arm.h
+++ b/y2023/control_loops/superstructure/arm/arm.h
@@ -21,7 +21,8 @@
 
 class Arm {
  public:
-  Arm(std::shared_ptr<const constants::Values> values);
+  Arm(std::shared_ptr<const constants::Values> values,
+      const ArmTrajectories &arm_trajectories);
 
   flatbuffers::Offset<superstructure::ArmStatus> Iterate(
       const ::aos::monotonic_clock::time_point /*monotonic_now*/,
@@ -52,6 +53,23 @@
            follower_.path_distance_to_go() < 1e-3;
   }
 
+  static SearchGraph GetSearchGraph(const ArmTrajectories &arm_trajectories) {
+    // Creating edges from fbs
+    std::vector<SearchGraph::Edge> edges;
+
+    for (const auto *edge : *arm_trajectories.edges()) {
+      SearchGraph::Edge edge_data{
+          .start = static_cast<size_t>(edge->start()),
+          .end = static_cast<size_t>(edge->end()),
+          .cost = edge->cost(),
+      };
+
+      edges.emplace_back(edge_data);
+    }
+
+    return SearchGraph(edges.size(), std::move(edges));
+  }
+
   std::shared_ptr<const constants::Values> values_;
 
   ArmState state_;
diff --git a/y2023/control_loops/superstructure/arm/arm_trajectories.fbs b/y2023/control_loops/superstructure/arm/arm_trajectories.fbs
new file mode 100644
index 0000000..b0570f0
--- /dev/null
+++ b/y2023/control_loops/superstructure/arm/arm_trajectories.fbs
@@ -0,0 +1,56 @@
+namespace y2023.control_loops.superstructure.arm;
+
+table SplineFbs {
+  // 2x4 Eigen matrix
+  control_points: [double] (id: 0);
+}
+
+struct AlphaThetaFbs {
+  // Parameterized value from 0-1
+  alpha: double;
+  // Roll joint angle
+  theta: double;
+}
+
+table CosSplineFbs {
+  spline: SplineFbs (id: 0);
+  roll: [AlphaThetaFbs] (id: 1);
+}
+
+table PathFbs {
+  spline: CosSplineFbs (id: 0);
+  distances: [float] (id: 1);
+}
+
+table TrajectoryFbs {
+  path: PathFbs (id: 0);
+  num_plan_points: uint64 (id: 1);
+  step_size: double (id: 2);
+  max_dvelocity_unfiltered: [double] (id: 3);
+  max_dvelocity_backward_accel: [double] (id: 4);
+  max_dvelocity_forwards_accel: [double] (id: 5);
+  max_dvelocity_backward_voltage: [double] (id: 6);
+  max_dvelocity_forwards_voltage: [double] (id: 7);
+  alpha_unitizer: [double] (id: 8);
+}
+
+struct EdgeFbs {
+  start: uint64;
+  end: uint64;
+  cost: double;
+}
+
+table TrajectoryAndParamsFbs {
+  vmax: double (id: 0);
+  alpha_unitizer: [double] (id: 1);
+  trajectory: TrajectoryFbs (id: 2);
+  name: string (id: 3);
+}
+
+table ArmTrajectories {
+  trajectories: [TrajectoryAndParamsFbs] (id: 0);
+  edges: [EdgeFbs] (id: 1);
+  // TODO(milind/maxwell): add vertexes in too
+}
+
+root_type ArmTrajectories;
diff --git a/y2023/control_loops/superstructure/arm/arm_trajectory_gen.cc b/y2023/control_loops/superstructure/arm/arm_trajectory_gen.cc
new file mode 100644
index 0000000..578bf1f
--- /dev/null
+++ b/y2023/control_loops/superstructure/arm/arm_trajectory_gen.cc
@@ -0,0 +1,196 @@
+#include <iostream>
+#include <memory>
+
+#include "aos/events/shm_event_loop.h"
+#include "aos/flatbuffers.h"
+#include "aos/init.h"
+#include "aos/json_to_flatbuffer.h"
+#include "frc971/control_loops/double_jointed_arm/graph.h"
+#include "gflags/gflags.h"
+#include "glog/logging.h"
+#include "y2023/constants.h"
+#include "y2023/control_loops/superstructure/arm/arm_constants.h"
+#include "y2023/control_loops/superstructure/arm/arm_trajectories_generated.h"
+#include "y2023/control_loops/superstructure/arm/generated_graph.h"
+#include "y2023/control_loops/superstructure/arm/trajectory.h"
+#include "y2023/control_loops/superstructure/roll/integral_hybrid_roll_plant.h"
+
+using frc971::control_loops::arm::SearchGraph;
+using y2023::control_loops::superstructure::arm::AlphaThetaFbs;
+using y2023::control_loops::superstructure::arm::CosSpline;
+using y2023::control_loops::superstructure::arm::kArmConstants;
+using y2023::control_loops::superstructure::arm::NSpline;
+using y2023::control_loops::superstructure::arm::Path;
+using y2023::control_loops::superstructure::arm::Trajectory;
+
+DEFINE_string(output, "", "Defines the location of the output file");
+
+int main(int argc, char **argv) {
+  aos::InitGoogle(&argc, &argv);
+
+  const frc971::control_loops::arm::Dynamics dynamics(kArmConstants);
+  std::vector<y2023::control_loops::superstructure::arm::TrajectoryAndParams>
+      trajectories;
+  Eigen::DiagonalMatrix<double, 3> alpha_unitizer(
+      (::Eigen::DiagonalMatrix<double, 3>().diagonal()
+           << (1.0 / y2023::constants::Values::kArmAlpha0Max()),
+       (1.0 / y2023::constants::Values::kArmAlpha1Max()),
+       (1.0 / y2023::constants::Values::kArmAlpha2Max()))
+          .finished());
+  StateFeedbackLoop<3, 1, 1, double, StateFeedbackHybridPlant<3, 1, 1>,
+                    HybridKalman<3, 1, 1>>
+      hybrid_roll_joint_loop = y2023::control_loops::superstructure::roll::
+          MakeIntegralHybridRollLoop();
+
+  // Optimizes paths
+  auto search_graph =
+      y2023::control_loops::superstructure::arm::MakeSearchGraph(
+          &dynamics, &trajectories, alpha_unitizer,
+          y2023::constants::Values::kArmVMax(), &hybrid_roll_joint_loop);
+
+  auto edges = search_graph.edges();
+
+  flatbuffers::FlatBufferBuilder fbb;
+  fbb.ForceDefaults(true);
+
+  std::vector<flatbuffers::Offset<
+      y2023::control_loops::superstructure::arm::TrajectoryAndParamsFbs>>
+      trajectory_and_params_offsets;
+
+  std::vector<y2023::control_loops::superstructure::arm::EdgeFbs> fbs_edges;
+
+  // Generating flatbuffer
+
+  for (const auto &trajectory : trajectories) {
+    auto nspline = trajectory.trajectory.path().spline().spline();
+    auto cos_spline = trajectory.trajectory.path().spline();
+    auto path = trajectory.trajectory.path();
+
+    auto control_points_offset = fbb.CreateVector(
+        nspline.control_points().data(), nspline.control_points().size());
+
+    std::vector<AlphaThetaFbs> roll;
+
+    for (const auto &alpha_theta : cos_spline.roll()) {
+      roll.emplace_back(alpha_theta.alpha, alpha_theta.theta);
+    }
+    auto cos_spline_roll = fbb.CreateVectorOfStructs(roll);
+
+    auto path_distances =
+        fbb.CreateVector(path.distances().data(), path.distances().size());
+
+    auto trajectory_max_dvelocity_unfiltered = fbb.CreateVector(
+        trajectory.trajectory.max_dvelocity_unfiltered().data(),
+        trajectory.trajectory.max_dvelocity_unfiltered().size());
+
+    auto trajectory_max_dvelocity_backward_accel = fbb.CreateVector(
+        trajectory.trajectory.max_dvelocity_backward_accel().data(),
+        trajectory.trajectory.max_dvelocity_backward_accel().size());
+
+    auto trajectory_max_dvelocity_forwards_accel = fbb.CreateVector(
+        trajectory.trajectory.max_dvelocity_forwards_accel().data(),
+        trajectory.trajectory.max_dvelocity_forwards_accel().size());
+
+    auto trajectory_max_dvelocity_backward_voltage = fbb.CreateVector(
+        trajectory.trajectory.max_dvelocity_backward_voltage().data(),
+        trajectory.trajectory.max_dvelocity_backward_voltage().size());
+
+    auto trajectory_max_dvelocity_forwards_voltage = fbb.CreateVector(
+        trajectory.trajectory.max_dvelocity_forwards_voltage().data(),
+        trajectory.trajectory.max_dvelocity_forwards_voltage().size());
+
+    auto trajectory_alpha_unitizer =
+        fbb.CreateVector(trajectory.trajectory.alpha_unitizer().data(),
+                         trajectory.trajectory.alpha_unitizer().size());
+
+    auto trajectory_and_params_name = fbb.CreateString(trajectory.name);
+
+    auto trajectory_and_params_alpha_unitizer = fbb.CreateVector(
+        trajectory.alpha_unitizer.data(), trajectory.alpha_unitizer.size());
+
+    y2023::control_loops::superstructure::arm::SplineFbs::Builder
+        spline_builder(fbb);
+
+    spline_builder.add_control_points(control_points_offset);
+
+    auto spline_offset = spline_builder.Finish();
+
+    y2023::control_loops::superstructure::arm::CosSplineFbs::Builder
+        cos_spline_builder(fbb);
+
+    cos_spline_builder.add_spline(spline_offset);
+
+    cos_spline_builder.add_roll(cos_spline_roll);
+
+    auto cos_spline_offset = cos_spline_builder.Finish();
+
+    y2023::control_loops::superstructure::arm::PathFbs::Builder path_builder(
+        fbb);
+
+    path_builder.add_spline(cos_spline_offset);
+    path_builder.add_distances(path_distances);
+
+    auto path_offset = path_builder.Finish();
+
+    y2023::control_loops::superstructure::arm::TrajectoryFbs::Builder
+        trajectory_builder(fbb);
+
+    trajectory_builder.add_path(path_offset);
+    trajectory_builder.add_num_plan_points(
+        trajectory.trajectory.num_plan_points());
+    trajectory_builder.add_step_size(trajectory.trajectory.step_size());
+    trajectory_builder.add_max_dvelocity_unfiltered(
+        trajectory_max_dvelocity_unfiltered);
+    trajectory_builder.add_max_dvelocity_backward_accel(
+        trajectory_max_dvelocity_backward_accel);
+    trajectory_builder.add_max_dvelocity_forwards_accel(
+        trajectory_max_dvelocity_forwards_accel);
+    trajectory_builder.add_max_dvelocity_backward_voltage(
+        trajectory_max_dvelocity_backward_voltage);
+    trajectory_builder.add_max_dvelocity_forwards_voltage(
+        trajectory_max_dvelocity_forwards_voltage);
+    trajectory_builder.add_alpha_unitizer(trajectory_alpha_unitizer);
+
+    auto trajectory_offset = trajectory_builder.Finish();
+
+    y2023::control_loops::superstructure::arm::TrajectoryAndParamsFbs::Builder
+        trajectory_and_params_builder(fbb);
+
+    trajectory_and_params_builder.add_vmax(trajectory.vmax);
+    trajectory_and_params_builder.add_alpha_unitizer(
+        trajectory_and_params_alpha_unitizer);
+    trajectory_and_params_builder.add_trajectory(trajectory_offset);
+    trajectory_and_params_builder.add_name(trajectory_and_params_name);
+
+    trajectory_and_params_offsets.emplace_back(
+        trajectory_and_params_builder.Finish());
+  }
+
+  for (const auto &edge : edges) {
+    fbs_edges.emplace_back(edge.start, edge.end, edge.cost);
+  }
+
+  auto arm_trajectories_trajctory_and_params_offsets =
+      fbb.CreateVector(trajectory_and_params_offsets.data(),
+                       trajectory_and_params_offsets.size());
+
+  auto arm_trajectories_edges = fbb.CreateVectorOfStructs(fbs_edges);
+
+  y2023::control_loops::superstructure::arm::ArmTrajectories::Builder
+      arm_trajectories_builder(fbb);
+
+  arm_trajectories_builder.add_trajectories(
+      arm_trajectories_trajctory_and_params_offsets);
+  arm_trajectories_builder.add_edges(arm_trajectories_edges);
+
+  auto arm_trajectories_offset = arm_trajectories_builder.Finish();
+
+  fbb.Finish(arm_trajectories_offset);
+
+  auto detatched_buffer = aos::FlatbufferDetachedBuffer<
+      y2023::control_loops::superstructure::arm::ArmTrajectories>(
+      fbb.Release());
+
+  // This writes to a binary file so we can cache the optimization results
+  aos::WriteFlatbufferToFile(FLAGS_output, detatched_buffer);
+}
diff --git a/y2023/control_loops/superstructure/arm/trajectory.cc b/y2023/control_loops/superstructure/arm/trajectory.cc
index 717a29e..d216467 100644
--- a/y2023/control_loops/superstructure/arm/trajectory.cc
+++ b/y2023/control_loops/superstructure/arm/trajectory.cc
@@ -7,6 +7,7 @@
 #include "frc971/control_loops/jacobian.h"
 #include "frc971/control_loops/runge_kutta.h"
 #include "gflags/gflags.h"
+#include "y2023/control_loops/superstructure/arm/arm_trajectories_generated.h"
 
 DEFINE_double(lqr_proximal_pos, 0.5, "Position LQR gain");
 DEFINE_double(lqr_proximal_vel, 5, "Velocity LQR gain");
@@ -165,6 +166,56 @@
   return ::std::make_unique<Path>(p->Reversed());
 }
 
+Trajectory::Trajectory(const frc971::control_loops::arm::Dynamics *dynamics,
+                       const StateFeedbackHybridPlant<3, 1, 1> *roll,
+                       const TrajectoryFbs &trajectory_fbs)
+    : dynamics_(dynamics),
+      roll_(roll),
+      num_plan_points_(trajectory_fbs.num_plan_points()),
+      step_size_(trajectory_fbs.step_size()),
+      max_dvelocity_unfiltered_(
+          trajectory_fbs.max_dvelocity_unfiltered()->data(),
+          trajectory_fbs.max_dvelocity_unfiltered()->data() +
+              trajectory_fbs.max_dvelocity_unfiltered()->size()),
+      max_dvelocity_backwards_voltage_(
+          trajectory_fbs.max_dvelocity_backward_voltage()->data(),
+          trajectory_fbs.max_dvelocity_backward_voltage()->data() +
+              trajectory_fbs.max_dvelocity_backward_voltage()->size()),
+      max_dvelocity_backwards_accel_(
+          trajectory_fbs.max_dvelocity_backward_accel()->data(),
+          trajectory_fbs.max_dvelocity_backward_accel()->data() +
+              trajectory_fbs.max_dvelocity_backward_accel()->size()),
+      max_dvelocity_forwards_accel_(
+          trajectory_fbs.max_dvelocity_forwards_accel()->data(),
+          trajectory_fbs.max_dvelocity_forwards_accel()->data() +
+              trajectory_fbs.max_dvelocity_forwards_accel()->size()),
+      max_dvelocity_forwards_voltage_(
+          trajectory_fbs.max_dvelocity_forwards_voltage()->data(),
+          trajectory_fbs.max_dvelocity_forwards_voltage()->data() +
+              trajectory_fbs.max_dvelocity_forwards_voltage()->size()),
+      alpha_unitizer_(trajectory_fbs.alpha_unitizer()->data()) {
+  auto control_points = ::Eigen::Matrix<double, 2, 4>(
+      trajectory_fbs.path()->spline()->spline()->control_points()->data());
+  NSpline<4, 2> spline(control_points);
+
+  std::vector<CosSpline::AlphaTheta> alpha_roll;
+
+  for (const auto &alpha_theta : *trajectory_fbs.path()->spline()->roll()) {
+    CosSpline::AlphaTheta atheta = {
+        .alpha = alpha_theta->alpha(),
+        .theta = alpha_theta->theta(),
+    };
+
+    alpha_roll.emplace_back(atheta);
+  }
+
+  CosSpline cos_spline(spline, alpha_roll);
+
+  path_ = std::make_unique<Path>(cos_spline,
+                                 (trajectory_fbs.path()->distances()->data(),
+                                  trajectory_fbs.path()->distances()->size()));
+}
+
 double Trajectory::MaxCurvatureSpeed(
     double goal_distance, const ::Eigen::Matrix<double, 3, 3> &alpha_unitizer,
     double plan_vmax) {
diff --git a/y2023/control_loops/superstructure/arm/trajectory.h b/y2023/control_loops/superstructure/arm/trajectory.h
index 17ec3aa..e498168 100644
--- a/y2023/control_loops/superstructure/arm/trajectory.h
+++ b/y2023/control_loops/superstructure/arm/trajectory.h
@@ -13,6 +13,7 @@
 #include "frc971/control_loops/fixed_quadrature.h"
 #include "frc971/control_loops/hybrid_state_feedback_loop.h"
 #include "frc971/control_loops/state_feedback_loop.h"
+#include "y2023/control_loops/superstructure/arm/arm_trajectories_generated.h"
 
 namespace y2023 {
 namespace control_loops {
@@ -152,6 +153,10 @@
   // Returns the d^2spline/dalpha^2 for a given alpha.
   ::Eigen::Matrix<double, 3, 1> Alpha(double alpha) const;
 
+  const NSpline<4, 2> &spline() const { return spline_; }
+
+  const std::vector<AlphaTheta> &roll() { return roll_; }
+
   CosSpline Reversed() const;
 
  private:
@@ -170,6 +175,9 @@
   Path(const CosSpline &spline, int num_alpha = 100)
       : spline_(spline), distances_(BuildDistances(num_alpha)) {}
 
+  Path(const CosSpline &spline, std::vector<float> distances)
+      : spline_(spline), distances_(distances) {}
+
   virtual ~Path() {}
 
   // Returns a point on the spline as a function of distance.
@@ -186,6 +194,8 @@
 
   const absl::Span<const float> distances() const { return distances_; }
 
+  const CosSpline &spline() const { return spline_; }
+
   Path Reversed() const;
   static ::std::unique_ptr<Path> Reversed(::std::unique_ptr<Path> p);
 
@@ -211,7 +221,7 @@
   // size.
   Trajectory(const frc971::control_loops::arm::Dynamics *dynamics,
              const StateFeedbackHybridPlant<3, 1, 1> *roll,
-             ::std::unique_ptr<const Path> path, double gridsize)
+             std::unique_ptr<const Path> path, double gridsize)
       : dynamics_(dynamics),
         roll_(roll),
         path_(::std::move(path)),
@@ -222,6 +232,10 @@
     alpha_unitizer_.setZero();
   }
 
+  Trajectory(const frc971::control_loops::arm::Dynamics *dynamics,
+             const StateFeedbackHybridPlant<3, 1, 1> *roll,
+             const TrajectoryFbs &trajectory_fbs);
+
   // Optimizes the trajectory.  The path will adhere to the constraints that
   // || angular acceleration * alpha_unitizer || < 1, and the applied voltage <
   // plan_vmax.
@@ -431,6 +445,8 @@
 
   const Path &path() const { return *path_; }
 
+  double step_size() const { return step_size_; }
+
  private:
   friend class testing::TrajectoryTest_IndicesForDistanceTest_Test;
 
@@ -457,7 +473,7 @@
   const StateFeedbackHybridPlant<3, 1, 1> *roll_;
 
   // The path to follow.
-  ::std::unique_ptr<const Path> path_;
+  std::unique_ptr<const Path> path_;
   // The number of points in the plan.
   const size_t num_plan_points_;
   // A cached version of the step size since we need this a *lot*.
@@ -607,8 +623,8 @@
 };
 
 }  // namespace arm
+}  // namespace superstructure
 }  // namespace control_loops
-}  // namespace frc971
 }  // namespace y2023
 
 #endif  // Y2023_CONTROL_LOOPS_SUPERSTRUCTURE_ARM_TRAJECTORY_H_
diff --git a/y2023/control_loops/superstructure/superstructure.cc b/y2023/control_loops/superstructure/superstructure.cc
index f59626c..9b9b119 100644
--- a/y2023/control_loops/superstructure/superstructure.cc
+++ b/y2023/control_loops/superstructure/superstructure.cc
@@ -4,6 +4,7 @@
 #include "aos/flatbuffer_merge.h"
 #include "aos/network/team_number.h"
 #include "frc971/zeroing/wrap.h"
+#include "y2023/control_loops/superstructure/arm/arm_trajectories_generated.h"
 
 DEFINE_bool(ignore_distance, false,
             "If true, ignore distance when shooting and obay joystick_reader");
@@ -18,9 +19,11 @@
 using frc971::control_loops::PotAndAbsoluteEncoderProfiledJointStatus;
 using frc971::control_loops::RelativeEncoderProfiledJointStatus;
 
-Superstructure::Superstructure(::aos::EventLoop *event_loop,
-                               std::shared_ptr<const constants::Values> values,
-                               const ::std::string &name)
+Superstructure::Superstructure(
+    ::aos::EventLoop *event_loop,
+    std::shared_ptr<const constants::Values> values,
+    const aos::FlatbufferVector<ArmTrajectories> &arm_trajectories,
+    const ::std::string &name)
     : frc971::controls::ControlLoop<Goal, Position, Status, Output>(event_loop,
                                                                     name),
       values_(values),
@@ -29,7 +32,7 @@
               "/drivetrain")),
       joystick_state_fetcher_(
           event_loop->MakeFetcher<aos::JoystickState>("/aos")),
-      arm_(values_),
+      arm_(values_, arm_trajectories.message()),
       end_effector_(),
       wrist_(values->wrist.subsystem_params) {
   event_loop->SetRuntimeRealtimePriority(30);
diff --git a/y2023/control_loops/superstructure/superstructure.h b/y2023/control_loops/superstructure/superstructure.h
index 87a5395..0c8b2c0 100644
--- a/y2023/control_loops/superstructure/superstructure.h
+++ b/y2023/control_loops/superstructure/superstructure.h
@@ -2,17 +2,22 @@
 #define Y2023_CONTROL_LOOPS_SUPERSTRUCTURE_SUPERSTRUCTURE_H_
 
 #include "aos/events/event_loop.h"
+#include "aos/json_to_flatbuffer.h"
 #include "frc971/control_loops/control_loop.h"
 #include "frc971/control_loops/drivetrain/drivetrain_status_generated.h"
 #include "y2023/constants.h"
 #include "y2023/control_loops/drivetrain/drivetrain_can_position_generated.h"
 #include "y2023/control_loops/superstructure/arm/arm.h"
+#include "y2023/control_loops/superstructure/arm/arm_trajectories_generated.h"
 #include "y2023/control_loops/superstructure/end_effector.h"
 #include "y2023/control_loops/superstructure/superstructure_goal_generated.h"
 #include "y2023/control_loops/superstructure/superstructure_output_generated.h"
 #include "y2023/control_loops/superstructure/superstructure_position_generated.h"
 #include "y2023/control_loops/superstructure/superstructure_status_generated.h"
 
+using y2023::control_loops::superstructure::arm::ArmTrajectories;
+using y2023::control_loops::superstructure::arm::TrajectoryAndParams;
+
 namespace y2023 {
 namespace control_loops {
 namespace superstructure {
@@ -35,9 +40,11 @@
           ::frc971::zeroing::AbsoluteEncoderZeroingEstimator,
           ::frc971::control_loops::AbsoluteEncoderProfiledJointStatus>;
 
-  explicit Superstructure(::aos::EventLoop *event_loop,
-                          std::shared_ptr<const constants::Values> values,
-                          const ::std::string &name = "/superstructure");
+  explicit Superstructure(
+      ::aos::EventLoop *event_loop,
+      std::shared_ptr<const constants::Values> values,
+      const aos::FlatbufferVector<ArmTrajectories> &arm_trajectories,
+      const ::std::string &name = "/superstructure");
 
   double robot_velocity() const;
 
@@ -45,6 +52,11 @@
   inline const EndEffector &end_effector() const { return end_effector_; }
   inline const AbsoluteEncoderSubsystem &wrist() const { return wrist_; }
 
+  static const aos::FlatbufferVector<ArmTrajectories> GetArmTrajectories(
+      const std::string &filename) {
+    return aos::FileToFlatbuffer<arm::ArmTrajectories>(filename);
+  }
+
  protected:
   virtual void RunIteration(const Goal *unsafe_goal, const Position *position,
                             aos::Sender<Output>::Builder *output,
diff --git a/y2023/control_loops/superstructure/superstructure_lib_test.cc b/y2023/control_loops/superstructure/superstructure_lib_test.cc
index 2a67adf..ee440ed 100644
--- a/y2023/control_loops/superstructure/superstructure_lib_test.cc
+++ b/y2023/control_loops/superstructure/superstructure_lib_test.cc
@@ -181,7 +181,7 @@
                PositionSensorSimulator(
                    values->wrist.subsystem_params.zeroing_constants
                        .one_revolution_distance),
-               values->wrist, constants::Values::kWristRange(),
+               values->wrist, constants::Values::kCompWristRange(),
                values->wrist.subsystem_params.zeroing_constants
                    .measured_absolute_position,
                dt_),
@@ -276,8 +276,12 @@
         values_(std::make_shared<constants::Values>(constants::MakeValues())),
         roborio_(aos::configuration::GetNode(configuration(), "roborio")),
         logger_pi_(aos::configuration::GetNode(configuration(), "logger")),
+        arm_trajectories_(superstructure::Superstructure::GetArmTrajectories(
+            "y2023/control_loops/superstructure/arm/"
+            "arm_trajectories_generated.bfbs")),
         superstructure_event_loop(MakeEventLoop("Superstructure", roborio_)),
-        superstructure_(superstructure_event_loop.get(), values_),
+        superstructure_(superstructure_event_loop.get(), values_,
+                        arm_trajectories_),
         test_event_loop_(MakeEventLoop("test", roborio_)),
         superstructure_goal_fetcher_(
             test_event_loop_->MakeFetcher<Goal>("/superstructure")),
@@ -410,6 +414,8 @@
   const aos::Node *const roborio_;
   const aos::Node *const logger_pi_;
 
+  const ::aos::FlatbufferVector<ArmTrajectories> arm_trajectories_;
+
   ::std::unique_ptr<::aos::EventLoop> superstructure_event_loop;
   ::y2023::control_loops::superstructure::Superstructure superstructure_;
   ::std::unique_ptr<::aos::EventLoop> test_event_loop_;
@@ -443,7 +449,7 @@
 
     flatbuffers::Offset<StaticZeroingSingleDOFProfiledSubsystemGoal>
         wrist_offset = CreateStaticZeroingSingleDOFProfiledSubsystemGoal(
-            *builder.fbb(), constants::Values::kWristRange().middle());
+            *builder.fbb(), constants::Values::kCompWristRange().middle());
 
     Goal::Builder goal_builder = builder.MakeBuilder<Goal>();
     goal_builder.add_arm_goal_position(arm::NeutralIndex());
@@ -468,7 +474,7 @@
 
     flatbuffers::Offset<StaticZeroingSingleDOFProfiledSubsystemGoal>
         wrist_offset = CreateStaticZeroingSingleDOFProfiledSubsystemGoal(
-            *builder.fbb(), constants::Values::kWristRange().upper);
+            *builder.fbb(), constants::Values::kCompWristRange().upper);
 
     Goal::Builder goal_builder = builder.MakeBuilder<Goal>();
 
@@ -499,7 +505,7 @@
 
     flatbuffers::Offset<StaticZeroingSingleDOFProfiledSubsystemGoal>
         wrist_offset = CreateStaticZeroingSingleDOFProfiledSubsystemGoal(
-            *builder.fbb(), constants::Values::kWristRange().upper);
+            *builder.fbb(), constants::Values::kCompWristRange().upper);
 
     Goal::Builder goal_builder = builder.MakeBuilder<Goal>();
 
@@ -518,7 +524,7 @@
 
     flatbuffers::Offset<StaticZeroingSingleDOFProfiledSubsystemGoal>
         wrist_offset = CreateStaticZeroingSingleDOFProfiledSubsystemGoal(
-            *builder.fbb(), constants::Values::kWristRange().lower,
+            *builder.fbb(), constants::Values::kCompWristRange().lower,
             CreateProfileParameters(*builder.fbb(), 20.0, 0.1));
 
     Goal::Builder goal_builder = builder.MakeBuilder<Goal>();
diff --git a/y2023/control_loops/superstructure/superstructure_main.cc b/y2023/control_loops/superstructure/superstructure_main.cc
index bb25ad8..10a9ca9 100644
--- a/y2023/control_loops/superstructure/superstructure_main.cc
+++ b/y2023/control_loops/superstructure/superstructure_main.cc
@@ -2,6 +2,12 @@
 #include "aos/init.h"
 #include "y2023/control_loops/superstructure/superstructure.h"
 
+DEFINE_string(arm_trajectories, "arm_trajectories_generated.bfbs",
+              "The path to the generated arm trajectories bfbs file.");
+
+using y2023::control_loops::superstructure::Superstructure;
+using y2023::control_loops::superstructure::arm::ArmTrajectories;
+
 int main(int argc, char **argv) {
   ::aos::InitGoogle(&argc, &argv);
 
@@ -9,11 +15,15 @@
       aos::configuration::ReadConfig("aos_config.json");
 
   ::aos::ShmEventLoop event_loop(&config.message());
+
+  auto trajectories =
+      y2023::control_loops::superstructure::Superstructure::GetArmTrajectories(
+          FLAGS_arm_trajectories);
+
   std::shared_ptr<const y2023::constants::Values> values =
       std::make_shared<const y2023::constants::Values>(
           y2023::constants::MakeValues());
-  ::y2023::control_loops::superstructure::Superstructure superstructure(
-      &event_loop, values);
+  Superstructure superstructure(&event_loop, values, trajectories);
 
   event_loop.Run();
 
diff --git a/y2023/control_loops/superstructure/superstructure_replay.cc b/y2023/control_loops/superstructure/superstructure_replay.cc
index ed97966..9bd3ae2 100644
--- a/y2023/control_loops/superstructure/superstructure_replay.cc
+++ b/y2023/control_loops/superstructure/superstructure_replay.cc
@@ -17,6 +17,8 @@
 DEFINE_int32(team, 971, "Team number to use for logfile replay.");
 DEFINE_string(output_folder, "/tmp/superstructure_replay/",
               "Logs all channels to the provided logfile.");
+DEFINE_string(arm_trajectories, "arm/arm_trajectories_generated.bfbs",
+              "The path to the generated arm trajectories bfbs file.");
 
 int main(int argc, char **argv) {
   aos::InitGoogle(&argc, &argv);
@@ -45,10 +47,16 @@
   auto logger = std::make_unique<aos::logger::Logger>(logger_event_loop.get());
   logger->StartLoggingOnRun(FLAGS_output_folder);
 
-  roborio->OnStartup([roborio]() {
+  auto trajectories =
+      y2023::control_loops::superstructure::Superstructure::GetArmTrajectories(
+          FLAGS_arm_trajectories);
+
+  roborio->OnStartup([roborio, &trajectories]() {
     roborio->AlwaysStart<y2023::control_loops::superstructure::Superstructure>(
-        "superstructure", std::make_shared<y2023::constants::Values>(
-                              y2023::constants::MakeValues()));
+        "superstructure",
+        std::make_shared<y2023::constants::Values>(
+            y2023::constants::MakeValues()),
+        trajectories);
   });
 
   std::unique_ptr<aos::EventLoop> print_loop = roborio->MakeEventLoop("print");
diff --git a/y2023/joystick_reader.cc b/y2023/joystick_reader.cc
index 135c987..43962dd 100644
--- a/y2023/joystick_reader.cc
+++ b/y2023/joystick_reader.cc
@@ -31,11 +31,12 @@
 using frc971::input::driver_station::ControlBit;
 using frc971::input::driver_station::JoystickAxis;
 using frc971::input::driver_station::POVLocation;
-using y2023::control_loops::superstructure::RollerGoal;
-using y2023::control_loops::drivetrain::RowSelectionHint;
 using y2023::control_loops::drivetrain::GridSelectionHint;
+using y2023::control_loops::drivetrain::RowSelectionHint;
 using y2023::control_loops::drivetrain::SpotSelectionHint;
 using y2023::control_loops::drivetrain::TargetSelectorHint;
+using y2023::control_loops::superstructure::RollerGoal;
+using Side = frc971::control_loops::drivetrain::RobotSide;
 
 namespace y2023 {
 namespace input {
@@ -77,11 +78,6 @@
   CUBE = 2,
 };
 
-enum class Side {
-  FRONT = 0,
-  BACK = 1,
-};
-
 struct ButtonData {
   ButtonLocation button;
   std::optional<SpotSelectionHint> spot = std::nullopt;
@@ -439,8 +435,7 @@
       auto hint_builder = builder.MakeBuilder<TargetSelectorHint>();
       hint_builder.add_row(placing_row.value());
       hint_builder.add_spot(placing_spot.value());
-      // TODO: Add field to TargetSelector hint for forwards vs. backwards
-      // placement.
+      hint_builder.add_robot_side(CHECK_NOTNULL(current_setpoint_)->side);
       if (builder.Send(hint_builder.Finish()) != aos::RawSender::Error::kOk) {
         AOS_LOG(ERROR, "Sending target selector hint failed.\n");
       }
diff --git a/y2023/localizer/localizer.cc b/y2023/localizer/localizer.cc
index ace3663..3a6de93 100644
--- a/y2023/localizer/localizer.cc
+++ b/y2023/localizer/localizer.cc
@@ -284,6 +284,7 @@
 
   // TODO(james): Tune this. Also, gain schedule for auto mode?
   Eigen::Matrix<double, 3, 1> noises(1.0, 1.0, 0.5);
+  noises /= 4.0;
   // Scale noise by the distortion factor for this detection
   noises *= (1.0 + FLAGS_distortion_noise_scalar * target.distortion_factor());
 
diff --git a/y2023/localizer/scoring_map.fbs b/y2023/localizer/scoring_map.fbs
index 92b48ca..e53ea0b 100644
--- a/y2023/localizer/scoring_map.fbs
+++ b/y2023/localizer/scoring_map.fbs
@@ -2,8 +2,8 @@
 
 namespace y2023.localizer;
 
-// "left" and "right" in this file are taken from the perspective of a robot
-// on the field.
+// "left" and "right" in this file are taken from the perspective of a driver
+// off the field.
 
 // A row of three scoring positions, where the cube scoring locations will be
 // in the middle.
diff --git a/y2023/vision/BUILD b/y2023/vision/BUILD
index 94803e1..e90b825 100644
--- a/y2023/vision/BUILD
+++ b/y2023/vision/BUILD
@@ -136,6 +136,32 @@
     ],
 )
 
+cc_test(
+    name = "april_detection_test",
+    srcs = [
+        "april_detection_test.cc",
+    ],
+    data = [
+        "//y2023:aos_config",
+        "//y2023/constants:constants.json",
+        "@apriltag_test_bfbs_images",
+    ],
+    deps = [
+        ":aprilrobotics_lib",
+        "//aos:flatbuffer_merge",
+        "//aos:json_to_flatbuffer",
+        "//aos/events:simulated_event_loop",
+        "//aos/testing:googletest",
+        "//aos/testing:path",
+        "//aos/testing:test_logging",
+        "//frc971/constants:constants_sender_lib",
+        "//frc971/vision:target_mapper",
+        "//frc971/vision:vision_fbs",
+        "//y2023/constants:constants_fbs",
+        "//y2023/constants:constants_list_fbs",
+    ],
+)
+
 filegroup(
     name = "image_streamer_start",
     srcs = ["image_streamer_start.sh"],
diff --git a/y2023/vision/april_detection_test.cc b/y2023/vision/april_detection_test.cc
new file mode 100644
index 0000000..b92557c
--- /dev/null
+++ b/y2023/vision/april_detection_test.cc
@@ -0,0 +1,128 @@
+#include <string>
+
+#include "aos/events/simulated_event_loop.h"
+#include "aos/flatbuffer_merge.h"
+#include "aos/json_to_flatbuffer.h"
+#include "aos/testing/path.h"
+#include "aprilrobotics.h"
+#include "frc971/constants/constants_sender_lib.h"
+#include "frc971/vision/target_mapper.h"
+#include "frc971/vision/vision_generated.h"
+#include "glog/logging.h"
+#include "gtest/gtest.h"
+#include "y2023/constants/constants_generated.h"
+#include "y2023/constants/constants_list_generated.h"
+
+namespace y2023::vision::testing {
+class AprilDetectionTest : public ::testing::Test {
+ public:
+  AprilDetectionTest()
+      : config_(aos::configuration::ReadConfig("y2023/aos_config.json")),
+        event_loop_factory_(&config_.message()),
+        pi_(aos::configuration::GetNode(event_loop_factory_.configuration(),
+                                        "pi4")),
+        send_pose_event_loop_(
+            event_loop_factory_.MakeEventLoop("Send pose", pi_)),
+        receive_pose_event_loop_(
+            event_loop_factory_.MakeEventLoop("Receive pose", pi_)),
+        april_pose_fetcher_(
+            receive_pose_event_loop_->MakeFetcher<frc971::vision::TargetMap>(
+                "/camera")),
+        image_sender_(
+            receive_pose_event_loop_->MakeSender<frc971::vision::CameraImage>(
+                "/camera")),
+        constants_sender_(receive_pose_event_loop_.get(),
+                          "y2023/constants/constants.json", 7971, "/constants"),
+        detector_(
+            AprilRoboticsDetector(send_pose_event_loop_.get(), "/camera")) {}
+
+  void SendImage(std::string path) {
+    aos::FlatbufferVector<frc971::vision::CameraImage> image =
+        aos::FileToFlatbuffer<frc971::vision::CameraImage>(
+            "external/apriltag_test_bfbs_images/" + path);
+
+    auto builder = image_sender_.MakeBuilder();
+    flatbuffers::Offset<frc971::vision::CameraImage> image_fbs =
+        aos::CopyFlatBuffer(image, builder.fbb());
+
+    builder.CheckOk(builder.Send(image_fbs));
+  }
+
+  void TestDistanceAngle(std::string image_path, double expected_distance,
+                         double expected_angle) {
+    receive_pose_event_loop_->OnRun([&]() { SendImage(image_path); });
+    event_loop_factory_.RunFor(std::chrono::milliseconds(5));
+
+    ASSERT_TRUE(april_pose_fetcher_.Fetch());
+    ASSERT_EQ(april_pose_fetcher_->target_poses()->size(), 1);
+
+    frc971::vision::TargetMapper::TargetPose target_pose =
+        frc971::vision::PoseUtils::TargetPoseFromFbs(
+            *april_pose_fetcher_->target_poses()->Get(0));
+
+    ASSERT_EQ(april_pose_fetcher_->target_poses()->Get(0)->id(), 8);
+
+    // Height
+    EXPECT_NEAR(target_pose.pose.p.y(), -0.28, 0.05);
+
+    // Tag to camera horizontal distance
+    double distance_norm =
+        sqrt(pow(target_pose.pose.p.x(), 2) + pow(target_pose.pose.p.z(), 2));
+    EXPECT_NEAR(distance_norm, expected_distance, 0.1);
+
+    Eigen::Vector3d rotation_euler =
+        frc971::vision::PoseUtils::QuaternionToEulerAngles(target_pose.pose.q);
+
+    EXPECT_NEAR(rotation_euler[0], 0, 0.1);
+    EXPECT_NEAR(rotation_euler[1], expected_angle, 0.05);
+    EXPECT_NEAR(rotation_euler[2], 0, 0.1);
+  }
+
+  aos::FlatbufferDetachedBuffer<aos::Configuration> config_;
+  aos::SimulatedEventLoopFactory event_loop_factory_;
+  const aos::Node *const pi_;
+  ::std::unique_ptr<::aos::EventLoop> send_pose_event_loop_;
+  ::std::unique_ptr<::aos::EventLoop> receive_pose_event_loop_;
+  aos::Fetcher<frc971::vision::TargetMap> april_pose_fetcher_;
+  aos::Sender<frc971::vision::CameraImage> image_sender_;
+  frc971::constants::ConstantSender<y2023::Constants, y2023::ConstantsList>
+      constants_sender_;
+  AprilRoboticsDetector detector_;
+};
+
+TEST_F(AprilDetectionTest, CheckPose5Feet) {
+  TestDistanceAngle("bfbs-capture-2013-01-18_08-54-09.501047728.bfbs", 1.5, 0);
+  TestDistanceAngle("bfbs-capture-2013-01-18_08-54-16.869057537.bfbs", 1.5,
+                    0.22);
+  TestDistanceAngle("bfbs-capture-2013-01-18_08-54-24.931661979.bfbs", 1.5,
+                    -0.37);
+}
+
+TEST_F(AprilDetectionTest, CheckPose10Feet) {
+  TestDistanceAngle("bfbs-capture-2013-01-18_08-51-24.861065764.bfbs", 3.07, 0);
+  TestDistanceAngle("bfbs-capture-2013-01-18_08-52-01.846912552.bfbs", 3.07,
+                    0.31);
+  TestDistanceAngle("bfbs-capture-2013-01-18_08-52-33.462848049.bfbs", 3.07,
+                    -0.27);
+}
+
+TEST_F(AprilDetectionTest, CheckPose15Feet) {
+  // The camera was not at a perfect angle of 0, so the angle is -0.15 radians
+  // instead of 0
+  TestDistanceAngle("bfbs-capture-2013-01-18_09-29-16.806073486.bfbs", 4.57,
+                    -0.15);
+  TestDistanceAngle("bfbs-capture-2013-01-18_09-33-00.993756514.bfbs", 4.57,
+                    0.38);
+}
+
+TEST_F(AprilDetectionTest, CheckPose20Feet) {
+  // The camera was not at a perfect angle of 0, so the angle is 0.09 radians
+  // instead of 0
+  TestDistanceAngle("bfbs-capture-2013-01-18_08-57-00.171120695.bfbs", 6.06,
+                    0.09);
+  TestDistanceAngle("bfbs-capture-2013-01-18_08-57-17.858752817.bfbs", 6.06,
+                    0.35);
+  TestDistanceAngle("bfbs-capture-2013-01-18_08-57-08.096597542.bfbs", 6.06,
+                    -0.45);
+}
+}  // namespace y2023::vision::testing
diff --git a/y2023/vision/aprilrobotics.cc b/y2023/vision/aprilrobotics.cc
index ec743c5..bd1c5fd 100644
--- a/y2023/vision/aprilrobotics.cc
+++ b/y2023/vision/aprilrobotics.cc
@@ -26,12 +26,13 @@
     : calibration_data_(event_loop),
       image_size_(0, 0),
       ftrace_(),
-      image_callback_(event_loop, channel_name,
-                      [&](cv::Mat image_color_mat,
-                          const aos::monotonic_clock::time_point eof) {
-                        HandleImage(image_color_mat, eof);
-                      },
-                      chrono::milliseconds(5)),
+      image_callback_(
+          event_loop, channel_name,
+          [&](cv::Mat image_color_mat,
+              const aos::monotonic_clock::time_point eof) {
+            HandleImage(image_color_mat, eof);
+          },
+          chrono::milliseconds(5)),
       target_map_sender_(
           event_loop->MakeSender<frc971::vision::TargetMap>("/camera")),
       image_annotations_sender_(
@@ -275,8 +276,8 @@
     }
   }
 
-  foxglove::ImageAnnotations::Builder annotation_builder(*builder.fbb());
   const auto corners_offset = builder.fbb()->CreateVector(foxglove_corners);
+  foxglove::ImageAnnotations::Builder annotation_builder(*builder.fbb());
   annotation_builder.add_points(corners_offset);
   builder.CheckOk(builder.Send(annotation_builder.Finish()));
 
diff --git a/y2023/vision/calib_files/calibration_pi-971-1_cam-23-05_ext_2023-02-22.json b/y2023/vision/calib_files/calibration_pi-971-1_cam-23-05_ext_2023-02-22.json
deleted file mode 100644
index b21da40..0000000
--- a/y2023/vision/calib_files/calibration_pi-971-1_cam-23-05_ext_2023-02-22.json
+++ /dev/null
@@ -1 +0,0 @@
-{ "node_name": "pi1", "team_number": 971, "intrinsics": [ 890.980713, 0.0, 619.298645, 0.0, 890.668762, 364.009766, 0.0, 0.0, 1.0 ], "fixed_extrinsics": { "data": [ -0.487722, 0.222354, 0.844207, 0.025116, 0.864934, -0.008067, 0.501821, -0.246003, 0.118392, 0.974933, -0.188387, 0.532497, 0.0, 0.0, 0.0, 1.0 ] }, "dist_coeffs": [ -0.449172, 0.252318, 0.000881, -0.000615, -0.082208 ], "calibration_timestamp": 1358501902915096335, "camera_id": "23-05" }
\ No newline at end of file
diff --git a/y2023/vision/calib_files/calibration_pi-971-1_cam-23-05_ext_2023-03-05.json b/y2023/vision/calib_files/calibration_pi-971-1_cam-23-05_ext_2023-03-05.json
new file mode 100644
index 0000000..f585519
--- /dev/null
+++ b/y2023/vision/calib_files/calibration_pi-971-1_cam-23-05_ext_2023-03-05.json
@@ -0,0 +1 @@
+{ "node_name": "pi1", "team_number": 971, "intrinsics": [ 890.980713, 0.0, 619.298645, 0.0, 890.668762, 364.009766, 0.0, 0.0, 1.0 ], "fixed_extrinsics": { "data": [ -0.487918, 0.221538, 0.844309, 0.190808, 0.866039, 0.00192, 0.499972, -0.218036, 0.109142, 0.97515, -0.192797, 0.544037, 0.0, 0.0, 0.0, 1.0 ] }, "dist_coeffs": [ -0.449172, 0.252318, 0.000881, -0.000615, -0.082208 ], "calibration_timestamp": 1358501902915096335, "camera_id": "23-05" }
\ No newline at end of file
diff --git a/y2023/vision/calib_files/calibration_pi-9971-1_cam-23-09_ext_2023-03-05.json b/y2023/vision/calib_files/calibration_pi-9971-1_cam-23-09_ext_2023-03-05.json
new file mode 100644
index 0000000..c79f7d3
--- /dev/null
+++ b/y2023/vision/calib_files/calibration_pi-9971-1_cam-23-09_ext_2023-03-05.json
@@ -0,0 +1 @@
+{ "node_name": "pi1", "team_number": 9971, "intrinsics": [ 893.617798, 0.0, 612.44397, 0.0, 893.193115, 375.196381, 0.0, 0.0, 1.0 ], "fixed_extrinsics": { "data": [ -0.483961, 0.220781, 0.84678, 0.176109, 0.868846, 0.005849, 0.495048, -0.191149, 0.104344, 0.975306, -0.194656, 0.550508, 0.0, 0.0, 0.0, 1.0 ] }, "dist_coeffs": [ -0.443805, 0.238734, 0.000133, 0.000448, -0.071068 ], "calibration_timestamp": 1358499779650270322, "camera_id": "23-09" }
\ No newline at end of file
diff --git a/y2023/vision/calib_files/calibration_pi-9971-1_cam-23-09_ext_from971_2023-02-23.json b/y2023/vision/calib_files/calibration_pi-9971-1_cam-23-09_ext_from971_2023-02-23.json
deleted file mode 100644
index b6e0def..0000000
--- a/y2023/vision/calib_files/calibration_pi-9971-1_cam-23-09_ext_from971_2023-02-23.json
+++ /dev/null
@@ -1,25 +0,0 @@
-{
- "node_name": "pi1",
- "team_number": 9971,
- "intrinsics": [
-  893.617798,
-  0.0,
-  612.44397,
-  0.0,
-  893.193115,
-  375.196381,
-  0.0,
-  0.0,
-  1.0
- ],
- "dist_coeffs": [
-  -0.443805,
-  0.238734,
-  0.000133,
-  0.000448,
-  -0.071068
- ],
- "fixed_extrinsics": { "data": [ -0.487722, 0.222354, 0.844207, 0.025116, 0.864934, -0.008067, 0.501821, -0.246003, 0.118392, 0.974933, -0.188387, 0.532497, 0.0, 0.0, 0.0, 1.0 ] },
- "calibration_timestamp": 1358499779650270322,
- "camera_id": "23-09"
-}
diff --git a/y2023/vision/calib_files/calibration_pi-9971-2_cam-23-10_ext_2023-03-08.json b/y2023/vision/calib_files/calibration_pi-9971-2_cam-23-10_ext_2023-03-08.json
new file mode 100644
index 0000000..547d8d7
--- /dev/null
+++ b/y2023/vision/calib_files/calibration_pi-9971-2_cam-23-10_ext_2023-03-08.json
@@ -0,0 +1 @@
+{ "node_name": "pi2", "team_number": 9971, "intrinsics": [ 894.002502, 0.0, 636.431335, 0.0, 893.723816, 377.069672, 0.0, 0.0, 1.0 ], "fixed_extrinsics": { "data": [ 0.830467, 0.233602, 0.505721, 0.242226, 0.519225, 0.004294, -0.854627, -0.167586, -0.201814, 0.972323, -0.117727, 0.614872, 0.0, 0.0, 0.0, 1.0 ] }, "dist_coeffs": [ -0.446659, 0.244189, 0.000632, 0.000171, -0.074849 ], "calibration_timestamp": 1358503360377380613, "camera_id": "23-10" }
\ No newline at end of file
diff --git a/y2023/vision/calib_files/calibration_pi-9971-2_cam-23-10_ext_from971_2023-02-23.json b/y2023/vision/calib_files/calibration_pi-9971-2_cam-23-10_ext_from971_2023-02-23.json
deleted file mode 100644
index 6293443..0000000
--- a/y2023/vision/calib_files/calibration_pi-9971-2_cam-23-10_ext_from971_2023-02-23.json
+++ /dev/null
@@ -1,25 +0,0 @@
-{
- "node_name": "pi2",
- "team_number": 9971,
- "intrinsics": [
-  894.002502,
-  0.0,
-  636.431335,
-  0.0,
-  893.723816,
-  377.069672,
-  0.0,
-  0.0,
-  1.0
- ],
- "dist_coeffs": [
-  -0.446659,
-  0.244189,
-  0.000632,
-  0.000171,
-  -0.074849
- ],
- "fixed_extrinsics": { "data": [ 0.852213, 0.227336, 0.471224, 0.220072, 0.485092, -0.005909, -0.874443, -0.175232, -0.196008, 0.973799, -0.115315, 0.61409, 0.0, 0.0, 0.0, 1.0 ] },
- "calibration_timestamp": 1358503360377380613,
- "camera_id": "23-10"
-}
diff --git a/y2023/vision/calib_files/calibration_pi-9971-3_cam-23-11_ext_2023-03-05.json b/y2023/vision/calib_files/calibration_pi-9971-3_cam-23-11_ext_2023-03-05.json
new file mode 100644
index 0000000..d4b8bd2
--- /dev/null
+++ b/y2023/vision/calib_files/calibration_pi-9971-3_cam-23-11_ext_2023-03-05.json
@@ -0,0 +1 @@
+{ "node_name": "pi3", "team_number": 9971, "intrinsics": [ 891.026001, 0.0, 620.086731, 0.0, 890.566895, 385.035126, 0.0, 0.0, 1.0 ], "fixed_extrinsics": { "data": [ 0.503726, -0.160611, -0.848802, 0.064621, -0.857988, 0.021383, -0.513224, -0.195154, 0.100579, 0.986786, -0.127032, 0.653821, 0.0, 0.0, 0.0, 1.0 ] }, "dist_coeffs": [ -0.448299, 0.250123, -0.00042, -0.000127, -0.078433 ], "calibration_timestamp": 1358503290177115986, "camera_id": "23-11" }
\ No newline at end of file
diff --git a/y2023/vision/calib_files/calibration_pi-9971-3_cam-23-11_ext_from971_2023-02-23.json b/y2023/vision/calib_files/calibration_pi-9971-3_cam-23-11_ext_from971_2023-02-23.json
deleted file mode 100644
index ec4c69c..0000000
--- a/y2023/vision/calib_files/calibration_pi-9971-3_cam-23-11_ext_from971_2023-02-23.json
+++ /dev/null
@@ -1,25 +0,0 @@
-{
- "node_name": "pi3",
- "team_number": 9971,
- "intrinsics": [
-  891.026001,
-  0.0,
-  620.086731,
-  0.0,
-  890.566895,
-  385.035126,
-  0.0,
-  0.0,
-  1.0
- ],
- "dist_coeffs": [
-  -0.448299,
-  0.250123,
-  -0.00042,
-  -0.000127,
-  -0.078433
- ],
- "fixed_extrinsics": { "data": [ 0.492232, -0.163335, -0.855002, 0.020122, -0.866067, 0.006706, -0.499883, -0.174518, 0.087382, 0.986548, -0.138158, 0.645307, 0.0, 0.0, 0.0, 1.0 ] },
- "calibration_timestamp": 1358503290177115986,
- "camera_id": "23-11"
-}
diff --git a/y2023/vision/calib_files/calibration_pi-9971-4_cam-23-12_ext_2023-03-05.json b/y2023/vision/calib_files/calibration_pi-9971-4_cam-23-12_ext_2023-03-05.json
new file mode 100644
index 0000000..7ea2f6d
--- /dev/null
+++ b/y2023/vision/calib_files/calibration_pi-9971-4_cam-23-12_ext_2023-03-05.json
@@ -0,0 +1 @@
+{ "node_name": "pi4", "team_number": 9971, "intrinsics": [ 891.127197, 0.0, 640.291321, 0.0, 891.176453, 359.578705, 0.0, 0.0, 1.0 ], "fixed_extrinsics": { "data": [ -0.845241, -0.169204, -0.506891, 0.008046, -0.511496, -0.018479, 0.859087, -0.243442, -0.154727, 0.985408, -0.070928, 0.69704, 0.0, 0.0, 0.0, 1.0 ] }, "dist_coeffs": [ -0.452948, 0.262567, 0.00088, -0.000253, -0.089368 ], "calibration_timestamp": 1358499579812698894, "camera_id": "23-12" }
\ No newline at end of file
diff --git a/y2023/vision/calib_files/calibration_pi-9971-4_cam-23-12_ext_from971_2023-02-23.json b/y2023/vision/calib_files/calibration_pi-9971-4_cam-23-12_ext_from971_2023-02-23.json
deleted file mode 100644
index 3d0f566..0000000
--- a/y2023/vision/calib_files/calibration_pi-9971-4_cam-23-12_ext_from971_2023-02-23.json
+++ /dev/null
@@ -1,25 +0,0 @@
-{
- "node_name": "pi4",
- "team_number": 9971,
- "intrinsics": [
-  891.127197,
-  0.0,
-  640.291321,
-  0.0,
-  891.176453,
-  359.578705,
-  0.0,
-  0.0,
-  1.0
- ],
- "dist_coeffs": [
-  -0.452948,
-  0.262567,
-  0.00088,
-  -0.000253,
-  -0.089368
- ],
- "fixed_extrinsics": { "data": [ -0.865915, -0.186983, -0.463928, -0.014873, -0.473362, 0.006652, 0.880843, -0.215738, -0.161617, 0.982341, -0.094271, 0.676433, 0.0, 0.0, 0.0, 1.0 ] },
- "calibration_timestamp": 1358499579812698894,
- "camera_id": "23-12"
-}
diff --git a/y2023/wpilib_interface.cc b/y2023/wpilib_interface.cc
index 921531e..033d3d7 100644
--- a/y2023/wpilib_interface.cc
+++ b/y2023/wpilib_interface.cc
@@ -104,7 +104,8 @@
     Values::kMaxProximalEncoderPulsesPerSecond(),
     Values::kMaxDistalEncoderPulsesPerSecond(),
     Values::kMaxRollJointEncoderPulsesPerSecond(),
-    Values::kMaxWristEncoderPulsesPerSecond(),
+    Values::kMaxCompWristEncoderPulsesPerSecond(),
+    Values::kMaxPracticeWristEncoderPulsesPerSecond(),
 });
 static_assert(kMaxFastEncoderPulsesPerSecond <= 1300000,
               "fast encoders are too fast");
@@ -461,7 +462,10 @@
       frc971::AbsolutePositionT wrist;
       CopyPosition(wrist_encoder_, &wrist,
                    Values::kWristEncoderCountsPerRevolution(),
-                   Values::kWristEncoderRatio(), false);
+                   values_->wrist.subsystem_params.zeroing_constants
+                           .one_revolution_distance /
+                       (M_PI * 2.0),
+                   values_->wrist_flipped);
 
       flatbuffers::Offset<frc971::PotAndAbsolutePosition> proximal_offset =
           frc971::PotAndAbsolutePosition::Pack(*builder.fbb(), &proximal);
diff --git a/y2023/y2023_roborio.json b/y2023/y2023_roborio.json
index ac7845f..03985c0 100644
--- a/y2023/y2023_roborio.json
+++ b/y2023/y2023_roborio.json
@@ -412,7 +412,7 @@
     },
     {
       "name": "/drivetrain",
-      "type": "y2019.control_loops.drivetrain.TargetSelectorHint",
+      "type": "y2023.control_loops.drivetrain.TargetSelectorStatus",
       "source_node": "roborio"
     },
     {
@@ -509,7 +509,7 @@
       "name": "joystick_reader",
       "executable_name": "joystick_reader",
       "args": [
-        "--die_on_malloc"
+        "--nodie_on_malloc"
       ],
       "nodes": [
         "roborio"
@@ -526,9 +526,9 @@
       "name": "autonomous_action",
       "executable_name": "autonomous_action",
       "args": [
-        "--die_on_malloc"
+        "--nodie_on_malloc"
       ],
-      "autostart": false,
+      "autostart": true,
       "nodes": [
         "roborio"
       ]