Merge "Adding check for valid charuco diamond markers"
diff --git a/frc971/control_loops/python/BUILD b/frc971/control_loops/python/BUILD
index c547833..e308954 100644
--- a/frc971/control_loops/python/BUILD
+++ b/frc971/control_loops/python/BUILD
@@ -188,7 +188,7 @@
     data = glob([
         "field_images/*.png",
         "field_images/*.svg",
-    ]),
+    ]) + ["//third_party/y2023/field:pictures"],
     legacy_create_init = False,
     target_compatible_with = ["@platforms//cpu:x86_64"],
     visibility = ["//visibility:public"],
diff --git a/frc971/control_loops/python/constants.py b/frc971/control_loops/python/constants.py
index b2d9d57..3a61b5e 100644
--- a/frc971/control_loops/python/constants.py
+++ b/frc971/control_loops/python/constants.py
@@ -15,6 +15,9 @@
 ROBOT_SIDE_TO_HATCH_PANEL = 0.1
 HATCH_PANEL_WIDTH = 0.4826
 
+# field_id is either just a file prefix for a .png in field_images/ or is a
+# full path preceded by // specifying a location relative to the root of the
+# repository.
 FieldType = namedtuple(
     'Field', ['name', 'tags', 'year', 'width', 'length', 'robot', 'field_id'])
 RobotType = namedtuple("Robot", ['width', 'length'])
@@ -33,6 +36,7 @@
 Robot2020 = RobotType(width=0.8128, length=0.8636)  # 32 in x 34 in
 Robot2021 = Robot2020
 Robot2022 = RobotType(width=0.8763, length=0.96647)
+Robot2023 = RobotType(width=0.8763, length=0.96647)
 
 FIELDS = {
     "2019 Field":
@@ -115,9 +119,17 @@
               length=8.2296,
               robot=Robot2022,
               field_id="2022"),
+    "2023 Field":
+    FieldType("2023 Field",
+              tags=[],
+              year=2023,
+              width=16.59255,
+              length=8.10895,
+              robot=Robot2023,
+              field_id="//third_party/y2023/field/2023.png"),
 }
 
-FIELD = FIELDS["2022 Field"]
+FIELD = FIELDS["2023 Field"]
 
 
 def get_json_folder(field):
diff --git a/frc971/control_loops/python/path_edit.py b/frc971/control_loops/python/path_edit.py
index 2b55e94..86777e5 100755
--- a/frc971/control_loops/python/path_edit.py
+++ b/frc971/control_loops/python/path_edit.py
@@ -81,9 +81,13 @@
     def set_field(self, field):
         self.field = field
         try:
-            self.field_png = cairo.ImageSurface.create_from_png(
-                "frc971/control_loops/python/field_images/" +
-                self.field.field_id + ".png")
+            if self.field.field_id.startswith('//'):
+                self.field_png = cairo.ImageSurface.create_from_png(
+                    self.field.field_id[2:])
+            else:
+                self.field_png = cairo.ImageSurface.create_from_png(
+                    "frc971/control_loops/python/field_images/" +
+                    self.field.field_id + ".png")
         except cairo.Error:
             self.field_png = None
 
diff --git a/frc971/control_loops/python/spline_graph.py b/frc971/control_loops/python/spline_graph.py
index fbe43bf..ce5efe1 100755
--- a/frc971/control_loops/python/spline_graph.py
+++ b/frc971/control_loops/python/spline_graph.py
@@ -107,7 +107,7 @@
 
         self.file_name_box = Gtk.Entry()
         self.file_name_box.set_size_request(50, 40)
-        self.file_name_box.set_text(FIELD.field_id + ".json")
+        self.file_name_box.set_text("test.json")
         self.file_name_box.set_editable(True)
 
         self.long_input = Gtk.SpinButton()
diff --git a/scouting/deploy/BUILD b/scouting/deploy/BUILD
index eb8b537..c8f68c4 100644
--- a/scouting/deploy/BUILD
+++ b/scouting/deploy/BUILD
@@ -23,8 +23,8 @@
     # So we work around it by manually adding some symlinks that let us pretend
     # that we're at the root of the runfiles tree.
     symlinks = {
-        "opt/frc971/scouting_server/org_frc971": ".",
-        "opt/frc971/scouting_server/bazel_tools": "external/bazel_tools",
+        "org_frc971": ".",
+        "bazel_tools": "external/bazel_tools",
     },
 )
 
diff --git a/scouting/deploy/deploy.py b/scouting/deploy/deploy.py
index f947398..61c0481 100644
--- a/scouting/deploy/deploy.py
+++ b/scouting/deploy/deploy.py
@@ -47,6 +47,8 @@
                 "-c 'create schema public;'",
                 # List all tables as a sanity check.
                 "-c '\dt'",
+                # Make sure we make the visualizations accessible.
+                "-c 'GRANT ALL ON SCHEMA public TO tableau;'",
                 "postgres\"",
             ]),
             shell=True,
diff --git a/y2023/www/2023.png b/third_party/y2023/field/2023.png
similarity index 100%
rename from y2023/www/2023.png
rename to third_party/y2023/field/2023.png
Binary files differ
diff --git a/third_party/y2023/field/BUILD b/third_party/y2023/field/BUILD
index e7ba4e2..de1d382 100644
--- a/third_party/y2023/field/BUILD
+++ b/third_party/y2023/field/BUILD
@@ -1,11 +1,14 @@
-# Pictures from FIRST modified by Tea Fazio.
-# https://firstfrc.blob.core.windows.net/frc2023/Manual/2023FRCGameManual.pdf
-# Copyright 2023 FIRST
-
 filegroup(
     name = "pictures",
     srcs = [
-        "field.jpg",
-    ],
+     # Picture from the FIRST inspires field drawings.
+     # https://www.firstinspires.org/robotics/frc/playing-field
+     # Copyright 2023 FIRST
+     "2023.png",
+     # Picture from FIRST modified by Tea Fazio.
+     # https://firstfrc.blob.core.windows.net/frc2023/Manual/2023FRCGameManual.pdf
+     # Copyright 2023 FIRST
+     "field.jpg",
+ ],
     visibility = ["//visibility:public"],
 )
diff --git a/y2023/control_loops/drivetrain/BUILD b/y2023/control_loops/drivetrain/BUILD
index 521fc09..063e972 100644
--- a/y2023/control_loops/drivetrain/BUILD
+++ b/y2023/control_loops/drivetrain/BUILD
@@ -162,6 +162,7 @@
         "//y2023/constants:constants_fbs",
         "//y2023/control_loops/superstructure:superstructure_position_fbs",
         "//y2023/control_loops/superstructure:superstructure_status_fbs",
+        "//y2023/vision:game_pieces_fbs",
     ],
 )
 
diff --git a/y2023/control_loops/drivetrain/target_selector.cc b/y2023/control_loops/drivetrain/target_selector.cc
index aacfbbf..16a0090 100644
--- a/y2023/control_loops/drivetrain/target_selector.cc
+++ b/y2023/control_loops/drivetrain/target_selector.cc
@@ -3,6 +3,7 @@
 #include "aos/containers/sized_array.h"
 #include "frc971/shooter_interpolation/interpolation.h"
 #include "y2023/control_loops/superstructure/superstructure_position_generated.h"
+#include "y2023/vision/game_pieces_generated.h"
 
 namespace y2023::control_loops::drivetrain {
 namespace {
@@ -15,7 +16,8 @@
     : 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")),
+      superstructure_status_fetcher_(
+          event_loop->MakeFetcher<superstructure::Status>("/superstructure")),
       status_sender_(
           event_loop->MakeSender<TargetSelectorStatus>("/drivetrain")),
       constants_fetcher_(event_loop) {
@@ -33,12 +35,14 @@
             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));
+  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() {
@@ -180,10 +184,13 @@
   superstructure_status_fetcher_.Fetch();
   if (superstructure_status_fetcher_.get() != nullptr) {
     switch (superstructure_status_fetcher_->game_piece()) {
-      case superstructure::GamePiece::NONE:
-      case superstructure::GamePiece::CUBE:
+      case vision::Class::NONE:
+      case vision::Class::CUBE:
         return 0.0;
-      case superstructure::GamePiece::CONE:
+      case vision::Class::CONE_UP:
+        // execute logic below.
+        break;
+      case vision::Class::CONE_DOWN:
         // execute logic below.
         break;
     }
diff --git a/y2023/control_loops/python/graph_edit.py b/y2023/control_loops/python/graph_edit.py
index f18a0b6..5faada9 100644
--- a/y2023/control_loops/python/graph_edit.py
+++ b/y2023/control_loops/python/graph_edit.py
@@ -13,7 +13,7 @@
 gi.require_version('Gtk', '3.0')
 from gi.repository import Gdk, Gtk
 import cairo
-from y2023.control_loops.python.graph_tools import to_theta, to_xy, alpha_blend, shift_angles
+from y2023.control_loops.python.graph_tools import to_theta, to_xy, alpha_blend, shift_angles, get_xy
 from y2023.control_loops.python.graph_tools import l1, l2, joint_center
 from y2023.control_loops.python.graph_tools import DRIVER_CAM_POINTS
 from y2023.control_loops.python import graph_paths
@@ -192,10 +192,45 @@
 DRIVER_CAM_HEIGHT = DRIVER_CAM_POINTS[-1][1] - DRIVER_CAM_POINTS[0][1]
 
 
+class SegmentSelector(basic_window.BaseWindow):
+
+    def __init__(self, segments):
+        super(SegmentSelector, self).__init__()
+
+        self.window = Gtk.Window()
+        self.window.set_title("Segment Selector")
+
+        self.segments = segments
+
+        self.segment_store = Gtk.ListStore(int, str)
+
+        for i, segment in enumerate(segments):
+            self.segment_store.append([i, segment.name])
+
+        self.segment_box = Gtk.ComboBox.new_with_model_and_entry(
+            self.segment_store)
+        self.segment_box.connect("changed", self.on_combo_changed)
+        self.segment_box.set_entry_text_column(1)
+
+        self.current_path_index = None
+
+        self.window.add(self.segment_box)
+        self.window.show_all()
+
+    def on_combo_changed(self, combo):
+        iter = combo.get_active_iter()
+
+        if iter is not None:
+            model = combo.get_model()
+            id, name = model[iter][:2]
+            print("Selected: ID=%d, name=%s" % (id, name))
+            self.current_path_index = id
+
+
 # Create a GTK+ widget on which we will draw using Cairo
 class ArmUi(basic_window.BaseWindow):
 
-    def __init__(self):
+    def __init__(self, segments):
         super(ArmUi, self).__init__()
 
         self.window = Gtk.Window()
@@ -221,7 +256,7 @@
         self.circular_index_select = 1
 
         # Extra stuff for drawing lines.
-        self.segments = []
+        self.segments = segments
         self.prev_segment_pt = None
         self.now_segment_pt = None
         self.spline_edit = 0
@@ -247,6 +282,11 @@
                                     [DRIVER_CAM_X, DRIVER_CAM_Y],
                                     DRIVER_CAM_WIDTH, DRIVER_CAM_HEIGHT)
 
+        self.segment_selector = SegmentSelector(self.segments)
+        self.segment_selector.show()
+
+        self.show_indicators = True
+
     def _do_button_press_internal(self, event):
         o_x = event.x
         o_y = event.y
@@ -295,6 +335,8 @@
     # Handle the expose-event by drawing
     def handle_draw(self, cr):
         # use "with px(cr): blah;" to transform to pixel coordinates.
+        if self.segment_selector.current_path_index is not None:
+            self.index = self.segment_selector.current_path_index
 
         # Fill the background color of the window with grey
         set_color(cr, palette["GREY"])
@@ -378,6 +420,7 @@
             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()
 
@@ -385,6 +428,26 @@
         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()
+        control1 = get_xy(self.segments[self.index].control1)
+        control2 = get_xy(self.segments[self.index].control2)
+
+        if self.theta_version:
+            control1 = shift_angles(self.segments[self.index].control1)
+            control2 = shift_angles(self.segments[self.index].control2)
+
+        if self.show_indicators:
+            set_color(cr, Color(1.0, 0.0, 1.0))
+            cr.move_to(control1[0] + 0.02, control1[1])
+            cr.arc(control1[0], control1[1], 0.02, 0, 2.0 * np.pi)
+            with px(cr):
+                cr.stroke()
+            set_color(cr, Color(1.0, 0.7, 0.0))
+            cr.move_to(control2[0] + 0.02, control2[1])
+            cr.arc(control2[0], control2[1], 0.02, 0, 2.0 * np.pi)
+
         with px(cr):
             cr.stroke()
 
@@ -498,6 +561,9 @@
             print("Switched to segment:", self.segments[self.index].name)
             self.segments[self.index].Print(graph_paths.points)
 
+        elif keyval == Gdk.KEY_i:
+            self.show_indicators = not self.show_indicators
+
         elif keyval == Gdk.KEY_n:
             self.index += 1
             self.index = self.index % len(self.segments)
@@ -563,8 +629,7 @@
         self.redraw()
 
 
-arm_ui = ArmUi()
-arm_ui.segments = graph_paths.segments
+arm_ui = ArmUi(graph_paths.segments)
 print('Starting with segment: ', arm_ui.segments[arm_ui.index].name)
 arm_ui.segments[arm_ui.index].Print(graph_paths.points)
 basic_window.RunApp()
diff --git a/y2023/control_loops/python/graph_paths.py b/y2023/control_loops/python/graph_paths.py
index f7571a6..12a8967 100644
--- a/y2023/control_loops/python/graph_paths.py
+++ b/y2023/control_loops/python/graph_paths.py
@@ -1,3 +1,5 @@
+import sys
+
 import numpy as np
 
 from y2023.control_loops.python.graph_tools import *
@@ -419,3 +421,26 @@
 back_points = []
 unnamed_segments = []
 segments = named_segments + unnamed_segments
+
+# This checks that all points are unique
+
+seen_segments = []
+
+for segment in segments:
+    # check for equality of the start and end values
+
+    if (segment.start.tolist(), segment.end.tolist()) in seen_segments:
+        print("Repeated value")
+        segment.Print(points)
+        sys.exit(1)
+    else:
+        seen_segments.append((segment.start.tolist(), segment.end.tolist()))
+
+seen_points = []
+
+for point in points:
+    if point in seen_points:
+        print(f"Repeated value {point}")
+        sys.exit(1)
+    else:
+        seen_points.append(point)
diff --git a/y2023/control_loops/superstructure/BUILD b/y2023/control_loops/superstructure/BUILD
index a4cb337..2700bbc 100644
--- a/y2023/control_loops/superstructure/BUILD
+++ b/y2023/control_loops/superstructure/BUILD
@@ -33,6 +33,7 @@
     deps = [
         "//frc971/control_loops:control_loops_fbs",
         "//frc971/control_loops:profiled_subsystem_fbs",
+        "//y2023/vision:game_pieces_fbs",
     ],
 )
 
@@ -44,6 +45,7 @@
     deps = [
         "//frc971/control_loops:control_loops_ts_fbs",
         "//frc971/control_loops:profiled_subsystem_ts_fbs",
+        "//y2023/vision:game_pieces_ts_fbs",
     ],
 )
 
@@ -76,6 +78,7 @@
         "//aos/time",
         "//frc971/control_loops:control_loop",
         "//y2023:constants",
+        "//y2023/vision:game_pieces_fbs",
     ],
 )
 
@@ -145,6 +148,7 @@
         "//frc971/control_loops:team_number_test_environment",
         "//frc971/control_loops/drivetrain:drivetrain_status_fbs",
         "//y2023/control_loops/superstructure/roll:roll_plants",
+        "//y2023/vision:game_pieces_fbs",
     ],
 )
 
diff --git a/y2023/control_loops/superstructure/arm/BUILD b/y2023/control_loops/superstructure/arm/BUILD
index 44b2aac..39e549b 100644
--- a/y2023/control_loops/superstructure/arm/BUILD
+++ b/y2023/control_loops/superstructure/arm/BUILD
@@ -22,6 +22,7 @@
         "//y2023/control_loops/superstructure/arm:arm_constants",
         "//y2023/control_loops/superstructure/arm:trajectory",
         "//y2023/control_loops/superstructure/roll:roll_plants",
+        "//y2023/vision:game_pieces_fbs",
     ],
 )
 
diff --git a/y2023/control_loops/superstructure/arm/arm.cc b/y2023/control_loops/superstructure/arm/arm.cc
index 6cd8d0d..fd3028c 100644
--- a/y2023/control_loops/superstructure/arm/arm.cc
+++ b/y2023/control_loops/superstructure/arm/arm.cc
@@ -292,7 +292,6 @@
 
   follower_.Update(X_hat, disable, constants::Values::kArmDt(), vmax_,
                    max_operating_voltage);
-  AOS_LOG(INFO, "Max voltage: %f\n", max_operating_voltage);
 
   arm_ekf_.Predict(follower_.U().head<2>(), constants::Values::kArmDt());
   roll_joint_loop_.UpdateObserver(follower_.U().tail<1>(),
diff --git a/y2023/control_loops/superstructure/end_effector.cc b/y2023/control_loops/superstructure/end_effector.cc
index 287f0e7..4d5d43e 100644
--- a/y2023/control_loops/superstructure/end_effector.cc
+++ b/y2023/control_loops/superstructure/end_effector.cc
@@ -3,6 +3,7 @@
 #include "aos/events/event_loop.h"
 #include "aos/time/time.h"
 #include "frc971/control_loops/control_loop.h"
+#include "y2023/vision/game_pieces_generated.h"
 
 namespace y2023 {
 namespace control_loops {
@@ -12,7 +13,7 @@
 
 EndEffector::EndEffector()
     : state_(EndEffectorState::IDLE),
-      game_piece_(GamePiece::NONE),
+      game_piece_(vision::Class::NONE),
       timer_(aos::monotonic_clock::min_time),
       beambreak_(false) {}
 
@@ -25,16 +26,22 @@
   constexpr double kMinCurrent = 40.0;
   constexpr double kMaxConePosition = 0.92;
 
-  bool beambreak_status = (beambreak || (falcon_current > kMinCurrent &&
-                                         cone_position < kMaxConePosition));
-
   // Let them switch game pieces
-  if (roller_goal == RollerGoal::INTAKE_CONE) {
-    game_piece_ = GamePiece::CONE;
+  if (roller_goal == RollerGoal::INTAKE_CONE_UP) {
+    game_piece_ = vision::Class::CONE_UP;
+  } else if (roller_goal == RollerGoal::INTAKE_CONE_DOWN) {
+    game_piece_ = vision::Class::CONE_DOWN;
   } else if (roller_goal == RollerGoal::INTAKE_CUBE) {
-    game_piece_ = GamePiece::CUBE;
+    game_piece_ = vision::Class::CUBE;
   }
 
+  bool beambreak_status =
+      (((game_piece_ == vision::Class::CUBE ||
+         game_piece_ == vision::Class::CONE_UP) &&
+        beambreak) ||
+       ((game_piece_ == vision::Class::CONE_DOWN &&
+         falcon_current > kMinCurrent && cone_position < kMaxConePosition)));
+
   // Go into spitting if we were told to, no matter where we are
   if (roller_goal == RollerGoal::SPIT && state_ != EndEffectorState::SPITTING) {
     state_ = EndEffectorState::SPITTING;
@@ -47,7 +54,8 @@
   switch (state_) {
     case EndEffectorState::IDLE:
       // If idle and intake requested, intake
-      if (roller_goal == RollerGoal::INTAKE_CONE ||
+      if (roller_goal == RollerGoal::INTAKE_CONE_UP ||
+          roller_goal == RollerGoal::INTAKE_CONE_DOWN ||
           roller_goal == RollerGoal::INTAKE_CUBE ||
           roller_goal == RollerGoal::INTAKE_LAST) {
         state_ = EndEffectorState::INTAKING;
@@ -56,7 +64,8 @@
       break;
     case EndEffectorState::INTAKING:
       // If intaking and beam break is not triggered, keep intaking
-      if (roller_goal == RollerGoal::INTAKE_CONE ||
+      if (roller_goal == RollerGoal::INTAKE_CONE_UP ||
+          roller_goal == RollerGoal::INTAKE_CONE_DOWN ||
           roller_goal == RollerGoal::INTAKE_CUBE ||
           roller_goal == RollerGoal::INTAKE_LAST) {
         timer_ = timestamp;
@@ -72,7 +81,7 @@
         break;
       }
 
-      if (game_piece_ == GamePiece::CUBE) {
+      if (game_piece_ == vision::Class::CUBE) {
         *roller_voltage = kRollerCubeSuckVoltage();
       } else {
         *roller_voltage = kRollerConeSuckVoltage();
@@ -88,7 +97,7 @@
       break;
     case EndEffectorState::SPITTING:
       // If spit requested, spit
-      if (game_piece_ == GamePiece::CUBE) {
+      if (game_piece_ == vision::Class::CUBE) {
         *roller_voltage = kRollerCubeSpitVoltage();
       } else {
         *roller_voltage = kRollerConeSpitVoltage();
@@ -100,7 +109,7 @@
       } else if (timestamp > timer_ + constants::Values::kExtraSpittingTime()) {
         // Finished spitting
         state_ = EndEffectorState::IDLE;
-        game_piece_ = GamePiece::NONE;
+        game_piece_ = vision::Class::NONE;
       }
 
       break;
diff --git a/y2023/control_loops/superstructure/end_effector.h b/y2023/control_loops/superstructure/end_effector.h
index 14245c8..5ae96da 100644
--- a/y2023/control_loops/superstructure/end_effector.h
+++ b/y2023/control_loops/superstructure/end_effector.h
@@ -7,6 +7,7 @@
 #include "y2023/constants.h"
 #include "y2023/control_loops/superstructure/superstructure_goal_generated.h"
 #include "y2023/control_loops/superstructure/superstructure_status_generated.h"
+#include "y2023/vision/game_pieces_generated.h"
 
 namespace y2023 {
 namespace control_loops {
@@ -26,12 +27,12 @@
                     double cone_position, bool beambreak,
                     double *intake_roller_voltage);
   EndEffectorState state() const { return state_; }
-  GamePiece game_piece() const { return game_piece_; }
+  vision::Class game_piece() const { return game_piece_; }
   void Reset();
 
  private:
   EndEffectorState state_;
-  GamePiece game_piece_;
+  vision::Class game_piece_;
 
   aos::monotonic_clock::time_point timer_;
 
diff --git a/y2023/control_loops/superstructure/superstructure_goal.fbs b/y2023/control_loops/superstructure/superstructure_goal.fbs
index ee99f1c..670351a 100644
--- a/y2023/control_loops/superstructure/superstructure_goal.fbs
+++ b/y2023/control_loops/superstructure/superstructure_goal.fbs
@@ -4,10 +4,11 @@
 
 enum RollerGoal: ubyte {
     IDLE = 0,
-    INTAKE_CONE = 1,
+    INTAKE_CONE_UP = 1,
     INTAKE_CUBE = 2,
     INTAKE_LAST = 3,
     SPIT = 4,
+    INTAKE_CONE_DOWN = 5,
 }
 
 table Goal {
diff --git a/y2023/control_loops/superstructure/superstructure_lib_test.cc b/y2023/control_loops/superstructure/superstructure_lib_test.cc
index ee440ed..e2524c1 100644
--- a/y2023/control_loops/superstructure/superstructure_lib_test.cc
+++ b/y2023/control_loops/superstructure/superstructure_lib_test.cc
@@ -562,10 +562,10 @@
 
 class SuperstructureBeambreakTest
     : public SuperstructureTest,
-      public ::testing::WithParamInterface<GamePiece> {
+      public ::testing::WithParamInterface<vision::Class> {
  public:
-  void SetBeambreak(GamePiece game_piece, bool status) {
-    if (game_piece == GamePiece::CONE) {
+  void SetBeambreak(vision::Class game_piece, bool status) {
+    if (game_piece == vision::Class::CONE_UP) {
       // TODO(milind): handle cone
     } else {
       superstructure_plant_.set_end_effector_cube_beam_break(status);
@@ -577,12 +577,20 @@
   SetEnabled(true);
   WaitUntilZeroed();
 
-  double spit_voltage =
-      (GetParam() == GamePiece::CUBE ? EndEffector::kRollerCubeSpitVoltage()
-                                     : EndEffector::kRollerConeSpitVoltage());
-  double suck_voltage =
-      (GetParam() == GamePiece::CUBE ? EndEffector::kRollerCubeSuckVoltage()
-                                     : EndEffector::kRollerConeSuckVoltage());
+  double spit_voltage = (GetParam() == vision::Class::CUBE
+                             ? EndEffector::kRollerCubeSpitVoltage()
+                             : EndEffector::kRollerConeSpitVoltage());
+  double suck_voltage = (GetParam() == vision::Class::CUBE
+                             ? EndEffector::kRollerCubeSuckVoltage()
+                             : EndEffector::kRollerConeSuckVoltage());
+
+  RollerGoal roller_goal = RollerGoal::INTAKE_CUBE;
+
+  if (GetParam() == vision::Class::CONE_DOWN) {
+    roller_goal = RollerGoal::INTAKE_CONE_DOWN;
+  } else if (GetParam() == vision::Class::CONE_UP) {
+    roller_goal = RollerGoal::INTAKE_CONE_UP;
+  }
 
   {
     auto builder = superstructure_goal_sender_.MakeBuilder();
@@ -591,9 +599,7 @@
 
     goal_builder.add_arm_goal_position(arm::NeutralIndex());
     goal_builder.add_trajectory_override(false);
-    goal_builder.add_roller_goal(GetParam() == GamePiece::CONE
-                                     ? RollerGoal::INTAKE_CONE
-                                     : RollerGoal::INTAKE_CUBE);
+    goal_builder.add_roller_goal(roller_goal);
 
     builder.CheckOk(builder.Send(goal_builder.Finish()));
   }
@@ -668,9 +674,7 @@
 
     goal_builder.add_arm_goal_position(arm::NeutralIndex());
     goal_builder.add_trajectory_override(false);
-    goal_builder.add_roller_goal(GetParam() == GamePiece::CONE
-                                     ? RollerGoal::INTAKE_CONE
-                                     : RollerGoal::INTAKE_CUBE);
+    goal_builder.add_roller_goal(roller_goal);
 
     builder.CheckOk(builder.Send(goal_builder.Finish()));
   }
@@ -757,7 +761,7 @@
   EXPECT_EQ(superstructure_output_fetcher_->roller_voltage(), 0.0);
   EXPECT_EQ(superstructure_status_fetcher_->end_effector_state(),
             EndEffectorState::IDLE);
-  EXPECT_EQ(superstructure_status_fetcher_->game_piece(), GamePiece::NONE);
+  EXPECT_EQ(superstructure_status_fetcher_->game_piece(), vision::Class::NONE);
 }
 
 // Tests that we don't freak out without a goal.
@@ -834,7 +838,7 @@
 
 // TODO(milind): add cone
 INSTANTIATE_TEST_SUITE_P(EndEffectorGoal, SuperstructureBeambreakTest,
-                         ::testing::Values(GamePiece::CUBE));
+                         ::testing::Values(vision::Class::CUBE));
 
 }  // namespace testing
 }  // namespace superstructure
diff --git a/y2023/control_loops/superstructure/superstructure_status.fbs b/y2023/control_loops/superstructure/superstructure_status.fbs
index 80a0d3d..5381b0a 100644
--- a/y2023/control_loops/superstructure/superstructure_status.fbs
+++ b/y2023/control_loops/superstructure/superstructure_status.fbs
@@ -1,4 +1,5 @@
 include "frc971/control_loops/control_loops.fbs";
+include "y2023/vision/game_pieces.fbs";
 include "frc971/control_loops/profiled_subsystem.fbs";
 
 namespace y2023.control_loops.superstructure;
@@ -76,12 +77,6 @@
   SPITTING = 3,
 }
 
-enum GamePiece : ubyte {
-  NONE = 0,
-  CONE = 1,
-  CUBE = 2,
-}
-
 table Status {
   // All subsystems know their location.
   zeroed:bool (id: 0);
@@ -94,7 +89,7 @@
   wrist:frc971.control_loops.AbsoluteEncoderProfiledJointStatus (id: 3);
 
   end_effector_state:EndEffectorState (id: 4);
-  game_piece:GamePiece (id: 5);
+  game_piece:vision.Class (id: 5);
 }
 
 root_type Status;
diff --git a/y2023/joystick_reader.cc b/y2023/joystick_reader.cc
index 43962dd..4c4b1e3 100644
--- a/y2023/joystick_reader.cc
+++ b/y2023/joystick_reader.cc
@@ -43,7 +43,7 @@
 namespace joysticks {
 
 // TODO(milind): add correct locations
-const ButtonLocation kScore(4, 4);
+const ButtonLocation kDriverSpit(2, 1);
 const ButtonLocation kSpit(4, 13);
 
 const ButtonLocation kHighConeScoreLeft(4, 14);
@@ -68,6 +68,7 @@
 const ButtonLocation kBack(4, 12);
 
 const ButtonLocation kWrist(4, 10);
+const ButtonLocation kStayIn(3, 4);
 
 namespace superstructure = y2023::control_loops::superstructure;
 namespace arm = superstructure::arm;
@@ -334,10 +335,10 @@
     std::optional<double> score_wrist_goal = std::nullopt;
 
     if (data.IsPressed(kGroundPickupConeUp) || data.IsPressed(kHPConePickup)) {
-      roller_goal = RollerGoal::INTAKE_CONE;
+      roller_goal = RollerGoal::INTAKE_CONE_UP;
       current_game_piece_ = GamePiece::CONE_UP;
     } else if (data.IsPressed(kGroundPickupConeDownBase)) {
-      roller_goal = RollerGoal::INTAKE_CONE;
+      roller_goal = RollerGoal::INTAKE_CONE_DOWN;
       current_game_piece_ = GamePiece::CONE_DOWN;
     } else if (data.IsPressed(kGroundPickupCube)) {
       roller_goal = RollerGoal::INTAKE_CUBE;
@@ -386,9 +387,12 @@
 
     // And, pull the bits out of it.
     if (current_setpoint_ != nullptr) {
-      wrist_goal = current_setpoint_->wrist_goal;
-      arm_goal_position_ = current_setpoint_->index;
-      score_wrist_goal = current_setpoint_->score_wrist_goal;
+      if (!data.IsPressed(kStayIn)) {
+        wrist_goal = current_setpoint_->wrist_goal;
+        arm_goal_position_ = current_setpoint_->index;
+        score_wrist_goal = current_setpoint_->score_wrist_goal;
+      }
+
       placing_row = current_setpoint_->row_hint;
     }
 
@@ -396,7 +400,7 @@
 
     if (data.IsPressed(kSuck)) {
       roller_goal = RollerGoal::INTAKE_LAST;
-    } else if (data.IsPressed(kSpit)) {
+    } else if (data.IsPressed(kSpit) || data.IsPressed(kDriverSpit)) {
       if (score_wrist_goal.has_value()) {
         wrist_goal = score_wrist_goal.value();
 
diff --git a/y2023/localizer/localizer.cc b/y2023/localizer/localizer.cc
index 3a6de93..ffc1df3 100644
--- a/y2023/localizer/localizer.cc
+++ b/y2023/localizer/localizer.cc
@@ -166,7 +166,7 @@
   }
 
   event_loop_->AddPhasedLoop([this](int) { SendOutput(); },
-                             std::chrono::milliseconds(5));
+                             std::chrono::milliseconds(20));
 
   event_loop_->MakeWatcher(
       "/drivetrain",
diff --git a/y2023/localizer/localizer_test.cc b/y2023/localizer/localizer_test.cc
index 947771f..57b9dd0 100644
--- a/y2023/localizer/localizer_test.cc
+++ b/y2023/localizer/localizer_test.cc
@@ -332,7 +332,9 @@
 // correctly.
 TEST_F(LocalizerTest, NominalSpinInPlace) {
   output_voltages_ << -1.0, 1.0;
-  event_loop_factory_.RunFor(std::chrono::seconds(2));
+  // Go 1 ms over 2 sec to make sure we actually see relatively recent messages
+  // on each channel.
+  event_loop_factory_.RunFor(std::chrono::milliseconds(2001));
   CHECK(output_fetcher_.Fetch());
   CHECK(status_fetcher_.Fetch());
   // The two can be different because they may've been sent at different
diff --git a/y2023/vision/BUILD b/y2023/vision/BUILD
index e90b825..68ba833 100644
--- a/y2023/vision/BUILD
+++ b/y2023/vision/BUILD
@@ -1,4 +1,5 @@
 load("@com_github_google_flatbuffers//:build_defs.bzl", "flatbuffer_cc_library")
+load("@com_github_google_flatbuffers//:typescript.bzl", "flatbuffer_ts_library")
 
 cc_binary(
     name = "camera_reader",
@@ -239,3 +240,10 @@
     target_compatible_with = ["@platforms//os:linux"],
     visibility = ["//visibility:public"],
 )
+
+flatbuffer_ts_library(
+    name = "game_pieces_ts_fbs",
+    srcs = ["game_pieces.fbs"],
+    target_compatible_with = ["@platforms//os:linux"],
+    visibility = ["//visibility:public"],
+)
diff --git a/y2023/vision/game_pieces.fbs b/y2023/vision/game_pieces.fbs
index 773cc93..89f7505 100644
--- a/y2023/vision/game_pieces.fbs
+++ b/y2023/vision/game_pieces.fbs
@@ -2,9 +2,10 @@
 
 // Object class.
 enum Class : byte {
-    CONE_DOWN,
-    CONE_UP,
-    CUBE
+    NONE = 0,
+    CONE_UP = 1,
+    CUBE = 2,
+    CONE_DOWN = 3,
 }
 
 // Bounding box dimensions and position.
@@ -27,4 +28,4 @@
     best_piece:uint (id: 1); // Index of the "best piece".
 }
 
-root_type GamePieces;
\ No newline at end of file
+root_type GamePieces;
diff --git a/y2023/www/BUILD b/y2023/www/BUILD
index f8b706c..09cd4d8 100644
--- a/y2023/www/BUILD
+++ b/y2023/www/BUILD
@@ -7,10 +7,17 @@
         "**/*.html",
         "**/*.css",
         "**/*.png",
-    ]),
+    ]) + ["2023.png"],
     visibility = ["//visibility:public"],
 )
 
+genrule(
+    name = "2023_field_png",
+    srcs = ["//third_party/y2023/field:pictures"],
+    outs = ["2023.png"],
+    cmd = "cp third_party/y2023/field/2023.png $@",
+)
+
 ts_project(
     name = "field_main",
     srcs = [
@@ -25,6 +32,7 @@
         "//aos/network/www:proxy",
         "//frc971/control_loops/drivetrain:drivetrain_status_ts_fbs",
         "//frc971/control_loops/drivetrain/localization:localizer_output_ts_fbs",
+        "//y2023/control_loops/superstructure:superstructure_status_ts_fbs",
         "//y2023/localizer:status_ts_fbs",
         "//y2023/localizer:visualization_ts_fbs",
         "@com_github_google_flatbuffers//ts:flatbuffers_ts",
diff --git a/y2023/www/constants.ts b/y2023/www/constants.ts
index b94d7a7..d6ecfaf 100644
--- a/y2023/www/constants.ts
+++ b/y2023/www/constants.ts
@@ -2,6 +2,7 @@
 export const IN_TO_M = 0.0254;
 export const FT_TO_M = 0.3048;
 // Dimensions of the field in meters
-export const FIELD_WIDTH = 26 * FT_TO_M + 11.25 * IN_TO_M;
-export const FIELD_LENGTH = 52 * FT_TO_M + 5.25 * IN_TO_M;
+// Numbers are slightly hand-tuned to match the PNG that we are using.
+export const FIELD_WIDTH = 26 * FT_TO_M + 7.25 * IN_TO_M;
+export const FIELD_LENGTH = 54 * FT_TO_M + 5.25 * IN_TO_M;
 
diff --git a/y2023/www/field.html b/y2023/www/field.html
index 72d8f54..52e0d11 100644
--- a/y2023/www/field.html
+++ b/y2023/www/field.html
@@ -34,6 +34,63 @@
           <td id="images_accepted"> NA </td>
         </tr>
       </table>
+
+      <table>
+	      <tr>
+		      <th colspan="2">Superstructure</th>
+		</tr>
+		<tr>
+			<td>End Effector State</td>
+			<td id="end_effector_state"> NA </td>
+		</tr>
+		<tr>
+			<td>Wrist</td>
+			<td id="wrist"> NA </td>
+		</tr>
+	</table>
+	<table>
+		<tr>
+			<th colspan="2">Game Piece</th>
+		</tr>
+		<tr>
+			<td>Game Piece Held</td>
+			<td id="game_piece"> NA </td>
+		</tr>
+	</table>
+
+	<table>
+		<tr>
+			<th colspan="2">Arm</th>
+		</tr>
+		<tr>
+			<td>State</td>
+			<td id="arm_state"> NA </td>
+		</tr>
+		<tr>
+			<td>X</td>
+			<td id="arm_x"> NA </td>
+		</tr>
+		<tr>
+			<td>Y</td>
+			<td id="arm_y"> NA </td>
+		</tr>
+		<tr>
+			<td>Circular Index</td>
+			<td id="arm_circular_index"> NA </td>
+		</tr>
+		<tr>
+			<td>Roll</td>
+			<td id="arm_roll"> NA </td>
+		</tr>
+		<tr>
+			<td>Proximal</td>
+			<td id="arm_proximal"> NA </td>
+		</tr>
+		<tr>
+			<td>Distal</td>
+			<td id="arm_distal"> NA </td>
+		</tr>
+	</table>
     </div>
     <div id="vision_readouts">
     </div>
diff --git a/y2023/www/field_handler.ts b/y2023/www/field_handler.ts
index 9e2d9ed..db881af 100644
--- a/y2023/www/field_handler.ts
+++ b/y2023/www/field_handler.ts
@@ -3,6 +3,8 @@
 import {LocalizerOutput} from '../../frc971/control_loops/drivetrain/localization/localizer_output_generated';
 import {RejectionReason} from '../localizer/status_generated';
 import {Status as DrivetrainStatus} from '../../frc971/control_loops/drivetrain/drivetrain_status_generated';
+import {Status as SuperstructureStatus, EndEffectorState, ArmState, ArmStatus} from '../control_loops/superstructure/superstructure_status_generated'
+import {Class} from '../vision/game_pieces_generated'
 import {Visualization, TargetEstimateDebug} from '../localizer/visualization_generated';
 
 import {FIELD_LENGTH, FIELD_WIDTH, FT_TO_M, IN_TO_M} from './constants';
@@ -21,6 +23,7 @@
   private canvas = document.createElement('canvas');
   private localizerOutput: LocalizerOutput|null = null;
   private drivetrainStatus: DrivetrainStatus|null = null;
+  private superstructureStatus: SuperstructureStatus|null = null;
 
   // Image information indexed by timestamp (seconds since the epoch), so that
   // we can stop displaying images after a certain amount of time.
@@ -33,6 +36,26 @@
       (document.getElementById('images_accepted') as HTMLElement);
   private rejectionReasonCells: HTMLElement[] = [];
   private fieldImage: HTMLImageElement = new Image();
+  private endEffectorState: HTMLElement =
+	  (document.getElementById('end_effector_state') as HTMLElement);
+  private wrist: HTMLElement =
+	  (document.getElementById('wrist') as HTMLElement);
+  private armState: HTMLElement =
+	  (document.getElementById('arm_state') as HTMLElement);
+  private gamePiece: HTMLElement =
+	  (document.getElementById('game_piece') as HTMLElement);
+  private armX: HTMLElement =
+	  (document.getElementById('arm_x') as HTMLElement);
+  private armY: HTMLElement =
+	  (document.getElementById('arm_y') as HTMLElement);
+  private circularIndex: HTMLElement =
+	  (document.getElementById('arm_circular_index') as HTMLElement);
+  private roll: HTMLElement =
+	  (document.getElementById('arm_roll') as HTMLElement);
+  private proximal: HTMLElement =
+	  (document.getElementById('arm_proximal') as HTMLElement);
+  private distal: HTMLElement =
+	  (document.getElementById('arm_distal') as HTMLElement);
 
   constructor(private readonly connection: Connection) {
     (document.getElementById('field') as HTMLElement).appendChild(this.canvas);
@@ -81,6 +104,11 @@
                '/localizer', "frc971.controls.LocalizerOutput", (data) => {
             this.handleLocalizerOutput(data);
           });
+	this.connection.addHandler(
+		'/superstructure', "y2023.control_loops.superstructure.Status",
+		(data) => {
+			this.handleSuperstructureStatus(data)
+		});
     });
   }
 
@@ -117,6 +145,11 @@
     this.drivetrainStatus = DrivetrainStatus.getRootAsStatus(fbBuffer);
   }
 
+  private handleSuperstructureStatus(data: Uint8Array): void {
+	  const fbBuffer = new ByteBuffer(data);
+	  this.superstructureStatus = SuperstructureStatus.getRootAsStatus(fbBuffer);
+  }
+
   drawField(): void {
     const ctx = this.canvas.getContext('2d');
     ctx.save();
@@ -179,6 +212,25 @@
     div.classList.remove('near');
   }
 
+  setEstopped(div: HTMLElement): void {
+	  div.innerHTML = 'estopped';
+	  div.classList.add('faulted');
+	  div.classList.remove('zeroing');
+	  div.classList.remove('near');
+  }
+
+  setTargetValue(
+	  div: HTMLElement, target: number, val: number, tolerance: number): void {
+	  div.innerHTML = val.toFixed(4);
+	  div.classList.remove('faulted');
+	  div.classList.remove('zeroing');
+	  if (Math.abs(target - val) < tolerance) {
+		  div.classList.add('near');
+	  } else {
+		  div.classList.remove('near');
+	  }
+  }
+
   setValue(div: HTMLElement, val: number): void {
     div.innerHTML = val.toFixed(4);
     div.classList.remove('faulted');
@@ -193,6 +245,39 @@
     // Draw the matches with debugging information from the localizer.
     const now = Date.now() / 1000.0;
 
+    if (this.superstructureStatus) {
+	    this.endEffectorState.innerHTML =
+		    EndEffectorState[this.superstructureStatus.endEffectorState()];
+	    if (!this.superstructureStatus.wrist() ||
+		!this.superstructureStatus.wrist().zeroed()) {
+		    this.setZeroing(this.wrist);
+	    } else if (this.superstructureStatus.wrist().estopped()) {
+		    this.setEstopped(this.wrist);
+	    } else {
+		    this.setTargetValue(
+		    	this.wrist,
+		    	this.superstructureStatus.wrist().unprofiledGoalPosition(),
+		    	this.superstructureStatus.wrist().estimatorState().position(),
+		    	1e-3);
+	    }
+	    this.armState.innerHTML =
+		    ArmState[this.superstructureStatus.arm().state()];
+	    this.gamePiece.innerHTML =
+		    Class[this.superstructureStatus.gamePiece()];
+	    this.armX.innerHTML =
+		    this.superstructureStatus.arm().armX().toFixed(2);
+	    this.armY.innerHTML =
+		    this.superstructureStatus.arm().armY().toFixed(2);
+	    this.circularIndex.innerHTML =
+		    this.superstructureStatus.arm().armCircularIndex().toFixed(0);
+	    this.roll.innerHTML =
+		    this.superstructureStatus.arm().rollJointEstimatorState().position().toFixed(2);
+	    this.proximal.innerHTML =
+		    this.superstructureStatus.arm().proximalEstimatorState().position().toFixed(2);
+	    this.distal.innerHTML =
+		    this.superstructureStatus.arm().distalEstimatorState().position().toFixed(2);
+    }
+
     if (this.drivetrainStatus && this.drivetrainStatus.trajectoryLogging()) {
       this.drawRobot(
           this.drivetrainStatus.trajectoryLogging().x(),
diff --git a/y2023/y2023_imu.json b/y2023/y2023_imu.json
index 07c3b08..96134f8 100644
--- a/y2023/y2023_imu.json
+++ b/y2023/y2023_imu.json
@@ -312,7 +312,7 @@
       "name": "/localizer",
       "type": "frc971.controls.LocalizerOutput",
       "source_node": "imu",
-      "frequency": 210,
+      "frequency": 52,
       "logger": "LOCAL_AND_REMOTE_LOGGER",
       "logger_nodes": [
         "roborio"
@@ -334,7 +334,7 @@
       "type": "aos.message_bridge.RemoteMessage",
       "source_node": "imu",
       "logger": "NOT_LOGGED",
-      "frequency": 210,
+      "frequency": 52,
       "num_senders": 2,
       "max_size": 200
     },
diff --git a/y2023/y2023_logger.json b/y2023/y2023_logger.json
index 861372a..f4ac45e 100644
--- a/y2023/y2023_logger.json
+++ b/y2023/y2023_logger.json
@@ -21,26 +21,6 @@
       ]
     },
     {
-      "name": "/drivetrain",
-      "type": "frc971.control_loops.drivetrain.Position",
-      "source_node": "roborio",
-      "logger": "LOCAL_AND_REMOTE_LOGGER",
-      "logger_nodes": [
-        "logger"
-      ],
-      "destination_nodes": [
-        {
-          "name": "logger",
-          "priority": 2,
-          "time_to_live": 500000000,
-          "timestamp_logger": "LOCAL_AND_REMOTE_LOGGER",
-          "timestamp_logger_nodes": [
-            "roborio"
-          ]
-        }
-      ]
-    },
-    {
       "name": "/logger/camera",
       "type": "y2023.vision.GamePieces",
       "source_node": "logger",
@@ -73,44 +53,6 @@
       "max_size": 200
     },
     {
-      "name": "/roborio/aos/remote_timestamps/logger/drivetrain/frc971-control_loops-drivetrain-Position",
-      "type": "aos.message_bridge.RemoteMessage",
-      "source_node": "roborio",
-      "logger": "NOT_LOGGED",
-      "frequency": 400,
-      "num_senders": 2,
-      "max_size": 200
-    },
-    {
-      "name": "/drivetrain",
-      "type": "frc971.control_loops.drivetrain.Output",
-      "source_node": "roborio",
-      "logger": "LOCAL_AND_REMOTE_LOGGER",
-      "logger_nodes": [
-        "logger"
-      ],
-      "destination_nodes": [
-        {
-          "name": "logger",
-          "priority": 2,
-          "time_to_live": 500000000,
-          "timestamp_logger": "LOCAL_AND_REMOTE_LOGGER",
-          "timestamp_logger_nodes": [
-            "roborio"
-          ]
-        }
-      ]
-    },
-    {
-      "name": "/roborio/aos/remote_timestamps/logger/drivetrain/frc971-control_loops-drivetrain-Output",
-      "type": "aos.message_bridge.RemoteMessage",
-      "source_node": "roborio",
-      "logger": "NOT_LOGGED",
-      "frequency": 400,
-      "num_senders": 2,
-      "max_size": 400
-    },
-    {
       "name": "/pi1/aos",
       "type": "aos.message_bridge.Timestamp",
       "source_node": "pi1",