Create y2024_bot3 folder

Change-Id: Iee232bde58c0425920a49eee145a0f93c7485391
Signed-off-by: Yash Maheshwari <yashmahe2018@gmail.com>
Signed-off-by: James (Peilun) Li <jamespeilunli@gmail.com>
diff --git a/y2024_bot3/control_loops/BUILD b/y2024_bot3/control_loops/BUILD
new file mode 100644
index 0000000..55c0974
--- /dev/null
+++ b/y2024_bot3/control_loops/BUILD
@@ -0,0 +1,6 @@
+py_library(
+    name = "python_init",
+    srcs = ["__init__.py"],
+    visibility = ["//visibility:public"],
+    deps = ["//y2024_bot3:python_init"],
+)
diff --git a/y2024_bot3/control_loops/__init__.py b/y2024_bot3/control_loops/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/y2024_bot3/control_loops/__init__.py
diff --git a/y2024_bot3/control_loops/python/BUILD b/y2024_bot3/control_loops/python/BUILD
new file mode 100644
index 0000000..9ec6b57
--- /dev/null
+++ b/y2024_bot3/control_loops/python/BUILD
@@ -0,0 +1,8 @@
+package(default_visibility = ["//y2024_bot3:__subpackages__"])
+
+py_library(
+    name = "python_init",
+    srcs = ["__init__.py"],
+    visibility = ["//visibility:public"],
+    deps = ["//y2024_bot3/control_loops:python_init"],
+)
diff --git a/y2024_bot3/control_loops/python/__init__.py b/y2024_bot3/control_loops/python/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/y2024_bot3/control_loops/python/__init__.py
diff --git a/y2024_bot3/control_loops/superstructure/BUILD b/y2024_bot3/control_loops/superstructure/BUILD
new file mode 100644
index 0000000..8d909bf
--- /dev/null
+++ b/y2024_bot3/control_loops/superstructure/BUILD
@@ -0,0 +1,170 @@
+load("@com_github_google_flatbuffers//:typescript.bzl", "flatbuffer_ts_library")
+load("//aos/flatbuffers:generate.bzl", "static_flatbuffer")
+load("//tools/build_rules:js.bzl", "ts_project")
+
+package(default_visibility = ["//visibility:public"])
+
+static_flatbuffer(
+    name = "superstructure_goal_fbs",
+    srcs = [
+        "superstructure_goal.fbs",
+    ],
+    deps = [
+        "//frc971/control_loops:control_loops_fbs",
+        "//frc971/control_loops:profiled_subsystem_fbs",
+    ],
+)
+
+static_flatbuffer(
+    name = "superstructure_output_fbs",
+    srcs = [
+        "superstructure_output.fbs",
+    ],
+)
+
+static_flatbuffer(
+    name = "superstructure_status_fbs",
+    srcs = [
+        "superstructure_status.fbs",
+    ],
+    deps = [
+        "//frc971/control_loops:control_loops_fbs",
+        "//frc971/control_loops:profiled_subsystem_fbs",
+    ],
+)
+
+flatbuffer_ts_library(
+    name = "superstructure_status_ts_fbs",
+    srcs = [
+        "superstructure_status.fbs",
+    ],
+    deps = [
+        "//frc971/control_loops:control_loops_ts_fbs",
+        "//frc971/control_loops:profiled_subsystem_ts_fbs",
+    ],
+)
+
+static_flatbuffer(
+    name = "superstructure_position_fbs",
+    srcs = [
+        "superstructure_position.fbs",
+    ],
+    deps = [
+        "//frc971/control_loops:control_loops_fbs",
+        "//frc971/control_loops:profiled_subsystem_fbs",
+    ],
+)
+
+flatbuffer_ts_library(
+    name = "superstructure_position_ts_fbs",
+    srcs = [
+        "superstructure_position.fbs",
+    ],
+    deps = [
+        "//frc971/control_loops:control_loops_ts_fbs",
+        "//frc971/control_loops:profiled_subsystem_ts_fbs",
+    ],
+)
+
+static_flatbuffer(
+    name = "superstructure_can_position_fbs",
+    srcs = ["superstructure_can_position.fbs"],
+    deps = ["//frc971/control_loops:can_talonfx_fbs"],
+)
+
+flatbuffer_ts_library(
+    name = "superstructure_can_position_ts_fbs",
+    srcs = ["superstructure_can_position.fbs"],
+    target_compatible_with = ["@platforms//os:linux"],
+    deps = ["//frc971/control_loops:can_talonfx_ts_fbs"],
+)
+
+cc_library(
+    name = "superstructure_lib",
+    srcs = [
+        "superstructure.cc",
+    ],
+    hdrs = [
+        "superstructure.h",
+    ],
+    data = [],
+    deps = [
+        ":superstructure_goal_fbs",
+        ":superstructure_output_fbs",
+        ":superstructure_position_fbs",
+        ":superstructure_status_fbs",
+        "//aos:flatbuffer_merge",
+        "//aos/events:event_loop",
+        "//frc971/constants:constants_sender_lib",
+        "//frc971/control_loops:control_loop",
+        "//frc971/zeroing:absolute_encoder",
+        "//frc971/zeroing:pot_and_absolute_encoder",
+        "//y2024_bot3:constants",
+        "//y2024_bot3/constants:constants_fbs",
+        "//y2024_bot3/constants:simulated_constants_sender",
+    ],
+)
+
+cc_binary(
+    name = "superstructure",
+    srcs = [
+        "superstructure_main.cc",
+    ],
+    deps = [
+        ":superstructure_lib",
+        "//aos:init",
+        "//aos/events:shm_event_loop",
+    ],
+)
+
+cc_test(
+    name = "superstructure_lib_test",
+    srcs = [
+        "superstructure_lib_test.cc",
+    ],
+    data = [
+        "//y2024_bot3:aos_config",
+    ],
+    deps = [
+        ":superstructure_can_position_fbs",
+        ":superstructure_goal_fbs",
+        ":superstructure_lib",
+        ":superstructure_output_fbs",
+        ":superstructure_position_fbs",
+        ":superstructure_status_fbs",
+        "//aos:json_to_flatbuffer",
+        "//aos:math",
+        "//aos/events/logging:log_writer",
+        "//aos/testing:googletest",
+        "//aos/time",
+        "//frc971/control_loops:capped_test_plant",
+        "//frc971/control_loops:control_loop_test",
+        "//frc971/control_loops:position_sensor_sim",
+        "//frc971/control_loops:subsystem_simulator",
+        "//frc971/control_loops:team_number_test_environment",
+    ],
+)
+
+cc_binary(
+    name = "superstructure_replay",
+    srcs = ["superstructure_replay.cc"],
+    deps = [
+        ":superstructure_lib",
+        "//aos:configuration",
+        "//aos:init",
+        "//aos/events:simulated_event_loop",
+        "//aos/events/logging:log_reader",
+        "//aos/network:team_number",
+    ],
+)
+
+ts_project(
+    name = "superstructure_plotter",
+    srcs = ["superstructure_plotter.ts"],
+    target_compatible_with = ["@platforms//os:linux"],
+    deps = [
+        "//aos/network/www:aos_plotter",
+        "//aos/network/www:colors",
+        "//aos/network/www:proxy",
+    ],
+)
diff --git a/y2024_bot3/control_loops/superstructure/superstructure.cc b/y2024_bot3/control_loops/superstructure/superstructure.cc
new file mode 100644
index 0000000..c18da15
--- /dev/null
+++ b/y2024_bot3/control_loops/superstructure/superstructure.cc
@@ -0,0 +1,71 @@
+#include "y2024_bot3/control_loops/superstructure/superstructure.h"
+
+#include <chrono>
+
+#include "aos/events/event_loop.h"
+#include "aos/flatbuffer_merge.h"
+#include "aos/network/team_number.h"
+#include "aos/time/time.h"
+#include "frc971/zeroing/wrap.h"
+
+ABSL_FLAG(bool, ignore_distance, false,
+          "If true, ignore distance when shooting and obey joystick_reader");
+
+namespace y2024_bot3::control_loops::superstructure {
+
+using ::aos::monotonic_clock;
+
+using frc971::control_loops::AbsoluteEncoderProfiledJointStatus;
+using frc971::control_loops::PotAndAbsoluteEncoderProfiledJointStatus;
+using frc971::control_loops::RelativeEncoderProfiledJointStatus;
+
+Superstructure::Superstructure(::aos::EventLoop *event_loop,
+                               const ::std::string &name)
+    : frc971::controls::ControlLoop<Goal, Position, Status, Output>(event_loop,
+                                                                    name),
+      constants_fetcher_(event_loop),
+      robot_constants_(&constants_fetcher_.constants()),
+      joystick_state_fetcher_(
+          event_loop->MakeFetcher<aos::JoystickState>("/aos")) {
+  event_loop->SetRuntimeRealtimePriority(30);
+}
+
+bool PositionNear(double position, double goal, double threshold) {
+  return std::abs(position - goal) < threshold;
+}
+
+void Superstructure::RunIteration(const Goal *unsafe_goal,
+                                  const Position *position,
+                                  aos::Sender<Output>::Builder *output,
+                                  aos::Sender<Status>::Builder *status) {
+  (void)position;
+
+  if (WasReset()) {
+    AOS_LOG(ERROR, "WPILib reset, restarting\n");
+  }
+
+  OutputT output_struct;
+
+  if (unsafe_goal != nullptr) {
+  }
+
+  if (joystick_state_fetcher_.Fetch() &&
+      joystick_state_fetcher_->has_alliance()) {
+    alliance_ = joystick_state_fetcher_->alliance();
+  }
+
+  if (output) {
+    output->CheckOk(output->Send(Output::Pack(*output->fbb(), &output_struct)));
+  }
+
+  Status::Builder status_builder = status->MakeBuilder<Status>();
+
+  const bool zeroed = true;
+  const bool estopped = false;
+
+  status_builder.add_zeroed(zeroed);
+  status_builder.add_estopped(estopped);
+
+  (void)status->Send(status_builder.Finish());
+}
+}  // namespace y2024_bot3::control_loops::superstructure
diff --git a/y2024_bot3/control_loops/superstructure/superstructure.h b/y2024_bot3/control_loops/superstructure/superstructure.h
new file mode 100644
index 0000000..6362bc0
--- /dev/null
+++ b/y2024_bot3/control_loops/superstructure/superstructure.h
@@ -0,0 +1,55 @@
+#ifndef Y2024_BOT3_CONTROL_LOOPS_SUPERSTRUCTURE_SUPERSTRUCTURE_H_
+#define Y2024_BOT3_CONTROL_LOOPS_SUPERSTRUCTURE_SUPERSTRUCTURE_H_
+
+#include "aos/events/event_loop.h"
+#include "aos/json_to_flatbuffer.h"
+#include "aos/time/time.h"
+#include "frc971/constants/constants_sender_lib.h"
+#include "frc971/control_loops/control_loop.h"
+#include "frc971/zeroing/absolute_encoder.h"
+#include "frc971/zeroing/pot_and_absolute_encoder.h"
+#include "y2024_bot3/constants.h"
+#include "y2024_bot3/constants/constants_generated.h"
+#include "y2024_bot3/control_loops/superstructure/superstructure_goal_generated.h"
+#include "y2024_bot3/control_loops/superstructure/superstructure_output_generated.h"
+#include "y2024_bot3/control_loops/superstructure/superstructure_position_generated.h"
+#include "y2024_bot3/control_loops/superstructure/superstructure_status_generated.h"
+
+namespace y2024_bot3::control_loops::superstructure {
+
+class Superstructure
+    : public ::frc971::controls::ControlLoop<Goal, Position, Status, Output> {
+ public:
+  using AbsoluteEncoderSubsystem =
+      ::frc971::control_loops::StaticZeroingSingleDOFProfiledSubsystem<
+          ::frc971::zeroing::AbsoluteEncoderZeroingEstimator,
+          ::frc971::control_loops::AbsoluteEncoderProfiledJointStatus>;
+
+  using PotAndAbsoluteEncoderSubsystem =
+      ::frc971::control_loops::StaticZeroingSingleDOFProfiledSubsystem<
+          ::frc971::zeroing::PotAndAbsoluteEncoderZeroingEstimator,
+          ::frc971::control_loops::PotAndAbsoluteEncoderProfiledJointStatus>;
+
+  explicit Superstructure(::aos::EventLoop *event_loop,
+                          const ::std::string &name = "/superstructure");
+
+  double robot_velocity() const;
+
+ protected:
+  virtual void RunIteration(const Goal *unsafe_goal, const Position *position,
+                            aos::Sender<Output>::Builder *output,
+                            aos::Sender<Status>::Builder *status) override;
+
+ private:
+  frc971::constants::ConstantsFetcher<Constants> constants_fetcher_;
+  const Constants *robot_constants_;
+  aos::Fetcher<aos::JoystickState> joystick_state_fetcher_;
+
+  aos::Alliance alliance_ = aos::Alliance::kInvalid;
+
+  DISALLOW_COPY_AND_ASSIGN(Superstructure);
+};
+
+}  // namespace y2024_bot3::control_loops::superstructure
+
+#endif  // Y2024_BOT3_CONTROL_LOOPS_SUPERSTRUCTURE_SUPERSTRUCTURE_H_
diff --git a/y2024_bot3/control_loops/superstructure/superstructure_can_position.fbs b/y2024_bot3/control_loops/superstructure/superstructure_can_position.fbs
new file mode 100644
index 0000000..15a0565
--- /dev/null
+++ b/y2024_bot3/control_loops/superstructure/superstructure_can_position.fbs
@@ -0,0 +1,16 @@
+include "frc971/control_loops/can_talonfx.fbs";
+
+namespace y2024_bot3.control_loops.superstructure;
+
+table CANPosition {
+    // The timestamp of the measurement on the canivore clock in nanoseconds
+    // This will have less jitter than the
+    // timestamp of the message being sent out.
+    timestamp:int64 (id: 0, deprecated);
+
+    // The ctre::phoenix::StatusCode of the measurement
+    // Should be OK = 0
+    status:int (id: 1);
+}
+
+root_type CANPosition;
diff --git a/y2024_bot3/control_loops/superstructure/superstructure_goal.fbs b/y2024_bot3/control_loops/superstructure/superstructure_goal.fbs
new file mode 100644
index 0000000..947f740
--- /dev/null
+++ b/y2024_bot3/control_loops/superstructure/superstructure_goal.fbs
@@ -0,0 +1,7 @@
+include "frc971/control_loops/profiled_subsystem.fbs";
+
+namespace y2024_bot3.control_loops.superstructure;
+
+table Goal {
+}
+root_type Goal;
diff --git a/y2024_bot3/control_loops/superstructure/superstructure_lib_test.cc b/y2024_bot3/control_loops/superstructure/superstructure_lib_test.cc
new file mode 100644
index 0000000..fe26af2
--- /dev/null
+++ b/y2024_bot3/control_loops/superstructure/superstructure_lib_test.cc
@@ -0,0 +1,243 @@
+#include <chrono>
+#include <memory>
+
+#include "absl/flags/flag.h"
+#include "gtest/gtest.h"
+
+#include "aos/events/logging/log_writer.h"
+#include "frc971/control_loops/capped_test_plant.h"
+#include "frc971/control_loops/control_loop_test.h"
+#include "frc971/control_loops/position_sensor_sim.h"
+#include "frc971/control_loops/subsystem_simulator.h"
+#include "frc971/control_loops/team_number_test_environment.h"
+#include "frc971/zeroing/absolute_encoder.h"
+#include "y2024_bot3/constants/simulated_constants_sender.h"
+#include "y2024_bot3/control_loops/superstructure/superstructure.h"
+#include "y2024_bot3/control_loops/superstructure/superstructure_can_position_generated.h"
+
+ABSL_FLAG(std::string, output_folder, "",
+          "If set, logs all channels to the provided logfile.");
+
+namespace y2024_bot3::control_loops::superstructure::testing {
+
+namespace chrono = std::chrono;
+
+using ::aos::monotonic_clock;
+using ::frc971::CreateProfileParameters;
+using ::frc971::control_loops::CappedTestPlant;
+using ::frc971::control_loops::
+    CreateStaticZeroingSingleDOFProfiledSubsystemGoal;
+using ::frc971::control_loops::PositionSensorSimulator;
+using ::frc971::control_loops::StaticZeroingSingleDOFProfiledSubsystemGoal;
+typedef Superstructure::PotAndAbsoluteEncoderSubsystem
+    PotAndAbsoluteEncoderSubsystem;
+typedef Superstructure::AbsoluteEncoderSubsystem AbsoluteEncoderSubsystem;
+using PotAndAbsoluteEncoderSimulator =
+    frc971::control_loops::SubsystemSimulator<
+        frc971::control_loops::PotAndAbsoluteEncoderProfiledJointStatus,
+        PotAndAbsoluteEncoderSubsystem::State,
+        constants::Values::PotAndAbsEncoderConstants>;
+using AbsoluteEncoderSimulator = frc971::control_loops::SubsystemSimulator<
+    frc971::control_loops::AbsoluteEncoderProfiledJointStatus,
+    AbsoluteEncoderSubsystem::State,
+    constants::Values::AbsoluteEncoderConstants>;
+
+class SuperstructureSimulation {
+ public:
+  SuperstructureSimulation(::aos::EventLoop *event_loop,
+                           const Constants *simulated_robot_constants,
+                           chrono::nanoseconds dt)
+      : event_loop_(event_loop),
+        dt_(dt),
+        superstructure_position_sender_(
+            event_loop_->MakeSender<Position>("/superstructure")),
+        superstructure_can_position_sender_(
+            event_loop_->MakeSender<CANPosition>("/superstructure/rio")),
+        superstructure_status_fetcher_(
+            event_loop_->MakeFetcher<Status>("/superstructure")),
+        superstructure_output_fetcher_(
+            event_loop_->MakeFetcher<Output>("/superstructure")) {
+    (void)dt_;
+    (void)simulated_robot_constants;
+
+    phased_loop_handle_ = event_loop_->AddPhasedLoop(
+        [this](int) {
+          // Skip this the first time.
+          if (!first_) {
+            EXPECT_TRUE(superstructure_output_fetcher_.Fetch());
+            EXPECT_TRUE(superstructure_status_fetcher_.Fetch());
+          }
+          first_ = false;
+          SendPositionMessage();
+        },
+        dt);
+  }
+
+  // Sends a queue message with the position of the superstructure.
+  void SendPositionMessage() {
+    ::aos::Sender<Position>::Builder builder =
+        superstructure_position_sender_.MakeBuilder();
+
+    Position::Builder position_builder = builder.MakeBuilder<Position>();
+
+    CHECK_EQ(builder.Send(position_builder.Finish()),
+             aos::RawSender::Error::kOk);
+  }
+
+ private:
+  ::aos::EventLoop *event_loop_;
+  const chrono::nanoseconds dt_;
+  ::aos::PhasedLoopHandler *phased_loop_handle_ = nullptr;
+
+  ::aos::Sender<Position> superstructure_position_sender_;
+  ::aos::Sender<CANPosition> superstructure_can_position_sender_;
+  ::aos::Fetcher<Status> superstructure_status_fetcher_;
+  ::aos::Fetcher<Output> superstructure_output_fetcher_;
+
+  bool first_ = true;
+};
+
+class SuperstructureTest : public ::frc971::testing::ControlLoopTest {
+ public:
+  SuperstructureTest()
+      : ::frc971::testing::ControlLoopTest(
+            aos::configuration::ReadConfig("y2024_bot3/aos_config.json"),
+            std::chrono::microseconds(5000)),
+        simulated_constants_dummy_(SendSimulationConstants(
+            event_loop_factory(), 9971,
+            "y2024_bot3/constants/test_constants.json")),
+        roborio_(aos::configuration::GetNode(configuration(), "roborio")),
+        logger_pi_(aos::configuration::GetNode(configuration(), "logger")),
+        superstructure_event_loop(MakeEventLoop("Superstructure", roborio_)),
+        superstructure_(superstructure_event_loop.get()),
+        test_event_loop_(MakeEventLoop("test", roborio_)),
+        constants_fetcher_(test_event_loop_.get()),
+        simulated_robot_constants_(&constants_fetcher_.constants()),
+        superstructure_goal_fetcher_(
+            test_event_loop_->MakeFetcher<Goal>("/superstructure")),
+        superstructure_goal_sender_(
+            test_event_loop_->MakeSender<Goal>("/superstructure")),
+        superstructure_status_fetcher_(
+            test_event_loop_->MakeFetcher<Status>("/superstructure")),
+        superstructure_output_fetcher_(
+            test_event_loop_->MakeFetcher<Output>("/superstructure")),
+        superstructure_position_fetcher_(
+            test_event_loop_->MakeFetcher<Position>("/superstructure")),
+        superstructure_position_sender_(
+            test_event_loop_->MakeSender<Position>("/superstructure")),
+        superstructure_can_position_sender_(
+            test_event_loop_->MakeSender<CANPosition>("/superstructure/rio")),
+        superstructure_plant_event_loop_(MakeEventLoop("plant", roborio_)),
+        superstructure_plant_(superstructure_plant_event_loop_.get(),
+                              simulated_robot_constants_, dt()) {
+    set_team_id(frc971::control_loops::testing::kTeamNumber);
+
+    SetEnabled(true);
+
+    if (!absl::GetFlag(FLAGS_output_folder).empty()) {
+      unlink(absl::GetFlag(FLAGS_output_folder).c_str());
+      logger_event_loop_ = MakeEventLoop("logger", roborio_);
+      logger_ = std::make_unique<aos::logger::Logger>(logger_event_loop_.get());
+      logger_->StartLoggingOnRun(absl::GetFlag(FLAGS_output_folder));
+    }
+  }
+
+  void VerifyNearGoal() {
+    superstructure_goal_fetcher_.Fetch();
+    superstructure_status_fetcher_.Fetch();
+    superstructure_output_fetcher_.Fetch();
+
+    ASSERT_FALSE(superstructure_status_fetcher_->estopped());
+
+    ASSERT_TRUE(superstructure_goal_fetcher_.get() != nullptr) << ": No goal";
+    ASSERT_TRUE(superstructure_status_fetcher_.get() != nullptr)
+        << ": No status";
+    ASSERT_TRUE(superstructure_output_fetcher_.get() != nullptr)
+        << ": No output";
+  }
+
+  void CheckIfZeroed() {
+    superstructure_status_fetcher_.Fetch();
+    ASSERT_TRUE(superstructure_status_fetcher_.get()->zeroed());
+  }
+
+  void WaitUntilZeroed() {
+    int i = 0;
+    do {
+      i++;
+      RunFor(dt());
+      superstructure_status_fetcher_.Fetch();
+      // 2 Seconds
+
+      ASSERT_LE(i, 2.0 / ::aos::time::DurationInSeconds(dt()));
+
+      // Since there is a delay when sending running, make sure we have a
+      // status before checking it.
+    } while (superstructure_status_fetcher_.get() == nullptr ||
+             !superstructure_status_fetcher_.get()->zeroed());
+  }
+
+  void WaitUntilNear() {}
+
+  const bool simulated_constants_dummy_;
+
+  const aos::Node *const roborio_;
+  const aos::Node *const logger_pi_;
+
+  ::std::unique_ptr<::aos::EventLoop> superstructure_event_loop;
+  ::y2024_bot3::control_loops::superstructure::Superstructure superstructure_;
+  ::std::unique_ptr<::aos::EventLoop> test_event_loop_;
+  ::aos::PhasedLoopHandler *phased_loop_handle_ = nullptr;
+
+  frc971::constants::ConstantsFetcher<Constants> constants_fetcher_;
+  const Constants *simulated_robot_constants_;
+
+  ::aos::Fetcher<Goal> superstructure_goal_fetcher_;
+  ::aos::Sender<Goal> superstructure_goal_sender_;
+  ::aos::Fetcher<Status> superstructure_status_fetcher_;
+  ::aos::Fetcher<Output> superstructure_output_fetcher_;
+  ::aos::Fetcher<Position> superstructure_position_fetcher_;
+  ::aos::Sender<Position> superstructure_position_sender_;
+  ::aos::Sender<CANPosition> superstructure_can_position_sender_;
+
+  ::std::unique_ptr<::aos::EventLoop> superstructure_plant_event_loop_;
+  SuperstructureSimulation superstructure_plant_;
+
+  std::unique_ptr<aos::EventLoop> logger_event_loop_;
+  std::unique_ptr<aos::logger::Logger> logger_;
+
+  const ::std::vector<::Eigen::Matrix<double, 3, 1>> points_;
+};
+
+// Tests that the superstructure does nothing when the goal is to remain
+// still.
+
+TEST_F(SuperstructureTest, DoesNothing) {
+  SetEnabled(true);
+  WaitUntilZeroed();
+
+  {
+    auto builder = superstructure_goal_sender_.MakeBuilder();
+    Goal::Builder goal_builder = builder.MakeBuilder<Goal>();
+
+    ASSERT_EQ(builder.Send(goal_builder.Finish()), aos::RawSender::Error::kOk);
+  }
+  RunFor(chrono::seconds(10));
+
+  VerifyNearGoal();
+}
+
+// Tests that the loop zeroes when run for a while without a goal.
+TEST_F(SuperstructureTest, ZeroNoGoal) {
+  SetEnabled(true);
+  WaitUntilZeroed();
+  RunFor(chrono::seconds(2));
+}
+
+// Tests that running disabled works
+TEST_F(SuperstructureTest, DisableTest) {
+  RunFor(chrono::seconds(2));
+  CheckIfZeroed();
+}
+
+}  // namespace y2024_bot3::control_loops::superstructure::testing
diff --git a/y2024_bot3/control_loops/superstructure/superstructure_main.cc b/y2024_bot3/control_loops/superstructure/superstructure_main.cc
new file mode 100644
index 0000000..a7d048c
--- /dev/null
+++ b/y2024_bot3/control_loops/superstructure/superstructure_main.cc
@@ -0,0 +1,25 @@
+#include "aos/events/shm_event_loop.h"
+#include "aos/init.h"
+#include "y2024_bot3/control_loops/superstructure/superstructure.h"
+
+ABSL_FLAG(std::string, arm_trajectories, "arm_trajectories_generated.bfbs",
+          "The path to the generated arm trajectories bfbs file.");
+
+using y2024_bot3::control_loops::superstructure::Superstructure;
+
+int main(int argc, char **argv) {
+  ::aos::InitGoogle(&argc, &argv);
+
+  aos::FlatbufferDetachedBuffer<aos::Configuration> config =
+      aos::configuration::ReadConfig("aos_config.json");
+
+  ::aos::ShmEventLoop event_loop(&config.message());
+
+  frc971::constants::WaitForConstants<y2024_bot3::Constants>(&config.message());
+
+  Superstructure superstructure(&event_loop);
+
+  event_loop.Run();
+
+  return 0;
+}
diff --git a/y2024_bot3/control_loops/superstructure/superstructure_output.fbs b/y2024_bot3/control_loops/superstructure/superstructure_output.fbs
new file mode 100644
index 0000000..28799de
--- /dev/null
+++ b/y2024_bot3/control_loops/superstructure/superstructure_output.fbs
@@ -0,0 +1,6 @@
+namespace y2024_bot3.control_loops.superstructure;
+
+table Output {
+}
+
+root_type Output;
diff --git a/y2024_bot3/control_loops/superstructure/superstructure_plotter.ts b/y2024_bot3/control_loops/superstructure/superstructure_plotter.ts
new file mode 100644
index 0000000..f3c90aa
--- /dev/null
+++ b/y2024_bot3/control_loops/superstructure/superstructure_plotter.ts
@@ -0,0 +1,117 @@
+// Provides a plot for debugging robot state-related issues.
+import {AosPlotter, MessageHandler} from '../../../aos/network/www/aos_plotter';
+import {BLUE, BROWN, CYAN, GREEN, PINK, RED, WHITE} from '../../../aos/network/www/colors';
+import * as proxy from '../../../aos/network/www/proxy';
+
+import Connection = proxy.Connection;
+
+const TIME = AosPlotter.TIME;
+const DEFAULT_WIDTH = AosPlotter.DEFAULT_WIDTH * 2;
+const DEFAULT_HEIGHT = AosPlotter.DEFAULT_HEIGHT * 1;
+
+// plot static zeroing single dof subsystem (generic function used by specific subsystem plotters)
+function plotSzsdofSubsystem(
+    name: string, plotter: AosPlotter, element: Element, position: MessageHandler, positionName: string,
+    status: MessageHandler, statusName: string, output: MessageHandler, outputName: string, hasPot:boolean = true): void {
+  {
+    const positionPlot =
+        plotter.addPlot(element, [DEFAULT_WIDTH, DEFAULT_HEIGHT]);
+    positionPlot.plot.getAxisLabels().setTitle(name + ' Position');
+    positionPlot.plot.getAxisLabels().setXLabel(TIME);
+    positionPlot.plot.getAxisLabels().setYLabel('Position [rad,m]');
+    positionPlot.addMessageLine(position, [positionName, 'encoder'])
+        .setColor(RED);
+    positionPlot.addMessageLine(position, [positionName, 'absolute_encoder'])
+        .setColor(GREEN);
+    if (hasPot) {
+      positionPlot.addMessageLine(position, [positionName, 'pot'])
+          .setColor(BLUE);
+    }
+    positionPlot
+        .addMessageLine(status, [statusName, 'estimator_state', 'position'])
+        .setColor(BROWN);
+    positionPlot.addMessageLine(status, [statusName, 'position'])
+        .setColor(WHITE);
+  }
+  {
+    const statesPlot =
+        plotter.addPlot(element, [DEFAULT_WIDTH, DEFAULT_HEIGHT / 2]);
+    statesPlot.plot.getAxisLabels().setTitle(name + ' State');
+    statesPlot.plot.getAxisLabels().setXLabel(TIME);
+    statesPlot.plot.getAxisLabels().setYLabel('[bool,ZeroingError]');
+    statesPlot.addMessageLine(status, [statusName, 'estopped']).setColor(RED);
+    statesPlot.addMessageLine(status, [statusName, 'zeroed']).setColor(GREEN);
+    statesPlot
+        .addMessageLine(status, [statusName, 'estimator_state', 'errors[]'])
+        .setColor(BLUE)
+        .setDrawLine(false);
+  }
+  {
+    const positionConvergencePlot =
+        plotter.addPlot(element, [DEFAULT_WIDTH, DEFAULT_HEIGHT]);
+    positionConvergencePlot.plot.getAxisLabels().setTitle(name + ' Position Goals');
+    positionConvergencePlot.plot.getAxisLabels().setXLabel(TIME);
+    positionConvergencePlot.plot.getAxisLabels().setYLabel('[rad,m]');
+    positionConvergencePlot.addMessageLine(status, [statusName, 'position'])
+        .setColor(RED);
+    positionConvergencePlot.addMessageLine(status, [statusName, 'goal_position'])
+        .setColor(GREEN);
+    positionConvergencePlot
+        .addMessageLine(status, [statusName, 'unprofiled_goal_position'])
+        .setColor(BROWN);
+  }
+  {
+    const velocityConvergencePlot =
+        plotter.addPlot(element, [DEFAULT_WIDTH, DEFAULT_HEIGHT]);
+    velocityConvergencePlot.plot.getAxisLabels().setTitle(name + ' Velocity Goals');
+    velocityConvergencePlot.plot.getAxisLabels().setXLabel(TIME);
+    velocityConvergencePlot.plot.getAxisLabels().setYLabel('[rad,m]');
+    velocityConvergencePlot.addMessageLine(status, [statusName, 'velocity'])
+        .setColor(RED);
+    velocityConvergencePlot.addMessageLine(status, [statusName, 'calculated_velocity'])
+        .setColor(RED).setDrawLine(false);
+    velocityConvergencePlot.addMessageLine(status, [statusName, 'goal_velocity'])
+        .setColor(GREEN);
+    velocityConvergencePlot
+        .addMessageLine(status, [statusName, 'unprofiled_goal_velocity'])
+        .setColor(BROWN);
+  }
+  {
+    const outputPlot =
+        plotter.addPlot(element, [DEFAULT_WIDTH, DEFAULT_HEIGHT]);
+    outputPlot.plot.getAxisLabels().setTitle(name + ' Outputs');
+    outputPlot.plot.getAxisLabels().setXLabel(TIME);
+    outputPlot.plot.getAxisLabels().setYLabel('[volts]');
+    outputPlot.addMessageLine(output, [outputName])
+        .setColor(RED);
+    outputPlot.addMessageLine(status, [statusName, 'voltage_error'])
+        .setColor(GREEN);
+    outputPlot.addMessageLine(status, [statusName, 'position_power'])
+        .setColor(BLUE);
+    outputPlot.addMessageLine(status, [statusName, 'velocity_power'])
+        .setColor(BROWN);
+    outputPlot.addMessageLine(status, [statusName, 'feedforwards_power'])
+        .setColor(WHITE);
+  }
+}
+
+export function plotSuperstructure(conn: Connection, element: Element): void {
+  const aosPlotter = new AosPlotter(conn);
+  const status = aosPlotter.addMessageSource(
+      '/superstructure', 'y2024_bot3.control_loops.superstructure.Status');
+  const robotState = aosPlotter.addMessageSource('/aos', 'aos.RobotState');
+
+  {
+    const robotStatePlot =
+        aosPlotter.addPlot(element, [DEFAULT_WIDTH, DEFAULT_HEIGHT]);
+    robotStatePlot.plot.getAxisLabels().setTitle('Robot State Plot');
+    robotStatePlot.plot.getAxisLabels().setXLabel(TIME);
+    robotStatePlot.plot.getAxisLabels().setYLabel('[bool]');
+    robotStatePlot.addMessageLine(robotState, ['outputs_enabled'])
+        .setColor(RED);
+    robotStatePlot.addMessageLine(status, ['zeroed'])
+        .setColor(GREEN);
+    robotStatePlot.addMessageLine(status, ['estopped'])
+        .setColor(BLUE);
+  }
+}
diff --git a/y2024_bot3/control_loops/superstructure/superstructure_position.fbs b/y2024_bot3/control_loops/superstructure/superstructure_position.fbs
new file mode 100644
index 0000000..f0553a5
--- /dev/null
+++ b/y2024_bot3/control_loops/superstructure/superstructure_position.fbs
@@ -0,0 +1,9 @@
+include "frc971/control_loops/control_loops.fbs";
+include "frc971/control_loops/profiled_subsystem.fbs";
+
+namespace y2024_bot3.control_loops.superstructure;
+
+table Position {
+}
+
+root_type Position;
diff --git a/y2024_bot3/control_loops/superstructure/superstructure_replay.cc b/y2024_bot3/control_loops/superstructure/superstructure_replay.cc
new file mode 100644
index 0000000..1058f4f
--- /dev/null
+++ b/y2024_bot3/control_loops/superstructure/superstructure_replay.cc
@@ -0,0 +1,75 @@
+// This binary allows us to replay the superstructure code over existing
+// logfile. When you run this code, it generates a new logfile with the data all
+// replayed, so that it can then be run through the plotting tool or analyzed
+// in some other way. The original superstructure status data will be on the
+// /original/superstructure channel.
+#include "absl/flags/flag.h"
+
+#include "aos/events/logging/log_reader.h"
+#include "aos/events/logging/log_writer.h"
+#include "aos/events/simulated_event_loop.h"
+#include "aos/init.h"
+#include "aos/json_to_flatbuffer.h"
+#include "aos/logging/log_message_generated.h"
+#include "aos/network/team_number.h"
+#include "y2024_bot3/constants.h"
+#include "y2024_bot3/control_loops/superstructure/superstructure.h"
+
+ABSL_FLAG(int32_t, team, 971, "Team number to use for logfile replay.");
+ABSL_FLAG(std::string, output_folder, "/tmp/superstructure_replay/",
+          "Logs all channels to the provided logfile.");
+
+int main(int argc, char **argv) {
+  aos::InitGoogle(&argc, &argv);
+
+  aos::network::OverrideTeamNumber(absl::GetFlag(FLAGS_team));
+
+  // open logfiles
+  aos::logger::LogReader reader(
+      aos::logger::SortParts(aos::logger::FindLogs(argc, argv)));
+  // TODO(james): Actually enforce not sending on the same buses as the logfile
+  // spews out.
+  reader.RemapLoggedChannel("/superstructure",
+                            "y2024_bot3.control_loops.superstructure.Status");
+  reader.RemapLoggedChannel("/superstructure",
+                            "y2024_bot3.control_loops.superstructure.Output");
+
+  aos::SimulatedEventLoopFactory factory(reader.configuration());
+  reader.Register(&factory);
+
+  aos::NodeEventLoopFactory *roborio =
+      factory.GetNodeEventLoopFactory("roborio");
+
+  unlink(absl::GetFlag(FLAGS_output_folder).c_str());
+  std::unique_ptr<aos::EventLoop> logger_event_loop =
+      roborio->MakeEventLoop("logger");
+  auto logger = std::make_unique<aos::logger::Logger>(logger_event_loop.get());
+  logger->StartLoggingOnRun(absl::GetFlag(FLAGS_output_folder));
+
+  roborio->OnStartup([roborio]() {
+    roborio->AlwaysStart<
+        y2024_bot3::control_loops::superstructure::Superstructure>(
+        "superstructure");
+  });
+
+  std::unique_ptr<aos::EventLoop> print_loop = roborio->MakeEventLoop("print");
+  print_loop->SkipAosLog();
+  print_loop->MakeWatcher(
+      "/aos", [&print_loop](const aos::logging::LogMessageFbs &msg) {
+        LOG(INFO) << print_loop->context().monotonic_event_time << " "
+                  << aos::FlatbufferToJson(&msg);
+      });
+  print_loop->MakeWatcher(
+      "/superstructure",
+      [&](const y2024_bot3::control_loops::superstructure::Status &status) {
+        if (status.estopped()) {
+          LOG(ERROR) << "Estopped";
+        }
+      });
+
+  factory.Run();
+
+  reader.Deregister();
+
+  return 0;
+}
diff --git a/y2024_bot3/control_loops/superstructure/superstructure_status.fbs b/y2024_bot3/control_loops/superstructure/superstructure_status.fbs
new file mode 100644
index 0000000..ca5876e
--- /dev/null
+++ b/y2024_bot3/control_loops/superstructure/superstructure_status.fbs
@@ -0,0 +1,14 @@
+include "frc971/control_loops/control_loops.fbs";
+include "frc971/control_loops/profiled_subsystem.fbs";
+
+namespace y2024_bot3.control_loops.superstructure;
+
+table Status {
+  // All subsystems know their location.
+  zeroed:bool (id: 0);
+
+  // If true, we have aborted. This is the or of all subsystem estops.
+  estopped:bool (id: 1);
+}
+
+root_type Status;