Validate flatbuffers before printing in aos_dump, log_cat, and simulated_event_loop

We had a corrupted flatbuffer being logged, and the error was very poor
when it was being printed.  Let's try to catch this a lot earlier.

Fix 1: log_cat should validate before printing.  That'll prevent us
from wasting time debugging why it is crashing and point the finger a
lot better.

Fix 2: aos_dump should do the same.  That'll save us from nasty crashes
there too and focus attention a lot better.

Fix 3: SimulatedEventLoop sender Send should also validate in debug
mode.  This will avoid too big a performance hit in the fast path, but
will catch corrupted flatbuffers in any simulations we do, or any log
replay that gets done with debug turned on.

This caught a couple of places where we were missing schemas, so add
those in too.

Change-Id: I1873ddd592d33fe4e64210a2e08aa9b937d61ab8
Signed-off-by: Austin Schuh <austin.schuh@bluerivertech.com>
diff --git a/aos/aos_dump.cc b/aos/aos_dump.cc
index 45168b2..e2467b3 100644
--- a/aos/aos_dump.cc
+++ b/aos/aos_dump.cc
@@ -31,6 +31,14 @@
   // information on stderr.
 
   builder->Reset();
+
+  CHECK(flatbuffers::Verify(*channel->schema(),
+                            *channel->schema()->root_table(),
+                            static_cast<const uint8_t *>(context.data),
+                            static_cast<size_t>(context.size)))
+      << ": Corrupted flatbuffer on " << channel->name()->c_str() << " "
+      << channel->type()->c_str();
+
   aos::FlatbufferToJson(
       builder, channel->schema(), static_cast<const uint8_t *>(context.data),
       {FLAGS_pretty, static_cast<size_t>(FLAGS_max_vector_size)});
diff --git a/aos/configuration.cc b/aos/configuration.cc
index b388bd3..faafcfc 100644
--- a/aos/configuration.cc
+++ b/aos/configuration.cc
@@ -915,6 +915,14 @@
 }
 }  // namespace
 
+aos::FlatbufferDetachedBuffer<aos::Configuration> AddSchema(
+    std::string_view json,
+    const std::vector<aos::FlatbufferVector<reflection::Schema>> &schemas) {
+  FlatbufferDetachedBuffer<Configuration> addition =
+      JsonToFlatbuffer(json, Configuration::MiniReflectTypeTable());
+  return MergeConfiguration(addition, schemas);
+}
+
 int GetNodeIndex(const Configuration *config, const Node *node) {
   if (!MultiNode(config)) {
     return 0;
diff --git a/aos/configuration.h b/aos/configuration.h
index c6d25df..c22a1cf 100644
--- a/aos/configuration.h
+++ b/aos/configuration.h
@@ -40,6 +40,12 @@
 FlatbufferDetachedBuffer<Configuration> MergeWithConfig(
     const Configuration *config, const Flatbuffer<Configuration> &addition);
 
+// Adds the list of schemas to the provide config json.  This should mostly be
+// used for testing and in conjunction with MergeWithConfig.
+FlatbufferDetachedBuffer<aos::Configuration> AddSchema(
+    std::string_view json,
+    const std::vector<FlatbufferVector<reflection::Schema>> &schemas);
+
 // Returns the resolved Channel for a name, type, and application name. Returns
 // nullptr if none is found.
 //
diff --git a/aos/controls/control_loop_test.h b/aos/controls/control_loop_test.h
index 7fbc94a..2a892fa 100644
--- a/aos/controls/control_loop_test.h
+++ b/aos/controls/control_loop_test.h
@@ -24,16 +24,6 @@
 template <typename TestBaseClass>
 class ControlLoopTestTemplated : public TestBaseClass {
  public:
-  // Builds a control loop test with the provided configuration.  Note: this
-  // merges and sorts the config to reduce user errors.
-  ControlLoopTestTemplated(std::string_view configuration,
-                           ::std::chrono::nanoseconds dt = kTimeTick)
-      : ControlLoopTestTemplated(
-            configuration::MergeConfiguration(
-                aos::FlatbufferDetachedBuffer<Configuration>(JsonToFlatbuffer(
-                    configuration, Configuration::MiniReflectTypeTable()))),
-            dt) {}
-
   ControlLoopTestTemplated(
       FlatbufferDetachedBuffer<Configuration> configuration,
       ::std::chrono::nanoseconds dt = kTimeTick)
diff --git a/aos/events/BUILD b/aos/events/BUILD
index aab764b..4194514 100644
--- a/aos/events/BUILD
+++ b/aos/events/BUILD
@@ -1,4 +1,5 @@
 load("@com_github_google_flatbuffers//:build_defs.bzl", "flatbuffer_cc_library", "flatbuffer_ts_library")
+load("//aos:flatbuffers.bzl", "cc_static_flatbuffer")
 load("//aos:config.bzl", "aos_config")
 
 package(default_visibility = ["//visibility:public"])
@@ -10,6 +11,12 @@
     target_compatible_with = ["@platforms//os:linux"],
 )
 
+cc_static_flatbuffer(
+    name = "test_message_schema",
+    function = "aos::TestMessageSchema",
+    target = ":test_message_fbs_reflection_out",
+)
+
 flatbuffer_cc_library(
     name = "event_loop_fbs",
     srcs = ["event_loop.fbs"],
@@ -20,6 +27,12 @@
     target_compatible_with = ["@platforms//os:linux"],
 )
 
+cc_static_flatbuffer(
+    name = "timing_report_schema",
+    function = "aos::timing::ReportSchema",
+    target = ":event_loop_fbs_reflection_out",
+)
+
 flatbuffer_cc_library(
     name = "ping_fbs",
     srcs = ["ping.fbs"],
@@ -293,7 +306,13 @@
     deps = [
         ":event_loop",
         ":test_message_fbs",
+        ":test_message_schema",
+        ":timing_report_schema",
         "//aos:realtime",
+        "//aos/logging:log_message_schema",
+        "//aos/network:message_bridge_client_schema",
+        "//aos/network:message_bridge_server_schema",
+        "//aos/network:timestamp_schema",
         "//aos/testing:googletest",
     ],
 )
diff --git a/aos/events/event_loop_param_test.cc b/aos/events/event_loop_param_test.cc
index ee77b8a..0fd1078 100644
--- a/aos/events/event_loop_param_test.cc
+++ b/aos/events/event_loop_param_test.cc
@@ -1801,7 +1801,8 @@
   auto loop2 = MakePrimary();
   auto loop3 = Make();
 
-  const std::string kData("971 is the best");
+  const FlatbufferDetachedBuffer<TestMessage> kMessage =
+      JsonToFlatbuffer<TestMessage>("{}");
 
   std::unique_ptr<aos::RawSender> sender =
       loop1->MakeRawSender(configuration::GetChannel(
@@ -1811,29 +1812,29 @@
       loop3->MakeRawFetcher(configuration::GetChannel(
           loop3->configuration(), "/test", "aos.TestMessage", "", nullptr));
 
-  loop2->OnRun(
-      [&]() { EXPECT_TRUE(sender->Send(kData.data(), kData.size())); });
+  loop2->OnRun([&]() {
+    EXPECT_TRUE(sender->Send(kMessage.span().data(), kMessage.span().size()));
+  });
 
   bool happened = false;
   loop2->MakeRawWatcher(
       configuration::GetChannel(loop2->configuration(), "/test",
                                 "aos.TestMessage", "", nullptr),
-      [this, &kData, &fetcher, &happened](const Context &context,
-                                          const void *message) {
+      [this, &kMessage, &fetcher, &happened](const Context &context,
+                                             const void *message) {
         happened = true;
-        EXPECT_EQ(std::string_view(kData),
-                  std::string_view(reinterpret_cast<const char *>(message),
-                                   context.size));
-        EXPECT_EQ(std::string_view(kData),
-                  std::string_view(reinterpret_cast<const char *>(context.data),
-                                   context.size));
+        EXPECT_EQ(
+            kMessage.span(),
+            absl::Span<const uint8_t>(
+                reinterpret_cast<const uint8_t *>(message), context.size));
+        EXPECT_EQ(message, context.data);
 
         ASSERT_TRUE(fetcher->Fetch());
 
-        EXPECT_EQ(std::string_view(kData),
-                  std::string_view(
-                      reinterpret_cast<const char *>(fetcher->context().data),
-                      fetcher->context().size));
+        EXPECT_EQ(kMessage.span(),
+                  absl::Span<const uint8_t>(reinterpret_cast<const uint8_t *>(
+                                                fetcher->context().data),
+                                            fetcher->context().size));
 
         this->Exit();
       });
@@ -1850,7 +1851,8 @@
   auto loop2 = MakePrimary();
   auto loop3 = Make();
 
-  const std::string kData("971 is the best");
+  const FlatbufferDetachedBuffer<TestMessage> kMessage =
+      JsonToFlatbuffer<TestMessage>("{}");
 
   const aos::monotonic_clock::time_point monotonic_remote_time =
       aos::monotonic_clock::time_point(chrono::seconds(1501));
@@ -1868,9 +1870,9 @@
           loop3->configuration(), "/test", "aos.TestMessage", "", nullptr));
 
   loop2->OnRun([&]() {
-    EXPECT_TRUE(sender->Send(kData.data(), kData.size(), monotonic_remote_time,
-                             realtime_remote_time, remote_queue_index,
-                             remote_boot_uuid));
+    EXPECT_TRUE(sender->Send(kMessage.span().data(), kMessage.span().size(),
+                             monotonic_remote_time, realtime_remote_time,
+                             remote_queue_index, remote_boot_uuid));
   });
 
   bool happened = false;
@@ -1904,7 +1906,8 @@
 TEST_P(AbstractEventLoopTest, RawSenderSentData) {
   auto loop1 = MakePrimary();
 
-  const std::string kData("971 is the best");
+  const FlatbufferDetachedBuffer<TestMessage> kMessage =
+      JsonToFlatbuffer<TestMessage>("{}");
 
   std::unique_ptr<aos::RawSender> sender =
       loop1->MakeRawSender(configuration::GetChannel(
@@ -1913,7 +1916,7 @@
   const aos::monotonic_clock::time_point monotonic_now = loop1->monotonic_now();
   const aos::realtime_clock::time_point realtime_now = loop1->realtime_now();
 
-  EXPECT_TRUE(sender->Send(kData.data(), kData.size()));
+  EXPECT_TRUE(sender->Send(kMessage.span().data(), kMessage.span().size()));
 
   EXPECT_GE(sender->monotonic_sent_time(), monotonic_now);
   EXPECT_LE(sender->monotonic_sent_time(),
@@ -1923,7 +1926,7 @@
             realtime_now + chrono::milliseconds(100));
   EXPECT_EQ(sender->sent_queue_index(), 0u);
 
-  EXPECT_TRUE(sender->Send(kData.data(), kData.size()));
+  EXPECT_TRUE(sender->Send(kMessage.span().data(), kMessage.span().size()));
 
   EXPECT_GE(sender->monotonic_sent_time(), monotonic_now);
   EXPECT_LE(sender->monotonic_sent_time(),
diff --git a/aos/events/event_loop_param_test.h b/aos/events/event_loop_param_test.h
index 0cbb5fe..3138c51 100644
--- a/aos/events/event_loop_param_test.h
+++ b/aos/events/event_loop_param_test.h
@@ -7,8 +7,14 @@
 
 #include "aos/events/event_loop.h"
 #include "aos/events/test_message_generated.h"
+#include "aos/events/test_message_schema.h"
+#include "aos/events/timing_report_schema.h"
 #include "aos/flatbuffers.h"
 #include "aos/json_to_flatbuffer.h"
+#include "aos/logging/log_message_schema.h"
+#include "aos/network/message_bridge_client_schema.h"
+#include "aos/network/message_bridge_server_schema.h"
+#include "aos/network/timestamp_schema.h"
 #include "gtest/gtest.h"
 
 namespace aos {
@@ -17,7 +23,8 @@
 class EventLoopTestFactory {
  public:
   EventLoopTestFactory()
-      : flatbuffer_(JsonToFlatbuffer<Configuration>(R"config({
+      : flatbuffer_(configuration::AddSchema(
+            R"config({
   "channels": [
     {
       "name": "/aos",
@@ -40,7 +47,11 @@
       "type": "aos.TestMessage"
     }
   ]
-})config")) {}
+})config",
+            {aos::FlatbufferSpan<reflection::Schema>(
+                 logging::LogMessageFbsSchema()),
+             aos::FlatbufferSpan<reflection::Schema>(timing::ReportSchema()),
+             aos::FlatbufferSpan<reflection::Schema>(TestMessageSchema())})) {}
 
   virtual ~EventLoopTestFactory() {}
 
@@ -61,7 +72,8 @@
 
   // Sets the config to a config with a max size with an invalid alignment.
   void InvalidChannelAlignment() {
-    flatbuffer_ = JsonToFlatbuffer<Configuration>(R"config({
+    flatbuffer_ = configuration::AddSchema(
+        R"config({
   "channels": [
     {
       "name": "/aos",
@@ -85,7 +97,11 @@
       "type": "aos.TestMessage"
     }
   ]
-})config");
+})config",
+        {aos::FlatbufferSpan<reflection::Schema>(
+             logging::LogMessageFbsSchema()),
+         aos::FlatbufferSpan<reflection::Schema>(timing::ReportSchema()),
+         aos::FlatbufferSpan<reflection::Schema>(TestMessageSchema())});
   }
 
   void PinReads() {
@@ -124,8 +140,11 @@
   ]
 })config";
 
-    flatbuffer_ = FlatbufferDetachedBuffer<Configuration>(
-        JsonToFlatbuffer(kJson, Configuration::MiniReflectTypeTable()));
+    flatbuffer_ = configuration::AddSchema(
+        kJson, {aos::FlatbufferSpan<reflection::Schema>(
+                    logging::LogMessageFbsSchema()),
+                aos::FlatbufferSpan<reflection::Schema>(timing::ReportSchema()),
+                aos::FlatbufferSpan<reflection::Schema>(TestMessageSchema())});
   }
 
   void EnableNodes(std::string_view my_node) {
@@ -239,8 +258,19 @@
 })config";
 
     flatbuffer_ = configuration::MergeConfiguration(
-        FlatbufferDetachedBuffer<Configuration>(
-            JsonToFlatbuffer(kJson, Configuration::MiniReflectTypeTable())));
+        configuration::MergeConfiguration(
+            aos::FlatbufferDetachedBuffer<Configuration>(
+                JsonToFlatbuffer<Configuration>(kJson))),
+        {aos::FlatbufferSpan<reflection::Schema>(
+             logging::LogMessageFbsSchema()),
+         aos::FlatbufferSpan<reflection::Schema>(timing::ReportSchema()),
+         aos::FlatbufferSpan<reflection::Schema>(TestMessageSchema()),
+         aos::FlatbufferSpan<reflection::Schema>(
+             message_bridge::ClientStatisticsSchema()),
+         aos::FlatbufferSpan<reflection::Schema>(
+             message_bridge::ServerStatisticsSchema()),
+         aos::FlatbufferSpan<reflection::Schema>(
+             message_bridge::TimestampSchema())});
 
     my_node_ = configuration::GetNode(&flatbuffer_.message(), my_node);
   }
diff --git a/aos/events/logging/log_cat.cc b/aos/events/logging/log_cat.cc
index 2032f9f..3469674 100644
--- a/aos/events/logging/log_cat.cc
+++ b/aos/events/logging/log_cat.cc
@@ -44,6 +44,13 @@
                   const aos::Context &context,
                   aos::FastStringBuilder *builder) {
   builder->Reset();
+  CHECK(flatbuffers::Verify(*channel->schema(),
+                            *channel->schema()->root_table(),
+                            static_cast<const uint8_t *>(context.data),
+                            static_cast<size_t>(context.size)))
+      << ": Corrupted flatbuffer on " << channel->name()->c_str() << " "
+      << channel->type()->c_str();
+
   aos::FlatbufferToJson(
       builder, channel->schema(), static_cast<const uint8_t *>(context.data),
       {FLAGS_pretty, static_cast<size_t>(FLAGS_max_vector_size)});
@@ -111,6 +118,17 @@
       CHECK_LT(channel_index, channels->size());
       const aos::Channel *const channel = channels->Get(channel_index);
 
+      if (message.value().message().data() != nullptr) {
+        CHECK(channel->has_schema());
+
+        CHECK(flatbuffers::Verify(*channel->schema(),
+                                  *channel->schema()->root_table(),
+                                  message.value().message().data()->data(),
+                                  message.value().message().data()->size()))
+            << ": Corrupted flatbuffer on " << channel->name()->c_str() << " "
+            << channel->type()->c_str();
+      }
+
       if (FLAGS_format_raw && message.value().message().data() != nullptr) {
         std::cout << aos::configuration::StrippedChannelToString(channel) << " "
                   << aos::FlatbufferToJson(
diff --git a/aos/events/simulated_event_loop.cc b/aos/events/simulated_event_loop.cc
index ef34839..51021b6 100644
--- a/aos/events/simulated_event_loop.cc
+++ b/aos/events/simulated_event_loop.cc
@@ -806,6 +806,17 @@
   message->context.queue_index = queue_index;
   message->context.data = message->data(channel()->max_size()) +
                           channel()->max_size() - message->context.size;
+
+  DCHECK(channel()->has_schema())
+      << ": Missing schema for channel "
+      << configuration::StrippedChannelToString(channel());
+  DCHECK(flatbuffers::Verify(
+      *channel()->schema(), *channel()->schema()->root_table(),
+      static_cast<const uint8_t *>(message->context.data),
+      message->context.size))
+      << ": Corrupted flatbuffer on " << channel()->name()->c_str() << " "
+      << channel()->type()->c_str();
+
   next_queue_index_ = next_queue_index_.Increment();
 
   latest_message_ = message;
diff --git a/aos/flatbuffers.bzl b/aos/flatbuffers.bzl
index eb19beb..3db4ca0 100644
--- a/aos/flatbuffers.bzl
+++ b/aos/flatbuffers.bzl
@@ -1,4 +1,4 @@
-def cc_static_flatbuffer(name, target, function):
+def cc_static_flatbuffer(name, target, function, visibility = None):
     """Creates a cc_library which encodes a file as a Span.
 
     args:
@@ -7,10 +7,10 @@
     """
     native.genrule(
         name = name + "_gen",
-        tools = ["//aos:flatbuffers_static"],
+        tools = ["@org_frc971//aos:flatbuffers_static"],
         srcs = [target],
         outs = [name + ".h"],
-        cmd = "$(location //aos:flatbuffers_static) $(SRCS) $(OUTS) '" + function + "'",
+        cmd = "$(location @org_frc971//aos:flatbuffers_static) $(SRCS) $(OUTS) '" + function + "'",
     )
 
     native.cc_library(
@@ -19,4 +19,5 @@
         deps = [
             "@com_google_absl//absl/types:span",
         ],
+        visibility = visibility,
     )
diff --git a/aos/logging/BUILD b/aos/logging/BUILD
index cf0c370..30fd0d1 100644
--- a/aos/logging/BUILD
+++ b/aos/logging/BUILD
@@ -1,4 +1,5 @@
 load("@com_github_google_flatbuffers//:build_defs.bzl", "flatbuffer_cc_library")
+load("//aos:flatbuffers.bzl", "cc_static_flatbuffer")
 
 # The primary client logging interface.
 cc_library(
@@ -95,3 +96,10 @@
     target_compatible_with = ["@platforms//os:linux"],
     visibility = ["//visibility:public"],
 )
+
+cc_static_flatbuffer(
+    name = "log_message_schema",
+    function = "aos::logging::LogMessageFbsSchema",
+    target = ":log_message_fbs_reflection_out",
+    visibility = ["//visibility:public"],
+)
diff --git a/aos/network/BUILD b/aos/network/BUILD
index 4980748..e6ff5af 100644
--- a/aos/network/BUILD
+++ b/aos/network/BUILD
@@ -53,6 +53,13 @@
     target_compatible_with = ["@platforms//os:linux"],
 )
 
+cc_static_flatbuffer(
+    name = "timestamp_schema",
+    function = "aos::message_bridge::TimestampSchema",
+    target = ":timestamp_fbs_reflection_out",
+    visibility = ["//visibility:public"],
+)
+
 flatbuffer_cc_library(
     name = "message_bridge_client_fbs",
     srcs = ["message_bridge_client.fbs"],
@@ -64,6 +71,13 @@
     target_compatible_with = ["@platforms//os:linux"],
 )
 
+cc_static_flatbuffer(
+    name = "message_bridge_client_schema",
+    function = "aos::message_bridge::ClientStatisticsSchema",
+    target = ":message_bridge_client_fbs_reflection_out",
+    visibility = ["//visibility:public"],
+)
+
 flatbuffer_cc_library(
     name = "message_bridge_server_fbs",
     srcs = ["message_bridge_server.fbs"],
@@ -74,6 +88,13 @@
     target_compatible_with = ["@platforms//os:linux"],
 )
 
+cc_static_flatbuffer(
+    name = "message_bridge_server_schema",
+    function = "aos::message_bridge::ServerStatisticsSchema",
+    target = ":message_bridge_server_fbs_reflection_out",
+    visibility = ["//visibility:public"],
+)
+
 cc_library(
     name = "team_number",
     srcs = [
diff --git a/frc971/control_loops/BUILD b/frc971/control_loops/BUILD
index 1945db9..11c93dc 100644
--- a/frc971/control_loops/BUILD
+++ b/frc971/control_loops/BUILD
@@ -1,5 +1,6 @@
 package(default_visibility = ["//visibility:public"])
 
+load("//aos:config.bzl", "aos_config")
 load("@com_github_google_flatbuffers//:build_defs.bzl", "flatbuffer_cc_library")
 
 cc_library(
@@ -417,10 +418,70 @@
 )
 
 flatbuffer_cc_library(
-    name = "static_zeroing_single_dof_profiled_subsystem_test_fbs",
+    name = "static_zeroing_single_dof_profiled_subsystem_test_subsystem_goal_fbs",
     srcs = [
-        "static_zeroing_single_dof_profiled_subsystem_test.fbs",
+        "static_zeroing_single_dof_profiled_subsystem_test_subsystem_goal.fbs",
     ],
+    gen_reflections = 1,
+    includes = [
+        ":control_loops_fbs_includes",
+        ":profiled_subsystem_fbs_includes",
+    ],
+    target_compatible_with = ["@platforms//os:linux"],
+)
+
+flatbuffer_cc_library(
+    name = "static_zeroing_single_dof_profiled_subsystem_test_subsystem_output_fbs",
+    srcs = [
+        "static_zeroing_single_dof_profiled_subsystem_test_subsystem_output.fbs",
+    ],
+    gen_reflections = 1,
+    target_compatible_with = ["@platforms//os:linux"],
+)
+
+flatbuffer_cc_library(
+    name = "static_zeroing_single_dof_profiled_subsystem_test_pot_and_absolute_position_fbs",
+    srcs = [
+        "static_zeroing_single_dof_profiled_subsystem_test_pot_and_absolute_position.fbs",
+    ],
+    gen_reflections = 1,
+    includes = [
+        ":control_loops_fbs_includes",
+    ],
+    target_compatible_with = ["@platforms//os:linux"],
+)
+
+flatbuffer_cc_library(
+    name = "static_zeroing_single_dof_profiled_subsystem_test_absolute_position_fbs",
+    srcs = [
+        "static_zeroing_single_dof_profiled_subsystem_test_absolute_position.fbs",
+    ],
+    gen_reflections = 1,
+    includes = [
+        ":control_loops_fbs_includes",
+    ],
+    target_compatible_with = ["@platforms//os:linux"],
+)
+
+flatbuffer_cc_library(
+    name = "static_zeroing_single_dof_profiled_subsystem_test_pot_and_absolute_encoder_status_fbs",
+    srcs = [
+        "static_zeroing_single_dof_profiled_subsystem_test_pot_and_absolute_encoder_status.fbs",
+    ],
+    gen_reflections = 1,
+    includes = [
+        ":control_loops_fbs_includes",
+        ":profiled_subsystem_fbs_includes",
+    ],
+    target_compatible_with = ["@platforms//os:linux"],
+)
+
+flatbuffer_cc_library(
+    name = "static_zeroing_single_dof_profiled_subsystem_test_absolute_encoder_status_fbs",
+    srcs = [
+        "static_zeroing_single_dof_profiled_subsystem_test_absolute_encoder_status.fbs",
+    ],
+    gen_reflections = 1,
     includes = [
         ":control_loops_fbs_includes",
         ":profiled_subsystem_fbs_includes",
@@ -433,14 +494,40 @@
     srcs = [
         "static_zeroing_single_dof_profiled_subsystem_test.cc",
     ],
+    data = [
+        ":static_zeroing_single_dof_profiled_subsystem_test_config",
+    ],
     target_compatible_with = ["@platforms//os:linux"],
     deps = [
         ":capped_test_plant",
         ":position_sensor_sim",
         ":static_zeroing_single_dof_profiled_subsystem",
-        ":static_zeroing_single_dof_profiled_subsystem_test_fbs",
+        ":static_zeroing_single_dof_profiled_subsystem_test_absolute_encoder_status_fbs",
+        ":static_zeroing_single_dof_profiled_subsystem_test_absolute_position_fbs",
         ":static_zeroing_single_dof_profiled_subsystem_test_plants",
+        ":static_zeroing_single_dof_profiled_subsystem_test_pot_and_absolute_encoder_status_fbs",
+        ":static_zeroing_single_dof_profiled_subsystem_test_pot_and_absolute_position_fbs",
+        ":static_zeroing_single_dof_profiled_subsystem_test_subsystem_goal_fbs",
+        ":static_zeroing_single_dof_profiled_subsystem_test_subsystem_output_fbs",
         "//aos/controls:control_loop_test",
         "//aos/testing:googletest",
     ],
 )
+
+aos_config(
+    name = "static_zeroing_single_dof_profiled_subsystem_test_config",
+    src = "static_zeroing_single_dof_profiled_subsystem_test_config_source.json",
+    flatbuffers = [
+        "//frc971/input:joystick_state_fbs",
+        "//frc971/input:robot_state_fbs",
+        "//aos/logging:log_message_fbs",
+        "//aos/events:event_loop_fbs",
+        ":static_zeroing_single_dof_profiled_subsystem_test_subsystem_output_fbs",
+        ":static_zeroing_single_dof_profiled_subsystem_test_absolute_encoder_status_fbs",
+        ":static_zeroing_single_dof_profiled_subsystem_test_subsystem_goal_fbs",
+        ":static_zeroing_single_dof_profiled_subsystem_test_absolute_position_fbs",
+        ":static_zeroing_single_dof_profiled_subsystem_test_pot_and_absolute_encoder_status_fbs",
+        ":static_zeroing_single_dof_profiled_subsystem_test_pot_and_absolute_position_fbs",
+    ],
+    target_compatible_with = ["@platforms//os:linux"],
+)
diff --git a/frc971/control_loops/static_zeroing_single_dof_profiled_subsystem_test.cc b/frc971/control_loops/static_zeroing_single_dof_profiled_subsystem_test.cc
index 3dbaf9a..1bebbe1 100644
--- a/frc971/control_loops/static_zeroing_single_dof_profiled_subsystem_test.cc
+++ b/frc971/control_loops/static_zeroing_single_dof_profiled_subsystem_test.cc
@@ -6,9 +6,14 @@
 #include "frc971/control_loops/capped_test_plant.h"
 #include "frc971/control_loops/position_sensor_sim.h"
 #include "frc971/control_loops/static_zeroing_single_dof_profiled_subsystem.h"
-#include "frc971/control_loops/static_zeroing_single_dof_profiled_subsystem_test_generated.h"
+#include "frc971/control_loops/static_zeroing_single_dof_profiled_subsystem_test_absolute_encoder_status_generated.h"
+#include "frc971/control_loops/static_zeroing_single_dof_profiled_subsystem_test_absolute_position_generated.h"
 #include "frc971/control_loops/static_zeroing_single_dof_profiled_subsystem_test_integral_plant.h"
 #include "frc971/control_loops/static_zeroing_single_dof_profiled_subsystem_test_plant.h"
+#include "frc971/control_loops/static_zeroing_single_dof_profiled_subsystem_test_pot_and_absolute_encoder_status_generated.h"
+#include "frc971/control_loops/static_zeroing_single_dof_profiled_subsystem_test_pot_and_absolute_position_generated.h"
+#include "frc971/control_loops/static_zeroing_single_dof_profiled_subsystem_test_subsystem_goal_generated.h"
+#include "frc971/control_loops/static_zeroing_single_dof_profiled_subsystem_test_subsystem_output_generated.h"
 #include "frc971/zeroing/zeroing.h"
 
 using ::frc971::control_loops::PositionSensorSimulator;
@@ -32,15 +37,18 @@
 using FBB = flatbuffers::FlatBufferBuilder;
 
 struct PotAndAbsoluteEncoderQueueGroup {
-  typedef PotAndAbsoluteEncoderProfiledJointStatus Status;
-  typedef PotAndAbsolutePosition Position;
+  typedef zeroing::testing::SubsystemPotAndAbsoluteEncoderProfiledJointStatus
+      Status;
+  typedef zeroing::testing::SubsystemPotAndAbsolutePosition Position;
+  typedef PotAndAbsolutePosition RealPosition;
   typedef ::frc971::control_loops::zeroing::testing::SubsystemGoal Goal;
   typedef ::frc971::control_loops::zeroing::testing::SubsystemOutput Output;
 };
 
 struct AbsoluteEncoderQueueGroup {
-  typedef AbsoluteEncoderProfiledJointStatus Status;
-  typedef AbsolutePosition Position;
+  typedef zeroing::testing::SubsystemAbsoluteEncoderProfiledJointStatus Status;
+  typedef zeroing::testing::SubsystemAbsolutePosition Position;
+  typedef AbsolutePosition RealPosition;
   typedef zeroing::testing::SubsystemGoal Goal;
   typedef zeroing::testing::SubsystemOutput Output;
 };
@@ -101,6 +109,7 @@
   typedef typename QueueGroup::Goal GoalType;
   typedef typename QueueGroup::Status StatusType;
   typedef typename QueueGroup::Position PositionType;
+  typedef typename QueueGroup::RealPosition RealPositionType;
   typedef typename QueueGroup::Output OutputType;
 
   TestIntakeSystemSimulation(::aos::EventLoop *event_loop,
@@ -154,10 +163,14 @@
     typename ::aos::Sender<PositionType>::Builder position =
         subsystem_position_sender_.MakeBuilder();
 
+    auto real_position_builder = position.template MakeBuilder<RealPositionType>();
+    flatbuffers::Offset<RealPositionType> position_offset =
+        this->subsystem_sensor_sim_
+            .template GetSensorValues<typename RealPositionType::Builder>(
+                &real_position_builder);
     auto position_builder = position.template MakeBuilder<PositionType>();
-    position.Send(this->subsystem_sensor_sim_
-                      .template GetSensorValues<typename PositionType::Builder>(
-                          &position_builder));
+    position_builder.add_position(position_offset);
+    position.Send(position_builder.Finish());
   }
 
   void set_peak_subsystem_acceleration(double value) {
@@ -176,7 +189,8 @@
 
     const double voltage_check_subsystem =
         (static_cast<typename SZSDPS::State>(
-             subsystem_status_fetcher_->state()) == SZSDPS::State::RUNNING)
+             subsystem_status_fetcher_->status()->state()) ==
+         SZSDPS::State::RUNNING)
             ? kOperatingVoltage
             : kZeroingVoltage;
 
@@ -277,7 +291,7 @@
     // TODO(austin): This mallocs...
     FBB fbb;
     ProfileParametersBuilder params_builder(fbb);
-    if (unsafe_goal != nullptr ) {
+    if (unsafe_goal != nullptr) {
       if (unsafe_goal->profile_params() != nullptr) {
         params_builder.add_max_velocity(
             unsafe_goal->profile_params()->max_velocity());
@@ -307,13 +321,17 @@
 
     double output_voltage;
 
-    flatbuffers::Offset<StatusType> status_offset = subsystem_.Iterate(
+    auto status_offset = subsystem_.Iterate(
         unsafe_goal == nullptr
             ? nullptr
             : flatbuffers::GetRoot<StaticZeroingSingleDOFProfiledSubsystemGoal>(
                   fbb.GetBufferPointer()),
-        position, output == nullptr ? nullptr : &output_voltage, status->fbb());
-    status->Send(status_offset);
+        position->position(), output == nullptr ? nullptr : &output_voltage,
+        status->fbb());
+    typename StatusType::Builder subsystem_status_builder =
+        status->template MakeBuilder<StatusType>();
+    subsystem_status_builder.add_status(status_offset);
+    status->Send(subsystem_status_builder.Finish());
     if (output != nullptr) {
       typename OutputType::Builder output_builder =
           output->template MakeBuilder<OutputType>();
@@ -343,50 +361,9 @@
 
   IntakeSystemTest()
       : ::aos::testing::ControlLoopTest(
-            "{\n"
-            "  \"channels\": [ \n"
-            "    {\n"
-            "      \"name\": \"/aos\",\n"
-            "      \"type\": \"aos.JoystickState\"\n"
-            "    },\n"
-            "    {\n"
-            "      \"name\": \"/aos\",\n"
-            "      \"type\": \"aos.logging.LogMessageFbs\"\n"
-            "    },\n"
-            "    {\n"
-            "      \"name\": \"/aos\",\n"
-            "      \"type\": \"aos.RobotState\"\n"
-            "    },\n"
-            "    {\n"
-            "      \"name\": \"/aos\",\n"
-            "      \"type\": \"aos.timing.Report\"\n"
-            "    },\n"
-            "    {\n"
-            "      \"name\": \"/loop\",\n"
-            "      \"type\": \"frc971.control_loops.zeroing.testing.SubsystemGoal\"\n"
-            "    },\n"
-            "    {\n"
-            "      \"name\": \"/loop\",\n"
-            "      \"type\": \"frc971.control_loops.zeroing.testing.SubsystemOutput\"\n"
-            "    },\n"
-            "    {\n"
-            "      \"name\": \"/loop\",\n"
-            "      \"type\": \"frc971.control_loops.PotAndAbsoluteEncoderProfiledJointStatus\"\n"
-            "    },\n"
-            "    {\n"
-            "      \"name\": \"/loop\",\n"
-            "      \"type\": \"frc971.AbsolutePosition\"\n"
-            "    },\n"
-            "    {\n"
-            "      \"name\": \"/loop\",\n"
-            "      \"type\": \"frc971.PotAndAbsolutePosition\"\n"
-            "    },\n"
-            "    {\n"
-            "      \"name\": \"/loop\",\n"
-            "      \"type\": \"frc971.control_loops.AbsoluteEncoderProfiledJointStatus\"\n"
-            "    }\n"
-            "  ]\n"
-            "}\n",
+            aos::configuration::ReadConfig("frc971/control_loops/"
+                                           "static_zeroing_single_dof_profiled_"
+                                           "subsystem_test_config.json"),
             chrono::microseconds(5050)),
         test_event_loop_(MakeEventLoop("test")),
         subsystem_goal_sender_(test_event_loop_->MakeSender<GoalType>("/loop")),
@@ -405,10 +382,10 @@
     EXPECT_TRUE(subsystem_status_fetcher_.Fetch());
 
     EXPECT_NEAR(subsystem_goal_fetcher_->unsafe_goal(),
-                subsystem_status_fetcher_->position(), 0.001);
+                subsystem_status_fetcher_->status()->position(), 0.001);
     EXPECT_NEAR(subsystem_goal_fetcher_->unsafe_goal(),
                 subsystem_plant_.subsystem_position(), 0.001);
-    EXPECT_NEAR(subsystem_status_fetcher_->velocity(), 0, 0.001);
+    EXPECT_NEAR(subsystem_status_fetcher_->status()->velocity(), 0, 0.001);
   }
 
   SZSDPS *subsystem() { return subsystem_.subsystem(); }
@@ -506,10 +483,10 @@
         profile_builder.add_max_velocity(0);
         profile_builder.add_max_acceleration(0);
         EXPECT_TRUE(message.Send(zeroing::testing::CreateSubsystemGoal(
-            *message.fbb(), kStartingGoal +
-                                aos::time::DurationInSeconds(
-                                    this->monotonic_now().time_since_epoch()) *
-                                    kVelocity,
+            *message.fbb(),
+            kStartingGoal + aos::time::DurationInSeconds(
+                                this->monotonic_now().time_since_epoch()) *
+                                kVelocity,
             profile_builder.Finish(), kVelocity, true)));
       },
       this->dt());
@@ -522,10 +499,11 @@
   EXPECT_TRUE(this->subsystem_status_fetcher_.Fetch());
 
   EXPECT_NEAR(kStartingGoal + kVelocity * kRunTimeSec,
-              this->subsystem_status_fetcher_->position(), 0.001);
+              this->subsystem_status_fetcher_->status()->position(), 0.001);
   EXPECT_NEAR(kStartingGoal + kVelocity * kRunTimeSec,
               this->subsystem_plant_.subsystem_position(), 0.001);
-  EXPECT_NEAR(kVelocity, this->subsystem_status_fetcher_->velocity(), 0.001);
+  EXPECT_NEAR(kVelocity, this->subsystem_status_fetcher_->status()->velocity(),
+              0.001);
 }
 
 // Makes sure that the voltage on a motor is properly pulled back after
@@ -545,7 +523,8 @@
   // acceleration is capped like expected.
   {
     auto message = this->subsystem_goal_sender_.MakeBuilder();
-    auto profile_builder = message.template MakeBuilder<frc971::ProfileParameters>();
+    auto profile_builder =
+        message.template MakeBuilder<frc971::ProfileParameters>();
     profile_builder.add_max_velocity(20.0);
     profile_builder.add_max_acceleration(0.1);
     EXPECT_TRUE(message.Send(zeroing::testing::CreateSubsystemGoal(
@@ -583,7 +562,8 @@
   // Set some ridiculous goals to test upper limits.
   {
     auto message = this->subsystem_goal_sender_.MakeBuilder();
-    auto profile_builder = message.template MakeBuilder<frc971::ProfileParameters>();
+    auto profile_builder =
+        message.template MakeBuilder<frc971::ProfileParameters>();
     profile_builder.add_max_velocity(1.0);
     profile_builder.add_max_acceleration(0.5);
     EXPECT_TRUE(message.Send(zeroing::testing::CreateSubsystemGoal(
@@ -593,12 +573,14 @@
 
   // Check that we are near our soft limit.
   EXPECT_TRUE(this->subsystem_status_fetcher_.Fetch());
-  EXPECT_NEAR(kRange.upper, this->subsystem_status_fetcher_->position(), 0.001);
+  EXPECT_NEAR(kRange.upper,
+              this->subsystem_status_fetcher_->status()->position(), 0.001);
 
   // Set some ridiculous goals to test lower limits.
   {
     auto message = this->subsystem_goal_sender_.MakeBuilder();
-    auto profile_builder = message.template MakeBuilder<frc971::ProfileParameters>();
+    auto profile_builder =
+        message.template MakeBuilder<frc971::ProfileParameters>();
     profile_builder.add_max_velocity(1.0);
     profile_builder.add_max_acceleration(0.5);
     EXPECT_TRUE(message.Send(zeroing::testing::CreateSubsystemGoal(
@@ -609,7 +591,8 @@
 
   // Check that we are near our soft limit.
   EXPECT_TRUE(this->subsystem_status_fetcher_.Fetch());
-  EXPECT_NEAR(kRange.lower, this->subsystem_status_fetcher_->position(), 0.001);
+  EXPECT_NEAR(kRange.lower,
+              this->subsystem_status_fetcher_->status()->position(), 0.001);
 }
 
 // Tests that the subsystem loop zeroes when run for a while.
@@ -618,7 +601,8 @@
 
   {
     auto message = this->subsystem_goal_sender_.MakeBuilder();
-    auto profile_builder = message.template MakeBuilder<frc971::ProfileParameters>();
+    auto profile_builder =
+        message.template MakeBuilder<frc971::ProfileParameters>();
     profile_builder.add_max_velocity(1.0);
     profile_builder.add_max_acceleration(0.5);
     EXPECT_TRUE(message.Send(zeroing::testing::CreateSubsystemGoal(
@@ -734,32 +718,32 @@
   this->SetEnabled(true);
   {
     auto message = this->subsystem_goal_sender_.MakeBuilder();
-    EXPECT_TRUE(message.Send(
-        zeroing::testing::CreateSubsystemGoal(*message.fbb(), kRange.lower_hard)));
+    EXPECT_TRUE(message.Send(zeroing::testing::CreateSubsystemGoal(
+        *message.fbb(), kRange.lower_hard)));
   }
   this->RunFor(chrono::seconds(2));
 
   // Check that kRange.lower is used as the default min position
   EXPECT_EQ(this->subsystem()->goal(0), kRange.lower);
   EXPECT_TRUE(this->subsystem_status_fetcher_.Fetch());
-  EXPECT_NEAR(kRange.lower, this->subsystem_status_fetcher_->position(),
-              0.001);
+  EXPECT_NEAR(kRange.lower,
+              this->subsystem_status_fetcher_->status()->position(), 0.001);
 
   // Set min position and check that the subsystem increases to that position
   this->subsystem()->set_min_position(kRange.lower + 0.05);
   this->RunFor(chrono::seconds(2));
   EXPECT_EQ(this->subsystem()->goal(0), kRange.lower + 0.05);
   EXPECT_TRUE(this->subsystem_status_fetcher_.Fetch());
-  EXPECT_NEAR(kRange.lower + 0.05, this->subsystem_status_fetcher_->position(),
-              0.001);
+  EXPECT_NEAR(kRange.lower + 0.05,
+              this->subsystem_status_fetcher_->status()->position(), 0.001);
 
   // Clear min position and check that the subsystem returns to kRange.lower
   this->subsystem()->clear_min_position();
   this->RunFor(chrono::seconds(2));
   EXPECT_EQ(this->subsystem()->goal(0), kRange.lower);
   EXPECT_TRUE(this->subsystem_status_fetcher_.Fetch());
-  EXPECT_NEAR(kRange.lower, this->subsystem_status_fetcher_->position(),
-              0.001);
+  EXPECT_NEAR(kRange.lower,
+              this->subsystem_status_fetcher_->status()->position(), 0.001);
 }
 
 // Tests that set_max_position limits range properly
@@ -776,24 +760,24 @@
   // Check that kRange.upper is used as the default max position
   EXPECT_EQ(this->subsystem()->goal(0), kRange.upper);
   EXPECT_TRUE(this->subsystem_status_fetcher_.Fetch());
-  EXPECT_NEAR(kRange.upper, this->subsystem_status_fetcher_->position(),
-              0.001);
+  EXPECT_NEAR(kRange.upper,
+              this->subsystem_status_fetcher_->status()->position(), 0.001);
 
   // Set max position and check that the subsystem lowers to that position
   this->subsystem()->set_max_position(kRange.upper - 0.05);
   this->RunFor(chrono::seconds(2));
   EXPECT_EQ(this->subsystem()->goal(0), kRange.upper - 0.05);
   EXPECT_TRUE(this->subsystem_status_fetcher_.Fetch());
-  EXPECT_NEAR(kRange.upper - 0.05, this->subsystem_status_fetcher_->position(),
-              0.001);
+  EXPECT_NEAR(kRange.upper - 0.05,
+              this->subsystem_status_fetcher_->status()->position(), 0.001);
 
   // Clear max position and check that the subsystem returns to kRange.upper
   this->subsystem()->clear_max_position();
   this->RunFor(chrono::seconds(2));
   EXPECT_EQ(this->subsystem()->goal(0), kRange.upper);
   EXPECT_TRUE(this->subsystem_status_fetcher_.Fetch());
-  EXPECT_NEAR(kRange.upper, this->subsystem_status_fetcher_->position(),
-              0.001);
+  EXPECT_NEAR(kRange.upper,
+              this->subsystem_status_fetcher_->status()->position(), 0.001);
 }
 
 // Tests that the subsystem maintains its current position when sent a null goal
@@ -820,13 +804,13 @@
 }
 
 REGISTER_TYPED_TEST_SUITE_P(IntakeSystemTest, DoesNothing, ReachesGoal,
-                           FunctionsWhenProfileDisabled,
-                           MaintainConstantVelocityWithoutProfile,
-                           SaturationTest, RespectsRange, ZeroTest, ZeroNoGoal,
-                           LowerHardstopStartup, UpperHardstopStartup,
-                           ResetTest, DisabledGoalTest, DisabledZeroTest,
-                           MinPositionTest, MaxPositionTest, NullGoalTest,
-                           ZeroingErrorTest);
+                            FunctionsWhenProfileDisabled,
+                            MaintainConstantVelocityWithoutProfile,
+                            SaturationTest, RespectsRange, ZeroTest, ZeroNoGoal,
+                            LowerHardstopStartup, UpperHardstopStartup,
+                            ResetTest, DisabledGoalTest, DisabledZeroTest,
+                            MinPositionTest, MaxPositionTest, NullGoalTest,
+                            ZeroingErrorTest);
 INSTANTIATE_TYPED_TEST_SUITE_P(My, IntakeSystemTest, TestTypes);
 
 }  // namespace control_loops
diff --git a/frc971/control_loops/static_zeroing_single_dof_profiled_subsystem_test_absolute_encoder_status.fbs b/frc971/control_loops/static_zeroing_single_dof_profiled_subsystem_test_absolute_encoder_status.fbs
new file mode 100644
index 0000000..5db1c3b
--- /dev/null
+++ b/frc971/control_loops/static_zeroing_single_dof_profiled_subsystem_test_absolute_encoder_status.fbs
@@ -0,0 +1,10 @@
+include "frc971/control_loops/control_loops.fbs";
+include "frc971/control_loops/profiled_subsystem.fbs";
+
+namespace frc971.control_loops.zeroing.testing;
+
+table SubsystemAbsoluteEncoderProfiledJointStatus {
+  status:frc971.control_loops.AbsoluteEncoderProfiledJointStatus (id: 0);
+}
+
+root_type SubsystemAbsoluteEncoderProfiledJointStatus;
diff --git a/frc971/control_loops/static_zeroing_single_dof_profiled_subsystem_test_absolute_position.fbs b/frc971/control_loops/static_zeroing_single_dof_profiled_subsystem_test_absolute_position.fbs
new file mode 100644
index 0000000..a44599a
--- /dev/null
+++ b/frc971/control_loops/static_zeroing_single_dof_profiled_subsystem_test_absolute_position.fbs
@@ -0,0 +1,9 @@
+include "frc971/control_loops/control_loops.fbs";
+
+namespace frc971.control_loops.zeroing.testing;
+
+table SubsystemAbsolutePosition {
+  position:frc971.AbsolutePosition (id: 0);
+}
+
+root_type SubsystemAbsolutePosition;
diff --git a/frc971/control_loops/static_zeroing_single_dof_profiled_subsystem_test_config_source.json b/frc971/control_loops/static_zeroing_single_dof_profiled_subsystem_test_config_source.json
new file mode 100644
index 0000000..e26dbbb
--- /dev/null
+++ b/frc971/control_loops/static_zeroing_single_dof_profiled_subsystem_test_config_source.json
@@ -0,0 +1,44 @@
+{
+  "channels": [
+    {
+      "name": "/aos",
+      "type": "aos.JoystickState"
+    },
+    {
+      "name": "/aos",
+      "type": "aos.logging.LogMessageFbs"
+    },
+    {
+      "name": "/aos",
+      "type": "aos.RobotState"
+    },
+    {
+      "name": "/aos",
+      "type": "aos.timing.Report"
+    },
+    {
+      "name": "/loop",
+      "type": "frc971.control_loops.zeroing.testing.SubsystemGoal"
+    },
+    {
+      "name": "/loop",
+      "type": "frc971.control_loops.zeroing.testing.SubsystemOutput"
+    },
+    {
+      "name": "/loop",
+      "type": "frc971.control_loops.zeroing.testing.SubsystemPotAndAbsoluteEncoderProfiledJointStatus"
+    },
+    {
+      "name": "/loop",
+      "type": "frc971.control_loops.zeroing.testing.SubsystemAbsolutePosition"
+    },
+    {
+      "name": "/loop",
+      "type": "frc971.control_loops.zeroing.testing.SubsystemPotAndAbsolutePosition"
+    },
+    {
+      "name": "/loop",
+      "type": "frc971.control_loops.zeroing.testing.SubsystemAbsoluteEncoderProfiledJointStatus"
+    }
+  ]
+}
diff --git a/frc971/control_loops/static_zeroing_single_dof_profiled_subsystem_test_pot_and_absolute_encoder_status.fbs b/frc971/control_loops/static_zeroing_single_dof_profiled_subsystem_test_pot_and_absolute_encoder_status.fbs
new file mode 100644
index 0000000..f6c52bf
--- /dev/null
+++ b/frc971/control_loops/static_zeroing_single_dof_profiled_subsystem_test_pot_and_absolute_encoder_status.fbs
@@ -0,0 +1,10 @@
+include "frc971/control_loops/control_loops.fbs";
+include "frc971/control_loops/profiled_subsystem.fbs";
+
+namespace frc971.control_loops.zeroing.testing;
+
+table SubsystemPotAndAbsoluteEncoderProfiledJointStatus {
+  status:frc971.control_loops.PotAndAbsoluteEncoderProfiledJointStatus (id: 0);
+}
+
+root_type SubsystemPotAndAbsoluteEncoderProfiledJointStatus;
diff --git a/frc971/control_loops/static_zeroing_single_dof_profiled_subsystem_test_pot_and_absolute_position.fbs b/frc971/control_loops/static_zeroing_single_dof_profiled_subsystem_test_pot_and_absolute_position.fbs
new file mode 100644
index 0000000..d204c5f
--- /dev/null
+++ b/frc971/control_loops/static_zeroing_single_dof_profiled_subsystem_test_pot_and_absolute_position.fbs
@@ -0,0 +1,9 @@
+include "frc971/control_loops/control_loops.fbs";
+
+namespace frc971.control_loops.zeroing.testing;
+
+table SubsystemPotAndAbsolutePosition {
+  position:frc971.PotAndAbsolutePosition (id: 0);
+}
+
+root_type SubsystemPotAndAbsolutePosition;
diff --git a/frc971/control_loops/static_zeroing_single_dof_profiled_subsystem_test.fbs b/frc971/control_loops/static_zeroing_single_dof_profiled_subsystem_test_subsystem_goal.fbs
similarity index 86%
rename from frc971/control_loops/static_zeroing_single_dof_profiled_subsystem_test.fbs
rename to frc971/control_loops/static_zeroing_single_dof_profiled_subsystem_test_subsystem_goal.fbs
index f824b3e..5e2ea7b 100644
--- a/frc971/control_loops/static_zeroing_single_dof_profiled_subsystem_test.fbs
+++ b/frc971/control_loops/static_zeroing_single_dof_profiled_subsystem_test_subsystem_goal.fbs
@@ -10,6 +10,4 @@
   ignore_profile:bool (id: 3);
 }
 
-table SubsystemOutput {
-  output:double (id: 0);
-}
+root_type SubsystemGoal;
diff --git a/frc971/control_loops/static_zeroing_single_dof_profiled_subsystem_test_subsystem_output.fbs b/frc971/control_loops/static_zeroing_single_dof_profiled_subsystem_test_subsystem_output.fbs
new file mode 100644
index 0000000..0001a4b
--- /dev/null
+++ b/frc971/control_loops/static_zeroing_single_dof_profiled_subsystem_test_subsystem_output.fbs
@@ -0,0 +1,7 @@
+namespace frc971.control_loops.zeroing.testing;
+
+table SubsystemOutput {
+  output:double (id: 0);
+}
+
+root_type SubsystemOutput;